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,963 @@
1
+ import {
2
+ TASK_ARTIFACT_STATES,
3
+ TASK_LIFECYCLES,
4
+ TASK_PHASES,
5
+ TASK_RECORD_CONTRACT_V2,
6
+ TASK_RECORD_CONTRACT_V3,
7
+ type ApprovalAuthorityRole,
8
+ type ApprovalKind,
9
+ type EvidenceStatus,
10
+ type FindingKind,
11
+ type FindingSource,
12
+ type FindingStatus,
13
+ type TaskAction,
14
+ type TaskApprovalV2,
15
+ type TaskAttestationV3,
16
+ type AuthorityAuditDescriptor,
17
+ type TaskEvidenceV2,
18
+ type TaskFinding,
19
+ type TaskHistoryEntryV2,
20
+ type TaskHistoryEntryV3,
21
+ type TaskIntentV1,
22
+ type TaskRecordV2,
23
+ type TaskRecordV3,
24
+ } from "./types";
25
+ import { canonicalIntentHash, parseTaskIntentV1 } from "./intent";
26
+
27
+ export class KernelValidationError extends Error {
28
+ readonly code = "kernel_schema_invalid";
29
+
30
+ constructor(readonly violations: string[]) {
31
+ super(violations.join("; "));
32
+ this.name = "KernelValidationError";
33
+ }
34
+ }
35
+
36
+ export class KernelInvariantError extends Error {
37
+ readonly code = "kernel_invariant_violation";
38
+
39
+ constructor(readonly violations: string[]) {
40
+ super(violations.join("; "));
41
+ this.name = "KernelInvariantError";
42
+ }
43
+ }
44
+
45
+ function objectAt(
46
+ value: unknown,
47
+ path: string,
48
+ violations: string[],
49
+ ): Record<string, unknown> {
50
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
51
+ violations.push(`${path} must be an object`);
52
+ return {};
53
+ }
54
+ return value as Record<string, unknown>;
55
+ }
56
+
57
+ function rejectUnknown(
58
+ record: Record<string, unknown>,
59
+ allowed: readonly string[],
60
+ path: string,
61
+ violations: string[],
62
+ ): void {
63
+ for (const key of Object.keys(record)) {
64
+ if (!allowed.includes(key))
65
+ violations.push(
66
+ path === "record" ? `unknown field: ${key}` : `unknown field: ${path}.${key}`,
67
+ );
68
+ }
69
+ }
70
+
71
+ function stringAt(
72
+ value: unknown,
73
+ path: string,
74
+ violations: string[],
75
+ ): string {
76
+ if (typeof value !== "string" || !value.trim()) {
77
+ violations.push(`${path} must be a non-empty string`);
78
+ return "";
79
+ }
80
+ return value;
81
+ }
82
+
83
+ function positiveInteger(
84
+ value: unknown,
85
+ path: string,
86
+ violations: string[],
87
+ ): number {
88
+ if (!Number.isInteger(value) || Number(value) < 1) {
89
+ violations.push(`${path} must be a positive integer`);
90
+ return 0;
91
+ }
92
+ return Number(value);
93
+ }
94
+
95
+ function nullableString(
96
+ value: unknown,
97
+ path: string,
98
+ violations: string[],
99
+ ): string | null {
100
+ if (value === null) return null;
101
+ return stringAt(value, path, violations);
102
+ }
103
+
104
+ function nullablePositiveInteger(
105
+ value: unknown,
106
+ path: string,
107
+ violations: string[],
108
+ ): number | null {
109
+ if (value === null) return null;
110
+ return positiveInteger(value, path, violations);
111
+ }
112
+
113
+ function enumAt<T extends string>(
114
+ value: unknown,
115
+ allowed: readonly T[],
116
+ path: string,
117
+ violations: string[],
118
+ ): T {
119
+ if (typeof value !== "string" || !allowed.includes(value as T)) {
120
+ violations.push(`${path} must be one of ${allowed.join(", ")}`);
121
+ return allowed[0];
122
+ }
123
+ return value as T;
124
+ }
125
+
126
+ function arrayAt(value: unknown, path: string, violations: string[]): unknown[] {
127
+ if (!Array.isArray(value)) {
128
+ violations.push(`${path} must be an array`);
129
+ return [];
130
+ }
131
+ return value;
132
+ }
133
+
134
+ function uniqueIds(
135
+ items: Array<{ id: string }>,
136
+ path: string,
137
+ violations: string[],
138
+ ): void {
139
+ const seen = new Set<string>();
140
+ for (const item of items) {
141
+ if (seen.has(item.id)) violations.push(`${path} contains duplicate id ${item.id}`);
142
+ seen.add(item.id);
143
+ }
144
+ }
145
+
146
+ const EVIDENCE_STATUSES: EvidenceStatus[] = ["passed", "failed", "blocked"];
147
+ const FINDING_KINDS: FindingKind[] = [
148
+ "blocking",
149
+ "advisory",
150
+ "unresolved_user_decision",
151
+ "replan_required",
152
+ ];
153
+ const FINDING_STATUSES: FindingStatus[] = ["open", "resolved"];
154
+ const FINDING_SOURCES: FindingSource[] = [
155
+ "execution",
156
+ "review",
157
+ "kernel",
158
+ "migration",
159
+ ];
160
+ const APPROVAL_KINDS: ApprovalKind[] = ["review", "qa", "user"];
161
+ const APPROVAL_AUTHORITY_ROLES: ApprovalAuthorityRole[] = [
162
+ "reviewer",
163
+ "qa",
164
+ "user",
165
+ ];
166
+
167
+ function parseFinding(
168
+ value: unknown,
169
+ index: number,
170
+ violations: string[],
171
+ ): TaskFinding {
172
+ const path = `record.findings[${index}]`;
173
+ const item = objectAt(value, path, violations);
174
+ rejectUnknown(
175
+ item,
176
+ ["id", "kind", "status", "acceptance_id", "source", "review_round", "summary"],
177
+ path,
178
+ violations,
179
+ );
180
+ return {
181
+ id: stringAt(item.id, `${path}.id`, violations),
182
+ kind: enumAt(item.kind, FINDING_KINDS, `${path}.kind`, violations),
183
+ status: enumAt(item.status, FINDING_STATUSES, `${path}.status`, violations),
184
+ acceptance_id: nullableString(item.acceptance_id, `${path}.acceptance_id`, violations),
185
+ source: enumAt(item.source, FINDING_SOURCES, `${path}.source`, violations),
186
+ review_round: nullablePositiveInteger(item.review_round, `${path}.review_round`, violations),
187
+ summary: stringAt(item.summary, `${path}.summary`, violations),
188
+ };
189
+ }
190
+
191
+ function parseHistoryV2(
192
+ value: unknown,
193
+ index: number,
194
+ violations: string[],
195
+ ): TaskHistoryEntryV2 {
196
+ const item = objectAt(value, `record.history[${index}]`, violations);
197
+ rejectUnknown(
198
+ item,
199
+ ["id", "at", "type", "from_phase", "to_phase", "reason", "authority"],
200
+ `record.history[${index}]`,
201
+ violations,
202
+ );
203
+ let authority: AuthorityAuditDescriptor | undefined;
204
+ if (item.authority !== undefined) {
205
+ const auth = objectAt(item.authority, `record.history[${index}].authority`, violations);
206
+ rejectUnknown(
207
+ auth,
208
+ ["authority_kind", "actor_id", "confirmation_ref", "issued_at", "expires_at"],
209
+ `record.history[${index}].authority`,
210
+ violations,
211
+ );
212
+ const kind = enumAt(
213
+ auth.authority_kind,
214
+ ["review", "qa", "user"],
215
+ `record.history[${index}].authority.authority_kind`,
216
+ violations,
217
+ );
218
+ authority = {
219
+ authority_kind: kind as AuthorityAuditDescriptor["authority_kind"],
220
+ actor_id: stringAt(auth.actor_id, `record.history[${index}].authority.actor_id`, violations),
221
+ confirmation_ref: stringAt(auth.confirmation_ref, `record.history[${index}].authority.confirmation_ref`, violations),
222
+ issued_at: stringAt(auth.issued_at, `record.history[${index}].authority.issued_at`, violations),
223
+ expires_at: stringAt(auth.expires_at, `record.history[${index}].authority.expires_at`, violations),
224
+ };
225
+ }
226
+ return {
227
+ id: stringAt(item.id, `record.history[${index}].id`, violations),
228
+ at: stringAt(item.at, `record.history[${index}].at`, violations),
229
+ type: stringAt(item.type, `record.history[${index}].type`, violations),
230
+ from_phase: enumAt(item.from_phase, TASK_PHASES, `record.history[${index}].from_phase`, violations),
231
+ to_phase: enumAt(item.to_phase, TASK_PHASES, `record.history[${index}].to_phase`, violations),
232
+ reason: stringAt(item.reason, `record.history[${index}].reason`, violations),
233
+ ...(authority ? { authority } : {}),
234
+ };
235
+ }
236
+
237
+
238
+ function parseHistoryV3(
239
+ value: unknown,
240
+ index: number,
241
+ violations: string[],
242
+ ): TaskHistoryEntryV3 {
243
+ const path = `record.history[${index}]`;
244
+ const item = objectAt(value, path, violations);
245
+ rejectUnknown(item, ["id", "at", "type", "from_state", "to_state", "reason", "authority"], path, violations);
246
+ let authority: AuthorityAuditDescriptor | undefined;
247
+ if (item.authority !== undefined) {
248
+ const auth = objectAt(item.authority, `${path}.authority`, violations);
249
+ rejectUnknown(auth, ["authority_kind", "actor_id", "confirmation_ref", "issued_at", "expires_at"], `${path}.authority`, violations);
250
+ authority = {
251
+ authority_kind: enumAt(auth.authority_kind, ["review", "qa", "user"], `${path}.authority.authority_kind`, violations) as AuthorityAuditDescriptor["authority_kind"],
252
+ actor_id: stringAt(auth.actor_id, `${path}.authority.actor_id`, violations),
253
+ confirmation_ref: stringAt(auth.confirmation_ref, `${path}.authority.confirmation_ref`, violations),
254
+ issued_at: stringAt(auth.issued_at, `${path}.authority.issued_at`, violations),
255
+ expires_at: stringAt(auth.expires_at, `${path}.authority.expires_at`, violations),
256
+ };
257
+ }
258
+ return {
259
+ id: stringAt(item.id, `${path}.id`, violations),
260
+ at: stringAt(item.at, `${path}.at`, violations),
261
+ type: stringAt(item.type, `${path}.type`, violations),
262
+ from_state: stringAt(item.from_state, `${path}.from_state`, violations),
263
+ to_state: stringAt(item.to_state, `${path}.to_state`, violations),
264
+ reason: stringAt(item.reason, `${path}.reason`, violations),
265
+ ...(authority ? { authority } : {}),
266
+ };
267
+ }
268
+
269
+ const SHA256_HEX = /^sha256:[a-f0-9]{64}$/;
270
+
271
+ function parseEvidenceV2(
272
+ value: unknown,
273
+ index: number,
274
+ acceptanceIds: Set<string> | null,
275
+ violations: string[],
276
+ ): TaskEvidenceV2 {
277
+ const item = objectAt(value, `record.evidence[${index}]`, violations);
278
+ rejectUnknown(
279
+ item,
280
+ ["id", "acceptance_id", "task_revision", "intent_content_hash", "diff_hash", "status", "actor_id", "summary"],
281
+ `record.evidence[${index}]`,
282
+ violations,
283
+ );
284
+ const acceptanceId = stringAt(
285
+ item.acceptance_id,
286
+ `record.evidence[${index}].acceptance_id`,
287
+ violations,
288
+ );
289
+ if (acceptanceIds && !acceptanceIds.has(acceptanceId))
290
+ violations.push(
291
+ `evidence ${String(item.id)} references unknown acceptance ${acceptanceId}`,
292
+ );
293
+ const intentContentHash = stringAt(
294
+ item.intent_content_hash,
295
+ `record.evidence[${index}].intent_content_hash`,
296
+ violations,
297
+ );
298
+ if (!SHA256_HEX.test(intentContentHash))
299
+ violations.push(`record.evidence[${index}].intent_content_hash must be sha256:<64 hex>`);
300
+ const diffHash = stringAt(item.diff_hash, `record.evidence[${index}].diff_hash`, violations);
301
+ if (!SHA256_HEX.test(diffHash))
302
+ violations.push(`record.evidence[${index}].diff_hash must be sha256:<64 hex>`);
303
+ return {
304
+ id: stringAt(item.id, `record.evidence[${index}].id`, violations),
305
+ acceptance_id: acceptanceId,
306
+ task_revision: positiveInteger(
307
+ item.task_revision,
308
+ `record.evidence[${index}].task_revision`,
309
+ violations,
310
+ ),
311
+ intent_content_hash: intentContentHash,
312
+ diff_hash: diffHash,
313
+ status: enumAt(item.status, EVIDENCE_STATUSES, `record.evidence[${index}].status`, violations),
314
+ actor_id: stringAt(item.actor_id, `record.evidence[${index}].actor_id`, violations),
315
+ summary: stringAt(item.summary, `record.evidence[${index}].summary`, violations),
316
+ };
317
+ }
318
+
319
+ function parseApprovalV2(
320
+ value: unknown,
321
+ index: number,
322
+ violations: string[],
323
+ ): TaskApprovalV2 {
324
+ const item = objectAt(value, `record.approvals[${index}]`, violations);
325
+ rejectUnknown(
326
+ item,
327
+ ["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary"],
328
+ `record.approvals[${index}]`,
329
+ violations,
330
+ );
331
+ const intentContentHash = stringAt(
332
+ item.intent_content_hash,
333
+ `record.approvals[${index}].intent_content_hash`,
334
+ violations,
335
+ );
336
+ if (!SHA256_HEX.test(intentContentHash))
337
+ violations.push(`record.approvals[${index}].intent_content_hash must be sha256:<64 hex>`);
338
+ const diffHash = stringAt(item.diff_hash, `record.approvals[${index}].diff_hash`, violations);
339
+ if (!SHA256_HEX.test(diffHash))
340
+ violations.push(`record.approvals[${index}].diff_hash must be sha256:<64 hex>`);
341
+ return {
342
+ id: stringAt(item.id, `record.approvals[${index}].id`, violations),
343
+ kind: enumAt(item.kind, APPROVAL_KINDS, `record.approvals[${index}].kind`, violations),
344
+ authority_role: enumAt(
345
+ item.authority_role,
346
+ APPROVAL_AUTHORITY_ROLES,
347
+ `record.approvals[${index}].authority_role`,
348
+ violations,
349
+ ),
350
+ task_revision: positiveInteger(
351
+ item.task_revision,
352
+ `record.approvals[${index}].task_revision`,
353
+ violations,
354
+ ),
355
+ intent_content_hash: intentContentHash,
356
+ diff_hash: diffHash,
357
+ actor_id: stringAt(item.actor_id, `record.approvals[${index}].actor_id`, violations),
358
+ summary: stringAt(item.summary, `record.approvals[${index}].summary`, violations),
359
+ };
360
+ }
361
+
362
+ function parseAttestationV3(
363
+ value: unknown,
364
+ index: number,
365
+ acceptanceIds: Set<string>,
366
+ violations: string[],
367
+ ): TaskAttestationV3 {
368
+ const path = `record.attestations[${index}]`;
369
+ const item = objectAt(value, path, violations);
370
+ rejectUnknown(item, ["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary", "acceptance_results"], path, violations);
371
+ const kind = enumAt(item.kind, APPROVAL_KINDS, `${path}.kind`, violations);
372
+ const intentContentHash = stringAt(item.intent_content_hash, `${path}.intent_content_hash`, violations);
373
+ const diffHash = stringAt(item.diff_hash, `${path}.diff_hash`, violations);
374
+ if (!SHA256_HEX.test(intentContentHash)) violations.push(`${path}.intent_content_hash must be sha256:<64 hex>`);
375
+ if (!SHA256_HEX.test(diffHash)) violations.push(`${path}.diff_hash must be sha256:<64 hex>`);
376
+ const acceptanceResults = arrayAt(item.acceptance_results, `${path}.acceptance_results`, violations).map((raw, resultIndex) => {
377
+ const resultPath = `${path}.acceptance_results[${resultIndex}]`;
378
+ const result = objectAt(raw, resultPath, violations);
379
+ rejectUnknown(result, ["acceptance_id", "status", "summary"], resultPath, violations);
380
+ const acceptanceId = stringAt(result.acceptance_id, `${resultPath}.acceptance_id`, violations);
381
+ if (!acceptanceIds.has(acceptanceId)) violations.push(`${resultPath}.acceptance_id is not in the current intent`);
382
+ return {
383
+ acceptance_id: acceptanceId,
384
+ status: enumAt(result.status, EVIDENCE_STATUSES, `${resultPath}.status`, violations),
385
+ summary: stringAt(result.summary, `${resultPath}.summary`, violations),
386
+ };
387
+ });
388
+ if (kind !== "qa" && acceptanceResults.length > 0)
389
+ violations.push(`${path}.acceptance_results is only valid for qa attestations`);
390
+ if (kind === "qa") {
391
+ const resultIds = acceptanceResults.map((result) => result.acceptance_id);
392
+ if (resultIds.length !== acceptanceIds.size || new Set(resultIds).size !== acceptanceIds.size)
393
+ violations.push(`${path}.acceptance_results must cover every acceptance exactly once`);
394
+ }
395
+ return {
396
+ id: stringAt(item.id, `${path}.id`, violations),
397
+ kind,
398
+ authority_role: enumAt(item.authority_role, APPROVAL_AUTHORITY_ROLES, `${path}.authority_role`, violations),
399
+ task_revision: positiveInteger(item.task_revision, `${path}.task_revision`, violations),
400
+ intent_content_hash: intentContentHash,
401
+ diff_hash: diffHash,
402
+ actor_id: stringAt(item.actor_id, `${path}.actor_id`, violations),
403
+ summary: stringAt(item.summary, `${path}.summary`, violations),
404
+ acceptance_results: acceptanceResults,
405
+ };
406
+ }
407
+
408
+ export function parseTaskRecordV2(raw: unknown): TaskRecordV2 {
409
+ const violations: string[] = [];
410
+ const value = objectAt(raw, "record", violations);
411
+ rejectUnknown(
412
+ value,
413
+ ["contract", "task_id", "intent_revision", "intent_snapshot", "intent_ref", "artifact_ref", "phase", "baseline", "evidence", "findings", "approvals", "history"],
414
+ "record",
415
+ violations,
416
+ );
417
+ if (value.contract !== TASK_RECORD_CONTRACT_V2)
418
+ violations.push(`contract must equal ${TASK_RECORD_CONTRACT_V2}`);
419
+
420
+ let snapshot: TaskIntentV1 | null = null;
421
+ try {
422
+ snapshot = parseTaskIntentV1(value.intent_snapshot);
423
+ } catch {
424
+ violations.push("record.intent_snapshot must be a valid TaskIntent v1");
425
+ }
426
+
427
+ const taskId = stringAt(value.task_id, "record.task_id", violations);
428
+ const intentRevision = positiveInteger(
429
+ value.intent_revision,
430
+ "record.intent_revision",
431
+ violations,
432
+ );
433
+
434
+ const refRaw = objectAt(value.intent_ref, "record.intent_ref", violations);
435
+ rejectUnknown(refRaw, ["path", "revision", "content_hash"], "record.intent_ref", violations);
436
+ const refPath = stringAt(refRaw.path, "record.intent_ref.path", violations);
437
+ const refRevision = positiveInteger(
438
+ refRaw.revision,
439
+ "record.intent_ref.revision",
440
+ violations,
441
+ );
442
+ const refContentHash = stringAt(
443
+ refRaw.content_hash,
444
+ "record.intent_ref.content_hash",
445
+ violations,
446
+ );
447
+ if (!SHA256_HEX.test(refContentHash))
448
+ violations.push("record.intent_ref.content_hash must be sha256:<64 hex>");
449
+
450
+ let artifactRef: TaskRecordV2["artifact_ref"];
451
+ if (value.artifact_ref !== undefined) {
452
+ const artifactRaw = objectAt(value.artifact_ref, "record.artifact_ref", violations);
453
+ rejectUnknown(artifactRaw, ["state", "spec_path"], "record.artifact_ref", violations);
454
+ const state = enumAt(artifactRaw.state, ["active", "frozen"], "record.artifact_ref.state", violations) as "active" | "frozen";
455
+ const specPath = artifactRaw.spec_path === undefined
456
+ ? undefined
457
+ : stringAt(artifactRaw.spec_path, "record.artifact_ref.spec_path", violations);
458
+ if (specPath !== undefined && (!/^docs\/specs\/(?!archive\/)[A-Za-z0-9._/-]+\.spec\.md$/.test(specPath) || specPath.includes("..")))
459
+ violations.push("record.artifact_ref.spec_path must be one canonical active Spec path");
460
+ artifactRef = { state, ...(specPath === undefined ? {} : { spec_path: specPath }) };
461
+ }
462
+
463
+ const activeIntentPath = `docs/plans/${taskId}.intent.json`;
464
+ const frozenIntentPath = `docs/plans/archive/${taskId}.intent.json`;
465
+ if (
466
+ snapshot &&
467
+ (snapshot.task_id !== taskId ||
468
+ snapshot.revision !== intentRevision ||
469
+ snapshot.revision !== refRevision ||
470
+ (refPath !== activeIntentPath && refPath !== frozenIntentPath))
471
+ )
472
+ violations.push("intent_snapshot and intent_ref must match record identity");
473
+ if (artifactRef?.state === "active" && refPath !== activeIntentPath)
474
+ violations.push("active artifact_ref requires the active intent path");
475
+ if (artifactRef?.state === "frozen" && refPath !== frozenIntentPath)
476
+ violations.push("frozen artifact_ref requires the archived intent path");
477
+ if (
478
+ snapshot &&
479
+ refContentHash !== "" &&
480
+ canonicalIntentHash(snapshot) !== refContentHash
481
+ )
482
+ violations.push("intent_ref.content_hash must equal the snapshot canonical hash");
483
+
484
+ const baseline = stringAt(value.baseline, "record.baseline", violations);
485
+ if (!SHA256_HEX.test(baseline))
486
+ violations.push("record.baseline must be sha256:<64 hex>");
487
+
488
+ const acceptanceIds = new Set(
489
+ snapshot ? snapshot.acceptance.map((item) => item.id) : [],
490
+ );
491
+ const evidence = arrayAt(value.evidence, "record.evidence", violations).map(
492
+ (item, index) => parseEvidenceV2(item, index, acceptanceIds, violations),
493
+ );
494
+ const findings = arrayAt(value.findings, "record.findings", violations).map(
495
+ (item, index) => parseFinding(item, index, violations),
496
+ );
497
+ const approvals = arrayAt(value.approvals, "record.approvals", violations).map(
498
+ (item, index) => parseApprovalV2(item, index, violations),
499
+ );
500
+ const history = arrayAt(value.history, "record.history", violations).map(
501
+ (item, index) => parseHistoryV2(item, index, violations),
502
+ );
503
+ uniqueIds(evidence, "record.evidence", violations);
504
+ uniqueIds(findings, "record.findings", violations);
505
+ uniqueIds(approvals, "record.approvals", violations);
506
+ uniqueIds(history, "record.history", violations);
507
+
508
+ const phase = enumAt(value.phase, TASK_PHASES, "phase", violations);
509
+
510
+ if (violations.length > 0) throw new KernelValidationError(violations);
511
+ return {
512
+ contract: TASK_RECORD_CONTRACT_V2,
513
+ task_id: taskId,
514
+ intent_revision: intentRevision,
515
+ intent_snapshot: snapshot as TaskIntentV1,
516
+ intent_ref: {
517
+ path: refPath,
518
+ revision: refRevision,
519
+ content_hash: refContentHash,
520
+ },
521
+ ...(artifactRef ? { artifact_ref: artifactRef } : {}),
522
+ phase,
523
+ baseline,
524
+ evidence,
525
+ findings,
526
+ approvals,
527
+ history,
528
+ };
529
+ }
530
+
531
+ export function parseTaskRecordV3(raw: unknown): TaskRecordV3 {
532
+ const violations: string[] = [];
533
+ const value = objectAt(raw, "record", violations);
534
+ rejectUnknown(
535
+ value,
536
+ ["contract", "task_id", "intent_snapshot", "intent_ref", "lifecycle", "artifact_state", "baseline", "attestations", "findings", "history"],
537
+ "record",
538
+ violations,
539
+ );
540
+ if (value.contract !== TASK_RECORD_CONTRACT_V3)
541
+ violations.push(`contract must equal ${TASK_RECORD_CONTRACT_V3}`);
542
+
543
+ let snapshot: TaskIntentV1 | null = null;
544
+ try {
545
+ snapshot = parseTaskIntentV1(value.intent_snapshot);
546
+ } catch {
547
+ violations.push("record.intent_snapshot must be a valid TaskIntent v1");
548
+ }
549
+ const taskId = stringAt(value.task_id, "record.task_id", violations);
550
+ const refRaw = objectAt(value.intent_ref, "record.intent_ref", violations);
551
+ rejectUnknown(refRaw, ["path", "content_hash"], "record.intent_ref", violations);
552
+ const refPath = stringAt(refRaw.path, "record.intent_ref.path", violations);
553
+ const refContentHash = stringAt(refRaw.content_hash, "record.intent_ref.content_hash", violations);
554
+ if (!SHA256_HEX.test(refContentHash))
555
+ violations.push("record.intent_ref.content_hash must be sha256:<64 hex>");
556
+ const lifecycle = enumAt(value.lifecycle, TASK_LIFECYCLES, "record.lifecycle", violations);
557
+ const artifactState = enumAt(value.artifact_state, TASK_ARTIFACT_STATES, "record.artifact_state", violations);
558
+ const activeIntentPath = `docs/plans/${taskId}.intent.json`;
559
+ const frozenIntentPath = `docs/plans/archive/${taskId}.intent.json`;
560
+ if (snapshot && (snapshot.task_id !== taskId || (refPath !== activeIntentPath && refPath !== frozenIntentPath)))
561
+ violations.push("intent_snapshot and intent_ref must match record identity");
562
+ if (artifactState === "active" && refPath !== activeIntentPath)
563
+ violations.push("active artifact_state requires the active intent path");
564
+ if (artifactState === "frozen" && refPath !== frozenIntentPath)
565
+ violations.push("frozen artifact_state requires the archived intent path");
566
+ if (snapshot && refContentHash !== "" && canonicalIntentHash(snapshot) !== refContentHash)
567
+ violations.push("intent_ref.content_hash must equal the snapshot canonical hash");
568
+ if (lifecycle !== "active" && artifactState !== "frozen")
569
+ violations.push("terminal lifecycle requires frozen artifacts");
570
+
571
+ const baseline = stringAt(value.baseline, "record.baseline", violations);
572
+ if (!SHA256_HEX.test(baseline)) violations.push("record.baseline must be sha256:<64 hex>");
573
+ const acceptanceIds = new Set(snapshot ? snapshot.acceptance.map((item) => item.id) : []);
574
+ const attestations = arrayAt(value.attestations, "record.attestations", violations).map((item, index) => parseAttestationV3(item, index, acceptanceIds, violations));
575
+ const findings = arrayAt(value.findings, "record.findings", violations).map((item, index) => parseFinding(item, index, violations));
576
+ const history = arrayAt(value.history, "record.history", violations).map((item, index) => parseHistoryV3(item, index, violations));
577
+ uniqueIds(attestations, "record.attestations", violations);
578
+ uniqueIds(findings, "record.findings", violations);
579
+ uniqueIds(history, "record.history", violations);
580
+ if (violations.length > 0) throw new KernelValidationError(violations);
581
+ return {
582
+ contract: TASK_RECORD_CONTRACT_V3,
583
+ task_id: taskId,
584
+ intent_snapshot: snapshot as TaskIntentV1,
585
+ intent_ref: { path: refPath, content_hash: refContentHash },
586
+ lifecycle: lifecycle as TaskRecordV3["lifecycle"],
587
+ artifact_state: artifactState as TaskRecordV3["artifact_state"],
588
+ baseline,
589
+ attestations,
590
+ findings,
591
+ history,
592
+ };
593
+ }
594
+
595
+ export function assertKernelInvariantsV3(intentRaw: TaskIntentV1, recordRaw: TaskRecordV3): void {
596
+ const intent = parseTaskIntentV1(intentRaw);
597
+ const record = parseTaskRecordV3(recordRaw);
598
+ const violations: string[] = [];
599
+ if (intent.task_id !== record.task_id) violations.push("intent and record task_id must match");
600
+ if (intent.revision !== record.intent_snapshot.revision) violations.push("intent revision must match record snapshot");
601
+ if (canonicalIntentHash(record.intent_snapshot) !== record.intent_ref.content_hash)
602
+ violations.push("record intent_ref.content_hash must match its snapshot");
603
+ const requiredRole: Record<ApprovalKind, ApprovalAuthorityRole> = { review: "reviewer", qa: "qa", user: "user" };
604
+ for (const approval of record.attestations) {
605
+ if (approval.authority_role !== requiredRole[approval.kind])
606
+ violations.push(`approval ${approval.id} kind ${approval.kind} requires authority_role ${requiredRole[approval.kind]}`);
607
+ }
608
+ if (violations.length > 0) throw new KernelInvariantError(violations);
609
+ }
610
+
611
+ export function assertKernelInvariantsV2(
612
+ intentRaw: TaskIntentV1,
613
+ recordRaw: TaskRecordV2,
614
+ ): void {
615
+ const intent = parseTaskIntentV1(intentRaw);
616
+ const record = parseTaskRecordV2(recordRaw);
617
+ const violations: string[] = [];
618
+ if (intent.task_id !== record.task_id)
619
+ violations.push("intent and record task_id must match");
620
+ if (intent.revision !== record.intent_revision)
621
+ violations.push("intent revision and record intent_revision must match");
622
+ if (canonicalIntentHash(record.intent_snapshot) !== record.intent_ref.content_hash)
623
+ violations.push("record intent_ref.content_hash must match its snapshot");
624
+ const requiredRole: Record<ApprovalKind, ApprovalAuthorityRole> = {
625
+ review: "reviewer",
626
+ qa: "qa",
627
+ user: "user",
628
+ };
629
+ for (const approval of record.approvals) {
630
+ if (approval.authority_role !== requiredRole[approval.kind])
631
+ violations.push(
632
+ `approval ${approval.id} kind ${approval.kind} requires authority_role ${requiredRole[approval.kind]}`,
633
+ );
634
+ }
635
+ if (violations.length > 0) throw new KernelInvariantError(violations);
636
+ }
637
+
638
+ // ---------------------------------------------------------------------------
639
+ // R2C2 TaskAction strict parser and update invariants.
640
+ // ---------------------------------------------------------------------------
641
+
642
+ const ACTION_V2_TYPES = [
643
+ "record_finding",
644
+ "resolve_finding",
645
+ "record_approval",
646
+ "record_user_approval",
647
+ "revise_intent",
648
+ "approve_breaking_intent_revision",
649
+ "request_rework",
650
+ "complete",
651
+ "stop",
652
+ "resolve_user_decision",
653
+ ] as const;
654
+
655
+ const ACTION_BASE_FIELDS = [
656
+ "type",
657
+ "event_id",
658
+ "at",
659
+ "actor_id",
660
+ "expected_record_hash",
661
+ "expected_workspace_hash",
662
+ "diff_hash",
663
+ ] as const;
664
+
665
+ function parseActionBase(
666
+ value: Record<string, unknown>,
667
+ path: string,
668
+ violations: string[],
669
+ ): {
670
+ type: (typeof ACTION_V2_TYPES)[number];
671
+ event_id: string;
672
+ at: string;
673
+ actor_id: string;
674
+ expected_record_hash: string;
675
+ expected_workspace_hash: string;
676
+ diff_hash: string;
677
+ } {
678
+ const type = enumAt(value.type, ACTION_V2_TYPES as unknown as string[], `${path}.type`, violations);
679
+ for (const field of ACTION_BASE_FIELDS) {
680
+ if (field !== "type" && !(field in value))
681
+ violations.push(`${path}.${field} is required`);
682
+ }
683
+ return {
684
+ type: type as (typeof ACTION_V2_TYPES)[number],
685
+ event_id: stringAt(value.event_id, `${path}.event_id`, violations),
686
+ at: stringAt(value.at, `${path}.at`, violations),
687
+ actor_id: stringAt(value.actor_id, `${path}.actor_id`, violations),
688
+ expected_record_hash: stringAt(
689
+ value.expected_record_hash,
690
+ `${path}.expected_record_hash`,
691
+ violations,
692
+ ),
693
+ expected_workspace_hash: stringAt(
694
+ value.expected_workspace_hash,
695
+ `${path}.expected_workspace_hash`,
696
+ violations,
697
+ ),
698
+ diff_hash: stringAt(value.diff_hash, `${path}.diff_hash`, violations),
699
+ };
700
+ }
701
+
702
+ export function parseTaskAction(raw: unknown): TaskAction {
703
+ const violations: string[] = [];
704
+ const value = objectAt(raw, "action", violations);
705
+ const base = parseActionBase(value, "action", violations);
706
+ if (!SHA256_HEX.test(base.expected_record_hash))
707
+ violations.push("action.expected_record_hash must be sha256:<64 hex>");
708
+ if (!SHA256_HEX.test(base.expected_workspace_hash))
709
+ violations.push("action.expected_workspace_hash must be sha256:<64 hex>");
710
+ if (!SHA256_HEX.test(base.diff_hash))
711
+ violations.push("action.diff_hash must be sha256:<64 hex>");
712
+
713
+ let action: TaskAction | null = null;
714
+ switch (base.type) {
715
+ case "record_finding": {
716
+ rejectUnknown(
717
+ value,
718
+ [...ACTION_BASE_FIELDS, "finding"],
719
+ "action",
720
+ violations,
721
+ );
722
+ action = {
723
+ ...base,
724
+ type: "record_finding",
725
+ finding: parseFinding(value.finding, 0, violations),
726
+ };
727
+ break;
728
+ }
729
+ case "resolve_finding": {
730
+ rejectUnknown(
731
+ value,
732
+ [...ACTION_BASE_FIELDS, "finding_id"],
733
+ "action",
734
+ violations,
735
+ );
736
+ action = {
737
+ ...base,
738
+ type: "resolve_finding",
739
+ finding_id: stringAt(
740
+ value.finding_id,
741
+ "action.finding_id",
742
+ violations,
743
+ ),
744
+ };
745
+ break;
746
+ }
747
+ case "record_approval":
748
+ case "record_user_approval": {
749
+ rejectUnknown(
750
+ value,
751
+ [...ACTION_BASE_FIELDS, "approval"],
752
+ "action",
753
+ violations,
754
+ );
755
+ const approval = parseApprovalV2(value.approval, 0, violations);
756
+ action = {
757
+ ...base,
758
+ type: base.type,
759
+ approval,
760
+ };
761
+ break;
762
+ }
763
+ case "revise_intent":
764
+ case "approve_breaking_intent_revision": {
765
+ rejectUnknown(
766
+ value,
767
+ [...ACTION_BASE_FIELDS, "next_intent", "next_intent_ref"],
768
+ "action",
769
+ violations,
770
+ );
771
+ let nextIntent: TaskIntentV1 | null = null;
772
+ try {
773
+ nextIntent = parseTaskIntentV1(value.next_intent);
774
+ } catch {
775
+ violations.push("action.next_intent must be a valid TaskIntent v1");
776
+ }
777
+ const refRaw = objectAt(
778
+ value.next_intent_ref,
779
+ "action.next_intent_ref",
780
+ violations,
781
+ );
782
+ rejectUnknown(
783
+ refRaw,
784
+ ["path", "content_hash"],
785
+ "action.next_intent_ref",
786
+ violations,
787
+ );
788
+ const refPath = stringAt(
789
+ refRaw.path,
790
+ "action.next_intent_ref.path",
791
+ violations,
792
+ );
793
+ const refContentHash = stringAt(
794
+ refRaw.content_hash,
795
+ "action.next_intent_ref.content_hash",
796
+ violations,
797
+ );
798
+ if (!SHA256_HEX.test(refContentHash))
799
+ violations.push(
800
+ "action.next_intent_ref.content_hash must be sha256:<64 hex>",
801
+ );
802
+ action = {
803
+ ...base,
804
+ type: base.type,
805
+ next_intent: nextIntent as TaskIntentV1,
806
+ next_intent_ref: {
807
+ path: refPath,
808
+ content_hash: refContentHash,
809
+ },
810
+ };
811
+ break;
812
+ }
813
+ case "complete": {
814
+ rejectUnknown(value, [...ACTION_BASE_FIELDS], "action", violations);
815
+ action = { ...base, type: base.type };
816
+ break;
817
+ }
818
+ case "request_rework": {
819
+ rejectUnknown(
820
+ value,
821
+ [...ACTION_BASE_FIELDS, "findings"],
822
+ "action",
823
+ violations,
824
+ );
825
+ const findings = arrayAt(
826
+ value.findings,
827
+ "action.findings",
828
+ violations,
829
+ ).map((item, index) => parseFinding(item, index, violations));
830
+ if (findings.length === 0)
831
+ violations.push("action.findings must contain at least one finding");
832
+ action = { ...base, type: "request_rework", findings };
833
+ break;
834
+ }
835
+ case "stop": {
836
+ rejectUnknown(value, [...ACTION_BASE_FIELDS, "reason"], "action", violations);
837
+ action = {
838
+ ...base,
839
+ type: "stop",
840
+ reason: stringAt(value.reason, "action.reason", violations),
841
+ };
842
+ break;
843
+ }
844
+ case "resolve_user_decision": {
845
+ rejectUnknown(
846
+ value,
847
+ [...ACTION_BASE_FIELDS, "finding_id", "resolution"],
848
+ "action",
849
+ violations,
850
+ );
851
+ action = {
852
+ ...base,
853
+ type: "resolve_user_decision",
854
+ finding_id: stringAt(
855
+ value.finding_id,
856
+ "action.finding_id",
857
+ violations,
858
+ ),
859
+ resolution: stringAt(
860
+ value.resolution,
861
+ "action.resolution",
862
+ violations,
863
+ ),
864
+ };
865
+ break;
866
+ }
867
+ }
868
+
869
+ if (violations.length > 0) throw new KernelValidationError(violations);
870
+ return action as TaskAction;
871
+ }
872
+
873
+ export function assertTaskRecordUpdateV3(
874
+ previousRaw: TaskRecordV3,
875
+ nextRaw: TaskRecordV3,
876
+ action: TaskAction,
877
+ ): void {
878
+ const previous = parseTaskRecordV3(previousRaw);
879
+ const next = parseTaskRecordV3(nextRaw);
880
+ const violations: string[] = [];
881
+
882
+ if (next.contract !== previous.contract)
883
+ violations.push("record contract must remain immutable");
884
+ if (next.task_id !== previous.task_id)
885
+ violations.push("record task_id must remain immutable");
886
+ if (next.baseline !== previous.baseline)
887
+ violations.push("record baseline must remain immutable");
888
+
889
+ const isIntentAction =
890
+ action.type === "revise_intent" ||
891
+ action.type === "approve_breaking_intent_revision";
892
+
893
+ if (isIntentAction) {
894
+ if (
895
+ next.intent_snapshot.task_id !== previous.intent_snapshot.task_id ||
896
+ next.intent_snapshot.goal !== previous.intent_snapshot.goal ||
897
+ next.intent_snapshot.owner !== previous.intent_snapshot.owner
898
+ )
899
+ violations.push("intent revision cannot change task identity, goal, or owner");
900
+ } else {
901
+ if (
902
+ next.intent_snapshot.task_id !== previous.intent_snapshot.task_id ||
903
+ next.intent_snapshot.goal !== previous.intent_snapshot.goal ||
904
+ next.intent_snapshot.risk !== previous.intent_snapshot.risk ||
905
+ next.intent_snapshot.revision !== previous.intent_snapshot.revision ||
906
+ next.intent_snapshot.owner !== previous.intent_snapshot.owner ||
907
+ canonicalIntentHash(next.intent_snapshot) !==
908
+ canonicalIntentHash(previous.intent_snapshot)
909
+ )
910
+ violations.push("non-intent action cannot change the intent snapshot");
911
+ if (next.intent_ref.content_hash !== previous.intent_ref.content_hash)
912
+ violations.push("non-intent action cannot change intent_ref content hash");
913
+ if (
914
+ next.intent_ref.path !== previous.intent_ref.path &&
915
+ action.type !== "request_rework" &&
916
+ action.type !== "stop"
917
+ )
918
+ violations.push("only artifact transitions may change intent_ref path");
919
+ }
920
+
921
+ // Attestations and history are append-only.
922
+ if (next.attestations.length < previous.attestations.length)
923
+ violations.push("attestations are append-only");
924
+ if (next.history.length !== previous.history.length + 1)
925
+ violations.push("exactly one history entry must be appended");
926
+
927
+ for (const prior of previous.attestations) {
928
+ const current = next.attestations.find((item) => item.id === prior.id);
929
+ if (!current || JSON.stringify(current) !== JSON.stringify(prior))
930
+ violations.push(`attestation ${prior.id} was rewritten`);
931
+ }
932
+ const resolvingFindingIds =
933
+ action.type === "resolve_finding"
934
+ ? [action.finding_id]
935
+ : action.type === "resolve_user_decision"
936
+ ? [action.finding_id]
937
+ : action.type === "approve_breaking_intent_revision"
938
+ ? previous.findings
939
+ .filter((item) => item.kind === "replan_required" && item.status === "open")
940
+ .map((item) => item.id)
941
+ : [];
942
+ const reworkFindingIds =
943
+ action.type === "request_rework"
944
+ ? new Set(action.findings.map((item) => item.id))
945
+ : new Set<string>();
946
+ for (const prior of previous.findings) {
947
+ const current = next.findings.find((item) => item.id === prior.id);
948
+ if (!current)
949
+ violations.push(`finding item ${prior.id} was removed`);
950
+ else if (
951
+ JSON.stringify(current) !== JSON.stringify(prior) &&
952
+ !(
953
+ resolvingFindingIds.includes(prior.id) &&
954
+ prior.status === "open" &&
955
+ current.status === "resolved"
956
+ ) &&
957
+ !(reworkFindingIds.has(prior.id) && current.review_round !== null)
958
+ )
959
+ violations.push(`finding item ${prior.id} was rewritten`);
960
+ }
961
+
962
+ if (violations.length > 0) throw new KernelInvariantError(violations);
963
+ }