@danceiny/gotry 0.0.1-rc.15 → 0.0.1-rc.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -13
- package/README.zh-CN.md +26 -12
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +357 -8
- package/bin/gotry-inner.js +442 -60
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +50 -0
- package/bin/gotry.js +1 -1
- package/cordis.gotry-patch.yml +15 -1
- package/data/airline-airports.json +39 -0
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +1 -1
- package/dist/capabilities/effect.js +293 -0
- package/dist/capabilities/fact-log.js +41 -0
- package/dist/capabilities/flyai.js +20 -6
- package/dist/capabilities/hbcli.js +43 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/model-override.js +18 -0
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +90 -0
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +2 -1
- package/dist/capabilities/session/extension-bridge.js +358 -0
- package/dist/capabilities/session/extension-channel.js +93 -0
- package/dist/capabilities/session/extension-distribution.js +234 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/golden-score.js +92 -0
- package/dist/capabilities/session/health-watch.js +216 -0
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/static-flight-golden.js +137 -0
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +147 -0
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +83 -1
- package/dist/capabilities/session-search.js +111 -2
- package/dist/capabilities/weather.js +168 -46
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +102 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-planning-budget-e2e.js +227 -0
- package/dist/scripts/agent-planning-budget-tests.js +173 -0
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
- package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
- package/dist/scripts/booking-saga-tests.js +1 -1
- package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
- package/dist/scripts/bootstrap-tests.js +59 -3
- package/dist/scripts/build-changelog.js +279 -0
- package/dist/scripts/changelog-tests.js +165 -0
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
- package/dist/scripts/dsh-runtime-closure.js +150 -0
- package/dist/scripts/effect-tests.js +474 -0
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/evaluation-cadence-tests.js +347 -0
- package/dist/scripts/evaluation-contract-tests.js +574 -0
- package/dist/scripts/extension-distribution-cli.js +35 -0
- package/dist/scripts/extension-distribution-tests.js +341 -0
- package/dist/scripts/extension-tests.js +573 -0
- package/dist/scripts/fact-gate-tests.js +318 -0
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +180 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +51 -0
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/model-override-e2e.js +176 -0
- package/dist/scripts/nightly-evidence-tests.js +67 -4
- package/dist/scripts/nightly-evidence.js +59 -3
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +186 -0
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
- package/dist/scripts/price-drift-tests.js +441 -0
- package/dist/scripts/price-drift-watch.js +491 -0
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +40 -4
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +70 -20
- package/dist/scripts/sf-live-benchmark.js +338 -0
- package/dist/scripts/sf-live-cli-tests.js +21 -0
- package/dist/scripts/sf-soft-score-tests.js +108 -0
- package/dist/scripts/sf-summary.js +93 -0
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke-session-gate-tests.js +29 -0
- package/dist/scripts/smoke.js +154 -33
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/static-golden-tests.js +299 -0
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +694 -44
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +310 -0
- package/dist/src/benchmark-agent-conformance.js +370 -0
- package/dist/src/benchmark-environment-bridge.js +384 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
- package/dist/src/benchmark-tool-isolation.js +124 -0
- package/dist/src/bookable-facts.js +349 -0
- package/dist/src/booking-saga.js +1 -1
- package/dist/src/booking-surface/availability-policy-v2.js +830 -0
- package/dist/src/booking-surface/canonical-schema.js +113 -0
- package/dist/src/booking-surface/contracts-v2.js +89 -0
- package/dist/src/booking-surface/contracts.js +46 -0
- package/dist/src/booking-surface/dsh-planner.js +453 -0
- package/dist/src/booking-surface/dsh-plugin.js +93 -0
- package/dist/src/booking-surface/error-codes.js +94 -0
- package/dist/src/booking-surface/index.js +15 -0
- package/dist/src/booking-surface/profile.js +68 -0
- package/dist/src/booking-surface/runtime-v2.js +1771 -0
- package/dist/src/booking-surface/runtime.js +351 -0
- package/dist/src/booking-surface/server-v2.js +334 -0
- package/dist/src/booking-surface/server.js +302 -0
- package/dist/src/booking-surface/startup.js +159 -0
- package/dist/src/booking-surface/validation-v2.js +319 -0
- package/dist/src/booking-surface/validation.js +809 -0
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +1 -1
- package/dist/src/engine.js +1 -1
- package/dist/src/evaluation-cadence.js +234 -0
- package/dist/src/evaluation-contracts.js +906 -0
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +234 -60
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +14 -7
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +2 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-budget.js +136 -0
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +50 -0
- package/extension/background.js +160 -0
- package/extension/content-bridge.js +41 -0
- package/extension/content-main.js +62 -0
- package/extension/manifest.json +32 -0
- package/package.json +291 -12
- package/schemas/booking.surface.v1.schema.json +927 -0
- package/schemas/booking.surface.v2.schema.json +61 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai.ts +16 -3
- package/ts/capabilities/hbcli.ts +46 -0
- package/ts/capabilities/session/benchmark.ts +4 -1
- package/ts/capabilities/session/extension-bridge.ts +377 -0
- package/ts/capabilities/session/extension-channel.ts +95 -0
- package/ts/capabilities/session/extension-distribution.ts +264 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +257 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-login.ts +63 -2
- package/ts/capabilities/session-search.ts +105 -3
- package/ts/capabilities/weather.ts +141 -52
- package/ts/package.json +3 -3
- package/ts/src/artifact-gate.ts +365 -0
- package/ts/src/benchmark-agent-conformance.ts +448 -0
- package/ts/src/benchmark-environment-bridge.ts +348 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +557 -0
- package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
- package/ts/src/booking-surface/canonical-schema.js +113 -0
- package/ts/src/booking-surface/contracts-v2.ts +118 -0
- package/ts/src/booking-surface/contracts.ts +380 -0
- package/ts/src/booking-surface/dsh-planner.ts +452 -0
- package/ts/src/booking-surface/dsh-plugin.js +93 -0
- package/ts/src/booking-surface/error-codes.ts +101 -0
- package/ts/src/booking-surface/index.ts +12 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/runtime-v2.ts +1466 -0
- package/ts/src/booking-surface/runtime.ts +483 -0
- package/ts/src/booking-surface/server-v2.ts +247 -0
- package/ts/src/booking-surface/server.ts +324 -0
- package/ts/src/booking-surface/startup.ts +196 -0
- package/ts/src/booking-surface/validation-v2.ts +205 -0
- package/ts/src/booking-surface/validation.ts +453 -0
- package/ts/src/index.ts +161 -14
- package/ts/src/state-ledger.ts +1 -0
- package/ts/src/tool-budget.ts +165 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BOOKING_READ_ACTION_KINDS,
|
|
3
|
+
BOOKING_RECEIPT_STATUSES,
|
|
4
|
+
BOOKING_SURFACES,
|
|
5
|
+
BOOKING_SURFACE_EVENT_KINDS,
|
|
6
|
+
type ActionReceiptV1,
|
|
7
|
+
type BookingCopilotIngressTurnV1,
|
|
8
|
+
type BookingCopilotTurnV1,
|
|
9
|
+
type BookingReadActionKindV1,
|
|
10
|
+
type BookingReadActionV1,
|
|
11
|
+
type BookingSurfaceEventV1,
|
|
12
|
+
type BookingWorkspaceIngressSnapshotV1,
|
|
13
|
+
type BookingWorkspaceSnapshotV1,
|
|
14
|
+
} from './contracts.ts'
|
|
15
|
+
import { Ajv2020, type ErrorObject, type ValidateFunction } from 'ajv/dist/2020.js'
|
|
16
|
+
import { readFileSync } from 'node:fs'
|
|
17
|
+
|
|
18
|
+
const CANONICAL_SCHEMA = JSON.parse(readFileSync(
|
|
19
|
+
new URL('../../../schemas/booking.surface.v1.schema.json', import.meta.url),
|
|
20
|
+
'utf8',
|
|
21
|
+
)) as Record<string, unknown>
|
|
22
|
+
const CANONICAL_SCHEMA_ID = 'https://gotry.dev/schemas/booking.surface.v1.schema.json'
|
|
23
|
+
const canonicalAjv = new Ajv2020({ allErrors: true, strict: true })
|
|
24
|
+
canonicalAjv.addSchema(CANONICAL_SCHEMA)
|
|
25
|
+
const validateCanonicalAction = canonicalAjv.compile({
|
|
26
|
+
$ref: `${CANONICAL_SCHEMA_ID}#/$defs/BookingReadActionV1`,
|
|
27
|
+
}) as ValidateFunction
|
|
28
|
+
const validateCanonicalReceipt = canonicalAjv.compile({
|
|
29
|
+
$ref: `${CANONICAL_SCHEMA_ID}#/$defs/ActionReceiptV1`,
|
|
30
|
+
}) as ValidateFunction
|
|
31
|
+
const validateCanonicalWorkspace = canonicalAjv.compile({ $ref: `${CANONICAL_SCHEMA_ID}#/$defs/BookingWorkspaceSnapshotV1` }) as ValidateFunction
|
|
32
|
+
const validateCanonicalIngressTurn = canonicalAjv.compile({ $ref: `${CANONICAL_SCHEMA_ID}#/$defs/BookingCopilotIngressTurnV1` }) as ValidateFunction
|
|
33
|
+
const validateCanonicalTurn = canonicalAjv.compile({ $ref: `${CANONICAL_SCHEMA_ID}#/$defs/BookingCopilotTurnV1` }) as ValidateFunction
|
|
34
|
+
|
|
35
|
+
export type BookingSurfaceValidationResult =
|
|
36
|
+
| { ok: true }
|
|
37
|
+
| { ok: false; errors: string[] }
|
|
38
|
+
|
|
39
|
+
const ACTION_KINDS = new Set<string>(BOOKING_READ_ACTION_KINDS)
|
|
40
|
+
const RECEIPT_STATUSES = new Set<string>(BOOKING_RECEIPT_STATUSES)
|
|
41
|
+
const SURFACES = new Set<string>(BOOKING_SURFACES)
|
|
42
|
+
const EVENT_KINDS = new Set<string>(BOOKING_SURFACE_EVENT_KINDS)
|
|
43
|
+
|
|
44
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
45
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
49
|
+
return typeof value === 'string' && value.length > 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isRevision(value: unknown): value is number {
|
|
53
|
+
return Number.isInteger(value) && Number(value) >= 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isStringArray(value: unknown): value is string[] {
|
|
57
|
+
return Array.isArray(value) && value.every(isNonEmptyString)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isCount(value: unknown): value is number {
|
|
61
|
+
return Number.isInteger(value) && Number(value) >= 0
|
|
62
|
+
}
|
|
63
|
+
function isSafeCode(value: unknown): value is string { return typeof value === 'string' && /^[a-z][a-z0-9._-]*$/.test(value) }
|
|
64
|
+
|
|
65
|
+
function receiptContainsUnsafeOpaqueText(value: unknown): boolean {
|
|
66
|
+
if (typeof value === 'string') return /[^\s@]+@[^\s@]+\.[^\s@]+|Bearer\s+[^\s]+|\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/i.test(value)
|
|
67
|
+
if (Array.isArray(value)) return value.some(receiptContainsUnsafeOpaqueText)
|
|
68
|
+
if (isRecord(value)) return Object.values(value).some(receiptContainsUnsafeOpaqueText)
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function exactKeys(
|
|
73
|
+
value: Record<string, unknown>,
|
|
74
|
+
allowed: readonly string[],
|
|
75
|
+
required: readonly string[],
|
|
76
|
+
at: string,
|
|
77
|
+
errors: string[],
|
|
78
|
+
): void {
|
|
79
|
+
const allow = new Set(allowed)
|
|
80
|
+
for (const key of Object.keys(value)) if (!allow.has(key)) errors.push(`${at}.${key}: additional property is forbidden`)
|
|
81
|
+
for (const key of required) if (!Object.hasOwn(value, key)) errors.push(`${at}.${key}: required`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateIngressWorkspace(value: unknown, at: string, errors: string[]): value is BookingWorkspaceIngressSnapshotV1 {
|
|
85
|
+
if (!isRecord(value)) { errors.push(`${at}: object required`); return false }
|
|
86
|
+
exactKeys(value, [
|
|
87
|
+
'schemaVersion', 'revision', 'locale', 'currency', 'searchDraft', 'results',
|
|
88
|
+
'visibleHotels', 'loadedOffers', 'focusedHotelRef', 'shortlistedOfferRefs',
|
|
89
|
+
'selectedOfferRef', 'verifiedOfferRef',
|
|
90
|
+
], [
|
|
91
|
+
'schemaVersion', 'revision', 'locale', 'currency', 'searchDraft', 'results',
|
|
92
|
+
'visibleHotels', 'loadedOffers', 'shortlistedOfferRefs',
|
|
93
|
+
], at, errors)
|
|
94
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push(`${at}.schemaVersion: booking.surface.v1 required`)
|
|
95
|
+
if (!isRevision(value.revision)) errors.push(`${at}.revision: non-negative integer required`)
|
|
96
|
+
if (!isNonEmptyString(value.locale)) errors.push(`${at}.locale: non-empty string required`)
|
|
97
|
+
if (typeof value.currency !== 'string' || !/^[A-Z]{3}$/.test(value.currency)) errors.push(`${at}.currency: ISO-4217 code required`)
|
|
98
|
+
if (!isRecord(value.searchDraft)) errors.push(`${at}.searchDraft: object required`)
|
|
99
|
+
if (!isRecord(value.results)) errors.push(`${at}.results: object required`)
|
|
100
|
+
if (!Array.isArray(value.visibleHotels)) errors.push(`${at}.visibleHotels: array required`)
|
|
101
|
+
if (!Array.isArray(value.loadedOffers)) errors.push(`${at}.loadedOffers: array required`)
|
|
102
|
+
if (!isStringArray(value.shortlistedOfferRefs)) errors.push(`${at}.shortlistedOfferRefs: opaque-ref array required`)
|
|
103
|
+
return errors.length === 0
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateWorkspace(value: unknown, at: string, errors: string[]): value is BookingWorkspaceSnapshotV1 {
|
|
107
|
+
if (!isRecord(value)) { errors.push(`${at}: object required`); return false }
|
|
108
|
+
const {
|
|
109
|
+
contextRef: _contextRef,
|
|
110
|
+
surface: _surface,
|
|
111
|
+
capabilities: _capabilities,
|
|
112
|
+
...ingressShape
|
|
113
|
+
} = value
|
|
114
|
+
validateIngressWorkspace(ingressShape, at, errors)
|
|
115
|
+
const allowed = [
|
|
116
|
+
'schemaVersion', 'contextRef', 'surface', 'revision', 'locale', 'currency',
|
|
117
|
+
'searchDraft', 'results', 'visibleHotels', 'loadedOffers', 'focusedHotelRef',
|
|
118
|
+
'shortlistedOfferRefs', 'selectedOfferRef', 'verifiedOfferRef', 'capabilities',
|
|
119
|
+
]
|
|
120
|
+
exactKeys(value, allowed, [
|
|
121
|
+
'schemaVersion', 'contextRef', 'surface', 'revision', 'locale', 'currency',
|
|
122
|
+
'searchDraft', 'results', 'visibleHotels', 'loadedOffers', 'shortlistedOfferRefs',
|
|
123
|
+
'capabilities',
|
|
124
|
+
], at, errors)
|
|
125
|
+
if (!isNonEmptyString(value.contextRef)) errors.push(`${at}.contextRef: server-minted opaque ref required`)
|
|
126
|
+
if (typeof value.surface !== 'string' || !SURFACES.has(value.surface)) errors.push(`${at}.surface: unsupported surface`)
|
|
127
|
+
if (!isRecord(value.capabilities)) {
|
|
128
|
+
errors.push(`${at}.capabilities: object required`)
|
|
129
|
+
} else {
|
|
130
|
+
exactKeys(value.capabilities, ['surface', 'allowedActions'], ['surface', 'allowedActions'], `${at}.capabilities`, errors)
|
|
131
|
+
if (value.capabilities.surface !== value.surface) errors.push(`${at}.capabilities.surface: must equal workspace.surface`)
|
|
132
|
+
const actionKinds = value.capabilities.allowedActions
|
|
133
|
+
if (!Array.isArray(actionKinds) || !actionKinds.every((kind) => typeof kind === 'string' && ACTION_KINDS.has(kind))) {
|
|
134
|
+
errors.push(`${at}.capabilities.allowedActions: closed action-kind array required`)
|
|
135
|
+
} else if (new Set(actionKinds).size !== actionKinds.length) {
|
|
136
|
+
errors.push(`${at}.capabilities.allowedActions: duplicates forbidden`)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
appendCanonicalErrors(validateCanonicalWorkspace, value, at, errors)
|
|
140
|
+
return errors.length === 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ACTION_INPUT_KEYS: Record<BookingReadActionKindV1, { allowed: readonly string[]; required: readonly string[] }> = {
|
|
144
|
+
'search.patch': { allowed: ['patch'], required: ['patch'] },
|
|
145
|
+
'search.run': { allowed: [], required: [] },
|
|
146
|
+
'results.view.patch': { allowed: ['patch'], required: ['patch'] },
|
|
147
|
+
'hotel.focus': { allowed: ['hotelRef'], required: ['hotelRef'] },
|
|
148
|
+
'hotel.select': { allowed: ['hotelRef'], required: ['hotelRef'] },
|
|
149
|
+
'offers.query': { allowed: ['hotelRefs', 'criteria'], required: ['hotelRefs', 'criteria'] },
|
|
150
|
+
'offers.view.patch': { allowed: ['hotelRef', 'criteria'], required: ['hotelRef', 'criteria'] },
|
|
151
|
+
'offers.compare': { allowed: ['offerRefs', 'requestedCount'], required: ['offerRefs', 'requestedCount'] },
|
|
152
|
+
'offer.select': { allowed: ['offerRef'], required: ['offerRef'] },
|
|
153
|
+
'offer.check': { allowed: ['offerRef'], required: ['offerRef'] },
|
|
154
|
+
'checkout.prepare': { allowed: ['offerRef', 'verifiedOfferRef'], required: ['offerRef', 'verifiedOfferRef'] },
|
|
155
|
+
'order.observe': { allowed: ['orderRef'], required: ['orderRef'] },
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function collectBookingReadActionErrors(value: unknown, at: string): string[] {
|
|
159
|
+
const errors: string[] = []
|
|
160
|
+
if (!isRecord(value)) return [`${at}: typed action object required; text is never executable`]
|
|
161
|
+
exactKeys(value, [
|
|
162
|
+
'schemaVersion', 'kind', 'actionId', 'contextRef', 'expectedRevision',
|
|
163
|
+
'reason', 'factRefs', 'input',
|
|
164
|
+
], [
|
|
165
|
+
'schemaVersion', 'kind', 'actionId', 'contextRef', 'expectedRevision',
|
|
166
|
+
'reason', 'factRefs', 'input',
|
|
167
|
+
], at, errors)
|
|
168
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push(`${at}.schemaVersion: booking.surface.v1 required`)
|
|
169
|
+
if (typeof value.kind !== 'string' || !ACTION_KINDS.has(value.kind)) {
|
|
170
|
+
errors.push(`${at}.kind: unregistered read action`)
|
|
171
|
+
}
|
|
172
|
+
if (!isNonEmptyString(value.actionId)) errors.push(`${at}.actionId: opaque ref required`)
|
|
173
|
+
if (!isNonEmptyString(value.contextRef)) errors.push(`${at}.contextRef: opaque ref required`)
|
|
174
|
+
if (!isRevision(value.expectedRevision)) errors.push(`${at}.expectedRevision: non-negative integer required`)
|
|
175
|
+
if (!isNonEmptyString(value.reason)) errors.push(`${at}.reason: non-empty explanation required`)
|
|
176
|
+
if (!isStringArray(value.factRefs)) errors.push(`${at}.factRefs: opaque-ref array required`)
|
|
177
|
+
if (!isRecord(value.input)) {
|
|
178
|
+
errors.push(`${at}.input: typed object required`)
|
|
179
|
+
} else if (typeof value.kind === 'string' && ACTION_KINDS.has(value.kind)) {
|
|
180
|
+
const spec = ACTION_INPUT_KEYS[value.kind as BookingReadActionKindV1]
|
|
181
|
+
exactKeys(value.input, spec.allowed, spec.required, `${at}.input`, errors)
|
|
182
|
+
}
|
|
183
|
+
if (!validateCanonicalAction(value)) {
|
|
184
|
+
for (const error of validateCanonicalAction.errors ?? []) {
|
|
185
|
+
errors.push(canonicalError(error, at))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return errors
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function canonicalError(error: ErrorObject, at: string): string {
|
|
192
|
+
const path = error.instancePath ? error.instancePath.replaceAll('/', '.') : ''
|
|
193
|
+
const property = error.keyword === 'additionalProperties'
|
|
194
|
+
? `.${String((error.params as { additionalProperty?: unknown }).additionalProperty ?? '')}`
|
|
195
|
+
: ''
|
|
196
|
+
return `${at}${path}${property}: ${error.message ?? error.keyword}`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function appendCanonicalErrors(validate: ValidateFunction, value: unknown, at: string, errors: string[]): void {
|
|
200
|
+
if (!validate(value)) for (const error of validate.errors ?? []) errors.push(canonicalError(error, at))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function validateBookingReadActionV1(value: unknown): BookingSurfaceValidationResult {
|
|
204
|
+
const errors = collectBookingReadActionErrors(value, '$')
|
|
205
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function collectReceiptErrors(value: unknown, at: string): string[] {
|
|
209
|
+
const errors: string[] = []
|
|
210
|
+
if (!isRecord(value)) return [`${at}: typed receipt object required`]
|
|
211
|
+
exactKeys(value, [
|
|
212
|
+
'schemaVersion', 'kind', 'actionId', 'contextRef', 'status', 'revision',
|
|
213
|
+
'observation', 'resultContract', 'undoToken',
|
|
214
|
+
], [
|
|
215
|
+
'schemaVersion', 'kind', 'actionId', 'contextRef', 'status', 'revision',
|
|
216
|
+
'observation', 'resultContract',
|
|
217
|
+
], at, errors)
|
|
218
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push(`${at}.schemaVersion: booking.surface.v1 required`)
|
|
219
|
+
if (value.kind !== 'action.receipt') errors.push(`${at}.kind: action.receipt required`)
|
|
220
|
+
if (!isNonEmptyString(value.actionId)) errors.push(`${at}.actionId: opaque ref required`)
|
|
221
|
+
if (!isNonEmptyString(value.contextRef)) errors.push(`${at}.contextRef: opaque ref required`)
|
|
222
|
+
if (typeof value.status !== 'string' || !RECEIPT_STATUSES.has(value.status)) errors.push(`${at}.status: unregistered receipt status`)
|
|
223
|
+
if (!isRevision(value.revision)) errors.push(`${at}.revision: non-negative integer required`)
|
|
224
|
+
if (value.undoToken !== undefined && !isNonEmptyString(value.undoToken)) errors.push(`${at}.undoToken: opaque ref required`)
|
|
225
|
+
validateActionObservation(value.observation, `${at}.observation`, errors)
|
|
226
|
+
validateResultContract(value.resultContract, `${at}.resultContract`, errors)
|
|
227
|
+
return errors
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function validateActionObservation(value: unknown, at: string, errors: string[]): void {
|
|
231
|
+
if (!isRecord(value)) {
|
|
232
|
+
errors.push(`${at}: discriminated object required`)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
switch (value.kind) {
|
|
237
|
+
case 'search.state':
|
|
238
|
+
exactKeys(value, ['kind', 'searchSessionRef', 'resultCount'], ['kind'], at, errors)
|
|
239
|
+
if (value.searchSessionRef !== undefined && !isNonEmptyString(value.searchSessionRef)) errors.push(`${at}.searchSessionRef: opaque ref required`)
|
|
240
|
+
if (value.resultCount !== undefined && !isCount(value.resultCount)) errors.push(`${at}.resultCount: non-negative integer required`)
|
|
241
|
+
return
|
|
242
|
+
case 'results.state':
|
|
243
|
+
exactKeys(value, ['kind', 'matchedHotelRefs', 'visibleCount'], ['kind', 'matchedHotelRefs', 'visibleCount'], at, errors)
|
|
244
|
+
if (!isStringArray(value.matchedHotelRefs)) errors.push(`${at}.matchedHotelRefs: opaque-ref array required`)
|
|
245
|
+
if (!isCount(value.visibleCount)) errors.push(`${at}.visibleCount: non-negative integer required`)
|
|
246
|
+
return
|
|
247
|
+
case 'hotel.focus':
|
|
248
|
+
exactKeys(value, ['kind', 'hotelRef'], ['kind', 'hotelRef'], at, errors)
|
|
249
|
+
if (!isNonEmptyString(value.hotelRef)) errors.push(`${at}.hotelRef: opaque ref required`)
|
|
250
|
+
return
|
|
251
|
+
case 'hotel.selection':
|
|
252
|
+
exactKeys(value, ['kind', 'hotelRef'], ['kind', 'hotelRef'], at, errors)
|
|
253
|
+
if (!isNonEmptyString(value.hotelRef)) errors.push(`${at}.hotelRef: opaque ref required`)
|
|
254
|
+
return
|
|
255
|
+
case 'offers.state':
|
|
256
|
+
exactKeys(value, ['kind', 'hotelRefs', 'offerRefs', 'loadedHotelCount'], ['kind', 'hotelRefs', 'offerRefs', 'loadedHotelCount'], at, errors)
|
|
257
|
+
if (!isStringArray(value.hotelRefs)) errors.push(`${at}.hotelRefs: opaque-ref array required`)
|
|
258
|
+
if (!isStringArray(value.offerRefs)) errors.push(`${at}.offerRefs: opaque-ref array required`)
|
|
259
|
+
if (!isCount(value.loadedHotelCount)) errors.push(`${at}.loadedHotelCount: non-negative integer required`)
|
|
260
|
+
return
|
|
261
|
+
case 'offer.selection':
|
|
262
|
+
exactKeys(value, ['kind', 'offerRef'], ['kind', 'offerRef'], at, errors)
|
|
263
|
+
if (!isNonEmptyString(value.offerRef)) errors.push(`${at}.offerRef: opaque ref required`)
|
|
264
|
+
return
|
|
265
|
+
case 'offer.availability':
|
|
266
|
+
exactKeys(value, ['kind', 'offerRef', 'verifiedOfferRef', 'available', 'changedFactRefs'], ['kind', 'offerRef', 'available', 'changedFactRefs'], at, errors)
|
|
267
|
+
if (!isNonEmptyString(value.offerRef)) errors.push(`${at}.offerRef: opaque ref required`)
|
|
268
|
+
if (value.verifiedOfferRef !== undefined && !isNonEmptyString(value.verifiedOfferRef)) errors.push(`${at}.verifiedOfferRef: opaque ref required`)
|
|
269
|
+
if (typeof value.available !== 'boolean') errors.push(`${at}.available: boolean required`)
|
|
270
|
+
if (!isStringArray(value.changedFactRefs)) errors.push(`${at}.changedFactRefs: opaque-ref array required`)
|
|
271
|
+
return
|
|
272
|
+
case 'checkout.handoff':
|
|
273
|
+
exactKeys(value, ['kind', 'offerRef', 'verifiedOfferRef', 'handoffRef'], ['kind', 'offerRef', 'verifiedOfferRef', 'handoffRef'], at, errors)
|
|
274
|
+
if (!isNonEmptyString(value.offerRef)) errors.push(`${at}.offerRef: opaque ref required`)
|
|
275
|
+
if (!isNonEmptyString(value.verifiedOfferRef)) errors.push(`${at}.verifiedOfferRef: opaque ref required`)
|
|
276
|
+
if (!isNonEmptyString(value.handoffRef)) errors.push(`${at}.handoffRef: opaque ref required`)
|
|
277
|
+
return
|
|
278
|
+
case 'order.state': {
|
|
279
|
+
exactKeys(value, ['kind', 'orderRef', 'state'], ['kind', 'orderRef', 'state'], at, errors)
|
|
280
|
+
const states = new Set(['pending', 'verified', 'failed', 'unknown'])
|
|
281
|
+
if (!isNonEmptyString(value.orderRef)) errors.push(`${at}.orderRef: opaque ref required`)
|
|
282
|
+
if (typeof value.state !== 'string' || !states.has(value.state)) errors.push(`${at}.state: unregistered order state`)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
case 'gap':
|
|
286
|
+
exactKeys(value, ['kind', 'code', 'factRefs'], ['kind', 'code', 'factRefs'], at, errors)
|
|
287
|
+
if (!isSafeCode(value.code)) errors.push(`${at}.code: safe identifier required`)
|
|
288
|
+
if (!isStringArray(value.factRefs)) errors.push(`${at}.factRefs: opaque-ref array required`)
|
|
289
|
+
return
|
|
290
|
+
default:
|
|
291
|
+
errors.push(`${at}.kind: unregistered observation kind`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function validateResultContract(value: unknown, at: string, errors: string[]): void {
|
|
296
|
+
if (!isRecord(value)) {
|
|
297
|
+
errors.push(`${at}: object required`)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
exactKeys(value, ['outcome', 'requestedCount', 'actualCount', 'hardCriteriaMet', 'factRefs', 'gapCodes'], ['outcome', 'hardCriteriaMet', 'factRefs', 'gapCodes'], at, errors)
|
|
301
|
+
const outcomes = new Set(['complete', 'partial', 'empty'])
|
|
302
|
+
if (typeof value.outcome !== 'string' || !outcomes.has(value.outcome)) errors.push(`${at}.outcome: complete, partial, or empty required`)
|
|
303
|
+
if (value.requestedCount !== undefined && !isCount(value.requestedCount)) errors.push(`${at}.requestedCount: non-negative integer required`)
|
|
304
|
+
if (value.actualCount !== undefined && !isCount(value.actualCount)) errors.push(`${at}.actualCount: non-negative integer required`)
|
|
305
|
+
if (typeof value.hardCriteriaMet !== 'boolean') errors.push(`${at}.hardCriteriaMet: boolean required`)
|
|
306
|
+
if (!isStringArray(value.factRefs)) errors.push(`${at}.factRefs: opaque-ref array required`)
|
|
307
|
+
if (!Array.isArray(value.gapCodes) || !value.gapCodes.every(isSafeCode)) errors.push(`${at}.gapCodes: safe identifier array required`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function validateActionReceiptV1(value: unknown): BookingSurfaceValidationResult {
|
|
311
|
+
const errors = collectReceiptErrors(value, '$')
|
|
312
|
+
if (receiptContainsUnsafeOpaqueText(value)) errors.push('$: receipt contains unsafe non-opaque text')
|
|
313
|
+
if (!validateCanonicalReceipt(value)) {
|
|
314
|
+
for (const error of validateCanonicalReceipt.errors ?? []) errors.push(canonicalError(error, '$'))
|
|
315
|
+
}
|
|
316
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function validateBookingCopilotIngressTurnV1(value: unknown): BookingSurfaceValidationResult {
|
|
320
|
+
const errors: string[] = []
|
|
321
|
+
if (!isRecord(value)) return { ok: false, errors: ['$: object required'] }
|
|
322
|
+
exactKeys(value, [
|
|
323
|
+
'schemaVersion', 'kind', 'taskId', 'contextRef', 'surfaceHint', 'workspace', 'request',
|
|
324
|
+
], ['schemaVersion', 'kind', 'surfaceHint', 'workspace', 'request'], '$', errors)
|
|
325
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push('$.schemaVersion: booking.surface.v1 required')
|
|
326
|
+
if (value.kind !== 'user.turn.ingress') errors.push('$.kind: user.turn.ingress required')
|
|
327
|
+
if (value.contextRef !== undefined && value.contextRef !== null) errors.push('$.contextRef: bootstrap may only omit it or send null')
|
|
328
|
+
if (typeof value.surfaceHint !== 'string' || !SURFACES.has(value.surfaceHint)) errors.push('$.surfaceHint: unsupported surface hint')
|
|
329
|
+
validateIngressWorkspace(value.workspace, '$.workspace', errors)
|
|
330
|
+
if (!isRecord(value.request)) {
|
|
331
|
+
errors.push('$.request: object required')
|
|
332
|
+
} else {
|
|
333
|
+
exactKeys(value.request, ['text'], ['text'], '$.request', errors)
|
|
334
|
+
if (!isNonEmptyString(value.request.text)) errors.push('$.request.text: non-empty string required')
|
|
335
|
+
}
|
|
336
|
+
appendCanonicalErrors(validateCanonicalIngressTurn, value, '$', errors)
|
|
337
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function validateBookingCopilotTurnV1(value: unknown): BookingSurfaceValidationResult {
|
|
341
|
+
const errors: string[] = []
|
|
342
|
+
if (!isRecord(value)) return { ok: false, errors: ['$: object required'] }
|
|
343
|
+
if (value.kind === 'user.turn') {
|
|
344
|
+
exactKeys(value, ['schemaVersion', 'kind', 'taskId', 'workspace', 'request'], ['schemaVersion', 'kind', 'workspace', 'request'], '$', errors)
|
|
345
|
+
if (!isRecord(value.request)) errors.push('$.request: object required')
|
|
346
|
+
else {
|
|
347
|
+
exactKeys(value.request, ['text'], ['text'], '$.request', errors)
|
|
348
|
+
if (!isNonEmptyString(value.request.text)) errors.push('$.request.text: non-empty string required')
|
|
349
|
+
}
|
|
350
|
+
} else if (value.kind === 'action.receipt.continuation') {
|
|
351
|
+
exactKeys(value, ['schemaVersion', 'kind', 'taskId', 'workspace', 'receipt'], ['schemaVersion', 'kind', 'taskId', 'workspace', 'receipt'], '$', errors)
|
|
352
|
+
if (!isNonEmptyString(value.taskId)) errors.push('$.taskId: opaque ref required')
|
|
353
|
+
errors.push(...collectReceiptErrors(value.receipt, '$.receipt'))
|
|
354
|
+
const receiptValidation = validateActionReceiptV1(value.receipt)
|
|
355
|
+
if (!receiptValidation.ok) errors.push(...receiptValidation.errors.map((error) => `$.receipt${error.slice(1)}`))
|
|
356
|
+
} else {
|
|
357
|
+
errors.push('$.kind: user.turn or action.receipt.continuation required')
|
|
358
|
+
}
|
|
359
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push('$.schemaVersion: booking.surface.v1 required')
|
|
360
|
+
validateWorkspace(value.workspace, '$.workspace', errors)
|
|
361
|
+
if (value.kind === 'action.receipt.continuation' && isRecord(value.workspace) && isRecord(value.receipt)) {
|
|
362
|
+
if (value.receipt.contextRef !== value.workspace.contextRef) errors.push('$.receipt.contextRef: must equal workspace.contextRef')
|
|
363
|
+
if (value.receipt.revision !== value.workspace.revision) errors.push('$.receipt.revision: must equal workspace.revision')
|
|
364
|
+
}
|
|
365
|
+
appendCanonicalErrors(validateCanonicalTurn, value, '$', errors)
|
|
366
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function validateBookingSurfaceEventV1(value: unknown): BookingSurfaceValidationResult {
|
|
370
|
+
const errors: string[] = []
|
|
371
|
+
if (!isRecord(value)) return { ok: false, errors: ['$: typed event object required'] }
|
|
372
|
+
const common = ['schemaVersion', 'eventId', 'taskId', 'contextRef', 'sequence', 'emittedAt', 'kind'] as const
|
|
373
|
+
const branchKeys: Record<string, string> = {
|
|
374
|
+
status: 'status',
|
|
375
|
+
question: 'question',
|
|
376
|
+
operation: 'action',
|
|
377
|
+
explanation: 'explanation',
|
|
378
|
+
terminal: 'terminal',
|
|
379
|
+
error: 'error',
|
|
380
|
+
}
|
|
381
|
+
const branchKey = typeof value.kind === 'string' ? branchKeys[value.kind] : undefined
|
|
382
|
+
exactKeys(value, [...common, ...(branchKey ? [branchKey] : [])], [...common, ...(branchKey ? [branchKey] : [])], '$', errors)
|
|
383
|
+
if (value.schemaVersion !== 'booking.surface.v1') errors.push('$.schemaVersion: booking.surface.v1 required')
|
|
384
|
+
if (!isNonEmptyString(value.eventId)) errors.push('$.eventId: opaque ref required')
|
|
385
|
+
if (!isNonEmptyString(value.taskId)) errors.push('$.taskId: opaque ref required')
|
|
386
|
+
if (!isNonEmptyString(value.contextRef)) errors.push('$.contextRef: server-minted opaque ref required')
|
|
387
|
+
if (!Number.isInteger(value.sequence) || Number(value.sequence) < 1) errors.push('$.sequence: positive integer required')
|
|
388
|
+
if (!isNonEmptyString(value.emittedAt)) errors.push('$.emittedAt: timestamp required')
|
|
389
|
+
if (typeof value.kind !== 'string' || !EVENT_KINDS.has(value.kind)) errors.push('$.kind: unregistered event kind')
|
|
390
|
+
if (value.kind === 'status') {
|
|
391
|
+
const statuses = new Set(['submitted', 'working', 'waiting_receipt', 'input_required'])
|
|
392
|
+
if (typeof value.status !== 'string' || !statuses.has(value.status)) errors.push('$.status: unregistered task status')
|
|
393
|
+
}
|
|
394
|
+
if (value.kind === 'question') {
|
|
395
|
+
if (!isRecord(value.question)) {
|
|
396
|
+
errors.push('$.question: typed object required')
|
|
397
|
+
} else {
|
|
398
|
+
exactKeys(value.question, ['questionId', 'prompt', 'missingFields'], ['questionId', 'prompt', 'missingFields'], '$.question', errors)
|
|
399
|
+
if (!isNonEmptyString(value.question.questionId)) errors.push('$.question.questionId: opaque ref required')
|
|
400
|
+
if (!isNonEmptyString(value.question.prompt)) errors.push('$.question.prompt: non-empty string required')
|
|
401
|
+
if (!isStringArray(value.question.missingFields)) errors.push('$.question.missingFields: string array required')
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (value.kind === 'operation') errors.push(...collectBookingReadActionErrors(value.action, '$.action'))
|
|
405
|
+
if (value.kind === 'explanation') {
|
|
406
|
+
if (!isRecord(value.explanation)) {
|
|
407
|
+
errors.push('$.explanation: typed object required')
|
|
408
|
+
} else {
|
|
409
|
+
exactKeys(value.explanation, ['text', 'factRefs'], ['text', 'factRefs'], '$.explanation', errors)
|
|
410
|
+
if (!isNonEmptyString(value.explanation.text)) errors.push('$.explanation.text: non-empty string required')
|
|
411
|
+
if (!isStringArray(value.explanation.factRefs)) errors.push('$.explanation.factRefs: opaque-ref array required')
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (value.kind === 'terminal') {
|
|
415
|
+
if (!isRecord(value.terminal)) {
|
|
416
|
+
errors.push('$.terminal: typed object required')
|
|
417
|
+
} else {
|
|
418
|
+
exactKeys(value.terminal, ['status', 'summary', 'factRefs'], ['status', 'summary', 'factRefs'], '$.terminal', errors)
|
|
419
|
+
const statuses = new Set(['completed', 'stopped'])
|
|
420
|
+
if (typeof value.terminal.status !== 'string' || !statuses.has(value.terminal.status)) errors.push('$.terminal.status: completed or stopped required')
|
|
421
|
+
if (!isNonEmptyString(value.terminal.summary)) errors.push('$.terminal.summary: non-empty string required')
|
|
422
|
+
if (!isStringArray(value.terminal.factRefs)) errors.push('$.terminal.factRefs: opaque-ref array required')
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (value.kind === 'error') {
|
|
426
|
+
if (!isRecord(value.error)) {
|
|
427
|
+
errors.push('$.error: typed object required')
|
|
428
|
+
} else {
|
|
429
|
+
exactKeys(value.error, ['code', 'message', 'retryable'], ['code', 'message', 'retryable'], '$.error', errors)
|
|
430
|
+
if (!isNonEmptyString(value.error.code)) errors.push('$.error.code: non-empty string required')
|
|
431
|
+
if (!isNonEmptyString(value.error.message)) errors.push('$.error.message: non-empty string required')
|
|
432
|
+
if (typeof value.error.retryable !== 'boolean') errors.push('$.error.retryable: boolean required')
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* The only executable event seam. It performs no text parsing: non-operation
|
|
440
|
+
* events always return null even when their prose happens to look like JSON.
|
|
441
|
+
*/
|
|
442
|
+
export function bookingOperationFromEvent(value: unknown): BookingReadActionV1 | null {
|
|
443
|
+
if (!isRecord(value) || value.kind !== 'operation') return null
|
|
444
|
+
if (!validateBookingSurfaceEventV1(value).ok) return null
|
|
445
|
+
return value.action as BookingReadActionV1
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Keep the public predicates assignable to their declared wire types.
|
|
449
|
+
void (null as unknown as BookingCopilotIngressTurnV1)
|
|
450
|
+
void (null as unknown as BookingCopilotTurnV1)
|
|
451
|
+
void (null as unknown as BookingReadActionV1)
|
|
452
|
+
void (null as unknown as ActionReceiptV1)
|
|
453
|
+
void (null as unknown as BookingSurfaceEventV1)
|