sillyspec 3.32.10 → 3.33.1
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 +66 -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/archive-delta.js +4 -3
- package/src/ceremony-tier.js +2 -1
- package/src/change-delete.js +6 -5
- package/src/contract-matrix.js +2 -1
- package/src/cross-placement.js +5 -1
- package/src/db.js +8 -2
- package/src/decision-distill.js +29 -3
- package/src/decision-redomain.js +148 -0
- package/src/deleted-ledger.js +2 -1
- package/src/design-facts.js +3 -3
- package/src/docs-debt.js +3 -3
- package/src/doctor-diagnostics.js +8 -3
- package/src/flow-draft.js +2 -1
- package/src/flow-parity.js +10 -2
- package/src/flow.js +112 -63
- package/src/forensics.js +236 -0
- package/src/fr-index.js +2 -1
- package/src/friction-ledger.js +2 -1
- package/src/friction-tally.js +8 -4
- package/src/handoff.js +2 -1
- package/src/hooks/worktree-guard.js +5 -5
- package/src/index.js +75 -8
- package/src/knowledge-graph.js +3 -4
- package/src/knowledge-hits.js +2 -1
- package/src/knowledge-vector.js +2 -1
- package/src/module-impact.js +2 -5
- package/src/modules.js +31 -0
- package/src/progress/consistency-doctor.js +5 -2
- package/src/progress/stage-machine.js +6 -2
- package/src/progress.js +19 -8
- package/src/quicklog.js +194 -9
- package/src/review-dispatch.js +4 -3
- package/src/review-tier.js +2 -1
- package/src/route-hindsight.js +4 -3
- package/src/run/bg-sync.js +10 -3
- package/src/run/command.js +13 -11
- package/src/run/complete-handlers.js +40 -37
- package/src/run/complete.js +12 -8
- package/src/run/gate-snapshot-ledger.js +2 -1
- package/src/run/gate-snapshot.js +57 -11
- package/src/run/gates.js +38 -21
- package/src/run/lint-ownership.js +37 -0
- package/src/run/prompt.js +46 -30
- package/src/run/scan-profile.js +2 -1
- package/src/run/shared.js +109 -5
- package/src/run/stage.js +5 -4
- package/src/run/test-ledger.js +2 -1
- package/src/run/verify-quality-scan.js +21 -6
- package/src/runtime-paths.js +189 -0
- package/src/scan-refresh.js +4 -3
- package/src/scope-audit.js +7 -6
- package/src/sentinel-assertions.js +37 -0
- package/src/spec-sync.js +118 -23
- package/src/stage-contract.js +24 -2
- package/src/stage-review.js +5 -3
- package/src/stages/brainstorm.js +72 -21
- package/src/stages/plan-postcheck.js +13 -2
- package/src/stages/plan.js +19 -0
- package/src/sync-noise.js +3 -2
- package/src/sync.js +71 -15
- package/src/task-done.js +2 -1
- package/src/task-review.js +7 -5
- package/src/task-tick.js +23 -1
- package/src/test-bindings.js +32 -3
- package/src/ui-visual.js +119 -4
- package/src/validate-artifacts.js +2 -1
- package/src/verify-draft.js +2 -1
- package/src/verify-postcheck.js +128 -27
- package/src/verify-probes.js +157 -52
- package/src/watcher.js +15 -9
- package/src/worktree-apply.js +28 -3
- package/src/write-audit.js +2 -1
- package/templates/agents-instruction.md +1 -1
package/src/spec-sync.js
CHANGED
|
@@ -19,6 +19,8 @@ import { createHash } from 'crypto';
|
|
|
19
19
|
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
20
20
|
import { join, relative, sep } from 'path';
|
|
21
21
|
import { bindSyncNoiseRoot, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess } from './sync-noise.js';
|
|
22
|
+
import { runtimeStatePath } from './runtime-paths.js';
|
|
23
|
+
import { writeAtomicSync } from './fs-atomic.js';
|
|
22
24
|
|
|
23
25
|
// 与 sillyhub-daemon/src/spec-sync.ts 共用排除口径(task-07 / FR-06 / D-008@v2):
|
|
24
26
|
// .runtime(有点)/ runtime(无点)/ projects(本地环境文件)在顶层排除;
|
|
@@ -63,6 +65,61 @@ export function describeSyncError(err) {
|
|
|
63
65
|
return err && err.message ? err.message : String(err);
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
// ── 同步失败连续计数与响应体摘要(2026-10-16 生产索引损坏事故可观测性加固)──
|
|
69
|
+
// 事故:平台 422 业务错误只打状态码(真因 HTTP_422_SPEC_BUNDLE_INVALID+路径要手动重放请求
|
|
70
|
+
// 才拿到),且失败走「下次自动重试」无限静默循环——归档半态(本地已归档、平台仍「进行中」)
|
|
71
|
+
// 挂数小时无人知晓。两道护栏:①非 2xx 响应体解析进告警行;②按 label 连续失败计数持久化,
|
|
72
|
+
// 连续 ≥3 次升级 🚨 显式告警,任一次成功清零。
|
|
73
|
+
const FAIL_STREAK_FILE = join('.runtime', 'state', 'spec-sync-fail-streak.json');
|
|
74
|
+
const FAIL_STREAK_ESCALATE_AT = 3;
|
|
75
|
+
|
|
76
|
+
export function describeErrorBody(status, rawBody) {
|
|
77
|
+
if (!rawBody) return '';
|
|
78
|
+
try {
|
|
79
|
+
const j = JSON.parse(rawBody);
|
|
80
|
+
const code = j.code || `HTTP_${status}`;
|
|
81
|
+
const p = j.details && j.details.path ? ` path=${j.details.path}` : '';
|
|
82
|
+
return j.message ? `${code}${p}: ${j.message}` : rawBody.slice(0, 200);
|
|
83
|
+
} catch {
|
|
84
|
+
return rawBody.slice(0, 200);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function readFailStreak(specRoot) {
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(readFileSync(join(specRoot, FAIL_STREAK_FILE), 'utf8')) || {};
|
|
91
|
+
} catch {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function bumpFailStreak(specRoot, label, detail) {
|
|
97
|
+
const all = readFailStreak(specRoot);
|
|
98
|
+
const cur = all[label] || { count: 0 };
|
|
99
|
+
cur.count += 1;
|
|
100
|
+
cur.lastAt = new Date().toISOString();
|
|
101
|
+
if (detail) cur.lastError = String(detail).slice(0, 300);
|
|
102
|
+
all[label] = cur;
|
|
103
|
+
try {
|
|
104
|
+
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
105
|
+
writeAtomicSync(join(specRoot, FAIL_STREAK_FILE), JSON.stringify(all, null, 2) + '\n');
|
|
106
|
+
} catch {
|
|
107
|
+
// fail-soft:计数落盘失败只影响告警升级,不阻断同步主流程
|
|
108
|
+
}
|
|
109
|
+
return cur.count;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function clearFailStreak(specRoot, label) {
|
|
113
|
+
const all = readFailStreak(specRoot);
|
|
114
|
+
if (!all[label]) return;
|
|
115
|
+
delete all[label];
|
|
116
|
+
try {
|
|
117
|
+
writeAtomicSync(join(specRoot, FAIL_STREAK_FILE), JSON.stringify(all, null, 2) + '\n');
|
|
118
|
+
} catch {
|
|
119
|
+
// fail-soft:残留计数只是少清一次,下次成功再清
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
66
123
|
function _hashBuffer(buf) {
|
|
67
124
|
return createHash('sha256').update(buf).digest('hex');
|
|
68
125
|
}
|
|
@@ -331,7 +388,7 @@ const LAST_SUCCESS_MARKER = 'spec-sync-last-success.json';
|
|
|
331
388
|
|
|
332
389
|
function readLastSyncTs(specRoot) {
|
|
333
390
|
try {
|
|
334
|
-
const raw = readFileSync(join(specRoot, '.runtime', LAST_SUCCESS_MARKER), 'utf8');
|
|
391
|
+
const raw = readFileSync(runtimeStatePath(join(specRoot, '.runtime'), LAST_SUCCESS_MARKER), 'utf8');
|
|
335
392
|
const ts = JSON.parse(raw).ts;
|
|
336
393
|
return Number.isFinite(ts) ? ts : null;
|
|
337
394
|
} catch {
|
|
@@ -342,7 +399,7 @@ function readLastSyncTs(specRoot) {
|
|
|
342
399
|
function writeLastSyncTs(specRoot) {
|
|
343
400
|
try {
|
|
344
401
|
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
345
|
-
writeFileSync(join(specRoot, '.runtime', LAST_SUCCESS_MARKER), JSON.stringify({ ts: Date.now() }) + '\n', 'utf8');
|
|
402
|
+
writeFileSync(runtimeStatePath(join(specRoot, '.runtime'), LAST_SUCCESS_MARKER), JSON.stringify({ ts: Date.now() }) + '\n', 'utf8');
|
|
346
403
|
} catch { /* 标记失败只损失下次的防回推过滤,不影响本次同步结果 */ }
|
|
347
404
|
}
|
|
348
405
|
|
|
@@ -359,7 +416,7 @@ const BASE_SNAPSHOT = 'spec-sync-base.json';
|
|
|
359
416
|
|
|
360
417
|
function readBaseSnapshot(specRoot) {
|
|
361
418
|
try {
|
|
362
|
-
const raw = readFileSync(join(specRoot, '.runtime', BASE_SNAPSHOT), 'utf8');
|
|
419
|
+
const raw = readFileSync(runtimeStatePath(join(specRoot, '.runtime'), BASE_SNAPSHOT), 'utf8');
|
|
363
420
|
const hashes = JSON.parse(raw).hashes;
|
|
364
421
|
return hashes && typeof hashes === 'object' ? hashes : null;
|
|
365
422
|
} catch {
|
|
@@ -372,7 +429,7 @@ function writeBaseSnapshot(specRoot, localFiles) {
|
|
|
372
429
|
const hashes = {};
|
|
373
430
|
for (const f of localFiles) hashes[f.path] = f.hash;
|
|
374
431
|
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
375
|
-
writeFileSync(join(specRoot, '.runtime', BASE_SNAPSHOT), JSON.stringify({ ts: Date.now(), hashes }, null, 2) + '\n', 'utf8');
|
|
432
|
+
writeFileSync(runtimeStatePath(join(specRoot, '.runtime'), BASE_SNAPSHOT), JSON.stringify({ ts: Date.now(), hashes }, null, 2) + '\n', 'utf8');
|
|
376
433
|
} catch { /* 快照失败只损失下次的内容级防回推,不影响本次同步结果 */ }
|
|
377
434
|
}
|
|
378
435
|
|
|
@@ -441,7 +498,7 @@ export function partitionConflictPaths(serverVersions, localFiles, baseHashes, l
|
|
|
441
498
|
*/
|
|
442
499
|
function isSamePendingSpecConflict(specRoot, changeName, realPaths) {
|
|
443
500
|
try {
|
|
444
|
-
const raw = readFileSync(join(specRoot, '.runtime', `spec-sync-conflict-${changeName}.json`), 'utf8');
|
|
501
|
+
const raw = readFileSync(runtimeStatePath(join(specRoot, '.runtime'), `spec-sync-conflict-${changeName}.json`), 'utf8');
|
|
445
502
|
const prev = JSON.parse(raw).conflicting_paths;
|
|
446
503
|
if (!Array.isArray(prev) || prev.length !== realPaths.length) return false;
|
|
447
504
|
const prevSet = new Set(prev);
|
|
@@ -458,7 +515,7 @@ function isSamePendingSpecConflict(specRoot, changeName, realPaths) {
|
|
|
458
515
|
*/
|
|
459
516
|
function clearSpecConflictMarker(specRoot, changeName) {
|
|
460
517
|
try {
|
|
461
|
-
const p = join(specRoot, '.runtime', `spec-sync-conflict-${changeName}.json`);
|
|
518
|
+
const p = runtimeStatePath(join(specRoot, '.runtime'), `spec-sync-conflict-${changeName}.json`);
|
|
462
519
|
if (existsSync(p)) unlinkSync(p);
|
|
463
520
|
} catch { /* 清理失败不影响同步结果 */ }
|
|
464
521
|
}
|
|
@@ -497,7 +554,7 @@ export function writeTombstoneAttribution(specRoot, platformDeleted) {
|
|
|
497
554
|
const byChange = extractTombstonedChanges(platformDeleted);
|
|
498
555
|
const written = [];
|
|
499
556
|
for (const [name, paths] of byChange) {
|
|
500
|
-
const filePath = join(specRoot, '.runtime', `spec-sync-conflict-${name}.json`);
|
|
557
|
+
const filePath = runtimeStatePath(join(specRoot, '.runtime'), `spec-sync-conflict-${name}.json`);
|
|
501
558
|
let prev = null;
|
|
502
559
|
try { prev = JSON.parse(readFileSync(filePath, 'utf8')); } catch { /* 首见 */ }
|
|
503
560
|
const record = {
|
|
@@ -528,13 +585,17 @@ export function writeTombstoneAttribution(specRoot, platformDeleted) {
|
|
|
528
585
|
* 清理条数。挂在两个全绿点:POST 成功无冲突分支与无差异跳过分支。
|
|
529
586
|
*/
|
|
530
587
|
export function clearStaleTombstoneMarkers(specRoot) {
|
|
588
|
+
// 2026-10-18 布局分层:冲突文件落 state/;根目录扫描保留旧布局兜底
|
|
531
589
|
const runtimeDir = join(specRoot, '.runtime');
|
|
532
590
|
let files = [];
|
|
533
|
-
|
|
591
|
+
const conflictDirs = [join(runtimeDir, 'state'), runtimeDir]
|
|
534
592
|
let cleared = 0;
|
|
535
|
-
for (const
|
|
593
|
+
for (const dir of conflictDirs) {
|
|
594
|
+
let dirFiles = [];
|
|
595
|
+
try { dirFiles = readdirSync(dir); } catch { continue; }
|
|
596
|
+
for (const f of dirFiles) {
|
|
536
597
|
if (!f.startsWith('spec-sync-conflict-') || !f.endsWith('.json')) continue;
|
|
537
|
-
const p = join(
|
|
598
|
+
const p = join(dir, f);
|
|
538
599
|
try {
|
|
539
600
|
const j = JSON.parse(readFileSync(p, 'utf8'));
|
|
540
601
|
const cp = Array.isArray(j.conflicting_paths) ? j.conflicting_paths : null;
|
|
@@ -545,6 +606,7 @@ export function clearStaleTombstoneMarkers(specRoot) {
|
|
|
545
606
|
}
|
|
546
607
|
} catch { /* 损坏文件不动 */ }
|
|
547
608
|
}
|
|
609
|
+
}
|
|
548
610
|
if (cleared > 0) {
|
|
549
611
|
console.log(`[spec-sync] 全绿同步清理 ${cleared} 条陈旧墓碑冲突记录(被删变更已收敛)`);
|
|
550
612
|
}
|
|
@@ -568,7 +630,7 @@ const FOLLOW_REPORT_MARKER = 'spec-sync-follow-reported.json';
|
|
|
568
630
|
*/
|
|
569
631
|
export function isFollowerSetChanged(specRoot, key, list) {
|
|
570
632
|
const sorted = [...(list || [])].sort();
|
|
571
|
-
const p = join(specRoot, '.runtime', FOLLOW_REPORT_MARKER);
|
|
633
|
+
const p = runtimeStatePath(join(specRoot, '.runtime'), FOLLOW_REPORT_MARKER);
|
|
572
634
|
let cur = {};
|
|
573
635
|
try { cur = JSON.parse(readFileSync(p, 'utf8')) || {}; } catch { /* 无 marker = 首报 */ }
|
|
574
636
|
const prev = Array.isArray(cur[key]) ? cur[key] : [];
|
|
@@ -609,10 +671,20 @@ function attributeChangeLabel(paths) {
|
|
|
609
671
|
return names.size === 1 ? [...names][0] : 'full-tree'
|
|
610
672
|
}
|
|
611
673
|
|
|
674
|
+
/**
|
|
675
|
+
* spec 树增量同步(以服务器清单为锚做全树 diff → POST ops)。
|
|
676
|
+
*
|
|
677
|
+
* 返回契约(2026-10-17-archive-half-state-location-stuck-active):`ok` 字段表达
|
|
678
|
+
* 「本轮文件树通道是否确认收敛」——ok:true = 确认(无差异 no-op / 推送成功 / 冲突自愈
|
|
679
|
+
* / 未连接本地合法态);ok:false = 确认失败(清单/推送 HTTP 失败、异常);**无 ok 字段
|
|
680
|
+
* = 未确认**(未决冲突待人裁等,fail-closed)。终态强校验消费方
|
|
681
|
+
* (run/shared.js triggerSyncTerminalStrict)按 `ok === true` 判收敛——历史调用方
|
|
682
|
+
* (syncSpecTreeOnly / resolve forcePush 等)不消费该字段,向后兼容。
|
|
683
|
+
*/
|
|
612
684
|
export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
613
685
|
if (!platform || !platform.url || !platform.token) {
|
|
614
686
|
debugLog('[spec-sync] 未连接平台(本地合法状态);跳过 spec 树增量同步');
|
|
615
|
-
return { synced: 0 };
|
|
687
|
+
return { synced: 0, ok: true };
|
|
616
688
|
}
|
|
617
689
|
// 全树入口(platform sync 无 --change,2026-10-11-sync-conflict-tri-fix FR-03)
|
|
618
690
|
// changeName=null:日志用 full-tree 标签,冲突归因见下方 conflictLabel。
|
|
@@ -640,17 +712,27 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
640
712
|
// 8 分钟才被后续步骤的同步补上)。失败可见、成功不打扰。
|
|
641
713
|
// 连接类失败(404=端点未就绪 / 5xx)走噪音闸(sync-noise.js):平台未就绪期每步
|
|
642
714
|
// 自动同步重刷同一行,淹没门禁输出;其余状态码(4xx 业务态)每轮该看见。
|
|
643
|
-
const
|
|
715
|
+
const raw = await res.text().catch(() => '');
|
|
716
|
+
const detail = describeErrorBody(res.status, raw);
|
|
717
|
+
const failCount = bumpFailStreak(specRoot, logLabel, detail);
|
|
718
|
+
const line = `[spec-sync] 拉取清单失败 HTTP ${res.status}(文件树本次未同步,下次自动重试): ${logLabel}${detail ? ` —— ${detail}` : ''}`;
|
|
644
719
|
if (isConnectionClassStatus(res.status)) syncConnectionWarn(line);
|
|
645
720
|
else console.warn(line);
|
|
646
|
-
|
|
721
|
+
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
722
|
+
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
723
|
+
}
|
|
724
|
+
return { synced: 0, ok: false };
|
|
647
725
|
}
|
|
648
726
|
const body = await res.json().catch(() => ({}));
|
|
649
727
|
serverManifest = body.files || {};
|
|
650
728
|
} catch (err) {
|
|
729
|
+
const failCount = bumpFailStreak(specRoot, logLabel, describeSyncError(err));
|
|
651
730
|
const line = `[spec-sync] 拉取清单异常(文件树本次未同步,下次自动重试): ${logLabel}: ${describeSyncError(err)}`;
|
|
652
731
|
syncConnectionWarn(line); // 网络/超时/熔断均连接类
|
|
653
|
-
|
|
732
|
+
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
733
|
+
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
734
|
+
}
|
|
735
|
+
return { synced: 0, ok: false };
|
|
654
736
|
}
|
|
655
737
|
|
|
656
738
|
// 2. walk/hash/diff
|
|
@@ -691,7 +773,8 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
691
773
|
if (ops.length === 0) {
|
|
692
774
|
debugLog(`[spec-sync] 无差异,跳过同步: ${logLabel}`);
|
|
693
775
|
clearStaleTombstoneMarkers(specRoot);
|
|
694
|
-
|
|
776
|
+
clearFailStreak(specRoot, logLabel);
|
|
777
|
+
return { synced: 0, ok: true };
|
|
695
778
|
}
|
|
696
779
|
|
|
697
780
|
// 3. POST ops
|
|
@@ -706,10 +789,16 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
706
789
|
signal: withSignal(30000),
|
|
707
790
|
});
|
|
708
791
|
if (!res.ok) {
|
|
709
|
-
const
|
|
792
|
+
const raw = await res.text().catch(() => '');
|
|
793
|
+
const detail = describeErrorBody(res.status, raw);
|
|
794
|
+
const failCount = bumpFailStreak(specRoot, logLabel, detail);
|
|
795
|
+
const line = `[spec-sync] 同步请求失败 HTTP ${res.status}(文件树本次未同步,下次自动重试): ${logLabel}${detail ? ` —— ${detail}` : ''}`;
|
|
710
796
|
if (isConnectionClassStatus(res.status)) syncConnectionWarn(line);
|
|
711
797
|
else console.warn(line);
|
|
712
|
-
|
|
798
|
+
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
799
|
+
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
800
|
+
}
|
|
801
|
+
return { synced: 0, ok: false };
|
|
713
802
|
}
|
|
714
803
|
const body = await res.json().catch(() => ({}));
|
|
715
804
|
if (body.conflict) {
|
|
@@ -770,7 +859,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
770
859
|
debugLog(`[spec-sync] ${followers.length} 个本地未改动文件自动跟随服务器: ${followers.slice(0, 5).join(', ')}${followers.length > 5 ? ' 等' : ''}`);
|
|
771
860
|
}
|
|
772
861
|
console.warn(`⚠️ [spec-sync] 变更 ${conflictLabel} 存在未决 spec 树冲突(${realPaths.length} 个文件待裁决,本轮非冲突改动已照常同步)。处理:sillyspec platform resolve ${conflictLabel} --keep-local | --take-platform | --abort`);
|
|
773
|
-
return { synced: 0, conflict: true, serverVersions: real, autoResolved: followers.length, conflictPath: join(specRoot, '.runtime', `spec-sync-conflict-${conflictLabel}.json`), suppressed: true };
|
|
862
|
+
return { synced: 0, conflict: true, serverVersions: real, autoResolved: followers.length, conflictPath: runtimeStatePath(join(specRoot, '.runtime'), `spec-sync-conflict-${conflictLabel}.json`), suppressed: true };
|
|
774
863
|
}
|
|
775
864
|
if (followers.length > 0) {
|
|
776
865
|
console.warn(`[spec-sync] ${followers.length} 个本地未改动文件自动跟随服务器(正常多端前进,无需人工裁决): ${followers.slice(0, 5).join(', ')}${followers.length > 5 ? ' 等' : ''}`);
|
|
@@ -780,14 +869,15 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
780
869
|
writeBaseSnapshot(specRoot, localFiles);
|
|
781
870
|
clearSpecConflictMarker(specRoot, conflictLabel);
|
|
782
871
|
noteSyncSuccess();
|
|
872
|
+
clearFailStreak(specRoot, logLabel);
|
|
783
873
|
console.log(`[spec-sync] 冲突已自动消解(0 个需人工裁决;本会话改动 ${ops.length - followers.length > 0 ? '已同步' : '无差异'}): ${conflictLabel}`);
|
|
784
|
-
return { synced: Math.max(ops.length - followers.length, 0), conflict: false, autoResolved: followers.length };
|
|
874
|
+
return { synced: Math.max(ops.length - followers.length, 0), ok: true, conflict: false, autoResolved: followers.length };
|
|
785
875
|
}
|
|
786
876
|
let conflictPath = null;
|
|
787
877
|
try {
|
|
788
878
|
const runtimeDir = join(specRoot, '.runtime');
|
|
789
879
|
mkdirSync(runtimeDir, { recursive: true });
|
|
790
|
-
conflictPath =
|
|
880
|
+
conflictPath = runtimeStatePath(runtimeDir, `spec-sync-conflict-${conflictLabel}.json`);
|
|
791
881
|
writeFileSync(conflictPath, JSON.stringify({
|
|
792
882
|
change: conflictLabel,
|
|
793
883
|
kind: 'spec-tree',
|
|
@@ -811,6 +901,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
811
901
|
return { synced: 0, conflict: true, serverVersions: real, autoResolved: followers.length, conflictPath };
|
|
812
902
|
}
|
|
813
903
|
noteSyncSuccess();
|
|
904
|
+
clearFailStreak(specRoot, logLabel);
|
|
814
905
|
console.log(`[spec-sync] 已同步 ${ops.length} 个文件变更: ${logLabel}`);
|
|
815
906
|
// 本轮无冲突 = 此前的未决 spec 树冲突已不存在,清残留标记(否则 status 永久红标、
|
|
816
907
|
// 横幅去重单行提示不消——坑 spec-sync-conflict-banner-spam)。全树入口
|
|
@@ -826,11 +917,15 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
826
917
|
clearStaleTombstoneMarkers(specRoot);
|
|
827
918
|
writeLastSyncTs(specRoot);
|
|
828
919
|
writeBaseSnapshot(specRoot, localFiles);
|
|
829
|
-
return { synced: ops.length };
|
|
920
|
+
return { synced: ops.length, ok: true };
|
|
830
921
|
} catch (err) {
|
|
922
|
+
const failCount = bumpFailStreak(specRoot, logLabel, describeSyncError(err));
|
|
831
923
|
const line = `[spec-sync] 同步异常(文件树本次未同步,下次自动重试): ${logLabel}: ${describeSyncError(err)}`;
|
|
832
924
|
syncConnectionWarn(line); // 网络/超时/熔断均连接类
|
|
833
|
-
|
|
925
|
+
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
926
|
+
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
927
|
+
}
|
|
928
|
+
return { synced: 0, ok: false };
|
|
834
929
|
}
|
|
835
930
|
}
|
|
836
931
|
|
package/src/stage-contract.js
CHANGED
|
@@ -897,7 +897,7 @@ function matrixEvidenceMissing(verdict, evidence) {
|
|
|
897
897
|
*/
|
|
898
898
|
export function extractAcceptanceMatrixSlots(verifyMd) {
|
|
899
899
|
const md = String(verifyMd || '')
|
|
900
|
-
const out = { present: false, rows: [], unfilled: 0, missingEvidence: 0 }
|
|
900
|
+
const out = { present: false, rows: [], unfilled: 0, missingEvidence: 0, strayRows: [] }
|
|
901
901
|
const hm = md.match(/^#### 探针 7[::]/m)
|
|
902
902
|
if (!hm) return out
|
|
903
903
|
out.present = true
|
|
@@ -905,15 +905,27 @@ export function extractAcceptanceMatrixSlots(verifyMd) {
|
|
|
905
905
|
const endMatch = rest.match(/^#{1,4}(?:[ \t]|$)/m)
|
|
906
906
|
const sectionText = endMatch ? rest.slice(0, endMatch.index) : rest
|
|
907
907
|
|
|
908
|
+
// 表头武装(坑 carry-orphan-rows,2026-10-16 下游实证):五列表行只在探针 7 段已被表头行
|
|
909
|
+
// 武装时才计数;refresh 携载注释块解除武装——其后残行(旧模板占位行/键漂移携载行)是无
|
|
910
|
+
// 表头归属的孤行,不栽给最近 **task-NN** 锚误报「判定未填」,进独立 strayRows 桶由
|
|
911
|
+
// validateAcceptanceMatrix 报错要求裁决。武装是段级粘性(首表头武装整段,锚行不重置——
|
|
912
|
+
// 锚下不重复表头的既有手写/历史形态照旧按锚归属);只有携载注释块与其后无新表头重新
|
|
913
|
+
// 武装的窗口内,行才进孤行桶。正常渲染文法(锚行→表头→分隔→数据行)行为与既有口径一致。
|
|
908
914
|
let currentTask = ''
|
|
915
|
+
let tableArmed = false
|
|
909
916
|
for (const rawLine of sectionText.split(/\r?\n/)) {
|
|
910
917
|
const anchor = rawLine.match(/^\*\*\s*(task-[A-Za-z0-9._-]+)\s*\*\*/)
|
|
911
918
|
if (anchor) { currentTask = anchor[1]; continue }
|
|
919
|
+
if (/<!--\s*refresh 携载/.test(rawLine)) { tableArmed = false; continue } // 携载注释块:其后表行是孤行(直至新表头重新武装)
|
|
912
920
|
const cells = splitMatrixRowCells(rawLine)
|
|
913
921
|
if (!cells || cells.length !== 5) continue // 列表防御行/不适用行/非五列表行不计槽
|
|
914
922
|
const c = cells.map(x => x.trim())
|
|
915
923
|
if (c.every(x => /^:?-{3,}:?$/.test(x))) continue // 分隔行 |---|---|…|
|
|
916
|
-
if (c[3] === '判定' && c[4] === '证据') continue //
|
|
924
|
+
if (c[3] === '判定' && c[4] === '证据') { tableArmed = true; continue } // 表头行(武装本段)
|
|
925
|
+
if (!tableArmed) {
|
|
926
|
+
out.strayRows.push({ task: currentTask, acceptance: c[0], verdict: c[3], evidence: c[4] })
|
|
927
|
+
continue
|
|
928
|
+
}
|
|
917
929
|
const verdict = c[3]
|
|
918
930
|
const evidence = c[4]
|
|
919
931
|
const row = { task: currentTask, acceptance: c[0], verdict, evidence }
|
|
@@ -985,6 +997,16 @@ function validateAcceptanceMatrix(cwd, changeName, context = {}) {
|
|
|
985
997
|
`修复:在证据列补测试锚点(如 \`test/foo.test.mjs\` 或 \`src/x.js:42\`)或 non-testable 理由(运行验证移交场景适用:构建回执+移交去向一句话)。`
|
|
986
998
|
)
|
|
987
999
|
}
|
|
1000
|
+
// 孤行报错(坑 carry-orphan-rows,2026-10-16 下游实证):无表头归属的五列残行(refresh
|
|
1001
|
+
// 携载块后的旧模板占位行/键漂移携载行/手误行)不归属任何 task 锚——不栽给最近锚计
|
|
1002
|
+
// unfilled(旧路径误报「文档同步任务判定未填」),独立报错要求裁决:删或补归属。
|
|
1003
|
+
if ((matrix.strayRows || []).length > 0) {
|
|
1004
|
+
const list = matrix.strayRows.map((r) => `${r.task || '(无 task 锚)'}|${clip(r.acceptance)}`).join(';')
|
|
1005
|
+
errors.push(
|
|
1006
|
+
`探针 7 验收×测试覆盖矩阵存在 ${matrix.strayRows.length} 行无表头归属的孤行(携载残行/旧模板占位行/手误——不属于任何 task 锚下的有效表,不计入任务矩阵):${list}。` +
|
|
1007
|
+
`修复:删除这些残行,或补全 **task-NN** 锚行与表头后按五列格式填判定与证据。`
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
988
1010
|
|
|
989
1011
|
// ── partial/uncovered × facts.handover 联动(D-003/FR-04,2026-09-17-pass-cap-semantics task-03)──
|
|
990
1012
|
// 部分实现必须有移交去向:矩阵含 partial/uncovered 行且 facts.handover 零有效行(任意
|
package/src/stage-review.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'fs'
|
|
17
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
17
18
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
18
19
|
import { join, dirname } from 'path'
|
|
19
20
|
import { createHash } from 'crypto'
|
|
@@ -139,7 +140,8 @@ export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier,
|
|
|
139
140
|
'',
|
|
140
141
|
'> 路径:`{SPEC_ROOT}/.runtime/stage-reviews/' + stageLbl + '-' + runIdLbl + '/review.json`(本次 reviewType=' + reviewType + ',主审查文档=' + mainDocRel + ')',
|
|
141
142
|
...(changeDir
|
|
142
|
-
? ['>
|
|
143
|
+
? ['> **本次完整绝对路径(直接复制给审查子代理——勿手拼、勿增删任何前缀)**: `' + join(dirname(dirname(changeDir)), '.runtime', 'stage-reviews', stageLbl + '-' + runIdLbl, 'review.json') + '`',
|
|
144
|
+
'> ⚠️ 坑 stage-review-path-prefix 双向实测:漏「<stage>-」前缀即错位(2026-10-10:marker 只有裸 runId,子代理按 runId 直写);**多加一层「review-」前缀同样错位**(2026-10-15 复发:手拼成 `<stage>-review-review-…`,空目录残渣+一轮纠正)。唯一可靠做法:整条路径直接粘贴。']
|
|
143
145
|
: []),
|
|
144
146
|
'',
|
|
145
147
|
'### 必填字段(validateStageReviewSchema 硬校验,缺任一即 schema 失败阻断)',
|
|
@@ -363,7 +365,7 @@ export function generateStageReviewRunId() {
|
|
|
363
365
|
*/
|
|
364
366
|
export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
|
|
365
367
|
const suffix = changeName ? `${stage}-${changeName}` : stage
|
|
366
|
-
return
|
|
368
|
+
return runtimeStatePath(runtimeRoot, `current-stage-review-run-id-${suffix}`)
|
|
367
369
|
}
|
|
368
370
|
|
|
369
371
|
/**
|
|
@@ -852,7 +854,7 @@ export function printStageReviewResult(result, context = {}) {
|
|
|
852
854
|
// fail-open:无 changeName / 无记录 / 读失败 → 零输出(旧口径)。
|
|
853
855
|
if (changeName && runtimeRoot) {
|
|
854
856
|
try {
|
|
855
|
-
const recPath =
|
|
857
|
+
const recPath = runtimeStatePath(runtimeRoot, `review-dispatch-${changeName}.json`)
|
|
856
858
|
if (existsSync(recPath)) {
|
|
857
859
|
const rec = JSON.parse(readFileSync(recPath, 'utf8'))
|
|
858
860
|
if (rec && typeof rec === 'object' && (rec.state === 'in-flight' || rec.state === 'dispatching')) {
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -58,7 +58,7 @@ export const definition = {
|
|
|
58
58
|
- **明显 small**(满足:单上下文可吞吐的纯执行类——改文案/修 bug/样式调整/配置微调,预计无 Wave 编排/上下文分片需求)→ 输出:「此变更规模较小,建议直接走轻量变更」+ 一句依据,给出可照抄建议命令(--input 引号内换行合法):
|
|
59
59
|
|
|
60
60
|
\`\`\`
|
|
61
|
-
sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
61
|
+
sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景
|
|
62
62
|
|
|
63
63
|
成功标准:
|
|
64
64
|
- <可验证标准>"
|
|
@@ -89,7 +89,9 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(
|
|
|
89
89
|
prompt: `### ⚡ 轻量跑判定(P0-3,2026-09-20 对撞实验驱动——需求已收敛来源走全仪探索是纯税)
|
|
90
90
|
**需求来源已收敛**(满足任一:--input 显式声明承接/已澄清/方案已定;fork 或关联会话已多轮澄清过本需求,你已能不问自答全部 P0 歧义;任务书/评审结论直接给定方案与边界)→ 本步可**轻量跑**:A-D 节按已收敛事实直书结论(歧义零清单、范围直裁、决策若已有则引用源会话不重问),一次性完成不再多轮追问。轻量跑不降质量底线:范围结论与决策落盘照常,仅省「明知故问」的交互轮。Grill(Step 7)与规范文件(Step 8)**不适用轻量跑、永不省**。
|
|
91
91
|
|
|
92
|
-
通过对话探索需求细节、分析原型(如有)、判断拆分/批量、并对歧义点做需求澄清 Grill。本步骤合并了原有的对话探索、原型分析、需求范围评估、需求澄清 Grill 四个环节——按下方 A→D 顺序按需执行。提问遵守「一次只问一个」(见 D
|
|
92
|
+
通过对话探索需求细节、分析原型(如有)、判断拆分/批量、并对歧义点做需求澄清 Grill。本步骤合并了原有的对话探索、原型分析、需求范围评估、需求澄清 Grill 四个环节——按下方 A→D 顺序按需执行。提问遵守「一次只问一个」(见 D 节追问策略——分层口径:P0 逐轮逐问、P1/P2 能自定的自定并标注、只有会改变产物的攒收尾一轮):必须问的问题逐个问,能查代码/文档解决的不问;不要把多个歧义打包成一屏清单一次抛给用户(用户记不住、答不全,反而漏掉关键约束);也不要为凑数把单个问题拖成多轮。
|
|
93
|
+
|
|
94
|
+
**规模重估(对话中如发现该升级,当场说)**:若对话或方案讨论中发现本次变更实际需要 Wave 并行编排、上下文分片或多阶段治理——超出当前档位(例:早期粗判的轻量面变成跨模块改造)——必须**当场向用户说明并给出升级建议**(如改走完整五阶段 / 带 \`--upgrade-thick\`),不静默按原档继续。档位仍由 CLI 客观定价(三轴)落值,你只能建议、不能自行升降档。
|
|
93
95
|
|
|
94
96
|
### A. 原型/设计图分析(如用户提供)
|
|
95
97
|
如果用户提供了截图、图片或 HTML 原型,先分析提取:
|
|
@@ -106,8 +108,18 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(
|
|
|
106
108
|
2. **提出问题后必须暂停等待用户回答**,不要替用户回答
|
|
107
109
|
3. 根据用户回答判断:信息够了 → 进入 C/D 或正常完成 / 需要追问 → 暂停等待下一个回答
|
|
108
110
|
4. 探索顺序(按需):目的 → 约束 → 边界 → 成功标准
|
|
109
|
-
5.
|
|
110
|
-
6.
|
|
111
|
+
5. **先挖为什么**:不只问「要什么」,要问出「为什么现在做、不做会怎样」(动机是后续替你补全没提的那一百个决定的依据);到本步出口仍说不清动机 → 不许进入「提出 2-3 种方案」,先把动机问出来
|
|
112
|
+
6. **实现方式的选择权归谁**:技术栈/平台/语言/库/存储介质这类「怎么做」的选择,问清哪些是用户有强意见要谈的、哪些是交给你选的;你选的按决策记录规则落盘并把 answer 标注「我的判断」(不是用户同意过的)
|
|
113
|
+
7. 多选题优于开放式问题;被问「你觉得呢/先猜一个」时给一个具体方案并标「我的判断」,不要空转追问
|
|
114
|
+
8. YAGNI — 砍掉不需要的功能
|
|
115
|
+
|
|
116
|
+
### B2. 分块回放(Playback——边理解边复述,与追问交替)
|
|
117
|
+
每理解到一块就先回放一块,别把回放攒到最后:
|
|
118
|
+
- 回放块 200-300 字,复述「我听到的是」——**凡是猜测都标为猜测,只有用户说过的才写成用户说的**
|
|
119
|
+
- 回放块结尾固定问一句:「哪里不对或漏了什么?」
|
|
120
|
+
- 回放与追问交替进行直到对话结束:回放 → 用户纠正 → 下一块追问 → 下一块回放;最后一块覆盖剩余部分
|
|
121
|
+
- 再次进入本步(\`--continue --answer\` 后)时,回放只复述新增/被纠正的理解,已确认且未被纠正的部分不重复回放(防上下文膨胀)
|
|
122
|
+
- 回放块放「**回放:**」分节、追问放「**追问:**」分节——两者分节标注,用户可分别纠正(回答的是上一轮追问时不影响回放块的语义)
|
|
111
123
|
|
|
112
124
|
### C. 需求范围评估(拆分/批量判断)
|
|
113
125
|
1. 根据分析结果判断复杂度。满足以下任意 2 条建议拆分:
|
|
@@ -155,19 +167,22 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(
|
|
|
155
167
|
- P1:影响用户场景、验收标准、错误处理、默认值
|
|
156
168
|
- P2:文案、展示细节、低风险交互偏好
|
|
157
169
|
|
|
158
|
-
|
|
170
|
+
**执行规则(分层——2026-10-15-brainstorm-interaction-refine FR-03)**:
|
|
159
171
|
- P1/P2 歧义 0-2 个且无 P0:不进入 Grill 追问,在后续设计中内联处理并记录依据
|
|
160
|
-
- P1/P2 歧义 >= 3
|
|
161
|
-
- 任意 P0
|
|
172
|
+
- P1/P2 歧义 >= 3 个:不逐个问——agent 能自行决定的**自行决定**(写进 decisions.md,answer 标注「我的判断」),剩下的攒成收尾一轮;只有「只有用户能答且答案会改变产物」的才问
|
|
173
|
+
- 任意 P0 歧义:必须澄清;逐轮逐问(一次只问一个),深度优先
|
|
174
|
+
- **收尾一轮(一次性问完)**:把「只有用户能答且答案会改变产物」的 P1/P2 项(含 Design Grill 回收的同类项)汇总成**一轮**问题,一次问完;问完即进入「提出 2-3 种方案」,不再追问、不做第二轮
|
|
175
|
+
- 可判定出口:进入「提出 2-3 种方案」前,P1/P2 未决项必须为 0,或已记录为「我的判断」落 decisions.md
|
|
162
176
|
- 不要问用户"要不要 Grill"。由你根据歧义风险决定;只在需要业务判断/取舍时等待用户回答
|
|
163
177
|
|
|
164
|
-
|
|
165
|
-
1.
|
|
166
|
-
2.
|
|
167
|
-
3.
|
|
168
|
-
4.
|
|
169
|
-
5.
|
|
170
|
-
6.
|
|
178
|
+
**追问策略**(进入澄清时,分层口径同上):
|
|
179
|
+
1. **P0 逐轮逐问**:P0 一次只问一个,深度优先
|
|
180
|
+
2. **P1/P2 能自定就自定**:能由源码/scan 文档/模块文档/既有决策确定,或能凭工程判断定且不改变产物的,直接定;按决策记录规则落 decisions.md 并在 answer 标注「我的判断」;只有真正需要用户判断/取舍的才等用户
|
|
181
|
+
3. **能查代码就不问**:问题可由源码、scan 文档、模块文档回答时,先查证并给出结论
|
|
182
|
+
4. **术语碰撞立即指出**:用户用词与 glossary/代码实体/模块文档冲突时,当场说明冲突并要求选择 canonical term
|
|
183
|
+
5. **模糊词精化**:把"账户/任务/状态/会话/执行"这类多义词拆成明确实体或状态
|
|
184
|
+
6. **场景压力测试**:用具体 case 逼出边界(失败重试、部分成功、历史数据、权限不足、并发修改、兼容旧配置)
|
|
185
|
+
7. **前提挑战优先**:如果现有设计或代码已有简单路径,先说明"可能不该新增"
|
|
171
186
|
|
|
172
187
|
**决策记录(增量落盘,防会话中断丢失)**:每解决一个有实现影响的问题(不要把闲聊都记录进去),**当场追加**写入 \`{SPEC_ROOT}/changes/<change-name>/decisions.md\`——不要只在对话里留草稿等「写设计文档」步统一落盘:会话随时可能中断,未落盘的决策无法跨会话恢复。落盘规则:
|
|
173
188
|
|
|
@@ -193,19 +208,33 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(
|
|
|
193
208
|
\`\`\`
|
|
194
209
|
|
|
195
210
|
- 幂等:写入前按 D-xxx@vN 查重,已存在的不重复追加;后续修正走新版本 D-001@v2 + supersedes(见 Design Grill 版本规则)
|
|
211
|
+
- **agent 自行决定的 P1/P2**(见执行规则分层):source 用 code(查证得来)或 docs(文档依据),answer 标注「我的判断:…」——不写成用户同意过的,用户事后可否决
|
|
212
|
+
- **「只有用户能答且改变产物」的项**:不落 pending 占位条目;先在收尾一轮问出答案,再按 source: user 落盘
|
|
196
213
|
|
|
197
214
|
### 铁律 — 不要自问自答 / 等待用户
|
|
198
215
|
- 这是人机协作步骤:提出问题后必须暂停等用户回答,**不要在输出里模拟用户回答然后说"需求已明确"**(命令由 CLI 在下方注入)
|
|
199
216
|
- 需要拆分或批量模式时:列出方案并暂停等用户确认
|
|
200
217
|
- 不需要追问、无歧义、需求已清晰时:可正常完成本步骤(不必强行 --wait)。但「正常完成」的前提是**输出一份可否决的确认清单**——在输出里明确列出「已确认需求点清单」(逐条),让用户能一眼看到并喊停;未与用户确认过任何需求点、纯凭自身判断的场景,不得仅凭「我觉得清晰」直接放行(这是把默认共识伪装成共识的逃逸口)
|
|
218
|
+
- **确认清单逐条带来源标记(两态)**:每条需求点标 \`[用户]\`(用户原话/回答得来)或 \`[推断]\`(agent 推断)——\`[推断]\` 即待用户点头项;未与用户确认过的推断点**不得混入已确认清单**(要么在收尾一轮问掉转 \`[用户]\`,要么显式留在「待确认」区)
|
|
201
219
|
- 2-3 轮问答即进入方案讨论;达到 maxWaitRounds=8 后必须总结已确认内容与剩余风险,不要无限追问
|
|
202
220
|
|
|
221
|
+
### 自欺逃逸口对照表
|
|
222
|
+
要输出「本步完成」「可以直接进方案讨论」之前,先对表自检一遍:
|
|
223
|
+
|
|
224
|
+
| 念头 | 现实 |
|
|
225
|
+
|---|---|
|
|
226
|
+
| "我觉得已经够清晰了" | 清晰不等于用户确认过——没确认过的点进清单必须标 \`[推断]\`,并留待收尾一轮问掉 |
|
|
227
|
+
| "这步这么简单,跳过吧" | 简单变复杂的路只有一条:跳过的步骤 |
|
|
228
|
+
| "用户说『听起来不错』" | 认可的是你展示过的东西——你没写出来的描述与你的推断,都不算批准 |
|
|
229
|
+
|
|
203
230
|
### 输出
|
|
204
231
|
需求理解摘要(用户确认的需求点列表)+ 拆分/批量结论(如适用)+ 本步新增落盘的 D-xxx@vN 清单 + 剩余风险(如有)
|
|
205
232
|
|
|
233
|
+
确认清单每条带来源标记 \`[用户]\` / \`[推断]\`;\`[推断]\` 项在收尾一轮问掉或显式留在「待确认」区——两者都不带 \`[用户]\` 标记时不得计入「已确认」。
|
|
234
|
+
|
|
206
235
|
### 注意
|
|
207
236
|
- 第一次进入此步骤时,按 A→D 顺序处理;有原型先分析,再探索,再判规模,再澄清
|
|
208
|
-
- 用户通过 \`--continue --answer "回答"\`
|
|
237
|
+
- 用户通过 \`--continue --answer "回答"\` 回答后,本步骤会再次执行,此时检查是否需要追问或可以结束;再次执行时输出只需增量汇报(**新回放块 + 新追问 + 新落盘决策**),已确认且未被纠正的内容不重复回放、已落盘内容不重复输出`,
|
|
209
238
|
outputHint: '需求理解与决策落盘清单',
|
|
210
239
|
optional: false
|
|
211
240
|
},
|
|
@@ -251,7 +280,8 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(
|
|
|
251
280
|
4. 分段设计含新机制时同上检索(固定动作):设计的分段内容引入新机制词(例词为举例非机制,不构成封闭清单)时,先跑 \`sillyspec knowledge search --query "<机制词>"\`,命中条目必读(尤其 status=rejected 的防复潮条目)再定稿设计
|
|
252
281
|
5. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth\`——CLI 已强制:新建/改名不带日期前缀直接 exit 2)
|
|
253
282
|
6. 按「HTML 原型分级」三选一必填:生成原型 / 登记复用既有原型 / 写出跳过原因(见下)
|
|
254
|
-
7.
|
|
283
|
+
7. 展示时附一节「**我替你做的决定**」(2026-10-15-brainstorm-interaction-refine FR-04):从 \`decisions.md\` 摘出 \`source\` 非 user 的条目(代码/文档查证得来的 + agent 自行决定的 P1/P2),逐条列 D-xxx@vN + 一句话决定 + 一句依据——让用户边读方案边核这些点,不要埋在决策文件里让他自己去翻;无此类条目时写「无(全部决定均来自你的选择)」
|
|
284
|
+
8. 暂停等待用户确认或修改意见
|
|
255
285
|
|
|
256
286
|
### HTML 原型分级(按改动程度,不是"是否碰前端文件")
|
|
257
287
|
判定对象是**改造后的界面相对现状的变化程度**:
|
|
@@ -272,10 +302,10 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(
|
|
|
272
302
|
- 纯后端逻辑/CLI/配置/文档,无任何界面变化
|
|
273
303
|
|
|
274
304
|
### 生成要求(定稿原型=真码产物,不是手绘图)
|
|
275
|
-
入口文件保存到 \`{SPEC_ROOT}/changes/<change-name>/prototype-<名称>.html\`(目录不存在则先创建;该命名是 execute
|
|
276
|
-
- **与实现同方言**:用项目真实前端栈构建——组件库/设计 token/样式从项目源码或 node_modules 真实引入(例:React 项目挂真实组件而非仿制标签、Vue 项目用真实 SFC
|
|
277
|
-
- **可复跑**:能被本项目工具链跑起来(dev server / 组件文档页 / 构建产物均可,入口 HTML 从本地 node_modules 引真实库也行)——先查 scan
|
|
278
|
-
-
|
|
305
|
+
入口文件保存到 \`{SPEC_ROOT}/changes/<change-name>/prototype-<名称>.html\`(目录不存在则先创建;该命名是 execute 注入契约,不改)。**本仓 local.yaml ui_prototype 段登记的形态约定优先于本节默认阶梯**(form: real-code | single-html;豁免须带 ruling 用户裁决留痕;多仓变更按触达子仓 repos override 优先——方案对比步须知会注入本仓登记态)。真码五要素:
|
|
306
|
+
- **与实现同方言**:用项目真实前端栈构建——组件库/设计 token/样式从项目源码或 node_modules 真实引入(例:React 项目挂真实组件而非仿制标签、Vue 项目用真实 SFC;例词非封闭清单),不手搓仿制样式;**把 token 数值抄进自定义 CSS 变量=复刻≠引入,仍属手搓仿制**(2026-10-15 knowledge-search 实证误读)
|
|
307
|
+
- **可复跑**:能被本项目工具链跑起来(dev server / 组件文档页 / 构建产物均可,入口 HTML 从本地 node_modules 引真实库也行)——先查 scan 文档与现有前端代码就近发现项目原型管线,已有则复用;**历史原型先例同为单文件不构成管线存在**(51 个手绘先例是惯例债不是证据);查无真码管线时**先单开一个轻量变更搭最小入口**(bootstrap 与业务变更分离),本变更按登记形态或降级声明执行
|
|
308
|
+
- **高保真=组件即实现原料,不是「看起来像」**:布局/组件/交互按真实效果呈现,execute 阶段直接对照搬组件,不经过"看图重新翻译";**生成档输出必带素材来源声明**(真值 token 源文件路径 / 组件来源 / 入口位置——缺声明=无源可查,2026-10-15 server-logs-panel 实证零声明)
|
|
279
309
|
- **手绘单文件(自造样式)仅限一次性粗选对比,不得作为定稿原型**(frontend-apple-style 实证:手绘定稿后每条意见都要反向翻译回真实代码);粗选定了方向,升级真码再收口
|
|
280
310
|
- **例外与降级**:项目本身即单文件 HTML 方言(纯静态站)时单文件即真码,不适用手绘限定;无法同方言真码化(如目标平台无 HTML 运行面)时显式声明降级与理由请用户裁决留痕——静默降级会被收口「UI 视觉证据」探针拦下
|
|
281
311
|
|
|
@@ -284,7 +314,7 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(
|
|
|
284
314
|
- 原型生成、复用登记或跳过声明必须在暂停**之前**完成(复用登记=原型文件已按 \`prototype-<名称>.html\` 落在变更目录)——暂停是本步骤最后动作,用户确认后直接进入写 design.md,不会回头补
|
|
285
315
|
|
|
286
316
|
### 输出
|
|
287
|
-
完整设计方案 + 变更名 + 原型文件路径 或 复用声明(来源+覆盖面+登记后路径)或 跳过原因(三选一必填;跳过时写明分级依据,用户可当场否决要求补)
|
|
317
|
+
完整设计方案 + 变更名 + 原型文件路径 或 复用声明(来源+覆盖面+登记后路径)或 跳过原因(三选一必填;跳过时写明分级依据,用户可当场否决要求补) + **「我替你做的决定」节**(source≠user 的 D-xxx@vN 逐条;无则写明)
|
|
288
318
|
|
|
289
319
|
### 注意
|
|
290
320
|
- 不要一次输出大段文字,按模块/Phase 分段
|
|
@@ -478,6 +508,20 @@ ${REVIEW_CHECKLISTS.brainstorm.slice(3).map((item) => '- ' + item).join('\n')}
|
|
|
478
508
|
- P0/P1 未决项必须进入 Unresolved Blockers,不能带着进入 plan。
|
|
479
509
|
4. 用户回答后,更新 design.md 和 decisions.md;如果推翻旧决策,新增版本 D-xxx@v2,而不是覆盖 D-xxx@v1。
|
|
480
510
|
|
|
511
|
+
### 实现者视角追问(builder check——与上方交叉审查并行执行,不替代它)
|
|
512
|
+
交叉审查问「文档自身矛盾吗」;本节问「照这份文档,我能建出它吗」。两问正交,都要做(tier=self 时你依次自问两遍并分节输出):
|
|
513
|
+
- **tier=independent(S2/S3)**:把下列四要素追加进审查子代理的派发 prompt(作为同一子代理的第二项任务,不额外计请求预算):
|
|
514
|
+
- 你是一位有才华的 [领域] 工程师,被交予本设计文档,要据此制定实现计划,**你联系不到写文档的人**
|
|
515
|
+
- 只报告你「必须被回答、否则得靠猜、而猜错会出事」的问题;每条附「为什么答案会改变产物」,按重要性排序
|
|
516
|
+
- 跳过:文档已显式留给实现者的部分、你自己能合理决定的、措辞挑刺
|
|
517
|
+
- 没有重要问题 → 回答 \`nothing important left\`
|
|
518
|
+
- 写成 review.json 的 \`reviewerNotes\` 一节:\`## 实现者视角追问\` + 逐条问题与理由(**不新增 schema 字段**——去向钉在现有 reviewerNotes,Stage Review Gate 照常读)
|
|
519
|
+
- **tier=self(S0/S1)**:以实现者身份快读一遍 design.md,同样三个跳过条件,输出「实现者视角追问」小节(可空 = nothing important left)
|
|
520
|
+
- **回收分三堆**(两档都适用):
|
|
521
|
+
1. **文档/对话已答** → 把答案补进 design.md 对应章节,不改决策
|
|
522
|
+
2. **minor(不改变产物的猜测点)** → 你自己定,按决策记录格式落 decisions.md 并标注「我的判断」
|
|
523
|
+
3. **只有用户能答且会改变产物** → 进「对话式探索」的收尾一轮(一次性问完,不做第二轮);无法回退到前一步时挂 Unresolved Blockers
|
|
524
|
+
|
|
481
525
|
### decisions.md 版本规则
|
|
482
526
|
\`\`\`markdown
|
|
483
527
|
## D-001@v2: 缓存异常时的 fallback 语义
|
|
@@ -514,6 +558,12 @@ status: passed | needs-user-input | blocked | skipped
|
|
|
514
558
|
| needs_thinking | N | 需要用户判断 |
|
|
515
559
|
| unresolved | N | 真正设计漏洞 |
|
|
516
560
|
|
|
561
|
+
## 实现者视角追问
|
|
562
|
+
(四要素口径;无重要问题写 "nothing important left")
|
|
563
|
+
| ID | 问题 | 为什么答案会改变产物 | 分堆(已答/我的判断/需用户) |
|
|
564
|
+
|---|---|---|---|
|
|
565
|
+
| B-001 | ... | ... | ... |
|
|
566
|
+
|
|
517
567
|
## Unresolved Blockers
|
|
518
568
|
| ID | priority | 问题 | 阻塞原因 | 下一步 |
|
|
519
569
|
|---|---|---|---|---|
|
|
@@ -521,6 +571,7 @@ status: passed | needs-user-input | blocked | skipped
|
|
|
521
571
|
|
|
522
572
|
### 铁律 — 等待用户
|
|
523
573
|
- 发现 P0/P1 结构性矛盾且需要用户判断时,暂停等用户(命令由 CLI 在下方注入)
|
|
574
|
+
- 「只有用户能答且会改变产物」的问题(本步交叉审查 + 实现者视角追问合计)攒成**一轮**问完即止,不做第二轮
|
|
524
575
|
- 用户显式跳过时必须在 decisions.md 记录 accepted risk;P0/P1 skip 仍必须写入 Unresolved Blockers
|
|
525
576
|
- 完成前必须确认没有 P0/P1 unresolved blocker,否则不能进入 plan。`,
|
|
526
577
|
outputHint: 'Design Grill 交叉审查结果',
|
|
@@ -88,9 +88,14 @@ export function parseAllowedPaths(content) {
|
|
|
88
88
|
// 剥首尾成对反引号(坑7 家族:`src/a.js` 整项以反引号开头时 js-yaml/正则路径带 ` 匹配失败报未覆盖;
|
|
89
89
|
// 与 change-list.js 的 cell 归一化同语义——剥两端包裹引号,不动路径内部字符)
|
|
90
90
|
return blockMatch[1].match(/[ \t]*-[ \t]+(.+)/g)?.map(s => {
|
|
91
|
-
|
|
91
|
+
let raw = s.replace(/^[ \t]*-[ \t]+/, '').trim()
|
|
92
|
+
// 双横线归一化(2026-10-15-zcode-engine-appserver 归档期实证:手编辑残留 "- - path"
|
|
93
|
+
// 嵌套子弹,剥一层外子弹后仍带 "- " 前缀 → 幽灵路径进 allow 面 → apply 越权误拦)。
|
|
94
|
+
// 循环剥除连续子弹前缀("- " / "- " 等任意空白形态)。
|
|
95
|
+
while (/^-[ \t]+/.test(raw)) raw = raw.replace(/^-+[ \t]+/, '').trim()
|
|
92
96
|
return raw.replace(/^`([^`]*)`$/, '$1').replace(/^['"]|['"]$/g, '').trim()
|
|
93
|
-
}).filter(Boolean)
|
|
97
|
+
}).filter(Boolean)
|
|
98
|
+
.filter((v, i, arr) => arr.indexOf(v) === i) // 去重(同卡重复条目不产生重复 allow 项)
|
|
94
99
|
}
|
|
95
100
|
return []
|
|
96
101
|
}
|
|
@@ -2308,6 +2313,12 @@ async function executePlanPostcheckRun(context, { autoAdoptBudget = 0 } = {}) {
|
|
|
2308
2313
|
const miGen = await generatePlanModuleImpactFirstVersion(changeDir, cwd, specRoot)
|
|
2309
2314
|
if (miGen.status === 'generated') {
|
|
2310
2315
|
console.log(` ✅ module-impact.md 首版已由 CLI 自动生成(归类 ${miGen.matchedCount} 个文件,未匹配 ${miGen.unmatchedCount} 个;影响类型列由 execute/verify 回填)`)
|
|
2316
|
+
} else if (miGen.status === 'skipped' && (miGen.reason === 'no-module-map' || miGen.reason === 'no-declared-files')) {
|
|
2317
|
+
// 坑 plan-module-impact-autogen-missing(2026-10-14 实证:指引说勿手写等 CLI 生成,
|
|
2318
|
+
// 无 module-map/无声明清单时静默 skip → gate 报缺失空转两轮)。非 small 变更在此显式
|
|
2319
|
+
// 告知生成前提缺失 + 逃生口径,agent 不再靠 gate 撞墙才知道。
|
|
2320
|
+
console.warn(` ⚠️ module-impact.md 首版未能自动生成(${miGen.reason === 'no-module-map' ? '本仓无 .sillyspec/docs/<项目>/modules/_module-map.yaml 模块图' : 'design.md 文件变更清单 main 段无声明文件'})——gate 仍要求该文件在场`)
|
|
2321
|
+
console.warn(' 逃生口径:按 gate 模板手写骨架(章节标题逐字「## 模块影响矩阵」「## 未匹配文件」+「## 更新结果」表骨架 pending,execute/verify 回填),或先跑 sillyspec run scan 建模块图')
|
|
2311
2322
|
}
|
|
2312
2323
|
|
|
2313
2324
|
// ── 1e. 阶段完成产物校验(plan stage contract)──
|