@accelerator-mzq/forge 1.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/archive.d.ts +73 -10
- package/dist/cli/commands/archive.d.ts.map +1 -1
- package/dist/cli/commands/archive.js +362 -89
- package/dist/cli/commands/archive.js.map +1 -1
- package/dist/cli/commands/legacy-bridge.d.ts +110 -0
- package/dist/cli/commands/legacy-bridge.d.ts.map +1 -1
- package/dist/cli/commands/legacy-bridge.js +1058 -438
- package/dist/cli/commands/legacy-bridge.js.map +1 -1
- package/dist/cli/commands/pause-capture.d.ts +24 -0
- package/dist/cli/commands/pause-capture.d.ts.map +1 -0
- package/dist/cli/commands/pause-capture.js +87 -0
- package/dist/cli/commands/pause-capture.js.map +1 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/ack-log.d.ts +17 -1
- package/dist/core/ack-log.d.ts.map +1 -1
- package/dist/core/ack-log.js.map +1 -1
- package/dist/core/archive/ack-log-consistency.d.ts.map +1 -1
- package/dist/core/archive/ack-log-consistency.js +2 -1
- package/dist/core/archive/ack-log-consistency.js.map +1 -1
- package/dist/core/archive/pause-decisions-fence.d.ts +24 -1
- package/dist/core/archive/pause-decisions-fence.d.ts.map +1 -1
- package/dist/core/archive/pause-decisions-fence.js +260 -46
- package/dist/core/archive/pause-decisions-fence.js.map +1 -1
- package/dist/core/backlog/assets/backlog-readme.md +4 -2
- package/dist/core/backlog/index.d.ts +1 -1
- package/dist/core/backlog/index.d.ts.map +1 -1
- package/dist/core/backlog/index.js +27 -8
- package/dist/core/backlog/index.js.map +1 -1
- package/dist/core/backlog/render.d.ts +48 -3
- package/dist/core/backlog/render.d.ts.map +1 -1
- package/dist/core/backlog/render.js +151 -15
- package/dist/core/backlog/render.js.map +1 -1
- package/dist/core/legacy-bridge/extractor.d.ts +79 -0
- package/dist/core/legacy-bridge/extractor.d.ts.map +1 -0
- package/dist/core/legacy-bridge/extractor.js +374 -0
- package/dist/core/legacy-bridge/extractor.js.map +1 -0
- package/dist/core/legacy-bridge/indexer.d.ts +8 -2
- package/dist/core/legacy-bridge/indexer.d.ts.map +1 -1
- package/dist/core/legacy-bridge/indexer.js +61 -9
- package/dist/core/legacy-bridge/indexer.js.map +1 -1
- package/dist/core/legacy-bridge/legacy-requirements.d.ts +53 -0
- package/dist/core/legacy-bridge/legacy-requirements.d.ts.map +1 -0
- package/dist/core/legacy-bridge/legacy-requirements.js +114 -0
- package/dist/core/legacy-bridge/legacy-requirements.js.map +1 -0
- package/dist/core/legacy-bridge/llm-task.d.ts +54 -0
- package/dist/core/legacy-bridge/llm-task.d.ts.map +1 -0
- package/dist/core/legacy-bridge/llm-task.js +87 -0
- package/dist/core/legacy-bridge/llm-task.js.map +1 -0
- package/dist/core/legacy-bridge/mapper.d.ts +8 -9
- package/dist/core/legacy-bridge/mapper.d.ts.map +1 -1
- package/dist/core/legacy-bridge/mapper.js +24 -39
- package/dist/core/legacy-bridge/mapper.js.map +1 -1
- package/dist/core/legacy-bridge/quality-judge.d.ts +5 -0
- package/dist/core/legacy-bridge/quality-judge.d.ts.map +1 -1
- package/dist/core/legacy-bridge/quality-judge.js +25 -21
- package/dist/core/legacy-bridge/quality-judge.js.map +1 -1
- package/dist/core/legacy-bridge/regenerator.d.ts +21 -1
- package/dist/core/legacy-bridge/regenerator.d.ts.map +1 -1
- package/dist/core/legacy-bridge/regenerator.js +132 -2
- package/dist/core/legacy-bridge/regenerator.js.map +1 -1
- package/dist/core/legacy-bridge/runners.d.ts +33 -0
- package/dist/core/legacy-bridge/runners.d.ts.map +1 -0
- package/dist/core/legacy-bridge/runners.js +77 -0
- package/dist/core/legacy-bridge/runners.js.map +1 -0
- package/dist/core/legacy-bridge/sync-check.d.ts +8 -0
- package/dist/core/legacy-bridge/sync-check.d.ts.map +1 -1
- package/dist/core/legacy-bridge/sync-check.js +83 -0
- package/dist/core/legacy-bridge/sync-check.js.map +1 -1
- package/dist/core/legacy-bridge/types.d.ts +2 -0
- package/dist/core/legacy-bridge/types.d.ts.map +1 -1
- package/dist/core/markers/types.d.ts +5 -0
- package/dist/core/markers/types.d.ts.map +1 -1
- package/dist/core/parse/unified-diff.d.ts +26 -0
- package/dist/core/parse/unified-diff.d.ts.map +1 -0
- package/dist/core/parse/unified-diff.js +61 -0
- package/dist/core/parse/unified-diff.js.map +1 -0
- package/dist/core/templates/commands/apply.md +12 -3
- package/dist/core/templates/commands/archive.md +15 -0
- package/dist/core/templates/commands/verify.md +2 -0
- package/dist/core/templates/skills/legacy-bridge-fulfillment.md +22 -0
- package/dist/core/validate/marker-schema.d.ts.map +1 -1
- package/dist/core/validate/marker-schema.js +15 -0
- package/dist/core/validate/marker-schema.js.map +1 -1
- package/package.json +21 -20
|
@@ -10,7 +10,6 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
10
10
|
import { execFileSync } from 'node:child_process';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { parse as parseYaml } from 'yaml';
|
|
13
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
14
13
|
import { parseMarker } from '../../core/markers/index.js';
|
|
15
14
|
import { validateMarkerSchema } from '../../core/validate/index.js';
|
|
16
15
|
import { validateEvidence, validateReviewGitIntegrity, validateReviewOutcomes, } from '../../core/validate/marker-integrity.js';
|
|
@@ -22,16 +21,18 @@ import { promptRecoverChoice } from '../../core/archive/recover-prompt.js';
|
|
|
22
21
|
import { applyDeltas } from '../../core/specs-sync/index.js';
|
|
23
22
|
import { loadAnchorsFile } from '../../core/legacy-bridge/anchors.js';
|
|
24
23
|
import { checkAck } from '../../core/legacy-bridge/ack.js';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
24
|
+
import { buildSyncCheckTask, applySyncCheckResult } from '../../core/legacy-bridge/sync-check.js';
|
|
25
|
+
import { AgentHandoffRunner, ApiRunner, makeForgeApiClient, } from '../../core/legacy-bridge/runners.js';
|
|
26
|
+
import { renderDiffMarkdown, renderDiffYaml } from '../../core/legacy-bridge/diff-report.js';
|
|
27
27
|
import { readAnchorFile } from '../../core/legacy-bridge/encoding.js';
|
|
28
|
+
import { FORGE_VERSION } from '../../index.js';
|
|
28
29
|
// Task 8 (plan-9a §9): cross-cutting fence framework — 9g 实施完整 13 不变量逻辑
|
|
29
30
|
import { crossCuttingFenceCheck } from '../../core/archive/fence.js';
|
|
30
31
|
// plan-9d Task 6:verify_findings fence + ack-log consistency
|
|
31
32
|
import { validateVerifyFindingsFence } from '../../core/archive/verify-findings-fence.js';
|
|
32
33
|
import { validateAckLogConsistency } from '../../core/archive/ack-log-consistency.js';
|
|
33
|
-
// plan-9c Task 2:pause_decisions fence
|
|
34
|
-
import { validatePauseDecisionsFence } from '../../core/archive/pause-decisions-fence.js';
|
|
34
|
+
// plan-9c Task 2:pause_decisions fence + Task 12:cross-check
|
|
35
|
+
import { validatePauseDecisionsFence, crossCheckPauseDecisions, } from '../../core/archive/pause-decisions-fence.js';
|
|
35
36
|
// plan-9e1 Task 4:三级 fence + summary builder + render + ScopeEntriesIntegrityError(v2 BLOCKER 4)
|
|
36
37
|
import { validateThreeLevelFence } from '../../core/archive/three-level-fence.js';
|
|
37
38
|
import { buildArchiveSummary, ScopeEntriesIntegrityError, } from '../../core/archive/summary-builder.js';
|
|
@@ -63,12 +64,17 @@ function isProjectActuallyGit(cwd) {
|
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
export function buildArchiveCommand() {
|
|
66
|
-
return new Command('archive')
|
|
67
|
+
return (new Command('archive')
|
|
67
68
|
.argument('[changeId]', 'change directory id (e.g., add-login)')
|
|
68
69
|
.description('Archive a verified+reviewed change')
|
|
69
70
|
.option('--force', 'accept human-override or non-git review markers')
|
|
70
71
|
.option('--recover', 'recover from a half-completed archive transaction')
|
|
71
72
|
.option('--resume-summary <archiveId>', 'resume rename archive_summary.tmp.yaml → archive_summary.yaml (rare half-completed state, plan-9e1)')
|
|
73
|
+
// Task 6.3:--api 模式 — preflight/posthook 的 sync-check 进程内直连 API 跑完,
|
|
74
|
+
// 不 emit manifest、不写暂停态、不 halt(高保证 CI 场景绕过 agent-pause)
|
|
75
|
+
.option('--api', '直连 Anthropic API 跑 sync-check(不 emit manifest、不暂停)')
|
|
76
|
+
// Task 6.4:--resume — agent 履行 manifest 并跑 sync-check --apply 后,续跑 archive 主流程
|
|
77
|
+
.option('--resume', 'agent 履行后续跑 archive(gate 复核:produced_from 绑定 + critical 幂等重评)')
|
|
72
78
|
.action(async (changeId, opts) => {
|
|
73
79
|
const forgeRoot = join(process.cwd(), 'forge');
|
|
74
80
|
// —— --recover 独立路径 ——
|
|
@@ -189,6 +195,28 @@ export function buildArchiveCommand() {
|
|
|
189
195
|
await release();
|
|
190
196
|
}
|
|
191
197
|
}
|
|
198
|
+
// —— Task 6.4:--resume 独立路径(agent 履行后 gate 复核 + 续跑 archive)——
|
|
199
|
+
// I-2:暂停态文件此处不删 —— 仅记下路径,延后到 archiveTransaction 成功后才删。
|
|
200
|
+
// 若 gate 通过但后续 marker check / fence / archiveTransaction 任一失败,
|
|
201
|
+
// 暂停态保留 → 用户修好后可再 `forge archive --resume` 重入(否则会因
|
|
202
|
+
// 「无暂停态文件」被拒,--resume 失去重入性陷入死角)。
|
|
203
|
+
let resumePausePath;
|
|
204
|
+
if (opts.resume) {
|
|
205
|
+
if (!changeId) {
|
|
206
|
+
console.error('✗ --resume 需要 changeId');
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
// gate 复核:produced_from 绑定 + critical 幂等重评
|
|
210
|
+
const gateResult = await resumeArchiveGateCheck(forgeRoot, changeId);
|
|
211
|
+
if (!gateResult.ok) {
|
|
212
|
+
console.error(`✗ --resume gate 复核失败:${gateResult.reason}`);
|
|
213
|
+
process.exit(2); // business-rule-fail
|
|
214
|
+
}
|
|
215
|
+
// gate 通过 → 记下暂停态路径(延后删),fall-through 续跑正常 archive 主流程
|
|
216
|
+
resumePausePath = join(forgeRoot, '.cache', `archive-pause-${changeId}.json`);
|
|
217
|
+
console.log(`✓ --resume gate 通过;续跑 archive ${changeId}…`);
|
|
218
|
+
// --resume 续跑后,opts.resume=true 不影响后续 opts 读取(changeId 已确认存在)
|
|
219
|
+
}
|
|
192
220
|
// —— 正常 archive 路径 ——
|
|
193
221
|
if (!changeId) {
|
|
194
222
|
console.error('✗ changeId required (unless using --recover)');
|
|
@@ -203,11 +231,18 @@ export function buildArchiveCommand() {
|
|
|
203
231
|
// 决策 #23:复用 archive.lock,不再 acquire legacy-bridge.lock。
|
|
204
232
|
// preflight 不再 process.exit,改返 PreflightResult,caller 在 try 块内手动 release+exit
|
|
205
233
|
// 与 marker check 现有 inline-release convention 一致(避免 process.exit 跳过 finally 致锁残留)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
234
|
+
// Task 6.3:--api 透传 —— agent 模式 emit manifest+halt;--api 模式进程内跑 sync-check
|
|
235
|
+
// Task 6.4:--resume 时跳过 preflight — gate 复核已在 --resume 分支完成(暂停态也已清除),
|
|
236
|
+
// 再跑 preflight 会因 enforce_sync=true 再次 emit manifest + halt,造成死循环。
|
|
237
|
+
if (!opts.resume) {
|
|
238
|
+
const preflightResult = await runArchivePreflight(forgeRoot, changeId, {
|
|
239
|
+
api: opts.api ?? false,
|
|
240
|
+
});
|
|
241
|
+
if (preflightResult.kind !== 'ok') {
|
|
242
|
+
console.error(preflightResult.message);
|
|
243
|
+
await archiveRelease();
|
|
244
|
+
process.exit(2);
|
|
245
|
+
}
|
|
211
246
|
}
|
|
212
247
|
// plan-9g 默认开启 14 不变量 fence(brainstorm v6 删两 opt-in flag);所有 archive 调用都跑
|
|
213
248
|
const fenceResult = await crossCuttingFenceCheck(join(forgeRoot, 'changes', changeId));
|
|
@@ -353,7 +388,15 @@ export function buildArchiveCommand() {
|
|
|
353
388
|
// v2 codex MAJOR 4 修订:对 verifyRec + reviewRec 都跑 fence
|
|
354
389
|
// v4 codex NEW-MAJOR A6 + B4 联动:fence 不再需要 ctx 参数(B4 改用 parseMarkdown 局部段校验,
|
|
355
390
|
// 不再调 validateScopeEntries → ctx unused → 沿 YAGNI 移除)
|
|
356
|
-
|
|
391
|
+
// Task 12:从 verify marker 取链固化值组成 pauseFenceOpts(design §6.2 verify 是最后 freeze)
|
|
392
|
+
// marker 由 AI 产出,字段运行时可能是 string / null / undefined — 断言须含 null,
|
|
393
|
+
// 否则 null 会被错误窄化为 string,绕过 checkOption2TaskChecked 的 == null fail-closed 检查
|
|
394
|
+
const pauseFenceOpts = {
|
|
395
|
+
verifyAckLogTailHash: verifyRec['ack_log_tail_hash'],
|
|
396
|
+
verifyAckLogEntryCount: verifyRec['ack_log_entry_count'],
|
|
397
|
+
changeId,
|
|
398
|
+
};
|
|
399
|
+
const pdVerifyResult = await validatePauseDecisionsFence(verifyRec, changeDir, process.cwd(), pauseFenceOpts, verifyPath);
|
|
357
400
|
if (!pdVerifyResult.valid) {
|
|
358
401
|
console.error('✗ pause_decisions fence 拒签(verify-passed):');
|
|
359
402
|
for (const e of pdVerifyResult.errors)
|
|
@@ -361,7 +404,8 @@ export function buildArchiveCommand() {
|
|
|
361
404
|
await archiveRelease();
|
|
362
405
|
process.exit(1);
|
|
363
406
|
}
|
|
364
|
-
|
|
407
|
+
// 两处调用都传同一 pauseFenceOpts(都用 verify marker 的 tail/count — design §6.2)
|
|
408
|
+
const pdReviewResult = await validatePauseDecisionsFence(reviewRec, changeDir, process.cwd(), pauseFenceOpts, reviewPath);
|
|
365
409
|
if (!pdReviewResult.valid) {
|
|
366
410
|
console.error('✗ pause_decisions fence 拒签(review-passed):');
|
|
367
411
|
for (const e of pdReviewResult.errors)
|
|
@@ -369,6 +413,22 @@ export function buildArchiveCommand() {
|
|
|
369
413
|
await archiveRelease();
|
|
370
414
|
process.exit(1);
|
|
371
415
|
}
|
|
416
|
+
// 步骤 3.7.1:Task 12 — verify/review pause_decisions cross-check(design §6.5)
|
|
417
|
+
// 对两侧共有 id 的 pause_decision 逐字段比对;单侧独有 id 不拒签
|
|
418
|
+
const vDecisions = Array.isArray(verifyRec['pause_decisions'])
|
|
419
|
+
? verifyRec['pause_decisions']
|
|
420
|
+
: [];
|
|
421
|
+
const rDecisions = Array.isArray(reviewRec['pause_decisions'])
|
|
422
|
+
? reviewRec['pause_decisions']
|
|
423
|
+
: [];
|
|
424
|
+
const ccResult = crossCheckPauseDecisions(vDecisions, rDecisions, verifyPath);
|
|
425
|
+
if (!ccResult.valid) {
|
|
426
|
+
console.error('✗ pause_decisions verify/review cross-check 拒签:');
|
|
427
|
+
for (const e of ccResult.errors)
|
|
428
|
+
console.error(` - ${e.field}: ${e.message}`);
|
|
429
|
+
await archiveRelease();
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
372
432
|
// 步骤 3.8:plan-9d Task 6 v2 B-4 — ack-log 一致性 cross-check
|
|
373
433
|
// v3 codex BLOCKER 2 修订:对 verifyRec + reviewRec 都跑 cross-check
|
|
374
434
|
// (review marker 同样可承载 pause_decisions superset additive,沿 9c Task 1 schema)
|
|
@@ -475,8 +535,25 @@ export function buildArchiveCommand() {
|
|
|
475
535
|
}
|
|
476
536
|
// 步骤 5:调 archiveTransaction(Move→Sync,含 .tmp 写 / rename / 回滚)
|
|
477
537
|
await archiveTransaction({ forgeRoot, changeId, archiveDate, archiveSummary });
|
|
538
|
+
// I-2:archiveTransaction await 成功返回(失败会 throw 走外层 catch)即 transaction 成功点 ——
|
|
539
|
+
// 此处删 --resume 暂停态文件(正常收尾)。transaction 之前任一步失败时暂停态保留,
|
|
540
|
+
// 用户修好后可再 --resume 重入。仅 opts.resume 路径有此文件,非 resume 路径 undefined 跳过。
|
|
541
|
+
if (resumePausePath) {
|
|
542
|
+
// transaction 已成功;删暂停态失败(罕见权限错)不应让 archive 报错 —— 降级为 warn
|
|
543
|
+
try {
|
|
544
|
+
await rm(resumePausePath, { force: true });
|
|
545
|
+
}
|
|
546
|
+
catch (e) {
|
|
547
|
+
console.warn(`⚠ archive 已完成,但暂停态文件 ${resumePausePath} 清理失败(可手动删除):${e.message}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
478
550
|
// 步骤 5.5:Plan 7 post-archive hook(enforce_sync=false 时不阻塞,只产报告)
|
|
479
|
-
|
|
551
|
+
// Task 6.3 I-2:透传 archiveDate(避免 posthook 自己算 new Date() 跨午夜偏一天)
|
|
552
|
+
// Task 6.3 C-1:透传 --api
|
|
553
|
+
await runArchivePostHook(forgeRoot, changeId, {
|
|
554
|
+
archiveDate,
|
|
555
|
+
api: opts.api ?? false,
|
|
556
|
+
});
|
|
480
557
|
// 步骤 6:plan-9e1 Task 4 — 渲染 archive_summary 输出(沿 design §2.4.4)
|
|
481
558
|
const archiveDirName = `${archiveDate}-${changeId}`;
|
|
482
559
|
console.log(renderArchiveSummaryOutput(archiveSummary, archiveDirName));
|
|
@@ -526,7 +603,7 @@ export function buildArchiveCommand() {
|
|
|
526
603
|
if (archiveRelease)
|
|
527
604
|
await archiveRelease();
|
|
528
605
|
}
|
|
529
|
-
});
|
|
606
|
+
}));
|
|
530
607
|
}
|
|
531
608
|
/**
|
|
532
609
|
* 从 backup 目录把所有备份的 specs 文件还原到 currentSpecsDir(case C [2] 撤销归档用)。
|
|
@@ -545,24 +622,28 @@ async function restoreSpecsFromBackup(backupDir, currentSpecsDir) {
|
|
|
545
622
|
}
|
|
546
623
|
}
|
|
547
624
|
/**
|
|
548
|
-
* Plan 7 §2.5:archive preflight — enforce_sync=true
|
|
625
|
+
* Plan 7 §2.5 + Task 6.3:archive preflight — enforce_sync=true 时拦截 sync 差异。
|
|
549
626
|
*
|
|
550
627
|
* Graceful skip 路径(全部返 {kind:'ok'}):
|
|
551
628
|
* 1. forge/config.yaml 不存在
|
|
552
629
|
* 2. legacy-anchors.yaml 不存在
|
|
553
630
|
* 3. legacy_bridge.allow_llm_calls != true
|
|
554
631
|
* 4. legacy_bridge.enforce_sync != true
|
|
632
|
+
* 5. 无受影响 anchor / 全部读取失败(buildSyncCheckTask 返 null)
|
|
555
633
|
*
|
|
556
|
-
*
|
|
557
|
-
* -
|
|
558
|
-
*
|
|
559
|
-
* -
|
|
634
|
+
* 双模式(ack 就绪后):
|
|
635
|
+
* - **agent 模式(默认,不带 --api)**:emit sync-check manifest 到 .cache +
|
|
636
|
+
* 写暂停态文件 → 返 {kind:'halted-for-fulfillment'},archive halt 等 agent fulfill。
|
|
637
|
+
* - **--api 模式**:进程内直连 Anthropic API 跑 sync-check + 写 sync-state →
|
|
638
|
+
* 含 critical pending 返 {kind:'critical-pending'};否则返 {kind:'ok'} 续跑 archive。
|
|
639
|
+
*
|
|
640
|
+
* ack 不就绪(两模式共有)→ 返 {kind:'ack-missing'}。
|
|
560
641
|
*
|
|
561
642
|
* **不再调 process.exit / console.error**:caller(archive 命令)负责打印消息 +
|
|
562
643
|
* release archive.lock + exit,避免本函数内 process.exit 跳过 caller finally
|
|
563
644
|
* 导致 archive.lock 文件残留。
|
|
564
645
|
*/
|
|
565
|
-
export async function runArchivePreflight(forgeRoot, changeId) {
|
|
646
|
+
export async function runArchivePreflight(forgeRoot, changeId, opts = { api: false }) {
|
|
566
647
|
const configPath = join(forgeRoot, 'config.yaml');
|
|
567
648
|
if (!existsSync(configPath))
|
|
568
649
|
return { kind: 'ok' };
|
|
@@ -582,61 +663,60 @@ export async function runArchivePreflight(forgeRoot, changeId) {
|
|
|
582
663
|
message: `legacy_bridge.enforce_sync=true 但 ack 未就绪:${ack.reason};请先跑 forge legacy-bridge --acknowledge-data-transfer`,
|
|
583
664
|
};
|
|
584
665
|
}
|
|
585
|
-
// 拼 change context(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
666
|
+
// 拼 change context(I-3:gate 已加载的 config + anchors 传入,不再二次 IO)
|
|
667
|
+
// preflight 时 change 还在 forge/changes/<id>/(archived=false,无需 archiveDate)
|
|
668
|
+
const ctx = await buildSyncCheckChangeContext(forgeRoot, changeId, { archived: false }, config, anchors);
|
|
669
|
+
if (opts.api) {
|
|
670
|
+
// —— --api 模式:进程内直连 API 跑 sync-check,不 emit manifest、不写暂停态 ——
|
|
671
|
+
const task = await buildSyncCheckTask(ctx, async (p) => (await readAnchorFile(p)).text);
|
|
672
|
+
if (!task)
|
|
673
|
+
return { kind: 'ok' }; // 无受影响 anchor → 无需 sync-check,续跑 archive
|
|
674
|
+
// 进程内调 Anthropic SDK(client 构造方式与旧 archive 代码一致:forge-eval/load-env)
|
|
675
|
+
const client = (await makeForgeApiClient());
|
|
676
|
+
const results = await new ApiRunner(client).run([task]);
|
|
677
|
+
// --api 无 manifest,produced_from 标 'api-inline' sentinel(区别于 agent 路径的 manifest_hash;
|
|
678
|
+
// 该 sync-state 不经 resume gate,Task 6.4 据此 sentinel 可识别 --api 来源)
|
|
679
|
+
const syncState = applySyncCheckResult(results[0]?.text ?? '', changeId, 'api-inline');
|
|
680
|
+
// 写 sync-state 到旧路径(沿旧 preflight 写盘方式)
|
|
681
|
+
await mkdir(join(forgeRoot, 'legacy-sync-state'), { recursive: true });
|
|
682
|
+
await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.md`), renderDiffMarkdown(syncState), 'utf8');
|
|
683
|
+
await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.yaml`), renderDiffYaml(syncState), 'utf8');
|
|
684
|
+
// 含 critical pending → critical-pending(死变体的归宿:archive 主流程 kind!=='ok' 会 halt)
|
|
685
|
+
const criticalDiffs = syncState.diffs.filter((d) => d.severity === 'critical' && d.status === 'pending');
|
|
686
|
+
if (criticalDiffs.length > 0) {
|
|
687
|
+
return {
|
|
688
|
+
kind: 'critical-pending',
|
|
689
|
+
criticalCount: criticalDiffs.length,
|
|
690
|
+
message: `✗ ${criticalDiffs.length} 项 critical 差异未 resolve;\n` +
|
|
691
|
+
`跑 forge legacy-bridge resolve ${changeId} 后重试,或在 forge/legacy-sync-state/${changeId}.yaml 标 ack`,
|
|
692
|
+
};
|
|
598
693
|
}
|
|
694
|
+
return { kind: 'ok' };
|
|
599
695
|
}
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const client = new Anthropic({ apiKey: anthropicApiKey });
|
|
606
|
-
const out = await runSyncCheck(client, {
|
|
607
|
-
changeId,
|
|
608
|
-
changeContext,
|
|
609
|
-
affectedModules,
|
|
610
|
-
anchors,
|
|
611
|
-
autoResolveCrossAnchor: config.legacy_bridge.auto_resolve_cross_anchor ?? false,
|
|
612
|
-
mtimeOf: (p) => {
|
|
613
|
-
try {
|
|
614
|
-
return Math.floor(statSync(p).mtimeMs / 1000);
|
|
615
|
-
}
|
|
616
|
-
catch {
|
|
617
|
-
return 0;
|
|
618
|
-
}
|
|
619
|
-
},
|
|
620
|
-
}, async (path) => (await readAnchorFile(path)).text);
|
|
621
|
-
await mkdir(join(forgeRoot, 'legacy-sync-state'), { recursive: true });
|
|
622
|
-
await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.md`), renderDiffMarkdown(out.syncState), 'utf8');
|
|
623
|
-
await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.yaml`), renderDiffYaml(out.syncState), 'utf8');
|
|
624
|
-
if (hasCriticalPending(out.syncState)) {
|
|
625
|
-
const criticalCount = out.syncState.diffs.filter((d) => d.severity === 'critical' && d.status === 'pending').length;
|
|
626
|
-
return {
|
|
627
|
-
kind: 'critical-pending',
|
|
628
|
-
criticalCount,
|
|
629
|
-
message: `✗ ${criticalCount} 项 critical 差异未 resolve;\n` +
|
|
630
|
-
`跑 forge legacy-bridge resolve ${changeId} 后重试,或在 forge/legacy-sync-state/${changeId}.yaml 标 ack`,
|
|
631
|
-
};
|
|
696
|
+
// —— agent 模式(默认):emit sync-check manifest + 写暂停态文件,halt archive ——
|
|
697
|
+
const emitResult = await emitPreflightSyncCheck(forgeRoot, changeId, ctx);
|
|
698
|
+
if (emitResult.kind === 'skip') {
|
|
699
|
+
// 无受影响 anchor / 全部读取失败 → graceful skip,继续 archive
|
|
700
|
+
return { kind: 'ok' };
|
|
632
701
|
}
|
|
633
|
-
|
|
702
|
+
// halted-for-fulfillment:manifest 已写,暂停态已写,告知 caller halt
|
|
703
|
+
return {
|
|
704
|
+
kind: 'halted-for-fulfillment',
|
|
705
|
+
message: `sync-check manifest 已 emit(preflight);\n` +
|
|
706
|
+
`forge agent 履行后跑 forge legacy-bridge sync-check --apply --change-id ${changeId};\n` +
|
|
707
|
+
`暂停态文件:forge/.cache/archive-pause-${changeId}.json`,
|
|
708
|
+
};
|
|
634
709
|
}
|
|
635
710
|
/**
|
|
636
|
-
* Plan 7 §2.5:post-archive hook — 不阻塞,仅在 enforce_sync=false 时跑(产报告)。
|
|
711
|
+
* Plan 7 §2.5 + Task 6.3:post-archive hook — 不阻塞,仅在 enforce_sync=false 时跑(产报告)。
|
|
637
712
|
* enforce_sync=true 已由 preflight 跑过;graceful skip 路径同 preflight。
|
|
713
|
+
*
|
|
714
|
+
* agent 模式:emit sync-check manifest(非阻塞,archive 已完成,fulfill 后跑 --apply 出报告)。
|
|
715
|
+
* --api 模式:进程内直连 API 跑 sync-check + 写 sync-state(非阻塞,直接出报告)。
|
|
716
|
+
*
|
|
717
|
+
* I-2:archiveDate 由 archive 主流程透传(不自己算 new Date(),避免跨午夜偏一天)。
|
|
638
718
|
*/
|
|
639
|
-
export async function runArchivePostHook(forgeRoot, changeId) {
|
|
719
|
+
export async function runArchivePostHook(forgeRoot, changeId, opts) {
|
|
640
720
|
const configPath = join(forgeRoot, 'config.yaml');
|
|
641
721
|
if (!existsSync(configPath))
|
|
642
722
|
return;
|
|
@@ -651,27 +731,76 @@ export async function runArchivePostHook(forgeRoot, changeId) {
|
|
|
651
731
|
const ack = await checkAck(forgeRoot, config, anchors);
|
|
652
732
|
if (!ack.ok)
|
|
653
733
|
return; // ack 不就绪 → graceful skip
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
734
|
+
// 拼 change context(I-3:gate 已加载的 config + anchors 传入;I-2:archiveDate 透传)
|
|
735
|
+
// posthook 时 change 已归档到 archive/<archiveDate>-<id>/
|
|
736
|
+
const ctx = await buildSyncCheckChangeContext(forgeRoot, changeId, { archived: true, archiveDate: opts.archiveDate }, config, anchors);
|
|
737
|
+
if (opts.api) {
|
|
738
|
+
// —— --api 模式:进程内直连 API 跑 sync-check + 写 sync-state(非阻塞)——
|
|
739
|
+
// archive transaction 已完成;posthook 的 API 调用失败不应让 archive 报 exit 1 ——
|
|
740
|
+
// I-A:整段包 try/catch,失败降级为 warn(agent 模式 posthook 只写本地文件无此风险)
|
|
741
|
+
try {
|
|
742
|
+
const task = await buildSyncCheckTask(ctx, async (p) => (await readAnchorFile(p)).text);
|
|
743
|
+
if (!task)
|
|
744
|
+
return; // 无受影响 anchor → graceful skip
|
|
745
|
+
const client = (await makeForgeApiClient());
|
|
746
|
+
const results = await new ApiRunner(client).run([task]);
|
|
747
|
+
const syncState = applySyncCheckResult(results[0]?.text ?? '', changeId, 'api-inline');
|
|
748
|
+
await mkdir(join(forgeRoot, 'legacy-sync-state'), { recursive: true });
|
|
749
|
+
await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.md`), renderDiffMarkdown(syncState), 'utf8');
|
|
750
|
+
await writeFile(join(forgeRoot, 'legacy-sync-state', `${changeId}.yaml`), renderDiffYaml(syncState), 'utf8');
|
|
751
|
+
console.log(`⚠ ${syncState.diffs.length} 项老文档可能需更新,详见 forge/legacy-sync-state/${changeId}.md`);
|
|
752
|
+
}
|
|
753
|
+
catch (e) {
|
|
754
|
+
console.warn(`⚠ --api sync-check(posthook)失败,已跳过(archive 已完成):${e.message}`);
|
|
755
|
+
}
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
// —— agent 模式(默认):emit sync-check manifest(非阻塞,archive 已完成)——
|
|
759
|
+
const emitResult = await emitPostHookSyncCheck(forgeRoot, changeId, ctx);
|
|
760
|
+
if (emitResult.kind === 'emitted') {
|
|
761
|
+
console.log(`⚠ sync-check manifest 已 emit(posthook);forge agent 履行后跑 forge legacy-bridge sync-check --apply --change-id ${changeId} 可得报告`);
|
|
662
762
|
}
|
|
763
|
+
// kind='skip':无受影响 anchor / 全部读取失败 → graceful skip,无输出
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* 抽出:合并 runArchivePreflight/runArchivePostHook 两处重复的 change context 拼装。
|
|
767
|
+
*
|
|
768
|
+
* archived=false → forge/changes/<id>/;
|
|
769
|
+
* archived=true → forge/changes/archive/<archiveDate>-<id>/。
|
|
770
|
+
*
|
|
771
|
+
* I-3:config 与 anchors 由调用方(preflight/posthook 的 gate 处)已加载并传入,
|
|
772
|
+
* 本函数不再二次 readFile/loadAnchorsFile —— 消除 double-IO + 避免
|
|
773
|
+
* loadAnchorsFile 损坏时裸抛 LegacyAnchorsError 穿透成 unhandled rejection。
|
|
774
|
+
* I-2:archived=true 时用调用方透传的 archiveDate(archive 主流程算出的那个),
|
|
775
|
+
* 不再 new Date() —— 避免 archive 跨午夜完成时目录名偏一天致静默 skip。
|
|
776
|
+
*/
|
|
777
|
+
async function buildSyncCheckChangeContext(forgeRoot, changeId, opts, config, anchors) {
|
|
778
|
+
// 决定 change 目录:归档后在 archive/<archiveDate>-<id>/,归档前在 changes/<id>/
|
|
779
|
+
// archived=true 时 archiveDate 必传(由 posthook 透传 archive 主流程算出的值)
|
|
780
|
+
const changesDir = opts.archived && opts.archiveDate
|
|
781
|
+
? join(forgeRoot, 'changes', 'archive', `${opts.archiveDate}-${changeId}`)
|
|
782
|
+
: join(forgeRoot, 'changes', changeId);
|
|
783
|
+
let changeContext = '';
|
|
663
784
|
const affectedModules = [];
|
|
664
|
-
//
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
785
|
+
// 读 proposal.md
|
|
786
|
+
if (existsSync(join(changesDir, 'proposal.md'))) {
|
|
787
|
+
changeContext += await readFile(join(changesDir, 'proposal.md'), 'utf8');
|
|
788
|
+
}
|
|
789
|
+
// 读 specs/ 下各 .md,拼 changeContext + 收集 affectedModules
|
|
790
|
+
const specsDir = join(changesDir, 'specs');
|
|
791
|
+
if (existsSync(specsDir)) {
|
|
792
|
+
for (const f of await readdir(specsDir)) {
|
|
793
|
+
changeContext += `\n## specs/${f}\n${await readFile(join(specsDir, f), 'utf8')}`;
|
|
794
|
+
affectedModules.push(f.replace(/\.md$/, ''));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
670
798
|
changeId,
|
|
671
799
|
changeContext,
|
|
672
800
|
affectedModules,
|
|
673
801
|
anchors,
|
|
674
|
-
autoResolveCrossAnchor: config.legacy_bridge
|
|
802
|
+
autoResolveCrossAnchor: config.legacy_bridge?.auto_resolve_cross_anchor ?? false,
|
|
803
|
+
// mtime 提供者:用于 cross-anchor 决策
|
|
675
804
|
mtimeOf: (p) => {
|
|
676
805
|
try {
|
|
677
806
|
return Math.floor(statSync(p).mtimeMs / 1000);
|
|
@@ -680,10 +809,154 @@ export async function runArchivePostHook(forgeRoot, changeId) {
|
|
|
680
809
|
return 0;
|
|
681
810
|
}
|
|
682
811
|
},
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* agent 模式:emit preflight sync-check manifest + 暂停态文件,halt archive(Task 6.3)。
|
|
816
|
+
*
|
|
817
|
+
* 调用前 gate 检查(config 存在 + allow_llm_calls + enforce_sync + anchors + ack)由
|
|
818
|
+
* runArchivePreflight 负责;本函数专注 emit 逻辑。
|
|
819
|
+
*
|
|
820
|
+
* @param ctx 已拼好的 SyncCheckInput(可选;不传则内部自拼,供测试直接调用)
|
|
821
|
+
*/
|
|
822
|
+
export async function emitPreflightSyncCheck(forgeRoot, changeId, ctx) {
|
|
823
|
+
// ctx 未传(测试直接调用)→ 自行加载 config + anchors 拼 context
|
|
824
|
+
const syncCtx = ctx ?? (await buildSyncCheckContextStandalone(forgeRoot, changeId, false));
|
|
825
|
+
if (!syncCtx)
|
|
826
|
+
return { kind: 'skip' }; // standalone 加载失败(无 config/anchors)
|
|
827
|
+
// 构建 LlmTask(确定性 prep:findAffectedAnchors + redact)
|
|
828
|
+
const task = await buildSyncCheckTask(syncCtx, async (p) => (await readAnchorFile(p)).text);
|
|
829
|
+
// 无受影响 anchor 或全部读取失败 → graceful skip
|
|
830
|
+
if (!task)
|
|
831
|
+
return { kind: 'skip' };
|
|
832
|
+
// emit sync-check manifest 到 forge/.cache/legacy-bridge-task-sync-check.json
|
|
833
|
+
// meta 键名与 Task 6.2 runSyncCheckCommand emit 保持一致:changeId(驼峰)
|
|
834
|
+
const manifest = await new AgentHandoffRunner(forgeRoot, FORGE_VERSION).emit('sync-check', 1, [task], { gate_context: 'archive-preflight', changeId });
|
|
835
|
+
// 写暂停态文件:archive-pause-<changeId>.json
|
|
836
|
+
await mkdir(join(forgeRoot, '.cache'), { recursive: true });
|
|
837
|
+
await writeFile(join(forgeRoot, '.cache', `archive-pause-${changeId}.json`), JSON.stringify({
|
|
838
|
+
changeId,
|
|
839
|
+
paused_step: 'preflight-sync-check',
|
|
840
|
+
manifest_hash: manifest.manifest_hash,
|
|
841
|
+
}, null, 2), 'utf8');
|
|
842
|
+
return { kind: 'halted-for-fulfillment' };
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* agent 模式:enforce_sync=false 时 post-archive emit sync-check manifest(非阻塞,无暂停态)(Task 6.3)。
|
|
846
|
+
*
|
|
847
|
+
* archive 已完成;fulfill 后跑 forge legacy-bridge sync-check --apply 出报告。
|
|
848
|
+
* 调用前 gate 检查由 runArchivePostHook 负责。
|
|
849
|
+
*
|
|
850
|
+
* @param ctx 已拼好的 SyncCheckInput(可选;不传则内部自拼,供测试直接调用)
|
|
851
|
+
*/
|
|
852
|
+
export async function emitPostHookSyncCheck(forgeRoot, changeId, ctx) {
|
|
853
|
+
// ctx 未传(测试直接调用)→ 自行加载 config + anchors 拼 context(archived=true)
|
|
854
|
+
const syncCtx = ctx ?? (await buildSyncCheckContextStandalone(forgeRoot, changeId, true));
|
|
855
|
+
if (!syncCtx)
|
|
856
|
+
return { kind: 'skip' }; // standalone 加载失败
|
|
857
|
+
// 构建 LlmTask
|
|
858
|
+
const task = await buildSyncCheckTask(syncCtx, async (p) => (await readAnchorFile(p)).text);
|
|
859
|
+
// 无受影响 anchor → graceful skip
|
|
860
|
+
if (!task)
|
|
861
|
+
return { kind: 'skip' };
|
|
862
|
+
// emit sync-check manifest(meta 键名与 Task 6.2 一致:changeId 驼峰)
|
|
863
|
+
await new AgentHandoffRunner(forgeRoot, FORGE_VERSION).emit('sync-check', 1, [task], {
|
|
864
|
+
gate_context: 'archive-posthook',
|
|
865
|
+
changeId,
|
|
866
|
+
});
|
|
867
|
+
// 非阻塞:不写暂停态文件(archive 已完成)
|
|
868
|
+
return { kind: 'emitted' };
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Task 6.3 I-3 配套:emitPreflightSyncCheck/emitPostHookSyncCheck 被测试直接调用
|
|
872
|
+
* (不经 runArchivePreflight/runArchivePostHook gate)时,自行加载 config + anchors。
|
|
873
|
+
*
|
|
874
|
+
* archived=true 时用「今日」作 archiveDate —— 仅 standalone 测试路径,生产路径
|
|
875
|
+
* 由 runArchivePostHook 透传准确的 archiveDate(I-2 已修主路径跨午夜 bug)。
|
|
876
|
+
*
|
|
877
|
+
* 返回 null:config 缺失 / anchors 缺失 → 调用方按 skip 处理。
|
|
878
|
+
*/
|
|
879
|
+
async function buildSyncCheckContextStandalone(forgeRoot, changeId, archived) {
|
|
880
|
+
const configPath = join(forgeRoot, 'config.yaml');
|
|
881
|
+
if (!existsSync(configPath))
|
|
882
|
+
return null;
|
|
883
|
+
const config = parseYaml(await readFile(configPath, 'utf8'));
|
|
884
|
+
const anchors = await loadAnchorsFile(forgeRoot).catch(() => null);
|
|
885
|
+
if (!anchors)
|
|
886
|
+
return null;
|
|
887
|
+
return buildSyncCheckChangeContext(forgeRoot, changeId, { archived, archiveDate: archived ? new Date().toISOString().slice(0, 10) : undefined }, config, anchors);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Task 6.4:forge archive --resume 的 gate 复核。
|
|
891
|
+
*
|
|
892
|
+
* 复核两项:
|
|
893
|
+
* ① 产物绑定 — sync-state 的 produced_from 必须等于暂停态文件的 manifest_hash,
|
|
894
|
+
* 确保 sync-check --apply 产出的 sync-state 确实来自本次 preflight emit 的 manifest。
|
|
895
|
+
* ② critical 幂等重评 — sync-state 中不得有 severity=critical + status=pending 的差异;
|
|
896
|
+
* 若有,说明 agent 未完全 resolve,不可续跑 archive。
|
|
897
|
+
*
|
|
898
|
+
* @param forgeRoot forge/ 根目录路径
|
|
899
|
+
* @param changeId change id(e.g. 'add-pay')
|
|
900
|
+
* @returns { ok: true } 表示 gate 通过;{ ok: false, reason } 表示拒绝并附原因
|
|
901
|
+
*/
|
|
902
|
+
export async function resumeArchiveGateCheck(forgeRoot, changeId) {
|
|
903
|
+
// 读暂停态文件:forge/.cache/archive-pause-<changeId>.json
|
|
904
|
+
const pausePath = join(forgeRoot, '.cache', `archive-pause-${changeId}.json`);
|
|
905
|
+
if (!existsSync(pausePath)) {
|
|
906
|
+
return {
|
|
907
|
+
ok: false,
|
|
908
|
+
reason: `无暂停态文件 archive-pause-${changeId}.json;archive 未在 preflight 暂停,无需 --resume`,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
// 暂停态文件格式(Task 6.3 emitPreflightSyncCheck 写入):{ changeId, paused_step, manifest_hash }
|
|
912
|
+
// I-1:parse 包 try/catch —— 文件截断/手动误编辑致 JSON 损坏时转 business-fail,
|
|
913
|
+
// 用户看得出是哪个文件坏(否则裸 SyntaxError 被 action 外层兜底当未知错误)。
|
|
914
|
+
// readFile 的 ENOENT 竞态(existsSync 后文件被删)也顺带被这层 catch 兜住,可接受。
|
|
915
|
+
let pause;
|
|
916
|
+
try {
|
|
917
|
+
pause = JSON.parse(await readFile(pausePath, 'utf8'));
|
|
918
|
+
}
|
|
919
|
+
catch (err) {
|
|
920
|
+
return {
|
|
921
|
+
ok: false,
|
|
922
|
+
reason: `暂停态文件 ${pausePath} 解析失败(可能损坏):${err.message}`,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
// 读 sync-state YAML:forge/legacy-sync-state/<changeId>.yaml
|
|
926
|
+
const statePath = join(forgeRoot, 'legacy-sync-state', `${changeId}.yaml`);
|
|
927
|
+
if (!existsSync(statePath)) {
|
|
928
|
+
return {
|
|
929
|
+
ok: false,
|
|
930
|
+
reason: `sync-state 缺失(forge/legacy-sync-state/${changeId}.yaml);请先让 agent fulfill manifest 后跑 forge legacy-bridge sync-check --apply --change-id ${changeId}`,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
// I-1:同样包 try/catch —— sync-state YAML 损坏时转 business-fail
|
|
934
|
+
let state;
|
|
935
|
+
try {
|
|
936
|
+
state = parseYaml(await readFile(statePath, 'utf8'));
|
|
937
|
+
}
|
|
938
|
+
catch (err) {
|
|
939
|
+
return {
|
|
940
|
+
ok: false,
|
|
941
|
+
reason: `sync-state 文件 ${statePath} 解析失败(可能损坏):${err.message}`,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
// ① 产物绑定:sync-state.produced_from 必须等于暂停态的 manifest_hash
|
|
945
|
+
if (state.produced_from !== pause.manifest_hash) {
|
|
946
|
+
return {
|
|
947
|
+
ok: false,
|
|
948
|
+
reason: `produced_from(${state.produced_from ?? 'undefined'}) ≠ 暂停态 manifest_hash(${pause.manifest_hash});sync-state 非本次 --apply 产物,请重新履行 manifest 并跑 sync-check --apply`,
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
// ② critical 幂等重评:sync-state 中不得有 critical+pending 差异
|
|
952
|
+
// 注:archive.ts Task 6.3 已用内联 filter 而非 hasCriticalPending import,此处保持一致
|
|
953
|
+
const criticalPending = (state.diffs ?? []).filter((d) => d.severity === 'critical' && d.status === 'pending');
|
|
954
|
+
if (criticalPending.length > 0) {
|
|
955
|
+
return {
|
|
956
|
+
ok: false,
|
|
957
|
+
reason: `仍有 ${criticalPending.length} 项 critical 差异未 resolve;跑 forge legacy-bridge resolve ${changeId} 后重试`,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
return { ok: true };
|
|
688
961
|
}
|
|
689
962
|
//# sourceMappingURL=archive.js.map
|