sillyspec 3.33.0 → 3.33.2
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-flow/SKILL.md +18 -10
- package/.github/workflows/ci.yml +11 -3
- package/AGENTS.md +3 -3
- package/README.md +2 -2
- package/assets/command-cards/flow.md +9 -5
- package/package.json +1 -1
- package/src/archive-delta.js +4 -3
- package/src/change-delete.js +6 -5
- package/src/contract-matrix.js +2 -1
- package/src/cross-placement.js +5 -1
- package/src/cross-repo-reconcile.js +4 -1
- package/src/db.js +8 -2
- package/src/decision-distill.js +22 -8
- package/src/decision-values.js +34 -0
- package/src/decisions-io.js +5 -2
- package/src/deleted-ledger.js +2 -1
- package/src/doctor-diagnostics.js +8 -3
- package/src/flow-draft.js +17 -10
- package/src/flow-parity.js +93 -5
- package/src/flow-review-pack.js +183 -0
- package/src/flow-review.js +45 -3
- package/src/flow.js +365 -109
- 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 +62 -21
- package/src/init.js +7 -30
- package/src/knowledge-hits.js +2 -1
- package/src/knowledge-vector.js +2 -1
- 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 +14 -12
- 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 +102 -23
- package/src/run/gates.js +32 -8
- package/src/run/prompt.js +6 -5
- 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 +11 -5
- package/src/runtime-paths.js +189 -0
- package/src/scan-refresh.js +4 -3
- package/src/scope-audit.js +13 -7
- package/src/sentinel-assertions.js +37 -0
- package/src/spec-sync.js +38 -22
- package/src/stage-contract.js +24 -2
- package/src/stage-review.js +3 -2
- package/src/stage-templates.js +3 -2
- package/src/stages/brainstorm.js +6 -5
- 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 +45 -1
- package/src/validate-artifacts.js +2 -1
- package/src/verify-draft.js +2 -1
- package/src/verify-postcheck.js +11 -10
- package/src/verify-probes.js +157 -52
- package/src/watcher.js +15 -9
- package/src/worktree-apply.js +2 -1
- package/src/write-audit.js +2 -1
- package/templates/agents-instruction.md +3 -3
package/src/spec-sync.js
CHANGED
|
@@ -19,6 +19,7 @@ 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';
|
|
22
23
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
23
24
|
|
|
24
25
|
// 与 sillyhub-daemon/src/spec-sync.ts 共用排除口径(task-07 / FR-06 / D-008@v2):
|
|
@@ -69,7 +70,7 @@ export function describeSyncError(err) {
|
|
|
69
70
|
// 才拿到),且失败走「下次自动重试」无限静默循环——归档半态(本地已归档、平台仍「进行中」)
|
|
70
71
|
// 挂数小时无人知晓。两道护栏:①非 2xx 响应体解析进告警行;②按 label 连续失败计数持久化,
|
|
71
72
|
// 连续 ≥3 次升级 🚨 显式告警,任一次成功清零。
|
|
72
|
-
const FAIL_STREAK_FILE = join('.runtime', 'spec-sync-fail-streak.json');
|
|
73
|
+
const FAIL_STREAK_FILE = join('.runtime', 'state', 'spec-sync-fail-streak.json');
|
|
73
74
|
const FAIL_STREAK_ESCALATE_AT = 3;
|
|
74
75
|
|
|
75
76
|
export function describeErrorBody(status, rawBody) {
|
|
@@ -387,7 +388,7 @@ const LAST_SUCCESS_MARKER = 'spec-sync-last-success.json';
|
|
|
387
388
|
|
|
388
389
|
function readLastSyncTs(specRoot) {
|
|
389
390
|
try {
|
|
390
|
-
const raw = readFileSync(join(specRoot, '.runtime', LAST_SUCCESS_MARKER), 'utf8');
|
|
391
|
+
const raw = readFileSync(runtimeStatePath(join(specRoot, '.runtime'), LAST_SUCCESS_MARKER), 'utf8');
|
|
391
392
|
const ts = JSON.parse(raw).ts;
|
|
392
393
|
return Number.isFinite(ts) ? ts : null;
|
|
393
394
|
} catch {
|
|
@@ -398,7 +399,7 @@ function readLastSyncTs(specRoot) {
|
|
|
398
399
|
function writeLastSyncTs(specRoot) {
|
|
399
400
|
try {
|
|
400
401
|
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
401
|
-
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');
|
|
402
403
|
} catch { /* 标记失败只损失下次的防回推过滤,不影响本次同步结果 */ }
|
|
403
404
|
}
|
|
404
405
|
|
|
@@ -415,7 +416,7 @@ const BASE_SNAPSHOT = 'spec-sync-base.json';
|
|
|
415
416
|
|
|
416
417
|
function readBaseSnapshot(specRoot) {
|
|
417
418
|
try {
|
|
418
|
-
const raw = readFileSync(join(specRoot, '.runtime', BASE_SNAPSHOT), 'utf8');
|
|
419
|
+
const raw = readFileSync(runtimeStatePath(join(specRoot, '.runtime'), BASE_SNAPSHOT), 'utf8');
|
|
419
420
|
const hashes = JSON.parse(raw).hashes;
|
|
420
421
|
return hashes && typeof hashes === 'object' ? hashes : null;
|
|
421
422
|
} catch {
|
|
@@ -428,7 +429,7 @@ function writeBaseSnapshot(specRoot, localFiles) {
|
|
|
428
429
|
const hashes = {};
|
|
429
430
|
for (const f of localFiles) hashes[f.path] = f.hash;
|
|
430
431
|
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
431
|
-
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');
|
|
432
433
|
} catch { /* 快照失败只损失下次的内容级防回推,不影响本次同步结果 */ }
|
|
433
434
|
}
|
|
434
435
|
|
|
@@ -497,7 +498,7 @@ export function partitionConflictPaths(serverVersions, localFiles, baseHashes, l
|
|
|
497
498
|
*/
|
|
498
499
|
function isSamePendingSpecConflict(specRoot, changeName, realPaths) {
|
|
499
500
|
try {
|
|
500
|
-
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');
|
|
501
502
|
const prev = JSON.parse(raw).conflicting_paths;
|
|
502
503
|
if (!Array.isArray(prev) || prev.length !== realPaths.length) return false;
|
|
503
504
|
const prevSet = new Set(prev);
|
|
@@ -514,7 +515,7 @@ function isSamePendingSpecConflict(specRoot, changeName, realPaths) {
|
|
|
514
515
|
*/
|
|
515
516
|
function clearSpecConflictMarker(specRoot, changeName) {
|
|
516
517
|
try {
|
|
517
|
-
const p = join(specRoot, '.runtime', `spec-sync-conflict-${changeName}.json`);
|
|
518
|
+
const p = runtimeStatePath(join(specRoot, '.runtime'), `spec-sync-conflict-${changeName}.json`);
|
|
518
519
|
if (existsSync(p)) unlinkSync(p);
|
|
519
520
|
} catch { /* 清理失败不影响同步结果 */ }
|
|
520
521
|
}
|
|
@@ -553,7 +554,7 @@ export function writeTombstoneAttribution(specRoot, platformDeleted) {
|
|
|
553
554
|
const byChange = extractTombstonedChanges(platformDeleted);
|
|
554
555
|
const written = [];
|
|
555
556
|
for (const [name, paths] of byChange) {
|
|
556
|
-
const filePath = join(specRoot, '.runtime', `spec-sync-conflict-${name}.json`);
|
|
557
|
+
const filePath = runtimeStatePath(join(specRoot, '.runtime'), `spec-sync-conflict-${name}.json`);
|
|
557
558
|
let prev = null;
|
|
558
559
|
try { prev = JSON.parse(readFileSync(filePath, 'utf8')); } catch { /* 首见 */ }
|
|
559
560
|
const record = {
|
|
@@ -584,13 +585,17 @@ export function writeTombstoneAttribution(specRoot, platformDeleted) {
|
|
|
584
585
|
* 清理条数。挂在两个全绿点:POST 成功无冲突分支与无差异跳过分支。
|
|
585
586
|
*/
|
|
586
587
|
export function clearStaleTombstoneMarkers(specRoot) {
|
|
588
|
+
// 2026-10-18 布局分层:冲突文件落 state/;根目录扫描保留旧布局兜底
|
|
587
589
|
const runtimeDir = join(specRoot, '.runtime');
|
|
588
590
|
let files = [];
|
|
589
|
-
|
|
591
|
+
const conflictDirs = [join(runtimeDir, 'state'), runtimeDir]
|
|
590
592
|
let cleared = 0;
|
|
591
|
-
for (const
|
|
593
|
+
for (const dir of conflictDirs) {
|
|
594
|
+
let dirFiles = [];
|
|
595
|
+
try { dirFiles = readdirSync(dir); } catch { continue; }
|
|
596
|
+
for (const f of dirFiles) {
|
|
592
597
|
if (!f.startsWith('spec-sync-conflict-') || !f.endsWith('.json')) continue;
|
|
593
|
-
const p = join(
|
|
598
|
+
const p = join(dir, f);
|
|
594
599
|
try {
|
|
595
600
|
const j = JSON.parse(readFileSync(p, 'utf8'));
|
|
596
601
|
const cp = Array.isArray(j.conflicting_paths) ? j.conflicting_paths : null;
|
|
@@ -601,6 +606,7 @@ export function clearStaleTombstoneMarkers(specRoot) {
|
|
|
601
606
|
}
|
|
602
607
|
} catch { /* 损坏文件不动 */ }
|
|
603
608
|
}
|
|
609
|
+
}
|
|
604
610
|
if (cleared > 0) {
|
|
605
611
|
console.log(`[spec-sync] 全绿同步清理 ${cleared} 条陈旧墓碑冲突记录(被删变更已收敛)`);
|
|
606
612
|
}
|
|
@@ -624,7 +630,7 @@ const FOLLOW_REPORT_MARKER = 'spec-sync-follow-reported.json';
|
|
|
624
630
|
*/
|
|
625
631
|
export function isFollowerSetChanged(specRoot, key, list) {
|
|
626
632
|
const sorted = [...(list || [])].sort();
|
|
627
|
-
const p = join(specRoot, '.runtime', FOLLOW_REPORT_MARKER);
|
|
633
|
+
const p = runtimeStatePath(join(specRoot, '.runtime'), FOLLOW_REPORT_MARKER);
|
|
628
634
|
let cur = {};
|
|
629
635
|
try { cur = JSON.parse(readFileSync(p, 'utf8')) || {}; } catch { /* 无 marker = 首报 */ }
|
|
630
636
|
const prev = Array.isArray(cur[key]) ? cur[key] : [];
|
|
@@ -665,10 +671,20 @@ function attributeChangeLabel(paths) {
|
|
|
665
671
|
return names.size === 1 ? [...names][0] : 'full-tree'
|
|
666
672
|
}
|
|
667
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
|
+
*/
|
|
668
684
|
export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
669
685
|
if (!platform || !platform.url || !platform.token) {
|
|
670
686
|
debugLog('[spec-sync] 未连接平台(本地合法状态);跳过 spec 树增量同步');
|
|
671
|
-
return { synced: 0 };
|
|
687
|
+
return { synced: 0, ok: true };
|
|
672
688
|
}
|
|
673
689
|
// 全树入口(platform sync 无 --change,2026-10-11-sync-conflict-tri-fix FR-03)
|
|
674
690
|
// changeName=null:日志用 full-tree 标签,冲突归因见下方 conflictLabel。
|
|
@@ -705,7 +721,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
705
721
|
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
706
722
|
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
707
723
|
}
|
|
708
|
-
return { synced: 0 };
|
|
724
|
+
return { synced: 0, ok: false };
|
|
709
725
|
}
|
|
710
726
|
const body = await res.json().catch(() => ({}));
|
|
711
727
|
serverManifest = body.files || {};
|
|
@@ -716,7 +732,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
716
732
|
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
717
733
|
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
718
734
|
}
|
|
719
|
-
return { synced: 0 };
|
|
735
|
+
return { synced: 0, ok: false };
|
|
720
736
|
}
|
|
721
737
|
|
|
722
738
|
// 2. walk/hash/diff
|
|
@@ -758,7 +774,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
758
774
|
debugLog(`[spec-sync] 无差异,跳过同步: ${logLabel}`);
|
|
759
775
|
clearStaleTombstoneMarkers(specRoot);
|
|
760
776
|
clearFailStreak(specRoot, logLabel);
|
|
761
|
-
return { synced: 0 };
|
|
777
|
+
return { synced: 0, ok: true };
|
|
762
778
|
}
|
|
763
779
|
|
|
764
780
|
// 3. POST ops
|
|
@@ -782,7 +798,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
782
798
|
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
783
799
|
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
784
800
|
}
|
|
785
|
-
return { synced: 0 };
|
|
801
|
+
return { synced: 0, ok: false };
|
|
786
802
|
}
|
|
787
803
|
const body = await res.json().catch(() => ({}));
|
|
788
804
|
if (body.conflict) {
|
|
@@ -843,7 +859,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
843
859
|
debugLog(`[spec-sync] ${followers.length} 个本地未改动文件自动跟随服务器: ${followers.slice(0, 5).join(', ')}${followers.length > 5 ? ' 等' : ''}`);
|
|
844
860
|
}
|
|
845
861
|
console.warn(`⚠️ [spec-sync] 变更 ${conflictLabel} 存在未决 spec 树冲突(${realPaths.length} 个文件待裁决,本轮非冲突改动已照常同步)。处理:sillyspec platform resolve ${conflictLabel} --keep-local | --take-platform | --abort`);
|
|
846
|
-
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 };
|
|
847
863
|
}
|
|
848
864
|
if (followers.length > 0) {
|
|
849
865
|
console.warn(`[spec-sync] ${followers.length} 个本地未改动文件自动跟随服务器(正常多端前进,无需人工裁决): ${followers.slice(0, 5).join(', ')}${followers.length > 5 ? ' 等' : ''}`);
|
|
@@ -855,13 +871,13 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
855
871
|
noteSyncSuccess();
|
|
856
872
|
clearFailStreak(specRoot, logLabel);
|
|
857
873
|
console.log(`[spec-sync] 冲突已自动消解(0 个需人工裁决;本会话改动 ${ops.length - followers.length > 0 ? '已同步' : '无差异'}): ${conflictLabel}`);
|
|
858
|
-
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 };
|
|
859
875
|
}
|
|
860
876
|
let conflictPath = null;
|
|
861
877
|
try {
|
|
862
878
|
const runtimeDir = join(specRoot, '.runtime');
|
|
863
879
|
mkdirSync(runtimeDir, { recursive: true });
|
|
864
|
-
conflictPath =
|
|
880
|
+
conflictPath = runtimeStatePath(runtimeDir, `spec-sync-conflict-${conflictLabel}.json`);
|
|
865
881
|
writeFileSync(conflictPath, JSON.stringify({
|
|
866
882
|
change: conflictLabel,
|
|
867
883
|
kind: 'spec-tree',
|
|
@@ -901,7 +917,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
901
917
|
clearStaleTombstoneMarkers(specRoot);
|
|
902
918
|
writeLastSyncTs(specRoot);
|
|
903
919
|
writeBaseSnapshot(specRoot, localFiles);
|
|
904
|
-
return { synced: ops.length };
|
|
920
|
+
return { synced: ops.length, ok: true };
|
|
905
921
|
} catch (err) {
|
|
906
922
|
const failCount = bumpFailStreak(specRoot, logLabel, describeSyncError(err));
|
|
907
923
|
const line = `[spec-sync] 同步异常(文件树本次未同步,下次自动重试): ${logLabel}: ${describeSyncError(err)}`;
|
|
@@ -909,7 +925,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
909
925
|
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
910
926
|
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
911
927
|
}
|
|
912
|
-
return { synced: 0 };
|
|
928
|
+
return { synced: 0, ok: false };
|
|
913
929
|
}
|
|
914
930
|
}
|
|
915
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'
|
|
@@ -364,7 +365,7 @@ export function generateStageReviewRunId() {
|
|
|
364
365
|
*/
|
|
365
366
|
export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
|
|
366
367
|
const suffix = changeName ? `${stage}-${changeName}` : stage
|
|
367
|
-
return
|
|
368
|
+
return runtimeStatePath(runtimeRoot, `current-stage-review-run-id-${suffix}`)
|
|
368
369
|
}
|
|
369
370
|
|
|
370
371
|
/**
|
|
@@ -853,7 +854,7 @@ export function printStageReviewResult(result, context = {}) {
|
|
|
853
854
|
// fail-open:无 changeName / 无记录 / 读失败 → 零输出(旧口径)。
|
|
854
855
|
if (changeName && runtimeRoot) {
|
|
855
856
|
try {
|
|
856
|
-
const recPath =
|
|
857
|
+
const recPath = runtimeStatePath(runtimeRoot, `review-dispatch-${changeName}.json`)
|
|
857
858
|
if (existsSync(recPath)) {
|
|
858
859
|
const rec = JSON.parse(readFileSync(recPath, 'utf8'))
|
|
859
860
|
if (rec && typeof rec === 'object' && (rec.state === 'in-flight' || rec.state === 'dispatching')) {
|
package/src/stage-templates.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
18
18
|
import { join } from 'path'
|
|
19
|
+
import { DECISION_STATUSES, DECISION_TYPES } from './decision-values.js'
|
|
19
20
|
|
|
20
21
|
/** brainstorm 四件套产物格式模板(自 stages/brainstorm.js「生成规范文件」步逐字迁移——
|
|
21
22
|
* 与 prompt 指针版本同源;decisions 九字段与可选字段说明一并迁入)。 */
|
|
@@ -110,9 +111,9 @@ created_at: <now-datetime>
|
|
|
110
111
|
# 决策记录(Decisions)
|
|
111
112
|
|
|
112
113
|
## D-001@v1: 决策短标题
|
|
113
|
-
- type:
|
|
114
|
+
- type: ${DECISION_TYPES.join(' | ')}
|
|
114
115
|
- priority: P0 | P1 | P2
|
|
115
|
-
- status:
|
|
116
|
+
- status: ${DECISION_STATUSES.join(' | ')}
|
|
116
117
|
- supersedes:
|
|
117
118
|
- source: user | code | docs
|
|
118
119
|
- question: 被解决的问题
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { REVIEW_CHECKLISTS } from '../stage-review-checklist.js'
|
|
2
|
+
import { DECISION_STATUSES, DECISION_TYPES } from '../decision-values.js'
|
|
2
3
|
|
|
3
4
|
export const definition = {
|
|
4
5
|
name: 'brainstorm',
|
|
@@ -58,7 +59,7 @@ export const definition = {
|
|
|
58
59
|
- **明显 small**(满足:单上下文可吞吐的纯执行类——改文案/修 bug/样式调整/配置微调,预计无 Wave 编排/上下文分片需求)→ 输出:「此变更规模较小,建议直接走轻量变更」+ 一句依据,给出可照抄建议命令(--input 引号内换行合法):
|
|
59
60
|
|
|
60
61
|
\`\`\`
|
|
61
|
-
sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
62
|
+
sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景
|
|
62
63
|
|
|
63
64
|
成功标准:
|
|
64
65
|
- <可验证标准>"
|
|
@@ -192,9 +193,9 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(
|
|
|
192
193
|
|
|
193
194
|
\`\`\`markdown
|
|
194
195
|
## D-001@v1: <短标题>
|
|
195
|
-
- type:
|
|
196
|
+
- type: ${DECISION_TYPES.join(' | ')}
|
|
196
197
|
- priority: P0 | P1 | P2
|
|
197
|
-
- status:
|
|
198
|
+
- status: ${DECISION_STATUSES.join(' | ')}
|
|
198
199
|
- source: user | code | docs
|
|
199
200
|
- question: <被解决的问题>
|
|
200
201
|
- answer: <用户确认或代码查证结果>
|
|
@@ -525,9 +526,9 @@ ${REVIEW_CHECKLISTS.brainstorm.slice(3).map((item) => '- ' + item).join('\n')}
|
|
|
525
526
|
### decisions.md 版本规则
|
|
526
527
|
\`\`\`markdown
|
|
527
528
|
## D-001@v2: 缓存异常时的 fallback 语义
|
|
528
|
-
- type:
|
|
529
|
+
- type: ${DECISION_TYPES.join(' | ')}
|
|
529
530
|
- priority: P0 | P1 | P2
|
|
530
|
-
- status:
|
|
531
|
+
- status: ${DECISION_STATUSES.join(' | ')}
|
|
531
532
|
- supersedes: D-001@v1
|
|
532
533
|
- source: design-grill
|
|
533
534
|
- question: §3 数据流与 §7 容错策略冲突时以哪个为准?
|
package/src/sync-noise.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* 调用方传 noMute 绕过。
|
|
25
25
|
*/
|
|
26
26
|
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs'
|
|
27
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
27
28
|
import { join } from 'path'
|
|
28
29
|
|
|
29
30
|
const NOISE_MARKER = 'sync-noise-mute.json'
|
|
@@ -254,7 +255,7 @@ let _openedSelfHealByThisProcess = new Set()
|
|
|
254
255
|
const _seenSelfHealInProcess = new Set()
|
|
255
256
|
|
|
256
257
|
function _readSelfHealWindows() {
|
|
257
|
-
const p = _runtimeDir ?
|
|
258
|
+
const p = _runtimeDir ? runtimeStatePath(_runtimeDir, SELF_HEAL_MARKER) : null
|
|
258
259
|
if (!p) return null
|
|
259
260
|
try {
|
|
260
261
|
const s = JSON.parse(readFileSync(p, 'utf8'))
|
|
@@ -279,7 +280,7 @@ function _selfHealLineVisible(changeName, kind, noMute) {
|
|
|
279
280
|
next[key] = now + MUTE_WINDOW_MS
|
|
280
281
|
try {
|
|
281
282
|
mkdirSync(_runtimeDir, { recursive: true })
|
|
282
|
-
writeFileSync(
|
|
283
|
+
writeFileSync(runtimeStatePath(_runtimeDir, SELF_HEAL_MARKER), JSON.stringify(next) + '\n', 'utf8')
|
|
283
284
|
} catch { /* marker 写失败 = 闸门失效退化为直通,仅损失降噪 */ }
|
|
284
285
|
_openedSelfHealByThisProcess.add(key)
|
|
285
286
|
return true
|
package/src/sync.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync, chmodSync, rmSync, statSync } from 'fs';
|
|
12
12
|
import { createHash } from 'crypto';
|
|
13
13
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
14
|
+
import { runtimeStatePath } from './runtime-paths.js';
|
|
14
15
|
import { join, dirname, basename, isAbsolute, relative, resolve as pathResolve } from 'path';
|
|
15
16
|
import { homedir } from 'os';
|
|
16
17
|
import { resolvePlatformSpecDir } from './progress.js';
|
|
@@ -718,7 +719,19 @@ export class SyncManager {
|
|
|
718
719
|
console.warn(`⚠️ [sync] 变更 ${changeName} 的未决冲突已过期(base_ts ≥ 平台 ts=${stalePlatTs}),自动清除并恢复推送`);
|
|
719
720
|
} else {
|
|
720
721
|
console.warn(`⚠️ [sync] 变更 ${changeName} 存在未决平台冲突,跳过自动推送。处理:sillyspec platform resolve ${changeName} --keep-local | --take-platform | --abort`);
|
|
721
|
-
|
|
722
|
+
// spec 树与 progress 六表是独立通道(独立端点/独立冲突文件):progress 冲突抑制
|
|
723
|
+
// 只应封锁六表上行,不应连带封锁 spec 树——否则删除 op 的唯一上行通道被冲突文件
|
|
724
|
+
// 长期封锁(2026-10-16 下游实证:409 窗口内本地删除的任务卡在服务器留存,后续整树
|
|
725
|
+
// 快照拉取把它们写回本地=复活,FR-01 上游成因)。补跑 best-effort:失败不改本分支
|
|
726
|
+
// 既有的 conflict/suppressed 返回语义(spec 树自身有失败可见性与下轮重试)。
|
|
727
|
+
let specTreeErrors = [];
|
|
728
|
+
try {
|
|
729
|
+
await syncSpecTree(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), this._getPlatform(), changeName, { signal: opts.signal });
|
|
730
|
+
} catch (err) {
|
|
731
|
+
specTreeErrors = [`spec 树补跑失败(不影响抑制语义): ${err.message}`];
|
|
732
|
+
debugLog(`[sync] 冲突抑制期间 spec 树同步异常: ${changeName}: ${err.message}`);
|
|
733
|
+
}
|
|
734
|
+
return { synced: 0, errors: [`conflict pending: ${changeName}`, ...specTreeErrors], conflict: true, suppressed: true, specTreeAttempted: true };
|
|
722
735
|
}
|
|
723
736
|
}
|
|
724
737
|
}
|
|
@@ -904,8 +917,14 @@ export class SyncManager {
|
|
|
904
917
|
// 树根与进度锚点同源(safePlatformSpecDir,平台模式 → specRoot):此前硬编码
|
|
905
918
|
// cwd/.sillyspec,平台模式下该目录只有 local.yaml(被 walk 排除)→ 本地树恒空
|
|
906
919
|
// → 服务器清单全量 delete(BUG-01,computeSpecOps 护栏为第二道防线)。
|
|
920
|
+
// treeOk 透传(2026-10-17-archive-half-state-location-stuck-active):树通道失败
|
|
921
|
+
// 不影响进度上行返回值(既有语义不变),但结果对象必须如实携带——终态强校验
|
|
922
|
+
// (triggerSyncTerminalStrict)按 treeOk 判「平台 location 是否收敛」,吞进
|
|
923
|
+
// debugLog 会让归档半态(status=archived + location=active)静默存活。
|
|
924
|
+
let treeOk = false;
|
|
907
925
|
try {
|
|
908
|
-
await syncSpecTree(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), this._getPlatform(), changeName, { signal: opts.signal });
|
|
926
|
+
const treeResult = await syncSpecTree(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), this._getPlatform(), changeName, { signal: opts.signal });
|
|
927
|
+
treeOk = treeResult && treeResult.ok === true;
|
|
909
928
|
} catch (err) {
|
|
910
929
|
debugLog(`[sync] spec 树增量同步失败(不影响进度): ${changeName}: ${err.message}`);
|
|
911
930
|
}
|
|
@@ -922,7 +941,7 @@ export class SyncManager {
|
|
|
922
941
|
}
|
|
923
942
|
}
|
|
924
943
|
|
|
925
|
-
return { synced: 1, errors: [] };
|
|
944
|
+
return { synced: 1, errors: [], treeOk };
|
|
926
945
|
}
|
|
927
946
|
|
|
928
947
|
if (res.status !== 409) {
|
|
@@ -1293,7 +1312,7 @@ export class SyncManager {
|
|
|
1293
1312
|
.digest('hex');
|
|
1294
1313
|
} catch { /* 指纹失败按有变化处理 */ }
|
|
1295
1314
|
if (!manual && docsFingerprint) {
|
|
1296
|
-
const markerPath = join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), '.runtime', `sync-docs-lastpush-${changeName}.json`);
|
|
1315
|
+
const markerPath = runtimeStatePath(join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), '.runtime'), `sync-docs-lastpush-${changeName}.json`);
|
|
1297
1316
|
try {
|
|
1298
1317
|
const prev = JSON.parse(readFileSync(markerPath, 'utf8'));
|
|
1299
1318
|
if (prev && prev.hash === docsFingerprint) {
|
|
@@ -1323,7 +1342,8 @@ export class SyncManager {
|
|
|
1323
1342
|
try {
|
|
1324
1343
|
const runtimeDir = join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), '.runtime');
|
|
1325
1344
|
mkdirSync(runtimeDir, { recursive: true });
|
|
1326
|
-
|
|
1345
|
+
try { mkdirSync(join(runtimeDir, 'state'), { recursive: true }); } catch { /* marker fail-open */ }
|
|
1346
|
+
writeFileSync(runtimeStatePath(runtimeDir, `sync-docs-lastpush-${changeName}.json`), JSON.stringify({ hash: docsFingerprint, at: Date.now() }) + '\n', 'utf8');
|
|
1327
1347
|
} catch { /* marker 失败只损失去重(下轮多推一次),不影响同步 */ }
|
|
1328
1348
|
}
|
|
1329
1349
|
console.log(`[sync] 已同步 ${syncedCount} 个文档: ${changeName}`);
|
|
@@ -1509,7 +1529,7 @@ export class SyncManager {
|
|
|
1509
1529
|
if (!specDir) return null;
|
|
1510
1530
|
const runtimeDir = join(specDir, '.runtime');
|
|
1511
1531
|
if (!existsSync(runtimeDir)) mkdirSync(runtimeDir, { recursive: true });
|
|
1512
|
-
const conflictPath =
|
|
1532
|
+
const conflictPath = runtimeStatePath(runtimeDir, `sync-conflict-${changeName}.json`);
|
|
1513
1533
|
const payload = {
|
|
1514
1534
|
change: changeName,
|
|
1515
1535
|
base_ts: info.base_ts ?? null,
|
|
@@ -1530,7 +1550,7 @@ export class SyncManager {
|
|
|
1530
1550
|
readConflictFile(changeName) {
|
|
1531
1551
|
const specDir = safePlatformSpecDir(this.cwd);
|
|
1532
1552
|
if (!specDir) return null;
|
|
1533
|
-
const conflictPath = join(specDir, '.runtime', `sync-conflict-${changeName}.json`);
|
|
1553
|
+
const conflictPath = runtimeStatePath(join(specDir, '.runtime'), `sync-conflict-${changeName}.json`);
|
|
1534
1554
|
if (!existsSync(conflictPath)) return null;
|
|
1535
1555
|
try {
|
|
1536
1556
|
return JSON.parse(readFileSync(conflictPath, 'utf8'));
|
|
@@ -1545,7 +1565,7 @@ export class SyncManager {
|
|
|
1545
1565
|
readSpecConflictFile(changeName) {
|
|
1546
1566
|
const specDir = safePlatformSpecDir(this.cwd);
|
|
1547
1567
|
if (!specDir) return null;
|
|
1548
|
-
const p = join(specDir, '.runtime', `spec-sync-conflict-${changeName}.json`);
|
|
1568
|
+
const p = runtimeStatePath(join(specDir, '.runtime'), `spec-sync-conflict-${changeName}.json`);
|
|
1549
1569
|
if (!existsSync(p)) return null;
|
|
1550
1570
|
try {
|
|
1551
1571
|
return JSON.parse(readFileSync(p, 'utf8'));
|
|
@@ -1558,7 +1578,7 @@ export class SyncManager {
|
|
|
1558
1578
|
clearSpecConflictFile(changeName) {
|
|
1559
1579
|
const specDir = safePlatformSpecDir(this.cwd);
|
|
1560
1580
|
if (!specDir) return false;
|
|
1561
|
-
const p = join(specDir, '.runtime', `spec-sync-conflict-${changeName}.json`);
|
|
1581
|
+
const p = runtimeStatePath(join(specDir, '.runtime'), `spec-sync-conflict-${changeName}.json`);
|
|
1562
1582
|
if (!existsSync(p)) return false;
|
|
1563
1583
|
try { unlinkSync(p); return true; }
|
|
1564
1584
|
catch { return false; }
|
|
@@ -1571,7 +1591,7 @@ export class SyncManager {
|
|
|
1571
1591
|
clearConflictFile(changeName) {
|
|
1572
1592
|
const specDir = safePlatformSpecDir(this.cwd);
|
|
1573
1593
|
if (!specDir) return false;
|
|
1574
|
-
const conflictPath = join(specDir, '.runtime', `sync-conflict-${changeName}.json`);
|
|
1594
|
+
const conflictPath = runtimeStatePath(join(specDir, '.runtime'), `sync-conflict-${changeName}.json`);
|
|
1575
1595
|
if (!existsSync(conflictPath)) return false;
|
|
1576
1596
|
try { unlinkSync(conflictPath); return true; }
|
|
1577
1597
|
catch { return false; }
|
|
@@ -1881,6 +1901,30 @@ export class SyncManager {
|
|
|
1881
1901
|
// ①rm 前先把内容原子写到树外 `<specDir 根名>.local.yaml.bak`(树内会被 rm 一并删掉,
|
|
1882
1902
|
// 故放父目录);②恢复主路径不变,失败时从 .bak 再兜底复制一次;③成功路径末尾
|
|
1883
1903
|
// 清理 .bak(明文 token 不留第二份;顺带清历史失败残留)。
|
|
1904
|
+
// 防复活抑制(坑 spec-tree-file-resurrect,2026-10-16 下游实证):progress 冲突/熔断
|
|
1905
|
+
// 窗口内本地删除的文件(删除 op 未上行成功,服务器清单仍含它)会在整树快照回放时被
|
|
1906
|
+
// 服务器旧内容写回=复活(幽灵任务卡 + 验证误报「文件未清理」)。抑制判据无状态现算:
|
|
1907
|
+
// 基线快照(.runtime/spec-sync-base.json = 上次成功同步时刻的本地文件全集,语义见
|
|
1908
|
+
// spec-sync.js BASE_SNAPSHOT)有、当前盘面无 = 本地删除,回放时跳过这些条目;删除 op
|
|
1909
|
+
// 后续上行成功 → 服务器清单不再含该路径 → bundle 不再携带 → 抑制面自然衰减。
|
|
1910
|
+
// 基线在下方 rm 时会被连带删除(bundle 恒不含 .runtime),故必须在 rm 前读取。
|
|
1911
|
+
// restoreDeleted: true = 用户显式意志优先,跳过抑制(恢复纯快照回放语义)。
|
|
1912
|
+
const suppressResurrect = new Set();
|
|
1913
|
+
const resurrectSkipped = []; // 被防复活抑制挡下的条目(suppressResurrect 命中面;try 外可见供返回值携带)
|
|
1914
|
+
if (!opts.restoreDeleted) {
|
|
1915
|
+
try {
|
|
1916
|
+
const baseRaw = JSON.parse(readFileSync(runtimeStatePath(join(specDir, '.runtime'), 'spec-sync-base.json'), 'utf8'));
|
|
1917
|
+
const baseHashes = baseRaw && typeof baseRaw.hashes === 'object' ? baseRaw.hashes : null;
|
|
1918
|
+
if (baseHashes) {
|
|
1919
|
+
for (const p of Object.keys(baseHashes)) {
|
|
1920
|
+
const rel = String(p).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
1921
|
+
if (!rel || rel === 'local.yaml' || rel.startsWith('.runtime/')) continue;
|
|
1922
|
+
if (!existsSync(join(specDir, rel))) suppressResurrect.add(rel);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
} catch { /* 基线缺失/损坏 = 无抑制面(全新空环境全量回放,既有行为不变) */ }
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1884
1928
|
const localYamlPath = join(specDir, 'local.yaml');
|
|
1885
1929
|
const localYamlBakPath = join(dirname(specDir), `${basename(specDir)}.local.yaml.bak`);
|
|
1886
1930
|
let localYamlSaved = null; // null = 本无此文件,无需恢复
|
|
@@ -1916,6 +1960,11 @@ export class SyncManager {
|
|
|
1916
1960
|
const crlfEntries = [];
|
|
1917
1961
|
for (const e of entries) {
|
|
1918
1962
|
_assertSafeTarName(e.name.replace(/\/+$/, ''), specDir);
|
|
1963
|
+
const relName = e.name.replace(/^\.\//, '').replace(/\\/g, '/');
|
|
1964
|
+
if (!e.isDir && suppressResurrect.has(relName)) {
|
|
1965
|
+
resurrectSkipped.push(relName); // 基线有、盘面无=本地删除:不从快照复活
|
|
1966
|
+
continue;
|
|
1967
|
+
}
|
|
1919
1968
|
const fullPath = join(specDir, e.name);
|
|
1920
1969
|
if (e.isDir) {
|
|
1921
1970
|
mkdirSync(fullPath, { recursive: true });
|
|
@@ -1926,6 +1975,10 @@ export class SyncManager {
|
|
|
1926
1975
|
if (e.data && e.data.length < 4 * 1024 * 1024) crlfEntries.push([e.name, e.data]); // CRLF 画像采样(超 4MB 大文件跳过采样,写入不受影响)
|
|
1927
1976
|
}
|
|
1928
1977
|
}
|
|
1978
|
+
// 防复活抑制可见(宁可吵不可静默复活):被挡路径点名(截断防刷屏),出路指 --restore-deleted
|
|
1979
|
+
if (resurrectSkipped.length > 0) {
|
|
1980
|
+
console.warn(`⚠️ [sync] 防复活抑制:${resurrectSkipped.length} 个本地已删除文件未从快照恢复(基线有、盘面无=本地删除尚未上行成功;确要从快照恢复用 --restore-deleted): ${resurrectSkipped.slice(0, 5).join(', ')}${resurrectSkipped.length > 5 ? ' 等' : ''}`);
|
|
1981
|
+
}
|
|
1929
1982
|
// 批量写入留痕(2026-09-17 反馈⑥②法证收口):整树解包=逐字节回放,他机 CRLF 内容
|
|
1930
1983
|
// 原样落盘——审计行记 时间/触发面/文件数/CRLF 命中数(fail-open)。
|
|
1931
1984
|
try {
|
|
@@ -1937,6 +1990,7 @@ export class SyncManager {
|
|
|
1937
1990
|
crlfPaths: crlfPaths.slice(0, WRITE_AUDIT_FILE_CAP),
|
|
1938
1991
|
crlfCount: crlfPaths.length,
|
|
1939
1992
|
force: opts.force === true,
|
|
1993
|
+
resurrectSkippedCount: resurrectSkipped.length,
|
|
1940
1994
|
});
|
|
1941
1995
|
} catch { /* 审计 fail-open */ }
|
|
1942
1996
|
if (localYamlSaved !== null) {
|
|
@@ -1962,7 +2016,7 @@ export class SyncManager {
|
|
|
1962
2016
|
return { ok: false, pulled: false, specDir, reason: `解压 spec bundle 失败: ${err.message}` };
|
|
1963
2017
|
}
|
|
1964
2018
|
console.log(`[sync] 已拉取 spec 快照并解压到 ${specDir}${specVersion ? `(平台 spec_version: ${specVersion},打包时刻快照非实时)` : ''}`);
|
|
1965
|
-
return { ok: true, pulled: true, specDir, specVersion };
|
|
2019
|
+
return { ok: true, pulled: true, specDir, specVersion, ...(resurrectSkipped.length > 0 ? { resurrectSkipped } : {}) };
|
|
1966
2020
|
}
|
|
1967
2021
|
|
|
1968
2022
|
/**
|
|
@@ -2063,7 +2117,7 @@ export class SyncManager {
|
|
|
2063
2117
|
// 基线快照刷新(best-effort):被覆盖路径回写当前盘面 hash(= 服务器内容 hash)、被删
|
|
2064
2118
|
// 路径移除条目——下次同步这些路径按「本地未改动」豁免回推(take-platform 语义闭环)。
|
|
2065
2119
|
try {
|
|
2066
|
-
const baseFile = join(specDir, '.runtime', 'spec-sync-base.json');
|
|
2120
|
+
const baseFile = runtimeStatePath(join(specDir, '.runtime'), 'spec-sync-base.json');
|
|
2067
2121
|
let base = null;
|
|
2068
2122
|
try { base = JSON.parse(readFileSync(baseFile, 'utf8')); } catch { base = null; }
|
|
2069
2123
|
const hashes = base && typeof base.hashes === 'object' ? base.hashes : {};
|
|
@@ -2290,9 +2344,11 @@ export class SyncManager {
|
|
|
2290
2344
|
if (!specDir) return [];
|
|
2291
2345
|
const runtimeDir = join(specDir, '.runtime');
|
|
2292
2346
|
if (!existsSync(runtimeDir)) return [];
|
|
2347
|
+
// 2026-10-18 布局分层:冲突文件落 state/;根目录扫描保留旧布局兜底
|
|
2293
2348
|
let files = [];
|
|
2294
|
-
|
|
2295
|
-
|
|
2349
|
+
for (const dir of [join(runtimeDir, 'state'), runtimeDir]) {
|
|
2350
|
+
try { files.push(...readdirSync(dir)); } catch { /* 可选层缺失跳过 */ }
|
|
2351
|
+
}
|
|
2296
2352
|
const conflicts = [];
|
|
2297
2353
|
// HUB-08:同时扫描进度冲突(sync-conflict-*)与 spec 树冲突(spec-sync-conflict-*),
|
|
2298
2354
|
// type 字段供 status 展示与 resolve 分流
|
|
@@ -2302,7 +2358,7 @@ export class SyncManager {
|
|
|
2302
2358
|
else if (f.startsWith('spec-sync-conflict-') && f.endsWith('.json')) prefix = 'spec-sync-conflict-';
|
|
2303
2359
|
else continue;
|
|
2304
2360
|
const type = prefix === 'sync-conflict-' ? 'progress' : 'spec-tree';
|
|
2305
|
-
const filePath = join(runtimeDir, f);
|
|
2361
|
+
const filePath = existsSync(runtimeStatePath(runtimeDir, f)) ? runtimeStatePath(runtimeDir, f) : join(runtimeDir, f);
|
|
2306
2362
|
try {
|
|
2307
2363
|
const cf = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
2308
2364
|
conflicts.push({
|
package/src/task-done.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* `task start` 的受影响测试族注入不在本命令面(开工前置提示,保留不动)。
|
|
20
20
|
*/
|
|
21
21
|
import { existsSync, readFileSync, unlinkSync } from 'node:fs'
|
|
22
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
22
23
|
import { join } from 'node:path'
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -50,7 +51,7 @@ export async function runTaskDone({ changeName, cwd, taskId, verdict, notes = ''
|
|
|
50
51
|
let reviewSkipped = false
|
|
51
52
|
try {
|
|
52
53
|
let runId = ''
|
|
53
|
-
const markerFile =
|
|
54
|
+
const markerFile = runtimeStatePath(runtimeRoot, 'current-execute-run-id-' + changeName)
|
|
54
55
|
try {
|
|
55
56
|
if (existsSync(markerFile)) {
|
|
56
57
|
const c = readFileSync(markerFile, 'utf8').trim()
|