sillyspec 3.31.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 +1 -1
- 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 +105 -7
- package/.zcodeignore +66 -0
- package/AGENTS.md +16 -16
- 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 +186 -19
- 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/quick.js +1 -1
- 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
|
@@ -52,7 +52,12 @@ function listReviewEvidence(changeDir, opts) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* 假勾选三态判定(L0
|
|
55
|
+
* 假勾选三态判定(L0 收口拒收支点)。
|
|
56
|
+
*
|
|
57
|
+
* 证据判据统一(2026-10-07-thin-tasks-v3,镜像豁免退役):tasks.md 是 agent 的工作分解
|
|
58
|
+
* 队列,每格勾选都是 agent 的完成主张——一律按 per-task 证据判(提交消息含完整 token
|
|
59
|
+
* task-NN(负向前瞻,task-01 不证 task-010)或对应 review.json 在场)。机器种子与
|
|
60
|
+
* agent 改写行同判,不再区分「镜像/覆写」。
|
|
56
61
|
*
|
|
57
62
|
* @param {object} args
|
|
58
63
|
* @param {string|null} args.changeDir 变更目录(推导 specBase 定位 execute-runs;null/探测
|
|
@@ -63,7 +68,7 @@ function listReviewEvidence(changeDir, opts) {
|
|
|
63
68
|
* @param {{listReviewsImpl?: Function}} [args.opts] 注入面(测试替身)
|
|
64
69
|
* @returns {{status:'complete'|'fake'|'none', claimTotal:number, checked:number, missing:string[]}}
|
|
65
70
|
* none=无完成主张(判集空或未全勾);complete=全勾且零 missing;fake=全勾且有零证据
|
|
66
|
-
* 任务(missing 列其 id
|
|
71
|
+
* 任务(missing 列其 id,收口侧拒收依据)
|
|
67
72
|
*/
|
|
68
73
|
export function detectFakeCheckCompletion({ changeDir, tasksMd, commits, opts = {} } = {}) {
|
|
69
74
|
const entries = [];
|
|
@@ -73,14 +78,15 @@ export function detectFakeCheckCompletion({ changeDir, tasksMd, commits, opts =
|
|
|
73
78
|
}
|
|
74
79
|
const claimTotal = entries.length;
|
|
75
80
|
const checked = entries.filter((e) => e.checked).length;
|
|
81
|
+
const messages = (Array.isArray(commits) ? commits : [])
|
|
82
|
+
.map((c) => (typeof c === 'string' ? c : String((c && (c.message ?? c.subject)) || '')));
|
|
76
83
|
if (claimTotal === 0 || checked < claimTotal) {
|
|
77
84
|
return { status: 'none', claimTotal, checked, missing: [] };
|
|
78
85
|
}
|
|
79
|
-
const messages = (Array.isArray(commits) ? commits : [])
|
|
80
|
-
.map((c) => (typeof c === 'string' ? c : String((c && (c.message ?? c.subject)) || '')));
|
|
81
86
|
const reviews = listReviewEvidence(changeDir, opts);
|
|
82
87
|
const missing = entries
|
|
83
88
|
.filter((e) => {
|
|
89
|
+
if (!e.checked) return false;
|
|
84
90
|
const re = taskTokenRe(e.id);
|
|
85
91
|
const byCommit = messages.some((msg) => re.test(msg));
|
|
86
92
|
const byReview = reviews.some((p) => p.includes(`/tasks/${e.id}/`));
|
|
@@ -91,24 +97,69 @@ export function detectFakeCheckCompletion({ changeDir, tasksMd, commits, opts =
|
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
/**
|
|
94
|
-
* 勾选节奏检测(2026-09-26-thin-check-cadence
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
100
|
+
* 勾选节奏检测(2026-09-26-thin-check-cadence;2026-10-07-thin-tasks-v3 采样去重):
|
|
101
|
+
* 事件流里 task-done 单拍跳 ≥2 格 = 一把全勾(未按工作单元逐个勾)。
|
|
102
|
+
* 事件两源同流:`task tick` 命令直写的精确事件(source:'task-tick',一次一格)与 watcher
|
|
103
|
+
* 3s 轮询的采样事件(快速连续 tick 被合并成一跳)。判最大跳以 CLI 精确事件为权威:
|
|
104
|
+
* ① 采样事件落点计数 M 与任一 CLI 事件落点相同 → 整跳被 CLI 精确序列覆盖,剔除;
|
|
105
|
+
* ② 其余采样事件起点按「小于其落点的最大 CLI 落点」抬高(CLI 已勾到的格不重复计入跳幅)。
|
|
106
|
+
* 纯函数:事件清单→最大跳格记录;无多格跳返回 null。detail 与事件生成格式成对
|
|
107
|
+
* (`checked N→M`);解析失配(格式漂移/坏行/非 task-done)按无证据静默——fail-open。
|
|
99
108
|
*/
|
|
100
109
|
export function detectBatchCheckCadence(events) {
|
|
101
|
-
|
|
110
|
+
const taskEvents = [];
|
|
102
111
|
for (const e of events || []) {
|
|
103
|
-
if (
|
|
112
|
+
if (e && e.kind === 'task-done') taskEvents.push(e);
|
|
113
|
+
}
|
|
114
|
+
const parse = (e) => {
|
|
104
115
|
const m = /^checked (\d+)→(\d+)$/.exec(String(e.detail || ''));
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
116
|
+
return m ? { from: Number(m[1]), to: Number(m[2]) } : null;
|
|
117
|
+
};
|
|
118
|
+
const cliParsed = [];
|
|
119
|
+
for (const e of taskEvents) {
|
|
120
|
+
if (e.source !== 'task-tick') continue;
|
|
121
|
+
const r = parse(e);
|
|
122
|
+
if (r) cliParsed.push(r);
|
|
123
|
+
}
|
|
124
|
+
const cliTargets = new Set(cliParsed.map((r) => r.to));
|
|
125
|
+
const maxCliBelow = (to) => {
|
|
126
|
+
let best = 0;
|
|
127
|
+
for (const r of cliParsed) if (r.to < to && r.to > best) best = r.to;
|
|
128
|
+
return best;
|
|
129
|
+
};
|
|
130
|
+
let worst = null;
|
|
131
|
+
for (const e of taskEvents) {
|
|
132
|
+
const r = parse(e);
|
|
133
|
+
if (!r) continue;
|
|
134
|
+
const isCli = e.source === 'task-tick';
|
|
135
|
+
if (!isCli && cliTargets.has(r.to)) continue; // 采样合并跳被 CLI 精确序列覆盖
|
|
136
|
+
const from = isCli ? r.from : Math.max(r.from, maxCliBelow(r.to));
|
|
137
|
+
if (r.to - from < 2) continue;
|
|
138
|
+
if (!worst || r.to - from > worst.to - worst.from) worst = { from, to: r.to, detail: e.detail, ts: e.ts };
|
|
110
139
|
}
|
|
111
140
|
return worst;
|
|
112
141
|
}
|
|
113
142
|
|
|
143
|
+
/**
|
|
144
|
+
* 单拍勾选门决策(2026-09-29-batch-tick-gate,纯函数):检出单拍跳(batchTick 非 null)
|
|
145
|
+
* 时的收口动作裁决。镜像豁免分支退役(2026-10-07-thin-tasks-v3——任务面统一为 agent
|
|
146
|
+
* 工作分解,无镜像面)。四态(决策顺序即豁免优先序):
|
|
147
|
+
* 'silent' 无单拍跳;
|
|
148
|
+
* 'bypass' --allow-batch-tick 显式旁路——留痕放行(同意门先例);
|
|
149
|
+
* 'advisory' 机器代勾可解释整跳(autopilotTicked >= 跳幅——governance-autopilot 代勾是
|
|
150
|
+
* 单拍多格机械写,非 agent 纪律面;代勾数不可解释最大跳时落到 reject——防「留
|
|
151
|
+
* 一格给代勾补、自身大跳蹭豁免」的对抗绕过,二轮评审 P3-E);
|
|
152
|
+
* 'reject' 非旁路、代勾解释不了整跳——agent 一把勾,拒收。
|
|
153
|
+
* @param {{batchTick:object|null, nonMirrorCount?:number|null, allowBatchTick?:boolean, autopilotTicked?:number}} args
|
|
154
|
+
* nonMirrorCount 保留为哨兵面未知信号(null → fail-open advisory);镜像-only 语义已退役。
|
|
155
|
+
* @returns {{action:'silent'|'advisory'|'bypass'|'reject', reason:string}}
|
|
156
|
+
*/
|
|
157
|
+
export function resolveBatchTickAction({ batchTick, nonMirrorCount = null, allowBatchTick = false, autopilotTicked = 0 } = {}) {
|
|
158
|
+
if (!batchTick) return { action: 'silent', reason: 'no-batch-tick' };
|
|
159
|
+
if (allowBatchTick === true) return { action: 'bypass', reason: 'flag' };
|
|
160
|
+
if (nonMirrorCount === null) return { action: 'advisory', reason: 'sentinel-unknown' };
|
|
161
|
+
if (autopilotTicked >= batchTick.to - batchTick.from) return { action: 'advisory', reason: 'autopilot-ticked' };
|
|
162
|
+
return { action: 'reject', reason: 'agent-batch-tick' };
|
|
163
|
+
}
|
|
164
|
+
|
|
114
165
|
export default { detectFakeCheckCompletion, detectBatchCheckCadence };
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (plan-postcheck 注入 LF-normalizing reader),引擎拿到什么判什么,不在引擎层二次引入差异。
|
|
11
11
|
* - regex 每次 new RegExp:manifest 存 {pattern, flags} 字符串,规避 lastIndex 状态坑
|
|
12
12
|
* (旧代码多处 /g 正则跨调用复用需手写 lastIndex=0,是潜在 bug)。
|
|
13
|
-
* - 内容规则(literal-any、literal-all、regex、contains-section、field-present、header-field、list-non-empty、
|
|
13
|
+
* - 内容规则(literal-any、literal-all、literal-none、regex、contains-section、field-present、header-field、list-non-empty、
|
|
14
14
|
* min-lines/no-placeholder-line)在 target 文件不存在时自动 skip(不报错)——存在性由独立的
|
|
15
15
|
* file-exists 规则保证,避免"文件缺失"被内容规则重复报。
|
|
16
16
|
* - custom kind 引擎 skip(不计 errors/warnings),由调用方 validator 保留判定算法,
|
|
@@ -75,6 +75,9 @@ function dispatchPure(rule, entry) {
|
|
|
75
75
|
switch (rule.kind) {
|
|
76
76
|
case 'literal-any': return (rule.data.literals || []).some(s => scoped.includes(s))
|
|
77
77
|
case 'literal-all': return (rule.data.literals || []).every(s => scoped.includes(s))
|
|
78
|
+
// literal-none(2026-09-29-brainstorm-closure-gates):「不得含字面量」判定——与 literal-any 互补,
|
|
79
|
+
// 服务 must-not-contain 契约(残留标记类:待确认/自审存疑等"在场即未闭合"语义)。
|
|
80
|
+
case 'literal-none': return !(rule.data.literals || []).some(s => scoped.includes(s))
|
|
78
81
|
case 'regex': return new RegExp(rule.data.pattern, rule.data.flags || '').test(scoped)
|
|
79
82
|
case 'contains-section': return hasAllSections(scoped, rule.data.sections || [])
|
|
80
83
|
case 'no-placeholder-line': return hasNoPlaceholderLine(scoped, rule.data.patterns)
|
|
@@ -25,11 +25,12 @@
|
|
|
25
25
|
* (小变更只产 design.md)。判定见 stage-contract-engine.js conditionHolds(fail-safe:ctx 字段缺失时 ne 成立)。
|
|
26
26
|
*
|
|
27
27
|
* 纯 kind(stage-contract-engine.dispatchPure 直接判定):
|
|
28
|
-
* file-exists / dir-exists / literal-any / literal-all / regex / contains-section /
|
|
28
|
+
* file-exists / dir-exists / literal-any / literal-all / literal-none / regex / contains-section /
|
|
29
29
|
* no-placeholder-line / field-present / header-field / list-non-empty / min-lines / dir-non-empty
|
|
30
|
-
* —— 语义:若 target 文件不存在,内容类规则(literal-any、literal-all、regex、contains-section、…、
|
|
31
|
-
* min-lines)自动 skip(不报错,存在性由独立的 file-exists 规则保证);file-exists/dir-exists
|
|
32
|
-
* 则是"不存在即 fail"。
|
|
30
|
+
* —— 语义:若 target 文件不存在,内容类规则(literal-any、literal-all、literal-none、regex、contains-section、…、
|
|
31
|
+
* list-non-empty、min-lines)自动 skip(不报错,存在性由独立的 file-exists 规则保证);file-exists/dir-exists
|
|
32
|
+
* 则是"不存在即 fail"。literal-none 是「不得含字面量」判定(2026-09-29-brainstorm-closure-gates),
|
|
33
|
+
* 服务 must-not-contain 契约(残留标记类:在场即未闭合)。
|
|
33
34
|
*
|
|
34
35
|
* custom kind(引擎 skip,validator 保留判定算法,但 import 本 manifest 的 data + failMessage):
|
|
35
36
|
* 见 CUSTOM_KINDS。这些是复杂条件校验(lifecycle 多层豁免 / 入口接线多步对账 / decisions 解析 /
|
|
@@ -44,6 +45,9 @@ const CUSTOM_KINDS = new Set([
|
|
|
44
45
|
'entry-point-wiring', // design.md 入口实例化 → task allowed_paths 覆盖对账(validatePlanOutputs)
|
|
45
46
|
'id-traceability', // FR/D 引用追踪(extractIds / warnMissingIds)
|
|
46
47
|
'decision-blocker', // decisions.md P0/P1 未决阻塞(parseDecisionRecords)
|
|
48
|
+
'decision-coverage', // requirements.md 对 decisions.md 当前版本 D 的覆盖缺口点名(2026-09-29-brainstorm-closure-gates,validateBrainstormOutputs)
|
|
49
|
+
'doubt-closure', // design.md「自审存疑:」应用形态标记行的闭合 token 判定(同上)
|
|
50
|
+
'risk-mitigation', // design.md 风险登记表 R-xx 行应对策略列空/占位判定(同上)
|
|
47
51
|
'cross-task-contract', // task 卡片 provides/expects_from 对账(plan-postcheck)
|
|
48
52
|
'design-file-coverage', // design.md 文件清单 vs allowed_paths 覆盖(plan-postcheck)
|
|
49
53
|
'task-id-continuity', // task-NN id 连续性
|
|
@@ -151,6 +155,101 @@ const BRAINSTORM_RULES = [
|
|
|
151
155
|
spec: 'tasks.md 至少有一个任务列表项(行首 `-` / `*` / `数字.`)',
|
|
152
156
|
failMessage: 'tasks.md 没有任务列表项',
|
|
153
157
|
},
|
|
158
|
+
// ── 闭环收口规则(2026-09-29-brainstorm-closure-gates)────────────────────────────
|
|
159
|
+
// 判据:开放可以,但每个开口必须能回答「谁、在哪个阶段、以什么证据关掉它」。六类无主开口全部
|
|
160
|
+
// 补机器收口,severity 全 warning 不阻断(存量零破坏——故障面/退役判据软警告棘轮先例,观测一个
|
|
161
|
+
// 周期误报率后再评估升 error)。纯 kind 走引擎,复杂判定 custom kind 留 validateBrainstormOutputs。
|
|
162
|
+
{
|
|
163
|
+
id: 'brainstorm.proposal.success-criteria',
|
|
164
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'literal-any',
|
|
165
|
+
target: { root: 'change', path: 'proposal.md', scope: 'full' },
|
|
166
|
+
data: { literals: ['成功标准', 'Success Criteria', 'success criteria'] },
|
|
167
|
+
condition: { ctxField: 'scale', ne: 'small' },
|
|
168
|
+
spec: 'proposal.md 含「成功标准 / Success Criteria」章节(字面命中;问题→方案→成功标准主环最后一环——轻量道 flow start --input 对成功标准 0 条 exit 2,完整路径同理)',
|
|
169
|
+
failMessage: 'proposal.md 缺少「成功标准」章节(需字面命中其一:成功标准 / Success Criteria)——可验证的成功标准是收口验收面,轻量道同款要求',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'brainstorm.design.no-placeholder',
|
|
173
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'no-placeholder-line',
|
|
174
|
+
target: { root: 'change', path: 'design.md', scope: 'full' },
|
|
175
|
+
data: { patterns: ['待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写', '待完善', '待设计', '待确认'] },
|
|
176
|
+
spec: 'design.md 无独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等——占位符在场=开口无人认领)',
|
|
177
|
+
failMessage: 'design.md 存在独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)——占位符须替换为真实内容或删除该行',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'brainstorm.proposal.no-placeholder',
|
|
181
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'no-placeholder-line',
|
|
182
|
+
target: { root: 'change', path: 'proposal.md', scope: 'full' },
|
|
183
|
+
data: { patterns: ['待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写', '待完善', '待设计', '待确认'] },
|
|
184
|
+
condition: { ctxField: 'scale', ne: 'small' },
|
|
185
|
+
spec: 'proposal.md 无独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)',
|
|
186
|
+
failMessage: 'proposal.md 存在独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)——占位符须替换为真实内容或删除该行',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'brainstorm.requirements.no-placeholder',
|
|
190
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'no-placeholder-line',
|
|
191
|
+
target: { root: 'change', path: 'requirements.md', scope: 'full' },
|
|
192
|
+
data: { patterns: ['待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写', '待完善', '待设计', '待确认'] },
|
|
193
|
+
condition: { ctxField: 'scale', ne: 'small' },
|
|
194
|
+
spec: 'requirements.md 无独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)',
|
|
195
|
+
failMessage: 'requirements.md 存在独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)——占位符须替换为真实内容或删除该行',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'brainstorm.tasks.no-placeholder',
|
|
199
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'no-placeholder-line',
|
|
200
|
+
target: { root: 'change', path: 'tasks.md', scope: 'full' },
|
|
201
|
+
data: { patterns: ['待补充', 'TODO', 'TBD', '未分析', '根据项目情况', '根据实际情况', '按需填写', '待完善', '待设计', '待确认'] },
|
|
202
|
+
condition: { ctxField: 'scale', ne: 'small' },
|
|
203
|
+
spec: 'tasks.md 无独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)',
|
|
204
|
+
failMessage: 'tasks.md 存在独立成行占位词(待补充/TODO/TBD/未分析/待完善/待设计/待确认等)——占位符须替换为真实内容或删除该行',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'brainstorm.design.pending-confirm-residue',
|
|
208
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'literal-none',
|
|
209
|
+
target: { root: 'change', path: 'design.md', scope: 'full' },
|
|
210
|
+
data: { literals: ['待确认'] },
|
|
211
|
+
spec: 'design.md 无「待确认」残留——design-init 决策追踪表预填的「待确认」须逐行补覆盖点后改「已覆盖」(确无决策记录时删除预填行或写「无决策记录」)',
|
|
212
|
+
failMessage: 'design.md 残留「待确认」——决策追踪表(design-init 预填)须逐行补覆盖点并把「待确认」改为「已覆盖」;确无决策记录时删除预填行或写「无决策记录」',
|
|
213
|
+
},
|
|
214
|
+
// decision-coverage(custom):requirements.md 必须引用 decisions.md 全部当前版本 D-xxx@vN
|
|
215
|
+
// (裸号词边界匹配,D-001 即算引用 D-001@v1,大小写不敏感——与 warnMissingIds 同语义)。
|
|
216
|
+
// 「剩余风险」行含裸号亦算归属(显式登记为剩余风险=开口有主)。算法留 validateBrainstormOutputs
|
|
217
|
+
// (复用 extractCurrentDecisionIds),匹配口径/failMessage 从本 manifest 同源。
|
|
218
|
+
{
|
|
219
|
+
id: 'brainstorm.requirements.decision-coverage',
|
|
220
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'decision-coverage',
|
|
221
|
+
target: { root: 'change', path: 'requirements.md', scope: 'full' },
|
|
222
|
+
data: {},
|
|
223
|
+
spec: 'requirements.md 引用 decisions.md 的全部当前版本 D-xxx@vN(裸号 D-001 词边界匹配即算;未覆盖的在「决策覆盖矩阵」补行,或显式标注为剩余风险——被覆盖或显式登记为剩余风险都算归属,不出现才算开口)',
|
|
224
|
+
failMessage: 'requirements.md 未引用 decisions.md 中的 ${id} — 在「决策覆盖矩阵」补覆盖行,或在剩余风险中显式标注该决策未覆盖(每个 D 须有归属:被覆盖或显式登记为剩余风险)',
|
|
225
|
+
},
|
|
226
|
+
// doubt-closure(custom):「自审存疑:」应用形态标记行须含闭合 token。查带冒号的应用形态而非
|
|
227
|
+
// 裸词——design-init 自审 checklist 模板行含裸词,裸词判定必常驻误报。算法留 validator。
|
|
228
|
+
{
|
|
229
|
+
id: 'brainstorm.design.doubt-closure',
|
|
230
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'doubt-closure',
|
|
231
|
+
target: { root: 'change', path: 'design.md', scope: 'full' },
|
|
232
|
+
data: {
|
|
233
|
+
markerPattern: { pattern: '自审存疑\\s*[::]', flags: '' },
|
|
234
|
+
closureTokens: ['D-\\d+', 'R-\\d+', '已解决', '已闭合', '已确认', '不适用'],
|
|
235
|
+
},
|
|
236
|
+
spec: 'design.md 的「自审存疑:」标记行须含闭合 token(D-xxx/R-xx/已解决/已闭合/已确认)——已解决的改写为结论并引用 D/R,未解决的转 decisions.md D-xxx 或风险登记 R-xx;存疑标记不得原样带进 plan',
|
|
237
|
+
failMessage: 'design.md 存在未闭合的自审存疑:「${line}」——已解决的改写为结论并引用 D-xxx/R-xx,未解决的转 decisions.md 决策或风险登记 R-xx 后去标记;开放须有归属,存疑标记不得带进 plan',
|
|
238
|
+
},
|
|
239
|
+
// risk-mitigation(custom):风险登记表 R-xx 行应对策略列(行末单元格)非空且非占位。design-init
|
|
240
|
+
// 骨架预填「(待填应对策略)」——骨架行命中即提醒填语义;显式「接受」是合法闭合(接受=归属明确)。
|
|
241
|
+
{
|
|
242
|
+
id: 'brainstorm.design.risk-mitigation',
|
|
243
|
+
stage: 'brainstorm', source: 'validateBrainstormOutputs', severity: 'warning', kind: 'risk-mitigation',
|
|
244
|
+
target: { root: 'change', path: 'design.md', scope: 'full' },
|
|
245
|
+
data: {
|
|
246
|
+
rowPattern: { pattern: '^\\|\\s*(R-\\d+)', flags: '' },
|
|
247
|
+
placeholderCell: '待填|待补充|待定|TODO|TBD|仅占位|^\\s*[-—–]?\\s*$',
|
|
248
|
+
acceptToken: '接受',
|
|
249
|
+
},
|
|
250
|
+
spec: 'design.md 风险登记表每行 R-xx 的应对策略列(行末单元格)非空且非占位词(待填/待补充/待定/TODO/TBD)——每条风险须有应对;确实接受的风险显式写「接受:<理由>」(接受也是闭合),不得留空/占位',
|
|
251
|
+
failMessage: 'design.md 风险登记 ${id} 的应对策略列为空/占位——每条风险须有应对策略;确实接受的风险显式写「接受:<理由>」,不得留空或占位(风险无应对=开口无归属)',
|
|
252
|
+
},
|
|
154
253
|
// lifecycle(custom kind):design.md 命中生命周期关键词时必须含契约表或紧邻豁免。判定算法留 validator
|
|
155
254
|
// (trigger/exemption/table 三段短路),data + failMessage/exemptionPassedMessage 从本 manifest 同源。
|
|
156
255
|
{
|
package/src/stage-contract.js
CHANGED
|
@@ -350,7 +350,9 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
|
350
350
|
const frKnowledgeRoot = specRoot ? join(specRoot, 'knowledge') : join(cwd, '.sillyspec', 'knowledge')
|
|
351
351
|
const req = parseChangeRequirements(changeDir)
|
|
352
352
|
if (!req.missing && req.frs.length > 0) {
|
|
353
|
-
|
|
353
|
+
// unmapped 过滤对齐 frDupGateFlow(2026-09-29-rot-retire-inject-cap):停车场池不进
|
|
354
|
+
// 跨域标题比对——原全量载入 723 条做 CPU 税且产出跨域误指认
|
|
355
|
+
const domains = resolveTouchedDomains(changeDir, discoverModuleIndex(frKnowledgeRoot)).filter((d) => d !== 'unmapped')
|
|
354
356
|
const active = readActiveFrDigest(frKnowledgeRoot, domains)
|
|
355
357
|
if (active.length > 0) {
|
|
356
358
|
for (const fr of req.frs) {
|
|
@@ -411,6 +413,37 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
|
411
413
|
)
|
|
412
414
|
}
|
|
413
415
|
}
|
|
416
|
+
|
|
417
|
+
// ── 闭环收口 custom 判定(2026-09-29-brainstorm-closure-gates)──
|
|
418
|
+
// doubt-closure:「自审存疑:」应用形态行须含闭合 token——只查带冒号的应用形态,裸词
|
|
419
|
+
// 判定会撞 design-init 自审 checklist 模板行(含裸词)常驻误报。
|
|
420
|
+
{
|
|
421
|
+
const doubtRule = getRule('brainstorm.design.doubt-closure')
|
|
422
|
+
const markerRe = new RegExp(doubtRule.data.markerPattern.pattern, doubtRule.data.markerPattern.flags)
|
|
423
|
+
const closureRe = new RegExp(doubtRule.data.closureTokens.join('|'), 'i')
|
|
424
|
+
for (const line of content.split(/\r?\n/)) {
|
|
425
|
+
if (!markerRe.test(line) || closureRe.test(line)) continue
|
|
426
|
+
warnings.push(doubtRule.failMessage.replaceAll('${line}', line.trim().slice(0, 60)))
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// risk-mitigation:风险登记表 R-xx 行的应对策略列(行末单元格)非空且非占位——design-init
|
|
430
|
+
// 骨架预填「(待填应对策略)」命中即提醒填语义;「接受」显式接受是合法闭合(归属明确)。
|
|
431
|
+
{
|
|
432
|
+
const riskRule = getRule('brainstorm.design.risk-mitigation')
|
|
433
|
+
const rowRe = new RegExp(riskRule.data.rowPattern.pattern, riskRule.data.rowPattern.flags)
|
|
434
|
+
const cellPlaceholderRe = new RegExp(riskRule.data.placeholderCell, 'i')
|
|
435
|
+
for (const line of content.split(/\r?\n/)) {
|
|
436
|
+
const rowMatch = line.match(rowRe)
|
|
437
|
+
if (!rowMatch) continue
|
|
438
|
+
let cells = line.split('|').slice(1).map(c => c.trim())
|
|
439
|
+
if (cells.length > 0 && cells[cells.length - 1] === '' && line.trimEnd().endsWith('|')) cells.pop()
|
|
440
|
+
if (cells.length < 2) continue // 行形态不完整(仅编号列)——表结构问题交 Grill 审查,不在此硬判
|
|
441
|
+
const lastCell = cells[cells.length - 1].replace(/^[((]/, '').replace(/[))]$/, '').trim()
|
|
442
|
+
if (cellPlaceholderRe.test(lastCell)) {
|
|
443
|
+
warnings.push(riskRule.failMessage.replaceAll('${id}', rowMatch[1]))
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
414
447
|
}
|
|
415
448
|
|
|
416
449
|
const decisionsFile = join(changeDir, 'decisions.md')
|
|
@@ -425,10 +458,22 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
|
425
458
|
warnings.push('decisions.md 存在但没有当前版本 D-xxx@vN 决策 ID')
|
|
426
459
|
} else {
|
|
427
460
|
const design = readIfExists(join(changeDir, 'design.md'))
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
//
|
|
431
|
-
// tasks
|
|
461
|
+
// requirements 的 D 覆盖点名(2026-09-29-brainstorm-closure-gates):prompt 一直要求
|
|
462
|
+
// 「requirements.md 必须引用全部当前版本 D-xxx@vN,未覆盖的标注为剩余风险」,机器面原只盯
|
|
463
|
+
// design.md——闭环收口补齐:裸号词边界匹配(同 warnMissingIds 语义),裸号出现在决策覆盖
|
|
464
|
+
// 矩阵或「剩余风险」行都算归属(覆盖或显式登记=开口有主),不出现才点名。tasks 仍不强求
|
|
465
|
+
// (骨架,plan 阶段展开——plan --done 的 id-traceability 已覆盖 plan.md 侧)。
|
|
466
|
+
const requirementsForCoverage = readIfExists(join(changeDir, 'requirements.md'))
|
|
467
|
+
if (requirementsForCoverage) {
|
|
468
|
+
const reqUpper = requirementsForCoverage.toUpperCase()
|
|
469
|
+
const coverageFail = getRule('brainstorm.requirements.decision-coverage').failMessage
|
|
470
|
+
for (const id of decisionIds) {
|
|
471
|
+
const base = id.replace(/@V\d+$/, '')
|
|
472
|
+
if (!new RegExp(`\\b${base}\\b`).test(reqUpper)) {
|
|
473
|
+
warnings.push(coverageFail.replaceAll('${id}', id))
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
432
477
|
warnMissingIds(warnings, decisionIds, design, 'design.md', 'decisions.md')
|
|
433
478
|
}
|
|
434
479
|
// 故障面/退役判据软警告(FR-01,2026-09-15-tax-governance):architecture+accepted 缺字段
|
|
@@ -1204,12 +1249,15 @@ export function judgeApiCoverageMatrix(args) {
|
|
|
1204
1249
|
if (!apiEvidenceHasAnchorForm(e)) {
|
|
1205
1250
|
// 分诊(2026-09-23 执行会话实证:design接口表# 字样在场但 # 后非 METHOD /path 形态时,
|
|
1206
1251
|
// 泛化「缺五形态之一」让人无法区分「形态没写对」与「写了没算数」——三轮试错后 agent 被逼
|
|
1207
|
-
// 换更弱的 DDL@ 形态过门,fail-closed
|
|
1252
|
+
// 换更弱的 DDL@ 形态过门,fail-closed 反而促成了更泛的锚点。定向报出该形态的命中条件。
|
|
1253
|
+
// 2026-09-30 二次实证(multi-agent-platform tool-report-activation 会话):泛化分支文案
|
|
1254
|
+
// 「用例依据锚点」里的「用例依据」是第 3 列列名(caseId),而校验只查第 5 列证据列——
|
|
1255
|
+
// 文案把 agent 引向错误的列,再坑三轮。锚点文案一律点名「证据列」,禁用列名歧义词。)
|
|
1208
1256
|
API_ANCHOR_DESIGN_API_RE.lastIndex = 0
|
|
1209
1257
|
const bareDesignAnchor = /design接口表#/.test(e) && !API_ANCHOR_DESIGN_API_RE.test(e)
|
|
1210
1258
|
anchorViolations.push(bareDesignAnchor
|
|
1211
1259
|
? `${rowLabel(r)}:证据含 design接口表# 但 # 后未提取到 METHOD /path——该形态仅 \`design接口表#POST /api/xx\` 形态计命中(仅写表名/行号/散文描述不计)`
|
|
1212
|
-
: `${rowLabel(r)}
|
|
1260
|
+
: `${rowLabel(r)}:证据列(矩阵第 5 列)缺锚点(五形态之一)——锚点只认证据列,写在第 3 列「用例依据」不计`)
|
|
1213
1261
|
}
|
|
1214
1262
|
}
|
|
1215
1263
|
if (anchorViolations.length > 0) {
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -54,10 +54,20 @@ export const definition = {
|
|
|
54
54
|
- 多项目且用户未指定:列出项目列表,需要用户确认本次需求属于哪个子项目
|
|
55
55
|
|
|
56
56
|
### 早期规模筛查(判断是否该走轻量变更)
|
|
57
|
-
加载上下文后,用需求描述 +
|
|
58
|
-
- **明显 small
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
加载上下文后,用需求描述 + 模块上下文**粗判**本次变更规模(判据同 Step 8 复杂度/上下文轴——2026-09-29-brainstorm-exit-thin-default):
|
|
58
|
+
- **明显 small**(满足:单上下文可吞吐的纯执行类——改文案/修 bug/样式调整/配置微调,预计无 Wave 编排/上下文分片需求)→ 输出:「此变更规模较小,建议直接走轻量变更」+ 一句依据,给出可照抄建议命令(--input 引号内换行合法):
|
|
59
|
+
|
|
60
|
+
\`\`\`
|
|
61
|
+
sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>
|
|
62
|
+
|
|
63
|
+
成功标准:
|
|
64
|
+
- <可验证标准>"
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
(2 调用协议收口)。用户同意则本阶段可在此收尾(\`--done\` 并提示转轻量变更),不必继续走完整设计流程。
|
|
68
|
+
- **明显 large**(需要 Wave 并行编排/上下文分片/多阶段治理,或用户显式要求完整流程)→ 不要提示轻量变更,继续进入下一步「对话式探索与需求澄清」。
|
|
69
|
+
- **拿不准 → 继续探索**(这正是头脑风暴的职责;澄清完成后 Step 8 按同轴精判——注意:经过头脑风暴澄清的需求通常已可单上下文吞吐,出口默认收编轻量道)。
|
|
70
|
+
- 这是**粗判**,只为让明显的纯执行小变更免走设计流程;后续「生成规范文件」步骤做精判兜底。
|
|
61
71
|
|
|
62
72
|
### 输出
|
|
63
73
|
项目现状理解摘要(3-5 句话,关键约定和架构决策)+ 可能涉及的模块列表 + 本次需求所属子项目 + (如命中)quick 建议
|
|
@@ -203,12 +213,13 @@ export const definition = {
|
|
|
203
213
|
waitReason: '等待用户选择方案',
|
|
204
214
|
waitOptions: ['方案A', '方案B', '方案C'],
|
|
205
215
|
prompt: `基于需求理解和澄清结果,提出 2-3 种实现方案。
|
|
206
|
-
|
|
216
|
+
{UI_VISUAL_GUIDANCE}
|
|
207
217
|
### 操作
|
|
208
218
|
1. 每种方案列出:核心思路、优势、劣势
|
|
209
219
|
2. 如果上一步产生了 D-xxx@vN 决策记录,方案必须说明覆盖/违反哪些当前版本决策
|
|
210
|
-
3.
|
|
211
|
-
4.
|
|
220
|
+
3. 定稿前机制词检索(固定动作):方案引入新机制词(如 分类/词表/枚举/门/协议/状态机——例词为举例非机制,不构成封闭清单)时,先跑 \`sillyspec knowledge search --query "<机制词>"\`,命中条目必读(尤其 status=rejected 的防复潮条目)再定稿方案
|
|
221
|
+
4. 给出推荐方案和理由
|
|
222
|
+
5. 用户选定方案后,把选择结果**当场追加**到 \`{SPEC_ROOT}/changes/<change-name>/decisions.md\`(增量落盘规则同「对话式探索」步:目录/文件缺失先创建、按 D-xxx@vN 判重):type 用 architecture、source 用 user、question 记方案取舍问题、answer 记用户所选方案与理由要点、evidence 记方案选择轮次——方案选择是本变更最关键的用户决策,必须随答随落盘,不能只留在对话里
|
|
212
223
|
|
|
213
224
|
### 铁律 — 必须等待用户选择方案
|
|
214
225
|
- 列出方案对比表和推荐后必须暂停等用户选择,**不要自己说"推荐方案 A"然后当用户选了**(命令由 CLI 在下方注入)
|
|
@@ -234,9 +245,10 @@ export const definition = {
|
|
|
234
245
|
1. 简单项目:几句话整体描述
|
|
235
246
|
2. 复杂项目:按模块/Phase 分段展示,每段 200-300 字
|
|
236
247
|
3. 展示完整设计方案(不要逐段停顿,一次性展示)
|
|
237
|
-
4.
|
|
238
|
-
5.
|
|
239
|
-
6.
|
|
248
|
+
4. 分段设计含新机制时同上检索(固定动作):设计的分段内容引入新机制词(例词为举例非机制,不构成封闭清单)时,先跑 \`sillyspec knowledge search --query "<机制词>"\`,命中条目必读(尤其 status=rejected 的防复潮条目)再定稿设计
|
|
249
|
+
5. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth\`——CLI 已强制:新建/改名不带日期前缀直接 exit 2)
|
|
250
|
+
6. 按「HTML 原型分级」生成原型或写出跳过原因(二选一必填,见下)
|
|
251
|
+
7. 暂停等待用户确认或修改意见
|
|
240
252
|
|
|
241
253
|
### HTML 原型分级(按改动程度,不是"是否碰前端文件")
|
|
242
254
|
判定对象是**改造后的界面相对现状的变化程度**:
|
|
@@ -515,11 +527,12 @@ status: passed | needs-user-input | blocked | skipped
|
|
|
515
527
|
prompt: `按变更规模生成规范文件并给出实现路径建议(设计已在前置步骤确认过,本步不再暂停等确认,生成后展示摘要即可)。
|
|
516
528
|
|
|
517
529
|
### 规模评估(展示前先做)
|
|
518
|
-
|
|
519
|
-
- **small
|
|
520
|
-
- **large
|
|
530
|
+
按**复杂度/上下文轴**判断本次变更规模(2026-09-29-brainstorm-exit-thin-default:此前文件数轴「≤2 文件才 small」与选道表第 3 行的复杂度措辞不一致,且模糊需求探索完很少只碰 2 文件——出口几乎必然五阶段,实测坐实):
|
|
531
|
+
- **small(收编轻量道)**:单上下文可吞吐——预计无需 Wave 并行编排、无需上下文分片(单会话吃得下全部任务)、无多阶段治理/设计期人机对抗需求
|
|
532
|
+
- **large(完整五阶段)**:需要 Wave 并行编排、或单任务规模大需上下文分片、或多阶段治理、或用户显式要求完整流程
|
|
533
|
+
- **拿不准 → small**(与 thin-default-flip 同哲学:预判反诱发误升厚——R16 实证;升厚留运行时证据:实测失败自动升厚+--upgrade-thick 用户决策)
|
|
521
534
|
在 design.md frontmatter 写入 \`scale: small\` 或 \`scale: large\`(frontmatter 不存在则补 \`author\`/\`created_at\`/\`scale\`)。规模决定下面的产物范围和实现路径。
|
|
522
|
-
(注:早期「加载项目上下文」步骤已做过一次粗判并可能建议过走 quick;此处基于 design
|
|
535
|
+
(注:早期「加载项目上下文」步骤已做过一次粗判并可能建议过走 quick;此处基于 design 方案实质做精判兜底——判据同轴。)
|
|
523
536
|
|
|
524
537
|
### 操作
|
|
525
538
|
1. **按规模生成规范文件**:
|
package/src/stages/knowledge.js
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
import { existsSync, readFileSync, readdirSync, mkdirSync, statSync } from 'fs'
|
|
20
20
|
import { join, basename } from 'path'
|
|
21
21
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
|
-
import { parseKnowledgeIndex
|
|
22
|
+
import { parseKnowledgeIndex } from '../knowledge-match.js'
|
|
23
|
+
import { matchKnowledgeHybrid } from '../knowledge-vector.js'
|
|
23
24
|
|
|
24
25
|
// ── 工具函数 ──
|
|
25
26
|
|
|
@@ -106,8 +107,8 @@ export async function cmdSearch(dir, args, opts = {}) {
|
|
|
106
107
|
return
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
// 复用 knowledge-match
|
|
110
|
-
const result =
|
|
110
|
+
// 复用 knowledge-match 引擎(三层 hybrid:路由→平台向量→本地词片,2026-09-29-knowledge-vector-recall)
|
|
111
|
+
const result = await matchKnowledgeHybrid(knowledgeDir, query, { cwd: dir })
|
|
111
112
|
|
|
112
113
|
if (!result.matched) {
|
|
113
114
|
output(true, { query, matches: [] })
|
|
@@ -21,7 +21,7 @@ import { parseFileChangeList, pathMatches } from '../change-list.js'
|
|
|
21
21
|
import { getRule } from '../stage-contract-spec.js'
|
|
22
22
|
import { TASKCARD_PLACEHOLDERS } from '../taskcard-placeholders.js'
|
|
23
23
|
import { validateScriptCommands } from './cmd-existence.js'
|
|
24
|
-
import { parseTaskFrontmatter } from '../taskcard-frontmatter.js'
|
|
24
|
+
import { parseTaskFrontmatter, diagnoseTaskYamlError } from '../taskcard-frontmatter.js'
|
|
25
25
|
|
|
26
26
|
// ═══════════════════════════════════════════════════════════════
|
|
27
27
|
// 解析工具(从 plan.js 迁移)
|
|
@@ -614,7 +614,8 @@ export function validateBlueprintConsistency(changeDir, opts = {}) {
|
|
|
614
614
|
if (ts.length > 1) {
|
|
615
615
|
errors.push(
|
|
616
616
|
`路径 ${p} 被 Wave ${w} 内 ${ts.length} 个 task 修改: ${ts.join(', ')} — 同 Wave 任务 execute 强制并行(execute.js:603),` +
|
|
617
|
-
`子代理会互相覆盖该文件。解法:把它们拆到不同 Wave
|
|
617
|
+
`子代理会互相覆盖该文件。解法:把它们拆到不同 Wave(串行)——手工拆易踩编号/依赖坑(非合法 Wave 号→伪并行串行链),` +
|
|
618
|
+
`推荐 sillyspec plan-adopt-waves --change <变更名> 按 depends_on 拓扑一键重排(重排后同波文件面有交集会被拒绝,再手工拆分那一个)。` +
|
|
618
619
|
`(注:quick 阶段对同文件并发只提示不阻断——那是轻量流程无并行子代理、最坏后果 git 可分离;` +
|
|
619
620
|
`此处硬拦是因并行覆盖不可自动恢复,两阶段宽严不同是设计使然)`
|
|
620
621
|
)
|
|
@@ -1309,8 +1310,12 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
|
|
|
1309
1310
|
if (!fmParse.ok) {
|
|
1310
1311
|
const isDupReported = dupKeys.length > 0 && /duplicated mapping key/i.test(fmParse.error.message)
|
|
1311
1312
|
if (!isDupReported) {
|
|
1313
|
+
// 分诊(2026-10-06-verify-friction-fix task-03):js-yaml 消息 + 出错行双信号给中文修复
|
|
1314
|
+
// 动作——知识源是 taskcard-rules.md,此前只透传英文原文,厚流程实证连撞 6 轮门禁
|
|
1315
|
+
const errLineText = (content.split('\n')[fmParse.error.line - 1] || '').replace(/\r$/, '')
|
|
1316
|
+
const fmDiag = diagnoseTaskYamlError(fmParse.error.message, errLineText)
|
|
1312
1317
|
errors.push(
|
|
1313
|
-
`${dupTaskId || file}: frontmatter 非法 YAML(${file}:${fmParse.error.line}:${fmParse.error.column} ${fmParse.error.message})——契约/验收/标量字段全链路不可读,plan
|
|
1318
|
+
`${dupTaskId || file}: frontmatter 非法 YAML(${file}:${fmParse.error.line}:${fmParse.error.column} ${fmParse.error.message})——契约/验收/标量字段全链路不可读,plan 门禁拒绝放行;分诊:${fmDiag}`
|
|
1314
1319
|
)
|
|
1315
1320
|
}
|
|
1316
1321
|
}
|
|
@@ -1472,6 +1477,45 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
|
|
|
1472
1477
|
return { ok: errors.length === 0, errors, warnings }
|
|
1473
1478
|
}
|
|
1474
1479
|
|
|
1480
|
+
/**
|
|
1481
|
+
* taskcard --validate 的 CLI 入口(2026-10-06-verify-friction-fix task-03):plan 门禁同源规则
|
|
1482
|
+
* 的零成本自检面——填卡后、--done 门禁前先跑,把「门禁时一次撞一批」前移成「提交前自检」。
|
|
1483
|
+
* 在 validatePlanFeasibility 之上补 target_files 严格形态检查(对账门规则,plan feasibility 不含)。
|
|
1484
|
+
* 注意防环:本函数留在 plan-postcheck(其依赖链已有 taskcard-frontmatter/placeholders),
|
|
1485
|
+
* taskcard.js 不得反向 import 本模块(taskcard-placeholders.js jsdoc 的 ESM 循环告诫同源)。
|
|
1486
|
+
* @param {{ changeDir: string, projectRoot?: string|null }} opts
|
|
1487
|
+
* @returns {{ ok: boolean, errors: string[], warnings: string[], files: number }}
|
|
1488
|
+
*/
|
|
1489
|
+
export function validateTaskcardsCli({ changeDir, projectRoot = null }) {
|
|
1490
|
+
const res = validatePlanFeasibility(changeDir, projectRoot)
|
|
1491
|
+
const errors = [...res.errors]
|
|
1492
|
+
const warnings = [...res.warnings]
|
|
1493
|
+
let files = 0
|
|
1494
|
+
try {
|
|
1495
|
+
const tasksDir = pJoin(changeDir, 'tasks')
|
|
1496
|
+
if (existsSync(tasksDir)) {
|
|
1497
|
+
const list = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).sort()
|
|
1498
|
+
files = list.length
|
|
1499
|
+
for (const f of list) {
|
|
1500
|
+
const content = _readFileSync(pJoin(tasksDir, f), 'utf8')
|
|
1501
|
+
const fmParse = parseTaskFrontmatter(content)
|
|
1502
|
+
if (!fmParse.ok) continue // 0b 已报,不双报
|
|
1503
|
+
const tf = parseTargetFiles(content)
|
|
1504
|
+
if (!tf.missing) {
|
|
1505
|
+
const bad = tf.entries.filter(e => e.invalid)
|
|
1506
|
+
if (bad.length > 0) {
|
|
1507
|
+
const id = (content.match(/^id:\s*(.+)/m)?.[1] || '').trim() || f
|
|
1508
|
+
errors.push(`${id}: target_files 形态非法——${bad.map(b => `${b.raw}(${b.invalid})`).join(';')}`)
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
} catch {
|
|
1514
|
+
// 目录不可读 → 可行性结果原样返回(其 errors/warnings 已如实反映)
|
|
1515
|
+
}
|
|
1516
|
+
return { ok: errors.length === 0, errors, warnings, files }
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1475
1519
|
/**
|
|
1476
1520
|
* Plan 产物校验:检查 plan.md 和 tasks/ 是否齐全
|
|
1477
1521
|
* @param {string} changeDir - 变更目录
|
|
@@ -2010,7 +2054,46 @@ function deriveModuleIdFor(filePath, mapYaml) {
|
|
|
2010
2054
|
* @param {{ cwd: string, specRoot?: string, resolveChangeDir: Function, progress?: object }} context
|
|
2011
2055
|
* @throws {Error} 校验失败时抛出
|
|
2012
2056
|
*/
|
|
2057
|
+
/**
|
|
2058
|
+
* Wave 形态错误自动重排判定(2026-10-07-wave-auto-adopt-review-dedup task-01):postmortem
|
|
2059
|
+
* (provider-model-list)实测 Wave 返工链三轮——同 Wave 冲突/伪并行串行链都是 adopt-waves
|
|
2060
|
+
* 机械可解的形态,却要 agent 撞 postcheck 后按文案找命令。此处错误全为 Wave 形态类时自动
|
|
2061
|
+
* 跑 plan-adopt-waves 重排并整体复跑 postcheck(预算 1 次,防环);逃生键 local.yaml
|
|
2062
|
+
* plan.auto_adopt_waves: false(缺省 true)。混有非 Wave 类错误不自动重排(行为=现状)——
|
|
2063
|
+
* 重排会掩盖真实问题的上下文。
|
|
2064
|
+
*/
|
|
2065
|
+
const WAVE_SHAPE_ERROR_RE = /被 Wave \d+ 内 \d+ 个 task 修改|伪并行串行链|Wave 依赖方向违规/
|
|
2066
|
+
function allWaveShapeErrors(errors) {
|
|
2067
|
+
const list = Array.isArray(errors) ? errors : []
|
|
2068
|
+
return list.length > 0 && list.every(e => WAVE_SHAPE_ERROR_RE.test(String(e)))
|
|
2069
|
+
}
|
|
2070
|
+
function autoAdoptWavesEnabled(specDir) {
|
|
2071
|
+
try {
|
|
2072
|
+
const p = pJoin(specDir, 'local.yaml')
|
|
2073
|
+
if (!existsSync(p)) return true
|
|
2074
|
+
const doc = jsYaml.load(_readFileSync(p, 'utf8'))
|
|
2075
|
+
return !(doc && doc.plan && doc.plan.auto_adopt_waves === false)
|
|
2076
|
+
} catch { return true }
|
|
2077
|
+
}
|
|
2078
|
+
async function tryAutoAdoptWaves(errors, changeDir, specDir, budget) {
|
|
2079
|
+
if (budget <= 0) return false
|
|
2080
|
+
if (!allWaveShapeErrors(errors)) return false
|
|
2081
|
+
if (!autoAdoptWavesEnabled(specDir)) return false
|
|
2082
|
+
const { adoptPlanWaves } = await import('../plan-adopt-waves.js')
|
|
2083
|
+
const res = adoptPlanWaves({ changeDir })
|
|
2084
|
+
if (!res || !res.ok) {
|
|
2085
|
+
console.warn(` ⚠️ Wave 自动拓扑重排未成(${res && res.error ? res.error : '未知原因'})——按原报错处理(手工出口:sillyspec plan-adopt-waves --change <变更名>)`)
|
|
2086
|
+
return false
|
|
2087
|
+
}
|
|
2088
|
+
console.log(`\n🔀 检测到 Wave 形态问题——已自动按 depends_on 拓扑重排 plan.md(plan-adopt-waves,任务总表 W 列同步)并整体复跑校验:`)
|
|
2089
|
+
return true
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2013
2092
|
export async function executePlanPostcheck(context) {
|
|
2093
|
+
return executePlanPostcheckRun(context, { autoAdoptBudget: 1 })
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
async function executePlanPostcheckRun(context, { autoAdoptBudget = 0 } = {}) {
|
|
2014
2097
|
const { cwd, specRoot, resolveChangeDir, progress } = context
|
|
2015
2098
|
|
|
2016
2099
|
const specDir = specRoot || pJoin(cwd, '.sillyspec')
|
|
@@ -2207,6 +2290,14 @@ export async function executePlanPostcheck(context) {
|
|
|
2207
2290
|
|
|
2208
2291
|
// ── 聚合输出:一轮 --done 暴露全部失败项(坑6③)──
|
|
2209
2292
|
if (failures.length > 0) {
|
|
2293
|
+
// Wave 形态自动重排判定(task-01):唯一失败组是蓝图一致性且错误全为 Wave 形态 → 自动
|
|
2294
|
+
// adopt-waves 重排并整体复跑一次(预算 1)。挂统一收口点而非 consistency 单点:混有
|
|
2295
|
+
// feasibility/coverage 等其他类错误时不重排(重排会掩盖真实问题上下文,实测 WA4 形态)。
|
|
2296
|
+
if (failures.length === 1 && failures[0].name === '蓝图一致性校验') {
|
|
2297
|
+
if (await tryAutoAdoptWaves(failures[0].errors, changeDir, specDir, autoAdoptBudget)) {
|
|
2298
|
+
return executePlanPostcheckRun(context, { autoAdoptBudget: 0 })
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2210
2301
|
console.error(`\n❌ plan postcheck 失败(${failures.length} 类问题,已全部列出——一次修复后重跑,无需逐个迭代):`)
|
|
2211
2302
|
for (const f of failures) {
|
|
2212
2303
|
console.error(`\n 【${f.name}】`)
|
|
@@ -2347,6 +2438,10 @@ export async function executePlanPostcheck(context) {
|
|
|
2347
2438
|
const structure = assessWaveStructure({ existingWaves, topoWaves: waves, depMap, wavePathSets })
|
|
2348
2439
|
if (structure.advisory) console.warn(` ⚠️ ${structure.advisory}`)
|
|
2349
2440
|
if (structure.error) {
|
|
2441
|
+
// 伪并行碎片自动重排(task-01):mergeable 对 ≥2 是 adopt-waves 的标准解
|
|
2442
|
+
if (await tryAutoAdoptWaves([structure.error], changeDir, specDir, autoAdoptBudget)) {
|
|
2443
|
+
return executePlanPostcheckRun(context, { autoAdoptBudget: 0 })
|
|
2444
|
+
}
|
|
2350
2445
|
console.error(`\n❌ ${structure.error}`)
|
|
2351
2446
|
throw new Error(structure.error)
|
|
2352
2447
|
}
|