@hobin/developer 0.1.9 → 0.1.11

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.
@@ -18,6 +18,19 @@ import {
18
18
  import { Container, Markdown, Text } from "@earendil-works/pi-tui";
19
19
  import { Type, type Static } from "typebox";
20
20
 
21
+ import {
22
+ COMPACTION_LANGUAGE_ENTRY,
23
+ applyCompactionLanguageEvent,
24
+ continuityConsumed,
25
+ continuityPending,
26
+ detectStrongUserLanguage,
27
+ initialCompactionLanguageState,
28
+ languageObserved,
29
+ projectCompactionContinuity,
30
+ reconstructCompactionLanguage,
31
+ settlementContinuityEvent,
32
+ type CompactionLanguageState,
33
+ } from "./compaction-language.ts";
21
34
  import {
22
35
  availablePackageSkills,
23
36
  isWithinRoot,
@@ -40,6 +53,7 @@ import {
40
53
  applyDeveloperEvent,
41
54
  canApplyDeveloperEvent,
42
55
  developerSnapshot,
56
+ formatInvariantHandling,
43
57
  initialState,
44
58
  normalizeDeveloperEvent,
45
59
  parseChoiceResponseSpec,
@@ -49,6 +63,7 @@ import {
49
63
  type ChoiceResponseSpec,
50
64
  type DeveloperState,
51
65
  type ImplementationProfile,
66
+ type InvariantHandling,
52
67
  type FocusEvent,
53
68
  type JudgmentEvent,
54
69
  type PendingQuestion,
@@ -202,6 +217,59 @@ interface ChoiceResponseSpecInput {
202
217
  fields: ChoiceResponseFieldInput[];
203
218
  }
204
219
 
220
+ type InvariantHandlingInput =
221
+ | {
222
+ kind: "not-applicable";
223
+ reason: string;
224
+ }
225
+ | {
226
+ kind: "evidence-preserving-boundary";
227
+ raw_representation: string;
228
+ refined_representation: string;
229
+ producer: string;
230
+ failure: string;
231
+ first_effect: string;
232
+ }
233
+ | {
234
+ kind: "trusted-compiler-gap";
235
+ assertion: string;
236
+ established_by: string;
237
+ limitation: string;
238
+ containment: string;
239
+ verification: string;
240
+ };
241
+
242
+ function invariantHandlingFromInput(
243
+ input: InvariantHandlingInput | undefined,
244
+ ): InvariantHandling {
245
+ if (!input) {
246
+ return {
247
+ kind: "not-applicable",
248
+ reason:
249
+ "Legacy direct tool execution omitted the now-required invariant-handling declaration.",
250
+ };
251
+ }
252
+ if (input.kind === "not-applicable") return input;
253
+ if (input.kind === "evidence-preserving-boundary") {
254
+ return {
255
+ kind: input.kind,
256
+ rawRepresentation: input.raw_representation,
257
+ refinedRepresentation: input.refined_representation,
258
+ producer: input.producer,
259
+ failure: input.failure,
260
+ firstEffect: input.first_effect,
261
+ };
262
+ }
263
+ return {
264
+ kind: input.kind,
265
+ assertion: input.assertion,
266
+ establishedBy: input.established_by,
267
+ limitation: input.limitation,
268
+ containment: input.containment,
269
+ verification: input.verification,
270
+ };
271
+ }
272
+
205
273
  interface OpenQuestionInput {
206
274
  question: string;
207
275
  context?: string;
@@ -449,7 +517,9 @@ function protocolPrompt(
449
517
  "- Adapt away from that topology when evidence makes a stage not applicable, but never jump directly from a resolved model to mutation: use sketch for feature implementation or signal for existing-code structural movement first.",
450
518
  "- Route by the requested work product, not keywords: use sketch to create an original caller-facing interface, representation boundary, ownership map, or collaboration; use abstraction-review only when a concrete candidate surface already exists to keep/revise/split/reject/defer; use model first only when unresolved cases, rules, or policy can materially change that surface.",
451
519
  `- Call ${ROUTE_TOOL} for exactly one concrete judgment or one green-to-green implementation movement.`,
452
- "- Use target=implementation only when the next local movement, stable landing, and narrow verification are already justified; otherwise choose the focused skill whose scope fits the current question.",
520
+ "- Use target=implementation only when the next local movement, stable landing, narrow verification, and invariant handling are already justified; otherwise choose the focused skill whose scope fits the current question.",
521
+ "- Invariant gate: an unchecked assertion, cast, non-null claim, ignored conversion result, or typed deserialization target is not evidence that broader, external, persisted, legacy, or otherwise less-trusted input satisfies a domain invariant. Parse or construct a refined value whose success carries the learned information before dependent effects; validation followed by narrowing does not satisfy this gate.",
522
+ "- Every implementation route must classify invariant handling as not applicable, an evidence-preserving boundary, or a trusted compiler gap. A trusted compiler gap is valid only after a complete check or trusted contract already established the fact, the language cannot retain it, the unsafe operation is isolated to one owner, and a falsifier is named. Encountering a missing boundary requires sketch, not an implementation assertion.",
453
523
  "- An implementation step has one observable difference and one structural or behavioral purpose. Stop when its failure is locally explainable and the repository is green, pausable, and reviewable.",
454
524
  "- For implementation structural work intended to preserve behavior, set execution_profile=behavior-preserving-structure; omit the field for other implementation actions and every skill route.",
455
525
  `- Follow the routed method, then close the route with ${JUDGMENT_TOOL}. After every implementation stable landing, route again from the new evidence before selecting another movement.`,
@@ -498,6 +568,11 @@ function protocolPrompt(
498
568
  lines.push(
499
569
  `Active route: ${state.activeRoute.routeId} · ${state.activeRoute.target} · ${state.activeRoute.question}`,
500
570
  );
571
+ if (state.activeRoute.implementationStep) {
572
+ lines.push(
573
+ `Active invariant handling: ${formatInvariantHandling(state.activeRoute.implementationStep.invariantHandling)}. Stop and route sketch instead of mutating if this declaration is contradicted by the code.`,
574
+ );
575
+ }
501
576
  if (state.activeRoute.methodLocation) {
502
577
  lines.push(
503
578
  `Active skill location: ${state.activeRoute.methodLocation}. Read it again if compaction or a later turn no longer contains the full instructions.`,
@@ -621,6 +696,8 @@ function expandedJudgment(
621
696
  export default async function developer(pi: ExtensionAPI) {
622
697
  let availableSkills = new Map<string, Skill>();
623
698
  let state = initialState();
699
+ let compactionLanguage: CompactionLanguageState =
700
+ initialCompactionLanguageState();
624
701
  let routeOpening = false;
625
702
  const routesWithMutation = new Set<string>();
626
703
  let toolPolicyMemory: ToolPolicyMemory = { withheldBuiltins: new Set() };
@@ -732,9 +809,11 @@ export default async function developer(pi: ExtensionAPI) {
732
809
  };
733
810
 
734
811
  const reconstruct = (ctx: ExtensionContext) => {
812
+ const branch = ctx.sessionManager.getBranch();
735
813
  state = toolPolicyRestartRequired
736
814
  ? initialState()
737
- : reconstructState(ctx.sessionManager.getBranch());
815
+ : reconstructState(branch);
816
+ compactionLanguage = reconstructCompactionLanguage(branch);
738
817
  syncProtocolTools();
739
818
  refreshUI(ctx);
740
819
  };
@@ -752,6 +831,16 @@ export default async function developer(pi: ExtensionAPI) {
752
831
  };
753
832
  pi.appendEntry(ACTIVATION_ENTRY, event);
754
833
  state = applyDeveloperEvent(state, event);
834
+ if (!enabled && compactionLanguage.pending) {
835
+ const consumed = continuityConsumed(
836
+ compactionLanguage.pending.compactionId,
837
+ );
838
+ pi.appendEntry(COMPACTION_LANGUAGE_ENTRY, consumed);
839
+ compactionLanguage = applyCompactionLanguageEvent(
840
+ compactionLanguage,
841
+ consumed,
842
+ );
843
+ }
755
844
  syncProtocolTools();
756
845
  refreshUI(ctx);
757
846
  return true;
@@ -798,6 +887,83 @@ export default async function developer(pi: ExtensionAPI) {
798
887
  },
799
888
  { additionalProperties: false },
800
889
  );
890
+ const InvariantHandlingParam = Type.Union([
891
+ Type.Object(
892
+ {
893
+ kind: Type.Literal("not-applicable"),
894
+ reason: Type.String({
895
+ minLength: 1,
896
+ maxLength: MAX_EVIDENCE_CHARS,
897
+ description:
898
+ "Why this movement neither creates nor narrows broader or less-trusted data into an invariant-carrying value",
899
+ }),
900
+ },
901
+ { additionalProperties: false },
902
+ ),
903
+ Type.Object(
904
+ {
905
+ kind: Type.Literal("evidence-preserving-boundary"),
906
+ raw_representation: Type.String({
907
+ minLength: 1,
908
+ maxLength: MAX_EVIDENCE_CHARS,
909
+ }),
910
+ refined_representation: Type.String({
911
+ minLength: 1,
912
+ maxLength: MAX_EVIDENCE_CHARS,
913
+ }),
914
+ producer: Type.String({
915
+ minLength: 1,
916
+ maxLength: MAX_EVIDENCE_CHARS,
917
+ description:
918
+ "Parser, refinement function, or smart constructor whose success returns the refined value",
919
+ }),
920
+ failure: Type.String({
921
+ minLength: 1,
922
+ maxLength: MAX_EVIDENCE_CHARS,
923
+ }),
924
+ first_effect: Type.String({
925
+ minLength: 1,
926
+ maxLength: MAX_EVIDENCE_CHARS,
927
+ description:
928
+ "First dependent domain effect allowed only after successful refinement",
929
+ }),
930
+ },
931
+ { additionalProperties: false },
932
+ ),
933
+ Type.Object(
934
+ {
935
+ kind: Type.Literal("trusted-compiler-gap"),
936
+ assertion: Type.String({
937
+ minLength: 1,
938
+ maxLength: MAX_EVIDENCE_CHARS,
939
+ }),
940
+ established_by: Type.String({
941
+ minLength: 1,
942
+ maxLength: MAX_EVIDENCE_CHARS,
943
+ description:
944
+ "Complete check or trusted runtime contract that already established the asserted fact",
945
+ }),
946
+ limitation: Type.String({
947
+ minLength: 1,
948
+ maxLength: MAX_EVIDENCE_CHARS,
949
+ description:
950
+ "Why ordinary language narrowing cannot retain the established fact",
951
+ }),
952
+ containment: Type.String({
953
+ minLength: 1,
954
+ maxLength: MAX_EVIDENCE_CHARS,
955
+ description: "Single parser or constructor owner containing the gap",
956
+ }),
957
+ verification: Type.String({
958
+ minLength: 1,
959
+ maxLength: MAX_EVIDENCE_CHARS,
960
+ description:
961
+ "Falsifier for the claimed prior evidence and containment",
962
+ }),
963
+ },
964
+ { additionalProperties: false },
965
+ ),
966
+ ]);
801
967
  const RouteParams = Type.Union([
802
968
  Type.Object(
803
969
  {
@@ -840,6 +1006,7 @@ export default async function developer(pi: ExtensionAPI) {
840
1006
  description:
841
1007
  "The narrowest check that can catch the likely break in this movement",
842
1008
  }),
1009
+ invariant_handling: InvariantHandlingParam,
843
1010
  alternatives_considered: Type.Optional(
844
1011
  Type.Array(RouteAlternativeParam, {
845
1012
  maxItems: 6,
@@ -865,11 +1032,11 @@ export default async function developer(pi: ExtensionAPI) {
865
1032
  name: ROUTE_TOOL,
866
1033
  label: "Developer Route Question",
867
1034
  description:
868
- "Route one concrete judgment or one green-to-green implementation movement. Route by work product: sketch creates an original design surface, abstraction-review judges an already-shaped candidate, and model settles unresolved conditions before design. Then use implementation stable landings and verify before completion.",
1035
+ "Route one concrete judgment or one green-to-green implementation movement. Route by work product: sketch creates an original design surface, abstraction-review judges an already-shaped candidate, and model settles unresolved conditions before design. Implementation must declare how invariant-bearing values are constructed without unchecked narrowing. Then use stable landings and verify before completion.",
869
1036
  promptSnippet: "Choose how to handle one development question",
870
1037
  promptGuidelines: [
871
1038
  `Call ${ROUTE_TOOL} only when there is no active Developer route.`,
872
- `Use ${ROUTE_TOOL} with the most focused skill supported by current evidence; choose sketch for an original interface or boundary, abstraction-review only for an already-shaped candidate, and model only for unresolved condition space. target=implementation requires one movement, one stable landing, and one narrow verification.`,
1039
+ `Use ${ROUTE_TOOL} with the most focused skill supported by current evidence; choose sketch for an original interface or boundary, abstraction-review only for an already-shaped candidate, and model only for unresolved condition space. target=implementation requires one movement, one stable landing, one narrow verification, and a truthful invariant_handling declaration. Validation followed by an assertion is not an evidence-preserving boundary.`,
873
1040
  `When ${ROUTE_TOOL} follows an implementation judgment with another implementation route, cite the previous landing in known_evidence and record plausible skill routes in alternatives_considered instead of selecting implementation by momentum.`,
874
1041
  `After a resolved model, use sketch for first feature implementation framing or signal for existing-code structural movement before implementation mutation.`,
875
1042
  ],
@@ -1014,6 +1181,10 @@ export default async function developer(pi: ExtensionAPI) {
1014
1181
  "# Implementation action",
1015
1182
  "",
1016
1183
  "The next local action is already justified. Keep this route open while using Pi implementation tools and collecting evidence.",
1184
+ "",
1185
+ "## Invariant gate",
1186
+ "",
1187
+ "Follow the route's invariant-handling declaration. Broader or less-trusted input must become an invariant-carrying value through a parser, refinement function, or smart constructor before dependent effects. Validation followed by an assertion, cast, non-null claim, ignored conversion result, or typed decode is not evidence. If the declared handling is incomplete, stop without mutation and route sketch. Keep any trusted compiler gap isolated to its declared owner and verify its prior evidence and containment.",
1017
1188
  ].join("\n");
1018
1189
  }
1019
1190
 
@@ -1032,6 +1203,11 @@ export default async function developer(pi: ExtensionAPI) {
1032
1203
  ? params.verification
1033
1204
  : "Run the narrowest relevant check and inspect the resulting diff or output."
1034
1205
  ).trim(),
1206
+ invariantHandling: invariantHandlingFromInput(
1207
+ "invariant_handling" in params
1208
+ ? params.invariant_handling
1209
+ : undefined,
1210
+ ),
1035
1211
  }
1036
1212
  : undefined;
1037
1213
 
@@ -1069,6 +1245,7 @@ export default async function developer(pi: ExtensionAPI) {
1069
1245
  `Movement: ${implementationStep.movement}`,
1070
1246
  `Stable landing: ${implementationStep.stopCondition}`,
1071
1247
  `Narrow verification: ${implementationStep.verification}`,
1248
+ `Invariant handling: ${formatInvariantHandling(implementationStep.invariantHandling)}`,
1072
1249
  "Stop this implementation route at that landing, record the evidence, and route again before another movement.",
1073
1250
  ]
1074
1251
  : []),
@@ -1146,7 +1323,8 @@ export default async function developer(pi: ExtensionAPI) {
1146
1323
  context.lastComponent,
1147
1324
  );
1148
1325
  }
1149
- const event = result.details as RouteEvent | undefined;
1326
+ const normalized = normalizeDeveloperEvent(result.details);
1327
+ const event = normalized?.kind === "route" ? normalized : undefined;
1150
1328
  let text = theme.fg("success", `routed ${routeRenderText(event)}`);
1151
1329
  if (expanded && event) {
1152
1330
  text += `\n${detailLine(theme, "route", event.routeId, "text")}`;
@@ -1180,6 +1358,7 @@ export default async function developer(pi: ExtensionAPI) {
1180
1358
  text += `\n${detailLine(theme, "movement", event.implementationStep.movement, "text")}`;
1181
1359
  text += `\n${detailLine(theme, "landing", event.implementationStep.stopCondition)}`;
1182
1360
  text += `\n${detailLine(theme, "verify", event.implementationStep.verification)}`;
1361
+ text += `\n${detailLine(theme, "invariant", formatInvariantHandling(event.implementationStep.invariantHandling))}`;
1183
1362
  }
1184
1363
  }
1185
1364
  if (!expanded && event)
@@ -1355,7 +1534,9 @@ export default async function developer(pi: ExtensionAPI) {
1355
1534
  context.lastComponent,
1356
1535
  );
1357
1536
  }
1358
- const event = result.details as ReferenceLoadEvent | undefined;
1537
+ const normalized = normalizeDeveloperEvent(result.details);
1538
+ const event =
1539
+ normalized?.kind === "reference-load" ? normalized : undefined;
1359
1540
  let text = theme.fg(
1360
1541
  "success",
1361
1542
  `loaded ${event?.path ?? "Developer reference"}`,
@@ -1984,7 +2165,8 @@ export default async function developer(pi: ExtensionAPI) {
1984
2165
  context.lastComponent,
1985
2166
  );
1986
2167
  }
1987
- const event = result.details as JudgmentEvent | undefined;
2168
+ const normalized = normalizeDeveloperEvent(result.details);
2169
+ const event = normalized?.kind === "judgment" ? normalized : undefined;
1988
2170
  const summary = judgmentRenderText(event);
1989
2171
  let text =
1990
2172
  event?.status === "resolved"
@@ -2274,6 +2456,41 @@ export default async function developer(pi: ExtensionAPI) {
2274
2456
  },
2275
2457
  );
2276
2458
 
2459
+ pi.on("input", (event) => {
2460
+ if (!state.enabled) return;
2461
+ const tag = detectStrongUserLanguage(event.text, event.source);
2462
+ if (!tag || tag === compactionLanguage.language) return;
2463
+ const observed = languageObserved(tag);
2464
+ pi.appendEntry(COMPACTION_LANGUAGE_ENTRY, observed);
2465
+ compactionLanguage = applyCompactionLanguageEvent(
2466
+ compactionLanguage,
2467
+ observed,
2468
+ );
2469
+ });
2470
+
2471
+ pi.on("session_compact", (event) => {
2472
+ if (!state.enabled || !compactionLanguage.language) return;
2473
+ const pending = continuityPending(
2474
+ event.compactionEntry.id,
2475
+ compactionLanguage.language,
2476
+ );
2477
+ const next = applyCompactionLanguageEvent(compactionLanguage, pending);
2478
+ if (next === compactionLanguage) return;
2479
+ pi.appendEntry(COMPACTION_LANGUAGE_ENTRY, pending);
2480
+ compactionLanguage = next;
2481
+ });
2482
+
2483
+ pi.on("context", (event) => {
2484
+ if (!state.enabled) return;
2485
+ const projection = projectCompactionContinuity(
2486
+ event.messages,
2487
+ compactionLanguage,
2488
+ );
2489
+ if (!projection) return;
2490
+ compactionLanguage = projection.state;
2491
+ return { messages: projection.messages as typeof event.messages };
2492
+ });
2493
+
2277
2494
  pi.on("before_agent_start", (event) => {
2278
2495
  availableSkills = availablePackageSkills(
2279
2496
  event.systemPromptOptions.skills ?? [],
@@ -2373,7 +2590,17 @@ export default async function developer(pi: ExtensionAPI) {
2373
2590
  if (startEnabled === true && !state.enabled) setEnabled(true, ctx);
2374
2591
  });
2375
2592
  pi.on("session_tree", (_event, ctx) => reconstruct(ctx));
2376
- pi.on("agent_settled", (_event, ctx) => refreshUI(ctx));
2593
+ pi.on("agent_settled", (_event, ctx) => {
2594
+ const consumed = settlementContinuityEvent(compactionLanguage);
2595
+ if (consumed) {
2596
+ pi.appendEntry(COMPACTION_LANGUAGE_ENTRY, consumed);
2597
+ compactionLanguage = applyCompactionLanguageEvent(
2598
+ compactionLanguage,
2599
+ consumed,
2600
+ );
2601
+ }
2602
+ refreshUI(ctx);
2603
+ });
2377
2604
  pi.on("session_shutdown", (_event, ctx) => {
2378
2605
  releaseProtocolTools();
2379
2606
  if (!toolPolicyRestartRequired) {
@@ -46,7 +46,10 @@ For movement across a boundary, update one sender or caller at a time when the
46
46
  old and new forms can safely coexist. Confirm that each intermediate state is
47
47
  valid. Do not rely on a final large diff to explain which step changed behavior.
48
48
  A forwarding interface over the old implementation can be a temporary seam, but
49
- it is not evidence that the interface is a stable public abstraction.
49
+ it is not evidence that the interface is a stable public abstraction. An
50
+ assertion, cast, non-null claim, or unchecked typed decode is not a compatibility
51
+ seam: if old or external data is broader than the new invariant, an adapter must
52
+ parse and return the new representation or explicit failure.
50
53
 
51
54
  For suspected dead code, combine static references, dynamic reachability, and a
52
55
  scoped observation window appropriate to rare jobs, reflection, configuration,
@@ -192,10 +195,15 @@ The protocol is being misused when:
192
195
  - behavior work is hidden inside a structural label;
193
196
  - the route continues after a new human-owned policy question appears;
194
197
  - an intermediate state cannot safely run or be reviewed;
195
- - cleanup continues past the accepted pressure because the code could be nicer.
198
+ - cleanup continues past the accepted pressure because the code could be nicer;
199
+ - validation is preserved while its result is discarded and an assertion claims
200
+ the new representation.
196
201
 
197
202
  ## Source Trace
198
203
 
204
+ - Alexis King, “Parse, don’t validate,” published November 5, 2019, for
205
+ evidence-preserving refinement before execution. The compatibility-seam and
206
+ migration wording is a Developer adaptation.
199
207
  - Sandi Metz, Katrina Owen, and TJ Stankus, *99 Bottles of OOP*, Second
200
208
  Edition, version 2.2.2, 2024: Chapters 3-5, pp. 51-137, especially
201
209
  pp. 58-71, 84-101, and 113-125, on methodical transformations, flocking,
@@ -86,13 +86,17 @@ export function availablePackageSkills(
86
86
  return available;
87
87
  }
88
88
 
89
+ function hasErrorCode(error: unknown, code: string): boolean {
90
+ return isRecord(error) && error.code === code;
91
+ }
92
+
89
93
  export async function skillReferencePaths(skill: Skill): Promise<string[]> {
90
94
  const referencesRoot = resolve(skill.baseDir, "references");
91
95
  let entries: Dirent[];
92
96
  try {
93
97
  entries = await readdir(referencesRoot, { withFileTypes: true });
94
98
  } catch (error) {
95
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
99
+ if (hasErrorCode(error, "ENOENT")) return [];
96
100
  throw error;
97
101
  }
98
102
 
@@ -248,7 +252,7 @@ export async function loadSkillReferencePolicy(
248
252
  try {
249
253
  source = await readFile(policyPath, "utf8");
250
254
  } catch (error) {
251
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
255
+ if (hasErrorCode(error, "ENOENT")) {
252
256
  if (catalog.length > 0) {
253
257
  throw new Error(
254
258
  `Developer skill ${skill.name} has references but no reference-policy.json.`,
@@ -46,6 +46,36 @@ export type QuestionUpdateStatus =
46
46
  export type ImplementationProfile =
47
47
  | "ordinary"
48
48
  | "behavior-preserving-structure";
49
+ export type InvariantHandling =
50
+ | {
51
+ kind: "not-applicable";
52
+ reason: string;
53
+ }
54
+ | {
55
+ kind: "evidence-preserving-boundary";
56
+ rawRepresentation: string;
57
+ refinedRepresentation: string;
58
+ producer: string;
59
+ failure: string;
60
+ firstEffect: string;
61
+ }
62
+ | {
63
+ kind: "trusted-compiler-gap";
64
+ assertion: string;
65
+ establishedBy: string;
66
+ limitation: string;
67
+ containment: string;
68
+ verification: string;
69
+ };
70
+
71
+ export function formatInvariantHandling(value: InvariantHandling): string {
72
+ if (value.kind === "not-applicable")
73
+ return `not applicable — ${value.reason}`;
74
+ if (value.kind === "evidence-preserving-boundary") {
75
+ return `${value.rawRepresentation} -> ${value.refinedRepresentation} via ${value.producer}; failure=${value.failure}; first effect=${value.firstEffect}`;
76
+ }
77
+ return `trusted compiler gap — ${value.assertion}; established by ${value.establishedBy}; limitation=${value.limitation}; contained at ${value.containment}; check=${value.verification}`;
78
+ }
49
79
 
50
80
  export interface ActivationEvent {
51
81
  protocol: typeof PROTOCOL;
@@ -63,6 +93,7 @@ export interface ImplementationStepContract {
63
93
  movement: string;
64
94
  stopCondition: string;
65
95
  verification: string;
96
+ invariantHandling: InvariantHandling;
66
97
  }
67
98
 
68
99
  export interface RouteAlternative {
@@ -248,6 +279,20 @@ function isStringArray(value: unknown): value is string[] {
248
279
  );
249
280
  }
250
281
 
282
+ function parseArray<T>(
283
+ value: unknown,
284
+ parse: (item: unknown) => T | undefined,
285
+ ): T[] | undefined {
286
+ if (!Array.isArray(value)) return undefined;
287
+ const parsed: T[] = [];
288
+ for (const item of value) {
289
+ const result = parse(item);
290
+ if (result === undefined) return undefined;
291
+ parsed.push(result);
292
+ }
293
+ return parsed;
294
+ }
295
+
251
296
  function isJudgmentStatus(value: unknown): value is JudgmentStatus {
252
297
  return (
253
298
  value === "resolved" ||
@@ -263,6 +308,51 @@ function isImplementationProfile(
263
308
  return value === "ordinary" || value === "behavior-preserving-structure";
264
309
  }
265
310
 
311
+ function parseInvariantHandling(value: unknown): InvariantHandling | undefined {
312
+ if (!isObject(value) || typeof value.kind !== "string") return undefined;
313
+ if (value.kind === "not-applicable") {
314
+ if (typeof value.reason !== "string") return undefined;
315
+ return { kind: value.kind, reason: value.reason };
316
+ }
317
+ if (value.kind === "evidence-preserving-boundary") {
318
+ if (
319
+ typeof value.rawRepresentation !== "string" ||
320
+ typeof value.refinedRepresentation !== "string" ||
321
+ typeof value.producer !== "string" ||
322
+ typeof value.failure !== "string" ||
323
+ typeof value.firstEffect !== "string"
324
+ ) {
325
+ return undefined;
326
+ }
327
+ return {
328
+ kind: value.kind,
329
+ rawRepresentation: value.rawRepresentation,
330
+ refinedRepresentation: value.refinedRepresentation,
331
+ producer: value.producer,
332
+ failure: value.failure,
333
+ firstEffect: value.firstEffect,
334
+ };
335
+ }
336
+ if (value.kind !== "trusted-compiler-gap") return undefined;
337
+ if (
338
+ typeof value.assertion !== "string" ||
339
+ typeof value.establishedBy !== "string" ||
340
+ typeof value.limitation !== "string" ||
341
+ typeof value.containment !== "string" ||
342
+ typeof value.verification !== "string"
343
+ ) {
344
+ return undefined;
345
+ }
346
+ return {
347
+ kind: value.kind,
348
+ assertion: value.assertion,
349
+ establishedBy: value.establishedBy,
350
+ limitation: value.limitation,
351
+ containment: value.containment,
352
+ verification: value.verification,
353
+ };
354
+ }
355
+
266
356
  function parseImplementationStep(
267
357
  value: unknown,
268
358
  ): ImplementationStepContract | undefined {
@@ -274,10 +364,20 @@ function parseImplementationStep(
274
364
  ) {
275
365
  return undefined;
276
366
  }
367
+ const invariantHandling =
368
+ value.invariantHandling === undefined
369
+ ? {
370
+ kind: "not-applicable" as const,
371
+ reason:
372
+ "Legacy implementation route predates the invariant-handling declaration.",
373
+ }
374
+ : parseInvariantHandling(value.invariantHandling);
375
+ if (!invariantHandling) return undefined;
277
376
  return {
278
377
  movement: value.movement,
279
378
  stopCondition: value.stopCondition,
280
379
  verification: value.verification,
380
+ invariantHandling,
281
381
  };
282
382
  }
283
383
 
@@ -622,19 +722,21 @@ export function normalizeDeveloperEvent(
622
722
  ) {
623
723
  return undefined;
624
724
  }
625
- const consideredAlternatives = Array.isArray(value.consideredAlternatives)
626
- ? value.consideredAlternatives.map(parseRouteAlternative)
627
- : [];
628
- if (consideredAlternatives.some((alternative) => !alternative))
629
- return undefined;
630
- const referenceRoutes = Array.isArray(value.referenceRoutes)
631
- ? value.referenceRoutes.map(parseReferencePolicyRoute)
632
- : [];
633
- if (referenceRoutes.some((route) => !route)) return undefined;
634
- const loadedReferences = Array.isArray(value.loadedReferences)
635
- ? value.loadedReferences.map(parseReferenceLoad)
636
- : [];
637
- if (loadedReferences.some((reference) => !reference)) return undefined;
725
+ const consideredAlternatives =
726
+ value.consideredAlternatives === undefined
727
+ ? []
728
+ : parseArray(value.consideredAlternatives, parseRouteAlternative);
729
+ if (!consideredAlternatives) return undefined;
730
+ const referenceRoutes =
731
+ value.referenceRoutes === undefined
732
+ ? []
733
+ : parseArray(value.referenceRoutes, parseReferencePolicyRoute);
734
+ if (!referenceRoutes) return undefined;
735
+ const loadedReferences =
736
+ value.loadedReferences === undefined
737
+ ? []
738
+ : parseArray(value.loadedReferences, parseReferenceLoad);
739
+ if (!loadedReferences) return undefined;
638
740
  return {
639
741
  protocol: PROTOCOL,
640
742
  kind: "route",
@@ -643,15 +745,15 @@ export function normalizeDeveloperEvent(
643
745
  target: value.target,
644
746
  reason: value.reason,
645
747
  knownEvidence: value.knownEvidence,
646
- consideredAlternatives: consideredAlternatives as RouteAlternative[],
748
+ consideredAlternatives,
647
749
  availableReferences: value.availableReferences ?? [],
648
- referenceRoutes: referenceRoutes as ReferencePolicyRoute[],
750
+ referenceRoutes,
649
751
  referenceExemptionCriteria:
650
752
  value.referenceExemptionCriteria === undefined
651
753
  ? undefined
652
754
  : parseReferencePolicyExemption(value.referenceExemptionCriteria),
653
755
  referencePolicySha256: value.referencePolicySha256,
654
- loadedReferences: loadedReferences as ReferenceLoad[],
756
+ loadedReferences,
655
757
  targetQuestionId: value.targetQuestionId,
656
758
  methodLocation: value.methodLocation,
657
759
  executionProfile: value.executionProfile,
@@ -698,21 +800,21 @@ export function normalizeDeveloperEvent(
698
800
  return undefined;
699
801
  }
700
802
 
701
- const openedQuestions = value.openedQuestions.map(parsePendingQuestion);
702
- if (openedQuestions.some((question) => !question)) return undefined;
703
- const referenceBasis = Array.isArray(value.referenceBasis)
704
- ? value.referenceBasis.map(parseReferenceBasis)
705
- : [];
706
- if (referenceBasis.some((basis) => !basis)) return undefined;
707
- if (
708
- value.questionUpdates !== undefined &&
709
- !Array.isArray(value.questionUpdates)
710
- )
711
- return undefined;
712
- const questionUpdates = Array.isArray(value.questionUpdates)
713
- ? value.questionUpdates.map(parseQuestionUpdate)
714
- : [];
715
- if (questionUpdates.some((update) => !update)) return undefined;
803
+ const openedQuestions = parseArray(
804
+ value.openedQuestions,
805
+ parsePendingQuestion,
806
+ );
807
+ if (!openedQuestions) return undefined;
808
+ const referenceBasis =
809
+ value.referenceBasis === undefined
810
+ ? []
811
+ : parseArray(value.referenceBasis, parseReferenceBasis);
812
+ if (!referenceBasis) return undefined;
813
+ const questionUpdates =
814
+ value.questionUpdates === undefined
815
+ ? []
816
+ : parseArray(value.questionUpdates, parseQuestionUpdate);
817
+ if (!questionUpdates) return undefined;
716
818
  return {
717
819
  protocol: PROTOCOL,
718
820
  kind: "judgment",
@@ -722,13 +824,13 @@ export function normalizeDeveloperEvent(
722
824
  status: value.status,
723
825
  result: value.result,
724
826
  basis: value.basis,
725
- referenceBasis: referenceBasis as ReferenceBasis[],
827
+ referenceBasis,
726
828
  referenceExemption:
727
829
  value.referenceExemption === undefined
728
830
  ? undefined
729
831
  : parseReferenceExemption(value.referenceExemption),
730
- openedQuestions: openedQuestions as PendingQuestion[],
731
- questionUpdates: questionUpdates as QuestionUpdate[],
832
+ openedQuestions,
833
+ questionUpdates,
732
834
  artifacts: value.artifacts,
733
835
  changedArtifacts:
734
836
  typeof value.changedArtifacts === "boolean"