maat-tools/maat

GitHub: maat-tools/maat

maat 是一款架构约定检查工具,通过将团队隐性的分层与模块边界规则编码为可执行检查,追踪代码库偏离约定的过程并留存完整的决策历史。

Stars: 3 | Forks: 0

# maat

maat balance icon

Linter 检查代码行。maat 则负责检查你的团队就代码库达成的约定。

每个团队都有任何 Linter 都不知道的规则。*领域层绝不直接与数据库交互。这两个模块绝不能互相耦合。这项策略仅能在一处实现。* 这些规则散落在代码审查的评论、新人入职的交流以及少数人的脑海中——并且它们在悄无声息地瓦解,每次伴随着一个看似合理的 PR 而逐渐崩坏。 maat 让你能够将这些约定以代码的形式记录下来,在每次运行时进行检查,并保留一份关于每次违规以及你团队对此做出的每一项决定的已提交历史记录。 ## 为什么会有 maat 大多数深受难以更改的代码库困扰的团队都面临着相同的困境:同类 Bug 反复出现,时间都花在四处救火上,却没人深究原因——工作似乎就是如此。当有人最终手动审查架构时,他们会找到原因:那些大家心照不宣的规则,在岁月中被一点一滴地破坏,而其中每一次单独的修改看起来都没问题。 然而,这种审查通常最终还是会失效——不是因为审查本身有误,而是因为它缺乏证据支撑。你无法展示每条规则是*何时*开始被打破的、恶化得有多快,或者它让团队付出了什么代价。这就成了一个工程师的单方面说辞对抗现状的局面。 maat 就是这项审查的自动化版本,并且自带证据。它的存在是为了回答一位优秀技术主管在每次提交时都会在脑海中思考的问题,并留下可追溯的记录: ## 它的样子 在一个真实的代码库上运行 `maat check`:

maat finding a layer violation in Cal.com

在 `maat.config.ts` 中编写约定: ``` import { defineConfig } from '@maat-tools/core' import { layer, Pure } from '@maat-tools/coupling-rules' export default defineConfig({ check: { strict: true }, collectors: [['@maat-tools/collector-ts', { tsConfigFilePath: './tsconfig.json' }]], rules: [ // "Business logic stays free of databases, HTTP, and frameworks." layer('@myapp/domain').is(Pure).build(), // "Infrastructure may use the domain and shared contracts. Nothing else." layer('@myapp/infra').allows('@myapp/domain', '@myapp/contracts').build(), ], }) ``` 然后 `maat check` 会告诉你现实在何处偏离了约定: ``` FINDINGS (2) ──────────── [layer-purity] — 2 finding(s) 9f3ac1d2 '@myapp/domain' imports 'pg' — declared Pure ↳ file: src/domain/billing/invoice.ts import: pg 4b81e07a '@myapp/domain' imports 'axios' — declared Pure ↳ file: src/domain/orders/pricing.ts import: axios ``` 每一条发现结果都有一个稳定的 ID,因此同一个问题在不同的 commit 和重命名后依然是同一个问题。你的团队决定如何处理它——是修复它,还是暂时接受它——而这个决定会被记录在一份与代码一起提交的历史文件(即 **ledger**)中。六个月后,没人需要去回忆为什么会有一个特例存在:历史记录会说明是谁接受的、何时接受的,以及接受的期限是多久。 ## maat 不是什么 maat 不是 Linter,不是代码评分工具,也不是 AI 审查器。 Linter 告诉你某一行代码违反了样式规则。SonarQube 为你的代码打分。maat 回答的是另一个不同的问题:**代码库是否依然在遵守你的团队对其做出的承诺——如果没有,是从什么时候开始没有的?** 它捕获到的问题对于逐行检查的工具来说是不可见的,因为每一行单独的代码看起来都没问题。真正的破坏在于代码间的关系:一个模块悄无声息地引入了对另一个模块的依赖,一项策略现在以三个略有不同的副本存在,一个“临时”的捷径变成了承重墙。 ## 工作原理 1. **收集器读取你的仓库** 并记录客观事实:哪些文件引入了哪些文件、什么位于哪一层、在 git 历史中哪些内容倾向于一起发生变化。 2. **规则将这些事实与你们的约定进行比对。** 规则被刻意设计得很无趣:输入相同的事实,就会输出相同的发现。没有随机性,没有网络调用,也没有隐藏状态。 3. **发现结果会获得稳定的 ID**,这样就可以随着时间的推移对它们进行追踪,而不是在每次运行时都重新发现一遍。 4. **决定会被记入 ledger** —— 这是一个随你的仓库一起提交的纯追加(append-only)文件。被接受的特例到期后会强制团队重新审视;没有任何东西会被永久扫到地毯下掩盖起来。 有些事实无法被解析,只能被阅读——比如注意到两个函数以不同的方式实现了相同的业务策略。针对这些情况,maat 的设计允许借助 AI 的收集器从代码中*提取*事实。但是 AI 永远没有决定权:它可以汇报它读取到的内容,而由同样无趣的规则来决定这是否构成违规。结果始终保持着可重复性。 maat 记录的是决定,而不是人。ledger 的存在是为了在掌握背景信息的人离开后,这些背景信息仍能留存——它的目的是帮助理解代码库的历史,而不是为了追究责任。 ## 谁最能从中受益 包含真正业务逻辑、分层以及模块边界的后端代码库——代码库越大、越古老,maat 能发挥的作用就越大。如果你的项目足够小,以至于一个人就能在大脑中记住所有的规则,那你可能暂时还不需要它。但无论如何还是把配置文件写出来吧;因为那个人不可能永远都在。 ## 快速开始 安装 CLI: ``` npm install -D @maat-tools/cli # 或 bun add -d @maat-tools/cli # 或在不安装的情况下运行 npx maat check bunx maat check ``` CLI 只是运行器。收集器、规则和 ledger 后端是独立的软件包,你可以根据需要自行安装: ``` npm install -D @maat-tools/core @maat-tools/collector-ts @maat-tools/coupling-rules ``` 在你的项目根目录下添加一个 `maat.config.ts`(参见上面的示例),然后运行: ``` maat check ``` CLI 会从当前目录向上搜索 `maat.config.ts`。你也可以显式地指定它: ``` maat --config ./path/to/maat.config.ts check # 或 MAAT_CONFIG=./maat.config.ts maat check ``` ### pnpm 与 Monorepo 在 pnpm workspace 中,使用 `-w` 在工作区根目录安装开发依赖: ``` pnpm add -D -w @maat-tools/cli @maat-tools/core @maat-tools/collector-ts @maat-tools/coupling-rules ``` 在 monorepo 中有两点需要了解: - **`tsConfigFilePath` 决定了哪些内容会被分析。** TypeScript 收集器会读取单个 `tsconfig.json` 包含的文件。将它指向某个包的 `tsconfig.json`,maat 就只会看到那个包。要检查*跨越*多个包的边界,请将它指向一个 `include` 覆盖了你关心的所有包的 tsconfig——只设置了 `exclude`(没有 `include`)的根 `tsconfig.json` 什么也收集不到。一种常见的做法是使用一个专门的 `tsconfig.maat.json` 来包含所有包的源代码: // tsconfig.maat.json { "extends": "./tsconfig.json", "include": ["packages/*/src/**/*.ts"] } collectors: [['@maat-tools/collector-ts', { tsConfigFilePath: './tsconfig.maat.json' }]], - **`minimumReleaseAge` 可能会阻止安装最新发布的版本。** 关注供应链安全的工作区(通过 pnpm 的 `minimumReleaseAge`)会拒绝安装发布时间晚于设定时间窗口的包。如果刚刚发布的 maat 版本无法安装,原因就是这个——等待该时间窗口过去,或者将 `@maat-tools/*` 添加到 `minimumReleaseAgeExclude` 中。 ## 开启新的代码库 在捷径定型之前就把规则写下来。保持 `check.strict: true`,并将 `maat check` 加入到 CI 中——任何可见的发现都会返回非零退出码,这样意外的依赖关系就会在成为先例之前让构建失败。 从那些在代码审查中很容易解释的规则开始:哪些包可以依赖哪些包、哪些层必须保持纯粹、依赖的流动方向是什么。当团队有了真正想要保护的模式时,再添加更具体的规则。 有些约定暂时还无法由机器来检查。但无论如何也要把它们写下来,这样它们就会被版本化并保持可见,而不是沦为团队内部口口相传的隐学: ``` maat axiom declare \ --id "domain-purity" \ --scope "@myapp/domain" \ --claim "The domain layer has no infrastructure dependencies." \ --note "Keeps the domain testable without spinning up real I/O." ``` ## 在现有代码库中采用 maat 在成熟的代码库上首次运行绝对会有所发现。这是意料之中的,而且这完全不怪任何人——maat 会将*新*的违规与*既有*的技术债区分开来,这样你就可以直接采用规则,而不必先去修复多年的历史遗留问题。 ``` import { defineConfig } from '@maat-tools/core' export default defineConfig({ check: { strict: true }, collectors: [['@maat-tools/collector-ts', { tsConfigFilePath: './tsconfig.json' }]], rules: [ // your rules ], ledger: ['@maat-tools/file-ledger', { path: './maat-ledger.ndjson' }], }) ``` ``` # 将当前 findings 保存到配置的 ledger maat check --ledger # 接受今天的 findings 作为起点(默认 30 天后过期) maat baseline # 接受较短的时间窗口(1–90 天) maat baseline --expires-in 30 # 将一个已修复的 finding 标记为 resolved maat resolve --fingerprint ``` 被接受的发现结果是有时间限制的,这是刻意为之:当时间窗口到期时,`maat check` 会因为它们而报错,团队必须重新审视。这里没有永久的“忽略”选项——一个你从不再去重新审视的例外,仅仅是披着文书外衣的侵蚀罢了。 ledger 保存着关于发现结果、声明的约定以及决定的纯追加历史记录。请把它和代码库一起提交,这样这些决定就能随着它们所描述的架构一起传承。 ## 官方插件 | 软件包 | 作用 | |---|---| | `@maat-tools/collector-ts` | 从 TypeScript 项目中读取事实 | | `@maat-tools/collector-git` | 从 git 历史记录中读取事实 | | `@maat-tools/coupling-rules` | 针对层级、包边界和依赖方向的规则 | | `@maat-tools/connascence-rules` | 针对那些必须一起更改但代码中并未声明的耦合规则 | | `@maat-tools/git-rules` | 针对代码 churn 以及随着时间推移总是频繁一起更改的文件的规则 | | `@maat-tools/presets-ts` | 为 TypeScript 准备的现成模式定义 | | `@maat-tools/enricher-llm` | 借助 AI 提取事实(仅限提取事实——决定仍由规则做出) | | `@maat-tools/insights` | 跨规则分析和模式检测 | | `@maat-tools/file-ledger` | 纯追加的历史文件后端 | maat 暴露了用于第三方收集器、规则、洞察分析和 ledger 后端的公共接口——内置的软件包使用的接口与你将使用的完全相同。第三方软件包不在官方的可重复性保证范围内,因此在将它们信任地应用于 CI 之前,请务必先行审查。 ## 命令 | 命令 | 用途 | |---|---| | `maat check` | 运行收集器和规则。`--ledger` 用于将发现结果与 ledger 同步;`--show ` 用于选择打印的版块。 | | `maat axiom declare` | 在 ledger 中记录由人工编写的约定。 | | `maat axiom supersede` | 将某项约定标记为已被更新的决定所取代。 | | `maat axiom revoke` | 撤销不再适用的约定。 | | `maat baseline` | 在有限时间内(1–90 天)接受当前的发现结果,从而强制进行周期性审查。 | | `maat resolve` | 将某一条确切的发现标记为已刻意修复。 | | `maat visualize` | 打印当前的 ledger 状态:发现结果、约定以及可选的洞察分析。 | ## 文档 - [快速开始](docs/guide/getting-started.md) - [适应度函数](docs/guide/fitness-functions.md) —— maat 如何与《演进式架构》相关联 - [命令](docs/commands/) - [确定性](docs/guide/determinism.md) —— 为什么规则会被刻意设计得很无趣 - [插件系统](docs/guide/plugins.md) - [架构决策](docs/adr/) ## 状态 maat 还处于 1.0 版本之前的阶段。CLI 可以运行检查、将发现结果与 ledger 同步,并通过基线和修复流程流转各项决定。目前收集器和规则接口还在不断完善中,因此软件包的 API 可能仍会发生变化。 ## 许可证 Apache-2.0
标签:MITM代理, SOC Prime, 云安全监控, 代码审查, 开发工具, 技术债务, 文档结构分析, 架构治理, 自动化攻击, 静态分析