@peasant-labs/schema 0.1.0-rc6 → 0.1.0-rc9

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.
@@ -29,7 +29,12 @@ export const zAnnotationDatatype = z.enum([
29
29
  export const zAnnotationEntryTarget = z.object({
30
30
  endIndex: z.int(),
31
31
  entryIndex: z.int(),
32
- sessionId: z.string()
32
+ sessionId: z.string().min(1)
33
+ }).superRefine((value, context) => {
34
+ if (value.sessionId === "")
35
+ context.addIssue({ code: "custom", message: "entry targets require a non-empty sessionId" });
36
+ if (value.endIndex <= value.entryIndex)
37
+ context.addIssue({ code: "custom", message: "entry targets require endIndex greater than entryIndex" });
33
38
  });
34
39
  export const zAnnotationManifestResponse = z.object({
35
40
  digest: z.string(),
@@ -89,6 +94,98 @@ export const zAnnotatorSummary = z.object({
89
94
  providerKey: z.string().nullish(),
90
95
  status: z.string()
91
96
  });
97
+ /**
98
+ * Association Conclusion
99
+ *
100
+ * Producer-supplied conclusion for a session-to-commit association: confirmed or candidate
101
+ */
102
+ export const zAssociationConclusion = z.enum(['confirmed', 'candidate']);
103
+ /**
104
+ * Association Evidence Kind
105
+ *
106
+ * Atomic observation supporting a session-to-commit association
107
+ */
108
+ export const zAssociationEvidenceKind = z.enum([
109
+ 'recorded_commit',
110
+ 'touched_file',
111
+ 'branch_membership',
112
+ 'time_window'
113
+ ]);
114
+ export const zAssociationEvidenceObservation = z.object({
115
+ branchName: z.string().nullish(),
116
+ kind: zAssociationEvidenceKind,
117
+ recordedCommitHash: z.string().nullish(),
118
+ touchedFilePath: z.string().nullish(),
119
+ windowEndMs: z.int().nullish(),
120
+ windowStartMs: z.int().nullish()
121
+ }).superRefine((value, context) => {
122
+ const isAbsent = (detail) => detail === undefined || detail === null;
123
+ const isGoWhitespace = (codePoint) => (codePoint >= 0x0009 && codePoint <= 0x000D)
124
+ || codePoint === 0x0020
125
+ || codePoint === 0x0085
126
+ || codePoint === 0x00A0
127
+ || codePoint === 0x1680
128
+ || (codePoint >= 0x2000 && codePoint <= 0x200A)
129
+ || codePoint === 0x2028
130
+ || codePoint === 0x2029
131
+ || codePoint === 0x202F
132
+ || codePoint === 0x205F
133
+ || codePoint === 0x3000;
134
+ const hasNonEmptyString = (detail) => {
135
+ if (typeof detail !== "string")
136
+ return false;
137
+ for (const character of detail) {
138
+ const codePoint = character.codePointAt(0);
139
+ if (codePoint !== undefined && !isGoWhitespace(codePoint))
140
+ return true;
141
+ }
142
+ return false;
143
+ };
144
+ const hasValidTouchedFilePath = (detail) => typeof detail === "string"
145
+ && detail !== ""
146
+ && !detail.startsWith("/")
147
+ && !detail.startsWith("\\")
148
+ && !detail.includes("\\")
149
+ && !/^[A-Za-z]:\//.test(detail)
150
+ && !detail.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
151
+ const addIssue = (message) => context.addIssue({ code: "custom", message });
152
+ switch (value.kind) {
153
+ case "recorded_commit":
154
+ if (!hasNonEmptyString(value.recordedCommitHash))
155
+ addIssue("recorded_commit evidence requires a non-empty recordedCommitHash");
156
+ if (!isAbsent(value.touchedFilePath) || !isAbsent(value.branchName) || !isAbsent(value.windowStartMs) || !isAbsent(value.windowEndMs))
157
+ addIssue("recorded_commit evidence must not populate another detail arm");
158
+ return;
159
+ case "touched_file":
160
+ if (!hasValidTouchedFilePath(value.touchedFilePath))
161
+ addIssue("touched_file evidence requires a non-empty repository-relative touchedFilePath");
162
+ if (!isAbsent(value.recordedCommitHash) || !isAbsent(value.branchName) || !isAbsent(value.windowStartMs) || !isAbsent(value.windowEndMs))
163
+ addIssue("touched_file evidence must not populate another detail arm");
164
+ return;
165
+ case "branch_membership":
166
+ if (!hasNonEmptyString(value.branchName))
167
+ addIssue("branch_membership evidence requires a non-empty branchName");
168
+ if (!isAbsent(value.recordedCommitHash) || !isAbsent(value.touchedFilePath) || !isAbsent(value.windowStartMs) || !isAbsent(value.windowEndMs))
169
+ addIssue("branch_membership evidence must not populate another detail arm");
170
+ return;
171
+ case "time_window":
172
+ if (value.windowStartMs === undefined || value.windowStartMs === null || value.windowEndMs === undefined || value.windowEndMs === null) {
173
+ addIssue("time_window evidence requires windowStartMs and windowEndMs");
174
+ }
175
+ else if (value.windowStartMs > value.windowEndMs) {
176
+ addIssue("time_window evidence requires windowStartMs less than or equal to windowEndMs");
177
+ }
178
+ if (!isAbsent(value.recordedCommitHash) || !isAbsent(value.touchedFilePath) || !isAbsent(value.branchName))
179
+ addIssue("time_window evidence must not populate another detail arm");
180
+ return;
181
+ }
182
+ });
183
+ /**
184
+ * Association ID
185
+ *
186
+ * Opaque durable Peasant identifier for one session-to-commit association
187
+ */
188
+ export const zAssociationID = z.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
92
189
  export const zBatchCreateAnnotationsErrorResponse = z.object({
93
190
  error: z.string(),
94
191
  failingIndex: z.int()
@@ -189,6 +286,16 @@ export const zCommitInfo = z.object({
189
286
  hash: z.string(),
190
287
  message: z.string()
191
288
  });
289
+ /**
290
+ * Confidence
291
+ *
292
+ * Strength of evidence behind a derived relationship: high, medium, or low
293
+ */
294
+ export const zConfidence = z.enum([
295
+ 'high',
296
+ 'medium',
297
+ 'low'
298
+ ]);
192
299
  /**
193
300
  * Content Kind
194
301
  *
@@ -352,13 +459,6 @@ export const zFrictionCluster = z.object({
352
459
  label: z.string(),
353
460
  sessions: z.int()
354
461
  });
355
- export const zGitContext = z.object({
356
- branch: z.string().nullish(),
357
- commits: z.array(zCommitInfo).optional(),
358
- remote: z.string().nullish(),
359
- tracking: z.string().nullish(),
360
- worktree: z.string().nullish()
361
- });
362
462
  /**
363
463
  * Harness
364
464
  *
@@ -381,6 +481,30 @@ export const zHealthResponse = z.object({
381
481
  * Sanitized, filesystem-safe identifier derived from git remote; contains only [a-zA-Z0-9._<>-]
382
482
  */
383
483
  export const zHostSlug = z.string().regex(/^[a-zA-Z0-9._<>-]+$/);
484
+ export const zInsightClassification = z.object({
485
+ category: z.string(),
486
+ cause: z.string(),
487
+ resolution: z.string(),
488
+ severityLocus: z.string(),
489
+ severityScope: z.string()
490
+ });
491
+ /**
492
+ * Insight Kind
493
+ *
494
+ * What a SessionInsight observed: a decision, friction, an unusual rate elevation, or a retry loop
495
+ */
496
+ export const zInsightKind = z.enum([
497
+ 'decision',
498
+ 'friction',
499
+ 'unusual',
500
+ 'retry_loop'
501
+ ]);
502
+ /**
503
+ * Insight Provenance
504
+ *
505
+ * How a SessionInsight was produced: mechanical (rule-derived) or mined
506
+ */
507
+ export const zInsightProvenance = z.enum(['mechanical', 'mined']);
384
508
  /**
385
509
  * Interaction Type
386
510
  *
@@ -417,26 +541,6 @@ export const zMapNodeKind = z.enum([
417
541
  'package',
418
542
  'file'
419
543
  ]);
420
- export const zMapNode = z.object({
421
- effortDensity: z.number(),
422
- fileCount: z.int(),
423
- id: z.string(),
424
- kind: zMapNodeKind,
425
- language: z.string().optional(),
426
- layer: z.int(),
427
- loc: z.int(),
428
- name: z.string(),
429
- order: z.int(),
430
- parent: z.string().optional(),
431
- recordedFiles: z.int(),
432
- totalFiles: z.int(),
433
- touchCount: z.int()
434
- });
435
- export const zMapSlice = z.object({
436
- activityEdges: z.array(zActivityEdge).nullable(),
437
- nodes: z.array(zMapNode).nullable(),
438
- structureEdges: z.array(zMapEdge).nullable()
439
- });
440
544
  /**
441
545
  * Message Type
442
546
  *
@@ -483,18 +587,6 @@ export const zModelInfo = z.object({
483
587
  * SHA-256 hex digest of the project's origin URL or local path
484
588
  */
485
589
  export const zProjectHash = z.string().regex(/^[0-9a-f]{64}$/).brand();
486
- export const zMapGraphPayload = z.object({
487
- activityEdges: z.array(zActivityEdge).nullable(),
488
- atCommit: z.string().optional(),
489
- generatedAtMs: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }),
490
- nodes: z.array(zMapNode).nullable(),
491
- parsedLanguages: z.array(z.string()).nullable(),
492
- projectHash: zProjectHash,
493
- repoFound: z.boolean(),
494
- repoPath: z.string().optional(),
495
- structureEdges: z.array(zMapEdge).nullable(),
496
- violations: z.array(zEdgeViolation).nullable()
497
- });
498
590
  export const zProjectContext = z.object({
499
591
  filePath: z.string().optional(),
500
592
  hash: zProjectHash,
@@ -530,6 +622,102 @@ export const zPublishResponse = z.object({
530
622
  transcriptId: z.string(),
531
623
  updatedAt: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' })
532
624
  });
625
+ export const zPublishedAssociation = z.object({
626
+ id: zAssociationID,
627
+ observedCommitHash: z.string()
628
+ });
629
+ export const zGitContext = z.object({
630
+ associations: z.array(zPublishedAssociation).optional(),
631
+ branch: z.string().nullish(),
632
+ commits: z.array(zCommitInfo).optional(),
633
+ remote: z.string().nullish(),
634
+ tracking: z.string().nullish(),
635
+ worktree: z.string().nullish()
636
+ }).superRefine((value, context) => {
637
+ if (value.associations === undefined || value.associations === null)
638
+ return;
639
+ const seenIDs = new Map();
640
+ const seenObservedHashes = new Map();
641
+ for (const [index, association] of value.associations.entries()) {
642
+ if (association.observedCommitHash.trim() === "") {
643
+ context.addIssue({ code: "custom", path: ["associations", index, "observedCommitHash"], message: "published associations require a non-empty observedCommitHash" });
644
+ }
645
+ const priorID = seenIDs.get(association.id);
646
+ if (priorID !== undefined) {
647
+ context.addIssue({ code: "custom", path: ["associations", index, "id"], message: "published association IDs must be unique within one request" });
648
+ }
649
+ else {
650
+ seenIDs.set(association.id, index);
651
+ }
652
+ const priorObservedHash = seenObservedHashes.get(association.observedCommitHash);
653
+ if (priorObservedHash !== undefined) {
654
+ context.addIssue({ code: "custom", path: ["associations", index, "observedCommitHash"], message: "published association observedCommitHash values must be unique within one request" });
655
+ }
656
+ else {
657
+ seenObservedHashes.set(association.observedCommitHash, index);
658
+ }
659
+ }
660
+ });
661
+ /**
662
+ * Read Attribution State
663
+ *
664
+ * Whether per-file read attribution is recoverable for a node's editing sessions: complete, partial, or unavailable
665
+ */
666
+ export const zReadAttributionState = z.enum([
667
+ 'complete',
668
+ 'partial',
669
+ 'unavailable'
670
+ ]);
671
+ /**
672
+ * Read State Grade
673
+ *
674
+ * Ordinal explicit read-state act: none, viewed, reviewed, or reviewed_in_detail
675
+ */
676
+ export const zReadStateGrade = z.enum([
677
+ 'none',
678
+ 'viewed',
679
+ 'reviewed',
680
+ 'reviewed_in_detail'
681
+ ]);
682
+ export const zMapNode = z.object({
683
+ agentEditedCount: z.int(),
684
+ attributedRegionCount: z.int(),
685
+ changedRegionCount: z.int(),
686
+ effortDensity: z.number(),
687
+ fileCount: z.int(),
688
+ id: z.string(),
689
+ kind: zMapNodeKind,
690
+ language: z.string().optional(),
691
+ layer: z.int(),
692
+ loc: z.int(),
693
+ name: z.string(),
694
+ order: z.int(),
695
+ parent: z.string().optional(),
696
+ readAttribution: zReadAttributionState,
697
+ readCount: z.int(),
698
+ readState: zReadStateGrade,
699
+ recordedFiles: z.int(),
700
+ reviewedRegionCount: z.int(),
701
+ totalFiles: z.int(),
702
+ touchCount: z.int()
703
+ });
704
+ export const zMapGraphPayload = z.object({
705
+ activityEdges: z.array(zActivityEdge).nullable(),
706
+ atCommit: z.string().optional(),
707
+ generatedAtMs: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }),
708
+ nodes: z.array(zMapNode).nullable(),
709
+ parsedLanguages: z.array(z.string()).nullable(),
710
+ projectHash: zProjectHash,
711
+ repoFound: z.boolean(),
712
+ repoPath: z.string().optional(),
713
+ structureEdges: z.array(zMapEdge).nullable(),
714
+ violations: z.array(zEdgeViolation).nullable()
715
+ });
716
+ export const zMapSlice = z.object({
717
+ activityEdges: z.array(zActivityEdge).nullable(),
718
+ nodes: z.array(zMapNode).nullable(),
719
+ structureEdges: z.array(zMapEdge).nullable()
720
+ });
533
721
  export const zRedactionInfo = z.object({
534
722
  applied: z.boolean(),
535
723
  content_hash_at_redact: z.string().optional(),
@@ -543,6 +731,29 @@ export const zReviewSuggestion = z.object({
543
731
  path: z.string(),
544
732
  suggestedPrompt: z.string()
545
733
  });
734
+ /**
735
+ * Rewrite Method
736
+ *
737
+ * Mechanism the resolver used to map a ghost commit to its successor
738
+ */
739
+ export const zRewriteMethod = z.enum([
740
+ 'hash',
741
+ 'patch_id',
742
+ 'author_identity',
743
+ 'message_embedded',
744
+ 'temporal',
745
+ 'none'
746
+ ]);
747
+ /**
748
+ * Rewrite Resolution
749
+ *
750
+ * Whether a ledger-observed commit hash is live, was rewritten, or could not be resolved
751
+ */
752
+ export const zRewriteResolution = z.enum([
753
+ 'live',
754
+ 'rewritten',
755
+ 'unresolved'
756
+ ]);
546
757
  /**
547
758
  * Role
548
759
  *
@@ -601,18 +812,95 @@ export const zServerMessage = z.object({
601
812
  * Unique session identifier (UUID, agent-{hex}, ses_{id}, sess_{id} (ACP), or msg_{id})
602
813
  */
603
814
  export const zSessionID = z.string().regex(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|agent-[a-f0-9]+|sess?_[a-zA-Z0-9]+|msg_[a-zA-Z0-9]+)$/);
815
+ export const zInsightEvidence = z.object({
816
+ commitHash: z.string().optional(),
817
+ entryIndex: z.int().nullish(),
818
+ file: z.string().optional(),
819
+ sessionId: zSessionID
820
+ });
821
+ export const zSessionAssociation = z.object({
822
+ conclusion: zAssociationConclusion,
823
+ confidence: zConfidence,
824
+ evidence: z.array(zAssociationEvidenceObservation).min(1),
825
+ id: zAssociationID,
826
+ sessionId: z.string().min(1)
827
+ }).superRefine((value, context) => {
828
+ const kindOrder = ["recorded_commit", "touched_file", "branch_membership", "time_window"];
829
+ const compareStrings = (left, right) => {
830
+ const leftBytes = new TextEncoder().encode(left);
831
+ const rightBytes = new TextEncoder().encode(right);
832
+ const sharedLength = Math.min(leftBytes.length, rightBytes.length);
833
+ for (let index = 0; index < sharedLength; index += 1) {
834
+ const leftByte = leftBytes[index];
835
+ const rightByte = rightBytes[index];
836
+ if (leftByte < rightByte)
837
+ return -1;
838
+ if (leftByte > rightByte)
839
+ return 1;
840
+ }
841
+ return compareNumbers(leftBytes.length, rightBytes.length);
842
+ };
843
+ const compareNumbers = (left, right) => left < right ? -1 : left > right ? 1 : 0;
844
+ const compareObservations = (left, right) => {
845
+ const leftKindOrder = kindOrder.indexOf(left.kind);
846
+ const rightKindOrder = kindOrder.indexOf(right.kind);
847
+ if (leftKindOrder !== rightKindOrder)
848
+ return compareNumbers(leftKindOrder, rightKindOrder);
849
+ switch (left.kind) {
850
+ case "recorded_commit": return compareStrings(typeof left.recordedCommitHash === "string" ? left.recordedCommitHash : "", typeof right.recordedCommitHash === "string" ? right.recordedCommitHash : "");
851
+ case "touched_file": return compareStrings(typeof left.touchedFilePath === "string" ? left.touchedFilePath : "", typeof right.touchedFilePath === "string" ? right.touchedFilePath : "");
852
+ case "branch_membership": return compareStrings(typeof left.branchName === "string" ? left.branchName : "", typeof right.branchName === "string" ? right.branchName : "");
853
+ case "time_window": {
854
+ const startOrder = compareNumbers(left.windowStartMs ?? 0, right.windowStartMs ?? 0);
855
+ return startOrder === 0 ? compareNumbers(left.windowEndMs ?? 0, right.windowEndMs ?? 0) : startOrder;
856
+ }
857
+ }
858
+ return 0;
859
+ };
860
+ for (let index = 1; index < value.evidence.length; index += 1) {
861
+ const previous = value.evidence[index - 1];
862
+ const current = value.evidence[index];
863
+ if (previous === undefined || current === undefined)
864
+ continue;
865
+ if (compareObservations(previous, current) >= 0) {
866
+ context.addIssue({ code: "custom", message: "association evidence must be duplicate-free and in canonical kind/detail order" });
867
+ }
868
+ }
869
+ });
604
870
  export const zCommitRef = z.object({
871
+ associations: z.array(zSessionAssociation),
605
872
  hasSession: z.boolean(),
606
873
  hash: z.string(),
607
874
  sessionIds: z.array(zSessionID),
608
875
  subject: z.string(),
609
876
  timeMs: z.int().nullish()
610
877
  });
878
+ export const zRewrittenCommit = z.object({
879
+ associations: z.array(zSessionAssociation),
880
+ authorTimeMs: z.int().nullish(),
881
+ confidence: zConfidence,
882
+ ghostHash: z.string(),
883
+ method: zRewriteMethod,
884
+ resolution: zRewriteResolution,
885
+ sessionIds: z.array(zSessionID),
886
+ subject: z.string(),
887
+ successorHash: z.string().nullish()
888
+ });
611
889
  export const zSessionIdentity = z.object({
612
890
  parentUuid: zSessionID.nullish(),
613
891
  schemaVersion: z.int(),
614
892
  sessionId: zSessionID
615
893
  });
894
+ export const zSessionInsight = z.object({
895
+ classification: zInsightClassification.nullish(),
896
+ confidence: zConfidence,
897
+ evidence: z.array(zInsightEvidence),
898
+ kind: zInsightKind,
899
+ provenance: zInsightProvenance,
900
+ subjects: z.array(z.string()),
901
+ summary: z.string().optional(),
902
+ title: z.string()
903
+ });
616
904
  /**
617
905
  * Session Outcome
618
906
  *
@@ -742,15 +1030,38 @@ export const zSubagentRef = z.object({
742
1030
  /**
743
1031
  * Target Kind
744
1032
  *
745
- * What is being annotated: session-level, entry-level (turn/tool call), meta-annotation, or project-level
1033
+ * What is being annotated: session-level, entry-level (turn/tool call), meta-annotation, project-level, a specific file version (content-hash keyed read-state receipt), or a durable session-to-commit association
746
1034
  */
747
1035
  export const zTargetKind = z.enum([
748
1036
  'session',
749
1037
  'entry',
750
1038
  'annotation',
751
- 'project'
1039
+ 'project',
1040
+ 'file_version',
1041
+ 'association'
752
1042
  ]);
753
- export const zAnnotationPushItem = z.object({
1043
+ export const zAnnotationPushItem = z.intersection(z.union([
1044
+ z.object({
1045
+ targetAssociationId: zAssociationID,
1046
+ targetKind: z.literal('association')
1047
+ }),
1048
+ z.object({
1049
+ sessionId: z.string().min(1),
1050
+ targetKind: z.literal('session')
1051
+ }),
1052
+ z.object({
1053
+ entryTarget: zAnnotationEntryTarget,
1054
+ targetKind: z.literal('entry')
1055
+ }),
1056
+ z.object({
1057
+ annotationId: z.string().min(1),
1058
+ targetKind: z.literal('annotation')
1059
+ }),
1060
+ z.object({
1061
+ projectHash: zProjectHash,
1062
+ targetKind: z.literal('project')
1063
+ })
1064
+ ]), z.object({
754
1065
  annotationId: z.string().nullish(),
755
1066
  annotatorName: z.string().optional(),
756
1067
  confidence: z.number().nullish(),
@@ -761,12 +1072,59 @@ export const zAnnotationPushItem = z.object({
761
1072
  provenance: zProvenance.nullish(),
762
1073
  reason: z.string().nullish(),
763
1074
  sessionId: z.string().nullish(),
1075
+ targetAssociationId: zAssociationID.nullish(),
764
1076
  targetKind: zTargetKind,
765
1077
  typeId: z.string(),
766
1078
  value: z.string()
1079
+ })).superRefine((value, context) => {
1080
+ const isPresent = (detail) => detail !== undefined && detail !== null;
1081
+ const hasNonEmptyString = (detail) => typeof detail === "string" && detail !== "";
1082
+ const hasOnly = (allowed) => {
1083
+ const targetFields = [
1084
+ ["targetAssociationId", value.targetAssociationId],
1085
+ ["sessionId", value.sessionId],
1086
+ ["entryTarget", value.entryTarget],
1087
+ ["annotationId", value.annotationId],
1088
+ ["projectHash", value.projectHash]
1089
+ ];
1090
+ return targetFields.every(([name, detail]) => allowed.includes(name) || !isPresent(detail));
1091
+ };
1092
+ const addIssue = (message) => context.addIssue({ code: "custom", message });
1093
+ switch (value.targetKind) {
1094
+ case "association":
1095
+ if (!isPresent(value.targetAssociationId))
1096
+ addIssue("association annotations require targetAssociationId");
1097
+ if (!hasOnly(["targetAssociationId"]))
1098
+ addIssue("association annotations must not mix target arms");
1099
+ return;
1100
+ case "session":
1101
+ if (!hasNonEmptyString(value.sessionId))
1102
+ addIssue("session annotations require a non-empty sessionId");
1103
+ if (!hasOnly(["sessionId"]))
1104
+ addIssue("session annotations must not mix target arms");
1105
+ return;
1106
+ case "entry":
1107
+ if (!isPresent(value.entryTarget))
1108
+ addIssue("entry annotations require entryTarget");
1109
+ if (!hasOnly(["entryTarget"]))
1110
+ addIssue("entry annotations must not mix target arms");
1111
+ return;
1112
+ case "annotation":
1113
+ if (!hasNonEmptyString(value.annotationId))
1114
+ addIssue("annotation targets require a non-empty annotationId");
1115
+ if (!hasOnly(["annotationId"]))
1116
+ addIssue("annotation targets must not mix target arms");
1117
+ return;
1118
+ case "project":
1119
+ if (!isPresent(value.projectHash))
1120
+ addIssue("project annotations require projectHash");
1121
+ if (!hasOnly(["projectHash"]))
1122
+ addIssue("project annotations must not mix target arms");
1123
+ return;
1124
+ }
767
1125
  });
768
1126
  export const zAnnotationPushRequest = z.object({
769
- annotations: z.array(zAnnotationPushItem).nullable(),
1127
+ annotations: z.array(zAnnotationPushItem),
770
1128
  retractions: z.array(z.string()).optional()
771
1129
  });
772
1130
  export const zAnnotationSummary = z.object({
@@ -781,14 +1139,74 @@ export const zAnnotationSummary = z.object({
781
1139
  reason: z.string().nullish(),
782
1140
  supersededBy: z.string().nullish(),
783
1141
  targetAnnotationId: z.string().nullish(),
1142
+ targetAssociationId: zAssociationID.nullish(),
1143
+ targetContentHash: z.string().nullish(),
784
1144
  targetEntryEndIndex: z.int().nullish(),
785
1145
  targetEntryIndex: z.int().nullish(),
1146
+ targetFilePath: z.string().nullish(),
786
1147
  targetKind: zTargetKind,
787
1148
  targetProjectHash: zProjectHash.nullish(),
788
1149
  targetSessionId: z.string().nullish(),
789
1150
  typeId: z.string(),
790
1151
  typeName: z.string(),
791
1152
  value: z.string()
1153
+ }).superRefine((value, context) => {
1154
+ const isPresent = (detail) => detail !== undefined && detail !== null;
1155
+ const hasNonEmptyString = (detail) => typeof detail === "string" && detail !== "";
1156
+ const hasOnly = (allowed) => {
1157
+ const targetFields = [
1158
+ ["targetAssociationId", value.targetAssociationId],
1159
+ ["targetSessionId", value.targetSessionId],
1160
+ ["targetEntryIndex", value.targetEntryIndex],
1161
+ ["targetEntryEndIndex", value.targetEntryEndIndex],
1162
+ ["targetAnnotationId", value.targetAnnotationId],
1163
+ ["targetProjectHash", value.targetProjectHash],
1164
+ ["targetFilePath", value.targetFilePath],
1165
+ ["targetContentHash", value.targetContentHash]
1166
+ ];
1167
+ return targetFields.every(([name, detail]) => allowed.includes(name) || !isPresent(detail));
1168
+ };
1169
+ const addIssue = (message) => context.addIssue({ code: "custom", message });
1170
+ switch (value.targetKind) {
1171
+ case "association":
1172
+ if (!isPresent(value.targetAssociationId))
1173
+ addIssue("association annotations require targetAssociationId");
1174
+ if (!hasOnly(["targetAssociationId"]))
1175
+ addIssue("association annotations must not mix target arms");
1176
+ return;
1177
+ case "session":
1178
+ if (!hasNonEmptyString(value.targetSessionId))
1179
+ addIssue("session annotations require a non-empty targetSessionId");
1180
+ if (!hasOnly(["targetSessionId"]))
1181
+ addIssue("session annotations must not mix target arms");
1182
+ return;
1183
+ case "entry":
1184
+ if (!hasNonEmptyString(value.targetSessionId) || !isPresent(value.targetEntryIndex))
1185
+ addIssue("entry annotations require targetSessionId and targetEntryIndex");
1186
+ if (!hasOnly(["targetSessionId", "targetEntryIndex", "targetEntryEndIndex"]))
1187
+ addIssue("entry annotations must not mix target arms");
1188
+ if (value.targetEntryIndex !== undefined && value.targetEntryIndex !== null && value.targetEntryEndIndex !== undefined && value.targetEntryEndIndex !== null && value.targetEntryEndIndex <= value.targetEntryIndex)
1189
+ addIssue("entry annotation targetEntryEndIndex must be greater than targetEntryIndex");
1190
+ return;
1191
+ case "annotation":
1192
+ if (!hasNonEmptyString(value.targetAnnotationId))
1193
+ addIssue("annotation targets require a non-empty targetAnnotationId");
1194
+ if (!hasOnly(["targetAnnotationId"]))
1195
+ addIssue("annotation targets must not mix target arms");
1196
+ return;
1197
+ case "project":
1198
+ if (!isPresent(value.targetProjectHash))
1199
+ addIssue("project annotations require targetProjectHash");
1200
+ if (!hasOnly(["targetProjectHash"]))
1201
+ addIssue("project annotations must not mix target arms");
1202
+ return;
1203
+ case "file_version":
1204
+ if (!hasNonEmptyString(value.targetFilePath) || !hasNonEmptyString(value.targetContentHash))
1205
+ addIssue("file_version annotations require non-empty targetFilePath and targetContentHash");
1206
+ if (!hasOnly(["targetFilePath", "targetContentHash"]))
1207
+ addIssue("file_version annotations must not mix target arms");
1208
+ return;
1209
+ }
792
1210
  });
793
1211
  export const zAnnotationsPayload = z.object({
794
1212
  annotations: z.array(zAnnotationSummary).nullable(),
@@ -809,8 +1227,11 @@ export const zPullAnnotation = z.object({
809
1227
  reason: z.string().nullish(),
810
1228
  supersededBy: z.string().nullish(),
811
1229
  targetAnnotationId: z.string().nullish(),
1230
+ targetAssociationId: zAssociationID.optional(),
1231
+ targetContentHash: z.string().nullish(),
812
1232
  targetEntryEndIndex: z.int().nullish(),
813
1233
  targetEntryIndex: z.int().nullish(),
1234
+ targetFilePath: z.string().nullish(),
814
1235
  targetKind: zTargetKind.optional(),
815
1236
  targetProjectHash: zProjectHash.optional(),
816
1237
  targetSessionId: z.string().nullish(),
@@ -852,6 +1273,7 @@ export const zTaskSummary = z.object({
852
1273
  labels: z.array(z.string()).nullable(),
853
1274
  outcome: z.string().optional(),
854
1275
  readCount: z.int(),
1276
+ readFiles: z.array(z.string()),
855
1277
  retryLoop: z.boolean(),
856
1278
  sessionId: z.string(),
857
1279
  startMs: z.int().nullish(),
@@ -868,6 +1290,7 @@ export const zChangeSession = z.object({
868
1290
  export const zMapNodeDetailPayload = z.object({
869
1291
  costUsd: z.number().nullish(),
870
1292
  dependsOn: z.array(z.string()).nullable(),
1293
+ insights: z.array(zSessionInsight),
871
1294
  kind: zMapNodeKind,
872
1295
  language: z.string().optional(),
873
1296
  lastTouchMs: z.int().nullish(),
@@ -877,6 +1300,7 @@ export const zMapNodeDetailPayload = z.object({
877
1300
  recentCommits: z.array(zCommitRef).nullable(),
878
1301
  recordedFiles: z.int(),
879
1302
  retryLoops: z.int(),
1303
+ rewrittenCommits: z.array(zRewrittenCommit),
880
1304
  sessionCount: z.int(),
881
1305
  shapedBy: z.array(zTaskSummary).nullable(),
882
1306
  taskCount: z.int(),
@@ -901,6 +1325,7 @@ export const zReviewListPayload = z.object({
901
1325
  projectHash: zProjectHash,
902
1326
  recentCommits: z.array(zCommitRef),
903
1327
  repoFound: z.boolean(),
1328
+ rewrittenCommits: z.array(zRewrittenCommit),
904
1329
  sessions: z.array(zTimelineSessionRef)
905
1330
  });
906
1331
  export const zTimestampInfo = z.object({
@@ -1091,6 +1516,7 @@ export const zChangeDetailPayload = z.object({
1091
1516
  defaultBranch: z.string(),
1092
1517
  files: z.array(zFileChange).nullable(),
1093
1518
  frictions: z.array(zFrictionCluster).nullable(),
1519
+ insights: z.array(zSessionInsight),
1094
1520
  linesAdded: z.int(),
1095
1521
  linesRemoved: z.int(),
1096
1522
  newEdges: z.array(zMapEdge).nullable(),