sillyspec 3.32.8 → 3.32.10
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 +2 -2
- package/.claude/skills/sillyspec-execute/SKILL.md +2 -2
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +2 -2
- package/AGENTS.md +5 -4
- package/assets/command-cards/flow.md +9 -4
- package/package.json +1 -1
- package/src/change-list.js +13 -0
- package/src/commit-guard.js +70 -2
- package/src/commit-suggest.js +45 -4
- package/src/config-schema.js +14 -0
- package/src/cross-repo-reconcile.js +54 -1
- package/src/decision-distill.js +77 -21
- package/src/decision-renumber.js +157 -0
- package/src/deleted-ledger.js +75 -0
- package/src/design-facts.js +5 -3
- package/src/dispatch/backends/local-agent.js +1 -1
- package/src/dispatch/backends/sillyhub-mcp.js +1 -1
- package/src/docs-check.js +30 -15
- package/src/docs-debt.js +3 -2
- package/src/doctor-diagnostics.js +3 -1
- package/src/flow-draft.js +24 -2
- package/src/flow-parity.js +32 -2
- package/src/flow.js +427 -69
- package/src/fr-index.js +15 -1
- package/src/hooks/worktree-guard.js +3 -3
- package/src/index.js +22 -10
- package/src/knowledge-digest.js +23 -6
- package/src/knowledge-graph.js +92 -14
- 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/progress/change-registry.js +7 -0
- package/src/progress.js +67 -0
- package/src/quicklog.js +6 -1
- package/src/redomain.js +27 -0
- package/src/route-hindsight.js +34 -2
- package/src/run/archive-distill.js +3 -0
- package/src/run/command.js +40 -44
- package/src/run/complete-handlers.js +8 -3
- package/src/run/complete.js +129 -63
- package/src/run/gates.js +94 -90
- package/src/run/prompt.js +13 -3
- package/src/run/shared.js +19 -0
- package/src/run/stage.js +1 -1
- package/src/scope-audit.js +2 -2
- package/src/sentinel-assertions.js +43 -1
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +44 -11
- package/src/stage-templates.js +2 -0
- package/src/stages/brainstorm.js +24 -12
- package/src/stages/execute.js +1 -1
- package/src/stages/explore.js +3 -5
- package/src/stages/knowledge.js +56 -5
- package/src/sync.js +98 -2
- package/src/task-tick.js +12 -0
- package/src/timeline.js +60 -19
- package/src/verify-postcheck.js +83 -6
- package/src/verify-probes.js +61 -18
- package/templates/agents-instruction.md +5 -4
- package/templates/prompts/testcase-design.md +5 -0
- package/src/stages/brainstorm-auto.js +0 -286
|
@@ -165,6 +165,48 @@ export function detectBatchCheckCadence(events) {
|
|
|
165
165
|
return worst;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* 侧车勾选突发检测(2026-10-14-tick-burst-sidecar-gate,纯函数):task-ticks.jsonl 的毫秒级
|
|
170
|
+
* 时戳按相邻间隔窗口分簇——CLI 逐格快连点(收口一把点的官方路径形态)在事件流里是单格精确跳,
|
|
171
|
+
* 10-07 采样去重后对 detectBatchCheckCadence 不可见(近期实证:6 格 2 秒内/4 格 1 秒内零检出);
|
|
172
|
+
* 侧车是 CLI tick 的事实源(随归档进 git、不受观测盲窗影响),收口节奏门回退消费本函数——
|
|
173
|
+
* CLI 与直接 Edit 两条路径同判。
|
|
174
|
+
* 语义:tickLog 同任务取最早(幂等重勾防御)+ 坏行跳过(task 形态/时戳有限性)+ 排序后按
|
|
175
|
+
* gap<=windowMs(缺省 60s,opts.windowMs 覆写)分簇;最大簇 size>=2 → 返回与事件流同形的
|
|
176
|
+
* batchTick 记录。from/to 是 CLI 勾序计数(Edit 翻格不记侧车,与事件流 checked N→M 计数空间
|
|
177
|
+
* 不同源)——resolveBatchTickAction 只消费跳幅(to-from)与 ts,语义等价;detail 带「侧车突发」
|
|
178
|
+
* 标注防口径混淆。fail-open:不足两格/空输入/全坏行 → null。
|
|
179
|
+
*/
|
|
180
|
+
export function detectTickBurst(tickLog, opts = {}) {
|
|
181
|
+
const windowMs = Number.isFinite(opts.windowMs) ? opts.windowMs : 60_000;
|
|
182
|
+
const byTask = new Map();
|
|
183
|
+
for (const r of Array.isArray(tickLog) ? tickLog : []) {
|
|
184
|
+
const task = String((r && r.task) || '').trim();
|
|
185
|
+
const ts = Number(r && r.ts);
|
|
186
|
+
if (!/^task-\d+$/.test(task) || !Number.isFinite(ts)) continue;
|
|
187
|
+
const prev = byTask.get(task);
|
|
188
|
+
if (prev === undefined || ts < prev) byTask.set(task, ts);
|
|
189
|
+
}
|
|
190
|
+
const ticks = [...byTask.values()].sort((a, b) => a - b);
|
|
191
|
+
if (ticks.length < 2) return null;
|
|
192
|
+
let worst = null;
|
|
193
|
+
let start = 0;
|
|
194
|
+
for (let i = 1; i <= ticks.length; i++) {
|
|
195
|
+
if (i < ticks.length && ticks[i] - ticks[i - 1] <= windowMs) continue;
|
|
196
|
+
const size = i - start;
|
|
197
|
+
if (size >= 2 && (!worst || size > worst.size)) {
|
|
198
|
+
worst = { size, from: start, to: i, ts: ticks[start], spanMs: ticks[i - 1] - ticks[start] };
|
|
199
|
+
}
|
|
200
|
+
start = i;
|
|
201
|
+
}
|
|
202
|
+
if (!worst) return null;
|
|
203
|
+
return {
|
|
204
|
+
from: worst.from, to: worst.to, size: worst.size, spanMs: worst.spanMs, ts: worst.ts,
|
|
205
|
+
source: 'tick-burst',
|
|
206
|
+
detail: `checked ${worst.from}→${worst.to}(task tick 侧车突发 ${worst.size} 格)`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
168
210
|
/**
|
|
169
211
|
* 单拍勾选门决策(2026-09-29-batch-tick-gate,纯函数):检出单拍跳(batchTick 非 null)
|
|
170
212
|
* 时的收口动作裁决。镜像豁免分支退役(2026-10-07-thin-tasks-v3——任务面统一为 agent
|
|
@@ -193,4 +235,4 @@ export function resolveBatchTickAction({ batchTick, nonMirrorCount = null, allow
|
|
|
193
235
|
return { action: 'reject', reason: 'agent-batch-tick' };
|
|
194
236
|
}
|
|
195
237
|
|
|
196
|
-
export default { detectFakeCheckCompletion, detectBatchCheckCadence };
|
|
238
|
+
export default { detectFakeCheckCompletion, detectBatchCheckCadence, detectTickBurst };
|
package/src/stage-contract.js
CHANGED
|
@@ -1059,8 +1059,12 @@ const API_COVERAGE_HEADING_RE = /^## 接口验证覆盖矩阵/m
|
|
|
1059
1059
|
const API_ENDPOINT_CELL_RE = /(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/[^\s|]+)/
|
|
1060
1060
|
/** 声明占位行(D-005 零解析降级侧,骨架「本变更接口面:N 端点(agent 声明)」字面同源) */
|
|
1061
1061
|
const API_DECLARED_ROW_RE = /本变更接口面[::]\s*(\d+)\s*端点/
|
|
1062
|
-
/** design接口表# 锚点提取(解析级校验用——防空指,Grill #10:非物理行号,METHOD /path
|
|
1063
|
-
|
|
1062
|
+
/** design接口表# 锚点提取(解析级校验用——防空指,Grill #10:非物理行号,METHOD /path 形态)。
|
|
1063
|
+
* 字符类左右括号对称排除(坑 verify-close-gate-staggered-exposure,2026-10-15 消费仓 node 直调
|
|
1064
|
+
* 实证:原只排右括号)不排左括号(——`GET /api/x(注记)` 的路径吞成 `/api/x(注记`,与解析面
|
|
1065
|
+
* 永不匹配 → 恒空指,报错文案却指向「design.md 未命中」误导排查方向)。中文(英文、全/半角
|
|
1066
|
+
* 四形与既有分隔符一并终止捕获。 */
|
|
1067
|
+
const API_ANCHOR_DESIGN_API_RE = /design接口表#\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/[^\s|,。;;()()]+)/g
|
|
1064
1068
|
/** 探索行标记(骨架文法注释字面同源:证据列含 [探索]) */
|
|
1065
1069
|
const API_EXPLORATION_MARK_RE = /\[探索\]/
|
|
1066
1070
|
|
|
@@ -1261,9 +1265,16 @@ export function judgeApiCoverageMatrix(args) {
|
|
|
1261
1265
|
}
|
|
1262
1266
|
}
|
|
1263
1267
|
if (anchorViolations.length > 0) {
|
|
1268
|
+
// 空指诊断(坑 verify-close-gate-staggered-exposure 建议③,2026-10-15 实证:报错只说「未命中
|
|
1269
|
+
// 解析面」不展示解析面实际内容——「锚点吞字」与「design 缺表」分不清,agent 被引去改 design
|
|
1270
|
+
// 文档白做功。附上 parseSet 实际端点集一眼对齐)。
|
|
1271
|
+
const parseFaceDesc = parseSet.size > 0
|
|
1272
|
+
? [...parseSet].join(';')
|
|
1273
|
+
: '(解析面为空——design.md 接口段未解析到端点,先表格化接口段再跑 verify-probes --init)'
|
|
1264
1274
|
errors.push(
|
|
1265
1275
|
`接口验证覆盖矩阵有 ${anchorViolations.length} 项证据锚点缺失/空指` +
|
|
1266
1276
|
`(covered/partial 证据须含锚点五形态之一,可复制样例:design接口表#POST /api/xx(# 后必须 METHOD /path 且命中 design 接口段解析面,仅表名/行号不计)/ 权限矩阵[admin×读] / 契约表@任务卡字段清单 / DDL@users.id / 载荷@e2e_body.json;covered-service 证据须含测试锚点三形态之一:\`.test.\` 文件名 / file:line / 反引号包裹的测试名):${anchorViolations.join(';')}。` +
|
|
1277
|
+
`design 接口段实际解析面端点集:${parseFaceDesc}——design接口表# 锚点路径须与之逐字一致。` +
|
|
1267
1278
|
`修复:在证据列补真实锚点(design接口表# 锚点须与 design.md 接口段端点一致,防编造端点)。`
|
|
1268
1279
|
)
|
|
1269
1280
|
}
|
|
@@ -1419,6 +1430,9 @@ function extractPermissionMatrixText(designMd) {
|
|
|
1419
1430
|
* - tasks/ 不存在(quick 会话/旧变更)或 verify-result.md 未落盘(中间步骤)→ no-op
|
|
1420
1431
|
* - factsExpected=false(存量未跑管线,resolveFactsExpected 同源口径)→ no-op 零行为
|
|
1421
1432
|
* - verify-facts.json(apiFace/consumerHints/handover,缺失容 null——fail-closed 归纯函数)
|
|
1433
|
+
* - context.apiFaceLive(可选,2026-10-14-gate-verify-apiface-preflight):gate/derive 预检侧
|
|
1434
|
+
* 以同一解析器实况解析 design.md 注入(与 --done backfill 产物同形同源,零落盘)——在场时
|
|
1435
|
+
* 覆写 facts.apiFace 作解析产出集(过期 facts 不得赢),未注入行为与现状完全一致
|
|
1422
1436
|
* - 矩阵段 MD 槽解析(X-05 防篡改锚点:锚定当前文档实态)
|
|
1423
1437
|
* - resolveChangeRisk 声明面判级(design 文件清单 × blast 声明 + explicit,task-03)
|
|
1424
1438
|
* - design.md 权限矩阵段提取(fs 只读,壳非纯函数)
|
|
@@ -1441,9 +1455,18 @@ export function validateApiCoverageMatrix(cwd, changeName, context = {}) {
|
|
|
1441
1455
|
// 2026-09-19-ceremony-pricing-five-cuts task-03:声明面判级(design 文件清单 × blast 声明 +
|
|
1442
1456
|
// explicit;level 为五级词兼容字段——evidenceRequired → integration-critical)
|
|
1443
1457
|
const riskProfile = resolveChangeRiskForChangeDir(changeDir, context.specRoot || specRoot || join(cwd, '.sillyspec'))
|
|
1458
|
+
// gate/derive 预检注入(2026-10-14-gate-verify-apiface-preflight FR-01/FR-02):facts.apiFace
|
|
1459
|
+
// 唯一生产者是 run verify --done 收尾前置 backfill(run/gates.js),gate 路径不回填且 --init
|
|
1460
|
+
// 刻意不落键 → 预检 parseSet 空/旧 → design接口表# 锚点恒空指(平台 6 轮 gate 实证)。调用方
|
|
1461
|
+
// (machine-interface runGate/derive)以同一解析器实况解析 design.md 经 context.apiFaceLive
|
|
1462
|
+
// 传入(本模块零 import verify-probes 纪律:注入不做函数调用)——与 --done backfill 产物同形
|
|
1463
|
+
// 同源,在场时必须覆写 facts(过期 facts 不得赢),未注入时行为与现状完全一致。
|
|
1464
|
+
const apiFace = context.apiFaceLive && typeof context.apiFaceLive === 'object'
|
|
1465
|
+
? context.apiFaceLive
|
|
1466
|
+
: (facts && facts.apiFace && typeof facts.apiFace === 'object' ? facts.apiFace : null)
|
|
1444
1467
|
return judgeApiCoverageMatrix({
|
|
1445
1468
|
matrix: extractApiCoverageMatrixSlots(readFileSync(verifyResultPath, 'utf8')),
|
|
1446
|
-
apiFace
|
|
1469
|
+
apiFace,
|
|
1447
1470
|
consumerHints: facts && facts.consumerHints && typeof facts.consumerHints === 'object' ? facts.consumerHints : null,
|
|
1448
1471
|
facts,
|
|
1449
1472
|
factsExpected: true,
|
package/src/stage-review.js
CHANGED
|
@@ -366,6 +366,31 @@ export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
|
|
|
366
366
|
return join(runtimeRoot, `current-stage-review-run-id-${suffix}`)
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
/**
|
|
370
|
+
* stage review 目录读侧归一(坑 stage-review-dir-naming-mismatch,2026-10-15 消费仓三阶段
|
|
371
|
+
* 连环实证):规范目录 = `<stage>-<runId>`(runId 自带 review- 前缀,拼出来恰一次 review 字样)。
|
|
372
|
+
* skill 文档字面 `<stage>-review-<run-id>` + 「run-id 必须 review- 前缀」诱导出两类错位目录:
|
|
373
|
+
* ① 裸 runId 目录(agent 拿 marker 值当目录名):<runId>/
|
|
374
|
+
* ② 双前缀目录(字面拼装):<stage>-review-<runId>/
|
|
375
|
+
* 本助手按 规范 → ②双前缀 → ①裸 runId 顺序探测 review.json 在场性,返回首个命中目录;全不在
|
|
376
|
+
* 则返回规范目录(保持既有「期望路径」报错文案语义)。只读不搬文件——写入侧恒规范路径,读侧
|
|
377
|
+
* 容错让 gate / refresh-hash / autoRefresh 对错位文件天然可用。
|
|
378
|
+
* @param {string} runtimeRoot - .runtime 绝对路径
|
|
379
|
+
* @param {string} stage - brainstorm|plan|execute
|
|
380
|
+
* @param {string} reviewRunId - run id(review-<ts>)
|
|
381
|
+
* @returns {string} review 目录绝对路径(容错命中或规范缺省)
|
|
382
|
+
*/
|
|
383
|
+
export function resolveStageReviewDir(runtimeRoot, stage, reviewRunId) {
|
|
384
|
+
const base = join(runtimeRoot, 'stage-reviews')
|
|
385
|
+
const canonical = join(base, `${stage}-${reviewRunId}`)
|
|
386
|
+
const doublePrefix = join(base, `${stage}-review-${reviewRunId}`)
|
|
387
|
+
const bare = join(base, reviewRunId)
|
|
388
|
+
for (const dir of [canonical, doublePrefix, bare]) {
|
|
389
|
+
if (existsSync(join(dir, 'review.json'))) return dir
|
|
390
|
+
}
|
|
391
|
+
return canonical
|
|
392
|
+
}
|
|
393
|
+
|
|
369
394
|
/**
|
|
370
395
|
* 获取最新的 stage review run id
|
|
371
396
|
*
|
|
@@ -395,33 +420,37 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
|
|
|
395
420
|
}
|
|
396
421
|
} catch {}
|
|
397
422
|
|
|
398
|
-
// fallback:扫描 stage-reviews
|
|
423
|
+
// fallback:扫描 stage-reviews/ 目录取字典序最新(向后兼容无 marker 旧数据)。候选两形:
|
|
424
|
+
// 规范/双前缀 `<stage>-review-*` + 裸 runId `review-*`(坑 stage-review-dir-naming-mismatch
|
|
425
|
+
// 错位目录——裸目录不编码 stage,须 reviewType 归属校验防跨段误认)
|
|
399
426
|
const dir = join(runtimeRoot, 'stage-reviews')
|
|
400
427
|
if (!existsSync(dir)) return null
|
|
401
428
|
try {
|
|
402
429
|
const entries = readdirSync(dir, { withFileTypes: true })
|
|
403
|
-
.filter(e => e.isDirectory() && e.name.startsWith(`${stage}-review-`))
|
|
404
|
-
.map(e => e.name)
|
|
405
|
-
.sort()
|
|
430
|
+
.filter(e => e.isDirectory() && (e.name.startsWith(`${stage}-review-`) || e.name.startsWith('review-')))
|
|
431
|
+
.map(e => ({ name: e.name, runId: e.name.startsWith(`${stage}-`) ? e.name.slice(stage.length + 1) : e.name }))
|
|
432
|
+
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
|
406
433
|
.reverse()
|
|
407
434
|
if (!entries[0]) return null
|
|
408
435
|
// cross-change 防护(execute 复盘 b1):changeName 提供时按 review.json 的 reviewedFiles[0] 归属过滤。
|
|
409
436
|
// 契约 reviewedFiles[0] = `changes/<change>/<mainDoc>`(renderReviewJsonContract),据此排除他变更的 review,
|
|
410
437
|
// 否则批量完成 marker 缺失时可能读到 proxy 等其他变更的 acceptance review 报错误导。
|
|
438
|
+
// 裸目录另加 reviewType 校验(STAGE_REVIEW_TYPE 段专属,防他 stage 的错位文件串台)。
|
|
411
439
|
if (changeName) {
|
|
412
440
|
const matching = entries.find(e => {
|
|
413
441
|
try {
|
|
414
|
-
const rj = JSON.parse(readFileSync(join(dir, e, 'review.json'), 'utf8'))
|
|
442
|
+
const rj = JSON.parse(readFileSync(join(dir, e.name, 'review.json'), 'utf8'))
|
|
443
|
+
if (!e.name.startsWith(`${stage}-`) && rj.reviewType && rj.reviewType !== STAGE_REVIEW_TYPE[stage]) return false
|
|
415
444
|
const r0 = Array.isArray(rj.reviewedFiles) ? String(rj.reviewedFiles[0]) : ''
|
|
416
445
|
return r0.includes(`changes/${changeName}/`)
|
|
417
446
|
} catch { return false }
|
|
418
447
|
})
|
|
419
|
-
if (matching) return matching.
|
|
448
|
+
if (matching) return matching.runId
|
|
420
449
|
console.warn(`⚠️ 无 ${stage} 的 review marker,且 stage-reviews/ 下无归属变更 ${changeName} 的 review(reviewedFiles 应含 changes/${changeName}/);为避免跨变更串台不再取最新,返回 null(fail-closed,由调用方报缺 review.json)`)
|
|
421
450
|
return null
|
|
422
451
|
}
|
|
423
|
-
//
|
|
424
|
-
return entries[0].
|
|
452
|
+
// 规范形 "plan-review-2026-..." 剥 `${stage}-` 前缀得 runId;裸形目录名即 runId
|
|
453
|
+
return entries[0].runId
|
|
425
454
|
} catch {
|
|
426
455
|
return null
|
|
427
456
|
}
|
|
@@ -613,7 +642,8 @@ export function validateStageReview(opts) {
|
|
|
613
642
|
const errors = []
|
|
614
643
|
const warnings = []
|
|
615
644
|
|
|
616
|
-
|
|
645
|
+
// 读侧目录归一(坑 stage-review-dir-naming-mismatch):容错命中裸 runId / 双前缀错位目录
|
|
646
|
+
const reviewDir = resolveStageReviewDir(runtimeRoot, stage, reviewRunId)
|
|
617
647
|
const reviewPath = join(reviewDir, 'review.json')
|
|
618
648
|
|
|
619
649
|
if (!reviewRunId || !existsSync(reviewPath)) {
|
|
@@ -705,7 +735,7 @@ export function validateStageReviewWithAutoRefresh(opts) {
|
|
|
705
735
|
&& result.errors.every(e => String(e).includes('docHash 与主审查文档'))
|
|
706
736
|
if (!mismatchOnly) return { result, autoRefreshed: false }
|
|
707
737
|
|
|
708
|
-
const reviewDir =
|
|
738
|
+
const reviewDir = resolveStageReviewDir(opts.runtimeRoot, opts.stage, opts.reviewRunId)
|
|
709
739
|
const reviewPath = join(reviewDir, 'review.json')
|
|
710
740
|
// 定位主文档并重算 hash(与 verifyStageReviewDocHash 同法:reviewedFiles[0] × searchDirs)
|
|
711
741
|
const primaryRel = Array.isArray(result.review.reviewedFiles) ? result.review.reviewedFiles[0] : null
|
|
@@ -922,8 +952,11 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
922
952
|
if (refreshHash) {
|
|
923
953
|
if (fromFile) throw new Error('register-stage-review: --refresh-hash 与 --from 互斥(refresh 就地更新既有 review,adopt 引入新文件)')
|
|
924
954
|
const existingRunId = getLatestStageReviewRunId(runtimeRoot, stage, changeName)
|
|
955
|
+
// 读侧目录归一(坑 stage-review-dir-naming-mismatch):--refresh-hash 按 marker 取 runId 后
|
|
956
|
+
// 经 resolveStageReviewDir 容错定位——review.json 落在裸 runId / 双前缀错位目录也能刷新,
|
|
957
|
+
// 不再恒报「无对应 run」。
|
|
925
958
|
const existingPath = existingRunId
|
|
926
|
-
? join(runtimeRoot,
|
|
959
|
+
? join(resolveStageReviewDir(runtimeRoot, stage, existingRunId), 'review.json')
|
|
927
960
|
: null
|
|
928
961
|
if (!existingPath || !existsSync(existingPath)) {
|
|
929
962
|
throw new Error(`register-stage-review: --refresh-hash 找不到 ${stage} 的既有 review.json(marker ${existingRunId || '无'} 无对应 run)——先去掉 --refresh-hash 生成骨架,或派审查子代理产出 review`)
|
package/src/stage-templates.js
CHANGED
|
@@ -120,6 +120,8 @@ created_at: <now-datetime>
|
|
|
120
120
|
- normalized_requirement: 可测试的约束
|
|
121
121
|
- impacts: [FR-01, task-01, verify-01]
|
|
122
122
|
- evidence: 用户回答轮次或代码/文档路径
|
|
123
|
+
- 否决理由: <一句话核心劣势>(status=rejected 时必填——distill 入库前置要求,缺失则该条目不进知识库防复潮通道)
|
|
124
|
+
- 复潮条件: <何种信号下可重议本方案>(status=rejected 时必填)
|
|
123
125
|
\`\`\`
|
|
124
126
|
|
|
125
127
|
> 可选字段(按需另加,旧格式决策缺这些字段不受影响):**锚点**(决策落点主文件,\`<src 路径>:<行号或符号>\`;status=confirmed 时必填);**模块域**(决策涉及的模块 ID,可多个逗号分隔——合法 id 只取**当前变更所属项目**的 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`,多项目仓勿读其他子项目的 map(核验不认,报错会点名归属);规划中的新模块用 \`NEW:<名>\` 前缀声明,冒号后不加空格:\`NEW:foo\` 合法、\`NEW: foo\` 属书写错误。「生成规范文件」步 --done 的模块域核验对不认识的 id 直接阻断);**否决理由**(status=rejected 时必填);**复潮条件**(status=rejected 时必填);**故障面**(本决策引入的新失败模式——新机制落地时留痕它引入什么失败模式;type=architecture 时建议填写,可选);**退役判据**(出现什么信号时简化或删除本机制——信号出现就该简化它;type=architecture 时建议填写,可选)。
|
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 "<一句中文概括(首行即变更标题);随后动机与背景
|
|
62
62
|
|
|
63
63
|
成功标准:
|
|
64
64
|
- <可验证标准>"
|
|
@@ -67,7 +67,7 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>
|
|
|
67
67
|
(2 调用协议收口)。用户同意则本阶段可在此收尾(\`--done\` 并提示转轻量变更),不必继续走完整设计流程。
|
|
68
68
|
- **明显 large**(需要 Wave 并行编排/上下文分片/多阶段治理,或用户显式要求完整流程)→ 不要提示轻量变更,继续进入下一步「对话式探索与需求澄清」。
|
|
69
69
|
- **拿不准 → 继续探索**(这正是头脑风暴的职责;澄清完成后 Step 8 按同轴精判——注意:经过头脑风暴澄清的需求通常已可单上下文吞吐,出口默认收编轻量道)。
|
|
70
|
-
-
|
|
70
|
+
- 这是**粗判**,只为让明显的纯执行小变更免走设计流程;后续「生成规范文件」步骤做精判兜底(方案在澄清中缩水后按定稿重判,本步印象不作数)。
|
|
71
71
|
|
|
72
72
|
### 输出
|
|
73
73
|
项目现状理解摘要(3-5 句话,关键约定和架构决策)+ 可能涉及的模块列表 + 本次需求所属子项目 + (如命中)quick 建议
|
|
@@ -188,6 +188,8 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>
|
|
|
188
188
|
- evidence: <文件路径/代码位置/用户回答轮次>(源码引用写仓根相对全路径+行号,如 src/stage-contract.js:789——裸文件名在 docs-check 层1 靠 basename 全仓扫描找候选,找不到候选或关键词窗口不匹配即失效,到 pre-push 才拦,2026-09-19 实证 64 处返工)
|
|
189
189
|
- 故障面: <本决策引入的新失败模式>(可选,type=architecture 时建议填写)
|
|
190
190
|
- 退役判据: <出现什么信号时简化/删除本机制>(可选,type=architecture 时建议填写)
|
|
191
|
+
- 否决理由: <一句话核心劣势>(status=rejected 时必填——distill 入库前置要求,缺失则该条目不进知识库防复潮通道)
|
|
192
|
+
- 复潮条件: <何种信号下可重议本方案>(status=rejected 时必填)
|
|
191
193
|
\`\`\`
|
|
192
194
|
|
|
193
195
|
- 幂等:写入前按 D-xxx@vN 查重,已存在的不重复追加;后续修正走新版本 D-001@v2 + supersedes(见 Design Grill 版本规则)
|
|
@@ -248,11 +250,15 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>
|
|
|
248
250
|
3. 展示完整设计方案(不要逐段停顿,一次性展示)
|
|
249
251
|
4. 分段设计含新机制时同上检索(固定动作):设计的分段内容引入新机制词(例词为举例非机制,不构成封闭清单)时,先跑 \`sillyspec knowledge search --query "<机制词>"\`,命中条目必读(尤其 status=rejected 的防复潮条目)再定稿设计
|
|
250
252
|
5. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth\`——CLI 已强制:新建/改名不带日期前缀直接 exit 2)
|
|
251
|
-
6. 按「HTML
|
|
253
|
+
6. 按「HTML 原型分级」三选一必填:生成原型 / 登记复用既有原型 / 写出跳过原因(见下)
|
|
252
254
|
7. 暂停等待用户确认或修改意见
|
|
253
255
|
|
|
254
256
|
### HTML 原型分级(按改动程度,不是"是否碰前端文件")
|
|
255
257
|
判定对象是**改造后的界面相对现状的变化程度**:
|
|
258
|
+
- **免生成·登记复用**(用户已提供**可复跑**的 HTML 原型且覆盖本次 UI 面——先判本档再判改动程度):
|
|
259
|
+
- 不重画。把原型文件**复制**进 \`{SPEC_ROOT}/changes/<change-name>/\` 并**必须命名为 \`prototype-<名称>.html\`**——execute 阶段按此命名扫描注入原型引用,保留中文名/原名=没登记=白复用
|
|
260
|
+
- 输出复用声明:原型来源路径 + 覆盖面(覆盖哪些页面/端);只覆盖部分 UI 面时如实声明,未覆盖部分仍按下两档生成(变更目录允许多个 prototype-*.html 共存)
|
|
261
|
+
- 截图/静态设计稿**不适用本档**(不可复跑)——仍按「必须生成」产出可跑 HTML
|
|
256
262
|
- **必须生成**(布局/结构/流程级变化):
|
|
257
263
|
- 新增页面、视图、路由
|
|
258
264
|
- 页面布局变化:区域增删、骨架重排、信息层级调整(如单栏改多栏、列表改看板)
|
|
@@ -265,19 +271,20 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>
|
|
|
265
271
|
- 文案、颜色、间距、字号、图标等样式微调
|
|
266
272
|
- 纯后端逻辑/CLI/配置/文档,无任何界面变化
|
|
267
273
|
|
|
268
|
-
###
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
-
|
|
272
|
-
-
|
|
273
|
-
-
|
|
274
|
+
### 生成要求(定稿原型=真码产物,不是手绘图)
|
|
275
|
+
入口文件保存到 \`{SPEC_ROOT}/changes/<change-name>/prototype-<名称>.html\`(目录不存在则先创建;该命名是 execute 注入契约,不改)。真码五要素:
|
|
276
|
+
- **与实现同方言**:用项目真实前端栈构建——组件库/设计 token/样式从项目源码或 node_modules 真实引入(例:React 项目挂真实组件而非仿制标签、Vue 项目用真实 SFC;例词非封闭清单),不手搓仿制样式
|
|
277
|
+
- **可复跑**:能被本项目工具链跑起来(dev server / 组件文档页 / 构建产物均可,入口 HTML 从本地 node_modules 引真实库也行)——先查 scan 文档与现有前端代码就近发现项目原型管线,已有则复用,没有则搭最小入口并留在项目惯例位置
|
|
278
|
+
- **高保真=组件即实现原料**:布局/组件/交互按真实效果呈现,execute 阶段直接对照搬组件,不经过"看图重新翻译"
|
|
279
|
+
- **手绘单文件(自造样式)仅限一次性粗选对比,不得作为定稿原型**(frontend-apple-style 实证:手绘定稿后每条意见都要反向翻译回真实代码);粗选定了方向,升级真码再收口
|
|
280
|
+
- **例外与降级**:项目本身即单文件 HTML 方言(纯静态站)时单文件即真码,不适用手绘限定;无法同方言真码化(如目标平台无 HTML 运行面)时显式声明降级与理由请用户裁决留痕——静默降级会被收口「UI 视觉证据」探针拦下
|
|
274
281
|
|
|
275
282
|
### 铁律 — 必须等待用户确认设计
|
|
276
283
|
- 展示完整设计方案后必须暂停等用户确认,**不要自己说"设计已充分确认"然后推进**(命令由 CLI 在下方注入)
|
|
277
|
-
-
|
|
284
|
+
- 原型生成、复用登记或跳过声明必须在暂停**之前**完成(复用登记=原型文件已按 \`prototype-<名称>.html\` 落在变更目录)——暂停是本步骤最后动作,用户确认后直接进入写 design.md,不会回头补
|
|
278
285
|
|
|
279
286
|
### 输出
|
|
280
|
-
完整设计方案 + 变更名 + 原型文件路径 或
|
|
287
|
+
完整设计方案 + 变更名 + 原型文件路径 或 复用声明(来源+覆盖面+登记后路径)或 跳过原因(三选一必填;跳过时写明分级依据,用户可当场否决要求补)
|
|
281
288
|
|
|
282
289
|
### 注意
|
|
283
290
|
- 不要一次输出大段文字,按模块/Phase 分段
|
|
@@ -486,6 +493,8 @@ ${REVIEW_CHECKLISTS.brainstorm.slice(3).map((item) => '- ' + item).join('\n')}
|
|
|
486
493
|
- evidence: design.md §3/§7, src/cache/...
|
|
487
494
|
- 故障面: <本决策引入的新失败模式>(可选,type=architecture 时建议填写)
|
|
488
495
|
- 退役判据: <出现什么信号时简化/删除本机制>(可选,type=architecture 时建议填写)
|
|
496
|
+
- 否决理由: <一句话核心劣势>(status=rejected 时必填——distill 入库前置要求,缺失则该条目不进知识库防复潮通道)
|
|
497
|
+
- 复潮条件: <何种信号下可重议本方案>(status=rejected 时必填)
|
|
489
498
|
\`\`\`
|
|
490
499
|
|
|
491
500
|
### 输出格式
|
|
@@ -531,9 +540,12 @@ status: passed | needs-user-input | blocked | skipped
|
|
|
531
540
|
按**复杂度/上下文轴**判断本次变更规模(2026-09-29-brainstorm-exit-thin-default:此前文件数轴「≤2 文件才 small」与选道表第 3 行的复杂度措辞不一致,且模糊需求探索完很少只碰 2 文件——出口几乎必然五阶段,实测坐实):
|
|
532
541
|
- **small(收编轻量道)**:单上下文可吞吐——预计无需 Wave 并行编排、无需上下文分片(单会话吃得下全部任务)、无多阶段治理/设计期人机对抗需求
|
|
533
542
|
- **large(完整五阶段)**:需要 Wave 并行编排、或单任务规模大需上下文分片、或多阶段治理、或用户显式要求完整流程
|
|
543
|
+
- **反判据(2026-10-14-scale-large-evidence,deploy-release-notes 实证:零建表定稿被早期印象判 large 走了五阶段)**:跨多端/多仓/多文件**不是** large 依据——轻量道支持多文件与跨仓交付;任务间**顺序依赖也不是**(B 等 A 的契约/产物正是轻量道任务循环的天然形态,一波等一波不构成 Wave)。Wave 证据=存在**可真并行**的任务簇(互相不等)**且单会话上下文吃不下**,两条同时成立才够 large
|
|
544
|
+
- **Wave 证据落盘**:判 large 时必须在 design.md(总体方案/Phase-Wave 处)写明——哪些任务簇可真并行、单会话上下文为何吃不下;**写不出 Wave 证据 = 改判 small**
|
|
545
|
+
- **按定稿方案重判**:Step 2 早期筛查只是路由粗判,方案在澄清中缩水(砍建表/砍迁移/砍端点)后必须按**定稿实质**重判——沿用早期筛查印象会把已缩水的方案误判厚(实证:plan 阶段自判单会话执行〔execution_mode=main〕反证 Wave 并非真需求)
|
|
534
546
|
- **拿不准 → small**(与 thin-default-flip 同哲学:预判反诱发误升厚——R16 实证;升厚留运行时证据:实测失败自动升厚+--upgrade-thick 用户决策)
|
|
535
547
|
在 design.md frontmatter 写入 \`scale: small\` 或 \`scale: large\`(frontmatter 不存在则补 \`author\`/\`created_at\`/\`scale\`)。规模决定下面的产物范围和实现路径。
|
|
536
|
-
(注:早期「加载项目上下文」步骤已做过一次粗判并可能建议过走 quick;此处基于 design
|
|
548
|
+
(注:早期「加载项目上下文」步骤已做过一次粗判并可能建议过走 quick;此处基于 design 方案实质做精判兜底——判据同轴,方案缩水后按定稿重判、早期印象不作数。)
|
|
537
549
|
|
|
538
550
|
### 操作
|
|
539
551
|
1. **按规模生成规范文件**:
|
package/src/stages/execute.js
CHANGED
|
@@ -1425,7 +1425,7 @@ ${worktreeNoticeSingle}
|
|
|
1425
1425
|
|
|
1426
1426
|
**主代理直写(execution_mode: main)——你直接实现,不派子代理。**
|
|
1427
1427
|
|
|
1428
|
-
逐任务闭环(串行,完成一个再下一个):读 task 卡(路径见下方任务摘要)→ 在 worktree 内实现(TDD
|
|
1428
|
+
逐任务闭环(串行,完成一个再下一个):读 task 卡(路径见下方任务摘要)→ 在 worktree 内实现(TDD 按层适用:纯逻辑先写测试再实现,行为契约断言用户可见行为,样式/纯配置不写——判据见「测试用例设计」)→ 跑该 task 卡 verify 命令 → \`sillyspec wt-commit --change <change-name> -- <task-files>\` 逐任务提交 → 手动勾选 tasks.md 对应 checkbox(完成=实现+测试绿+wt-commit 即勾,2026-09-26 Task Review 退役起勾选回归 agent 手动,同 thin 工作单元语义)→ 下一任务。
|
|
1429
1429
|
|
|
1430
1430
|
worktree 隔离 / 写入守卫(allowed_paths)/ verify 门禁全部保留——只换执行宿主,不换任何防线。
|
|
1431
1431
|
|
package/src/stages/explore.js
CHANGED
|
@@ -25,12 +25,10 @@ export const definition = {
|
|
|
25
25
|
6. 讨论 UI/交互方案且用户明确要求原型时,按下方「HTML 原型生成」执行
|
|
26
26
|
|
|
27
27
|
### HTML 原型生成(仅用户明确要求时)
|
|
28
|
-
探索保持只读姿态:不主动写原型文件;UI 讨论成形时可以提议("要不要出个原型看效果"
|
|
29
|
-
-
|
|
30
|
-
- 高保真呈现讨论中的布局/组件/交互流程——用户看到的是"这个方案做出来长什么样",不是示意图
|
|
31
|
-
- 与项目现有界面风格一致:先读 scan 文档与现有前端代码,复用现有组件库/设计 token 的观感,不另起风格
|
|
28
|
+
探索保持只读姿态:不主动写原型文件;UI 讨论成形时可以提议("要不要出个原型看效果"),用户同意后再生成。探索期原型是**粗选对比件**(求快不求真,手绘单文件含内联 CSS+JS 在探索期合法,浏览器直接打开):
|
|
29
|
+
- 呈现讨论中的布局/组件/交互流程——用户看到的是"这个方案做出来长什么样",不是示意图;能用项目真实组件库/设计 token 观感的尽量用(先读 scan 文档与现有前端代码),降低与真实效果的偏差
|
|
32
30
|
- 保存到 \`{SPEC_ROOT}/explore/prototype-<名称>.html\`(目录不存在则先创建;用户指定了其他位置则从其指定)
|
|
33
|
-
- 原型是探索讨论工具、不是交付物,也不是实现代码——不要顺手把原型接进项目源码或构建;话题后续进入 brainstorm
|
|
31
|
+
- 原型是探索讨论工具、不是交付物,也不是实现代码——不要顺手把原型接进项目源码或构建;话题后续进入 brainstorm 时分两路:已是可复跑真码的原型复制到 \`{SPEC_ROOT}/changes/<变更名>/\`(**保留 \`prototype-<名称>.html\` 命名**——execute 按此命名注入,改名=不注入)走登记复用;手绘粗选件只是方向输入,brainstorm 定稿须按其「生成要求」升级为真码(同方言/可复跑),不得把手绘件直接当定稿登记
|
|
34
32
|
|
|
35
33
|
### 输出
|
|
36
34
|
探索结论、选项对比、风险清单或 ASCII 图;如生成了原型,附原型文件路径
|
package/src/stages/knowledge.js
CHANGED
|
@@ -110,8 +110,16 @@ export async function cmdSearch(dir, args, opts = {}) {
|
|
|
110
110
|
// 复用 knowledge-match 引擎(三层 hybrid:路由→平台向量→本地词片,2026-09-29-knowledge-vector-recall)
|
|
111
111
|
const result = await matchKnowledgeHybrid(knowledgeDir, query, { cwd: dir })
|
|
112
112
|
|
|
113
|
+
// 向量开关发现性提示(2026-10-15-vector-switch-discoverability):平台已连接+开关未开
|
|
114
|
+
// (缺省关)→ hint 键(vectorSwitchHint 单源,flow 注入零命中行同款文案);其余空串零输出。
|
|
115
|
+
let vectorHint = ''
|
|
116
|
+
try {
|
|
117
|
+
const { vectorSwitchHint } = await import('../knowledge-vector.js')
|
|
118
|
+
vectorHint = vectorSwitchHint(dir)
|
|
119
|
+
} catch { /* 提示 fail-open */ }
|
|
120
|
+
|
|
113
121
|
if (!result.matched) {
|
|
114
|
-
output(true, { query, matches: [] })
|
|
122
|
+
output(true, { query, matches: [], ...(vectorHint ? { hint: vectorHint } : {}) })
|
|
115
123
|
return
|
|
116
124
|
}
|
|
117
125
|
|
|
@@ -139,7 +147,7 @@ export async function cmdSearch(dir, args, opts = {}) {
|
|
|
139
147
|
}
|
|
140
148
|
})
|
|
141
149
|
|
|
142
|
-
output(true, { query, matches })
|
|
150
|
+
output(true, { query, matches, ...(vectorHint ? { hint: vectorHint } : {}) })
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
// ── inspect ──
|
|
@@ -610,7 +618,8 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
610
618
|
' digest 治理信号卡(rot/伪域/坏绑定——纯读)',
|
|
611
619
|
' stats 覆盖率/死条目统计(纯读)',
|
|
612
620
|
' refresh 重扫 scan 文档重写 generated/ 区(**写盘**——先 validate 再用)',
|
|
613
|
-
' propose / classify / inbox / graph / stats',
|
|
621
|
+
' propose / classify / inbox / graph / stats / renumber',
|
|
622
|
+
' renumber [--dry-run|--apply] 存量决策号迁移为全局号(dry-run 先行;apply 写盘幂等)',
|
|
614
623
|
'',
|
|
615
624
|
'写盘面子命令(refresh/propose/classify)先跑同名只读命令确认现状;探索用法先看本帮助。',
|
|
616
625
|
]
|
|
@@ -693,11 +702,53 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
693
702
|
}
|
|
694
703
|
return
|
|
695
704
|
}
|
|
705
|
+
case 'renumber': {
|
|
706
|
+
// 存量决策号一次性迁移(2026-10-14-decision-legacy-renumber):--dry-run 打印计划,
|
|
707
|
+
// --apply 执行重写(快照指纹校验 + 原子写;重跑幂等——原编号在场节段零触碰)。
|
|
708
|
+
const specBase = opts.specDir || join(dir, '.sillyspec')
|
|
709
|
+
const rest = args.slice(1)
|
|
710
|
+
const apply = rest.includes('--apply')
|
|
711
|
+
try {
|
|
712
|
+
const { planRenumber, applyRenumberPlan } = await import('../decision-renumber.js')
|
|
713
|
+
const plan = planRenumber(join(specBase, 'knowledge'))
|
|
714
|
+
const st = plan.stats
|
|
715
|
+
console.log(`🗺️ 决策号迁移计划:legacy ${st.legacySections} 节段|fanout ${st.fanoutGroups} 组(${st.fanoutCopies} 副本→单一身份)|发号 ${st.mintedNumbers}|无变更条目 ${st.noChangeEntries}(逐节独立发号)|epoch 零触碰 ${st.untouchedEpoch}`)
|
|
716
|
+
for (const [dom, n] of Object.entries(st.perDomainStart)) console.log(` 域 ${dom}:号段自 ${String(n).padStart(3, '0')} 起`)
|
|
717
|
+
if (!apply) {
|
|
718
|
+
console.log('(--dry-run 形态——加 --apply 执行重写;git 可回退)')
|
|
719
|
+
output(true, { subcommand: 'renumber', dryRun: true, ...st })
|
|
720
|
+
return
|
|
721
|
+
}
|
|
722
|
+
if (plan.files.every((f) => !f.ops || f.ops.length === 0)) {
|
|
723
|
+
console.log('✅ 无可迁移节段(幂等重跑——零变化)')
|
|
724
|
+
output(true, { subcommand: 'renumber', filesWritten: 0, ...st })
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
let filesWritten = 0
|
|
728
|
+
for (const fe of plan.files) {
|
|
729
|
+
if (!fe.ops || !fe.ops.length) continue
|
|
730
|
+
const p = join(specBase, 'knowledge', fe.file)
|
|
731
|
+
const cur = readFileSync(p, 'utf8')
|
|
732
|
+
if (cur !== fe.content) {
|
|
733
|
+
console.error(`❌ 快照漂移:${fe.file} 在计划后被并行修改——中止不覆盖(重跑重新计划)`)
|
|
734
|
+
process.exitCode = 1
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
writeAtomicSync(p, applyRenumberPlan(new Map([[fe.file, cur]]), plan).get(fe.file))
|
|
738
|
+
filesWritten++
|
|
739
|
+
}
|
|
740
|
+
console.log(`✅ 迁移完成:${filesWritten} 文件重写、${st.mintedNumbers} 号铸出(epoch 形态)——重跑 --apply 验证零变化;平台侧随后 platform sync 推送`)
|
|
741
|
+
output(true, { subcommand: 'renumber', filesWritten, ...st })
|
|
742
|
+
} catch (e) {
|
|
743
|
+
output(false, {}, { code: 'renumber_failed', message: String((e && e.message) || e) })
|
|
744
|
+
}
|
|
745
|
+
return
|
|
746
|
+
}
|
|
696
747
|
default:
|
|
697
748
|
output(false, {}, {
|
|
698
749
|
code: 'unknown_subcommand',
|
|
699
|
-
|
|
700
|
-
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'inbox', 'stats', 'digest', 'graph'], // dump 随 graph 路由面(2026-10-09-graph-dump-layout)
|
|
750
|
+
subCommand: subCommand,
|
|
751
|
+
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'inbox', 'stats', 'digest', 'graph', 'renumber'], // dump 随 graph 路由面(2026-10-09-graph-dump-layout)
|
|
701
752
|
})
|
|
702
753
|
}
|
|
703
754
|
}
|
package/src/sync.js
CHANGED
|
@@ -18,6 +18,7 @@ import { safeGit } from './git-helper.js';
|
|
|
18
18
|
import { openDatabase } from './db-engine.js';
|
|
19
19
|
import { PLATFORM_MANAGED_FILENAME, QUICK_SID_RE } from './run/shared.js';
|
|
20
20
|
import { syncSpecTree } from './spec-sync.js';
|
|
21
|
+
import { lookupDeletedLedger } from './deleted-ledger.js';
|
|
21
22
|
import { bindSyncNoiseFromCwd, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess, syncChangeDeletedLog, syncChangeDeletedWarn, noteChangeDeletedResolved, syncSelfHealWarn } from './sync-noise.js';
|
|
22
23
|
|
|
23
24
|
// sync 是 best-effort(网络失败只 warn):平台指针失效时不抛,跳过平台、回退本地。
|
|
@@ -272,6 +273,32 @@ function combineSignals(external, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
272
273
|
return external ? AbortSignal.any([timeoutSignal, external]) : timeoutSignal;
|
|
273
274
|
}
|
|
274
275
|
|
|
276
|
+
/**
|
|
277
|
+
* 非 2xx 回显折叠(坑 probe-noise-and-commit-suggest-quality 坑C,2026-10-15 消费仓实证:
|
|
278
|
+
* 409 冲突响应体含整段 platform_progress 大对象,text.slice(0,200) 密排回显挂在每条 CLI 命令
|
|
279
|
+
* 尾部刷屏)。结构化冲突体折叠为一行摘要(conflict 标识 + 关键时间戳/变更字段,不回显大
|
|
280
|
+
* 对象);其他 JSON 体打键名摘要;非 JSON/解析失败退回截断原文(既有形态零回归)。
|
|
281
|
+
*/
|
|
282
|
+
export function formatSyncHttpErrorLine(method, url, status, text) {
|
|
283
|
+
const raw = String(text || '');
|
|
284
|
+
let body = null;
|
|
285
|
+
try { body = JSON.parse(raw); } catch { body = null; }
|
|
286
|
+
const m = method || 'GET';
|
|
287
|
+
if (body && typeof body === 'object') {
|
|
288
|
+
if (body.conflict === true) {
|
|
289
|
+
const parts = ['conflict'];
|
|
290
|
+
if (body.change) parts.push(`change=${body.change}`);
|
|
291
|
+
if (typeof body.last_pushed_at === 'string') parts.push(`last_pushed_at=${body.last_pushed_at}`);
|
|
292
|
+
if (body.base_ts !== undefined) parts.push(`base_ts=${body.base_ts}`);
|
|
293
|
+
if (body.reason) parts.push(`reason=${String(body.reason).slice(0, 80)}`);
|
|
294
|
+
return `[sync] ${m} ${url} → ${status} ${parts.join(' ')}(冲突响应体已折叠为一行)`;
|
|
295
|
+
}
|
|
296
|
+
const keys = Object.keys(body).slice(0, 6).join(',');
|
|
297
|
+
return `[sync] ${m} ${url} → ${status} ${keys ? `json(${keys})` : '(空响应体)'}`;
|
|
298
|
+
}
|
|
299
|
+
return `[sync] ${m} ${url} → ${status} ${raw.slice(0, 200)}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
275
302
|
async function fetchJson(url, options = {}) {
|
|
276
303
|
const signal = combineSignals(options.signal);
|
|
277
304
|
// noMute 是本模块的闸门开关不是 fetch 参数,先剥掉再透传
|
|
@@ -280,7 +307,7 @@ async function fetchJson(url, options = {}) {
|
|
|
280
307
|
const res = await fetch(url, { ...init, signal });
|
|
281
308
|
if (!res.ok) {
|
|
282
309
|
const text = await res.text().catch(() => '');
|
|
283
|
-
const line =
|
|
310
|
+
const line = formatSyncHttpErrorLine(options.method, url, res.status, text);
|
|
284
311
|
// 连接类失败(404=端点未就绪 / 5xx)走跨进程噪音闸:平台未就绪期每条命令的自动
|
|
285
312
|
// 同步都撞同一批失败,重复刷屏淹没门禁输出(见 sync-noise.js 模块头)。409 冲突与
|
|
286
313
|
// 其余 4xx 业务态每轮都该看见,不走闸。用户显式命令(connect health ping)传 noMute。
|
|
@@ -319,7 +346,7 @@ async function fetchJsonWithStatus(url, options = {}) {
|
|
|
319
346
|
try { body = JSON.parse(text); } catch { body = null; }
|
|
320
347
|
}
|
|
321
348
|
if (!res.ok) {
|
|
322
|
-
const line =
|
|
349
|
+
const line = formatSyncHttpErrorLine(options.method, url, res.status, text);
|
|
323
350
|
if (!noMute && isConnectionClassStatus(res.status)) syncConnectionWarn(line);
|
|
324
351
|
else console.warn(line);
|
|
325
352
|
}
|
|
@@ -747,6 +774,18 @@ export class SyncManager {
|
|
|
747
774
|
return { synced: 0, errors: [`读取 progress 失败: ${err.message}`] };
|
|
748
775
|
}
|
|
749
776
|
if (progressData === null) {
|
|
777
|
+
// 删除台账兜底(2026-10-13-sync-ledger-tombstone,事故 2026-10-13-new-change-e5a27813):
|
|
778
|
+
// DB 行被外部物理清除(rm + 裸 SQL)后 serializeForSync 恒 null、X1 墓碑链失源,
|
|
779
|
+
// 平台行永挂 active。台账里「存在过且已达终态」时合成最小载荷补推墓碑,让平台
|
|
780
|
+
// _apply_cli_tombstone 收敛 location。台账无此名(从未存在,如 --change 打错字)
|
|
781
|
+
// 不发——误发会把平台同名真变更软删。quick 会话豁免(与 tombstoneDue 同口径)。
|
|
782
|
+
if (!QUICK_SID_RE.test(changeName)) {
|
|
783
|
+
const ledger = lookupDeletedLedger(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), changeName);
|
|
784
|
+
if (ledger && (ledger.status === 'deleted' || ledger.status === 'archived')) {
|
|
785
|
+
const pushed = await this._pushLedgerTombstone(changeName, ledger, { signal: opts.signal });
|
|
786
|
+
return { synced: 0, errors: [], ledgerTombstone: pushed ? 'pushed' : 'best-effort-failed' };
|
|
787
|
+
}
|
|
788
|
+
}
|
|
750
789
|
console.warn(`[sync] 变更无进度数据 (无活跃进度): ${changeName}`);
|
|
751
790
|
return { synced: 0, errors: [`变更无进度数据: ${changeName}`] };
|
|
752
791
|
}
|
|
@@ -1098,6 +1137,63 @@ export class SyncManager {
|
|
|
1098
1137
|
return { synced: 0 };
|
|
1099
1138
|
}
|
|
1100
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* 台账墓碑(2026-10-13-sync-ledger-tombstone):DB 行已被外部物理清除、终态凭证只剩
|
|
1142
|
+
* 删除台账时的合成墓碑上行。与 _pushTombstone 的差异:不读 DB(无行可读),载荷由
|
|
1143
|
+
* 台账条目合成——changes[0] 携带台账 status(平台 _apply_cli_tombstone 据此收敛
|
|
1144
|
+
* location)+ current_stage='archive',其余五表空数组(平台端解析容错,字段缺失即空集)。
|
|
1145
|
+
* base_ts 不设:平台契约「base_ts 缺失无条件接受」(platform_sync/service.py 分支 1),
|
|
1146
|
+
* 不与既有 last_pushed_at 比较,无 409 面。best-effort:失败 debugLog 不抛(与 X1 链
|
|
1147
|
+
* 语义一致——平台 7 天 reparse 兜底仍在)。
|
|
1148
|
+
* @param {string} changeName
|
|
1149
|
+
* @param {{name: string, status: string, at: string}} ledger 台账终态条目
|
|
1150
|
+
* @param {{signal?: AbortSignal}} [opts]
|
|
1151
|
+
* @returns {Promise<boolean>} 是否推送成功
|
|
1152
|
+
*/
|
|
1153
|
+
async _pushLedgerTombstone(changeName, ledger, opts = {}) {
|
|
1154
|
+
const platform = this._getPlatform();
|
|
1155
|
+
if (!platform) return false; // 未连接平台:台账墓碑无从上行,行缺失也无处可回填,静默
|
|
1156
|
+
const payload = {
|
|
1157
|
+
changes: [{
|
|
1158
|
+
name: changeName,
|
|
1159
|
+
status: ledger.status,
|
|
1160
|
+
current_stage: 'archive',
|
|
1161
|
+
created_at: ledger.at,
|
|
1162
|
+
last_active: ledger.at,
|
|
1163
|
+
title: changeName,
|
|
1164
|
+
}],
|
|
1165
|
+
stages: [],
|
|
1166
|
+
steps: [],
|
|
1167
|
+
approvals: [],
|
|
1168
|
+
batch_progress: [],
|
|
1169
|
+
};
|
|
1170
|
+
const headers = {
|
|
1171
|
+
'Content-Type': 'application/json',
|
|
1172
|
+
Authorization: `Bearer ${platform.token}`,
|
|
1173
|
+
'X-SillySpec-Pushed-At': new Date().toISOString(),
|
|
1174
|
+
};
|
|
1175
|
+
const pushUser = platform.user || resolvePlatformUser(this.cwd) || null;
|
|
1176
|
+
if (pushUser) headers['X-SillySpec-User'] = pushUser;
|
|
1177
|
+
const syncUrl = `${platform.url}/api/changes/${encodeURIComponent(changeName)}/progress`;
|
|
1178
|
+
try {
|
|
1179
|
+
const res = await fetchJsonWithStatus(syncUrl, {
|
|
1180
|
+
method: 'POST',
|
|
1181
|
+
headers,
|
|
1182
|
+
body: JSON.stringify(payload),
|
|
1183
|
+
signal: opts.signal,
|
|
1184
|
+
});
|
|
1185
|
+
if (res.ok) {
|
|
1186
|
+
console.log(`[sync] 已上行台账墓碑(行已清除,status=${ledger.status}): ${changeName}`);
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
debugLog(`[sync] 台账墓碑上行未成功: ${changeName} (status=${res.status},平台 reparse 兜底)`);
|
|
1190
|
+
return false;
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
debugLog(`[sync] 台账墓碑上行失败: ${changeName}: ${(err && err.message) || err}`);
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1101
1197
|
/**
|
|
1102
1198
|
* 自竞态判定辅助:fresh 读本地 DB 的 last_synced_platform_ts(base_ts),判断它是否已覆盖
|
|
1103
1199
|
* 平台 409 回执的 last_pushed_at(字符串字典序比较,契约 §4.2——ISO 8601 字典序 == 时间序)。
|
package/src/task-tick.js
CHANGED
|
@@ -145,6 +145,18 @@ export async function runTaskTick({ changeName, cwd, taskId, specBase, evidence
|
|
|
145
145
|
...(ev.text ? { evidence: ev.text.slice(0, 300) } : {}), // 事件面截 300 字符(全文在 task-evidence.jsonl)
|
|
146
146
|
}) + '\n', 'utf8')
|
|
147
147
|
} catch { /* 事件 best-effort:写失败翻格不受影响(节奏门退回采样判) */ }
|
|
148
|
+
// 侧车勾选留痕(2026-10-14-timeline-ticktime-anchor-scope):{ts,task,source} 追加进 change 目录
|
|
149
|
+
// 的 task-ticks.jsonl——勾选时刻的事实源(timeline 任务面精确时刻),落 change 目录随归档进 git
|
|
150
|
+
// 跨机可读,不随 .runtime 机本位/观测盲窗蒸发(2026-10-14-deploy-release-notes 实证:盲窗内
|
|
151
|
+
// 勾选两个记录面全空)。事件面不带任务 id(节奏门只读 checked N→M),id 口径以本侧车为权威。
|
|
152
|
+
// best-effort:写失败仅告警,翻格不受影响(timeline 退事件流推断)。
|
|
153
|
+
try {
|
|
154
|
+
appendFileSync(join(changesRoot, changeName, 'task-ticks.jsonl'), JSON.stringify({
|
|
155
|
+
ts: Date.now(), task: normalizeTaskId(taskId) || taskId, source: 'task-tick',
|
|
156
|
+
}) + '\n', 'utf8')
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.warn(`⚠️ 勾选留痕写失败(task-ticks.jsonl:${e && e.code ? e.code : e})——翻格不受影响,timeline 勾选时刻退事件流推断`)
|
|
159
|
+
}
|
|
148
160
|
// 工件即时重推(2026-10-07-allticked-gate-docs-resync):翻格即任务面变更——顺带触发
|
|
149
161
|
// spec-sync,平台不再停留 start 时点快照(真实会话实证:改写后平台恒显初稿)。best-effort。
|
|
150
162
|
try {
|