repo-audit-tool 1.2.3 → 1.3.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/BEST-PRACTICES.md CHANGED
@@ -159,7 +159,7 @@ gh repo edit <org>/dsh-<name> --add-topic deepseek-harness --add-topic dsh-plugi
159
159
  2. **peer 同范围复制进 devDependencies**(社区事实标准):peer 不进自身依赖树 → tsc/测试/CI 找不到类型
160
160
  3. **单库化坑(subagent-router 实战)**:单库独立安装不装宿主包的 peer——**运行时可达的宿主 peer 包全部显式进 devDependencies**;判定法:`npm test` 报 `Cannot find package` 逐个补
161
161
  4. **devDep 精确 pin 特例**:`dsh-client-ui-slots` 的 module augmentation 要求与 runtime 解析副本一致,caret 会漂移致 SlotMap 双副本 → 该包 pin 精确版本(0.2.0 踩过 TS2664/TS2345)
162
- 5. **版本锁定分工**:范围写 package.json(caret),具体版靠提交入库的 `package-lock.json`;CI `npm ci --legacy-peer-deps`(dsh alpha 生态 peer 链不完整)
162
+ 5. **版本锁定分工**:范围写 package.json(caret),具体版靠提交入库的 `package-lock.json`;CI `npm ci --legacy-peer-deps`(dsh alpha 生态 peer 链不完整)。锁文件的核心价值在**被消费**:CI/验证链必须实际安装它(`npm ci` / `pip install -r requirements.lock`)——只入库不安装是装饰性合规;`requirements.lock` 非 pip 原生锁格式,没有默认消费者,CI 须显式安装;零依赖仓(依赖声明为空)无锁对象,可豁免。
163
163
 
164
164
  ### 5.3 dual ESM/CJS(外部调研结论)
165
165
 
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "title": "repo-audit 文档索引",
4
4
  "description": "AI Agent 可读的文档索引 — 包含所有文档的元数据、结构、和调用协议",
5
- "version": "1.2.3",
5
+ "version": "1.3.0",
6
6
  "updated": "2026-09-06",
7
7
  "repo": "NinjaSln-labs/repo-audit",
8
8
  "npm": "repo-audit-tool",
@@ -20,7 +20,7 @@
20
20
  "name": "repo-audit",
21
21
  "description": "仓库标准化审计工具 + 工程脚手架生成器(零依赖)",
22
22
  "bin": "repo-audit",
23
- "version": "1.2.3",
23
+ "version": "1.3.0",
24
24
  "language": "JavaScript (ESM)",
25
25
  "runtime": "Node.js >= 18",
26
26
  "dependencies": [],
@@ -231,6 +231,7 @@
231
231
  },
232
232
 
233
233
  "changelog": {
234
+ "1.3.0": "反馈修复:yaml_field 行内注释剥离 + fallback_field 通配(SEC-004 兼容 job 级权限);AGENTS-core 来源声明二选一;QUA-004 锁文件消费意图;手册补 --update 存量补齐动线",
234
235
  "1.2.3": "package.json 补充 repository 字段,修复 npm provenance E422;完成 Trusted Publisher tag 自动发布",
235
236
  "1.2.2": "publish CI 改用 node 22 + 条件升级 npm,修复 EBADENGINE(tag 发布因 1.2.1 已存在未出包)",
236
237
  "1.2.1": "README 链接改为绝对 URL(修复 npm 页面 404)",
@@ -191,7 +191,29 @@ cat audit-report/report.json | jq '.findings[] | select(.status=="fail") | .id'
191
191
  # 5. 如有 Critical 发现,阻断生成流程
192
192
  ```
193
193
 
194
- ### 4.2 模板升级后校验
194
+ ### 4.2 存量仓库补齐缺失文档(Issue#3 动线)
195
+
196
+ 审计 fail 后需要补 AGENTS.md / CONTRIBUTING.md 等模板文档时,**优先用 `scaffold --update` 而不是手写**:
197
+
198
+ ```bash
199
+ # 1. 存量仓首次接入(无 .scaffold/lock/)——adopt 模式,零覆盖保证
200
+ node scaffold.mjs --update <repo> --type <type> --dry-run # 先看会落位什么
201
+ node scaffold.mjs --update <repo> --type <type> # 实际执行
202
+
203
+ # 2. 行为:
204
+ # - 仓库缺失的文件(如 AGENTS.md)→ ADDED 直拷
205
+ # - 仓库已有且与模板不同 → .scaffold-merge/ 人工评审区(不覆盖你的内容)
206
+ # - 工作区必须 clean(先 commit/stash)
207
+ # 3. 补齐后重跑审计确认
208
+ node repo-audit.mjs --repo <repo> --format json
209
+ ```
210
+
211
+ 要点:
212
+ - `--update` 对**从未被 scaffold 管理的仓**同样可用(自动 adopt,建立 `.scaffold/lock/`)
213
+ - 手写 AGENTS.md 时,**按来源声明二选一**:无 lock 的仓删除「单源拼装」行、保留「自主维护」行(模板头部已内置两种措辞)
214
+ - `--skip <path>` 可排除不想让模板接管的文件(可多次)
215
+
216
+ ### 4.3 模板升级后校验
195
217
 
196
218
  ```bash
197
219
  # 1. 审计当前状态
@@ -209,7 +231,7 @@ after=$(cat ./audit-after/report.json | jq '.summary')
209
231
  # 如果 after.critical > before.critical → 告警:模板升级引入了问题
210
232
  ```
211
233
 
212
- ### 4.3 CI 门禁集成
234
+ ### 4.4 CI 门禁集成
213
235
 
214
236
  ```yaml
215
237
  # GitHub Actions 示例
@@ -219,7 +241,7 @@ after=$(cat ./audit-after/report.json | jq '.summary')
219
241
  echo "Score: $(jq '.summary | 100 - (.critical * 25) - (.major * 10) - (.minor * 3)' ./.ci-audit/report.json)"
220
242
  ```
221
243
 
222
- ### 4.4 增量维度审计
244
+ ### 4.5 增量维度审计
223
245
 
224
246
  ```bash
225
247
  # 只审计安全(快速)
@@ -232,7 +254,7 @@ node repo-audit.mjs --dim docs --format json
232
254
  node repo-audit.mjs --dim git --dim docs --dim security --dim quality --format json
233
255
  ```
234
256
 
235
- ### 4.5 自定义规则叠加
257
+ ### 4.6 自定义规则叠加
236
258
 
237
259
  ```bash
238
260
  # 创建自定义规则
@@ -507,10 +507,13 @@ node repo-audit.mjs --repo ./my-repo --type python-app
507
507
  | 操作 | 命令序列 |
508
508
  |---|---|
509
509
  | 新建仓库后验证 | `scaffold.mjs --type xxx --name yyy` → `repo-audit.mjs --repo ./yyy` |
510
+ | **存量仓补齐缺失文档** | `scaffold.mjs --update ./zzz --type xxx --dry-run`(预览)→ `scaffold.mjs --update ./zzz`(执行;adopt 模式零覆盖)→ `repo-audit.mjs --repo ./zzz` 复审 |
510
511
  | 模板升级后对比 | `repo-audit.mjs --repo ./zzz --output ./before` → `scaffold.mjs --update ./zzz` → `repo-audit.mjs --repo ./zzz --output ./after` |
511
512
  | CI 门禁 | `repo-audit.mjs --repo . --strict --format json` |
512
513
  | PR 描述附件 | `repo-audit.mjs --repo . --format md --output ./pr-audit` → 附 `pr-audit/report.md` 到 PR |
513
514
 
515
+ > **补 AGENTS.md 提示**(DOC-004 fail 时):优先 `scaffold --update` 补齐(模板头部自带来源声明二选一);确需手写时,无 `.scaffold/lock/` 的仓按「自主维护」措辞写头部,勿照抄「单源拼装」行。
516
+
514
517
  ---
515
518
 
516
519
  ## 十一、操作授权与完成路径
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repo-audit-tool",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "仓库标准化审计工具 + 工程脚手架生成器(零依赖)",
5
5
  "type": "module",
6
6
  "main": "repo-audit.mjs",
package/repo-audit.mjs CHANGED
@@ -202,7 +202,48 @@ function countNonHiddenFiles(path) {
202
202
  return count
203
203
  }
204
204
 
205
+ // Issue#2 fix: 定位指定缩进层级下的具名子键(返回行号与缩进;未找到返回 null)
206
+ function findNestedYamlKey(lines, startIdx, baseIndent, key) {
207
+ for (let i = startIdx + 1; i < lines.length; i++) {
208
+ const l = lines[i]
209
+ if (!l.trim() || l.trim().startsWith('#')) continue
210
+ const ind = l.length - l.trimStart().length
211
+ if (ind <= baseIndent) return null
212
+ if (ind !== baseIndent + 2) continue // 只看直接子层(YAML 常规 2 空格缩进)
213
+ const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
214
+ if (m && m[1] === key) return { idx: i, indent: ind }
215
+ }
216
+ return null
217
+ }
218
+ // Issue#2 fix: 列出指定父键下一层的所有子键名(通配 * 展开用)
219
+ function listChildKeys(lines, startIdx, baseIndent) {
220
+ const keys = []
221
+ for (let i = startIdx + 1; i < lines.length; i++) {
222
+ const l = lines[i]
223
+ if (!l.trim() || l.trim().startsWith('#')) continue
224
+ const ind = l.length - l.trimStart().length
225
+ if (ind <= baseIndent) break
226
+ if (ind !== baseIndent + 2) continue
227
+ const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
228
+ if (m) keys.push(m[1])
229
+ }
230
+ return keys
231
+ }
232
+
205
233
  // P0-3 fix: 嵌套 YAML 路径下钻查找(替代行级正则)
234
+ // Issue#1 fix: 剥离行内注释(# 前须有空白;引号内 # 不算注释——按引号配对状态跳过)
235
+ export function stripYamlComment(s) {
236
+ let str = s
237
+ // 整段以 # 开头(允许前导空白)→ 值为空(父级保持嵌套下钻语义)
238
+ if (/^\s*#/.test(str)) return ''
239
+ // 引号包裹:取第一个配对引号内的内容,引号后的尾注释丢弃
240
+ const q = str.match(/^(\s*)(['"])([\s\S]*?)\2(?:\s+#.*)?$/)
241
+ if (q) return q[3].trim()
242
+ // 无引号:以「空白+#」为注释分隔,截断尾部
243
+ const cut = str.search(/\s#/)
244
+ if (cut !== -1) str = str.slice(0, cut)
245
+ return str.trim()
246
+ }
206
247
  function findNestedYaml(lines, startIdx, baseIndent, segments) {
207
248
  let currentIndent = baseIndent
208
249
  for (let i = startIdx + 1; i < lines.length; i++) {
@@ -213,7 +254,7 @@ function findNestedYaml(lines, startIdx, baseIndent, segments) {
213
254
  const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
214
255
  if (m) {
215
256
  const key = m[1]
216
- const valStr = m[2].trim()
257
+ const valStr = stripYamlComment(m[2])
217
258
  const seg = segments[0]
218
259
  if (key === seg) {
219
260
  if (valStr && valStr !== '' && valStr !== '{}') {
@@ -650,7 +691,7 @@ function loadRules(type, customPaths = [], repoPath = null) {
650
691
  grep: { pattern: ['string'] },
651
692
  json_field: { path: ['string'], field: ['string'], fallback_field: ['array', 'string'] },
652
693
  toml_field: { path: ['string'], field: ['string'] },
653
- yaml_field: { path: ['string'], field: ['string'] },
694
+ yaml_field: { path: ['string'], field: ['string'], fallback_field: ['array', 'string'] },
654
695
  directory_exists: { paths: ['array', 'string'] },
655
696
  not_exists: { paths: ['array', 'string'] },
656
697
  glob_count: { pattern: ['string'] },
@@ -1007,7 +1048,7 @@ function runCheck(rule, repoPath, mpj = null) {
1007
1048
  const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
1008
1049
  if (m && m[1] === segments[0]) {
1009
1050
  currentIndent = ind
1010
- const valStr = m[2].trim()
1051
+ const valStr = stripYamlComment(m[2])
1011
1052
  if (valStr && valStr !== '' && valStr !== '{}') {
1012
1053
  // 标量值(非嵌套)
1013
1054
  val = valStr
@@ -1028,6 +1069,77 @@ function runCheck(rule, repoPath, mpj = null) {
1028
1069
  passed = false
1029
1070
  evidence = '字段不存在或值不匹配'
1030
1071
  }
1072
+ // Issue#2 fix: yaml_field fallback_field——主字段不匹配时逐个尝试备选字段路径(数组形态)。
1073
+ // 用途:同一语义在不同 YAML 形态下的位置差异(如 id-token: write 可在 workflow 顶层
1074
+ // 或 publish job 级——PyPA 最小化权限模式),任一命中即 pass。
1075
+ // 段内 `*` 为通配(如 jobs.*.permissions.id-token):在该层级所有子键下逐个尝试。
1076
+ if (!passed && params?.fallback_field) {
1077
+ const fbList = Array.isArray(params.fallback_field) ? params.fallback_field : [params.fallback_field]
1078
+ for (const fbField of fbList) {
1079
+ if (!fbField) continue
1080
+ const fbSegments = String(fbField).split('.')
1081
+ const candidates = [] // 每项 {segments} —— 通配展开后的具体路径列表
1082
+ const expandWildcard = (segs) => {
1083
+ const wIdx = segs.indexOf('*')
1084
+ if (wIdx === -1) { candidates.push({ segments: segs }); return }
1085
+ // 找通配段父级的所有子键:先定位父级前缀(逐段下钻收集行号)
1086
+ const lines = content.split('\n')
1087
+ const prefix = segs.slice(0, wIdx)
1088
+ const suffix = segs.slice(wIdx + 1)
1089
+ // 顶层段
1090
+ if (prefix.length === 0) return
1091
+ for (let i = 0; i < lines.length; i++) {
1092
+ const l = lines[i]
1093
+ if (!l.trim() || l.trim().startsWith('#')) continue
1094
+ const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
1095
+ if (!m || m[1] !== prefix[0]) continue
1096
+ // 沿 prefix 下钻
1097
+ let curIdx = i, curIndent = l.length - l.trimStart().length
1098
+ let ok = true
1099
+ for (let d = 1; d < prefix.length; d++) {
1100
+ const sub = findNestedYamlKey(lines, curIdx, curIndent, prefix[d])
1101
+ if (!sub) { ok = false; break }
1102
+ curIdx = sub.idx; curIndent = sub.indent
1103
+ }
1104
+ if (!ok) return
1105
+ // 列出 curIndent 下一层的所有子键
1106
+ const childKeys = listChildKeys(lines, curIdx, curIndent)
1107
+ for (const k of childKeys) expandWildcard([...prefix, k, ...suffix])
1108
+ return
1109
+ }
1110
+ }
1111
+ expandWildcard(fbSegments)
1112
+ for (const cand of candidates) {
1113
+ let fbVal = null
1114
+ let fbFound = false
1115
+ try {
1116
+ const lines = content.split('\n')
1117
+ for (let i = 0; i < lines.length; i++) {
1118
+ const l = lines[i]
1119
+ if (!l.trim() || l.trim().startsWith('#')) continue
1120
+ const ind = l.length - l.trimStart().length
1121
+ const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
1122
+ if (m && m[1] === cand.segments[0]) {
1123
+ const v = stripYamlComment(m[2])
1124
+ if (v && v !== '' && v !== '{}') { fbVal = v; fbFound = true }
1125
+ else {
1126
+ const sub = findNestedYaml(lines, i, ind, cand.segments.slice(1))
1127
+ if (sub.found) { fbVal = sub.value; fbFound = true }
1128
+ }
1129
+ break
1130
+ }
1131
+ }
1132
+ } catch {}
1133
+ if (fbFound && String(fbVal).trim() === expected) {
1134
+ passed = true
1135
+ const concrete = cand.segments.join('.')
1136
+ evidence = `fallback ${concrete} = ${String(fbVal).trim()}`
1137
+ break
1138
+ }
1139
+ }
1140
+ if (passed) break
1141
+ }
1142
+ }
1031
1143
  break
1032
1144
  }
1033
1145
 
@@ -1539,7 +1651,11 @@ ${message}
1539
1651
  process.exit(hasCritical && strict ? 1 : 0)
1540
1652
  }
1541
1653
 
1542
- main().catch(err => {
1543
- console.error(`✗ 运行时错误: ${err.message}`)
1544
- process.exit(2)
1545
- })
1654
+ // Issue#1 fix: 入口守卫——仅直接作为 CLI 运行时执行主流程;被 import(测试/编程复用)不跑
1655
+ const isDirectRun = import.meta.url === `file://${process.argv[1]}`
1656
+ if (isDirectRun) {
1657
+ main().catch(err => {
1658
+ console.error(`✗ 运行时错误: ${err.message}`)
1659
+ process.exit(2)
1660
+ })
1661
+ }
@@ -54,8 +54,9 @@ rules:
54
54
  - "go.sum"
55
55
  - "requirements.lock"
56
56
  anyOf: true
57
- description: "锁文件应入库以确保可复现构建(BEST-PRACTICES §5.4 验证链)"
58
- note: "无包管理的仓库豁免"
57
+ description: "锁文件应入库以确保可复现构建(BEST-PRACTICES §5.4)——锁文件还须被 CI/验证链实际安装消费(npm ci / pip install -r requirements.lock 等),只入库不安装是装饰性合规;requirements.lock 非 pip 原生锁格式,CI 须显式写明安装它"
58
+ note: "无包管理的仓库豁免;零依赖仓(依赖声明为空,无锁对象)可用 .auditrc.yaml 豁免并注明原因"
59
+ fix_hint: "提交锁文件(package-lock.json / poetry.lock / go.sum / requirements.lock 任一)并确认 CI 安装步骤实际使用它(npm ci / pip install -r …);零依赖仓在 .auditrc.yaml 豁免注明"
59
60
 
60
61
  - id: QUA-005
61
62
  title: "CLAUDE.md 桥接"
@@ -72,8 +72,9 @@ rules:
72
72
  params:
73
73
  path: ".github/workflows/publish.yml"
74
74
  field: "permissions.id-token"
75
+ fallback_field: ["jobs.*.permissions.id-token"]
75
76
  expected: "write"
76
77
  skip_if_no_file: ".github/workflows/publish.yml"
77
- description: "publish workflow 必须请求 id-token: write 权限(OIDC Trusted Publishing"
78
+ description: "publish workflow 必须请求 id-token: write 权限(OIDC Trusted Publishing)——工作流顶层或 publish job 级任一即可(job 级为 PyPA 推荐的最小化模式)"
78
79
  template_ref: "templates/repo-root/.github/workflows/publish.yml"
79
- fix_hint: "在 workflow 顶部添加 permissions: id-token: write"
80
+ fix_hint: "在 publish workflow 声明 permissions.id-token: write——顶层(permissions: 块内)或仅 publish job(jobs.publish.permissions: 块内)均可;推荐 job 级(最小化,其他 job 不持有 OIDC 权限)"
@@ -1,7 +1,10 @@
1
1
  # AGENTS(AI 协作与工程纪律)
2
2
 
3
- > 本文件由模板**单源拼装**(`common/AGENTS-core.md` + 分类 append)——重复段不要在仓库里手改;
4
- > 改规则先改模板源,再重新生成。人工协作者同样适用本文件全部条款。
3
+ > **来源声明(二选一,按实际来源保留其一,删除另一行)**:
4
+ > - 由 scaffold 生成/更新(仓在 `.scaffold/lock/`):单源拼装(`common/AGENTS-core.md` + 分类 append)——重复段勿手改,改规则先改模板源,再跑 `node scaffold.mjs --update <repo>` 合并。
5
+ > - 手写/移植(无 `.scaffold/lock/`):本文件归本仓库自主维护,直接在仓库内改,单源约束不适用。
6
+ >
7
+ > 人工协作者同样适用本文件全部条款。
5
8
 
6
9
  ## 项目概览
7
10
 
@@ -61,12 +61,17 @@ jobs:
61
61
  # smoke/mount/vitest 为探测式:文件存在才跑,接入即自动进链。
62
62
  run: node scripts/verify.mjs
63
63
  - name: Publish to npm (OIDC trusted publishing)
64
- # 升级 npm:trusted publishing 需 npm CLI >= 11.5.1(含 OIDC 支持)。
64
+ # trusted publishing 需 npm CLI >= 11.5.1(含 OIDC 支持)。
65
+ # 上游 runner Node 自带的 npm 已满足时直接用——不装 npm@latest:
66
+ # 其 engine 门槛会随版本漂移(npm@12 要求 node ^22.22.2,曾致本仓 EBADENGINE),
67
+ # 仅当自带版本过旧时兜底升级到 npm@11(比 latest 门槛低、仍含 OIDC)。
65
68
  # prerelease → dist-tag next(忘加 --tag next 是最常见事故:
66
69
  # 用户 npm install 会装到 prerelease);稳定版 → latest。
67
70
  run: |
68
- npm install -g npm@latest
69
71
  echo "npm version: $(npm --version)"
72
+ if ! npm --version | grep -qE '^(11\.(5\.[1-9]|[6-9]|[0-9]{2})|1[2-9]|2[0-9])'; then
73
+ npm install -g npm@11
74
+ fi
70
75
  VERSION="$(node -p "require('./package.json').version")"
71
76
  if [[ "$VERSION" == *-* ]]; then
72
77
  echo "canary prerelease $VERSION → dist-tag next"