@peasant-labs/schema 0.17.0 → 0.19.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 (28) hide show
  1. package/README.md +4 -0
  2. package/dist/fixtures/session-graph-capability.d.ts +454 -0
  3. package/dist/fixtures/session-graph-capability.js +4 -0
  4. package/dist/fixtures/session-graph.d.ts +7349 -0
  5. package/dist/fixtures/session-graph.js +4 -0
  6. package/dist/fixtures.d.ts +2 -0
  7. package/dist/fixtures.js +2 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +227 -39
  10. package/dist/internal/generated/content-capabilities.gen.d.ts +7 -0
  11. package/dist/internal/generated/content-capabilities.gen.js +48 -2
  12. package/dist/internal/generated/contract/zod.gen.d.ts +9558 -1328
  13. package/dist/internal/generated/contract/zod.gen.js +597 -27
  14. package/dist/internal/generated/enums.gen.d.ts +136 -0
  15. package/dist/internal/generated/enums.gen.js +149 -0
  16. package/dist/internal/generated/local-api.d.ts +308 -6
  17. package/dist/internal/generated/metadata-limits.gen.d.ts +1 -0
  18. package/dist/internal/generated/metadata-limits.gen.js +2 -0
  19. package/dist/internal/generated/public-contract.gen.d.ts +2 -2
  20. package/dist/internal/generated/public-contract.gen.js +1 -1
  21. package/dist/internal/generated/session-graph-capability-fixtures.gen.d.ts +454 -0
  22. package/dist/internal/generated/session-graph-capability-fixtures.gen.js +570 -0
  23. package/dist/internal/generated/session-graph-fixtures.gen.d.ts +7349 -0
  24. package/dist/internal/generated/session-graph-fixtures.gen.js +8052 -0
  25. package/dist/internal/generated/versions.gen.d.ts +4 -4
  26. package/dist/internal/generated/versions.gen.js +4 -4
  27. package/dist/internal/generated/village-api.d.ts +512 -8
  28. package/package.json +9 -1
@@ -0,0 +1,4 @@
1
+ import { canonicalSessionGraphFixtures } from "../internal/generated/session-graph-fixtures.gen.js";
2
+ export function sessionGraphFixtures() {
3
+ return structuredClone(canonicalSessionGraphFixtures);
4
+ }
@@ -1,2 +1,4 @@
1
1
  export * from "./fixtures/quality.js";
2
2
  export * from "./fixtures/timeline.js";
3
+ export * from "./fixtures/session-graph.js";
4
+ export * from "./fixtures/session-graph-capability.js";
package/dist/fixtures.js CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from "./fixtures/quality.js";
2
2
  export * from "./fixtures/timeline.js";
3
+ export * from "./fixtures/session-graph.js";
4
+ export * from "./fixtures/session-graph-capability.js";
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export interface RawJsonPathPolicy {
15
15
  export declare function scanRawJsonText(text: string, policy?: RawJsonPathPolicy): void;
16
16
  export declare function parseSessionDetailPayloadValue(value: unknown): import("./internal/generated/contract/zod.gen.js").SessionDetailPayload;
17
17
  export declare function parseSessionDetailPayloadText(text: string): import("./internal/generated/contract/zod.gen.js").SessionDetailPayload;
18
+ export declare function parseSchemaVersionAdvertisement(text: string): string[];
18
19
  export declare function parseTranscriptContentText(text: string): import("./internal/generated/contract/zod.gen.js").TranscriptContent;
19
20
  export declare function parseServerMessageRaw(text: string): import("./internal/generated/contract/zod.gen.js").ServerMessage;
20
21
  type Detail = import("./internal/generated/contract/zod.gen.js").SessionDetailPayload;
package/dist/index.js CHANGED
@@ -2,7 +2,9 @@ export * from "./internal/generated/public-contract.gen.js";
2
2
  export * from "./internal/generated/enums.gen.js";
3
3
  export * from "./internal/generated/content-capabilities.gen.js";
4
4
  export * from "./internal/generated/versions.gen.js";
5
- import { zNativeMetadataRecord, zTurnDetail, zProjectHash, zServerMessage, zSessionDetailPayload, zTranscriptContent } from "./internal/generated/contract/zod.gen.js";
5
+ import { requiredContentCapabilities, KnownContentCapability } from "./internal/generated/content-capabilities.gen.js";
6
+ import { zNativeMetadataRecord, zTurnDetail, zProjectHash, zServerMessage, zSessionDetailPayload, zSessionDetailReadPayload, zTranscriptContent } from "./internal/generated/contract/zod.gen.js";
7
+ import { maxNativeMetadataStringBytes } from "./internal/generated/metadata-limits.gen.js";
6
8
  function assertProjectHash(value, operation) {
7
9
  if (!isProjectHash(value)) {
8
10
  const rendered = renderProjectHashInput(value);
@@ -48,20 +50,75 @@ function scanRawJson(text, policy) {
48
50
  }
49
51
  export function parseSessionDetailPayloadValue(value) {
50
52
  rejectExplicitNullEvidence(value);
51
- const payload = zSessionDetailPayload.parse(value);
53
+ validateRawGraphValue(value);
54
+ const normalized = structuredClone(value);
55
+ normalizeEmptyOptionalPublicRefs(normalized);
56
+ const payload = zSessionDetailPayload.parse(normalized);
52
57
  validateSessionDetail(payload);
53
58
  return payload;
54
59
  }
60
+ function normalizeEmptyOptionalPublicRefs(value) {
61
+ if (!isRecord(value))
62
+ return;
63
+ if (value.parentSessionId === null)
64
+ delete value.parentSessionId;
65
+ for (const relation of Array.isArray(value.relationships) ? value.relationships : []) {
66
+ if (isRecord(relation) && isRecord(relation.anchor)) {
67
+ if (relation.anchor.sourceEntryRef === "")
68
+ delete relation.anchor.sourceEntryRef;
69
+ if (relation.anchor.sourceRevisionRef === "")
70
+ delete relation.anchor.sourceRevisionRef;
71
+ }
72
+ }
73
+ const visitTurns = (turns) => {
74
+ if (!Array.isArray(turns))
75
+ return;
76
+ for (const turn of turns)
77
+ if (isRecord(turn)) {
78
+ if (turn.sourceEntryRef === "")
79
+ delete turn.sourceEntryRef;
80
+ for (const tool of Array.isArray(turn.toolCalls) ? turn.toolCalls : [])
81
+ if (isRecord(tool)) {
82
+ if (tool.callEntryRef === "")
83
+ delete tool.callEntryRef;
84
+ if (tool.resultEntryRef === "")
85
+ delete tool.resultEntryRef;
86
+ }
87
+ for (const provenance of [turn.provenance, ...(Array.isArray(turn.toolCalls) ? turn.toolCalls.flatMap(tool => isRecord(tool) ? [tool.callProvenance, tool.resultProvenance] : []) : [])]) {
88
+ if (isRecord(provenance) && provenance.submissionRef === "")
89
+ delete provenance.submissionRef;
90
+ }
91
+ }
92
+ };
93
+ visitTurns(value.turns);
94
+ for (const section of Array.isArray(value.earlierHistory) ? value.earlierHistory : [])
95
+ if (isRecord(section))
96
+ visitTurns(section.turns);
97
+ }
55
98
  export function parseSessionDetailPayloadText(text) {
56
99
  scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/nativeMetadata/*/data"] });
57
100
  return parseSessionDetailPayloadValue(JSON.parse(text));
58
101
  }
102
+ export function parseSchemaVersionAdvertisement(text) {
103
+ scanRawJsonText(text, { maxDocumentBytes: 1 << 20, maxDocumentDepth: 16 });
104
+ const value = JSON.parse(text);
105
+ if (!isRecord(value))
106
+ throw new TypeError("Schema capability validation failed at @peasant-labs/schema parseSchemaVersionAdvertisement during discovery decoding: response is not an object; callers cannot negotiate preservation support; send a JSON object and retry.");
107
+ if (value.contentCapabilities === null)
108
+ throw new TypeError("Schema capability validation failed at @peasant-labs/schema parseSchemaVersionAdvertisement during discovery decoding: contentCapabilities is null; callers cannot distinguish omission from malformed support; omit it or send an array of strings.");
109
+ if (value.contentCapabilities === undefined)
110
+ return [];
111
+ if (!Array.isArray(value.contentCapabilities) || value.contentCapabilities.some((token) => typeof token !== "string"))
112
+ throw new TypeError("Schema capability validation failed at @peasant-labs/schema parseSchemaVersionAdvertisement during discovery decoding: contentCapabilities is not an array of strings; callers cannot negotiate support; send opaque string tokens and retry.");
113
+ return [...value.contentCapabilities];
114
+ }
59
115
  export function parseTranscriptContentText(text) {
60
116
  scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/sessionDetail/nativeMetadata/*/data"] });
61
117
  const raw = JSON.parse(text);
62
- if (isRecord(raw))
63
- rejectExplicitNullEvidence(raw.sessionDetail);
64
- const content = zTranscriptContent.parse(raw);
118
+ if (!isRecord(raw))
119
+ return zTranscriptContent.parse(raw);
120
+ const normalizedEnvelope = { ...raw, sessionDetail: parseSessionDetailPayloadValue(raw.sessionDetail) };
121
+ const content = zTranscriptContent.parse(normalizedEnvelope);
65
122
  if (content.sessionDetail === undefined || content.sessionDetail === null)
66
123
  failSemantic("sessionDetail is required");
67
124
  parseSessionDetailPayloadValue(content.sessionDetail);
@@ -75,13 +132,10 @@ export function parseServerMessageRaw(text) {
75
132
  scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/data/nativeMetadata/*/data"] });
76
133
  }
77
134
  const value = JSON.parse(text);
78
- if (isRecord(value) && value.type === "session_detail")
79
- rejectExplicitNullEvidence(value.data);
80
- const message = zServerMessage.parse(value);
81
- if (message.type === "session_detail") {
82
- parseSessionDetailPayloadValue(message.data);
83
- }
84
- return message;
135
+ const normalized = isRecord(value) && value.type === "session_detail"
136
+ ? { ...value, data: parseSessionDetailReadPayloadValue(value.data) }
137
+ : value;
138
+ return zServerMessage.parse(normalized);
85
139
  }
86
140
  class RawJsonScanner {
87
141
  text;
@@ -124,8 +178,8 @@ class RawJsonScanner {
124
178
  const value = this.string();
125
179
  if (path === "/type")
126
180
  this.rootMessageType = value;
127
- if (opaque && new TextEncoder().encode(value).length > 16384)
128
- this.fail("metadata string exceeds 16 KiB");
181
+ if (opaque && new TextEncoder().encode(value).length > maxNativeMetadataStringBytes)
182
+ this.fail(`metadata string exceeds ${maxNativeMetadataStringBytes} UTF-8 bytes`);
129
183
  return;
130
184
  }
131
185
  if (c === "t")
@@ -243,29 +297,75 @@ class RawJsonScanner {
243
297
  }
244
298
  function pointerMatches(pattern, path) { const expected = pattern.split("/"); const actual = path.split("/"); return expected.length === actual.length && expected.every((part, index) => part === "*" || part === actual[index]); }
245
299
  function validateSessionDetail(payload) {
246
- const turns = validateTurnEvidence(payload.turns ?? []);
247
- if ((payload.nativeMetadata?.length ?? 0) > 0 && String(payload.harness) !== "pi")
248
- failSemantic("Pi native metadata requires harness pi");
249
- validateMetadataRecords(payload.nativeMetadata ?? [], turns);
300
+ validateRelationships(payload);
301
+ const state = newDetailState(payload);
302
+ const turns = validateTurnEvidence(payload.turns ?? [], state);
303
+ validateMetadataRecords(payload.nativeMetadata ?? [], turns, state);
304
+ for (const section of payload.earlierHistory ?? []) {
305
+ const earlierTurns = validateTurnEvidence(section.turns ?? [], state);
306
+ validateMetadataRecords(section.nativeMetadata ?? [], earlierTurns, state);
307
+ }
308
+ if (state.nativeCount > 0 && String(payload.harness) !== "pi")
309
+ failSemantic("Pi native metadata requires harness pi across every history partition");
250
310
  }
311
+ function parseSessionDetailReadPayloadValue(value) {
312
+ rejectExplicitNullEvidence(value);
313
+ if (!isRecord(value))
314
+ failSemantic("session detail read payload is not an object");
315
+ const durable = { ...value };
316
+ delete durable.relationshipNavigation;
317
+ const normalized = parseSessionDetailPayloadValue(durable);
318
+ return zSessionDetailReadPayload.parse(Object.hasOwn(value, "relationshipNavigation")
319
+ ? { ...normalized, relationshipNavigation: value.relationshipNavigation }
320
+ : normalized);
321
+ }
322
+ function validateRelationships(payload) {
323
+ const seen = new Set();
324
+ let startedBy;
325
+ for (const relationship of payload.relationships ?? []) {
326
+ if (seen.has(relationship.kind))
327
+ failSemantic("relationship kind occurs more than once");
328
+ seen.add(relationship.kind);
329
+ const known = relationship.targetState === "target_known" || relationship.targetState === "target_known_retained";
330
+ if (known !== (relationship.targetLocalId != null))
331
+ failSemantic("relationship target state and targetLocalId presence disagree");
332
+ if (relationship.targetLocalId != null && !validSessionId(relationship.targetLocalId))
333
+ failSemantic("relationship targetLocalId is malformed");
334
+ if (relationship.anchor != null) {
335
+ if (relationship.kind !== "context_from" || !known)
336
+ failSemantic("anchor is allowed only on a known context_from relationship");
337
+ const exact = relationship.anchor.kind !== "general_source_session";
338
+ const hasEntry = relationship.anchor.sourceEntryRef !== undefined, hasRevision = relationship.anchor.sourceRevisionRef !== undefined;
339
+ if ((!exact && (hasEntry || hasRevision)) || (exact && (!hasEntry || !hasRevision)))
340
+ failSemantic("anchor references disagree with anchor kind");
341
+ if (exact && (!validRef(relationship.anchor.sourceEntryRef) || !validRef(relationship.anchor.sourceRevisionRef)))
342
+ failSemantic("anchor reference is invalid");
343
+ }
344
+ if (relationship.kind === "started_by" && known)
345
+ startedBy = relationship.targetLocalId ?? undefined;
346
+ }
347
+ if ((payload.relationships?.length ?? 0) > 0 && payload.parentSessionId != null && payload.parentSessionId !== startedBy)
348
+ failSemantic("parentSessionId disagrees with durable started_by relationship");
349
+ }
350
+ function validSessionId(value) { return !value.includes("/") && !value.includes("\\") && !value.includes("..") && /^(?:[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}|agent-[a-f0-9]+|ses_[a-zA-Z0-9]+|sess_[a-zA-Z0-9]+|msg_[a-zA-Z0-9]+|[A-Z2-7]{26}|[0-9]{8}T[0-9]{6}\.[0-9]{9}Z-[A-Z2-7]{26})$/.test(value); }
351
+ function newDetailState(payload) { return { strictBlockIdentity: requiredContentCapabilities(payload).includes(KnownContentCapability.SessionGraphProvenanceV1), blockRefs: new Set(), owners: new Set(), sources: new Set(), toolIds: new Set(), nativeIds: new Set(), nativeSources: new Set(), nativeCount: 0, nativeBytes: 0 }; }
251
352
  // Producers can validate metadata before attaching it to a harness envelope.
252
353
  // The full detail parsers reuse this same record and target validation.
253
354
  export function parseNativeMetadataRecordsValue(value, targets = []) {
254
355
  rejectExplicitNullEvidence({ nativeMetadata: value, turns: targets });
255
356
  const turns = zTurnDetail.array().parse(targets);
256
357
  const records = zNativeMetadataRecord.array().parse(value);
257
- validateMetadataRecords(records, validateTurnEvidence(turns));
358
+ const state = newDetailState({ turns });
359
+ validateMetadataRecords(records, validateTurnEvidence(turns, state), state);
258
360
  return records;
259
361
  }
260
362
  export function parseNativeMetadataRecordsText(text, targets = []) {
261
363
  scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/*/data"] });
262
364
  return parseNativeMetadataRecordsValue(JSON.parse(text), targets);
263
365
  }
264
- function validateTurnEvidence(targets) {
265
- const owners = new Set();
266
- const sources = new Set();
366
+ function validateTurnEvidence(targets, state = newDetailState({ turns: targets })) {
367
+ const blockRefs = state.blockRefs, owners = state.owners, sources = state.sources, toolIds = state.toolIds;
267
368
  const turnIndexes = new Set();
268
- const toolIds = new Set();
269
369
  const turns = new Map();
270
370
  for (const turn of targets) {
271
371
  if (turn.observedModel !== undefined && (turn.role !== "assistant" || !validUnicode(turn.observedModel)))
@@ -274,8 +374,15 @@ function validateTurnEvidence(targets) {
274
374
  failSemantic("duplicate turn index makes metadata attachment ambiguous");
275
375
  turnIndexes.add(turn.index);
276
376
  turns.set(turn.index, turn);
277
- if (turn.sourceEntryRef !== undefined && !validOptionalRef(turn.sourceEntryRef))
278
- failSemantic("turn source reference is invalid or exceeds 96 bytes");
377
+ if (turn.sourceEntryRef !== undefined && (!validOptionalRef(turn.sourceEntryRef) || (state.strictBlockIdentity && blockRefs.has(turn.sourceEntryRef))))
378
+ failSemantic("turn source reference is invalid, duplicated, or exceeds 96 bytes");
379
+ if (turn.sourceEntryRef !== undefined)
380
+ blockRefs.add(turn.sourceEntryRef);
381
+ if (turn.provenance != null)
382
+ validateProvenance(turn.provenance);
383
+ const projected = turn.sourceEntryRef !== undefined || turn.provenance !== undefined || (turn.toolCalls ?? []).some((tool) => tool.callEntryRef !== undefined || tool.resultEntryRef !== undefined || tool.callProvenance !== undefined || tool.resultProvenance !== undefined);
384
+ if (projected && (((turn.depth ?? 0) === 0) !== (turn.parentIndex == null)))
385
+ failSemantic("turn depth and parentIndex disagree");
279
386
  if (turn.usage != null) {
280
387
  if (!((turn.role === "assistant" && turn.usage.scope === "assistant") || (turn.role === "system" && turn.usage.scope === "summary")))
281
388
  failSemantic("turn role and usage scope disagree");
@@ -284,16 +391,24 @@ function validateTurnEvidence(targets) {
284
391
  validateUsage(turn.usage, owners, sources);
285
392
  }
286
393
  for (const tool of turn.toolCalls ?? []) {
394
+ if (tool.namespace !== undefined && !validUnicode(tool.namespace))
395
+ failSemantic("tool namespace contains invalid Unicode; provide a valid string or omit unrecorded namespace");
396
+ if (tool.callProvenance != null)
397
+ validateProvenance(tool.callProvenance);
398
+ if (tool.resultProvenance != null)
399
+ validateProvenance(tool.resultProvenance);
287
400
  if (!validOptionalRef(tool.id))
288
401
  failSemantic("tool id reference is invalid or exceeds 96 bytes");
289
402
  if (tool.id && toolIds.has(tool.id))
290
403
  failSemantic("duplicate tool call id makes metadata attachment ambiguous");
291
404
  if (tool.id)
292
405
  toolIds.add(tool.id);
293
- if (tool.callEntryRef !== undefined && !validOptionalRef(tool.callEntryRef))
294
- failSemantic("tool call reference is invalid or exceeds 96 bytes");
295
- if (tool.resultEntryRef !== undefined && !validOptionalRef(tool.resultEntryRef))
296
- failSemantic("tool result reference is invalid or exceeds 96 bytes");
406
+ for (const ref of [tool.callEntryRef, tool.resultEntryRef]) {
407
+ if (ref !== undefined && (!validOptionalRef(ref) || (state.strictBlockIdentity && blockRefs.has(ref))))
408
+ failSemantic("tool reference is invalid, duplicated, or exceeds 96 bytes");
409
+ if (ref !== undefined)
410
+ blockRefs.add(ref);
411
+ }
297
412
  if (tool.usage != null) {
298
413
  if (tool.usage.scope !== "tool" || !tool.resultEntryRef || tool.usage.sourceEntryRef !== tool.resultEntryRef)
299
414
  failSemantic("tool usage attribution disagrees");
@@ -301,23 +416,29 @@ function validateTurnEvidence(targets) {
301
416
  }
302
417
  }
303
418
  }
419
+ for (const turn of targets)
420
+ if (turn.parentIndex != null && !turns.has(turn.parentIndex))
421
+ failSemantic("parentIndex does not target a turn in the same partition");
304
422
  return turns;
305
423
  }
306
- function validateMetadataRecords(records, turns) {
307
- const ids = new Set();
308
- let metadataBytes = 0;
309
- if (records.length > 256)
424
+ function validateMetadataRecords(records, turns, state) {
425
+ const blockRefs = state.blockRefs;
426
+ if (state.nativeCount + records.length > 256)
310
427
  failSemantic("native metadata record count exceeds 256");
428
+ state.nativeCount += records.length;
311
429
  for (const metadata of records) {
312
- if (!validRef(metadata.id) || !validRef(metadata.source.entryRef) || ids.has(metadata.id))
430
+ if (!validRef(metadata.id) || !validRef(metadata.source.entryRef) || state.nativeIds.has(metadata.id) || state.nativeSources.has(metadata.source.entryRef) || (state.strictBlockIdentity && metadata.attachment === undefined && blockRefs.has(metadata.source.entryRef)))
313
431
  failSemantic("metadata reference is invalid or duplicated");
314
- ids.add(metadata.id);
432
+ state.nativeIds.add(metadata.id);
433
+ state.nativeSources.add(metadata.source.entryRef);
434
+ if (metadata.attachment === undefined)
435
+ blockRefs.add(metadata.source.entryRef);
315
436
  if (metadata.customType !== undefined && (!validUnicode(metadata.customType) || new TextEncoder().encode(metadata.customType).length > 128))
316
437
  failSemantic("customType is invalid Unicode or exceeds 128 bytes");
317
438
  validateMetadataValue(metadata.data, 1);
318
439
  const encoded = new TextEncoder().encode(JSON.stringify(metadata.data)).length;
319
- metadataBytes += encoded;
320
- if (encoded > 65536 || metadataBytes > 1048576)
440
+ state.nativeBytes += encoded;
441
+ if (encoded > 65536 || state.nativeBytes > 1048576)
321
442
  failSemantic("metadata data exceeds its byte budget");
322
443
  const attachment = metadata.attachment;
323
444
  const target = attachment?.turnIndex == null ? undefined : turns.get(attachment.turnIndex);
@@ -350,6 +471,13 @@ function validateMetadataRecords(records, turns) {
350
471
  }
351
472
  }
352
473
  }
474
+ function validateProvenance(value) {
475
+ const sets = [[value.origin, ["submitted_input", "harness_context", "agent_output", "agent_communication", "tool_activity", "system_control", "generated_summary", "unknown"]], [value.actor, ["operator", "agent_delegate", "harness", "unknown"]], [value.delivery, ["session_admission", "guardian_review", "subagent_delivery", "inherited_context", "tool_delivery", "system_lifecycle", "unknown"]], [value.ownership, ["local", "inherited", "uncertain"]], [value.evidence, ["native_typed", "lifecycle_typed", "existing_adapter", "retained_last_good", "unknown", "conflict"]], [value.inputModality, ["none", "text", "media", "user_action", "mixed", "unknown"]]];
476
+ if (sets.some(([v, allowed]) => !allowed.includes(v)))
477
+ failSemantic("provenance dimension is missing or outside its closed set");
478
+ if (value.submissionRef !== undefined && value.submissionRef !== "" && !validRef(value.submissionRef))
479
+ failSemantic("submissionRef is invalid");
480
+ }
353
481
  function validateUsage(usage, owners, sources) {
354
482
  if (!validRef(usage.ownerId) || !validRef(usage.sourceEntryRef) || owners.has(usage.ownerId) || sources.has(usage.sourceEntryRef))
355
483
  failSemantic("usage owner/source reference is invalid or duplicated");
@@ -396,8 +524,8 @@ function validUnicode(value) {
396
524
  function validateMetadataValue(value, depth) {
397
525
  if (depth > 32)
398
526
  failSemantic("metadata data exceeds depth 32");
399
- if (typeof value === "string" && (!validUnicode(value) || new TextEncoder().encode(value).length > 16384))
400
- failSemantic("metadata string is invalid Unicode or exceeds 16 KiB");
527
+ if (typeof value === "string" && (!validUnicode(value) || new TextEncoder().encode(value).length > maxNativeMetadataStringBytes))
528
+ failSemantic(`metadata string is invalid Unicode or exceeds ${maxNativeMetadataStringBytes} UTF-8 bytes`);
401
529
  if (typeof value === "number" && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))))
402
530
  failSemantic("metadata number is outside the finite JS-safe domain");
403
531
  if (value !== null && !["string", "number", "boolean", "object"].includes(typeof value))
@@ -425,10 +553,19 @@ function isRecord(value) { return typeof value === "object" && value !== null &&
425
553
  function rejectExplicitNullEvidence(value) {
426
554
  if (!isRecord(value))
427
555
  return;
556
+ if (value.inputSubmissionCount === null)
557
+ failSemantic("inputSubmissionCount is explicitly null");
558
+ if (value.inputSubmissionCount !== undefined && (typeof value.inputSubmissionCount !== "number" || !Number.isSafeInteger(value.inputSubmissionCount) || value.inputSubmissionCount < 0))
559
+ failSemantic("inputSubmissionCount is outside 0..9007199254740991");
428
560
  for (const turn of Array.isArray(value.turns) ? value.turns : []) {
429
561
  if (!isRecord(turn))
430
562
  continue;
563
+ if (turn.provenance === null)
564
+ failSemantic("turn provenance is explicitly null");
431
565
  const tools = Array.isArray(turn.toolCalls) ? turn.toolCalls : [];
566
+ for (const tool of tools)
567
+ if (isRecord(tool) && (tool.callProvenance === null || tool.resultProvenance === null))
568
+ failSemantic("folded provenance is explicitly null");
432
569
  for (const usage of [turn.usage, ...tools.map(tool => isRecord(tool) ? tool.usage : undefined)]) {
433
570
  if (!isRecord(usage) || !isRecord(usage.tokens))
434
571
  continue;
@@ -437,6 +574,9 @@ function rejectExplicitNullEvidence(value) {
437
574
  failSemantic(`token ${name} is explicitly null`);
438
575
  }
439
576
  }
577
+ for (const section of Array.isArray(value.earlierHistory) ? value.earlierHistory : [])
578
+ if (isRecord(section))
579
+ rejectExplicitNullEvidence({ turns: section.turns, nativeMetadata: section.nativeMetadata });
440
580
  for (const record of Array.isArray(value.nativeMetadata) ? value.nativeMetadata : []) {
441
581
  if (!isRecord(record))
442
582
  continue;
@@ -455,4 +595,52 @@ function rejectExplicitNullEvidence(value) {
455
595
  failSemantic("metadata source.messageRole is forbidden for this kind");
456
596
  }
457
597
  }
598
+ const rootForbiddenGraphFields = new Set(["relationshipNavigation", "label", "explanation", "cooked", "collapsed", "url", "resolved", "detail"]);
599
+ const forbiddenGraphFields = new Set(["relationshipNavigation", "status", "transcriptId", "label", "explanation", "cooked", "collapsed", "url", "resolved", "detail"]);
600
+ function validateRawGraphValue(value) {
601
+ if (!isRecord(value))
602
+ return;
603
+ for (const key of Object.keys(value))
604
+ if (rootForbiddenGraphFields.has(key))
605
+ failSemantic(`durable payload contains forbidden read-only or cooked field ${key}`);
606
+ for (const relationship of Array.isArray(value.relationships) ? value.relationships : [])
607
+ if (isRecord(relationship)) {
608
+ for (const key of Object.keys(relationship))
609
+ if (!["kind", "targetState", "targetLocalId", "evidence", "anchor"].includes(key))
610
+ failSemantic(`relationship contains forbidden field ${key}`);
611
+ if (isRecord(relationship.anchor))
612
+ for (const key of Object.keys(relationship.anchor))
613
+ if (!["kind", "sourceEntryRef", "sourceRevisionRef"].includes(key))
614
+ failSemantic(`anchor contains forbidden field ${key}`);
615
+ }
616
+ const visitTurns = (turns) => {
617
+ if (!Array.isArray(turns))
618
+ return;
619
+ for (const turn of turns) {
620
+ if (!isRecord(turn))
621
+ continue;
622
+ for (const key of Object.keys(turn))
623
+ if (forbiddenGraphFields.has(key))
624
+ failSemantic(`turn contains forbidden read-only or cooked field ${key}`);
625
+ for (const tool of Array.isArray(turn.toolCalls) ? turn.toolCalls : [])
626
+ if (isRecord(tool))
627
+ for (const key of Object.keys(tool))
628
+ if (forbiddenGraphFields.has(key))
629
+ failSemantic(`tool contains forbidden read-only or cooked field ${key}`);
630
+ for (const provenance of [turn.provenance, ...(Array.isArray(turn.toolCalls) ? turn.toolCalls.flatMap((tool) => isRecord(tool) ? [tool.callProvenance, tool.resultProvenance] : []) : [])])
631
+ if (isRecord(provenance))
632
+ for (const key of Object.keys(provenance))
633
+ if (!["origin", "actor", "delivery", "ownership", "evidence", "inputModality", "submissionRef"].includes(key))
634
+ failSemantic(`provenance contains forbidden field ${key}`);
635
+ }
636
+ };
637
+ visitTurns(value.turns);
638
+ for (const section of Array.isArray(value.earlierHistory) ? value.earlierHistory : [])
639
+ if (isRecord(section)) {
640
+ for (const key of Object.keys(section))
641
+ if (!["state", "turns", "nativeMetadata"].includes(key))
642
+ failSemantic(`earlier history contains forbidden field ${key}`);
643
+ visitTurns(section.turns);
644
+ }
645
+ }
458
646
  function failSemantic(reason) { throw new TypeError(`Session detail validation failed at @peasant-labs/schema public parser during semantic validation: ${reason}; consumers cannot safely attribute transcript evidence; correct the payload and retry.`); }
@@ -1,3 +1,4 @@
1
+ import type { SessionDetailPayload } from "./contract/zod.gen.js";
1
2
  /**
2
3
  * KnownContentCapability is the closed inventory of content-capability tokens known to this
3
4
  * pinned schema release. It is intentionally distinct from the OPEN discovery
@@ -11,7 +12,13 @@ export declare const KnownContentCapability: Readonly<{
11
12
  readonly DetailedUsageV1: "detailed_usage_v1";
12
13
  readonly NativeMetadataV1: "native_metadata_v1";
13
14
  readonly ObservedModelV1: "observed_model_v1";
15
+ readonly SessionGraphProvenanceV1: "session_graph_provenance_v1";
16
+ readonly ToolNamespaceV1: "tool_namespace_v1";
14
17
  }>;
15
18
  export type KnownContentCapability = (typeof KnownContentCapability)[keyof typeof KnownContentCapability];
16
19
  export declare const AllContentCapabilities: readonly KnownContentCapability[];
17
20
  export declare function isContentCapability(value: unknown): value is KnownContentCapability;
21
+ export declare function knownContentCapabilities(values: readonly string[]): KnownContentCapability[];
22
+ export declare function missingContentCapabilities(advertised: readonly string[], required: readonly KnownContentCapability[]): KnownContentCapability[];
23
+ export declare function validateContentCapabilityAdvertisements(values: readonly string[]): void;
24
+ export declare function requiredContentCapabilities(detail: SessionDetailPayload): KnownContentCapability[];
@@ -1,4 +1,3 @@
1
- // Code generated from the canonical Go/OpenAPI contract. DO NOT EDIT.
2
1
  /**
3
2
  * KnownContentCapability is the closed inventory of content-capability tokens known to this
4
3
  * pinned schema release. It is intentionally distinct from the OPEN discovery
@@ -12,8 +11,55 @@ export const KnownContentCapability = Object.freeze({
12
11
  DetailedUsageV1: "detailed_usage_v1",
13
12
  NativeMetadataV1: "native_metadata_v1",
14
13
  ObservedModelV1: "observed_model_v1",
14
+ SessionGraphProvenanceV1: "session_graph_provenance_v1",
15
+ ToolNamespaceV1: "tool_namespace_v1",
15
16
  });
16
- export const AllContentCapabilities = Object.freeze([KnownContentCapability.DetailedUsageV1, KnownContentCapability.NativeMetadataV1, KnownContentCapability.ObservedModelV1]);
17
+ export const AllContentCapabilities = Object.freeze([KnownContentCapability.DetailedUsageV1, KnownContentCapability.NativeMetadataV1, KnownContentCapability.ObservedModelV1, KnownContentCapability.SessionGraphProvenanceV1, KnownContentCapability.ToolNamespaceV1]);
17
18
  export function isContentCapability(value) {
18
19
  return typeof value === "string" && AllContentCapabilities.includes(value);
19
20
  }
21
+ export function knownContentCapabilities(values) {
22
+ return [...new Set(values.filter(isContentCapability))].sort();
23
+ }
24
+ export function missingContentCapabilities(advertised, required) {
25
+ const present = new Set(knownContentCapabilities(advertised));
26
+ return [...new Set(required)].filter((token) => !present.has(token)).sort();
27
+ }
28
+ export function validateContentCapabilityAdvertisements(values) {
29
+ for (let index = 0; index < values.length; index++) {
30
+ const token = values[index];
31
+ if (token === undefined || !isContentCapability(token))
32
+ throw new TypeError(`content capability producer validation failed at @peasant-labs/schema validateContentCapabilityAdvertisements: unknown token ${JSON.stringify(token)}; callers cannot advertise unimplemented preservation; emit only AllContentCapabilities after deployment support is proven`);
33
+ const previous = values[index - 1];
34
+ if (previous !== undefined && previous === token)
35
+ throw new TypeError("content capability producer validation failed at @peasant-labs/schema validateContentCapabilityAdvertisements: token is duplicated; callers cannot emit a canonical advertisement; deduplicate and sort the known inventory");
36
+ if (previous !== undefined && previous > token)
37
+ throw new TypeError("content capability producer validation failed at @peasant-labs/schema validateContentCapabilityAdvertisements: tokens are not in canonical lexicographic order; callers cannot emit a canonical advertisement; sort the unique known inventory");
38
+ }
39
+ }
40
+ function visitTurns(turns, found) {
41
+ for (const turn of turns ?? []) {
42
+ if ((turn.observedModel ?? "") !== "")
43
+ found.add(KnownContentCapability.ObservedModelV1);
44
+ if (turn.toolCalls?.some((tool) => tool.namespace !== undefined))
45
+ found.add(KnownContentCapability.ToolNamespaceV1);
46
+ if (turn.usage != null || turn.toolCalls?.some((tool) => tool.usage != null))
47
+ found.add(KnownContentCapability.DetailedUsageV1);
48
+ if (turn.provenance != null || turn.toolCalls?.some((tool) => tool.callProvenance != null || tool.resultProvenance != null))
49
+ found.add(KnownContentCapability.SessionGraphProvenanceV1);
50
+ }
51
+ }
52
+ export function requiredContentCapabilities(detail) {
53
+ const found = new Set();
54
+ if (detail.inputSubmissionCount !== undefined || detail.rootSessionId != null || (detail.purpose ?? "") !== "" || (detail.relationships?.length ?? 0) > 0 || (detail.earlierHistory?.length ?? 0) > 0)
55
+ found.add(KnownContentCapability.SessionGraphProvenanceV1);
56
+ if ((detail.nativeMetadata?.length ?? 0) > 0)
57
+ found.add(KnownContentCapability.NativeMetadataV1);
58
+ visitTurns(detail.turns, found);
59
+ for (const section of detail.earlierHistory ?? []) {
60
+ if ((section.nativeMetadata?.length ?? 0) > 0)
61
+ found.add(KnownContentCapability.NativeMetadataV1);
62
+ visitTurns(section.turns, found);
63
+ }
64
+ return [...found].sort();
65
+ }