repo-audit-tool 1.4.0 → 1.4.1

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.
@@ -2,12 +2,11 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "title": "repo-audit 文档索引",
4
4
  "description": "AI Agent 可读的文档索引 — 包含所有文档的元数据、结构、和调用协议",
5
- "version": "1.4.0",
6
- "updated": "2026-09-08",
5
+ "version": "1.4.1",
6
+ "updated": "2026-09-09",
7
7
  "repo": "NinjaSln-labs/repo-audit",
8
8
  "npm": "repo-audit-tool",
9
9
  "pages": "https://ninjasln-labs.github.io/repo-audit/",
10
-
11
10
  "entry_points": {
12
11
  "json_index": "/AGENT-INDEX.json",
13
12
  "schema": "/SCHEMA.json",
@@ -15,33 +14,72 @@
15
14
  "human_guide": "/repo-audit/HUMAN-GUIDE.md",
16
15
  "agent_guide": "/repo-audit/AGENT-GUIDE.md"
17
16
  },
18
-
19
17
  "tool": {
20
18
  "name": "repo-audit",
21
19
  "description": "仓库标准化审计工具 + 工程脚手架生成器(零依赖)",
22
20
  "bin": "repo-audit",
23
- "version": "1.4.0",
21
+ "version": "1.4.1",
24
22
  "language": "JavaScript (ESM)",
25
23
  "runtime": "Node.js >= 18",
26
24
  "dependencies": [],
27
25
  "license": "MIT",
28
-
29
26
  "invocation": {
30
27
  "command": "repo-audit [options]",
31
28
  "options": {
32
- "--repo": { "type": "string", "default": ".", "description": "目标仓库路径" },
33
- "--type": { "type": "string", "description": "强制指定分类(跳过自动检测)" },
34
- "--format": { "type": "string", "enum": ["md", "json", "both"], "default": "both", "description": "输出格式" },
35
- "--output": { "type": "string", "default": "<repo>/audit-report/", "description": "输出目录" },
36
- "--llm-provider": { "type": "string", "description": "LLM 协议:openai/anthropic 或厂商别名" },
37
- "--llm-model": { "type": "string", "description": "模型名称" },
38
- "--strict": { "type": "boolean", "default": false, "description": "严格模式:Critical/Major 即 exit 1" },
39
- "--rules": { "type": "string", "description": "自定义规则 YAML(可多次)" },
40
- "--dim": { "type": "string", "description": "只审计指定维度(可多次)" },
41
- "--feedback": { "type": "string", "description": "提交反馈(创建 GitHub Issue)" },
42
- "--help": { "type": "boolean", "description": "显示帮助" }
29
+ "--repo": {
30
+ "type": "string",
31
+ "default": ".",
32
+ "description": "目标仓库路径"
33
+ },
34
+ "--type": {
35
+ "type": "string",
36
+ "description": "强制指定分类(跳过自动检测)"
37
+ },
38
+ "--format": {
39
+ "type": "string",
40
+ "enum": [
41
+ "md",
42
+ "json",
43
+ "both"
44
+ ],
45
+ "default": "both",
46
+ "description": "输出格式"
47
+ },
48
+ "--output": {
49
+ "type": "string",
50
+ "default": "<repo>/audit-report/",
51
+ "description": "输出目录"
52
+ },
53
+ "--llm-provider": {
54
+ "type": "string",
55
+ "description": "LLM 协议:openai/anthropic 或厂商别名"
56
+ },
57
+ "--llm-model": {
58
+ "type": "string",
59
+ "description": "模型名称"
60
+ },
61
+ "--strict": {
62
+ "type": "boolean",
63
+ "default": false,
64
+ "description": "严格模式:Critical/Major 即 exit 1"
65
+ },
66
+ "--rules": {
67
+ "type": "string",
68
+ "description": "自定义规则 YAML(可多次)"
69
+ },
70
+ "--dim": {
71
+ "type": "string",
72
+ "description": "只审计指定维度(可多次)"
73
+ },
74
+ "--feedback": {
75
+ "type": "string",
76
+ "description": "提交反馈(创建 GitHub Issue)"
77
+ },
78
+ "--help": {
79
+ "type": "boolean",
80
+ "description": "显示帮助"
81
+ }
43
82
  },
44
-
45
83
  "exit_codes": {
46
84
  "0": "审计通过(无 Critical/Major 或 --strict 未启用)",
47
85
  "1": "有 Critical/Major 发现且 --strict 启用",
@@ -49,7 +87,6 @@
49
87
  "3": "目标不是 git 仓库"
50
88
  }
51
89
  },
52
-
53
90
  "environment_variables": {
54
91
  "LLM_PROVIDER": "LLM 协议(openai/anthropic/none),也可用厂商别名如 deepseek/qwen/glm",
55
92
  "LLM_API_KEY": "API Key(必须手动指定)",
@@ -58,35 +95,58 @@
58
95
  "REPO_AUDIT_TYPES": "自定义分类列表(JSON 数组)"
59
96
  }
60
97
  },
61
-
62
98
  "audit": {
63
99
  "dimensions": [
64
100
  {
65
101
  "id": "docs",
66
102
  "label": "文档",
67
- "rules": ["DOC-001", "DOC-002", "DOC-003", "DOC-003b", "DOC-004", "DOC-005", "DOC-006", "DOC-007"],
103
+ "rules": [
104
+ "DOC-001",
105
+ "DOC-002",
106
+ "DOC-003",
107
+ "DOC-003b",
108
+ "DOC-004",
109
+ "DOC-005",
110
+ "DOC-006",
111
+ "DOC-007"
112
+ ],
68
113
  "description": "README/LICENSE/CONTRIBUTING/SECURITY 等文档完整性"
69
114
  },
70
115
  {
71
116
  "id": "git",
72
117
  "label": "Git",
73
- "rules": ["GIT-001", "GIT-002", "GIT-003", "GIT-004"],
118
+ "rules": [
119
+ "GIT-001",
120
+ "GIT-002",
121
+ "GIT-003",
122
+ "GIT-004"
123
+ ],
74
124
  "description": "commit 规范、分支策略、scaffold 管理"
75
125
  },
76
126
  {
77
127
  "id": "quality",
78
128
  "label": "质量",
79
- "rules": ["QUA-001", "QUA-002", "QUA-003", "QUA-004", "QUA-005"],
129
+ "rules": [
130
+ "QUA-001",
131
+ "QUA-002",
132
+ "QUA-003",
133
+ "QUA-004",
134
+ "QUA-005"
135
+ ],
80
136
  "description": "构建配置、测试、lint、typecheck"
81
137
  },
82
138
  {
83
139
  "id": "security",
84
140
  "label": "安全",
85
- "rules": ["SEC-001", "SEC-002", "SEC-003", "SEC-004"],
141
+ "rules": [
142
+ "SEC-001",
143
+ "SEC-002",
144
+ "SEC-003",
145
+ "SEC-004"
146
+ ],
86
147
  "description": "密钥泄露、依赖漏洞、权限配置"
87
148
  }
88
149
  ],
89
-
90
150
  "scoring": {
91
151
  "formula": "score = 100 - (critical × 25) - (major × 10) - (minor × 3)",
92
152
  "grades": {
@@ -98,14 +158,26 @@
98
158
  },
99
159
  "note": "score 和 grade 不在 JSON 输出中,Agent 需根据 summary 字段自行计算"
100
160
  },
101
-
102
161
  "categories": [
103
- "dsh-plugin", "python-app", "go-service", "content", "sandbox",
104
- "archive", "product-oss", "javascript", "typescript", "java",
105
- "python", "rust", "cpp", "dotnet", "php", "ruby", "unknown"
162
+ "dsh-plugin",
163
+ "python-app",
164
+ "go-service",
165
+ "content",
166
+ "sandbox",
167
+ "archive",
168
+ "product-oss",
169
+ "javascript",
170
+ "typescript",
171
+ "java",
172
+ "python",
173
+ "rust",
174
+ "cpp",
175
+ "dotnet",
176
+ "php",
177
+ "ruby",
178
+ "unknown"
106
179
  ]
107
180
  },
108
-
109
181
  "documents": [
110
182
  {
111
183
  "id": "human-guide",
@@ -189,12 +261,10 @@
189
261
  "description": "JSON Schema 定义,用于验证审计报告输出"
190
262
  }
191
263
  ],
192
-
193
264
  "feedback": {
194
265
  "description": "反馈通道 — Agent 可通过 GitHub Issues API 检测和处理用户反馈",
195
266
  "label": "audit-feedback",
196
267
  "repo": "NinjaSln-labs/repo-audit",
197
-
198
268
  "detect": {
199
269
  "command": "gh issue list --repo NinjaSln-labs/repo-audit --label audit-feedback --state open --json number,title,createdAt,body",
200
270
  "description": "列出未处理的反馈"
@@ -211,15 +281,29 @@
211
281
  "command": "node repo-audit.mjs --feedback '<反馈内容>'",
212
282
  "description": "通过 CLI 创建反馈 Issue(自动预填环境信息)"
213
283
  },
214
-
215
284
  "priorities": [
216
- { "level": "critical", "sla": "24h", "description": "工具崩溃/数据损坏" },
217
- { "level": "major", "sla": "72h", "description": "功能严重受损" },
218
- { "level": "minor", "sla": "1周", "description": "功能部分受损" },
219
- { "level": "info", "sla": "排期", "description": "建议改进" }
285
+ {
286
+ "level": "critical",
287
+ "sla": "24h",
288
+ "description": "工具崩溃/数据损坏"
289
+ },
290
+ {
291
+ "level": "major",
292
+ "sla": "72h",
293
+ "description": "功能严重受损"
294
+ },
295
+ {
296
+ "level": "minor",
297
+ "sla": "1周",
298
+ "description": "功能部分受损"
299
+ },
300
+ {
301
+ "level": "info",
302
+ "sla": "排期",
303
+ "description": "建议改进"
304
+ }
220
305
  ]
221
306
  },
222
-
223
307
  "quick_start": {
224
308
  "install": "npm install -g repo-audit-tool",
225
309
  "audit": "repo-audit --repo /path/to/repo",
@@ -229,7 +313,6 @@
229
313
  "custom_rules": "repo-audit --repo /path/to/repo --rules my-rules.yaml",
230
314
  "llm_enhanced": "LLM_API_KEY=sk-xxx repo-audit --repo . --llm-provider deepseek"
231
315
  },
232
-
233
316
  "changelog": {
234
317
  "1.4.0": "json_field workspace 递归——monorepo 场景下递归检查所有 workspace 包的 license 字段;findWorkspacePackages 发现 npm/pnpm workspaces + 常见目录;checkJsonFieldContent 提取为可复用函数;DOC-003b 启用;15 用例回归",
235
318
  "1.3.3": "Issue#5 修复:.auditrc.yaml since 字段 YAML 引号剥离——loadAuditrc 中 since: 分支补 .replace(/['\"]/g, ''),与 reason 分支对称,修复 waived_since 输出含字面引号的 bug;5 用例回归(test/auditrc-since.test.mjs)",
@@ -239,6 +322,7 @@
239
322
  "1.2.3": "package.json 补充 repository 字段,修复 npm provenance E422;完成 Trusted Publisher tag 自动发布",
240
323
  "1.2.2": "publish CI 改用 node 22 + 条件升级 npm,修复 EBADENGINE(tag 发布因 1.2.1 已存在未出包)",
241
324
  "1.2.1": "README 链接改为绝对 URL(修复 npm 页面 404)",
242
- "1.2.0": "开源就绪:AGENTS.md、CI/publish workflow、json_field monorepo、fallback_field 数组、英文 README、参数类型校验、占位符扫描"
325
+ "1.2.0": "开源就绪:AGENTS.md、CI/publish workflow、json_field monorepo、fallback_field 数组、英文 README、参数类型校验、占位符扫描",
326
+ "1.4.1": "yaml_field 解析健壮性——CRLF 行尾剥离 + 引号键名识别 + 子层缩进以首个子键为准(SEC-004 job 级 id-token 误判 fail,Issue#6);6 用例回归"
243
327
  }
244
328
  }
@@ -275,6 +275,10 @@ EOF
275
275
  node repo-audit.mjs --repo /path/to/repo --rules /tmp/my-rules.yaml --format json
276
276
  ```
277
277
 
278
+ ### 4.7 yaml_field fallback_field 与解析健壮性(Issue#6)
279
+
280
+ `yaml_field` 支持 `fallback_field`(数组):主字段不匹配时逐个尝试备选路径,段内 `*` 为通配展开(如 `jobs.*.permissions.id-token`——任一 job 级命中即 pass,覆盖 PyPA 最小化权限模式)。解析层(v1.4.1)统一剥离 CRLF 行尾 `\r`、识别引号键名、按首个子键实际缩进确定子层(2/4 空格风格均支持)。误判排查时优先确认 workflow 文件是否踩过这三类形态。
281
+
278
282
  ---
279
283
 
280
284
  ## 五、错误处理协议
@@ -688,6 +688,15 @@ params:
688
688
  fallback_field: "scripts.test"
689
689
  ```
690
690
 
691
+ ### 12.4b yaml_field fallback_field 与解析健壮性(Issue#6)
692
+
693
+ `yaml_field` 支持 `fallback_field`(数组):主字段不匹配时逐个尝试备选路径,段内 `*` 为通配(如 `jobs.*.permissions.id-token`——顶层无 `permissions.id-token` 时,任一 job 级命中即 pass,覆盖 PyPA 最小化权限模式)。
694
+
695
+ 解析健壮性(v1.4.1 修复,Issue#6):
696
+ - **CRLF 行尾**:行解析前统一剥离 `\r`,Windows 编辑的 workflow 不再标量失配
697
+ - **引号键名**:`"publish":` / `'publish':` 与裸键等价识别
698
+ - **缩进风格**:子层缩进以首个子键实际缩进为准(2/4 空格等风格均支持,不再硬编码 +2)
699
+
691
700
  ### 12.5 toml_field expect_table
692
701
 
693
702
  检查 TOML 表头存在性(而非表内键值):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repo-audit-tool",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "仓库标准化审计工具 + 工程脚手架生成器(零依赖)",
5
5
  "type": "module",
6
6
  "main": "repo-audit.mjs",
package/repo-audit.mjs CHANGED
@@ -325,30 +325,49 @@ function countNonHiddenFiles(path) {
325
325
  return count
326
326
  }
327
327
 
328
- // Issue#2 fix: 定位指定缩进层级下的具名子键(返回行号与缩进;未找到返回 null)
328
+ // Issue#6 fix: 规范化 YAML 行——剥离行尾 \r(CRLF 文件在 Linux 审计时 (.*)$ 不吞 \r,
329
+ // 标量值带尾 \r 导致与 expected 比较全部失配);行内键名引号在匹配处单独处理
330
+ function normalizeYamlLines(content) {
331
+ return String(content).split('\n').map(l => l.replace(/\r$/, ''))
332
+ }
333
+ // Issue#6 fix: YAML 键名匹配——兼容裸键与引号键("publish": / 'publish':),
334
+ // 返回 [key, valueRest];非键行返回 null
335
+ function matchYamlKey(l) {
336
+ const m = l.match(/^(\s*)(?:"([^"]+)"|'([^']+)'|(\w[\w\-]*))\s*:\s*(.*)$/)
337
+ if (!m) return null
338
+ return [m[2] ?? m[3] ?? m[4], m[5]]
339
+ }
340
+
341
+ // Issue#6 fix: 定位指定缩进层级下的具名子键(返回行号与缩进;未找到返回 null)
342
+ // 子层缩进以本层首个子键的实际缩进为准(兼容 2/4 空格等缩进风格),不再硬编码 +2
329
343
  function findNestedYamlKey(lines, startIdx, baseIndent, key) {
344
+ let childIndent = -1
330
345
  for (let i = startIdx + 1; i < lines.length; i++) {
331
346
  const l = lines[i]
332
347
  if (!l.trim() || l.trim().startsWith('#')) continue
333
348
  const ind = l.length - l.trimStart().length
334
349
  if (ind <= baseIndent) return null
335
- if (ind !== baseIndent + 2) continue // 只看直接子层(YAML 常规 2 空格缩进)
336
- const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
337
- if (m && m[1] === key) return { idx: i, indent: ind }
350
+ if (childIndent === -1) childIndent = ind // 本层首个子键确定子层缩进
351
+ if (ind !== childIndent) continue // 只看直接子层
352
+ const km = matchYamlKey(l)
353
+ if (km && km[0] === key) return { idx: i, indent: ind }
338
354
  }
339
355
  return null
340
356
  }
341
357
  // Issue#2 fix: 列出指定父键下一层的所有子键名(通配 * 展开用)
358
+ // 子层缩进同样以首个子键实际缩进为准(与 findNestedYamlKey 对称)
342
359
  function listChildKeys(lines, startIdx, baseIndent) {
343
360
  const keys = []
361
+ let childIndent = -1
344
362
  for (let i = startIdx + 1; i < lines.length; i++) {
345
363
  const l = lines[i]
346
364
  if (!l.trim() || l.trim().startsWith('#')) continue
347
365
  const ind = l.length - l.trimStart().length
348
366
  if (ind <= baseIndent) break
349
- if (ind !== baseIndent + 2) continue
350
- const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
351
- if (m) keys.push(m[1])
367
+ if (childIndent === -1) childIndent = ind
368
+ if (ind !== childIndent) continue
369
+ const km = matchYamlKey(l)
370
+ if (km) keys.push(km[0])
352
371
  }
353
372
  return keys
354
373
  }
@@ -374,10 +393,10 @@ function findNestedYaml(lines, startIdx, baseIndent, segments) {
374
393
  if (!l.trim() || l.trim().startsWith('#')) continue
375
394
  const ind = l.length - l.trimStart().length
376
395
  if (ind <= currentIndent) break // 退出当前层级
377
- const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
378
- if (m) {
379
- const key = m[1]
380
- const valStr = stripYamlComment(m[2])
396
+ const km = matchYamlKey(l)
397
+ if (km) {
398
+ const key = km[0]
399
+ const valStr = stripYamlComment(km[1])
381
400
  const seg = segments[0]
382
401
  if (key === seg) {
383
402
  if (valStr && valStr !== '' && valStr !== '{}') {
@@ -902,6 +921,11 @@ function hasEquivalentDocs(repoPath) {
902
921
  // 规则检查引擎
903
922
  // ============================================================
904
923
 
924
+ // Issue#6 回归需要进程内直调(win32 下 spawnSync node 走 PATHEXT 解析不可靠,P-007 同族坑)
925
+ export function runCheckForTest(rule, repoPath, mpj = null) {
926
+ return runCheck(rule, repoPath, mpj)
927
+ }
928
+
905
929
  function runCheck(rule, repoPath, mpj = null) {
906
930
  const { check, params } = rule
907
931
  let passed = false
@@ -1187,7 +1211,7 @@ function runCheck(rule, repoPath, mpj = null) {
1187
1211
  let val = null
1188
1212
  let found = false
1189
1213
  try {
1190
- const lines = content.split('\n')
1214
+ const lines = normalizeYamlLines(content)
1191
1215
  let currentIndent = -1
1192
1216
  let currentObj = null
1193
1217
  // 第一遍:找到顶层键
@@ -1195,10 +1219,10 @@ function runCheck(rule, repoPath, mpj = null) {
1195
1219
  const l = lines[i]
1196
1220
  if (!l.trim() || l.trim().startsWith('#')) continue
1197
1221
  const ind = l.length - l.trimStart().length
1198
- const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
1199
- if (m && m[1] === segments[0]) {
1222
+ const km = matchYamlKey(l)
1223
+ if (km && km[0] === segments[0]) {
1200
1224
  currentIndent = ind
1201
- const valStr = stripYamlComment(m[2])
1225
+ const valStr = stripYamlComment(km[1])
1202
1226
  if (valStr && valStr !== '' && valStr !== '{}') {
1203
1227
  // 标量值(非嵌套)
1204
1228
  val = valStr
@@ -1233,7 +1257,7 @@ function runCheck(rule, repoPath, mpj = null) {
1233
1257
  const wIdx = segs.indexOf('*')
1234
1258
  if (wIdx === -1) { candidates.push({ segments: segs }); return }
1235
1259
  // 找通配段父级的所有子键:先定位父级前缀(逐段下钻收集行号)
1236
- const lines = content.split('\n')
1260
+ const lines = normalizeYamlLines(content)
1237
1261
  const prefix = segs.slice(0, wIdx)
1238
1262
  const suffix = segs.slice(wIdx + 1)
1239
1263
  // 顶层段
@@ -1241,8 +1265,8 @@ function runCheck(rule, repoPath, mpj = null) {
1241
1265
  for (let i = 0; i < lines.length; i++) {
1242
1266
  const l = lines[i]
1243
1267
  if (!l.trim() || l.trim().startsWith('#')) continue
1244
- const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
1245
- if (!m || m[1] !== prefix[0]) continue
1268
+ const km = matchYamlKey(l)
1269
+ if (!km || km[0] !== prefix[0]) continue
1246
1270
  // 沿 prefix 下钻
1247
1271
  let curIdx = i, curIndent = l.length - l.trimStart().length
1248
1272
  let ok = true
@@ -1263,14 +1287,14 @@ function runCheck(rule, repoPath, mpj = null) {
1263
1287
  let fbVal = null
1264
1288
  let fbFound = false
1265
1289
  try {
1266
- const lines = content.split('\n')
1290
+ const lines = normalizeYamlLines(content)
1267
1291
  for (let i = 0; i < lines.length; i++) {
1268
1292
  const l = lines[i]
1269
1293
  if (!l.trim() || l.trim().startsWith('#')) continue
1270
1294
  const ind = l.length - l.trimStart().length
1271
- const m = l.match(/^\s*(\w[\w\-]*)\s*:\s*(.*)$/)
1272
- if (m && m[1] === cand.segments[0]) {
1273
- const v = stripYamlComment(m[2])
1295
+ const km = matchYamlKey(l)
1296
+ if (km && km[0] === cand.segments[0]) {
1297
+ const v = stripYamlComment(km[1])
1274
1298
  if (v && v !== '' && v !== '{}') { fbVal = v; fbFound = true }
1275
1299
  else {
1276
1300
  const sub = findNestedYaml(lines, i, ind, cand.segments.slice(1))