@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 账本 tenant 归属修复:授权 apply / backup / rollback / 回执(issue #254 执行面)。
|
|
3
|
+
*
|
|
4
|
+
* 硬纪律:
|
|
5
|
+
* A. 三重门:显式 `--i-authorize-apply` + 匹配的 `planDigest` + 可 apply 的证据映射计划。
|
|
6
|
+
* B. 写前强制 backup(db + -wal/-shm + manifest 校验和);损坏 backup fail-closed。
|
|
7
|
+
* C. 原地 UPDATE 保留 seq;CAS `WHERE seq=? AND tenant_id=?`;整批单一事务。
|
|
8
|
+
* D. 成功后对受影响 tenant 调用既有 rebuildProjections;不做启发式归属。
|
|
9
|
+
* E. 幂等:若映射目标已全部就位且存在同 digest 的 applied stamp,重跑成功且无额外变更。
|
|
10
|
+
* F. 本模块默认不触碰 founder 真账本;真实 repair 另走 owner gate + 私有回执。
|
|
11
|
+
*/
|
|
12
|
+
import Database from 'better-sqlite3';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { ledgerDbPath, ledgerExists, openDb } from './state-ledger.js';
|
|
17
|
+
import { parseEvidenceMappings, planLedgerRepair, } from './ledger-repair-plan.js';
|
|
18
|
+
export const REPAIR_BACKUP_SCHEMA = 'gotry.ledger-repair-backup/1';
|
|
19
|
+
export const REPAIR_APPLY_SCHEMA = 'gotry.ledger-repair-apply/1';
|
|
20
|
+
export const REPAIR_RECEIPT_SCHEMA = 'gotry.ledger-repair-receipt/1';
|
|
21
|
+
export const REPAIR_APPLIED_STAMP_SCHEMA = 'gotry.ledger-repair-applied/1';
|
|
22
|
+
const DB_SIDECARS = ['', '-wal', '-shm'];
|
|
23
|
+
export class RepairApplyError extends Error {
|
|
24
|
+
code;
|
|
25
|
+
constructor(code, message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'RepairApplyError';
|
|
28
|
+
this.code = code;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function sha256Bytes(bytes) {
|
|
32
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
33
|
+
}
|
|
34
|
+
function sha256File(path) {
|
|
35
|
+
const bytes = readFileSync(path);
|
|
36
|
+
return { sha256: sha256Bytes(bytes), bytes: bytes.length };
|
|
37
|
+
}
|
|
38
|
+
function fileDigestOrMissing(path) {
|
|
39
|
+
if (!existsSync(path))
|
|
40
|
+
return { present: false, sha256: null, bytes: null };
|
|
41
|
+
const d = sha256File(path);
|
|
42
|
+
return { present: true, sha256: d.sha256, bytes: d.bytes };
|
|
43
|
+
}
|
|
44
|
+
function sourceComponentDigest(path) {
|
|
45
|
+
const d = fileDigestOrMissing(path);
|
|
46
|
+
if (!d.present)
|
|
47
|
+
return 'missing';
|
|
48
|
+
return `present:${d.bytes}:${d.sha256}`;
|
|
49
|
+
}
|
|
50
|
+
function sourceGroupDigest(dbPath) {
|
|
51
|
+
return DB_SIDECARS.map(suffix => `${suffix}\0${sourceComponentDigest(dbPath + suffix)}`).join('\n');
|
|
52
|
+
}
|
|
53
|
+
export function mappingContentSha256(mappings) {
|
|
54
|
+
return sha256Bytes(JSON.stringify(mappings));
|
|
55
|
+
}
|
|
56
|
+
export function redactedPath(path) {
|
|
57
|
+
return sha256Bytes(path).slice(0, 16);
|
|
58
|
+
}
|
|
59
|
+
export function stateRootFingerprint(stateRoot) {
|
|
60
|
+
const dbPath = ledgerDbPath(stateRoot);
|
|
61
|
+
return sha256Bytes(sourceGroupDigest(dbPath)).slice(0, 32);
|
|
62
|
+
}
|
|
63
|
+
function tryGitHead() {
|
|
64
|
+
try {
|
|
65
|
+
// Lazy require via spawn would be heavy; allow env override for tests / CI.
|
|
66
|
+
if (process.env['GOTRY_REPAIR_GIT_HEAD'])
|
|
67
|
+
return process.env['GOTRY_REPAIR_GIT_HEAD'];
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function backupRoot(stateRoot) {
|
|
75
|
+
return join(stateRoot, '.gotry-repair-backup');
|
|
76
|
+
}
|
|
77
|
+
function stampDir(stateRoot) {
|
|
78
|
+
return join(stateRoot, 'gotry-state', '.gotry-repair-applied');
|
|
79
|
+
}
|
|
80
|
+
export function stampPath(stateRoot, planDigest) {
|
|
81
|
+
return join(stampDir(stateRoot), `${planDigest}.json`);
|
|
82
|
+
}
|
|
83
|
+
function isoStamp() {
|
|
84
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
85
|
+
}
|
|
86
|
+
function atomicWriteJson(path, value) {
|
|
87
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
88
|
+
const tmp = `${path}.tmp`;
|
|
89
|
+
writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', 'utf-8');
|
|
90
|
+
renameSync(tmp, path);
|
|
91
|
+
}
|
|
92
|
+
function readJsonFile(path) {
|
|
93
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
94
|
+
}
|
|
95
|
+
function censusFromDb(db) {
|
|
96
|
+
const hasEvents = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='events'").get();
|
|
97
|
+
if (!hasEvents)
|
|
98
|
+
return [];
|
|
99
|
+
const cols = db.prepare('PRAGMA table_info(events)').all();
|
|
100
|
+
if (!cols.some(c => c.name === 'tenant_id')) {
|
|
101
|
+
const n = db.prepare('SELECT COUNT(*) AS n FROM events').get().n;
|
|
102
|
+
return n ? [{ tenant: 'local', events: n }] : [];
|
|
103
|
+
}
|
|
104
|
+
return db.prepare(`SELECT tenant_id AS tenant, COUNT(*) AS events FROM events GROUP BY tenant_id ORDER BY tenant_id`).all();
|
|
105
|
+
}
|
|
106
|
+
function openWritableDb(stateRoot) {
|
|
107
|
+
const path = ledgerDbPath(stateRoot);
|
|
108
|
+
if (!existsSync(path))
|
|
109
|
+
throw new RepairApplyError('ledger_missing', `账本不存在:${path}`);
|
|
110
|
+
const db = new Database(path);
|
|
111
|
+
db.pragma('journal_mode = WAL');
|
|
112
|
+
db.pragma('synchronous = FULL');
|
|
113
|
+
db.pragma('busy_timeout = 5000');
|
|
114
|
+
return db;
|
|
115
|
+
}
|
|
116
|
+
/** Checkpoint WAL into the main db so backup/restore is a coherent snapshot. */
|
|
117
|
+
function checkpointWal(db) {
|
|
118
|
+
db.pragma('wal_checkpoint(FULL)');
|
|
119
|
+
}
|
|
120
|
+
export function createRepairBackup(opts) {
|
|
121
|
+
const dbPath = ledgerDbPath(opts.stateRoot);
|
|
122
|
+
if (!ledgerExists(opts.stateRoot)) {
|
|
123
|
+
throw new RepairApplyError('ledger_missing', `账本不存在:${dbPath}`);
|
|
124
|
+
}
|
|
125
|
+
// Open briefly to checkpoint, then close before copying.
|
|
126
|
+
{
|
|
127
|
+
const db = openWritableDb(opts.stateRoot);
|
|
128
|
+
try {
|
|
129
|
+
checkpointWal(db);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
db.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const before = sourceGroupDigest(dbPath);
|
|
136
|
+
const backupDir = join(backupRoot(opts.stateRoot), `${isoStamp()}-${opts.planDigest}`);
|
|
137
|
+
mkdirSync(backupDir, { recursive: true });
|
|
138
|
+
const files = [];
|
|
139
|
+
for (const suffix of DB_SIDECARS) {
|
|
140
|
+
const src = dbPath + suffix;
|
|
141
|
+
const dest = join(backupDir, `gotry-state.db${suffix}`);
|
|
142
|
+
if (!existsSync(src)) {
|
|
143
|
+
files.push({ suffix, present: false, sha256: null, bytes: null });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
copyFileSync(src, dest);
|
|
147
|
+
const d = sha256File(dest);
|
|
148
|
+
files.push({ suffix, present: true, sha256: d.sha256, bytes: d.bytes });
|
|
149
|
+
}
|
|
150
|
+
if (sourceGroupDigest(dbPath) !== before) {
|
|
151
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
152
|
+
throw new RepairApplyError('source_changed_during_backup', '备份期间正本发生变化,已拒绝继续');
|
|
153
|
+
}
|
|
154
|
+
const manifest = {
|
|
155
|
+
schema: REPAIR_BACKUP_SCHEMA,
|
|
156
|
+
planDigest: opts.planDigest,
|
|
157
|
+
mappingSha256: opts.mappingSha256,
|
|
158
|
+
createdAt: new Date().toISOString(),
|
|
159
|
+
sourceTenant: opts.sourceTenant,
|
|
160
|
+
tenantCensusBefore: opts.tenantCensusBefore,
|
|
161
|
+
files,
|
|
162
|
+
gitHead: tryGitHead(),
|
|
163
|
+
};
|
|
164
|
+
atomicWriteJson(join(backupDir, 'manifest.json'), manifest);
|
|
165
|
+
return { backupDir, manifest };
|
|
166
|
+
}
|
|
167
|
+
export function loadBackupManifest(backupDir) {
|
|
168
|
+
const path = join(backupDir, 'manifest.json');
|
|
169
|
+
if (!existsSync(path))
|
|
170
|
+
throw new RepairApplyError('backup_manifest_missing', `缺少 manifest:${path}`);
|
|
171
|
+
const manifest = readJsonFile(path);
|
|
172
|
+
if (manifest.schema !== REPAIR_BACKUP_SCHEMA) {
|
|
173
|
+
throw new RepairApplyError('backup_schema_invalid', `未知 backup schema:${String(manifest.schema)}`);
|
|
174
|
+
}
|
|
175
|
+
return manifest;
|
|
176
|
+
}
|
|
177
|
+
/** Verify backup files match manifest checksums. */
|
|
178
|
+
export function validateRepairBackup(backupDir) {
|
|
179
|
+
const manifest = loadBackupManifest(backupDir);
|
|
180
|
+
for (const f of manifest.files) {
|
|
181
|
+
const path = join(backupDir, `gotry-state.db${f.suffix}`);
|
|
182
|
+
if (!f.present) {
|
|
183
|
+
if (existsSync(path)) {
|
|
184
|
+
throw new RepairApplyError('backup_corrupt', `manifest 标记缺失但文件存在:${path}`);
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!existsSync(path)) {
|
|
189
|
+
throw new RepairApplyError('backup_corrupt', `backup 文件缺失:${path}`);
|
|
190
|
+
}
|
|
191
|
+
const d = sha256File(path);
|
|
192
|
+
if (d.sha256 !== f.sha256 || d.bytes !== f.bytes) {
|
|
193
|
+
throw new RepairApplyError('backup_checksum_mismatch', `backup 校验和不匹配:${path}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return manifest;
|
|
197
|
+
}
|
|
198
|
+
export function restoreRepairBackup(opts) {
|
|
199
|
+
if (opts.authorizeRollback !== true) {
|
|
200
|
+
throw new RepairApplyError('rollback_not_authorized', '缺少显式 rollback 授权');
|
|
201
|
+
}
|
|
202
|
+
const manifest = validateRepairBackup(opts.backupDir);
|
|
203
|
+
const dbPath = ledgerDbPath(opts.stateRoot);
|
|
204
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
205
|
+
// Replace live db files. Callers must not hold open SQLite handles on this root.
|
|
206
|
+
for (const suffix of DB_SIDECARS) {
|
|
207
|
+
const live = dbPath + suffix;
|
|
208
|
+
if (existsSync(live))
|
|
209
|
+
rmSync(live);
|
|
210
|
+
}
|
|
211
|
+
for (const f of manifest.files) {
|
|
212
|
+
if (!f.present)
|
|
213
|
+
continue;
|
|
214
|
+
const src = join(opts.backupDir, `gotry-state.db${f.suffix}`);
|
|
215
|
+
copyFileSync(src, dbPath + f.suffix);
|
|
216
|
+
}
|
|
217
|
+
// Re-validate live files against manifest.
|
|
218
|
+
for (const f of manifest.files) {
|
|
219
|
+
const live = dbPath + f.suffix;
|
|
220
|
+
if (!f.present) {
|
|
221
|
+
if (existsSync(live)) {
|
|
222
|
+
throw new RepairApplyError('rollback_verify_failed', `还原后出现不应存在的 sidecar:${live}`);
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const d = sha256File(live);
|
|
227
|
+
if (d.sha256 !== f.sha256) {
|
|
228
|
+
throw new RepairApplyError('rollback_verify_failed', `还原后校验和与 backup 不一致:${live}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { restoredFrom: opts.backupDir, manifest };
|
|
232
|
+
}
|
|
233
|
+
function loadAppliedStamp(stateRoot, planDigest) {
|
|
234
|
+
const path = stampPath(stateRoot, planDigest);
|
|
235
|
+
if (!existsSync(path))
|
|
236
|
+
return null;
|
|
237
|
+
const stamp = readJsonFile(path);
|
|
238
|
+
if (stamp.schema !== REPAIR_APPLIED_STAMP_SCHEMA)
|
|
239
|
+
return null;
|
|
240
|
+
if (stamp.planDigest !== planDigest)
|
|
241
|
+
return null;
|
|
242
|
+
return stamp;
|
|
243
|
+
}
|
|
244
|
+
function eventRow(db, seq) {
|
|
245
|
+
return db.prepare(`SELECT seq, tenant_id, kind, subject_id, idem_key FROM events WHERE seq = ?`).get(seq);
|
|
246
|
+
}
|
|
247
|
+
function verifyAlreadyApplied(db, mappings, planMoves) {
|
|
248
|
+
const bySeq = new Map(planMoves.map(m => [m.seq, m]));
|
|
249
|
+
const records = [];
|
|
250
|
+
for (const m of mappings) {
|
|
251
|
+
const planned = bySeq.get(m.seq);
|
|
252
|
+
const row = eventRow(db, m.seq);
|
|
253
|
+
if (!row)
|
|
254
|
+
return null;
|
|
255
|
+
if (row.tenant_id !== m.toTenant)
|
|
256
|
+
return null;
|
|
257
|
+
records.push({
|
|
258
|
+
seq: m.seq,
|
|
259
|
+
beforeTenant: m.fromTenant,
|
|
260
|
+
afterTenant: m.toTenant,
|
|
261
|
+
status: 'already_applied',
|
|
262
|
+
kind: planned?.kind ?? row.kind,
|
|
263
|
+
subjectId: planned?.subjectId ?? row.subject_id,
|
|
264
|
+
idemKey: planned?.idemKey ?? row.idem_key,
|
|
265
|
+
evidence: m.evidence,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return records;
|
|
269
|
+
}
|
|
270
|
+
function buildReceipt(opts) {
|
|
271
|
+
return {
|
|
272
|
+
schema: REPAIR_RECEIPT_SCHEMA,
|
|
273
|
+
planDigest: opts.planDigest,
|
|
274
|
+
mappingSha256: opts.mappingSha256,
|
|
275
|
+
authorizedBy: opts.authorizedBy,
|
|
276
|
+
executedAt: new Date().toISOString(),
|
|
277
|
+
gitHead: tryGitHead(),
|
|
278
|
+
stateRootFingerprint: stateRootFingerprint(opts.stateRoot),
|
|
279
|
+
stateRootRedacted: redactedPath(opts.stateRoot),
|
|
280
|
+
backupDirRedacted: redactedPath(opts.backupDir),
|
|
281
|
+
sourceTenant: opts.sourceTenant,
|
|
282
|
+
tenantCensusBefore: opts.before,
|
|
283
|
+
tenantCensusAfter: opts.after,
|
|
284
|
+
affectedSeqs: opts.moves.map(m => m.seq),
|
|
285
|
+
moves: opts.moves,
|
|
286
|
+
mode: opts.mode,
|
|
287
|
+
notes: opts.notes,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
export function applyLedgerRepair(opts) {
|
|
291
|
+
if (opts.authorizeApply !== true) {
|
|
292
|
+
throw new RepairApplyError('apply_not_authorized', '缺少显式 apply 授权');
|
|
293
|
+
}
|
|
294
|
+
if (!/^[0-9a-f]{16}$/.test(opts.planDigest)) {
|
|
295
|
+
throw new RepairApplyError('plan_digest_invalid', `planDigest 必须是 16 位小写 hex:${opts.planDigest}`);
|
|
296
|
+
}
|
|
297
|
+
if (!ledgerExists(opts.stateRoot)) {
|
|
298
|
+
throw new RepairApplyError('ledger_missing', `账本不存在:${ledgerDbPath(opts.stateRoot)}`);
|
|
299
|
+
}
|
|
300
|
+
const sourceTenant = opts.sourceTenant ?? 'local';
|
|
301
|
+
const mappingSha = mappingContentSha256(opts.mappings);
|
|
302
|
+
const authorizedBy = opts.authorizedBy ?? 'cli:i-authorize-apply';
|
|
303
|
+
const plan = planLedgerRepair({
|
|
304
|
+
stateRoot: opts.stateRoot,
|
|
305
|
+
sourceTenant,
|
|
306
|
+
mappings: opts.mappings,
|
|
307
|
+
});
|
|
308
|
+
const parsed = parseEvidenceMappings(opts.mappings, sourceTenant);
|
|
309
|
+
const intendedMoves = parsed.valid.filter(m => m.toTenant !== m.fromTenant);
|
|
310
|
+
// Path A: fresh applicable plan with matching digest.
|
|
311
|
+
if (plan.planDigest === opts.planDigest && plan.applyable) {
|
|
312
|
+
return executeMoves({
|
|
313
|
+
stateRoot: opts.stateRoot,
|
|
314
|
+
sourceTenant,
|
|
315
|
+
plan,
|
|
316
|
+
mappingSha,
|
|
317
|
+
authorizedBy,
|
|
318
|
+
receiptOut: opts.receiptOut,
|
|
319
|
+
hooks: opts.hooks,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
// Path B: idempotent already-applied (stamp + live rows at destinations).
|
|
323
|
+
const stamp = loadAppliedStamp(opts.stateRoot, opts.planDigest);
|
|
324
|
+
if (stamp && stamp.mappingSha256 === mappingSha) {
|
|
325
|
+
const db = openWritableDb(opts.stateRoot);
|
|
326
|
+
try {
|
|
327
|
+
const records = verifyAlreadyApplied(db, intendedMoves.length ? intendedMoves : stamp.moves.map(m => ({
|
|
328
|
+
seq: m.seq,
|
|
329
|
+
fromTenant: m.beforeTenant,
|
|
330
|
+
toTenant: m.afterTenant,
|
|
331
|
+
evidence: m.evidence ?? 'stamp',
|
|
332
|
+
})), plan.entries.filter(e => e.decision === 'move' || e.decision === 'reject'));
|
|
333
|
+
if (records) {
|
|
334
|
+
const after = censusFromDb(db);
|
|
335
|
+
const notes = [
|
|
336
|
+
'幂等重跑:applied stamp 与目标 tenant 行均已就位,未做额外写入。',
|
|
337
|
+
'fixture 绿不等于真实 founder repair 已执行。',
|
|
338
|
+
];
|
|
339
|
+
const receipt = buildReceipt({
|
|
340
|
+
planDigest: opts.planDigest,
|
|
341
|
+
mappingSha256: mappingSha,
|
|
342
|
+
authorizedBy,
|
|
343
|
+
stateRoot: opts.stateRoot,
|
|
344
|
+
backupDir: stamp.backupDir,
|
|
345
|
+
sourceTenant,
|
|
346
|
+
before: stamp.tenantCensusAfter,
|
|
347
|
+
after,
|
|
348
|
+
moves: records,
|
|
349
|
+
mode: 'already_applied',
|
|
350
|
+
notes,
|
|
351
|
+
});
|
|
352
|
+
if (opts.receiptOut)
|
|
353
|
+
atomicWriteJson(opts.receiptOut, receipt);
|
|
354
|
+
return {
|
|
355
|
+
schema: REPAIR_APPLY_SCHEMA,
|
|
356
|
+
applyAuthorized: true,
|
|
357
|
+
mode: 'already_applied',
|
|
358
|
+
planDigest: opts.planDigest,
|
|
359
|
+
mappingSha256: mappingSha,
|
|
360
|
+
backupDir: stamp.backupDir,
|
|
361
|
+
sourceTenant,
|
|
362
|
+
tenantCensusBefore: stamp.tenantCensusAfter,
|
|
363
|
+
tenantCensusAfter: after,
|
|
364
|
+
moves: records,
|
|
365
|
+
rebuiltTenants: [],
|
|
366
|
+
receipt,
|
|
367
|
+
notes,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
db.close();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (plan.planDigest !== opts.planDigest) {
|
|
376
|
+
throw new RepairApplyError('plan_digest_mismatch', `planDigest 不匹配:expected=${opts.planDigest} actual=${plan.planDigest}`);
|
|
377
|
+
}
|
|
378
|
+
throw new RepairApplyError('plan_not_applyable', `计划不可 apply(applyable=${plan.applyable}, move=${plan.totals.move}, reject=${plan.totals.reject})`);
|
|
379
|
+
}
|
|
380
|
+
function executeMoves(opts) {
|
|
381
|
+
const moves = opts.plan.entries.filter(e => e.decision === 'move');
|
|
382
|
+
if (moves.length === 0) {
|
|
383
|
+
throw new RepairApplyError('plan_not_applyable', '计划无 move 条目');
|
|
384
|
+
}
|
|
385
|
+
const { backupDir } = createRepairBackup({
|
|
386
|
+
stateRoot: opts.stateRoot,
|
|
387
|
+
planDigest: opts.plan.planDigest,
|
|
388
|
+
mappingSha256: opts.mappingSha,
|
|
389
|
+
sourceTenant: opts.sourceTenant,
|
|
390
|
+
tenantCensusBefore: opts.plan.tenantCensusBefore,
|
|
391
|
+
});
|
|
392
|
+
validateRepairBackup(backupDir);
|
|
393
|
+
const db = openWritableDb(opts.stateRoot);
|
|
394
|
+
const records = [];
|
|
395
|
+
const injectAfter = opts.hooks?.injectFailureAfterMoves;
|
|
396
|
+
try {
|
|
397
|
+
const run = db.transaction(() => {
|
|
398
|
+
const update = db.prepare(`UPDATE events SET tenant_id = ? WHERE seq = ? AND tenant_id = ?`);
|
|
399
|
+
let movedCount = 0;
|
|
400
|
+
for (const m of moves) {
|
|
401
|
+
const row = eventRow(db, m.seq);
|
|
402
|
+
if (!row) {
|
|
403
|
+
throw new RepairApplyError('cas_miss', `seq ${m.seq} 不存在`);
|
|
404
|
+
}
|
|
405
|
+
if (row.tenant_id === m.afterTenant) {
|
|
406
|
+
records.push({
|
|
407
|
+
seq: m.seq,
|
|
408
|
+
beforeTenant: m.beforeTenant,
|
|
409
|
+
afterTenant: m.afterTenant,
|
|
410
|
+
status: 'already_applied',
|
|
411
|
+
kind: m.kind,
|
|
412
|
+
subjectId: m.subjectId,
|
|
413
|
+
idemKey: m.idemKey,
|
|
414
|
+
evidence: m.evidence,
|
|
415
|
+
});
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (row.tenant_id !== m.beforeTenant) {
|
|
419
|
+
throw new RepairApplyError('cas_miss', `seq ${m.seq} 当前 tenant=${row.tenant_id},期望 before=${m.beforeTenant}`);
|
|
420
|
+
}
|
|
421
|
+
const info = update.run(m.afterTenant, m.seq, m.beforeTenant);
|
|
422
|
+
if (info.changes !== 1) {
|
|
423
|
+
throw new RepairApplyError('cas_miss', `seq ${m.seq} CAS UPDATE 未命中`);
|
|
424
|
+
}
|
|
425
|
+
movedCount++;
|
|
426
|
+
records.push({
|
|
427
|
+
seq: m.seq,
|
|
428
|
+
beforeTenant: m.beforeTenant,
|
|
429
|
+
afterTenant: m.afterTenant,
|
|
430
|
+
status: 'moved',
|
|
431
|
+
kind: m.kind,
|
|
432
|
+
subjectId: m.subjectId,
|
|
433
|
+
idemKey: m.idemKey,
|
|
434
|
+
evidence: m.evidence,
|
|
435
|
+
});
|
|
436
|
+
if (injectAfter !== undefined && movedCount >= injectAfter) {
|
|
437
|
+
throw new RepairApplyError('injected_failure', `测试注入失败:已成功 ${movedCount} 条后中止`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
run();
|
|
442
|
+
checkpointWal(db);
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
db.close();
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
db.close();
|
|
449
|
+
// Rebuild projections for every affected tenant via the normal ledger API.
|
|
450
|
+
const tenants = [...new Set(records.flatMap(r => [r.beforeTenant, r.afterTenant]))].sort();
|
|
451
|
+
for (const tenant of tenants) {
|
|
452
|
+
const led = openDb(opts.stateRoot, tenant);
|
|
453
|
+
led.rebuildProjections();
|
|
454
|
+
led.close();
|
|
455
|
+
}
|
|
456
|
+
const verifyDb = openWritableDb(opts.stateRoot);
|
|
457
|
+
let after;
|
|
458
|
+
try {
|
|
459
|
+
after = censusFromDb(verifyDb);
|
|
460
|
+
for (const r of records) {
|
|
461
|
+
const row = eventRow(verifyDb, r.seq);
|
|
462
|
+
if (!row || row.tenant_id !== r.afterTenant) {
|
|
463
|
+
throw new RepairApplyError('post_apply_verify_failed', `seq ${r.seq} 未落到 ${r.afterTenant}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
finally {
|
|
468
|
+
verifyDb.close();
|
|
469
|
+
}
|
|
470
|
+
const notes = [
|
|
471
|
+
'已通过显式授权门执行 apply;事件 seq 保留,仅更新 tenant_id。',
|
|
472
|
+
'已对受影响 tenant 重建投影。',
|
|
473
|
+
'fixture/隔离 stateRoot 证据不等于真实 founder repair。',
|
|
474
|
+
];
|
|
475
|
+
const stamp = {
|
|
476
|
+
schema: REPAIR_APPLIED_STAMP_SCHEMA,
|
|
477
|
+
planDigest: opts.plan.planDigest,
|
|
478
|
+
mappingSha256: opts.mappingSha,
|
|
479
|
+
appliedAt: new Date().toISOString(),
|
|
480
|
+
sourceTenant: opts.sourceTenant,
|
|
481
|
+
moves: records,
|
|
482
|
+
tenantCensusAfter: after,
|
|
483
|
+
backupDir,
|
|
484
|
+
};
|
|
485
|
+
atomicWriteJson(stampPath(opts.stateRoot, opts.plan.planDigest), stamp);
|
|
486
|
+
const receipt = buildReceipt({
|
|
487
|
+
planDigest: opts.plan.planDigest,
|
|
488
|
+
mappingSha256: opts.mappingSha,
|
|
489
|
+
authorizedBy: opts.authorizedBy,
|
|
490
|
+
stateRoot: opts.stateRoot,
|
|
491
|
+
backupDir,
|
|
492
|
+
sourceTenant: opts.sourceTenant,
|
|
493
|
+
before: opts.plan.tenantCensusBefore,
|
|
494
|
+
after,
|
|
495
|
+
moves: records,
|
|
496
|
+
mode: 'applied',
|
|
497
|
+
notes,
|
|
498
|
+
});
|
|
499
|
+
if (opts.receiptOut)
|
|
500
|
+
atomicWriteJson(opts.receiptOut, receipt);
|
|
501
|
+
return {
|
|
502
|
+
schema: REPAIR_APPLY_SCHEMA,
|
|
503
|
+
applyAuthorized: true,
|
|
504
|
+
mode: 'applied',
|
|
505
|
+
planDigest: opts.plan.planDigest,
|
|
506
|
+
mappingSha256: opts.mappingSha,
|
|
507
|
+
backupDir,
|
|
508
|
+
sourceTenant: opts.sourceTenant,
|
|
509
|
+
tenantCensusBefore: opts.plan.tenantCensusBefore,
|
|
510
|
+
tenantCensusAfter: after,
|
|
511
|
+
moves: records,
|
|
512
|
+
rebuiltTenants: tenants,
|
|
513
|
+
receipt,
|
|
514
|
+
notes,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
export function formatRepairApplyResult(result) {
|
|
518
|
+
const lines = [];
|
|
519
|
+
lines.push(`账本 tenant 修复 apply(${result.mode})`);
|
|
520
|
+
lines.push(`plan-digest=${result.planDigest}`);
|
|
521
|
+
lines.push(`mapping-sha256=${result.mappingSha256}`);
|
|
522
|
+
lines.push(`backup=${result.backupDir}`);
|
|
523
|
+
lines.push(`源租户 scope: ${result.sourceTenant}`);
|
|
524
|
+
lines.push(`census before: ${result.tenantCensusBefore.map(t => `${t.tenant}=${t.events}`).join(' ') || '(空)'}`);
|
|
525
|
+
lines.push(`census after : ${result.tenantCensusAfter.map(t => `${t.tenant}=${t.events}`).join(' ') || '(空)'}`);
|
|
526
|
+
lines.push(`rebuilt tenants: ${result.rebuiltTenants.join(',') || '(none)'}`);
|
|
527
|
+
lines.push('');
|
|
528
|
+
lines.push(` seq status before -> after`);
|
|
529
|
+
for (const m of result.moves) {
|
|
530
|
+
lines.push(`${String(m.seq).padStart(5)} ${m.status.padEnd(16)} ${m.beforeTenant} -> ${m.afterTenant}`);
|
|
531
|
+
}
|
|
532
|
+
lines.push('');
|
|
533
|
+
for (const n of result.notes)
|
|
534
|
+
lines.push(`* ${n}`);
|
|
535
|
+
lines.push(`applyAuthorized=${result.applyAuthorized}`);
|
|
536
|
+
lines.push(`receipt-schema=${result.receipt.schema}`);
|
|
537
|
+
return lines.join('\n');
|
|
538
|
+
}
|