sillyspec 3.20.2 → 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.
Files changed (133) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -21
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -21
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
  19. package/.husky/pre-push +13 -13
  20. package/CLAUDE.md +18 -18
  21. package/README.md +198 -188
  22. package/SKILL.md +90 -91
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -64
  25. package/docs/plan-execute-contract.md +123 -123
  26. package/docs/platform-scan-protocol.md +298 -298
  27. package/docs/revision-mode.md +115 -115
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
  33. package/docs/sillyspec/file-lifecycle.md +125 -125
  34. package/docs/workflow-contract-regression.md +106 -106
  35. package/docs/worktree-isolation.md +252 -252
  36. package/package.json +40 -40
  37. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/index.html +15 -15
  41. package/packages/dashboard/package-lock.json +2384 -2384
  42. package/packages/dashboard/package.json +25 -25
  43. package/packages/dashboard/server/executor.js +86 -86
  44. package/packages/dashboard/server/index.js +588 -588
  45. package/packages/dashboard/server/parser.js +526 -526
  46. package/packages/dashboard/server/watcher.js +344 -344
  47. package/packages/dashboard/src/App.vue +558 -558
  48. package/packages/dashboard/src/components/ActionBar.vue +93 -93
  49. package/packages/dashboard/src/components/CommandPalette.vue +96 -96
  50. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  51. package/packages/dashboard/src/components/LogStream.vue +65 -65
  52. package/packages/dashboard/src/components/PipelineStage.vue +95 -95
  53. package/packages/dashboard/src/components/PipelineView.vue +156 -156
  54. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  55. package/packages/dashboard/src/components/StageBadge.vue +67 -67
  56. package/packages/dashboard/src/components/StepCard.vue +94 -94
  57. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  58. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  59. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  60. package/packages/dashboard/src/composables/useDashboard.js +170 -170
  61. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  62. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  63. package/packages/dashboard/src/main.js +8 -8
  64. package/packages/dashboard/src/style.css +132 -132
  65. package/packages/dashboard/vite.config.js +18 -18
  66. package/src/brainstorm-postcheck.js +158 -158
  67. package/src/change-list.js +52 -52
  68. package/src/change-risk-profile.js +352 -352
  69. package/src/classify-change.js +73 -73
  70. package/src/constants.js +70 -70
  71. package/src/contract-matrix.js +278 -278
  72. package/src/db.js +201 -201
  73. package/src/endpoint-extractor.js +315 -315
  74. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  75. package/src/hooks/worktree-guard.js +653 -653
  76. package/src/index.js +922 -900
  77. package/src/init.js +431 -431
  78. package/src/knowledge-match.js +130 -130
  79. package/src/migrate.js +117 -117
  80. package/src/modules.js +482 -482
  81. package/src/progress.js +1734 -1734
  82. package/src/run.js +3465 -3358
  83. package/src/scan-postcheck.js +387 -383
  84. package/src/setup.js +398 -398
  85. package/src/stage-contract.js +700 -700
  86. package/src/stages/archive.js +160 -160
  87. package/src/stages/brainstorm-auto.js +229 -229
  88. package/src/stages/brainstorm.js +645 -645
  89. package/src/stages/doctor.js +365 -365
  90. package/src/stages/execute.js +625 -625
  91. package/src/stages/explore.js +34 -34
  92. package/src/stages/index.js +29 -29
  93. package/src/stages/knowledge.js +498 -498
  94. package/src/stages/plan-postcheck.js +511 -513
  95. package/src/stages/plan.js +582 -582
  96. package/src/stages/propose.js +174 -174
  97. package/src/stages/quick.js +82 -82
  98. package/src/stages/scan.js +558 -558
  99. package/src/stages/status.js +65 -65
  100. package/src/stages/verify.js +322 -322
  101. package/src/sync.js +497 -497
  102. package/src/task-review.js +346 -346
  103. package/src/workflow.js +785 -785
  104. package/src/worktree-apply.js +549 -549
  105. package/src/worktree-deps.js +185 -0
  106. package/src/worktree.js +982 -932
  107. package/templates/workflows/archive-impact.yaml +79 -79
  108. package/templates/workflows/scan-docs.yaml +132 -132
  109. package/test/brainstorm-plan-contract.test.mjs +273 -273
  110. package/test/check-syntax.mjs +26 -26
  111. package/test/contract-artifacts.test.mjs +323 -323
  112. package/test/decision-supersede.test.mjs +277 -277
  113. package/test/knowledge-match.test.mjs +231 -231
  114. package/test/plan-execute-contract.test.mjs +330 -330
  115. package/test/plan-optimization.test.mjs +572 -572
  116. package/test/platform-artifacts.test.mjs +166 -166
  117. package/test/platform-failure-samples.test.mjs +199 -199
  118. package/test/platform-recovery-chain.test.mjs +167 -167
  119. package/test/platform-recovery.test.mjs +136 -136
  120. package/test/platform-scan-p0.test.mjs +168 -168
  121. package/test/revision-v1.test.mjs +1145 -1145
  122. package/test/run-scan-project-parse.test.mjs +200 -200
  123. package/test/run-tests.mjs +48 -48
  124. package/test/scan-knowledge.test.mjs +175 -175
  125. package/test/scan-paths.test.mjs +68 -68
  126. package/test/scan-postcheck.test.mjs +197 -197
  127. package/test/spec-dir.test.mjs +206 -206
  128. package/test/stage-contract.test.mjs +299 -299
  129. package/test/stage-definitions.test.mjs +39 -39
  130. package/test/wait-gates.test.mjs +496 -496
  131. package/test/worktree-deps-provision.test.mjs +148 -0
  132. package/test/worktree-guard.test.mjs +71 -71
  133. package/test/worktree-native-overlay.test.mjs +188 -188
@@ -1,231 +1,231 @@
1
- /**
2
- * knowledge-match.test.mjs — knowledge 关键词匹配引擎测试
3
- */
4
-
5
- import { join } 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 = join(__filename, '..') // join(__dirname, '..') to get parent
12
- const root = join(__dirname, '..')
13
-
14
- const { parseKnowledgeIndex, matchKnowledge } = await import(
15
- pathToFileURL(join(root, 'src', 'knowledge-match.js')).href
16
- )
17
-
18
- let passed = 0, failed = 0
19
-
20
- function assert(cond, msg) {
21
- if (cond) { console.log(` ✅ PASS: ${msg}`); passed++ }
22
- else { console.log(` ❌ FAIL: ${msg}`); failed++ }
23
- }
24
-
25
- function setup(name) {
26
- const dir = join(tmpdir(), `kh-test-${name}`)
27
- mkdirSync(dir, { recursive: true })
28
- return dir
29
- }
30
- function clean(...dirs) {
31
- for (const d of dirs) try { rmSync(d, { recursive: true, force: true }) } catch {}
32
- }
33
-
34
- // ── 1: 有匹配条目 → matched=true, entries.length > 0 ──
35
- console.log('\n=== Test 1: 有匹配条目 → matched=true ===')
36
- {
37
- const dir = setup('t1')
38
- try {
39
- writeFileSync(join(dir, 'INDEX.md'), [
40
- '# Knowledge Index',
41
- '',
42
- '## Conventions',
43
- '- ESM|module|import → [ESM Only](conventions.md#esm-only)',
44
- '',
45
- '## Patterns',
46
- '- 阶段定义|stage|stages → [Stage Pattern](patterns.md#stage-step-pattern)',
47
- ].join('\n'))
48
-
49
- const result = matchKnowledge(dir, 'setup ESM module imports')
50
- assert(result.matched === true, 'matched is true')
51
- assert(result.entries.length === 1, 'one entry matched')
52
- assert(result.entries[0].file === 'conventions.md', 'file is conventions.md')
53
- assert(result.entries[0].anchor === 'esm-only', 'anchor is esm-only')
54
- assert(result.entries[0].keywords.includes('ESM'), 'keywords include ESM')
55
- } finally { clean(dir) }
56
- }
57
-
58
- // ── 2: 无匹配条目 → matched=false, report 包含 "no matches" ──
59
- console.log('\n=== Test 2: 无匹配条目 → matched=false ===')
60
- {
61
- const dir = setup('t2')
62
- try {
63
- writeFileSync(join(dir, 'INDEX.md'), [
64
- '# Knowledge Index',
65
- '',
66
- '## Patterns',
67
- '- 阶段定义|stage → [Stage Pattern](patterns.md#stage-step-pattern)',
68
- ].join('\n'))
69
-
70
- const result = matchKnowledge(dir, 'implement authentication flow')
71
- assert(result.matched === false, 'matched is false')
72
- assert(result.entries.length === 0, 'no entries matched')
73
- assert(result.report.includes('no matches'), 'report says no matches')
74
- } finally { clean(dir) }
75
- }
76
-
77
- // ── 3: INDEX.md 不存在 → matched=false, report 包含 "not found" ──
78
- console.log('\n=== Test 3: INDEX.md 不存在 → matched=false ===')
79
- {
80
- const dir = setup('t3')
81
- try {
82
- const result = matchKnowledge(dir, 'any context')
83
- assert(result.matched === false, 'matched is false')
84
- assert(result.report.includes('not found'), 'report says not found')
85
- assert(result.json.matched === false, 'json.matched is false')
86
- assert(result.json.entry_count === 0, 'json.entry_count is 0')
87
- } finally { clean(dir) }
88
- }
89
-
90
- // ── 4: INDEX.md 有空锚点引用 → 正常处理(不过度校验)──
91
- console.log('\n=== Test 4: INDEX.md 有空锚点引用 → 正常处理 ===')
92
- {
93
- const dir = setup('t4')
94
- try {
95
- writeFileSync(join(dir, 'INDEX.md'), [
96
- '# Knowledge Index',
97
- '',
98
- '## Conventions',
99
- '- naming|camelCase → [Naming](conventions.md)',
100
- ].join('\n'))
101
-
102
- const result = matchKnowledge(dir, 'naming conventions')
103
- assert(result.matched === true, 'matched is true with empty anchor')
104
- assert(result.entries[0].anchor === '', 'anchor is empty string')
105
- assert(result.entries[0].file === 'conventions.md', 'file is correct')
106
- } finally { clean(dir) }
107
- }
108
-
109
- // ── 5: 大小写不敏感匹配 → "STAGE" 匹配 "stage" ──
110
- console.log('\n=== Test 5: 大小写不敏感匹配 ===')
111
- {
112
- const dir = setup('t5')
113
- try {
114
- writeFileSync(join(dir, 'INDEX.md'), [
115
- '# Knowledge Index',
116
- '',
117
- '## Patterns',
118
- '- stage|stages → [Stage Pattern](patterns.md#stage-step-pattern)',
119
- ].join('\n'))
120
-
121
- const result = matchKnowledge(dir, 'implement STAGE definition')
122
- assert(result.matched === true, 'STAGE matches stage')
123
- assert(result.entries.length === 1, 'one entry matched')
124
- } finally { clean(dir) }
125
- }
126
-
127
- // ── 6: report 格式包含 "Knowledge Context" header ──
128
- console.log('\n=== Test 6: report 格式包含 Knowledge Context header ===')
129
- {
130
- const dir = setup('t6')
131
- try {
132
- writeFileSync(join(dir, 'INDEX.md'), [
133
- '# Knowledge Index',
134
- '',
135
- '## Patterns',
136
- '- 阶段定义|stage → [Stage Pattern](patterns.md#stage-step-pattern)',
137
- ].join('\n'))
138
-
139
- const result = matchKnowledge(dir, 'stage configuration')
140
- assert(result.report.includes('Knowledge Context'), 'report has Knowledge Context header')
141
- assert(result.report.includes('Status: matched'), 'report has Status: matched')
142
- assert(result.report.includes('Entries: 1'), 'report has Entries: 1')
143
- assert(result.report.includes('patterns.md#stage-step-pattern'), 'report has source')
144
- } finally { clean(dir) }
145
- }
146
-
147
- // ── 7: json 结构正确 ──
148
- console.log('\n=== Test 7: json 结构正确 ===')
149
- {
150
- const dir = setup('t7')
151
- try {
152
- writeFileSync(join(dir, 'INDEX.md'), [
153
- '# Knowledge Index',
154
- '',
155
- '## Conventions',
156
- '- ESM|module → [ESM Only](conventions.md#esm-only)',
157
- '',
158
- '## Patterns',
159
- '- stage|stages → [Stage Pattern](patterns.md#stage-step-pattern)',
160
- ].join('\n'))
161
-
162
- const result = matchKnowledge(dir, 'stage module setup')
163
- const j = result.json
164
- assert(j.matched === true, 'json.matched is true')
165
- assert(j.entry_count === 2, 'json.entry_count is 2')
166
- assert(Array.isArray(j.entries), 'json.entries is array')
167
- assert(j.entries.length === 2, 'json.entries length is 2')
168
-
169
- const stageEntry = j.entries.find(e => e.anchor === 'stage-step-pattern')
170
- assert(!!stageEntry, 'stage entry found in json')
171
- assert(stageEntry.keywords.includes('stage'), 'stage keywords correct')
172
- assert(stageEntry.category === 'Patterns', 'stage category correct')
173
- assert(stageEntry.file === 'patterns.md', 'stage file correct')
174
- } finally { clean(dir) }
175
- }
176
-
177
- // ── 8: 空目录(目录存在但无 INDEX.md)→ no matches ──
178
- console.log('\n=== Test 8: 空目录(无 INDEX.md)→ no matches ===')
179
- {
180
- const dir = setup('t8')
181
- try {
182
- const result = matchKnowledge(dir, 'any task')
183
- assert(result.matched === false, 'no INDEX.md → matched false')
184
- assert(result.report.includes('not found'), 'report says not found')
185
- } finally { clean(dir) }
186
- }
187
-
188
- // ── 9: parseKnowledgeIndex 返回正确分类 ──
189
- console.log('\n=== Test 9: parseKnowledgeIndex 分类正确 ===')
190
- {
191
- const dir = setup('t9')
192
- try {
193
- writeFileSync(join(dir, 'INDEX.md'), [
194
- '# Knowledge Index',
195
- '',
196
- '## Known Issues',
197
- '- GLM|proxy → [GLM Proxy](known-issues.md#glm-proxy)',
198
- '',
199
- '## Conventions',
200
- '- 命名|naming → [命名规范](conventions.md#naming)',
201
- ].join('\n'))
202
-
203
- const entries = parseKnowledgeIndex(dir)
204
- assert(entries.length === 2, 'parsed 2 entries')
205
- assert(entries[0].category === 'Known Issues', 'first entry category correct')
206
- assert(entries[1].category === 'Conventions', 'second entry category correct')
207
- assert(entries[0].display === 'GLM Proxy', 'display text correct')
208
- } finally { clean(dir) }
209
- }
210
-
211
- // ── 10: taskContext 为空 → no matches ──
212
- console.log('\n=== Test 10: taskContext 为空 → no matches ===')
213
- {
214
- const dir = setup('t10')
215
- try {
216
- writeFileSync(join(dir, 'INDEX.md'), [
217
- '# Knowledge Index',
218
- '',
219
- '## Patterns',
220
- '- stage → [Stage](patterns.md#stage)',
221
- ].join('\n'))
222
-
223
- const result = matchKnowledge(dir, '')
224
- assert(result.matched === false, 'empty context → matched false')
225
- } finally { clean(dir) }
226
- }
227
-
228
- // ── 汇总 ──
229
- console.log(`\n${'='.repeat(40)}`)
230
- console.log(`knowledge-match tests: ${passed} passed, ${failed} failed, ${passed + failed} total`)
231
- if (failed > 0) process.exit(1)
1
+ /**
2
+ * knowledge-match.test.mjs — knowledge 关键词匹配引擎测试
3
+ */
4
+
5
+ import { join } 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 = join(__filename, '..') // join(__dirname, '..') to get parent
12
+ const root = join(__dirname, '..')
13
+
14
+ const { parseKnowledgeIndex, matchKnowledge } = await import(
15
+ pathToFileURL(join(root, 'src', 'knowledge-match.js')).href
16
+ )
17
+
18
+ let passed = 0, failed = 0
19
+
20
+ function assert(cond, msg) {
21
+ if (cond) { console.log(` ✅ PASS: ${msg}`); passed++ }
22
+ else { console.log(` ❌ FAIL: ${msg}`); failed++ }
23
+ }
24
+
25
+ function setup(name) {
26
+ const dir = join(tmpdir(), `kh-test-${name}`)
27
+ mkdirSync(dir, { recursive: true })
28
+ return dir
29
+ }
30
+ function clean(...dirs) {
31
+ for (const d of dirs) try { rmSync(d, { recursive: true, force: true }) } catch {}
32
+ }
33
+
34
+ // ── 1: 有匹配条目 → matched=true, entries.length > 0 ──
35
+ console.log('\n=== Test 1: 有匹配条目 → matched=true ===')
36
+ {
37
+ const dir = setup('t1')
38
+ try {
39
+ writeFileSync(join(dir, 'INDEX.md'), [
40
+ '# Knowledge Index',
41
+ '',
42
+ '## Conventions',
43
+ '- ESM|module|import → [ESM Only](conventions.md#esm-only)',
44
+ '',
45
+ '## Patterns',
46
+ '- 阶段定义|stage|stages → [Stage Pattern](patterns.md#stage-step-pattern)',
47
+ ].join('\n'))
48
+
49
+ const result = matchKnowledge(dir, 'setup ESM module imports')
50
+ assert(result.matched === true, 'matched is true')
51
+ assert(result.entries.length === 1, 'one entry matched')
52
+ assert(result.entries[0].file === 'conventions.md', 'file is conventions.md')
53
+ assert(result.entries[0].anchor === 'esm-only', 'anchor is esm-only')
54
+ assert(result.entries[0].keywords.includes('ESM'), 'keywords include ESM')
55
+ } finally { clean(dir) }
56
+ }
57
+
58
+ // ── 2: 无匹配条目 → matched=false, report 包含 "no matches" ──
59
+ console.log('\n=== Test 2: 无匹配条目 → matched=false ===')
60
+ {
61
+ const dir = setup('t2')
62
+ try {
63
+ writeFileSync(join(dir, 'INDEX.md'), [
64
+ '# Knowledge Index',
65
+ '',
66
+ '## Patterns',
67
+ '- 阶段定义|stage → [Stage Pattern](patterns.md#stage-step-pattern)',
68
+ ].join('\n'))
69
+
70
+ const result = matchKnowledge(dir, 'implement authentication flow')
71
+ assert(result.matched === false, 'matched is false')
72
+ assert(result.entries.length === 0, 'no entries matched')
73
+ assert(result.report.includes('no matches'), 'report says no matches')
74
+ } finally { clean(dir) }
75
+ }
76
+
77
+ // ── 3: INDEX.md 不存在 → matched=false, report 包含 "not found" ──
78
+ console.log('\n=== Test 3: INDEX.md 不存在 → matched=false ===')
79
+ {
80
+ const dir = setup('t3')
81
+ try {
82
+ const result = matchKnowledge(dir, 'any context')
83
+ assert(result.matched === false, 'matched is false')
84
+ assert(result.report.includes('not found'), 'report says not found')
85
+ assert(result.json.matched === false, 'json.matched is false')
86
+ assert(result.json.entry_count === 0, 'json.entry_count is 0')
87
+ } finally { clean(dir) }
88
+ }
89
+
90
+ // ── 4: INDEX.md 有空锚点引用 → 正常处理(不过度校验)──
91
+ console.log('\n=== Test 4: INDEX.md 有空锚点引用 → 正常处理 ===')
92
+ {
93
+ const dir = setup('t4')
94
+ try {
95
+ writeFileSync(join(dir, 'INDEX.md'), [
96
+ '# Knowledge Index',
97
+ '',
98
+ '## Conventions',
99
+ '- naming|camelCase → [Naming](conventions.md)',
100
+ ].join('\n'))
101
+
102
+ const result = matchKnowledge(dir, 'naming conventions')
103
+ assert(result.matched === true, 'matched is true with empty anchor')
104
+ assert(result.entries[0].anchor === '', 'anchor is empty string')
105
+ assert(result.entries[0].file === 'conventions.md', 'file is correct')
106
+ } finally { clean(dir) }
107
+ }
108
+
109
+ // ── 5: 大小写不敏感匹配 → "STAGE" 匹配 "stage" ──
110
+ console.log('\n=== Test 5: 大小写不敏感匹配 ===')
111
+ {
112
+ const dir = setup('t5')
113
+ try {
114
+ writeFileSync(join(dir, 'INDEX.md'), [
115
+ '# Knowledge Index',
116
+ '',
117
+ '## Patterns',
118
+ '- stage|stages → [Stage Pattern](patterns.md#stage-step-pattern)',
119
+ ].join('\n'))
120
+
121
+ const result = matchKnowledge(dir, 'implement STAGE definition')
122
+ assert(result.matched === true, 'STAGE matches stage')
123
+ assert(result.entries.length === 1, 'one entry matched')
124
+ } finally { clean(dir) }
125
+ }
126
+
127
+ // ── 6: report 格式包含 "Knowledge Context" header ──
128
+ console.log('\n=== Test 6: report 格式包含 Knowledge Context header ===')
129
+ {
130
+ const dir = setup('t6')
131
+ try {
132
+ writeFileSync(join(dir, 'INDEX.md'), [
133
+ '# Knowledge Index',
134
+ '',
135
+ '## Patterns',
136
+ '- 阶段定义|stage → [Stage Pattern](patterns.md#stage-step-pattern)',
137
+ ].join('\n'))
138
+
139
+ const result = matchKnowledge(dir, 'stage configuration')
140
+ assert(result.report.includes('Knowledge Context'), 'report has Knowledge Context header')
141
+ assert(result.report.includes('Status: matched'), 'report has Status: matched')
142
+ assert(result.report.includes('Entries: 1'), 'report has Entries: 1')
143
+ assert(result.report.includes('patterns.md#stage-step-pattern'), 'report has source')
144
+ } finally { clean(dir) }
145
+ }
146
+
147
+ // ── 7: json 结构正确 ──
148
+ console.log('\n=== Test 7: json 结构正确 ===')
149
+ {
150
+ const dir = setup('t7')
151
+ try {
152
+ writeFileSync(join(dir, 'INDEX.md'), [
153
+ '# Knowledge Index',
154
+ '',
155
+ '## Conventions',
156
+ '- ESM|module → [ESM Only](conventions.md#esm-only)',
157
+ '',
158
+ '## Patterns',
159
+ '- stage|stages → [Stage Pattern](patterns.md#stage-step-pattern)',
160
+ ].join('\n'))
161
+
162
+ const result = matchKnowledge(dir, 'stage module setup')
163
+ const j = result.json
164
+ assert(j.matched === true, 'json.matched is true')
165
+ assert(j.entry_count === 2, 'json.entry_count is 2')
166
+ assert(Array.isArray(j.entries), 'json.entries is array')
167
+ assert(j.entries.length === 2, 'json.entries length is 2')
168
+
169
+ const stageEntry = j.entries.find(e => e.anchor === 'stage-step-pattern')
170
+ assert(!!stageEntry, 'stage entry found in json')
171
+ assert(stageEntry.keywords.includes('stage'), 'stage keywords correct')
172
+ assert(stageEntry.category === 'Patterns', 'stage category correct')
173
+ assert(stageEntry.file === 'patterns.md', 'stage file correct')
174
+ } finally { clean(dir) }
175
+ }
176
+
177
+ // ── 8: 空目录(目录存在但无 INDEX.md)→ no matches ──
178
+ console.log('\n=== Test 8: 空目录(无 INDEX.md)→ no matches ===')
179
+ {
180
+ const dir = setup('t8')
181
+ try {
182
+ const result = matchKnowledge(dir, 'any task')
183
+ assert(result.matched === false, 'no INDEX.md → matched false')
184
+ assert(result.report.includes('not found'), 'report says not found')
185
+ } finally { clean(dir) }
186
+ }
187
+
188
+ // ── 9: parseKnowledgeIndex 返回正确分类 ──
189
+ console.log('\n=== Test 9: parseKnowledgeIndex 分类正确 ===')
190
+ {
191
+ const dir = setup('t9')
192
+ try {
193
+ writeFileSync(join(dir, 'INDEX.md'), [
194
+ '# Knowledge Index',
195
+ '',
196
+ '## Known Issues',
197
+ '- GLM|proxy → [GLM Proxy](known-issues.md#glm-proxy)',
198
+ '',
199
+ '## Conventions',
200
+ '- 命名|naming → [命名规范](conventions.md#naming)',
201
+ ].join('\n'))
202
+
203
+ const entries = parseKnowledgeIndex(dir)
204
+ assert(entries.length === 2, 'parsed 2 entries')
205
+ assert(entries[0].category === 'Known Issues', 'first entry category correct')
206
+ assert(entries[1].category === 'Conventions', 'second entry category correct')
207
+ assert(entries[0].display === 'GLM Proxy', 'display text correct')
208
+ } finally { clean(dir) }
209
+ }
210
+
211
+ // ── 10: taskContext 为空 → no matches ──
212
+ console.log('\n=== Test 10: taskContext 为空 → no matches ===')
213
+ {
214
+ const dir = setup('t10')
215
+ try {
216
+ writeFileSync(join(dir, 'INDEX.md'), [
217
+ '# Knowledge Index',
218
+ '',
219
+ '## Patterns',
220
+ '- stage → [Stage](patterns.md#stage)',
221
+ ].join('\n'))
222
+
223
+ const result = matchKnowledge(dir, '')
224
+ assert(result.matched === false, 'empty context → matched false')
225
+ } finally { clean(dir) }
226
+ }
227
+
228
+ // ── 汇总 ──
229
+ console.log(`\n${'='.repeat(40)}`)
230
+ console.log(`knowledge-match tests: ${passed} passed, ${failed} failed, ${passed + failed} total`)
231
+ if (failed > 0) process.exit(1)