repo-audit-tool 1.3.2 → 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 +4 -0
- package/docs/AGENT-INDEX.json +5 -3
- package/package.json +1 -1
- package/repo-audit.mjs +158 -43
- package/rules/domains/docs.yaml +2 -1
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)
|
package/docs/AGENT-INDEX.json
CHANGED
|
@@ -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.
|
|
6
|
-
"updated": "2026-09-
|
|
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.
|
|
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
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 }
|
|
@@ -274,7 +397,7 @@ function findNestedYaml(lines, startIdx, baseIndent, segments) {
|
|
|
274
397
|
// P3: 项目定位识别 —— 扫描 README 首段检测个人/自用项目
|
|
275
398
|
|
|
276
399
|
// P5: 加载仓库级 .auditrc.yaml 豁免配置
|
|
277
|
-
function loadAuditrc(repoPath) {
|
|
400
|
+
export function loadAuditrc(repoPath) {
|
|
278
401
|
const rcPath = join(repoPath, '.auditrc.yaml')
|
|
279
402
|
if (!existsSync(rcPath)) return { waive: [] }
|
|
280
403
|
const content = readFileSafe(rcPath)
|
|
@@ -295,7 +418,7 @@ function loadAuditrc(repoPath) {
|
|
|
295
418
|
} else if (cur && trimmed.startsWith('reason:')) {
|
|
296
419
|
cur.reason = trimmed.replace('reason:', '').replace(/['"]/g, '').trim()
|
|
297
420
|
} else if (cur && trimmed.startsWith('since:')) {
|
|
298
|
-
cur.since = trimmed.replace('since:', '').trim()
|
|
421
|
+
cur.since = trimmed.replace('since:', '').replace(/['"]/g, '').trim()
|
|
299
422
|
}
|
|
300
423
|
}
|
|
301
424
|
if (cur) waive.push(cur)
|
|
@@ -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,
|
|
1116
|
+
let actualPath = join(repoPath, pathParam)
|
|
967
1117
|
let workspaceHint = ''
|
|
968
|
-
if (!readFileSafe(actualPath) &&
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
|
package/rules/domains/docs.yaml
CHANGED
|
@@ -43,7 +43,8 @@ rules:
|
|
|
43
43
|
path: "package.json"
|
|
44
44
|
field: "license"
|
|
45
45
|
fallback_field: ["dependencies", "peerDependencies", "devDependencies"]
|
|
46
|
-
|
|
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
|