sillyspec 3.30.0 → 3.32.0
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-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +10 -6
- package/.claude/skills/sillyspec-quick/SKILL.md +116 -18
- package/.zcodeignore +66 -0
- package/AGENTS.md +15 -15
- package/assets/command-cards/flow.md +9 -3
- package/assets/command-cards/run-quick.md +4 -1
- package/package.json +2 -2
- package/src/agent-session-log.js +27 -5
- package/src/config-schema.js +21 -0
- package/src/datetime.js +51 -0
- package/src/db.js +10 -3
- package/src/decision-distill.js +21 -12
- package/src/design-facts.js +46 -6
- package/src/flow-draft.js +468 -95
- package/src/flow-parity.js +62 -4
- package/src/flow-review.js +136 -6
- package/src/flow.js +729 -125
- package/src/foreign-declared.js +71 -3
- package/src/fr-index.js +148 -162
- package/src/git-helper.js +16 -3
- package/src/hooks/worktree-guard.js +16 -5
- package/src/hunk-attribution.js +28 -5
- package/src/index.js +184 -17
- package/src/knowledge-classify.js +3 -1
- package/src/knowledge-digest.js +55 -26
- package/src/knowledge-match.js +135 -7
- package/src/knowledge-stats.js +66 -4
- package/src/knowledge-vector.js +160 -0
- package/src/machine-interface.js +90 -2
- package/src/module-impact.js +22 -1
- package/src/modules.js +9 -0
- package/src/progress/change-registry.js +20 -0
- package/src/progress/stage-machine.js +4 -7
- package/src/progress.js +8 -1
- package/src/quicklog.js +20 -0
- package/src/redomain.js +23 -7
- package/src/route-hindsight.js +276 -0
- package/src/run/archive-distill.js +3 -1
- package/src/run/command.js +65 -12
- package/src/run/complete-handlers.js +219 -5
- package/src/run/complete.js +77 -11
- package/src/run/gate-snapshot.js +15 -0
- package/src/run/gates.js +60 -4
- package/src/run/prompt.js +159 -31
- package/src/run/quick-audit.js +66 -7
- package/src/run/shared.js +6 -11
- package/src/run/stage.js +5 -1
- package/src/run/verify-quality-scan.js +84 -0
- package/src/scan-facts.js +2 -1
- package/src/scope-audit.js +9 -2
- package/src/sentinel-assertions.js +67 -16
- package/src/stage-contract-engine.js +4 -1
- package/src/stage-contract-spec.js +103 -4
- package/src/stage-contract.js +55 -7
- package/src/stages/brainstorm.js +27 -14
- package/src/stages/knowledge.js +4 -3
- package/src/stages/plan-postcheck.js +98 -3
- package/src/stages/plan.js +22 -2
- package/src/stages/verify.js +2 -1
- package/src/task-review.js +1 -1
- package/src/task-tick.js +121 -0
- package/src/taskcard-frontmatter.js +31 -0
- package/src/ui-visual.js +43 -8
- package/src/verify-postcheck.js +509 -37
- package/src/verify-probes.js +216 -5
- package/src/watcher.js +1067 -1031
- package/src/worktree-apply.js +9 -1
- package/src/worktree.js +21 -6
- package/templates/agents-instruction.md +15 -15
- package/templates/prompts/taskcard-rules.md +1 -0
package/src/hunk-attribution.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* 三类信号(诚实口径:机器无法判定「哪行是谁的」,门的目标是把静默混合变成显式中断):
|
|
10
10
|
* ① 未归因文件:提交面文件不在本变更声明面(design 清单 ∪ requirements 测试绑定,
|
|
11
11
|
* 复用 parseFileChangeList / extractRequirementBindings / testAnchorFile 同源解析);
|
|
12
|
+
* 其中「窗口内全部提交均属他侧变更名」的文件改判他侧归因(提交事实,2026-10-05-
|
|
13
|
+
* hunk-gate-commit-attribution:归档他侧变更声明面不可见致误报——信息行不计数 ok 面);
|
|
12
14
|
* ② 跨变更竞争:其他活跃变更的声明面与提交面相交——同文件无法按行归属,显式列出
|
|
13
15
|
* 竞争文件、对方变更名与 hunk 数,收口前人工核对;
|
|
14
16
|
* ③ 在途残留:提交面文件当前工作树仍有未提交 diff——活跃并发 WIP 信号。
|
|
@@ -115,7 +117,7 @@ function collectWorkingTreeModified(gitFn, cwd) {
|
|
|
115
117
|
* contended: Array, residue: Array, notes: string[] }}
|
|
116
118
|
*/
|
|
117
119
|
export async function runHunkAttributionGate({ cwd, specBase, changeName, baselineCommit, committedFiles, gitFn, gate = 'warn' }) {
|
|
118
|
-
const result = { ok: true, degraded: false, gate, faceCount: 0, unattributed: [], contended: [], residue: [], notes: [] }
|
|
120
|
+
const result = { ok: true, degraded: false, gate, faceCount: 0, unattributed: [], foreignByCommit: [], contended: [], residue: [], notes: [] }
|
|
119
121
|
if (gate === 'off') {
|
|
120
122
|
result.notes.push('hunk_gate=off——归属门关闭')
|
|
121
123
|
return result
|
|
@@ -134,8 +136,26 @@ export async function runHunkAttributionGate({ cwd, specBase, changeName, baseli
|
|
|
134
136
|
|
|
135
137
|
// ① 未归因文件(声明面全空时降为提示,不指认夹带——advisory 同口径)
|
|
136
138
|
if (declared.size > 0) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
const undeclared = deliverables.filter((f) => !declared.has(f))
|
|
140
|
+
// 他侧归因改判(提交事实,2026-10-05-hunk-gate-commit-attribution):collectForeignDeclarations
|
|
141
|
+
// 只扫活跃变更(排 archive/)——归档他侧交付落本窗口时报「未归因」,指引「pathspec 隔离/
|
|
142
|
+
// 补 design 自声明」双误导(F-4 实证:他侧 546720ea 已归档交付被指为在途)。按提交 message
|
|
143
|
+
// 变更名归属(commitAttributionForChange+isForeignByCommit,与 patch 冻结 filterCommittedFace
|
|
144
|
+
// 同源):窗口内全部提交均属他侧变更名 → 他侧归因(ℹ️ 信息行、不计 ok 阻断面);
|
|
145
|
+
// 裸提交/本变更名提交触碰的文件保持未归因(fail-closed 原口径)。切分不可得(非 git/
|
|
146
|
+
// git 失败 → null)保持全量未归因,不放宽。
|
|
147
|
+
let attr = null
|
|
148
|
+
let isForeign = null
|
|
149
|
+
try {
|
|
150
|
+
const m = await import('./foreign-declared.js')
|
|
151
|
+
attr = m.commitAttributionForChange(cwd, specBase, changeName, baselineCommit)
|
|
152
|
+
isForeign = m.isForeignByCommit
|
|
153
|
+
} catch { /* 同源切分不可得 → 原口径 */ }
|
|
154
|
+
for (const f of undeclared) {
|
|
155
|
+
const entry = attr ? attr.get(f) : null
|
|
156
|
+
if (isForeign && isForeign(entry, changeName)) {
|
|
157
|
+
result.foreignByCommit.push({ file: f, owners: [...entry.owners] })
|
|
158
|
+
} else {
|
|
139
159
|
result.unattributed.push({ file: f, hunks: countHunks(gitFn, cwd, baselineCommit, f) })
|
|
140
160
|
}
|
|
141
161
|
}
|
|
@@ -172,8 +192,11 @@ export function renderHunkAttributionLines(r) {
|
|
|
172
192
|
for (const n of (r && r.notes) || []) L.push(`- ℹ️ ${n}`)
|
|
173
193
|
return L
|
|
174
194
|
}
|
|
175
|
-
const attributed = r.faceCount - r.unattributed.length
|
|
176
|
-
L.push(`- ℹ️ 提交面 ${r.faceCount} 个交付文件对账:声明面归因 ${attributed}、未归因 ${r.unattributed.length}、跨变更竞争 ${r.contended.length}、在途残留 ${r.residue.length}`)
|
|
195
|
+
const attributed = r.faceCount - r.unattributed.length - (r.foreignByCommit || []).length
|
|
196
|
+
L.push(`- ℹ️ 提交面 ${r.faceCount} 个交付文件对账:声明面归因 ${attributed}、他侧归因(提交事实)${(r.foreignByCommit || []).length}、未归因 ${r.unattributed.length}、跨变更竞争 ${r.contended.length}、在途残留 ${r.residue.length}`)
|
|
197
|
+
for (const fb of (r.foreignByCommit || [])) {
|
|
198
|
+
L.push(`- ℹ️ 他侧归因(提交事实):\`${fb.file}\` 窗口提交全部属 ${fb.owners.join('、')}——已归档他侧交付非本变更未归因,patch 冻结面同口径剔除`)
|
|
199
|
+
}
|
|
177
200
|
for (const u of r.unattributed) {
|
|
178
201
|
L.push(`- ⚠️ 未归因:\`${u.file}\`(${u.hunks >= 0 ? `${u.hunks} hunk` : 'hunk 数不可得'})不在 design 清单与 requirements 测试绑定——属并行会话在途交付则须显式 pathspec 隔离并协调归属;属本变更则补 design 自声明`)
|
|
179
202
|
}
|
package/src/index.js
CHANGED
|
@@ -168,7 +168,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
168
168
|
示例:
|
|
169
169
|
sillyspec init
|
|
170
170
|
sillyspec run brainstorm --change 2026-07-03-add-login
|
|
171
|
-
sillyspec flow start --change my-fix --input "<动机与背景+独立一行『成功标准:』+每行一条『- 可验证标准』>" # 轻量变更(默认快道,2
|
|
171
|
+
sillyspec flow start --change 2026-07-03-my-fix --input "<动机与背景+独立一行『成功标准:』+每行一条『- 可验证标准』>" # 轻量变更(默认快道,2 次调用;变更名须 YYYY-MM-DD-<简短描述> 日期前缀)
|
|
172
172
|
sillyspec run verify --done --output "验证通过,测试全绿"
|
|
173
173
|
sillyspec run archive --done --confirm --output "归档完成"
|
|
174
174
|
sillyspec run plan --reopen --from-step 2 # 修订 plan,从第 2 步重做
|
|
@@ -221,7 +221,12 @@ function refuseRetiredQuickFreshStart(stageArgs) {
|
|
|
221
221
|
})()
|
|
222
222
|
if (passThrough) return false
|
|
223
223
|
console.error('❌ quick 通道已退役,不再接受新会话(v3.30.0 起)。')
|
|
224
|
-
|
|
224
|
+
// 可照抄实例(2026-10-05-input-teach-copyable):引号内换行合法,行级 trim 容忍缩进
|
|
225
|
+
console.error(' 新工作请走轻量变更(--input 引号内换行合法,下例可照抄):')
|
|
226
|
+
console.error(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
|
|
227
|
+
console.error('')
|
|
228
|
+
console.error(' 成功标准:')
|
|
229
|
+
console.error(' - <可验证标准>"')
|
|
225
230
|
console.error(' 升级前进行中的 quick 会话仍可收尾:sillyspec run quick --change <会话ID> 续跑 / --done 收口 / --cancel 取消。')
|
|
226
231
|
process.exit(1)
|
|
227
232
|
}
|
|
@@ -559,7 +564,7 @@ async function main() {
|
|
|
559
564
|
const gateChangeIdx = args.indexOf('--change');
|
|
560
565
|
const gateChange = gateChangeIdx >= 0 && args[gateChangeIdx + 1] && !String(args[gateChangeIdx + 1]).startsWith("--") ? args[gateChangeIdx + 1] : null;
|
|
561
566
|
if (!gateStage || gateStage.startsWith('-') || !gateChange) {
|
|
562
|
-
console.error('用法: sillyspec gate <stage> --change <name> [--json]\n stage: brainstorm | plan | execute | verify | archive |
|
|
567
|
+
console.error('用法: sillyspec gate <stage|last> --change <name> [--json] [--full|--docs-only]\n stage: brainstorm | plan | execute | verify | archive | ...\n --full 覆盖 --done 独有贵检查(reconcile/stage review 预检);--docs-only 轻量档(verify)跳过 test/lint 执行只核文档契约面;两者互斥\n gate last --change <name>:读上轮 verify --done 阻断稳定锚点(gate-last-<change>.json)并打印 blocked 明细,不重跑检查');
|
|
563
568
|
process.exit(2);
|
|
564
569
|
}
|
|
565
570
|
// 与 run 入口同源消毒(防路径穿越;gate 下游拼 marker/changes 路径)
|
|
@@ -573,6 +578,16 @@ async function main() {
|
|
|
573
578
|
// P1 --full 只读预检档(batch3 task-03):覆盖 --done 独有检查面(reconcile/stage review)——
|
|
574
579
|
// 预检过=必过(同因),省一轮 gate 失败重跑的整 agent 回合
|
|
575
580
|
if (args.includes('--full')) gateOpts.full = true;
|
|
581
|
+
// --docs-only 轻量预检档(2026-10-06-verify-docs-prefill task-01):跳过 verify-test/lint
|
|
582
|
+
// 执行只核文档契约面(毫秒级)——纯文档缺项(visual-evidence/结论槽/移交项)一次列清再
|
|
583
|
+
// --done,免逐轮撞门。与 --full 互斥(一个减执行检查一个加检查面,并存语义矛盾)。
|
|
584
|
+
if (args.includes('--docs-only')) {
|
|
585
|
+
if (args.includes('--full')) {
|
|
586
|
+
console.error('❌ --docs-only 与 --full 互斥——docs-only 跳过 test/lint 执行只核文档契约面,full 追加 --done 独有的贵检查;两者并存语义矛盾,按需择一。');
|
|
587
|
+
process.exit(2);
|
|
588
|
+
}
|
|
589
|
+
gateOpts.docsOnly = true;
|
|
590
|
+
}
|
|
576
591
|
// A4:平台指针 / 显式 --spec-dir / 本地 fallback 三合一解析 specBase——平台模式(指针指向外部
|
|
577
592
|
// specDir)下 gate 也能核验(原只传 cwd 致 runGate 的 resolveSpecDir 指向本地孤儿库,恒
|
|
578
593
|
// 「无法核验」exit 2)。resolvePlatformSpecDir 指针失效时抛 PointerUnreachableError,由顶层 catch 优雅 fail-closed。
|
|
@@ -597,6 +612,41 @@ async function main() {
|
|
|
597
612
|
} catch (e) {
|
|
598
613
|
console.warn(`⚠️ gate ctx 构造失败,降级单仓核验(${e.message})`);
|
|
599
614
|
}
|
|
615
|
+
// ── gate last(2026-10-06-verify-friction-fix task-02)── 读 verify-runs 稳定锚点:
|
|
616
|
+
// writeVerifyGatePointer(quick-D)只写不读,看上轮 blocked 原因只能重跑全量 gate
|
|
617
|
+
// (postmortem 实证 --json 超时转后台 + task id 抄错绕 3 轮)或手翻两层 JSON。此处
|
|
618
|
+
// 直读指针 + 其指向取证目录的 reconcile/probe-consistency 明细;exit code 反映是否存在
|
|
619
|
+
// 阻断(无记录/未阻断=0,blocked=1),只读不写不重跑。
|
|
620
|
+
if (gateStage === 'last') {
|
|
621
|
+
const { summarizeVerifyGatePointer } = await import('./machine-interface.js');
|
|
622
|
+
const summary = summarizeVerifyGatePointer({ specBase: gateSpecBase || gateOpts.specBase || dir, changeName: gateChange });
|
|
623
|
+
if (json) {
|
|
624
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
625
|
+
} else if (!summary.found) {
|
|
626
|
+
console.log(`📭 无 gate-last 记录(${summary.pointerPath})——该变更尚未发生 verify --done 阻断落锚。`);
|
|
627
|
+
} else if (summary.unreadable) {
|
|
628
|
+
console.error(`❌ gate-last 指针不可读: ${summary.pointerPath}(${summary.error || 'JSON 损坏'})——删除该文件后重跑一次 verify --done 重建锚点。`);
|
|
629
|
+
} else {
|
|
630
|
+
console.log(`📌 gate-last [${gateChange}] blocked=${summary.blocked}${summary.note ? `(${summary.note})` : ''} written_at=${summary.written_at || '?'}`);
|
|
631
|
+
console.log(` 最新取证目录: ${summary.run_dir_resolved || summary.latest_run_dir || '?'}`);
|
|
632
|
+
if (summary.reconcile) {
|
|
633
|
+
const rc = summary.reconcile;
|
|
634
|
+
console.log(` target_files 对账: status=${rc.status} missing=${(rc.missing || []).length} undeclared=${(rc.undeclared || []).length} matched=${(rc.matched || []).length}`);
|
|
635
|
+
for (const m of (rc.missing || []).slice(0, 10)) console.log(` ✗ missing: ${typeof m === 'string' ? m : JSON.stringify(m)}`);
|
|
636
|
+
for (const u of (rc.undeclared || []).slice(0, 10)) console.log(` ⚠ undeclared: ${typeof u === 'string' ? u : JSON.stringify(u)}`);
|
|
637
|
+
if ((rc.missing || []).length > 10) console.log(` …另有 ${rc.missing.length - 10} 条 missing,直读 ${summary.run_dir_resolved || summary.latest_run_dir}`);
|
|
638
|
+
}
|
|
639
|
+
if (summary.probeConsistency) {
|
|
640
|
+
const pc = summary.probeConsistency;
|
|
641
|
+
const mm = pc.mismatches || [];
|
|
642
|
+
console.log(` 探针一致性抽查: status=${pc.status} severity=${pc.result_severity || pc.severity || '?'} mismatches=${mm.length}`);
|
|
643
|
+
for (const m of mm.slice(0, 10)) console.log(` ✗ 探针${m && m.probe ? m.probe : '?'}: ${(m && (m.note || m.expected)) ? String(m.note || m.expected) : JSON.stringify(m)}`);
|
|
644
|
+
if (pc.skip_reason) console.log(` skip: ${pc.skip_reason}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
process.exitCode = summary.found && !summary.unreadable && summary.blocked ? 1 : 0;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
600
650
|
const { envelope, exitCode } = await withJsonOutput(json, () => runGate(gateStage, gateChange, gateOpts));
|
|
601
651
|
if (json) {
|
|
602
652
|
process.stdout.write(JSON.stringify(envelope));
|
|
@@ -877,6 +927,11 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
877
927
|
}
|
|
878
928
|
}
|
|
879
929
|
} catch { /* 注入 best-effort */ }
|
|
930
|
+
} else if (taskSub === 'tick') {
|
|
931
|
+
// ── 轻量勾选动词(2026-10-03-voluntary-task-tick):翻格+进度回显+下一任务指针——
|
|
932
|
+
// 纯反馈闭环(watcher task-done 事件自动上平台),自愿使用;幂等,无 review/verdict 仪式。──
|
|
933
|
+
const { runTaskTick } = await import('./task-tick.js')
|
|
934
|
+
await runTaskTick({ changeName: tChange, cwd: dir, taskId: tTask, specBase: tSpecBase })
|
|
880
935
|
} else if (taskSub === 'done') {
|
|
881
936
|
// ── task done 四合一(r5l-forensic-verdict 方案 1 / 评审护栏#3):13 任务 × 4 连收尾
|
|
882
937
|
// 往返(review write+勾选 / finish / wt-commit)合并为单进程串行 + 合并输出——每次省
|
|
@@ -1262,16 +1317,24 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1262
1317
|
const vpForce = args.includes('--force');
|
|
1263
1318
|
const vpDraft = args.includes('--draft');
|
|
1264
1319
|
const vpAmendDraft = args.includes('--amend-draft');
|
|
1320
|
+
// --refresh-probes(2026-10-06-verify-friction-fix task-01):定向刷新通道——只刷新仍含
|
|
1321
|
+
// 待填占位的探针机械段(表格已填行按行携载),已手填段保留;与 --force 互斥(force 是
|
|
1322
|
+
// 显式全量重置通道,refresh 是保手填通道,并存语义矛盾拒跑)。
|
|
1323
|
+
const vpRefresh = args.includes('--refresh-probes');
|
|
1265
1324
|
if (vpDraft && !vpInit) {
|
|
1266
1325
|
console.error('❌ --draft 需与 --init 同用(draft 是骨架生成的机器起草模式)——sillyspec verify-probes --change <名> --init --draft');
|
|
1267
1326
|
process.exit(2);
|
|
1268
1327
|
}
|
|
1328
|
+
if (vpRefresh && vpForce) {
|
|
1329
|
+
console.error('❌ --refresh-probes 与 --force 互斥——refresh 只刷新未手填的探针段(保手填),force 是全量重置(自动备份后覆盖);两者并存语义矛盾。');
|
|
1330
|
+
process.exit(2);
|
|
1331
|
+
}
|
|
1269
1332
|
if (!vpChange) {
|
|
1270
|
-
console.error('用法: sillyspec verify-probes --change <name> [--init [--force]] [--draft] [--amend-draft] [--json] [--spec-dir <path>]\n 跑机械探针(TODO 标记/测试覆盖/API 契约/删除对账)输出 markdown;--init 生成 verify-result.md
|
|
1333
|
+
console.error('用法: sillyspec verify-probes --change <name> [--init [--force]] [--refresh-probes] [--draft] [--amend-draft] [--json] [--spec-dir <path>]\n 跑机械探针(TODO 标记/测试覆盖/API 契约/删除对账)输出 markdown;--init 生成 verify-result.md 骨架(探针预填,已存在不覆盖);--force 覆盖重生成全骨架(覆盖前自动备份到 .runtime/verify-runs/ 并打印路径);\n --refresh-probes 定向刷新:只替换仍含待填占位的探针预填段(表格已填行携载保留),已手填段不动——预填段过期但手填内容舍不得时的安全刷新通道(同样先自动备份);\n --init --draft 填槽模式(r5l 方案3):任务完成度/风险等级/测试结果/决策追踪四节机器预填完整句子+指纹防篡改,agent 只填三处 <!--AGENT:--> 槽(结论枚举/移交项/审查叙述);\n --amend-draft 留痕重锚:确要修改机器预填段时重算指纹并记 sidecar 审计(--done 篡改门禁的显式例外通道)');
|
|
1271
1334
|
process.exit(2);
|
|
1272
1335
|
}
|
|
1273
1336
|
assertSafeChangeName(vpChange, '--change 变更名');
|
|
1274
|
-
const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts, formatPlatformPathNote } = await import('./verify-probes.js');
|
|
1337
|
+
const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts, formatPlatformPathNote, backupVerifyResult, refreshProbeSections } = await import('./verify-probes.js');
|
|
1275
1338
|
// spec 根统一走漂移锚定(坑 worktree-spec-artifact-misplace):在 worktree 内跑时锚回主仓,
|
|
1276
1339
|
// 探针读取与 --init 骨架都落主仓——与 plan/execute/verify/archive 的 command.js 守卫同口径。
|
|
1277
1340
|
// resolvePlatformSpecDir 仍先调(保留平台接管 fail-closed 检查副作用),但仅 pointer 存在时
|
|
@@ -1317,6 +1380,37 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1317
1380
|
break;
|
|
1318
1381
|
}
|
|
1319
1382
|
console.log(renderVerifyProbesReport(vpResult));
|
|
1383
|
+
// ── --refresh-probes 定向刷新(2026-10-06-verify-friction-fix task-01)── 独立于 --init:
|
|
1384
|
+
// 预填段过期但手填内容舍不得时的安全刷新通道。先自动备份,再只替换仍含待填占位的探针
|
|
1385
|
+
// 机械段(表格已填行按行携载),已手填段原样保留并逐段报告。刷新后 enricher 不在此跑
|
|
1386
|
+
// (那些是 --init 的幂等补段面,与探针段刷新正交;需要时再跑 --init,已存在 no-op 安全)。
|
|
1387
|
+
if (vpRefresh) {
|
|
1388
|
+
const vpReportPath = join(vpSpecBase, 'changes', vpChange, 'verify-result.md');
|
|
1389
|
+
if (!existsSync(vpReportPath)) {
|
|
1390
|
+
console.error(`❌ 无 verify-result.md 可刷新: ${vpReportPath}——先 sillyspec verify-probes --change ${vpChange} --init 生成骨架`);
|
|
1391
|
+
process.exit(2);
|
|
1392
|
+
}
|
|
1393
|
+
const { resolveRuntimeRoot: vpRefreshRTR } = await import('./run/shared.js');
|
|
1394
|
+
const vpBackupPath = backupVerifyResult({ mdPath: vpReportPath, runtimeRoot: vpRefreshRTR({}, vpSpecBase), changeName: vpChange });
|
|
1395
|
+
const vpOld = readFileSync(vpReportPath, 'utf8');
|
|
1396
|
+
const vpRef = refreshProbeSections(vpOld, renderVerifyProbesReport(vpResult));
|
|
1397
|
+
if (vpRef.text !== vpOld) {
|
|
1398
|
+
writeFileSync(vpReportPath, vpRef.text);
|
|
1399
|
+
await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpRef.text);
|
|
1400
|
+
}
|
|
1401
|
+
console.log(`\n🔄 --refresh-probes 定向刷新完成: ${vpReportPath}${vpPlatformNote}`);
|
|
1402
|
+
if (vpBackupPath) console.log(`🗄️ 刷新前已自动备份: ${vpBackupPath}`);
|
|
1403
|
+
const fmt = (arr) => arr.length > 0 ? arr.map(n => `探针${n}`).join('、') : '无';
|
|
1404
|
+
console.log(` 替换(仍含待填占位的机械段): ${fmt(vpRef.replaced)};保留(已手填/无占位,原样不动): ${fmt(vpRef.kept)};新注入: ${fmt(vpRef.appended)};表格已填行携载 ${vpRef.carriedRows} 行`);
|
|
1405
|
+
if (vpRef.kept.length > 0) {
|
|
1406
|
+
console.log(` 保留段如确需强刷(预填过期且确认无手填内容),用 --init --force(自动备份后全量重生成)。`);
|
|
1407
|
+
}
|
|
1408
|
+
try {
|
|
1409
|
+
const { triggerSync } = await import('./run/shared.js');
|
|
1410
|
+
triggerSync(dir, vpChange, { specRoot: vpSpecBase });
|
|
1411
|
+
} catch { /* 同步触发失败不阻断探针命令 */ }
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1320
1414
|
if (vpInit) {
|
|
1321
1415
|
const vpReportPath = join(vpSpecBase, 'changes', vpChange, 'verify-result.md');
|
|
1322
1416
|
if (existsSync(vpReportPath)) {
|
|
@@ -1335,12 +1429,21 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1335
1429
|
await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpInjected);
|
|
1336
1430
|
console.log(`\n📄 存量旧格式 verify-result.md 已补注入探针预填段: ${vpReportPath}(正文其余未动;补注后按新预填段如实核对结论)${vpPlatformNote}`);
|
|
1337
1431
|
} else if (vpForce) {
|
|
1338
|
-
// quick-B:--force 覆盖重生成全骨架(含接口矩阵等 CLI
|
|
1339
|
-
//
|
|
1432
|
+
// quick-B:--force 覆盖重生成全骨架(含接口矩阵等 CLI 预填段)——手填内容会重置。
|
|
1433
|
+
// 2026-10-06-verify-friction-fix:覆盖前自动备份(postmortem 实证 38 格手填复核成果
|
|
1434
|
+
// 被清后只能靠对话记录重建——「从 git 或备份找回」的话术备份此前并不存在);备份
|
|
1435
|
+
// 失败时醒目提示手动复制再重跑(fail-soft 但不静默)。
|
|
1436
|
+
const { resolveRuntimeRoot: vpForceRTR } = await import('./run/shared.js');
|
|
1437
|
+
const vpBackupPath = backupVerifyResult({ mdPath: vpReportPath, runtimeRoot: vpForceRTR({}, vpSpecBase), changeName: vpChange });
|
|
1340
1438
|
const vpSkeleton = generateVerifyResultSkeleton(vpResult);
|
|
1341
1439
|
writeFileSync(vpReportPath, vpSkeleton);
|
|
1342
1440
|
await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpSkeleton);
|
|
1343
|
-
|
|
1441
|
+
if (vpBackupPath) {
|
|
1442
|
+
console.log(`\n🗄️ --force 覆盖前已自动备份: ${vpBackupPath}(手填内容被重置时从这里找回回填)`);
|
|
1443
|
+
} else {
|
|
1444
|
+
console.error(`\n⚠️ 自动备份失败——若 verify-result.md 含手填内容,请立即手动复制保存后重跑本命令。`);
|
|
1445
|
+
}
|
|
1446
|
+
console.log(`\n📄 --force 已重生成 verify-result.md 骨架: ${vpReportPath}(探针与预填段已刷新;⚠️ 手填的结论/移交项等已被重置——需保留的内容从上方备份找回后回填)${vpPlatformNote}`);
|
|
1344
1447
|
} else {
|
|
1345
1448
|
console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}${vpPlatformNote}(预填段过期需刷新时用 --init --force)`);
|
|
1346
1449
|
}
|
|
@@ -1620,13 +1723,35 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
|
|
|
1620
1723
|
case 'module-impact': {
|
|
1621
1724
|
// archive 模块影响矩阵骨架(2026-08-21 审计第三批 D3):文件×模块归属按 _module-map.yaml
|
|
1622
1725
|
// paths 前缀匹配预填(机械),影响类型/review 标记留 <!--TODO-->(语义)。已存在不覆盖。
|
|
1726
|
+
// --fill-skipped(2026-10-07-flow-friction-batch3 task-04):pending 死信一键回填——verify
|
|
1727
|
+
// 硬拦「更新结果」pending/待办行后 agent 实测写一次性脚本批量回填,CLI 化(与门禁
|
|
1728
|
+
// extractPendingDocSyncRows 同源解析口径)。
|
|
1623
1729
|
const miChangeIdx = args.indexOf('--change');
|
|
1624
1730
|
const miChange = miChangeIdx >= 0 && args[miChangeIdx + 1] && !String(args[miChangeIdx + 1]).startsWith("--") ? args[miChangeIdx + 1] : null;
|
|
1625
1731
|
if (!miChange) {
|
|
1626
|
-
console.error('用法: sillyspec module-impact --change <name> [--json] [--spec-dir <path>]\n 生成 module-impact.md 骨架(模块影响矩阵按 module-map 预填 +
|
|
1732
|
+
console.error('用法: sillyspec module-impact --change <name> [--json] [--spec-dir <path>]\n 生成 module-impact.md 骨架(模块影响矩阵按 module-map 预填 + 未匹配文件清单;已存在不覆盖)\n sillyspec module-impact --change <name> --fill-skipped [--reason "<一句话原因>"]\n 「更新结果」表 pending/待办行一键回填 skipped(--reason 追加进操作列——对齐文件内规则「确定不同步的行改 skipped 并在操作列写明原因」);done 行与其余内容逐字不动,幂等');
|
|
1627
1733
|
process.exit(2);
|
|
1628
1734
|
}
|
|
1629
1735
|
assertSafeChangeName(miChange, '--change 变更名');
|
|
1736
|
+
if (args.includes('--fill-skipped')) {
|
|
1737
|
+
const miSpecBase0 = resolvePlatformSpecDir(dir, specDir) || specDir || join(dir, '.sillyspec');
|
|
1738
|
+
const miPath0 = join(miSpecBase0, 'changes', miChange, 'module-impact.md');
|
|
1739
|
+
const miReasonIdx = args.indexOf('--reason');
|
|
1740
|
+
const miReason = miReasonIdx >= 0 && args[miReasonIdx + 1] && !String(args[miReasonIdx + 1]).startsWith("--") ? args[miReasonIdx + 1] : '';
|
|
1741
|
+
const { fillModuleImpactSkipped } = await import('./run/complete-handlers.js');
|
|
1742
|
+
const miRes = fillModuleImpactSkipped(miPath0, { reason: miReason });
|
|
1743
|
+
if (!miRes.ok) {
|
|
1744
|
+
console.error(`❌ ${miRes.error}`);
|
|
1745
|
+
process.exit(2);
|
|
1746
|
+
}
|
|
1747
|
+
if (miRes.filled === 0) {
|
|
1748
|
+
console.log(`ℹ️ 无 pending/待办行可回填(幂等零改动): ${miPath0}`);
|
|
1749
|
+
} else {
|
|
1750
|
+
console.log(`✅ 已回填 ${miRes.filled} 行 pending/待办 → skipped${miReason ? `(操作列追加——skipped:${miReason})` : '(未给 --reason 只翻状态——建议补真实原因)'}: ${miPath0}`);
|
|
1751
|
+
console.log(` verify 门禁(extractPendingDocSyncRows 同源口径)将不再拦这些行;reason 是审计面,请确认真实。`);
|
|
1752
|
+
}
|
|
1753
|
+
break;
|
|
1754
|
+
}
|
|
1630
1755
|
const { generateModuleImpactSkeleton } = await import('./module-impact.js');
|
|
1631
1756
|
const miResult = generateModuleImpactSkeleton({ cwd: dir, changeName: miChange, specDir });
|
|
1632
1757
|
if (!miResult) {
|
|
@@ -3088,7 +3213,7 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
3088
3213
|
const reason = flag('--reason') || 'spec'
|
|
3089
3214
|
const fail = (msg) => { console.error(`❌ ${msg}`); process.exitCode = 1 }
|
|
3090
3215
|
// repair-paths/--redomain 自带用法门,不走 anchor/change 前置守卫
|
|
3091
|
-
if (!anchor && !change && !has('--repair-paths') && !filteredArgs.slice(1).includes('repair-paths') && !has('--redomain')) { fail('用法: sillyspec tests --anchor <FR-…|ql-…> | --change <名> [--bind|--unbind] [--tests <p1,p2>] [--row-id <id>] [--reason spec|capability|regression] | repair-paths | --redomain --from <域> --to
|
|
3216
|
+
if (!anchor && !change && !has('--repair-paths') && !filteredArgs.slice(1).includes('repair-paths') && !has('--redomain')) { fail('用法: sillyspec tests --anchor <FR-…|ql-…> | --change <名> [--bind|--unbind] [--tests <p1,p2>] [--row-id <id>] [--reason spec|capability|regression] | repair-paths | --redomain --from <域> --to <域>\n 语义:--bind 追加一条新绑定行(不替换旧行;row_id 缺省自动生成 manual:*);--unbind 按 --row-id <id> 删行(或 --tests 路径反查);不带 bind/unbind 只读展示'); break }
|
|
3092
3217
|
// 变更期局部锚(FR-NN 未铸全局)只读拒绝修改——修理工仅面向提升后行
|
|
3093
3218
|
const isLocalAnchor = anchor && /^FR-\d+$/.test(anchor)
|
|
3094
3219
|
if ((has('--bind') || has('--unbind')) && isLocalAnchor) { fail(`变更期局部锚「${anchor}」只读——修理工仅面向提升后的全局锚(FR-<域>-NNN / ql-…)`); break }
|
|
@@ -3162,22 +3287,28 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
3162
3287
|
if (has('--redomain')) {
|
|
3163
3288
|
const fromD = flag('--from')
|
|
3164
3289
|
const toD = flag('--to')
|
|
3165
|
-
|
|
3290
|
+
// --by-change(坑 fr-domain-suggest-typo-and-no-split-migration 缺陷2):unmapped 等
|
|
3291
|
+
// 混合池按「变更:<名>」字段分批迁移——整域迁移会错置大部分条目,来源变更才是分批键。
|
|
3292
|
+
const byChangeD = flag('--by-change')
|
|
3293
|
+
if (!fromD || !toD) { fail('用法: sillyspec tests --redomain --from <源域> --to <目标域> [--anchor <FR-id>] [--by-change <变更名>] [--write]'); break }
|
|
3166
3294
|
// 多 --anchor 收集
|
|
3167
3295
|
const anchorList = []
|
|
3168
3296
|
for (let i = 0; i < filteredArgs.length - 1; i++) if (filteredArgs[i] === '--anchor') anchorList.push(filteredArgs[i + 1])
|
|
3169
3297
|
try {
|
|
3170
3298
|
const { planRedomain, redomainFrEntries } = await import('./redomain.js')
|
|
3171
3299
|
if (!has('--write')) {
|
|
3172
|
-
|
|
3173
|
-
|
|
3300
|
+
// 预览与落盘同口径透传 byChange(坑 redomain-plan-preview-by-change-ignored,
|
|
3301
|
+
// 2026-10-02 实证:76338b4d 收编在途工作时此行漏透传——预览列整域、落盘只迁
|
|
3302
|
+
// 分批子集,用户按预览理解会误判整域迁移)。planRedomain 侧过滤本就在场。
|
|
3303
|
+
const plan = planRedomain({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null, byChange: byChangeD || null })
|
|
3304
|
+
console.log(`🔧 域迁移预览(--write 落盘):fr/${fromD}.md → fr/${toD}.md,${plan.entries.length} 条${byChangeD ? `(仅「变更:${byChangeD}」)` : ''}`)
|
|
3174
3305
|
for (const e of plan.entries.slice(0, 10)) console.log(` ${e.id} ${e.title}`)
|
|
3175
3306
|
if (plan.entries.length > 10) console.log(` … 共 ${plan.entries.length} 条`)
|
|
3176
3307
|
console.log(` 目标域文件${plan.targetExists ? '在场' : '将新建'};源域${plan.sourceWillDelete ? '将删空壳' : '保留余条'};ID 不变(身份保持)`)
|
|
3177
3308
|
break
|
|
3178
3309
|
}
|
|
3179
|
-
const r = redomainFrEntries({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null })
|
|
3180
|
-
console.log(`✅ 域迁移完成:${r.moved.length} 条 fr/${fromD}.md → fr/${toD}.md(ID
|
|
3310
|
+
const r = redomainFrEntries({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null, byChange: byChangeD || null })
|
|
3311
|
+
console.log(`✅ 域迁移完成:${r.moved.length} 条 fr/${fromD}.md → fr/${toD}.md(ID 不变${byChangeD ? `,仅「变更:${byChangeD}」` : ''})`)
|
|
3181
3312
|
for (const m of r.moved.slice(0, 10)) console.log(` ${m.id} ${m.title}`)
|
|
3182
3313
|
if (r.sourceDeleted) console.log(` 源域空壳已删:fr/${fromD}.md`)
|
|
3183
3314
|
if (r.targetCreated) console.log(` 目标域文件新建:fr/${toD}.md`)
|
|
@@ -4355,6 +4486,9 @@ SillySpec taskcard — 生成 Windows 安全的 TaskCard 骨架
|
|
|
4355
4486
|
用法:
|
|
4356
4487
|
sillyspec taskcard <change-name> --task task-01[,task-02...] [--title <t>] [--title-zh <t>] [--set key=value]... [--force]
|
|
4357
4488
|
sillyspec taskcard <change-name> --all [--set key=value]... [--force]
|
|
4489
|
+
sillyspec taskcard <change-name> --validate
|
|
4490
|
+
校验全部任务卡(plan 门禁同源规则:frontmatter YAML 合法性含分诊、必要字段、占位符、
|
|
4491
|
+
target_files 严格形态),失败 exit 1——填卡后先自检,免 plan Step 4 门禁批量报错返工。
|
|
4358
4492
|
|
|
4359
4493
|
骨架由 CLI 直写(LF 行尾 + frontmatter 闭合 + 硬校验 9 字段齐全),标题自动取自 tasks.md
|
|
4360
4494
|
checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";--set 批量注入 design
|
|
@@ -4394,6 +4528,35 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
|
|
|
4394
4528
|
console.error('❌ --all 与 --task 互斥(--all 取 plan.md 全部任务,--task 显式指定)');
|
|
4395
4529
|
process.exit(1);
|
|
4396
4530
|
}
|
|
4531
|
+
// ── --validate(2026-10-06-verify-friction-fix task-03)── plan 门禁同源规则的零成本自检:
|
|
4532
|
+
// 填卡后、plan Step 4 门禁前先跑,把「门禁时一次撞一批」前移成「提交前自检」(厚流程
|
|
4533
|
+
// 实证 6 轮 YAML 门禁试错)。规则同源 plan-postcheck(frontmatter 合法性/必要字段/占位符
|
|
4534
|
+
// /target_files 严格形态),失败 exit 1——此处报的错 --done 门禁同样会拦。
|
|
4535
|
+
if (filteredArgs.includes('--validate')) {
|
|
4536
|
+
if (all || taskVal) {
|
|
4537
|
+
console.error('❌ --validate 与 --all/--task 互斥(--validate 校验变更下全部任务卡)');
|
|
4538
|
+
process.exit(2);
|
|
4539
|
+
}
|
|
4540
|
+
const tcValSpecDir = resolvePlatformSpecDir(dir, specDir) || specDir;
|
|
4541
|
+
const tcValChangeDir = join(tcValSpecDir, 'changes', tcName);
|
|
4542
|
+
if (!existsSync(tcValChangeDir)) {
|
|
4543
|
+
console.error(`❌ 变更目录不存在: ${tcValChangeDir}`);
|
|
4544
|
+
process.exit(2);
|
|
4545
|
+
}
|
|
4546
|
+
const { validateTaskcardsCli } = await import('./stages/plan-postcheck.js');
|
|
4547
|
+
const tcVal = validateTaskcardsCli({ changeDir: tcValChangeDir, projectRoot: dir });
|
|
4548
|
+
if (tcVal.ok) {
|
|
4549
|
+
console.log(`✅ 任务卡校验通过(${tcVal.files} 张卡${tcVal.files === 0 ? '——无 tasks/ 目录(thin 变更无可校验卡)' : tcVal.warnings.length > 0 ? `,${tcVal.warnings.length} 条 warning(不阻断,供审查线索)` : ''})`);
|
|
4550
|
+
for (const w of tcVal.warnings) console.log(` ⚠ ${w}`);
|
|
4551
|
+
} else {
|
|
4552
|
+
console.error(`❌ 任务卡校验失败(${tcVal.files} 张卡,${tcVal.errors.length} 项 error):`);
|
|
4553
|
+
for (const e of tcVal.errors) console.error(` ✗ ${e}`);
|
|
4554
|
+
for (const w of tcVal.warnings) console.error(` ⚠ ${w}`);
|
|
4555
|
+
console.error(`修复后重跑: sillyspec taskcard ${tcName} --validate(这些错误在 plan Step 4 门禁同样会拦——同源规则)。`);
|
|
4556
|
+
process.exitCode = 1;
|
|
4557
|
+
}
|
|
4558
|
+
break;
|
|
4559
|
+
}
|
|
4397
4560
|
if (!all && !taskVal) {
|
|
4398
4561
|
console.error('❌ 用法: sillyspec taskcard <change-name> --task task-01[,task-02...] | --all [--force]');
|
|
4399
4562
|
process.exit(1);
|
|
@@ -4552,9 +4715,12 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
|
|
|
4552
4715
|
break;
|
|
4553
4716
|
}
|
|
4554
4717
|
case 'flow': {
|
|
4555
|
-
// R7 切片二:2-调用轻量协议命令族(start/done/amend-draft)——协议必需交互=2
|
|
4718
|
+
// R7 切片二:2-调用轻量协议命令族(start/done/amend-draft)——协议必需交互=2。
|
|
4719
|
+
// --json 全局透传(2026-10-06-flow-status-json):该 flag 在上方全局解析段被剥出
|
|
4720
|
+
// filteredArgs,flow 族经此收到 {json} 才在真实入口下生效(status 机器可读输出 /
|
|
4721
|
+
// start JSON 信封同门,与 gate/derive 族「index 消费全局 flag、显式传子命令」同款形状)
|
|
4556
4722
|
const flowMod = await import('./flow.js');
|
|
4557
|
-
await flowMod.cmdFlow(filteredArgs.slice(1), dir, specDir);
|
|
4723
|
+
await flowMod.cmdFlow(filteredArgs.slice(1), dir, specDir, { json });
|
|
4558
4724
|
break;
|
|
4559
4725
|
}
|
|
4560
4726
|
case 'workflow': {
|
|
@@ -4975,6 +5141,7 @@ SillySpec config — local.yaml 配置键速查
|
|
|
4975
5141
|
'scan-runs': '平台模式 scan/workflow 取证目录',
|
|
4976
5142
|
'artifacts': '派生产物目录(init 预建)',
|
|
4977
5143
|
'contract-artifacts': 'execute endpoint 契约产物(verify 阶段读取)',
|
|
5144
|
+
'verify-runs': 'verify --done 门禁取证目录(reconcile-result.json / probe-consistency-result.json 明细 + gate-last-<change>.json 稳定锚点;sillyspec gate last --change <名> 直读锚点与 blocked 明细,verify-result-backup-*.md 是 --force/--refresh-probes 的自动备份)',
|
|
4978
5145
|
'history': '历史记录目录(init 预建)',
|
|
4979
5146
|
'logs': '日志目录(init 预建)',
|
|
4980
5147
|
'templates': '工作流模板目录(init 预建)',
|
|
@@ -391,7 +391,9 @@ export async function cmdKnowledgeClassify(dir, args, opts = {}) {
|
|
|
391
391
|
export async function cmdKnowledgeInbox(dir, args, opts = {}) {
|
|
392
392
|
const base = opts.specDir || join(dir, '.sillyspec')
|
|
393
393
|
const knowledgeDir = join(base, 'knowledge')
|
|
394
|
-
|
|
394
|
+
// --json 双路判定(对齐 stages/knowledge.js digest 惯例):全局旗标被 index.js 顶层吞进
|
|
395
|
+
// opts.json(filteredArgs 不含 --json),args.includes 仅兜底直调/测试场景
|
|
396
|
+
const wantJson = args.includes('--json') || opts.json === true
|
|
395
397
|
const uncPath = join(knowledgeDir, 'uncategorized.md')
|
|
396
398
|
let entries = []
|
|
397
399
|
if (existsSync(uncPath)) {
|
package/src/knowledge-digest.js
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* knowledge-digest.js — 知识资产治理信号收集器(2026-09-27-knowledge-digest)
|
|
3
3
|
*
|
|
4
4
|
* 现状:通用知识走 propose→平台人工合并闭环,规格资产(FR 索引/绑定)全自动入库零人审——
|
|
5
|
-
*
|
|
5
|
+
* 信号沉睡库内无人工出口(R23-full 载体仓收件箱积压 40 条实证)。本模块把沉睡数据
|
|
6
6
|
* 变结构化摘要:`sillyspec knowledge digest`(人读文本)/ `--json`(平台 RPC 消费)。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* ①
|
|
10
|
-
* ②
|
|
11
|
-
* ③
|
|
12
|
-
* ④ 绑定路径解析失败:fr/*.md 绑定行经 resolveTestFileRel(与 repair-paths/门禁读侧
|
|
8
|
+
* 三信号(阈值内静默、超阈才进摘要——安静即健康态,防仪式化):
|
|
9
|
+
* ① 收件箱积压:knowledge/uncategorized.md 的 `## <qlId> | <标题>` 条目(阈值 20)
|
|
10
|
+
* ② 伪域落库:auto-* 域文件条目数 + unmapped 池条目数(阈值 0——伪域本不该增长)
|
|
11
|
+
* ③ 绑定路径解析失败:fr/*.md 绑定行经 resolveTestFileRel(与 repair-paths/门禁读侧
|
|
13
12
|
* 同口径单源)无法自仓根解析(阈值 0)
|
|
13
|
+
* (rot 待复核标记信号已随标记层拆除——2026-09-29-rot-retire-inject-cap:471 条零消费实证)
|
|
14
14
|
*
|
|
15
15
|
* 只读扫描,零写入——动作(迁移/清账/repair)由消费方另行执行。
|
|
16
16
|
*/
|
|
@@ -21,8 +21,50 @@ import { readFrBindings, resolveTestFileRel, testAnchorFile } from './test-bindi
|
|
|
21
21
|
/** 伪域判定:auto- 前缀(scaffold 时代机器蒸馏域)或 unmapped 池 */
|
|
22
22
|
const isPseudoDomain = (domain) => /^auto[-_]/.test(domain) || domain === 'unmapped'
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/** 编辑距离 ≤1 判定(短域串专用,单遍扫描) */
|
|
25
|
+
function levenshteinAtMost1(a, b) {
|
|
26
|
+
if (a === b) return true
|
|
27
|
+
const [s, l] = a.length <= b.length ? [a, b] : [b, a]
|
|
28
|
+
if (l.length - s.length > 1) return false
|
|
29
|
+
let i = 0, j = 0, diff = 0
|
|
30
|
+
while (i < s.length && j < l.length) {
|
|
31
|
+
if (s[i] === l[j]) { i++; j++; continue }
|
|
32
|
+
if (++diff > 1) return false
|
|
33
|
+
if (s.length === l.length) { i++; j++ } else { j++ } // 等长=替换;差一=长串跳一格
|
|
34
|
+
}
|
|
35
|
+
return true // 尾部剩余 ≤1 字符差
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 域词典守卫(坑 fr-domain-suggest-typo-and-no-split-migration 缺陷1,2026-09-28 生产实证
|
|
40
|
+
* auto-rontend):候选域与既有域(fr/*.md 文件名 = 真域+伪域全集)编辑距离 ≤1 → 判拼写
|
|
41
|
+
* 漂移,吸附返回既有域名;已在典内 / 远距离(绿地新模块)/ 无词典 → null(不干预)。
|
|
42
|
+
* 域路由与建议器共用(pseudoDomainFromPaths / suggestDomainFromFiles)——任一上游路径
|
|
43
|
+
* 形态再出 mangle,伪域也不会凭空铸造拼写错域。
|
|
44
|
+
*/
|
|
45
|
+
export function snapDomainToDictionary(candidate, { knowledgeRoot } = {}) {
|
|
46
|
+
if (!candidate || !knowledgeRoot) return null
|
|
47
|
+
let names = []
|
|
48
|
+
try {
|
|
49
|
+
names = readdirSync(join(knowledgeRoot, 'fr')).filter((f) => f.endsWith('.md')).map((f) => f.slice(0, -3))
|
|
50
|
+
} catch { /* fr 目录缺席=无词典,不干预 */ }
|
|
51
|
+
for (const n of names) {
|
|
52
|
+
if (n === candidate) return null
|
|
53
|
+
if (Math.abs(n.length - candidate.length) > 1) continue
|
|
54
|
+
if (levenshteinAtMost1(n, candidate)) return n
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 交付路径 → 建议域(落域机械改进的建议器,与 indexRequirements 的告警共用单源)。
|
|
60
|
+
* 产出过词典守卫(拼写漂移吸附,坑 fr-domain-suggest 缺陷1)。 */
|
|
61
|
+
export function suggestDomainFromFiles(files, { knowledgeRoot } = {}) {
|
|
62
|
+
const raw = suggestDomainFromFilesRaw(files)
|
|
63
|
+
if (raw == null) return null
|
|
64
|
+
return snapDomainToDictionary(raw, { knowledgeRoot }) || raw
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function suggestDomainFromFilesRaw(files) {
|
|
26
68
|
for (const raw of files || []) {
|
|
27
69
|
const p = String(raw || '').replace(/\\/g, '/')
|
|
28
70
|
let m
|
|
@@ -36,17 +78,12 @@ export function suggestDomainFromFiles(files) {
|
|
|
36
78
|
return null
|
|
37
79
|
}
|
|
38
80
|
|
|
39
|
-
/** 条目级解析:fr/<域>.md 的 `## <id> <标题>`
|
|
81
|
+
/** 条目级解析:fr/<域>.md 的 `## <id> <标题>` 段头(rot 待复核行解析已随标记层拆除,2026-09-29-rot-retire-inject-cap) */
|
|
40
82
|
function scanFrFile(text) {
|
|
41
83
|
const entries = []
|
|
42
|
-
let cur = null
|
|
43
84
|
for (const line of text.split(/\r?\n/)) {
|
|
44
85
|
const h = /^## (FR-[\w.-]+-\d+|FR-\d+)\s*(.*)$/.exec(line)
|
|
45
|
-
if (h) {
|
|
46
|
-
if (cur) {
|
|
47
|
-
const r = /^待复核:(.+)$/.exec(line)
|
|
48
|
-
if (r) cur.needsReview = r[1].trim()
|
|
49
|
-
}
|
|
86
|
+
if (h) entries.push({ id: h[1] })
|
|
50
87
|
}
|
|
51
88
|
return entries
|
|
52
89
|
}
|
|
@@ -59,9 +96,8 @@ export function collectKnowledgeDigest({ specBase, projectRoot }) {
|
|
|
59
96
|
const knowledgeRoot = join(specBase, 'knowledge')
|
|
60
97
|
const frDir = join(knowledgeRoot, 'fr')
|
|
61
98
|
const signals = []
|
|
62
|
-
const totals = {
|
|
99
|
+
const totals = { inbox: 0, pseudo: 0, unresolvedBindings: 0 }
|
|
63
100
|
|
|
64
|
-
const rotByDomain = new Map()
|
|
65
101
|
const pseudoByDomain = new Map()
|
|
66
102
|
const unresolved = []
|
|
67
103
|
const frFiles = existsSync(frDir) ? readdirSync(frDir).filter(f => f.endsWith('.md')).sort() : []
|
|
@@ -77,8 +113,6 @@ export function collectKnowledgeDigest({ specBase, projectRoot }) {
|
|
|
77
113
|
const domain = f.replace(/\.md$/, '')
|
|
78
114
|
const text = readFileSync(join(frDir, f), 'utf8')
|
|
79
115
|
const entries = scanFrFile(text)
|
|
80
|
-
const rot = entries.filter(e => e.needsReview).length
|
|
81
|
-
if (rot > 0) { rotByDomain.set(domain, rot); totals.rot += rot }
|
|
82
116
|
if (isPseudoDomain(domain) && entries.length > 0) {
|
|
83
117
|
// 基线内的 unmapped 池不计(历史跨仓基线,非新增堆积);auto-* 伪域无基线语义恒计
|
|
84
118
|
const count = domain === 'unmapped' ? Math.max(0, entries.length - unmappedBaseline) : entries.length
|
|
@@ -114,11 +148,6 @@ export function collectKnowledgeDigest({ specBase, projectRoot }) {
|
|
|
114
148
|
const mk = (kind, level, title, count, detail, suggestion) =>
|
|
115
149
|
signals.push({ kind, level, title, count, detail, ...(suggestion ? { suggestion } : {}) })
|
|
116
150
|
|
|
117
|
-
if (totals.rot > 100) {
|
|
118
|
-
mk('rot', 'warn', `rot 待复核批量标记(${totals.rot} 条 > 100)`, totals.rot,
|
|
119
|
-
[...rotByDomain.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([d, n]) => `${d} ${n}`).join('、'),
|
|
120
|
-
'批量复核:文案漂移类可批量承接,行为类逐条对账')
|
|
121
|
-
}
|
|
122
151
|
if (totals.inbox > 20) {
|
|
123
152
|
mk('inbox', 'warn', `知识收件箱积压(${totals.inbox} 条 > 20)`, totals.inbox,
|
|
124
153
|
inboxTitles.slice(0, 5).join(';') + (inboxTitles.length > 5 ? ' 等' : ''),
|
|
@@ -148,7 +177,7 @@ export function renderKnowledgeDigestText(d) {
|
|
|
148
177
|
const lines = []
|
|
149
178
|
lines.push(`📬 知识资产治理摘要(${d.generated_at.slice(0, 16).replace('T', ' ')})`)
|
|
150
179
|
if (d.healthy) {
|
|
151
|
-
lines.push('✅
|
|
180
|
+
lines.push('✅ 三类信号全部在阈内(inbox/伪域/坏绑定)——安静即健康态,无需动作')
|
|
152
181
|
} else {
|
|
153
182
|
lines.push(`⚠️ ${d.signals.length} 类信号超阈:`)
|
|
154
183
|
for (const s of d.signals) {
|
|
@@ -157,6 +186,6 @@ export function renderKnowledgeDigestText(d) {
|
|
|
157
186
|
if (s.suggestion) lines.push(` 处置:${s.suggestion}`)
|
|
158
187
|
}
|
|
159
188
|
}
|
|
160
|
-
lines.push(
|
|
189
|
+
lines.push(`底数:收件箱 ${d.totals.inbox}|伪域条目 ${d.totals.pseudo}|坏绑定 ${d.totals.unresolvedBindings}`)
|
|
161
190
|
return lines.join('\n')
|
|
162
191
|
}
|