sillyspec 3.28.8 → 3.28.10
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/full.json +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +62 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +89 -20
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +206 -18
- package/src/knowledge-classify.js +382 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/probe7-anchor-check.js +69 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +164 -18
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +131 -13
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +123 -23
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gate-snapshot.js +110 -2
- package/src/run/gates.js +141 -16
- package/src/run/prompt.js +221 -11
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +147 -11
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +456 -56
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +76 -6
- package/src/stages/plan-postcheck.js +32 -5
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +170 -6
- package/src/sync.js +141 -9
- package/src/task-review.js +366 -46
- package/src/verify-postcheck.js +212 -55
- package/src/verify-probes.js +727 -16
- package/src/worktree-apply.js +685 -48
- package/src/worktree-deps.js +124 -3
- package/src/worktree.js +391 -15
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- package/sa-rename-probe/src/new.js +0 -4
package/src/stage-contract.js
CHANGED
|
@@ -123,7 +123,12 @@ function buildDecisionRecord(id, body) {
|
|
|
123
123
|
const supersedes = supersedesRaw
|
|
124
124
|
? supersedesRaw.split(',').map(s => s.trim().toUpperCase().replace(/['"]/g, '')).filter(Boolean)
|
|
125
125
|
: []
|
|
126
|
-
|
|
126
|
+
// 故障面/退役判据(FR-01/D-001@v1,2026-09-15-tax-governance):自维护税治理字段,
|
|
127
|
+
// 软警告素材(warnMissingGovernanceFields)——type 缺省为 ''(非 architecture 不警告)
|
|
128
|
+
const type = readDecisionField(body, 'type').toLowerCase()
|
|
129
|
+
const failureMode = readDecisionField(body, '故障面')
|
|
130
|
+
const retireWhen = readDecisionField(body, '退役判据')
|
|
131
|
+
return { id: id.toUpperCase(), body, status, priority, blocker, priorityMissing, supersedes, type, failureMode, retireWhen }
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
function findNextDecisionBoundary(content, startIndex) {
|
|
@@ -233,6 +238,22 @@ function warnMissingIds(warnings, ids, targetContent, targetName, sourceName) {
|
|
|
233
238
|
}
|
|
234
239
|
}
|
|
235
240
|
|
|
241
|
+
/**
|
|
242
|
+
* 故障面/退役判据软警告(FR-01/D-001@v1,change: 2026-09-15-tax-governance):
|
|
243
|
+
* type=architecture 且 status=accepted 的条目缺任一字段 → warnings.push(不阻断)。
|
|
244
|
+
* 机制落地时被迫留痕「它引入什么失败模式、什么信号出现就该简化它」;definition/boundary
|
|
245
|
+
* 等其他类型不要求。扫描无法区分新旧条目(design R-03)——文案标注存量可忽略,
|
|
246
|
+
* 软警告不升 error(一个观测周期后再评估棘轮升级,D-001 退役判据锚定)。
|
|
247
|
+
*/
|
|
248
|
+
function warnMissingGovernanceFields(warnings, decisionsContent) {
|
|
249
|
+
if (!decisionsContent) return
|
|
250
|
+
for (const r of parseDecisionRecords(decisionsContent)) {
|
|
251
|
+
if (r.type !== 'architecture' || r.status !== 'accepted') continue
|
|
252
|
+
if (r.failureMode && r.retireWhen) continue
|
|
253
|
+
warnings.push(`decisions.md ${r.id}(architecture)缺「故障面/退役判据」——新决策建议补齐(存量条目可忽略)`)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
236
257
|
/**
|
|
237
258
|
* scan 完成校验:检查 7 份 scan 文档 + manifest
|
|
238
259
|
*/
|
|
@@ -356,6 +377,9 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
|
356
377
|
// tasks(骨架,待 plan 展开)不强求逐条引用每个架构决策,否则批量误报。
|
|
357
378
|
warnMissingIds(warnings, decisionIds, design, 'design.md', 'decisions.md')
|
|
358
379
|
}
|
|
380
|
+
// 故障面/退役判据软警告(FR-01,2026-09-15-tax-governance):architecture+accepted 缺字段
|
|
381
|
+
// → warning 不阻断(经 gates warnings 通道打印,gates.js 零改动)
|
|
382
|
+
warnMissingGovernanceFields(warnings, decisions)
|
|
359
383
|
}
|
|
360
384
|
|
|
361
385
|
return { ok: errors.length === 0, errors, warnings }
|
|
@@ -397,6 +421,8 @@ function validatePlanOutputs(cwd, changeName, context = {}) {
|
|
|
397
421
|
}
|
|
398
422
|
const decisionIds = extractCurrentDecisionIds(decisions)
|
|
399
423
|
warnMissingIds(warnings, decisionIds, plan, 'plan.md', 'decisions.md')
|
|
424
|
+
// 故障面/退役判据软警告(FR-01,2026-09-15-tax-governance):brainstorm 侧同款(软警告不阻断)
|
|
425
|
+
warnMissingGovernanceFields(warnings, decisions)
|
|
400
426
|
}
|
|
401
427
|
// ── P0: 生产接线路径检查:design 提到入口但 task 的 allowed_paths 不含入口文件 ──
|
|
402
428
|
// entry-point-wiring(custom):trigger/file 抽取/exemption/failMessage 从 manifest 同源
|
|
@@ -689,6 +715,161 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
689
715
|
return { ok: errors.length === 0, errors, warnings }
|
|
690
716
|
}
|
|
691
717
|
|
|
718
|
+
// ============ 探针 7 验收×测试覆盖矩阵门禁(2026-09-14-acceptance-test-matrix FR-02) ============
|
|
719
|
+
// 段格式与 src/verify-probes.js renderProbe7Lines 骨架渲染字面同源(禁第二套解析文法):
|
|
720
|
+
// 段标题「#### 探针 7:验收×测试覆盖矩阵」(全/半角冒号皆认);每 task 前 **task-NN** 锚行;
|
|
721
|
+
// 五列表 | acceptance 条目 | 归属测试文件 | 关键词命中 | 判定 | 证据 |;判定槽 <待填:四选一>、
|
|
722
|
+
// 证据槽 <TODO>;列表防御行(卡无 acceptance…)与不适用行无槽不计。
|
|
723
|
+
|
|
724
|
+
/** 判定列四枚举白名单(骨架口径注记字面同源) */
|
|
725
|
+
const MATRIX_VERDICT_WHITELIST = new Set(['covered', 'partial', 'uncovered', 'non-testable'])
|
|
726
|
+
/** 证据列未填占位(骨架字面同源) */
|
|
727
|
+
const MATRIX_EVIDENCE_TODO = '<TODO>'
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* 切分 markdown 表格行:按未转义管道切列(`\|` 是单元格内字面量,切列须忽略),
|
|
731
|
+
* 行首管道消费、行尾有管道则末段为收尾空段不产列。非 | 起始行返回 null。
|
|
732
|
+
* @returns {string[]|null} 单元格数组(未 trim);null = 非表行
|
|
733
|
+
*/
|
|
734
|
+
function splitMatrixRowCells(line) {
|
|
735
|
+
const s = String(line).trim()
|
|
736
|
+
if (!s.startsWith('|')) return null
|
|
737
|
+
const cells = []
|
|
738
|
+
let cur = ''
|
|
739
|
+
for (let i = 1; i < s.length; i++) {
|
|
740
|
+
const ch = s[i]
|
|
741
|
+
if (ch === '\\' && s[i + 1] === '|') { cur += '|'; i++; continue }
|
|
742
|
+
if (ch === '|') { cells.push(cur); cur = ''; continue }
|
|
743
|
+
cur += ch
|
|
744
|
+
}
|
|
745
|
+
if (!s.endsWith('|')) cells.push(cur) // 行尾缺管道的宽容形态:尾段仍是单元格
|
|
746
|
+
return cells
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* covered/partial 行证据是否含测试锚点形态之一:`.test.` 文件名 / file:line(x.js:123)/
|
|
751
|
+
* 反引号包裹的标识符(骨架归属列即反引号路径,agent 引用复制即命中)。
|
|
752
|
+
*/
|
|
753
|
+
function matrixEvidenceHasAnchor(evidence) {
|
|
754
|
+
const e = String(evidence || '')
|
|
755
|
+
if (e.includes('.test.')) return true
|
|
756
|
+
if (/[A-Za-z0-9_\-./\\]+\.[A-Za-z0-9]+:\d+/.test(e)) return true
|
|
757
|
+
if (/`[^`]+`/.test(e)) return true
|
|
758
|
+
return false
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* 行级证据口径:covered/partial 须非 TODO 且含测试锚点;non-testable 须非 TODO 且非空
|
|
763
|
+
* (一句话理由);uncovered 无证据要求;判定未填(不在三枚举内)的行只计 unfilled 不重复计证据。
|
|
764
|
+
*/
|
|
765
|
+
function matrixEvidenceMissing(verdict, evidence) {
|
|
766
|
+
if (verdict !== 'covered' && verdict !== 'partial' && verdict !== 'non-testable') return false
|
|
767
|
+
const e = String(evidence || '').trim()
|
|
768
|
+
if (e === '' || e === MATRIX_EVIDENCE_TODO || e.startsWith('<待填')) return true
|
|
769
|
+
if (verdict === 'non-testable') return false // 非 TODO 且非空即合规(理由一句话)
|
|
770
|
+
return !matrixEvidenceHasAnchor(e)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* 行级提取 verify-result.md「#### 探针 7:验收×测试覆盖矩阵」段的判定/证据槽(纯字符串解析)。
|
|
775
|
+
* 段定位:探针 7 标题(全/半角冒号)到下个同级(####)或更高级(#~###)标题;##### 属段内。
|
|
776
|
+
* 只认五列表行(按忽略 \| 转义切列);表头/分隔行、**task-NN** 锚行、列表防御行与
|
|
777
|
+
* 「不适用(无 TaskCard)」行不计槽。
|
|
778
|
+
* @param {string} verifyMd - verify-result.md 全文
|
|
779
|
+
* @returns {{
|
|
780
|
+
* present: boolean,
|
|
781
|
+
* rows: Array<{task: string, acceptance: string, verdict: string, evidence: string, unfilled?: boolean, evidenceMissing?: boolean}>,
|
|
782
|
+
* unfilled: number,
|
|
783
|
+
* missingEvidence: number
|
|
784
|
+
* }} 无段时 present=false、rows=[]、计数 0/0(调用方区分「无段」与「段在而未填」)
|
|
785
|
+
*/
|
|
786
|
+
export function extractAcceptanceMatrixSlots(verifyMd) {
|
|
787
|
+
const md = String(verifyMd || '')
|
|
788
|
+
const out = { present: false, rows: [], unfilled: 0, missingEvidence: 0 }
|
|
789
|
+
const hm = md.match(/^#### 探针 7[::]/m)
|
|
790
|
+
if (!hm) return out
|
|
791
|
+
out.present = true
|
|
792
|
+
const rest = md.slice(hm.index + hm[0].length)
|
|
793
|
+
const endMatch = rest.match(/^#{1,4}(?:[ \t]|$)/m)
|
|
794
|
+
const sectionText = endMatch ? rest.slice(0, endMatch.index) : rest
|
|
795
|
+
|
|
796
|
+
let currentTask = ''
|
|
797
|
+
for (const rawLine of sectionText.split(/\r?\n/)) {
|
|
798
|
+
const anchor = rawLine.match(/^\*\*\s*(task-[A-Za-z0-9._-]+)\s*\*\*/)
|
|
799
|
+
if (anchor) { currentTask = anchor[1]; continue }
|
|
800
|
+
const cells = splitMatrixRowCells(rawLine)
|
|
801
|
+
if (!cells || cells.length !== 5) continue // 列表防御行/不适用行/非五列表行不计槽
|
|
802
|
+
const c = cells.map(x => x.trim())
|
|
803
|
+
if (c.every(x => /^:?-{3,}:?$/.test(x))) continue // 分隔行 |---|---|…|
|
|
804
|
+
if (c[3] === '判定' && c[4] === '证据') continue // 表头行
|
|
805
|
+
const verdict = c[3]
|
|
806
|
+
const evidence = c[4]
|
|
807
|
+
const row = { task: currentTask, acceptance: c[0], verdict, evidence }
|
|
808
|
+
if (!MATRIX_VERDICT_WHITELIST.has(verdict)) { row.unfilled = true; out.unfilled++ }
|
|
809
|
+
if (matrixEvidenceMissing(verdict, evidence)) { row.evidenceMissing = true; out.missingEvidence++ }
|
|
810
|
+
out.rows.push(row)
|
|
811
|
+
}
|
|
812
|
+
return out
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* 探针 7 矩阵门禁 validator(注册 contracts.verify.validators,走 runValidators errors
|
|
817
|
+
* 阻断链——勿进 gates.js fail-soft 回填块,catch 只 warn 会吞阻断):
|
|
818
|
+
* - 无 tasks/ 目录(quick 会话/旧变更,无 TaskCard)→ no-op 零行为变化
|
|
819
|
+
* - verify-result.md 未落盘(verify 中间步骤)→ no-op:存在性归引擎 manifest,此处只读已落盘内容
|
|
820
|
+
* - 段缺失:严格档(isIrStrictVerifyChange / IR_STRICT_SINCE 同源常量,与结论槽
|
|
821
|
+
* 「删槽回退已关闭」同口径)ERROR;非严格(存量变更)warning 提示补段不阻断
|
|
822
|
+
* - 段在场:unfilled / missingEvidence > 0 → ERROR 列出违规行(task+acceptance 截断 40 字)
|
|
823
|
+
*/
|
|
824
|
+
function validateAcceptanceMatrix(cwd, changeName, context = {}) {
|
|
825
|
+
const { specRoot } = context
|
|
826
|
+
const errors = []
|
|
827
|
+
const warnings = []
|
|
828
|
+
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
829
|
+
|
|
830
|
+
// 无 TaskCard(tasks/ 不存在)→ 不校验(设计兼容策略:brownfield 零行为变化)
|
|
831
|
+
if (!existsSync(join(changeDir, 'tasks'))) return { ok: true, errors, warnings }
|
|
832
|
+
// verify-result.md 中间步骤未落盘 → 不校验(末步引擎存在性规则兜底)
|
|
833
|
+
const verifyResultPath = join(changeDir, 'verify-result.md')
|
|
834
|
+
if (!existsSync(verifyResultPath)) return { ok: true, errors, warnings }
|
|
835
|
+
|
|
836
|
+
const matrix = extractAcceptanceMatrixSlots(readFileSync(verifyResultPath, 'utf8'))
|
|
837
|
+
if (!matrix.present) {
|
|
838
|
+
if (isIrStrictVerifyChange(changeDir)) {
|
|
839
|
+
errors.push(
|
|
840
|
+
`探针 7 矩阵段缺失(有 TaskCard 且严格档,created_at ≥ ${IR_STRICT_SINCE})——` +
|
|
841
|
+
`verify-result.md 缺「#### 探针 7:验收×测试覆盖矩阵」段。` +
|
|
842
|
+
`修复:跑 \`sillyspec verify-probes --change ${changeName} --init\` 幂等补段(已有正文不覆盖),再逐行填判定与证据。`
|
|
843
|
+
)
|
|
844
|
+
} else {
|
|
845
|
+
warnings.push(
|
|
846
|
+
`verify-result.md 缺探针 7 验收×测试覆盖矩阵段(非严格档,存量变更不强制)——` +
|
|
847
|
+
`可跑 \`sillyspec verify-probes --change ${changeName} --init\` 幂等补段。`
|
|
848
|
+
)
|
|
849
|
+
}
|
|
850
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const clip = (s) => { const t = String(s || ''); return t.length > 40 ? t.slice(0, 40) + '…' : t }
|
|
854
|
+
const rowLabel = (r) => `${r.task || '(未知 task)'}|${clip(r.acceptance)}`
|
|
855
|
+
if (matrix.unfilled > 0) {
|
|
856
|
+
const list = matrix.rows.filter(r => r.unfilled).map(rowLabel).join(';')
|
|
857
|
+
errors.push(
|
|
858
|
+
`探针 7 验收×测试覆盖矩阵有 ${matrix.unfilled} 行判定未填(四选一 covered/partial/uncovered/non-testable):${list}。` +
|
|
859
|
+
`修复:编辑 verify-result.md 探针 7 段,把 <待填:四选一> 替换为判定值。`
|
|
860
|
+
)
|
|
861
|
+
}
|
|
862
|
+
if (matrix.missingEvidence > 0) {
|
|
863
|
+
const list = matrix.rows.filter(r => r.evidenceMissing).map(rowLabel).join(';')
|
|
864
|
+
errors.push(
|
|
865
|
+
`探针 7 验收×测试覆盖矩阵有 ${matrix.missingEvidence} 行证据缺失` +
|
|
866
|
+
`(covered/partial 证据须含测试锚点:\`.test.\` 文件名 / file:line / 反引号包裹的测试名;non-testable 证据须写一句理由):${list}。` +
|
|
867
|
+
`修复:在证据列补测试锚点(如 \`test/foo.test.mjs\` 或 \`src/x.js:42\`)或 non-testable 理由。`
|
|
868
|
+
)
|
|
869
|
+
}
|
|
870
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
871
|
+
}
|
|
872
|
+
|
|
692
873
|
/**
|
|
693
874
|
* 计算归档目标目录名:保持原变更名不变,直接移入 archive/。
|
|
694
875
|
*/
|
|
@@ -881,7 +1062,9 @@ const contracts = {
|
|
|
881
1062
|
description: '验证与测试',
|
|
882
1063
|
allowedFrom: ['execute'],
|
|
883
1064
|
allowedTo: ['archive'],
|
|
884
|
-
|
|
1065
|
+
// validateAcceptanceMatrix:探针 7 矩阵槽 fail-closed(2026-09-14-acceptance-test-matrix FR-02),
|
|
1066
|
+
// 注册进 validator 链即覆盖 gates / machine-interface 等全部 runValidators 调用方(gates.js 零改动)。
|
|
1067
|
+
validators: [validateVerifyOutputs, validateAcceptanceMatrix],
|
|
885
1068
|
},
|
|
886
1069
|
archive: {
|
|
887
1070
|
stage: 'archive',
|
package/src/stage-review.js
CHANGED
|
@@ -200,10 +200,17 @@ export function computeDocHash(filePath) {
|
|
|
200
200
|
* - 新增 reviewType(design/plan/proposal/code/acceptance)+ checklist
|
|
201
201
|
* - 复用 VALID_VERDICTS 三态 + cannot_verify 联动规则(task-review.js:65-69)
|
|
202
202
|
*
|
|
203
|
+
* ql-20260915-001 修复③(坑 stage-review-type-error-no-expected,2026-09-14 用户实证:写错
|
|
204
|
+
* reviewType 时报错只列全部合法值不说本 stage 期望什么,试错才发现):可选 stage 入参——
|
|
205
|
+
* 提供时 reviewType / reviewedFiles 缺失类错误同时给出本 stage 的期望值(期望 reviewType 取
|
|
206
|
+
* STAGE_REVIEW_TYPE[stage]、期望主文档取 STAGE_MAIN_DOC[stage]),消错一次到位。不提供时
|
|
207
|
+
* 报错文案与旧版一致(向后兼容,含 test 直接调用)。
|
|
208
|
+
*
|
|
203
209
|
* @param {object} review - 解析后的 JSON 对象
|
|
210
|
+
* @param {string} [stage] - brainstorm|plan|execute(错误文案给期望值用;不影响判定逻辑)
|
|
204
211
|
* @returns {{ ok: boolean, errors: string[] }}
|
|
205
212
|
*/
|
|
206
|
-
export function validateStageReviewSchema(review) {
|
|
213
|
+
export function validateStageReviewSchema(review, stage = null) {
|
|
207
214
|
const errors = []
|
|
208
215
|
if (!review || typeof review !== 'object') {
|
|
209
216
|
return { ok: false, errors: ['stage review.json 不是有效 JSON 对象'] }
|
|
@@ -214,7 +221,11 @@ export function validateStageReviewSchema(review) {
|
|
|
214
221
|
}
|
|
215
222
|
|
|
216
223
|
if (!STAGE_REVIEW_TYPES.includes(review.reviewType)) {
|
|
217
|
-
|
|
224
|
+
// ql-20260915-001 修复③:合法值之外同时给本 stage 期望——agent 不用从 5 个合法值里试错
|
|
225
|
+
const expectedType = stage && STAGE_REVIEW_TYPE[stage]
|
|
226
|
+
errors.push(expectedType
|
|
227
|
+
? `reviewType 无效:${review.reviewType}(本 stage=${stage} 期望 "${expectedType}";合法值 ${STAGE_REVIEW_TYPES.join('/')})`
|
|
228
|
+
: `reviewType 无效:${review.reviewType}(应为 ${STAGE_REVIEW_TYPES.join('/')})`)
|
|
218
229
|
}
|
|
219
230
|
|
|
220
231
|
if (!VALID_VERDICTS.includes(review.specVerdict)) {
|
|
@@ -233,7 +244,11 @@ export function validateStageReviewSchema(review) {
|
|
|
233
244
|
|
|
234
245
|
// reviewedFiles 必须是非空数组
|
|
235
246
|
if (!Array.isArray(review.reviewedFiles) || review.reviewedFiles.length === 0) {
|
|
236
|
-
|
|
247
|
+
// ql-20260915-001 修复③顺手补:同款缺期望值问题——给本 stage 的主审查文档示例
|
|
248
|
+
const expectedDoc = stage && STAGE_MAIN_DOC[stage]
|
|
249
|
+
errors.push(expectedDoc
|
|
250
|
+
? `缺少 reviewedFiles 字段(应为被审查文档路径数组,[0] 为主审查对象——本 stage=${stage} 期望主文档 ${expectedDoc}(如 changes/<变更名>/${expectedDoc}),docHash 对应它)`
|
|
251
|
+
: '缺少 reviewedFiles 字段(应为被审查文档路径数组,[0] 为主审查对象,docHash 对应它)')
|
|
237
252
|
}
|
|
238
253
|
|
|
239
254
|
// docHash 非空
|
|
@@ -480,7 +495,8 @@ export function validateStageReview(opts) {
|
|
|
480
495
|
return { ok: false, errors: [`${stage} review.json 解析失败: ${e.message}`], warnings, review: null }
|
|
481
496
|
}
|
|
482
497
|
|
|
483
|
-
|
|
498
|
+
// ql-20260915-001 修复③:透传 stage——schema 错误文案给本 stage 期望值
|
|
499
|
+
const schemaResult = validateStageReviewSchema(parsed, stage)
|
|
484
500
|
if (!schemaResult.ok) {
|
|
485
501
|
return { ok: false, errors: [`${stage} review.json schema 校验失败 — ${schemaResult.errors.join('; ')}`], warnings, review: parsed }
|
|
486
502
|
}
|
|
@@ -700,7 +716,7 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
700
716
|
reviewerNotes: `${existing.reviewerNotes || ''}\ndocHash refreshed at ${refreshedAt}(主文档改版后机械重算;verdict 结论是否仍适用需人工确认)`.trim(),
|
|
701
717
|
}
|
|
702
718
|
writeAtomicSync(existingPath, JSON.stringify(review, null, 2) + '\n')
|
|
703
|
-
const schemaRecheck = validateStageReviewSchema(review)
|
|
719
|
+
const schemaRecheck = validateStageReviewSchema(review, stage)
|
|
704
720
|
if (!schemaRecheck.ok) {
|
|
705
721
|
throw new Error(`register-stage-review: refresh 后 schema 自检失败 — ${schemaRecheck.errors.join('; ')}(原 review 字段异常,先修原文件)`)
|
|
706
722
|
}
|
|
@@ -725,7 +741,7 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
725
741
|
} catch (e) {
|
|
726
742
|
throw new Error(`register-stage-review: --from 文件 JSON 解析失败 ${fromAbs}: ${e.message}`)
|
|
727
743
|
}
|
|
728
|
-
const schemaResult = validateStageReviewSchema(parsed)
|
|
744
|
+
const schemaResult = validateStageReviewSchema(parsed, stage)
|
|
729
745
|
if (!schemaResult.ok) {
|
|
730
746
|
throw new Error(`register-stage-review: --from 文件 schema 校验失败 — ${schemaResult.errors.join('; ')}`)
|
|
731
747
|
}
|
|
@@ -765,7 +781,7 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
765
781
|
|
|
766
782
|
// self-check(fail-closed):刚写的 review 必过 schema + docHash 真实性。只验 mechanics,
|
|
767
783
|
// 不判 verdict(verdict 是 agent/子代理的审查结论,even fail 也如实落盘,由 Stage Review Gate 裁决)。
|
|
768
|
-
const schemaRecheck = validateStageReviewSchema(review)
|
|
784
|
+
const schemaRecheck = validateStageReviewSchema(review, stage)
|
|
769
785
|
if (!schemaRecheck.ok) {
|
|
770
786
|
throw new Error(`register-stage-review: 写入后 schema 自检失败(不应发生)— ${schemaRecheck.errors.join('; ')}`)
|
|
771
787
|
}
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -173,6 +173,8 @@ export const definition = {
|
|
|
173
173
|
- normalized_requirement: <可测试的约束>
|
|
174
174
|
- impacts: [FR-?, task-?, verify-?]
|
|
175
175
|
- evidence: <文件路径/代码位置/用户回答轮次>
|
|
176
|
+
- 故障面: <本决策引入的新失败模式>(可选,type=architecture 时建议填写)
|
|
177
|
+
- 退役判据: <出现什么信号时简化/删除本机制>(可选,type=architecture 时建议填写)
|
|
176
178
|
\`\`\`
|
|
177
179
|
|
|
178
180
|
- 幂等:写入前按 D-xxx@vN 查重,已存在的不重复追加;后续修正走新版本 D-001@v2 + supersedes(见 Design Grill 版本规则)
|
|
@@ -434,6 +436,8 @@ ${REVIEW_CHECKLISTS.brainstorm.slice(3).map((item) => '- ' + item).join('\n')}
|
|
|
434
436
|
- normalized_requirement: TTL 过期且上游仍异常时返回 stale 标记,不刷新缓存。
|
|
435
437
|
- impacts: [FR-02, task-03, verify-02]
|
|
436
438
|
- evidence: design.md §3/§7, src/cache/...
|
|
439
|
+
- 故障面: <本决策引入的新失败模式>(可选,type=architecture 时建议填写)
|
|
440
|
+
- 退役判据: <出现什么信号时简化/删除本机制>(可选,type=architecture 时建议填写)
|
|
437
441
|
\`\`\`
|
|
438
442
|
|
|
439
443
|
### 输出格式
|
|
@@ -590,6 +594,8 @@ created_at: <now-datetime>
|
|
|
590
594
|
- evidence: 用户回答轮次或代码/文档路径
|
|
591
595
|
\`\`\`
|
|
592
596
|
|
|
597
|
+
> 可选字段(按需另加,旧格式决策缺这些字段不受影响):**锚点**(决策落点主文件,\`<src 路径>:<行号或符号>\`;status=confirmed 时必填);**模块域**(决策涉及的模块 ID,可多个逗号分隔——合法 id 只取**当前变更所属项目**的 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`,多项目仓勿读其他子项目的 map(核验不认,报错会点名归属);规划中的新模块用 \`NEW:<名>\` 前缀声明,冒号后不加空格:\`NEW:foo\` 合法、\`NEW: foo\` 属书写错误。本步 --done 的模块域核验对不认识的 id 直接阻断);**否决理由**(status=rejected 时必填);**复潮条件**(status=rejected 时必填);**故障面**(本决策引入的新失败模式——新机制落地时留痕它引入什么失败模式;type=architecture 时建议填写,可选);**退役判据**(出现什么信号时简化或删除本机制——信号出现就该简化它;type=architecture 时建议填写,可选)。
|
|
598
|
+
|
|
593
599
|
### 后续变更包处理
|
|
594
600
|
如果 MASTER.md 中规划了后续变更包(拆分后的子阶段),**必须同时为每个后续包创建独立变更目录**:
|
|
595
601
|
1. 读取 MASTER.md 中的变更包列表(包名 + 边界描述)
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { existsSync, readFileSync } from 'fs'
|
|
16
|
-
import { join } from 'path'
|
|
16
|
+
import { join, isAbsolute } from 'path'
|
|
17
17
|
|
|
18
18
|
// 提取 `npm|pnpm|yarn run <script>` 命令。
|
|
19
19
|
// 对齐 scan-postcheck:136 的 `/npm run (\S+)/g`,扩展包管理器 + 容忍多空白。
|
|
@@ -50,7 +50,12 @@ function readScripts(pkgPath) {
|
|
|
50
50
|
* 为单条命令计算候选 package.json 路径列表。
|
|
51
51
|
*
|
|
52
52
|
* 规则(blueprint task-02 + design §5 H2):
|
|
53
|
-
* - 有 `cd <subdir> &&` 前缀 → 仅查 `<
|
|
53
|
+
* - 有 `cd <subdir> &&` 前缀 → 仅查 `<subdir>/package.json`(cd 锁定子目录,不回退根)。
|
|
54
|
+
* subdir 为绝对路径(win 盘符 / posix 根)时不再拼 projectRoot——坑
|
|
55
|
+
* verify-cmd-absolute-cd-path(2026-09-15 wp 会话实证):跨仓 task 卡 verify 写
|
|
56
|
+
* `cd E:/PZwangge/sub-grid-security && npm run lint`,join(主仓根, 'E:/...') 拼出
|
|
57
|
+
* 病态路径 → package.json 恒 missing → 真实命令被误判死命令,agent 只能改写成
|
|
58
|
+
* ../ 相对路径绕过
|
|
54
59
|
* - 无前缀 + modules 块提供 → 查每个 module.path 的子包 + 根 package.json(多候选任一命中即视为存在)
|
|
55
60
|
* - 无前缀 + 无 modules → 仅查 `<projectRoot>/package.json`
|
|
56
61
|
*
|
|
@@ -65,7 +70,8 @@ function resolveCandidates(text, matchIndex, projectRoot, modules) {
|
|
|
65
70
|
const cdMatch = before.match(CD_PREFIX_RE)
|
|
66
71
|
if (cdMatch) {
|
|
67
72
|
const subdir = cdMatch[1].replace(/^["']|["']$/g, '')
|
|
68
|
-
|
|
73
|
+
const base = isAbsolute(subdir) ? subdir : join(projectRoot, subdir)
|
|
74
|
+
return [join(base, 'package.json')]
|
|
69
75
|
}
|
|
70
76
|
if (modules && typeof modules === 'object') {
|
|
71
77
|
const candidates = []
|
package/src/stages/execute.js
CHANGED
|
@@ -15,6 +15,84 @@ import { resolveChangeModuleCards } from '../module-resolve.js'
|
|
|
15
15
|
// 决策锚点触碰事实(2026-08-24-decision-touch-cli-drift task-01,W-A 主渲染点 D-003):
|
|
16
16
|
// changedFiles 口径与 {DOCS_DEBT} 现算同源(collectExecuteChangedFiles 唯一实现,勿双写)
|
|
17
17
|
import { collectExecuteChangedFiles, computeDecisionTouches, renderDecisionTouchFacts } from '../docs-debt.js'
|
|
18
|
+
// Wave 粒度机械知识注入(2026-09-14-knowledge-loop-close task-04,FR-04):两模块均为 fs/path
|
|
19
|
+
// 叶子,无环。为什么不 import run/prompt.js 的 buildKnowledgeInjection:prompt.js → stages/index.js
|
|
20
|
+
// → 本文件的既有依赖方向(run 编排 stages),本文件反向静态 import 会在「本文件作模块图入口」
|
|
21
|
+
// (test 直 import 本文件皆此形态)触发 stages/index.js 顶层 stageRegistry 对本文件 definition
|
|
22
|
+
// const 的 TDZ 求值(ESM 环实证 2026-09-14)。此处为同格式本地孪生,漂移由
|
|
23
|
+
// test/knowledge-inject.test.mjs 格式等价断言锁定——改 prompt.js 版必同步改此处。
|
|
24
|
+
import { matchKnowledge } from '../knowledge-match.js'
|
|
25
|
+
import { appendKnowledgeHit } from '../knowledge-hits.js'
|
|
26
|
+
|
|
27
|
+
// 机械知识注入限额(与 run/prompt.js KNOWLEDGE_INJECT_MAX_FILES/MAX_LINES 同值孪生,R-02 膨胀控制)
|
|
28
|
+
const KNOWLEDGE_INJECT_MAX_FILES = 3
|
|
29
|
+
const KNOWLEDGE_INJECT_MAX_LINES = 40
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Wave 粒度机械知识注入段(task-04,FR-04 / D-002@v1 总体方案 D):用本 Wave 各 task 标题拼接
|
|
33
|
+
* 串跑 matchKnowledge,命中文件正文以纯文本段注入 buildWavePrompt 输出(**禁止 {{include:}} 形式**
|
|
34
|
+
* ——execute-testcase-design-include.test.mjs:74 include 计数恰为 1 的断言约束)。未命中返回 ''
|
|
35
|
+
* (零字节零变化)。命中同时 appendKnowledgeHit 落 hits.jsonl 一条 type:inject 记录。
|
|
36
|
+
* best-effort:任何异常返回 '',不阻断 Wave prompt 组装(INDEX/knowledge 缺失时 matchKnowledge
|
|
37
|
+
* 本就返回 matched:false,brownfield no-op)。
|
|
38
|
+
*/
|
|
39
|
+
function buildWaveKnowledgeSection(changeDir, wave) {
|
|
40
|
+
if (!changeDir) return ''
|
|
41
|
+
try {
|
|
42
|
+
const specBase = path.dirname(path.dirname(changeDir))
|
|
43
|
+
const knowledgeDir = path.join(specBase, 'knowledge')
|
|
44
|
+
// Wave 任务名串:本 Wave 各 task 标题拼接(buildWavePrompt 唯一稳定的 Wave 级任务语料)
|
|
45
|
+
const query = (Array.isArray(wave && wave.tasks) ? wave.tasks : [])
|
|
46
|
+
.map(t => String((t && t.name) || '')).filter(Boolean).join(' ')
|
|
47
|
+
if (!query.trim()) return ''
|
|
48
|
+
const result = matchKnowledge(knowledgeDir, query)
|
|
49
|
+
if (!result.matched) return ''
|
|
50
|
+
// ── 以下与 run/prompt.js renderKnowledgeInjectSection 同格式孪生(含段头文案/截断标记)──
|
|
51
|
+
const ref = e => (e.anchor ? `${e.file}#${e.anchor}` : e.file)
|
|
52
|
+
const lines = []
|
|
53
|
+
lines.push(`### 📚 命中知识(CLI 按 Wave 任务名机械匹配,top-${KNOWLEDGE_INJECT_MAX_FILES}——勿自行重跑 INDEX 匹配)`)
|
|
54
|
+
lines.push(`Status: matched | Entries: ${result.entries.length} | Sources:`)
|
|
55
|
+
for (const e of result.entries) lines.push(` - ${ref(e)}`)
|
|
56
|
+
lines.push(`(命中清单如上;正文按 INDEX 行序注入前 ${KNOWLEDGE_INJECT_MAX_FILES} 个不同文件,单文件首 ${KNOWLEDGE_INJECT_MAX_LINES} 行截断——未注入条目需要时按 Sources 路径自行读取)`)
|
|
57
|
+
const seen = new Set()
|
|
58
|
+
const picked = []
|
|
59
|
+
for (const e of result.entries) {
|
|
60
|
+
if (seen.has(e.file)) continue
|
|
61
|
+
seen.add(e.file)
|
|
62
|
+
picked.push(e)
|
|
63
|
+
if (picked.length >= KNOWLEDGE_INJECT_MAX_FILES) break
|
|
64
|
+
}
|
|
65
|
+
for (const e of picked) {
|
|
66
|
+
lines.push('')
|
|
67
|
+
lines.push(`── ${ref(e)}${e.display ? `(${e.display})` : ''} ──`)
|
|
68
|
+
let body = ''
|
|
69
|
+
try {
|
|
70
|
+
body = readFileSync(path.join(knowledgeDir, e.file), 'utf8').replace(/\r\n?/g, '\n')
|
|
71
|
+
} catch (err) {
|
|
72
|
+
lines.push(`(文件不可读:${err && err.message ? err.message : err})`)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
const bodyLines = body.split('\n')
|
|
76
|
+
if (bodyLines.length > KNOWLEDGE_INJECT_MAX_LINES) {
|
|
77
|
+
lines.push(bodyLines.slice(0, KNOWLEDGE_INJECT_MAX_LINES).join('\n'))
|
|
78
|
+
lines.push('…(截断)')
|
|
79
|
+
} else {
|
|
80
|
+
lines.push(body.replace(/\n+$/, ''))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
appendKnowledgeHit(path.join(specBase, '.runtime'), {
|
|
85
|
+
type: 'inject',
|
|
86
|
+
change: path.basename(changeDir),
|
|
87
|
+
query,
|
|
88
|
+
matchedFiles: result.entries.map(ref),
|
|
89
|
+
})
|
|
90
|
+
} catch { /* 遥测 fail-soft(R-04) */ }
|
|
91
|
+
return lines.join('\n') + '\n'
|
|
92
|
+
} catch {
|
|
93
|
+
return ''
|
|
94
|
+
}
|
|
95
|
+
}
|
|
18
96
|
|
|
19
97
|
/**
|
|
20
98
|
* 任务注册表解析(2026-08-20-task-truth-unify D-001@v1:tasks.md 唯一任务真相)。
|
|
@@ -311,12 +389,12 @@ worktree 路径 + 分支名 + 模式
|
|
|
311
389
|
1. 从 plan 中解析 Wave 分组和任务列表
|
|
312
390
|
2. 模型档位:若 tasks.md 中某 task 标注了 [model:xxx],启动该 task 子代理时按标签选模型(档位由 plan 阶段或用户在 tasks.md 显式标注,execute 不在此自动建议——关键词→档位无统一映射,自动建议反而易误导)
|
|
313
391
|
3. 确认频率:默认每个 Wave 完成后展示结果(wave 模式);用户口头指定按 Task 展示或全自动时遵从
|
|
314
|
-
4.
|
|
392
|
+
4. 知识库:CLI 已按任务上下文机械匹配(changeName + tasks.md 任务行)——命中时下方「知识命中报告」段直接含命中文件正文(top-3 截断),勿自行重跑 INDEX 匹配;无命中且确有需要时才读 \`.sillyspec/knowledge/INDEX.md\`
|
|
315
393
|
|
|
316
394
|
### 知识命中报告
|
|
317
395
|
{KNOWLEDGE_HIT_REPORT}
|
|
318
396
|
|
|
319
|
-
|
|
397
|
+
命中条目正文已由 CLI 机械注入如上(INDEX 行序 top-3 不同文件、单文件截断)——直接阅读获取项目约定与已知模式;Sources 清单中未注入正文的其他命中条目,需要时按路径自行读取。
|
|
320
398
|
如无命中条目(Status: no matches),跳过本节。
|
|
321
399
|
|
|
322
400
|
### 模块文档欠账(CLI 算事实)
|
|
@@ -956,6 +1034,10 @@ ${indexLines}
|
|
|
956
1034
|
}
|
|
957
1035
|
} catch { /* 触碰事实 best-effort:失败不注入不阻断 */ }
|
|
958
1036
|
|
|
1037
|
+
// ── Wave 粒度机械知识注入(task-04,FR-04):Wave 任务名串匹配 → 命中正文纯文本段(未命中 '',
|
|
1038
|
+
// Wave prompt 与无知识库时字节一致)。pure text 注入,禁 {{include:}}(include 计数测试约束)。──
|
|
1039
|
+
const waveKnowledgeSection = buildWaveKnowledgeSection(changeDir, wave)
|
|
1040
|
+
|
|
959
1041
|
// 子代理要点 4 / Wave 开始前第 1 条:模块表 / 热区存在时换成「按注入内容执行」版文案,
|
|
960
1042
|
// 否则保留原文(零回归——无 map / 无 design 的项目 prompt 不变)
|
|
961
1043
|
const moduleDocPoint = moduleSection
|
|
@@ -1188,7 +1270,7 @@ ${taskSummary}
|
|
|
1188
1270
|
|
|
1189
1271
|
{{include: testcase-design}}
|
|
1190
1272
|
|
|
1191
|
-
${designHotzone}${decisionTouchSection}
|
|
1273
|
+
${designHotzone}${decisionTouchSection}${waveKnowledgeSection}
|
|
1192
1274
|
### Wave 开始前
|
|
1193
1275
|
${waveStartItem1}
|
|
1194
1276
|
2. 读取 plan.md 了解全局任务划分和依赖关系
|
package/src/stages/knowledge.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* validate — 校验知识库完整性
|
|
13
13
|
* refresh — 从 scan 文档刷新自动知识(仅写 generated/)
|
|
14
14
|
* propose — 提议新知识(写入 proposed/)
|
|
15
|
+
* classify — 归类 uncategorized 条目到目标知识文件(动态 import src/knowledge-classify.js,task-02 交付)
|
|
16
|
+
* stats — 命中矩阵聚合与死重对照(动态 import src/knowledge-stats.js,task-05 交付)
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import { existsSync, readFileSync, readdirSync, mkdirSync, statSync } from 'fs'
|
|
@@ -74,10 +76,15 @@ function extractTitle(content) {
|
|
|
74
76
|
|
|
75
77
|
export async function cmdSearch(dir, args, opts = {}) {
|
|
76
78
|
const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
|
|
79
|
+
// flag-as-value 防护(坑 knowledge-flag-as-value):值以 '--' 开头视为漏传(只读命令,
|
|
80
|
+
// 按缺参处理落入既有 required 校验/默认值,不静默拿 flag 字面量当查询词)
|
|
77
81
|
const queryIdx = args.indexOf('--query')
|
|
78
|
-
const
|
|
82
|
+
const queryRaw = queryIdx >= 0 ? args[queryIdx + 1] : ''
|
|
83
|
+
const query = queryRaw && !queryRaw.startsWith('--') ? queryRaw : ''
|
|
79
84
|
const limitIdx = args.indexOf('--limit')
|
|
80
|
-
const
|
|
85
|
+
const limitRaw = limitIdx >= 0 ? args[limitIdx + 1] : ''
|
|
86
|
+
const parsedLimit = limitRaw && !limitRaw.startsWith('--') ? parseInt(limitRaw, 10) : NaN
|
|
87
|
+
const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? parsedLimit : 10
|
|
81
88
|
|
|
82
89
|
if (!query) {
|
|
83
90
|
output(false, {}, '--query is required')
|
|
@@ -128,8 +135,10 @@ export async function cmdSearch(dir, args, opts = {}) {
|
|
|
128
135
|
|
|
129
136
|
export async function cmdInspect(dir, args, opts = {}) {
|
|
130
137
|
const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
|
|
138
|
+
// flag-as-value 防护同 cmdSearch(坑 knowledge-flag-as-value)
|
|
131
139
|
const idIdx = args.indexOf('--id')
|
|
132
|
-
const
|
|
140
|
+
const idRaw = idIdx >= 0 ? args[idIdx + 1] : ''
|
|
141
|
+
const id = idRaw && !idRaw.startsWith('--') ? idRaw : ''
|
|
133
142
|
|
|
134
143
|
if (!id) {
|
|
135
144
|
output(false, {}, '--id is required')
|
|
@@ -213,6 +222,20 @@ export async function cmdValidate(dir, args, opts = {}) {
|
|
|
213
222
|
})
|
|
214
223
|
}
|
|
215
224
|
}
|
|
225
|
+
|
|
226
|
+
// 2b. flag 形态关键词(坑 knowledge-flag-as-value):classify 曾把 flag-as-value 的
|
|
227
|
+
// 字面量 '--file' 写进路由行——目标文件存在、行格式合法,既有检查放行,此处补侦测
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const flagKw = entry.keywords.find(kw => kw.startsWith('--'))
|
|
230
|
+
if (flagKw) {
|
|
231
|
+
warnings.push({
|
|
232
|
+
code: 'flag_like_keyword',
|
|
233
|
+
path: 'knowledge/INDEX.md',
|
|
234
|
+
display: entry.display,
|
|
235
|
+
keyword: flagKw,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
}
|
|
216
239
|
}
|
|
217
240
|
|
|
218
241
|
// 3. 扫描所有 .md 文件,检查是否在 INDEX 中注册
|
|
@@ -223,8 +246,8 @@ export async function cmdValidate(dir, args, opts = {}) {
|
|
|
223
246
|
|
|
224
247
|
for (const mdFile of allMdFiles) {
|
|
225
248
|
if (mdFile === 'INDEX.md') continue
|
|
226
|
-
// generated/ 和 proposed/ 区不强制要求 INDEX
|
|
227
|
-
if (mdFile.startsWith('generated/') || mdFile.startsWith('proposed/')) continue
|
|
249
|
+
// generated/ 和 proposed/ 区不强制要求 INDEX 注册;uncategorized.md 是暂存区(条目归类即迁出,语义上永不注册 INDEX)
|
|
250
|
+
if (mdFile.startsWith('generated/') || mdFile.startsWith('proposed/') || mdFile === 'uncategorized.md') continue
|
|
228
251
|
if (!indexedFiles.has(mdFile)) {
|
|
229
252
|
warnings.push({
|
|
230
253
|
code: 'unregistered_file',
|
|
@@ -498,6 +521,29 @@ function extractSections(content) {
|
|
|
498
521
|
|
|
499
522
|
// ── 入口路由 ──
|
|
500
523
|
|
|
524
|
+
/**
|
|
525
|
+
* 动态加载 classify / stats 子命令实现(2026-09-14-knowledge-loop-close)。
|
|
526
|
+
*
|
|
527
|
+
* 实现文件由 Wave 2/3 的 task-02(src/knowledge-classify.js)/ task-05(src/knowledge-stats.js)交付,
|
|
528
|
+
* 落地前可能缺席——静态 import 会断链整个 knowledge 命令,故循 index.js case 'knowledge'
|
|
529
|
+
* 先例用动态 import;模块缺席时返回 null 由调用方输出友好错误,其余异常照抛。
|
|
530
|
+
* 实现侧须导出 CLI 入口 cmdKnowledgeClassify / cmdKnowledgeStats(dir, args, opts),
|
|
531
|
+
* 形态对齐本文件 cmdSearch 等既有子命令。
|
|
532
|
+
*
|
|
533
|
+
* 遥测底座:两命令的归类/命中审计统一经 src/knowledge-hits.js 的
|
|
534
|
+
* appendKnowledgeHit / readKnowledgeHits(KnowledgeHitsAPI 契约,task-02/04/05 消费)。
|
|
535
|
+
*/
|
|
536
|
+
async function tryImportSubcommandImpl(importPath, moduleName) {
|
|
537
|
+
try {
|
|
538
|
+
return await import(importPath)
|
|
539
|
+
} catch (err) {
|
|
540
|
+
if (err && err.code === 'ERR_MODULE_NOT_FOUND' && String(err.message || '').includes(moduleName)) {
|
|
541
|
+
return null
|
|
542
|
+
}
|
|
543
|
+
throw err
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
501
547
|
/**
|
|
502
548
|
* @param {string[]} args - filteredArgs.slice(1)(去掉 'knowledge')
|
|
503
549
|
* @param {string} dir - 项目根目录
|
|
@@ -517,11 +563,35 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
517
563
|
return cmdRefresh(dir, args.slice(1), opts)
|
|
518
564
|
case 'propose':
|
|
519
565
|
return cmdPropose(dir, args.slice(1), opts)
|
|
566
|
+
case 'classify': {
|
|
567
|
+
const mod = await tryImportSubcommandImpl('../knowledge-classify.js', 'knowledge-classify.js')
|
|
568
|
+
if (!mod || typeof mod.cmdKnowledgeClassify !== 'function') {
|
|
569
|
+
output(false, {}, {
|
|
570
|
+
code: 'not_implemented',
|
|
571
|
+
subcommand: 'classify',
|
|
572
|
+
message: '实现未落地(task-02 交付 src/knowledge-classify.js)',
|
|
573
|
+
})
|
|
574
|
+
return
|
|
575
|
+
}
|
|
576
|
+
return mod.cmdKnowledgeClassify(dir, args.slice(1), opts)
|
|
577
|
+
}
|
|
578
|
+
case 'stats': {
|
|
579
|
+
const mod = await tryImportSubcommandImpl('../knowledge-stats.js', 'knowledge-stats.js')
|
|
580
|
+
if (!mod || typeof mod.cmdKnowledgeStats !== 'function') {
|
|
581
|
+
output(false, {}, {
|
|
582
|
+
code: 'not_implemented',
|
|
583
|
+
subcommand: 'stats',
|
|
584
|
+
message: '实现未落地(task-05 交付 src/knowledge-stats.js)',
|
|
585
|
+
})
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
return mod.cmdKnowledgeStats(dir, args.slice(1), opts)
|
|
589
|
+
}
|
|
520
590
|
default:
|
|
521
591
|
output(false, {}, {
|
|
522
592
|
code: 'unknown_subcommand',
|
|
523
593
|
subcommand: subCommand,
|
|
524
|
-
available: ['search', 'inspect', 'validate', 'refresh', 'propose'],
|
|
594
|
+
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'stats'],
|
|
525
595
|
})
|
|
526
596
|
}
|
|
527
597
|
}
|