@mcptoolshop/housekeeping 1.3.1 → 1.4.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
@@ -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,30 +21,87 @@
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
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) (`@dogfood-lab/atlas`) 映射一个仓库:它的各个部分以及它的入口,这意味着每个工作流及其运行、发布和部署的内容。该映射作为 `atlas/` 提交并检查到 CI 中。housekeeping 是组织视图。这两个工具是为协同工作而设计的。
86
+
87
+ - **每个映射,都进行扫描。** 扫描读取每个仓库的已提交映射以及所有其他内容,并报告哪些仓库没有映射,哪些仓库从未运行 `atlas check`,以及哪些仓库包含与组织中其他仓库使用的引擎不同的引擎。
88
+ - **红色运行失败的位置。** 当默认分支显示为红色时,该发现会命名失败的作业和步骤,并通过映射,命名该步骤运行的命令,因此修复从正确的文件开始。
89
+ - **从映射中获取部署事实。** 作业部署到的环境是从映射中获取的,映射中记录了该环境,因此这两个工具以相同的方式读取工作流。
90
+ - **整个组织中的警告。** Atlas 标记一个工作流步骤,该步骤将在运行之前失败,例如需要比作业安装的更新运行时环境的工具。该报告一次性列出所有仓库中的这些步骤。
91
+
92
+ 使用这两个服务器的代理从“这十二个仓库被阻止” (housekeeping) 变为“这是工作流、作业和更改将影响的文件” (Atlas),而无需手动打开每个仓库。
93
+
94
+ ## 为什么它具有这种结构
95
+
96
+ | 层 | 选择 | 原因 |
39
97
  |---|---|---|
40
- | 传输 | `gh` CLI | 它已经存储了您的令牌。该工具不会存储或请求任何凭据。 |
41
- | 收集 | GitHub GraphQL,分页 | 一个查询返回一个页面中仓库的元数据、未解决的问题、未解决的拉取请求、发布版本和文件树。如果某个页面持续超时,则将其减半并从相同的游标重新请求。 |
98
+ | 传输 | `gh` CLI | 它已经持有您的令牌。该工具从不存储或请求任何凭据。 |
99
+ | 收集 | GitHub GraphQL,分页 | 一个查询返回一个页面中仓库的元数据、未解决的问题、未解决的拉取请求、发布和文件树。如果一个页面持续超时,则将其减半并从相同的游标重新请求。 |
42
100
  | Actions 运行 | REST | GraphQL 没有 Actions 接口。 |
43
- | Actions 成本 | 计费 API 加上每个任务的持续时间 | 发票显示哪个仓库;只有任务会显示哪个工作流。GitHub 的 `/timing` 端点返回零,因此会重新计算每个任务的总和,然后将其与发票进行核对。 |
44
- | 运行器费率 | 从发票中读取 | 已计费的费率可能与列表价格不同,并且源中的常量值会错误地表示每个数字。 |
101
+ | 操作需要成本 | 计费 API 加上每个作业的持续时间 | 发票上会显示哪个仓库;只有作业会显示哪个工作流程。GitHub 的 `/timing` 端点返回零,因此会重新计算每个作业的总和,然后与发票进行核对。 |
102
+ | 运行器费率 | 从发票中读取 | 已计费的费率可能与列表价格不同,并且源中的常量会错误地显示每个数字。 |
45
103
  | 存储 | 通过内置的 `node:sqlite` 使用 SQLite | 没有本地构建步骤。 |
46
- | 事实来源 | `data/snapshots/*.json` | 原始、可比较且仅追加。数据库是派生的:`npm run rebuild` 在离线状态下重新构建它。 |
104
+ | 事实来源 | `data/snapshots/*.json` | 原始、可比较且仅可追加。数据库是派生的:`npm run rebuild` 在离线状态下重建它。 |
47
105
  | 扫描日志 | `data/sweeps.jsonl` | 每条扫描记录一行,包括失败的扫描和未发现任何新内容的扫描。 |
48
106
 
49
107
  快照是不可变的且具有累积性,因此两个日期之间的差异是 `JOIN`。
@@ -51,8 +109,12 @@
51
109
  ## 要求
52
110
 
53
111
  - Node.js 22.5 或更高版本。
54
- - [GitHub CLI](https://cli.github.com/),已登录(`gh auth status`),并且该帐户可以读取组织。读取计费和安全警报需要相应的权限;如果缺少权限,扫描会记录“未测量”并继续。它绝不会将缺少权限记录为干净的结果。
55
- - Windows 或 Linux。它在 Windows 上开发,其测试在 CI 中在 Linux 上运行。macOS 应该可以工作,但未进行测试。
112
+ - [GitHub CLI](https://cli.github.com/),已登录(`gh auth status`),并且是
113
+ 可以读取组织的帐户。读取计费和安全警报
114
+ 需要相应的权限;如果没有,扫描会记录“未测量”并
115
+ 继续。它绝不会将缺少权限记录为干净的结果。
116
+ - Windows 或 Linux。它在 Windows 上开发,其测试在 Linux 上运行
117
+ CI。macOS 应该可以工作,但未进行测试。
56
118
 
57
119
  ## 使用方法
58
120
 
@@ -62,9 +124,10 @@ 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` 目录中放置一个
128
+ 文件(请参阅“配置”);然后 `hk refresh` 不需要任何参数。
66
129
 
67
- 或者,从克隆版本运行,这样它的数据将保存在克隆版本中:
130
+ 或者从克隆运行,这样它的数据将保存在克隆中:
68
131
 
69
132
  ```bash
70
133
  git clone https://github.com/mcp-tool-shop-org/housekeeping.git
@@ -75,7 +138,7 @@ cp housekeeping.config.example.json housekeeping.config.json # then set "org"
75
138
  hk refresh
76
139
  ```
77
140
 
78
- 然后对其进行查询:
141
+ 然后查询它:
79
142
 
80
143
  ```bash
81
144
  hk summary # portfolio totals
@@ -91,17 +154,20 @@ hk sql "SELECT ..." # one read-only statement
91
154
  hk help # every command and flag
92
155
  ```
93
156
 
94
- 在没有 `npm link` 的克隆版本中,每次 `hk <command>` 都是 `node src/cli.mjs <command>`。
157
+ 在没有 `npm link` 的克隆中,每次 `hk <command>` 都是 `node src/cli.mjs <command>`。
95
158
 
96
- `npm run rebuild` 重新从磁盘上已有的快照中派生数据库和报告,无需任何网络访问。
159
+ `npm run rebuild` 会重新派生数据库和报告,这些报告来自已
160
+ 存储在磁盘上的快照,无需网络访问。
97
161
 
98
- 一次完整的扫描会发出几百个 API 调用。不要在循环中运行 `refresh`。
162
+ 一次完整的扫描会进行数百次 API 调用。不要在循环中运行 `refresh`。
99
163
 
100
- 结果输出到标准输出;进度和错误输出到标准错误。每个错误都会打印一个代码和一个提示。退出代码:0 表示正常,1 表示用户错误,2 表示运行时错误,3 表示部分完成(扫描已完成,但存在差距)。
164
+ 结果输出到标准输出;进度和错误输出到标准错误。每个
165
+ 错误都会打印一个代码和一个提示。退出代码:0 表示正常,1 表示用户错误,2 表示运行时
166
+ 错误,3 表示部分(扫描已完成,但存在差距)。
101
167
 
102
168
  ## 配置
103
169
 
104
- `housekeeping.config.json`,位于 housekeeping 工具运行的目录中(如下所示):
170
+ `housekeeping.config.json`,位于 housekeeping 运行的目录中(请参阅下文):
105
171
 
106
172
  ```json
107
173
  {
@@ -110,73 +176,71 @@ hk help # every command and flag
110
176
  }
111
177
  ```
112
178
 
113
- - `org` 是要扫描的组织。`hk refresh <org>` 会覆盖它。如果两者都没有,扫描将拒绝启动。
114
- - `metaRepos` 是包含组织默认设置、资源或工具的仓库,而不是已发布的最终产品。它们不适用于仅对最终产品有意义的发现:缺少 README、LICENSE 和类似文件,没有工作流,没有发布版本。
179
+ - `org` 是要扫描的组织。`hk refresh <org>` 会覆盖它。如果
180
+ 两者都没有,扫描将拒绝启动。
181
+ - `metaRepos` 是包含组织默认设置、资源或
182
+ 工具的仓库,而不是已发布的最终产品。它们不适用于仅
183
+ 对最终产品有意义的发现:缺少 README、LICENSE 和类似文件,没有
184
+ 工作流程,以及没有发布。
115
185
 
116
- 格式不正确的配置文件会引发错误,绝不会静默地使用默认值:未知的键、错误的类型或损坏的 JSON 都会停止运行。
186
+ 格式不正确的文件是一个错误,绝不会是无声的默认值:未知的键、错误的
187
+ 类型或损坏的 JSON 会停止运行。
117
188
 
118
189
  Run from a clone, housekeeping keeps `data/`, `reports/` and the config file
119
190
  in the clone. Run as an installed package, it keeps them in the directory you
120
191
  run it from, or in `HK_HOME` when that is set.
121
192
 
122
- 环境:`HK_HOME`(数据、报告和配置文件所在的位置)、`HK_CONFIG`(配置文件路径)、`HK_DB`(数据库路径)、`HK_LOG`(`silent`、`normal`、`verbose` 或 `debug`)、`HK_COST_REPOS` 和 `HK_COST_BUDGET`(每个任务成本传递的上限)、`GH_PATH`(`gh` 的路径)。
193
+ 环境:`HK_HOME`(数据、报告和配置所在的位置)、
194
+ `HK_CONFIG`(配置路径)、`HK_DB`(数据库路径)、`HK_LOG`
195
+ (`silent`、`normal`、`verbose` 或 `debug`)、`HK_COST_REPOS` 和
196
+ `HK_COST_BUDGET`(每个作业成本的上限)、`GH_PATH`(`gh` 的路径)。
123
197
 
124
198
  ## 保护数据
125
199
 
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
- ```
200
+ **扫描写入的内容是敏感信息。请勿将 `data/` 或 `reports/` 提交到
201
+ 公共仓库。**
145
202
 
146
- `HK_HOME` 是您进行扫描的目录。从克隆版本开始,请使用 `"command": "node", "args": ["/path/to/housekeeping/src/mcp.mjs"]`。
203
+ 快照会记录私有仓库的名称和描述、每个
204
+ 公开的安全警报以及它引用的软件包、工作流程文件和 Actions
205
+ 计费。GitHub 会故意将公共仓库的安全警报隐藏起来,只向其维护者显示;发布的快照会将该列表提供给所有人。
147
206
 
148
- `hk_sql` 接受单个 `SELECT` 或 `WITH` 语句,并以只读模式打开数据库。如果调用失败,则返回结构化的错误,而不是堆栈跟踪。
207
+ 此仓库的 `.gitignore` 排除 `data/`、`reports/` 和
208
+ `housekeeping.config.json`。为了保留历史记录,这是快照的目的,
209
+ 请从您自己的**私有**仓库运行该工具,并将它们提交到那里。
149
210
 
150
211
  ## 发现
151
212
 
152
- 规则位于 `src/analyze.mjs` 中。每个规则都引用了它所执行的编写规则,因此可以根据标准而不是个人喜好来争论发现结果。此仓库中包含的规则位于 [`rules/`](rules/) 中:
213
+ 规则位于 `src/analyze.mjs` 中。每个规则都引用了它所执行的书面规则,
214
+ 因此可以针对标准而不是针对个人喜好来争论发现。此仓库提供的规则位于 [`rules/`](rules/) 中:
153
215
 
154
- - [`rules/github-actions.md`](rules/github-actions.md):路径过滤器、运行器、矩阵大小、工作流文件限制、并发性。
216
+ - [`rules/github-actions.md`](rules/github-actions.md):路径过滤器、运行器、矩阵大小、工作流程文件限制、并发。
155
217
  - [`rules/shipcheck-product-standards.md`](rules/shipcheck-product-standards.md):CI 必须通过、发布门控、版本。
156
218
  - [`rules/repo-first.md`](rules/repo-first.md):默认分支。
157
- - [`rules/atlas-map.md`](rules/atlas-map.md):每个仓库的已提交的映射,在 CI 中进行检查。
219
+ - [`rules/atlas-map.md`](rules/atlas-map.md):每个仓库的已提交映射,在 CI 中进行检查。
158
220
 
159
- 这些是某个组织的标准。如果您的标准不同,请更改规则文件和规则。
221
+ 这些是一个组织的标准。如果您的标准不同,请同时更改规则文件
222
+ 和规则。
160
223
 
161
- 这些规则会注意区分看似相同的事物,因为将它们合并会导致产生噪音:
224
+ 规则会注意区分看起来相似的事物,因为将它们合并会产生噪音:
162
225
 
163
- - **红色默认分支与红色拉取请求分支不同。** 失败的 Dependabot 分支是积压;在默认分支上失败的 `push` 运行是故障。运行的事件决定了是哪一种。
164
- - **历史记录与故障不同。** 移动到仅发布触发器的工作流会永久保留其上次默认分支的失败记录。已删除的工作流会保留其运行记录。两者都不是当前的缺陷。
165
- - **一个无法产生任何结果的必需检查,与一个未在此处运行的检查不同。** 一个表示删除该要求;另一个表示修复触发器。这些修复措施相互矛盾。
166
- - **失败的运行与已取消的运行不同。** `cancel-in-progress` 用于终止过时的运行,因此通常,已取消的运行是并发规则在起作用。
167
- - **总成本与净成本不同。** GitHub 以全价计量公共仓库,并将其折扣为零。总成本是实际的计算量;净成本是实际的支出。两者不能相加。
168
- - **未测量到的,不等于干净的。** GitHub 未扫描的仓库报告为零个警报。成本检查未达到的仓库没有成本行。两者都报告为未知。
226
+ - **默认分支为红色的状态并不意味着拉取请求分支也为红色。** 如果 Dependabot 分支出现故障,则表示存在未处理的问题;如果在默认分支上出现 `push` 运行失败,则表示存在错误。运行的事件决定了是哪种情况。
227
+ - **历史记录并不等同于错误。** 如果工作流程被移动到仅触发发布,则其最后一次在默认分支上的失败状态将永久保留。即使删除了工作流程,其运行记录也会保留。两者都不是当前存在的缺陷。
228
+ - **如果某个必需的检查无法执行,这并不意味着该检查未在此处运行。** 一种情况是建议取消该要求;另一种情况是建议修复触发器。这两种修复方法相互矛盾。
229
+ - **如果触发器跳过了一个拉取请求,这并不意味着该拉取请求存在冲突。** GitHub 不会对存在冲突的拉取请求运行任何拉取请求工作流程,因此,如果缺少检查,则说明该触发器没有问题。在这种情况下,需要重新进行基准测试。
230
+ - **失败的运行并不等同于已取消的运行。** `cancel-in-progress` 的作用是终止过时的运行,因此,通常情况下,已取消的运行是并发规则在起作用。
231
+ - **总成本不等于净成本。** GitHub 对公共仓库收取全部费用,然后将其折扣为零。总成本是实际的计算成本;净成本是实际的支出。两者不能简单地相加。
232
+ - **未测量的数据并不意味着数据是干净的。** 如果 GitHub 未对某个仓库进行扫描,则报告的警报数量为零。如果成本评估未达到某个阈值,则该仓库不会显示任何成本行。两者都将报告为未知。
169
233
 
170
- 严重程度决定健康评分:严重 40,高 15,中 6,低 2,信息 0,从 100 中扣除。评分衡量的是需要关注的程度,而不是质量。
234
+ 严重程度决定了健康评分:严重 40,高 15,中 6,低 2,信息 0,从 100 中扣除。评分衡量的是需要关注的程度,而不是质量。
171
235
 
172
- [手册](https://mcp-tool-shop-org.github.io/housekeeping/handbook/) 列出了每个发现、命令、错误代码和表格。
236
+ [手册](https://mcp-tool-shop-org.github.io/housekeeping/handbook/) 列出了所有发现、命令、错误代码和表格。
173
237
 
174
238
  ## 安全性
175
239
 
176
240
  - **只读。** 收集器会发出 GraphQL 查询和 REST `GET` 请求。它绝不会合并、推送、发布、公开或编辑任何设置。
177
- - **无凭据。** 身份验证使用 `gh` 已经拥有的凭据。没有任何内容会写入磁盘。
178
- - **无遥测。** 唯一联系的主机是 GitHub API(通过 `gh`)和 npm 注册表,用于版本和安全建议查询。
179
- - **它存储的内容** 是敏感部分:请参阅“保持数据私密”。
241
+ - **不存储凭据。** 身份验证使用 `gh` 中已有的凭据。不会将任何凭据写入磁盘。
242
+ - **不收集遥测数据。** 唯一联系的主机是 GitHub API(通过 `gh`)和 npm 注册表,用于查找版本和安全建议。
243
+ - **它存储的内容** 是敏感部分:请参阅“保护数据”。
180
244
 
181
245
  如 [SECURITY.md](SECURITY.md) 中所述,报告漏洞。
182
246
 
@@ -186,7 +250,7 @@ run it from, or in `HK_HOME` when that is set.
186
250
  npm test
187
251
  ```
188
252
 
189
- 每个规则都进行双向测试:必须触发的模式,以及不应触发的相邻模式。一个单独的验证器会从实时 GitHub 通过其自身的传输重新推导出样本发现,并在没有数据库或没有网络时,会发出警告。
253
+ 每个规则都从两个方向进行测试:必须触发的模式,以及不应触发的相邻模式。一个单独的验证器会从实时 GitHub 中重新推导出样本发现,并通过其自身的传输方式进行验证,并在没有数据库或没有网络时发出明确的警告。
190
254
 
191
255
  ## 许可证
192
256
 
@@ -1,4 +1,5 @@
1
1
  {
2
2
  "org": "your-org",
3
- "metaRepos": [".github"]
3
+ "metaRepos": [".github"],
4
+ "homeOrgs": ["your-org"]
4
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcptoolshop/housekeeping",
3
- "version": "1.3.1",
3
+ "version": "1.4.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}.`,