@danceiny/gotry 0.2.0-rc.27 → 0.2.0-rc.29

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 (79) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/bin/gotry-bootstrap.js +56 -12
  4. package/data/yunnan-pack.json +30 -27
  5. package/dist/capabilities/anything.js +26 -0
  6. package/dist/capabilities/channel-registry.js +4 -4
  7. package/dist/capabilities/doctor.js +14 -7
  8. package/dist/capabilities/effect.js +1 -1
  9. package/dist/capabilities/ground-transfer.js +168 -1
  10. package/dist/capabilities/provider-detail-sanitize.js +64 -0
  11. package/dist/capabilities/route-provider-conformance.js +341 -0
  12. package/dist/capabilities/session/extension-bridge.js +3 -3
  13. package/dist/capabilities/session-search.js +1 -1
  14. package/dist/scripts/anything-path-measure-tests.js +133 -0
  15. package/dist/scripts/anything-path-measure.js +150 -0
  16. package/dist/scripts/anything-tests.js +136 -2
  17. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +5 -2
  18. package/dist/scripts/bootstrap-tests.js +77 -7
  19. package/dist/scripts/city-scenario-tier-tests.js +234 -0
  20. package/dist/scripts/doctor-tests.js +39 -3
  21. package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
  22. package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
  23. package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
  24. package/dist/scripts/dsh-llm-tests.js +437 -0
  25. package/dist/scripts/g5-guard-tests.js +20 -1
  26. package/dist/scripts/g5-guard.js +5 -1
  27. package/dist/scripts/gotry-backend-kernel-tests.js +1 -1
  28. package/dist/scripts/m3-cohort-tests.js +714 -0
  29. package/dist/scripts/m3-cohort.js +310 -0
  30. package/dist/scripts/nightly-evidence-tests.js +44 -2
  31. package/dist/scripts/nightly-evidence.js +21 -4
  32. package/dist/scripts/outcome-projection-tests.js +427 -0
  33. package/dist/scripts/persona-sim-tests.js +621 -0
  34. package/dist/scripts/persona-sim.js +1081 -0
  35. package/dist/scripts/replay-async.js +2 -0
  36. package/dist/scripts/replay.js +2 -0
  37. package/dist/scripts/route-provider-conformance-tests.js +391 -0
  38. package/dist/scripts/session-dida-live-e2e.js +1 -1
  39. package/dist/scripts/session-zone-crash.js +49 -0
  40. package/dist/scripts/session-zone-ledger-tests.js +432 -0
  41. package/dist/scripts/session-zone-observation-tests.js +492 -0
  42. package/dist/scripts/session-zone-observe.js +135 -0
  43. package/dist/scripts/session-zone-wiring-tests.js +566 -0
  44. package/dist/scripts/session-zones-tests.js +17 -1
  45. package/dist/scripts/spawn-bounded-abort-tests.js +3 -0
  46. package/dist/scripts/state-cli.js +16 -0
  47. package/dist/scripts/time-eval-tests.js +2 -0
  48. package/dist/scripts/unified-tests.js +79 -2
  49. package/dist/scripts/z3-race-tests.js +6 -1
  50. package/dist/src/backend/modules/session-search.js +1 -1
  51. package/dist/src/city-scenario-tier.js +378 -0
  52. package/dist/src/dsh-llm.js +84 -14
  53. package/dist/src/index.js +166 -7
  54. package/dist/src/loop.js +16 -5
  55. package/dist/src/m3-cohort.js +1577 -0
  56. package/dist/src/outcome-projection.js +746 -0
  57. package/dist/src/session-zone-ledger.js +312 -0
  58. package/dist/src/session-zone-observation.js +354 -0
  59. package/dist/src/session-zone-wiring.js +658 -0
  60. package/dist/src/session-zones.js +7 -4
  61. package/dist/src/state-ledger.js +293 -40
  62. package/dist/src/unified.js +69 -5
  63. package/extension/README.md +1 -1
  64. package/extension/README.zh-CN.md +1 -1
  65. package/extension/background.js +1 -1
  66. package/extension/content-bridge.js +1 -1
  67. package/extension/content-main.js +1 -1
  68. package/extension/manifest.json +2 -2
  69. package/package.json +1 -1
  70. package/ts/capabilities/anything.ts +27 -0
  71. package/ts/capabilities/session/extension-bridge.ts +3 -3
  72. package/ts/capabilities/session-search.ts +1 -1
  73. package/ts/scripts/state-cli.ts +18 -0
  74. package/ts/src/contracts.ts +4 -0
  75. package/ts/src/dsh-llm.ts +111 -9
  76. package/ts/src/index.ts +181 -8
  77. package/ts/src/loop.ts +15 -5
  78. package/ts/src/state-ledger.ts +294 -40
  79. package/ts/src/unified.ts +91 -18
@@ -7,6 +7,8 @@ import { join } from 'node:path';
7
7
  import { createMockLlm } from '../src/mock-llm.js';
8
8
  import { collectDeepPlanning, isComplex, newState, persistAsyncTicket, requestDeepPlanning, runTurn } from '../src/loop.js';
9
9
  import { solveUnified } from '../src/unified.js';
10
+ // issue #617:mock 重放是离线演练;runTurn 的 PoI 探针缺省不得 spawn 本机真实 hbcli(显式设置的值不覆盖)
11
+ process.env['GOTRY_HBCLI_LIVE'] ??= '0';
10
12
  const llm = createMockLlm(join('..', 'data', 'flights_2026.json'));
11
13
  const state = newState();
12
14
  const history = [];
@@ -8,6 +8,8 @@ import { createMockLlm } from '../src/mock-llm.js';
8
8
  import { interviewNext, newState, runTurn } from '../src/loop.js';
9
9
  import { solveUnified } from '../src/unified.js';
10
10
  import { join } from 'node:path';
11
+ // issue #617:mock 重放是离线演练;runTurn 的 PoI 探针缺省不得 spawn 本机真实 hbcli(显式设置的值不覆盖)
12
+ process.env['GOTRY_HBCLI_LIVE'] ??= '0';
11
13
  const llm = createMockLlm(join('..', 'data', 'flights_2026.json'));
12
14
  let state = newState();
13
15
  const userTurns = [
@@ -0,0 +1,391 @@
1
+ /**
2
+ * D-39 live-route provider CONFORMANCE harness tests (issue #429 contract slice,
3
+ * SIMULATED-TRIGGER DRILL — fully offline, zero network, zero real routing provider).
4
+ *
5
+ * ⚠ Evidence label: `simulated_trigger_drill` / `fixture_contract`. The trigger is
6
+ * SIMULATED by declaring a NAMED use case and driving a MOCK adapter through eight
7
+ * fault modes. That is not #429's real trigger: no provider is named, licensed,
8
+ * measured for coverage/freshness or contacted, so no D-39 path is admitted, every
9
+ * path stays default-OFF, and #341's accepted narrow scope is untouched.
10
+ *
11
+ * Sections:
12
+ * A. Default-off: trigger frozen false, registry frozen empty, `consultRouteProvider`
13
+ * refuses before touching the adapter (fetch-spy zero-call proof).
14
+ * B. Mock adapter: all nine conformance clauses probed → admissible.
15
+ * C. The harness is itself falsifiable: per clause, a non-conformant outcome flips
16
+ * that clause to fail; an unprobed clause blocks admission.
17
+ * D. Fault-detail sanitization: a challenge page body with a cookie/token is
18
+ * scrubbed, and the leak check catches markup / credentials / URLs / length.
19
+ * E. Static fallback preservation + its own falsification.
20
+ * F. Layering: the conformance module imports nothing (zero IO / zero kernel
21
+ * coupling) and has zero product callers.
22
+ * G. The EXISTING ground-transfer logic (ts/capabilities/ground-transfer.ts, the
23
+ * accepted #341/#364 narrow path) driven through the applicable clauses. All
24
+ * three gaps the drill found (GAP-429-1/2/3) are now FIXED, so the former
25
+ * characterization assertions are FLIPPED: a contradicting provider claim —
26
+ * declared mode, or resolved origin/destination — fails closed to the static
27
+ * fallback instead of binding its minutes. Details and minimal repros live in
28
+ * docs/evaluation/trigger-drill-report-contracts.md.
29
+ *
30
+ * Run (from ts/): npx tsx scripts/route-provider-conformance-tests.ts
31
+ */
32
+ import { readFileSync } from 'node:fs';
33
+ import { join } from 'node:path';
34
+ import { fileURLToPath } from 'node:url';
35
+ import { ADMITTED_LIVE_ROUTE_PROVIDERS, D39_LIVE_ROUTE_TRIGGER_FIRED, FAULT_DETAIL_MAX_CHARS, ROUTE_CONFORMANCE_CLAUSES, ROUTE_EVIDENCE_CLASSES, ROUTE_MODES, ROUTE_PROVIDER_CONFORMANCE_SCHEMA, ROUTE_PROVIDER_FAULT_MODES, admitRouteFact, consultRouteProvider, faultDetailLeak, runRouteProviderConformance, sanitizeFaultDetail, staticFallbackViolation, } from '../capabilities/route-provider-conformance.js';
36
+ import { GROUND_TRANSFER_STATIC_PRICE_EVIDENCE, createGroundTransferResolver, exposeGroundTransferEvidence, resolveGroundTransferPayload, } from '../capabilities/ground-transfer.js';
37
+ let passed = 0;
38
+ function check(cond, msg) {
39
+ if (cond) {
40
+ passed++;
41
+ console.log(` ok - ${msg}`);
42
+ }
43
+ else {
44
+ console.error(` FAIL - ${msg}`);
45
+ process.exitCode = 1;
46
+ }
47
+ }
48
+ function expectRefusal(result, code, needle, msg) {
49
+ if (result.ok) {
50
+ check(false, `${msg} (expected refusal ${code}, got ok)`);
51
+ return;
52
+ }
53
+ check(result.code === code && result.detail.includes(needle), `${msg} (refused ${result.code}${result.code === code ? '' : ` ≠ ${code}`}, evidence contains "${needle}": ${result.detail.includes(needle)})`);
54
+ }
55
+ const NOW = '2026-10-04T12:00:00.000Z';
56
+ const OBSERVED = '2026-10-04T11:59:00.000Z'; // 60s old
57
+ const O = '100.1,13.1';
58
+ const D = '100.5,13.5';
59
+ /**
60
+ * The SIMULATED named use case. It is deliberately the already-accepted #341 shape
61
+ * (destination driving estimate, no fare authority) so the drill adds no new path.
62
+ */
63
+ const USE_CASE = {
64
+ id: 'drill:destination-driving-transfer',
65
+ mode: 'driving',
66
+ evidence_class: 'route_estimate',
67
+ freshness_max_age_s: 15 * 60,
68
+ access_boundary: 'read_only_public',
69
+ fare_authority: 'none',
70
+ };
71
+ /** TEST-ONLY descriptor: no real provider is named, licensed or contacted. */
72
+ const MOCK_DESCRIPTOR = {
73
+ id: 'mock-offline-route-provider',
74
+ legal_basis: 'test-only offline mock (issue #429 drill; no live provider admitted)',
75
+ access_boundary: 'read_only_public',
76
+ source_sha: '0000000000000000000000000000000000000000',
77
+ };
78
+ const REQUEST_OUT = { direction: 'outbound', origin: O, destination: D, mode: 'driving' };
79
+ const REQUEST_RET = { direction: 'return', origin: D, destination: O, mode: 'driving' };
80
+ function goodResponse(overrides = {}) {
81
+ return {
82
+ provider_id: MOCK_DESCRIPTOR.id,
83
+ mode: 'driving',
84
+ evidence_class: 'route_estimate',
85
+ echoed_origin: O,
86
+ echoed_destination: D,
87
+ distance_m: 30_000,
88
+ duration_s: 2_400,
89
+ observed_at: OBSERVED,
90
+ ...overrides,
91
+ };
92
+ }
93
+ function okOutcome(overrides = {}) {
94
+ return { ok: true, response: goodResponse(overrides) };
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // A. Default-off
98
+ // ---------------------------------------------------------------------------
99
+ console.log('A. default-off: trigger frozen false, registry frozen empty, zero providers contacted');
100
+ check(D39_LIVE_ROUTE_TRIGGER_FIRED === false, 'D-39 wider-path trigger gate frozen false');
101
+ check(ADMITTED_LIVE_ROUTE_PROVIDERS.length === 0, 'the admitted live-route provider registry is frozen empty');
102
+ check(ROUTE_PROVIDER_CONFORMANCE_SCHEMA === 'gotry_route_provider_conformance.v1', 'the conformance schema id is versioned');
103
+ check(ROUTE_PROVIDER_FAULT_MODES.length === 8 && ROUTE_CONFORMANCE_CLAUSES.length === 9, 'eight fault modes and nine conformance clauses are declared as closed sets');
104
+ check(ROUTE_MODES.includes('transit') && ROUTE_MODES.includes('rail') && ROUTE_EVIDENCE_CLASSES.includes('live_traffic'), 'the closed sets name the D-39 remainder paths explicitly (transit / rail / live traffic)');
105
+ let spyCalls = 0;
106
+ const spyAdapter = {
107
+ descriptor: MOCK_DESCRIPTOR,
108
+ fetchRoute: async () => {
109
+ spyCalls++;
110
+ return okOutcome();
111
+ },
112
+ };
113
+ const deferred = await consultRouteProvider(spyAdapter, USE_CASE, REQUEST_OUT, { now: NOW });
114
+ expectRefusal(deferred, 'trigger_deferred', 'zero routing providers contacted', 'before the trigger, the adapter is refused structurally');
115
+ check(spyCalls === 0, 'fetch-spy zero-call proof: no routing provider is contacted before the trigger fires');
116
+ check(!deferred.ok && deferred.detail.includes('static fallback stands'), 'the pre-trigger refusal states that the static fallback stands');
117
+ const notAdmitted = await consultRouteProvider(spyAdapter, USE_CASE, REQUEST_OUT, { triggerFired: true, now: NOW });
118
+ expectRefusal(notAdmitted, 'provider_not_admitted', 'per-path founder decision', 'trigger on + empty registry: the provider is still not admitted');
119
+ check(spyCalls === 0, 'registry refusal also precedes any adapter call');
120
+ const viaSpy = await consultRouteProvider(spyAdapter, USE_CASE, REQUEST_OUT, { triggerFired: true, registry: [MOCK_DESCRIPTOR], now: NOW });
121
+ check(viaSpy.ok && spyCalls === 1, 'the seam is usable once a provider is explicitly admitted (drill only)');
122
+ check(viaSpy.ok && viaSpy.value.provider_id === MOCK_DESCRIPTOR.id && viaSpy.value.source_sha === MOCK_DESCRIPTOR.source_sha, 'an admitted fact names its source identity and reviewed source SHA');
123
+ check(viaSpy.ok && viaSpy.value.access_boundary === 'read_only_public' && viaSpy.value.fare === null, 'an admitted read-only fact carries no fare (read-only routing authorizes no price claim)');
124
+ // ---------------------------------------------------------------------------
125
+ // B. Mock adapter: the full conformance report
126
+ // ---------------------------------------------------------------------------
127
+ console.log('B. mock adapter driven through all eight fault modes → nine clauses');
128
+ const CHALLENGE_BODY = 'HTTP 403 <html><body>Please complete the CAPTCHA. token=abc123 cookie=sid=XYZ see https://provider.example/challenge</body></html>';
129
+ const FULL_PROBES = [
130
+ { clause: 'direction_binding', label: 'happy path outbound', request: REQUEST_OUT, outcome: okOutcome(), expect: null },
131
+ { clause: 'direction_binding', label: 'return request answered with the outbound pair', request: REQUEST_RET, outcome: okOutcome(), expect: 'direction_mismatch' },
132
+ { clause: 'direction_binding', label: 'provider routed a different O/D', request: REQUEST_OUT, outcome: okOutcome({ echoed_origin: '0,0', echoed_destination: '1,1' }), expect: 'direction_mismatch' },
133
+ { clause: 'direction_binding', label: 'declared mismatched_direction fault', request: REQUEST_OUT, outcome: { ok: false, fault: 'mismatched_direction', detail: 'provider resolved a different pair' }, expect: 'direction_mismatch' },
134
+ { clause: 'mode_isolation', label: 'transit route offered for a driving use case', request: REQUEST_OUT, outcome: okOutcome({ mode: 'transit' }), expect: 'mode_mismatch' },
135
+ { clause: 'mode_isolation', label: 'rail route offered for a driving use case', request: REQUEST_OUT, outcome: okOutcome({ mode: 'rail' }), expect: 'mode_mismatch' },
136
+ { clause: 'mode_isolation', label: 'declared mode_relabel fault', request: REQUEST_OUT, outcome: { ok: false, fault: 'mode_relabel', detail: 'provider relabelled transit as driving' }, expect: 'mode_mismatch' },
137
+ { clause: 'evidence_class_isolation', label: 'estimate claiming live traffic', request: REQUEST_OUT, outcome: okOutcome({ evidence_class: 'live_traffic' }), expect: 'evidence_class_escalation' },
138
+ { clause: 'evidence_class_isolation', label: 'static estimate filling a route-estimate promise', request: REQUEST_OUT, outcome: okOutcome({ evidence_class: 'static_estimate' }), expect: 'evidence_class_shortfall' },
139
+ { clause: 'evidence_class_isolation', label: 'declared estimate_as_live_traffic fault', request: REQUEST_OUT, outcome: { ok: false, fault: 'estimate_as_live_traffic', detail: 'provider advertised live traffic for a cached estimate' }, expect: 'evidence_class_escalation' },
140
+ { clause: 'freshness_contract', label: 'observation older than the freshness window', request: REQUEST_OUT, outcome: okOutcome({ observed_at: '2026-10-04T11:00:00.000Z' }), expect: 'stale' },
141
+ { clause: 'freshness_contract', label: 'declared stale fault', request: REQUEST_OUT, outcome: { ok: false, fault: 'stale', detail: 'cache entry beyond max age' }, expect: 'stale' },
142
+ { clause: 'freshness_contract', label: 'observation postdating the gate clock', request: REQUEST_OUT, outcome: okOutcome({ observed_at: '2026-10-04T12:00:01.000Z' }), expect: 'bad_ts' },
143
+ { clause: 'source_identity', label: 'response attributed to another provider', request: REQUEST_OUT, outcome: okOutcome({ provider_id: 'some-other-provider' }), expect: 'binding_incomplete' },
144
+ { clause: 'fault_fail_closed', label: 'declared unavailable fault', request: REQUEST_OUT, outcome: { ok: false, fault: 'unavailable', detail: 'connection refused' }, expect: 'unavailable' },
145
+ { clause: 'fault_fail_closed', label: 'declared rate_limited fault', request: REQUEST_OUT, outcome: { ok: false, fault: 'rate_limited', detail: 'HTTP 429 retry-after 60' }, expect: 'rate_limited' },
146
+ { clause: 'fault_fail_closed', label: 'declared partial_result fault', request: REQUEST_OUT, outcome: { ok: false, fault: 'partial_result', detail: 'duration missing' }, expect: 'partial_result' },
147
+ { clause: 'fault_fail_closed', label: 'partial payload (duration 0 over positive distance)', request: REQUEST_OUT, outcome: okOutcome({ duration_s: 0 }), expect: 'partial_result' },
148
+ { clause: 'fault_fail_closed', label: 'partial payload (negative distance)', request: REQUEST_OUT, outcome: okOutcome({ distance_m: -1 }), expect: 'partial_result' },
149
+ { clause: 'fault_detail_sanitized', label: 'challenge page body with cookie + token + URL', request: REQUEST_OUT, outcome: { ok: false, fault: 'challenge', detail: CHALLENGE_BODY }, expect: 'challenge' },
150
+ { clause: 'fare_authority_separate', label: 'provider fare offered where no fare authority is declared', request: REQUEST_OUT, outcome: okOutcome({ fare: { amount: '42.00', currency: 'CNY', authority: 'mock-offline-route-provider' } }), expect: 'fare_not_authoritative' },
151
+ { clause: 'static_fallback_preserved', label: 'a refusal must leave the static fallback untouched', request: REQUEST_OUT, outcome: { ok: false, fault: 'unavailable', detail: 'connection refused' }, expect: 'unavailable' },
152
+ ];
153
+ const report = runRouteProviderConformance(USE_CASE, MOCK_DESCRIPTOR, FULL_PROBES, NOW);
154
+ check(report.unprobed.length === 0, 'all nine clauses were probed');
155
+ for (const clause of report.clauses) {
156
+ check(clause.verdict === 'pass', `clause ${clause.clause} passes (${clause.probes} probe(s))${clause.verdict === 'pass' ? '' : `: ${clause.evidence.join(' | ')}`}`);
157
+ }
158
+ check(report.admissible, 'the mock adapter is conformant: every clause probed and passed');
159
+ check(report.schema === ROUTE_PROVIDER_CONFORMANCE_SCHEMA && report.use_case_id === USE_CASE.id, 'the report binds its schema and the named use case');
160
+ // ---------------------------------------------------------------------------
161
+ // C. The harness is itself falsifiable
162
+ // ---------------------------------------------------------------------------
163
+ console.log('C. the harness is falsifiable: a silently-accepted fault flips its clause to fail');
164
+ const silentlyAccepting = FULL_PROBES.map(p => p.clause === 'mode_isolation' && p.expect === 'mode_mismatch' && p.outcome.ok
165
+ ? { ...p, outcome: okOutcome() } // the adapter "fixed" the relabel by dropping the mode claim
166
+ : p);
167
+ const silentReport = runRouteProviderConformance(USE_CASE, MOCK_DESCRIPTOR, silentlyAccepting, NOW);
168
+ check(!silentReport.admissible, 'an adapter whose transit payload is accepted as driving is NOT admissible');
169
+ const modeClause = silentReport.clauses.find(c => c.clause === 'mode_isolation');
170
+ check(modeClause?.verdict === 'fail' && (modeClause?.evidence ?? []).some(e => e.includes('a fault was silently accepted')), 'the mode_isolation clause reports the silent acceptance explicitly');
171
+ const partialReport = runRouteProviderConformance(USE_CASE, MOCK_DESCRIPTOR, FULL_PROBES.filter(p => p.clause !== 'freshness_contract'), NOW);
172
+ check(!partialReport.admissible && partialReport.unprobed.join(',') === 'freshness_contract', 'an UNPROBED clause blocks admission ("untested" is not conformance)');
173
+ check(partialReport.clauses.find(c => c.clause === 'freshness_contract')?.verdict === 'fail', 'an unprobed clause is reported as fail, never as pass');
174
+ const emptyReport = runRouteProviderConformance(USE_CASE, MOCK_DESCRIPTOR, [], NOW);
175
+ check(!emptyReport.admissible && emptyReport.unprobed.length === ROUTE_CONFORMANCE_CLAUSES.length, 'a provider with zero probes is not admissible on any clause');
176
+ console.log('C2. gate-level negative cases (use case / descriptor hygiene)');
177
+ expectRefusal(admitRouteFact({ ...USE_CASE, id: '' }, MOCK_DESCRIPTOR, REQUEST_OUT, okOutcome(), NOW), 'binding_incomplete', 'per NAMED product use case', 'an unnamed use case is refused (no global admission)');
178
+ expectRefusal(admitRouteFact({ ...USE_CASE, freshness_max_age_s: 0 }, MOCK_DESCRIPTOR, REQUEST_OUT, okOutcome(), NOW), 'binding_incomplete', 'without a freshness contract', 'a use case without a freshness contract is refused');
179
+ expectRefusal(admitRouteFact(USE_CASE, { ...MOCK_DESCRIPTOR, legal_basis: '' }, REQUEST_OUT, okOutcome(), NOW), 'binding_incomplete', 'no legal_basis', 'a provider without a legal basis is refused');
180
+ expectRefusal(admitRouteFact(USE_CASE, { ...MOCK_DESCRIPTOR, source_sha: '' }, REQUEST_OUT, okOutcome(), NOW), 'binding_incomplete', 'no source_sha', 'a provider without the reviewed source SHA is refused');
181
+ expectRefusal(admitRouteFact(USE_CASE, { ...MOCK_DESCRIPTOR, access_boundary: 'supplier_transaction' }, REQUEST_OUT, okOutcome(), NOW), 'closed_set', 'never widens into a transaction boundary', 'a read-only use case cannot be served by a transaction-boundary provider (a route fact grants no write path)');
182
+ expectRefusal(admitRouteFact(USE_CASE, MOCK_DESCRIPTOR, REQUEST_OUT, { ok: false, fault: 'mystery', detail: 'x' }, NOW), 'closed_set', 'not a degrade licence', 'an unclassified provider failure is refused (no silent degrade)');
183
+ expectRefusal(admitRouteFact(USE_CASE, MOCK_DESCRIPTOR, REQUEST_OUT, { ok: true, response: { ...goodResponse(), mode: undefined } }, NOW), 'closed_set', 'may not be labelled by the request', 'a response with no explicit mode claim is refused (the request never supplies the label)');
184
+ const fareUseCase = { ...USE_CASE, id: 'drill:fare-path', fare_authority: 'separate_authoritative_source' };
185
+ expectRefusal(admitRouteFact(fareUseCase, MOCK_DESCRIPTOR, REQUEST_OUT, okOutcome({ fare: { amount: '42.00', currency: 'CNY', authority: MOCK_DESCRIPTOR.id } }), NOW), 'fare_not_authoritative', 'never its own fare authority', 'even on a fare-declaring use case, the route provider cannot be its own fare authority');
186
+ const separateFare = admitRouteFact(fareUseCase, MOCK_DESCRIPTOR, REQUEST_OUT, okOutcome({ fare: { amount: '42.00', currency: 'CNY', authority: 'separate-fare-authority-fixture' } }), NOW);
187
+ check(separateFare.ok && separateFare.value.fare?.authority === 'separate-fare-authority-fixture', 'a fare from a separately named authority is admitted and keeps its authority on the fact');
188
+ // ---------------------------------------------------------------------------
189
+ // D. Fault-detail sanitization
190
+ // ---------------------------------------------------------------------------
191
+ console.log('D. fault details are labels, never provider response bodies');
192
+ const scrubbed = sanitizeFaultDetail(CHALLENGE_BODY);
193
+ check(!scrubbed.includes('<html>') && !scrubbed.includes('</body>'), 'markup is stripped from the fault detail');
194
+ check(!scrubbed.includes('abc123') && !scrubbed.includes('sid=XYZ'), 'the cookie and token values never survive sanitization');
195
+ check(!scrubbed.includes('https://provider.example'), 'the challenge URL is redacted');
196
+ check(scrubbed.length <= FAULT_DETAIL_MAX_CHARS, `the sanitized detail is bounded at ${FAULT_DETAIL_MAX_CHARS} chars`);
197
+ check(faultDetailLeak(scrubbed) === null, 'the sanitized detail passes the leak check');
198
+ check(sanitizeFaultDetail('') === '(no detail)' && sanitizeFaultDetail(undefined) === '(no detail)', 'a missing detail becomes an explicit placeholder, never undefined');
199
+ check(faultDetailLeak('<b>oops</b>') !== null, 'the leak check catches markup');
200
+ check(faultDetailLeak('cookie=sid=XYZ') !== null, 'the leak check catches a credential assignment');
201
+ check(faultDetailLeak('see https://x.example') !== null, 'the leak check catches a URL');
202
+ check(sanitizeFaultDetail('x'.repeat(FAULT_DETAIL_MAX_CHARS + 50)).length === FAULT_DETAIL_MAX_CHARS, 'the sanitizer truncates an over-long provider fragment to exactly the bound');
203
+ check(faultDetailLeak('provider fault: connection refused after 3000 ms') === null, 'a host-authored explanation without leak shapes passes (length is bounded by the sanitizer, not by the leak check)');
204
+ const challengeRefusal = admitRouteFact(USE_CASE, MOCK_DESCRIPTOR, REQUEST_OUT, { ok: false, fault: 'challenge', detail: CHALLENGE_BODY }, NOW);
205
+ check(!challengeRefusal.ok && faultDetailLeak(challengeRefusal.detail) === null, 'the gate refusal for a challenge carries no leak');
206
+ check(!challengeRefusal.ok && challengeRefusal.code === 'challenge', 'a bot challenge is its own typed refusal, not a generic error');
207
+ // ---------------------------------------------------------------------------
208
+ // E. Static fallback preservation
209
+ // ---------------------------------------------------------------------------
210
+ console.log('E. a refusal leaves the static fallback and its original price label intact');
211
+ const staticBefore = { mode: 'taxi', minutes: 45, priceCny: 180, priceEvidence: GROUND_TRANSFER_STATIC_PRICE_EVIDENCE };
212
+ check(staticFallbackViolation(staticBefore, { ...staticBefore }) === null, 'an untouched fallback passes the mechanical check');
213
+ check(staticFallbackViolation(staticBefore, { ...staticBefore, minutes: 40 }) !== null, 'the check CATCHES a mutated static minutes value');
214
+ check(staticFallbackViolation(staticBefore, { ...staticBefore, priceCny: 42 }) !== null, 'the check CATCHES a route-supplied price');
215
+ check(staticFallbackViolation(staticBefore, { ...staticBefore, priceEvidence: '[实时API:provider]' }) !== null, 'the check CATCHES a rewritten price label (the original label stands until a separate fare authority exists)');
216
+ // ---------------------------------------------------------------------------
217
+ // F. Layering
218
+ // ---------------------------------------------------------------------------
219
+ console.log('F. layering: the conformance module is pure and has zero product callers');
220
+ const CAPABILITY_DIR = join(fileURLToPath(new URL('.', import.meta.url)), '..', 'capabilities');
221
+ const CONFORMANCE_SOURCE = readFileSync(join(CAPABILITY_DIR, 'route-provider-conformance.ts'), 'utf8');
222
+ const importLines = CONFORMANCE_SOURCE.split('\n').filter(l => /^\s*import\s/.test(l));
223
+ check(importLines.length === 1 && importLines[0].includes("'./provider-detail-sanitize.js'"), 'the conformance module imports exactly one thing: the pure sibling sanitizer (zero network/cache/timer/clock/filesystem surface)');
224
+ const SANITIZER_SOURCE = readFileSync(join(CAPABILITY_DIR, 'provider-detail-sanitize.ts'), 'utf8');
225
+ check(SANITIZER_SOURCE.split('\n').filter(l => /^\s*import\s/.test(l)).length === 0, 'the shared sanitizer itself declares zero imports (one pure implementation, two callers)');
226
+ check(!/from '.*(model|unified)\.js'/.test(CONFORMANCE_SOURCE), 'the conformance module never imports the kernel evaluate/solve layer');
227
+ check(!/new Date\(\)/.test(CONFORMANCE_SOURCE), 'the conformance module reads no clock: `now` is injected by the caller');
228
+ check(CONFORMANCE_SOURCE.includes('export const D39_LIVE_ROUTE_TRIGGER_FIRED = false'), 'the trigger declaration is literally `= false` in source');
229
+ check(CONFORMANCE_SOURCE.includes('export const ADMITTED_LIVE_ROUTE_PROVIDERS: readonly RouteProviderDescriptor[] = []'), 'the provider registry declaration is literally `= []` in source');
230
+ const INDEX_SOURCE = readFileSync(join(fileURLToPath(new URL('.', import.meta.url)), '..', 'src', 'index.ts'), 'utf8');
231
+ check(!INDEX_SOURCE.includes('route-provider-conformance'), 'the product entry point (ts/src/index.ts) does not reference the conformance module (zero callers)');
232
+ // ---------------------------------------------------------------------------
233
+ // G. The EXISTING ground-transfer logic against the applicable clauses
234
+ // ---------------------------------------------------------------------------
235
+ console.log('G. the accepted #341/#364 ground-transfer path driven through the applicable clauses');
236
+ const GT_CLOCK = () => new Date('2026-10-04T00:00:00.000Z');
237
+ const GT_CANDIDATES = [
238
+ { id: 'cand-1', destTransfers: [{ mode: 'taxi', minutes: 45, priceCny: 180 }] },
239
+ ];
240
+ const GT_REQUEST = {
241
+ candidate_id: 'cand-1',
242
+ transfer_index: 0,
243
+ mode: 'driving',
244
+ position: 'destination',
245
+ outbound: { origin: { longitude: 100.1, latitude: 13.1 }, destination: { longitude: 100.5, latitude: 13.5 } },
246
+ return: { origin: { longitude: 100.5, latitude: 13.5 }, destination: { longitude: 100.1, latitude: 13.1 } },
247
+ };
248
+ async function driveGroundTransfer(provider, clock = GT_CLOCK) {
249
+ const resolver = createGroundTransferResolver({ provider, clock });
250
+ return resolveGroundTransferPayload(GT_REQUEST, GT_CANDIDATES, resolver);
251
+ }
252
+ console.log('G1. clauses that PASS today');
253
+ const gtFare = await driveGroundTransfer(async () => ({ provider: 'mock', distanceM: 30000, durationS: 2400, fareCny: 42, fareAuthoritative: true }));
254
+ check(gtFare.resolution.priceCny === 180 && gtFare.resolution.priceEvidence === GROUND_TRANSFER_STATIC_PRICE_EVIDENCE, 'fare_authority_separate PASSES: a provider-asserted fare is not admitted; the static price and label stand');
255
+ check(gtFare.candidates[0].destTransfers[0].priceCny === 180, 'fare_authority_separate PASSES: the patched candidate keeps its static priceCny');
256
+ const gtLive = await driveGroundTransfer(async () => ({ provider: 'mock', distanceM: 30000, durationS: 2400, trafficStatus: 'live', liveTraffic: true }));
257
+ check(gtLive.resolution.outbound.routeFact?.trafficStatus === 'not-live-route-estimate', 'evidence_class_isolation PASSES in the escalating direction: a provider claiming live traffic still yields not-live-route-estimate');
258
+ check(gtLive.resolution.outbound.routeFact?.evidenceClass === 'public_map_route_estimate', 'evidence_class_isolation PASSES: the fact stays a public route estimate');
259
+ const gtUnavailable = await driveGroundTransfer(async () => { throw new Error('connection refused'); });
260
+ const gtStaticAfter = {
261
+ mode: gtUnavailable.resolution.staticMode ?? '(absent)',
262
+ minutes: gtUnavailable.resolution.staticMinutes ?? -1,
263
+ priceCny: gtUnavailable.resolution.priceCny ?? -1,
264
+ priceEvidence: gtUnavailable.resolution.priceEvidence ?? '(absent)',
265
+ };
266
+ check(staticFallbackViolation(staticBefore, gtStaticAfter) === null, 'static_fallback_preserved PASSES: an unavailable provider leaves mode/minutes/price/label byte-identical');
267
+ check(gtUnavailable.resolution.applied === false && gtUnavailable.candidates[0].destTransfers[0].minutesOut === undefined, 'fault_fail_closed PASSES: a provider error applies no minutes override');
268
+ const gtContradiction = await driveGroundTransfer(async () => ({ provider: 'mock', distanceM: 30000, durationS: 0 }));
269
+ check(gtContradiction.resolution.applied === false, 'fault_fail_closed PASSES: a self-contradictory route (positive distance, zero duration) falls back instead of binding zero minutes');
270
+ let gtCallLog = [];
271
+ const gtDirectional = await driveGroundTransfer(async (req) => {
272
+ gtCallLog.push(`${req.origin}>${req.destination}`);
273
+ return { provider: 'mock', distanceM: 30000, durationS: req.origin === '100.1,13.1' ? 2400 : 3600 };
274
+ });
275
+ check(gtCallLog.length === 2 && gtCallLog[0] !== gtCallLog[1], 'direction_binding PASSES on the REQUEST side: each direction is queried with its own ordered pair (no cache sharing)');
276
+ check(gtDirectional.resolution.outboundMinutes === 40 && gtDirectional.resolution.returnMinutes === 60, 'direction_binding PASSES: per-direction minutes never borrow the other direction value');
277
+ let staleCalls = 0;
278
+ const staleClockState = { ms: Date.parse('2026-10-04T00:00:00.000Z') };
279
+ const staleResolver = createGroundTransferResolver({
280
+ clock: () => new Date(staleClockState.ms),
281
+ provider: async () => {
282
+ staleCalls++;
283
+ if (staleCalls <= 2)
284
+ return { provider: 'mock', distanceM: 30000, durationS: 2400 };
285
+ throw new Error('rate limited');
286
+ },
287
+ });
288
+ const first = await resolveGroundTransferPayload(GT_REQUEST, GT_CANDIDATES, staleResolver);
289
+ check(first.resolution.applied && first.resolution.freshness === 'fresh', 'freshness_contract PASSES: the first resolution is fresh');
290
+ staleClockState.ms += 20 * 60 * 1000; // beyond the 15-minute window
291
+ const afterStale = await resolveGroundTransferPayload(GT_REQUEST, GT_CANDIDATES, staleResolver);
292
+ check(afterStale.resolution.applied === false && afterStale.resolution.outbound.cache.status === 'stale', 'freshness_contract PASSES: a stale cache entry is re-queried and, on failure, reported stale — never served as a hit');
293
+ check(afterStale.candidates[0].destTransfers[0].minutesOut === undefined, 'freshness_contract PASSES: a stale-then-failed direction applies no minutes override');
294
+ console.log('G2. fault_detail_sanitized: FIXED — the real resolution path no longer leaks a provider body');
295
+ // Regression for GAP-429-3 through the REAL resolution path: a throwing provider
296
+ // whose message carries markup, a token and a cookie. `safeErrorMessage` now routes
297
+ // through the shared sanitizeFaultDetail, so nothing reaches the exposed surface.
298
+ const gtLeakFixed = await driveGroundTransfer(async () => { throw new Error(CHALLENGE_BODY); });
299
+ const sanitizedReason = gtLeakFixed.resolution.outbound.fallbackReason ?? '';
300
+ check(sanitizedReason !== '', 'a throwing provider still produces a fallbackReason (the fix redacts, it does not swallow)');
301
+ check(!sanitizedReason.includes('token=abc123') && !sanitizedReason.includes('sid=XYZ'), 'the token and cookie values never reach fallbackReason');
302
+ check(!sanitizedReason.includes('<html>') && !sanitizedReason.includes('</body>'), 'provider markup never reaches fallbackReason');
303
+ check(!sanitizedReason.includes('https://provider.example'), 'the challenge URL never reaches fallbackReason');
304
+ check(sanitizedReason.includes('[credential-redacted]') && sanitizedReason.includes('[markup-redacted]'), 'the redaction is explicit and auditable, not a silent drop');
305
+ check(sanitizedReason.startsWith('map_driving_route_provider_error:outbound:'), 'the structured reason prefix is unchanged (classification still readable)');
306
+ check(faultDetailLeak(sanitizedReason) === null, 'the mechanical leak check passes on the real resolution path output');
307
+ const exposedFixed = exposeGroundTransferEvidence({ verdicts: [{ candidate_id: 'cand-1' }] }, gtLeakFixed.resolution);
308
+ const exposedJson = JSON.stringify(exposedFixed);
309
+ check(!exposedJson.includes('token=abc123') && !exposedJson.includes('sid=XYZ') && !exposedJson.includes('<html>'), 'exposeGroundTransferEvidence (wired at ts/src/index.ts:801) carries no provider body into the tool result or the verdict transfer_evidence');
310
+ check(faultDetailLeak(gtLeakFixed.resolution.fallbackReason ?? '') === null, 'the aggregate both_directions_failed reason is clean too');
311
+ check(faultDetailLeak(gtLeakFixed.resolution.return.fallbackReason ?? '') === null, 'the return-direction reason is clean too');
312
+ // No behaviour change for the fixed, already-safe error strings.
313
+ for (const safeMessage of [
314
+ 'provider unavailable',
315
+ 'invalid public result',
316
+ 'GROUND_TRANSFER_NESTED_EXECUTION_UNSUPPORTED: current tool execution context is unavailable',
317
+ 'GROUND_TRANSFER_PUBLIC_MAP_TOOL_UNAVAILABLE: map_driving_route is not registered in the current tool scope',
318
+ 'map_driving_route failed: upstream timeout after 3000 ms',
319
+ ]) {
320
+ const drive = await driveGroundTransfer(async () => { throw new Error(safeMessage); });
321
+ check(drive.resolution.outbound.fallbackReason === `map_driving_route_provider_error:outbound:${safeMessage}`, `an already-safe provider message passes through byte-identical: "${safeMessage.slice(0, 44)}"`);
322
+ }
323
+ check(sanitizeFaultDetail('a b', { maxChars: 400 }) === 'a b', 'whitespace runs collapse (the only normalization difference vs the previous newline-only strip)');
324
+ console.log('G3. GAP-429-1 / GAP-429-2: FIXED — a contradicting provider claim fails closed (FLIPPED assertions)');
325
+ function gtStaticAfterOf(resolution) {
326
+ return {
327
+ mode: resolution.staticMode ?? '(absent)',
328
+ minutes: resolution.staticMinutes ?? -1,
329
+ priceCny: resolution.priceCny ?? -1,
330
+ priceEvidence: resolution.priceEvidence ?? '(absent)',
331
+ };
332
+ }
333
+ // GAP-429-1 (mode_isolation). Was: the mode claim was dropped, the fact was
334
+ // labelled mode='driving' and the transit duration was bound into the solver.
335
+ const gtModeRelabel = await driveGroundTransfer(async () => ({ provider: 'mock-transit', distanceM: 30000, durationS: 3600, mode: 'transit' }));
336
+ check(gtModeRelabel.resolution.applied === false
337
+ && gtModeRelabel.resolution.outbound.routeFact === undefined
338
+ && gtModeRelabel.candidates[0].destTransfers[0].minutesOut === undefined
339
+ && gtModeRelabel.candidates[0].destTransfers[0].minutesRet === undefined, 'GAP-429-1 (mode_isolation) FIXED: a payload asserting mode="transit" is refused — no route fact, and its 60 minutes are never bound into the solver (was: silently relabelled driving and bound). FLIPPED from the characterization assertion; reverting the ground-transfer fix makes this fail.');
340
+ check((gtModeRelabel.resolution.outbound.fallbackReason ?? '').startsWith('mode_mismatch:outbound:')
341
+ && (gtModeRelabel.resolution.return.fallbackReason ?? '').startsWith('mode_mismatch:return:'), 'GAP-429-1 FIXED: both directions carry the conformance refusal vocabulary in the existing `<code>:<direction>:<detail>` reason shape');
342
+ check(gtModeRelabel.resolution.provenance === 'static-transfer-pack' && gtModeRelabel.resolution.evidenceClass === 'static_transfer_estimate', 'GAP-429-1 FIXED: the refusal reports the static pack as the provenance, never a map route estimate');
343
+ check(staticFallbackViolation(staticBefore, gtStaticAfterOf(gtModeRelabel.resolution)) === null, 'GAP-429-1 FIXED: static_fallback_preserved still holds — mode, minutes, price and label byte-identical');
344
+ check(faultDetailLeak(gtModeRelabel.resolution.outbound.fallbackReason ?? '') === null, 'GAP-429-1 FIXED: the provider-authored mode claim is quoted through the shared sanitizer (no leak)');
345
+ expectRefusal(admitRouteFact(USE_CASE, MOCK_DESCRIPTOR, REQUEST_OUT, okOutcome({ mode: 'transit' }), NOW), 'mode_mismatch', 'must never be relabelled', 'GAP-429-1: the conformance gate refuses the same payload with the same code (gate and product path now agree)');
346
+ // GAP-429-2 (direction_binding, RESPONSE side). Was: the provider's own resolved
347
+ // O/D was never read, the fact echoed the REQUESTED pair, and a 999 m / 60 s
348
+ // route was bound as the airport transfer.
349
+ const gtWrongOd = await driveGroundTransfer(async () => ({ provider: 'mock-wrong-od', distanceM: 999, durationS: 60, resolvedOrigin: '0,0', resolvedDestination: '1,1' }));
350
+ check(gtWrongOd.resolution.applied === false
351
+ && gtWrongOd.resolution.outbound.routeFact === undefined
352
+ && gtWrongOd.candidates[0].destTransfers[0].minutesOut === undefined
353
+ && gtWrongOd.candidates[0].destTransfers[0].minutesRet === undefined, 'GAP-429-2 (direction_binding, RESPONSE side) FIXED: a provider that resolved 0,0→1,1 is refused — the 999 m / 60 s route is never bound as the airport transfer. FLIPPED from the characterization assertion; reverting the ground-transfer fix makes this fail.');
354
+ check((gtWrongOd.resolution.outbound.fallbackReason ?? '').startsWith('direction_mismatch:outbound:')
355
+ && (gtWrongOd.resolution.return.fallbackReason ?? '').startsWith('direction_mismatch:return:'), 'GAP-429-2 FIXED: both directions carry the `direction_mismatch:<direction>:<detail>` reason');
356
+ check(staticFallbackViolation(staticBefore, gtStaticAfterOf(gtWrongOd.resolution)) === null, 'GAP-429-2 FIXED: static_fallback_preserved still holds after a direction refusal');
357
+ check(faultDetailLeak(gtWrongOd.resolution.outbound.fallbackReason ?? '') === null, 'GAP-429-2 FIXED: the echoed pair is quoted through the shared sanitizer (no leak)');
358
+ expectRefusal(admitRouteFact(USE_CASE, MOCK_DESCRIPTOR, REQUEST_OUT, okOutcome({ echoed_origin: '0,0', echoed_destination: '1,1' }), NOW), 'direction_mismatch', 'verified against the RESPONSE, not assumed from the request', 'GAP-429-2: the conformance gate verifies the echoed pair with the same code (gate and product path now agree)');
359
+ // The enforcement is a verification, not a blanket refusal: a conforming claim is
360
+ // admitted exactly as before, and a road-node snap inside the documented
361
+ // tolerance still matches. Absent claims (the wired `map_driving_route` tool)
362
+ // keep today's behaviour byte for byte — asserted throughout G1/G2 above and by
363
+ // the unchanged ts/scripts/ground-transfer-tests.ts suite.
364
+ function offsetEndpoint(text, deltaDeg) {
365
+ const [longitude, latitude] = text.split(',').map(Number);
366
+ return `${String(longitude + deltaDeg)},${String(latitude + deltaDeg)}`;
367
+ }
368
+ const gtConforming = await driveGroundTransfer(async (req) => ({
369
+ provider: 'mock-conforming', distanceM: 30000, durationS: 2400,
370
+ mode: 'driving', resolvedOrigin: req.origin, resolvedDestination: req.destination,
371
+ }));
372
+ check(gtConforming.resolution.applied === true
373
+ && gtConforming.resolution.outbound.routeFact?.mode === 'driving'
374
+ && gtConforming.candidates[0].destTransfers[0].minutesOut === 40, 'a provider that declares driving AND echoes the requested pair is admitted unchanged (the fix verifies claims, it does not refuse them)');
375
+ check(gtConforming.resolution.outbound.routeFact?.trafficStatus === 'not-live-route-estimate'
376
+ && gtConforming.resolution.outbound.routeFact?.evidenceClass === 'public_map_route_estimate', 'a verified fact is still only a route estimate: verification never upgrades the evidence class');
377
+ const gtSnapped = await driveGroundTransfer(async (req) => ({
378
+ provider: 'mock-snapped', distanceM: 30000, durationS: 2400,
379
+ resolvedOrigin: offsetEndpoint(req.origin, 0.00005), resolvedDestination: offsetEndpoint(req.destination, 0.00005),
380
+ }));
381
+ check(gtSnapped.resolution.applied === true && gtSnapped.candidates[0].destTransfers[0].minutesOut === 40, 'an endpoint snapped to the nearest road node (5e-5 deg ≈ 5 m, inside the documented 1e-4 tolerance) is still the requested place');
382
+ const gtDrifted = await driveGroundTransfer(async (req) => ({
383
+ provider: 'mock-drifted', distanceM: 30000, durationS: 2400,
384
+ resolvedOrigin: offsetEndpoint(req.origin, 0.01), resolvedDestination: req.destination,
385
+ }));
386
+ check(gtDrifted.resolution.applied === false && (gtDrifted.resolution.outbound.fallbackReason ?? '').startsWith('direction_mismatch:outbound:'), 'an endpoint ~1.1 km away is beyond the tolerance and is refused: a snap is not a different place');
387
+ const gtUnparseableEcho = await driveGroundTransfer(async () => ({
388
+ provider: 'mock-unparseable', distanceM: 30000, durationS: 2400, resolvedDestination: 'Suvarnabhumi Airport',
389
+ }));
390
+ check(gtUnparseableEcho.resolution.applied === false && (gtUnparseableEcho.resolution.outbound.fallbackReason ?? '').startsWith('direction_mismatch:outbound:'), 'a present-but-unverifiable echo fails closed (an unparseable pair is never assumed to be the requested one)');
391
+ console.log(`\nROUTE PROVIDER CONFORMANCE TESTS (#429, simulated_trigger_drill / fixture_contract; no real provider contacted): ${passed} pass${process.exitCode ? ', FAIL' : ' (all green)'}`);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Dida 供应商门户会话面 LIVE E2E(hotel-be portal integration 迁移线;GOTRY_SESSION_LIVE=1 显式开启)。
3
3
  *
4
- * 全链:真 Chrome(加载 unpacked Session Bridge 扩展)→ 人/驱动在 dida 官网完成登录
4
+ * 全链:真 Chrome(加载 unpacked Stai Travel Bridge 扩展)→ 人/驱动在 dida 官网完成登录
5
5
  * (账密经 env 注入,gotry 永不经手)→ sessionDidaSearch 扩展车道(桥自动拉起,
6
6
  * 扩展 cookie-names 名字级登录闸 → search job 后台标签被动嗅探)→ dida SPA 页面
7
7
  * 自身发起 SearchRealTime → parseDidaRates 结构化报价。
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 崩溃注入探针(session-zone-ledger-tests 的子进程,P4-2 验收「kill -9 中途 =
3
+ * 要么全有要么全无」):在**同一事务**里追加两条分区事件,然后按 mode 决定命运。
4
+ *
5
+ * pre-commit 全部 INSERT 已执行、事务尚未提交时 SIGKILL 自己 → 账本必须零新行
6
+ * committed 同样两次追加,正常提交后 exit 0 → 账本必须有且仅有两行
7
+ *
8
+ * 用法:npx tsx scripts/session-zone-crash.ts <stateRoot> pre-commit|committed
9
+ * 全离线:临时 stateRoot,零网络,零真实 LLM。
10
+ */
11
+ import { ensureLedger } from '../src/state-ledger.js';
12
+ import { appendZoneWrites, readZoneLog } from '../src/session-zone-ledger.js';
13
+ const [root, mode] = process.argv.slice(2);
14
+ if (!root || (mode !== 'pre-commit' && mode !== 'committed')) {
15
+ console.error('用法:npx tsx scripts/session-zone-crash.ts <stateRoot> pre-commit|committed');
16
+ process.exit(1);
17
+ }
18
+ const TS = '2026-10-04T08:00:00.000Z';
19
+ const SESSION = 'sess-crash-probe';
20
+ const ledger = ensureLedger(root);
21
+ const results = appendZoneWrites(ledger, [
22
+ {
23
+ op: 'capture',
24
+ input: {
25
+ session_ref: SESSION,
26
+ tier: 'intent',
27
+ kind: 'destination',
28
+ payload: { city: '大理' },
29
+ evidence_ref: { session_ref: SESSION, turn: 1 },
30
+ ts: TS,
31
+ },
32
+ },
33
+ {
34
+ op: 'capture',
35
+ input: {
36
+ session_ref: SESSION,
37
+ tier: 'resource',
38
+ kind: 'price_band',
39
+ payload: { low: 400, high: 900 },
40
+ evidence_ref: { session_ref: SESSION, turn: 2 },
41
+ ts: TS,
42
+ },
43
+ },
44
+ ], mode === 'pre-commit'
45
+ ? { beforeCommit: () => process.kill(process.pid, 'SIGKILL') }
46
+ : {});
47
+ // pre-commit 模式永远到不了这里(上一行已被 SIGKILL)
48
+ const appended = results.filter(r => r.ok && r.appended).length;
49
+ console.log(`committed appended=${appended} folded=${Object.keys(readZoneLog(ledger).state.hot).length}`);