sillyspec 3.20.3 → 3.20.4
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/.claude/skills/sillyspec-archive/SKILL.md +21 -17
- package/.claude/skills/sillyspec-auto/SKILL.md +83 -77
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -17
- package/.claude/skills/sillyspec-commit/SKILL.md +106 -105
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -44
- package/.claude/skills/sillyspec-doctor/SKILL.md +31 -22
- package/.claude/skills/sillyspec-execute/SKILL.md +30 -17
- package/.claude/skills/sillyspec-explore/SKILL.md +109 -96
- package/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +21 -52
- package/.claude/skills/sillyspec-propose/SKILL.md +21 -17
- package/.claude/skills/sillyspec-quick/SKILL.md +21 -17
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -111
- package/.claude/skills/sillyspec-scan/SKILL.md +21 -17
- package/.claude/skills/sillyspec-state/SKILL.md +54 -54
- package/.claude/skills/sillyspec-status/SKILL.md +21 -17
- package/.claude/skills/sillyspec-verify/SKILL.md +21 -17
- package/.claude/skills/sillyspec-workspace/SKILL.md +157 -149
- package/.husky/pre-push +13 -0
- package/CLAUDE.md +18 -0
- package/README.md +198 -186
- package/SKILL.md +90 -87
- package/bin/sillyspec.js +2 -2
- package/docs/brainstorm-plan-contract.md +64 -0
- package/docs/plan-execute-contract.md +123 -0
- package/docs/platform-scan-protocol.md +298 -0
- package/docs/revision-mode.md +115 -0
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -0
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -0
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -0
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -0
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -0
- package/docs/sillyspec/file-lifecycle.md +125 -0
- package/docs/workflow-contract-regression.md +106 -0
- package/docs/worktree-isolation.md +252 -0
- package/package.json +40 -34
- package/packages/dashboard/dist/assets/{index-D1EVTLmc.js → index-Bq_Z2hne.js} +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/packages/dashboard/dist/prototype-overview.html +256 -0
- package/packages/dashboard/index.html +15 -15
- package/packages/dashboard/package-lock.json +2384 -2384
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/public/prototype-dashboard.html +836 -0
- package/packages/dashboard/public/prototype-overview.html +256 -0
- package/packages/dashboard/server/executor.js +86 -86
- package/packages/dashboard/server/index.js +588 -508
- package/packages/dashboard/server/parser.js +526 -458
- package/packages/dashboard/server/watcher.js +350 -342
- package/packages/dashboard/src/App.vue +558 -325
- package/packages/dashboard/src/components/ActionBar.vue +93 -84
- package/packages/dashboard/src/components/CommandPalette.vue +96 -92
- package/packages/dashboard/src/components/DetailPanel.vue +137 -137
- package/packages/dashboard/src/components/DocPreview.vue +105 -8
- package/packages/dashboard/src/components/DocTree.vue +75 -19
- package/packages/dashboard/src/components/HResizeHandle.vue +48 -0
- package/packages/dashboard/src/components/LogStream.vue +65 -65
- package/packages/dashboard/src/components/PipelineStage.vue +95 -75
- package/packages/dashboard/src/components/PipelineView.vue +156 -129
- package/packages/dashboard/src/components/ProjectCard.vue +187 -0
- package/packages/dashboard/src/components/ProjectList.vue +210 -210
- package/packages/dashboard/src/components/ProjectOverview.vue +113 -139
- package/packages/dashboard/src/components/StageBadge.vue +67 -53
- package/packages/dashboard/src/components/StepCard.vue +94 -89
- package/packages/dashboard/src/components/VResizeHandle.vue +61 -0
- package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
- package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
- package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
- package/packages/dashboard/src/composables/useDashboard.js +192 -164
- package/packages/dashboard/src/composables/useKeyboard.js +119 -119
- package/packages/dashboard/src/composables/useLayout.js +131 -0
- package/packages/dashboard/src/composables/useWebSocket.js +129 -129
- package/packages/dashboard/src/main.js +8 -8
- package/packages/dashboard/src/style.css +132 -132
- package/packages/dashboard/vite.config.js +18 -18
- package/src/brainstorm-postcheck.js +158 -0
- package/src/change-list.js +52 -0
- package/src/change-risk-profile.js +352 -0
- package/src/classify-change.js +73 -0
- package/src/constants.js +70 -0
- package/src/contract-matrix.js +278 -0
- package/src/db.js +201 -0
- package/src/endpoint-extractor.js +315 -0
- package/src/hooks/claude-pre-tool-use.cjs +125 -0
- package/src/hooks/worktree-guard.js +761 -0
- package/src/index.js +922 -252
- package/src/init.js +431 -375
- package/src/knowledge-match.js +130 -0
- package/src/migrate.js +117 -117
- package/src/modules.js +482 -0
- package/src/progress.js +1734 -508
- package/src/run.js +3465 -652
- package/src/scan-postcheck.js +387 -0
- package/src/setup.js +398 -460
- package/src/stage-contract.js +700 -0
- package/src/stages/archive.js +160 -54
- package/src/stages/brainstorm-auto.js +229 -0
- package/src/stages/brainstorm.js +645 -239
- package/src/stages/doctor.js +365 -312
- package/src/stages/execute.js +625 -264
- package/src/stages/explore.js +34 -0
- package/src/stages/index.js +29 -35
- package/src/stages/knowledge.js +498 -0
- package/src/stages/plan-postcheck.js +511 -0
- package/src/stages/plan.js +582 -279
- package/src/stages/propose.js +174 -115
- package/src/stages/quick.js +102 -63
- package/src/stages/scan.js +558 -141
- package/src/stages/status.js +65 -65
- package/src/stages/verify.js +322 -135
- package/src/sync.js +497 -0
- package/src/task-review.js +346 -0
- package/src/workflow.js +785 -0
- package/src/worktree-apply.js +549 -0
- package/src/worktree-deps.js +185 -0
- package/src/worktree.js +982 -0
- package/templates/workflows/archive-impact.yaml +79 -0
- package/templates/workflows/scan-docs.yaml +132 -0
- package/test/brainstorm-plan-contract.test.mjs +273 -0
- package/test/check-syntax.mjs +26 -0
- package/test/cli-top-level-aliases.test.mjs +174 -0
- package/test/contract-artifacts.test.mjs +323 -0
- package/test/decision-supersede.test.mjs +277 -0
- package/test/knowledge-match.test.mjs +231 -0
- package/test/plan-execute-contract.test.mjs +330 -0
- package/test/plan-optimization.test.mjs +572 -0
- package/test/platform-artifacts.test.mjs +190 -0
- package/test/platform-failure-samples.test.mjs +199 -0
- package/test/platform-recovery-chain.test.mjs +179 -0
- package/test/platform-recovery.test.mjs +167 -0
- package/test/platform-scan-p0.test.mjs +175 -0
- package/test/revision-v1.test.mjs +1145 -0
- package/test/run-sanitize-project-name.test.mjs +51 -0
- package/test/run-scan-postcheck-fail.test.mjs +64 -0
- package/test/run-scan-project-parse.test.mjs +200 -0
- package/test/run-tests.mjs +48 -0
- package/test/scan-docs-yaml-placeholders.test.mjs +84 -0
- package/test/scan-knowledge.test.mjs +175 -0
- package/test/scan-paths.test.mjs +68 -0
- package/test/scan-postcheck-project-priority.test.mjs +85 -0
- package/test/scan-postcheck.test.mjs +197 -0
- package/test/scan-workflow-anyfailed-block.test.mjs +52 -0
- package/test/spec-dir.test.mjs +206 -0
- package/test/stage-contract-failed-post-check.test.mjs +102 -0
- package/test/stage-contract.test.mjs +299 -0
- package/test/stage-definitions.test.mjs +39 -0
- package/test/wait-gates.test.mjs +501 -0
- package/test/workflow-spec-base.test.mjs +142 -0
- package/test/worktree-deps-provision.test.mjs +148 -0
- package/test/worktree-guard.test.mjs +136 -0
- package/test/worktree-native-overlay.test.mjs +188 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +0 -97
- package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +0 -51
- package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +0 -29
- package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +0 -34
- package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +0 -13
- package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +0 -43
- package/.sillyspec/changes/auto-mode/design.md +0 -50
- package/.sillyspec/changes/auto-mode/proposal.md +0 -19
- package/.sillyspec/changes/auto-mode/requirements.md +0 -21
- package/.sillyspec/changes/auto-mode/tasks.md +0 -7
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +0 -206
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +0 -199
- package/.sillyspec/changes/dashboard/design.md +0 -219
- package/.sillyspec/changes/dashboard/design.md.braindraft +0 -206
- package/.sillyspec/changes/run-command-design/design.md +0 -1230
- package/.sillyspec/changes/unified-docs-design/design.md +0 -199
- package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
- package/.sillyspec/knowledge/INDEX.md +0 -8
- package/.sillyspec/knowledge/uncategorized.md +0 -3
- package/.sillyspec/plans/2026-04-05-dashboard.md +0 -737
- package/.sillyspec/projects/sillyspec.yaml +0 -3
- package/dist/steps/brainstorm/01-load-context.md +0 -30
- package/dist/steps/brainstorm/02-reuse-check.md +0 -6
- package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
- package/dist/steps/brainstorm/04-module-split.md +0 -23
- package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
- package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
- package/dist/steps/brainstorm/07-present-design.md +0 -3
- package/dist/steps/brainstorm/08-write-design.md +0 -21
- package/dist/steps/brainstorm/09-self-review.md +0 -15
- package/dist/steps/brainstorm/10-user-confirm.md +0 -3
- package/dist/steps/brainstorm/11-output-spec.md +0 -7
- package/dist/steps/brainstorm/manifest.yaml +0 -26
- package/dist/steps/execute/01-load-context.md +0 -41
- package/dist/steps/execute/02-scan-conventions.md +0 -47
- package/dist/steps/execute/03-skill-mcp.md +0 -19
- package/dist/steps/execute/04-assign-task.md +0 -22
- package/dist/steps/execute/04b-prompt-template.md +0 -54
- package/dist/steps/execute/05-write-test.md +0 -7
- package/dist/steps/execute/06-write-code.md +0 -8
- package/dist/steps/execute/07-run-test.md +0 -26
- package/dist/steps/execute/08-fix-issues.md +0 -28
- package/dist/steps/execute/09-next-task.md +0 -33
- package/dist/steps/execute/manifest.yaml +0 -28
- package/dist/steps/plan/01-load-context.md +0 -22
- package/dist/steps/plan/02-anchor-confirm.md +0 -1
- package/dist/steps/plan/03-expand-tasks.md +0 -33
- package/dist/steps/plan/04-mark-order.md +0 -15
- package/dist/steps/plan/05-e2e-planning.md +0 -17
- package/dist/steps/plan/06-self-check.md +0 -16
- package/dist/steps/plan/07-save.md +0 -1
- package/dist/steps/plan/manifest.yaml +0 -18
- package/dist/steps/scan/01-env-detect.md +0 -51
- package/dist/steps/scan/02-tech-stack.md +0 -16
- package/dist/steps/scan/03-conventions.md +0 -16
- package/dist/steps/scan/04-structure.md +0 -19
- package/dist/steps/scan/05-quality.md +0 -18
- package/dist/steps/scan/06-complete.md +0 -49
- package/dist/steps/scan/manifest.yaml +0 -16
- package/dist/steps/verify/01-load-specs.md +0 -28
- package/dist/steps/verify/02-check-tasks.md +0 -1
- package/dist/steps/verify/03-check-design.md +0 -6
- package/dist/steps/verify/04-run-tests.md +0 -7
- package/dist/steps/verify/05-e2e-tests.md +0 -27
- package/dist/steps/verify/05b-e2e-fix.md +0 -33
- package/dist/steps/verify/06-code-quality.md +0 -25
- package/dist/steps/verify/07-lint-check.md +0 -27
- package/dist/steps/verify/08-output-report.md +0 -14
- package/dist/steps/verify/manifest.yaml +0 -22
- package/packages/dashboard/dist/assets/index-DGe8CqeP.css +0 -1
- package/src/derive.js +0 -147
- package/src/step.js +0 -543
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-09: sanitizeProjectName 字母校验 + 长度≥2 + 编号正则收紧
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* - 纯数字 "0"/"7" → null(核心目标,scan-projects.json 脏数据来源)
|
|
6
|
+
* - "0/7" 清洗 "07" 无字母 → null
|
|
7
|
+
* - "a" 长度<2 → null
|
|
8
|
+
* - "fe" 含字母长度≥2 → 通过
|
|
9
|
+
* - "frontend" / "order-service" → 通过
|
|
10
|
+
* - "前端项目" 全中文清洗后 "" → null
|
|
11
|
+
* - "" → null
|
|
12
|
+
*/
|
|
13
|
+
import { sanitizeProjectName } from '../src/run.js'
|
|
14
|
+
|
|
15
|
+
let passed = 0
|
|
16
|
+
let failed = 0
|
|
17
|
+
function assertEqual (actual, expected, msg) {
|
|
18
|
+
const ok = actual === expected
|
|
19
|
+
if (ok) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
20
|
+
else { console.error(`❌ FAIL: ${msg}\n expected: ${JSON.stringify(expected)}\n actual: ${JSON.stringify(actual)}`); failed++ }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 通过用例
|
|
24
|
+
assertEqual(sanitizeProjectName('frontend'), 'frontend', '"frontend" 含字母长度≥2 通过')
|
|
25
|
+
assertEqual(sanitizeProjectName('order-service'), 'order-service', '"order-service" 含字母+横线 通过')
|
|
26
|
+
assertEqual(sanitizeProjectName('fe'), 'fe', '"fe" 最小长度 2 通过')
|
|
27
|
+
assertEqual(sanitizeProjectName('user_service'), 'user_service', '"user_service" 含下划线 通过')
|
|
28
|
+
assertEqual(sanitizeProjectName('app.v2'), 'app.v2', '"app.v2" 含点+数字 通过')
|
|
29
|
+
|
|
30
|
+
// 拒绝用例(核心目标)
|
|
31
|
+
assertEqual(sanitizeProjectName('0'), null, '"0" 纯数字拒绝')
|
|
32
|
+
assertEqual(sanitizeProjectName('7'), null, '"7" 纯数字拒绝')
|
|
33
|
+
assertEqual(sanitizeProjectName('07'), null, '"07" 纯数字拒绝(无字母)')
|
|
34
|
+
assertEqual(sanitizeProjectName('0/7'), null, '"0/7" 清洗后 "07" 无字母拒绝')
|
|
35
|
+
assertEqual(sanitizeProjectName('123'), null, '"123" 纯数字拒绝')
|
|
36
|
+
|
|
37
|
+
// 长度<2
|
|
38
|
+
assertEqual(sanitizeProjectName('a'), null, '"a" 长度<2 拒绝(即使含字母)')
|
|
39
|
+
assertEqual(sanitizeProjectName('z'), null, '"z" 长度<2 拒绝')
|
|
40
|
+
|
|
41
|
+
// 中文 / 空
|
|
42
|
+
assertEqual(sanitizeProjectName('前端项目'), null, '"前端项目" 全中文清洗后 "" 拒绝')
|
|
43
|
+
assertEqual(sanitizeProjectName(''), null, '空字符串拒绝')
|
|
44
|
+
assertEqual(sanitizeProjectName(' '), null, '纯空白拒绝')
|
|
45
|
+
assertEqual(sanitizeProjectName(null), null, 'null 拒绝')
|
|
46
|
+
assertEqual(sanitizeProjectName(undefined), null, 'undefined 拒绝')
|
|
47
|
+
|
|
48
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
49
|
+
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
50
|
+
console.log(`${'='.repeat(50)}`)
|
|
51
|
+
if (failed > 0) process.exit(1)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-06: scan post-check 失败补 return + completed 标记推迟 + 平台模式 exit(1)
|
|
3
|
+
*
|
|
4
|
+
* 验证点:
|
|
5
|
+
* - AC-1: run.js:2433-2438 失败分支末尾补 return { stageCompleted:false, currentIdx, nextPendingIdx: currentIdx }
|
|
6
|
+
* - AC-2: 平台模式 (platformOpts.specRoot || platformOpts.runtimeRoot) 时 process.exit(1)
|
|
7
|
+
* - AC-3: 返回结构与 plan contract (run.js:2551 附近) 完全一致
|
|
8
|
+
* - AC-4: 非平台模式不 exit(只有 if platformOpts 守卫内的 exit)
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'fs'
|
|
11
|
+
import { join, dirname } from 'path'
|
|
12
|
+
import { fileURLToPath } from 'url'
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
const runPath = join(__dirname, '..', 'src', 'run.js')
|
|
16
|
+
const src = readFileSync(runPath, 'utf8')
|
|
17
|
+
|
|
18
|
+
let passed = 0
|
|
19
|
+
let failed = 0
|
|
20
|
+
function check (cond, msg) {
|
|
21
|
+
if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
22
|
+
else { console.error(`❌ FAIL: ${msg}`); failed++ }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 定位 scan post-check failed_post_check 分支
|
|
26
|
+
// 已知关键锚:stageData.status = SCAN_STATUS.FAILED_POST_CHECK
|
|
27
|
+
const failedAnchor = src.indexOf("postResult.status === 'failed_post_check'")
|
|
28
|
+
check(failedAnchor > 0, "源码含 'failed_post_check' 失败分支锚点")
|
|
29
|
+
|
|
30
|
+
// 从锚点向后 800 字符内应该有 return { stageCompleted: false ... }
|
|
31
|
+
const tail = src.slice(failedAnchor, failedAnchor + 1200)
|
|
32
|
+
const hasReturnFalse = /return\s*\{\s*stageCompleted:\s*false/.test(tail)
|
|
33
|
+
check(hasReturnFalse, '失败分支末尾补 return { stageCompleted: false, ... }')
|
|
34
|
+
|
|
35
|
+
// nextPendingIdx: currentIdx 字面出现(与 plan contract 一致)
|
|
36
|
+
const hasNextPendingCurrent = /nextPendingIdx:\s*currentIdx/.test(tail)
|
|
37
|
+
check(hasNextPendingCurrent, '失败分支返回 nextPendingIdx: currentIdx(plan contract 对齐)')
|
|
38
|
+
|
|
39
|
+
// 平台模式 process.exit(1)
|
|
40
|
+
const hasExitGuard = /platformOpts\.specRoot\s*\|\|\s*platformOpts\.runtimeRoot/.test(tail)
|
|
41
|
+
const hasExitOne = /process\.exit\(1\)/.test(tail)
|
|
42
|
+
check(hasExitGuard && hasExitOne, '失败分支含平台模式守卫 + process.exit(1)')
|
|
43
|
+
|
|
44
|
+
// 验证 exit(1) 在 if 守卫内(不是裸调)—— 取 exit(1) 位置向前回溯到最近的 } 或 {
|
|
45
|
+
const exitPos = tail.indexOf('process.exit(1)')
|
|
46
|
+
const segmentBeforeExit = tail.slice(0, exitPos)
|
|
47
|
+
const lastOpenBrace = segmentBeforeExit.lastIndexOf('{')
|
|
48
|
+
const lastCloseBrace = segmentBeforeExit.lastIndexOf('}')
|
|
49
|
+
check(lastOpenBrace > lastCloseBrace, 'process.exit(1) 在某个 if 块的 { ... } 内(受守卫保护)')
|
|
50
|
+
|
|
51
|
+
// 验证 exit 守卫表达式在 exit 之前的同一块内
|
|
52
|
+
const guardSegment = tail.slice(Math.max(0, exitPos - 300), exitPos)
|
|
53
|
+
check(/platformOpts\.(specRoot|runtimeRoot)/.test(guardSegment),
|
|
54
|
+
'process.exit(1) 前的代码块内含平台模式守卫条件')
|
|
55
|
+
|
|
56
|
+
// 对照 plan contract(run.js 内 plan 失败分支 return 结构)
|
|
57
|
+
// 定位 plan 阶段失败分支的 stageCompleted:false return
|
|
58
|
+
const planFailMatch = src.match(/plan[\s\S]{0,2000}?return\s*\{\s*stageCompleted:\s*false[\s\S]{0,200}?\}/)
|
|
59
|
+
check(!!planFailMatch, 'plan contract 存在 stageCompleted:false return(对照基准)')
|
|
60
|
+
|
|
61
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
62
|
+
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
63
|
+
console.log(`${'='.repeat(50)}`)
|
|
64
|
+
if (failed > 0) process.exit(1)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scan step 2 项目列表解析 — 强契约测试
|
|
3
|
+
*
|
|
4
|
+
* 解析只接受两种结构化格式:
|
|
5
|
+
* A) YAML block: scan_projects:\n - id: name
|
|
6
|
+
* B) BEGIN_PROJECT_LIST ... END_PROJECT_LIST
|
|
7
|
+
*
|
|
8
|
+
* 自由文本列表不再解析,防止误识别垃圾项目名。
|
|
9
|
+
*/
|
|
10
|
+
import { sanitizeProjectName, validateParsedProjects } from '../src/run.js'
|
|
11
|
+
|
|
12
|
+
let passed = 0
|
|
13
|
+
let failed = 0
|
|
14
|
+
function assertDeepEqual (actual, expected, msg) {
|
|
15
|
+
const a = JSON.stringify(actual)
|
|
16
|
+
const e = JSON.stringify(expected)
|
|
17
|
+
if (a === e) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
18
|
+
else { console.error(`❌ FAIL: ${msg}\n expected: ${e}\n actual: ${a}`); failed++ }
|
|
19
|
+
}
|
|
20
|
+
function assertEqual (actual, expected, msg) {
|
|
21
|
+
if (actual === expected) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
22
|
+
else { console.error(`❌ FAIL: ${msg}\n expected: ${expected}\n actual: ${actual}`); failed++ }
|
|
23
|
+
}
|
|
24
|
+
function assertIncludes (str, sub, msg) {
|
|
25
|
+
if (str.includes(sub)) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
26
|
+
else { console.error(`❌ FAIL: ${msg}\n expected to include: ${sub}\n actual: ${str}`); failed++ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---- 复刻 run.js 中新的解析逻辑 ----
|
|
30
|
+
function parseProjectListFromOutput(outputText) {
|
|
31
|
+
let parsedProjects = [] // Array<{id, path?}>
|
|
32
|
+
let parsedFromStructuredOutput = false
|
|
33
|
+
if (outputText) {
|
|
34
|
+
// 格式 A: YAML block
|
|
35
|
+
const yamlBlock = outputText.match(/scan_projects:\s*\n([\s\S]+?)(?=$|\n[^\s])/)
|
|
36
|
+
if (yamlBlock) {
|
|
37
|
+
const entries = [...yamlBlock[1].matchAll(/-\s+id:\s*(\S+)(?:[\s\S]*?)(?=\n\s+-\s+id:|$)/g)]
|
|
38
|
+
for (const m of entries) {
|
|
39
|
+
const id = sanitizeProjectName(m[1])
|
|
40
|
+
if (!id) continue
|
|
41
|
+
const pathMatch = m[0].match(/path:\s*(\S+)/)
|
|
42
|
+
const entry = pathMatch ? { id, path: pathMatch[1].trim() } : { id }
|
|
43
|
+
parsedProjects.push(entry)
|
|
44
|
+
}
|
|
45
|
+
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
46
|
+
}
|
|
47
|
+
// 格式 B: BEGIN_PROJECT_LIST
|
|
48
|
+
if (!parsedFromStructuredOutput) {
|
|
49
|
+
const blockMatch = outputText.match(/BEGIN_PROJECT_LIST\s*\n([\s\S]*?)\n*END_PROJECT_LIST/)
|
|
50
|
+
if (blockMatch) {
|
|
51
|
+
const raw = [...blockMatch[1].matchAll(/^-\s+(\S+)/gm)].map(m => m[1])
|
|
52
|
+
parsedProjects = raw.map(s => sanitizeProjectName(s)).filter(Boolean).map(id => ({ id }))
|
|
53
|
+
parsedFromStructuredOutput = parsedProjects.length > 0
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { parsedProjects, parsedFromStructuredOutput }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ======== 解析测试 ========
|
|
61
|
+
|
|
62
|
+
// 1. YAML block 正常解析
|
|
63
|
+
assertDeepEqual(
|
|
64
|
+
parseProjectListFromOutput(`scan_projects:
|
|
65
|
+
- id: backend
|
|
66
|
+
- id: frontend
|
|
67
|
+
- id: daemon
|
|
68
|
+
`).parsedProjects,
|
|
69
|
+
[{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }],
|
|
70
|
+
'YAML block 正常解析 3 个项目'
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// 2. YAML block 带 path 字段(多行属性)
|
|
74
|
+
const r2 = parseProjectListFromOutput(`scan_projects:
|
|
75
|
+
- id: api
|
|
76
|
+
path: backend/
|
|
77
|
+
- id: web
|
|
78
|
+
path: frontend/
|
|
79
|
+
`).parsedProjects
|
|
80
|
+
assertDeepEqual(r2, [{ id: 'api', path: 'backend/' }, { id: 'web', path: 'frontend/' }],
|
|
81
|
+
'YAML block 带 path 字段正常解析')
|
|
82
|
+
|
|
83
|
+
// 3. BEGIN_PROJECT_LIST 正常解析
|
|
84
|
+
assertDeepEqual(
|
|
85
|
+
parseProjectListFromOutput(`BEGIN_PROJECT_LIST
|
|
86
|
+
- backend
|
|
87
|
+
- frontend
|
|
88
|
+
- daemon
|
|
89
|
+
END_PROJECT_LIST`).parsedProjects,
|
|
90
|
+
[{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }],
|
|
91
|
+
'BEGIN_PROJECT_LIST 标记块正常解析'
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
// 4. 自由文本编号列表 → 不解析
|
|
95
|
+
const { parsedProjects: fn, parsedFromStructuredOutput: fnParsed } =
|
|
96
|
+
parseProjectListFromOutput('扫描项目列表:\n1. scan1backendmulti-agent-platform-api FastAPI Python\n2. frontendmulti-agent-platform-web Next.js TS\n3. sillyhub-daemon Node.js TS')
|
|
97
|
+
assertEqual(fnParsed, false, '自由文本编号列表不触发解析')
|
|
98
|
+
assertDeepEqual(fn, [], '自由文本编号列表不产生任何项目名')
|
|
99
|
+
|
|
100
|
+
// 5. 自由文本括号枚举 → 不解析
|
|
101
|
+
const { parsedProjects: fn2, parsedFromStructuredOutput: fn2Parsed } =
|
|
102
|
+
parseProjectListFromOutput('子项目: backend / frontend / user-service')
|
|
103
|
+
assertEqual(fn2Parsed, false, '自由文本括号枚举不触发解析')
|
|
104
|
+
assertDeepEqual(fn2, [], '自由文本括号枚举不产生任何项目名')
|
|
105
|
+
|
|
106
|
+
// 6. 空 outputText → 不解析
|
|
107
|
+
const { parsedFromStructuredOutput: emptyParsed } = parseProjectListFromOutput('')
|
|
108
|
+
assertEqual(emptyParsed, false, '空 outputText 不触发解析')
|
|
109
|
+
|
|
110
|
+
// 7. 普通摘要文本 → 不解析
|
|
111
|
+
const { parsedProjects: summary, parsedFromStructuredOutput: summaryParsed } =
|
|
112
|
+
parseProjectListFromOutput('确认扫描 3 个子项目:backend、frontend、daemon,全部重新扫描')
|
|
113
|
+
assertEqual(summaryParsed, false, '普通摘要文本不触发解析')
|
|
114
|
+
assertDeepEqual(summary, [], '普通摘要文本不产生任何项目名')
|
|
115
|
+
|
|
116
|
+
// ======== validateParsedProjects — 基础校验 ========
|
|
117
|
+
|
|
118
|
+
// 8. 正常列表(无 path)
|
|
119
|
+
assertDeepEqual(
|
|
120
|
+
validateParsedProjects([{ id: 'backend' }, { id: 'frontend' }, { id: 'daemon' }], '/tmp/proj'),
|
|
121
|
+
{ ok: true, errors: [] },
|
|
122
|
+
'正常列表校验通过(无 path)'
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// 9. 正常列表(有合法 path)——需要用真实路径
|
|
126
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
127
|
+
import { join } from 'path'
|
|
128
|
+
import os from 'os'
|
|
129
|
+
const tmpDir = mkdtempSync(join(os.tmpdir(), 'sillyspec-test-'))
|
|
130
|
+
mkdirSync(join(tmpDir, 'backend'))
|
|
131
|
+
mkdirSync(join(tmpDir, 'frontend'))
|
|
132
|
+
assertDeepEqual(
|
|
133
|
+
validateParsedProjects([
|
|
134
|
+
{ id: 'backend', path: 'backend' },
|
|
135
|
+
{ id: 'frontend', path: 'frontend' }
|
|
136
|
+
], tmpDir),
|
|
137
|
+
{ ok: true, errors: [] },
|
|
138
|
+
'合法 path 校验通过'
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// 10. path 不存在 → 失败
|
|
142
|
+
const v10 = validateParsedProjects([{ id: 'backend', path: 'nonexistent' }], tmpDir)
|
|
143
|
+
assertEqual(v10.ok, false, 'path 不存在校验失败')
|
|
144
|
+
assertIncludes(v10.errors[0], '不存在', '错误提示包含"不存在"')
|
|
145
|
+
|
|
146
|
+
// 11. path 含 .. 越界 → 失败
|
|
147
|
+
const v11 = validateParsedProjects([{ id: 'backend', path: '../etc/passwd' }], tmpDir)
|
|
148
|
+
assertEqual(v11.ok, false, 'path 包含 .. 校验失败')
|
|
149
|
+
assertIncludes(v11.errors[0], '..', '错误提示包含 ..')
|
|
150
|
+
|
|
151
|
+
// 12. path 解析后超出 source_root → 失败
|
|
152
|
+
const v12 = validateParsedProjects([{ id: 'backend', path: '/etc/passwd' }], tmpDir)
|
|
153
|
+
assertEqual(v12.ok, false, '绝对路径超出 source_root 校验失败')
|
|
154
|
+
assertIncludes(v12.errors[0], '超出', '错误提示包含"超出"')
|
|
155
|
+
|
|
156
|
+
// 13. 超过 10 个项目 → 失败
|
|
157
|
+
const many = Array.from({ length: 11 }, (_, i) => ({ id: `project${i}` }))
|
|
158
|
+
const v13 = validateParsedProjects(many, tmpDir)
|
|
159
|
+
assertEqual(v13.ok, false, '超过 10 个项目校验失败')
|
|
160
|
+
assertIncludes(v13.errors[0], '超过上限', '错误提示包含"超过上限"')
|
|
161
|
+
|
|
162
|
+
// 14. 重复项目名 → 失败
|
|
163
|
+
const v14 = validateParsedProjects([{ id: 'backend' }, { id: 'frontend' }, { id: 'backend' }], tmpDir)
|
|
164
|
+
assertEqual(v14.ok, false, '重复项目名校验失败')
|
|
165
|
+
assertIncludes(v14.errors[0], '重复', '错误提示包含"重复"')
|
|
166
|
+
|
|
167
|
+
// 15. 非法 slug(中文)→ 失败
|
|
168
|
+
const v15 = validateParsedProjects([{ id: 'backend' }, { id: '前端服务' }], tmpDir)
|
|
169
|
+
assertEqual(v15.ok, false, '非法 slug 校验失败')
|
|
170
|
+
|
|
171
|
+
// 16. 空列表 → 失败
|
|
172
|
+
const v16 = validateParsedProjects([], tmpDir)
|
|
173
|
+
assertEqual(v16.ok, false, '空列表校验失败')
|
|
174
|
+
|
|
175
|
+
// 17. null 列表 → 失败
|
|
176
|
+
const v17 = validateParsedProjects(null, tmpDir)
|
|
177
|
+
assertEqual(v17.ok, false, 'null 列表校验失败')
|
|
178
|
+
|
|
179
|
+
// 18. 单字符 → 失败
|
|
180
|
+
const v18 = validateParsedProjects([{ id: 'a' }], tmpDir)
|
|
181
|
+
assertEqual(v18.ok, false, '单字符项目名校验失败')
|
|
182
|
+
|
|
183
|
+
// 19. 纯数字 → 失败
|
|
184
|
+
const v19 = validateParsedProjects([{ id: '123' }], tmpDir)
|
|
185
|
+
assertEqual(v19.ok, false, '纯数字项目名校验失败')
|
|
186
|
+
|
|
187
|
+
// 20. 兼容旧 API(纯字符串数组)
|
|
188
|
+
assertDeepEqual(
|
|
189
|
+
validateParsedProjects(['backend', 'frontend'], tmpDir),
|
|
190
|
+
{ ok: true, errors: [] },
|
|
191
|
+
'旧 API 兼容:纯字符串数组仍通过'
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// 清理
|
|
195
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
196
|
+
|
|
197
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
198
|
+
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
199
|
+
console.log(`${'='.repeat(50)}`)
|
|
200
|
+
if (failed > 0) process.exit(1)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { execFileSync } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
const testDir = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const files = readdirSync(testDir)
|
|
8
|
+
.filter(file => file.endsWith('.test.mjs'))
|
|
9
|
+
.sort()
|
|
10
|
+
|
|
11
|
+
if (files.length === 0) {
|
|
12
|
+
console.log('No test files found')
|
|
13
|
+
process.exit(0)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let passed = 0
|
|
17
|
+
let failed = 0
|
|
18
|
+
const failures = []
|
|
19
|
+
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const fullPath = join(testDir, file)
|
|
22
|
+
console.log(`\nRunning ${file}`)
|
|
23
|
+
try {
|
|
24
|
+
const output = execFileSync(process.execPath, [fullPath], {
|
|
25
|
+
cwd: testDir,
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
timeout: 120_000
|
|
29
|
+
})
|
|
30
|
+
if (output) process.stdout.write(output)
|
|
31
|
+
passed++
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err.stdout) process.stdout.write(err.stdout)
|
|
34
|
+
if (err.stderr) process.stderr.write(err.stderr)
|
|
35
|
+
failed++
|
|
36
|
+
failures.push(file)
|
|
37
|
+
console.log(` ❌ ${file} exited with code ${err.status || 1}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
42
|
+
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
43
|
+
if (failures.length > 0) {
|
|
44
|
+
console.log(`失败文件: ${failures.join(', ')}`)
|
|
45
|
+
}
|
|
46
|
+
console.log(`${'='.repeat(50)}`)
|
|
47
|
+
|
|
48
|
+
process.exit(failed > 0 ? 1 : 0)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-05: scan-docs.yaml 占位符 {SPEC_ROOT} + 项目名优先级
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* - AC-01: 8 处 outputs.path 均为 {SPEC_ROOT}/docs/<project>/scan/*.md
|
|
6
|
+
* - AC-02: write_scope 含 {SPEC_ROOT}/docs/<project>/scan/
|
|
7
|
+
* - AC-07: 旧 yaml(无 {SPEC_ROOT})兼容,replace 不命中也不报错
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from 'fs'
|
|
10
|
+
import { join, dirname } from 'path'
|
|
11
|
+
import { fileURLToPath } from 'url'
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
const yamlPath = join(__dirname, '..', 'templates', 'workflows', 'scan-docs.yaml')
|
|
15
|
+
const yaml = readFileSync(yamlPath, 'utf8')
|
|
16
|
+
|
|
17
|
+
let passed = 0
|
|
18
|
+
let failed = 0
|
|
19
|
+
function assert (cond, msg) {
|
|
20
|
+
if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
|
|
21
|
+
else { console.error(`❌ FAIL: ${msg}`); failed++ }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// AC-01: 8 处 outputs.path 改占位符
|
|
25
|
+
const expectedDocs = [
|
|
26
|
+
'ARCHITECTURE.md',
|
|
27
|
+
'CONVENTIONS.md',
|
|
28
|
+
'STRUCTURE.md',
|
|
29
|
+
'INTEGRATIONS.md',
|
|
30
|
+
'TESTING.md',
|
|
31
|
+
'CONCERNS.md',
|
|
32
|
+
'PROJECT.md',
|
|
33
|
+
]
|
|
34
|
+
console.log('=== AC-01: outputs.path 改占位符 {SPEC_ROOT} ===')
|
|
35
|
+
for (const doc of expectedDocs) {
|
|
36
|
+
const expectedLine = `path: "{SPEC_ROOT}/docs/<project>/scan/${doc}"`
|
|
37
|
+
assert(yaml.includes(expectedLine), `outputs.path 含 {SPEC_ROOT} 占位符 → ${doc}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// AC-01 反向:不应再有硬编码 .sillyspec/docs/<project>/scan/X.md
|
|
41
|
+
console.log('\n=== AC-01 反向:不再有硬编码 .sillyspec/docs/<project>/scan/ ===')
|
|
42
|
+
const hardcodedMatches = yaml.match(/\.sillyspec\/docs\/<project>\/scan\//g) || []
|
|
43
|
+
assert(hardcodedMatches.length === 0,
|
|
44
|
+
`outputs.path/write_scope 不再含 ".sillyspec/docs/<project>/scan/"(找到 ${hardcodedMatches.length} 处)`)
|
|
45
|
+
|
|
46
|
+
// AC-02: write_scope 含 {SPEC_ROOT}
|
|
47
|
+
console.log('\n=== AC-02: write_scope 含 {SPEC_ROOT} ===')
|
|
48
|
+
assert(yaml.includes('- "{SPEC_ROOT}/docs/<project>/scan/"'),
|
|
49
|
+
'write_scope 含 {SPEC_ROOT}/docs/<project>/scan/')
|
|
50
|
+
|
|
51
|
+
// AC-07 兼容:旧 yaml(无 {SPEC_ROOT})replace 不命中(用代码模拟)
|
|
52
|
+
console.log('\n=== AC-07: 旧 yaml 占位符兼容性 ===')
|
|
53
|
+
{
|
|
54
|
+
const legacyPrompt = '写文件到 .sillyspec/docs/<project>/scan/X.md'
|
|
55
|
+
const replaced = legacyPrompt.replace(/\{SPEC_ROOT\}/g, '/tmp/spec')
|
|
56
|
+
assert(!replaced.includes('{SPEC_ROOT}'), '旧 yaml 无 {SPEC_ROOT} 字面:replace 后无残留')
|
|
57
|
+
assert(replaced.includes('.sillyspec/docs/'), '旧 yaml 行为:保留原 .sillyspec/docs/ 路径(向后兼容)')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// AC-03: 新 yaml 替换后路径正确
|
|
61
|
+
console.log('\n=== AC-03: 新 yaml 占位符替换 ===')
|
|
62
|
+
{
|
|
63
|
+
const newPrompt = '写文件到 {SPEC_ROOT}/docs/<project>/scan/X.md'
|
|
64
|
+
const step1 = newPrompt.replace(/\{SPEC_ROOT\}/g, '/tmp/spec')
|
|
65
|
+
const step2 = step1.replace(/<project>/g, 'myaaa')
|
|
66
|
+
assert(step2 === '写文件到 /tmp/spec/docs/myaaa/scan/X.md',
|
|
67
|
+
`占位符替换正确:${step2}`)
|
|
68
|
+
assert(!step2.includes('{SPEC_ROOT}'), '替换后无 {SPEC_ROOT} 残留')
|
|
69
|
+
assert(!step2.includes('<project>'), '替换后无 <project> 残留')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 结构完整性:其他字段未动
|
|
73
|
+
console.log('\n=== 结构完整性:其他字段未变 ===')
|
|
74
|
+
assert(yaml.includes('name: scan-docs'), 'yaml name 字段保留')
|
|
75
|
+
assert(yaml.includes('checks:'), 'checks 段保留')
|
|
76
|
+
assert(yaml.includes('path: "scan/"'), 'workflow_level.path "scan/" 相对子路径保留(不动)')
|
|
77
|
+
assert(yaml.includes('retry:'), 'retry 段保留')
|
|
78
|
+
assert(yaml.includes('on_check_failure: prompt_retry'), 'on_check_failure 保留')
|
|
79
|
+
assert(yaml.includes('allow_shell: true'), 'permissions.allow_shell 保留')
|
|
80
|
+
|
|
81
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
82
|
+
console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
|
|
83
|
+
console.log(`${'='.repeat(50)}`)
|
|
84
|
+
if (failed > 0) process.exit(1)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scan-knowledge.test.mjs — knowledge 产物校验测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { join, resolve, dirname, basename } from 'path'
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
8
|
+
import { tmpdir } from 'os'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = dirname(__filename)
|
|
12
|
+
const root = resolve(__dirname, '..')
|
|
13
|
+
|
|
14
|
+
const { runScanPostCheck } = await import(pathToFileURL(join(root, 'src', 'scan-postcheck.js')).href)
|
|
15
|
+
|
|
16
|
+
let passed = 0, failed = 0
|
|
17
|
+
|
|
18
|
+
function assert(cond, msg) {
|
|
19
|
+
if (cond) { console.log(` ✅ PASS: ${msg}`); passed++ }
|
|
20
|
+
else { console.log(` ❌ FAIL: ${msg}`); failed++ }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function setup(name) {
|
|
24
|
+
const cwd = join(tmpdir(), `kn-${name}`)
|
|
25
|
+
mkdirSync(cwd, { recursive: true })
|
|
26
|
+
return cwd
|
|
27
|
+
}
|
|
28
|
+
function specSetup(name) {
|
|
29
|
+
const d = join(tmpdir(), `kn-${name}-spec`)
|
|
30
|
+
mkdirSync(d, { recursive: true })
|
|
31
|
+
return d
|
|
32
|
+
}
|
|
33
|
+
function clean(...dirs) { for (const d of dirs) try { rmSync(d, { recursive: true, force: true }) } catch {} }
|
|
34
|
+
|
|
35
|
+
const DOCS = ['ARCHITECTURE.md','CONVENTIONS.md','STRUCTURE.md','INTEGRATIONS.md','TESTING.md','CONCERNS.md','PROJECT.md']
|
|
36
|
+
|
|
37
|
+
function writeFull(cwd, specDir) {
|
|
38
|
+
const proj = basename(cwd)
|
|
39
|
+
for (const d of DOCS) {
|
|
40
|
+
const p = join(specDir, 'docs', proj, 'scan', d)
|
|
41
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
42
|
+
writeFileSync(p, 'author: bot\ncreated_at: 2026-06-19 10:00:00\n# doc\n')
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── 1: knowledge INDEX.md 存在,引用真实文件 → 通过 ──
|
|
47
|
+
console.log('\n=== Test 1: knowledge INDEX.md 存在且引用有效 → no knowledge warning ===')
|
|
48
|
+
{
|
|
49
|
+
const cwd = setup('t1'), spec = specSetup('t1')
|
|
50
|
+
try {
|
|
51
|
+
writeFull(cwd, spec)
|
|
52
|
+
const knowledgeDir = join(spec, 'knowledge')
|
|
53
|
+
mkdirSync(knowledgeDir, { recursive: true })
|
|
54
|
+
writeFileSync(join(knowledgeDir, 'INDEX.md'),
|
|
55
|
+
'# Knowledge Index\n\n## Conventions\n- [kebab-case naming](conventions.md#kebab-case-naming)\n')
|
|
56
|
+
writeFileSync(join(knowledgeDir, 'conventions.md'),
|
|
57
|
+
'# Conventions\n\n## kebab-case-naming\nFiles use kebab-case.\n')
|
|
58
|
+
|
|
59
|
+
const result = runScanPostCheck({ cwd, specDir: spec })
|
|
60
|
+
const knowledgeChecks = result.checks.filter(c => c.name.startsWith('knowledge_'))
|
|
61
|
+
assert(knowledgeChecks.length === 0, 'no knowledge warnings when INDEX.md + referenced files exist')
|
|
62
|
+
} finally { clean(cwd, spec) }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 2: knowledge 目录不存在 → dir_missing 警告 ──
|
|
66
|
+
console.log('\n=== Test 2: knowledge 目录不存在 → knowledge_dir_missing ===')
|
|
67
|
+
{
|
|
68
|
+
const cwd = setup('t2'), spec = specSetup('t2')
|
|
69
|
+
try {
|
|
70
|
+
writeFull(cwd, spec)
|
|
71
|
+
|
|
72
|
+
const result = runScanPostCheck({ cwd, specDir: spec })
|
|
73
|
+
const missing = result.checks.find(c => c.name === 'knowledge_dir_missing')
|
|
74
|
+
assert(!!missing, 'knowledge_dir_missing check exists')
|
|
75
|
+
assert(missing.detail.includes('knowledge/ 目录不存在'), 'detail text correct')
|
|
76
|
+
} finally { clean(cwd, spec) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── 3: INDEX.md 不存在 → index_missing 警告 ──
|
|
80
|
+
console.log('\n=== Test 3: INDEX.md 不存在 → knowledge_index_missing ===')
|
|
81
|
+
{
|
|
82
|
+
const cwd = setup('t3'), spec = specSetup('t3')
|
|
83
|
+
try {
|
|
84
|
+
writeFull(cwd, spec)
|
|
85
|
+
const knowledgeDir = join(spec, 'knowledge')
|
|
86
|
+
mkdirSync(knowledgeDir, { recursive: true })
|
|
87
|
+
// INDEX.md not created
|
|
88
|
+
|
|
89
|
+
const result = runScanPostCheck({ cwd, specDir: spec })
|
|
90
|
+
const missing = result.checks.find(c => c.name === 'knowledge_index_missing')
|
|
91
|
+
assert(!!missing, 'knowledge_index_missing check exists')
|
|
92
|
+
assert(missing.detail.includes('INDEX.md 不存在'), 'detail text correct')
|
|
93
|
+
} finally { clean(cwd, spec) }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── 4: INDEX.md 引用不存在文件 → broken_refs 警告 ──
|
|
97
|
+
console.log('\n=== Test 4: INDEX.md 引用不存在的文件 → knowledge_broken_refs ===')
|
|
98
|
+
{
|
|
99
|
+
const cwd = setup('t4'), spec = specSetup('t4')
|
|
100
|
+
try {
|
|
101
|
+
writeFull(cwd, spec)
|
|
102
|
+
const knowledgeDir = join(spec, 'knowledge')
|
|
103
|
+
mkdirSync(knowledgeDir, { recursive: true })
|
|
104
|
+
writeFileSync(join(knowledgeDir, 'INDEX.md'),
|
|
105
|
+
'# Knowledge Index\n\n## Conventions\n- [naming](conventions.md#naming)\n\n## Patterns\n- [auth pattern](patterns.md#auth-pattern)\n')
|
|
106
|
+
// conventions.md exists, patterns.md does not
|
|
107
|
+
writeFileSync(join(knowledgeDir, 'conventions.md'), '# Conventions\n')
|
|
108
|
+
|
|
109
|
+
const result = runScanPostCheck({ cwd, specDir: spec })
|
|
110
|
+
const broken = result.checks.find(c => c.name === 'knowledge_broken_refs')
|
|
111
|
+
assert(!!broken, 'knowledge_broken_refs check exists')
|
|
112
|
+
assert(broken.detail.includes('patterns.md'), 'reports missing patterns.md')
|
|
113
|
+
} finally { clean(cwd, spec) }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── 5: uncategorized.md 为空但存在 → 通过 ──
|
|
117
|
+
console.log('\n=== Test 5: uncategorized.md 空但存在 → no knowledge warning ===')
|
|
118
|
+
{
|
|
119
|
+
const cwd = setup('t5'), spec = specSetup('t5')
|
|
120
|
+
try {
|
|
121
|
+
writeFull(cwd, spec)
|
|
122
|
+
const knowledgeDir = join(spec, 'knowledge')
|
|
123
|
+
mkdirSync(knowledgeDir, { recursive: true })
|
|
124
|
+
writeFileSync(join(knowledgeDir, 'INDEX.md'),
|
|
125
|
+
'# Knowledge Index\n\n(no entries yet)\n')
|
|
126
|
+
writeFileSync(join(knowledgeDir, 'uncategorized.md'), '# Uncategorized\n')
|
|
127
|
+
|
|
128
|
+
const result = runScanPostCheck({ cwd, specDir: spec })
|
|
129
|
+
const knowledgeChecks = result.checks.filter(c => c.name.startsWith('knowledge_'))
|
|
130
|
+
assert(knowledgeChecks.length === 0, 'empty uncategorized.md passes')
|
|
131
|
+
} finally { clean(cwd, spec) }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── 6: categorized 文件存在但 INDEX.md 无条目 → 通过(不强求 categorized 非空)──
|
|
135
|
+
console.log('\n=== Test 6: categorized 文件存在 + INDEX.md 有效 → no knowledge warning ===')
|
|
136
|
+
{
|
|
137
|
+
const cwd = setup('t6'), spec = specSetup('t6')
|
|
138
|
+
try {
|
|
139
|
+
writeFull(cwd, spec)
|
|
140
|
+
const knowledgeDir = join(spec, 'knowledge')
|
|
141
|
+
mkdirSync(knowledgeDir, { recursive: true })
|
|
142
|
+
writeFileSync(join(knowledgeDir, 'INDEX.md'),
|
|
143
|
+
'# Knowledge Index\n\n## Known Issues\n- [GLM proxy](known-issues.md#glm-proxy)\n')
|
|
144
|
+
writeFileSync(join(knowledgeDir, 'known-issues.md'),
|
|
145
|
+
'# Known Issues\n\n## glm-proxy\nGLM proxy has usage metadata limits.\n')
|
|
146
|
+
writeFileSync(join(knowledgeDir, 'uncategorized.md'), '# Uncategorized\n')
|
|
147
|
+
|
|
148
|
+
const result = runScanPostCheck({ cwd, specDir: spec })
|
|
149
|
+
const knowledgeChecks = result.checks.filter(c => c.name.startsWith('knowledge_'))
|
|
150
|
+
assert(knowledgeChecks.length === 0, 'at least one categorized file + valid INDEX passes')
|
|
151
|
+
} finally { clean(cwd, spec) }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── 7: source_root knowledge 泄漏检测(平台模式已有检查)──
|
|
155
|
+
console.log('\n=== Test 7: source_root knowledge 泄漏 → source_root_leak ===')
|
|
156
|
+
{
|
|
157
|
+
const cwd = setup('t7'), spec = specSetup('t7')
|
|
158
|
+
try {
|
|
159
|
+
writeFull(cwd, spec)
|
|
160
|
+
// 写入 source_root/.sillyspec/knowledge/ 泄漏
|
|
161
|
+
const leakedKnowledgeDir = join(cwd, '.sillyspec', 'knowledge')
|
|
162
|
+
mkdirSync(leakedKnowledgeDir, { recursive: true })
|
|
163
|
+
writeFileSync(join(leakedKnowledgeDir, 'INDEX.md'), '# leaked\n')
|
|
164
|
+
|
|
165
|
+
const result = runScanPostCheck({ cwd, specDir: spec })
|
|
166
|
+
const leak = result.checks.find(c => c.name === 'source_root_leak')
|
|
167
|
+
assert(!!leak, 'source_root knowledge leak detected')
|
|
168
|
+
assert(leak.severity === 'failed', 'leak severity is failed')
|
|
169
|
+
} finally { clean(cwd, spec) }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── 汇总 ──
|
|
173
|
+
console.log(`\n${'='.repeat(40)}`)
|
|
174
|
+
console.log(`scan-knowledge tests: ${passed} passed, ${failed} failed, ${passed + failed} total`)
|
|
175
|
+
if (failed > 0) process.exit(1)
|