@principles/core 1.289.1 → 1.290.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/runtime-v2/activation/activation-dispatcher.d.ts +4 -0
  2. package/dist/runtime-v2/activation/activation-dispatcher.d.ts.map +1 -1
  3. package/dist/runtime-v2/activation/activation-dispatcher.js +215 -63
  4. package/dist/runtime-v2/activation/activation-dispatcher.js.map +1 -1
  5. package/dist/runtime-v2/activation/activation-types.d.ts +48 -0
  6. package/dist/runtime-v2/activation/activation-types.d.ts.map +1 -1
  7. package/dist/runtime-v2/activation/activation-types.js.map +1 -1
  8. package/dist/runtime-v2/activation/approval-completion-service.d.ts +9 -0
  9. package/dist/runtime-v2/activation/approval-completion-service.d.ts.map +1 -1
  10. package/dist/runtime-v2/activation/approval-completion-service.js +4 -1
  11. package/dist/runtime-v2/activation/approval-completion-service.js.map +1 -1
  12. package/dist/runtime-v2/activation/index.d.ts +4 -2
  13. package/dist/runtime-v2/activation/index.d.ts.map +1 -1
  14. package/dist/runtime-v2/activation/index.js +4 -1
  15. package/dist/runtime-v2/activation/index.js.map +1 -1
  16. package/dist/runtime-v2/activation/low-risk-writers.d.ts.map +1 -1
  17. package/dist/runtime-v2/activation/low-risk-writers.js +2 -1
  18. package/dist/runtime-v2/activation/low-risk-writers.js.map +1 -1
  19. package/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts +11 -0
  20. package/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts.map +1 -1
  21. package/dist/runtime-v2/activation/prompt-activation-reader-contract.js +12 -2
  22. package/dist/runtime-v2/activation/prompt-activation-reader-contract.js.map +1 -1
  23. package/dist/runtime-v2/activation/prompt-replacement.d.ts +79 -0
  24. package/dist/runtime-v2/activation/prompt-replacement.d.ts.map +1 -0
  25. package/dist/runtime-v2/activation/prompt-replacement.js +171 -0
  26. package/dist/runtime-v2/activation/prompt-replacement.js.map +1 -0
  27. package/dist/runtime-v2/activation/sqlite-activation-state-store.d.ts +17 -1
  28. package/dist/runtime-v2/activation/sqlite-activation-state-store.d.ts.map +1 -1
  29. package/dist/runtime-v2/activation/sqlite-activation-state-store.js +132 -0
  30. package/dist/runtime-v2/activation/sqlite-activation-state-store.js.map +1 -1
  31. package/dist/runtime-v2/candidate-audit.d.ts +19 -0
  32. package/dist/runtime-v2/candidate-audit.d.ts.map +1 -1
  33. package/dist/runtime-v2/candidate-audit.js +72 -12
  34. package/dist/runtime-v2/candidate-audit.js.map +1 -1
  35. package/dist/runtime-v2/index.d.ts +8 -2
  36. package/dist/runtime-v2/index.d.ts.map +1 -1
  37. package/dist/runtime-v2/index.js +8 -2
  38. package/dist/runtime-v2/index.js.map +1 -1
  39. package/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts +13 -1
  40. package/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts.map +1 -1
  41. package/dist/runtime-v2/internalization/scribe-prompt-builder.js +15 -2
  42. package/dist/runtime-v2/internalization/scribe-prompt-builder.js.map +1 -1
  43. package/dist/runtime-v2/intervention-evidence-normalizer.d.ts +28 -0
  44. package/dist/runtime-v2/intervention-evidence-normalizer.d.ts.map +1 -0
  45. package/dist/runtime-v2/intervention-evidence-normalizer.js +654 -0
  46. package/dist/runtime-v2/intervention-evidence-normalizer.js.map +1 -0
  47. package/dist/runtime-v2/pain-chain-read-model.d.ts.map +1 -1
  48. package/dist/runtime-v2/pain-chain-read-model.js +48 -14
  49. package/dist/runtime-v2/pain-chain-read-model.js.map +1 -1
  50. package/dist/runtime-v2/receipt-coverage.d.ts +8 -0
  51. package/dist/runtime-v2/receipt-coverage.d.ts.map +1 -1
  52. package/dist/runtime-v2/receipt-coverage.js +18 -0
  53. package/dist/runtime-v2/receipt-coverage.js.map +1 -1
  54. package/dist/runtime-v2/store/intervention-evidence-schema.d.ts +6 -0
  55. package/dist/runtime-v2/store/intervention-evidence-schema.d.ts.map +1 -0
  56. package/dist/runtime-v2/store/intervention-evidence-schema.js +122 -0
  57. package/dist/runtime-v2/store/intervention-evidence-schema.js.map +1 -0
  58. package/dist/runtime-v2/store/sqlite-connection.d.ts +8 -0
  59. package/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
  60. package/dist/runtime-v2/store/sqlite-connection.js +62 -3
  61. package/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
  62. package/dist/runtime-v2/store/sqlite-intervention-evidence-store.d.ts +67 -0
  63. package/dist/runtime-v2/store/sqlite-intervention-evidence-store.d.ts.map +1 -0
  64. package/dist/runtime-v2/store/sqlite-intervention-evidence-store.js +737 -0
  65. package/dist/runtime-v2/store/sqlite-intervention-evidence-store.js.map +1 -0
  66. package/dist/runtime-v2/types/event-types.d.ts +6 -0
  67. package/dist/runtime-v2/types/event-types.d.ts.map +1 -1
  68. package/dist/runtime-v2/types/event-types.js +2 -0
  69. package/dist/runtime-v2/types/event-types.js.map +1 -1
  70. package/dist/runtime-v2/types/intervention-evidence-contract.d.ts +287 -0
  71. package/dist/runtime-v2/types/intervention-evidence-contract.d.ts.map +1 -0
  72. package/dist/runtime-v2/types/intervention-evidence-contract.js +84 -0
  73. package/dist/runtime-v2/types/intervention-evidence-contract.js.map +1 -0
  74. package/package.json +5 -1
@@ -0,0 +1,654 @@
1
+ /**
2
+ * Intervention Evidence Normalizer — deterministic validation and
3
+ * canonicalization for the Phase 1 evidence ingress (ADR-0027 §2.2–2.4,
4
+ * receipt-design SPEC §13.3–13.5).
5
+ *
6
+ * Trust boundary: a batch arrives as `unknown` runtime data (rc-1) and is
7
+ * validated here — never `as`-cast past validation (rc-2), required facts
8
+ * fail explicitly (rc-3), and unknown fields are rejected, not skipped.
9
+ * Pure module: no I/O, no clocks, no randomness. Replaying the same source
10
+ * batch yields the same normalized records, the same evidenceIds and the
11
+ * same recordDigests — that determinism is what store-level idempotency and
12
+ * source-conflict detection are built on.
13
+ *
14
+ * Cross-field rules deliberately encode the anti-forgery boundary:
15
+ * - `delivered` requires a confirmation ≥ host_accepted; prepared/submitted
16
+ * stay delivery-unknown attempts;
17
+ * - `runtime_verified` applications require a trusted activation reference
18
+ * AND an explicit enforcement boundary — an agent's self-report can never
19
+ * satisfy either;
20
+ * - effects require an exact episode reference; nothing is associated by
21
+ * session proximity or text similarity.
22
+ */
23
+ import { createHash } from 'node:crypto';
24
+ import { DELIVERED_CONFIRMATIONS, INTERVENTION_CAPABILITIES, INTERVENTION_RECORD_KINDS, INTERVENTION_SOURCE_KINDS, INTERVENTION_TEXT_BOUNDS, } from './types/intervention-evidence-contract.js';
25
+ const RECORD_KINDS = new Set(INTERVENTION_RECORD_KINDS);
26
+ const SOURCE_KINDS = new Set(INTERVENTION_SOURCE_KINDS);
27
+ const DELIVERED_CONFIRMATION_SET = new Set(DELIVERED_CONFIRMATIONS);
28
+ const DELIVERY_OUTCOMES = new Set(['attempted', 'delivered', 'failed', 'unsupported']);
29
+ const DELIVERY_TARGETS = new Set(['agent_context', 'runtime_enforcement']);
30
+ const CONFIRMATIONS = new Set(['prepared', 'submitted', 'host_accepted', 'context_present', 'runtime_loaded']);
31
+ const PROOF_METHODS = new Set(['agent_claimed', 'runtime_verified']);
32
+ const RUNTIME_VERIFIED_ACTIONS = new Set(['tool_blocked', 'auto_correct_applied']);
33
+ const EPISODE_STATUSES = new Set(['open', 'closed', 'interrupted']);
34
+ const EFFECT_STATUSES = new Set(['observed', 'disputed', 'invalidated']);
35
+ const OUTCOME_SOURCES = new Set(['authorized_result_observation', 'owner_feedback']);
36
+ const CAPABILITY_STATUSES = new Set(['supported', 'unsupported', 'unknown']);
37
+ const CAPABILITY_SET = new Set(INTERVENTION_CAPABILITIES);
38
+ const HOST_KINDS = new Set(['openclaw', 'codex']);
39
+ const BATCH_KEYS = new Set([
40
+ 'evidenceScopeId', 'sourceKind', 'adapterVersion', 'recordedAt',
41
+ 'observations', 'capabilityDeclarations',
42
+ ]);
43
+ const OBSERVATION_KEYS = new Set([
44
+ 'observationKey', 'sourceLocator', 'kind', 'occurredAt', 'nativeRefs',
45
+ 'principleId', 'contentRef', 'activationId', 'activationRef', 'deliveryKey', 'episodeKey',
46
+ 'effectKey', 'correctionOf', 'correctionReason', 'payload',
47
+ ]);
48
+ const NATIVE_REF_KEYS = new Set([
49
+ 'hostKind', 'sessionId', 'runId', 'turnId', 'toolCallId', 'toolName', 'rolloutIdentity',
50
+ ]);
51
+ const CONTENT_REF_KEYS = new Set([
52
+ 'principleId', 'artifactId', 'version', 'payloadDigest', 'approvalRef', 'resolution',
53
+ ]);
54
+ const ACTIVATION_REF_KEYS = new Set([
55
+ 'activationId', 'idempotencyKey', 'artifactId', 'channel', 'activatedAt', 'sourceSnapshotDigest',
56
+ ]);
57
+ const PAYLOAD_KEYS_BY_KIND = {
58
+ delivery: new Set(['targetKind', 'confirmation', 'outcome', 'failureReason', 'nonAttemptReason', 'unsupportedNote']),
59
+ application: new Set(['proofMethod', 'action', 'claimText', 'enforcementBoundary']),
60
+ behavior_episode: new Set(['status', 'actionSummary', 'inputPreview', 'resultSummary']),
61
+ effect: new Set(['status', 'observationSummary', 'disputeReason']),
62
+ outcome: new Set(['outcomeSource', 'observationSummary', 'feedbackText', 'actorId']),
63
+ };
64
+ const CAPABILITY_KEYS = new Set(['hostKind', 'capability', 'status', 'adapterVersion', 'channel', 'maxConfirmation', 'note']);
65
+ function isRecord(value) {
66
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
67
+ }
68
+ function rejectUnknownFields(obj, allowed, context) {
69
+ for (const key of Object.keys(obj)) {
70
+ if (!allowed.has(key))
71
+ return `${context}_unknown_field:${key}`;
72
+ }
73
+ return null;
74
+ }
75
+ function isIsoTimestamp(value) {
76
+ const match = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,3}))?Z$/.exec(value);
77
+ if (!match)
78
+ return false;
79
+ const milliseconds = (match[2] ?? '').padEnd(3, '0');
80
+ const parsed = Date.parse(value);
81
+ return Number.isFinite(parsed) && new Date(parsed).toISOString() === `${match[1]}.${milliseconds}Z`;
82
+ }
83
+ function isSha256Digest(value) {
84
+ return /^sha256:[a-f0-9]{64}$/.test(value);
85
+ }
86
+ function stringChecks(context) {
87
+ const read = (obj, key, bound) => {
88
+ if (!Object.hasOwn(obj, key) || obj[key] === undefined)
89
+ return {};
90
+ const val = obj[key];
91
+ if (typeof val !== 'string')
92
+ return { error: `${context}_${key}_not_a_string` };
93
+ if (val.length === 0)
94
+ return { error: `${context}_${key}_empty` };
95
+ if (val.length > bound)
96
+ return { error: `${context}_${key}_too_long:${val.length}>${bound}` };
97
+ return { value: val };
98
+ };
99
+ return {
100
+ read,
101
+ require(obj, key, bound) {
102
+ const raw = read(obj, key, bound);
103
+ if (raw.error || raw.value === undefined) {
104
+ return { value: '', error: raw.error ?? `${context}_${key}_missing` };
105
+ }
106
+ return { value: raw.value };
107
+ },
108
+ };
109
+ }
110
+ function enumChecks(context) {
111
+ const read = (obj, key, allowed) => {
112
+ if (!Object.hasOwn(obj, key) || obj[key] === undefined)
113
+ return {};
114
+ const val = obj[key];
115
+ if (typeof val !== 'string' || !allowed.has(val)) {
116
+ return { error: `${context}_${key}_invalid:${String(val)}` };
117
+ }
118
+ return { value: val };
119
+ };
120
+ return {
121
+ read,
122
+ require(obj, key, allowed) {
123
+ const raw = read(obj, key, allowed);
124
+ if (raw.error || raw.value === undefined) {
125
+ return { value: '', error: raw.error ?? `${context}_${key}_missing` };
126
+ }
127
+ return { value: raw.value };
128
+ },
129
+ };
130
+ }
131
+ /** Deterministic key minting for producers: joins parts with '|', mapping absent parts to '-'. */
132
+ export function mintObservationKey(parts) {
133
+ return parts.map((p) => (p === undefined || p === '' ? '-' : p)).join('|');
134
+ }
135
+ function sha256Hex(value) {
136
+ return createHash('sha256').update(value, 'utf8').digest('hex');
137
+ }
138
+ /** Canonical JSON: recursively key-sorted, no whitespace — stable across replays. */
139
+ export function canonicalJson(value) {
140
+ if (Array.isArray(value)) {
141
+ return `[${value.map((v) => canonicalJson(v)).join(',')}]`;
142
+ }
143
+ if (isRecord(value)) {
144
+ const keys = Object.keys(value).sort();
145
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`;
146
+ }
147
+ return JSON.stringify(value) ?? 'null';
148
+ }
149
+ export function computeEvidenceId(sourceKind, sourceLocator, observationKey) {
150
+ return `sha256:${sha256Hex(`${sourceKind}|${sourceLocator}|${observationKey}`)}`;
151
+ }
152
+ /**
153
+ * Record content fingerprint. Deliberately EXCLUDES evidenceId (derived from
154
+ * the identity triple), evidenceScopeId (rewritten by the ingress to the
155
+ * workspace scope) and recordedAt (batch record time): two observations of
156
+ * the SAME fact recorded at different times or through different producer
157
+ * scope placeholders are the same content — only a change in the observed
158
+ * fact itself is a source conflict.
159
+ */
160
+ const DIGEST_EXCLUDED_KEYS = new Set(['evidenceId', 'evidenceScopeId', 'recordedAt']);
161
+ export function computeRecordDigest(record) {
162
+ const factView = {};
163
+ for (const [key, value] of Object.entries(record)) {
164
+ if (!DIGEST_EXCLUDED_KEYS.has(key))
165
+ factView[key] = value;
166
+ }
167
+ return `sha256:${sha256Hex(canonicalJson(factView))}`;
168
+ }
169
+ function normalizeNativeRefs(raw) {
170
+ if (!isRecord(raw))
171
+ return { error: 'observation_nativeRefs_not_an_object' };
172
+ const err = rejectUnknownFields(raw, NATIVE_REF_KEYS, 'observation_nativeRefs');
173
+ if (err)
174
+ return { error: err };
175
+ const enums = enumChecks('observation_nativeRefs');
176
+ const strings = stringChecks('observation_nativeRefs');
177
+ const hostKind = enums.require(raw, 'hostKind', HOST_KINDS);
178
+ if (hostKind.error)
179
+ return { error: hostKind.error };
180
+ const value = { hostKind: hostKind.value };
181
+ for (const key of ['sessionId', 'runId', 'turnId', 'toolCallId', 'toolName', 'rolloutIdentity']) {
182
+ const field = strings.read(raw, key, 256);
183
+ if (field.error)
184
+ return { error: field.error };
185
+ if (field.value !== undefined)
186
+ value[key] = field.value;
187
+ }
188
+ return { value };
189
+ }
190
+ function normalizeContentRef(raw) {
191
+ if (!isRecord(raw))
192
+ return { error: 'observation_contentRef_not_an_object' };
193
+ const err = rejectUnknownFields(raw, CONTENT_REF_KEYS, 'observation_contentRef');
194
+ if (err)
195
+ return { error: err };
196
+ const strings = stringChecks('observation_contentRef');
197
+ const enums = enumChecks('observation_contentRef');
198
+ const principleId = strings.require(raw, 'principleId', 256);
199
+ if (principleId.error)
200
+ return { error: principleId.error };
201
+ const resolution = enums.require(raw, 'resolution', new Set(['resolved', 'revision_reference_unresolved']));
202
+ if (resolution.error)
203
+ return { error: resolution.error };
204
+ const payloadDigest = strings.read(raw, 'payloadDigest', 256);
205
+ if (payloadDigest.error)
206
+ return { error: payloadDigest.error };
207
+ if (resolution.value === 'resolved' && payloadDigest.value === undefined) {
208
+ return { error: 'observation_contentRef_resolved_requires_payloadDigest' };
209
+ }
210
+ if (payloadDigest.value !== undefined && !isSha256Digest(payloadDigest.value)) {
211
+ return { error: 'observation_contentRef_payloadDigest_not_sha256' };
212
+ }
213
+ const value = {
214
+ principleId: principleId.value,
215
+ resolution: resolution.value,
216
+ };
217
+ if (payloadDigest.value !== undefined)
218
+ value.payloadDigest = payloadDigest.value;
219
+ for (const key of ['artifactId', 'version', 'approvalRef']) {
220
+ const field = strings.read(raw, key, 256);
221
+ if (field.error)
222
+ return { error: field.error };
223
+ if (field.value !== undefined)
224
+ value[key] = field.value;
225
+ }
226
+ return { value };
227
+ }
228
+ function normalizeActivationRef(raw) {
229
+ if (!isRecord(raw))
230
+ return { error: 'observation_activationRef_not_an_object' };
231
+ const err = rejectUnknownFields(raw, ACTIVATION_REF_KEYS, 'observation_activationRef');
232
+ if (err)
233
+ return { error: err };
234
+ const strings = stringChecks('observation_activationRef');
235
+ const activationId = strings.require(raw, 'activationId', 256);
236
+ if (activationId.error)
237
+ return { error: activationId.error };
238
+ const sourceSnapshotDigest = strings.require(raw, 'sourceSnapshotDigest', 256);
239
+ if (sourceSnapshotDigest.error)
240
+ return { error: sourceSnapshotDigest.error };
241
+ if (!isSha256Digest(sourceSnapshotDigest.value)) {
242
+ return { error: 'observation_activationRef_sourceSnapshotDigest_not_sha256' };
243
+ }
244
+ const value = {
245
+ activationId: activationId.value,
246
+ sourceSnapshotDigest: sourceSnapshotDigest.value,
247
+ };
248
+ for (const key of ['idempotencyKey', 'artifactId', 'channel']) {
249
+ const field = strings.read(raw, key, 256);
250
+ if (field.error)
251
+ return { error: field.error };
252
+ if (field.value !== undefined)
253
+ value[key] = field.value;
254
+ }
255
+ if (Object.hasOwn(raw, 'activatedAt') && raw.activatedAt !== undefined) {
256
+ if (typeof raw.activatedAt !== 'string' || !isIsoTimestamp(raw.activatedAt)) {
257
+ return { error: 'observation_activationRef_activatedAt_not_iso_utc' };
258
+ }
259
+ value.activatedAt = raw.activatedAt;
260
+ }
261
+ return { value };
262
+ }
263
+ function normalizePayload(kind, raw) {
264
+ if (!isRecord(raw))
265
+ return { error: `observation_payload_${kind}_not_an_object` };
266
+ const err = rejectUnknownFields(raw, PAYLOAD_KEYS_BY_KIND[kind], `observation_payload_${kind}`);
267
+ if (err)
268
+ return { error: err };
269
+ const strings = stringChecks(`observation_payload_${kind}`);
270
+ const enums = enumChecks(`observation_payload_${kind}`);
271
+ if (kind === 'delivery') {
272
+ const targetKind = enums.require(raw, 'targetKind', DELIVERY_TARGETS);
273
+ if (targetKind.error)
274
+ return { error: targetKind.error };
275
+ const confirmation = enums.require(raw, 'confirmation', CONFIRMATIONS);
276
+ if (confirmation.error)
277
+ return { error: confirmation.error };
278
+ const outcome = enums.require(raw, 'outcome', DELIVERY_OUTCOMES);
279
+ if (outcome.error)
280
+ return { error: outcome.error };
281
+ if (outcome.value === 'delivered' && !DELIVERED_CONFIRMATION_SET.has(confirmation.value)) {
282
+ return { error: 'delivery_delivered_requires_confirmed_boundary' };
283
+ }
284
+ const value = {
285
+ targetKind: targetKind.value,
286
+ confirmation: confirmation.value,
287
+ outcome: outcome.value,
288
+ };
289
+ for (const key of ['failureReason', 'nonAttemptReason', 'unsupportedNote']) {
290
+ const field = strings.read(raw, key, INTERVENTION_TEXT_BOUNDS.reason);
291
+ if (field.error)
292
+ return { error: field.error };
293
+ if (field.value !== undefined)
294
+ value[key] = field.value;
295
+ }
296
+ if (value.outcome === 'failed' && !value.failureReason) {
297
+ return { error: 'delivery_failed_requires_failure_reason' };
298
+ }
299
+ if (value.outcome === 'unsupported' && !value.unsupportedNote) {
300
+ return { error: 'delivery_unsupported_requires_boundary_note' };
301
+ }
302
+ return { value };
303
+ }
304
+ if (kind === 'application') {
305
+ const proofMethod = enums.require(raw, 'proofMethod', PROOF_METHODS);
306
+ if (proofMethod.error)
307
+ return { error: proofMethod.error };
308
+ const action = enums.require(raw, 'action', new Set(['self_reported', 'tool_blocked', 'auto_correct_applied']));
309
+ if (action.error)
310
+ return { error: action.error };
311
+ const value = { proofMethod: proofMethod.value, action: action.value };
312
+ const claim = strings.read(raw, 'claimText', INTERVENTION_TEXT_BOUNDS.claimText);
313
+ if (claim.error)
314
+ return { error: claim.error };
315
+ if (claim.value !== undefined)
316
+ value.claimText = claim.value;
317
+ const boundary = strings.read(raw, 'enforcementBoundary', 128);
318
+ if (boundary.error)
319
+ return { error: boundary.error };
320
+ if (boundary.value !== undefined)
321
+ value.enforcementBoundary = boundary.value;
322
+ if (proofMethod.value === 'runtime_verified') {
323
+ if (!RUNTIME_VERIFIED_ACTIONS.has(action.value)) {
324
+ return { error: 'runtime_verified_requires_runtime_action' };
325
+ }
326
+ if (!value.enforcementBoundary) {
327
+ return { error: 'runtime_verified_requires_enforcement_boundary' };
328
+ }
329
+ }
330
+ else {
331
+ if (action.value !== 'self_reported')
332
+ return { error: 'agent_claimed_requires_self_reported_action' };
333
+ if (!value.claimText)
334
+ return { error: 'agent_claimed_requires_claim_text' };
335
+ if (value.enforcementBoundary !== undefined) {
336
+ return { error: 'agent_claimed_forbids_enforcement_boundary' };
337
+ }
338
+ }
339
+ return { value };
340
+ }
341
+ if (kind === 'behavior_episode') {
342
+ const status = enums.require(raw, 'status', EPISODE_STATUSES);
343
+ if (status.error)
344
+ return { error: status.error };
345
+ const actionSummary = strings.require(raw, 'actionSummary', INTERVENTION_TEXT_BOUNDS.summary);
346
+ if (actionSummary.error)
347
+ return { error: actionSummary.error };
348
+ const value = { status: status.value, actionSummary: actionSummary.value };
349
+ for (const key of ['inputPreview', 'resultSummary']) {
350
+ const field = strings.read(raw, key, INTERVENTION_TEXT_BOUNDS.summary);
351
+ if (field.error)
352
+ return { error: field.error };
353
+ if (field.value !== undefined)
354
+ value[key] = field.value;
355
+ }
356
+ return { value };
357
+ }
358
+ if (kind === 'effect') {
359
+ const status = enums.require(raw, 'status', EFFECT_STATUSES);
360
+ if (status.error)
361
+ return { error: status.error };
362
+ const observationSummary = strings.require(raw, 'observationSummary', INTERVENTION_TEXT_BOUNDS.summary);
363
+ if (observationSummary.error)
364
+ return { error: observationSummary.error };
365
+ const value = { status: status.value, observationSummary: observationSummary.value };
366
+ const dispute = strings.read(raw, 'disputeReason', INTERVENTION_TEXT_BOUNDS.reason);
367
+ if (dispute.error)
368
+ return { error: dispute.error };
369
+ if (dispute.value !== undefined)
370
+ value.disputeReason = dispute.value;
371
+ if (status.value !== 'observed' && !value.disputeReason) {
372
+ return { error: 'effect_disputed_or_invalidated_requires_dispute_reason' };
373
+ }
374
+ return { value };
375
+ }
376
+ // kind === 'outcome'
377
+ const outcomeSource = enums.require(raw, 'outcomeSource', OUTCOME_SOURCES);
378
+ if (outcomeSource.error)
379
+ return { error: outcomeSource.error };
380
+ const observationSummary = strings.require(raw, 'observationSummary', INTERVENTION_TEXT_BOUNDS.summary);
381
+ if (observationSummary.error)
382
+ return { error: observationSummary.error };
383
+ const value = { outcomeSource: outcomeSource.value, observationSummary: observationSummary.value };
384
+ const feedback = strings.read(raw, 'feedbackText', INTERVENTION_TEXT_BOUNDS.feedbackText);
385
+ if (feedback.error)
386
+ return { error: feedback.error };
387
+ if (feedback.value !== undefined)
388
+ value.feedbackText = feedback.value;
389
+ const actor = strings.read(raw, 'actorId', 256);
390
+ if (actor.error)
391
+ return { error: actor.error };
392
+ if (actor.value !== undefined)
393
+ value.actorId = actor.value;
394
+ if (outcomeSource.value === 'owner_feedback' && !value.actorId) {
395
+ return { error: 'owner_feedback_requires_resolved_actor' };
396
+ }
397
+ return { value };
398
+ }
399
+ function normalizeObservation(raw, ctx) {
400
+ if (!isRecord(raw))
401
+ return { error: 'observation_not_an_object' };
402
+ const fieldErr = rejectUnknownFields(raw, OBSERVATION_KEYS, 'observation');
403
+ if (fieldErr)
404
+ return { error: fieldErr };
405
+ const strings = stringChecks('observation');
406
+ const enums = enumChecks('observation');
407
+ const observationKey = strings.require(raw, 'observationKey', INTERVENTION_TEXT_BOUNDS.observationKey);
408
+ if (observationKey.error)
409
+ return { error: observationKey.error };
410
+ const sourceLocator = strings.require(raw, 'sourceLocator', INTERVENTION_TEXT_BOUNDS.sourceLocator);
411
+ if (sourceLocator.error)
412
+ return { error: sourceLocator.error };
413
+ const kind = enums.require(raw, 'kind', RECORD_KINDS);
414
+ if (kind.error)
415
+ return { error: kind.error };
416
+ const nativeRefs = normalizeNativeRefs(raw.nativeRefs);
417
+ if (nativeRefs.error || !nativeRefs.value)
418
+ return { error: nativeRefs.error ?? 'observation_nativeRefs_missing' };
419
+ const payload = normalizePayload(kind.value, raw.payload);
420
+ if (payload.error || !payload.value)
421
+ return { error: payload.error ?? 'observation_payload_missing' };
422
+ if (ctx.sourceKind === 'codex_pd_hook_event_log' && isRecord(raw.payload)) {
423
+ for (const sensitiveField of ['claimText', 'inputPreview', 'feedbackText']) {
424
+ if (Object.hasOwn(raw.payload, sensitiveField)) {
425
+ return { error: `codex_hook_evidence_forbids_source_content:${sensitiveField}` };
426
+ }
427
+ }
428
+ }
429
+ const record = {
430
+ evidenceId: computeEvidenceId(ctx.sourceKind, sourceLocator.value, observationKey.value),
431
+ evidenceScopeId: ctx.evidenceScopeId,
432
+ sourceKind: ctx.sourceKind,
433
+ observationKey: observationKey.value,
434
+ sourceLocator: sourceLocator.value,
435
+ kind: kind.value,
436
+ recordedAt: ctx.recordedAt,
437
+ nativeRefs: nativeRefs.value,
438
+ payload: payload.value,
439
+ };
440
+ if (Object.hasOwn(raw, 'occurredAt') && raw.occurredAt !== undefined) {
441
+ if (typeof raw.occurredAt !== 'string' || !isIsoTimestamp(raw.occurredAt)) {
442
+ return { error: 'observation_occurredAt_not_iso_utc' };
443
+ }
444
+ if (Date.parse(raw.occurredAt) > Date.parse(ctx.recordedAt) + 5 * 60 * 1000) {
445
+ return { error: 'observation_occurredAt_after_recordedAt' };
446
+ }
447
+ record.occurredAt = raw.occurredAt;
448
+ }
449
+ for (const key of ['principleId', 'deliveryKey', 'episodeKey', 'effectKey', 'correctionOf']) {
450
+ const field = strings.read(raw, key, INTERVENTION_TEXT_BOUNDS.observationKey);
451
+ if (field.error)
452
+ return { error: field.error };
453
+ if (field.value !== undefined)
454
+ record[key] = field.value;
455
+ }
456
+ const correctionReason = strings.read(raw, 'correctionReason', INTERVENTION_TEXT_BOUNDS.reason);
457
+ if (correctionReason.error)
458
+ return { error: correctionReason.error };
459
+ if (correctionReason.value !== undefined)
460
+ record.correctionReason = correctionReason.value;
461
+ if ((record.correctionOf !== undefined) !== (record.correctionReason !== undefined)) {
462
+ return { error: 'correction_requires_both_reference_and_reason' };
463
+ }
464
+ if (Object.hasOwn(raw, 'contentRef') && raw.contentRef !== undefined) {
465
+ const contentRef = normalizeContentRef(raw.contentRef);
466
+ if (contentRef.error || !contentRef.value)
467
+ return { error: contentRef.error ?? 'observation_contentRef_invalid' };
468
+ record.contentRef = contentRef.value;
469
+ }
470
+ if (Object.hasOwn(raw, 'activationRef') && raw.activationRef !== undefined) {
471
+ const activationRef = normalizeActivationRef(raw.activationRef);
472
+ if (activationRef.error || !activationRef.value)
473
+ return { error: activationRef.error ?? 'observation_activationRef_invalid' };
474
+ record.activationRef = activationRef.value;
475
+ }
476
+ const activationId = strings.read(raw, 'activationId', 256);
477
+ if (activationId.error)
478
+ return { error: activationId.error };
479
+ if (activationId.value !== undefined && record.activationRef
480
+ && activationId.value !== record.activationRef.activationId) {
481
+ return { error: 'observation_activation_id_mismatch' };
482
+ }
483
+ if (activationId.value !== undefined && !record.activationRef) {
484
+ record.activationId = activationId.value;
485
+ }
486
+ if (record.contentRef && record.principleId !== undefined && record.contentRef.principleId !== record.principleId) {
487
+ return { error: 'content_reference_principle_mismatch' };
488
+ }
489
+ if (record.activationRef?.activatedAt && record.occurredAt && Date.parse(record.activationRef.activatedAt) > Date.parse(record.occurredAt)) {
490
+ return { error: 'activation_reference_occurs_after_evidence' };
491
+ }
492
+ // ── Cross-field relationship rules (SPEC §13.3) ──
493
+ if (record.kind === 'delivery') {
494
+ if (!record.contentRef || !record.activationRef) {
495
+ return { error: 'delivery_requires_content_and_activation_reference' };
496
+ }
497
+ }
498
+ if (record.kind === 'application') {
499
+ const proof = record.payload.proofMethod;
500
+ if (proof === 'runtime_verified' && !record.activationRef) {
501
+ return { error: 'runtime_verified_application_requires_activation_reference' };
502
+ }
503
+ if (!record.principleId) {
504
+ return { error: 'application_requires_principle_reference' };
505
+ }
506
+ }
507
+ if (record.kind === 'behavior_episode') {
508
+ const refs = record.nativeRefs;
509
+ const hasNativeEventKey = refs.toolCallId !== undefined
510
+ || refs.turnId !== undefined
511
+ || refs.runId !== undefined
512
+ || refs.rolloutIdentity !== undefined;
513
+ if (!hasNativeEventKey) {
514
+ return { error: 'episode_requires_native_event_key' };
515
+ }
516
+ }
517
+ if (record.kind === 'effect') {
518
+ if (!record.episodeKey || !record.principleId || !record.contentRef) {
519
+ return { error: 'effect_requires_episode_principle_and_content_reference' };
520
+ }
521
+ if (record.episodeKey === record.observationKey) {
522
+ return { error: 'effect_cannot_reference_itself' };
523
+ }
524
+ }
525
+ if (record.kind === 'outcome' && !record.episodeKey) {
526
+ return { error: 'outcome_requires_episode_reference' };
527
+ }
528
+ return { value: { ...record, recordDigest: computeRecordDigest(record) } };
529
+ }
530
+ function normalizeCapabilityDeclaration(raw) {
531
+ if (!isRecord(raw))
532
+ return { error: 'capability_not_an_object' };
533
+ const err = rejectUnknownFields(raw, CAPABILITY_KEYS, 'capability');
534
+ if (err)
535
+ return { error: err };
536
+ const strings = stringChecks('capability');
537
+ const enums = enumChecks('capability');
538
+ const hostKind = enums.require(raw, 'hostKind', HOST_KINDS);
539
+ if (hostKind.error)
540
+ return { error: hostKind.error };
541
+ const capability = enums.require(raw, 'capability', CAPABILITY_SET);
542
+ if (capability.error)
543
+ return { error: capability.error };
544
+ const status = enums.require(raw, 'status', CAPABILITY_STATUSES);
545
+ if (status.error)
546
+ return { error: status.error };
547
+ const adapterVersion = strings.require(raw, 'adapterVersion', INTERVENTION_TEXT_BOUNDS.adapterVersion);
548
+ if (adapterVersion.error)
549
+ return { error: adapterVersion.error };
550
+ const channel = strings.require(raw, 'channel', 128);
551
+ if (channel.error)
552
+ return { error: channel.error };
553
+ const value = {
554
+ hostKind: hostKind.value,
555
+ capability: capability.value,
556
+ status: status.value,
557
+ adapterVersion: adapterVersion.value,
558
+ channel: channel.value,
559
+ };
560
+ if (Object.hasOwn(raw, 'maxConfirmation') && raw.maxConfirmation !== undefined) {
561
+ const confirmation = enums.require(raw, 'maxConfirmation', CONFIRMATIONS);
562
+ if (confirmation.error)
563
+ return { error: confirmation.error };
564
+ value.maxConfirmation = confirmation.value;
565
+ }
566
+ const note = strings.read(raw, 'note', INTERVENTION_TEXT_BOUNDS.reason);
567
+ if (note.error)
568
+ return { error: note.error };
569
+ if (note.value !== undefined)
570
+ value.note = note.value;
571
+ // SPEC §13.6: Supported requires a real proof reference — a bare "supported"
572
+ // without an evidence citation is exactly the false capability this forbids.
573
+ if (status.value === 'supported' && !value.note) {
574
+ return { error: 'capability_supported_requires_proof_note' };
575
+ }
576
+ return { value };
577
+ }
578
+ /**
579
+ * Validate and normalize one evidence batch. Never throws; the first
580
+ * validation failure is returned as a stable reason string.
581
+ */
582
+ export function normalizeInterventionEvidenceBatch(raw) {
583
+ if (!isRecord(raw))
584
+ return { ok: false, reason: 'batch_is_not_an_object' };
585
+ const fieldErr = rejectUnknownFields(raw, BATCH_KEYS, 'batch');
586
+ if (fieldErr)
587
+ return { ok: false, reason: fieldErr };
588
+ const batchStrings = stringChecks('batch');
589
+ const batchEnums = enumChecks('batch');
590
+ const scopeId = batchStrings.require(raw, 'evidenceScopeId', INTERVENTION_TEXT_BOUNDS.evidenceScopeId);
591
+ if (scopeId.error)
592
+ return { ok: false, reason: scopeId.error };
593
+ const sourceKind = batchEnums.require(raw, 'sourceKind', SOURCE_KINDS);
594
+ if (sourceKind.error)
595
+ return { ok: false, reason: sourceKind.error };
596
+ const adapterVersion = batchStrings.require(raw, 'adapterVersion', INTERVENTION_TEXT_BOUNDS.adapterVersion);
597
+ if (adapterVersion.error)
598
+ return { ok: false, reason: adapterVersion.error };
599
+ if (!Object.hasOwn(raw, 'recordedAt') || typeof raw.recordedAt !== 'string' || !isIsoTimestamp(raw.recordedAt)) {
600
+ return { ok: false, reason: 'batch_recordedAt_missing_or_not_iso_utc' };
601
+ }
602
+ if (Date.parse(raw.recordedAt) > Date.now() + 5 * 60 * 1000) {
603
+ return { ok: false, reason: 'batch_recordedAt_in_future' };
604
+ }
605
+ if (!Array.isArray(raw.observations))
606
+ return { ok: false, reason: 'batch_observations_not_an_array' };
607
+ if (raw.observations.length > 64)
608
+ return { ok: false, reason: `batch_observations_too_large:${raw.observations.length}>64` };
609
+ const ctx = { sourceKind: sourceKind.value, evidenceScopeId: scopeId.value, recordedAt: raw.recordedAt };
610
+ const records = [];
611
+ const seenKeys = new Set();
612
+ for (const observationRaw of raw.observations) {
613
+ const normalized = normalizeObservation(observationRaw, ctx);
614
+ if (normalized.error || !normalized.value) {
615
+ return { ok: false, reason: normalized.error ?? 'observation_invalid' };
616
+ }
617
+ const naturalKey = `${normalized.value.sourceKind}|${normalized.value.sourceLocator}|${normalized.value.observationKey}`;
618
+ if (seenKeys.has(naturalKey)) {
619
+ return { ok: false, reason: `batch_duplicate_observation_key:${normalized.value.observationKey}` };
620
+ }
621
+ seenKeys.add(naturalKey);
622
+ records.push(normalized.value);
623
+ }
624
+ const capabilityDeclarations = [];
625
+ if (Object.hasOwn(raw, 'capabilityDeclarations') && raw.capabilityDeclarations !== undefined) {
626
+ if (!Array.isArray(raw.capabilityDeclarations)) {
627
+ return { ok: false, reason: 'batch_capabilityDeclarations_not_an_array' };
628
+ }
629
+ const seenCapabilities = new Set();
630
+ for (const capRaw of raw.capabilityDeclarations) {
631
+ const cap = normalizeCapabilityDeclaration(capRaw);
632
+ if (cap.error || !cap.value)
633
+ return { ok: false, reason: cap.error ?? 'capability_invalid' };
634
+ const capKey = `${cap.value.hostKind}|${cap.value.capability}`;
635
+ if (seenCapabilities.has(capKey)) {
636
+ return { ok: false, reason: `batch_duplicate_capability:${capKey}` };
637
+ }
638
+ seenCapabilities.add(capKey);
639
+ capabilityDeclarations.push(cap.value);
640
+ }
641
+ }
642
+ return {
643
+ ok: true,
644
+ batch: {
645
+ evidenceScopeId: scopeId.value,
646
+ sourceKind: sourceKind.value,
647
+ adapterVersion: adapterVersion.value,
648
+ recordedAt: ctx.recordedAt,
649
+ records,
650
+ capabilityDeclarations,
651
+ },
652
+ };
653
+ }
654
+ //# sourceMappingURL=intervention-evidence-normalizer.js.map