sillyspec 3.17.7 → 3.17.8
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/package.json +1 -1
- package/src/run.js +63 -4
- package/src/scan-postcheck.js +27 -13
- package/src/stages/quick.js +5 -3
- package/src/stages/scan.js +6 -6
- package/test/platform-scan-p0.test.mjs +172 -0
package/package.json
CHANGED
package/src/run.js
CHANGED
|
@@ -10,6 +10,24 @@ import { createRequire } from 'module'
|
|
|
10
10
|
const require = createRequire(import.meta.url)
|
|
11
11
|
import { ProgressManager } from './progress.js'
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* 在容器/Docker 环境下,git 可能因目录所有权不匹配报 dubious ownership。
|
|
15
|
+
* 使用 -c safe.directory= 临时参数,不污染全局 git config。
|
|
16
|
+
* @param {string} cwd - 仓库根目录
|
|
17
|
+
* @param {string[]} args - git 子命令及参数,如 ['rev-parse', 'HEAD']
|
|
18
|
+
* @returns {{ value: string, error: string|null }}
|
|
19
|
+
*/
|
|
20
|
+
function safeGit(cwd, args) {
|
|
21
|
+
const { execSync } = require('child_process')
|
|
22
|
+
const fullArgs = ['-c', `safe.directory=${cwd}`, '-C', cwd, ...args]
|
|
23
|
+
try {
|
|
24
|
+
const value = execSync(['git', ...fullArgs].join(' '), { encoding: 'utf8', timeout: 5000 }).trim()
|
|
25
|
+
return { value, error: null }
|
|
26
|
+
} catch (e) {
|
|
27
|
+
return { value: null, error: e.message.split('\n')[0] }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
// ── Wait State Constants ──
|
|
14
32
|
// 正则匹配:只识别独立一行的标记,避免误伤文档正文引用
|
|
15
33
|
const WAIT_MARKER_RE = /^\s*\[(WAIT_FOR_USER|NEEDS_CONFIRM|NEEDS_DECISION)\]\s*$/m
|
|
@@ -129,6 +147,21 @@ async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
129
147
|
result.status = 'warning'
|
|
130
148
|
}
|
|
131
149
|
|
|
150
|
+
// quicklog 存在性检查
|
|
151
|
+
try {
|
|
152
|
+
const quicklogDir = join(cwd, '.sillyspec', 'quicklog')
|
|
153
|
+
if (existsSync(quicklogDir)) {
|
|
154
|
+
const qlFiles = readdirSync(quicklogDir).filter(f => f.endsWith('.md'))
|
|
155
|
+
if (qlFiles.length === 0) {
|
|
156
|
+
result.reasons.push('quicklog 目录为空(无任务记录)')
|
|
157
|
+
if (result.status === 'safe') result.status = 'warning'
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
result.reasons.push('quicklog 目录不存在(agent 未创建任务记录)')
|
|
161
|
+
if (result.status === 'safe') result.status = 'warning'
|
|
162
|
+
}
|
|
163
|
+
} catch {}
|
|
164
|
+
|
|
132
165
|
// --confirm 模式:展示 diff 并等待确认
|
|
133
166
|
if (isConfirm && (result.status === 'warning' || result.status === 'blocked')) {
|
|
134
167
|
console.log(`\n📋 quick 变更概览:`)
|
|
@@ -351,7 +384,7 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
351
384
|
if (promptText.includes('<git-user>')) {
|
|
352
385
|
const { execSync } = await import('child_process')
|
|
353
386
|
try {
|
|
354
|
-
const gitUser =
|
|
387
|
+
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
355
388
|
promptText = promptText.replace(/<git-user>/g, gitUser)
|
|
356
389
|
} catch {
|
|
357
390
|
promptText = promptText.replace(/<git-user>/g, 'unknown')
|
|
@@ -377,9 +410,14 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
377
410
|
const docsRoot = join(specSillyspec, 'docs', projectName)
|
|
378
411
|
const projectsRoot = join(specSillyspec, 'projects')
|
|
379
412
|
const changesRoot = join(specSillyspec, 'changes')
|
|
413
|
+
const workflowsRoot = join(specSillyspec, 'workflows')
|
|
414
|
+
const knowledgeRoot = join(specSillyspec, 'knowledge')
|
|
380
415
|
|
|
381
416
|
promptText = promptText.replace(/\{DOCS_ROOT\}/g, docsRoot)
|
|
382
417
|
promptText = promptText.replace(/\{PROJECTS_ROOT\}/g, projectsRoot)
|
|
418
|
+
promptText = promptText.replace(/\{WORKFLOWS_ROOT\}/g, workflowsRoot)
|
|
419
|
+
promptText = promptText.replace(/\{KNOWLEDGE_ROOT\}/g, knowledgeRoot)
|
|
420
|
+
promptText = promptText.replace(/\{SPEC_ROOT\}/g, specSillyspec)
|
|
383
421
|
|
|
384
422
|
const platformDirectives = []
|
|
385
423
|
platformDirectives.push(
|
|
@@ -389,6 +427,8 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
389
427
|
`- 文档根目录: \`${docsRoot}/\`\n` +
|
|
390
428
|
`- 项目注册表: \`${projectsRoot}/\`\n` +
|
|
391
429
|
`- 变更目录: \`${changesRoot}/\`\n` +
|
|
430
|
+
`- 工作流目录: \`${workflowsRoot}/\`\n` +
|
|
431
|
+
`- 术语目录: \`${knowledgeRoot}/\`\n` +
|
|
392
432
|
`\n` +
|
|
393
433
|
`### ⛔ 写入规则\n` +
|
|
394
434
|
`1. **所有文档、配置、产物只能写入上述路径**。严禁写入源码目录或相对路径 \`.sillyspec/\`。\n` +
|
|
@@ -437,8 +477,25 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
437
477
|
const specSillyspec = join(cwd, '.sillyspec')
|
|
438
478
|
const docsRoot = join(specSillyspec, 'docs', projectName)
|
|
439
479
|
const projectsRoot = join(specSillyspec, 'projects')
|
|
480
|
+
const workflowsRoot = join(specSillyspec, 'workflows')
|
|
481
|
+
const knowledgeRoot = join(specSillyspec, 'knowledge')
|
|
440
482
|
promptText = promptText.replace(/\{DOCS_ROOT\}/g, docsRoot)
|
|
441
483
|
promptText = promptText.replace(/\{PROJECTS_ROOT\}/g, projectsRoot)
|
|
484
|
+
promptText = promptText.replace(/\{WORKFLOWS_ROOT\}/g, workflowsRoot)
|
|
485
|
+
promptText = promptText.replace(/\{KNOWLEDGE_ROOT\}/g, knowledgeRoot)
|
|
486
|
+
promptText = promptText.replace(/\{SPEC_ROOT\}/g, specSillyspec)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 平台模式 prompt 自检:确保没有裸相对输出路径
|
|
491
|
+
// 只匹配正向写入指令中的裸路径,避免误杀「禁止写入 .sillyspec/」等安全说明
|
|
492
|
+
if ((platformOpts?.specRoot || platformOpts?.runtimeRoot) && stageName === 'scan') {
|
|
493
|
+
const writeCtx = /(?<!不要|禁止|严禁)(?:save[\s.]+to|write|create|mkdir|git add|写入|保存到|写入到)[^a-zA-Z]*\.sillyspec\/[a-z]/i
|
|
494
|
+
if (writeCtx.test(promptText)) {
|
|
495
|
+
console.error(`❌ [sillyspec] BUG: 平台模式 scan prompt 包含写入指令指向裸相对路径 .sillyspec/`)
|
|
496
|
+
console.error(` 这会导致 agent 写入源码目录而非 spec-root,属于源码污染 bug。`)
|
|
497
|
+
console.error(` 请将路径改为对应的 {DOCS_ROOT}/{PROJECTS_ROOT}/{WORKFLOWS_ROOT}/{KNOWLEDGE_ROOT}/{SPEC_ROOT} 占位符。`)
|
|
498
|
+
process.exit(1)
|
|
442
499
|
}
|
|
443
500
|
}
|
|
444
501
|
|
|
@@ -651,7 +708,7 @@ async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
|
|
|
651
708
|
const manifestDir = platformOpts.specRoot
|
|
652
709
|
let sourceCommit = null
|
|
653
710
|
try {
|
|
654
|
-
sourceCommit =
|
|
711
|
+
const { value: sourceCommit, error: scErr } = safeGit(cwd, ['rev-parse', 'HEAD'])
|
|
655
712
|
} catch {}
|
|
656
713
|
mkdirSync(manifestDir, { recursive: true })
|
|
657
714
|
const manifest = {
|
|
@@ -665,6 +722,7 @@ async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
|
|
|
665
722
|
workspace_id: platformOpts.workspaceId || null,
|
|
666
723
|
scan_run_id: platformOpts.scanRunId || null,
|
|
667
724
|
source_commit: sourceCommit,
|
|
725
|
+
source_commit_error: sourceCommit === null ? (scErr || 'unknown') : undefined,
|
|
668
726
|
generated_at: new Date().toISOString(),
|
|
669
727
|
schema_version: 2,
|
|
670
728
|
}
|
|
@@ -1074,7 +1132,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1074
1132
|
const allowNew = quickOpts?.isAllowNew || false
|
|
1075
1133
|
const forceBaseline = quickOpts?.isForceBaseline || false
|
|
1076
1134
|
progress.quickGuard = {
|
|
1077
|
-
baselineCommit:
|
|
1135
|
+
baselineCommit: safeGit(cwd, ['rev-parse', 'HEAD']).value,
|
|
1078
1136
|
baselineFiles,
|
|
1079
1137
|
allowedFiles,
|
|
1080
1138
|
allowNew,
|
|
@@ -1668,12 +1726,13 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1668
1726
|
mkdirSync(manifestDir, { recursive: true })
|
|
1669
1727
|
let sourceCommit = null
|
|
1670
1728
|
try {
|
|
1671
|
-
sourceCommit =
|
|
1729
|
+
const { value: sourceCommit, error: scErr } = safeGit(cwd, ['rev-parse', 'HEAD'])
|
|
1672
1730
|
} catch {}
|
|
1673
1731
|
const manifest = {
|
|
1674
1732
|
workspace_id: platformOpts.workspaceId || null,
|
|
1675
1733
|
scan_run_id: platformOpts.scanRunId || null,
|
|
1676
1734
|
source_commit: sourceCommit,
|
|
1735
|
+
source_commit_error: sourceCommit === null ? (scErr || 'unknown') : undefined,
|
|
1677
1736
|
generated_at: new Date().toISOString(),
|
|
1678
1737
|
schema_version: 1,
|
|
1679
1738
|
}
|
package/src/scan-postcheck.js
CHANGED
|
@@ -52,19 +52,33 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {}
|
|
|
52
52
|
|
|
53
53
|
const projectName = basename(cwd)
|
|
54
54
|
|
|
55
|
-
// 1. source_root
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
// 1. source_root 污染检查(docs/projects/workflows/knowledge/manifest/local)
|
|
56
|
+
const pollutePaths = ['docs', 'projects', 'workflows', 'knowledge']
|
|
57
|
+
const polluteFiles = ['manifest.json', 'local.yaml']
|
|
58
|
+
for (const sub of pollutePaths) {
|
|
59
|
+
const localSub = join(cwd, '.sillyspec', sub)
|
|
60
|
+
if (existsSync(localSub)) {
|
|
61
|
+
try {
|
|
62
|
+
const leaked = readdirSync(localSub, { recursive: true }).filter(e => String(e).endsWith('.md') || String(e).endsWith('.yaml') || String(e).endsWith('.json'))
|
|
63
|
+
if (leaked.length > 0) {
|
|
64
|
+
checks.push({
|
|
65
|
+
name: 'source_root_leak',
|
|
66
|
+
severity: 'failed',
|
|
67
|
+
detail: `source_root/.sillyspec/${sub}/ 下存在 ${leaked.length} 个文件(${localSub}/),agent 写入到了错误路径`
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const file of polluteFiles) {
|
|
74
|
+
const filePath = join(cwd, '.sillyspec', file)
|
|
75
|
+
if (existsSync(filePath)) {
|
|
76
|
+
checks.push({
|
|
77
|
+
name: 'source_root_leak',
|
|
78
|
+
severity: 'failed',
|
|
79
|
+
detail: `source_root/.sillyspec/${file} 存在,agent 写入到了错误路径(${filePath})`
|
|
80
|
+
})
|
|
81
|
+
}
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
// 2. spec_root 检查 7 份必需文档
|
package/src/stages/quick.js
CHANGED
|
@@ -22,8 +22,8 @@ export const definition = {
|
|
|
22
22
|
9. 根据任务描述初步判断可能涉及的模块
|
|
23
23
|
10. 读取匹配到的 \`.sillyspec/docs/<project>/modules/<module>.md\`
|
|
24
24
|
|
|
25
|
-
###
|
|
26
|
-
|
|
25
|
+
### 创建任务记录(⛔ 此步骤不能跳过,没有 quicklog 记录 = 未完成)
|
|
26
|
+
理解完任务后,**必须**立即创建记录文件,再输出任何其他内容:
|
|
27
27
|
1. 使用预注入的 git 用户名:\`<git-user>\`
|
|
28
28
|
2. 无 \`--change\`:创建 .sillyspec/quicklog/QUICKLOG-\`<git-user>\`.md\`(已存在则追加),写入:
|
|
29
29
|
\`\`\`
|
|
@@ -41,7 +41,9 @@ export const definition = {
|
|
|
41
41
|
这样 Gate 检测到 .sillyspec/\` 下有变更,就不会拦截后续的代码修改。
|
|
42
42
|
|
|
43
43
|
### 输出
|
|
44
|
-
任务理解 + 上下文摘要
|
|
44
|
+
quicklog 已创建(必须放在输出的第一行确认)+ 任务理解 + 上下文摘要
|
|
45
|
+
|
|
46
|
+
⚠️ **先创建 quicklog,再输出任务理解。** 如果 quicklog 未创建,CLI post-check 会报 warning。`,
|
|
45
47
|
outputHint: '任务理解',
|
|
46
48
|
optional: false
|
|
47
49
|
},
|
package/src/stages/scan.js
CHANGED
|
@@ -103,13 +103,13 @@ export const definition = {
|
|
|
103
103
|
{
|
|
104
104
|
name: '深度扫描 — 7 份文档(子代理并行)',
|
|
105
105
|
perProject: true,
|
|
106
|
-
prompt: `按照
|
|
106
|
+
prompt: `按照 \`{WORKFLOWS_ROOT}/scan-docs.yaml\` 中定义的角色和检查规则,使用子代理并行生成当前项目的 7 份扫描文档。
|
|
107
107
|
|
|
108
108
|
**你必须使用子代理执行,不要自己写文档。**
|
|
109
109
|
**对扫描列表中的每个项目分别执行以下流程。**
|
|
110
110
|
|
|
111
111
|
### 操作
|
|
112
|
-
1. 读取
|
|
112
|
+
1. 读取 \`{WORKFLOWS_ROOT}/scan-docs.yaml\`,了解角色定义、输出要求和检查规则
|
|
113
113
|
2. 对每个项目(扫描列表中标记为需生成/覆盖的项目):
|
|
114
114
|
a. 将 \`<project>\` 替换为实际项目名,得到该项目的目标文件路径
|
|
115
115
|
b. 为每个角色启动独立子代理(可并行),每个子代理负责 1-2 份文档
|
|
@@ -141,16 +141,16 @@ export const definition = {
|
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
name: '生成本地配置',
|
|
144
|
-
prompt: `自动生成
|
|
144
|
+
prompt: `自动生成 local.yaml 本地配置文件。
|
|
145
145
|
|
|
146
146
|
### 操作
|
|
147
|
-
1. 检查
|
|
147
|
+
1. 检查 {SPEC_ROOT}/local.yaml 是否已存在,已存在则跳过(提示"local.yaml 已存在,跳过生成")
|
|
148
148
|
2. 根据项目类型生成默认配置:
|
|
149
149
|
- **Node.js**(有 package.json):build: "npm run build", test: "npm test", lint: "npm run lint", type: nodejs
|
|
150
150
|
- **Maven**(有 pom.xml):build: "mvn compile", test: "mvn test", lint: "mvn checkstyle:check", type: maven
|
|
151
151
|
- **Gradle**(有 build.gradle):build: "./gradlew build", test: "./gradlew test", type: gradle
|
|
152
152
|
- **通用项目**:只写注释模板, type: generic
|
|
153
|
-
3. 确保目录存在:mkdir -p
|
|
153
|
+
3. 确保目录存在:mkdir -p {SPEC_ROOT}
|
|
154
154
|
4. 原子写入(先写 tmp 文件再 rename)
|
|
155
155
|
|
|
156
156
|
### 文件格式
|
|
@@ -454,7 +454,7 @@ step1 → step2 → step3
|
|
|
454
454
|
3. 自检门控:ARCHITECTURE(技术栈+Schema摘要)、CONVENTIONS(隐形规则+代码风格)、STRUCTURE(目录结构)、INTEGRATIONS(外部依赖)、TESTING(测试现状)、CONCERNS(技术债务)、PROJECT(项目概览)
|
|
455
455
|
4. 检查 flows/ 和 glossary.md 是否已生成(如有)
|
|
456
456
|
5. 清理:\`rm -f {DOCS_ROOT}/scan/_env-detect.md\`
|
|
457
|
-
6.
|
|
457
|
+
6. 如果非平台模式:\`git add .sillyspec/\` — 暂存扫描结果(不要 commit,由用户通过统一提交工具处理)。如果平台模式:跳过 git add(specRoot 不在 sourceRoot 的 git repo 内)。
|
|
458
458
|
|
|
459
459
|
### ⛔ 路径合规检查(平台模式下必须执行)
|
|
460
460
|
7. 确认所有文档都写入 \`{DOCS_ROOT}/\`(spec-root 下),**而非源码目录下的 .sillyspec/**
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P0 补丁验证 — 4 类测试
|
|
3
|
+
*
|
|
4
|
+
* 1. 平台 scan prompt 不含正向写入 .sillyspec/ 指令
|
|
5
|
+
* 2. 安全说明文字不被 prompt 自检误杀
|
|
6
|
+
* 3. 平台模式不执行 git add {SPEC_ROOT}
|
|
7
|
+
* 4. source_root 污染检查覆盖所有子目录/文件
|
|
8
|
+
* 5. run.js 占位符替换补齐(平台+本地)
|
|
9
|
+
*
|
|
10
|
+
* 跑法: node test/platform-scan-p0.test.mjs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join, basename } from 'path'
|
|
14
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync, readdirSync } from 'fs'
|
|
15
|
+
import { execSync } from 'child_process'
|
|
16
|
+
import { readFile } from 'fs/promises'
|
|
17
|
+
import { fileURLToPath } from 'url'
|
|
18
|
+
|
|
19
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
20
|
+
const passed = []
|
|
21
|
+
const failed = []
|
|
22
|
+
|
|
23
|
+
function assert(label, condition, detail) {
|
|
24
|
+
if (condition) {
|
|
25
|
+
passed.push(label)
|
|
26
|
+
} else {
|
|
27
|
+
failed.push({ label, detail })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── 测试 1:scan.js 模板不含裸 .sillyspec 输出路径 ──
|
|
32
|
+
{
|
|
33
|
+
const { definition } = await import('../src/stages/scan.js')
|
|
34
|
+
const prompts = definition.steps.map(s => s.prompt).join('\n')
|
|
35
|
+
|
|
36
|
+
// 所有 .sillyspec 出现都应该是「禁止说明」,不是写入指令
|
|
37
|
+
const writePatterns = [
|
|
38
|
+
/write.*\.sillyspec\/docs/i,
|
|
39
|
+
/save.*\.sillyspec\/docs/i,
|
|
40
|
+
/create.*\.sillyspec\/docs/i,
|
|
41
|
+
/mkdir.*\.sillyspec\/docs/i,
|
|
42
|
+
]
|
|
43
|
+
for (const p of writePatterns) {
|
|
44
|
+
assert(`scan 模板无写入 .sillyspec/docs: ${p}`, !p.test(prompts),
|
|
45
|
+
p.test(prompts) ? `命中: ${prompts.match(p)[0]}` : '')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 应该使用占位符
|
|
49
|
+
assert('scan 模板使用 {WORKFLOWS_ROOT}', prompts.includes('{WORKFLOWS_ROOT}'))
|
|
50
|
+
assert('scan 模板使用 {SPEC_ROOT}', prompts.includes('{SPEC_ROOT}'))
|
|
51
|
+
assert('scan 模板使用 {DOCS_ROOT}', prompts.includes('{DOCS_ROOT}'))
|
|
52
|
+
|
|
53
|
+
// 平台模式下 git add 应该是条件判断
|
|
54
|
+
assert('scan 模板平台模式跳过 git add', prompts.includes('如果平台模式:跳过 git add'))
|
|
55
|
+
assert('scan 模板非平台模式 git add .sillyspec/', prompts.includes('git add .sillyspec/'))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── 测试 2:prompt 自检不误杀安全说明 ──
|
|
59
|
+
{
|
|
60
|
+
// 导入 run.js 中的正则
|
|
61
|
+
const writeCtxRe = /(?<!不要|禁止|严禁)(?:save[\s.]+to|write|create|mkdir|git add|写入|保存到|写入到)[^a-zA-Z]*\.sillyspec\/[a-z]/i
|
|
62
|
+
|
|
63
|
+
// 安全说明 — 不应命中
|
|
64
|
+
const safeLines = [
|
|
65
|
+
'严禁写入源码目录或相对路径 `.sillyspec/`',
|
|
66
|
+
'⚠️ 不要写入 .sillyspec/docs',
|
|
67
|
+
'source_root 下存在 .sillyspec/docs 文件',
|
|
68
|
+
'不允许从 cwd 推导 .sillyspec 路径',
|
|
69
|
+
]
|
|
70
|
+
for (const line of safeLines) {
|
|
71
|
+
assert(`安全说明不误杀: "${line.slice(0, 40)}"`, !writeCtxRe.test(line),
|
|
72
|
+
'误杀! 命中了安全说明文字')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 写入指令 — 应该命中
|
|
76
|
+
const badLines = [
|
|
77
|
+
'写入 `.sillyspec/docs/ARCHITECTURE.md`',
|
|
78
|
+
'save to `.sillyspec/docs/ARCHITECTURE.md`',
|
|
79
|
+
'create `.sillyspec/docs/ARCH.md`',
|
|
80
|
+
'git add .sillyspec/docs/',
|
|
81
|
+
'write 到 .sillyspec/docs/',
|
|
82
|
+
]
|
|
83
|
+
for (const line of badLines) {
|
|
84
|
+
assert(`写入指令应命中: "${line.slice(0, 40)}"`, writeCtxRe.test(line),
|
|
85
|
+
'漏杀! 没有捕获写入指令')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── 测试 3:safeGit 使用 -c safe.directory(不污染全局 config) ──
|
|
90
|
+
{
|
|
91
|
+
const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')
|
|
92
|
+
|
|
93
|
+
assert('safeGit 不含 --global', !runSrc.includes('git config --global'),
|
|
94
|
+
'发现 --global,会污染容器 git config')
|
|
95
|
+
|
|
96
|
+
assert('safeGit 使用 -c safe.directory', runSrc.includes("safe.directory=${cwd}"),
|
|
97
|
+
'未发现 -c safe.directory per-command 参数')
|
|
98
|
+
|
|
99
|
+
assert('safeGit 使用 -C cwd', runSrc.includes("-C', cwd"),
|
|
100
|
+
'未发现 -C cwd 参数')
|
|
101
|
+
|
|
102
|
+
assert('safeGit 返回 { value, error }', runSrc.includes("return { value, error: "),
|
|
103
|
+
'safeGit 返回值不是 { value, error } 结构')
|
|
104
|
+
|
|
105
|
+
assert('manifest 包含 source_commit_error 字段',
|
|
106
|
+
runSrc.includes("source_commit_error:"), 'manifest 缺少 source_commit_error')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 测试 4:postcheck 污染检查覆盖所有子目录 ──
|
|
110
|
+
{
|
|
111
|
+
const postcheckSrc = await readFile(join(__dirname, '..', 'src', 'scan-postcheck.js'), 'utf8')
|
|
112
|
+
|
|
113
|
+
const requiredSubs = ['docs', 'projects', 'workflows', 'knowledge']
|
|
114
|
+
for (const sub of requiredSubs) {
|
|
115
|
+
assert(`postcheck 检查 ${sub} 污染`,
|
|
116
|
+
postcheckSrc.includes(`'${sub}'`) && postcheckSrc.includes("'.sillyspec', sub)"),
|
|
117
|
+
`postcheck 未检查 .sillyspec/${sub}/ 污染`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
assert('postcheck 检查 manifest.json', postcheckSrc.includes('manifest.json'))
|
|
121
|
+
assert('postcheck 检查 local.yaml', postcheckSrc.includes('local.yaml'))
|
|
122
|
+
assert('污染 severity 为 failed', postcheckSrc.includes("severity: 'failed'"))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── 测试 5:run.js 占位符替换补齐 ──
|
|
126
|
+
{
|
|
127
|
+
const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')
|
|
128
|
+
|
|
129
|
+
// 平台模式块
|
|
130
|
+
const platformMarker = 'promptText.replace(/\\{WORKFLOWS_ROOT\\}'
|
|
131
|
+
assert('平台模式替换 {WORKFLOWS_ROOT}', runSrc.includes('{WORKFLOWS_ROOT}') && runSrc.includes('workflowsRoot'))
|
|
132
|
+
assert('平台模式替换 {KNOWLEDGE_ROOT}', runSrc.includes('{KNOWLEDGE_ROOT}') && runSrc.includes('knowledgeRoot'))
|
|
133
|
+
assert('平台模式替换 {SPEC_ROOT}', runSrc.includes('{SPEC_ROOT}') && runSrc.includes("specSillyspec"))
|
|
134
|
+
|
|
135
|
+
// 非平台模式块
|
|
136
|
+
assert('非平台模式替换 {WORKFLOWS_ROOT}', runSrc.includes('workflowsRoot'))
|
|
137
|
+
assert('非平台模式替换 {KNOWLEDGE_ROOT}', runSrc.includes('knowledgeRoot'))
|
|
138
|
+
assert('非平台模式替换 {SPEC_ROOT}', runSrc.includes('{SPEC_ROOT}'))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── 测试 6:quick step 1 prompt 强制要求 quicklog ──
|
|
142
|
+
{
|
|
143
|
+
const { definition } = await import('../src/stages/quick.js')
|
|
144
|
+
const step1Prompt = definition.steps[0].prompt
|
|
145
|
+
|
|
146
|
+
assert('quick step 1 包含 ⛔ 标记', step1Prompt.includes('⛔'))
|
|
147
|
+
assert('quick step 1 包含「不能跳过」', step1Prompt.includes('不能跳过'))
|
|
148
|
+
assert('quick step 1 包含 quicklog 未创建 warning', step1Prompt.includes('quicklog 未创建'))
|
|
149
|
+
assert('quick step 1 输出要求 quicklog 第一行', step1Prompt.includes('第一行确认'))
|
|
150
|
+
|
|
151
|
+
// run.js 审计包含 quicklog 检查
|
|
152
|
+
const runSrc = await readFile(join(__dirname, '..', 'src', 'run.js'), 'utf8')
|
|
153
|
+
assert('quick 审计检查 quicklog 目录存在', runSrc.includes('quicklog 目录不存在'))
|
|
154
|
+
assert('quick 审计检查 quicklog 为空', runSrc.includes('quicklog 目录为空'))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── 结果 ──
|
|
158
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
159
|
+
console.log(`✅ 通过: ${passed.length}`)
|
|
160
|
+
console.log(`❌ 失败: ${failed.length}`)
|
|
161
|
+
console.log(`${'='.repeat(50)}`)
|
|
162
|
+
|
|
163
|
+
if (failed.length > 0) {
|
|
164
|
+
console.log('\n失败详情:')
|
|
165
|
+
for (const f of failed) {
|
|
166
|
+
console.log(` ❌ ${f.label}`)
|
|
167
|
+
if (f.detail) console.log(` ${f.detail}`)
|
|
168
|
+
}
|
|
169
|
+
process.exit(1)
|
|
170
|
+
} else {
|
|
171
|
+
console.log('\n🎉 全部 P0 测试通过!')
|
|
172
|
+
}
|