@capsiynau/intelligence-contracts 0.1.1 → 0.1.2
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 +15 -3
- package/SCHEMA.md +214 -0
- package/package.json +5 -3
- package/src/errors.js +7 -0
- package/src/execution.js +546 -0
- package/src/index.js +1 -0
- package/types/index.d.ts +193 -0
package/README.md
CHANGED
|
@@ -22,9 +22,21 @@ in the platform.
|
|
|
22
22
|
|
|
23
23
|
## The additive rule
|
|
24
24
|
|
|
25
|
-
Within a major version, a status, code, scope or
|
|
26
|
-
never removed or repurposed. `scripts/check-contract-additive.mjs`
|
|
27
|
-
build on a removal, checked against `contract-baseline.json`.
|
|
25
|
+
Within a major version, a status, code, scope, capability or record field may
|
|
26
|
+
be **added**, never removed or repurposed. `scripts/check-contract-additive.mjs`
|
|
27
|
+
fails the build on a removal, checked against `contract-baseline.json`.
|
|
28
|
+
|
|
29
|
+
## Records
|
|
30
|
+
|
|
31
|
+
The execution record (AIOG's audit account of one execution) and the external
|
|
32
|
+
execution report (a product-side fallback reported to AIOG) are defined in
|
|
33
|
+
`src/execution.js` and documented field by field in [SCHEMA.md](./SCHEMA.md).
|
|
34
|
+
Both are contract only as of 18.09.2026: nothing writes them yet. Every
|
|
35
|
+
jurisdiction in them carries its source, so "unknown" is recorded rather than
|
|
36
|
+
left out. An execution is identified at four separate layers - route
|
|
37
|
+
(`provider`), execution platform (`platform`), model (`model_id`,
|
|
38
|
+
`model_version`) and runtime/worker (`runtime_version`, `placement`) - and a
|
|
39
|
+
platform is neither a language nor a jurisdiction; see SCHEMA.md.
|
|
28
40
|
|
|
29
41
|
## Usage
|
|
30
42
|
|
package/SCHEMA.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# @capsiynau/intelligence-contracts: record schemas
|
|
2
|
+
|
|
3
|
+
Shapes that are records rather than messages. Today that is the execution
|
|
4
|
+
record and the external execution report, both added on 18.09.2026 (remediation
|
|
5
|
+
plan stage 0.5). Source: `src/execution.js`. Types: `types/index.d.ts`.
|
|
6
|
+
|
|
7
|
+
## The rules both shapes follow
|
|
8
|
+
|
|
9
|
+
- **Contract only, for now.** Nothing in the platform writes either shape yet.
|
|
10
|
+
Populating the execution record is stage 0.2; the endpoint that accepts an
|
|
11
|
+
external report comes after that. The existing table,
|
|
12
|
+
`aiog.intelligence_api_execution_records`, is unchanged.
|
|
13
|
+
- **Every key is present.** A value that is not known is `null`, never an
|
|
14
|
+
omitted key. A record that omits a field looks complete; one that says `null`
|
|
15
|
+
is honestly incomplete.
|
|
16
|
+
- **Unknown is recorded as unknown.** Every jurisdiction and every serving
|
|
17
|
+
region is paired with its source: `unknown`, `declared-config` (an operator
|
|
18
|
+
set it, on the provider's written attestation) or `provider-attested` (the
|
|
19
|
+
provider's own response said so). `null` goes with `unknown` and only with
|
|
20
|
+
`unknown`. Nothing is inferred from a provider's name, endpoint host, model
|
|
21
|
+
name or publishing country, and a `null` jurisdiction never satisfies
|
|
22
|
+
`uk_only`.
|
|
23
|
+
- **Additive.** `record_version` and `report_version` are integers, currently
|
|
24
|
+
`1`. A later version may add fields; it may not remove or repurpose one. The
|
|
25
|
+
validators accept a later version's extra fields. Every field path, nested
|
|
26
|
+
ones included (`attempts[].provider`), is in `scripts/contract-baseline.json`,
|
|
27
|
+
and `scripts/check-contract-additive.mjs` fails the build if one disappears.
|
|
28
|
+
- **Audit documents, not job results.** Provider names appear here because an
|
|
29
|
+
operator, a regulator or a product's compliance team reads these. They are
|
|
30
|
+
opaque strings: the contract enumerates no provider, no model and no fallback
|
|
31
|
+
order. A job result still never names its provider (ADR 0006).
|
|
32
|
+
|
|
33
|
+
## Four identities, kept apart
|
|
34
|
+
|
|
35
|
+
Corrected 18.09.2026, before the contract shipped. One execution is identified
|
|
36
|
+
at four layers, and no field stands in for another:
|
|
37
|
+
|
|
38
|
+
capability + requirements -> AIOG route -> execution platform -> model -> worker
|
|
39
|
+
|
|
40
|
+
| Layer | Field | What it is | What it is NOT |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| Route | `provider` | AIOG's adapter: the route AIOG chose (`openai-whisper`, `welsh-selfhosted`). An AIOG concept. | The company that ran the work. Several routes can reach one platform. |
|
|
43
|
+
| Platform | `platform` | The execution platform the route called (`pendra`, `openai`). | A language, a jurisdiction, or a model. |
|
|
44
|
+
| Model | `model_id`, `model_version` | The model the platform ran, as it reported executing, and the model's own identity or version: a weights digest, or a version label the platform publishes for that model. | The runtime build. A worker's software version never goes in `model_version`, even when it is the only version on offer. |
|
|
45
|
+
| Runtime / worker | `runtime_version`, `placement.node_id` | The serving runtime or worker software build, and the worker instance that served. | The model's version. The same weights under a new decoder build can score differently, which is why the two are separate. |
|
|
46
|
+
|
|
47
|
+
**`model_version` and `runtime_version` answer different questions.**
|
|
48
|
+
`model_version` says *which weights ran*: two runs with the same value ran the
|
|
49
|
+
same model. `runtime_version` says *which serving software ran them*. For
|
|
50
|
+
Pendra today that maps as:
|
|
51
|
+
|
|
52
|
+
| Pendra source | Field |
|
|
53
|
+
|---|---|
|
|
54
|
+
| response `model` | `model_id` |
|
|
55
|
+
| catalogue `gguf_sha256` for that variant | `model_version` |
|
|
56
|
+
| header `x-worker-version` / `x-runtime-version` | `runtime_version` |
|
|
57
|
+
| header `x-worker-id`, `x-worker-name` | `placement.node_id`, `placement.node_name` |
|
|
58
|
+
| header `x-request-id` | `provider_execution_id` |
|
|
59
|
+
|
|
60
|
+
A value is never moved between rows to fill a gap: a missing `model_version`
|
|
61
|
+
stays `null` rather than borrowing the worker's build number.
|
|
62
|
+
|
|
63
|
+
**`provider` means the route, and only the route.** It is used that way
|
|
64
|
+
consistently: in the runners (`attempts.push({ provider: provider.name })`), the
|
|
65
|
+
policy observation (`{ id: routeProvider }`), and the `route_provider` column.
|
|
66
|
+
Nobody should read `provider === 'pendra'`. It is not, and will not be: the
|
|
67
|
+
platform has its own field for exactly that reason.
|
|
68
|
+
|
|
69
|
+
An execution on Pendra therefore reads the same whichever model it ran:
|
|
70
|
+
|
|
71
|
+
| | Welsh ASR on Pendra | Gemma on Pendra |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `provider` | the ASR route | the analysis route |
|
|
74
|
+
| `platform` | `pendra` | `pendra` |
|
|
75
|
+
| `model_id` | the Welsh ASR model id | the Gemma model id |
|
|
76
|
+
| `model_version` | its weights hash, or `null` | its weights hash, or `null` |
|
|
77
|
+
| `runtime_version` | Pendra's worker build | Pendra's worker build |
|
|
78
|
+
| `placement.node_id` | the worker that served | the worker that served |
|
|
79
|
+
| `provider_execution_id` | Pendra's `x-request-id` | Pendra's `x-request-id` |
|
|
80
|
+
| `jurisdiction`, `jurisdiction_source` | as evidenced, else `null` / `unknown` | as evidenced, else `null` / `unknown` |
|
|
81
|
+
|
|
82
|
+
Gemma needs no special case. It is an ordinary model id on a platform.
|
|
83
|
+
|
|
84
|
+
### Two invariants
|
|
85
|
+
|
|
86
|
+
- **A platform is not a language.** Pendra is not "Welsh". Language and
|
|
87
|
+
specialism belong to the model and the capability.
|
|
88
|
+
- **A platform is not a jurisdiction.** Pendra is not "UK-only". Where work ran
|
|
89
|
+
is `jurisdiction` and `placement.region`, each with its evidence source. A
|
|
90
|
+
platform's name, like a route's name, is never that evidence, and a `null`
|
|
91
|
+
never satisfies `uk_only`.
|
|
92
|
+
|
|
93
|
+
### Retry and fallback
|
|
94
|
+
|
|
95
|
+
Defined across all three identities, not by "the provider changed", because one
|
|
96
|
+
platform hosts several models:
|
|
97
|
+
|
|
98
|
+
| Event | Means | `fallback_scope` |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `retry` | Another attempt at the same route, platform and model, after a transient failure. `to_*` repeats `from_*`. | `null` |
|
|
101
|
+
| `fallback` | Execution moved. `fallback_scope` is the widest identity that changed, and must agree with `from_*` / `to_*` (`fallbackScopeOf`). | `model` (same route and platform, another model), `route` (another route onto the same platform), `platform` (another platform) |
|
|
102
|
+
|
|
103
|
+
A fallback that changes none of the three is a retry recorded under the wrong
|
|
104
|
+
type, and is refused.
|
|
105
|
+
|
|
106
|
+
## The execution record
|
|
107
|
+
|
|
108
|
+
AIOG's authoritative account of one intelligence-layer execution. It must be
|
|
109
|
+
sufficient to audit a request without consulting the calling product's
|
|
110
|
+
database. Validate with `validateExecutionRecord(record)`.
|
|
111
|
+
|
|
112
|
+
"Today" says where each value can come from at `origin/main` on 18.09.2026:
|
|
113
|
+
**column** means the existing row already stores it, **runner** means the
|
|
114
|
+
runner or job holds it but does not store it, **not yet** means nothing
|
|
115
|
+
produces it and stage 0.2 has to.
|
|
116
|
+
|
|
117
|
+
| Field | Meaning | Today |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `record_version` | Schema version, `1` | new |
|
|
120
|
+
| `execution_record_id` | `exr_…` | column |
|
|
121
|
+
| `request_id`, `job_id` | Platform request and job | column |
|
|
122
|
+
| `client_id` | The calling product's credential client | column |
|
|
123
|
+
| `tenant_reference` | The product's organisation, opaque to AIOG | column |
|
|
124
|
+
| `user_reference` | The product's user, when sent | column (writer passes none) |
|
|
125
|
+
| `capability`, `task_class` | What was asked for | column (`task_class` never set) |
|
|
126
|
+
| `requirements.language` | `source_language` as submitted | runner (job request) |
|
|
127
|
+
| `requirements.residency` | A per-request residency requirement | not yet: no request field exists |
|
|
128
|
+
| `requirements.quality_profile` | As submitted | column `quality_profile` |
|
|
129
|
+
| `requirements.diarisation` | `off`, `auto`, `speakers:<n>` | runner (job request) |
|
|
130
|
+
| `requirements.data_class` | As submitted | column `data_class` |
|
|
131
|
+
| `policy_id`, `policy_version` | Citation of the tenant's policy | column (null without a policy) |
|
|
132
|
+
| `effective_policy` | Resolved hard constraints plus `enforced`; `null` when the tenant has no policy | runner can resolve it (`resolveHard`); stored only as `execution_profile.policy_observation` |
|
|
133
|
+
| `eligible_candidates[]` | `provider`, `platform`, `model_id`, `reason`, `jurisdiction`, `jurisdiction_source`. A candidate is a route, a platform AND a model: one platform can offer several models, and policy may admit one and block another | runner can have the route (`candidatesFor`); no adapter declares its platform, and no reason is produced today |
|
|
134
|
+
| `blocked_candidates[]` | `provider`, `platform`, `model_id`, `rule` (the `rejectionReason` vocabulary), `jurisdiction`, `jurisdiction_source` | not yet: policy is not evaluated per candidate before dispatch, and `supports()` returns a bare false with no reason |
|
|
135
|
+
| `selected_provider`, `selected_platform`, `selected_model_id` | The routing decision, before any attempt | runner can have the route (first candidate); not stored |
|
|
136
|
+
| `route_reason` | Why that route (`static-chain` today) | column |
|
|
137
|
+
| `attempts[]` | `provider` (route), `platform` (required), `model_id`, `model_version`, `runtime_version` (worker build), `started_at`, `latency_ms`, `outcome`, `error_code`, `provider_execution_id`, `jurisdiction`, `jurisdiction_source`, `placement` | partial: the runner sees each attempt start (`onAttempt`) but keeps only the last; a failed attempt's error is swallowed inside the registry. Pendra adapters return the model as `execution.runtimeId` (an internal name that pre-dates this contract; it maps to `model_id`) and the worker build as `execution.runtimeVersion` |
|
|
138
|
+
| `attempts[].placement` | `node_id`, `node_name`, `region`, `region_source` | Pendra adapters return node id and name; no provider reports a region |
|
|
139
|
+
| `events[]` | `type` (`retry`, `fallback`), `fallback_scope`, `at`, `from_provider`, `from_platform`, `from_model_id`, `to_provider`, `to_platform`, `to_model_id`, `reason` | not yet: fallbacks happen inside the registry unreported; AIOG has no in-place retry, so `retry` stays empty |
|
|
140
|
+
| `final_provider` | The route whose answer was kept | column `route_provider` on success (on failure that column holds the last attempted) |
|
|
141
|
+
| `final_platform`, `final_model_id`, `final_model_version`, `final_runtime_version` | The platform, model and worker build that served. Required to match a succeeded attempt | not yet: no column; the Pendra adapters return model and worker build, the transcription runner keeps neither |
|
|
142
|
+
| `final_jurisdiction`, `final_jurisdiction_source` | Where the served attempt ran, and how that is known | not yet: no adapter declares one |
|
|
143
|
+
| `provider_execution_id` | The provider's own request id (Pendra's `x-request-id`) | column; the transcription runner fills it when the adapter reports one (stage 0.1) |
|
|
144
|
+
| `prompt_version` | Prompt identity for prompted capabilities | column (writer passes none) |
|
|
145
|
+
| `usage` | `{ unit, amount }` in platform units | column |
|
|
146
|
+
| `provider_cost_internal` | Platform margin analysis, never shown to a caller | column (never populated) |
|
|
147
|
+
| `queue_ms`, `provider_ms`, `total_ms` | Timings | column |
|
|
148
|
+
| `outcome` | A terminal job status | column |
|
|
149
|
+
| `error_code` | Contract error code; required when `outcome` is `failed` | column |
|
|
150
|
+
| `created_at` | ISO 8601 | column |
|
|
151
|
+
|
|
152
|
+
The validator also refuses contradictions: a `completed` or
|
|
153
|
+
`partially_completed` record with no succeeded attempt or no `final_platform`, a
|
|
154
|
+
`final_provider`, `final_platform` or `final_model_id` that no succeeded attempt
|
|
155
|
+
matches, an attempt with no platform, a failed attempt without an `error_code`,
|
|
156
|
+
a succeeded one with one, an event that does not name both ends in full, a
|
|
157
|
+
retry that changed anything, and a fallback whose `fallback_scope` disagrees
|
|
158
|
+
with what changed.
|
|
159
|
+
|
|
160
|
+
## The external execution report
|
|
161
|
+
|
|
162
|
+
While traffic migrates, a product still makes some provider calls itself and
|
|
163
|
+
may fall back on its own side (Capsiynau's direct Pendra path falls back to
|
|
164
|
+
other engines, for example). **A product-side fallback that is not reported to
|
|
165
|
+
AIOG cannot count as canary evidence**, because AIOG's own records would then
|
|
166
|
+
describe a cleaner route than the one the customer got. This is the shape the
|
|
167
|
+
report takes. There is no endpoint for it yet.
|
|
168
|
+
|
|
169
|
+
Validate with `validateExternalExecutionReport(report)`.
|
|
170
|
+
|
|
171
|
+
| Field | Meaning |
|
|
172
|
+
|---|---|
|
|
173
|
+
| `report_version` | Schema version, `1` |
|
|
174
|
+
| `report_id` | Chosen by the product; the idempotency key for the report |
|
|
175
|
+
| `tenant_reference` | The product's organisation |
|
|
176
|
+
| `capability` | As in the record |
|
|
177
|
+
| `product_reference` | The product's own job or project id, opaque to AIOG |
|
|
178
|
+
| `job_id`, `request_id` | The AIOG job and request this relates to, or `null` when the work never touched AIOG |
|
|
179
|
+
| `occurred_at` | ISO 8601 |
|
|
180
|
+
| `requirements` | Same shape as the record's |
|
|
181
|
+
| `attempts[]` | Same shape as the record's, product-side providers included |
|
|
182
|
+
| `events[]` | Same shape as the record's; at least one `fallback` is required |
|
|
183
|
+
| `final_provider`, `final_platform`, `final_model_id`, `final_model_version` | What served in the end: the route, the platform and the model |
|
|
184
|
+
| `final_jurisdiction`, `final_jurisdiction_source` | As in the record |
|
|
185
|
+
| `provider_execution_id` | The final provider's request id, when known |
|
|
186
|
+
| `usage`, `outcome`, `error_code` | As in the record |
|
|
187
|
+
|
|
188
|
+
There is no `client_id`. The reporting product is whoever holds the credential
|
|
189
|
+
the report arrives with; a body that could name its own client could name
|
|
190
|
+
another's.
|
|
191
|
+
|
|
192
|
+
## What stage 0.2 has to populate
|
|
193
|
+
|
|
194
|
+
This contract is the target; filling it is stage 0.2. Found while correcting
|
|
195
|
+
the identities on 18.09.2026 and deliberately NOT fixed here:
|
|
196
|
+
|
|
197
|
+
- **Every adapter declares its platform.** No adapter carries one today, so
|
|
198
|
+
nothing can fill `platform` yet.
|
|
199
|
+
- **Pendra analysis drops all provenance.** `pendraAnalysis.js` returns text and
|
|
200
|
+
usage only, discarding the `execution` its runtime call returned: model,
|
|
201
|
+
worker build, worker and request id never reach a record.
|
|
202
|
+
- **The transcription runner keeps only `provider_execution_id`.**
|
|
203
|
+
`normaliseResult` carries the model, worker build and placement up; the runner
|
|
204
|
+
stores none of them.
|
|
205
|
+
- **`model_version` has no producer.** Pendra publishes a weights hash in its
|
|
206
|
+
catalogue (`gguf_sha256`, read as `contentHash`) but not on an execution
|
|
207
|
+
response, so `identifiesExecution()` can never be satisfied by a Pendra run.
|
|
208
|
+
- **Policy lists match route labels.** Observation builds candidates as
|
|
209
|
+
`{ id: routeProvider }`, so a `provider_denylist` naming a platform matches
|
|
210
|
+
nothing. Platform- and model-level policy needs the candidate identity above.
|
|
211
|
+
- **Usage and cost carry no route, platform or model.** The usage ledger joins
|
|
212
|
+
to the record by job only, and `provider_cost_internal` is never populated.
|
|
213
|
+
- **Internal names pre-date the contract.** Adapters say `runtimeId` for what the
|
|
214
|
+
contract calls `model_id`; align them when 0.2 maps one onto the other.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capsiynau/intelligence-contracts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Wire contracts between products and the Intelligence Layer: job statuses, error codes, capability names, the segment model, and event schemas. Types and constants only - no implementation, no prompts, no provider names.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,12 +16,14 @@
|
|
|
16
16
|
"./errors": "./src/errors.js",
|
|
17
17
|
"./capabilities": "./src/capabilities.js",
|
|
18
18
|
"./segments": "./src/segments.js",
|
|
19
|
-
"./events": "./src/events.js"
|
|
19
|
+
"./events": "./src/events.js",
|
|
20
|
+
"./execution": "./src/execution.js"
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
22
23
|
"src",
|
|
23
24
|
"types",
|
|
24
25
|
"README.md",
|
|
26
|
+
"SCHEMA.md",
|
|
25
27
|
"LICENSE"
|
|
26
28
|
],
|
|
27
29
|
"engines": {
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
},
|
|
37
39
|
"repository": {
|
|
38
40
|
"type": "git",
|
|
39
|
-
"url": "git+https://github.com/aledprysparry/
|
|
41
|
+
"url": "git+https://github.com/aledprysparry/aiog.git",
|
|
40
42
|
"directory": "packages/intelligence-contracts"
|
|
41
43
|
}
|
|
42
44
|
}
|
package/src/errors.js
CHANGED
|
@@ -49,6 +49,13 @@ export const ERROR_CODE = Object.freeze({
|
|
|
49
49
|
RATE_LIMIT_EXCEEDED: { status: 429, retryable: true, message: 'Too many requests.' },
|
|
50
50
|
PROVIDER_UNAVAILABLE: { status: 503, retryable: true, message: 'The upstream service is temporarily unavailable.' },
|
|
51
51
|
PROVIDER_REJECTED: { status: 502, retryable: false, message: 'The upstream service rejected the request.' },
|
|
52
|
+
// Added 18.09.2026. The upstream ACCEPTED the request and answered success,
|
|
53
|
+
// but the answer cannot be used (a transcript with no timed segments), or it
|
|
54
|
+
// failed in some other way a retry will not fix. PROVIDER_REJECTED says the
|
|
55
|
+
// upstream refused, which is untrue here; PROCESSING_FAILED and
|
|
56
|
+
// PROVIDER_UNAVAILABLE both tell a client to retry, which would resubmit
|
|
57
|
+
// work that fails the same way and is paid for each time.
|
|
58
|
+
PROVIDER_FAILED: { status: 502, retryable: false, message: 'The upstream service failed in a way that retrying will not fix.' },
|
|
52
59
|
PROCESSING_FAILED: { status: 500, retryable: true, message: 'Processing failed.' },
|
|
53
60
|
TIMEOUT_EXCEEDED: { status: 504, retryable: true, message: 'The operation timed out.' },
|
|
54
61
|
CONFLICT_JOB_TERMINAL: { status: 409, retryable: false, message: 'The job has already reached a terminal state.' },
|
package/src/execution.js
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @capsiynau/intelligence-contracts/execution
|
|
3
|
+
*
|
|
4
|
+
* The execution record: AIOG's authoritative account of one intelligence-layer
|
|
5
|
+
* execution (remediation plan stage 0.5, 18.09.2026). It must be sufficient to
|
|
6
|
+
* audit a request without consulting the calling product's database, so it
|
|
7
|
+
* carries who asked, what they required, what policy was in force, which
|
|
8
|
+
* candidates were eligible or blocked and why, every attempt, every retry and
|
|
9
|
+
* fallback, what finally served, what it cost and how it ended.
|
|
10
|
+
*
|
|
11
|
+
* And the external execution report: the shape a PRODUCT uses to report a
|
|
12
|
+
* fallback that happened on its own side, outside AIOG, while traffic is
|
|
13
|
+
* migrating. The rule it exists for: a product-side fallback that is not
|
|
14
|
+
* reported to AIOG cannot count as canary evidence, because AIOG's records
|
|
15
|
+
* would then describe a cleaner route than the one customers actually got.
|
|
16
|
+
*
|
|
17
|
+
* CONTRACT ONLY. Nothing in the platform writes this shape yet: populating it
|
|
18
|
+
* is stage 0.2, and the endpoint that accepts a report comes later. The
|
|
19
|
+
* existing row (`aiog.intelligence_api_execution_records`) keeps its columns;
|
|
20
|
+
* every field below reuses a column's name where one exists, so filling the
|
|
21
|
+
* record is a mapping rather than a renaming. See SCHEMA.md for the field
|
|
22
|
+
* table and where each value comes from today.
|
|
23
|
+
*
|
|
24
|
+
* WHY PROVIDER NAMES MAY APPEAR HERE, when ADR 0006 keeps them from callers:
|
|
25
|
+
* this is an audit document read by an operator, a regulator or the product's
|
|
26
|
+
* own compliance team, not a job result. Providers are opaque strings; the
|
|
27
|
+
* contract enumerates none of them, and no model id, prompt or fallback ORDER
|
|
28
|
+
* is published by it. The record says what happened, never how routing works.
|
|
29
|
+
*
|
|
30
|
+
* FOUR IDENTITIES, KEPT APART (18.09.2026). One execution is identified at four
|
|
31
|
+
* layers, and no field stands in for another:
|
|
32
|
+
*
|
|
33
|
+
* provider AIOG's route: the adapter that made the call
|
|
34
|
+
* (`openai-whisper`, `welsh-selfhosted`). An AIOG concept.
|
|
35
|
+
* It is NOT the company that ran the work, and it never
|
|
36
|
+
* will be - several routes can reach one platform.
|
|
37
|
+
* platform The execution platform the route called (`pendra`,
|
|
38
|
+
* `openai`). Where the work was sent.
|
|
39
|
+
* model_id The model the platform ran (a Gemma variant, a Welsh ASR
|
|
40
|
+
* model), as the platform reported it executing.
|
|
41
|
+
* model_version The identity or version of the MODEL ITSELF: a digest of
|
|
42
|
+
* its weights (e.g. Pendra's catalogue `gguf_sha256`), or a
|
|
43
|
+
* version label the platform publishes for that model. Two
|
|
44
|
+
* runs with the same model_version ran the same weights.
|
|
45
|
+
* NEVER the serving software: a worker or runtime build
|
|
46
|
+
* (`x-worker-version`, `x-runtime-version`) is
|
|
47
|
+
* runtime_version, and must not be copied here even when it
|
|
48
|
+
* is the only version on offer. Null when the platform gives
|
|
49
|
+
* no model identity - an honest gap, not a reason to borrow
|
|
50
|
+
* another layer's number.
|
|
51
|
+
* runtime_version The serving runtime or worker BUILD. Not the model's
|
|
52
|
+
* version: the same weights under a new decoder build can
|
|
53
|
+
* score differently, which is why it is kept separately.
|
|
54
|
+
* placement The worker INSTANCE that served (`node_id`), and where it
|
|
55
|
+
* ran as far as is evidenced.
|
|
56
|
+
*
|
|
57
|
+
* So an execution on Pendra reads `provider: <route>, platform: 'pendra',
|
|
58
|
+
* model_id: <model>`, whether the model is Gemma or Welsh ASR. Two invariants
|
|
59
|
+
* follow, and nothing in this contract may encode either assumption:
|
|
60
|
+
*
|
|
61
|
+
* A PLATFORM IS NOT A LANGUAGE. Pendra is not "Welsh": language and
|
|
62
|
+
* specialism belong to the model and the capability, not to the platform.
|
|
63
|
+
*
|
|
64
|
+
* A PLATFORM IS NOT A JURISDICTION. Pendra is not "UK-only": where work ran
|
|
65
|
+
* is `jurisdiction` / `placement.region` with their evidence sources, and a
|
|
66
|
+
* platform's name, like a route's name, is never that evidence.
|
|
67
|
+
*
|
|
68
|
+
* UNKNOWN IS RECORDED AS UNKNOWN. Wherever the record carries a jurisdiction
|
|
69
|
+
* or a serving region it carries the SOURCE beside it: `unknown`,
|
|
70
|
+
* `declared-config` (an operator set it, on a provider's written attestation)
|
|
71
|
+
* or `provider-attested` (the provider's own response said so). A null with
|
|
72
|
+
* source `unknown` is a finding; a missing field would be a gap nobody can
|
|
73
|
+
* tell from one. Nothing here may be inferred from a provider's name, host,
|
|
74
|
+
* model or publishing country, and a null never satisfies `uk_only`.
|
|
75
|
+
*
|
|
76
|
+
* ADDITIVE, like everything in this package. `record_version` and
|
|
77
|
+
* `report_version` are integers; a later version may add fields and must not
|
|
78
|
+
* remove or repurpose one, and a validator for version N accepts a record of a
|
|
79
|
+
* later version whose extra fields it does not know. The field lists are
|
|
80
|
+
* guarded by scripts/check-contract-additive.mjs.
|
|
81
|
+
*/
|
|
82
|
+
import { ALL_CAPABILITIES } from './capabilities.js'
|
|
83
|
+
import { TERMINAL_STATUSES, JOB_STATUS } from './jobs.js'
|
|
84
|
+
import { isValidDiarisation } from './segments.js'
|
|
85
|
+
|
|
86
|
+
export const EXECUTION_RECORD_VERSION = 1
|
|
87
|
+
export const EXTERNAL_EXECUTION_REPORT_VERSION = 1
|
|
88
|
+
|
|
89
|
+
/** How one attempt ended. A skipped candidate is not an attempt. */
|
|
90
|
+
export const ATTEMPT_OUTCOME = Object.freeze({
|
|
91
|
+
SUCCEEDED: 'succeeded',
|
|
92
|
+
FAILED: 'failed',
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Something that happened BETWEEN attempts, with the identity it moved from and
|
|
97
|
+
* to. Both carry the reason, which is normally the error code of the attempt
|
|
98
|
+
* that prompted them.
|
|
99
|
+
*
|
|
100
|
+
* retry another attempt at the SAME route, platform and model, after a
|
|
101
|
+
* transient failure. `to_*` repeats `from_*`.
|
|
102
|
+
* fallback execution moved to a different identity. Which kind of move is
|
|
103
|
+
* `fallback_scope`, below - "the provider changed" is not the
|
|
104
|
+
* definition, because a platform hosts several models and a model
|
|
105
|
+
* change on one platform is a fallback too.
|
|
106
|
+
*/
|
|
107
|
+
export const EXECUTION_EVENT_TYPE = Object.freeze({
|
|
108
|
+
RETRY: 'retry',
|
|
109
|
+
FALLBACK: 'fallback',
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The widest identity a fallback changed. Derived from `from_*` / `to_*` and
|
|
114
|
+
* checked against them, so the label cannot contradict the evidence:
|
|
115
|
+
*
|
|
116
|
+
* platform a different execution platform (and so, normally, a different route)
|
|
117
|
+
* route a different AIOG route onto the same platform
|
|
118
|
+
* model the same route and platform, a different model
|
|
119
|
+
*
|
|
120
|
+
* A retry carries `fallback_scope: null`. A fallback that changes none of the
|
|
121
|
+
* three is a retry recorded under the wrong type, and is refused.
|
|
122
|
+
*/
|
|
123
|
+
export const FALLBACK_SCOPE = Object.freeze({
|
|
124
|
+
MODEL: 'model',
|
|
125
|
+
ROUTE: 'route',
|
|
126
|
+
PLATFORM: 'platform',
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Where a jurisdiction or region value came from. `unknown` pairs with null
|
|
131
|
+
* and only with null: a value with no source is a guess, and a source with no
|
|
132
|
+
* value is a contradiction.
|
|
133
|
+
*/
|
|
134
|
+
export const JURISDICTION_SOURCE = Object.freeze({
|
|
135
|
+
UNKNOWN: 'unknown',
|
|
136
|
+
DECLARED_CONFIG: 'declared-config',
|
|
137
|
+
PROVIDER_ATTESTED: 'provider-attested',
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
/** What the caller required. Every key present; null where it was not stated. */
|
|
141
|
+
export const EXECUTION_REQUIREMENT_KEYS = Object.freeze([
|
|
142
|
+
'language', 'residency', 'quality_profile', 'diarisation', 'data_class',
|
|
143
|
+
])
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The policy in force, resolved for this execution's data class. Named as the
|
|
147
|
+
* policy resolver names its hard constraints, plus `enforced`, which records
|
|
148
|
+
* whether anything actually gated on it (observe-only records false).
|
|
149
|
+
*/
|
|
150
|
+
export const EXECUTION_POLICY_KEYS = Object.freeze([
|
|
151
|
+
'enforced', 'residency', 'external_providers', 'training_use', 'data_class',
|
|
152
|
+
'on_unsatisfiable', 'provider_allowlist', 'provider_denylist',
|
|
153
|
+
])
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A candidate is a route, a platform AND a model, because one platform can
|
|
157
|
+
* offer several models for the same requirement and policy may admit one and
|
|
158
|
+
* block another. `model_id` is null only where a route has not resolved a
|
|
159
|
+
* model at candidate time.
|
|
160
|
+
*/
|
|
161
|
+
const CANDIDATE_IDENTITY_KEYS = Object.freeze(['provider', 'platform', 'model_id'])
|
|
162
|
+
|
|
163
|
+
/** An eligible candidate, why it was eligible, and where it runs as far as is known. */
|
|
164
|
+
export const ELIGIBLE_CANDIDATE_KEYS = Object.freeze([
|
|
165
|
+
...CANDIDATE_IDENTITY_KEYS, 'reason', 'jurisdiction', 'jurisdiction_source',
|
|
166
|
+
])
|
|
167
|
+
|
|
168
|
+
/** A blocked candidate and the rule that blocked it (`residency_undeclared`...). */
|
|
169
|
+
export const BLOCKED_CANDIDATE_KEYS = Object.freeze([
|
|
170
|
+
...CANDIDATE_IDENTITY_KEYS, 'rule', 'jurisdiction', 'jurisdiction_source',
|
|
171
|
+
])
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Where one attempt was served, as evidenced. A worker id with no serving
|
|
175
|
+
* region is recorded exactly so: `region: null`, `region_source: 'unknown'`.
|
|
176
|
+
*/
|
|
177
|
+
export const EXECUTION_PLACEMENT_KEYS = Object.freeze(['node_id', 'node_name', 'region', 'region_source'])
|
|
178
|
+
|
|
179
|
+
/** One call, through one route, to one model on one platform. */
|
|
180
|
+
export const EXECUTION_ATTEMPT_KEYS = Object.freeze([
|
|
181
|
+
'provider', 'platform', 'model_id', 'model_version', 'runtime_version',
|
|
182
|
+
'started_at', 'latency_ms', 'outcome', 'error_code', 'provider_execution_id',
|
|
183
|
+
'jurisdiction', 'jurisdiction_source', 'placement',
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
export const EXECUTION_EVENT_KEYS = Object.freeze([
|
|
187
|
+
'type', 'fallback_scope', 'at',
|
|
188
|
+
'from_provider', 'from_platform', 'from_model_id',
|
|
189
|
+
'to_provider', 'to_platform', 'to_model_id',
|
|
190
|
+
'reason',
|
|
191
|
+
])
|
|
192
|
+
|
|
193
|
+
/** Every top-level key of the execution record, version 1. */
|
|
194
|
+
export const EXECUTION_RECORD_KEYS = Object.freeze([
|
|
195
|
+
'record_version', 'execution_record_id', 'request_id', 'job_id',
|
|
196
|
+
'client_id', 'tenant_reference', 'user_reference',
|
|
197
|
+
'capability', 'task_class', 'requirements',
|
|
198
|
+
'policy_id', 'policy_version', 'effective_policy',
|
|
199
|
+
'eligible_candidates', 'blocked_candidates',
|
|
200
|
+
'selected_provider', 'selected_platform', 'selected_model_id', 'route_reason',
|
|
201
|
+
'attempts', 'events',
|
|
202
|
+
'final_provider', 'final_platform', 'final_model_id', 'final_model_version', 'final_runtime_version',
|
|
203
|
+
'final_jurisdiction', 'final_jurisdiction_source', 'provider_execution_id',
|
|
204
|
+
'prompt_version', 'usage', 'provider_cost_internal',
|
|
205
|
+
'queue_ms', 'provider_ms', 'total_ms',
|
|
206
|
+
'outcome', 'error_code', 'created_at',
|
|
207
|
+
])
|
|
208
|
+
|
|
209
|
+
/** Keys that must be present AND non-null. The rest may be an honest null. */
|
|
210
|
+
const RECORD_NON_NULL = Object.freeze([
|
|
211
|
+
'record_version', 'execution_record_id', 'client_id', 'tenant_reference', 'capability',
|
|
212
|
+
'requirements', 'eligible_candidates', 'blocked_candidates', 'route_reason',
|
|
213
|
+
'attempts', 'events', 'final_jurisdiction_source', 'outcome', 'created_at',
|
|
214
|
+
])
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Every top-level key of an external execution report, version 1. There is no
|
|
218
|
+
* `client_id`: the reporting product is whoever holds the credential it
|
|
219
|
+
* reports with, and a body that named its own client could name another's.
|
|
220
|
+
*/
|
|
221
|
+
export const EXTERNAL_EXECUTION_REPORT_KEYS = Object.freeze([
|
|
222
|
+
'report_version', 'report_id', 'tenant_reference', 'capability',
|
|
223
|
+
'product_reference', 'job_id', 'request_id', 'occurred_at',
|
|
224
|
+
'requirements', 'attempts', 'events',
|
|
225
|
+
'final_provider', 'final_platform', 'final_model_id', 'final_model_version',
|
|
226
|
+
'final_jurisdiction', 'final_jurisdiction_source',
|
|
227
|
+
'provider_execution_id', 'usage', 'outcome', 'error_code',
|
|
228
|
+
])
|
|
229
|
+
|
|
230
|
+
const REPORT_NON_NULL = Object.freeze([
|
|
231
|
+
'report_version', 'report_id', 'tenant_reference', 'capability', 'product_reference',
|
|
232
|
+
'occurred_at', 'requirements', 'attempts', 'events', 'final_jurisdiction_source', 'outcome',
|
|
233
|
+
])
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Dotted paths, `[]` marking an array of objects (`attempts[].provider`): the
|
|
237
|
+
* unit the additive guard compares, so removing a nested field fails the build
|
|
238
|
+
* exactly as removing a top-level one does.
|
|
239
|
+
*/
|
|
240
|
+
function fieldPaths(topKeys, objects = {}, arrays = {}) {
|
|
241
|
+
const paths = []
|
|
242
|
+
for (const key of topKeys) {
|
|
243
|
+
paths.push(key)
|
|
244
|
+
for (const child of objects[key] || []) paths.push(`${key}.${child}`)
|
|
245
|
+
for (const child of arrays[key] || []) {
|
|
246
|
+
paths.push(`${key}[].${child}`)
|
|
247
|
+
if (key === 'attempts' && child === 'placement') {
|
|
248
|
+
for (const leaf of EXECUTION_PLACEMENT_KEYS) paths.push(`${key}[].${child}.${leaf}`)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return Object.freeze(paths.sort())
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Every field of the execution record, as a sorted list of paths. */
|
|
256
|
+
export const EXECUTION_RECORD_FIELDS = fieldPaths(EXECUTION_RECORD_KEYS, {
|
|
257
|
+
requirements: EXECUTION_REQUIREMENT_KEYS,
|
|
258
|
+
effective_policy: EXECUTION_POLICY_KEYS,
|
|
259
|
+
}, {
|
|
260
|
+
eligible_candidates: ELIGIBLE_CANDIDATE_KEYS,
|
|
261
|
+
blocked_candidates: BLOCKED_CANDIDATE_KEYS,
|
|
262
|
+
attempts: EXECUTION_ATTEMPT_KEYS,
|
|
263
|
+
events: EXECUTION_EVENT_KEYS,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
/** Every field of the external execution report, as a sorted list of paths. */
|
|
267
|
+
export const EXTERNAL_EXECUTION_REPORT_FIELDS = fieldPaths(EXTERNAL_EXECUTION_REPORT_KEYS, {
|
|
268
|
+
requirements: EXECUTION_REQUIREMENT_KEYS,
|
|
269
|
+
}, {
|
|
270
|
+
attempts: EXECUTION_ATTEMPT_KEYS,
|
|
271
|
+
events: EXECUTION_EVENT_KEYS,
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// ── Shared checks ───────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
277
|
+
const isText = (v) => typeof v === 'string' && v.trim().length > 0
|
|
278
|
+
const isInstant = (v) => typeof v === 'string' && Number.isFinite(Date.parse(v))
|
|
279
|
+
const isQuantity = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0
|
|
280
|
+
|
|
281
|
+
const SOURCES = Object.values(JURISDICTION_SOURCE)
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* A value and its source, checked together. Null pairs with `unknown`; a
|
|
285
|
+
* value pairs with a source that is not `unknown`.
|
|
286
|
+
*/
|
|
287
|
+
function checkSourced(obj, valueKey, sourceKey, where, problems) {
|
|
288
|
+
const value = obj[valueKey]
|
|
289
|
+
const source = obj[sourceKey]
|
|
290
|
+
if (source === undefined) return
|
|
291
|
+
if (!SOURCES.includes(source)) return problems.push(`${where}${sourceKey} must be one of ${SOURCES.join(', ')}`)
|
|
292
|
+
if (value != null && !isText(value)) return problems.push(`${where}${valueKey} must be a string or null`)
|
|
293
|
+
if (value == null && source !== JURISDICTION_SOURCE.UNKNOWN) {
|
|
294
|
+
problems.push(`${where}${valueKey} is null, so ${sourceKey} must be unknown`)
|
|
295
|
+
}
|
|
296
|
+
if (value != null && source === JURISDICTION_SOURCE.UNKNOWN) {
|
|
297
|
+
problems.push(`${where}${valueKey} has a value, so ${sourceKey} cannot be unknown`)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function requireKeys(obj, keys, nonNull, where, problems) {
|
|
302
|
+
for (const key of keys) {
|
|
303
|
+
if (obj[key] === undefined) problems.push(`${where}missing ${key}`)
|
|
304
|
+
else if (obj[key] === null && nonNull.includes(key)) problems.push(`${where}${key} must not be null`)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function checkRequirements(req, problems) {
|
|
309
|
+
if (!isObject(req)) return problems.push('requirements must be an object')
|
|
310
|
+
requireKeys(req, EXECUTION_REQUIREMENT_KEYS, [], 'requirements: ', problems)
|
|
311
|
+
for (const key of EXECUTION_REQUIREMENT_KEYS) {
|
|
312
|
+
if (req[key] != null && typeof req[key] !== 'string') problems.push(`requirements: ${key} must be a string or null`)
|
|
313
|
+
}
|
|
314
|
+
if (req.diarisation != null && !isValidDiarisation(req.diarisation)) {
|
|
315
|
+
problems.push('requirements: diarisation is not off, auto or speakers:<n>')
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function checkAttempts(attempts, problems) {
|
|
320
|
+
if (!Array.isArray(attempts)) return problems.push('attempts must be an array')
|
|
321
|
+
attempts.forEach((a, i) => {
|
|
322
|
+
const at = `attempts[${i}]: `
|
|
323
|
+
if (!isObject(a)) return problems.push(`${at}not an object`)
|
|
324
|
+
requireKeys(a, EXECUTION_ATTEMPT_KEYS, ['provider', 'platform', 'started_at', 'outcome', 'jurisdiction_source', 'placement'], at, problems)
|
|
325
|
+
checkSourced(a, 'jurisdiction', 'jurisdiction_source', at, problems)
|
|
326
|
+
// The route and the platform are always known: AIOG chose the one and the
|
|
327
|
+
// one it chose names the other. The model and its versions may honestly
|
|
328
|
+
// not be.
|
|
329
|
+
for (const key of ['provider', 'platform']) {
|
|
330
|
+
if (a[key] != null && !isText(a[key])) problems.push(`${at}${key} must be a non-empty string`)
|
|
331
|
+
}
|
|
332
|
+
for (const key of ['model_id', 'model_version', 'runtime_version']) {
|
|
333
|
+
if (a[key] != null && !isText(a[key])) problems.push(`${at}${key} must be a non-empty string or null`)
|
|
334
|
+
}
|
|
335
|
+
if (a.started_at != null && !isInstant(a.started_at)) problems.push(`${at}started_at must be an ISO 8601 instant`)
|
|
336
|
+
if (a.latency_ms != null && !isQuantity(a.latency_ms)) problems.push(`${at}latency_ms must be a non-negative number or null`)
|
|
337
|
+
if (a.outcome != null && !Object.values(ATTEMPT_OUTCOME).includes(a.outcome)) problems.push(`${at}unknown outcome ${a.outcome}`)
|
|
338
|
+
// A failure with no code cannot be audited; a success with one is a contradiction.
|
|
339
|
+
if (a.outcome === ATTEMPT_OUTCOME.FAILED && !isText(a.error_code)) problems.push(`${at}a failed attempt needs an error_code`)
|
|
340
|
+
if (a.outcome === ATTEMPT_OUTCOME.SUCCEEDED && a.error_code != null) problems.push(`${at}a succeeded attempt carries no error_code`)
|
|
341
|
+
if (a.placement != null) {
|
|
342
|
+
if (!isObject(a.placement)) problems.push(`${at}placement must be an object`)
|
|
343
|
+
else {
|
|
344
|
+
requireKeys(a.placement, EXECUTION_PLACEMENT_KEYS, ['region_source'], `${at}placement: `, problems)
|
|
345
|
+
checkSourced(a.placement, 'region', 'region_source', `${at}placement: `, problems)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The widest identity that differs between the two ends of an event, or null
|
|
353
|
+
* when none does. Platform outranks route outranks model.
|
|
354
|
+
*/
|
|
355
|
+
export function fallbackScopeOf(e) {
|
|
356
|
+
if (e?.from_platform !== e?.to_platform) return FALLBACK_SCOPE.PLATFORM
|
|
357
|
+
if (e?.from_provider !== e?.to_provider) return FALLBACK_SCOPE.ROUTE
|
|
358
|
+
if ((e?.from_model_id ?? null) !== (e?.to_model_id ?? null)) return FALLBACK_SCOPE.MODEL
|
|
359
|
+
return null
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function checkEvents(events, problems) {
|
|
363
|
+
if (!Array.isArray(events)) return problems.push('events must be an array')
|
|
364
|
+
events.forEach((e, i) => {
|
|
365
|
+
const at = `events[${i}]: `
|
|
366
|
+
if (!isObject(e)) return problems.push(`${at}not an object`)
|
|
367
|
+
requireKeys(e, EXECUTION_EVENT_KEYS, ['type', 'at', 'from_provider', 'from_platform', 'reason'], at, problems)
|
|
368
|
+
if (e.type != null && !Object.values(EXECUTION_EVENT_TYPE).includes(e.type)) problems.push(`${at}unknown type ${e.type}`)
|
|
369
|
+
if (e.at != null && !isInstant(e.at)) problems.push(`${at}at must be an ISO 8601 instant`)
|
|
370
|
+
|
|
371
|
+
// BOTH ENDS NAMED IN FULL. An event that says where it came from and not
|
|
372
|
+
// where it went is half a route; the audit needs both halves to explain it.
|
|
373
|
+
for (const end of ['from', 'to']) {
|
|
374
|
+
for (const key of ['provider', 'platform']) {
|
|
375
|
+
if (!isText(e[`${end}_${key}`])) problems.push(`${at}${end}_${key} must be a non-empty string`)
|
|
376
|
+
}
|
|
377
|
+
const model = e[`${end}_model_id`]
|
|
378
|
+
if (model != null && !isText(model)) problems.push(`${at}${end}_model_id must be a non-empty string or null`)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const scope = fallbackScopeOf(e)
|
|
382
|
+
if (e.type === EXECUTION_EVENT_TYPE.RETRY) {
|
|
383
|
+
if (scope !== null) problems.push(`${at}a retry repeats the same route, platform and model; this one changed ${scope}`)
|
|
384
|
+
if (e.fallback_scope != null) problems.push(`${at}a retry carries no fallback_scope`)
|
|
385
|
+
}
|
|
386
|
+
if (e.type === EXECUTION_EVENT_TYPE.FALLBACK) {
|
|
387
|
+
if (scope === null) problems.push(`${at}a fallback that changes no route, platform or model is a retry`)
|
|
388
|
+
else if (e.fallback_scope !== scope) {
|
|
389
|
+
problems.push(`${at}fallback_scope must be ${scope}, the widest identity that changed`)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function checkCandidates(list, keys, name, problems) {
|
|
396
|
+
if (!Array.isArray(list)) return problems.push(`${name} must be an array`)
|
|
397
|
+
// `reason` for an eligible candidate, `rule` for a blocked one.
|
|
398
|
+
const because = keys.find((k) => k === 'reason' || k === 'rule')
|
|
399
|
+
list.forEach((c, i) => {
|
|
400
|
+
const at = `${name}[${i}]: `
|
|
401
|
+
if (!isObject(c)) return problems.push(`${at}not an object`)
|
|
402
|
+
requireKeys(c, keys, ['jurisdiction_source'], at, problems)
|
|
403
|
+
for (const key of ['provider', 'platform', because]) {
|
|
404
|
+
if (!isText(c[key])) problems.push(`${at}${key} must be a non-empty string`)
|
|
405
|
+
}
|
|
406
|
+
if (c.model_id != null && !isText(c.model_id)) problems.push(`${at}model_id must be a non-empty string or null`)
|
|
407
|
+
checkSourced(c, 'jurisdiction', 'jurisdiction_source', at, problems)
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Checks both shapes share: capability, outcome and its code, usage, the served provider. */
|
|
412
|
+
function checkEnding(doc, problems) {
|
|
413
|
+
checkSourced(doc, 'final_jurisdiction', 'final_jurisdiction_source', '', problems)
|
|
414
|
+
if (doc.capability != null && !ALL_CAPABILITIES.includes(doc.capability)) problems.push(`unknown capability ${doc.capability}`)
|
|
415
|
+
if (doc.outcome != null && !TERMINAL_STATUSES.includes(doc.outcome)) {
|
|
416
|
+
problems.push(`outcome must be one of ${TERMINAL_STATUSES.join(', ')}`)
|
|
417
|
+
}
|
|
418
|
+
if (doc.outcome === JOB_STATUS.FAILED && !isText(doc.error_code)) problems.push('a failed execution needs an error_code')
|
|
419
|
+
if (doc.usage != null && (!isObject(doc.usage) || !isText(doc.usage.unit) || !isQuantity(doc.usage.amount))) {
|
|
420
|
+
problems.push('usage must be null or { unit, amount } with a non-negative amount')
|
|
421
|
+
}
|
|
422
|
+
// WHAT SERVED MUST BE AN ATTEMPT THAT SUCCEEDED, at every identity layer. A
|
|
423
|
+
// record naming a final provider, platform or model that nothing in its own
|
|
424
|
+
// attempt list succeeded with is describing a route that did not happen - and
|
|
425
|
+
// "succeeded on Pendra" is not "succeeded with THIS model on Pendra".
|
|
426
|
+
const served = Array.isArray(doc.attempts)
|
|
427
|
+
? doc.attempts.filter((a) => a?.outcome === ATTEMPT_OUTCOME.SUCCEEDED)
|
|
428
|
+
: []
|
|
429
|
+
const resultBearing = doc.outcome === JOB_STATUS.COMPLETED || doc.outcome === JOB_STATUS.PARTIALLY_COMPLETED
|
|
430
|
+
if (resultBearing && served.length === 0) problems.push(`a ${doc.outcome} execution needs a succeeded attempt`)
|
|
431
|
+
if (doc.final_provider != null) {
|
|
432
|
+
const byProvider = served.filter((a) => a.provider === doc.final_provider)
|
|
433
|
+
if (byProvider.length === 0) problems.push('final_provider is not the provider of any succeeded attempt')
|
|
434
|
+
else {
|
|
435
|
+
const byPlatform = byProvider.filter((a) => a.platform === doc.final_platform)
|
|
436
|
+
if (byPlatform.length === 0) problems.push('final_platform is not the platform of the succeeded attempt it names')
|
|
437
|
+
else if (doc.final_model_id != null && !byPlatform.some((a) => a.model_id === doc.final_model_id)) {
|
|
438
|
+
problems.push('final_model_id is not the model of the succeeded attempt it names')
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (resultBearing && doc.final_provider == null) problems.push(`a ${doc.outcome} execution names its final_provider`)
|
|
443
|
+
if (resultBearing && doc.final_platform == null) problems.push(`a ${doc.outcome} execution names its final_platform`)
|
|
444
|
+
for (const key of ['final_platform', 'final_model_id', 'final_model_version']) {
|
|
445
|
+
if (doc[key] != null && !isText(doc[key])) problems.push(`${key} must be a non-empty string or null`)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function checkVersion(value, name, problems) {
|
|
450
|
+
if (value != null && !(Number.isInteger(value) && value >= 1)) problems.push(`${name} must be a positive integer`)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ── Validators ──────────────────────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Structural check on an execution record.
|
|
457
|
+
*
|
|
458
|
+
* Every key is PRESENT, with null where the value is honestly unknown: a
|
|
459
|
+
* record that omits a field looks complete, and one that says null is
|
|
460
|
+
* honestly incomplete. Unknown extra keys are accepted, so a record of a later
|
|
461
|
+
* version still validates here.
|
|
462
|
+
*
|
|
463
|
+
* @param {unknown} record
|
|
464
|
+
* @returns {{ valid: boolean, problems: string[] }}
|
|
465
|
+
*/
|
|
466
|
+
export function validateExecutionRecord(record) {
|
|
467
|
+
if (!isObject(record)) return { valid: false, problems: ['execution record is not an object'] }
|
|
468
|
+
const problems = []
|
|
469
|
+
requireKeys(record, EXECUTION_RECORD_KEYS, RECORD_NON_NULL, '', problems)
|
|
470
|
+
checkVersion(record.record_version, 'record_version', problems)
|
|
471
|
+
for (const key of ['execution_record_id', 'client_id', 'tenant_reference', 'route_reason']) {
|
|
472
|
+
if (record[key] != null && !isText(record[key])) problems.push(`${key} must be a non-empty string`)
|
|
473
|
+
}
|
|
474
|
+
if (record.created_at != null && !isInstant(record.created_at)) problems.push('created_at must be an ISO 8601 instant')
|
|
475
|
+
if (record.requirements != null) checkRequirements(record.requirements, problems)
|
|
476
|
+
if (record.effective_policy != null) {
|
|
477
|
+
if (!isObject(record.effective_policy)) problems.push('effective_policy must be an object or null')
|
|
478
|
+
else {
|
|
479
|
+
requireKeys(record.effective_policy, EXECUTION_POLICY_KEYS, ['enforced'], 'effective_policy: ', problems)
|
|
480
|
+
if (typeof record.effective_policy.enforced !== 'boolean') problems.push('effective_policy: enforced must be a boolean')
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (record.eligible_candidates != null) checkCandidates(record.eligible_candidates, ELIGIBLE_CANDIDATE_KEYS, 'eligible_candidates', problems)
|
|
484
|
+
if (record.blocked_candidates != null) checkCandidates(record.blocked_candidates, BLOCKED_CANDIDATE_KEYS, 'blocked_candidates', problems)
|
|
485
|
+
if (record.attempts != null) checkAttempts(record.attempts, problems)
|
|
486
|
+
if (record.events != null) checkEvents(record.events, problems)
|
|
487
|
+
for (const key of ['provider_cost_internal', 'queue_ms', 'provider_ms', 'total_ms']) {
|
|
488
|
+
if (record[key] != null && !isQuantity(record[key])) problems.push(`${key} must be a non-negative number or null`)
|
|
489
|
+
}
|
|
490
|
+
checkEnding(record, problems)
|
|
491
|
+
return { valid: problems.length === 0, problems }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Structural check on an external execution report.
|
|
496
|
+
*
|
|
497
|
+
* A report exists to describe a product-side FALLBACK, so one without a
|
|
498
|
+
* fallback event is refused: it would add a record to the canary evidence
|
|
499
|
+
* that says nothing the evidence needs.
|
|
500
|
+
*
|
|
501
|
+
* @param {unknown} report
|
|
502
|
+
* @returns {{ valid: boolean, problems: string[] }}
|
|
503
|
+
*/
|
|
504
|
+
export function validateExternalExecutionReport(report) {
|
|
505
|
+
if (!isObject(report)) return { valid: false, problems: ['external execution report is not an object'] }
|
|
506
|
+
const problems = []
|
|
507
|
+
requireKeys(report, EXTERNAL_EXECUTION_REPORT_KEYS, REPORT_NON_NULL, '', problems)
|
|
508
|
+
checkVersion(report.report_version, 'report_version', problems)
|
|
509
|
+
for (const key of ['report_id', 'tenant_reference', 'product_reference']) {
|
|
510
|
+
if (report[key] != null && !isText(report[key])) problems.push(`${key} must be a non-empty string`)
|
|
511
|
+
}
|
|
512
|
+
if (report.occurred_at != null && !isInstant(report.occurred_at)) problems.push('occurred_at must be an ISO 8601 instant')
|
|
513
|
+
if (report.requirements != null) checkRequirements(report.requirements, problems)
|
|
514
|
+
if (report.attempts != null) checkAttempts(report.attempts, problems)
|
|
515
|
+
if (report.events != null) {
|
|
516
|
+
checkEvents(report.events, problems)
|
|
517
|
+
if (Array.isArray(report.events) && !report.events.some((e) => e?.type === EXECUTION_EVENT_TYPE.FALLBACK)) {
|
|
518
|
+
problems.push('a report describes at least one fallback event')
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
checkEnding(report, problems)
|
|
522
|
+
return { valid: problems.length === 0, problems }
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export default {
|
|
526
|
+
EXECUTION_RECORD_VERSION,
|
|
527
|
+
EXTERNAL_EXECUTION_REPORT_VERSION,
|
|
528
|
+
ATTEMPT_OUTCOME,
|
|
529
|
+
EXECUTION_EVENT_TYPE,
|
|
530
|
+
FALLBACK_SCOPE,
|
|
531
|
+
JURISDICTION_SOURCE,
|
|
532
|
+
EXECUTION_REQUIREMENT_KEYS,
|
|
533
|
+
EXECUTION_POLICY_KEYS,
|
|
534
|
+
ELIGIBLE_CANDIDATE_KEYS,
|
|
535
|
+
BLOCKED_CANDIDATE_KEYS,
|
|
536
|
+
EXECUTION_PLACEMENT_KEYS,
|
|
537
|
+
EXECUTION_ATTEMPT_KEYS,
|
|
538
|
+
EXECUTION_EVENT_KEYS,
|
|
539
|
+
EXECUTION_RECORD_KEYS,
|
|
540
|
+
EXTERNAL_EXECUTION_REPORT_KEYS,
|
|
541
|
+
EXECUTION_RECORD_FIELDS,
|
|
542
|
+
EXTERNAL_EXECUTION_REPORT_FIELDS,
|
|
543
|
+
fallbackScopeOf,
|
|
544
|
+
validateExecutionRecord,
|
|
545
|
+
validateExternalExecutionReport,
|
|
546
|
+
}
|
package/src/index.js
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -93,3 +93,196 @@ export declare const REQUIRED_REVIEW_EVENT_KEYS: readonly string[]
|
|
|
93
93
|
/** The family an event type belongs to ("job", "review"), from its own prefix. */
|
|
94
94
|
export declare function eventFamily(type: unknown): string | null
|
|
95
95
|
export declare function validateEvent(event: unknown): ValidationResult
|
|
96
|
+
|
|
97
|
+
// ── Execution record and external execution report (stage 0.5, 18.09.2026) ──
|
|
98
|
+
// Audit shapes, not job results. Every key is present; null means "not known",
|
|
99
|
+
// never "not recorded". See SCHEMA.md.
|
|
100
|
+
|
|
101
|
+
export type AttemptOutcome = 'succeeded' | 'failed'
|
|
102
|
+
export type ExecutionEventType = 'retry' | 'fallback'
|
|
103
|
+
/** The widest identity a fallback changed: platform > route > model. */
|
|
104
|
+
export type FallbackScope = 'model' | 'route' | 'platform'
|
|
105
|
+
/** Where a jurisdiction or region came from. 'unknown' pairs with null, only. */
|
|
106
|
+
export type JurisdictionSource = 'unknown' | 'declared-config' | 'provider-attested'
|
|
107
|
+
|
|
108
|
+
export interface ExecutionPlacement {
|
|
109
|
+
node_id: string | null
|
|
110
|
+
node_name: string | null
|
|
111
|
+
region: string | null
|
|
112
|
+
region_source: JurisdictionSource
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ExecutionRequirements {
|
|
116
|
+
language: string | null
|
|
117
|
+
residency: string | null
|
|
118
|
+
quality_profile: string | null
|
|
119
|
+
diarisation: string | null
|
|
120
|
+
data_class: string | null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface EffectivePolicy {
|
|
124
|
+
/** Whether anything gated on the policy. Observe-only records false. */
|
|
125
|
+
enforced: boolean
|
|
126
|
+
residency: string | null
|
|
127
|
+
external_providers: string | null
|
|
128
|
+
training_use: string | null
|
|
129
|
+
data_class: string | null
|
|
130
|
+
on_unsatisfiable: string | null
|
|
131
|
+
provider_allowlist: string[] | null
|
|
132
|
+
provider_denylist: string[] | null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Four identities, kept apart (SCHEMA.md):
|
|
137
|
+
* provider AIOG's route/adapter - never the company that ran the work
|
|
138
|
+
* platform the execution platform, e.g. 'pendra'
|
|
139
|
+
* model_id the model the platform ran
|
|
140
|
+
* runtime_version the serving runtime/worker BUILD, not the model's version
|
|
141
|
+
* A platform is neither a language nor a jurisdiction.
|
|
142
|
+
*/
|
|
143
|
+
export interface ExecutionAttempt {
|
|
144
|
+
/** AIOG's route: the adapter that made the call. */
|
|
145
|
+
provider: string
|
|
146
|
+
/** The execution platform the route called. */
|
|
147
|
+
platform: string
|
|
148
|
+
/** The model the platform ran, as it reported executing. */
|
|
149
|
+
model_id: string | null
|
|
150
|
+
/**
|
|
151
|
+
* The model's own identity or version: a weights digest or a model version
|
|
152
|
+
* label the platform publishes. Never the worker/runtime build, which is
|
|
153
|
+
* `runtime_version`. Null when the platform gives no model identity.
|
|
154
|
+
*/
|
|
155
|
+
model_version: string | null
|
|
156
|
+
/** The serving runtime or worker build. Not the model's version. */
|
|
157
|
+
runtime_version: string | null
|
|
158
|
+
started_at: string
|
|
159
|
+
latency_ms: number | null
|
|
160
|
+
outcome: AttemptOutcome
|
|
161
|
+
/** Required when the attempt failed, null when it succeeded. */
|
|
162
|
+
error_code: string | null
|
|
163
|
+
/** The provider's own id for the request, e.g. Pendra's x-request-id. */
|
|
164
|
+
provider_execution_id: string | null
|
|
165
|
+
jurisdiction: string | null
|
|
166
|
+
jurisdiction_source: JurisdictionSource
|
|
167
|
+
placement: ExecutionPlacement
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A retry repeats the same route, platform and model (`to_*` equals `from_*`,
|
|
172
|
+
* `fallback_scope` null). A fallback changes at least one, and
|
|
173
|
+
* `fallback_scope` names the widest that changed.
|
|
174
|
+
*/
|
|
175
|
+
export interface ExecutionEvent {
|
|
176
|
+
type: ExecutionEventType
|
|
177
|
+
fallback_scope: FallbackScope | null
|
|
178
|
+
at: string
|
|
179
|
+
from_provider: string
|
|
180
|
+
from_platform: string
|
|
181
|
+
from_model_id: string | null
|
|
182
|
+
to_provider: string
|
|
183
|
+
to_platform: string
|
|
184
|
+
to_model_id: string | null
|
|
185
|
+
reason: string
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A route, a platform and a model: one platform can offer several models. */
|
|
189
|
+
export interface ExecutionCandidateIdentity {
|
|
190
|
+
provider: string
|
|
191
|
+
platform: string
|
|
192
|
+
model_id: string | null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface ExecutionUsage { unit: string; amount: number; [extra: string]: unknown }
|
|
196
|
+
|
|
197
|
+
export interface ExecutionRecord {
|
|
198
|
+
record_version: number
|
|
199
|
+
execution_record_id: string
|
|
200
|
+
request_id: string | null
|
|
201
|
+
job_id: string | null
|
|
202
|
+
client_id: string
|
|
203
|
+
tenant_reference: string
|
|
204
|
+
user_reference: string | null
|
|
205
|
+
capability: Capability
|
|
206
|
+
task_class: string | null
|
|
207
|
+
requirements: ExecutionRequirements
|
|
208
|
+
policy_id: string | null
|
|
209
|
+
policy_version: string | null
|
|
210
|
+
effective_policy: EffectivePolicy | null
|
|
211
|
+
eligible_candidates: Array<ExecutionCandidateIdentity & { reason: string; jurisdiction: string | null; jurisdiction_source: JurisdictionSource }>
|
|
212
|
+
blocked_candidates: Array<ExecutionCandidateIdentity & { rule: string; jurisdiction: string | null; jurisdiction_source: JurisdictionSource }>
|
|
213
|
+
selected_provider: string | null
|
|
214
|
+
selected_platform: string | null
|
|
215
|
+
selected_model_id: string | null
|
|
216
|
+
route_reason: string
|
|
217
|
+
attempts: ExecutionAttempt[]
|
|
218
|
+
events: ExecutionEvent[]
|
|
219
|
+
final_provider: string | null
|
|
220
|
+
final_platform: string | null
|
|
221
|
+
final_model_id: string | null
|
|
222
|
+
final_model_version: string | null
|
|
223
|
+
final_runtime_version: string | null
|
|
224
|
+
final_jurisdiction: string | null
|
|
225
|
+
final_jurisdiction_source: JurisdictionSource
|
|
226
|
+
provider_execution_id: string | null
|
|
227
|
+
prompt_version: string | null
|
|
228
|
+
usage: ExecutionUsage | null
|
|
229
|
+
provider_cost_internal: number | null
|
|
230
|
+
queue_ms: number | null
|
|
231
|
+
provider_ms: number | null
|
|
232
|
+
total_ms: number | null
|
|
233
|
+
outcome: JobStatus
|
|
234
|
+
error_code: string | null
|
|
235
|
+
created_at: string
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A product-side fallback, reported to AIOG. The client is the credential's. */
|
|
239
|
+
export interface ExternalExecutionReport {
|
|
240
|
+
report_version: number
|
|
241
|
+
report_id: string
|
|
242
|
+
tenant_reference: string
|
|
243
|
+
capability: Capability
|
|
244
|
+
product_reference: string
|
|
245
|
+
job_id: string | null
|
|
246
|
+
request_id: string | null
|
|
247
|
+
occurred_at: string
|
|
248
|
+
requirements: ExecutionRequirements
|
|
249
|
+
attempts: ExecutionAttempt[]
|
|
250
|
+
/** At least one event of type 'fallback'. */
|
|
251
|
+
events: ExecutionEvent[]
|
|
252
|
+
final_provider: string | null
|
|
253
|
+
final_platform: string | null
|
|
254
|
+
final_model_id: string | null
|
|
255
|
+
final_model_version: string | null
|
|
256
|
+
final_jurisdiction: string | null
|
|
257
|
+
final_jurisdiction_source: JurisdictionSource
|
|
258
|
+
provider_execution_id: string | null
|
|
259
|
+
usage: ExecutionUsage | null
|
|
260
|
+
outcome: JobStatus
|
|
261
|
+
error_code: string | null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export declare const EXECUTION_RECORD_VERSION: number
|
|
265
|
+
export declare const EXTERNAL_EXECUTION_REPORT_VERSION: number
|
|
266
|
+
export declare const ATTEMPT_OUTCOME: Readonly<{ SUCCEEDED: 'succeeded'; FAILED: 'failed' }>
|
|
267
|
+
export declare const EXECUTION_EVENT_TYPE: Readonly<{ RETRY: 'retry'; FALLBACK: 'fallback' }>
|
|
268
|
+
export declare const FALLBACK_SCOPE: Readonly<{ MODEL: 'model'; ROUTE: 'route'; PLATFORM: 'platform' }>
|
|
269
|
+
export declare const JURISDICTION_SOURCE: Readonly<{
|
|
270
|
+
UNKNOWN: 'unknown'; DECLARED_CONFIG: 'declared-config'; PROVIDER_ATTESTED: 'provider-attested'
|
|
271
|
+
}>
|
|
272
|
+
export declare const EXECUTION_PLACEMENT_KEYS: readonly string[]
|
|
273
|
+
export declare const EXECUTION_REQUIREMENT_KEYS: readonly string[]
|
|
274
|
+
export declare const EXECUTION_POLICY_KEYS: readonly string[]
|
|
275
|
+
export declare const ELIGIBLE_CANDIDATE_KEYS: readonly string[]
|
|
276
|
+
export declare const BLOCKED_CANDIDATE_KEYS: readonly string[]
|
|
277
|
+
export declare const EXECUTION_ATTEMPT_KEYS: readonly string[]
|
|
278
|
+
export declare const EXECUTION_EVENT_KEYS: readonly string[]
|
|
279
|
+
export declare const EXECUTION_RECORD_KEYS: readonly string[]
|
|
280
|
+
export declare const EXTERNAL_EXECUTION_REPORT_KEYS: readonly string[]
|
|
281
|
+
/** Sorted field paths, nested ones as `attempts[].provider`. Guarded additive. */
|
|
282
|
+
export declare const EXECUTION_RECORD_FIELDS: readonly string[]
|
|
283
|
+
export declare const EXTERNAL_EXECUTION_REPORT_FIELDS: readonly string[]
|
|
284
|
+
/** The widest identity that differs between an event's two ends, or null. */
|
|
285
|
+
export declare function fallbackScopeOf(event: Pick<ExecutionEvent,
|
|
286
|
+
'from_provider' | 'from_platform' | 'from_model_id' | 'to_provider' | 'to_platform' | 'to_model_id'>): FallbackScope | null
|
|
287
|
+
export declare function validateExecutionRecord(record: unknown): ValidationResult
|
|
288
|
+
export declare function validateExternalExecutionReport(report: unknown): ValidationResult
|