sillyspec 3.32.9 → 3.33.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-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-explore/SKILL.md +107 -107
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.github/workflows/ci.yml +58 -0
- package/.husky/pre-push +7 -6
- package/AGENTS.md +1 -1
- package/assets/command-cards/flow.md +1 -1
- package/eval/eval.config.json +3 -3
- package/eval/lib/config.mjs +6 -3
- package/eval/lib/util.mjs +16 -3
- package/eval/lib/workdir.mjs +4 -1
- package/eval/results/eval.db +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-00-05-625Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stdout.txt +1 -0
- package/eval/results/workdirs/001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/flow.md +44 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-archive.md +27 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-brainstorm.md +34 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-execute.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-plan.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-verify.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/status.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-auto/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-continue/SKILL.md +17 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-execute/SKILL.md +222 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-explore/SKILL.md +107 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-flow/SKILL.md +62 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-knowledge/SKILL.md +40 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-propose/SKILL.md +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-resume/SKILL.md +21 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-state/SKILL.md +25 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-workspace/SKILL.md +33 -0
- package/eval/results/workdirs/004-phase-stats__armB/AGENTS.md +45 -0
- package/eval/results/workdirs/004-phase-stats__armB/CLAUDE.md +3 -0
- package/eval/results/workdirs/004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/run-002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/run-003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/run-003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/bin/phase-stats.mjs +37 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-005-orders-export__armA/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armA/out/summary.json +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armB/out/summary.json +1 -0
- package/eval/tasks/004-phase-stats/files/data/events.log +16 -0
- package/eval/tasks/004-phase-stats/solution/bin/phase-stats.mjs +37 -0
- package/eval/tasks/004-phase-stats/task.json +6 -0
- package/eval/tasks/004-phase-stats/verify.mjs +38 -0
- package/eval/tasks/005-orders-export/files/data/.gitattributes +1 -0
- package/eval/tasks/005-orders-export/files/data/orders.csv +9 -0
- package/eval/tasks/005-orders-export/solution/bin/export-orders.mjs +57 -0
- package/eval/tasks/005-orders-export/task.json +6 -0
- package/eval/tasks/005-orders-export/verify.mjs +28 -0
- package/package.json +1 -1
- package/quicklog/tmp-fix-review.mjs +21 -0
- package/src/ceremony-tier.js +2 -1
- package/src/commit-suggest.js +45 -4
- package/src/config-schema.js +14 -0
- package/src/decision-distill.js +106 -24
- package/src/decision-redomain.js +148 -0
- package/src/decision-renumber.js +157 -0
- package/src/design-facts.js +3 -3
- package/src/docs-check.js +30 -15
- package/src/docs-debt.js +6 -5
- package/src/doctor-diagnostics.js +3 -1
- package/src/flow-parity.js +28 -4
- package/src/flow.js +76 -13
- package/src/forensics.js +236 -0
- package/src/fr-index.js +17 -2
- package/src/hooks/worktree-guard.js +3 -3
- package/src/index.js +41 -6
- package/src/knowledge-digest.js +23 -6
- package/src/knowledge-graph.js +95 -18
- package/src/knowledge-match.js +34 -9
- package/src/knowledge-stats.js +12 -13
- package/src/knowledge-vector.js +98 -7
- package/src/machine-interface.js +26 -0
- package/src/module-impact.js +2 -5
- package/src/modules.js +31 -0
- package/src/quicklog.js +6 -1
- package/src/run/archive-distill.js +3 -0
- package/src/run/command.js +3 -3
- package/src/run/complete-handlers.js +8 -3
- package/src/run/complete.js +113 -44
- package/src/run/gates.js +100 -103
- package/src/run/lint-ownership.js +37 -0
- package/src/run/prompt.js +43 -28
- package/src/run/stage.js +1 -1
- package/src/run/verify-quality-scan.js +10 -1
- package/src/sentinel-assertions.js +43 -1
- package/src/spec-sync.js +81 -2
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +46 -12
- package/src/stage-templates.js +2 -0
- package/src/stages/brainstorm.js +76 -21
- package/src/stages/knowledge.js +56 -5
- package/src/stages/plan-postcheck.js +13 -2
- package/src/stages/plan.js +19 -0
- package/src/sync.js +28 -2
- package/src/task-tick.js +12 -0
- package/src/test-bindings.js +32 -3
- package/src/timeline.js +60 -19
- package/src/ui-visual.js +119 -4
- package/src/verify-postcheck.js +166 -18
- package/src/verify-probes.js +61 -18
- package/src/worktree-apply.js +26 -2
- package/templates/agents-instruction.md +1 -1
package/src/timeline.js
CHANGED
|
@@ -24,6 +24,33 @@ export function parseTaskLines(tasksMd) {
|
|
|
24
24
|
return out;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** task id 归一:task-2 → task-02(与 task-tick normalizeTaskId 同口径,非 task-N 形态返 null)。 */
|
|
28
|
+
function normTaskId(s) {
|
|
29
|
+
const m = /^task-(\d+)$/.exec(String(s || '').trim());
|
|
30
|
+
return m ? `task-${String(Number(m[1])).padStart(2, '0')}` : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 侧车勾选留痕解析(2026-10-14-timeline-ticktime-anchor-scope):task-ticks.jsonl 行集
|
|
35
|
+
* ({ts,task,source})→ Array<{ts,task}>。坏行跳过;id 归一;同任务取最早(时钟回拨/重复
|
|
36
|
+
* 留痕容错——勾选时刻语义=首次翻格时刻)。文件缺失/不可读由调用方 catch 成空数组。
|
|
37
|
+
*/
|
|
38
|
+
export function parseTickLog(raw) {
|
|
39
|
+
const byTask = new Map();
|
|
40
|
+
for (const line of String(raw || '').split(/\r?\n/)) {
|
|
41
|
+
if (!line.trim()) continue;
|
|
42
|
+
try {
|
|
43
|
+
const o = JSON.parse(line);
|
|
44
|
+
const id = normTaskId(o && o.task);
|
|
45
|
+
const ts = Number(o && o.ts);
|
|
46
|
+
if (!id || !Number.isFinite(ts)) continue;
|
|
47
|
+
const p = byTask.get(id);
|
|
48
|
+
if (!p || ts < p.ts) byTask.set(id, { ts, task: id });
|
|
49
|
+
} catch { /* 坏行跳过 */ }
|
|
50
|
+
}
|
|
51
|
+
return [...byTask.values()];
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
/**
|
|
28
55
|
* 翻格顺序推断(FR-04):task-done(stage='tasks')事件序列 → 每任务序号的勾选时刻。
|
|
29
56
|
* 计数链衔接(from === 游标)才赋值;**中段**断裂(from ≠ 游标)标 broken 并停止赋值。
|
|
@@ -60,9 +87,12 @@ function tokensOf(text) {
|
|
|
60
87
|
/**
|
|
61
88
|
* 提交锚解析:事件流 kind=commit 的短 hash → gitLook 解析出主题/时间/token。
|
|
62
89
|
* @param {Function} gitLook (hash) => null | {hash, dateISO, message}(注入面,接线层包 gitQuiet)
|
|
63
|
-
* @
|
|
90
|
+
* @param {string|null} change 变更名(2026-10-14-timeline-ticktime-anchor-scope):在场时逐 anchor
|
|
91
|
+
* 算 sameChange=提交全消息(%B 含正文)includes(change)——锚定面据此收敛,杀跨变更 token 撞车
|
|
92
|
+
* (litellm-retire 实证:他变更提交消息含 task-01/04/05 被误锚);缺省恒 true 向后兼容。
|
|
93
|
+
* @returns {Array<{hash, ts, subject, tokens, resolvable, sameChange}>} ts 取事件观测时刻(git 日期仅存证)。
|
|
64
94
|
*/
|
|
65
|
-
export function resolveCommitAnchors(events, gitLook) {
|
|
95
|
+
export function resolveCommitAnchors(events, gitLook, change = null) {
|
|
66
96
|
const out = [];
|
|
67
97
|
for (const e of events || []) {
|
|
68
98
|
if (!e || e.kind !== 'commit' || !e.detail) continue;
|
|
@@ -75,6 +105,7 @@ export function resolveCommitAnchors(events, gitLook) {
|
|
|
75
105
|
subject: info ? String(info.message || '').split(/\r?\n/)[0] : null,
|
|
76
106
|
tokens: info ? tokensOf(info.message) : [],
|
|
77
107
|
resolvable: Boolean(info),
|
|
108
|
+
sameChange: !change || (info ? String(info.message || '').includes(change) : false),
|
|
78
109
|
});
|
|
79
110
|
}
|
|
80
111
|
return out;
|
|
@@ -125,11 +156,13 @@ function fmtDur(ms) {
|
|
|
125
156
|
* @param {string} args.change 变更名
|
|
126
157
|
* @param {Array} args.events 事件流(readWatcherEvents.events)
|
|
127
158
|
* @param {Array|null} args.tasks parseTaskLines 产物(null=任务面缺失)
|
|
128
|
-
* @param {Array} args.anchors resolveCommitAnchors
|
|
159
|
+
* @param {Array} args.anchors resolveCommitAnchors 产物(锚定只取 sameChange!==false 的行)
|
|
129
160
|
* @param {number|null} args.birthTs 诞生锚(工件 frontmatter created_at 毫秒;null=缺)
|
|
130
161
|
* @param {string|null} args.tier flow-state tier(null=缺/未知)
|
|
162
|
+
* @param {Array|null} args.tickLog parseTickLog 产物(2026-10-14-timeline-ticktime-anchor-scope:
|
|
163
|
+
* 侧车勾选留痕——任务行时刻三级降级:侧车精确(无 ≈)> 事件流推断(≈)> ?)
|
|
131
164
|
*/
|
|
132
|
-
export function renderTimeline({ change, events, tasks, anchors, birthTs, tier }) {
|
|
165
|
+
export function renderTimeline({ change, events, tasks, anchors, birthTs, tier, tickLog = null }) {
|
|
133
166
|
const L = [];
|
|
134
167
|
L.push(`📅 ${change} — 合成时间线(${tier || 'tier 未知'}|事件流 × tasks.md × git 提交锚)`);
|
|
135
168
|
L.push('');
|
|
@@ -140,16 +173,22 @@ export function renderTimeline({ change, events, tasks, anchors, birthTs, tier }
|
|
|
140
173
|
|
|
141
174
|
if (tasks) {
|
|
142
175
|
L.push('');
|
|
143
|
-
L.push('──
|
|
176
|
+
L.push('── 任务面(勾选时刻:精确=task tick 侧车留痕|≈=事件流顺序推断|tasks.md 描述行 × 提交锚)──');
|
|
144
177
|
const { times, broken } = inferFlipTimes(events, tasks.length);
|
|
145
|
-
|
|
146
|
-
|
|
178
|
+
const tickMap = new Map((tickLog || []).map((r) => [String(r && r.task), Number(r && r.ts)]));
|
|
179
|
+
// 断裂注按需打(2026-10-14-timeline-ticktime-anchor-scope):侧车已覆盖全部已勾位时推断层
|
|
180
|
+
// 与展示无关——watcher 重复采样(0→1 双拍)也会撞断游标,此时打断裂注是误导噪音。
|
|
181
|
+
const inferenceMatters = tasks.some((t) => t.checked && !tickMap.has(t.id));
|
|
182
|
+
if (broken && inferenceMatters) L.push('(计数链中段断裂——断裂点后勾选时刻推断不可用,标 ?)');
|
|
183
|
+
if (tasks.some((t, i) => t.checked && times[i] == null && !tickMap.has(t.id))) L.push('(已勾任务缺推断时刻——观测盲窗/水位丢失,标 ?)');
|
|
147
184
|
for (let i = 0; i < tasks.length; i++) {
|
|
148
185
|
const t = tasks[i];
|
|
149
|
-
const when = t.checked
|
|
150
|
-
?
|
|
151
|
-
:
|
|
152
|
-
|
|
186
|
+
const when = !t.checked
|
|
187
|
+
? '未勾'
|
|
188
|
+
: tickMap.has(t.id) ? fmtHMS(tickMap.get(t.id))
|
|
189
|
+
: times[i] != null ? `≈${fmtHMS(times[i])}`
|
|
190
|
+
: '?';
|
|
191
|
+
const anchor = anchors.filter((a) => a && a.sameChange !== false && a.tokens.includes(t.id)).sort((a, b) => a.ts - b.ts)[0];
|
|
153
192
|
const ev = anchor ? anchor.hash : (t.checked ? '无提交锚⚠️' : '—');
|
|
154
193
|
L.push(`${t.id} ${when.padEnd(10)} ${clip(t.desc, 44).padEnd(46)}${ev}`);
|
|
155
194
|
}
|
|
@@ -169,7 +208,7 @@ export function renderTimeline({ change, events, tasks, anchors, birthTs, tier }
|
|
|
169
208
|
if (stages.length > 0) {
|
|
170
209
|
L.push(`阶段墙钟:${stages.map((s) => `${s.stage} ${fmtDur(s.durationMs)}`).join('|')}`);
|
|
171
210
|
}
|
|
172
|
-
L.push('注:观测起点≠诞生时刻(watcher
|
|
211
|
+
L.push('注:观测起点≠诞生时刻(watcher 后拉起/单飞锁盲窗);勾选时刻精确值=task tick 侧车留痕(task-ticks.jsonl),≈=事件流顺序推断(事件不记任务 id);描述行含机器稿截断。');
|
|
173
212
|
return L.join('\n');
|
|
174
213
|
}
|
|
175
214
|
|
|
@@ -179,27 +218,29 @@ export function renderTimeline({ change, events, tasks, anchors, birthTs, tier }
|
|
|
179
218
|
* 烤制落盘——诚实注记而非数据错误。eventsCopySkipped=副本未随包形态(尺寸超帽或读源失败,
|
|
180
219
|
* 评审 P3-1:注记必须与实际产出一致,不虚报副本在场)。
|
|
181
220
|
*/
|
|
182
|
-
export function renderBakedTimeline({ change, events, tasks, anchors, birthTs, tier, bakedAtIso, eventsCopySkipped = false }) {
|
|
221
|
+
export function renderBakedTimeline({ change, events, tasks, anchors, birthTs, tier, bakedAtIso, eventsCopySkipped = false, tickLog = null }) {
|
|
183
222
|
const head = [
|
|
184
223
|
`# 合成时间线快照 — ${change}`,
|
|
185
224
|
'',
|
|
186
225
|
`> 烤制于归档链(${bakedAtIso}):事件流机本位(.runtime gitignore),本快照随归档包进 git 跨机可读。`,
|
|
187
|
-
'> 末尾事件(含「目录已移入 archive
|
|
226
|
+
'> 末尾事件(含「目录已移入 archive」终拍)可能晚于烤制未入快照;勾选时刻:精确=task tick 侧车留痕,≈=事件流顺序推断。',
|
|
188
227
|
eventsCopySkipped
|
|
189
228
|
? '> 原始事件副本未随包(尺寸超帽或读源失败)——细粒度复核回本机 .runtime 或平台 observation 事件面。'
|
|
190
229
|
: '> 原始事件副本:本目录 watcher-events.jsonl(时点快照)。',
|
|
191
230
|
'',
|
|
192
231
|
]
|
|
193
|
-
return head.join('\n') + renderTimeline({ change, events, tasks, anchors, birthTs, tier })
|
|
232
|
+
return head.join('\n') + renderTimeline({ change, events, tasks, anchors, birthTs, tier, tickLog })
|
|
194
233
|
}
|
|
195
234
|
|
|
196
|
-
/** change 目录双路径探测(活跃 > 归档)+ tasks.md 读取 → {changeDir, tasksMd}(缺 tasksMd=null
|
|
235
|
+
/** change 目录双路径探测(活跃 > 归档)+ tasks.md / task-ticks.jsonl 读取 → {changeDir, tasksMd, tickLog}(缺 tasksMd=null、tickLog=空数组)。 */
|
|
197
236
|
export function loadChangeTasks(specBase, change) {
|
|
198
237
|
const active = join(specBase, 'changes', change);
|
|
199
238
|
const archived = join(specBase, 'changes', 'archive', change);
|
|
200
239
|
const dir = [active, archived].find((d) => { try { return readFileSync(join(d, 'tasks.md'), 'utf8') != null } catch { return false } });
|
|
201
|
-
if (!dir) return { changeDir: null, tasksMd: null };
|
|
202
|
-
|
|
240
|
+
if (!dir) return { changeDir: null, tasksMd: null, tickLog: [] };
|
|
241
|
+
let tickLog = [];
|
|
242
|
+
try { tickLog = parseTickLog(readFileSync(join(dir, 'task-ticks.jsonl'), 'utf8')); } catch { /* 侧车缺失/不可读按空 */ }
|
|
243
|
+
return { changeDir: dir, tasksMd: readFileSync(join(dir, 'tasks.md'), 'utf8'), tickLog };
|
|
203
244
|
}
|
|
204
245
|
|
|
205
246
|
/** 诞生锚:change 目录工件 frontmatter created_at(requirements > proposal > design > tasks)。 */
|
|
@@ -215,4 +256,4 @@ export function readBirthTs(changeDir) {
|
|
|
215
256
|
return null;
|
|
216
257
|
}
|
|
217
258
|
|
|
218
|
-
export default { parseTaskLines, inferFlipTimes, resolveCommitAnchors, renderTimeline, renderBakedTimeline, loadChangeTasks, readBirthTs };
|
|
259
|
+
export default { parseTaskLines, parseTickLog, inferFlipTimes, resolveCommitAnchors, renderTimeline, renderBakedTimeline, loadChangeTasks, readBirthTs };
|
package/src/ui-visual.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, readFileSync } from 'node:fs'
|
|
19
19
|
import { join } from 'node:path'
|
|
20
|
+
import jsYaml from 'js-yaml'
|
|
21
|
+
import { parseRepoRegistry } from './stages/plan-postcheck.js'
|
|
20
22
|
|
|
21
23
|
export const UI_VISUAL_PROBE_HEADING = '#### 探针 12:UI 视觉证据(分级门)'
|
|
22
24
|
export const UI_EVIDENCE_FILENAME = 'visual-evidence.md'
|
|
@@ -48,8 +50,114 @@ export function detectUiTouchInPaths(paths) {
|
|
|
48
50
|
|
|
49
51
|
/* ── flow start 须知(仓中立;证据约定与探针同源)── */
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
/**
|
|
54
|
+
* local.yaml ui_prototype 段解析(2026-10-15-ui-proto-form-config,FR-01)。
|
|
55
|
+
* 每仓原型形态契约:form: real-code | single-html;豁免(single-html)须带 ruling{by,at,reason}
|
|
56
|
+
* 全三字段(用户裁决留痕——缺任一=登记不完整,消费方须警告);repos.<子仓键> 同构 override。
|
|
57
|
+
* fail-soft:段缺席/文件不可读/YAML 坏/值非法 → registered:false 零抛出(与 readUiVisualGate
|
|
58
|
+
* 同根同哲学)。诚实边界:工具只消费登记与注入指引,不校验原型质量。
|
|
59
|
+
*/
|
|
60
|
+
export function readUiPrototypeForm(specBase) {
|
|
61
|
+
const empty = { registered: false, form: null, ruling: null, rulingComplete: false, repos: {} }
|
|
62
|
+
if (!specBase) return empty
|
|
63
|
+
let raw
|
|
64
|
+
try {
|
|
65
|
+
raw = jsYaml.load(readFileSync(join(specBase, 'local.yaml'), 'utf8'))
|
|
66
|
+
} catch { return empty }
|
|
67
|
+
const section = raw && typeof raw === 'object' ? raw.ui_prototype : null
|
|
68
|
+
if (!section || typeof section !== 'object') return empty
|
|
69
|
+
const parseEntry = (e) => {
|
|
70
|
+
if (!e || typeof e !== 'object') return null
|
|
71
|
+
const form = e.form === 'real-code' || e.form === 'single-html' ? e.form : null
|
|
72
|
+
if (!form) return null
|
|
73
|
+
const r = e.ruling && typeof e.ruling === 'object' ? e.ruling : null
|
|
74
|
+
const rulingComplete = !!(r && r.by && r.at && r.reason)
|
|
75
|
+
return { form, ruling: r, rulingComplete }
|
|
76
|
+
}
|
|
77
|
+
const top = parseEntry(section)
|
|
78
|
+
const repos = {}
|
|
79
|
+
if (section.repos && typeof section.repos === 'object') {
|
|
80
|
+
for (const [key, val] of Object.entries(section.repos)) {
|
|
81
|
+
const entry = parseEntry(val)
|
|
82
|
+
if (entry) repos[key] = entry
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!top) return { registered: false, form: null, ruling: null, rulingComplete: false, repos }
|
|
86
|
+
return { registered: true, ...top, repos }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 多仓 override 解析(FR-02):触达路径与 local.yaml repos 注册匹配——子仓若在
|
|
91
|
+
* ui_prototype.repos 有 override 则该形态优先;未命中走仓级兜底。返回形态条目数组(去重):
|
|
92
|
+
* 每项 { scope: '仓级'|'<子仓键>', form, ruling, rulingComplete, registered }。
|
|
93
|
+
* 匹配三通道(评审 P2 清偿:注入语料常为相对路径,纯绝对前缀会让 override 静默失效):
|
|
94
|
+
* ① 绝对前缀:归一(反斜杠→斜杠/去尾斜杠/大小写不敏感)后 startsWith 注册路径;
|
|
95
|
+
* ② 相对键前缀:路径以 `<repoKey>/` 开头(语料常见 `spdemo/src/...` 形态);
|
|
96
|
+
* ③ 相对基名前缀:路径以注册路径 basename + `/` 开头(键与目录名不同名时兜底)。
|
|
97
|
+
*/
|
|
98
|
+
export function resolveUiPrototypeFor(specBase, paths = []) {
|
|
99
|
+
const proto = readUiPrototypeForm(specBase)
|
|
100
|
+
const out = []
|
|
101
|
+
const seen = new Set()
|
|
102
|
+
const push = (entry) => {
|
|
103
|
+
const key = `${entry.form}|${entry.rulingComplete}`
|
|
104
|
+
if (seen.has(key)) return
|
|
105
|
+
seen.add(key)
|
|
106
|
+
out.push(entry)
|
|
107
|
+
}
|
|
108
|
+
if (paths.length > 0) {
|
|
109
|
+
let repoPaths = {}
|
|
110
|
+
try {
|
|
111
|
+
// 复用既有注册解析(plan-postcheck _parseRepoEntries 同语义出口:字符串条目=路径、
|
|
112
|
+
// 对象条目取 path 子键)——不走 js-yaml 裸读,防对象形态静默漏配。
|
|
113
|
+
repoPaths = Object.fromEntries(parseRepoRegistry(readFileSync(join(specBase, 'local.yaml'), 'utf8')))
|
|
114
|
+
} catch { /* 解析失败=零子仓命中,仓级兜底 */ }
|
|
115
|
+
const norm = (p) => String(p).replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
|
|
116
|
+
for (const [key, registeredPath] of Object.entries(repoPaths)) {
|
|
117
|
+
if (typeof registeredPath !== 'string' || !registeredPath) continue
|
|
118
|
+
const reg = norm(registeredPath)
|
|
119
|
+
const regBase = reg.split('/').pop()
|
|
120
|
+
const hit = paths.some((p) => {
|
|
121
|
+
const np = norm(p)
|
|
122
|
+
return np.startsWith(reg + '/') || np === reg
|
|
123
|
+
|| np.startsWith(key.toLowerCase() + '/')
|
|
124
|
+
|| np.startsWith(regBase + '/')
|
|
125
|
+
})
|
|
126
|
+
const override = proto.repos[key]
|
|
127
|
+
if (hit && override) push({ scope: key, registered: true, ...override })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
push(proto.registered
|
|
131
|
+
? { scope: '仓级', registered: true, form: proto.form, ruling: proto.ruling, rulingComplete: proto.rulingComplete }
|
|
132
|
+
: { scope: '仓级', registered: false, form: null, ruling: null, rulingComplete: false })
|
|
133
|
+
return out
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 须知第 5 节:按形态条目组合「本仓原型形态」行(FR-03 四态;生态命令词禁入——neutrality 契约)。 */
|
|
137
|
+
function uiPrototypeFormLines(entries) {
|
|
138
|
+
const lines = [`📐 本仓原型形态(local.yaml ui_prototype 登记;工具只查声明在场不验质量):`]
|
|
139
|
+
let anyContent = false
|
|
140
|
+
for (const e of entries) {
|
|
141
|
+
const scope = e.scope === '仓级' ? '' : `【子仓 ${e.scope}】`
|
|
142
|
+
if (!e.registered) {
|
|
143
|
+
anyContent = true
|
|
144
|
+
lines.push(` ⚠️ ${scope}未登记原型形态——本次按阶梯执行:有真实前端工具链→定稿原型必须真码;查无真码管线→先单开一个轻量变更搭最小入口(管线建设与业务变更分离),或登记豁免(form: single-html + ruling 用户裁决留痕)。收口前把 local.yaml ui_prototype 登记补上(强提醒,不阻断)。`)
|
|
145
|
+
} else if (e.form === 'single-html' && !e.rulingComplete) {
|
|
146
|
+
anyContent = true
|
|
147
|
+
lines.push(` ⚠️ ${scope}豁免登记不完整(single-html 缺 ruling{by,at,reason} 某字段)——这是静默漂移入口:补齐用户裁决留痕(by/at/reason)后再依赖豁免;本次仍按阶梯执行。`)
|
|
148
|
+
} else if (e.form === 'single-html') {
|
|
149
|
+
anyContent = true
|
|
150
|
+
lines.push(` 📋 ${scope}豁免生效(single-html;裁决 by=${e.ruling.by} at=${e.ruling.at}):定稿原型=自包含 HTML + 真值 token(来源须声明:token 源文件/组件参照),反向翻译税为已裁决接受项。`)
|
|
151
|
+
} else {
|
|
152
|
+
anyContent = true
|
|
153
|
+
lines.push(` 🔒 ${scope}真码契约(real-code):定稿原型必须与实现同方言真码;查无管线先单开轻量变更搭最小入口(bootstrap 与业务变更分离)。`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return anyContent ? lines : []
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function buildUiGuidanceLines(specBase = null, paths = []) {
|
|
160
|
+
const lines = [
|
|
53
161
|
`🎨 UI 变更执行须知(本变更触及前端页面或 UI——advisory 引导非门禁):`,
|
|
54
162
|
` 1. 定稿原型必须是可复跑的真码产物(与实现同方言、能被本项目工具链构建)——手绘单文件`,
|
|
55
163
|
` 仅限一次性粗选对比,不作为交付原型(2026-09-28 frontend-apple-style 实证:手绘定稿后每条意见都要反向翻译回真实代码);`,
|
|
@@ -62,6 +170,13 @@ export function buildUiGuidanceLines() {
|
|
|
62
170
|
` 必须停下找用户签字,裁决记录写进 ${UI_EVIDENCE_FILENAME}(含「用户裁决」字样段)——`,
|
|
63
171
|
` 静默降级会在收口被「UI 视觉证据」探针硬拦(error 门)。`,
|
|
64
172
|
]
|
|
173
|
+
if (specBase) {
|
|
174
|
+
try {
|
|
175
|
+
const entries = resolveUiPrototypeFor(specBase, paths)
|
|
176
|
+
lines.push('', ...uiPrototypeFormLines(entries))
|
|
177
|
+
} catch { /* 登记解析失败零打扰(fail-soft 同 readUiVisualGate) */ }
|
|
178
|
+
}
|
|
179
|
+
return lines
|
|
65
180
|
}
|
|
66
181
|
|
|
67
182
|
/* ── 分级探针 ── */
|
|
@@ -173,8 +288,8 @@ export function uiEvidenceExecuteAdvisory({ changeDir, specBase = null }) {
|
|
|
173
288
|
}
|
|
174
289
|
}
|
|
175
290
|
|
|
176
|
-
/** design 文件变更清单行的路径粗提(反引号/裸路径行——探针侧仅作 UI
|
|
177
|
-
function extractQuotedPaths(designText) {
|
|
291
|
+
/** design 文件变更清单行的路径粗提(反引号/裸路径行——探针侧仅作 UI 扩展名兜底命中,不追求全解;导出供 prompt.js 组子仓 override 匹配路径用)。 */
|
|
292
|
+
export function extractQuotedPaths(designText) {
|
|
178
293
|
if (!designText) return []
|
|
179
294
|
const paths = []
|
|
180
295
|
for (const m of designText.matchAll(/`([^`]+)`/g)) {
|
package/src/verify-postcheck.js
CHANGED
|
@@ -2821,6 +2821,31 @@ function inferFullTestCommand(projectRoot) {
|
|
|
2821
2821
|
* 相关回归)。组卷改配额制:每组优先面整跑,普通 import 依赖只填剩余席位(可为 0)——帽
|
|
2822
2822
|
* 只界普通依赖尾。批尺寸上界护栏由 TEST_TIMEOUT_MS 兜底(超时=失败 fail-closed 不变)。
|
|
2823
2823
|
*/
|
|
2824
|
+
/**
|
|
2825
|
+
* 批命令长度分块(2026-10-15 平台侧实证,跨仓事故驱动最小修复):Windows cmd 执行器
|
|
2826
|
+
* 命令行上限 ~8191 字符——大依赖面(广被引用的前端文件变更 → 依赖测试 100+ 个)拼单条
|
|
2827
|
+
* vitest/node --test 命令必炸「命令行太长」exit 1,且失败批进增量账本自我循环恒败
|
|
2828
|
+
* (实测:137 文件批反复重跑复败,verify 门禁无法收口)。按「命令前缀+文件参数拼接
|
|
2829
|
+
* 总长」分块:单块保持原名(零回归),多块命名 <base>:N(N 从 1 起);每块独立计账。
|
|
2830
|
+
*/
|
|
2831
|
+
function chunkByCommandLength(prefix, files, maxLen = 6000) {
|
|
2832
|
+
const chunks = []
|
|
2833
|
+
let cur = []
|
|
2834
|
+
let curLen = prefix.length
|
|
2835
|
+
for (const f of files) {
|
|
2836
|
+
const add = f.length + 1
|
|
2837
|
+
if (cur.length > 0 && curLen + add > maxLen) {
|
|
2838
|
+
chunks.push(cur)
|
|
2839
|
+
cur = []
|
|
2840
|
+
curLen = prefix.length
|
|
2841
|
+
}
|
|
2842
|
+
cur.push(f)
|
|
2843
|
+
curLen += add
|
|
2844
|
+
}
|
|
2845
|
+
if (cur.length > 0) chunks.push(cur)
|
|
2846
|
+
return chunks
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2824
2849
|
function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, priorityFiles = [] }) {
|
|
2825
2850
|
const norm = (p2) => String(p2).replace(/\\/g, '/')
|
|
2826
2851
|
const changedSet = new Set((changedFiles || []).map(norm))
|
|
@@ -2930,10 +2955,11 @@ function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, prio
|
|
|
2930
2955
|
const jsProjectRun = jsProject.filter(f => !E2E_DIR_RE.test(_norm(f)))
|
|
2931
2956
|
const e2eDroppedFiles = jsProject.filter(f => E2E_DIR_RE.test(_norm(f)))
|
|
2932
2957
|
let jsxRunner = null
|
|
2958
|
+
let jsxRunnerFromHits = false
|
|
2933
2959
|
for (const h of hits || []) {
|
|
2934
2960
|
const cmd = String(h.test || '')
|
|
2935
2961
|
const m = /^((?:cd\s+[^&|;]+&&\s*)*[^&|;]*?(?:vitest|jest))(?=\s|$)/.exec(cmd) || /(?:^|&&|;|\|\|)\s*([^&|;]*(?:vitest|jest)(?:\s|$))/.exec(cmd)
|
|
2936
|
-
if (m) { jsxRunner = m[1].trim(); break }
|
|
2962
|
+
if (m) { jsxRunner = m[1].trim(); jsxRunnerFromHits = true; break }
|
|
2937
2963
|
}
|
|
2938
2964
|
if (!jsxRunner && cwd && jsProjectRun.length > 0) {
|
|
2939
2965
|
const inferred = inferJsxRunner(cwd, jsProjectRun[0])
|
|
@@ -2958,24 +2984,59 @@ function buildDepsBatches({ deps, changedFiles = [], hits = [], cwd = null, prio
|
|
|
2958
2984
|
gi++
|
|
2959
2985
|
}
|
|
2960
2986
|
}
|
|
2961
|
-
if (jsNative.length > 0)
|
|
2987
|
+
if (jsNative.length > 0) {
|
|
2988
|
+
const jsPrefix = 'node --test --test-reporter=spec --test-reporter-destination=stderr --test-reporter=tap --test-reporter-destination=stdout '
|
|
2989
|
+
const jsChunks = chunkByCommandLength(jsPrefix, jsNative)
|
|
2990
|
+
jsChunks.forEach((chunk, i) => {
|
|
2991
|
+
const isMulti = jsChunks.length > 1
|
|
2992
|
+
batches.push({ name: isMulti ? `deps(auto-js:${i + 1}/${jsChunks.length})` : 'deps(auto-js)', short: 'js', command: jsPrefix + chunk.join(' '), count: chunk.length, prioCount: chunk.filter(f => prio(f) === 0).length, dropped: i === 0 ? (jsTest.length - jsRun.length) : 0, prioDropped: i === 0 ? jsQ.prioDropped : 0, tap: true, files: chunk })
|
|
2993
|
+
})
|
|
2994
|
+
}
|
|
2962
2995
|
if (jsMeta.length > 0) batches.push({ name: 'deps(auto-js-meta-skip)', short: 'js-meta-skip', command: null, count: jsMeta.length, dropped: 0, skip: true, files: jsMeta, reason: '套件编排器(run-tests.mjs=npm test 组卷入口)非单测文件——内嵌执行=递归全量套件(嵌套 runner 污染假败);全量门由收口实测/CI 的 npm test 承担' })
|
|
2963
2996
|
|
|
2964
2997
|
if (nonTestFiles.length > 0) batches.push({ name: 'deps(auto-nontest-skip)', short: 'nontest-skip', command: null, count: nonTestFiles.length, dropped: 0, skip: true, files: nonTestFiles, reason: `非测试形态文件 ${nonTestFiles.length} 个不进单测执行批(node --test 语义「文件即测试」/ vitest No test suite found / pytest 0 collected exit 5——直跑必假败;坑 verify-dynamic-subset-node-test-ts-source):${nonTestFiles.slice(0, 3).join('、')}${nonTestFiles.length > 3 ? ' 等' : ''}——FR 绑定面脏数据可 sillyspec tests --unbind --tests <路径> 清行` })
|
|
2965
2998
|
if (jsProjectRun.length > 0) {
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2999
|
+
// R20(2026-09-27-thin-display-fix 实证,用户授权修复):Windows cmd 执行器对含
|
|
3000
|
+
// 括号/方括号/空格的路径(src/app/(dashboard)/workspaces/[id]/...)不加引号会以
|
|
3001
|
+
// 「此时不应有 <。」语法错误炸整批——文件参数含特殊字符时逐个 wrap 双引号。
|
|
3002
|
+
const quoteWinPath = (f) => (/[()\[\]\s&^%,;!]/.test(f) ? `"${f}"` : f)
|
|
3003
|
+
// jsx 批按包根分组(2026-10-15-zcode-engine-appserver verify 实证,跨仓事故驱动修复):
|
|
3004
|
+
// 混合包根(frontend/ 的 tsx 测试 + sillyhub-daemon/ 的 ts 测试同批)共享单一 runner
|
|
3005
|
+
// 前缀时,非本根路径在 runner 的 cwd 下「No test files found」exit 1 恒败(分块修复
|
|
3006
|
+
// 后暴露:第二块全 daemon 路径仍跑 cd frontend)。与 pyGroups 同款:每文件按最近
|
|
3007
|
+
// package.json 推断自身 runner/根;显式命中命令(hits)保持整批单组旧语义。
|
|
3008
|
+
const jsxGroups = new Map() // key: 组命令前缀
|
|
3009
|
+
const jsxUngrouped = []
|
|
3010
|
+
for (const f of jsProjectRun) {
|
|
3011
|
+
// 命中命令(hits)→ 整批单组旧语义;全局推断(jsProjectRun[0])仅兜底,逐文件按自身包根推断优先。
|
|
3012
|
+
let command = null
|
|
3013
|
+
let dirKey = null
|
|
3014
|
+
if (!jsxRunnerFromHits && cwd) {
|
|
3015
|
+
const inferred = inferJsxRunner(cwd, f)
|
|
3016
|
+
if (inferred) { command = inferred.command; dirKey = inferred.dir || null }
|
|
3017
|
+
}
|
|
3018
|
+
if (!command) command = jsxRunner
|
|
3019
|
+
if (!command) { jsxUngrouped.push(f); continue }
|
|
3020
|
+
if (!jsxGroups.has(command)) jsxGroups.set(command, { files: [], dirKey })
|
|
3021
|
+
jsxGroups.get(command).files.push(f)
|
|
3022
|
+
}
|
|
3023
|
+
const multiGroup = jsxGroups.size > 1
|
|
3024
|
+
for (const [command, grp] of jsxGroups) {
|
|
3025
|
+
const isVitest = /vitest/.test(command)
|
|
3026
|
+
const jsxPrefix = `${command} ${isVitest ? 'run ' : ''}`
|
|
3027
|
+
const grpDir = grp.dirKey || /(?:^|\s)cd\s+(\S+)\s*&&/.exec(command)?.[1] || null
|
|
3028
|
+
const rebaseJsx = (f) => (grpDir && f.startsWith(grpDir + '/')) ? f.slice(grpDir.length + 1) : f
|
|
3029
|
+
const jsxArgs = grp.files.map(rebaseJsx).map(quoteWinPath)
|
|
3030
|
+
const jsxChunks = chunkByCommandLength(jsxPrefix, jsxArgs)
|
|
3031
|
+
const baseName = multiGroup ? `deps(auto-jsx:${grp.dirKey || 'root'})` : 'deps(auto-jsx)'
|
|
3032
|
+
jsxChunks.forEach((chunk, i) => {
|
|
3033
|
+
const isMulti = jsxChunks.length > 1
|
|
3034
|
+
batches.push({ name: isMulti ? `${baseName}:${i + 1}/${jsxChunks.length}` : baseName, short: 'jsx', command: jsxPrefix + chunk.join(' '), count: chunk.length, dropped: 0, files: chunk })
|
|
3035
|
+
})
|
|
3036
|
+
}
|
|
3037
|
+
if (jsxUngrouped.length > 0) {
|
|
3038
|
+
// 无运行器可推断的文件(含 !jsxRunner && !cwd 全量形态):skip 批如实披露(原整批 skip 语义按文件收窄)。
|
|
3039
|
+
batches.push({ name: multiGroup || jsxGroups.size > 0 ? 'deps(auto-jsx-skip:unresolved)' : 'deps(auto-jsx-skip)', short: 'jsx-skip', command: null, count: jsxUngrouped.length, dropped: 0, skip: true, files: jsxUngrouped, reason: `JSX 测试文件(tsx/jsx,${jsxUngrouped.length} 个)node 原生不可跑且各自最近 package.json 无 vitest/jest 运行器可推断——转项目运行器执行并如实披露,不制造恒败段(R18-SF-full 残差段 11 连败的坑)` })
|
|
2979
3040
|
}
|
|
2980
3041
|
}
|
|
2981
3042
|
if (e2eDroppedFiles.length > 0) {
|
|
@@ -3042,6 +3103,26 @@ function traceRepoRoots({ specBase, cwd }) {
|
|
|
3042
3103
|
}
|
|
3043
3104
|
|
|
3044
3105
|
/** 残差解析:active·非 superseded·锚命中的 trace 行 → { anchors, rows, files, dangling, perFile } */
|
|
3106
|
+
/**
|
|
3107
|
+
* 主仓侧候选解析根(worktree 感知;2026-10-15-zcode-engine-appserver verify 实证,跨仓
|
|
3108
|
+
* 事故驱动最小修复):变更在隔离 worktree 内实现时,新增测试文件在主仓根不可见——
|
|
3109
|
+
* 悬空判定对主仓文件按「主仓根 ∪ 变更 worktree 根」多根解析(任一存在即非悬空);
|
|
3110
|
+
* 残差执行根同款(文件在 worktree 存在则从 worktree 根跑)。meta 缺失/不可读 → 仅主仓。
|
|
3111
|
+
*/
|
|
3112
|
+
function traceMainRoots({ specBase, changeName, cwd }) {
|
|
3113
|
+
const roots = [cwd]
|
|
3114
|
+
try {
|
|
3115
|
+
const metaPath = join(specBase, '.runtime', 'worktrees', changeName, 'meta.json')
|
|
3116
|
+
if (existsSync(metaPath)) {
|
|
3117
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
|
|
3118
|
+
if (meta && typeof meta.worktreePath === 'string' && meta.worktreePath && existsSync(meta.worktreePath) && meta.worktreePath !== cwd) {
|
|
3119
|
+
roots.push(meta.worktreePath)
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
} catch { /* meta 不可读 → 仅主仓(零回归) */ }
|
|
3123
|
+
return roots
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3045
3126
|
export function resolveTraceResidual({ specBase, changeName, cwd }) {
|
|
3046
3127
|
const anchors = resolveVerifyAnchorSet({ specBase, changeName })
|
|
3047
3128
|
if (anchors.length === 0) return { anchors, rows: [], files: [], dangling: [], perFile: [] }
|
|
@@ -3050,6 +3131,7 @@ export function resolveTraceResidual({ specBase, changeName, cwd }) {
|
|
|
3050
3131
|
// tests 条目可携带用例锚(2026-09-26-binding-anchor-fidelity)——文件面/悬空判定走剥锚。
|
|
3051
3132
|
// FR-06(2026-10-09):文件按行 repo 归属配对(perFile),悬空判定/残差执行按行根解析。
|
|
3052
3133
|
const repoRoots = traceRepoRoots({ specBase, cwd })
|
|
3134
|
+
const mainRoots = traceMainRoots({ specBase, changeName, cwd })
|
|
3053
3135
|
const seen = new Set()
|
|
3054
3136
|
const perFile = []
|
|
3055
3137
|
for (const r of rows) {
|
|
@@ -3061,8 +3143,13 @@ export function resolveTraceResidual({ specBase, changeName, cwd }) {
|
|
|
3061
3143
|
}
|
|
3062
3144
|
}
|
|
3063
3145
|
const files = [...new Set(perFile.map(x => x.file))].sort()
|
|
3146
|
+
// 主仓文件多根解析:解析到 worktree 根则记录 resolvedRoot(残差执行同款消费)。
|
|
3147
|
+
for (const x of perFile) {
|
|
3148
|
+
if (x.repo) { x.resolvedRoot = repoRoots.get(x.repo) || cwd; continue }
|
|
3149
|
+
x.resolvedRoot = mainRoots.find(root => existsSync(resolve(root, x.file))) || cwd
|
|
3150
|
+
}
|
|
3064
3151
|
const dangling = perFile
|
|
3065
|
-
.filter(x => !existsSync(resolve(
|
|
3152
|
+
.filter(x => !existsSync(resolve(x.resolvedRoot, x.file)))
|
|
3066
3153
|
.map(x => x.repo ? `${x.repo}:${x.file}` : x.file)
|
|
3067
3154
|
return { anchors, rows, files, dangling, perFile }
|
|
3068
3155
|
}
|
|
@@ -3141,7 +3228,8 @@ export function applyTraceResidual({ mainResult, action, cwd, specBase, changeNa
|
|
|
3141
3228
|
for (const x of (tr.perFile || [])) {
|
|
3142
3229
|
if (provable.has(String(x.file).replace(/\\/g, '/'))) continue
|
|
3143
3230
|
const _rk = x.repo || null
|
|
3144
|
-
|
|
3231
|
+
// worktree 感知(同悬空判定口径):主仓行按 resolvedRoot(主仓或变更 worktree 根)取执行根。
|
|
3232
|
+
const _root = (_rk && _repoRoots.get(_rk)) || x.resolvedRoot || cwd
|
|
3145
3233
|
const _gk = _rk + '' + _root
|
|
3146
3234
|
if (!_gmap.has(_gk)) _gmap.set(_gk, { repo: _rk, root: _root, files: [] })
|
|
3147
3235
|
_gmap.get(_gk).files.push(x.file)
|
|
@@ -3284,8 +3372,20 @@ export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures
|
|
|
3284
3372
|
// not ok 以测试文件为套件名(not ok N - test/b.test.mjs),用判账行集匹配批内文件名把
|
|
3285
3373
|
// 「批失败」收窄到「文件失败」;归因不出(非 TAP/行集缺文件名)保守全记批文件。
|
|
3286
3374
|
failedFiles: (() => {
|
|
3375
|
+
// 基建失败甄别(坑 verify-incremental-rerun-infra-batch-growth,2026-10-15 两变更实证:
|
|
3376
|
+
// Windows argv 超限/runner 启动失败类批失败无任何测试级失败行,重跑不可能转绿——计入
|
|
3377
|
+
// 前轮失败批只会让增量面随轮膨胀(141→159 自锁)。判据:输出无 not ok/FAILED/ERROR/
|
|
3378
|
+
// AssertionError 任何测试级痕迹,且 reason 命中基建关键词(命令行太长/E2BIG/spawn/
|
|
3379
|
+
// Cannot find module/No test files found)——此类批不进失败批账本,loud 留痕供人工。
|
|
3380
|
+
const INFRA_REASON_RE = /命令行太长|E2BIG|spawn|Cannot find module|No test files found|ENAMETOOLONG/i
|
|
3381
|
+
const TEST_FAILURE_RE = /not ok \d|FAILED|ERROR|AssertionError|assert/i
|
|
3287
3382
|
const out = new Set()
|
|
3288
3383
|
for (const r of perModule.filter(x => x.status === 'failed')) {
|
|
3384
|
+
const text = String(r.outputFull || r.outputTail || '')
|
|
3385
|
+
if (!TEST_FAILURE_RE.test(text) && INFRA_REASON_RE.test(String(r.reason || '') + text)) {
|
|
3386
|
+
console.warn(`⚠️ 批 ${r.name} 判定基建失败(${String(r.reason || 'runner 层失败').slice(0, 80)})——不进增量失败批账本(重跑不可能转绿),修复基建后整轮重测`)
|
|
3387
|
+
continue
|
|
3388
|
+
}
|
|
3289
3389
|
for (const f of attributeFailedFiles(r)) out.add(f)
|
|
3290
3390
|
}
|
|
3291
3391
|
return [...out].sort()
|
|
@@ -4223,6 +4323,51 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
|
|
|
4223
4323
|
}
|
|
4224
4324
|
}
|
|
4225
4325
|
|
|
4326
|
+
// —— EOL-only 噪声过滤(坑 verify-close-gate-staggered-exposure 建议④,2026-10-15 消费仓实证:
|
|
4327
|
+
// gen:types 幂等重生成纯行尾 diff 进 actual 面 → ③类 undeclared 假红,agent 被迫为零内容
|
|
4328
|
+
// 改动补声明或回退)。手法与 run/shared.js quick 审计的 eolOnly 先例同源:同一 git 范围
|
|
4329
|
+
// 跑 plain 名单与 --ignore-cr-at-eol 名单,差集 = 纯行尾差异 → 剔出对账面,eolOnlyFiles
|
|
4330
|
+
// 软留痕。两条硬边界:① 必须正向出现在 plain diff 且内容零变更才剔——absence-based
|
|
4331
|
+
// (「不在真实集即剔」)会误伤 B3 pathspec/救援窗口来源的已交付文件(它们不在任何
|
|
4332
|
+
// git diff 里);② --name-only 不吃 --ignore-cr-at-eol(git 实测怪癖),内容过滤必须走
|
|
4333
|
+
// --numstat。探测失败 fail-open 不过滤;untracked 天然不在 plain diff 不受影响;删除/
|
|
4334
|
+
// 重命名是内容变更(numstat 在场)不受影响。——
|
|
4335
|
+
const eolOnlyFiles = []
|
|
4336
|
+
try {
|
|
4337
|
+
const eolOnlySet = new Set()
|
|
4338
|
+
const probePair = (dir, refs) => {
|
|
4339
|
+
const plainOut = gitQuiet(dir, ['diff', '--name-only', ...refs], { timeout: 30 * 1000, trim: false })
|
|
4340
|
+
if (plainOut === null) return
|
|
4341
|
+
const realOut = gitQuiet(dir, ['diff', '--numstat', '--ignore-cr-at-eol', ...refs], { timeout: 30 * 1000, trim: false })
|
|
4342
|
+
if (realOut === null) return
|
|
4343
|
+
const real = new Set()
|
|
4344
|
+
for (const l of realOut.split('\n')) {
|
|
4345
|
+
const i = l.lastIndexOf('\t')
|
|
4346
|
+
if (i > 0) real.add(normalizeReconcilePath(l.slice(i + 1)))
|
|
4347
|
+
}
|
|
4348
|
+
for (const f of plainOut.split('\n').map(x => x.trim()).filter(Boolean)) {
|
|
4349
|
+
const n = normalizeReconcilePath(f)
|
|
4350
|
+
if (!real.has(n)) eolOnlySet.add(n)
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
// 工作树(HEAD 对照):主仓 + worktree(form A 双仓各自 HEAD)
|
|
4354
|
+
const probeDirs = [cwd]
|
|
4355
|
+
if (form === 'worktree' && wtMeta && wtMeta.gitDir && wtMeta.gitDir !== cwd) probeDirs.push(wtMeta.gitDir)
|
|
4356
|
+
for (const dir of probeDirs) probePair(dir, ['HEAD'])
|
|
4357
|
+
// 已提交侧:form B B1 同基点(mergeBase 对照工作树);form A worktree 已提交段(meta 锚点..HEAD)
|
|
4358
|
+
if (form === 'post-apply' && baseAnchor) {
|
|
4359
|
+
probePair(cwd, [baseAnchor])
|
|
4360
|
+
} else if (form === 'worktree' && wtMeta) {
|
|
4361
|
+
const mb = wtMeta.meta && (wtMeta.meta.baselineCommit || wtMeta.meta.actualBaseHash || wtMeta.meta.baseHash)
|
|
4362
|
+
if (mb) probePair(wtMeta.gitDir, [mb, 'HEAD'])
|
|
4363
|
+
}
|
|
4364
|
+
for (const f of eolOnlySet) {
|
|
4365
|
+
if (union.has(f)) {
|
|
4366
|
+
union.delete(f)
|
|
4367
|
+
eolOnlyFiles.push(f)
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
} catch { /* EOL 探测异常 → 不过滤(fail-open,行为与修复前一致) */ }
|
|
4226
4371
|
const files = [...new Set(filterDeliverableFiles([...union]).filter(Boolean))].sort()
|
|
4227
4372
|
// 多会话推进死锁诊断(reconcile-source-isolation,R18-SF-full 实证):post-apply 形态下
|
|
4228
4373
|
// 实际面全空(B1 无 ref + porcelain 无未提交面 + 无 apply-pathspec 兜底)= 典型「主仓被
|
|
@@ -4232,7 +4377,7 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
|
|
|
4232
4377
|
const parallelAdvanceHint = (form === 'post-apply' && files.length === 0 && union.size === 0 && !sources.some((s) => s.startsWith('main:diff-merge-base') || s.startsWith('apply-pathspec')))
|
|
4233
4378
|
? '⚠️ actual 源全空(无分支锚点 diff + 主仓无未提交面 + 无 apply-pathspec 兜底)——典型「主仓被并行会话推进」形态(本变更的提交/暂存可能已被他侧裸提交扫走)。切勿用「暂存物化」自救(touch 文件进共享暂存区——会被再次扫走且污染他侧,R18-SF-full 实证 matched=0 死循环);出路:① 恢复/登记本变更分支(worktree meta.json 的 branch 字段)让锚定 diff 生效后重跑 ② 确属全量已在他侧提交:与该会话按 AGENTS 规则 18 对账后以该提交为锚核销'
|
|
4234
4379
|
: null
|
|
4235
|
-
return { ok: true, form, files, sources, foreignExcluded, foreignExcludedFiles, degradedReason: null, baseAnchor, commitWindowFiles, parallelAdvanceHint }
|
|
4380
|
+
return { ok: true, form, files, sources, foreignExcluded, foreignExcludedFiles, degradedReason: null, baseAnchor, commitWindowFiles, parallelAdvanceHint, eolOnlyFiles }
|
|
4236
4381
|
}
|
|
4237
4382
|
|
|
4238
4383
|
/**
|
|
@@ -4462,6 +4607,9 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
4462
4607
|
notes.push(`③类另有 ${scaffoldTotal} 个工具/平台脚手架文件(platform ${scaffoldBuckets.platform} / tool ${scaffoldBuckets.tool},如 ${scaffoldSamples.join('、')}${scaffoldTotal > scaffoldSamples.length ? ' 等' : ''})已聚合不逐条列——非交付物(scope-audit 设施桶同口径)`)
|
|
4463
4608
|
}
|
|
4464
4609
|
|
|
4610
|
+
if (Array.isArray(actual.eolOnlyFiles) && actual.eolOnlyFiles.length > 0) {
|
|
4611
|
+
notes.push(`EOL-only 剔除:${actual.eolOnlyFiles.length} 个文件仅行尾差异(--ignore-cr-at-eol 对照零内容变更,如 gen 脚本幂等重生成)已剔出对账面——${actual.eolOnlyFiles.slice(0, 5).join('、')}${actual.eolOnlyFiles.length > 5 ? ' 等' : ''}(.gitattributes 统一行尾可根治)`)
|
|
4612
|
+
}
|
|
4465
4613
|
// 状态聚合:②在场即 ERROR 态优先(gates 阻断语义靠它兑现);仅③ → WARNING 态
|
|
4466
4614
|
const status = missing.length > 0 ? 'missing_declared' : (undeclared.length > 0 ? 'undeclared' : 'ok')
|
|
4467
4615
|
return { status, matched, missing, undeclared, undeclaredScaffold: scaffoldTotal, skipReason: null, notes, form: actual.form, sources: actual.sources, crossRepo }
|