sillyspec 3.28.9 → 3.28.11
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-execute/SKILL.md +223 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
- package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +51 -3
- package/src/config-schema.js +51 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/design-facts.js +94 -18
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/endpoint-extractor.js +50 -2
- package/src/foreign-declared.js +25 -16
- package/src/hooks/worktree-guard.js +75 -2
- package/src/index.js +147 -2
- package/src/knowledge-classify.js +20 -1
- package/src/machine-interface.js +26 -0
- package/src/probe7-anchor-check.js +87 -0
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +240 -5
- package/src/run/command.js +25 -2
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +143 -11
- package/src/run/gate-snapshot.js +372 -2
- package/src/run/gates.js +130 -12
- package/src/run/prompt.js +166 -7
- package/src/run/shared.js +81 -1
- package/src/scope-audit.js +101 -31
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +314 -8
- package/src/stage-review-checklist.js +3 -1
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +62 -8
- package/src/stages/knowledge.js +24 -3
- package/src/stages/plan-postcheck.js +74 -10
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +3 -2
- package/src/sync-noise.js +57 -1
- package/src/sync.js +77 -4
- package/src/task-review.js +84 -29
- package/src/verify-facts-schema.js +78 -9
- package/src/verify-postcheck.js +439 -68
- package/src/verify-probes.js +1156 -45
- package/src/worktree-apply.js +323 -8
- package/src/worktree-deps.js +24 -1
- package/src/worktree.js +274 -10
- package/src/write-audit.js +55 -0
package/src/sync.js
CHANGED
|
@@ -18,7 +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 { bindSyncNoiseFromCwd, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess, syncChangeDeletedLog, syncChangeDeletedWarn, noteChangeDeletedResolved } from './sync-noise.js';
|
|
21
|
+
import { bindSyncNoiseFromCwd, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess, syncChangeDeletedLog, syncChangeDeletedWarn, noteChangeDeletedResolved, syncSelfHealWarn } from './sync-noise.js';
|
|
22
22
|
|
|
23
23
|
// sync 是 best-effort(网络失败只 warn):平台指针失效时不抛,跳过平台、回退本地。
|
|
24
24
|
function safePlatformSpecDir(cwd) {
|
|
@@ -844,7 +844,9 @@ export class SyncManager {
|
|
|
844
844
|
// 说明赢者是本机自己人——刷新 base_ts 重试一次即收敛,不落冲突文件(外来推送不可能
|
|
845
845
|
// 推进本机 DB 的 base_ts,不满足条件自然落回下方真冲突分支,零误放行)
|
|
846
846
|
if (attempt < MAX_PUSH_ATTEMPTS && await this._localBaseTsCovers(changeName, platformLastPushedAt)) {
|
|
847
|
-
|
|
847
|
+
// 409 自愈键控闸(2026-09-15 复盘:自愈不阻断但每步刷屏)——首报可见、窗口内同变更
|
|
848
|
+
// 同类型静默;手动 platform sync(manual)旁路
|
|
849
|
+
syncSelfHealWarn(changeName, 'self-race', `⚠️ [sync] push 409 自竞态判定:本机并发推送已回填 base_ts(平台 ts=${platformLastPushedAt}),刷新后自动重试`, { noMute: manual });
|
|
848
850
|
continue;
|
|
849
851
|
}
|
|
850
852
|
|
|
@@ -863,7 +865,8 @@ export class SyncManager {
|
|
|
863
865
|
'UPDATE changes SET last_synced_platform_ts = MAX(?, COALESCE(last_synced_platform_ts, ?)) WHERE name = ?'
|
|
864
866
|
).run(platformLastPushedAt || pushedAt, platformLastPushedAt || pushedAt, changeName);
|
|
865
867
|
} catch {}
|
|
866
|
-
|
|
868
|
+
// 409 自愈键控闸同上(kind=content-equal)
|
|
869
|
+
syncSelfHealWarn(changeName, 'content-equal', `⚠️ [sync] push 409 内容一致自愈: 平台 ts=${platformLastPushedAt} 为外来噪声重推(六表内容与本地一致),base_ts 已推进,不落冲突文件`, { noMute: manual });
|
|
867
870
|
return { synced: 1, errors: [], selfHealed: true, reason: 'push 409 平台内容与本地一致(外来噪声重推),base_ts 已推进' };
|
|
868
871
|
}
|
|
869
872
|
} catch { /* 比对失败维持原判(fail-closed 到真冲突分支) */ }
|
|
@@ -895,7 +898,8 @@ export class SyncManager {
|
|
|
895
898
|
pm._ensureDB(this.cwd).getDb().prepare(
|
|
896
899
|
'UPDATE changes SET last_synced_platform_ts = MAX(?, COALESCE(last_synced_platform_ts, ?)) WHERE name = ?'
|
|
897
900
|
).run(healTs, healTs, changeName);
|
|
898
|
-
|
|
901
|
+
// 409 自愈键控闸同上(kind=self-echo)
|
|
902
|
+
syncSelfHealWarn(changeName, 'self-echo', `⚠️ [sync] push 409 自回声判定:平台 ts=${platformLastPushedAt} 为本机自推回声(血统 ${platLin},推送者 ${platformLastPusher || '未回传·窗口判定'}),base_ts 已推进到 ${healTs},自动重推`, { noMute: manual });
|
|
899
903
|
if (attempt < MAX_PUSH_ATTEMPTS) continue;
|
|
900
904
|
// 重试额度耗尽(attempt=2 仍进此分支,理论少见):不落冲突文件——base 已推进,
|
|
901
905
|
// 下次常规同步按新 base 直接推送收敛
|
|
@@ -1116,6 +1120,28 @@ export class SyncManager {
|
|
|
1116
1120
|
return { synced: 0, errors: [...errors, '无可用文档'] };
|
|
1117
1121
|
}
|
|
1118
1122
|
|
|
1123
|
+
// 无变化推送去重(2026-09-17 用户反馈⑥-②,ql-20260917-005):每条 CLI 命令的自动同步都
|
|
1124
|
+
// 全量 POST 四件套 + 打「已同步 N 个文档」——内容未变时的重复推送是纯噪音/流量,也放大
|
|
1125
|
+
// 「同步在反复回写」的干扰体感。四件套内容集合的 sha256 与上次成功推送比对,一致即跳过
|
|
1126
|
+
// (debug 一行);marker 读取失败/缺失按「有变化」处理(多推一次无害,fail-open)。
|
|
1127
|
+
// manual 路径不去重(显式意图,允许空跑确认链路)。
|
|
1128
|
+
let docsFingerprint = null;
|
|
1129
|
+
try {
|
|
1130
|
+
docsFingerprint = createHash('sha256')
|
|
1131
|
+
.update(DOCUMENT_FILES.map(f => `${f}\u0000${documents[f] ?? ''}`).join('\u0001'))
|
|
1132
|
+
.digest('hex');
|
|
1133
|
+
} catch { /* 指纹失败按有变化处理 */ }
|
|
1134
|
+
if (!manual && docsFingerprint) {
|
|
1135
|
+
const markerPath = join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), '.runtime', `sync-docs-lastpush-${changeName}.json`);
|
|
1136
|
+
try {
|
|
1137
|
+
const prev = JSON.parse(readFileSync(markerPath, 'utf8'));
|
|
1138
|
+
if (prev && prev.hash === docsFingerprint) {
|
|
1139
|
+
debugLog(`[sync] 四件套内容与上次成功推送一致,跳过文档直推: ${changeName}`);
|
|
1140
|
+
return { synced: 0, errors, deduped: true };
|
|
1141
|
+
}
|
|
1142
|
+
} catch { /* 无 marker / 损坏 = 首推或按有变化处理 */ }
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1119
1145
|
const docUrl = `${platform.url}/api/changes/${encodeURIComponent(changeName)}/documents`;
|
|
1120
1146
|
const result = await fetchJson(docUrl, {
|
|
1121
1147
|
method: 'POST',
|
|
@@ -1131,6 +1157,14 @@ export class SyncManager {
|
|
|
1131
1157
|
return { synced: 0, errors: [...errors, '文档同步请求失败'] };
|
|
1132
1158
|
}
|
|
1133
1159
|
|
|
1160
|
+
// 推送成功才落指纹 marker(失败不落——下轮重推)
|
|
1161
|
+
if (docsFingerprint) {
|
|
1162
|
+
try {
|
|
1163
|
+
const runtimeDir = join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), '.runtime');
|
|
1164
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
1165
|
+
writeFileSync(join(runtimeDir, `sync-docs-lastpush-${changeName}.json`), JSON.stringify({ hash: docsFingerprint, at: Date.now() }) + '\n', 'utf8');
|
|
1166
|
+
} catch { /* marker 失败只损失去重(下轮多推一次),不影响同步 */ }
|
|
1167
|
+
}
|
|
1134
1168
|
console.log(`[sync] 已同步 ${syncedCount} 个文档: ${changeName}`);
|
|
1135
1169
|
return { synced: syncedCount, errors };
|
|
1136
1170
|
}
|
|
@@ -1722,6 +1756,8 @@ export class SyncManager {
|
|
|
1722
1756
|
return { ok: false, pulled: false, specDir, reason: `删除本地 specDir 失败${holdHint}: ${err.message}` };
|
|
1723
1757
|
}
|
|
1724
1758
|
try {
|
|
1759
|
+
let pulledFiles = 0;
|
|
1760
|
+
const crlfEntries = [];
|
|
1725
1761
|
for (const e of entries) {
|
|
1726
1762
|
_assertSafeTarName(e.name.replace(/\/+$/, ''), specDir);
|
|
1727
1763
|
const fullPath = join(specDir, e.name);
|
|
@@ -1730,8 +1766,23 @@ export class SyncManager {
|
|
|
1730
1766
|
} else {
|
|
1731
1767
|
mkdirSync(dirname(fullPath), { recursive: true });
|
|
1732
1768
|
writeFileSync(fullPath, e.data);
|
|
1769
|
+
pulledFiles++;
|
|
1770
|
+
if (e.data && e.data.length < 4 * 1024 * 1024) crlfEntries.push([e.name, e.data]); // CRLF 画像采样(超 4MB 大文件跳过采样,写入不受影响)
|
|
1733
1771
|
}
|
|
1734
1772
|
}
|
|
1773
|
+
// 批量写入留痕(2026-09-17 反馈⑥②法证收口):整树解包=逐字节回放,他机 CRLF 内容
|
|
1774
|
+
// 原样落盘——审计行记 时间/触发面/文件数/CRLF 命中数(fail-open)。
|
|
1775
|
+
try {
|
|
1776
|
+
const { appendWriteAudit, detectCrlfFiles, WRITE_AUDIT_FILE_CAP } = await import('./write-audit.js');
|
|
1777
|
+
const crlfPaths = detectCrlfFiles(crlfEntries);
|
|
1778
|
+
appendWriteAudit(specDir, {
|
|
1779
|
+
via: 'pull-spec-bundle',
|
|
1780
|
+
files: pulledFiles,
|
|
1781
|
+
crlfPaths: crlfPaths.slice(0, WRITE_AUDIT_FILE_CAP),
|
|
1782
|
+
crlfCount: crlfPaths.length,
|
|
1783
|
+
force: opts.force === true,
|
|
1784
|
+
});
|
|
1785
|
+
} catch { /* 审计 fail-open */ }
|
|
1735
1786
|
if (localYamlSaved !== null) {
|
|
1736
1787
|
mkdirSync(specDir, { recursive: true }); // 空 bundle(零条目)时树根未被条目重建
|
|
1737
1788
|
writeAtomicSync(localYamlPath, localYamlSaved); // fs-atomic 契约:hook/probe 并发读 local.yaml
|
|
@@ -1812,6 +1863,7 @@ export class SyncManager {
|
|
|
1812
1863
|
let overwritten = 0;
|
|
1813
1864
|
let removed = 0;
|
|
1814
1865
|
const skipped = [];
|
|
1866
|
+
const writtenEntries = []; // [path, data] 对——写后审计 CRLF 画像用(2026-09-17 反馈⑥②法证收口)
|
|
1815
1867
|
for (const p of normalized) {
|
|
1816
1868
|
try {
|
|
1817
1869
|
_assertSafeTarName(p, specDir);
|
|
@@ -1821,6 +1873,7 @@ export class SyncManager {
|
|
|
1821
1873
|
mkdirSync(dirname(full), { recursive: true });
|
|
1822
1874
|
writeFileSync(full, data);
|
|
1823
1875
|
overwritten++;
|
|
1876
|
+
writtenEntries.push([p, data]);
|
|
1824
1877
|
} else if (existsSync(join(specDir, p))) {
|
|
1825
1878
|
// 服务器已无该文件(远端删除)→ 接受服务器 = 删本地
|
|
1826
1879
|
rmSync(join(specDir, p), { force: true });
|
|
@@ -1831,6 +1884,26 @@ export class SyncManager {
|
|
|
1831
1884
|
}
|
|
1832
1885
|
}
|
|
1833
1886
|
|
|
1887
|
+
// 批量写入留痕(2026-09-17 用户反馈⑥②法证收口):take-platform 是「单命令批量整文件
|
|
1888
|
+
// 重写 + 逐字节保留来源行尾」的回放面——他机 CRLF 内容会原样落盘且事后无任何日志可查
|
|
1889
|
+
// (console 打印即蒸发,实证只能靠 mtime 考古)。落一条审计行:时间/触发面/文件集/CRLF
|
|
1890
|
+
// 命中。fail-open:审计失败不影响覆盖结果。
|
|
1891
|
+
try {
|
|
1892
|
+
const { appendWriteAudit, detectCrlfFiles, WRITE_AUDIT_FILE_CAP } = await import('./write-audit.js');
|
|
1893
|
+
const crlfPaths = detectCrlfFiles(writtenEntries);
|
|
1894
|
+
if (crlfPaths.length > 0) {
|
|
1895
|
+
console.warn(`⚠️ [sync] take-platform 覆盖的 ${crlfPaths.length} 个文件内容为 CRLF 行尾(他机编辑器产物按字节回放,本仓自产文件一律 LF)——git add 时 autocrlf 会提示转换,属预期:${crlfPaths.slice(0, 5).join('、')}${crlfPaths.length > 5 ? ' 等' : ''}(明细见 .runtime/write-audit.jsonl)`);
|
|
1896
|
+
}
|
|
1897
|
+
appendWriteAudit(specDir, {
|
|
1898
|
+
via: 'platform-resolve-take-platform',
|
|
1899
|
+
overwritten, removed,
|
|
1900
|
+
files: writtenEntries.map(([p]) => p).slice(0, WRITE_AUDIT_FILE_CAP)
|
|
1901
|
+
.concat(writtenEntries.length > WRITE_AUDIT_FILE_CAP ? [`…+${writtenEntries.length - WRITE_AUDIT_FILE_CAP}`] : []),
|
|
1902
|
+
crlfPaths: crlfPaths.slice(0, WRITE_AUDIT_FILE_CAP),
|
|
1903
|
+
crlfCount: crlfPaths.length,
|
|
1904
|
+
});
|
|
1905
|
+
} catch { /* 审计 fail-open */ }
|
|
1906
|
+
|
|
1834
1907
|
// 基线快照刷新(best-effort):被覆盖路径回写当前盘面 hash(= 服务器内容 hash)、被删
|
|
1835
1908
|
// 路径移除条目——下次同步这些路径按「本地未改动」豁免回推(take-platform 语义闭环)。
|
|
1836
1909
|
try {
|
package/src/task-review.js
CHANGED
|
@@ -341,8 +341,11 @@ export function readReview(reviewPath) {
|
|
|
341
341
|
* (ql-20260915-001 修复②,坑 review-write-draft-marker-stuck:execute 期间反复
|
|
342
342
|
* 「草稿 review changedFiles 为空,跳过自动勾选」,主仓文档面任务自动勾选断链。)
|
|
343
343
|
*
|
|
344
|
-
* 判据:reviewProvenanceStamp 落的 writtenBy
|
|
345
|
-
*
|
|
344
|
+
* 判据:reviewProvenanceStamp 落的 writtenBy 以显式写入通道前缀开头——writeTaskReview
|
|
345
|
+
* (`sillyspec review write`,含 ':force' 变体)或 adoptTaskReviewMechanics(adopt 通道
|
|
346
|
+
* 一键重算代填,落 writtenBy=adoptTaskReviewMechanics#pid;task-05 / FR-08 补入:该通道
|
|
347
|
+
* 合并写入的 review 带 agent 供给 verdict,此前被判草稿、跨仓 task 自动勾选断链)。
|
|
348
|
+
* 两通道均为显式写入(非草稿兜底通道),即使 reviewerNotes 字面残留
|
|
346
349
|
* 'auto-generated draft'(agent 升级草稿时 --notes 引用/复述草稿文案、或 adopt 通道合并
|
|
347
350
|
* 保留旧 notes 的变体)也一律按非草稿处理——verdict 判定不再受草稿零 diff 守卫限制。
|
|
348
351
|
*
|
|
@@ -352,8 +355,11 @@ export function readReview(reviewPath) {
|
|
|
352
355
|
* @param {object|null} review readReview 返回的 review 对象
|
|
353
356
|
* @returns {boolean}
|
|
354
357
|
*/
|
|
358
|
+
const EXPLICIT_REVIEW_WRITE_PREFIXES = ['writeTaskReview', 'adoptTaskReviewMechanics']
|
|
359
|
+
|
|
355
360
|
export function isExplicitReviewWrite(review) {
|
|
356
|
-
|
|
361
|
+
if (typeof review?.writtenBy !== 'string') return false
|
|
362
|
+
return EXPLICIT_REVIEW_WRITE_PREFIXES.some(p => review.writtenBy.startsWith(p))
|
|
357
363
|
}
|
|
358
364
|
|
|
359
365
|
/**
|
|
@@ -1249,6 +1255,70 @@ function resolveAttributionDiffFiles({ cwd, changeName, specBase, platformOpts =
|
|
|
1249
1255
|
return { mode: 'main-window', diffFiles: resolveVerifyChangedFiles(cwd, changeName, null, { specBase }), notes, meta, mergeGitDir }
|
|
1250
1256
|
}
|
|
1251
1257
|
|
|
1258
|
+
/**
|
|
1259
|
+
* worktree/in-place 改动文件集:porcelain 未提交 ∪ committed merge-base 补齐——口径单一化。
|
|
1260
|
+
* 口径出处:D-004@v1(worktree 归因唯一事实源 = worktree 分支 diff + porcelain,决策库
|
|
1261
|
+
* decisions/worktree.md)+ 变更 2026-09-15-worktree-dual-truth-gates §4 / FR-04;坑④
|
|
1262
|
+
* worktree-tick-guard-commit-only(2026-09-15 实证):勾选守卫 prefetchDiffFileSet
|
|
1263
|
+
* (run/complete.js)只算 base..head(已提交),子代理默认不 commit 时守卫恒空跳过勾选——
|
|
1264
|
+
* 本 helper 是「草稿归属并入段(generateTaskReviewDrafts)」与「勾选守卫(prefetchDiffFileSet)」
|
|
1265
|
+
* 两处的单一真相。已知残留 R-09:resolveVerifyChangedFiles(verify-postcheck.js)内另有同型
|
|
1266
|
+
* 补齐段(form A 专属:滤网保 .sillyspec/docs/** 交付物,与本 helper 的整前缀滤网不同),
|
|
1267
|
+
* 本次不动,待该函数下次触碰时统一。
|
|
1268
|
+
*
|
|
1269
|
+
* 行为契约:
|
|
1270
|
+
* - meta 缺省时内部 WorktreeManager.getMeta 自取(try/catch → null)。
|
|
1271
|
+
* - gitDir = meta.worktreePath(mode 非 in-place-fallback 且目录存在)否则 cwd——
|
|
1272
|
+
* in-place 退化取 cwd(保住现状 in-place 主仓 porcelain 并入,不丢,R-08);
|
|
1273
|
+
* meta 完全缺失时同样取 cwd(与被并入段 mergeGitDir 语义一致)。
|
|
1274
|
+
* - porcelain:status --porcelain --untracked-files=all(parsePorcelainFiles 解析),
|
|
1275
|
+
* 滤 .sillyspec 前缀(与被替换的草稿并入段同口径)。
|
|
1276
|
+
* - committed 补齐(仅 gitDir !== cwd,防双并):merge-base(主仓 HEAD, wtHEAD)..wtHEAD
|
|
1277
|
+
* 的 diff --name-only——worktree 内已 commit 的改动对主仓 base..head 与 status 双不可见
|
|
1278
|
+
* (「先提交则 diff 空」时序两难);git 失败 fail-open 忽略。
|
|
1279
|
+
* - 返回正斜杠归一去重数组;任何 git 异常 → 已收集部分或 [](fail-open,不阻断调用方)。
|
|
1280
|
+
*
|
|
1281
|
+
* @param {string} cwd 主仓根
|
|
1282
|
+
* @param {string} changeName 变更名
|
|
1283
|
+
* @param {object|null} [meta] 预取的 worktree meta(缺省内部 getMeta 自取)
|
|
1284
|
+
* @param {object} [opts] 预留(对齐 design 接口签名)
|
|
1285
|
+
* @returns {string[]} 正斜杠归一文件集(git 失败 → 已收集部分或 [],fail-open)
|
|
1286
|
+
*/
|
|
1287
|
+
export function collectWorktreeChangedFiles(cwd, changeName, meta = null, opts = {}) {
|
|
1288
|
+
const files = new Set()
|
|
1289
|
+
if (!cwd || !changeName) return []
|
|
1290
|
+
let m = meta
|
|
1291
|
+
if (m == null) {
|
|
1292
|
+
try { m = new WorktreeManager({ cwd }).getMeta(changeName) } catch { m = null }
|
|
1293
|
+
}
|
|
1294
|
+
const gitDir = (m?.worktreePath && m.mode !== 'in-place-fallback' && existsSync(m.worktreePath))
|
|
1295
|
+
? m.worktreePath
|
|
1296
|
+
: cwd
|
|
1297
|
+
const keep = (p) => p && p !== '.sillyspec' && !p.startsWith('.sillyspec/')
|
|
1298
|
+
// 未提交:worktree(in-place 退化=主仓)porcelain 全量(含 untracked)
|
|
1299
|
+
try {
|
|
1300
|
+
const st = runGit(gitDir, ['status', '--porcelain', '--untracked-files=all'], { trim: false })
|
|
1301
|
+
for (const p of parsePorcelainFiles(st)) if (keep(p)) files.add(p)
|
|
1302
|
+
} catch { /* porcelain 失败 → 返回已收集部分(fail-open) */ }
|
|
1303
|
+
// 已提交补齐:merge-base(主仓 HEAD, wtHEAD)(= 创建锚点)..wtHEAD 的 commit diff。
|
|
1304
|
+
// in-place(gitDir===cwd)时已提交改动在主仓 diff 内,跳过防双并;失败 fail-open 忽略。
|
|
1305
|
+
if (gitDir !== cwd) {
|
|
1306
|
+
try {
|
|
1307
|
+
const mainHead = runGit(cwd, ['rev-parse', 'HEAD'])
|
|
1308
|
+
const wtHead = runGit(gitDir, ['rev-parse', 'HEAD'])
|
|
1309
|
+
const mb = (mainHead && wtHead) ? runGit(gitDir, ['merge-base', wtHead, mainHead]) : null
|
|
1310
|
+
if (mb) {
|
|
1311
|
+
const out = String(runGit(gitDir, ['diff', '--name-only', mb, wtHead]) || '')
|
|
1312
|
+
for (let p of out.split('\n')) {
|
|
1313
|
+
p = p.replace(/^"|"$/g, '').replace(/\\/g, '/').trim()
|
|
1314
|
+
if (keep(p)) files.add(p)
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
} catch { /* 已提交补齐失败忽略(fail-open,不拖垮未提交并入) */ }
|
|
1318
|
+
}
|
|
1319
|
+
return [...files]
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1252
1322
|
/**
|
|
1253
1323
|
* worktree execute「主 agent 直接实现」模式收尾兜底:per-task review.json 缺失时,
|
|
1254
1324
|
* 据 git diff base..head 按 task allowed_paths 归属,自动落盘 cannot_verify 草稿。
|
|
@@ -1339,32 +1409,17 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1339
1409
|
// worktree-branch/cleaned 归因的未提交并入已在 resolveAttributionDiffFiles 内按同口径完成。
|
|
1340
1410
|
if (attribution.mergeGitDir) {
|
|
1341
1411
|
try {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
//
|
|
1348
|
-
//
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
if (wtGitDir !== cwd) {
|
|
1354
|
-
try {
|
|
1355
|
-
const mainHead = runGit(cwd, ['rev-parse', 'HEAD'])
|
|
1356
|
-
const wtHead = runGit(wtGitDir, ['rev-parse', 'HEAD'])
|
|
1357
|
-
const mb = (mainHead && wtHead) ? runGit(wtGitDir, ['merge-base', wtHead, mainHead]) : null
|
|
1358
|
-
if (mb) {
|
|
1359
|
-
committedFiles = String(runGit(wtGitDir, ['diff', '--name-only', mb, wtHead]) || '')
|
|
1360
|
-
.split('\n').map(p => p.replace(/^"|"$/g, '').replace(/\\/g, '/').trim())
|
|
1361
|
-
.filter(p => p && p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
|
|
1362
|
-
}
|
|
1363
|
-
} catch { /* 已提交补齐失败退回 status-only(fail-open,不拖垮未提交并入) */ }
|
|
1364
|
-
}
|
|
1365
|
-
if (wtFiles.length > 0 || committedFiles.length > 0) {
|
|
1366
|
-
diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...wtFiles, ...committedFiles])]
|
|
1367
|
-
console.log(`[sillyspec] 草稿归属并入 worktree 改动 ${wtFiles.length + committedFiles.length} 个(未提交 ${wtFiles.length} + 已提交 ${committedFiles.length},按 allowed_paths 归属)`)
|
|
1412
|
+
// D-004@v1 口径单一化(坑④ worktree-tick-guard-commit-only,2026-09-15 实证):原手写
|
|
1413
|
+
// 并入段(porcelain 未提交 + committed merge-base 补齐两段)收口为
|
|
1414
|
+
// collectWorktreeChangedFiles 单一真相——勾选守卫 prefetchDiffFileSet(run/complete.js)
|
|
1415
|
+
// 与本草稿归属自此消费同一口径(原「两处口径须同步改」注记就此作废,单一真相在 helper)。
|
|
1416
|
+
// in-place(mergeGitDir===cwd)时 helper 同取 cwd porcelain(现状主仓并入不丢)。
|
|
1417
|
+
// resolveVerifyChangedFiles(verify-postcheck.js)内另有同型补齐段(form A 专属,滤网保
|
|
1418
|
+
// .sillyspec/docs/** 交付物),已知残留 R-09,本次不动。
|
|
1419
|
+
const mergedFiles = collectWorktreeChangedFiles(cwd, changeName, attribution.meta)
|
|
1420
|
+
if (mergedFiles.length > 0) {
|
|
1421
|
+
diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...mergedFiles])]
|
|
1422
|
+
console.log(`[sillyspec] 草稿归属并入 worktree 改动 ${mergedFiles.length} 个(未提交 porcelain + 已提交 merge-base 补齐,按 allowed_paths 归属)`)
|
|
1368
1423
|
}
|
|
1369
1424
|
} catch { /* working-tree 并入失败退回 commit diff 口径(fail-open,不阻断草稿) */ }
|
|
1370
1425
|
}
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* 双层写入模型(D-001@v2):机器数据(probes/tests/factsConsistency)CLI 直写;判断层
|
|
9
9
|
* (结论枚举/证据状态/回执声明)经 verify-result.md 受控槽段录入、--done 时 CLI 解析固化
|
|
10
10
|
* (slot-backfill),单向渲染约束对象是机器数据。
|
|
11
|
+
*
|
|
12
|
+
* PASS 封顶事实面扩展(2026-09-17-pass-cap-semantics task-01 / D-011):db 脚本执行声明的
|
|
13
|
+
* 录入文法(X-03)= 回执槽条目 command 含 db/<file>.sql,或声明行「已对目标库执行:db/<file>.sql」,
|
|
14
|
+
* 解析器 parseDbScriptDeclarations 落 verify-probes.js(producer:backfillFactsFromMdAndTests
|
|
15
|
+
* 产出 facts.dbScriptDeclarations;跨仓消费方:worktree-apply 兜底声明门按「文件集 ∩ db/*.sql
|
|
16
|
+
* ⊆ parseDbScriptDeclarations(verifyMd)」对账)。
|
|
11
17
|
*/
|
|
12
18
|
export const FACTS_SCHEMA_VERSION = 2
|
|
13
19
|
|
|
@@ -52,6 +58,14 @@ const EVIDENCE_LINE_RE = /^- (task-\d+):\s*(satisfied|missing|partial)\b([^\n]*)
|
|
|
52
58
|
*/
|
|
53
59
|
const RECEIPT_LINE_RE = /^- claim:\s*([^||\n]+)[||]\s*command:\s*([^||\n]+)[||]\s*exit:\s*(\d+)\s*[||]\s*log:\s*(.+)$/
|
|
54
60
|
|
|
61
|
+
/**
|
|
62
|
+
* 多行 YAML 形态续行:` key: value`(缩进键值行,2026-09-16-friction5-hardening FR-01 / D-001@v1)。
|
|
63
|
+
* 单行管道形态 agent 手写时列错位/字段序调换/多行书写整行不命中 → 回执槽收 0 条 →
|
|
64
|
+
* integration-critical 误报无绿回执——多行聚合根除形态自由度问题;字段序无关(command 可在
|
|
65
|
+
* claim 前),聚合遇首个不匹配行(下一列表项/非缩进行/空行/标题)即止。
|
|
66
|
+
*/
|
|
67
|
+
const RECEIPT_CONT_LINE_RE = /^[ \t]+([A-Za-z_][\w-]*):\s*(.*)$/
|
|
68
|
+
|
|
55
69
|
/**
|
|
56
70
|
* 解析 verify-result.md 的两个受控槽段(D-001@v2 slot-backfill 录入界面)。
|
|
57
71
|
* 占位形态(<待填:三选一> / <待填:0 或非 0>)不含枚举词,行首锚定 + 枚举词双保险 fail-closed。
|
|
@@ -101,15 +115,47 @@ export function parseEvidenceSlots(mdText) {
|
|
|
101
115
|
|
|
102
116
|
const runtimeEvidence = []
|
|
103
117
|
if (receiptBody) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
// 回执双形态解析(FR-01 / D-001@v1):①单行管道形态 RECEIPT_LINE_RE 命中即收(正则与
|
|
119
|
+
// 字段语义逐字节不变,存量回执零回归);②不命中且行首 `- claim:` 起头时进入多行 YAML
|
|
120
|
+
// 聚合——收集缩进续行(RECEIPT_CONT_LINE_RE)为 key→value(值 trim、剥首尾成对反引号),
|
|
121
|
+
// 首行行内剩余只作 claim 值(claim 含管道符不截断);四字段 = claim/command 非空(trim 后)
|
|
122
|
+
// + exit 纯数字 + log 非空(值内 |/| 尾注取首段,同单行口径),齐才 push——任一缺失/空
|
|
123
|
+
// 整条不收(fail-closed 宁漏不误收,坑 receipt-fullwidth-parse 形态自由度的根除收口)。
|
|
124
|
+
const stripPairedBackticks = (v) => {
|
|
125
|
+
const t = v.trim()
|
|
126
|
+
return t.length >= 2 && t.startsWith('`') && t.endsWith('`') ? t.slice(1, -1) : t
|
|
127
|
+
}
|
|
128
|
+
for (let i = 0; i < receiptBody.length; i++) {
|
|
129
|
+
const m = receiptBody[i].match(RECEIPT_LINE_RE)
|
|
130
|
+
if (m) {
|
|
131
|
+
runtimeEvidence.push({
|
|
132
|
+
claim: m[1].trim(),
|
|
133
|
+
command: m[2].trim(),
|
|
134
|
+
exitCode: Number(m[3]),
|
|
135
|
+
logPath: m[4].split(/[||]/)[0].trim().replace(/^`|`$/g, ''),
|
|
136
|
+
})
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
const head = receiptBody[i].match(/^-\s*claim:\s*(.*)$/)
|
|
140
|
+
if (!head) continue
|
|
141
|
+
const fields = {}
|
|
142
|
+
let j = i + 1
|
|
143
|
+
while (j < receiptBody.length) {
|
|
144
|
+
const cm = receiptBody[j].match(RECEIPT_CONT_LINE_RE)
|
|
145
|
+
if (!cm) break
|
|
146
|
+
if (!(cm[1] in fields)) fields[cm[1]] = stripPairedBackticks(cm[2])
|
|
147
|
+
j++
|
|
148
|
+
}
|
|
149
|
+
i = j - 1 // 聚合消费至 j-1,回退一格交还外层 for 推进
|
|
150
|
+
const claim = [head[1].trim(), typeof fields.claim === 'string' ? fields.claim.trim() : '']
|
|
151
|
+
.filter(s => s.length > 0).join(' ').trim()
|
|
152
|
+
const command = typeof fields.command === 'string' ? fields.command : ''
|
|
153
|
+
const exit = typeof fields.exit === 'string' ? fields.exit.trim() : ''
|
|
154
|
+
const log = typeof fields.log === 'string'
|
|
155
|
+
? fields.log.split(/[||]/)[0].trim().replace(/^`|`$/g, '')
|
|
156
|
+
: ''
|
|
157
|
+
if (!claim || !command || !/^\d+$/.test(exit) || !log) continue
|
|
158
|
+
runtimeEvidence.push({ claim, command, exitCode: Number(exit), logPath: log })
|
|
113
159
|
}
|
|
114
160
|
}
|
|
115
161
|
|
|
@@ -144,5 +190,28 @@ export function validateFactsV2(facts) {
|
|
|
144
190
|
errors.push(`requiredEvidence[${item.task}] satisfied 但 verifiedFiles 为空`)
|
|
145
191
|
}
|
|
146
192
|
}
|
|
193
|
+
// PASS 封顶事实面五字段 additive 登记(2026-09-17-pass-cap-semantics task-01 / D-011 故障面):
|
|
194
|
+
// 字段不在场一律不报错(存量 facts 零迁移通过,producer=backfillFactsFromMdAndTests 首次
|
|
195
|
+
// backfill 产出);在场才校验类型/枚举。schemaVersion 保持 2。
|
|
196
|
+
if (facts.integrationRan != null && !['ran', 'not-ran'].includes(facts.integrationRan)) {
|
|
197
|
+
errors.push(`integrationRan 非法枚举值:${facts.integrationRan}`)
|
|
198
|
+
}
|
|
199
|
+
if (facts.dbScriptDeclarations != null
|
|
200
|
+
&& (!Array.isArray(facts.dbScriptDeclarations) || !facts.dbScriptDeclarations.every(x => typeof x === 'string'))) {
|
|
201
|
+
errors.push('dbScriptDeclarations 应为 string[]')
|
|
202
|
+
}
|
|
203
|
+
if (facts.matrixPartialRows != null
|
|
204
|
+
&& (!Number.isInteger(facts.matrixPartialRows) || facts.matrixPartialRows < 0)) {
|
|
205
|
+
errors.push(`matrixPartialRows 应为非负整数,实际 ${facts.matrixPartialRows}`)
|
|
206
|
+
}
|
|
207
|
+
if (facts.runtimeEndpointExcluded != null && typeof facts.runtimeEndpointExcluded !== 'boolean') {
|
|
208
|
+
errors.push(`runtimeEndpointExcluded 应为 boolean,实际 ${typeof facts.runtimeEndpointExcluded}`)
|
|
209
|
+
}
|
|
210
|
+
const handoverItems = (facts.handover && Array.isArray(facts.handover.items)) ? facts.handover.items : []
|
|
211
|
+
for (const [i, item] of handoverItems.entries()) {
|
|
212
|
+
if (item && item.severity != null && !['blocking', 'advisory'].includes(item.severity)) {
|
|
213
|
+
errors.push(`handover.items[${i}] severity 非法枚举值:${item.severity}`)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
147
216
|
return { ok: errors.length === 0, errors }
|
|
148
217
|
}
|