@mcptoolshop/housekeeping 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.zh.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <a href="README.ja.md">日本語</a> | <a href="README.md">English</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.hi.md">हिन्दी</a> | <a href="README.it.md">Italiano</a> | <a href="README.pt-BR.md">Português (BR)</a>
2
+ <a href="README.md">English</a> | <a href="README.ja.md">日本語</a> | <a href="README.zh.md">中文</a> | <a href="README.es.md">Español</a> | <a href="README.fr.md">Français</a> | <a href="README.hi.md">हिन्दी</a> | <a href="README.it.md">Italiano</a> | <a href="README.pt-BR.md">Português (BR)</a>
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -10,7 +10,8 @@
10
10
 
11
11
  <p align="center">
12
12
  An operational-health warehouse for a GitHub organization.<br>
13
- One sweep, one SQLite database, findings you can argue with.
13
+ One sweep, one SQLite database, findings you can argue with,<br>
14
+ and the whole-organization view an AI agent needs to coordinate every repository.
14
15
  </p>
15
16
 
16
17
  <p align="center">
@@ -20,39 +21,100 @@
20
21
  <a href="https://mcp-tool-shop-org.github.io/housekeeping/handbook/"><img src="https://img.shields.io/badge/Handbook-read-blue" alt="Handbook" /></a>
21
22
  </p>
22
23
 
23
- 一次扫描即可收集每个仓库的 CI 状态、未解决的问题和拉取请求、发布版本、版本标签、工作流文件、分支保护、安全警报、锁文件和 Actions 计费信息,并将其存储到 SQLite 数据库中,然后根据编写的规则对所有信息进行审计。
24
+ 一次扫描收集每个仓库的 CI 状态、未解决的问题和拉取请求、发布版本、版本标签、工作流文件、分支保护、安全警报、锁文件和 Actions 账单,并将所有这些信息汇总到 SQLite 中,然后根据编写的规则对其进行审核。
24
25
 
25
- 它可以一次性回答整个组织的问题:
26
+ 它一次性回答整个组织的问题:
26
27
 
27
- - 哪些默认分支显示为红色,这是否表示主分支已损坏或历史记录已过时?
28
- - 哪些拉取请求无法合并,因为必需的检查未通过,从而阻止了合并?
28
+ - 哪些默认分支显示为红色,这是否表示主线出现问题或历史记录已过时?
29
+ - 哪些拉取请求永远无法合并,因为一个必需的检查未发出,从而阻止了它们?
29
30
  - 哪些部署被仓库自身的设置拒绝?
30
31
  - `package.json`、Git 标签和 npm 注册表之间的差异在哪里?
31
- - 哪些工作流违反了 Actions 成本规则,它们花费了多少,哪些任务消耗了这些成本?
32
- - 哪些仓库包含 GitHub 自身警报计数未包含的安全建议?
32
+ - 哪些工作流违反了 Actions 成本规则,它们花费了多少,哪些任务花费了这些成本?
33
+ - 哪些仓库包含 GitHub 自身的警报计数未识别的建议?
33
34
 
34
- 它是一种审计工具,而不是修复工具。它读取 GitHub 数据,但不进行任何更改。
35
+ 它是一个审计工具,而不是一个修复工具。它读取 GitHub,但不进行任何更改。
35
36
 
36
- ## 其设计方式的原因
37
+ ## 每个仓库都有一个代理
37
38
 
38
- | 分层 | 选择 | 理由 |
39
+ 拥有数十个仓库的组织没有一个统一的地方来存储其状态。housekeeping 就是这个地方,它的 MCP 服务器 `hk-mcp` 将数据提供给一个 AI 代理。通过它,一个代理可以充当整个组织的协调器:
40
+
41
+ 1. **扫描。** `hk refresh` 对每个仓库进行一次快照。
42
+ 2. **分类。** 代理询问哪些内容显示为红色、哪些内容被阻止以及哪些内容变得更糟,并获得答案,这些答案可以区分出现问题的主线和过时的历史记录,以及区分停滞的拉取请求和冲突的拉取请求。
43
+ 3. **规划一个批次。** 一个 `hk_sql` 查询找到所有具有相同特征的仓库,因此修复可以变成每个仓库的一个拉取请求,而不是进行搜索。
44
+ 4. **执行工作。** housekeeping 从不进行写入。代理使用自己的工具,在自己的权限下打开拉取请求,并由您进行审核。
45
+ 5. **验证。** 再次扫描。如果发现的问题已解决,或者没有解决,那么两次快照之间的差异就是一个查询。
46
+
47
+ 代理在一次或两次调用中回答的问题:
48
+
49
+ - 哪些默认分支显示为红色,以及哪个任务和步骤导致了问题?
50
+ - 哪些拉取请求永远无法合并,是触发器还是冲突导致了问题?
51
+ - 哪些仓库包含一个生产建议,而 GitHub 没有显示任何警报?
52
+ - 哪些计划的工作流违反了计划规则,以及如何违反?
53
+ - Actions 花费了多少,哪个任务花费了这些成本?
54
+ - 自上次扫描以来,发生了哪些变化?
55
+
56
+ | 工具 | 它回答的问题 |
57
+ |---|---|
58
+ | `hk_summary` | 最新扫描的总计:仓库、问题、拉取请求、工作流、发现。 |
59
+ | `hk_findings` | 按代码、严重程度、类别或仓库分类的发现;未筛选时,按代码进行计数。红色主线的发现会显示导致问题的任务和步骤。 |
60
+ | `hk_ci` | 默认分支显示为红色的仓库以及失败的工作流;还包括失败的计划、失败的拉取请求分支以及没有 CI 的仓库。 |
61
+ | `hk_repo` | 完整的单个仓库:发现、工作流、未解决的拉取请求和问题。 |
62
+ | `hk_backlog` | 整个组织中未解决的拉取请求或问题,按时间顺序排列。 |
63
+ | `hk_health` | 每个仓库的健康评分,按从好到坏的顺序排列。 |
64
+ | `hk_cost` | 按仓库、工作流或任务分类的 Actions 支出,区分总支出和净支出。 |
65
+ | `hk_sql` | 一个只读 `SELECT` 或 `WITH` 语句,用于查询仓库。 |
66
+ | `hk_schema` | 用于编写 `hk_sql` 查询的表和列。 |
67
+
68
+ 每个答案都来自最新的快照,并且以只读方式打开。如果调用失败,则返回一个结构化的错误,而不是堆栈跟踪。
69
+
70
+ ### 连接它
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "housekeeping": { "command": "hk-mcp", "env": { "HK_HOME": "/path/to/warehouse" } },
76
+ "atlas": { "command": "atlas", "args": ["mcp"] }
77
+ }
78
+ }
79
+ ```
80
+
81
+ `HK_HOME` 是您从中进行扫描的目录。从克隆副本开始,使用 `"command": "node", "args": ["/path/to/housekeeping/src/mcp.mjs"]`。第二个服务器是 [Atlas](https://github.com/dogfood-lab/testing-os/tree/main/packages/atlas),它回答关于单个仓库的相同代理的问题(请参见下文)。在 Windows 上,通过 `cmd /c` 启动全局安装的命令。
82
+
83
+ ## housekeeping 和 Atlas
84
+
85
+ [Atlas](https://github.com/dogfood-lab/testing-os/tree/main/packages/atlas)
86
+ (`@dogfood-lab/atlas`) 将一个仓库(包括其组成部分和入口)映射出来,即
87
+ 映射每个工作流程以及它所运行、发布和部署的内容。该映射会被提交
88
+ 为 `atlas/`,并在 CI 中进行检查。组织视图即为“家务管理”。两者
89
+ 的设计目的是协同工作。
90
+
91
+ - **每个映射,都进行扫描。** 扫描读取每个仓库的已提交映射以及所有其他内容,并报告哪些仓库没有映射,哪些仓库从未运行 `atlas check`,以及哪些仓库使用与组织中其他仓库使用的不同的引擎。
92
+ - **红色运行失败的位置。** 当默认分支显示为红色时,该发现会显示失败的任务和步骤,并通过映射显示该步骤运行的命令,因此修复从正确的文件开始。
93
+ - **从映射中获取部署事实。** 工作流部署到的环境是从映射中获取的,映射中记录了该环境,因此这两个工具以相同的方式读取工作流。
94
+ - **整个组织中的警告。** Atlas 标记一个工作流步骤,该步骤将在运行之前失败,例如需要比任务安装的更新运行时环境的工具。该报告一次性列出所有仓库中的这些步骤。
95
+
96
+ 使用这两个服务器的代理可以从“这十二个仓库被阻止” (housekeeping) 变为“这是工作流、任务和更改将影响的文件” (Atlas),而无需手动打开每个仓库。
97
+
98
+ ## 为什么它具有这种结构
99
+
100
+ | 层 | 选择 | 原因 |
39
101
  |---|---|---|
40
- | 传输 | `gh` CLI | 它已经存储了您的令牌。该工具不会存储或请求任何凭据。 |
41
- | 收集 | GitHub GraphQL,分页 | 一个查询返回一个页面中仓库的元数据、未解决的问题、未解决的拉取请求、发布版本和文件树。如果某个页面持续超时,则将其减半并从相同的游标重新请求。 |
102
+ | 传输 | `gh` CLI | 它已经拥有您的令牌。该工具从不存储或请求任何凭据。 |
103
+ | 收集 | GitHub GraphQL,分页 | 一个查询返回元数据、未解决的问题、未解决的拉取请求、发布版本和文件树,用于一页仓库。如果一页的查询超时,则将其减半并从相同的游标重新查询。 |
42
104
  | Actions 运行 | REST | GraphQL 没有 Actions 接口。 |
43
- | Actions 成本 | 计费 API 加上每个任务的持续时间 | 发票显示哪个仓库;只有任务会显示哪个工作流。GitHub 的 `/timing` 端点返回零,因此会重新计算每个任务的总和,然后将其与发票进行核对。 |
44
- | 运行器费率 | 从发票中读取 | 已计费的费率可能与列表价格不同,并且源中的常量值会错误地表示每个数字。 |
45
- | 存储 | 通过内置的 `node:sqlite` 使用 SQLite | 没有本地构建步骤。 |
46
- | 事实来源 | `data/snapshots/*.json` | 原始、可比较且仅追加。数据库是派生的:`npm run rebuild` 在离线状态下重新构建它。 |
105
+ | Actions 成本 | 计费 API 加上每个任务的持续时间 | 发票上会显示哪个仓库;只有作业会显示哪个工作流程。GitHub的`/timing`端点返回零,因此会重新计算每个作业的总和,然后与发票进行核对。 |
106
+ | 运行器费率 | 从发票中读取 | 已结算的费率可能与定价表中的价格不同,并且源中的常量会错误地显示每个数字。 |
107
+ | 存储 | 通过内置的`node:sqlite`,使用SQLite | 没有本地构建步骤。 |
108
+ | 事实来源 | `data/snapshots/*.json` | 原始、可比较且仅追加。数据库是派生的:`npm run rebuild`会离线重建它。 |
47
109
  | 扫描日志 | `data/sweeps.jsonl` | 每条扫描记录一行,包括失败的扫描和未发现任何新内容的扫描。 |
48
110
 
49
- 快照是不可变的且具有累积性,因此两个日期之间的差异是 `JOIN`。
111
+ 快照是不可变的且具有累积性,因此两个日期之间的差异是`JOIN`。
50
112
 
51
113
  ## 要求
52
114
 
53
115
  - Node.js 22.5 或更高版本。
54
- - [GitHub CLI](https://cli.github.com/),已登录(`gh auth status`),并且该帐户可以读取组织。读取计费和安全警报需要相应的权限;如果缺少权限,扫描会记录“未测量”并继续。它绝不会将缺少权限记录为干净的结果。
55
- - Windows 或 Linux。它在 Windows 上开发,其测试在 CI 中在 Linux 上运行。macOS 应该可以工作,但未进行测试。
116
+ - [GitHub CLI](https://cli.github.com/),已登录(作为可以读取组织的账户),登录账户为`gh auth status`。读取计费和安全警报需要相应的权限;如果没有这些权限,扫描会记录“未测量”,然后继续。它绝不会将缺少权限记录为干净的结果。
117
+ - Windows 或 Linux。它在 Windows 上开发,其测试在 CI 中在 Linux 上运行。macOS应该可以工作,但未进行测试。
56
118
 
57
119
  ## 使用方法
58
120
 
@@ -62,9 +124,9 @@ mkdir warehouse && cd warehouse # housekeeping keeps its data here
62
124
  hk refresh your-org # collect, load, analyze, write reports/AUDIT-<date>.md
63
125
  ```
64
126
 
65
- 为了在每次扫描之间保留设置,请在该目录中放置一个 `housekeeping.config.json`(请参阅“配置”);然后,`hk refresh` 不需要任何参数。
127
+ 为了在扫描之间保留设置,请在该目录中放置一个`housekeeping.config.json`(参见“配置”);然后`hk refresh`不需要任何参数。
66
128
 
67
- 或者,从克隆版本运行,这样它的数据将保存在克隆版本中:
129
+ 或者从克隆运行,这样它的数据将保存在克隆中:
68
130
 
69
131
  ```bash
70
132
  git clone https://github.com/mcp-tool-shop-org/housekeeping.git
@@ -75,7 +137,7 @@ cp housekeeping.config.example.json housekeeping.config.json # then set "org"
75
137
  hk refresh
76
138
  ```
77
139
 
78
- 然后对其进行查询:
140
+ 然后查询它:
79
141
 
80
142
  ```bash
81
143
  hk summary # portfolio totals
@@ -84,6 +146,8 @@ hk findings # findings grouped by severity and code
84
146
  hk health # per-repository health score, worst first
85
147
  hk repo <name> # one repository in full
86
148
  hk prs # every open pull request by age
149
+ hk branches # branches by state: merged, moved after merge, no PR
150
+ hk watch # open PRs, alerts and branches for a short watch list
87
151
  hk versions # package.json against git tag against npm
88
152
  hk actions # every workflow and its rule flags
89
153
  hk cost # Actions cost, gross and net side by side
@@ -91,17 +155,17 @@ hk sql "SELECT ..." # one read-only statement
91
155
  hk help # every command and flag
92
156
  ```
93
157
 
94
- 在没有 `npm link` 的克隆版本中,每次 `hk <command>` 都是 `node src/cli.mjs <command>`。
158
+ 在没有`npm link`的克隆中,每次`hk <command>`都是`node src/cli.mjs <command>`。
95
159
 
96
- `npm run rebuild` 重新从磁盘上已有的快照中派生数据库和报告,无需任何网络访问。
160
+ `npm run rebuild`会重新从已在磁盘上的快照中派生数据库和报告,无需网络访问。
97
161
 
98
- 一次完整的扫描会发出几百个 API 调用。不要在循环中运行 `refresh`。
162
+ 一次完整的扫描会进行几百次 API 调用。请勿在循环中运行`refresh`。
99
163
 
100
164
  结果输出到标准输出;进度和错误输出到标准错误。每个错误都会打印一个代码和一个提示。退出代码:0 表示正常,1 表示用户错误,2 表示运行时错误,3 表示部分完成(扫描已完成,但存在差距)。
101
165
 
102
166
  ## 配置
103
167
 
104
- `housekeeping.config.json`,位于 housekeeping 工具运行的目录中(如下所示):
168
+ `housekeeping.config.json`,位于 housekeeping 运行的目录中(参见下文):
105
169
 
106
170
  ```json
107
171
  {
@@ -110,75 +174,58 @@ hk help # every command and flag
110
174
  }
111
175
  ```
112
176
 
113
- - `org` 是要扫描的组织。`hk refresh <org>` 会覆盖它。如果两者都没有,扫描将拒绝启动。
114
- - `metaRepos` 是包含组织默认设置、资源或工具的仓库,而不是已发布的最终产品。它们不适用于仅对最终产品有意义的发现:缺少 README、LICENSE 和类似文件,没有工作流,没有发布版本。
177
+ - `org`是要扫描的组织。`hk refresh <org>`会覆盖它。如果两者都没有,扫描将拒绝启动。
178
+ - `metaRepos`是包含组织默认设置、资产或工具的仓库,而不是已发布的最终产品。它们不包括仅对最终产品有意义的发现:缺少 README、LICENSE 和类似文件,没有工作流程,没有发布。
115
179
 
116
- 格式不正确的配置文件会引发错误,绝不会静默地使用默认值:未知的键、错误的类型或损坏的 JSON 都会停止运行。
180
+ 格式不正确的文件是一个错误,绝不会是无声的默认值:未知的键、错误的类型或损坏的 JSON 会停止运行。
117
181
 
118
- Run from a clone, housekeeping keeps `data/`, `reports/` and the config file
119
- in the clone. Run as an installed package, it keeps them in the directory you
120
- run it from, or in `HK_HOME` when that is set.
182
+ 从克隆运行,housekeeping 会将`data/`、`reports/`和配置文件保存在克隆中。作为已安装的软件包运行,它会将它们保存在您运行它的目录中,或者在设置了`HK_HOME`时保存在那里。
121
183
 
122
- 环境:`HK_HOME`(数据、报告和配置文件所在的位置)、`HK_CONFIG`(配置文件路径)、`HK_DB`(数据库路径)、`HK_LOG`(`silent`、`normal`、`verbose` 或 `debug`)、`HK_COST_REPOS` 和 `HK_COST_BUDGET`(每个任务成本传递的上限)、`GH_PATH`(`gh` 的路径)。
184
+ 环境:`HK_HOME`(数据、报告和配置所在的位置)、`HK_CONFIG`(配置路径)、`HK_DB`(数据库路径)、`HK_LOG`(`silent`、`normal`、`verbose`或`debug`)、`HK_COST_REPOS`和`HK_COST_BUDGET`(每个作业成本限制)、`GH_PATH`(`gh`的路径)。
123
185
 
124
186
  ## 保护数据
125
187
 
126
- **扫描写入的内容是敏感信息。请勿将 `data/` 或 `reports/` 提交到公共仓库。**
127
-
128
- 快照会记录私有仓库的名称和描述、每个未解决的安全警报以及它引用的软件包、工作流文件和 Actions 计费信息。GitHub 会故意将公共仓库的安全警报隐藏起来,只向其维护者显示;发布的快照会将该列表提供给其他人。
129
-
130
- 此仓库的 `.gitignore` 排除 `data/`、`reports/` 和 `housekeeping.config.json`。为了保留历史记录,这是快照的目的,请从您自己的**私有**仓库中运行该工具,并将它们提交到那里。
131
-
132
- ## MCP 服务器
133
-
134
- `hk-mcp`(或克隆版本中的 `npm run mcp`)通过标准输入/输出 (stdio) 为仓库提供服务,因此助手可以在不读取多兆字节快照的情况下提出问题:
135
-
136
- `hk_summary` · `hk_findings` · `hk_ci` · `hk_repo` · `hk_backlog` · `hk_health` · `hk_cost` · `hk_sql` · `hk_schema`
137
-
138
- ```json
139
- {
140
- "mcpServers": {
141
- "housekeeping": { "command": "hk-mcp", "env": { "HK_HOME": "/path/to/warehouse" } }
142
- }
143
- }
144
- ```
188
+ **扫描写入的内容是敏感信息。请勿将`data/`或`reports/`提交到公共仓库。**
145
189
 
146
- `HK_HOME` 是您进行扫描的目录。从克隆版本开始,请使用 `"command": "node", "args": ["/path/to/housekeeping/src/mcp.mjs"]`。
190
+ 快照会记录私有仓库的名称和描述、每个与该仓库关联的开放安全警报、工作流程文件和 Actions 计费。GitHub会故意将公共仓库的安全警报从所有人(包括其维护者)那里隐藏;发布的快照会将该列表提供给所有人。
147
191
 
148
- `hk_sql` 接受单个 `SELECT` 或 `WITH` 语句,并以只读模式打开数据库。如果调用失败,则返回结构化的错误,而不是堆栈跟踪。
192
+ 此仓库的`.gitignore`不包括`data/`、`reports/`和`housekeeping.config.json`。为了保留历史记录(这是快照的目的),请从您自己的**私有**仓库运行该工具,并将它们提交到那里。
149
193
 
150
194
  ## 发现
151
195
 
152
- 规则位于 `src/analyze.mjs` 中。每个规则都引用了它所执行的编写规则,因此可以根据标准而不是个人喜好来争论发现结果。此仓库中包含的规则位于 [`rules/`](rules/) 中:
196
+ 规则位于`src/analyze.mjs`中。每个规则都引用了它所执行的书面规则,因此可以针对标准而不是针对个人喜好来争论发现结果。此仓库提供的规则位于[`rules/`](rules/)中:
153
197
 
154
- - [`rules/github-actions.md`](rules/github-actions.md):路径过滤器、运行器、矩阵大小、工作流文件限制、并发性。
198
+ - [`rules/github-actions.md`](rules/github-actions.md):路径过滤器、运行器、矩阵大小、工作流程文件限制、并发。
155
199
  - [`rules/shipcheck-product-standards.md`](rules/shipcheck-product-standards.md):CI 必须通过、发布门控、版本。
156
200
  - [`rules/repo-first.md`](rules/repo-first.md):默认分支。
157
- - [`rules/atlas-map.md`](rules/atlas-map.md):每个仓库的已提交的映射,在 CI 中进行检查。
201
+ - [`rules/atlas-map.md`](rules/atlas-map.md):每个仓库的已提交映射,在 CI 中进行检查。
158
202
 
159
- 这些是某个组织的标准。如果您的标准不同,请更改规则文件和规则。
203
+ 这些是一个组织的标准。如果您的标准不同,请同时更改规则文件和规则。
160
204
 
161
- 这些规则会注意区分看似相同的事物,因为将它们合并会导致产生噪音:
205
+ 规则会注意区分看起来相似的事物,因为将它们合并会产生噪音:
162
206
 
163
- - **红色默认分支与红色拉取请求分支不同。** 失败的 Dependabot 分支是积压;在默认分支上失败的 `push` 运行是故障。运行的事件决定了是哪一种。
164
- - **历史记录与故障不同。** 移动到仅发布触发器的工作流会永久保留其上次默认分支的失败记录。已删除的工作流会保留其运行记录。两者都不是当前的缺陷。
165
- - **一个无法产生任何结果的必需检查,与一个未在此处运行的检查不同。** 一个表示删除该要求;另一个表示修复触发器。这些修复措施相互矛盾。
166
- - **失败的运行与已取消的运行不同。** `cancel-in-progress` 用于终止过时的运行,因此通常,已取消的运行是并发规则在起作用。
167
- - **总成本与净成本不同。** GitHub 以全价计量公共仓库,并将其折扣为零。总成本是实际的计算量;净成本是实际的支出。两者不能相加。
168
- - **未测量到的,不等于干净的。** GitHub 未扫描的仓库报告为零个警报。成本检查未达到的仓库没有成本行。两者都报告为未知。
207
+ - **红色默认分支与红色拉取请求分支不同。** 失败的 Dependabot 分支是积压;在默认分支上失败的`push`运行是中断。事件决定了哪种情况。
208
+ - **历史记录不是中断。** 移动到仅发布触发器的工作流程会永久保留其上次默认分支的失败。删除的工作流程会保留其运行记录。两者都不是实时缺陷。
209
+ - **无法发出任何内容的必需检查不是未运行的检查。** 一个表示删除该要求;另一个表示修复触发器。这些修复相互矛盾。
210
+ - **触发器跳过的拉取请求不是冲突的请求。** GitHub不会在具有冲突的拉取请求上运行任何拉取请求工作流程,因此其缺失的检查不会说明触发器。后者需要重新提交。
211
+ - **失败的运行不是已取消的运行。** `cancel-in-progress`用于终止过时的运行,因此取消的分钟通常是并发规则在起作用。
212
+ - **总成本不是净成本。** GitHub以全价对公共仓库进行计量,然后将其折扣为零。总成本是实际的计算量;净成本是实际的支出。两者绝不会相加。
213
+ - **未测量不是干净。** 仓库GitHub不会扫描报告零个警报。成本通行证未到达的仓库没有成本行。两者都报告为未知。
169
214
 
170
- 严重程度决定健康评分:严重 40,高 15,中 6,低 2,信息 0,从 100 中扣除。评分衡量的是需要关注的程度,而不是质量。
215
+ 严重程度会影响健康评分:严重 40,高 15,中等 6,低 2,信息 0,从 100 中扣除。评分衡量的是需要关注的程度,而不是质量。
171
216
 
172
- [手册](https://mcp-tool-shop-org.github.io/housekeeping/handbook/) 列出了每个发现、命令、错误代码和表格。
217
+ [手册](https://mcp-tool-shop-org.github.io/housekeeping/handbook/)
218
+ 列出了所有发现、命令、错误代码和表格。
173
219
 
174
220
  ## 安全性
175
221
 
176
- - **只读。** 收集器会发出 GraphQL 查询和 REST `GET` 请求。它绝不会合并、推送、发布、公开或编辑任何设置。
177
- - **无凭据。** 身份验证使用 `gh` 已经拥有的凭据。没有任何内容会写入磁盘。
178
- - **无遥测。** 唯一联系的主机是 GitHub API(通过 `gh`)和 npm 注册表,用于版本和安全建议查询。
179
- - **它存储的内容** 是敏感部分:请参阅“保持数据私密”。
222
+ - **只读。** 收集器会发出 GraphQL 个查询和 REST `GET` 请求。它绝不会
223
+ 合并、推送、发布、公开或编辑任何设置。
224
+ - **无需凭据。** 身份验证使用 `gh` 中已有的凭据。不会将任何信息写入磁盘。
225
+ - **不收集遥测数据。** 唯一联系的主机是 GitHub API(通过 `gh`)以及 npm 注册表,用于版本和安全建议查询。
226
+ - **它存储的内容** 是敏感部分:请参阅“保护数据”。
180
227
 
181
- 如 [SECURITY.md](SECURITY.md) 中所述,报告漏洞。
228
+ 请按照 [SECURITY.md](SECURITY.md) 中描述的方式报告漏洞。
182
229
 
183
230
  ## 测试
184
231
 
@@ -186,9 +233,9 @@ run it from, or in `HK_HOME` when that is set.
186
233
  npm test
187
234
  ```
188
235
 
189
- 每个规则都进行双向测试:必须触发的模式,以及不应触发的相邻模式。一个单独的验证器会从实时 GitHub 通过其自身的传输重新推导出样本发现,并在没有数据库或没有网络时,会发出警告。
236
+ 每个规则都会进行双向测试:测试必须触发的模式,以及不应触发的相邻模式。一个独立的验证器会通过其自身的传输方式,从实时 GitHub 中重新推导出样本发现,并在没有数据库或没有网络时发出警告。
190
237
 
191
- ## 许可证
238
+ ## 许可
192
239
 
193
240
  MIT。请参阅 [LICENSE](LICENSE)。
194
241
 
@@ -1,4 +1,6 @@
1
1
  {
2
2
  "org": "your-org",
3
- "metaRepos": [".github"]
3
+ "metaRepos": [".github"],
4
+ "homeOrgs": ["your-org"],
5
+ "watch": []
4
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcptoolshop/housekeeping",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "Operational-health warehouse for a GitHub organization: CI, issues, PRs, releases, Actions cost and repo hygiene, collected into SQLite and audited against written rules.",
5
5
  "keywords": [
6
6
  "github",
@@ -2,7 +2,8 @@
2
2
 
3
3
  Every repository that runs workflows keeps a committed Atlas map on its
4
4
  default branch, made by the fleet's current engine version, and runs
5
- `atlas check` in CI. Archived repositories are exempt.
5
+ `atlas check` in CI. Archived repositories, and forks of another
6
+ organization's project, are exempt.
6
7
 
7
8
  ## What it means in a repository
8
9
 
@@ -15,6 +16,10 @@ default branch, made by the fleet's current engine version, and runs
15
16
  - The version is pinned, never floating, and the whole fleet carries the same
16
17
  one. It moves by pull request: the pin and the regenerated map in the same
17
18
  change.
19
+ - A fork of another organization's project is exempt. Its workflows are the
20
+ upstream's, and a map and a CI step would make the fork diverge from the
21
+ project it tracks and conflict on every sync. "Fork" means GitHub records it
22
+ as a fork whose parent belongs to an organization outside your own.
18
23
 
19
24
  ## Transition (ended)
20
25
 
@@ -11,6 +11,10 @@ These are the rules the `actions` findings enforce.
11
11
  - Every workflow carries `workflow_dispatch` as a manual fallback.
12
12
  - Release, publish and container workflows trigger on `release: published`
13
13
  only, with no push trigger.
14
+ - A workflow that emits a required status check takes no paths filter on its
15
+ `pull_request` trigger; its `push` trigger stays paths-gated. Otherwise a
16
+ pull request that touches none of the listed paths, such as a docs-only
17
+ change, never reports the check and can never merge.
14
18
 
15
19
  ## Runners
16
20
 
@@ -54,6 +58,6 @@ concurrency:
54
58
 
55
59
  A scheduled workflow is allowed when it does something a push trigger cannot
56
60
  (dependency or pin freshness, drift detection against an outside source), runs
57
- weekly or slower, is bounded (`ubuntu-latest`, an explicit `timeout-minutes`, a
58
- `concurrency` block), opens a pull request rather than pushing to a protected
59
- branch, and carries `workflow_dispatch`.
61
+ weekly or slower (daily needs a stated reason), is bounded (`ubuntu-latest`, an
62
+ explicit `timeout-minutes`, a `concurrency` block), opens a pull request rather
63
+ than pushing to a protected branch, and carries `workflow_dispatch`.
package/src/analyze.mjs CHANGED
@@ -258,12 +258,80 @@ export function environmentAdmitsBranch(env, settings, branch) {
258
258
  // a missing check counted from the start, and are unaffected by it.
259
259
  export const ATLAS_ENGINE_BEHIND_COUNTS_SINCE = '2026-09-30T21:44:11Z';
260
260
 
261
+ /**
262
+ * Is this repository a fork of another organization's project? Pure.
263
+ *
264
+ * rules/atlas-map.md: "A fork of another organization's project is exempt."
265
+ * `repo` is a repo row (is_fork, fork_parent_owner); `homeOrgs` the
266
+ * organizations whose projects are the org's own, compared without case. A
267
+ * fork whose parent was not read (a snapshot before collector 1.5.0) is NOT
268
+ * exempt: not measured is never read as someone else's.
269
+ */
270
+ export function forkOfAnotherOrg(repo, homeOrgs = []) {
271
+ if (repo?.is_fork !== 1) return false;
272
+ const owner = repo.fork_parent_owner;
273
+ if (typeof owner !== 'string' || !owner) return false;
274
+ return !homeOrgs.some(o => String(o).toLowerCase() === owner.toLowerCase());
275
+ }
276
+
261
277
  /** Does an engine behind the fleet's count, for a snapshot taken at `takenAt`? Pure. */
262
278
  export function engineBehindCounts(takenAt, since = ATLAS_ENGINE_BEHIND_COUNTS_SINCE) {
263
279
  const t = Date.parse(takenAt ?? ''), s = Date.parse(since);
264
280
  return Number.isFinite(t) && Number.isFinite(s) && t >= s;
265
281
  }
266
282
 
283
+ /**
284
+ * The Atlas version the fleet carries: the `@dogfood-lab/atlas@<v> check` pin
285
+ * that the most repositories use in push-triggered CI. Pure.
286
+ *
287
+ * rules/atlas-map.md: "the whole fleet carries the same one. It moves by pull
288
+ * request." So the fleet's version is what the repositories pin, and it moves
289
+ * when a pin-bump wave lands -- not when npm publishes a newer engine. Each
290
+ * repository counts once per pin it uses; a tie goes to the higher version.
291
+ * `rows` are { repo, atlas_check, on_triggers } for non-archived repositories.
292
+ * Returns { version, repos, pinned } or null when no repository pins one.
293
+ */
294
+ export function fleetPin(rows) {
295
+ const byPin = new Map();
296
+ const pinned = new Set();
297
+ for (const w of rows ?? []) {
298
+ const push = (w.on_triggers ?? '').split(',').some(t => t === 'push' || t === 'pull_request');
299
+ if (!push) continue;
300
+ for (const pin of (w.atlas_check ?? '').split('\n')) {
301
+ if (!pin || pin === 'unpinned' || !parseSemver(pin)) continue;
302
+ if (!byPin.has(pin)) byPin.set(pin, new Set());
303
+ byPin.get(pin).add(w.repo);
304
+ pinned.add(w.repo);
305
+ }
306
+ }
307
+ if (!byPin.size) return null;
308
+ // compareSemver sorts newest first, which is the tie-break wanted here.
309
+ const [version, repos] = [...byPin].sort(([a, ra], [b, rb]) =>
310
+ rb.size - ra.size || compareSemver(parseSemver(a), parseSemver(b)))[0];
311
+ return { version, repos: repos.size, pinned: pinned.size };
312
+ }
313
+
314
+ /**
315
+ * The version maps and pins are measured against, for one snapshot.
316
+ *
317
+ * From the end of the first pin-bump wave (ATLAS_ENGINE_BEHIND_COUNTS_SINCE)
318
+ * it is the fleet's own pin (fleetPin). Before it, it is npm's latest, which
319
+ * is what those snapshots were reported against; keeping it means a rebuild
320
+ * reproduces their findings. npm's latest is returned beside it either way.
321
+ */
322
+ export function fleetEngineFor(db, sid) {
323
+ const npm = db.prepare('SELECT version, error FROM atlas_fleet WHERE snapshot_id = ?').get(sid) ?? null;
324
+ const takenAt = db.prepare('SELECT taken_at FROM snapshot WHERE id = ?').get(sid)?.taken_at;
325
+ if (!engineBehindCounts(takenAt)) {
326
+ return { version: npm?.version ?? null, source: 'npm-latest', npmLatest: npm?.version ?? null, npmError: npm?.error ?? null };
327
+ }
328
+ const pin = fleetPin(db.prepare(`SELECT w.repo, w.atlas_check, w.on_triggers FROM workflow w
329
+ JOIN repo r ON r.snapshot_id = w.snapshot_id AND r.name = w.repo
330
+ WHERE w.snapshot_id = ? AND r.is_archived = 0 AND w.atlas_check <> ''`).all(sid));
331
+ return { version: pin?.version ?? null, source: 'fleet-pin', repos: pin?.repos ?? 0, pinned: pin?.pinned ?? 0,
332
+ npmLatest: npm?.version ?? null, npmError: npm?.error ?? null };
333
+ }
334
+
267
335
  const ATLAS_RULE = 'rules/atlas-map.md';
268
336
  const isOlder = (v, fleet) => {
269
337
  const a = parseSemver(v), b = parseSemver(fleet);
@@ -449,14 +517,62 @@ export function classifyRequiredChecks({ required, observedNames, declaredJobs }
449
517
  return { stale, live: required.filter(c => !staleSet.has(c)) };
450
518
  }
451
519
 
520
+ /**
521
+ * Which open PRs are missing a live required check because their files missed
522
+ * the workflow's paths filter. `live` comes from classifyRequiredChecks;
523
+ * `observed` is the repo's check_context rows.
524
+ */
525
+ export function gatedPrs({ prs, live, observed }) {
526
+ const seenByPr = new Map();
527
+ for (const c of observed) {
528
+ if (c.source !== 'pr' || c.pr_number == null) continue;
529
+ if (!seenByPr.has(c.pr_number)) seenByPr.set(c.pr_number, new Set());
530
+ seenByPr.get(c.pr_number).add(c.name);
531
+ }
532
+ // Drafts are not trying to merge, and a PR opened moments ago may have
533
+ // checks still being created. The window is measured off `created_at`,
534
+ // NOT `updated_at`: updated_at moves for comments, labels and rebases,
535
+ // none of which say anything about whether CI ran, so a long-open PR
536
+ // that someone commented on today would read as "too fresh to judge".
537
+ // created_at never moves.
538
+ //
539
+ // This deliberately holds fire for a day. Dependabot PRs opened 90
540
+ // minutes before a snapshot can have zero checks and never get any
541
+ // (site-only changes against a paths-gated ci.yml), but at that age
542
+ // "no checks yet" and "no checks ever" are genuinely indistinguishable.
543
+ // They surface on the next run.
544
+ // Residual gap: a week-old PR force-pushed seconds before a snapshot.
545
+ // Rare, self-heals, and the cheap direction to be wrong in.
546
+ //
547
+ // A conflicting PR is left out. GitHub builds no merge ref for it, so no
548
+ // pull_request workflow runs at all, and a check missing from it says
549
+ // nothing about paths; the repair is a rebase, which PR_CONFLICTED already
550
+ // asks for. Counting them called eight repos' trigger broken when every
551
+ // blocked PR there was simply conflicted. The cost: a conflicted PR that
552
+ // ALSO misses the paths filter is only reported once it is rebased.
553
+ const settled = prs.filter(p => !p.is_draft && (p.age_days ?? 0) >= 1 && p.mergeable !== 'CONFLICTING');
554
+ const blocked = settled
555
+ .map(p => ({ p, absent: live.filter(c => !(seenByPr.get(p.number)?.has(c))) }))
556
+ .filter(x => x.absent.length);
557
+ return { settled, blocked };
558
+ }
559
+
452
560
  /**
453
561
  * `metaRepos` are exempt from product-hygiene rules: repos that hold org
454
562
  * defaults, assets or tooling rather than a shipped product. Which repos those
455
563
  * are is a fact about the org, so it comes from the config file (config.mjs),
456
564
  * not from this file. Tests pass it explicitly.
457
565
  */
458
- export function analyze(db, sid, { metaRepos = loadConfig().metaRepos } = {}) {
566
+ export function analyze(db, sid, opts = {}) {
567
+ // The config file is read only when the caller passes nothing, so a caller
568
+ // that names its metaRepos (every test) never picks up a local config.
569
+ const cfg = opts.metaRepos === undefined ? loadConfig() : null;
570
+ const metaRepos = opts.metaRepos ?? cfg.metaRepos;
459
571
  const META_REPOS = new Set(metaRepos);
572
+ // The organizations whose projects are the org's own (rules/atlas-map.md:
573
+ // a fork of another organization's project is exempt). Unset: the swept org.
574
+ const homeOrgs = opts.homeOrgs ?? cfg?.homeOrgs
575
+ ?? [db.prepare('SELECT org FROM snapshot WHERE id = ?').get(sid)?.org].filter(Boolean);
460
576
  db.prepare('DELETE FROM finding WHERE snapshot_id = ?').run(sid);
461
577
  const add = db.prepare('INSERT INTO finding VALUES (?,?,?,?,?,?,?)');
462
578
  const F = (repo, code, severity, category, message, evidence = null) =>
@@ -524,13 +640,13 @@ export function analyze(db, sid, { metaRepos = loadConfig().metaRepos } = {}) {
524
640
  const envBy = byRepo(q('SELECT * FROM environment WHERE snapshot_id = ?'));
525
641
  const wfEnvBy = byRepo(q('SELECT * FROM workflow_environment WHERE snapshot_id = ?'));
526
642
 
527
- // The committed maps and the engine version they are measured against. No
528
- // fleet row (older snapshot, or the registry did not answer) leaves the
529
- // engine check silent.
643
+ // The committed maps and the engine version they are measured against
644
+ // (fleetEngineFor: the fleet's own pin after the first wave, npm's latest
645
+ // before it). No version to measure against leaves the engine check silent.
530
646
  const atlasBy = new Map(
531
647
  q('SELECT * FROM atlas_map WHERE snapshot_id = ?').map(x => [x.repo, x]),
532
648
  );
533
- const fleetEngine = db.prepare('SELECT version FROM atlas_fleet WHERE snapshot_id = ?').get(sid)?.version ?? null;
649
+ const fleetEngine = fleetEngineFor(db, sid).version;
534
650
  const engineCounts = engineBehindCounts(db.prepare('SELECT taken_at FROM snapshot WHERE id = ?').get(sid)?.taken_at);
535
651
 
536
652
  // Where each red run broke, and the door it broke in. Keyed by run id and by
@@ -877,35 +993,13 @@ export function analyze(db, sid, { metaRepos = loadConfig().metaRepos } = {}) {
877
993
  + stale.map(c => `"${c}"`).join(', '));
878
994
  }
879
995
  if (live.length) {
880
- const seenByPr = new Map();
881
- for (const c of observed) {
882
- if (c.source !== 'pr' || c.pr_number == null) continue;
883
- if (!seenByPr.has(c.pr_number)) seenByPr.set(c.pr_number, new Set());
884
- seenByPr.get(c.pr_number).add(c.name);
885
- }
886
- // Drafts are not trying to merge, and a PR opened moments ago may have
887
- // checks still being created. The window is measured off `created_at`,
888
- // NOT `updated_at`: updated_at moves for comments, labels and rebases,
889
- // none of which say anything about whether CI ran, so a long-open PR
890
- // that someone commented on today would read as "too fresh to judge".
891
- // created_at never moves.
892
- //
893
- // This deliberately holds fire for a day. Dependabot PRs opened 90
894
- // minutes before a snapshot can have zero checks and never get any
895
- // (site-only changes against a paths-gated ci.yml), but at that age
896
- // "no checks yet" and "no checks ever" are genuinely indistinguishable.
897
- // They surface on the next run.
898
- // Residual gap: a week-old PR force-pushed seconds before a snapshot.
899
- // Rare, self-heals, and the cheap direction to be wrong in.
900
- const settled = myPrs.filter(p => !p.is_draft && (p.age_days ?? 0) >= 1);
901
- const blocked = settled
902
- .map(p => ({ p, absent: live.filter(c => !(seenByPr.get(p.number)?.has(c))) }))
903
- .filter(x => x.absent.length);
996
+ const { settled, blocked } = gatedPrs({ prs: myPrs, live, observed });
904
997
  if (blocked.length) {
905
998
  const worst = blocked.slice().sort((a, b) => b.absent.length - a.absent.length)[0];
906
999
  F(name, 'CI_REQUIRED_CHECK_GATED', 'medium', 'ci',
907
- `${blocked.length} of ${settled.length} open PRs are BLOCKED by required checks that `
908
- + `never ran on them: the job still exists, the PR's files just missed its paths filter.`,
1000
+ `${blocked.length} of ${settled.length} open PRs without conflicts are BLOCKED by required `
1001
+ + `checks that never ran on them: the job still exists, the PR's files just missed its paths filter. `
1002
+ + `rules/github-actions.md: a workflow that emits a required check takes no paths filter on pull_request.`,
909
1003
  `#${worst.p.number} is missing ${worst.absent.map(c => `"${c}"`).join(', ')}`
910
1004
  + (blocked.length > 1 ? `; also #${blocked.filter(x => x !== worst).map(x => x.p.number).join(', #')}` : ''));
911
1005
  }
@@ -997,7 +1091,12 @@ export function analyze(db, sid, { metaRepos = loadConfig().metaRepos } = {}) {
997
1091
  // "Map older than the branch" is deliberately not a finding: a map needs
998
1092
  // regenerating only when the structure changes, and `atlas check` in CI is
999
1093
  // what enforces that.
1000
- for (const f of atlasMapFindings({
1094
+ //
1095
+ // A fork of another organization's project is exempt too (amended
1096
+ // 2026-10-01): its workflows are the upstream's, and a map and a CI step
1097
+ // would make it diverge from the project it tracks and conflict on every
1098
+ // sync. forkOfAnotherOrg() decides; an unknown parent is not exempt.
1099
+ if (!forkOfAnotherOrg(r, homeOrgs)) for (const f of atlasMapFindings({
1001
1100
  hasMap: r.has_atlas_map,
1002
1101
  workflows: myWfs,
1003
1102
  scripts: myScripts.map(x => x.command ?? ''),
@@ -1101,10 +1200,19 @@ export function analyze(db, sid, { metaRepos = loadConfig().metaRepos } = {}) {
1101
1200
  + '(off by default). Push a branch and open an issue instead, or supply a PAT.',
1102
1201
  at);
1103
1202
  }
1104
- if (triggers.includes('schedule')) {
1105
- F(name, 'WF_SCHEDULED', 'medium', 'actions',
1106
- 'Scheduled (cron) workflow. rules/github-actions.md allows one only when it does what a push '
1107
- + 'cannot, runs weekly or slower, is bounded, and opens a pull request; check it against those.',
1203
+ // rules/github-actions.md, "Scheduled workflows" (amended 2026-09-08): a
1204
+ // schedule is permitted in an org repo when it does what a push cannot, runs weekly or slower ("daily
1205
+ // needs a stated reason"), is bounded, opens a PR rather than pushing, and
1206
+ // carries workflow_dispatch. Only a condition shown to fail fires; the
1207
+ // first is judgment and is never checked. A gap in cadence alone is `low`:
1208
+ // the rule allows a faster schedule with a reason, and the reason lives in
1209
+ // prose this cannot read. A NULL column is a snapshot loaded before the
1210
+ // check existed, which says nothing either way.
1211
+ const gaps = (w.schedule_gaps ?? '').split('\n').filter(Boolean);
1212
+ if (triggers.includes('schedule') && gaps.length) {
1213
+ const cadenceOnly = gaps.every(g => g.startsWith('runs more often than weekly'));
1214
+ F(name, 'WF_SCHEDULED', cadenceOnly ? 'low' : 'medium', 'actions',
1215
+ `Scheduled workflow outside rules/github-actions.md: ${gaps.join('; ')}.`,
1108
1216
  at);
1109
1217
  }
1110
1218
  }
@@ -1245,11 +1353,10 @@ export function analyze(db, sid, { metaRepos = loadConfig().metaRepos } = {}) {
1245
1353
  `Workspace root package "${r.pkg_name}" v${pv}; per-package versions are not audited yet.`,
1246
1354
  `latest tag ${r.latest_tag ?? 'none'}`);
1247
1355
  } else {
1248
- if (pv && /^0\./.test(pv)) {
1249
- F(name, 'PRE_1_0', 'medium', 'version',
1250
- `package.json is v${pv}; standard requires promotion to 1.0.0 before shipping.`,
1251
- r.pkg_name ?? '');
1252
- }
1356
+ // PRE_1_0 was retired on 2026-09-30. It filed every 0.x package as a
1357
+ // defect, but the standard changed on 2026-09-09: 1.0.0 means the product
1358
+ // is 1.0, and new or unfinished work stays 0.x. A finding against a
1359
+ // version that follows the standard is not arguable against it.
1253
1360
  if (pv && tag && pv !== tag) {
1254
1361
  F(name, 'VERSION_TAG_DRIFT', 'high', 'version',
1255
1362
  `package.json v${pv} != latest release tag v${tag}.`,