sillyspec 3.28.7 → 3.28.9
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-quick/SKILL.md +2 -0
- package/.husky/pre-push +3 -1
- 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/agent-session-log.js +446 -35
- package/src/change-risk-profile.js +20 -0
- package/src/config-cat.js +6 -3
- package/src/config-schema.js +32 -0
- package/src/constants.js +3 -0
- package/src/db.js +23 -6
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- package/src/docs-check.js +137 -12
- package/src/docs-gate.js +108 -6
- package/src/doctor-diagnostics.js +243 -9
- package/src/friction-ledger.js +171 -0
- package/src/friction-tally.js +7 -2
- package/src/git-helper.js +6 -4
- package/src/hooks/worktree-guard.js +72 -30
- package/src/index.js +356 -81
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/local-detect.js +30 -15
- package/src/local-register.js +13 -4
- package/src/mcp-server.js +38 -8
- package/src/progress/change-registry.js +224 -43
- package/src/progress/consistency-doctor.js +11 -4
- package/src/progress/shared.js +3 -2
- package/src/progress/stage-machine.js +3 -1
- package/src/progress/step-store.js +30 -17
- package/src/progress.js +123 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +199 -32
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +124 -15
- package/src/run/complete-handlers.js +514 -24
- package/src/run/complete.js +82 -25
- package/src/run/concurrent-detect.js +99 -1
- package/src/run/gate-snapshot.js +115 -5
- package/src/run/gates.js +85 -14
- package/src/run/prompt.js +143 -6
- package/src/run/quick-audit.js +136 -15
- package/src/run/shared.js +174 -20
- package/src/run/stage.js +6 -0
- package/src/run/verify-quality-scan.js +14 -3
- 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 +365 -35
- package/src/spec-sync.js +19 -14
- package/src/stage-contract-spec.js +4 -3
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +29 -12
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +91 -4
- package/src/stages/knowledge.js +52 -3
- package/src/stages/plan-postcheck.js +57 -12
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +114 -6
- package/src/sync.js +201 -11
- package/src/task-review.js +333 -27
- package/src/verify-facts-schema.js +10 -3
- package/src/verify-postcheck.js +41 -6
- package/src/verify-probes.js +348 -7
- package/src/worktree-apply.js +594 -62
- package/src/worktree-cross.js +12 -18
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +255 -30
- package/src/wt-commit.js +128 -0
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- 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
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* (平台模式落 <runtimeRoot>/stage-reviews/...,与 execute-runs 同构)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { existsSync, readFileSync, mkdirSync, readdirSync
|
|
16
|
+
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'fs'
|
|
17
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
17
18
|
import { join } from 'path'
|
|
18
19
|
import { createHash } from 'crypto'
|
|
19
20
|
import jsYaml from 'js-yaml'
|
|
@@ -199,10 +200,17 @@ export function computeDocHash(filePath) {
|
|
|
199
200
|
* - 新增 reviewType(design/plan/proposal/code/acceptance)+ checklist
|
|
200
201
|
* - 复用 VALID_VERDICTS 三态 + cannot_verify 联动规则(task-review.js:65-69)
|
|
201
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
|
+
*
|
|
202
209
|
* @param {object} review - 解析后的 JSON 对象
|
|
210
|
+
* @param {string} [stage] - brainstorm|plan|execute(错误文案给期望值用;不影响判定逻辑)
|
|
203
211
|
* @returns {{ ok: boolean, errors: string[] }}
|
|
204
212
|
*/
|
|
205
|
-
export function validateStageReviewSchema(review) {
|
|
213
|
+
export function validateStageReviewSchema(review, stage = null) {
|
|
206
214
|
const errors = []
|
|
207
215
|
if (!review || typeof review !== 'object') {
|
|
208
216
|
return { ok: false, errors: ['stage review.json 不是有效 JSON 对象'] }
|
|
@@ -213,7 +221,11 @@ export function validateStageReviewSchema(review) {
|
|
|
213
221
|
}
|
|
214
222
|
|
|
215
223
|
if (!STAGE_REVIEW_TYPES.includes(review.reviewType)) {
|
|
216
|
-
|
|
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('/')})`)
|
|
217
229
|
}
|
|
218
230
|
|
|
219
231
|
if (!VALID_VERDICTS.includes(review.specVerdict)) {
|
|
@@ -232,7 +244,11 @@ export function validateStageReviewSchema(review) {
|
|
|
232
244
|
|
|
233
245
|
// reviewedFiles 必须是非空数组
|
|
234
246
|
if (!Array.isArray(review.reviewedFiles) || review.reviewedFiles.length === 0) {
|
|
235
|
-
|
|
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 对应它)')
|
|
236
252
|
}
|
|
237
253
|
|
|
238
254
|
// docHash 非空
|
|
@@ -479,7 +495,8 @@ export function validateStageReview(opts) {
|
|
|
479
495
|
return { ok: false, errors: [`${stage} review.json 解析失败: ${e.message}`], warnings, review: null }
|
|
480
496
|
}
|
|
481
497
|
|
|
482
|
-
|
|
498
|
+
// ql-20260915-001 修复③:透传 stage——schema 错误文案给本 stage 期望值
|
|
499
|
+
const schemaResult = validateStageReviewSchema(parsed, stage)
|
|
483
500
|
if (!schemaResult.ok) {
|
|
484
501
|
return { ok: false, errors: [`${stage} review.json schema 校验失败 — ${schemaResult.errors.join('; ')}`], warnings, review: parsed }
|
|
485
502
|
}
|
|
@@ -561,7 +578,7 @@ export function validateStageReviewWithAutoRefresh(opts) {
|
|
|
561
578
|
reviewerNotes: `${result.review.reviewerNotes || ''}\ndocHash auto-refreshed at ${refreshedAt}(gate 检测主文档改版,机械重算;verdict 保留,结论是否仍适用需人工确认)`.trim(),
|
|
562
579
|
}
|
|
563
580
|
try {
|
|
564
|
-
|
|
581
|
+
writeAtomicSync(reviewPath, JSON.stringify(refreshed, null, 2) + '\n')
|
|
565
582
|
} catch {
|
|
566
583
|
return { result, autoRefreshed: false } // 写失败回落原失败结果,不静默放行
|
|
567
584
|
}
|
|
@@ -698,8 +715,8 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
698
715
|
reviewedFiles,
|
|
699
716
|
reviewerNotes: `${existing.reviewerNotes || ''}\ndocHash refreshed at ${refreshedAt}(主文档改版后机械重算;verdict 结论是否仍适用需人工确认)`.trim(),
|
|
700
717
|
}
|
|
701
|
-
|
|
702
|
-
const schemaRecheck = validateStageReviewSchema(review)
|
|
718
|
+
writeAtomicSync(existingPath, JSON.stringify(review, null, 2) + '\n')
|
|
719
|
+
const schemaRecheck = validateStageReviewSchema(review, stage)
|
|
703
720
|
if (!schemaRecheck.ok) {
|
|
704
721
|
throw new Error(`register-stage-review: refresh 后 schema 自检失败 — ${schemaRecheck.errors.join('; ')}(原 review 字段异常,先修原文件)`)
|
|
705
722
|
}
|
|
@@ -724,7 +741,7 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
724
741
|
} catch (e) {
|
|
725
742
|
throw new Error(`register-stage-review: --from 文件 JSON 解析失败 ${fromAbs}: ${e.message}`)
|
|
726
743
|
}
|
|
727
|
-
const schemaResult = validateStageReviewSchema(parsed)
|
|
744
|
+
const schemaResult = validateStageReviewSchema(parsed, stage)
|
|
728
745
|
if (!schemaResult.ok) {
|
|
729
746
|
throw new Error(`register-stage-review: --from 文件 schema 校验失败 — ${schemaResult.errors.join('; ')}`)
|
|
730
747
|
}
|
|
@@ -750,7 +767,7 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
750
767
|
const reviewDir = join(runtimeRoot, 'stage-reviews', `${stage}-${reviewRunId}`)
|
|
751
768
|
const reviewPath = join(reviewDir, 'review.json')
|
|
752
769
|
mkdirSync(reviewDir, { recursive: true })
|
|
753
|
-
|
|
770
|
+
writeAtomicSync(reviewPath, JSON.stringify(review, null, 2) + '\n')
|
|
754
771
|
// stage-reviews 回收不在此处(ql-20260908-005 已定分野):变更归属类证据走归档时精确
|
|
755
772
|
// 回收(pruneArchivedChangeRuntime 按 reviewedFiles 归属);写入侧滚动是启发式,
|
|
756
773
|
// 活跃变更的 review 可能因他端高频写而跌出 keep 窗,不进证据类目录。
|
|
@@ -760,11 +777,11 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
760
777
|
console.warn(`⚠️ register-stage-review: marker 已存在 ${markerPath},将被覆盖为 ${reviewRunId}`)
|
|
761
778
|
}
|
|
762
779
|
mkdirSync(runtimeRoot, { recursive: true })
|
|
763
|
-
|
|
780
|
+
writeAtomicSync(markerPath, reviewRunId + '\n')
|
|
764
781
|
|
|
765
782
|
// self-check(fail-closed):刚写的 review 必过 schema + docHash 真实性。只验 mechanics,
|
|
766
783
|
// 不判 verdict(verdict 是 agent/子代理的审查结论,even fail 也如实落盘,由 Stage Review Gate 裁决)。
|
|
767
|
-
const schemaRecheck = validateStageReviewSchema(review)
|
|
784
|
+
const schemaRecheck = validateStageReviewSchema(review, stage)
|
|
768
785
|
if (!schemaRecheck.ok) {
|
|
769
786
|
throw new Error(`register-stage-review: 写入后 schema 自检失败(不应发生)— ${schemaRecheck.errors.join('; ')}`)
|
|
770
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 版本规则)
|
|
@@ -293,7 +295,7 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
|
|
|
293
295
|
- 仅改内部实现、无对外字段变动时,说明列照常写「新增 xx 方法」即可,无需数据流。
|
|
294
296
|
|
|
295
297
|
**跨仓变更写法**(本次变更涉及 local.yaml repos: 注册的其他仓时必读;纯主仓变更跳过):
|
|
296
|
-
- 清单**按仓分段**:每个仓一个小节,标题格式固定 \`## <repo-key> 仓变更\`(如 \`## sub-grid-security
|
|
298
|
+
- 清单**按仓分段**:每个仓一个小节,标题格式固定 \`## <repo-key> 仓变更\`(如 \`## sub-grid-security 仓变更\`;可加编号前缀或(跨仓,X1-X4)这类括号备注后缀,其余后缀/写法解析不了)。plan-postcheck 文件覆盖对账按此段头解析仓归属,写成「前端仓 / backend」等其他标题 → 对账不上且会点名段头格式错误
|
|
297
299
|
- 段内路径相对**该仓根**写(\`src/routes/x.js\`);❌ 禁止带仓库名前缀(\`sub-grid-security/src/routes/x.js\`)、❌ 禁止绝对路径/盘符路径(\`C:/repo/src/...\`)——后续 TaskCard 的 allowed_paths 与 review 对账(\`git -C <仓根> diff\` 产仓根相对路径)都用同一口径,前缀/绝对路径永不命中
|
|
298
300
|
- 主仓文件放在第一个段头之前(无段头区域 = main),或单独写 \`## main 仓变更\` 段
|
|
299
301
|
- repo-key 必须是 local.yaml repos: 段已注册的键(未注册先跑 \`sillyspec local register-repo <key> <仓根路径>\`,main 隐式不用注册)
|
|
@@ -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 中的变更包列表(包名 + 边界描述)
|
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
|
|
@@ -1074,6 +1156,10 @@ ${crossLines}${crossWorktreeSection}${crossLegacySection}
|
|
|
1074
1156
|
` - task-${i.taskNum} (repo: ${i.repo}) → workdir: "${i.workdir}"`
|
|
1075
1157
|
).join('\n')
|
|
1076
1158
|
worktreeSection = `
|
|
1159
|
+
### task 进行中标记(中断续跑的半成品归属,2026-09-12 起)
|
|
1160
|
+
|
|
1161
|
+
每个 task **开工/完工各跑一条命令**(低摩擦,主代理或子代理均可):开工「sillyspec task start --change {CHANGE_NAME} --task task-NN --note 一句话在做什么」;完工「sillyspec task finish --change {CHANGE_NAME} --task task-NN」。中断续跑时主代理用「sillyspec task list --change {CHANGE_NAME}」点名接管(>2h 标红为中断遗留),不再靠翻 git 工作区猜哪个半成品是谁的。
|
|
1162
|
+
|
|
1077
1163
|
### 工作目录(per-task)
|
|
1078
1164
|
|
|
1079
1165
|
调用 Task 工具启动子代理时,**workdir 参数是强制必传的**,且本 Wave 内不同 task 的 workdir 不同(主仓 task vs 跨仓 task)。
|
|
@@ -1184,7 +1270,7 @@ ${taskSummary}
|
|
|
1184
1270
|
|
|
1185
1271
|
{{include: testcase-design}}
|
|
1186
1272
|
|
|
1187
|
-
${designHotzone}${decisionTouchSection}
|
|
1273
|
+
${designHotzone}${decisionTouchSection}${waveKnowledgeSection}
|
|
1188
1274
|
### Wave 开始前
|
|
1189
1275
|
${waveStartItem1}
|
|
1190
1276
|
2. 读取 plan.md 了解全局任务划分和依赖关系
|
|
@@ -1213,7 +1299,8 @@ ${taskList}
|
|
|
1213
1299
|
- 最后一个 Wave 完成后做一次全量编译验证
|
|
1214
1300
|
- 用户明确要求编译时
|
|
1215
1301
|
4. 每个任务完成后:
|
|
1216
|
-
-
|
|
1302
|
+
- **使用 \`sillyspec wt-commit --change <change-name> -- <task-files>\` 串行提交**(坑 wt-parallel-commit-race:同一 Wave 多子代理共享 worktree,裸 \`git add -A\` 互卷 WIP / 撞 index.lock;\`git add -A\` 还会把并行子代理未提交的半成品一并暂存——**禁 \`git add -A\`**;sillyspec wt-commit 自动处理分支切换与序列化,无需手动 git 操作;未 commit 的新文件不在 base commit 也不在 index,apply --3way 报 "does not exist in index")
|
|
1303
|
+
- commit 后 base..HEAD diff 完整、apply 顺畅,review.json 的 head 也有真实锚点
|
|
1217
1304
|
- **写 review.json 即可**(checkbox 由 CLI 自动勾选,见下方 Task Review Gate)
|
|
1218
1305
|
- **任务边界上报(每任务一次,主仓根目录)**:review write 落盘(CLI 已自动勾选)后跑一次 \`sillyspec platform sync --change <change-name>\`——以任务粒度把「最后信号」(last_pushed_at)与 tasks.md 勾选状态推上平台(变更中心「进行中」可见性);未连接平台时该命令静默跳过,无需先检查连接状态
|
|
1219
1306
|
- **既跑 lint check 也跑 formatter**:凡变更涉及的源码跑项目的 lint 检查 **和** 格式化(如 \`ruff format\` / \`prettier --write\`),不要只跑 check——只 check 不 format 会把格式问题留到 commit 时被 pre-commit hook 拦截(worktree 内二进制可能缺失,先 \`which <bin>\` 确认,缺则 \`uv tool install\` / \`uv sync\`)
|
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'
|
|
@@ -223,8 +225,8 @@ export async function cmdValidate(dir, args, opts = {}) {
|
|
|
223
225
|
|
|
224
226
|
for (const mdFile of allMdFiles) {
|
|
225
227
|
if (mdFile === 'INDEX.md') continue
|
|
226
|
-
// generated/ 和 proposed/ 区不强制要求 INDEX
|
|
227
|
-
if (mdFile.startsWith('generated/') || mdFile.startsWith('proposed/')) continue
|
|
228
|
+
// generated/ 和 proposed/ 区不强制要求 INDEX 注册;uncategorized.md 是暂存区(条目归类即迁出,语义上永不注册 INDEX)
|
|
229
|
+
if (mdFile.startsWith('generated/') || mdFile.startsWith('proposed/') || mdFile === 'uncategorized.md') continue
|
|
228
230
|
if (!indexedFiles.has(mdFile)) {
|
|
229
231
|
warnings.push({
|
|
230
232
|
code: 'unregistered_file',
|
|
@@ -498,6 +500,29 @@ function extractSections(content) {
|
|
|
498
500
|
|
|
499
501
|
// ── 入口路由 ──
|
|
500
502
|
|
|
503
|
+
/**
|
|
504
|
+
* 动态加载 classify / stats 子命令实现(2026-09-14-knowledge-loop-close)。
|
|
505
|
+
*
|
|
506
|
+
* 实现文件由 Wave 2/3 的 task-02(src/knowledge-classify.js)/ task-05(src/knowledge-stats.js)交付,
|
|
507
|
+
* 落地前可能缺席——静态 import 会断链整个 knowledge 命令,故循 index.js case 'knowledge'
|
|
508
|
+
* 先例用动态 import;模块缺席时返回 null 由调用方输出友好错误,其余异常照抛。
|
|
509
|
+
* 实现侧须导出 CLI 入口 cmdKnowledgeClassify / cmdKnowledgeStats(dir, args, opts),
|
|
510
|
+
* 形态对齐本文件 cmdSearch 等既有子命令。
|
|
511
|
+
*
|
|
512
|
+
* 遥测底座:两命令的归类/命中审计统一经 src/knowledge-hits.js 的
|
|
513
|
+
* appendKnowledgeHit / readKnowledgeHits(KnowledgeHitsAPI 契约,task-02/04/05 消费)。
|
|
514
|
+
*/
|
|
515
|
+
async function tryImportSubcommandImpl(importPath, moduleName) {
|
|
516
|
+
try {
|
|
517
|
+
return await import(importPath)
|
|
518
|
+
} catch (err) {
|
|
519
|
+
if (err && err.code === 'ERR_MODULE_NOT_FOUND' && String(err.message || '').includes(moduleName)) {
|
|
520
|
+
return null
|
|
521
|
+
}
|
|
522
|
+
throw err
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
501
526
|
/**
|
|
502
527
|
* @param {string[]} args - filteredArgs.slice(1)(去掉 'knowledge')
|
|
503
528
|
* @param {string} dir - 项目根目录
|
|
@@ -517,11 +542,35 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
517
542
|
return cmdRefresh(dir, args.slice(1), opts)
|
|
518
543
|
case 'propose':
|
|
519
544
|
return cmdPropose(dir, args.slice(1), opts)
|
|
545
|
+
case 'classify': {
|
|
546
|
+
const mod = await tryImportSubcommandImpl('../knowledge-classify.js', 'knowledge-classify.js')
|
|
547
|
+
if (!mod || typeof mod.cmdKnowledgeClassify !== 'function') {
|
|
548
|
+
output(false, {}, {
|
|
549
|
+
code: 'not_implemented',
|
|
550
|
+
subcommand: 'classify',
|
|
551
|
+
message: '实现未落地(task-02 交付 src/knowledge-classify.js)',
|
|
552
|
+
})
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
return mod.cmdKnowledgeClassify(dir, args.slice(1), opts)
|
|
556
|
+
}
|
|
557
|
+
case 'stats': {
|
|
558
|
+
const mod = await tryImportSubcommandImpl('../knowledge-stats.js', 'knowledge-stats.js')
|
|
559
|
+
if (!mod || typeof mod.cmdKnowledgeStats !== 'function') {
|
|
560
|
+
output(false, {}, {
|
|
561
|
+
code: 'not_implemented',
|
|
562
|
+
subcommand: 'stats',
|
|
563
|
+
message: '实现未落地(task-05 交付 src/knowledge-stats.js)',
|
|
564
|
+
})
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
return mod.cmdKnowledgeStats(dir, args.slice(1), opts)
|
|
568
|
+
}
|
|
520
569
|
default:
|
|
521
570
|
output(false, {}, {
|
|
522
571
|
code: 'unknown_subcommand',
|
|
523
572
|
subcommand: subCommand,
|
|
524
|
-
available: ['search', 'inspect', 'validate', 'refresh', 'propose'],
|
|
573
|
+
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'stats'],
|
|
525
574
|
})
|
|
526
575
|
}
|
|
527
576
|
}
|