immune-brain 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +239 -0
  2. package/README.zh-CN.md +239 -0
  3. package/package.json +84 -0
  4. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +666 -0
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +1622 -0
  6. package/plugins/immune-brain/.pi-extension/package.json +11 -0
  7. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +711 -0
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance.ts +154 -0
  9. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +349 -0
  10. package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +90 -0
  11. package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +214 -0
  12. package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +50 -0
  13. package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +262 -0
  14. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +28 -0
  15. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +237 -0
  16. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +414 -0
  17. package/plugins/immune-brain/.pi-extension/tsconfig.json +20 -0
  18. package/plugins/immune-brain/bin/imm-kernel +4 -0
  19. package/plugins/immune-brain/bin/imm-plan +4 -0
  20. package/plugins/immune-brain/bin/imm-pr-diag +230 -0
  21. package/plugins/immune-brain/bin/imm-retire-stale-wrapper +4 -0
  22. package/plugins/immune-brain/bin/imm-retired +4 -0
  23. package/plugins/immune-brain/bin/imm-tracker +4 -0
  24. package/plugins/immune-brain/dist/BASELINE.md +138 -0
  25. package/plugins/immune-brain/dist/docs/reference/HANDOFF-template.md +122 -0
  26. package/plugins/immune-brain/dist/docs/reference/design-contract-audit-rubric.md +149 -0
  27. package/plugins/immune-brain/dist/docs/reference/design-contract-review-checklist.md +55 -0
  28. package/plugins/immune-brain/dist/docs/reference/i18n-review-checklist.md +110 -0
  29. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +52 -0
  30. package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +94 -0
  31. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +44 -0
  32. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +105 -0
  33. package/plugins/immune-brain/dist/docs/reference/ux-heuristic-checklist.md +131 -0
  34. package/plugins/immune-brain/dist/imm-brainstorm.md +140 -0
  35. package/plugins/immune-brain/dist/imm-doc-prune.md +137 -0
  36. package/plugins/immune-brain/dist/imm-loop.md +158 -0
  37. package/plugins/immune-brain/dist/imm-planner.md +387 -0
  38. package/plugins/immune-brain/dist/imm-pr-fix.md +71 -0
  39. package/plugins/immune-brain/dist/registry.yaml +49 -0
  40. package/plugins/immune-brain/dist/role-prompts/advisory-reviewer.md +16 -0
  41. package/plugins/immune-brain/dist/role-prompts/arch-explorer.md +14 -0
  42. package/plugins/immune-brain/dist/role-prompts/code-review.md +15 -0
  43. package/plugins/immune-brain/dist/role-prompts/compounder.md +20 -0
  44. package/plugins/immune-brain/dist/role-prompts/executor.md +13 -0
  45. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +81 -0
  46. package/plugins/immune-brain/dist/role-prompts/qa.md +23 -0
  47. package/plugins/immune-brain/dist/role-prompts/test-fixer.md +3 -0
  48. package/plugins/immune-brain/dist/role-prompts/ui-review.md +14 -0
  49. package/plugins/immune-brain/runtime/authority_commit_receipts.ts +716 -0
  50. package/plugins/immune-brain/runtime/canonical_json.ts +19 -0
  51. package/plugins/immune-brain/runtime/commands/kernel.ts +1160 -0
  52. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1009 -0
  53. package/plugins/immune-brain/runtime/kernel/application.ts +300 -0
  54. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +284 -0
  55. package/plugins/immune-brain/runtime/kernel/authority_port.ts +208 -0
  56. package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +451 -0
  57. package/plugins/immune-brain/runtime/kernel/backend_claim.ts +197 -0
  58. package/plugins/immune-brain/runtime/kernel/canary_application.ts +507 -0
  59. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +73 -0
  60. package/plugins/immune-brain/runtime/kernel/completion.ts +160 -0
  61. package/plugins/immune-brain/runtime/kernel/enrollment.ts +194 -0
  62. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +123 -0
  63. package/plugins/immune-brain/runtime/kernel/index.ts +29 -0
  64. package/plugins/immune-brain/runtime/kernel/intent.ts +563 -0
  65. package/plugins/immune-brain/runtime/kernel/intent_token_registry.ts +80 -0
  66. package/plugins/immune-brain/runtime/kernel/legacy.ts +299 -0
  67. package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +153 -0
  68. package/plugins/immune-brain/runtime/kernel/observation.ts +395 -0
  69. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +169 -0
  70. package/plugins/immune-brain/runtime/kernel/readiness.ts +282 -0
  71. package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +132 -0
  72. package/plugins/immune-brain/runtime/kernel/reducer.ts +624 -0
  73. package/plugins/immune-brain/runtime/kernel/storage.ts +1780 -0
  74. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +791 -0
  75. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +492 -0
  76. package/plugins/immune-brain/runtime/kernel/types.ts +295 -0
  77. package/plugins/immune-brain/runtime/kernel/validation.ts +963 -0
  78. package/plugins/immune-brain/runtime/loop_contract.ts +362 -0
  79. package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +462 -0
  80. package/plugins/immune-brain/runtime/plan_core.ts +1053 -0
  81. package/plugins/immune-brain/runtime/prompts/advisory-reviewer.md +16 -0
  82. package/plugins/immune-brain/runtime/prompts/arch-explorer.md +14 -0
  83. package/plugins/immune-brain/runtime/prompts/code-review.md +15 -0
  84. package/plugins/immune-brain/runtime/prompts/compounder.md +20 -0
  85. package/plugins/immune-brain/runtime/prompts/executor.md +13 -0
  86. package/plugins/immune-brain/runtime/prompts/pr-fix.md +81 -0
  87. package/plugins/immune-brain/runtime/prompts/qa.md +23 -0
  88. package/plugins/immune-brain/runtime/prompts/test-fixer.md +3 -0
  89. package/plugins/immune-brain/runtime/prompts/ui-review.md +14 -0
  90. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +160 -0
  91. package/plugins/immune-brain/runtime/v4_runtime.ts +295 -0
  92. package/plugins/immune-brain/runtime/verification_descriptor.ts +162 -0
  93. package/plugins/immune-brain/runtime/workspace_scope.ts +623 -0
  94. package/plugins/immune-brain/skills/.ignore +1 -0
  95. package/plugins/immune-brain/skills/BASELINE.md +138 -0
  96. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +66 -0
  97. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +11 -0
  98. package/plugins/immune-brain/skills/imm-loop/SKILL.md +52 -0
  99. package/plugins/immune-brain/skills/imm-planner/SKILL.md +221 -0
  100. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +10 -0
  101. package/plugins/immune-brain/skills/registry.yaml +49 -0
@@ -0,0 +1,299 @@
1
+ import type { LegacyMapping } from "./types";
2
+
3
+ const STEP_STATES = new Set([
4
+ "pending",
5
+ "active",
6
+ "probing",
7
+ "executing",
8
+ "ready_for_review",
9
+ "rework_needed",
10
+ "closed",
11
+ "replanning",
12
+ ]);
13
+ const FOLLOW_UP_STATES = new Set([
14
+ "pending",
15
+ "executing",
16
+ "ready_for_review",
17
+ "rework_needed",
18
+ "closed",
19
+ "replanning",
20
+ ]);
21
+ const FOLLOW_UP_GATES = new Set(["imm-code-review", "imm-ui-review"]);
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
25
+ }
26
+
27
+ function asRecord(value: unknown): Record<string, unknown> {
28
+ return isRecord(value) ? value : {};
29
+ }
30
+
31
+ function nonEmptyString(value: unknown): value is string {
32
+ return typeof value === "string" && Boolean(value.trim());
33
+ }
34
+
35
+ function latestFinishResetPlanPath(state: Record<string, unknown>): string | null {
36
+ if (!Array.isArray(state.history)) return null;
37
+ for (let index = state.history.length - 1; index >= 0; index -= 1) {
38
+ const entry = asRecord(state.history[index]);
39
+ if (entry.action !== "finish_reset") continue;
40
+ const planPath = asRecord(entry.details).plan_path;
41
+ return nonEmptyString(planPath) ? planPath : null;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ function isFinishedPlan(
47
+ state: Record<string, unknown>,
48
+ planPath: string,
49
+ ): boolean {
50
+ const steps = Object.values(asRecord(state.steps));
51
+ return (
52
+ steps.length > 0 &&
53
+ steps.every((step) => asRecord(step).state === "closed") &&
54
+ state.runtime_status === "idle" &&
55
+ state.reset_reason === "intentional_reset" &&
56
+ state.requires_replan === false &&
57
+ (state.active_step === null || state.active_step === undefined) &&
58
+ (state.pending_follow_up === null ||
59
+ state.pending_follow_up === undefined) &&
60
+ latestFinishResetPlanPath(state) === planPath
61
+ );
62
+ }
63
+
64
+ function validFollowUp(value: unknown): value is Record<string, unknown> {
65
+ if (!isRecord(value) || !FOLLOW_UP_STATES.has(String(value.state))) return false;
66
+ const origin = value.origin_review;
67
+ return (
68
+ nonEmptyString(value.id) &&
69
+ Array.isArray(value.scope) &&
70
+ value.scope.length > 0 &&
71
+ value.scope.every(nonEmptyString) &&
72
+ nonEmptyString(value.change_goal) &&
73
+ nonEmptyString(value.verification_hint) &&
74
+ isRecord(origin) &&
75
+ FOLLOW_UP_GATES.has(String(origin.gate)) &&
76
+ nonEmptyString(origin.evidence_ref) &&
77
+ (value.execution_evidence === null || isRecord(value.execution_evidence)) &&
78
+ nonEmptyString(value.opened_at) &&
79
+ Number.isInteger(value.round) &&
80
+ Number(value.round) >= 1 &&
81
+ (value.qa_decision === undefined || isRecord(value.qa_decision)) &&
82
+ (value.closed_at === undefined || nonEmptyString(value.closed_at))
83
+ );
84
+ }
85
+
86
+ function inspectTerminal(
87
+ state: Record<string, unknown>,
88
+ ): "absent" | "valid" | "invalid" | "conflict" {
89
+ if (state.plan_terminal === null || state.plan_terminal === undefined)
90
+ return "absent";
91
+ if (!isRecord(state.plan_terminal)) return "invalid";
92
+ const terminalPath = state.plan_terminal.plan_path;
93
+ if (
94
+ !nonEmptyString(terminalPath) ||
95
+ !(["cancelled", "superseded"] as unknown[]).includes(
96
+ state.plan_terminal.status,
97
+ )
98
+ )
99
+ return "invalid";
100
+ if (
101
+ nonEmptyString(state.plan_path) &&
102
+ state.plan_path !== terminalPath
103
+ )
104
+ return "conflict";
105
+ return "valid";
106
+ }
107
+
108
+ export interface LegacyAggregateInspection {
109
+ step_states: Array<[string, string]>;
110
+ current_steps: Array<[string, string]>;
111
+ source_states: string[];
112
+ follow_up_present: boolean;
113
+ follow_up_state: string | null;
114
+ follow_up_malformed: boolean;
115
+ steps_malformed: boolean;
116
+ active_step_mismatch: boolean;
117
+ ownership_conflict: boolean;
118
+ replan_signal: boolean;
119
+ replan_mismatch: boolean;
120
+ replan_type_invalid: boolean;
121
+ terminal: "absent" | "valid" | "invalid" | "conflict";
122
+ }
123
+
124
+ export function inspectLegacyAggregate(raw: unknown): LegacyAggregateInspection {
125
+ const state = asRecord(raw);
126
+ const rawSteps = state.steps;
127
+ const stepsRecord = isRecord(rawSteps) ? rawSteps : {};
128
+ const stepEntries = Object.entries(stepsRecord);
129
+ const stepStates: Array<[string, string]> = [];
130
+ let stepsMalformed = !isRecord(rawSteps);
131
+ for (const [key, value] of stepEntries) {
132
+ const stepState = asRecord(value).state;
133
+ if (!nonEmptyString(stepState) || !STEP_STATES.has(stepState)) {
134
+ stepsMalformed = true;
135
+ continue;
136
+ }
137
+ stepStates.push([key, stepState]);
138
+ }
139
+ const currentSteps = stepStates.filter(
140
+ ([, stepState]) => stepState !== "closed" && stepState !== "pending",
141
+ );
142
+
143
+ const followUpPresent =
144
+ state.pending_follow_up !== null && state.pending_follow_up !== undefined;
145
+ const followUpValid = followUpPresent && validFollowUp(state.pending_follow_up);
146
+ const followUpState = followUpValid
147
+ ? String(asRecord(state.pending_follow_up).state)
148
+ : null;
149
+
150
+ let activeStepKey: string | null = null;
151
+ let activeStepMalformed = false;
152
+ if (state.active_step !== null && state.active_step !== undefined) {
153
+ if (
154
+ typeof state.active_step === "number" ||
155
+ nonEmptyString(state.active_step)
156
+ )
157
+ activeStepKey = String(state.active_step);
158
+ else activeStepMalformed = true;
159
+ }
160
+ const activeStepMismatch = activeStepMalformed ||
161
+ (activeStepKey !== null &&
162
+ (currentSteps.length !== 1 || currentSteps[0][0] !== activeStepKey)) ||
163
+ (activeStepKey === null &&
164
+ currentSteps.length === 1 &&
165
+ !followUpPresent &&
166
+ currentSteps[0][1] !== "replanning");
167
+ const ownershipConflict = currentSteps.length > 1 ||
168
+ (currentSteps.length > 0 && followUpValid && followUpState !== "closed");
169
+ const replanSignal =
170
+ currentSteps.some(([, stepState]) => stepState === "replanning") ||
171
+ followUpState === "replanning";
172
+ const replanTypeInvalid =
173
+ state.requires_replan !== undefined &&
174
+ typeof state.requires_replan !== "boolean";
175
+ const replanMismatch =
176
+ !replanTypeInvalid && (state.requires_replan === true) !== replanSignal;
177
+ const sourceStates = Array.from(
178
+ new Set([
179
+ ...stepStates.map(([, stepState]) => stepState),
180
+ ...(followUpState ? [`follow_up:${followUpState}`] : []),
181
+ ]),
182
+ ).sort();
183
+
184
+ const terminal = inspectTerminal(state);
185
+ return {
186
+ step_states: stepStates,
187
+ current_steps: currentSteps,
188
+ source_states: sourceStates,
189
+ follow_up_present: followUpPresent,
190
+ follow_up_state: followUpState,
191
+ follow_up_malformed: followUpPresent && !followUpValid,
192
+ steps_malformed: stepsMalformed,
193
+ active_step_mismatch: activeStepMismatch,
194
+ ownership_conflict: ownershipConflict,
195
+ replan_signal: replanSignal,
196
+ replan_mismatch: replanMismatch,
197
+ replan_type_invalid: replanTypeInvalid,
198
+ terminal:
199
+ terminal === "valid" &&
200
+ (currentSteps.length > 0 || followUpPresent)
201
+ ? "conflict"
202
+ : terminal,
203
+ };
204
+ }
205
+
206
+ function stopped(
207
+ states: string[],
208
+ reason = "legacy-inconsistent",
209
+ ambiguous = true,
210
+ ): LegacyMapping {
211
+ return {
212
+ phase: "stopped",
213
+ reason,
214
+ ambiguous,
215
+ source_states: states,
216
+ };
217
+ }
218
+
219
+ function mapCurrentState(state: string, sourceStates: string[]): LegacyMapping {
220
+ if (state === "ready_for_review")
221
+ return {
222
+ phase: "review",
223
+ reason: "legacy-review",
224
+ ambiguous: false,
225
+ source_states: sourceStates,
226
+ };
227
+ if (["active", "executing", "probing", "rework_needed"].includes(state))
228
+ return {
229
+ phase: "working",
230
+ reason: "legacy-working",
231
+ ambiguous: false,
232
+ source_states: sourceStates,
233
+ };
234
+ return stopped(sourceStates);
235
+ }
236
+
237
+ export function mapLegacyState(raw: unknown): LegacyMapping {
238
+ const state = asRecord(raw);
239
+ const aggregate = inspectLegacyAggregate(raw);
240
+ const states = aggregate.source_states;
241
+
242
+ if (
243
+ aggregate.steps_malformed ||
244
+ aggregate.follow_up_malformed ||
245
+ aggregate.active_step_mismatch ||
246
+ aggregate.ownership_conflict ||
247
+ aggregate.replan_mismatch ||
248
+ aggregate.replan_type_invalid ||
249
+ aggregate.terminal === "invalid" ||
250
+ aggregate.terminal === "conflict"
251
+ )
252
+ return stopped(states);
253
+ if (aggregate.terminal === "valid")
254
+ return stopped(states, "legacy-terminal");
255
+ if (aggregate.follow_up_state === "closed") return stopped(states);
256
+ if (aggregate.replan_signal)
257
+ return stopped(states, "legacy-replan", false);
258
+ if (aggregate.follow_up_state) {
259
+ if (aggregate.follow_up_state === "ready_for_review")
260
+ return {
261
+ phase: "review",
262
+ reason: "legacy-review",
263
+ ambiguous: false,
264
+ source_states: states,
265
+ };
266
+ return {
267
+ phase: "working",
268
+ reason: "legacy-working",
269
+ ambiguous: false,
270
+ source_states: states,
271
+ };
272
+ }
273
+
274
+ if (aggregate.step_states.length === 0)
275
+ return stopped(states, "legacy-empty");
276
+ if (aggregate.current_steps.length === 1)
277
+ return mapCurrentState(aggregate.current_steps[0][1], states);
278
+ if (aggregate.step_states.some(([, stepState]) => stepState === "pending"))
279
+ return {
280
+ phase: "working",
281
+ reason: "legacy-working",
282
+ ambiguous: false,
283
+ source_states: states,
284
+ };
285
+ if (!nonEmptyString(state.plan_path)) return stopped(states);
286
+ if (isFinishedPlan(state, state.plan_path))
287
+ return {
288
+ phase: "done",
289
+ reason: "legacy-finished",
290
+ ambiguous: false,
291
+ source_states: states,
292
+ };
293
+ return {
294
+ phase: "review",
295
+ reason: "legacy-review",
296
+ ambiguous: false,
297
+ source_states: states,
298
+ };
299
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Explicit read-only legacy audit projection (v4 storage retirement).
3
+ *
4
+ * This module is the ONLY retained reader for historical v3 State Ledger
5
+ * artifacts. It performs a bounded, no-symlink, deterministic, redacted
6
+ * projection and never writes journal, workflow, migration, receipt,
7
+ * observation, TaskRecord, or workspace state. It never imports, synthesizes,
8
+ * or activates a Kernel TaskRecord from legacy data.
9
+ *
10
+ * Not exported from kernel/index.ts; reached only through the v4 CLI
11
+ * `imm-kernel audit --legacy` surface.
12
+ */
13
+ import { createHash } from "node:crypto";
14
+ import { lstatSync, readFileSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+ import { LEGACY_V3_RELATIVE, legacyV3Path } from "./storage_paths";
17
+
18
+ const MAX_BYTES = 2 * 1024 * 1024;
19
+
20
+ export interface LegacyAuditProjection {
21
+ contract: "assurance_kernel/legacy_audit/v1";
22
+ source: string;
23
+ read_only: true;
24
+ writes_performed: false;
25
+ plan_path: string | null;
26
+ runtime_status: string | null;
27
+ active_step: unknown;
28
+ step_count: number;
29
+ phase: string | null;
30
+ digest: string;
31
+ redacted: true;
32
+ }
33
+
34
+ function sha256Hex(bytes: string | Buffer): string {
35
+ return createHash("sha256").update(bytes).digest("hex");
36
+ }
37
+
38
+ /**
39
+ * The archived v3 Ledger lives at `.imm/audit/legacy-v3/` after the
40
+ * one-release migration; before migration it is still read from the legacy
41
+ * `.imm/memory/` path. This is the ONLY production caller of the legacy path
42
+ * after the cutover; Slice 2 deletes the fallback branch once every target
43
+ * repository has migrated.
44
+ */
45
+ export function legacyLedgerSourceRelative(canonicalRoot: string): string {
46
+ // review-5: the explicit legacy audit reads ONLY the archived audit
47
+ // layout. A live old-layout Ledger (pre-migration) is reported as a
48
+ // layout condition by the shared gate; this module never falls back to
49
+ // the old authority path again. Absence keeps the empty-projection
50
+ // semantics (ENOENT propagates to readLegacyLedgerBounded -> null).
51
+ let stat;
52
+ try {
53
+ stat = lstatSync(resolve(canonicalRoot, legacyV3Path("current_iteration.json")));
54
+ } catch (error) {
55
+ const code = (error as NodeJS.ErrnoException).code;
56
+ if (code === "ENOENT" || code === "ENOTDIR") return legacyV3Path("current_iteration.json");
57
+ throw error;
58
+ }
59
+ if (stat.isSymbolicLink())
60
+ throw new Error("legacy audit rejected: archived v3 Ledger must not be a symlink");
61
+ if (stat.isFile()) return legacyV3Path("current_iteration.json");
62
+ throw new Error("legacy audit rejected: archived v3 Ledger is not a regular file");
63
+ }
64
+
65
+ /**
66
+ * Read a bounded regular file with no symlink segments, returning canonical
67
+ * content bytes. Throws KernelStoreSecurityError-compatible errors; only
68
+ * ENOENT returns null.
69
+ */
70
+ export function readLegacyLedgerBounded(root: string): {
71
+ content: string;
72
+ path: string;
73
+ } | null {
74
+ const canonicalRoot = resolve(root);
75
+ const sourceRelative = legacyLedgerSourceRelative(canonicalRoot);
76
+ const target = join(canonicalRoot, sourceRelative);
77
+ let stat;
78
+ try {
79
+ stat = lstatSync(target);
80
+ } catch (error) {
81
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
82
+ throw error;
83
+ }
84
+ if (stat.isSymbolicLink())
85
+ throw new Error("legacy audit rejected: v3 Ledger must not be a symlink");
86
+ if (!stat.isFile())
87
+ throw new Error("legacy audit rejected: v3 Ledger is not a regular file");
88
+ if (stat.size > MAX_BYTES)
89
+ throw new Error("legacy audit rejected: v3 Ledger exceeds the 2 MiB audit bound");
90
+ const content = readFileSync(target, "utf8");
91
+ // Post-read identity re-verification prevents symlink drift.
92
+ const after = lstatSync(target);
93
+ if (after.isSymbolicLink() || after.dev !== stat.dev || after.ino !== stat.ino)
94
+ throw new Error("legacy audit rejected: v3 Ledger identity changed during read");
95
+ return { content, path: sourceRelative };
96
+ }
97
+
98
+ /**
99
+ * Deterministic redacted projection. Only bounded metadata is surfaced;
100
+ * step/history/evidence payloads are never echoed.
101
+ */
102
+ export function projectLegacyAudit(
103
+ root: string,
104
+ ): LegacyAuditProjection {
105
+ const canonicalRoot = resolve(root);
106
+ const read = readLegacyLedgerBounded(root);
107
+ const source = legacyLedgerSourceRelative(canonicalRoot);
108
+ if (!read) {
109
+ return {
110
+ contract: "assurance_kernel/legacy_audit/v1",
111
+ source,
112
+ read_only: true,
113
+ writes_performed: false,
114
+ plan_path: null,
115
+ runtime_status: null,
116
+ active_step: null,
117
+ step_count: 0,
118
+ phase: null,
119
+ digest: "sha256:none",
120
+ redacted: true,
121
+ };
122
+ }
123
+ const raw = JSON.parse(read.content) as Record<string, unknown>;
124
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
125
+ throw new Error("legacy audit rejected: v3 Ledger root must be an object");
126
+ const steps = raw.steps as Record<string, unknown> | undefined;
127
+ if (steps && typeof steps !== "object")
128
+ throw new Error("legacy audit rejected: v3 Ledger steps must be an object");
129
+ const stepCount = steps ? Object.keys(steps).length : 0;
130
+ if (stepCount > 1024)
131
+ throw new Error("legacy audit rejected: v3 Ledger exceeds the step limit");
132
+ const runtimeStatus = typeof raw.runtime_status === "string" ? raw.runtime_status : null;
133
+ const phase =
134
+ typeof raw.runtime_status === "string" && raw.runtime_status !== "idle"
135
+ ? "nonterminal"
136
+ : raw.runtime_status === "idle" &&
137
+ raw.reset_reason === "intentional_reset"
138
+ ? "finished"
139
+ : "idle";
140
+ return {
141
+ contract: "assurance_kernel/legacy_audit/v1",
142
+ source,
143
+ read_only: true,
144
+ writes_performed: false,
145
+ plan_path: typeof raw.plan_path === "string" ? raw.plan_path : null,
146
+ runtime_status: runtimeStatus,
147
+ active_step: raw.active_step ?? null,
148
+ step_count: stepCount,
149
+ phase,
150
+ digest: `sha256:${sha256Hex(`assurance_kernel/legacy_audit/v1\0${read.content}`)}`,
151
+ redacted: true,
152
+ };
153
+ }