immune-brain 3.6.9 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +18 -2
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +76 -121
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +106 -600
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +1 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +3 -3
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +17 -43
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7587 -5049
- package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +11 -12
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
- package/plugins/immune-brain/dist/imm-loop.md +27 -25
- package/plugins/immune-brain/dist/imm-planner.md +34 -27
- package/plugins/immune-brain/dist/imm-review-retro.md +2 -2
- package/plugins/immune-brain/dist/role-prompts/code-review.md +3 -1
- package/plugins/immune-brain/dist/role-prompts/executor.md +4 -4
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +183 -40
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +240 -0
- package/plugins/immune-brain/runtime/assurance/qa.ts +132 -58
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +15 -7
- package/plugins/immune-brain/runtime/assurance/verification.ts +246 -206
- package/plugins/immune-brain/runtime/authorization_operation.ts +20 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +288 -721
- package/plugins/immune-brain/runtime/commands/kernel.ts +158 -67
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/actor_identity.ts +33 -0
- package/plugins/immune-brain/runtime/kernel/application.ts +22 -6
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +94 -5
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +27 -6
- package/plugins/immune-brain/runtime/kernel/backend_claim.ts +43 -16
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +10 -6
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +50 -63
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +13 -4
- package/plugins/immune-brain/runtime/kernel/completion.ts +5 -14
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +124 -34
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +13 -5
- package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
- package/plugins/immune-brain/runtime/kernel/intent.ts +7 -11
- package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/legacy_task_record.ts +323 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +10 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +32 -31
- package/plugins/immune-brain/runtime/kernel/run_identity.ts +121 -0
- package/plugins/immune-brain/runtime/kernel/spec_binding.ts +100 -0
- package/plugins/immune-brain/runtime/kernel/sqlite_migration.ts +950 -0
- package/plugins/immune-brain/runtime/kernel/sqlite_store.ts +1193 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +1254 -1206
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +129 -755
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +419 -46
- package/plugins/immune-brain/runtime/kernel/types.ts +12 -43
- package/plugins/immune-brain/runtime/kernel/validation.ts +60 -274
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +0 -1
- package/plugins/immune-brain/runtime/plan_core.ts +27 -65
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +3 -1
- package/plugins/immune-brain/runtime/prompts/executor.md +4 -4
- package/plugins/immune-brain/runtime/staged_intent.ts +58 -0
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +37 -7
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +42 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +771 -0
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +189 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +35 -0
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +33 -0
- package/plugins/immune-brain/runtime/unattended/types.ts +14 -1
- package/plugins/immune-brain/runtime/v4_runtime.ts +19 -23
- package/plugins/immune-brain/runtime/verification_descriptor.ts +92 -136
- package/plugins/immune-brain/runtime/workspace_scope.ts +98 -13
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -3
- package/plugins/immune-brain/bin/imm-retire-stale-wrapper +0 -4
- package/plugins/immune-brain/bin/imm-retired +0 -4
- package/plugins/immune-brain/runtime/authority_commit_receipts.ts +0 -716
- package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +0 -451
- package/plugins/immune-brain/runtime/kernel/legacy.ts +0 -299
- package/plugins/immune-brain/runtime/kernel/observation.ts +0 -397
- package/plugins/immune-brain/runtime/kernel/readiness.ts +0 -282
- package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +0 -132
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TASK_ARTIFACT_STATES,
|
|
3
3
|
TASK_LIFECYCLES,
|
|
4
|
-
TASK_PHASES,
|
|
5
|
-
TASK_RECORD_CONTRACT_V2,
|
|
6
4
|
TASK_RECORD_CONTRACT_V3,
|
|
7
5
|
TASK_RECORD_CONTRACT_V4,
|
|
8
6
|
REVIEW_REVISION_IDENTITY_CONTRACT,
|
|
@@ -15,17 +13,15 @@ import {
|
|
|
15
13
|
type FindingSource,
|
|
16
14
|
type FindingStatus,
|
|
17
15
|
type TaskAction,
|
|
16
|
+
type ReviewAdvisoryFindingV1,
|
|
18
17
|
type TaskApprovalV2,
|
|
19
18
|
type TaskAttestationV3,
|
|
20
19
|
type AuthorityAuditDescriptor,
|
|
21
|
-
type TaskEvidenceV2,
|
|
22
20
|
type TaskFinding,
|
|
23
|
-
type TaskHistoryEntryV2,
|
|
24
21
|
type TaskHistoryEntryV3,
|
|
25
22
|
type TaskIntentV1,
|
|
26
23
|
type ReviewRevisionIdentityV1,
|
|
27
24
|
type TaskRecord,
|
|
28
|
-
type TaskRecordV2,
|
|
29
25
|
type TaskRecordV3,
|
|
30
26
|
type TaskRecordV4,
|
|
31
27
|
} from "./types";
|
|
@@ -52,7 +48,7 @@ export class KernelInvariantError extends Error {
|
|
|
52
48
|
}
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
function objectAt(
|
|
51
|
+
export function objectAt(
|
|
56
52
|
value: unknown,
|
|
57
53
|
path: string,
|
|
58
54
|
violations: string[],
|
|
@@ -64,7 +60,7 @@ function objectAt(
|
|
|
64
60
|
return value as Record<string, unknown>;
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
function rejectUnknown(
|
|
63
|
+
export function rejectUnknown(
|
|
68
64
|
record: Record<string, unknown>,
|
|
69
65
|
allowed: readonly string[],
|
|
70
66
|
path: string,
|
|
@@ -78,7 +74,7 @@ function rejectUnknown(
|
|
|
78
74
|
}
|
|
79
75
|
}
|
|
80
76
|
|
|
81
|
-
function stringAt(
|
|
77
|
+
export function stringAt(
|
|
82
78
|
value: unknown,
|
|
83
79
|
path: string,
|
|
84
80
|
violations: string[],
|
|
@@ -90,7 +86,7 @@ function stringAt(
|
|
|
90
86
|
return value;
|
|
91
87
|
}
|
|
92
88
|
|
|
93
|
-
function positiveInteger(
|
|
89
|
+
export function positiveInteger(
|
|
94
90
|
value: unknown,
|
|
95
91
|
path: string,
|
|
96
92
|
violations: string[],
|
|
@@ -120,7 +116,7 @@ function nullablePositiveInteger(
|
|
|
120
116
|
return positiveInteger(value, path, violations);
|
|
121
117
|
}
|
|
122
118
|
|
|
123
|
-
function enumAt<T extends string>(
|
|
119
|
+
export function enumAt<T extends string>(
|
|
124
120
|
value: unknown,
|
|
125
121
|
allowed: readonly T[],
|
|
126
122
|
path: string,
|
|
@@ -133,7 +129,7 @@ function enumAt<T extends string>(
|
|
|
133
129
|
return value as T;
|
|
134
130
|
}
|
|
135
131
|
|
|
136
|
-
function arrayAt(value: unknown, path: string, violations: string[]): unknown[] {
|
|
132
|
+
export function arrayAt(value: unknown, path: string, violations: string[]): unknown[] {
|
|
137
133
|
if (!Array.isArray(value)) {
|
|
138
134
|
violations.push(`${path} must be an array`);
|
|
139
135
|
return [];
|
|
@@ -141,7 +137,7 @@ function arrayAt(value: unknown, path: string, violations: string[]): unknown[]
|
|
|
141
137
|
return value;
|
|
142
138
|
}
|
|
143
139
|
|
|
144
|
-
function uniqueIds(
|
|
140
|
+
export function uniqueIds(
|
|
145
141
|
items: Array<{ id: string }>,
|
|
146
142
|
path: string,
|
|
147
143
|
violations: string[],
|
|
@@ -153,7 +149,7 @@ function uniqueIds(
|
|
|
153
149
|
}
|
|
154
150
|
}
|
|
155
151
|
|
|
156
|
-
const EVIDENCE_STATUSES: EvidenceStatus[] = ["passed", "failed", "blocked"];
|
|
152
|
+
export const EVIDENCE_STATUSES: EvidenceStatus[] = ["passed", "failed", "blocked"];
|
|
157
153
|
const FINDING_KINDS: FindingKind[] = [
|
|
158
154
|
"blocking",
|
|
159
155
|
"advisory",
|
|
@@ -224,7 +220,7 @@ function parseFindingCounterevidence(
|
|
|
224
220
|
};
|
|
225
221
|
}
|
|
226
222
|
|
|
227
|
-
function parseFinding(
|
|
223
|
+
export function parseFinding(
|
|
228
224
|
value: unknown,
|
|
229
225
|
index: number,
|
|
230
226
|
violations: string[],
|
|
@@ -274,53 +270,6 @@ function parseFinding(
|
|
|
274
270
|
};
|
|
275
271
|
}
|
|
276
272
|
|
|
277
|
-
function parseHistoryV2(
|
|
278
|
-
value: unknown,
|
|
279
|
-
index: number,
|
|
280
|
-
violations: string[],
|
|
281
|
-
): TaskHistoryEntryV2 {
|
|
282
|
-
const item = objectAt(value, `record.history[${index}]`, violations);
|
|
283
|
-
rejectUnknown(
|
|
284
|
-
item,
|
|
285
|
-
["id", "at", "type", "from_phase", "to_phase", "reason", "authority"],
|
|
286
|
-
`record.history[${index}]`,
|
|
287
|
-
violations,
|
|
288
|
-
);
|
|
289
|
-
let authority: AuthorityAuditDescriptor | undefined;
|
|
290
|
-
if (item.authority !== undefined) {
|
|
291
|
-
const auth = objectAt(item.authority, `record.history[${index}].authority`, violations);
|
|
292
|
-
rejectUnknown(
|
|
293
|
-
auth,
|
|
294
|
-
["authority_kind", "actor_id", "confirmation_ref", "issued_at", "expires_at"],
|
|
295
|
-
`record.history[${index}].authority`,
|
|
296
|
-
violations,
|
|
297
|
-
);
|
|
298
|
-
const kind = enumAt(
|
|
299
|
-
auth.authority_kind,
|
|
300
|
-
["review", "qa", "user"],
|
|
301
|
-
`record.history[${index}].authority.authority_kind`,
|
|
302
|
-
violations,
|
|
303
|
-
);
|
|
304
|
-
authority = {
|
|
305
|
-
authority_kind: kind as AuthorityAuditDescriptor["authority_kind"],
|
|
306
|
-
actor_id: stringAt(auth.actor_id, `record.history[${index}].authority.actor_id`, violations),
|
|
307
|
-
confirmation_ref: stringAt(auth.confirmation_ref, `record.history[${index}].authority.confirmation_ref`, violations),
|
|
308
|
-
issued_at: stringAt(auth.issued_at, `record.history[${index}].authority.issued_at`, violations),
|
|
309
|
-
expires_at: stringAt(auth.expires_at, `record.history[${index}].authority.expires_at`, violations),
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
return {
|
|
313
|
-
id: stringAt(item.id, `record.history[${index}].id`, violations),
|
|
314
|
-
at: stringAt(item.at, `record.history[${index}].at`, violations),
|
|
315
|
-
type: stringAt(item.type, `record.history[${index}].type`, violations),
|
|
316
|
-
from_phase: enumAt(item.from_phase, TASK_PHASES, `record.history[${index}].from_phase`, violations),
|
|
317
|
-
to_phase: enumAt(item.to_phase, TASK_PHASES, `record.history[${index}].to_phase`, violations),
|
|
318
|
-
reason: stringAt(item.reason, `record.history[${index}].reason`, violations),
|
|
319
|
-
...(authority ? { authority } : {}),
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
273
|
function parseHistoryV3(
|
|
325
274
|
value: unknown,
|
|
326
275
|
index: number,
|
|
@@ -352,57 +301,9 @@ function parseHistoryV3(
|
|
|
352
301
|
};
|
|
353
302
|
}
|
|
354
303
|
|
|
355
|
-
const SHA256_HEX = /^sha256:[a-f0-9]{64}$/;
|
|
304
|
+
export const SHA256_HEX = /^sha256:[a-f0-9]{64}$/;
|
|
356
305
|
|
|
357
|
-
function
|
|
358
|
-
value: unknown,
|
|
359
|
-
index: number,
|
|
360
|
-
acceptanceIds: Set<string> | null,
|
|
361
|
-
violations: string[],
|
|
362
|
-
): TaskEvidenceV2 {
|
|
363
|
-
const item = objectAt(value, `record.evidence[${index}]`, violations);
|
|
364
|
-
rejectUnknown(
|
|
365
|
-
item,
|
|
366
|
-
["id", "acceptance_id", "task_revision", "intent_content_hash", "diff_hash", "status", "actor_id", "summary"],
|
|
367
|
-
`record.evidence[${index}]`,
|
|
368
|
-
violations,
|
|
369
|
-
);
|
|
370
|
-
const acceptanceId = stringAt(
|
|
371
|
-
item.acceptance_id,
|
|
372
|
-
`record.evidence[${index}].acceptance_id`,
|
|
373
|
-
violations,
|
|
374
|
-
);
|
|
375
|
-
if (acceptanceIds && !acceptanceIds.has(acceptanceId))
|
|
376
|
-
violations.push(
|
|
377
|
-
`evidence ${String(item.id)} references unknown acceptance ${acceptanceId}`,
|
|
378
|
-
);
|
|
379
|
-
const intentContentHash = stringAt(
|
|
380
|
-
item.intent_content_hash,
|
|
381
|
-
`record.evidence[${index}].intent_content_hash`,
|
|
382
|
-
violations,
|
|
383
|
-
);
|
|
384
|
-
if (!SHA256_HEX.test(intentContentHash))
|
|
385
|
-
violations.push(`record.evidence[${index}].intent_content_hash must be sha256:<64 hex>`);
|
|
386
|
-
const diffHash = stringAt(item.diff_hash, `record.evidence[${index}].diff_hash`, violations);
|
|
387
|
-
if (!SHA256_HEX.test(diffHash))
|
|
388
|
-
violations.push(`record.evidence[${index}].diff_hash must be sha256:<64 hex>`);
|
|
389
|
-
return {
|
|
390
|
-
id: stringAt(item.id, `record.evidence[${index}].id`, violations),
|
|
391
|
-
acceptance_id: acceptanceId,
|
|
392
|
-
task_revision: positiveInteger(
|
|
393
|
-
item.task_revision,
|
|
394
|
-
`record.evidence[${index}].task_revision`,
|
|
395
|
-
violations,
|
|
396
|
-
),
|
|
397
|
-
intent_content_hash: intentContentHash,
|
|
398
|
-
diff_hash: diffHash,
|
|
399
|
-
status: enumAt(item.status, EVIDENCE_STATUSES, `record.evidence[${index}].status`, violations),
|
|
400
|
-
actor_id: stringAt(item.actor_id, `record.evidence[${index}].actor_id`, violations),
|
|
401
|
-
summary: stringAt(item.summary, `record.evidence[${index}].summary`, violations),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function parseApprovalV2(
|
|
306
|
+
export function parseApprovalV2(
|
|
406
307
|
value: unknown,
|
|
407
308
|
index: number,
|
|
408
309
|
violations: string[],
|
|
@@ -411,7 +312,7 @@ function parseApprovalV2(
|
|
|
411
312
|
const item = objectAt(value, `record.approvals[${index}]`, violations);
|
|
412
313
|
rejectUnknown(
|
|
413
314
|
item,
|
|
414
|
-
["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary",
|
|
315
|
+
["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary", "advisory_findings",
|
|
415
316
|
...(allowReviewRevision ? ["review_revision"] : [])],
|
|
416
317
|
`record.approvals[${index}]`,
|
|
417
318
|
violations,
|
|
@@ -447,6 +348,37 @@ function parseApprovalV2(
|
|
|
447
348
|
...(allowReviewRevision && item.review_revision !== undefined
|
|
448
349
|
? { review_revision: parseReviewRevisionIdentity(item.review_revision, `record.approvals[${index}].review_revision`, violations) }
|
|
449
350
|
: {}),
|
|
351
|
+
...(item.advisory_findings !== undefined
|
|
352
|
+
? {
|
|
353
|
+
advisory_findings: arrayAt(
|
|
354
|
+
item.advisory_findings,
|
|
355
|
+
`record.approvals[${index}].advisory_findings`,
|
|
356
|
+
violations,
|
|
357
|
+
).map((entry, advisoryIndex) =>
|
|
358
|
+
parseAdvisoryFinding(
|
|
359
|
+
entry,
|
|
360
|
+
`record.approvals[${index}].advisory_findings[${advisoryIndex}]`,
|
|
361
|
+
violations,
|
|
362
|
+
),
|
|
363
|
+
),
|
|
364
|
+
}
|
|
365
|
+
: {}),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* An advisory is recorded inside the review attestation, so it needs the same
|
|
371
|
+
* machine-checkable provenance a blocking finding carries but no lifecycle.
|
|
372
|
+
*/
|
|
373
|
+
function parseAdvisoryFinding(value: unknown, path: string, violations: string[]): ReviewAdvisoryFindingV1 {
|
|
374
|
+
const item = objectAt(value, path, violations);
|
|
375
|
+
rejectUnknown(item, ["id", "acceptance_id", "summary", "anchor", "evidence"], path, violations);
|
|
376
|
+
return {
|
|
377
|
+
id: stringAt(item.id, `${path}.id`, violations),
|
|
378
|
+
acceptance_id: nullableString(item.acceptance_id, `${path}.acceptance_id`, violations),
|
|
379
|
+
summary: stringAt(item.summary, `${path}.summary`, violations),
|
|
380
|
+
anchor: item.anchor === undefined ? null : nullableAnchor(item.anchor, `${path}.anchor`, violations),
|
|
381
|
+
evidence: parseFindingEvidence(item.evidence ?? null, `${path}.evidence`, violations),
|
|
450
382
|
};
|
|
451
383
|
}
|
|
452
384
|
|
|
@@ -485,7 +417,7 @@ function parseAttestationV3(
|
|
|
485
417
|
const item = objectAt(value, path, violations);
|
|
486
418
|
rejectUnknown(
|
|
487
419
|
item,
|
|
488
|
-
["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary", "acceptance_results",
|
|
420
|
+
["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary", "acceptance_results", "advisory_findings",
|
|
489
421
|
...(allowReviewRevision ? ["review_revision"] : [])],
|
|
490
422
|
path,
|
|
491
423
|
violations,
|
|
@@ -521,6 +453,13 @@ function parseAttestationV3(
|
|
|
521
453
|
violations.push(`${path}.review_revision is only valid on review attestations`);
|
|
522
454
|
if (allowReviewRevision && kind === "review" && !reviewRevision)
|
|
523
455
|
violations.push(`${path}.review_revision is required for v4 review attestations`);
|
|
456
|
+
const advisoryFindings = item.advisory_findings === undefined
|
|
457
|
+
? undefined
|
|
458
|
+
: arrayAt(item.advisory_findings, `${path}.advisory_findings`, violations).map((entry, advisoryIndex) =>
|
|
459
|
+
parseAdvisoryFinding(entry, `${path}.advisory_findings[${advisoryIndex}]`, violations),
|
|
460
|
+
);
|
|
461
|
+
if (advisoryFindings !== undefined && kind !== "review")
|
|
462
|
+
violations.push(`${path}.advisory_findings is only valid on review attestations`);
|
|
524
463
|
return {
|
|
525
464
|
id: stringAt(item.id, `${path}.id`, violations),
|
|
526
465
|
kind,
|
|
@@ -532,133 +471,11 @@ function parseAttestationV3(
|
|
|
532
471
|
summary: stringAt(item.summary, `${path}.summary`, violations),
|
|
533
472
|
acceptance_results: acceptanceResults,
|
|
534
473
|
...(reviewRevision ? { review_revision: reviewRevision } : {}),
|
|
474
|
+
...(advisoryFindings !== undefined ? { advisory_findings: advisoryFindings } : {}),
|
|
535
475
|
};
|
|
536
476
|
}
|
|
537
477
|
|
|
538
|
-
export function
|
|
539
|
-
const violations: string[] = [];
|
|
540
|
-
const value = objectAt(raw, "record", violations);
|
|
541
|
-
rejectUnknown(
|
|
542
|
-
value,
|
|
543
|
-
["contract", "task_id", "intent_revision", "intent_snapshot", "intent_ref", "artifact_ref", "phase", "baseline", "evidence", "findings", "approvals", "history"],
|
|
544
|
-
"record",
|
|
545
|
-
violations,
|
|
546
|
-
);
|
|
547
|
-
if (value.contract !== TASK_RECORD_CONTRACT_V2)
|
|
548
|
-
violations.push(`contract must equal ${TASK_RECORD_CONTRACT_V2}`);
|
|
549
|
-
|
|
550
|
-
let snapshot: TaskIntentV1 | null = null;
|
|
551
|
-
try {
|
|
552
|
-
snapshot = parseTaskIntentV1(value.intent_snapshot);
|
|
553
|
-
} catch {
|
|
554
|
-
violations.push("record.intent_snapshot must be a valid TaskIntent v1");
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const taskId = stringAt(value.task_id, "record.task_id", violations);
|
|
558
|
-
const intentRevision = positiveInteger(
|
|
559
|
-
value.intent_revision,
|
|
560
|
-
"record.intent_revision",
|
|
561
|
-
violations,
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
const refRaw = objectAt(value.intent_ref, "record.intent_ref", violations);
|
|
565
|
-
rejectUnknown(refRaw, ["path", "revision", "content_hash"], "record.intent_ref", violations);
|
|
566
|
-
const refPath = stringAt(refRaw.path, "record.intent_ref.path", violations);
|
|
567
|
-
const refRevision = positiveInteger(
|
|
568
|
-
refRaw.revision,
|
|
569
|
-
"record.intent_ref.revision",
|
|
570
|
-
violations,
|
|
571
|
-
);
|
|
572
|
-
const refContentHash = stringAt(
|
|
573
|
-
refRaw.content_hash,
|
|
574
|
-
"record.intent_ref.content_hash",
|
|
575
|
-
violations,
|
|
576
|
-
);
|
|
577
|
-
if (!SHA256_HEX.test(refContentHash))
|
|
578
|
-
violations.push("record.intent_ref.content_hash must be sha256:<64 hex>");
|
|
579
|
-
|
|
580
|
-
let artifactRef: TaskRecordV2["artifact_ref"];
|
|
581
|
-
if (value.artifact_ref !== undefined) {
|
|
582
|
-
const artifactRaw = objectAt(value.artifact_ref, "record.artifact_ref", violations);
|
|
583
|
-
rejectUnknown(artifactRaw, ["state", "spec_path"], "record.artifact_ref", violations);
|
|
584
|
-
const state = enumAt(artifactRaw.state, ["active", "frozen"], "record.artifact_ref.state", violations) as "active" | "frozen";
|
|
585
|
-
const specPath = artifactRaw.spec_path === undefined
|
|
586
|
-
? undefined
|
|
587
|
-
: stringAt(artifactRaw.spec_path, "record.artifact_ref.spec_path", violations);
|
|
588
|
-
if (specPath !== undefined && (!/^docs\/specs\/(?!archive\/)[A-Za-z0-9._/-]+\.spec\.md$/.test(specPath) || specPath.includes("..")))
|
|
589
|
-
violations.push("record.artifact_ref.spec_path must be one canonical active Spec path");
|
|
590
|
-
artifactRef = { state, ...(specPath === undefined ? {} : { spec_path: specPath }) };
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const activeIntentPath = `docs/plans/${taskId}.intent.json`;
|
|
594
|
-
const frozenIntentPath = `docs/plans/archive/${taskId}.intent.json`;
|
|
595
|
-
if (
|
|
596
|
-
snapshot &&
|
|
597
|
-
(snapshot.task_id !== taskId ||
|
|
598
|
-
snapshot.revision !== intentRevision ||
|
|
599
|
-
snapshot.revision !== refRevision ||
|
|
600
|
-
(refPath !== activeIntentPath && refPath !== frozenIntentPath))
|
|
601
|
-
)
|
|
602
|
-
violations.push("intent_snapshot and intent_ref must match record identity");
|
|
603
|
-
if (artifactRef?.state === "active" && refPath !== activeIntentPath)
|
|
604
|
-
violations.push("active artifact_ref requires the active intent path");
|
|
605
|
-
if (artifactRef?.state === "frozen" && refPath !== frozenIntentPath)
|
|
606
|
-
violations.push("frozen artifact_ref requires the archived intent path");
|
|
607
|
-
if (
|
|
608
|
-
snapshot &&
|
|
609
|
-
refContentHash !== "" &&
|
|
610
|
-
canonicalIntentHash(snapshot) !== refContentHash
|
|
611
|
-
)
|
|
612
|
-
violations.push("intent_ref.content_hash must equal the snapshot canonical hash");
|
|
613
|
-
|
|
614
|
-
const baseline = stringAt(value.baseline, "record.baseline", violations);
|
|
615
|
-
if (!SHA256_HEX.test(baseline))
|
|
616
|
-
violations.push("record.baseline must be sha256:<64 hex>");
|
|
617
|
-
|
|
618
|
-
const acceptanceIds = new Set(
|
|
619
|
-
snapshot ? snapshot.acceptance.map((item) => item.id) : [],
|
|
620
|
-
);
|
|
621
|
-
const evidence = arrayAt(value.evidence, "record.evidence", violations).map(
|
|
622
|
-
(item, index) => parseEvidenceV2(item, index, acceptanceIds, violations),
|
|
623
|
-
);
|
|
624
|
-
const findings = arrayAt(value.findings, "record.findings", violations).map(
|
|
625
|
-
(item, index) => parseFinding(item, index, violations),
|
|
626
|
-
);
|
|
627
|
-
const approvals = arrayAt(value.approvals, "record.approvals", violations).map(
|
|
628
|
-
(item, index) => parseApprovalV2(item, index, violations),
|
|
629
|
-
);
|
|
630
|
-
const history = arrayAt(value.history, "record.history", violations).map(
|
|
631
|
-
(item, index) => parseHistoryV2(item, index, violations),
|
|
632
|
-
);
|
|
633
|
-
uniqueIds(evidence, "record.evidence", violations);
|
|
634
|
-
uniqueIds(findings, "record.findings", violations);
|
|
635
|
-
uniqueIds(approvals, "record.approvals", violations);
|
|
636
|
-
uniqueIds(history, "record.history", violations);
|
|
637
|
-
|
|
638
|
-
const phase = enumAt(value.phase, TASK_PHASES, "phase", violations);
|
|
639
|
-
|
|
640
|
-
if (violations.length > 0) throw new KernelValidationError(violations);
|
|
641
|
-
return {
|
|
642
|
-
contract: TASK_RECORD_CONTRACT_V2,
|
|
643
|
-
task_id: taskId,
|
|
644
|
-
intent_revision: intentRevision,
|
|
645
|
-
intent_snapshot: snapshot as TaskIntentV1,
|
|
646
|
-
intent_ref: {
|
|
647
|
-
path: refPath,
|
|
648
|
-
revision: refRevision,
|
|
649
|
-
content_hash: refContentHash,
|
|
650
|
-
},
|
|
651
|
-
...(artifactRef ? { artifact_ref: artifactRef } : {}),
|
|
652
|
-
phase,
|
|
653
|
-
baseline,
|
|
654
|
-
evidence,
|
|
655
|
-
findings,
|
|
656
|
-
approvals,
|
|
657
|
-
history,
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 | TaskRecordV4 {
|
|
478
|
+
export function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 | TaskRecordV4 {
|
|
662
479
|
const violations: string[] = [];
|
|
663
480
|
const value = objectAt(raw, "record", violations);
|
|
664
481
|
rejectUnknown(
|
|
@@ -693,8 +510,8 @@ function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 |
|
|
|
693
510
|
violations.push("intent_snapshot and intent_ref must match record identity");
|
|
694
511
|
if (artifactState === "active" && refPath !== activeIntentPath)
|
|
695
512
|
violations.push("active artifact_state requires the active intent path");
|
|
696
|
-
if (artifactState === "frozen" && refPath !== frozenIntentPath)
|
|
697
|
-
violations.push("frozen artifact_state requires the archived intent path");
|
|
513
|
+
if (artifactState === "frozen" && refPath !== activeIntentPath && refPath !== frozenIntentPath)
|
|
514
|
+
violations.push("frozen artifact_state requires the active or archived intent path");
|
|
698
515
|
if (snapshot && refContentHash !== "" && canonicalIntentHash(snapshot) !== refContentHash)
|
|
699
516
|
violations.push("intent_ref.content_hash must equal the snapshot canonical hash");
|
|
700
517
|
if (lifecycle !== "active" && artifactState !== "frozen")
|
|
@@ -790,21 +607,17 @@ function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 |
|
|
|
790
607
|
return record as TaskRecordV3 | TaskRecordV4;
|
|
791
608
|
}
|
|
792
609
|
|
|
793
|
-
/** Strict v3 drain parser: unknown fields and revision identity stay illegal. */
|
|
794
|
-
export function parseTaskRecordV3(raw: unknown): TaskRecordV3 {
|
|
795
|
-
return parseTaskRecordAtVersion(raw, 3) as TaskRecordV3;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
610
|
/** Strict v4 parser: the Enrollment base commit is mandatory. */
|
|
799
611
|
export function parseTaskRecordV4(raw: unknown): TaskRecordV4 {
|
|
800
612
|
return parseTaskRecordAtVersion(raw, 4) as TaskRecordV4;
|
|
801
613
|
}
|
|
802
614
|
|
|
803
|
-
/**
|
|
615
|
+
/** Live TaskRecord parser. The v3 drain window is closed: a non-v4 contract is rejected, not fallen back on. */
|
|
804
616
|
export function parseTaskRecord(raw: unknown): TaskRecord {
|
|
805
617
|
const contract = (raw as { contract?: unknown } | null)?.contract;
|
|
806
|
-
if (contract
|
|
807
|
-
|
|
618
|
+
if (contract !== TASK_RECORD_CONTRACT_V4)
|
|
619
|
+
throw new KernelValidationError([`contract must equal ${TASK_RECORD_CONTRACT_V4}`]);
|
|
620
|
+
return parseTaskRecordV4(raw);
|
|
808
621
|
}
|
|
809
622
|
|
|
810
623
|
export function assertKernelInvariantsV3(intentRaw: TaskIntentV1, recordRaw: TaskRecord): void {
|
|
@@ -823,33 +636,6 @@ export function assertKernelInvariantsV3(intentRaw: TaskIntentV1, recordRaw: Tas
|
|
|
823
636
|
if (violations.length > 0) throw new KernelInvariantError(violations);
|
|
824
637
|
}
|
|
825
638
|
|
|
826
|
-
export function assertKernelInvariantsV2(
|
|
827
|
-
intentRaw: TaskIntentV1,
|
|
828
|
-
recordRaw: TaskRecordV2,
|
|
829
|
-
): void {
|
|
830
|
-
const intent = parseTaskIntentV1(intentRaw);
|
|
831
|
-
const record = parseTaskRecordV2(recordRaw);
|
|
832
|
-
const violations: string[] = [];
|
|
833
|
-
if (intent.task_id !== record.task_id)
|
|
834
|
-
violations.push("intent and record task_id must match");
|
|
835
|
-
if (intent.revision !== record.intent_revision)
|
|
836
|
-
violations.push("intent revision and record intent_revision must match");
|
|
837
|
-
if (canonicalIntentHash(record.intent_snapshot) !== record.intent_ref.content_hash)
|
|
838
|
-
violations.push("record intent_ref.content_hash must match its snapshot");
|
|
839
|
-
const requiredRole: Record<ApprovalKind, ApprovalAuthorityRole> = {
|
|
840
|
-
review: "reviewer",
|
|
841
|
-
qa: "qa",
|
|
842
|
-
user: "user",
|
|
843
|
-
};
|
|
844
|
-
for (const approval of record.approvals) {
|
|
845
|
-
if (approval.authority_role !== requiredRole[approval.kind])
|
|
846
|
-
violations.push(
|
|
847
|
-
`approval ${approval.id} kind ${approval.kind} requires authority_role ${requiredRole[approval.kind]}`,
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
if (violations.length > 0) throw new KernelInvariantError(violations);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
639
|
// ---------------------------------------------------------------------------
|
|
854
640
|
// R2C2 TaskAction strict parser and update invariants.
|
|
855
641
|
// ---------------------------------------------------------------------------
|