@peasant-labs/schema 0.18.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.
@@ -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,8 @@ 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";
6
7
  import { maxNativeMetadataStringBytes } from "./internal/generated/metadata-limits.gen.js";
7
8
  function assertProjectHash(value, operation) {
8
9
  if (!isProjectHash(value)) {
@@ -49,20 +50,75 @@ function scanRawJson(text, policy) {
49
50
  }
50
51
  export function parseSessionDetailPayloadValue(value) {
51
52
  rejectExplicitNullEvidence(value);
52
- const payload = zSessionDetailPayload.parse(value);
53
+ validateRawGraphValue(value);
54
+ const normalized = structuredClone(value);
55
+ normalizeEmptyOptionalPublicRefs(normalized);
56
+ const payload = zSessionDetailPayload.parse(normalized);
53
57
  validateSessionDetail(payload);
54
58
  return payload;
55
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
+ }
56
98
  export function parseSessionDetailPayloadText(text) {
57
99
  scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/nativeMetadata/*/data"] });
58
100
  return parseSessionDetailPayloadValue(JSON.parse(text));
59
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
+ }
60
115
  export function parseTranscriptContentText(text) {
61
116
  scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/sessionDetail/nativeMetadata/*/data"] });
62
117
  const raw = JSON.parse(text);
63
- if (isRecord(raw))
64
- rejectExplicitNullEvidence(raw.sessionDetail);
65
- 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);
66
122
  if (content.sessionDetail === undefined || content.sessionDetail === null)
67
123
  failSemantic("sessionDetail is required");
68
124
  parseSessionDetailPayloadValue(content.sessionDetail);
@@ -76,13 +132,10 @@ export function parseServerMessageRaw(text) {
76
132
  scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/data/nativeMetadata/*/data"] });
77
133
  }
78
134
  const value = JSON.parse(text);
79
- if (isRecord(value) && value.type === "session_detail")
80
- rejectExplicitNullEvidence(value.data);
81
- const message = zServerMessage.parse(value);
82
- if (message.type === "session_detail") {
83
- parseSessionDetailPayloadValue(message.data);
84
- }
85
- return message;
135
+ const normalized = isRecord(value) && value.type === "session_detail"
136
+ ? { ...value, data: parseSessionDetailReadPayloadValue(value.data) }
137
+ : value;
138
+ return zServerMessage.parse(normalized);
86
139
  }
87
140
  class RawJsonScanner {
88
141
  text;
@@ -244,29 +297,75 @@ class RawJsonScanner {
244
297
  }
245
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]); }
246
299
  function validateSessionDetail(payload) {
247
- const turns = validateTurnEvidence(payload.turns ?? []);
248
- if ((payload.nativeMetadata?.length ?? 0) > 0 && String(payload.harness) !== "pi")
249
- failSemantic("Pi native metadata requires harness pi");
250
- 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");
251
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 }; }
252
352
  // Producers can validate metadata before attaching it to a harness envelope.
253
353
  // The full detail parsers reuse this same record and target validation.
254
354
  export function parseNativeMetadataRecordsValue(value, targets = []) {
255
355
  rejectExplicitNullEvidence({ nativeMetadata: value, turns: targets });
256
356
  const turns = zTurnDetail.array().parse(targets);
257
357
  const records = zNativeMetadataRecord.array().parse(value);
258
- validateMetadataRecords(records, validateTurnEvidence(turns));
358
+ const state = newDetailState({ turns });
359
+ validateMetadataRecords(records, validateTurnEvidence(turns, state), state);
259
360
  return records;
260
361
  }
261
362
  export function parseNativeMetadataRecordsText(text, targets = []) {
262
363
  scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/*/data"] });
263
364
  return parseNativeMetadataRecordsValue(JSON.parse(text), targets);
264
365
  }
265
- function validateTurnEvidence(targets) {
266
- const owners = new Set();
267
- 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;
268
368
  const turnIndexes = new Set();
269
- const toolIds = new Set();
270
369
  const turns = new Map();
271
370
  for (const turn of targets) {
272
371
  if (turn.observedModel !== undefined && (turn.role !== "assistant" || !validUnicode(turn.observedModel)))
@@ -275,8 +374,15 @@ function validateTurnEvidence(targets) {
275
374
  failSemantic("duplicate turn index makes metadata attachment ambiguous");
276
375
  turnIndexes.add(turn.index);
277
376
  turns.set(turn.index, turn);
278
- if (turn.sourceEntryRef !== undefined && !validOptionalRef(turn.sourceEntryRef))
279
- 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");
280
386
  if (turn.usage != null) {
281
387
  if (!((turn.role === "assistant" && turn.usage.scope === "assistant") || (turn.role === "system" && turn.usage.scope === "summary")))
282
388
  failSemantic("turn role and usage scope disagree");
@@ -287,16 +393,22 @@ function validateTurnEvidence(targets) {
287
393
  for (const tool of turn.toolCalls ?? []) {
288
394
  if (tool.namespace !== undefined && !validUnicode(tool.namespace))
289
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);
290
400
  if (!validOptionalRef(tool.id))
291
401
  failSemantic("tool id reference is invalid or exceeds 96 bytes");
292
402
  if (tool.id && toolIds.has(tool.id))
293
403
  failSemantic("duplicate tool call id makes metadata attachment ambiguous");
294
404
  if (tool.id)
295
405
  toolIds.add(tool.id);
296
- if (tool.callEntryRef !== undefined && !validOptionalRef(tool.callEntryRef))
297
- failSemantic("tool call reference is invalid or exceeds 96 bytes");
298
- if (tool.resultEntryRef !== undefined && !validOptionalRef(tool.resultEntryRef))
299
- 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
+ }
300
412
  if (tool.usage != null) {
301
413
  if (tool.usage.scope !== "tool" || !tool.resultEntryRef || tool.usage.sourceEntryRef !== tool.resultEntryRef)
302
414
  failSemantic("tool usage attribution disagrees");
@@ -304,23 +416,29 @@ function validateTurnEvidence(targets) {
304
416
  }
305
417
  }
306
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");
307
422
  return turns;
308
423
  }
309
- function validateMetadataRecords(records, turns) {
310
- const ids = new Set();
311
- let metadataBytes = 0;
312
- if (records.length > 256)
424
+ function validateMetadataRecords(records, turns, state) {
425
+ const blockRefs = state.blockRefs;
426
+ if (state.nativeCount + records.length > 256)
313
427
  failSemantic("native metadata record count exceeds 256");
428
+ state.nativeCount += records.length;
314
429
  for (const metadata of records) {
315
- 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)))
316
431
  failSemantic("metadata reference is invalid or duplicated");
317
- 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);
318
436
  if (metadata.customType !== undefined && (!validUnicode(metadata.customType) || new TextEncoder().encode(metadata.customType).length > 128))
319
437
  failSemantic("customType is invalid Unicode or exceeds 128 bytes");
320
438
  validateMetadataValue(metadata.data, 1);
321
439
  const encoded = new TextEncoder().encode(JSON.stringify(metadata.data)).length;
322
- metadataBytes += encoded;
323
- if (encoded > 65536 || metadataBytes > 1048576)
440
+ state.nativeBytes += encoded;
441
+ if (encoded > 65536 || state.nativeBytes > 1048576)
324
442
  failSemantic("metadata data exceeds its byte budget");
325
443
  const attachment = metadata.attachment;
326
444
  const target = attachment?.turnIndex == null ? undefined : turns.get(attachment.turnIndex);
@@ -353,6 +471,13 @@ function validateMetadataRecords(records, turns) {
353
471
  }
354
472
  }
355
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
+ }
356
481
  function validateUsage(usage, owners, sources) {
357
482
  if (!validRef(usage.ownerId) || !validRef(usage.sourceEntryRef) || owners.has(usage.ownerId) || sources.has(usage.sourceEntryRef))
358
483
  failSemantic("usage owner/source reference is invalid or duplicated");
@@ -428,10 +553,19 @@ function isRecord(value) { return typeof value === "object" && value !== null &&
428
553
  function rejectExplicitNullEvidence(value) {
429
554
  if (!isRecord(value))
430
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");
431
560
  for (const turn of Array.isArray(value.turns) ? value.turns : []) {
432
561
  if (!isRecord(turn))
433
562
  continue;
563
+ if (turn.provenance === null)
564
+ failSemantic("turn provenance is explicitly null");
434
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");
435
569
  for (const usage of [turn.usage, ...tools.map(tool => isRecord(tool) ? tool.usage : undefined)]) {
436
570
  if (!isRecord(usage) || !isRecord(usage.tokens))
437
571
  continue;
@@ -440,6 +574,9 @@ function rejectExplicitNullEvidence(value) {
440
574
  failSemantic(`token ${name} is explicitly null`);
441
575
  }
442
576
  }
577
+ for (const section of Array.isArray(value.earlierHistory) ? value.earlierHistory : [])
578
+ if (isRecord(section))
579
+ rejectExplicitNullEvidence({ turns: section.turns, nativeMetadata: section.nativeMetadata });
443
580
  for (const record of Array.isArray(value.nativeMetadata) ? value.nativeMetadata : []) {
444
581
  if (!isRecord(record))
445
582
  continue;
@@ -458,4 +595,52 @@ function rejectExplicitNullEvidence(value) {
458
595
  failSemantic("metadata source.messageRole is forbidden for this kind");
459
596
  }
460
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
+ }
461
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,8 +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";
14
16
  readonly ToolNamespaceV1: "tool_namespace_v1";
15
17
  }>;
16
18
  export type KnownContentCapability = (typeof KnownContentCapability)[keyof typeof KnownContentCapability];
17
19
  export declare const AllContentCapabilities: readonly KnownContentCapability[];
18
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,9 +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
15
  ToolNamespaceV1: "tool_namespace_v1",
16
16
  });
17
- export const AllContentCapabilities = Object.freeze([KnownContentCapability.DetailedUsageV1, KnownContentCapability.NativeMetadataV1, KnownContentCapability.ObservedModelV1, KnownContentCapability.ToolNamespaceV1]);
17
+ export const AllContentCapabilities = Object.freeze([KnownContentCapability.DetailedUsageV1, KnownContentCapability.NativeMetadataV1, KnownContentCapability.ObservedModelV1, KnownContentCapability.SessionGraphProvenanceV1, KnownContentCapability.ToolNamespaceV1]);
18
18
  export function isContentCapability(value) {
19
19
  return typeof value === "string" && AllContentCapabilities.includes(value);
20
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
+ }