sillyspec 3.20.2 → 3.20.3
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 +1 -5
- package/.claude/skills/sillyspec-auto/SKILL.md +2 -8
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -28
- package/.claude/skills/sillyspec-commit/SKILL.md +3 -4
- package/.claude/skills/sillyspec-continue/SKILL.md +4 -5
- package/.claude/skills/sillyspec-doctor/SKILL.md +3 -12
- package/.claude/skills/sillyspec-execute/SKILL.md +2 -15
- package/.claude/skills/sillyspec-explore/SKILL.md +1 -14
- package/.claude/skills/sillyspec-plan/SKILL.md +36 -5
- package/.claude/skills/sillyspec-propose/SKILL.md +0 -4
- package/.claude/skills/sillyspec-quick/SKILL.md +1 -5
- package/.claude/skills/sillyspec-resume/SKILL.md +66 -23
- package/.claude/skills/sillyspec-scan/SKILL.md +1 -5
- package/.claude/skills/sillyspec-state/SKILL.md +8 -8
- package/.claude/skills/sillyspec-status/SKILL.md +1 -5
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -5
- package/.claude/skills/sillyspec-workspace/SKILL.md +3 -11
- package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +97 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +51 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +29 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +34 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +13 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +43 -0
- package/.sillyspec/changes/auto-mode/design.md +50 -0
- package/.sillyspec/changes/auto-mode/proposal.md +19 -0
- package/.sillyspec/changes/auto-mode/requirements.md +21 -0
- package/.sillyspec/changes/auto-mode/tasks.md +7 -0
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +206 -0
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +199 -0
- package/.sillyspec/changes/dashboard/design.md +219 -0
- package/.sillyspec/changes/dashboard/design.md.braindraft +206 -0
- package/.sillyspec/changes/run-command-design/design.md +1230 -0
- package/.sillyspec/changes/unified-docs-design/design.md +199 -0
- package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
- package/.sillyspec/knowledge/INDEX.md +8 -0
- package/.sillyspec/knowledge/uncategorized.md +3 -0
- package/.sillyspec/plans/2026-04-05-dashboard.md +737 -0
- package/.sillyspec/projects/sillyspec.yaml +3 -0
- package/README.md +11 -13
- package/SKILL.md +40 -44
- package/dist/steps/brainstorm/01-load-context.md +30 -0
- package/dist/steps/brainstorm/02-reuse-check.md +6 -0
- package/dist/steps/brainstorm/03-prototype-analysis.md +11 -0
- package/dist/steps/brainstorm/04-module-split.md +23 -0
- package/dist/steps/brainstorm/05-dialog-explore.md +8 -0
- package/dist/steps/brainstorm/06-propose-approaches.md +3 -0
- package/dist/steps/brainstorm/07-present-design.md +3 -0
- package/dist/steps/brainstorm/08-write-design.md +21 -0
- package/dist/steps/brainstorm/09-self-review.md +15 -0
- package/dist/steps/brainstorm/10-user-confirm.md +3 -0
- package/dist/steps/brainstorm/11-output-spec.md +7 -0
- package/dist/steps/brainstorm/manifest.yaml +26 -0
- package/dist/steps/execute/01-load-context.md +41 -0
- package/dist/steps/execute/02-scan-conventions.md +47 -0
- package/dist/steps/execute/03-skill-mcp.md +19 -0
- package/dist/steps/execute/04-assign-task.md +22 -0
- package/dist/steps/execute/04b-prompt-template.md +54 -0
- package/dist/steps/execute/05-write-test.md +7 -0
- package/dist/steps/execute/06-write-code.md +8 -0
- package/dist/steps/execute/07-run-test.md +26 -0
- package/dist/steps/execute/08-fix-issues.md +28 -0
- package/dist/steps/execute/09-next-task.md +33 -0
- package/dist/steps/execute/manifest.yaml +28 -0
- package/dist/steps/plan/01-load-context.md +22 -0
- package/dist/steps/plan/02-anchor-confirm.md +1 -0
- package/dist/steps/plan/03-expand-tasks.md +33 -0
- package/dist/steps/plan/04-mark-order.md +15 -0
- package/dist/steps/plan/05-e2e-planning.md +17 -0
- package/dist/steps/plan/06-self-check.md +16 -0
- package/dist/steps/plan/07-save.md +1 -0
- package/dist/steps/plan/manifest.yaml +18 -0
- package/dist/steps/scan/01-env-detect.md +51 -0
- package/dist/steps/scan/02-tech-stack.md +16 -0
- package/dist/steps/scan/03-conventions.md +16 -0
- package/dist/steps/scan/04-structure.md +19 -0
- package/dist/steps/scan/05-quality.md +18 -0
- package/dist/steps/scan/06-complete.md +49 -0
- package/dist/steps/scan/manifest.yaml +16 -0
- package/dist/steps/verify/01-load-specs.md +28 -0
- package/dist/steps/verify/02-check-tasks.md +1 -0
- package/dist/steps/verify/03-check-design.md +6 -0
- package/dist/steps/verify/04-run-tests.md +7 -0
- package/dist/steps/verify/05-e2e-tests.md +27 -0
- package/dist/steps/verify/05b-e2e-fix.md +33 -0
- package/dist/steps/verify/06-code-quality.md +25 -0
- package/dist/steps/verify/07-lint-check.md +27 -0
- package/dist/steps/verify/08-output-report.md +14 -0
- package/dist/steps/verify/manifest.yaml +22 -0
- package/package.json +1 -7
- package/packages/dashboard/dist/assets/{index-Bq_Z2hne.js → index-D1EVTLmc.js} +1264 -1264
- package/packages/dashboard/dist/assets/index-DGe8CqeP.css +1 -0
- package/packages/dashboard/dist/index.html +2 -2
- package/packages/dashboard/package-lock.json +6 -6
- package/packages/dashboard/server/executor.js +1 -1
- package/packages/dashboard/server/index.js +18 -98
- package/packages/dashboard/server/parser.js +71 -139
- package/packages/dashboard/server/watcher.js +6 -14
- package/packages/dashboard/src/App.vue +185 -418
- package/packages/dashboard/src/components/ActionBar.vue +1 -10
- package/packages/dashboard/src/components/CommandPalette.vue +1 -5
- package/packages/dashboard/src/components/DocPreview.vue +8 -105
- package/packages/dashboard/src/components/DocTree.vue +19 -75
- package/packages/dashboard/src/components/PipelineStage.vue +2 -22
- package/packages/dashboard/src/components/PipelineView.vue +5 -32
- package/packages/dashboard/src/components/ProjectOverview.vue +139 -113
- package/packages/dashboard/src/components/StageBadge.vue +3 -17
- package/packages/dashboard/src/components/StepCard.vue +2 -7
- package/packages/dashboard/src/composables/useDashboard.js +0 -28
- package/src/derive.js +147 -0
- package/src/index.js +40 -688
- package/src/init.js +63 -119
- package/src/migrate.js +7 -7
- package/src/progress.js +248 -1474
- package/src/run.js +302 -3008
- package/src/setup.js +64 -2
- package/src/stages/archive.js +17 -123
- package/src/stages/brainstorm.js +48 -454
- package/src/stages/doctor.js +46 -99
- package/src/stages/execute.js +59 -420
- package/src/stages/index.js +18 -12
- package/src/stages/plan.js +189 -492
- package/src/stages/propose.js +11 -70
- package/src/stages/quick.js +13 -52
- package/src/stages/scan.js +68 -485
- package/src/stages/status.js +1 -1
- package/src/stages/verify.js +16 -203
- package/src/step.js +543 -0
- package/.claude/skills/sillyspec-knowledge/SKILL.md +0 -270
- package/.husky/pre-push +0 -13
- package/CLAUDE.md +0 -18
- package/docs/brainstorm-plan-contract.md +0 -64
- package/docs/plan-execute-contract.md +0 -123
- package/docs/platform-scan-protocol.md +0 -298
- package/docs/revision-mode.md +0 -115
- package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
- package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -218
- package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
- package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -193
- package/docs/sillyspec/file-lifecycle.md +0 -125
- package/docs/workflow-contract-regression.md +0 -106
- package/docs/worktree-isolation.md +0 -252
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +0 -1
- package/packages/dashboard/dist/prototype-dashboard.html +0 -836
- package/packages/dashboard/dist/prototype-overview.html +0 -256
- package/packages/dashboard/public/prototype-dashboard.html +0 -836
- package/packages/dashboard/public/prototype-overview.html +0 -256
- package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
- package/packages/dashboard/src/components/ProjectCard.vue +0 -187
- package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
- package/packages/dashboard/src/composables/useLayout.js +0 -131
- package/src/brainstorm-postcheck.js +0 -158
- package/src/change-list.js +0 -52
- package/src/change-risk-profile.js +0 -352
- package/src/classify-change.js +0 -73
- package/src/constants.js +0 -70
- package/src/contract-matrix.js +0 -278
- package/src/db.js +0 -201
- package/src/endpoint-extractor.js +0 -315
- package/src/hooks/claude-pre-tool-use.cjs +0 -125
- package/src/hooks/worktree-guard.js +0 -761
- package/src/knowledge-match.js +0 -130
- package/src/modules.js +0 -482
- package/src/scan-postcheck.js +0 -383
- package/src/stage-contract.js +0 -700
- package/src/stages/brainstorm-auto.js +0 -229
- package/src/stages/explore.js +0 -34
- package/src/stages/knowledge.js +0 -498
- package/src/stages/plan-postcheck.js +0 -513
- package/src/sync.js +0 -497
- package/src/task-review.js +0 -346
- package/src/workflow.js +0 -785
- package/src/worktree-apply.js +0 -549
- package/src/worktree.js +0 -932
- package/templates/workflows/archive-impact.yaml +0 -79
- package/templates/workflows/scan-docs.yaml +0 -132
- package/test/brainstorm-plan-contract.test.mjs +0 -273
- package/test/check-syntax.mjs +0 -26
- package/test/cli-top-level-aliases.test.mjs +0 -174
- package/test/contract-artifacts.test.mjs +0 -323
- package/test/decision-supersede.test.mjs +0 -277
- package/test/knowledge-match.test.mjs +0 -231
- package/test/plan-execute-contract.test.mjs +0 -330
- package/test/plan-optimization.test.mjs +0 -572
- package/test/platform-artifacts.test.mjs +0 -190
- package/test/platform-failure-samples.test.mjs +0 -199
- package/test/platform-recovery-chain.test.mjs +0 -179
- package/test/platform-recovery.test.mjs +0 -167
- package/test/platform-scan-p0.test.mjs +0 -175
- package/test/revision-v1.test.mjs +0 -1145
- package/test/run-sanitize-project-name.test.mjs +0 -51
- package/test/run-scan-postcheck-fail.test.mjs +0 -64
- package/test/run-scan-project-parse.test.mjs +0 -200
- package/test/run-tests.mjs +0 -48
- package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
- package/test/scan-knowledge.test.mjs +0 -175
- package/test/scan-paths.test.mjs +0 -68
- package/test/scan-postcheck-project-priority.test.mjs +0 -85
- package/test/scan-postcheck.test.mjs +0 -197
- package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
- package/test/spec-dir.test.mjs +0 -206
- package/test/stage-contract-failed-post-check.test.mjs +0 -102
- package/test/stage-contract.test.mjs +0 -299
- package/test/stage-definitions.test.mjs +0 -39
- package/test/wait-gates.test.mjs +0 -501
- package/test/workflow-spec-base.test.mjs +0 -142
- package/test/worktree-guard.test.mjs +0 -136
- package/test/worktree-native-overlay.test.mjs +0 -188
package/src/stages/knowledge.js
DELETED
|
@@ -1,498 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* knowledge.js — agent-safe knowledge 管理命令
|
|
3
|
-
*
|
|
4
|
-
* 设计原则:
|
|
5
|
-
* - 所有输出为 JSON(--json 是默认行为,不需要显式传)
|
|
6
|
-
* - agent 可安全调用,不打开编辑器,不直接覆盖人工区
|
|
7
|
-
* - 失败有明确错误码
|
|
8
|
-
*
|
|
9
|
-
* 子命令:
|
|
10
|
-
* search — 关键词搜索知识库
|
|
11
|
-
* inspect — 读取单条知识详情
|
|
12
|
-
* validate — 校验知识库完整性
|
|
13
|
-
* refresh — 从 scan 文档刷新自动知识(仅写 generated/)
|
|
14
|
-
* propose — 提议新知识(写入 proposed/)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, statSync } from 'fs'
|
|
18
|
-
import { join, basename } from 'path'
|
|
19
|
-
import { parseKnowledgeIndex, matchKnowledge } from '../knowledge-match.js'
|
|
20
|
-
|
|
21
|
-
// ── 工具函数 ──
|
|
22
|
-
|
|
23
|
-
function resolveKnowledgeDir(dir, specDir) {
|
|
24
|
-
const base = specDir || join(dir, '.sillyspec')
|
|
25
|
-
return join(base, 'knowledge')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function output(ok, data, error) {
|
|
29
|
-
const result = { ok, ...data }
|
|
30
|
-
if (error) result.error = error
|
|
31
|
-
console.log(JSON.stringify(result, null, 2))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 从文件路径生成知识 ID
|
|
36
|
-
* knowledge/conventions.md → conventions
|
|
37
|
-
* knowledge/generated/patterns.md → generated/patterns
|
|
38
|
-
*/
|
|
39
|
-
function pathToId(basePath, knowledgeDir) {
|
|
40
|
-
let rel = basePath
|
|
41
|
-
if (rel.startsWith(knowledgeDir + '/')) rel = rel.slice(knowledgeDir.length + 1)
|
|
42
|
-
if (rel.endsWith('.md')) rel = rel.slice(0, -3)
|
|
43
|
-
return rel
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 从知识条目提取摘要(取第一段非空非标题文本)
|
|
48
|
-
*/
|
|
49
|
-
function extractSummary(content, maxLen = 120) {
|
|
50
|
-
const lines = content.split('\n')
|
|
51
|
-
for (const line of lines) {
|
|
52
|
-
const trimmed = line.trim()
|
|
53
|
-
if (!trimmed) continue
|
|
54
|
-
if (trimmed.startsWith('#')) continue
|
|
55
|
-
if (trimmed.startsWith('---')) continue
|
|
56
|
-
if (trimmed.startsWith('author:') || trimmed.startsWith('created_at:')) continue
|
|
57
|
-
if (trimmed.startsWith('>')) continue
|
|
58
|
-
if (trimmed.startsWith('<!--')) continue
|
|
59
|
-
return trimmed.slice(0, maxLen) + (trimmed.length > maxLen ? '...' : '')
|
|
60
|
-
}
|
|
61
|
-
return ''
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* 提取文件标题(第一个 # 标题)
|
|
66
|
-
*/
|
|
67
|
-
function extractTitle(content) {
|
|
68
|
-
const match = content.match(/^#\s+(.+)$/m)
|
|
69
|
-
return match ? match[1].trim() : ''
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── search ──
|
|
73
|
-
|
|
74
|
-
export async function cmdSearch(dir, args, opts = {}) {
|
|
75
|
-
const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
|
|
76
|
-
const queryIdx = args.indexOf('--query')
|
|
77
|
-
const query = queryIdx >= 0 && args[queryIdx + 1] ? args[queryIdx + 1] : ''
|
|
78
|
-
const limitIdx = args.indexOf('--limit')
|
|
79
|
-
const limit = limitIdx >= 0 && args[limitIdx + 1] ? parseInt(args[limitIdx + 1]) : 10
|
|
80
|
-
|
|
81
|
-
if (!query) {
|
|
82
|
-
output(false, {}, '--query is required')
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!existsSync(knowledgeDir)) {
|
|
87
|
-
output(false, {}, { code: 'knowledge_dir_missing', path: knowledgeDir })
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 复用 knowledge-match 引擎
|
|
92
|
-
const result = matchKnowledge(knowledgeDir, query)
|
|
93
|
-
|
|
94
|
-
if (!result.matched) {
|
|
95
|
-
output(true, { query, matches: [] })
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 增强:为每个匹配项补充 path、title、summary、score
|
|
100
|
-
const matches = result.entries.slice(0, limit).map(entry => {
|
|
101
|
-
const filePath = join(knowledgeDir, entry.file)
|
|
102
|
-
let title = entry.display
|
|
103
|
-
let summary = ''
|
|
104
|
-
let score = entry.keywords.length // 简单评分:匹配关键词数量
|
|
105
|
-
|
|
106
|
-
if (existsSync(filePath)) {
|
|
107
|
-
const content = readFileSync(filePath, 'utf8')
|
|
108
|
-
title = extractTitle(content) || entry.display
|
|
109
|
-
summary = extractSummary(content)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
id: pathToId(entry.file, knowledgeDir),
|
|
114
|
-
path: join('knowledge', entry.file),
|
|
115
|
-
title,
|
|
116
|
-
summary,
|
|
117
|
-
score,
|
|
118
|
-
tags: entry.keywords,
|
|
119
|
-
category: entry.category,
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
output(true, { query, matches })
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ── inspect ──
|
|
127
|
-
|
|
128
|
-
export async function cmdInspect(dir, args, opts = {}) {
|
|
129
|
-
const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
|
|
130
|
-
const idIdx = args.indexOf('--id')
|
|
131
|
-
const id = idIdx >= 0 && args[idIdx + 1] ? args[idIdx + 1] : ''
|
|
132
|
-
|
|
133
|
-
if (!id) {
|
|
134
|
-
output(false, {}, '--id is required')
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!existsSync(knowledgeDir)) {
|
|
139
|
-
output(false, {}, { code: 'knowledge_dir_missing', path: knowledgeDir })
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ID → 文件路径:conventions → knowledge/conventions.md
|
|
144
|
-
// generated/patterns → knowledge/generated/patterns.md
|
|
145
|
-
const filePath = join(knowledgeDir, id + '.md')
|
|
146
|
-
|
|
147
|
-
if (!existsSync(filePath)) {
|
|
148
|
-
output(false, {}, { code: 'not_found', id, path: filePath })
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const content = readFileSync(filePath, 'utf8')
|
|
153
|
-
const title = extractTitle(content)
|
|
154
|
-
const summary = extractSummary(content)
|
|
155
|
-
|
|
156
|
-
// 元数据
|
|
157
|
-
const meta = {}
|
|
158
|
-
const authorMatch = content.match(/^author:\s*(.+)$/m)
|
|
159
|
-
const createdAtMatch = content.match(/^created_at:\s*(.+)$/m)
|
|
160
|
-
if (authorMatch) meta.author = authorMatch[1].trim()
|
|
161
|
-
if (createdAtMatch) meta.created_at = createdAtMatch[1].trim()
|
|
162
|
-
|
|
163
|
-
// 文件修改时间
|
|
164
|
-
const stat = statSync(filePath)
|
|
165
|
-
meta.updated_at = stat.mtime.toISOString()
|
|
166
|
-
|
|
167
|
-
// 分区检测
|
|
168
|
-
let zone = 'manual'
|
|
169
|
-
if (id.startsWith('generated/')) zone = 'generated'
|
|
170
|
-
else if (id.startsWith('proposed/')) zone = 'proposed'
|
|
171
|
-
|
|
172
|
-
output(true, {
|
|
173
|
-
entry: {
|
|
174
|
-
id,
|
|
175
|
-
title,
|
|
176
|
-
summary,
|
|
177
|
-
zone,
|
|
178
|
-
path: join('knowledge', id + '.md'),
|
|
179
|
-
meta,
|
|
180
|
-
body: content,
|
|
181
|
-
}
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ── validate ──
|
|
186
|
-
|
|
187
|
-
export async function cmdValidate(dir, args, opts = {}) {
|
|
188
|
-
const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
|
|
189
|
-
const errors = []
|
|
190
|
-
const warnings = []
|
|
191
|
-
|
|
192
|
-
if (!existsSync(knowledgeDir)) {
|
|
193
|
-
output(false, { errors, warnings }, { code: 'knowledge_dir_missing', path: knowledgeDir })
|
|
194
|
-
return
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 1. INDEX.md 存在
|
|
198
|
-
const indexPath = join(knowledgeDir, 'INDEX.md')
|
|
199
|
-
if (!existsSync(indexPath)) {
|
|
200
|
-
errors.push({ code: 'missing_index', path: 'knowledge/INDEX.md' })
|
|
201
|
-
} else {
|
|
202
|
-
// 2. INDEX.md 引用的文件是否存在
|
|
203
|
-
const entries = parseKnowledgeIndex(knowledgeDir)
|
|
204
|
-
for (const entry of entries) {
|
|
205
|
-
const refPath = join(knowledgeDir, entry.file)
|
|
206
|
-
if (!existsSync(refPath)) {
|
|
207
|
-
errors.push({
|
|
208
|
-
code: 'broken_reference',
|
|
209
|
-
path: join('knowledge', entry.file),
|
|
210
|
-
referenced_in: 'INDEX.md',
|
|
211
|
-
display: entry.display,
|
|
212
|
-
})
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// 3. 扫描所有 .md 文件,检查是否在 INDEX 中注册
|
|
218
|
-
const allMdFiles = scanMdFiles(knowledgeDir, '')
|
|
219
|
-
const indexedFiles = new Set(
|
|
220
|
-
parseKnowledgeIndex(knowledgeDir).map(e => e.file)
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
for (const mdFile of allMdFiles) {
|
|
224
|
-
if (mdFile === 'INDEX.md') continue
|
|
225
|
-
// generated/ 和 proposed/ 区不强制要求 INDEX 注册
|
|
226
|
-
if (mdFile.startsWith('generated/') || mdFile.startsWith('proposed/')) continue
|
|
227
|
-
if (!indexedFiles.has(mdFile)) {
|
|
228
|
-
warnings.push({
|
|
229
|
-
code: 'unregistered_file',
|
|
230
|
-
path: join('knowledge', mdFile),
|
|
231
|
-
})
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// 4. uncategorized.md 条目数(多了提示需清理)
|
|
236
|
-
const uncategorizedPath = join(knowledgeDir, 'uncategorized.md')
|
|
237
|
-
if (existsSync(uncategorizedPath)) {
|
|
238
|
-
const content = readFileSync(uncategorizedPath, 'utf8')
|
|
239
|
-
const entryCount = (content.match(/^###\s+/gm) || []).length
|
|
240
|
-
if (entryCount >= 10) {
|
|
241
|
-
warnings.push({
|
|
242
|
-
code: 'too_many_uncategorized',
|
|
243
|
-
count: entryCount,
|
|
244
|
-
path: 'knowledge/uncategorized.md',
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// 5. 空文件检测
|
|
250
|
-
for (const mdFile of allMdFiles) {
|
|
251
|
-
if (mdFile === 'INDEX.md') continue
|
|
252
|
-
const fullPath = join(knowledgeDir, mdFile)
|
|
253
|
-
const content = readFileSync(fullPath, 'utf8')
|
|
254
|
-
if (content.trim().length === 0) {
|
|
255
|
-
errors.push({
|
|
256
|
-
code: 'empty_file',
|
|
257
|
-
path: join('knowledge', mdFile),
|
|
258
|
-
})
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
output(errors.length === 0, { errors, warnings })
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ── refresh ──
|
|
266
|
-
|
|
267
|
-
export async function cmdRefresh(dir, args, opts = {}) {
|
|
268
|
-
const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
|
|
269
|
-
const generatedDir = join(knowledgeDir, 'generated')
|
|
270
|
-
|
|
271
|
-
if (!existsSync(knowledgeDir)) {
|
|
272
|
-
output(false, {}, { code: 'knowledge_dir_missing', path: knowledgeDir })
|
|
273
|
-
return
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// refresh 只写 generated/ 区
|
|
277
|
-
mkdirSync(generatedDir, { recursive: true })
|
|
278
|
-
|
|
279
|
-
// 扫描 scan 文档
|
|
280
|
-
const base = opts.specDir || join(dir, '.sillyspec')
|
|
281
|
-
const projectName = basename(dir)
|
|
282
|
-
const scanDir = join(base, 'docs', projectName, 'scan')
|
|
283
|
-
|
|
284
|
-
if (!existsSync(scanDir)) {
|
|
285
|
-
output(false, {}, { code: 'scan_docs_missing', path: scanDir })
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const scanFiles = readdirSync(scanDir).filter(f => f.endsWith('.md'))
|
|
290
|
-
const generated = []
|
|
291
|
-
const overwritten = []
|
|
292
|
-
|
|
293
|
-
for (const scanFile of scanFiles) {
|
|
294
|
-
const scanPath = join(scanDir, scanFile)
|
|
295
|
-
const content = readFileSync(scanPath, 'utf8')
|
|
296
|
-
const title = extractTitle(content) || scanFile.replace('.md', '')
|
|
297
|
-
|
|
298
|
-
// 简单提取:取每个 ## 章节作为一个知识条目候选
|
|
299
|
-
const sections = extractSections(content)
|
|
300
|
-
for (const section of sections) {
|
|
301
|
-
if (section.lines < 3) continue // 太短的章节跳过
|
|
302
|
-
|
|
303
|
-
const slug = section.heading.toLowerCase()
|
|
304
|
-
.replace(/[^\w\u4e00-\u9fff]+/g, '-')
|
|
305
|
-
.replace(/^-|-$/g, '')
|
|
306
|
-
.slice(0, 60)
|
|
307
|
-
|
|
308
|
-
const fileName = `${slug}.md`
|
|
309
|
-
const genPath = join(generatedDir, fileName)
|
|
310
|
-
const isNew = !existsSync(genPath)
|
|
311
|
-
|
|
312
|
-
const entryContent = [
|
|
313
|
-
`---`,
|
|
314
|
-
`source: scan/${scanFile}`,
|
|
315
|
-
`section: ${section.heading}`,
|
|
316
|
-
`generated_at: ${new Date().toISOString()}`,
|
|
317
|
-
`---`,
|
|
318
|
-
``,
|
|
319
|
-
`# ${title} — ${section.heading}`,
|
|
320
|
-
``,
|
|
321
|
-
section.body,
|
|
322
|
-
].join('\n')
|
|
323
|
-
|
|
324
|
-
writeFileSync(genPath, entryContent)
|
|
325
|
-
generated.push({ file: `generated/${fileName}`, title: `${title} — ${section.heading}`, new: isNew })
|
|
326
|
-
if (!isNew) overwritten.push(`generated/${fileName}`)
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// 更新 generated/INDEX.md
|
|
331
|
-
const genIndexPath = join(generatedDir, 'INDEX.md')
|
|
332
|
-
const indexLines = [
|
|
333
|
-
'---',
|
|
334
|
-
`generated_at: ${new Date().toISOString()}`,
|
|
335
|
-
'---',
|
|
336
|
-
'',
|
|
337
|
-
'# Generated Knowledge Index',
|
|
338
|
-
'',
|
|
339
|
-
'> Auto-generated from scan documents. Do not edit manually.',
|
|
340
|
-
'',
|
|
341
|
-
]
|
|
342
|
-
for (const g of generated) {
|
|
343
|
-
indexLines.push(`- ${g.title} → [${g.file}](${g.file})`)
|
|
344
|
-
}
|
|
345
|
-
writeFileSync(genIndexPath, indexLines.join('\n'))
|
|
346
|
-
|
|
347
|
-
output(true, {
|
|
348
|
-
generated_count: generated.length,
|
|
349
|
-
new_count: generated.filter(g => g.new).length,
|
|
350
|
-
overwritten_count: overwritten.length,
|
|
351
|
-
files: generated,
|
|
352
|
-
})
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// ── propose ──
|
|
356
|
-
|
|
357
|
-
export async function cmdPropose(dir, args, opts = {}) {
|
|
358
|
-
const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
|
|
359
|
-
const proposedDir = join(knowledgeDir, 'proposed')
|
|
360
|
-
|
|
361
|
-
const titleIdx = args.indexOf('--title')
|
|
362
|
-
const title = titleIdx >= 0 && args[titleIdx + 1] ? args[titleIdx + 1] : ''
|
|
363
|
-
const categoryIdx = args.indexOf('--category')
|
|
364
|
-
const category = categoryIdx >= 0 && args[categoryIdx + 1] ? args[categoryIdx + 1] : 'uncategorized'
|
|
365
|
-
const bodyIdx = args.indexOf('--body')
|
|
366
|
-
const body = bodyIdx >= 0 && args[bodyIdx + 1] ? args[bodyIdx + 1] : ''
|
|
367
|
-
const fromIdx = args.indexOf('--from')
|
|
368
|
-
const from = fromIdx >= 0 && args[fromIdx + 1] ? args[fromIdx + 1] : ''
|
|
369
|
-
|
|
370
|
-
if (!title) {
|
|
371
|
-
output(false, {}, '--title is required')
|
|
372
|
-
return
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
mkdirSync(proposedDir, { recursive: true })
|
|
376
|
-
|
|
377
|
-
// 生成 slug
|
|
378
|
-
const slug = title.toLowerCase()
|
|
379
|
-
.replace(/[^\w\u4e00-\u9fff]+/g, '-')
|
|
380
|
-
.replace(/^-|-$/g, '')
|
|
381
|
-
.slice(0, 60)
|
|
382
|
-
|
|
383
|
-
const fileName = `${slug}.md`
|
|
384
|
-
const propPath = join(proposedDir, fileName)
|
|
385
|
-
const isNew = !existsSync(propPath)
|
|
386
|
-
|
|
387
|
-
const content = [
|
|
388
|
-
'---',
|
|
389
|
-
`proposed_at: ${new Date().toISOString()}`,
|
|
390
|
-
`category: ${category}`,
|
|
391
|
-
...(from ? [`source: ${from}`] : []),
|
|
392
|
-
'status: pending_review',
|
|
393
|
-
'---',
|
|
394
|
-
'',
|
|
395
|
-
`# ${title}`,
|
|
396
|
-
'',
|
|
397
|
-
body || '(no body provided)',
|
|
398
|
-
'',
|
|
399
|
-
'---',
|
|
400
|
-
'> This is a proposed knowledge entry. Review and merge into manual/ or generated/.',
|
|
401
|
-
].join('\n')
|
|
402
|
-
|
|
403
|
-
writeFileSync(propPath, content)
|
|
404
|
-
|
|
405
|
-
output(true, {
|
|
406
|
-
id: `proposed/${slug}`,
|
|
407
|
-
path: `knowledge/proposed/${fileName}`,
|
|
408
|
-
title,
|
|
409
|
-
category,
|
|
410
|
-
new: isNew,
|
|
411
|
-
action: isNew ? 'created' : 'updated',
|
|
412
|
-
})
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// ── 辅助函数 ──
|
|
416
|
-
|
|
417
|
-
function scanMdFiles(baseDir, prefix) {
|
|
418
|
-
const results = []
|
|
419
|
-
const items = readdirSync(baseDir, { withFileTypes: true })
|
|
420
|
-
for (const item of items) {
|
|
421
|
-
const rel = prefix ? `${prefix}/${item.name}` : item.name
|
|
422
|
-
if (item.isDirectory()) {
|
|
423
|
-
results.push(...scanMdFiles(join(baseDir, item.name), rel))
|
|
424
|
-
} else if (item.name.endsWith('.md')) {
|
|
425
|
-
results.push(rel)
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
return results
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function extractSections(content) {
|
|
432
|
-
const lines = content.split('\n')
|
|
433
|
-
const sections = []
|
|
434
|
-
let currentHeading = null
|
|
435
|
-
let currentLines = []
|
|
436
|
-
let currentBody = []
|
|
437
|
-
|
|
438
|
-
for (const line of lines) {
|
|
439
|
-
if (line.startsWith('## ')) {
|
|
440
|
-
if (currentHeading) {
|
|
441
|
-
sections.push({
|
|
442
|
-
heading: currentHeading,
|
|
443
|
-
body: currentBody.join('\n').trim(),
|
|
444
|
-
lines: currentLines.length,
|
|
445
|
-
})
|
|
446
|
-
}
|
|
447
|
-
currentHeading = line.slice(3).trim()
|
|
448
|
-
currentLines = [line]
|
|
449
|
-
currentBody = []
|
|
450
|
-
} else if (currentHeading) {
|
|
451
|
-
currentLines.push(line)
|
|
452
|
-
// 跳过 frontmatter 和标题行
|
|
453
|
-
if (!line.startsWith('---') && !line.startsWith('#')) {
|
|
454
|
-
currentBody.push(line)
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (currentHeading) {
|
|
460
|
-
sections.push({
|
|
461
|
-
heading: currentHeading,
|
|
462
|
-
body: currentBody.join('\n').trim(),
|
|
463
|
-
lines: currentLines.length,
|
|
464
|
-
})
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return sections.filter(s => s.lines >= 3)
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// ── 入口路由 ──
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* @param {string[]} args - filteredArgs.slice(1)(去掉 'knowledge')
|
|
474
|
-
* @param {string} dir - 项目根目录
|
|
475
|
-
* @param {object} opts - { specDir }
|
|
476
|
-
*/
|
|
477
|
-
export async function cmdKnowledge(args, dir, opts = {}) {
|
|
478
|
-
const subCommand = args[0] || ''
|
|
479
|
-
|
|
480
|
-
switch (subCommand) {
|
|
481
|
-
case 'search':
|
|
482
|
-
return cmdSearch(dir, args.slice(1), opts)
|
|
483
|
-
case 'inspect':
|
|
484
|
-
return cmdInspect(dir, args.slice(1), opts)
|
|
485
|
-
case 'validate':
|
|
486
|
-
return cmdValidate(dir, args.slice(1), opts)
|
|
487
|
-
case 'refresh':
|
|
488
|
-
return cmdRefresh(dir, args.slice(1), opts)
|
|
489
|
-
case 'propose':
|
|
490
|
-
return cmdPropose(dir, args.slice(1), opts)
|
|
491
|
-
default:
|
|
492
|
-
output(false, {}, {
|
|
493
|
-
code: 'unknown_subcommand',
|
|
494
|
-
subcommand: subCommand,
|
|
495
|
-
available: ['search', 'inspect', 'validate', 'refresh', 'propose'],
|
|
496
|
-
})
|
|
497
|
-
}
|
|
498
|
-
}
|