repo-audit-tool 1.3.3 → 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/PUBLISHING.md CHANGED
@@ -10,6 +10,10 @@
10
10
 
11
11
  ## 版本历史
12
12
 
13
+ - **1.4.0** — json_field workspace 递归:monorepo 场景下递归检查所有 workspace 包
14
+ 的 license 字段;findWorkspacePackages(npm workspaces 数组 / pnpm workspaces.packages /
15
+ 常见目录扫描兜底)+ checkJsonFieldContent 提取复用 + DOC-003b 启用;
16
+ 15 用例回归(2026-09-08)
13
17
  - **1.3.2** — Issue#4 修复:入口守卫 `pathToFileURL` 归一化,修复 Windows 全形态
14
18
  静默不执行(v1.3.1 回归);守卫 stderr 诊断提示;command 检查器 Git Bash 探测;
15
19
  CI 加 windows-latest 矩阵 + npm 安装 smoke 步骤(2026-09-07)
@@ -2,8 +2,8 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "title": "repo-audit 文档索引",
4
4
  "description": "AI Agent 可读的文档索引 — 包含所有文档的元数据、结构、和调用协议",
5
- "version": "1.3.2",
6
- "updated": "2026-09-07",
5
+ "version": "1.4.0",
6
+ "updated": "2026-09-08",
7
7
  "repo": "NinjaSln-labs/repo-audit",
8
8
  "npm": "repo-audit-tool",
9
9
  "pages": "https://ninjasln-labs.github.io/repo-audit/",
@@ -20,7 +20,7 @@
20
20
  "name": "repo-audit",
21
21
  "description": "仓库标准化审计工具 + 工程脚手架生成器(零依赖)",
22
22
  "bin": "repo-audit",
23
- "version": "1.3.2",
23
+ "version": "1.4.0",
24
24
  "language": "JavaScript (ESM)",
25
25
  "runtime": "Node.js >= 18",
26
26
  "dependencies": [],
@@ -231,6 +231,8 @@
231
231
  },
232
232
 
233
233
  "changelog": {
234
+ "1.4.0": "json_field workspace 递归——monorepo 场景下递归检查所有 workspace 包的 license 字段;findWorkspacePackages 发现 npm/pnpm workspaces + 常见目录;checkJsonFieldContent 提取为可复用函数;DOC-003b 启用;15 用例回归",
235
+ "1.3.3": "Issue#5 修复:.auditrc.yaml since 字段 YAML 引号剥离——loadAuditrc 中 since: 分支补 .replace(/['\"]/g, ''),与 reason 分支对称,修复 waived_since 输出含字面引号的 bug;5 用例回归(test/auditrc-since.test.mjs)",
234
236
  "1.3.2": "Issue#4 修复:入口守卫 pathToFileURL 归一化——修复 Windows 全形态静默不执行(v1.3.1 回归);守卫不命中时 stderr 诊断提示;command 检查器 Git Bash 探测;CI 双平台矩阵 + npm 安装 smoke",
235
237
  "1.3.1": "入口守卫 realpath 归一化——修复 npm bin symlink 下主流程不执行;1.3.0 反馈修复:注释剥离/SEC-004/AGENTS 声明/QUA-004/--update 动线",
236
238
  "1.3.0": "反馈修复:yaml_field 行内注释剥离 + fallback_field 通配(SEC-004 兼容 job 级权限);AGENTS-core 来源声明二选一;QUA-004 锁文件消费意图;手册补 --update 存量补齐动线",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repo-audit-tool",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "仓库标准化审计工具 + 工程脚手架生成器(零依赖)",
5
5
  "type": "module",
6
6
  "main": "repo-audit.mjs",
package/repo-audit.mjs CHANGED
@@ -170,6 +170,129 @@ function findPackageJson(repoPath) {
170
170
  return null
171
171
  }
172
172
 
173
+ // ============================================================
174
+ // workspace 递归 — 发现所有 workspace 包(根 + workspace globs + 常见目录)
175
+ // ============================================================
176
+
177
+ export function findWorkspacePackages(repoPath) {
178
+ const results = []
179
+ const seen = new Set()
180
+
181
+ const tryAdd = (pkgPath) => {
182
+ const resolved = resolve(pkgPath)
183
+ if (seen.has(resolved)) return
184
+ seen.add(resolved)
185
+ // 跳过 node_modules 和隐藏目录
186
+ const parts = resolved.split('/').join('\/')
187
+ if (parts.includes('node_modules')) return
188
+ const pkg = readJsonSafe(pkgPath)
189
+ if (pkg) {
190
+ results.push({ path: pkgPath, pkg, dir: dirname(pkgPath) })
191
+ }
192
+ }
193
+
194
+ // 1. 根 package.json
195
+ const rootPkgPath = join(repoPath, 'package.json')
196
+ tryAdd(rootPkgPath)
197
+
198
+ // 2. 根 package.json 的 workspaces 字段(npm: 数组 / pnpm: {packages:[...]})
199
+ const rootPkg = readJsonSafe(rootPkgPath)
200
+ if (rootPkg?.workspaces) {
201
+ const globs = Array.isArray(rootPkg.workspaces)
202
+ ? rootPkg.workspaces
203
+ : (rootPkg.workspaces.packages || [])
204
+ for (const glob of globs) {
205
+ expandWorkspaceGlob(repoPath, glob, tryAdd)
206
+ }
207
+ }
208
+
209
+ // 3. 常见 workspace 目录扫描(无 workspaces 字段时的兜底)
210
+ const patterns = ['apps', 'packages', 'workspaces', 'components', 'modules', 'libs', 'projects']
211
+ for (const pattern of patterns) {
212
+ const patternDir = join(repoPath, pattern)
213
+ if (!existsSync(patternDir)) continue
214
+ try {
215
+ for (const entry of readdirSync(patternDir, { withFileTypes: true })) {
216
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue
217
+ tryAdd(join(patternDir, entry.name, 'package.json'))
218
+ }
219
+ } catch {}
220
+ }
221
+
222
+ return results
223
+ }
224
+
225
+ function expandWorkspaceGlob(repoPath, glob, tryAdd) {
226
+ // 直接路径(无 glob 字符)
227
+ if (!glob.includes('*')) {
228
+ tryAdd(join(repoPath, glob, 'package.json'))
229
+ return
230
+ }
231
+ // 常见 glob 模式:base/* 或 base/**/*
232
+ const baseDir = join(repoPath, glob.split('*')[0].replace(/\/$/, ''))
233
+ if (!existsSync(baseDir)) return
234
+ try {
235
+ for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
236
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue
237
+ tryAdd(join(baseDir, entry.name, 'package.json'))
238
+ }
239
+ } catch {}
240
+ }
241
+
242
+ // ============================================================
243
+ // json_field 核心检查逻辑(供 workspace 递归复用)
244
+ // ============================================================
245
+
246
+ export function checkJsonFieldContent(content, params) {
247
+ const result = { passed: false, evidence: '', value: undefined }
248
+ try {
249
+ const obj = JSON.parse(content)
250
+ const fields = (params?.field || '').split('.')
251
+ let val = obj
252
+ let missing = false
253
+ let missingField = null
254
+ for (const f of fields) {
255
+ if (val === undefined || val === null) { missing = true; missingField = f; break }
256
+ val = val[f]
257
+ }
258
+ if (missing) {
259
+ result.evidence = `字段 ${missingField} 不存在`
260
+ } else if (val !== undefined && val !== null) {
261
+ result.passed = true
262
+ result.value = val
263
+ result.evidence = `字段 ${params.field} = ${JSON.stringify(val)?.slice(0, 100)}`
264
+ } else {
265
+ result.evidence = `字段 ${params.field} = ${JSON.stringify(val)?.slice(0, 100)}`
266
+ }
267
+ // fallback_field(如 peerDeps 不存在时检查 devDeps)
268
+ if (!result.passed && params?.fallback_field) {
269
+ const fbFields = Array.isArray(params.fallback_field)
270
+ ? params.fallback_field
271
+ : params.fallback_field.split('.')
272
+ for (const candidate of fbFields) {
273
+ const segs = Array.isArray(candidate) ? candidate : candidate.split('.')
274
+ let fbVal = obj
275
+ for (const f of segs) {
276
+ if (fbVal === undefined || fbVal === null) break
277
+ fbVal = fbVal[f]
278
+ }
279
+ if (fbVal !== undefined && fbVal !== null) {
280
+ result.passed = true
281
+ result.value = fbVal
282
+ const matchedField = Array.isArray(params.fallback_field)
283
+ ? String(candidate)
284
+ : params.fallback_field
285
+ result.evidence = `字段 ${params.field} 不存在,但 ${matchedField} = ${JSON.stringify(fbVal)?.slice(0, 100)}`
286
+ break
287
+ }
288
+ }
289
+ }
290
+ } catch (e) {
291
+ result.evidence = `JSON 解析失败: ${e.message}`
292
+ }
293
+ return result
294
+ }
295
+
173
296
  function readFileSafe(path) {
174
297
 
175
298
  try { return readFileSync(path, 'utf-8') } catch { return null }
@@ -724,7 +847,7 @@ function loadRules(type, customPaths = [], repoPath = null) {
724
847
  file_header: { path: ['string'], pattern: ['string'] },
725
848
  regex: { path: ['string'], pattern: ['string'] },
726
849
  grep: { pattern: ['string'] },
727
- json_field: { path: ['string'], field: ['string'], fallback_field: ['array', 'string'] },
850
+ json_field: { path: ['string'], field: ['string'], fallback_field: ['array', 'string'], workspace_recursive: ['boolean'] },
728
851
  toml_field: { path: ['string'], field: ['string'] },
729
852
  yaml_field: { path: ['string'], field: ['string'], fallback_field: ['array', 'string'] },
730
853
  directory_exists: { paths: ['array', 'string'] },
@@ -962,53 +1085,45 @@ function runCheck(rule, repoPath, mpj = null) {
962
1085
  }
963
1086
 
964
1087
  case 'json_field': {
1088
+ const pathParam = params?.path
1089
+ const workspaceRecursive = params?.workspace_recursive === true
1090
+
1091
+ // workspace 递归:检查所有 workspace 包
1092
+ if (workspaceRecursive && pathParam === 'package.json') {
1093
+ const allPkgs = findWorkspacePackages(repoPath)
1094
+ if (allPkgs.length === 0) {
1095
+ passed = false; evidence = '未找到任何 package.json'; break
1096
+ }
1097
+ const results = []
1098
+ let allPassed = true
1099
+ for (const wp of allPkgs) {
1100
+ const content = readFileSafe(wp.path)
1101
+ if (!content) {
1102
+ results.push(`${wp.dir === repoPath ? '根' : relative(repoPath, wp.dir)}: 文件不可读 ✗`)
1103
+ allPassed = false; continue
1104
+ }
1105
+ const r = checkJsonFieldContent(content, params)
1106
+ const relPath = wp.dir === repoPath ? '根' : relative(repoPath, wp.dir)
1107
+ results.push(`${relPath}: ${r.evidence} ${r.passed ? '✓' : '✗'}`)
1108
+ if (!r.passed) allPassed = false
1109
+ }
1110
+ passed = allPassed
1111
+ evidence = results.join(' | ')
1112
+ break
1113
+ }
1114
+
965
1115
  // P6: monorepo 感知 — package.json 不在根时 fallback 到 workspace 目录
966
- let actualPath = join(repoPath, params?.path)
1116
+ let actualPath = join(repoPath, pathParam)
967
1117
  let workspaceHint = ''
968
- if (!readFileSafe(actualPath) && params?.path === 'package.json' && mpj) {
1118
+ if (!readFileSafe(actualPath) && pathParam === 'package.json' && mpj) {
969
1119
  actualPath = mpj.path
970
1120
  workspaceHint = `(workspace: ${mpj.dir})`
971
1121
  }
972
1122
  const content = readFileSafe(actualPath)
973
1123
  if (!content) { passed = false; evidence = '文件不存在'; break }
974
- try {
975
- const obj = JSON.parse(content)
976
- const fields = (params?.field || '').split('.')
977
- let val = obj
978
- let missing = false
979
- for (const f of fields) {
980
- if (val === undefined || val === null) { passed = false; evidence = `字段 ${f} 不存在`; missing = true; break }
981
- val = val[f]
982
- }
983
- if (!missing) {
984
- passed = val !== undefined && val !== null
985
- evidence = `字段 ${params.field} = ${JSON.stringify(val)?.slice(0, 100)}${workspaceHint}`
986
- }
987
- // P1-2 fix: 支持 fallback_field(如 peerDeps 不存在时检查 devDeps)
988
- // 支持两种格式:数组 ["deps", "peerDeps", "devDeps"] 或字符串 "deps"(点路径)
989
- if (!passed && params?.fallback_field) {
990
- const fbFields = Array.isArray(params.fallback_field)
991
- ? params.fallback_field
992
- : params.fallback_field.split('.')
993
- // 对每个候选字段,尝试在 obj 中查找
994
- for (const candidate of fbFields) {
995
- const segs = Array.isArray(candidate) ? candidate : candidate.split('.')
996
- let fbVal = obj
997
- for (const f of segs) {
998
- if (fbVal === undefined || fbVal === null) break
999
- fbVal = fbVal[f]
1000
- }
1001
- if (fbVal !== undefined && fbVal !== null) {
1002
- passed = true
1003
- const matchedField = Array.isArray(params.fallback_field)
1004
- ? String(candidate)
1005
- : params.fallback_field
1006
- evidence = `字段 ${params.field} 不存在,但 ${matchedField} = ${JSON.stringify(fbVal)?.slice(0, 100)}${workspaceHint}`
1007
- break
1008
- }
1009
- }
1010
- }
1011
- } catch (e) { passed = false; evidence = `JSON 解析失败: ${e.message}` }
1124
+ const r = checkJsonFieldContent(content, params)
1125
+ passed = r.passed
1126
+ evidence = r.evidence + (workspaceHint || '')
1012
1127
  break
1013
1128
  }
1014
1129
 
@@ -43,7 +43,8 @@ rules:
43
43
  path: "package.json"
44
44
  field: "license"
45
45
  fallback_field: ["dependencies", "peerDependencies", "devDependencies"]
46
- description: "package.json 应有 license 字段(SPDX 标识)"
46
+ workspace_recursive: true
47
+ description: "package.json 应有 license 字段(SPDX 标识);monorepo 需所有 workspace 包均声明 license 字段"
47
48
  note: "仅对含 package.json 的 JS 项目检查"
48
49
 
49
50
  - id: DOC-004