sillyspec 3.29.4 → 3.30.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 +57 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +18 -116
- package/AGENTS.md +38 -20
- package/SKILL.md +9 -3
- package/assets/command-cards/flow.md +32 -0
- package/assets/command-cards/run-archive.md +26 -0
- package/assets/command-cards/run-brainstorm.md +33 -0
- package/assets/command-cards/run-execute.md +29 -0
- package/assets/command-cards/run-plan.md +27 -0
- package/assets/command-cards/run-quick.md +17 -0
- package/assets/command-cards/run-verify.md +29 -0
- package/assets/command-cards/status.md +27 -0
- package/package.json +2 -1
- package/packages/dashboard/server/executor.js +2 -1
- package/packages/dashboard/server/parser.js +7 -7
- package/r7-test-env/README-R7-TEST.md +93 -0
- package/r7-test-env/demo-project/lib/calc.js +6 -0
- package/r7-test-env/demo-project/package.json +1 -0
- package/r7-test-env/demo-project/test.js +10 -0
- package/r7-test-env/prompt-R7-SMOKE.md +41 -0
- package/r7-test-env/sillyspec-r7.cmd +3 -0
- package/round4/account.mjs +98 -0
- package/round4/b1-analyze-task08.mjs +40 -0
- package/round4/b1-b1-target-validation.md +32 -0
- package/round4/b3a-prompt-audit-result.md +66 -0
- package/round4/b3a-prompt-audit.mjs +89 -0
- package/round4/brief-autocompact.md +34 -0
- package/round4/brief-session-replay.md +50 -0
- package/round4/deep-read-synthesis.md +38 -0
- package/round4/extract-full.mjs +38 -0
- package/round4/extract-timeline.mjs +43 -0
- package/round4/find-sessions.mjs +13 -0
- package/round4/full-r4-l.md +4707 -0
- package/round4/full-r4-o-l.md +4300 -0
- package/round4/full-r4-os.md +1681 -0
- package/round4/full-r4-sf.md +4255 -0
- package/round4/full-r4-sq.md +1734 -0
- package/round4/gate-audit-relay-receipt.md +30 -0
- package/round4/gate-value-audit.md +52 -0
- package/round4/optimization-plan.md +165 -0
- package/round4/prereg.md +67 -0
- package/round4/prompt-R4-L.md +63 -0
- package/round4/prompt-R4-O-L.md +59 -0
- package/round4/prompt-R4-O-S.md +56 -0
- package/round4/prompt-R4-S-F.md +60 -0
- package/round4/prompt-R4-S-Q.md +66 -0
- package/round4/r4-final-report.html +347 -0
- package/round4/r4-os-timeline.txt +297 -0
- package/round4/r4-sf-timeline.txt +738 -0
- package/round4/r4-sq-timeline.txt +322 -0
- package/round4/r4l-timeline.txt +793 -0
- package/round4/r4ol-timeline.txt +729 -0
- package/round4/r4sf-timeline.txt +852 -0
- package/round4/r5-scenario-matrix.md +46 -0
- package/round4/result-S.md +171 -0
- package/round4/result-final.md +46 -0
- package/round4/result-quality.md +63 -0
- package/src/change-list.js +9 -0
- package/src/classify-change.js +6 -0
- package/src/command-cards.js +74 -0
- package/src/commit-guard.js +2 -0
- package/src/config-schema.js +64 -12
- package/src/contract-matrix.js +26 -5
- package/src/db.js +35 -4
- package/src/decision-distill.js +9 -3
- package/src/diagnostic-codes.js +2 -1
- package/src/docs-check.js +1 -1
- package/src/doctor-diagnostics.js +44 -2
- package/src/flow-draft.js +748 -0
- package/src/flow-parity.js +236 -0
- package/src/flow-review.js +184 -0
- package/src/flow.js +1492 -0
- package/src/foreign-declared.js +17 -1
- package/src/fr-index.js +396 -22
- package/src/fs-atomic.js +5 -1
- package/src/git-helper.js +2 -2
- package/src/greenfield-bootstrap.js +98 -0
- package/src/handoff.js +120 -7
- package/src/hunk-attribution.js +191 -0
- package/src/index.js +602 -21
- package/src/init.js +25 -8
- package/src/knowledge-classify.js +47 -1
- package/src/knowledge-digest.js +162 -0
- package/src/knowledge-quicklog.js +246 -0
- package/src/machine-draft.js +128 -0
- package/src/machine-interface.js +194 -7
- package/src/mcp-server.js +1 -0
- package/src/module-impact.js +51 -22
- package/src/module-resolve.js +3 -1
- package/src/preview-progress.js +160 -0
- package/src/progress/change-registry.js +11 -3
- package/src/progress/shared.js +3 -2
- package/src/progress.js +56 -13
- package/src/quicklog.js +44 -6
- package/src/redomain.js +147 -0
- package/src/review-material-pack.js +204 -1
- package/src/run/archive-distill.js +4 -2
- package/src/run/bg-sync.js +67 -0
- package/src/run/command.js +127 -10
- package/src/run/complete-handlers.js +363 -73
- package/src/run/complete.js +356 -74
- package/src/run/gate-snapshot-ledger.js +213 -0
- package/src/run/gate-snapshot.js +260 -38
- package/src/run/gates.js +482 -268
- package/src/run/green-cache.js +123 -0
- package/src/run/junction-rm.js +70 -0
- package/src/run/next.js +14 -0
- package/src/run/prompt.js +185 -55
- package/src/run/quick-audit.js +213 -14
- package/src/run/shared.js +123 -4
- package/src/run/stage.js +182 -216
- package/src/run/test-ledger.js +264 -0
- package/src/run/verify-quality-scan.js +84 -7
- package/src/scope-audit.js +6 -1
- package/src/sentinel-assertions.js +114 -0
- package/src/setup.js +3 -3
- package/src/spec-sync.js +41 -1
- package/src/stage-contract.js +26 -8
- package/src/stage-review-checklist.js +2 -0
- package/src/stage-review.js +7 -2
- package/src/stage-templates.js +157 -0
- package/src/stages/archive.js +2 -0
- package/src/stages/brainstorm.js +24 -106
- package/src/stages/execute.js +222 -85
- package/src/stages/knowledge.js +74 -1
- package/src/stages/plan-postcheck.js +385 -20
- package/src/stages/plan.js +23 -1
- package/src/stages/quick.js +10 -2
- package/src/stages/verify.js +4 -2
- package/src/sync.js +81 -23
- package/src/task-done.js +181 -0
- package/src/task-review.js +8 -55
- package/src/taskcard-frontmatter.js +57 -0
- package/src/test-bindings.js +549 -0
- package/src/timeline.js +218 -0
- package/src/ui-visual.js +175 -0
- package/src/verify-draft.js +214 -0
- package/src/verify-postcheck.js +919 -159
- package/src/verify-probes.js +103 -25
- package/src/watcher.js +1031 -0
- package/src/worktree-apply.js +44 -4
- package/src/worktree-deps.js +22 -8
- package/src/worktree-policy.js +42 -0
- package/src/worktree.js +102 -15
- package/templates/agents-instruction.md +37 -18
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* machine-draft.js — MACHINE-DRAFT 指纹三件套泛化(R7 切片三 task-04 / D-004 / FR-08)。
|
|
3
|
+
*
|
|
4
|
+
* 从 verify-draft.js(verify 填槽制 v2 件二)抽出通用原语,供 flow 四件机器稿(flow-draft)
|
|
5
|
+
* 复用;verify-draft 改为消费方(导出签名与行为逐字不动,零回归硬门)。
|
|
6
|
+
*
|
|
7
|
+
* 三件套语义(护栏#2:守卫必须是验收侧的机制,永远不是生成侧的劝说):
|
|
8
|
+
* - wrapSection:机器预填段以 MACHINE-DRAFT begin/end 注释对包裹,begin 内嵌段内容
|
|
9
|
+
* sha256(CRLF 归一);amendCmd 按消费方参数化(verify-result 指向 verify-probes
|
|
10
|
+
* --amend-draft,flow 稿指向 flow amend-draft——勿把 agent 引向错误命令);
|
|
11
|
+
* - verifyMarkers:三态拒收判定——标记缺失(被删/整份重写)/内容哈希失配(改写)/
|
|
12
|
+
* 手工重锚未审计(标记哈希与台账不一致);
|
|
13
|
+
* - reanchorText:按当前内容重锚哈希的纯文本变换(amend 留痕通道的机械半边;
|
|
14
|
+
* 台账审计与落盘归消费方)。
|
|
15
|
+
*
|
|
16
|
+
* 本模块纯函数(无 fs)——md 文本与台账对象由消费方读写;段 key 命名空间归消费方
|
|
17
|
+
* (verify-result 单文件段键 / flow 按文件名+段键组合)。
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from 'node:crypto'
|
|
20
|
+
|
|
21
|
+
export const MARK_BEGIN_RE = /^<!--\s*MACHINE-DRAFT:([\w.-]+):([0-9a-f]{64}):begin.*-->\s*$/
|
|
22
|
+
const MARK_END_RE = /^<!--\s*MACHINE-DRAFT:([\w.-]+):end.*-->\s*$/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 任务勾选态归一(2026-09-25-thin-done-gate-calibration 坑1):勾选是 flow start 横幅教的
|
|
26
|
+
* 预期写面(收口哨兵的证据面),不该参与「被改写」判定——哈希前把 `- [x] task-NN` 归一回
|
|
27
|
+
* `- [ ]`(draft 产出恒 [ ],存量台账两侧归一后同哈希,兼容;真实改写任务文本/增删行仍失配)。
|
|
28
|
+
* 口径只认 task-NN 行,普通清单勾选行不归一(不扩大书写面)。
|
|
29
|
+
*/
|
|
30
|
+
const TASK_CHECK_STATE_RE = /^([-*] \[)[ xX]\](?= task-\d)/gm
|
|
31
|
+
function normalizeTaskCheckboxState(s) {
|
|
32
|
+
return String(s).replace(TASK_CHECK_STATE_RE, '$1 ]')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 段内容哈希(CRLF 归一——跨平台写盘行尾漂移不炸指纹;task-NN 勾选态归一——见上)。 */
|
|
36
|
+
export function bodyHash(s) {
|
|
37
|
+
return createHash('sha256').update(normalizeTaskCheckboxState(String(s).replace(/\r\n/g, '\n'))).digest('hex')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 包裹机器段(begin 标记 + body + end 标记 + 尾空行)。标记格式与既有 MARK_*_RE 逐字兼容。
|
|
42
|
+
* @returns {string} 四行块(含尾空行,消费方按原位替换)
|
|
43
|
+
*/
|
|
44
|
+
export function wrapSection({ key, body, amendCmd, guardNote = '整段改写会被 verify --done 拒收' }) {
|
|
45
|
+
return [
|
|
46
|
+
`<!-- MACHINE-DRAFT:${key}:${bodyHash(body)}:begin 机器预填段——${guardNote};确要修改:${amendCmd} 留痕重锚 -->`,
|
|
47
|
+
body,
|
|
48
|
+
`<!-- MACHINE-DRAFT:${key}:end -->`,
|
|
49
|
+
'',
|
|
50
|
+
].join('\n')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 解析 md 中全部标记段(CRLF 归一)。
|
|
55
|
+
* @returns {{key:string, markerHash:string, contentLines:string[]}[]}
|
|
56
|
+
*/
|
|
57
|
+
export function parseMarkerBlocks(text) {
|
|
58
|
+
const lines = String(text).replace(/\r\n/g, '\n').split('\n')
|
|
59
|
+
const blocks = []
|
|
60
|
+
let current = null
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
const b = line.match(MARK_BEGIN_RE)
|
|
63
|
+
if (b) { current = { key: b[1], markerHash: b[2], contentLines: [] }; blocks.push(current); continue }
|
|
64
|
+
const e = line.match(MARK_END_RE)
|
|
65
|
+
if (e) { current = null; continue }
|
|
66
|
+
if (current) current.contentLines.push(line)
|
|
67
|
+
}
|
|
68
|
+
return blocks
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 三态拒收判定(--done 验收侧承重件的纯函数半边)。
|
|
73
|
+
* @param {object} p
|
|
74
|
+
* @param {string} p.text md 全文
|
|
75
|
+
* @param {Object<string,{hash:string}>} p.sections 台账段哈希(sidecar.sections 形态)
|
|
76
|
+
* @param {string} p.amendCmd 修复指引命令文案(消费方参数化)
|
|
77
|
+
* @returns {string[]} violations(空=干净;文案含修复指引)
|
|
78
|
+
*/
|
|
79
|
+
export function verifyMarkers({ text, sections, amendCmd }) {
|
|
80
|
+
const blocks = parseMarkerBlocks(text)
|
|
81
|
+
const byKey = new Map(blocks.map((b) => [b.key, b]))
|
|
82
|
+
const violations = []
|
|
83
|
+
const fix = (key) => `还原机器段原文,或确要修改时跑 ${amendCmd} 留痕重锚(段键 ${key})`
|
|
84
|
+
for (const [key, rec] of Object.entries(sections || {})) {
|
|
85
|
+
const b = byKey.get(key)
|
|
86
|
+
if (!b) {
|
|
87
|
+
violations.push(`机器段「${key}」的 MACHINE-DRAFT 标记缺失(被删除或整份重写)——${fix(key)}`)
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
const actual = bodyHash(b.contentLines.join('\n'))
|
|
91
|
+
if (actual !== rec.hash) {
|
|
92
|
+
violations.push(`机器段「${key}」内容与指纹失配(被改写)——${fix(key)}`)
|
|
93
|
+
} else if (b.markerHash !== rec.hash) {
|
|
94
|
+
// 内容未动但标记哈希与台账不一致 = 标记被手工重锚而未经 amend 审计
|
|
95
|
+
violations.push(`机器段「${key}」标记指纹与 sidecar 台账不一致(未经 --amend-draft 的手工重锚)——${fix(key)}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return violations
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 按当前内容重锚全部标记段(纯文本变换;不落盘不记台账——消费方持 mdPath/台账)。
|
|
103
|
+
* @returns {{text:string, keys:string[], contentByKey:Object<string,string>}}
|
|
104
|
+
*/
|
|
105
|
+
export function reanchorText({ text, amendCmd, guardNote = '整段改写会被 verify --done 拒收' }) {
|
|
106
|
+
const lines = String(text).replace(/\r\n/g, '\n').split('\n')
|
|
107
|
+
const contentLinesByKey = new Map()
|
|
108
|
+
let current = null
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
const b = line.match(MARK_BEGIN_RE)
|
|
111
|
+
if (b) { current = b[1]; contentLinesByKey.set(current, []); continue }
|
|
112
|
+
const e = line.match(MARK_END_RE)
|
|
113
|
+
if (e) { current = null; continue }
|
|
114
|
+
if (current) contentLinesByKey.get(current).push(line)
|
|
115
|
+
}
|
|
116
|
+
const keys = [...contentLinesByKey.keys()]
|
|
117
|
+
const next = lines.map((line) => {
|
|
118
|
+
const b = line.match(MARK_BEGIN_RE)
|
|
119
|
+
if (!b) return line
|
|
120
|
+
const content = (contentLinesByKey.get(b[1]) || []).join('\n')
|
|
121
|
+
return `<!-- MACHINE-DRAFT:${b[1]}:${bodyHash(content)}:begin 机器预填段——${guardNote};确要修改:${amendCmd} 留痕重锚 -->`
|
|
122
|
+
}).join('\n')
|
|
123
|
+
const contentByKey = {}
|
|
124
|
+
for (const k of keys) contentByKey[k] = (contentLinesByKey.get(k) || []).join('\n')
|
|
125
|
+
return { text: next, keys, contentByKey }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default { wrapSection, parseMarkerBlocks, verifyMarkers, reanchorText, bodyHash, MARK_BEGIN_RE }
|
package/src/machine-interface.js
CHANGED
|
@@ -15,17 +15,17 @@
|
|
|
15
15
|
* 仅依赖 Node 18+ 原生 API,零新增外部依赖。
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { existsSync, readFileSync } from 'fs';
|
|
18
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
19
19
|
import { join } from 'path';
|
|
20
20
|
import { ProgressManager, resolveSpecDir } from './progress.js';
|
|
21
21
|
import { resolveRuntimeRoot } from './run/shared.js';
|
|
22
22
|
import { runValidators, checkTransition, checkExecuteCodeEvidence } from './stage-contract.js';
|
|
23
23
|
import { validateTaskReviews, resolveLatestExecuteRunId } from './task-review.js';
|
|
24
24
|
import { runVerifyTestCheck } from './verify-postcheck.js';
|
|
25
|
+
import { computeGateFingerprint, lookupGreenCache, storeGreenCache, greenCacheNotice } from './run/green-cache.js';
|
|
25
26
|
import { checkCode } from './diagnostic-codes.js';
|
|
26
27
|
|
|
27
28
|
// ============ 退出码常量(D-004@v1)============
|
|
28
|
-
|
|
29
29
|
export const EXIT_OK = 0; // 核验通过(可含 warnings)
|
|
30
30
|
export const EXIT_BLOCKED = 1; // 事实性阻断(JSON 含 errors)
|
|
31
31
|
export const EXIT_UNKNOWN = 2; // 无法核验(用法错/变更不存在/环境错/内部异常)
|
|
@@ -117,7 +117,31 @@ export function buildEnvelope({
|
|
|
117
117
|
* 让两函数按 ctx 切跨仓 gitDir / per-repo cwd。null 时两函数按既有单仓行为零回归。
|
|
118
118
|
* @returns {Promise<{ envelope: object, exitCode: number }>}
|
|
119
119
|
*/
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* verify 门禁 check 的绿结果缓存包装(R8 对撞修复 2026-09-23,实现见 src/run/green-cache.js)。
|
|
122
|
+
* 同指纹(HEAD+代码脏面+local.yaml)近期真跑的绿结果复用:命中 → synth(hit) 合成等价结果
|
|
123
|
+
* (带 cached 披露字段,调用方进 warnings——「结果是真的、本次没跑」必须明示);未命中 →
|
|
124
|
+
* 真跑,status==='passed' 时写缓存。fail-open:无根/指纹失败/缓存异常一律回退真跑(零回归)。
|
|
125
|
+
*/
|
|
126
|
+
function greenCachedCheck({ kind, greenRoot, cwd, specBase, changeName }, runReal, synth) {
|
|
127
|
+
if (!greenRoot) return runReal();
|
|
128
|
+
let fingerprint = null;
|
|
129
|
+
try { fingerprint = computeGateFingerprint({ cwd, specBase }); } catch { fingerprint = null; }
|
|
130
|
+
if (!fingerprint) return runReal();
|
|
131
|
+
const scope = `change:${changeName}`;
|
|
132
|
+
let hit = null;
|
|
133
|
+
try { hit = lookupGreenCache({ runtimeRoot: greenRoot, scope, kind, fingerprint }); } catch { hit = null; }
|
|
134
|
+
if (hit) return synth(hit);
|
|
135
|
+
const result = runReal();
|
|
136
|
+
try {
|
|
137
|
+
if (result && result.status === 'passed') {
|
|
138
|
+
storeGreenCache({ runtimeRoot: greenRoot, scope, kind, fingerprint, result: { status: 'passed', exitCode: result.exitCode ?? 0, mode: result.mode ?? null } });
|
|
139
|
+
}
|
|
140
|
+
} catch { /* best-effort 写缓存 */ }
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, specDriftAnchor, ctx = null, full = false } = {}) {
|
|
121
145
|
const specRoot = specBase || resolveSpecDir(cwd);
|
|
122
146
|
// A4:pm 用 specRoot(而非默认 resolveSpecDir(cwd))——--spec-dir/平台模式下 specBase 是真实
|
|
123
147
|
// specDir,默认解析会指向本地孤儿库,与下方 planContent/changeDir 的 specRoot 两套事实源混拼。
|
|
@@ -289,14 +313,21 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
289
313
|
|
|
290
314
|
// ── d. verify 阶段追加 verify-test(CLI 实测 local.yaml commands.test)──
|
|
291
315
|
if (stage === 'verify') {
|
|
292
|
-
|
|
316
|
+
// 绿缓存根:与 runtimeRoot 解析同源(drift 锚主仓口径);解析失败 null → 不缓存不命中
|
|
317
|
+
let greenRoot = null;
|
|
318
|
+
try { greenRoot = resolveRuntimeRoot({ runtimeRoot, specDriftAnchor }, specRoot); } catch { greenRoot = null; }
|
|
319
|
+
const vt = greenCachedCheck({ kind: 'test', greenRoot, cwd, specBase: specRoot, changeName },
|
|
320
|
+
() => runVerifyTestCheck({ cwd, specBase: specRoot, changeName, ctx }),
|
|
321
|
+
(hit) => ({ status: 'passed', exitCode: 0, durationMs: 0, resultPath: null, mode: null, fallbackReason: null, exemptedCount: 0, cached: true, reason: greenCacheNotice(hit, 'verify-test') }));
|
|
293
322
|
const vtWarnings = [];
|
|
323
|
+
if (vt.cached) vtWarnings.push(vt.reason);
|
|
294
324
|
if (vt.status === 'skipped') {
|
|
295
325
|
vtWarnings.push(`⚠️ verify-test SKIPPED — gate 未核验测试(${vt.reason || 'local.yaml 未配置 commands.test 或显式无测试'})。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL`);
|
|
296
326
|
}
|
|
297
327
|
// known_failures 豁免披露:本次 PASS 含豁免,driver 应提示人工复核清单是否过宽
|
|
328
|
+
// (quick-E,2026-09-20 报告问题 E:提示原不带清单位置,复核要先翻一遍才找到——带路径)
|
|
298
329
|
if (vt.exemptedCount > 0) {
|
|
299
|
-
vtWarnings.push(`⚠️ verify-test PASS 含 ${vt.exemptedCount} 个 known_failures 豁免(${vt.reason || ''});driver
|
|
330
|
+
vtWarnings.push(`⚠️ verify-test PASS 含 ${vt.exemptedCount} 个 known_failures 豁免(${vt.reason || ''});driver 应提示人工复核豁免清单是否过宽(避免误豁免本变更引入的真实失败)——清单位置:${join(specRoot, 'local.yaml')} 的 known_failures 键`);
|
|
300
331
|
}
|
|
301
332
|
// 全量 fallback 明示:跑了全量 commands.test 但非变更范围子集,失败可能含未变更
|
|
302
333
|
// 模块的预存错误——driver 不应据 exit 0 判定本次变更范围已客观测试(见 3.24 verify 坑1)。
|
|
@@ -311,6 +342,7 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
311
342
|
warnings: vtWarnings,
|
|
312
343
|
data: {
|
|
313
344
|
status: vt.status,
|
|
345
|
+
cached: vt.cached === true,
|
|
314
346
|
exitCode: vt.exitCode,
|
|
315
347
|
durationMs: vt.durationMs,
|
|
316
348
|
resultPath: vt.resultPath,
|
|
@@ -318,6 +350,127 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
318
350
|
fallbackReason: vt.fallbackReason ?? null,
|
|
319
351
|
},
|
|
320
352
|
});
|
|
353
|
+
|
|
354
|
+
// ── d2. verify-lint(parity 补齐,2026-09-21 R4-S-F 实证)──
|
|
355
|
+
// gate verify 原先只查 test 不查 lint,而 verify --done 的 lint 隔离快照门会拦——
|
|
356
|
+
// gate PASS / --done FAIL 两套口径并存,误导 agent 诊断 16 分钟(对撞会话 db 实证)。
|
|
357
|
+
// 本检查与 --done 同引擎(runVerifyLintCheck + triageLintOwnership 归属降档);口径差异
|
|
358
|
+
// 永久明示:gate 在工作树直跑 lint,--done 在隔离快照(HEAD+归属文件 overlay)跑——
|
|
359
|
+
// 不一致时以 --done 为准。lint 未配置/不可跑 → informational,不阻断只读查询。
|
|
360
|
+
try {
|
|
361
|
+
const { runVerifyLintCheck, triageLintOwnership, resolveVerifyChangedFiles } = await import('./verify-postcheck.js');
|
|
362
|
+
const lc = greenCachedCheck({ kind: 'lint', greenRoot, cwd, specBase: specRoot, changeName },
|
|
363
|
+
() => runVerifyLintCheck({ cwd, specBase: specRoot }),
|
|
364
|
+
(hit) => ({ status: 'passed', exitCode: 0, durationMs: 0, failureFiles: [], cached: true, reason: greenCacheNotice(hit, 'verify-lint') }));
|
|
365
|
+
if (lc.status === 'skipped') {
|
|
366
|
+
checks.push({
|
|
367
|
+
id: 'verify-lint',
|
|
368
|
+
code: checkCode('verify-lint'),
|
|
369
|
+
ok: true,
|
|
370
|
+
informational: true,
|
|
371
|
+
warnings: [`verify-lint SKIPPED — ${lc.reason || 'local.yaml 未配置 commands.lint'};本次 gate 结论不含 lint 客观核验`],
|
|
372
|
+
data: { status: lc.status },
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
const lcWarnings = [
|
|
376
|
+
'口径说明:gate verify 的 lint 在工作树直跑;verify --done 的 lint 在隔离快照(HEAD+归属文件 overlay)跑——两口径不一致时以 --done 为准。',
|
|
377
|
+
];
|
|
378
|
+
if (lc.cached) lcWarnings.push(lc.reason);
|
|
379
|
+
let ownership = null;
|
|
380
|
+
let lintOk = lc.status !== 'failed';
|
|
381
|
+
if (lc.status === 'failed') {
|
|
382
|
+
const changed = resolveVerifyChangedFiles(cwd, changeName, null, { specBase: specRoot }) || [];
|
|
383
|
+
ownership = triageLintOwnership({ failureFiles: lc.failureFiles || [], changeFiles: changed });
|
|
384
|
+
if (ownership.verdict === 'pre-existing') {
|
|
385
|
+
lintOk = true; // 存量债不拦只读查询(与 --done 的 advisory 降档同口径)
|
|
386
|
+
lcWarnings.push('lint 失败文件与本变更零交集(HEAD 存量债)→ 本 check 降档放行;--done 同口径 advisory,建议顺手清偿解锁全仓 lint 门。');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
checks.push({
|
|
390
|
+
id: 'verify-lint',
|
|
391
|
+
code: checkCode('verify-lint'),
|
|
392
|
+
ok: lintOk,
|
|
393
|
+
errors: !lintOk ? [`lint 失败(工作树口径): ${lc.reason || ''}`] : [],
|
|
394
|
+
warnings: lcWarnings,
|
|
395
|
+
data: {
|
|
396
|
+
status: lc.status,
|
|
397
|
+
cached: lc.cached === true,
|
|
398
|
+
exitCode: lc.exitCode,
|
|
399
|
+
durationMs: lc.durationMs,
|
|
400
|
+
ownership: ownership ? ownership.verdict : null,
|
|
401
|
+
overlap: ownership ? ownership.overlap : [],
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
} catch (e) {
|
|
406
|
+
checks.push({
|
|
407
|
+
id: 'verify-lint',
|
|
408
|
+
code: checkCode('verify-lint'),
|
|
409
|
+
ok: true,
|
|
410
|
+
informational: true,
|
|
411
|
+
warnings: [`verify-lint 检查装配失败(fail-open): ${e && e.message ? e.message : e}`],
|
|
412
|
+
data: { status: 'error' },
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ── e. --full 只读预检档(P1 / FR-01 / D-002@batch3,2026-09-21-r5-efficiency-batch3 task-03)──
|
|
418
|
+
// 覆盖 --done 独有的贵门(batch2 实证四类盲区:reconcile 两轮/stage review 缺失各吃一整 agent
|
|
419
|
+
// 回合):①target_files reconcile 只读跑(reconcileTargetFiles 纯计算不落盘,--done 的写结果
|
|
420
|
+
// 文件在其调用方);②stage review 在场探测(marker+review.json——缺则报预期路径,同 --done
|
|
421
|
+
// Stage Review Gate 口径,只报缺不生成)。全只读零副作用;未传 full 零行为变化。
|
|
422
|
+
if (full && (stage === 'verify' || stage === 'execute')) {
|
|
423
|
+
// e1. reconcile(verify --done 的 target_files 对账门)
|
|
424
|
+
if (stage === 'verify') {
|
|
425
|
+
try {
|
|
426
|
+
const { reconcileTargetFiles } = await import('./verify-postcheck.js');
|
|
427
|
+
const rec = reconcileTargetFiles({ cwd, specBase: specRoot, changeName });
|
|
428
|
+
const recOk = rec.status !== 'missing_declared';
|
|
429
|
+
checks.push({
|
|
430
|
+
id: 'full-target-files-reconcile',
|
|
431
|
+
code: checkCode('full-target-files-reconcile') || 'full_target_files_reconcile',
|
|
432
|
+
ok: recOk,
|
|
433
|
+
errors: !recOk
|
|
434
|
+
? (rec.missing || []).map((m) => `task-${m.task || '?'}: ${m.path} 声明未做(②类计划落空)——补做交付或按声明修正通道改卡(与 review 结论一致)`)
|
|
435
|
+
: [],
|
|
436
|
+
warnings: (rec.undeclared || []).length > 0
|
|
437
|
+
? [`${(rec.undeclared || []).length} 个未声明文件(warning 级,${(rec.undeclared || []).slice(0, 3).map((u) => u.path).join('、')}${(rec.undeclared || []).length > 3 ? ' 等' : ''})——并行会话归因噪音或漏声明,verify 报告须裁决`]
|
|
438
|
+
: [],
|
|
439
|
+
data: { status: rec.status, matched: (rec.matched || []).length, missing: (rec.missing || []).length, undeclared: (rec.undeclared || []).length },
|
|
440
|
+
});
|
|
441
|
+
} catch (e) {
|
|
442
|
+
checks.push({
|
|
443
|
+
id: 'full-target-files-reconcile',
|
|
444
|
+
code: 'full_target_files_reconcile',
|
|
445
|
+
ok: true,
|
|
446
|
+
informational: true,
|
|
447
|
+
warnings: [`--full reconcile 检查装配失败(fail-open): ${e && e.message ? e.message : e}`],
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// e2. stage review 在场探测(verify/execute --done 的 Stage Review Gate)
|
|
452
|
+
try {
|
|
453
|
+
const { getLatestStageReviewRunId } = await import('./stage-review.js');
|
|
454
|
+
const rrRoot = runtimeRoot || join(specRoot, '.runtime');
|
|
455
|
+
const runId = getLatestStageReviewRunId(rrRoot, stage, changeName);
|
|
456
|
+
checks.push({
|
|
457
|
+
id: 'full-stage-review',
|
|
458
|
+
code: checkCode('full-stage-review') || 'full_stage_review_missing',
|
|
459
|
+
ok: !!runId,
|
|
460
|
+
errors: !runId
|
|
461
|
+
? [`缺 ${stage} 阶段的 stage review.json(tier=independent 要求独立审查产出)——--done 将被 Stage Review Gate 拦;可先 sillyspec register-stage-review --change ${changeName} --stage ${stage} 生成骨架再填(降级条款照 review.json reviewerNotes 首行留痕)`]
|
|
462
|
+
: [],
|
|
463
|
+
data: { runId: runId || null },
|
|
464
|
+
});
|
|
465
|
+
} catch (e) {
|
|
466
|
+
checks.push({
|
|
467
|
+
id: 'full-stage-review',
|
|
468
|
+
code: 'full_stage_review_missing',
|
|
469
|
+
ok: true,
|
|
470
|
+
informational: true,
|
|
471
|
+
warnings: [`--full stage review 探测装配失败(fail-open): ${e && e.message ? e.message : e}`],
|
|
472
|
+
});
|
|
473
|
+
}
|
|
321
474
|
}
|
|
322
475
|
|
|
323
476
|
// ── 综合结论:所有非 informational check 均 ok ──
|
|
@@ -461,9 +614,14 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
|
|
|
461
614
|
|
|
462
615
|
// ── b. verify-test:真实执行测试命令并取证 ──
|
|
463
616
|
case 'verify-test': {
|
|
464
|
-
|
|
617
|
+
let greenRoot = null;
|
|
618
|
+
try { greenRoot = resolveRuntimeRoot({ runtimeRoot, specDriftAnchor }, specRoot); } catch { greenRoot = null; }
|
|
619
|
+
const vt = greenCachedCheck({ kind: 'test', greenRoot, cwd, specBase: specRoot, changeName },
|
|
620
|
+
() => runVerifyTestCheck({ cwd, specBase: specRoot, changeName, ctx }),
|
|
621
|
+
(hit) => ({ status: 'passed', exitCode: 0, durationMs: 0, resultPath: null, mode: null, fallbackReason: null, exemptedCount: 0, cached: true, reason: greenCacheNotice(hit, 'verify-test') }));
|
|
465
622
|
data = {
|
|
466
623
|
status: vt.status,
|
|
624
|
+
cached: vt.cached === true,
|
|
467
625
|
exitCode: vt.exitCode,
|
|
468
626
|
durationMs: vt.durationMs,
|
|
469
627
|
resultPath: vt.resultPath,
|
|
@@ -472,7 +630,9 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
|
|
|
472
630
|
};
|
|
473
631
|
ok = vt.status !== 'failed';
|
|
474
632
|
errors = vt.status === 'failed' ? [`测试失败: ${vt.reason || ''}`] : [];
|
|
475
|
-
warnings = vt.
|
|
633
|
+
warnings = vt.cached
|
|
634
|
+
? [vt.reason]
|
|
635
|
+
: vt.status === 'skipped'
|
|
476
636
|
? ['测试被跳过']
|
|
477
637
|
: (vt.mode === 'full' && vt.fallbackReason)
|
|
478
638
|
? [`⚠️ verify-test 跑的是全量 commands.test(${vt.fallbackReason});失败可能含未变更模块的预存错误`]
|
|
@@ -667,3 +827,30 @@ export function runStatusOverview({ cwd, specBase } = {}) {
|
|
|
667
827
|
return { envelope, exitCode: EXIT_UNKNOWN };
|
|
668
828
|
}
|
|
669
829
|
}
|
|
830
|
+
|
|
831
|
+
// ============ gate 失败明细稳定路径落盘(quick-D,2026-09-20 报告问题 D)============
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* `sillyspec gate <stage> --change <name>` 的失败信封落盘(稳定路径,每次覆盖)。
|
|
835
|
+
* 背景:gate 失败明细只在 stdout——宿主把长命令转后台截断输出后 ❌ 段落实际取不回
|
|
836
|
+
* (2026-09-20 实证:agent 靠推断绕 3 轮)。runGate 本体保持只读契约(D-002@v1,不写
|
|
837
|
+
* sillyspec.db),落盘由本函数承担(CLI 层调用,fail-soft)——审计附件非状态。
|
|
838
|
+
* @param {{ specBase: string, stage: string, changeName: string, envelope: object }} opts
|
|
839
|
+
* @returns {string|null} 成功返回落盘路径;失败返回 null
|
|
840
|
+
*/
|
|
841
|
+
export function writeGateResultArtifact({ specBase, stage, changeName, envelope }) {
|
|
842
|
+
try {
|
|
843
|
+
const runsDir = join(specBase, '.runtime', 'verify-runs')
|
|
844
|
+
const resultPath = join(runsDir, `gate-${stage}-${changeName}.json`)
|
|
845
|
+
mkdirSync(runsDir, { recursive: true })
|
|
846
|
+
writeFileSync(resultPath, JSON.stringify({
|
|
847
|
+
written_at: new Date().toISOString(),
|
|
848
|
+
stage,
|
|
849
|
+
change: changeName,
|
|
850
|
+
envelope,
|
|
851
|
+
}, null, 2) + '\n')
|
|
852
|
+
return resultPath
|
|
853
|
+
} catch {
|
|
854
|
+
return null // fail-soft:落盘是审计附件,失败只缺附件不误门
|
|
855
|
+
}
|
|
856
|
+
}
|
package/src/mcp-server.js
CHANGED
package/src/module-impact.js
CHANGED
|
@@ -48,7 +48,15 @@ export function parseModuleMapPaths(yamlText) {
|
|
|
48
48
|
}
|
|
49
49
|
if (inPaths && currentModule) {
|
|
50
50
|
const item = line.match(/^ - (.+)$/)
|
|
51
|
-
if (item)
|
|
51
|
+
if (item) {
|
|
52
|
+
// 引号剥离(坑 map-quoted-path-deadmatch,2026-09-21 R4-L 实证):docs/<p>/modules/
|
|
53
|
+
// _module-map.yaml 存在带引号条目("frontend/src/app/page.tsx")——只 trim 不剥引号时
|
|
54
|
+
// classifyFile 的 === / startsWith 恒失配,引号条目全是死路径(frontend map 790 条
|
|
55
|
+
// 几乎全带引号=整图失效)。仅首尾成对引号才剥(路径内部引号不动)。
|
|
56
|
+
const raw = item[1].trim()
|
|
57
|
+
const unquoted = raw.replace(/^(['"])([\s\S]*)\1$/, '$2')
|
|
58
|
+
map.get(currentModule).push(unquoted.replace(/\\/g, '/'))
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
return map
|
|
@@ -89,18 +97,33 @@ export function syncModuleDocSidecars({ cwd, changeName, note, specDir = null })
|
|
|
89
97
|
// 复用 generateModuleImpactSkeleton 的 map 发现 + 归类(diff 来源)
|
|
90
98
|
const skel = generateModuleImpactSkeleton({ cwd, changeName, specDir })
|
|
91
99
|
if (!skel) return { synced: [], unmatched: [], skipped: [], reason: '无 module-map 或无 diff 可归类' }
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
// 结构化归类直用(classified 含 project 归属;multimap-first-found-mismatch 同坑修复面)——
|
|
101
|
+
// 不再反解 markdown(正则脆且丢 project 维度)。classified 缺席时兜底反解 + 首个 modules 目录。
|
|
102
|
+
let mods = []
|
|
103
|
+
if (Array.isArray(skel.classified) && skel.classified.length >= 0) {
|
|
104
|
+
const seen = new Set()
|
|
105
|
+
for (const r of skel.classified) {
|
|
106
|
+
const key = r.project + '/' + r.module
|
|
107
|
+
if (seen.has(key)) continue
|
|
108
|
+
seen.add(key)
|
|
109
|
+
mods.push({ project: r.project, module: r.module, modulesDir: join(specBase, 'docs', r.project, 'modules') })
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
const modulesDir = findModulesDir(specBase)
|
|
113
|
+
if (!modulesDir) return { synced: [], unmatched: [], skipped: [], reason: 'modules 目录不存在' }
|
|
114
|
+
mods = [...new Set([...skel.markdown.matchAll(/^\| (\S+) \| `([^`]+)`/gm)].map(m => m[1]))]
|
|
115
|
+
.map(mod => ({ project: null, module: mod, modulesDir }))
|
|
116
|
+
}
|
|
117
|
+
const unmatched = Array.isArray(skel.unmatchedFiles)
|
|
118
|
+
? skel.unmatchedFiles
|
|
119
|
+
: [...skel.markdown.matchAll(/^- `([^`]+)` /gm)].map(m => m[1])
|
|
97
120
|
const synced = []
|
|
98
121
|
const skipped = []
|
|
99
|
-
for (const mod of
|
|
122
|
+
for (const { project: proj, module: mod, modulesDir } of mods) {
|
|
100
123
|
const sidecarPath = join(modulesDir, `${mod}.changelog.md`)
|
|
101
124
|
let sidecar = ''
|
|
102
125
|
try { sidecar = readFileSync(sidecarPath, 'utf8') } catch { sidecar = '' }
|
|
103
|
-
if (sidecar.includes(`- ${changeName} |`)) { skipped.push(mod); continue }
|
|
126
|
+
if (sidecar.includes(`- ${changeName} |`)) { skipped.push(proj ? `${proj}/${mod}` : mod); continue }
|
|
104
127
|
const NL = String.fromCharCode(10)
|
|
105
128
|
const line = `- ${changeName} | ${note || '(见该变更 quicklog/归档)'}`
|
|
106
129
|
const base = sidecar
|
|
@@ -114,7 +137,7 @@ export function syncModuleDocSidecars({ cwd, changeName, note, specDir = null })
|
|
|
114
137
|
const stamped = card.replace(/^updated_at:.*$/m, `updated_at: ${new Date().toISOString().slice(0, 19) + '+08:00'}`)
|
|
115
138
|
if (stamped !== card) writeFileSync(cardPath, stamped)
|
|
116
139
|
} catch { /* 卡不存在——sidecar 照写,卡内容留给 agent */ }
|
|
117
|
-
synced.push(mod)
|
|
140
|
+
synced.push(proj ? `${proj}/${mod}` : mod)
|
|
118
141
|
}
|
|
119
142
|
return { synced, unmatched, skipped }
|
|
120
143
|
}
|
|
@@ -133,22 +156,22 @@ function findModulesDir(specBase) {
|
|
|
133
156
|
export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null, sourceFiles = null, origin = null }) {
|
|
134
157
|
const specBase = resolveSpecDir(cwd, { specDir })
|
|
135
158
|
|
|
136
|
-
//
|
|
137
|
-
|
|
159
|
+
// 多项目 map 联合归类(坑 multimap-first-found-mismatch,2026-09-21 R4-L 实证):docs/ 下
|
|
160
|
+
// 各项目可各带 _module-map.yaml(对撞仓 5 个),原「取首个命中」让 daemon/frontend 文件对
|
|
161
|
+
// 按目录序排前的 backend map 全失配——24 文件全未匹配、sidecar 也写错项目目录。联合口径:
|
|
162
|
+
// 逐项目 classify、目录序首个命中 wins(单项目时联合退化为一图,行为不变)。
|
|
163
|
+
const moduleMaps = []
|
|
138
164
|
try {
|
|
139
165
|
for (const p of readdirSync(join(specBase, 'docs'), { withFileTypes: true })) {
|
|
140
166
|
if (!p.isDirectory()) continue
|
|
141
167
|
const candidate = join(specBase, 'docs', p.name, 'modules', '_module-map.yaml')
|
|
142
168
|
if (existsSync(candidate)) {
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
const pm = parseModuleMapPaths(readFileSync(candidate, 'utf8'))
|
|
170
|
+
if (pm.size > 0) moduleMaps.push({ project: p.name, modulePaths: pm })
|
|
145
171
|
}
|
|
146
172
|
}
|
|
147
173
|
} catch { /* 无 docs/ */ }
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
const modulePaths = parseModuleMapPaths(readFileSync(moduleMapPath, 'utf8'))
|
|
151
|
-
if (modulePaths.size === 0) return null
|
|
174
|
+
if (moduleMaps.length === 0) return null
|
|
152
175
|
|
|
153
176
|
let sourceFileList
|
|
154
177
|
if (sourceFiles) {
|
|
@@ -169,12 +192,18 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null,
|
|
|
169
192
|
if (sourceFileList.length === 0) return null
|
|
170
193
|
|
|
171
194
|
const byModule = new Map()
|
|
195
|
+
const classified = [] // {project, module, file}——syncModuleDocSidecars 按 project 落 sidecar 用
|
|
172
196
|
const unmatched = []
|
|
173
197
|
for (const f of sourceFileList) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
198
|
+
let hit = null
|
|
199
|
+
for (const m of moduleMaps) {
|
|
200
|
+
const mod = classifyFile(f, m.modulePaths)
|
|
201
|
+
if (mod) { hit = { project: m.project, moduleId: mod }; break }
|
|
202
|
+
}
|
|
203
|
+
if (hit) {
|
|
204
|
+
if (!byModule.has(hit.moduleId)) byModule.set(hit.moduleId, [])
|
|
205
|
+
byModule.get(hit.moduleId).push(f)
|
|
206
|
+
classified.push({ project: hit.project, module: hit.moduleId, file: f })
|
|
178
207
|
} else {
|
|
179
208
|
unmatched.push(f)
|
|
180
209
|
}
|
|
@@ -234,5 +263,5 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null,
|
|
|
234
263
|
L.push('规则:execute/verify 完成文档同步后把对应行回填 done;确定不同步的行改 skipped 并在操作列写明原因。')
|
|
235
264
|
L.push('')
|
|
236
265
|
|
|
237
|
-
return { markdown: L.join('\n'), matchedCount: sourceFileList.length - unmatched.length, unmatchedCount: unmatched.length }
|
|
266
|
+
return { markdown: L.join('\n'), matchedCount: sourceFileList.length - unmatched.length, unmatchedCount: unmatched.length, classified, unmatchedFiles: unmatched }
|
|
238
267
|
}
|
package/src/module-resolve.js
CHANGED
|
@@ -78,8 +78,10 @@ export function collectModuleMaps({ cwd, specBase }) {
|
|
|
78
78
|
/**
|
|
79
79
|
* 路径归一:反斜杠→正斜杠,剥尾部 glob(/** 与 /*)与目录斜杠。
|
|
80
80
|
* `backend/**` / `backend/` / `backend` → `backend`。
|
|
81
|
+
* (导出供 flow-parity.reconcileModuleDocs 复用——2026-09-27-thin-module-scope-persist:
|
|
82
|
+
* 模块对账与模块卡解析同一归一口径,防两处漂移)
|
|
81
83
|
*/
|
|
82
|
-
function normalizeMapPath(raw) {
|
|
84
|
+
export function normalizeMapPath(raw) {
|
|
83
85
|
return String(raw).replace(/\\/g, '/').replace(/\/\*\*$/, '').replace(/\/\*$/, '').replace(/\/$/, '')
|
|
84
86
|
}
|
|
85
87
|
|