@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
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Outcome ↔ planning-estimate projection contract tests (issue #340 contract slice,
3
+ * SIMULATED-TRIGGER DRILL — fully offline, zero network, zero subprocess, zero CLI).
4
+ *
5
+ * ⚠ Evidence label: `simulated_trigger_drill` / `fixture_contract`. The external
6
+ * trigger of #340 is SIMULATED here by replaying the hotelbyte fake-CLI outcome
7
+ * vocabulary as fixtures (read-only reuse of ts/scripts/hotelbyte-spawn-e2e-tests.ts
8
+ * and ts/capabilities/hotelbyte-transaction.ts). That is NOT the real trigger: no
9
+ * real order, refund authority record or WriteGate admission exists, so nothing
10
+ * below counts as real-order evidence and #340 stays open and default-off.
11
+ *
12
+ * Sections:
13
+ * A. Association key (plan estimate ↔ immutable quote ↔ attempt ↔ ledger intent):
14
+ * four mandatory keys, deterministic derivation, each omission fail-closed.
15
+ * B. Status closed set + settlement semantics: `unknown` is never success and
16
+ * never comparable; the LOCAL ledger word `compensated` is never an outcome.
17
+ * C. Transition table: open→open / open→terminal admitted; terminal→open refused
18
+ * (regression); terminal→unreachable terminal refused (conflict); same→same
19
+ * idempotent; the frozen progression DAG pinned member by member.
20
+ * D. Append-only ingest: fixture vocabulary mapping, idempotent replay, key
21
+ * reuse with a different payload refused, LATE terminal accepted and flagged,
22
+ * clock sanity, negative list (sensitive fields + credential value shapes),
23
+ * refunded without authority refused, cancel serviceFee as refund refused.
24
+ * E. Revocation + deterministic fold: nothing mutated or deleted, re-fold == read.
25
+ * F. Deviation: confirmed only; unknown/pending → `not_comparable` and explicitly
26
+ * NOT zero; mixed valuation instant / currency refused; cross-currency works
27
+ * only through real FX evidence pinned to ONE instant.
28
+ * G. Calibration: indeterminate counted but never valued, thin sample → neutral,
29
+ * bounds clamped, ranking reorder allowed; hard-budget falsification — a
30
+ * calibrated amount can never flip the user's budget verdict.
31
+ * H. Ingestion seam: trigger frozen false, registry frozen empty, fetch-spy
32
+ * zero-call proof (zero supplier records consulted before the trigger).
33
+ *
34
+ * Run (from ts/): npx tsx scripts/outcome-projection-tests.ts
35
+ */
36
+ import { OUTCOME_CALIBRATION_SCHEMA, OUTCOME_EVIDENCE_ALLOWED_KEYS, OUTCOME_PROJECTION_SCHEMA, OUTCOME_STATUSES, OUTCOME_TRIGGER_FIRED, OPEN_OUTCOME_STATUSES, TERMINAL_OUTCOME_STATUSES, TERMINAL_PROGRESSIONS, CALIBRATION_MIN_SAMPLE, CALIBRATION_MODIFIER_PPM_MAX, CALIBRATION_MODIFIER_PPM_MIN, CALIBRATION_MODIFIER_PPM_NEUTRAL, LIVE_SUPPLIER_OUTCOME_SOURCES, MAX_REASON_CHARS, appendOutcome, applyCalibrationToRankingScore, associationKey, authoritativeTotal, calibratedDisplayEstimate, calibrationFromDeviations, comparableForDeviation, countsAsSettledSuccess, deviationOf, emptyProjection, foldProjection, hardBudgetVerdict, ingestSupplierOutcome, isOutcomeStatus, outcomeEvidenceViolation, revokeEntry, statusTerminality, transitionAllowed, } from '../src/outcome-projection.js';
37
+ import { REFUND_AUTHORITY_SOURCES, SERVICE_FEE_SOURCE, } from '../src/booking-surface/cancel-refund-commission.js';
38
+ import { buildFxFact, convertMoney, parseCurrency, parseMoney, pinNativeMoney, } from '../capabilities/fx-contract.js';
39
+ let passed = 0;
40
+ function check(cond, msg) {
41
+ if (cond) {
42
+ passed++;
43
+ console.log(` ok - ${msg}`);
44
+ }
45
+ else {
46
+ console.error(` FAIL - ${msg}`);
47
+ process.exitCode = 1;
48
+ }
49
+ }
50
+ /** Negative assertion: must be a typed refusal with the expected code and evidence. */
51
+ function expectRefusal(result, code, needle, msg) {
52
+ if (result.ok) {
53
+ check(false, `${msg} (expected refusal ${code}, got ok)`);
54
+ return;
55
+ }
56
+ check(result.code === code && result.detail.includes(needle), `${msg} (refused ${result.code}${result.code === code ? '' : ` ≠ ${code}`}, evidence contains "${needle}": ${result.detail.includes(needle)})`);
57
+ }
58
+ const T_QUOTE = '2026-10-04T09:00:00.000Z';
59
+ const T_BOOK = '2026-10-04T09:05:00.000Z';
60
+ const T_PROBE_1 = '2026-10-04T09:08:00.000Z';
61
+ const T_PROBE_2 = '2026-10-04T09:20:00.000Z';
62
+ const T_LATE = '2026-10-04T09:06:00.000Z'; // predates T_PROBE_1: a LATE supplier observation
63
+ const T_NOW = '2026-10-04T10:00:00.000Z';
64
+ function freshProjection() {
65
+ const key = associationKey({
66
+ planEstimateId: 'plan-est-7a1',
67
+ quoteId: 'quote-immutable-33c',
68
+ attemptId: 'attempt-9f2',
69
+ intentIdemKey: 'intent-idem-0001',
70
+ });
71
+ if (!key.ok)
72
+ throw new Error(`fixture association key failed: ${key.detail}`);
73
+ return emptyProjection(key.value);
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // A. Association key
77
+ // ---------------------------------------------------------------------------
78
+ console.log('A. association key: plan estimate ↔ immutable quote ↔ attempt ↔ ledger intent');
79
+ const keyA = associationKey({ planEstimateId: 'p1', quoteId: 'q1', attemptId: 'a1', intentIdemKey: 'i1' });
80
+ check(keyA.ok && keyA.value.projectionKey.length > 0, 'four complete keys admitted and a projection key derived');
81
+ const keyAagain = associationKey({ planEstimateId: ' p1 ', quoteId: 'q1', attemptId: 'a1', intentIdemKey: 'i1' });
82
+ check(keyA.ok && keyAagain.ok && keyA.value.projectionKey === keyAagain.value.projectionKey, 'derivation is deterministic and whitespace-normalized');
83
+ const keyOtherQuote = associationKey({ planEstimateId: 'p1', quoteId: 'q2', attemptId: 'a1', intentIdemKey: 'i1' });
84
+ check(keyA.ok && keyOtherQuote.ok && keyA.value.projectionKey !== keyOtherQuote.value.projectionKey, 'a different immutable quote yields a different association (quotes are not interchangeable)');
85
+ expectRefusal(associationKey({ planEstimateId: '', quoteId: 'q1', attemptId: 'a1', intentIdemKey: 'i1' }), 'binding_incomplete', 'planEstimateId', 'missing planEstimateId refused');
86
+ expectRefusal(associationKey({ planEstimateId: 'p1', quoteId: undefined, attemptId: 'a1', intentIdemKey: 'i1' }), 'binding_incomplete', 'quoteId', 'missing quoteId refused');
87
+ expectRefusal(associationKey({ planEstimateId: 'p1', quoteId: 'q1', attemptId: ' ', intentIdemKey: 'i1' }), 'binding_incomplete', 'attemptId', 'blank attemptId refused');
88
+ expectRefusal(associationKey({ planEstimateId: 'p1', quoteId: 'q1', attemptId: 'a1', intentIdemKey: 42 }), 'binding_incomplete', 'intentIdemKey', 'non-string intentIdemKey refused');
89
+ expectRefusal(associationKey({ planEstimateId: 'p1', quoteId: 'q1', attemptId: 'a1', intentIdemKey: '' }), 'binding_incomplete', 'no default, no derivation from a sibling key', 'refusal states that no key is defaulted or derived');
90
+ // ---------------------------------------------------------------------------
91
+ // B. Status closed set + settlement semantics
92
+ // ---------------------------------------------------------------------------
93
+ console.log('B. status closed set: unknown is never a sale, `compensated` is never an outcome');
94
+ check(OUTCOME_STATUSES.length === 6 && OUTCOME_STATUSES.join('/') === 'pending/confirmed/failed/cancelled/refunded/unknown', 'alphabet frozen: pending/confirmed/failed/cancelled/refunded/unknown');
95
+ check(OPEN_OUTCOME_STATUSES.length + TERMINAL_OUTCOME_STATUSES.length === OUTCOME_STATUSES.length, 'open ∪ terminal partitions the alphabet exactly');
96
+ check(!isOutcomeStatus('compensated') && !isOutcomeStatus('reconciled_success') && !isOutcomeStatus('success'), 'local-ledger / reconciliation / raw-CLI words are not outcome statuses');
97
+ check(countsAsSettledSuccess('confirmed'), 'confirmed is the only settled success');
98
+ check(!countsAsSettledSuccess('unknown') && !countsAsSettledSuccess('pending'), 'unknown and pending are never settled success');
99
+ check(!countsAsSettledSuccess('refunded') && !countsAsSettledSuccess('cancelled') && !countsAsSettledSuccess('failed'), 'no other terminal is a settled success');
100
+ check(comparableForDeviation('confirmed') && !comparableForDeviation('unknown') && !comparableForDeviation('pending'), 'only confirmed is deviation-comparable');
101
+ check(statusTerminality('unknown') === 'open' && statusTerminality('pending') === 'open' && statusTerminality('refunded') === 'terminal', 'terminality classification');
102
+ // ---------------------------------------------------------------------------
103
+ // C. Transition table
104
+ // ---------------------------------------------------------------------------
105
+ console.log('C. transitions: terminal states never regress');
106
+ check(transitionAllowed('unknown', 'pending').ok, 'open → open admitted (a probe may refine without settling)');
107
+ check(transitionAllowed('pending', 'unknown').ok, 'open → open admitted in both directions (a probe failure returns to unknown)');
108
+ for (const terminal of TERMINAL_OUTCOME_STATUSES) {
109
+ check(transitionAllowed('unknown', terminal).ok, `open unknown → terminal ${terminal} admitted (late settlement)`);
110
+ for (const open of OPEN_OUTCOME_STATUSES) {
111
+ expectRefusal(transitionAllowed(terminal, open), 'terminal_regression', 'must not regress', `terminal ${terminal} → open ${open} refused`);
112
+ }
113
+ const r = transitionAllowed(terminal, terminal);
114
+ check(r.ok && r.value === 'idempotent', `terminal ${terminal} re-observed is idempotent, not a conflict`);
115
+ }
116
+ check(TERMINAL_PROGRESSIONS.confirmed.join('/') === 'cancelled/refunded' && TERMINAL_PROGRESSIONS.cancelled.join('/') === 'refunded', 'frozen progression DAG: confirmed→{cancelled,refunded}, cancelled→{refunded}');
117
+ check(TERMINAL_PROGRESSIONS.failed.length === 0 && TERMINAL_PROGRESSIONS.refunded.length === 0, 'failed and refunded are absorbing');
118
+ check(transitionAllowed('confirmed', 'cancelled').ok && transitionAllowed('cancelled', 'refunded').ok, 'admitted terminal progressions');
119
+ expectRefusal(transitionAllowed('failed', 'confirmed'), 'terminal_conflict', 'does not reach terminal confirmed', 'failed → confirmed refused (no silent flip)');
120
+ expectRefusal(transitionAllowed('cancelled', 'confirmed'), 'terminal_conflict', 'does not reach terminal confirmed', 'cancelled → confirmed refused');
121
+ expectRefusal(transitionAllowed('refunded', 'cancelled'), 'terminal_conflict', 'reachable: [none]', 'refunded → cancelled refused (absorbing terminal)');
122
+ expectRefusal(transitionAllowed('confirmed', 'failed'), 'terminal_conflict', 'does not reach terminal failed', 'confirmed → failed refused');
123
+ expectRefusal(transitionAllowed('confirmed', 'bogus'), 'closed_set', 'must be in', 'a word outside the alphabet refused');
124
+ // ---------------------------------------------------------------------------
125
+ // D. Append-only ingest, driven by the hotelbyte fake-CLI fixture vocabulary
126
+ // ---------------------------------------------------------------------------
127
+ console.log('D. append-only ingest (fixture vocabulary from the hotelbyte fake CLI)');
128
+ /**
129
+ * Fixture adapter (test-only; the product has no such adapter): the fake-CLI /
130
+ * query-orders words observed in ts/scripts/hotelbyte-spawn-e2e-tests.ts and the
131
+ * SUPPLIER_TERMINAL_NEGATIVE set of ts/capabilities/hotelbyte-transaction.ts,
132
+ * mapped to the #340 alphabet. Every indeterminate word maps to `unknown`.
133
+ */
134
+ const FIXTURE_CLI_VOCABULARY = {
135
+ // book stdout words
136
+ success_with_binding: 'confirmed',
137
+ pending: 'pending',
138
+ failed: 'failed',
139
+ success_without_binding: 'unknown', // exit0 success but no customerReferenceNo
140
+ reference_mismatch: 'unknown',
141
+ exit0_garbage: 'unknown',
142
+ nonzero_exit: 'unknown',
143
+ timeout: 'unknown',
144
+ process_killed: 'unknown',
145
+ // query-orders supplierStatus words
146
+ confirmed: 'confirmed',
147
+ completed: 'confirmed',
148
+ processing: 'pending',
149
+ cancelled: 'cancelled',
150
+ auto_canceled: 'cancelled',
151
+ expired: 'cancelled',
152
+ query_miss: 'unknown',
153
+ query_failed: 'unknown',
154
+ query_malformed: 'unknown',
155
+ permission_denied: 'unknown',
156
+ };
157
+ const INDETERMINATE_FIXTURE_WORDS = [
158
+ 'success_without_binding', 'reference_mismatch', 'exit0_garbage', 'nonzero_exit',
159
+ 'timeout', 'process_killed', 'query_miss', 'query_failed', 'query_malformed', 'permission_denied',
160
+ ];
161
+ check(INDETERMINATE_FIXTURE_WORDS.every(w => FIXTURE_CLI_VOCABULARY[w] === 'unknown'), `all ${INDETERMINATE_FIXTURE_WORDS.length} indeterminate fixture words map to unknown (exit0 is not a success proof)`);
162
+ check(Object.values(FIXTURE_CLI_VOCABULARY).every(isOutcomeStatus), 'the fixture vocabulary maps only into the frozen alphabet');
163
+ const EVIDENCE_OK = { supplier_status_word: 'success', supplier_order_ref_digest: 'sha16:aa11bb22', probe_digest: 'sha256:deadbeef' };
164
+ const p0 = freshProjection();
165
+ // The host probe at T_PROBE_1 timed out → unknown. The supplier's own confirmation
166
+ // instant (T_LATE) predates that probe and will arrive afterwards: the LATE terminal.
167
+ const step1 = appendOutcome(p0, { idemKey: 'obs-1', status: 'unknown', observedAt: T_PROBE_1, evidence: { classification: 'timeout' } }, T_NOW);
168
+ check(step1.ok && step1.value.appended && step1.value.projection.entries.length === 1, 'first observation (timeout → unknown) appended');
169
+ check(p0.entries.length === 0, 'the input projection is not mutated (pure, append-only returns a new value)');
170
+ const afterUnknown = step1.ok ? step1.value.projection : p0;
171
+ const replay = appendOutcome(afterUnknown, { idemKey: 'obs-1', status: 'unknown', observedAt: T_PROBE_1, evidence: { classification: 'timeout' } }, T_NOW);
172
+ check(replay.ok && replay.value.appended === false && replay.value.transition === 'idempotent' && replay.value.projection.entries.length === 1, 'replay of the same idempotency key with the same payload is a no-op (zero new entries)');
173
+ expectRefusal(appendOutcome(afterUnknown, { idemKey: 'obs-1', status: 'confirmed', observedAt: T_BOOK, evidence: EVIDENCE_OK }, T_NOW), 'idempotency_conflict', 'is a conflict, never an overwrite', 'the same idempotency key with a different payload is a conflict');
174
+ const lateTerminal = appendOutcome(afterUnknown, { idemKey: 'obs-2', status: 'confirmed', observedAt: T_LATE, amount: { money: parseMoney('CNY', '880.00'), source: 'supplier_final_charge' }, evidence: EVIDENCE_OK }, T_NOW);
175
+ check(lateTerminal.ok && lateTerminal.value.appended && lateTerminal.value.lateObservation === true, 'a LATE terminal observation (observed_at predating the last probe) is accepted and flagged');
176
+ const confirmedProjection = lateTerminal.ok ? lateTerminal.value.projection : afterUnknown;
177
+ check(foldProjection(confirmedProjection).status === 'confirmed' && foldProjection(confirmedProjection).settledSuccess, 'fold settles on the late terminal');
178
+ expectRefusal(appendOutcome(confirmedProjection, { idemKey: 'obs-3', status: 'unknown', observedAt: T_PROBE_2, evidence: { classification: 'query_failed' } }, T_NOW), 'terminal_regression', 'a later probe failure is not evidence', 'after a terminal, a later unknown probe cannot regress the projection');
179
+ expectRefusal(appendOutcome(confirmedProjection, { idemKey: 'obs-4', status: 'failed', observedAt: T_PROBE_2, evidence: EVIDENCE_OK }, T_NOW), 'terminal_conflict', 'does not reach terminal failed', 'after confirmed, a contradicting failed is an explicit conflict');
180
+ expectRefusal(appendOutcome(freshProjection(), { idemKey: 'o', status: 'compensated', observedAt: T_BOOK }, T_NOW), 'local_ledger_status_not_supplier_outcome', 'never means refunded', 'the LOCAL ledger word `compensated` is refused as a supplier outcome (compensated ≠ refunded)');
181
+ expectRefusal(appendOutcome(freshProjection(), { idemKey: 'o', status: 'success', observedAt: T_BOOK }, T_NOW), 'closed_set', 'must be one of', 'the raw CLI word `success` is not an outcome status (adapters must map explicitly)');
182
+ expectRefusal(appendOutcome(freshProjection(), { idemKey: '', status: 'unknown', observedAt: T_BOOK }, T_NOW), 'binding_incomplete', 'idemKey', 'an observation without an idempotency key is refused');
183
+ expectRefusal(appendOutcome(freshProjection(), { idemKey: 'o', status: 'unknown', observedAt: '2026-10-04' }, T_NOW), 'bad_ts', 'date-only', 'a date-only observed_at is refused (observations must be orderable)');
184
+ expectRefusal(appendOutcome(freshProjection(), { idemKey: 'o', status: 'unknown', observedAt: '2026-10-04T11:00:00.000Z' }, T_NOW), 'bad_ts', 'postdates the injected clock', 'an observation postdating the injected clock is refused (impossible provenance)');
185
+ expectRefusal(appendOutcome(freshProjection(), { idemKey: 'o', status: 'unknown', observedAt: T_BOOK, reason: 'x'.repeat(MAX_REASON_CHARS + 1) }, T_NOW), 'bound_exceeded', 'a label, never a transcript', 'an over-long reason is refused (bounded text)');
186
+ console.log('D2. negative list: sensitive order fields and credentials never enter the projection');
187
+ check(outcomeEvidenceViolation(EVIDENCE_OK) === null, 'an allow-listed digest/pointer record passes');
188
+ check(OUTCOME_EVIDENCE_ALLOWED_KEYS.length === 10, 'the evidence key allow-list is a closed set of 10 digest/pointer fields');
189
+ for (const [field, label] of [['guest_name', 'guest name'], ['passport', 'passport'], ['card_number', 'card number'], ['authorization', 'authorization header'], ['raw_response', 'raw supplier response'], ['stdout', 'CLI stdout']]) {
190
+ expectRefusal(appendOutcome(freshProjection(), { idemKey: 'o', status: 'unknown', observedAt: T_BOOK, evidence: { [field]: 'whatever' } }, T_NOW), 'negative_list', `sensitive order field '${field}'`, `${label} field refused by name`);
191
+ }
192
+ expectRefusal(appendOutcome(freshProjection(), { idemKey: 'o', status: 'unknown', observedAt: T_BOOK, evidence: { hotel_name: 'Some Hotel' } }, T_NOW), 'negative_list', 'is not in the evidence allow-list', 'an unlisted key is refused even when it looks harmless (allow-list, not deny-list)');
193
+ for (const [value, label] of [
194
+ ['bearer abcdefgh12345678', 'bearer token value'],
195
+ ['sk-abcdefghijklmnop', 'secret-key value'],
196
+ ['api_key=zzzzzz', 'credential assignment value'],
197
+ ['13800138000', 'mobile number value'],
198
+ ['123456789012345678', 'document/card number value'],
199
+ ['guest@example.com', 'email value'],
200
+ ['https://supplier.example/order/1', 'URL value'],
201
+ ]) {
202
+ expectRefusal(appendOutcome(freshProjection(), { idemKey: 'o', status: 'unknown', observedAt: T_BOOK, evidence: { probe_digest: value } }, T_NOW), 'negative_list', 'in an evidence value', `${label} refused by value scan even under an allow-listed key`);
203
+ }
204
+ check(outcomeEvidenceViolation('not-an-object') !== null && outcomeEvidenceViolation([1, 2]) !== null, 'a non-object evidence payload is refused');
205
+ // A hex digest legitimately contains long runs of decimal digits a few percent of
206
+ // the time. The numeric-id patterns are anchored to a non-alphanumeric boundary so
207
+ // such a digest is NOT mistaken for a document number (unanchored, these would all
208
+ // be refused and the negative list would reject legitimate evidence).
209
+ for (const digest of [
210
+ 'a12345678901234567b', // 17-digit run inside hex
211
+ 'sha256:bb123456789012345678901234567890cc', // long run after a prefix
212
+ '0123456789012345abcdef0123456789abcdef0123456789abcdef0123456789', // 64-char digest, leading run
213
+ 'ab1380013800012345ef', // embeds a mobile-shaped run
214
+ 'sha16:00000000000000000a', // digit run + one hex letter
215
+ 'HB123456789012345678ORD', // non-hex ref: isolates the anchors
216
+ ]) {
217
+ check(outcomeEvidenceViolation({ probe_digest: digest, supplier_order_ref_digest: digest }) === null, `a hex digest with a 15+ digit run is accepted, not flagged as a document number: ${digest.slice(0, 28)}`);
218
+ const appended = appendOutcome(freshProjection(), { idemKey: 'o', status: 'unknown', observedAt: T_BOOK, evidence: { probe_digest: digest } }, T_NOW);
219
+ check(appended.ok, `the same digest is accepted by appendOutcome: ${digest.slice(0, 28)}`);
220
+ }
221
+ // Standalone identifier-shaped runs stay refused at any length, including the
222
+ // pure-decimal case that is deliberately NOT given the hex-digest exemption.
223
+ for (const [value, label] of [
224
+ ['12345678901234567', '17-digit standalone run (pure decimal: no hex exemption)'],
225
+ ['123456789012345678', '18-digit ID-card-length run'],
226
+ ['12345678901234567X', 'ID-card run with an X check digit'],
227
+ ['1234567890123456789', '19-digit card-length run'],
228
+ ['sha256:12345678901234567', 'pure-decimal run behind a digest prefix'],
229
+ ['ref 13800138000 ok', 'standalone mobile number inside prose'],
230
+ ]) {
231
+ check(outcomeEvidenceViolation({ probe_digest: value }) !== null, `a standalone identifier-shaped value is still refused: ${label}`);
232
+ }
233
+ check(outcomeEvidenceViolation({ probe_digest: 12345678901234567 }) !== null, 'a numeric (non-string) leaf carrying an identifier-shaped run is refused too');
234
+ check(outcomeEvidenceViolation({ probe_digest: ['ok', '123456789012345678'] }) !== null, 'the scan reaches identifier-shaped runs nested in an array leaf');
235
+ console.log('D3. refund money discipline (serviceFee ≠ refund; refunded needs authority)');
236
+ const confirmedThenCancelled = (() => {
237
+ const a = appendOutcome(freshProjection(), { idemKey: 'c1', status: 'confirmed', observedAt: T_BOOK, amount: { money: parseMoney('CNY', '880.00'), source: 'supplier_final_charge' }, evidence: EVIDENCE_OK }, T_NOW);
238
+ if (!a.ok)
239
+ throw new Error(a.detail);
240
+ const b = appendOutcome(a.value.projection, { idemKey: 'c2', status: 'cancelled', observedAt: T_PROBE_1, evidence: { supplier_status_word: 'cancelled' } }, T_NOW);
241
+ if (!b.ok)
242
+ throw new Error(b.detail);
243
+ return b.value.projection;
244
+ })();
245
+ expectRefusal(appendOutcome(confirmedThenCancelled, { idemKey: 'c3', status: 'refunded', observedAt: T_PROBE_2, evidence: { supplier_status_word: 'refunded' } }, T_NOW), 'evidence_required', 'refunded only from supplier/wallet refund records', 'refunded without an amount/authority is refused');
246
+ expectRefusal(appendOutcome(confirmedThenCancelled, { idemKey: 'c3', status: 'refunded', observedAt: T_PROBE_2, amount: { money: parseMoney('CNY', '50.00'), source: SERVICE_FEE_SOURCE }, evidence: { amount_source: SERVICE_FEE_SOURCE } }, T_NOW), 'service_fee_as_refund', 'never the customer refund amount', 'the cancel service fee offered as the refund amount is refused');
247
+ expectRefusal(appendOutcome(confirmedThenCancelled, { idemKey: 'c3', status: 'refunded', observedAt: T_PROBE_2, amount: { money: parseMoney('CNY', '830.00'), source: 'model_guess' }, evidence: {} }, T_NOW), 'evidence_required', REFUND_AUTHORITY_SOURCES[0], 'a refund amount from a non-authority source is refused');
248
+ const refundOk = appendOutcome(confirmedThenCancelled, { idemKey: 'c3', status: 'refunded', observedAt: T_PROBE_2, amount: { money: parseMoney('CNY', '830.00'), source: 'supplier_refund_record' }, evidence: { refund_evidence_kind: 'supplier_refund_record', refund_evidence_digest: 'sha256:cafe' } }, T_NOW);
249
+ check(refundOk.ok && foldProjection(refundOk.value.projection).status === 'refunded', 'refunded with an authoritative refund record is admitted (confirmed→cancelled→refunded)');
250
+ check(refundOk.ok && !foldProjection(refundOk.value.projection).settledSuccess, 'a refunded projection is terminal but is not a settled success');
251
+ expectRefusal(appendOutcome(freshProjection(), { idemKey: 'f1', status: 'confirmed', observedAt: T_BOOK, amount: { money: { currency: parseCurrency('CNY'), amountMinor: 880.0 }, source: 's' }, evidence: {} }, T_NOW), 'closed_set', 'floats are refused', 'a float amount is refused (fx-contract MoneyAmount only)');
252
+ // ---------------------------------------------------------------------------
253
+ // E. Revocation + deterministic fold
254
+ // ---------------------------------------------------------------------------
255
+ console.log('E. revocable projection: append-only, nothing deleted, re-fold == direct read');
256
+ const mistaken = (() => {
257
+ const a = appendOutcome(freshProjection(), { idemKey: 'm1', status: 'unknown', observedAt: T_BOOK, evidence: { classification: 'timeout' } }, T_NOW);
258
+ if (!a.ok)
259
+ throw new Error(a.detail);
260
+ const b = appendOutcome(a.value.projection, { idemKey: 'm2', status: 'confirmed', observedAt: T_PROBE_1, amount: { money: parseMoney('CNY', '880.00'), source: 'supplier_final_charge' }, evidence: EVIDENCE_OK }, T_NOW);
261
+ if (!b.ok)
262
+ throw new Error(b.detail);
263
+ return { projection: b.value.projection, confirmedEntryId: b.value.entry.entry_id };
264
+ })();
265
+ check(foldProjection(mistaken.projection).status === 'confirmed', 'before revocation the fold reads confirmed');
266
+ const revoked = revokeEntry(mistaken.projection, { targetEntryId: mistaken.confirmedEntryId, reason: 'ingested against the wrong attempt during the drill', actor: 'operator:drill', idemKey: 'rev-1' }, T_NOW);
267
+ check(revoked.ok && revoked.value.projection.entries.length === 3, 'a revocation is itself an appended entry (3 entries, nothing deleted)');
268
+ const revokedProjection = revoked.ok ? revoked.value.projection : mistaken.projection;
269
+ check(revokedProjection.entries.some(e => e.entry_id === mistaken.confirmedEntryId), 'the revoked observation is still physically present (append-only)');
270
+ const revokedView = foldProjection(revokedProjection);
271
+ check(revokedView.status === 'unknown' && revokedView.revokedEntryIds.length === 1 && !revokedView.settledSuccess, 'the fold skips the revoked observation and falls back to the prior effective status');
272
+ check(JSON.stringify(foldProjection(revokedProjection)) === JSON.stringify(revokedView), 're-folding the same projection is deterministic (replay == direct read)');
273
+ check(revokedView.foldConflicts.length === 0, 'a well-formed projection folds with zero conflicts');
274
+ expectRefusal(revokeEntry(revokedProjection, { targetEntryId: mistaken.confirmedEntryId, reason: 'again', actor: 'op', idemKey: 'rev-2' }, T_NOW), 'unknown_entry', 'already revoked', 'revoking an already-revoked entry is refused');
275
+ expectRefusal(revokeEntry(revokedProjection, { targetEntryId: 'no-such-entry', reason: 'x', actor: 'op', idemKey: 'rev-3' }, T_NOW), 'unknown_entry', 'no observed entry', 'revoking a missing entry is refused');
276
+ expectRefusal(revokeEntry(mistaken.projection, { targetEntryId: mistaken.confirmedEntryId, reason: '', actor: 'op', idemKey: 'rev-4' }, T_NOW), 'binding_incomplete', 'not auditable', 'a revocation without a stated reason is refused');
277
+ expectRefusal(revokeEntry(mistaken.projection, { targetEntryId: mistaken.confirmedEntryId, reason: 'x', actor: '', idemKey: 'rev-5' }, T_NOW), 'binding_incomplete', 'actor', 'a revocation without an actor is refused');
278
+ check(foldProjection(freshProjection()).status === null && foldProjection(freshProjection()).indeterminate, 'an empty projection has status null (never defaulted to a status) and is indeterminate');
279
+ // ---------------------------------------------------------------------------
280
+ // F. Deviation
281
+ // ---------------------------------------------------------------------------
282
+ console.log('F. deviation: one valuation instant, never a guessed FX rate, never zero for unknown');
283
+ const estimate800 = pinNativeMoney(parseMoney('CNY', '800.00'), T_QUOTE);
284
+ const actual880 = pinNativeMoney(parseMoney('CNY', '880.00'), T_QUOTE);
285
+ const basisSameInstant = { estimate: estimate800, actual: actual880 };
286
+ const dev = deviationOf(confirmedProjection, basisSameInstant);
287
+ check(dev.ok && dev.value.deltaMinor === 8000n && dev.value.direction === 'over', 'confirmed deviation: ¥880 actual vs ¥800 estimate = +8000 minor units, direction over');
288
+ check(dev.ok && dev.value.valuationAsOf === T_QUOTE && dev.value.explanation.includes('valuation instant'), 'the deviation carries the single declared valuation instant and an explanation');
289
+ check(dev.ok && dev.value.basisEntryIds.length > 0, 'the deviation names the effective entries it was computed from');
290
+ const unknownProjection = (() => {
291
+ const a = appendOutcome(freshProjection(), { idemKey: 'u1', status: 'unknown', observedAt: T_BOOK, evidence: { classification: 'timeout' } }, T_NOW);
292
+ if (!a.ok)
293
+ throw new Error(a.detail);
294
+ return a.value.projection;
295
+ })();
296
+ const devUnknown = deviationOf(unknownProjection, basisSameInstant);
297
+ expectRefusal(devUnknown, 'not_comparable', 'never a zero deviation', 'unknown (timeout) is refused, NOT reported as zero deviation');
298
+ check(!devUnknown.ok, 'the unknown refusal returns no numeric value at all (no 0 to accidentally average)');
299
+ const pendingProjection = (() => {
300
+ const a = appendOutcome(freshProjection(), { idemKey: 'pd1', status: 'pending', observedAt: T_BOOK, evidence: { supplier_status_word: 'pending' } }, T_NOW);
301
+ if (!a.ok)
302
+ throw new Error(a.detail);
303
+ return a.value.projection;
304
+ })();
305
+ expectRefusal(deviationOf(pendingProjection, basisSameInstant), 'not_comparable', 'never a zero deviation', 'pending is refused, not zero');
306
+ expectRefusal(deviationOf(freshProjection(), basisSameInstant), 'not_comparable', 'absence of an outcome is not a zero deviation', 'an empty projection is refused');
307
+ expectRefusal(deviationOf(confirmedThenCancelled, basisSameInstant), 'not_comparable', 'aftercare surface', 'a cancelled outcome has no comparable final charge (aftercare belongs to #233)');
308
+ const actualOtherInstant = pinNativeMoney(parseMoney('CNY', '880.00'), T_PROBE_2);
309
+ expectRefusal(deviationOf(confirmedProjection, { estimate: estimate800, actual: actualOtherInstant }), 'mixed_valuation_basis', 'valuation instant', 'estimate and actual pinned to different instants are refused (no silent re-pinning)');
310
+ const TEST_FX_REGISTRY = [
311
+ { id: 'fixture-offline-table', tier: 'primary', legal_basis: 'test-only offline fixture (issue #340 drill; no live FX source)' },
312
+ ];
313
+ const usdCnyAtQuote = buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.2', provider: 'fixture-offline-table', provenance: '[汇率:fixture@#340 drill]', as_of: T_QUOTE, fetched_at: T_QUOTE }, TEST_FX_REGISTRY);
314
+ const actualUsd = convertMoney(parseMoney('USD', '120.00'), 'CNY', usdCnyAtQuote);
315
+ const devCross = deviationOf(confirmedProjection, { estimate: estimate800, actual: actualUsd });
316
+ check(devCross.ok && devCross.value.actualMinor === 86400n && devCross.value.deltaMinor === 6400n, 'a cross-currency actual is comparable only through real FX evidence pinned to the same instant (USD 120 × 7.2 = ¥864)');
317
+ const usdCnyLater = buildFxFact({ base: 'USD', quote: 'CNY', rate: '7.3', provider: 'fixture-offline-table', provenance: '[汇率:fixture@#340 drill]', as_of: T_PROBE_2, fetched_at: T_PROBE_2 }, TEST_FX_REGISTRY);
318
+ expectRefusal(deviationOf(confirmedProjection, { estimate: estimate800, actual: convertMoney(parseMoney('USD', '120.00'), 'CNY', usdCnyLater) }), 'mixed_valuation_basis', 'valuation instant', 'an FX fact from another instant cannot be mixed into the comparison (no guessed rate, no re-pinning)');
319
+ // ---------------------------------------------------------------------------
320
+ // G. Calibration + hard budget falsification
321
+ // ---------------------------------------------------------------------------
322
+ console.log('G. calibration: ranking/future estimates only — never the user hard budget');
323
+ function confirmedAt(suffix, actualDecimal) {
324
+ const key = associationKey({ planEstimateId: `plan-${suffix}`, quoteId: `quote-${suffix}`, attemptId: `attempt-${suffix}`, intentIdemKey: `intent-${suffix}` });
325
+ if (!key.ok)
326
+ throw new Error(key.detail);
327
+ const a = appendOutcome(emptyProjection(key.value), { idemKey: `obs-${suffix}`, status: 'confirmed', observedAt: T_BOOK, amount: { money: parseMoney('CNY', actualDecimal), source: 'supplier_final_charge' }, evidence: EVIDENCE_OK }, T_NOW);
328
+ if (!a.ok)
329
+ throw new Error(a.detail);
330
+ return { projection: a.value.projection, basis: { estimate: estimate800, actual: pinNativeMoney(parseMoney('CNY', actualDecimal), T_QUOTE) } };
331
+ }
332
+ const thin = calibrationFromDeviations([confirmedAt('s1', '880.00')]);
333
+ check(thin.ok && thin.value.modifierPpm === CALIBRATION_MODIFIER_PPM_NEUTRAL && thin.value.sampleCount === 1, `a thin sample (1 < ${CALIBRATION_MIN_SAMPLE}) yields the neutral modifier ×1.0`);
334
+ check(thin.ok && thin.value.explanation.includes('a thin sample is not evidence'), 'the neutral advice states why it is neutral');
335
+ const withIndeterminate = calibrationFromDeviations([
336
+ confirmedAt('s1', '880.00'), confirmedAt('s2', '880.00'), confirmedAt('s3', '880.00'),
337
+ { projection: unknownProjection, basis: basisSameInstant },
338
+ { projection: pendingProjection, basis: basisSameInstant },
339
+ { projection: freshProjection() },
340
+ ]);
341
+ check(withIndeterminate.ok && withIndeterminate.value.sampleCount === 3 && withIndeterminate.value.indeterminateCount === 3, 'indeterminate projections are COUNTED (3) but never valued — the sample stays 3');
342
+ check(withIndeterminate.ok && withIndeterminate.value.modifierPpm === 1_100_000, 'the modifier reflects only the comparable samples (2640/2400 = ×1.1), unaffected by the indeterminate ones');
343
+ check(withIndeterminate.ok && withIndeterminate.value.schema === OUTCOME_CALIBRATION_SCHEMA && withIndeterminate.value.appliesTo === 'ranking_and_future_estimate_display', 'advice is versioned and names its only application surface');
344
+ check(withIndeterminate.ok && withIndeterminate.value.admissibleForHardBudget === false, 'advice advertises admissibleForHardBudget = false');
345
+ check(withIndeterminate.ok && withIndeterminate.value.basisProjectionKeys.length === 3 && withIndeterminate.value.explanation.includes('indeterminate counted separately'), 'advice is explainable: basis keys + an indeterminate count in the explanation');
346
+ const extreme = calibrationFromDeviations([confirmedAt('x1', '400.00'), confirmedAt('x2', '400.00'), confirmedAt('x3', '400.00')]);
347
+ check(extreme.ok && extreme.value.modifierPpm === CALIBRATION_MODIFIER_PPM_MIN && extreme.value.explanation.includes('clamped'), 'an extreme ratio is clamped to the declared lower bound and the clamp is disclosed');
348
+ const extremeHigh = calibrationFromDeviations([confirmedAt('y1', '2400.00'), confirmedAt('y2', '2400.00'), confirmedAt('y3', '2400.00')]);
349
+ check(extremeHigh.ok && extremeHigh.value.modifierPpm === CALIBRATION_MODIFIER_PPM_MAX, 'an extreme ratio is clamped to the declared upper bound');
350
+ const cheapAdvice = extreme.ok ? extreme.value : null;
351
+ check(cheapAdvice !== null, 'a deflating (×0.8) advice is available for the falsification below');
352
+ if (cheapAdvice) {
353
+ const scoreA = applyCalibrationToRankingScore(0.9, cheapAdvice);
354
+ const scoreB = applyCalibrationToRankingScore(0.8, cheapAdvice);
355
+ check(scoreA.ok && scoreB.ok && scoreA.value > scoreB.value, 'the modifier applies to a ranking score monotonically (reordering is its legitimate effect)');
356
+ expectRefusal(applyCalibrationToRankingScore(Number.NaN, cheapAdvice), 'closed_set', 'must be finite', 'a non-finite ranking score is refused');
357
+ expectRefusal(applyCalibrationToRankingScore(1, { ...cheapAdvice, modifierPpm: 10 }), 'bound_exceeded', 'outside the declared bounds', 'a modifier outside the declared bounds is refused (bounds are structural, not advisory)');
358
+ // --- falsification: calibration must never flip the hard budget verdict ---
359
+ const overBudgetTotal = pinNativeMoney(parseMoney('CNY', '1200.00'), T_QUOTE);
360
+ const budget1000 = pinNativeMoney(parseMoney('CNY', '1000.00'), T_QUOTE);
361
+ const authoritative = authoritativeTotal(overBudgetTotal, 'immutable_quote');
362
+ check(authoritative.ok, 'an immutable-quote total is budget-admissible');
363
+ const rawVerdict = authoritative.ok ? hardBudgetVerdict(authoritative.value, budget1000) : null;
364
+ check(rawVerdict !== null && rawVerdict.ok && rawVerdict.value.withinBudget === false, 'the raw authoritative total ¥1200 is over the ¥1000 hard budget');
365
+ const display = calibratedDisplayEstimate(parseMoney('CNY', '1200.00'), cheapAdvice);
366
+ check(display.ok && display.value.amountMinor === 96000n && display.value.admissibleForHardBudget === false, 'the ×0.8 calibrated DISPLAY amount is ¥960 — which would look within budget');
367
+ if (display.ok) {
368
+ const smuggled = { authoritativeSource: 'immutable_quote', amount: { ...overBudgetTotal, amountMinor: display.value.amountMinor }, admissibleForHardBudget: false };
369
+ expectRefusal(hardBudgetVerdict(smuggled, budget1000), 'hard_budget_guard', 'can never override the user hard budget', 'FALSIFICATION: a calibrated amount smuggled into the hard budget verdict is refused');
370
+ }
371
+ const modeled = authoritativeTotal(pinNativeMoney(parseMoney('CNY', '960.00'), T_QUOTE), 'calibrated_estimate');
372
+ expectRefusal(modeled, 'hard_budget_guard', 'never budget-admissible', 'a calibrated/modeled source can never become an AuthoritativeTotal');
373
+ const unlabelled = { amount: overBudgetTotal };
374
+ expectRefusal(hardBudgetVerdict(unlabelled, budget1000), 'hard_budget_guard', 'admits only an AuthoritativeTotal', 'an unlabelled total is refused by the hard budget verdict');
375
+ const mixedBudget = pinNativeMoney(parseMoney('CNY', '1000.00'), T_PROBE_2);
376
+ if (authoritative.ok) {
377
+ expectRefusal(hardBudgetVerdict(authoritative.value, mixedBudget), 'mixed_valuation_basis', 'one valuation instant', 'a budget pinned to another instant is refused');
378
+ }
379
+ // ranking may reorder but the budget-filtered set is unchanged
380
+ const candidates = [
381
+ { id: 'c-over', total: parseMoney('CNY', '1200.00'), semantic: 0.95 },
382
+ { id: 'c-under', total: parseMoney('CNY', '900.00'), semantic: 0.60 },
383
+ ];
384
+ const admitted = candidates.filter(c => {
385
+ const t = authoritativeTotal(pinNativeMoney(c.total, T_QUOTE), 'immutable_quote');
386
+ if (!t.ok)
387
+ return false;
388
+ const v = hardBudgetVerdict(t.value, budget1000);
389
+ return v.ok && v.value.withinBudget;
390
+ });
391
+ check(admitted.length === 1 && admitted[0].id === 'c-under', 'the hard budget admits exactly the under-budget candidate, computed from authoritative totals only');
392
+ const reranked = [...candidates].sort((a, b) => {
393
+ const sa = applyCalibrationToRankingScore(a.semantic, cheapAdvice);
394
+ const sb = applyCalibrationToRankingScore(b.semantic, cheapAdvice);
395
+ return (sb.ok ? sb.value : 0) - (sa.ok ? sa.value : 0);
396
+ });
397
+ check(reranked.length === candidates.length, 'calibrated reranking preserves the candidate count (it reorders, it does not filter)');
398
+ const admittedAfter = reranked.filter(c => admitted.some(a => a.id === c.id));
399
+ check(admittedAfter.length === 1 && admittedAfter[0].id === 'c-under', 'after calibrated reranking the budget-admitted set is byte-identical (calibration cannot admit an over-budget plan)');
400
+ }
401
+ // ---------------------------------------------------------------------------
402
+ // H. Ingestion seam
403
+ // ---------------------------------------------------------------------------
404
+ console.log('H. real-order ingestion seam: trigger frozen false, zero supplier records consulted');
405
+ check(OUTCOME_TRIGGER_FIRED === false, '#340 trigger gate frozen false');
406
+ check(LIVE_SUPPLIER_OUTCOME_SOURCES.length === 0, 'the supplier outcome source registry is frozen empty');
407
+ check(OUTCOME_PROJECTION_SCHEMA === 'gotry_outcome_projection.v1', 'schema id is versioned');
408
+ const deferred = await ingestSupplierOutcome({ attemptId: 'attempt-9f2', customerReferenceNo: 'ref-1' });
409
+ expectRefusal(deferred, 'trigger_deferred', 'zero supplier outcome sources consulted', 'before the trigger, ingestion refuses structurally');
410
+ check(!deferred.ok && deferred.detail.includes('fixtures, model self-reports and booking attempts do not count'), 'the refusal restates the #340 启动条件 (fixtures/self-reports/attempts are not a sale)');
411
+ let spyCalls = 0;
412
+ const spySource = {
413
+ id: 'spy-fixture-source',
414
+ authorization_basis: 'test-only spy (no real authorization exists)',
415
+ fetchOutcome: async () => {
416
+ spyCalls++;
417
+ return { ok: true, observation: { idemKey: 'spy', status: 'confirmed', observedAt: T_BOOK } };
418
+ },
419
+ };
420
+ const refusedWithSources = await ingestSupplierOutcome({ attemptId: 'a', customerReferenceNo: 'r' }, { sources: [spySource], triggerFired: false });
421
+ expectRefusal(refusedWithSources, 'trigger_deferred', 'trigger has not fired', 'even with sources supplied, the pre-trigger refusal happens before any read');
422
+ check(spyCalls === 0, 'fetch spy zero-call proof: zero supplier records consulted before the trigger fired');
423
+ const noSource = await ingestSupplierOutcome({ attemptId: 'a', customerReferenceNo: 'r' }, { sources: [], triggerFired: true });
424
+ expectRefusal(noSource, 'binding_incomplete', 'authorization_basis', 'trigger on + empty registry: no source is invented');
425
+ const viaSpy = await ingestSupplierOutcome({ attemptId: 'a', customerReferenceNo: 'r' }, { sources: [spySource], triggerFired: true });
426
+ check(viaSpy.ok && spyCalls === 1, 'the seam data shape is usable once a source is explicitly admitted (drill only)');
427
+ console.log(`\nOUTCOME PROJECTION CONTRACT TESTS (#340, simulated_trigger_drill / fixture_contract): ${passed} pass${process.exitCode ? ', FAIL' : ' (all green)'}`);