repo-audit-tool 1.3.3 → 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.
- package/PUBLISHING.md +4 -0
- package/docs/AGENT-INDEX.json +126 -40
- package/docs/repo-audit/AGENT-GUIDE.md +4 -0
- package/docs/repo-audit/HUMAN-GUIDE.md +9 -0
- package/package.json +1 -1
- package/repo-audit.mjs +202 -63
- 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,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.
|
|
6
|
-
"updated": "2026-09-
|
|
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.
|
|
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": {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"--
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"--
|
|
42
|
-
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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": [
|
|
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",
|
|
104
|
-
"
|
|
105
|
-
"
|
|
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
|
-
{
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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,14 +313,16 @@
|
|
|
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": {
|
|
317
|
+
"1.4.0": "json_field workspace 递归——monorepo 场景下递归检查所有 workspace 包的 license 字段;findWorkspacePackages 发现 npm/pnpm workspaces + 常见目录;checkJsonFieldContent 提取为可复用函数;DOC-003b 启用;15 用例回归",
|
|
318
|
+
"1.3.3": "Issue#5 修复:.auditrc.yaml since 字段 YAML 引号剥离——loadAuditrc 中 since: 分支补 .replace(/['\"]/g, ''),与 reason 分支对称,修复 waived_since 输出含字面引号的 bug;5 用例回归(test/auditrc-since.test.mjs)",
|
|
234
319
|
"1.3.2": "Issue#4 修复:入口守卫 pathToFileURL 归一化——修复 Windows 全形态静默不执行(v1.3.1 回归);守卫不命中时 stderr 诊断提示;command 检查器 Git Bash 探测;CI 双平台矩阵 + npm 安装 smoke",
|
|
235
320
|
"1.3.1": "入口守卫 realpath 归一化——修复 npm bin symlink 下主流程不执行;1.3.0 反馈修复:注释剥离/SEC-004/AGENTS 声明/QUA-004/--update 动线",
|
|
236
321
|
"1.3.0": "反馈修复:yaml_field 行内注释剥离 + fallback_field 通配(SEC-004 兼容 job 级权限);AGENTS-core 来源声明二选一;QUA-004 锁文件消费意图;手册补 --update 存量补齐动线",
|
|
237
322
|
"1.2.3": "package.json 补充 repository 字段,修复 npm provenance E422;完成 Trusted Publisher tag 自动发布",
|
|
238
323
|
"1.2.2": "publish CI 改用 node 22 + 条件升级 npm,修复 EBADENGINE(tag 发布因 1.2.1 已存在未出包)",
|
|
239
324
|
"1.2.1": "README 链接改为绝对 URL(修复 npm 页面 404)",
|
|
240
|
-
"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 用例回归"
|
|
241
327
|
}
|
|
242
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
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 }
|
|
@@ -202,30 +325,49 @@ function countNonHiddenFiles(path) {
|
|
|
202
325
|
return count
|
|
203
326
|
}
|
|
204
327
|
|
|
205
|
-
// Issue#
|
|
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
|
|
206
343
|
function findNestedYamlKey(lines, startIdx, baseIndent, key) {
|
|
344
|
+
let childIndent = -1
|
|
207
345
|
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
208
346
|
const l = lines[i]
|
|
209
347
|
if (!l.trim() || l.trim().startsWith('#')) continue
|
|
210
348
|
const ind = l.length - l.trimStart().length
|
|
211
349
|
if (ind <= baseIndent) return null
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
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 }
|
|
215
354
|
}
|
|
216
355
|
return null
|
|
217
356
|
}
|
|
218
357
|
// Issue#2 fix: 列出指定父键下一层的所有子键名(通配 * 展开用)
|
|
358
|
+
// 子层缩进同样以首个子键实际缩进为准(与 findNestedYamlKey 对称)
|
|
219
359
|
function listChildKeys(lines, startIdx, baseIndent) {
|
|
220
360
|
const keys = []
|
|
361
|
+
let childIndent = -1
|
|
221
362
|
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
222
363
|
const l = lines[i]
|
|
223
364
|
if (!l.trim() || l.trim().startsWith('#')) continue
|
|
224
365
|
const ind = l.length - l.trimStart().length
|
|
225
366
|
if (ind <= baseIndent) break
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
367
|
+
if (childIndent === -1) childIndent = ind
|
|
368
|
+
if (ind !== childIndent) continue
|
|
369
|
+
const km = matchYamlKey(l)
|
|
370
|
+
if (km) keys.push(km[0])
|
|
229
371
|
}
|
|
230
372
|
return keys
|
|
231
373
|
}
|
|
@@ -251,10 +393,10 @@ function findNestedYaml(lines, startIdx, baseIndent, segments) {
|
|
|
251
393
|
if (!l.trim() || l.trim().startsWith('#')) continue
|
|
252
394
|
const ind = l.length - l.trimStart().length
|
|
253
395
|
if (ind <= currentIndent) break // 退出当前层级
|
|
254
|
-
const
|
|
255
|
-
if (
|
|
256
|
-
const key =
|
|
257
|
-
const valStr = stripYamlComment(
|
|
396
|
+
const km = matchYamlKey(l)
|
|
397
|
+
if (km) {
|
|
398
|
+
const key = km[0]
|
|
399
|
+
const valStr = stripYamlComment(km[1])
|
|
258
400
|
const seg = segments[0]
|
|
259
401
|
if (key === seg) {
|
|
260
402
|
if (valStr && valStr !== '' && valStr !== '{}') {
|
|
@@ -724,7 +866,7 @@ function loadRules(type, customPaths = [], repoPath = null) {
|
|
|
724
866
|
file_header: { path: ['string'], pattern: ['string'] },
|
|
725
867
|
regex: { path: ['string'], pattern: ['string'] },
|
|
726
868
|
grep: { pattern: ['string'] },
|
|
727
|
-
json_field: { path: ['string'], field: ['string'], fallback_field: ['array', 'string'] },
|
|
869
|
+
json_field: { path: ['string'], field: ['string'], fallback_field: ['array', 'string'], workspace_recursive: ['boolean'] },
|
|
728
870
|
toml_field: { path: ['string'], field: ['string'] },
|
|
729
871
|
yaml_field: { path: ['string'], field: ['string'], fallback_field: ['array', 'string'] },
|
|
730
872
|
directory_exists: { paths: ['array', 'string'] },
|
|
@@ -779,6 +921,11 @@ function hasEquivalentDocs(repoPath) {
|
|
|
779
921
|
// 规则检查引擎
|
|
780
922
|
// ============================================================
|
|
781
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
|
+
|
|
782
929
|
function runCheck(rule, repoPath, mpj = null) {
|
|
783
930
|
const { check, params } = rule
|
|
784
931
|
let passed = false
|
|
@@ -962,53 +1109,45 @@ function runCheck(rule, repoPath, mpj = null) {
|
|
|
962
1109
|
}
|
|
963
1110
|
|
|
964
1111
|
case 'json_field': {
|
|
1112
|
+
const pathParam = params?.path
|
|
1113
|
+
const workspaceRecursive = params?.workspace_recursive === true
|
|
1114
|
+
|
|
1115
|
+
// workspace 递归:检查所有 workspace 包
|
|
1116
|
+
if (workspaceRecursive && pathParam === 'package.json') {
|
|
1117
|
+
const allPkgs = findWorkspacePackages(repoPath)
|
|
1118
|
+
if (allPkgs.length === 0) {
|
|
1119
|
+
passed = false; evidence = '未找到任何 package.json'; break
|
|
1120
|
+
}
|
|
1121
|
+
const results = []
|
|
1122
|
+
let allPassed = true
|
|
1123
|
+
for (const wp of allPkgs) {
|
|
1124
|
+
const content = readFileSafe(wp.path)
|
|
1125
|
+
if (!content) {
|
|
1126
|
+
results.push(`${wp.dir === repoPath ? '根' : relative(repoPath, wp.dir)}: 文件不可读 ✗`)
|
|
1127
|
+
allPassed = false; continue
|
|
1128
|
+
}
|
|
1129
|
+
const r = checkJsonFieldContent(content, params)
|
|
1130
|
+
const relPath = wp.dir === repoPath ? '根' : relative(repoPath, wp.dir)
|
|
1131
|
+
results.push(`${relPath}: ${r.evidence} ${r.passed ? '✓' : '✗'}`)
|
|
1132
|
+
if (!r.passed) allPassed = false
|
|
1133
|
+
}
|
|
1134
|
+
passed = allPassed
|
|
1135
|
+
evidence = results.join(' | ')
|
|
1136
|
+
break
|
|
1137
|
+
}
|
|
1138
|
+
|
|
965
1139
|
// P6: monorepo 感知 — package.json 不在根时 fallback 到 workspace 目录
|
|
966
|
-
let actualPath = join(repoPath,
|
|
1140
|
+
let actualPath = join(repoPath, pathParam)
|
|
967
1141
|
let workspaceHint = ''
|
|
968
|
-
if (!readFileSafe(actualPath) &&
|
|
1142
|
+
if (!readFileSafe(actualPath) && pathParam === 'package.json' && mpj) {
|
|
969
1143
|
actualPath = mpj.path
|
|
970
1144
|
workspaceHint = `(workspace: ${mpj.dir})`
|
|
971
1145
|
}
|
|
972
1146
|
const content = readFileSafe(actualPath)
|
|
973
1147
|
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}` }
|
|
1148
|
+
const r = checkJsonFieldContent(content, params)
|
|
1149
|
+
passed = r.passed
|
|
1150
|
+
evidence = r.evidence + (workspaceHint || '')
|
|
1012
1151
|
break
|
|
1013
1152
|
}
|
|
1014
1153
|
|
|
@@ -1072,7 +1211,7 @@ function runCheck(rule, repoPath, mpj = null) {
|
|
|
1072
1211
|
let val = null
|
|
1073
1212
|
let found = false
|
|
1074
1213
|
try {
|
|
1075
|
-
const lines = content
|
|
1214
|
+
const lines = normalizeYamlLines(content)
|
|
1076
1215
|
let currentIndent = -1
|
|
1077
1216
|
let currentObj = null
|
|
1078
1217
|
// 第一遍:找到顶层键
|
|
@@ -1080,10 +1219,10 @@ function runCheck(rule, repoPath, mpj = null) {
|
|
|
1080
1219
|
const l = lines[i]
|
|
1081
1220
|
if (!l.trim() || l.trim().startsWith('#')) continue
|
|
1082
1221
|
const ind = l.length - l.trimStart().length
|
|
1083
|
-
const
|
|
1084
|
-
if (
|
|
1222
|
+
const km = matchYamlKey(l)
|
|
1223
|
+
if (km && km[0] === segments[0]) {
|
|
1085
1224
|
currentIndent = ind
|
|
1086
|
-
const valStr = stripYamlComment(
|
|
1225
|
+
const valStr = stripYamlComment(km[1])
|
|
1087
1226
|
if (valStr && valStr !== '' && valStr !== '{}') {
|
|
1088
1227
|
// 标量值(非嵌套)
|
|
1089
1228
|
val = valStr
|
|
@@ -1118,7 +1257,7 @@ function runCheck(rule, repoPath, mpj = null) {
|
|
|
1118
1257
|
const wIdx = segs.indexOf('*')
|
|
1119
1258
|
if (wIdx === -1) { candidates.push({ segments: segs }); return }
|
|
1120
1259
|
// 找通配段父级的所有子键:先定位父级前缀(逐段下钻收集行号)
|
|
1121
|
-
const lines = content
|
|
1260
|
+
const lines = normalizeYamlLines(content)
|
|
1122
1261
|
const prefix = segs.slice(0, wIdx)
|
|
1123
1262
|
const suffix = segs.slice(wIdx + 1)
|
|
1124
1263
|
// 顶层段
|
|
@@ -1126,8 +1265,8 @@ function runCheck(rule, repoPath, mpj = null) {
|
|
|
1126
1265
|
for (let i = 0; i < lines.length; i++) {
|
|
1127
1266
|
const l = lines[i]
|
|
1128
1267
|
if (!l.trim() || l.trim().startsWith('#')) continue
|
|
1129
|
-
const
|
|
1130
|
-
if (!
|
|
1268
|
+
const km = matchYamlKey(l)
|
|
1269
|
+
if (!km || km[0] !== prefix[0]) continue
|
|
1131
1270
|
// 沿 prefix 下钻
|
|
1132
1271
|
let curIdx = i, curIndent = l.length - l.trimStart().length
|
|
1133
1272
|
let ok = true
|
|
@@ -1148,14 +1287,14 @@ function runCheck(rule, repoPath, mpj = null) {
|
|
|
1148
1287
|
let fbVal = null
|
|
1149
1288
|
let fbFound = false
|
|
1150
1289
|
try {
|
|
1151
|
-
const lines = content
|
|
1290
|
+
const lines = normalizeYamlLines(content)
|
|
1152
1291
|
for (let i = 0; i < lines.length; i++) {
|
|
1153
1292
|
const l = lines[i]
|
|
1154
1293
|
if (!l.trim() || l.trim().startsWith('#')) continue
|
|
1155
1294
|
const ind = l.length - l.trimStart().length
|
|
1156
|
-
const
|
|
1157
|
-
if (
|
|
1158
|
-
const v = stripYamlComment(
|
|
1295
|
+
const km = matchYamlKey(l)
|
|
1296
|
+
if (km && km[0] === cand.segments[0]) {
|
|
1297
|
+
const v = stripYamlComment(km[1])
|
|
1159
1298
|
if (v && v !== '' && v !== '{}') { fbVal = v; fbFound = true }
|
|
1160
1299
|
else {
|
|
1161
1300
|
const sub = findNestedYaml(lines, i, ind, cand.segments.slice(1))
|
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
|