@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,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 账本 tenant 归属修复:只读 inventory + dry-run 计划(issue #254 第一切片)。
|
|
3
|
+
*
|
|
4
|
+
* 背景(docs/design/milestone-delivery-plan.md §161):legacy/v1 数据只安全归入默认
|
|
5
|
+
* `local`;历史上被误写为 `local` 的非 local tenant 事件**无法无证据自动反推**,
|
|
6
|
+
* 必须走人工 data-repair。本模块只回答两个问题:
|
|
7
|
+
* 1) inventory:源 tenant scope 下有哪些候选事件、全库租户分布如何;
|
|
8
|
+
* 2) dry-run:给定**人工确认的显式证据映射**,计划会把哪些事件搬到哪个 tenant,
|
|
9
|
+
* 哪些因无证据原地保留,哪些因冲突被拒。
|
|
10
|
+
*
|
|
11
|
+
* 三条硬纪律:
|
|
12
|
+
*
|
|
13
|
+
* A. **物理零写**。本模块从不打开正本账本:先把 db(连同 -wal/-shm)复制到临时
|
|
14
|
+
* 目录,只在副本上开 readonly 句柄读,用完即删。两个理由:
|
|
15
|
+
* - `openDb`/`openLedgerIfExists` 在 v1 形态账本上会 ALTER events 加
|
|
16
|
+
* tenant_id、把 user_version 顶到 2、DROP 重建全部投影(实测:一次
|
|
17
|
+
* 「只读」意图的 countEvents 就改写了 v1 账本);
|
|
18
|
+
* - 即便老老实实用 readonly 直开正本,SQLite 为 WAL 库建共享内存索引时
|
|
19
|
+
* 仍会在正本目录里生成 -shm/-wal(实测),只读连接也无权清理。
|
|
20
|
+
* 巡检工具不允许有任何一种副作用,所以零写做成结构性的:正本只被读取一次。
|
|
21
|
+
*
|
|
22
|
+
* B. **无证据不搬移**。没有出现在映射里的事件一律 `retain`;映射本身缺 evidence、
|
|
23
|
+
* 格式不对、seq 不在 scope 内的,一律 `reject`。推测不是证据,本模块不做任何
|
|
24
|
+
* 基于 payload/时间/命名的启发式归属推断。
|
|
25
|
+
*
|
|
26
|
+
* C. **计划不是执行**。产出只是计划对象与报告;真实 apply/migration 属于单独的
|
|
27
|
+
* owner gate,本模块不提供写路径(`applyAuthorized` 恒为 false)。
|
|
28
|
+
*
|
|
29
|
+
* 计划对同一输入是确定性的(entries 按 seq 排序,digest 只覆盖判定内容),
|
|
30
|
+
* 因此重复 dry-run 必然产出同一 `planDigest`。
|
|
31
|
+
*/
|
|
32
|
+
import Database from 'better-sqlite3';
|
|
33
|
+
import { createHash } from 'node:crypto';
|
|
34
|
+
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
35
|
+
import { tmpdir } from 'node:os';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import { ledgerDbPath, ledgerExists } from './state-ledger.js';
|
|
38
|
+
export const REPAIR_PLAN_SCHEMA = 'gotry.ledger-repair-plan/1';
|
|
39
|
+
// ---- 理由码 ------------------------------------------------------------------
|
|
40
|
+
export const REASON = {
|
|
41
|
+
NO_EVIDENCE: 'no_explicit_evidence',
|
|
42
|
+
MAPPED: 'explicit_evidence_mapping',
|
|
43
|
+
MAPPING_NOT_OBJECT: 'mapping_not_object',
|
|
44
|
+
MAPPING_SEQ_INVALID: 'mapping_seq_invalid',
|
|
45
|
+
MAPPING_FROM_INVALID: 'mapping_from_tenant_invalid',
|
|
46
|
+
MAPPING_TO_INVALID: 'mapping_to_tenant_invalid',
|
|
47
|
+
MAPPING_EVIDENCE_MISSING: 'mapping_evidence_missing',
|
|
48
|
+
MAPPING_DUPLICATE_SEQ: 'mapping_duplicate_seq',
|
|
49
|
+
MAPPING_OUT_OF_SCOPE: 'mapping_from_tenant_out_of_scope',
|
|
50
|
+
EVENT_NOT_IN_SCOPE: 'event_not_found_in_scope',
|
|
51
|
+
TARGET_EQUALS_SOURCE: 'target_tenant_equals_source',
|
|
52
|
+
IDEM_CONFLICT: 'target_idem_key_conflict',
|
|
53
|
+
SUBJECT_CONFLICT: 'target_subject_conflict',
|
|
54
|
+
};
|
|
55
|
+
export const REPAIR_MAPPING_ROOT_NOT_ARRAY = 'mapping_root_not_array';
|
|
56
|
+
export const REPAIR_SOURCE_CHANGED = 'source_changed_during_copy_or_read';
|
|
57
|
+
// ---- 只读句柄 ----------------------------------------------------------------
|
|
58
|
+
/** WAL 形态下 DB 的全部物理组成:主库 + -wal + -shm,缺一不可完整复制 */
|
|
59
|
+
const DB_SIDECARS = ['', '-wal', '-shm'];
|
|
60
|
+
/**
|
|
61
|
+
* 在**账本的临时不可变副本**上执行 fn。
|
|
62
|
+
*
|
|
63
|
+
* 为什么不直接只读打开正本:SQLite 以 readonly 打开一个 WAL 模式库时,仍需要
|
|
64
|
+
* 建立 `-shm`(必要时还有 `-wal`)共享内存索引——实测这会在正本目录里**凭空生成
|
|
65
|
+
* 两个 sidecar 文件**,只读连接又无权在关闭时 checkpoint 清理它们。对一个数据
|
|
66
|
+
* 修复巡检工具来说,「读一下就在现场留下文件」是不可接受的。
|
|
67
|
+
*
|
|
68
|
+
* 所以零写在这里是**结构性**的:正本自始至终没有被任何 SQLite 连接打开过,
|
|
69
|
+
* 只被文件系统读取用于复制与指纹对账。sidecar 一并复制,保证未 checkpoint 的 WAL 数据
|
|
70
|
+
* 也在视图内(直接只拷主库会读到陈旧状态)。
|
|
71
|
+
*
|
|
72
|
+
* 复制前后以及只读回调结束后都会对 db/-wal/-shm 做内容指纹对账。账本在复制或
|
|
73
|
+
* 盘点期间发生变化时,本模块 fail-closed,不把可能混合了不同时间点的副本呈现为
|
|
74
|
+
* 可用计划;调用方应在静默/不可变的账本副本上重试。
|
|
75
|
+
*/
|
|
76
|
+
export class LedgerSourceChangedError extends Error {
|
|
77
|
+
code = REPAIR_SOURCE_CHANGED;
|
|
78
|
+
constructor(phase) {
|
|
79
|
+
super(`账本正本在${phase === 'copy' ? '复制' : '只读盘点'}期间发生变化,已拒绝生成计划(${REPAIR_SOURCE_CHANGED});请在静默或不可变副本上重试`);
|
|
80
|
+
this.name = 'LedgerSourceChangedError';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function sourceComponentDigest(path) {
|
|
84
|
+
try {
|
|
85
|
+
const bytes = readFileSync(path);
|
|
86
|
+
return `present:${bytes.length}:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof Error && error.code === 'ENOENT')
|
|
90
|
+
return 'missing';
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** 只读快照:存在性 + 字节摘要,覆盖主库及两个 WAL sidecar。 */
|
|
95
|
+
function sourceDigest(src) {
|
|
96
|
+
return DB_SIDECARS.map(suffix => `${suffix}\0${sourceComponentDigest(src + suffix)}`).join('\n');
|
|
97
|
+
}
|
|
98
|
+
export function withImmutableLedgerCopy(stateRoot, fn) {
|
|
99
|
+
const src = ledgerDbPath(stateRoot);
|
|
100
|
+
if (!ledgerExists(stateRoot))
|
|
101
|
+
throw new Error(`账本不存在(未迁移 root):${src}`);
|
|
102
|
+
const beforeCopy = sourceDigest(src);
|
|
103
|
+
const scratch = mkdtempSync(join(tmpdir(), 'gotry-repair-plan-'));
|
|
104
|
+
try {
|
|
105
|
+
const dest = join(scratch, 'ledger-copy.db');
|
|
106
|
+
for (const suffix of DB_SIDECARS) {
|
|
107
|
+
if (!existsSync(src + suffix))
|
|
108
|
+
continue;
|
|
109
|
+
try {
|
|
110
|
+
copyFileSync(src + suffix, dest + suffix);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error instanceof Error && error.code === 'ENOENT') {
|
|
114
|
+
throw new LedgerSourceChangedError('copy');
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (sourceDigest(src) !== beforeCopy)
|
|
120
|
+
throw new LedgerSourceChangedError('copy');
|
|
121
|
+
// 副本上再叠一层 readonly:即便有 bug 想写,也在 SQLite 引擎层被拒。
|
|
122
|
+
const db = new Database(dest, { readonly: true, fileMustExist: true });
|
|
123
|
+
let result;
|
|
124
|
+
let callbackThrew = false;
|
|
125
|
+
let callbackError;
|
|
126
|
+
try {
|
|
127
|
+
result = fn(db);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
callbackThrew = true;
|
|
131
|
+
callbackError = error;
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
db.close();
|
|
135
|
+
}
|
|
136
|
+
if (sourceDigest(src) !== beforeCopy)
|
|
137
|
+
throw new LedgerSourceChangedError('read');
|
|
138
|
+
if (callbackThrew)
|
|
139
|
+
throw callbackError;
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function eventsHasTenantColumn(db) {
|
|
147
|
+
const cols = db.prepare('PRAGMA table_info(events)').all();
|
|
148
|
+
return cols.some(c => c.name === 'tenant_id');
|
|
149
|
+
}
|
|
150
|
+
function eventsTableExists(db) {
|
|
151
|
+
return db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='events'").get() !== undefined;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 只读盘点:全库租户分布 + 源 tenant scope 下的候选事件。
|
|
155
|
+
* v1 形态(无 tenant_id 列)按 `local` 呈现,绝不就地迁移。
|
|
156
|
+
*/
|
|
157
|
+
export function inventoryLedger(db, dbPath, sourceTenant) {
|
|
158
|
+
if (!eventsTableExists(db)) {
|
|
159
|
+
return { dbPath, schemaShape: 'v2-tenant-scoped', tenantCensus: [], candidates: [] };
|
|
160
|
+
}
|
|
161
|
+
const hasTenant = eventsHasTenantColumn(db);
|
|
162
|
+
if (!hasTenant) {
|
|
163
|
+
const rows = db.prepare(`SELECT seq, ts, actor, kind, subject_id, idem_key FROM events ORDER BY seq`).all();
|
|
164
|
+
const candidates = sourceTenant === 'local'
|
|
165
|
+
? rows.map(r => ({
|
|
166
|
+
seq: r.seq, tenantId: 'local', ts: r.ts, actor: r.actor,
|
|
167
|
+
kind: r.kind, subjectId: r.subject_id, idemKey: r.idem_key,
|
|
168
|
+
}))
|
|
169
|
+
: [];
|
|
170
|
+
return {
|
|
171
|
+
dbPath,
|
|
172
|
+
schemaShape: 'v1-no-tenant-column',
|
|
173
|
+
tenantCensus: rows.length ? [{ tenant: 'local', events: rows.length }] : [],
|
|
174
|
+
candidates,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const census = db.prepare(`SELECT tenant_id AS tenant, COUNT(*) AS events FROM events GROUP BY tenant_id ORDER BY tenant_id`).all();
|
|
178
|
+
const candidates = db.prepare(`SELECT seq, tenant_id, ts, actor, kind, subject_id, idem_key
|
|
179
|
+
FROM events WHERE tenant_id = ? ORDER BY seq`).all(sourceTenant)
|
|
180
|
+
.map(r => ({
|
|
181
|
+
seq: r.seq, tenantId: r.tenant_id, ts: r.ts, actor: r.actor,
|
|
182
|
+
kind: r.kind, subjectId: r.subject_id, idemKey: r.idem_key,
|
|
183
|
+
}));
|
|
184
|
+
return { dbPath, schemaShape: 'v2-tenant-scoped', tenantCensus: census, candidates };
|
|
185
|
+
}
|
|
186
|
+
function isPositiveInt(v) {
|
|
187
|
+
return typeof v === 'number' && Number.isSafeInteger(v) && v > 0;
|
|
188
|
+
}
|
|
189
|
+
function nonEmptyString(v) {
|
|
190
|
+
return typeof v === 'string' && v.trim() !== '';
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 解析映射数组。任何一条不合法只影响它自己(记为 rejected),但整个计划因此
|
|
194
|
+
* 不可 apply——**绝不**「跳过坏条目继续搬好条目」地默默降级。
|
|
195
|
+
*/
|
|
196
|
+
export function parseEvidenceMappings(raw, sourceTenant) {
|
|
197
|
+
const valid = [];
|
|
198
|
+
const rejected = [];
|
|
199
|
+
if (raw === undefined || raw === null)
|
|
200
|
+
return { valid, rejected };
|
|
201
|
+
if (!Array.isArray(raw)) {
|
|
202
|
+
return { valid, rejected: [{ index: -1, reason: REPAIR_MAPPING_ROOT_NOT_ARRAY, detail: '映射文件根节点必须是数组', raw }] };
|
|
203
|
+
}
|
|
204
|
+
const seen = new Map();
|
|
205
|
+
raw.forEach((item, index) => {
|
|
206
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
|
|
207
|
+
rejected.push({ index, reason: REASON.MAPPING_NOT_OBJECT, detail: '映射条目必须是对象', raw: item });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const m = item;
|
|
211
|
+
if (!isPositiveInt(m['seq'])) {
|
|
212
|
+
rejected.push({ index, reason: REASON.MAPPING_SEQ_INVALID, detail: 'seq 必须是正整数', raw: item });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const seq = m['seq'];
|
|
216
|
+
if (!nonEmptyString(m['fromTenant'])) {
|
|
217
|
+
rejected.push({ index, reason: REASON.MAPPING_FROM_INVALID, detail: 'fromTenant 必须是非空字符串', raw: item });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!nonEmptyString(m['toTenant'])) {
|
|
221
|
+
rejected.push({ index, reason: REASON.MAPPING_TO_INVALID, detail: 'toTenant 必须是非空字符串', raw: item });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!nonEmptyString(m['evidence'])) {
|
|
225
|
+
rejected.push({ index, reason: REASON.MAPPING_EVIDENCE_MISSING, detail: 'evidence 必填且非空——无证据不得搬移', raw: item });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (m['fromTenant'].trim() !== sourceTenant) {
|
|
229
|
+
rejected.push({ index, reason: REASON.MAPPING_OUT_OF_SCOPE, detail: `fromTenant 必须等于本次 scope 的源租户 ${sourceTenant}`, raw: item });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const prev = seen.get(seq);
|
|
233
|
+
if (prev !== undefined) {
|
|
234
|
+
rejected.push({ index, reason: REASON.MAPPING_DUPLICATE_SEQ, detail: `seq ${seq} 已在第 ${prev} 条出现,重复映射意图不明`, raw: item });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
seen.set(seq, index);
|
|
238
|
+
valid.push({
|
|
239
|
+
seq,
|
|
240
|
+
fromTenant: m['fromTenant'].trim(),
|
|
241
|
+
toTenant: m['toTenant'].trim(),
|
|
242
|
+
evidence: m['evidence'].trim(),
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
return { valid, rejected };
|
|
246
|
+
}
|
|
247
|
+
/** 读映射文件;文件缺失/非 JSON 一律抛错(fail-closed,不退化成「无映射」) */
|
|
248
|
+
export function loadEvidenceMappingFile(path) {
|
|
249
|
+
let text;
|
|
250
|
+
try {
|
|
251
|
+
text = readFileSync(path, 'utf-8');
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
throw new Error(`证据映射文件不可读:${path}`);
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
return JSON.parse(text);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
throw new Error(`证据映射文件不是合法 JSON:${path}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// ---- 计划 --------------------------------------------------------------------
|
|
264
|
+
function canonical(entries, sourceTenant) {
|
|
265
|
+
const lines = entries.map(e => [e.seq, e.decision, e.beforeTenant, e.afterTenant, e.reason, e.kind, e.subjectId, e.idemKey ?? ''].join(' '));
|
|
266
|
+
return [sourceTenant, ...lines].join('\n');
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* 构造 dry-run 计划。纯函数式:只读 db、只算、不写。
|
|
270
|
+
*
|
|
271
|
+
* 冲突检测覆盖 issue 点名的两类跨租户同 id 场景:
|
|
272
|
+
* - 目标租户已存在同 `idem_key`(events_idem 唯一索引会撞)→ reject
|
|
273
|
+
* - 目标租户已存在同 kind+subject_id(如同 `wish_id`)→ reject
|
|
274
|
+
* 两者都只判定不改动,冲突事件留在原地。
|
|
275
|
+
*/
|
|
276
|
+
export function buildRepairPlan(db, opts) {
|
|
277
|
+
const dbPath = ledgerDbPath(opts.stateRoot);
|
|
278
|
+
const sourceTenant = opts.sourceTenant;
|
|
279
|
+
const inv = inventoryLedger(db, dbPath, sourceTenant);
|
|
280
|
+
const { valid, rejected } = parseEvidenceMappings(opts.mappings, sourceTenant);
|
|
281
|
+
const bySeq = new Map();
|
|
282
|
+
for (const c of inv.candidates)
|
|
283
|
+
bySeq.set(c.seq, c);
|
|
284
|
+
const hasTenant = inv.schemaShape === 'v2-tenant-scoped';
|
|
285
|
+
const idemHit = hasTenant
|
|
286
|
+
? db.prepare(`SELECT 1 AS hit FROM events WHERE tenant_id = ? AND idem_key = ? LIMIT 1`)
|
|
287
|
+
: null;
|
|
288
|
+
const subjectHit = hasTenant
|
|
289
|
+
? db.prepare(`SELECT 1 AS hit FROM events WHERE tenant_id = ? AND kind = ? AND subject_id = ? LIMIT 1`)
|
|
290
|
+
: null;
|
|
291
|
+
const decided = new Map();
|
|
292
|
+
for (const m of valid) {
|
|
293
|
+
const ev = bySeq.get(m.seq);
|
|
294
|
+
if (!ev) {
|
|
295
|
+
// seq 不在源 scope 内:可能不存在,也可能属于别的租户——都不许动
|
|
296
|
+
decided.set(m.seq, {
|
|
297
|
+
seq: m.seq, kind: '<unknown>', subjectId: '<unknown>', idemKey: null, ts: '<unknown>',
|
|
298
|
+
decision: 'reject', beforeTenant: sourceTenant, afterTenant: sourceTenant,
|
|
299
|
+
reason: REASON.EVENT_NOT_IN_SCOPE,
|
|
300
|
+
detail: `seq ${m.seq} 不在源租户 ${sourceTenant} 的事件范围内`,
|
|
301
|
+
evidence: m.evidence,
|
|
302
|
+
});
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const base = {
|
|
306
|
+
seq: ev.seq, kind: ev.kind, subjectId: ev.subjectId, idemKey: ev.idemKey, ts: ev.ts,
|
|
307
|
+
beforeTenant: ev.tenantId, evidence: m.evidence,
|
|
308
|
+
};
|
|
309
|
+
if (m.toTenant === sourceTenant) {
|
|
310
|
+
decided.set(ev.seq, {
|
|
311
|
+
...base, decision: 'reject', afterTenant: ev.tenantId,
|
|
312
|
+
reason: REASON.TARGET_EQUALS_SOURCE, detail: '目标租户与源租户相同,无搬移语义',
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (!hasTenant) {
|
|
317
|
+
// v1 形态没有 tenant_id 列,任何搬移都得先做 schema 迁移——不在只读计划的能力内
|
|
318
|
+
decided.set(ev.seq, {
|
|
319
|
+
...base, decision: 'reject', afterTenant: ev.tenantId,
|
|
320
|
+
reason: REASON.EVENT_NOT_IN_SCOPE,
|
|
321
|
+
detail: 'v1 形态账本(events 无 tenant_id 列)无法在不迁移 schema 的前提下计划搬移',
|
|
322
|
+
});
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (ev.idemKey !== null && idemHit.get(m.toTenant, ev.idemKey) !== undefined) {
|
|
326
|
+
decided.set(ev.seq, {
|
|
327
|
+
...base, decision: 'reject', afterTenant: ev.tenantId,
|
|
328
|
+
reason: REASON.IDEM_CONFLICT,
|
|
329
|
+
detail: `目标租户 ${m.toTenant} 已存在同 idem_key「${ev.idemKey}」,搬移会撞唯一索引`,
|
|
330
|
+
});
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// subject 冲突只对具名主体有意义;subject_id 为空的日志类事件(forget.executed 等)
|
|
334
|
+
// 天然同名,不能据此判冲突,否则会把合法搬移误拒。
|
|
335
|
+
if (ev.subjectId !== '' && subjectHit.get(m.toTenant, ev.kind, ev.subjectId) !== undefined) {
|
|
336
|
+
decided.set(ev.seq, {
|
|
337
|
+
...base, decision: 'reject', afterTenant: ev.tenantId,
|
|
338
|
+
reason: REASON.SUBJECT_CONFLICT,
|
|
339
|
+
detail: `目标租户 ${m.toTenant} 已存在同 ${ev.kind}/${ev.subjectId},归属需人工再确认`,
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
decided.set(ev.seq, {
|
|
344
|
+
...base, decision: 'move', afterTenant: m.toTenant,
|
|
345
|
+
reason: REASON.MAPPED, detail: `依据人工证据搬移到 ${m.toTenant}`,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
// 未被映射覆盖的候选事件:一律原地保留
|
|
349
|
+
const entries = [];
|
|
350
|
+
for (const c of inv.candidates) {
|
|
351
|
+
const d = decided.get(c.seq);
|
|
352
|
+
if (d) {
|
|
353
|
+
entries.push(d);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
entries.push({
|
|
357
|
+
seq: c.seq, kind: c.kind, subjectId: c.subjectId, idemKey: c.idemKey, ts: c.ts,
|
|
358
|
+
decision: 'retain', beforeTenant: c.tenantId, afterTenant: c.tenantId,
|
|
359
|
+
reason: REASON.NO_EVIDENCE, detail: '无显式证据映射,原地保留(不做任何归属推测)',
|
|
360
|
+
evidence: null,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
// 映射指向 scope 外 seq 的拒绝项也要出现在计划里
|
|
364
|
+
for (const [seq, d] of decided)
|
|
365
|
+
if (!bySeq.has(seq))
|
|
366
|
+
entries.push(d);
|
|
367
|
+
entries.sort((a, b) => a.seq - b.seq);
|
|
368
|
+
const moves = entries.filter(e => e.decision === 'move');
|
|
369
|
+
const rejects = entries.filter(e => e.decision === 'reject');
|
|
370
|
+
const retains = entries.filter(e => e.decision === 'retain');
|
|
371
|
+
// after 分布:纯内存模拟
|
|
372
|
+
const after = new Map();
|
|
373
|
+
for (const t of inv.tenantCensus)
|
|
374
|
+
after.set(t.tenant, t.events);
|
|
375
|
+
for (const m of moves) {
|
|
376
|
+
after.set(m.beforeTenant, (after.get(m.beforeTenant) ?? 0) - 1);
|
|
377
|
+
after.set(m.afterTenant, (after.get(m.afterTenant) ?? 0) + 1);
|
|
378
|
+
}
|
|
379
|
+
const tenantCensusAfter = [...after.entries()]
|
|
380
|
+
.filter(([, n]) => n > 0)
|
|
381
|
+
.map(([tenant, events]) => ({ tenant, events }))
|
|
382
|
+
.sort((a, b) => a.tenant.localeCompare(b.tenant));
|
|
383
|
+
const blocked = rejects.length > 0 || rejected.length > 0;
|
|
384
|
+
const notes = [
|
|
385
|
+
'本报告是 dry-run:未对账本做任何写入(正本从未被打开,只在临时副本上只读读取)。',
|
|
386
|
+
'未提供显式证据映射的事件一律原地保留;本工具不做任何归属推测。',
|
|
387
|
+
'真实 apply/migration 属于单独的 owner gate,本切片不提供写路径。',
|
|
388
|
+
];
|
|
389
|
+
if (blocked)
|
|
390
|
+
notes.push('存在被拒条目,计划整体不可 apply——请先人工消解冲突或补充证据。');
|
|
391
|
+
if (inv.schemaShape === 'v1-no-tenant-column') {
|
|
392
|
+
notes.push('账本为 v1 形态(events 无 tenant_id 列):全部事件物理上只有 local 归属;本工具只读呈现,不就地迁移 schema。');
|
|
393
|
+
}
|
|
394
|
+
const planDigest = createHash('sha256').update(canonical(entries, sourceTenant)).digest('hex').slice(0, 16);
|
|
395
|
+
return {
|
|
396
|
+
schema: REPAIR_PLAN_SCHEMA,
|
|
397
|
+
dbPath,
|
|
398
|
+
sourceTenant,
|
|
399
|
+
tenantCensusBefore: inv.tenantCensus,
|
|
400
|
+
tenantCensusAfter,
|
|
401
|
+
entries,
|
|
402
|
+
rejectedMappings: rejected,
|
|
403
|
+
totals: {
|
|
404
|
+
candidates: inv.candidates.length,
|
|
405
|
+
move: moves.length,
|
|
406
|
+
retain: retains.length,
|
|
407
|
+
reject: rejects.length,
|
|
408
|
+
rejectedMappings: rejected.length,
|
|
409
|
+
},
|
|
410
|
+
applyable: !blocked && moves.length > 0,
|
|
411
|
+
applyAuthorized: false,
|
|
412
|
+
planDigest,
|
|
413
|
+
notes,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/** 一次性只读入口:临时不可变副本 → 出计划 → 清理副本(正本零触碰) */
|
|
417
|
+
export function planLedgerRepair(opts) {
|
|
418
|
+
return withImmutableLedgerCopy(opts.stateRoot, db => buildRepairPlan(db, {
|
|
419
|
+
stateRoot: opts.stateRoot,
|
|
420
|
+
sourceTenant: opts.sourceTenant ?? 'local',
|
|
421
|
+
mappings: opts.mappings,
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
// ---- 报告 --------------------------------------------------------------------
|
|
425
|
+
function census(list) {
|
|
426
|
+
return list.length ? list.map(t => `${t.tenant}=${t.events}`).join(' ') : '(空)';
|
|
427
|
+
}
|
|
428
|
+
/** 人读 before/after 报告(确定性:同一计划逐字节相同) */
|
|
429
|
+
export function formatRepairPlan(plan) {
|
|
430
|
+
const out = [];
|
|
431
|
+
out.push(`账本 tenant 修复计划(dry-run,零写)`);
|
|
432
|
+
out.push(`db: ${plan.dbPath}`);
|
|
433
|
+
out.push(`源租户 scope: ${plan.sourceTenant}`);
|
|
434
|
+
out.push(`租户分布 before: ${census(plan.tenantCensusBefore)}`);
|
|
435
|
+
out.push(`租户分布 after : ${census(plan.tenantCensusAfter)}`);
|
|
436
|
+
out.push('');
|
|
437
|
+
out.push(`候选 ${plan.totals.candidates} / 搬移 ${plan.totals.move} / 保留 ${plan.totals.retain} / 拒绝 ${plan.totals.reject} / 非法映射 ${plan.totals.rejectedMappings}`);
|
|
438
|
+
out.push('');
|
|
439
|
+
if (plan.entries.length === 0) {
|
|
440
|
+
out.push('(源租户 scope 下无候选事件)');
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
out.push(` seq 判定 before -> after kind/subject 理由`);
|
|
444
|
+
for (const e of plan.entries) {
|
|
445
|
+
const move = `${e.beforeTenant} -> ${e.afterTenant}`;
|
|
446
|
+
out.push(`${String(e.seq).padStart(5)} ${e.decision.padEnd(8)} ${move.padEnd(24)} ${`${e.kind}/${e.subjectId}`.padEnd(32)} ${e.reason}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (plan.rejectedMappings.length) {
|
|
450
|
+
out.push('');
|
|
451
|
+
out.push('非法映射条目(不产生任何搬移):');
|
|
452
|
+
for (const r of plan.rejectedMappings) {
|
|
453
|
+
out.push(` #${r.index} ${r.reason} ${r.detail}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
out.push('');
|
|
457
|
+
for (const n of plan.notes)
|
|
458
|
+
out.push(`* ${n}`);
|
|
459
|
+
out.push('');
|
|
460
|
+
out.push(`applyable=${plan.applyable} applyAuthorized=${plan.applyAuthorized}`);
|
|
461
|
+
out.push(`plan-digest=${plan.planDigest}`);
|
|
462
|
+
return out.join('\n');
|
|
463
|
+
}
|