@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.
Files changed (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -0,0 +1,375 @@
1
+ /**
2
+ * 账本 tenant 修复计划测试(issue #254 第一切片,run-all §29b)。
3
+ *
4
+ * 三类断言:
5
+ * A. **零写**:每个场景跑完,state 目录内容哈希与跑之前逐字节一致
6
+ * (含 -wal/-shm sidecar——正本目录里多出文件也算写)。
7
+ * B. **判定**:无显式证据零搬移;跨租户同 idem_key / 同 wish_id 一律拒绝;
8
+ * 重复 dry-run 幂等。
9
+ * C. **CLI seam**:repair-plan 路由、fail-closed 参数、输出稳定。
10
+ *
11
+ * 全部使用 mkdtemp 隔离 stateRoot,绝不触碰 ts/dsh-runtime/gotry-state/ 真实产品数据。
12
+ * 当前断言数:76;运行(在 ts/ 下):npx tsx scripts/ledger-repair-plan-tests.ts
13
+ */
14
+ import Database from 'better-sqlite3';
15
+ import { createHash } from 'node:crypto';
16
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import { spawnSync } from 'node:child_process';
20
+ import { ensureLedger, ledgerDbPath } from '../src/state-ledger.js';
21
+ import { REASON, REPAIR_MAPPING_ROOT_NOT_ARRAY, REPAIR_SOURCE_CHANGED, formatRepairPlan, planLedgerRepair, withImmutableLedgerCopy } from '../src/ledger-repair-plan.js';
22
+ let pass = 0;
23
+ let fail = 0;
24
+ function assert(cond, msg) {
25
+ if (cond) {
26
+ pass++;
27
+ console.log(` ok - ${msg}`);
28
+ }
29
+ else {
30
+ fail++;
31
+ console.error(` FAIL - ${msg}`);
32
+ }
33
+ }
34
+ function expectSourceChange(fn, label) {
35
+ try {
36
+ fn();
37
+ assert(false, `${label} 应 fail-closed`);
38
+ }
39
+ catch (e) {
40
+ assert(e instanceof Error && e.code === REPAIR_SOURCE_CHANGED, `${label} → ${REPAIR_SOURCE_CHANGED}`);
41
+ }
42
+ }
43
+ /** 目录内容哈希:文件名 + 大小 + 字节。多出 -wal/-shm 会改变哈希 = 判定为写。 */
44
+ function snapshotDir(path) {
45
+ const h = createHash('sha256');
46
+ const walk = (p, rel) => {
47
+ const st = statSync(p);
48
+ if (st.isDirectory()) {
49
+ h.update(`dir:${rel}\n`);
50
+ for (const name of readdirSync(p).sort())
51
+ walk(join(p, name), rel ? `${rel}/${name}` : name);
52
+ }
53
+ else if (st.isFile()) {
54
+ h.update(`file:${rel}:${st.size}\n`);
55
+ h.update(readFileSync(p));
56
+ h.update('\n');
57
+ }
58
+ else {
59
+ h.update(`other:${rel}\n`);
60
+ }
61
+ };
62
+ walk(path, '');
63
+ return h.digest('hex');
64
+ }
65
+ const roots = [];
66
+ function newRoot(tag) {
67
+ const r = mkdtempSync(join(tmpdir(), `gotry-repair-${tag}-`));
68
+ roots.push(r);
69
+ return r;
70
+ }
71
+ function stateDir(root) { return join(root, 'gotry-state'); }
72
+ function insertRaw(dbPath, rows) {
73
+ const db = new Database(dbPath);
74
+ const stmt = db.prepare(`INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
75
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`);
76
+ for (const r of rows)
77
+ stmt.run(r.tenant, r.ts ?? '2026-09-01T00:00:00.000Z', 'test:fixture', r.kind, r.subject, '{}', r.idem);
78
+ db.close();
79
+ }
80
+ /** 建一个 v2 账本并直接用 SQL 种事件(不依赖写路径,便于造跨租户夹具) */
81
+ function seedLedger(root, rows) {
82
+ const led = ensureLedger(root, 'local');
83
+ led.close(); // 关掉句柄,让 WAL checkpoint 落盘,后续哈希才稳定
84
+ const dbPath = ledgerDbPath(root);
85
+ insertRaw(dbPath, rows);
86
+ return dbPath;
87
+ }
88
+ function writeMapping(root, name, value) {
89
+ const p = join(root, name);
90
+ writeFileSync(p, typeof value === 'string' ? value : JSON.stringify(value), 'utf-8');
91
+ return p;
92
+ }
93
+ function cli(args) {
94
+ return spawnSync('npx', ['tsx', 'scripts/state-cli.ts', ...args], { encoding: 'utf-8' });
95
+ }
96
+ /** 跑一段逻辑并断言 state 目录零变化 */
97
+ function assertZeroWrite(root, label, fn) {
98
+ const before = snapshotDir(stateDir(root));
99
+ fn();
100
+ const after = snapshotDir(stateDir(root));
101
+ assert(before === after, `零写:${label}`);
102
+ }
103
+ function entryBySeq(plan, seq) {
104
+ return plan.entries.find(e => e.seq === seq);
105
+ }
106
+ console.log('\n== A. 无映射:只读盘点,零搬移零写 ==');
107
+ {
108
+ const root = newRoot('inv');
109
+ seedLedger(root, [
110
+ { tenant: 'local', kind: 'wish.added', subject: 'w-1', idem: 'import:wish:w-1' },
111
+ { tenant: 'local', kind: 'trip.logged', subject: 't-1', idem: 'trip:t-1' },
112
+ { tenant: 'tenant-a', kind: 'wish.added', subject: 'w-9', idem: 'import:wish:w-9' },
113
+ ]);
114
+ let plan;
115
+ assertZeroWrite(root, '无映射 dry-run', () => { plan = planLedgerRepair({ stateRoot: root }); });
116
+ assert(plan.totals.candidates === 2, '源租户 local 下候选事件 = 2(不含 tenant-a)');
117
+ assert(plan.totals.move === 0, '无显式证据映射 → 零搬移');
118
+ assert(plan.totals.retain === 2, '全部候选原地保留');
119
+ assert(plan.entries.every(e => e.reason === REASON.NO_EVIDENCE), '保留理由均为 no_explicit_evidence');
120
+ assert(plan.applyable === false && plan.applyAuthorized === false, '无搬移时 applyable=false,且 applyAuthorized 恒 false');
121
+ const censusBefore = plan.tenantCensusBefore.map(t => `${t.tenant}=${t.events}`).join(' ');
122
+ assert(censusBefore === 'local=2 tenant-a=1', `before 分布可审计:${censusBefore}`);
123
+ assert(JSON.stringify(plan.tenantCensusBefore) === JSON.stringify(plan.tenantCensusAfter), '零搬移时 after 分布与 before 相同');
124
+ }
125
+ console.log('\n== B. 显式证据映射:计划出可审计 before/after,仍零写 ==');
126
+ {
127
+ const root = newRoot('move');
128
+ seedLedger(root, [
129
+ { tenant: 'local', kind: 'wish.added', subject: 'w-1', idem: 'import:wish:w-1' },
130
+ { tenant: 'local', kind: 'wish.added', subject: 'w-2', idem: 'import:wish:w-2' },
131
+ { tenant: 'local', kind: 'trip.logged', subject: 't-1', idem: 'trip:t-1' },
132
+ ]);
133
+ const mappings = [
134
+ { seq: 1, fromTenant: 'local', toTenant: 'tenant-a', evidence: 'issue #254 comment: 用户 A 于 2026-03 确认 w-1 属于其账户' },
135
+ ];
136
+ let plan;
137
+ assertZeroWrite(root, '有效映射 dry-run', () => { plan = planLedgerRepair({ stateRoot: root, mappings }); });
138
+ assert(plan.totals.move === 1 && plan.totals.retain === 2, '仅被映射覆盖的 1 条计划搬移,其余保留');
139
+ const moved = entryBySeq(plan, 1);
140
+ assert(moved.decision === 'move' && moved.beforeTenant === 'local' && moved.afterTenant === 'tenant-a', 'before/after 逐条可读:local -> tenant-a');
141
+ assert(moved.evidence !== null && moved.evidence.includes('#254'), '搬移条目带人工证据原文');
142
+ const after = plan.tenantCensusAfter.map(t => `${t.tenant}=${t.events}`).join(' ');
143
+ assert(after === 'local=2 tenant-a=1', `after 分布按计划模拟:${after}`);
144
+ assert(plan.applyable === true, '无冲突且有搬移 → applyable=true');
145
+ assert(plan.applyAuthorized === false, '但 applyAuthorized 仍为 false:真实 apply 在单独 owner gate 后');
146
+ // 计划归计划,账本里 tenant 分布一个字节没动
147
+ const db = new Database(ledgerDbPath(root), { readonly: true });
148
+ const live = db.prepare('SELECT tenant_id AS t, COUNT(*) AS n FROM events GROUP BY tenant_id ORDER BY tenant_id').all()
149
+ .map(r => `${r.t}=${r.n}`).join(' ');
150
+ db.close();
151
+ assert(live === 'local=3', `dry-run 后账本真实分布未变:${live}`);
152
+ }
153
+ console.log('\n== C. 无证据不搬移:映射缺 evidence / 格式非法 → 拒绝且零写 ==');
154
+ {
155
+ const root = newRoot('malformed');
156
+ seedLedger(root, [
157
+ { tenant: 'local', kind: 'wish.added', subject: 'w-1', idem: 'import:wish:w-1' },
158
+ { tenant: 'local', kind: 'wish.added', subject: 'w-2', idem: 'import:wish:w-2' },
159
+ ]);
160
+ const bad = [
161
+ { seq: 1, fromTenant: 'local', toTenant: 'tenant-a' }, // 缺 evidence
162
+ { seq: 2, fromTenant: 'local', toTenant: 'tenant-a', evidence: ' ' }, // evidence 空白
163
+ { seq: 0, fromTenant: 'local', toTenant: 'tenant-a', evidence: 'x' }, // seq 非正整数
164
+ { seq: 1, fromTenant: 'local', toTenant: '', evidence: 'x' }, // toTenant 空
165
+ { seq: 1, fromTenant: '', toTenant: 'tenant-a', evidence: 'x' }, // fromTenant 空
166
+ 'not-an-object', // 条目非对象
167
+ { seq: 1, fromTenant: 'tenant-zzz', toTenant: 'tenant-a', evidence: 'x' }, // fromTenant 越出 scope
168
+ ];
169
+ let plan;
170
+ assertZeroWrite(root, '非法映射 dry-run', () => { plan = planLedgerRepair({ stateRoot: root, mappings: bad }); });
171
+ assert(plan.totals.move === 0, '任何非法映射都不产生搬移');
172
+ const codes = plan.rejectedMappings.map(r => r.reason);
173
+ assert(codes.includes(REASON.MAPPING_EVIDENCE_MISSING), '缺 evidence → mapping_evidence_missing');
174
+ assert(codes.filter(c => c === REASON.MAPPING_EVIDENCE_MISSING).length === 2, '空白 evidence 同样按缺失处理');
175
+ assert(codes.includes(REASON.MAPPING_SEQ_INVALID), 'seq 非正整数 → mapping_seq_invalid');
176
+ assert(codes.includes(REASON.MAPPING_TO_INVALID), 'toTenant 空 → mapping_to_tenant_invalid');
177
+ assert(codes.includes(REASON.MAPPING_FROM_INVALID), 'fromTenant 空 → mapping_from_tenant_invalid');
178
+ assert(codes.includes(REASON.MAPPING_NOT_OBJECT), '条目非对象 → mapping_not_object');
179
+ assert(codes.includes(REASON.MAPPING_OUT_OF_SCOPE), 'fromTenant 越 scope → mapping_from_tenant_out_of_scope');
180
+ assert(plan.applyable === false, '存在非法映射 → 计划整体不可 apply');
181
+ assert(plan.entries.every(e => e.decision === 'retain'), '非法映射不影响其它事件:全部原地保留');
182
+ }
183
+ console.log('\n== D. 重复映射 / 目标即源 / seq 越 scope ==');
184
+ {
185
+ const root = newRoot('dup');
186
+ seedLedger(root, [{ tenant: 'local', kind: 'wish.added', subject: 'w-1', idem: 'k-1' }]);
187
+ const plan = planLedgerRepair({ stateRoot: root, mappings: [
188
+ { seq: 1, fromTenant: 'local', toTenant: 'tenant-a', evidence: 'e1' },
189
+ { seq: 1, fromTenant: 'local', toTenant: 'tenant-b', evidence: 'e2' },
190
+ ] });
191
+ assert(plan.rejectedMappings.some(r => r.reason === REASON.MAPPING_DUPLICATE_SEQ), '同 seq 重复映射 → mapping_duplicate_seq');
192
+ assert(plan.applyable === false, '重复映射使计划不可 apply(意图不明不猜)');
193
+ const p2 = planLedgerRepair({ stateRoot: root, mappings: [{ seq: 1, fromTenant: 'local', toTenant: 'local', evidence: 'e' }] });
194
+ assert(entryBySeq(p2, 1).reason === REASON.TARGET_EQUALS_SOURCE, '目标租户 = 源租户 → target_tenant_equals_source');
195
+ assert(p2.totals.move === 0, '无搬移语义时零搬移');
196
+ const p3 = planLedgerRepair({ stateRoot: root, mappings: [{ seq: 999, fromTenant: 'local', toTenant: 'tenant-a', evidence: 'e' }] });
197
+ const e999 = entryBySeq(p3, 999);
198
+ assert(e999.decision === 'reject' && e999.reason === REASON.EVENT_NOT_IN_SCOPE, 'seq 不在源 scope → event_not_found_in_scope');
199
+ assert(p3.totals.move === 0, '越 scope 的 seq 零搬移');
200
+ }
201
+ console.log('\n== E. 跨租户同 idem_key / 同 wish_id 夹具:拒绝且零写 ==');
202
+ {
203
+ const root = newRoot('conflict');
204
+ seedLedger(root, [
205
+ { tenant: 'local', kind: 'wish.added', subject: 'w-same', idem: 'import:wish:w-same' }, // seq 1
206
+ { tenant: 'local', kind: 'trip.logged', subject: 't-1', idem: 'dup-idem' }, // seq 2
207
+ { tenant: 'tenant-a', kind: 'wish.added', subject: 'w-same', idem: 'other-1' }, // seq 3 同 wish_id
208
+ { tenant: 'tenant-a', kind: 'trip.logged', subject: 't-9', idem: 'dup-idem' }, // seq 4 同 idem_key
209
+ ]);
210
+ let plan;
211
+ assertZeroWrite(root, '跨租户同 id 夹具 dry-run', () => {
212
+ plan = planLedgerRepair({ stateRoot: root, mappings: [
213
+ { seq: 1, fromTenant: 'local', toTenant: 'tenant-a', evidence: '人工确认 A' },
214
+ { seq: 2, fromTenant: 'local', toTenant: 'tenant-a', evidence: '人工确认 B' },
215
+ ] });
216
+ });
217
+ assert(entryBySeq(plan, 1).reason === REASON.SUBJECT_CONFLICT, '目标租户已存在同 wish_id → target_subject_conflict');
218
+ assert(entryBySeq(plan, 2).reason === REASON.IDEM_CONFLICT, '目标租户已存在同 idem_key → target_idem_key_conflict');
219
+ assert(plan.totals.move === 0 && plan.totals.reject === 2, '两类跨租户同 id 冲突都零搬移');
220
+ assert(entryBySeq(plan, 1).afterTenant === 'local' && entryBySeq(plan, 2).afterTenant === 'local', '冲突事件 after 仍是原租户(留在原地)');
221
+ assert(plan.applyable === false, '有冲突 → 计划不可 apply');
222
+ }
223
+ console.log('\n== F. 重复 dry-run 幂等 ==');
224
+ {
225
+ const root = newRoot('idem');
226
+ seedLedger(root, [
227
+ { tenant: 'local', kind: 'wish.added', subject: 'w-1', idem: 'k1' },
228
+ { tenant: 'local', kind: 'wish.added', subject: 'w-2', idem: 'k2' },
229
+ { tenant: 'tenant-a', kind: 'wish.added', subject: 'w-3', idem: 'k3' },
230
+ ]);
231
+ const mappings = [{ seq: 1, fromTenant: 'local', toTenant: 'tenant-a', evidence: 'e' }];
232
+ const before = snapshotDir(stateDir(root));
233
+ const digests = [];
234
+ const reports = [];
235
+ for (let i = 0; i < 3; i++) {
236
+ const p = planLedgerRepair({ stateRoot: root, mappings });
237
+ digests.push(p.planDigest);
238
+ reports.push(formatRepairPlan(p));
239
+ }
240
+ const after = snapshotDir(stateDir(root));
241
+ assert(before === after, '零写:连续 3 次 dry-run 后 state 目录逐字节不变');
242
+ assert(new Set(digests).size === 1, `重复 dry-run 幂等:plan-digest 恒为 ${digests[0]}`);
243
+ assert(new Set(reports).size === 1, '重复 dry-run 报告逐字节相同');
244
+ }
245
+ console.log('\n== G. v1 形态账本:只读呈现,绝不就地迁移 ==');
246
+ {
247
+ const root = newRoot('v1');
248
+ const dir = stateDir(root);
249
+ mkdirSync(dir, { recursive: true });
250
+ const dbPath = join(dir, 'gotry-state.db');
251
+ const v1 = new Database(dbPath);
252
+ v1.exec(`
253
+ CREATE TABLE events (
254
+ seq INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, actor TEXT NOT NULL, kind TEXT NOT NULL,
255
+ subject_id TEXT NOT NULL DEFAULT '', payload TEXT NOT NULL, idem_key TEXT, run_id TEXT
256
+ );
257
+ CREATE UNIQUE INDEX events_idem ON events(idem_key) WHERE idem_key IS NOT NULL;`);
258
+ v1.prepare(`INSERT INTO events (ts, actor, kind, subject_id, payload, idem_key) VALUES (?,?,?,?,?,?)`)
259
+ .run('2026-01-01T00:00:00.000Z', 'legacy', 'wish.added', 'w-legacy', '{}', 'import:wish:w-legacy');
260
+ v1.close();
261
+ let plan;
262
+ assertZeroWrite(root, 'v1 账本 dry-run(对照:openLedgerIfExists 会 ALTER/DROP 重建)', () => {
263
+ plan = planLedgerRepair({ stateRoot: root });
264
+ });
265
+ const probe = new Database(dbPath, { readonly: true });
266
+ const cols = probe.prepare('PRAGMA table_info(events)').all().map(c => c.name);
267
+ const uv = probe.pragma('user_version', { simple: true });
268
+ const tables = probe.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all().map(t => t.name);
269
+ probe.close();
270
+ assert(!cols.includes('tenant_id'), 'v1 的 events 仍无 tenant_id 列(未被偷偷迁移)');
271
+ assert(uv === 0, 'user_version 仍为 0(未被顶到 2)');
272
+ assert(!tables.includes('projection_docs') && !tables.includes('kv'), '未凭空建出 v2 的投影/kv 表');
273
+ assert(plan.totals.candidates === 1 && plan.totals.retain === 1, 'v1 事件按 local 只读呈现');
274
+ assert(plan.notes.some(n => n.includes('v1 形态')), '报告显式提示 v1 形态与其边界');
275
+ }
276
+ console.log('\n== H. 未 checkpoint WAL:副本包含 WAL 可见事件 ==');
277
+ {
278
+ const root = newRoot('wal');
279
+ const dbPath = seedLedger(root, [
280
+ { tenant: 'local', kind: 'wish.added', subject: 'w-main', idem: 'k-main' },
281
+ ]);
282
+ const writer = new Database(dbPath);
283
+ writer.pragma('journal_mode = WAL');
284
+ writer.prepare(`INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
285
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`).run('local', '2026-09-01T00:00:00.000Z', 'test:wal', 'wish.added', 'w-wal', '{}', 'k-wal');
286
+ assert(existsSync(`${dbPath}-wal`), '夹具保持未 checkpoint 的 -wal sidecar');
287
+ const before = snapshotDir(stateDir(root));
288
+ const plan = planLedgerRepair({ stateRoot: root });
289
+ assert(plan.totals.candidates === 2, '只读副本可见主库 + 未 checkpoint WAL 的 2 条事件');
290
+ assert(plan.entries.some(e => e.subjectId === 'w-wal'), 'WAL 中事件进入计划视图');
291
+ assert(snapshotDir(stateDir(root)) === before, '读取未 checkpoint WAL 后正本目录零写');
292
+ writer.close();
293
+ }
294
+ console.log('\n== I. 并发变化:复制/读取期间 source mutation fail-closed ==');
295
+ {
296
+ const root = newRoot('mutation');
297
+ const dbPath = seedLedger(root, [
298
+ { tenant: 'local', kind: 'wish.added', subject: 'w-before', idem: 'k-before' },
299
+ ]);
300
+ expectSourceChange(() => withImmutableLedgerCopy(root, db => {
301
+ // Deterministic stand-in for a concurrent writer after the immutable copy is opened.
302
+ insertRaw(dbPath, [{ tenant: 'local', kind: 'wish.added', subject: 'w-during-read', idem: 'k-during-read' }]);
303
+ db.prepare('SELECT count(*) AS n FROM events').get();
304
+ }), 'source 在只读回调期间变化');
305
+ const probe = new Database(dbPath, { readonly: true });
306
+ const count = probe.prepare('SELECT count(*) AS n FROM events').get().n;
307
+ probe.close();
308
+ assert(count === 2, '反例只改变隔离夹具正本,未被计划副本隐式吸收');
309
+ }
310
+ console.log('\n== J. tenant scope:非 local 源租户只看自己 ==');
311
+ {
312
+ const root = newRoot('scope');
313
+ seedLedger(root, [
314
+ { tenant: 'local', kind: 'wish.added', subject: 'w-1', idem: 'k1' },
315
+ { tenant: 'tenant-a', kind: 'wish.added', subject: 'w-2', idem: 'k2' },
316
+ { tenant: 'tenant-a', kind: 'wish.added', subject: 'w-3', idem: 'k3' },
317
+ ]);
318
+ const p = planLedgerRepair({ stateRoot: root, sourceTenant: 'tenant-a' });
319
+ assert(p.totals.candidates === 2, 'sourceTenant=tenant-a 只盘点 tenant-a 的 2 条');
320
+ assert(p.entries.every(e => e.beforeTenant === 'tenant-a'), '候选不串到 local');
321
+ }
322
+ console.log('\n== K. CLI seam ==');
323
+ {
324
+ const root = newRoot('cli');
325
+ seedLedger(root, [
326
+ { tenant: 'local', kind: 'wish.added', subject: 'w-1', idem: 'k1' },
327
+ { tenant: 'local', kind: 'wish.added', subject: 'w-2', idem: 'k2' },
328
+ ]);
329
+ const before = snapshotDir(stateDir(root));
330
+ const text = cli(['repair-plan', root]);
331
+ assert(text.status === 0, 'repair-plan 命令存在且退出码 0');
332
+ assert(/plan-digest=/.test(text.stdout ?? ''), '文本报告带 plan-digest');
333
+ assert(/applyAuthorized=false/.test(text.stdout ?? ''), '文本报告显式声明 applyAuthorized=false');
334
+ assert(/no_explicit_evidence/.test(text.stdout ?? ''), '无映射时逐条标注 no_explicit_evidence');
335
+ const again = cli(['repair-plan', root]);
336
+ assert(again.stdout === text.stdout, 'CLI 重复执行 stdout 逐字节相同(幂等)');
337
+ const mapPath = writeMapping(root, 'mapping.json', [
338
+ { seq: 1, fromTenant: 'local', toTenant: 'tenant-a', evidence: '人工确认:issue #254 附件' },
339
+ ]);
340
+ const withMap = cli(['repair-plan', root, '--mapping', mapPath, '--format', 'json']);
341
+ assert(withMap.status === 0, '--mapping + --format json 可用');
342
+ const parsed = JSON.parse(withMap.stdout);
343
+ assert(parsed.schema === 'gotry.ledger-repair-plan/1', 'JSON 输出带 schema 版本');
344
+ assert(parsed.totals.move === 1 && parsed.applyAuthorized === false, 'JSON 计划:1 条搬移,applyAuthorized=false');
345
+ const missing = cli(['repair-plan', root, '--mapping', join(root, 'nope.json')]);
346
+ assert(missing.status === 1, '映射文件缺失 → fail-closed 退出码 1');
347
+ assert(/不可读/.test(missing.stderr ?? ''), '缺失映射有明确报错');
348
+ writeMapping(root, 'bad.json', '{ not json');
349
+ const badJson = cli(['repair-plan', root, '--mapping', join(root, 'bad.json')]);
350
+ assert(badJson.status === 1, '映射文件非法 JSON → fail-closed 退出码 1');
351
+ const notArray = writeMapping(root, 'obj.json', { seq: 1 });
352
+ const rootNotArray = cli(['repair-plan', root, '--mapping', notArray, '--format', 'json']);
353
+ const rna = JSON.parse(rootNotArray.stdout);
354
+ assert(rna.rejectedMappings[0]?.reason === REPAIR_MAPPING_ROOT_NOT_ARRAY, '映射根节点非数组 → mapping_root_not_array');
355
+ assert(rna.totals.move === 0, '根节点非数组时零搬移');
356
+ const badFormat = cli(['repair-plan', root, '--format', 'yaml']);
357
+ assert(badFormat.status === 1, '--format 非法值 fail-closed');
358
+ const mapOnLog = cli(['log', root, '--mapping', mapPath]);
359
+ assert(mapOnLog.status === 1, '--mapping 只属于 repair-plan,用在 log 上被拒');
360
+ const fmtOnStats = cli(['stats', root, '--format', 'json']);
361
+ assert(fmtOnStats.status === 1, '--format 只属于 repair-plan,用在 stats 上被拒');
362
+ const extraPos = cli(['repair-plan', root, 'extra']);
363
+ assert(extraPos.status === 1, 'repair-plan 不接受额外位置参数');
364
+ const after = snapshotDir(stateDir(root));
365
+ assert(before === after, '零写:全部 CLI repair-plan 调用后 state 目录逐字节不变');
366
+ const empty = newRoot('cli-empty');
367
+ const noLedger = cli(['repair-plan', empty]);
368
+ assert(noLedger.status === 1 && /无账本/.test(noLedger.stderr ?? ''), '无账本 root → 退出码 1,不建库');
369
+ assert(readdirSync(empty).length === 0, '无账本时不在 root 下创建任何文件/目录');
370
+ }
371
+ for (const r of roots)
372
+ rmSync(r, { recursive: true, force: true });
373
+ console.log(`\nLEDGER-REPAIR-PLAN TESTS: ${pass} ok, ${fail} fail`);
374
+ if (fail > 0)
375
+ process.exit(1);