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.
- package/README.md +239 -0
- package/README.zh-CN.md +239 -0
- package/package.json +84 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +666 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +1622 -0
- package/plugins/immune-brain/.pi-extension/package.json +11 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +711 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance.ts +154 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +349 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +90 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +214 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +50 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +262 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +28 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +237 -0
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +414 -0
- package/plugins/immune-brain/.pi-extension/tsconfig.json +20 -0
- package/plugins/immune-brain/bin/imm-kernel +4 -0
- package/plugins/immune-brain/bin/imm-plan +4 -0
- package/plugins/immune-brain/bin/imm-pr-diag +230 -0
- package/plugins/immune-brain/bin/imm-retire-stale-wrapper +4 -0
- package/plugins/immune-brain/bin/imm-retired +4 -0
- package/plugins/immune-brain/bin/imm-tracker +4 -0
- package/plugins/immune-brain/dist/BASELINE.md +138 -0
- package/plugins/immune-brain/dist/docs/reference/HANDOFF-template.md +122 -0
- package/plugins/immune-brain/dist/docs/reference/design-contract-audit-rubric.md +149 -0
- package/plugins/immune-brain/dist/docs/reference/design-contract-review-checklist.md +55 -0
- package/plugins/immune-brain/dist/docs/reference/i18n-review-checklist.md +110 -0
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +52 -0
- package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +94 -0
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +44 -0
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +105 -0
- package/plugins/immune-brain/dist/docs/reference/ux-heuristic-checklist.md +131 -0
- package/plugins/immune-brain/dist/imm-brainstorm.md +140 -0
- package/plugins/immune-brain/dist/imm-doc-prune.md +137 -0
- package/plugins/immune-brain/dist/imm-loop.md +158 -0
- package/plugins/immune-brain/dist/imm-planner.md +387 -0
- package/plugins/immune-brain/dist/imm-pr-fix.md +71 -0
- package/plugins/immune-brain/dist/registry.yaml +49 -0
- package/plugins/immune-brain/dist/role-prompts/advisory-reviewer.md +16 -0
- package/plugins/immune-brain/dist/role-prompts/arch-explorer.md +14 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +15 -0
- package/plugins/immune-brain/dist/role-prompts/compounder.md +20 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +13 -0
- package/plugins/immune-brain/dist/role-prompts/pr-fix.md +81 -0
- package/plugins/immune-brain/dist/role-prompts/qa.md +23 -0
- package/plugins/immune-brain/dist/role-prompts/test-fixer.md +3 -0
- package/plugins/immune-brain/dist/role-prompts/ui-review.md +14 -0
- package/plugins/immune-brain/runtime/authority_commit_receipts.ts +716 -0
- package/plugins/immune-brain/runtime/canonical_json.ts +19 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +1160 -0
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +1009 -0
- package/plugins/immune-brain/runtime/kernel/application.ts +300 -0
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +284 -0
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +208 -0
- package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +451 -0
- package/plugins/immune-brain/runtime/kernel/backend_claim.ts +197 -0
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +507 -0
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +73 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +160 -0
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +194 -0
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +123 -0
- package/plugins/immune-brain/runtime/kernel/index.ts +29 -0
- package/plugins/immune-brain/runtime/kernel/intent.ts +563 -0
- package/plugins/immune-brain/runtime/kernel/intent_token_registry.ts +80 -0
- package/plugins/immune-brain/runtime/kernel/legacy.ts +299 -0
- package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +153 -0
- package/plugins/immune-brain/runtime/kernel/observation.ts +395 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +169 -0
- package/plugins/immune-brain/runtime/kernel/readiness.ts +282 -0
- package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +132 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +624 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +1780 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +791 -0
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +492 -0
- package/plugins/immune-brain/runtime/kernel/types.ts +295 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +963 -0
- package/plugins/immune-brain/runtime/loop_contract.ts +362 -0
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +462 -0
- package/plugins/immune-brain/runtime/plan_core.ts +1053 -0
- package/plugins/immune-brain/runtime/prompts/advisory-reviewer.md +16 -0
- package/plugins/immune-brain/runtime/prompts/arch-explorer.md +14 -0
- package/plugins/immune-brain/runtime/prompts/code-review.md +15 -0
- package/plugins/immune-brain/runtime/prompts/compounder.md +20 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +13 -0
- package/plugins/immune-brain/runtime/prompts/pr-fix.md +81 -0
- package/plugins/immune-brain/runtime/prompts/qa.md +23 -0
- package/plugins/immune-brain/runtime/prompts/test-fixer.md +3 -0
- package/plugins/immune-brain/runtime/prompts/ui-review.md +14 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +160 -0
- package/plugins/immune-brain/runtime/v4_runtime.ts +295 -0
- package/plugins/immune-brain/runtime/verification_descriptor.ts +162 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +623 -0
- package/plugins/immune-brain/skills/.ignore +1 -0
- package/plugins/immune-brain/skills/BASELINE.md +138 -0
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +66 -0
- package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +11 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +52 -0
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +221 -0
- package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +10 -0
- package/plugins/immune-brain/skills/registry.yaml +49 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
constants,
|
|
5
|
+
existsSync,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { dirname, relative, resolve } from "node:path";
|
|
14
|
+
import { stableStringify } from "./canonical_json";
|
|
15
|
+
|
|
16
|
+
export const AUTHORITY_COMMIT_RECEIPT_CONTRACT =
|
|
17
|
+
"assurance_kernel/authority_commit_receipt/v1" as const;
|
|
18
|
+
export const AUTHORITY_COMMIT_RECEIPT_V2_CONTRACT =
|
|
19
|
+
"assurance_kernel/authority_commit_receipt/v2" as const;
|
|
20
|
+
export const AUTHORITY_OBSERVATION_GENERATION_V2 =
|
|
21
|
+
"automatic-observation/v2" as const;
|
|
22
|
+
export const AUTHORITY_OBSERVER_VERSION_V2 =
|
|
23
|
+
"assurance-kernel-p2a-observer/v2" as const;
|
|
24
|
+
|
|
25
|
+
export type AuthorityCommitSourceKind =
|
|
26
|
+
| "state_mutation"
|
|
27
|
+
| "project_migration";
|
|
28
|
+
export type AuthorityCommitReceiptStatus =
|
|
29
|
+
| "prepared"
|
|
30
|
+
| "committed"
|
|
31
|
+
| "aborted"
|
|
32
|
+
| "recovered_committed"
|
|
33
|
+
| "recovered_aborted";
|
|
34
|
+
|
|
35
|
+
export interface AuthorityObservationSeedV2 {
|
|
36
|
+
contract: "assurance_kernel/authority_observation_seed/v2";
|
|
37
|
+
observer_version: typeof AUTHORITY_OBSERVER_VERSION_V2;
|
|
38
|
+
source_kind: AuthorityCommitSourceKind;
|
|
39
|
+
source_ref: string;
|
|
40
|
+
state_path_identity: string;
|
|
41
|
+
committed_bytes_sha256: string;
|
|
42
|
+
committed_revision: string;
|
|
43
|
+
committed_at: string;
|
|
44
|
+
plan_path: string | null;
|
|
45
|
+
plan_signature: string | null;
|
|
46
|
+
source_events: Array<{ id: string; action: string; at: string | null }>;
|
|
47
|
+
shadow: {
|
|
48
|
+
phase: "working" | "review" | "done" | "stopped" | null;
|
|
49
|
+
reason: string;
|
|
50
|
+
ambiguous: boolean;
|
|
51
|
+
source_states: string[];
|
|
52
|
+
};
|
|
53
|
+
divergence: { detected: boolean; fields: string[] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AuthorityCommitTargetInput {
|
|
57
|
+
absolute_path: string;
|
|
58
|
+
before_bytes: string | null;
|
|
59
|
+
after_bytes: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AuthorityCommitTarget {
|
|
63
|
+
path: string;
|
|
64
|
+
before_sha256: string | null;
|
|
65
|
+
after_sha256: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PrepareAuthorityCommitInput {
|
|
69
|
+
source_kind: AuthorityCommitSourceKind;
|
|
70
|
+
targets: AuthorityCommitTargetInput[];
|
|
71
|
+
ledger_revision: string;
|
|
72
|
+
source_ref: string;
|
|
73
|
+
attempt_id?: string;
|
|
74
|
+
observation_generation?: typeof AUTHORITY_OBSERVATION_GENERATION_V2;
|
|
75
|
+
observation_seed?: AuthorityObservationSeedV2;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface AuthorityCommitReceipt {
|
|
79
|
+
contract:
|
|
80
|
+
| typeof AUTHORITY_COMMIT_RECEIPT_CONTRACT
|
|
81
|
+
| typeof AUTHORITY_COMMIT_RECEIPT_V2_CONTRACT;
|
|
82
|
+
record_id: string;
|
|
83
|
+
attempt_id: string;
|
|
84
|
+
source_kind: AuthorityCommitSourceKind;
|
|
85
|
+
status: AuthorityCommitReceiptStatus;
|
|
86
|
+
state_path_identity: string;
|
|
87
|
+
targets: AuthorityCommitTarget[];
|
|
88
|
+
before_sha256: string | null;
|
|
89
|
+
after_sha256: string;
|
|
90
|
+
ledger_revision: string;
|
|
91
|
+
source_ref: string;
|
|
92
|
+
previous_record_hash: string | null;
|
|
93
|
+
recorded_at: string;
|
|
94
|
+
observation_generation?: typeof AUTHORITY_OBSERVATION_GENERATION_V2;
|
|
95
|
+
observation_seed?: AuthorityObservationSeedV2;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type PreparedAuthorityCommit = AuthorityCommitReceipt & {
|
|
99
|
+
status: "prepared";
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const JOURNAL_NAME = ".current_iteration.authority_commit_receipts.jsonl";
|
|
103
|
+
const ATTEMPT_ID_RE =
|
|
104
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
105
|
+
const SHA256_RE = /^sha256:[0-9a-f]{64}$/;
|
|
106
|
+
const TERMINAL_STATUSES = new Set<AuthorityCommitReceiptStatus>([
|
|
107
|
+
"committed",
|
|
108
|
+
"aborted",
|
|
109
|
+
"recovered_committed",
|
|
110
|
+
"recovered_aborted",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
let beforeAppendForTest:
|
|
114
|
+
| ((record: Omit<AuthorityCommitReceipt, "record_id">) => void)
|
|
115
|
+
| null = null;
|
|
116
|
+
|
|
117
|
+
function sha256(value: string): string {
|
|
118
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function recordHash(
|
|
122
|
+
record: Omit<AuthorityCommitReceipt, "record_id">,
|
|
123
|
+
): string {
|
|
124
|
+
const domain =
|
|
125
|
+
record.contract === AUTHORITY_COMMIT_RECEIPT_V2_CONTRACT
|
|
126
|
+
? "assurance-kernel-authority-commit-receipt/v2"
|
|
127
|
+
: "assurance-kernel-authority-commit-receipt/v1";
|
|
128
|
+
return sha256(`${domain}\0${stableStringify(record)}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function projectRootForStatePath(statePath: string): string {
|
|
132
|
+
const absoluteStatePath = resolve(statePath);
|
|
133
|
+
const memoryDir = dirname(absoluteStatePath);
|
|
134
|
+
if (dirname(memoryDir) === memoryDir) {
|
|
135
|
+
throw new Error("authority receipt state path has no project root");
|
|
136
|
+
}
|
|
137
|
+
return dirname(dirname(memoryDir));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function authorityStatePathIdentity(statePath: string): string {
|
|
141
|
+
return sha256(
|
|
142
|
+
`assurance-kernel-state-path/v1\0${resolve(statePath).replace(/\\/g, "/")}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function targetPath(root: string, absolutePath: string): string {
|
|
147
|
+
const absolute = resolve(absolutePath);
|
|
148
|
+
const rel = relative(root, absolute).replace(/\\/g, "/");
|
|
149
|
+
if (!rel || rel === ".." || rel.startsWith("../") || rel.startsWith("/")) {
|
|
150
|
+
throw new Error("authority receipt target must be inside the project root");
|
|
151
|
+
}
|
|
152
|
+
return rel;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function currentTargetHash(root: string, target: AuthorityCommitTarget): string | null {
|
|
156
|
+
const absolute = resolve(root, target.path);
|
|
157
|
+
const rel = relative(root, absolute).replace(/\\/g, "/");
|
|
158
|
+
if (rel !== target.path || rel.startsWith("../")) {
|
|
159
|
+
throw new Error("authority receipt target escaped the project root");
|
|
160
|
+
}
|
|
161
|
+
if (!existsSync(absolute)) return null;
|
|
162
|
+
const stat = lstatSync(absolute);
|
|
163
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
164
|
+
throw new Error(`authority receipt target is not a regular file: ${target.path}`);
|
|
165
|
+
}
|
|
166
|
+
return sha256(readFileSync(absolute, "utf8"));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function fsyncDirectory(path: string): void {
|
|
170
|
+
const descriptor = openSync(path, "r");
|
|
171
|
+
try {
|
|
172
|
+
fsyncSync(descriptor);
|
|
173
|
+
} finally {
|
|
174
|
+
closeSync(descriptor);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function receiptJournalPath(statePath: string): string {
|
|
179
|
+
return resolve(dirname(resolve(statePath)), JOURNAL_NAME);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
183
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
184
|
+
throw new Error("authority receipt record must be an object");
|
|
185
|
+
}
|
|
186
|
+
return value as Record<string, unknown>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function parseTarget(value: unknown): AuthorityCommitTarget {
|
|
190
|
+
const record = asRecord(value);
|
|
191
|
+
if (
|
|
192
|
+
Object.keys(record).sort().join(",") !==
|
|
193
|
+
"after_sha256,before_sha256,path"
|
|
194
|
+
) {
|
|
195
|
+
throw new Error("authority receipt target has unsupported fields");
|
|
196
|
+
}
|
|
197
|
+
if (typeof record.path !== "string" || !record.path) {
|
|
198
|
+
throw new Error("authority receipt target path is invalid");
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
(record.before_sha256 !== null &&
|
|
202
|
+
(typeof record.before_sha256 !== "string" ||
|
|
203
|
+
!SHA256_RE.test(record.before_sha256))) ||
|
|
204
|
+
typeof record.after_sha256 !== "string" ||
|
|
205
|
+
!SHA256_RE.test(record.after_sha256)
|
|
206
|
+
) {
|
|
207
|
+
throw new Error("authority receipt target hashes are invalid");
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
path: record.path,
|
|
211
|
+
before_sha256: record.before_sha256 as string | null,
|
|
212
|
+
after_sha256: record.after_sha256,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parseObservationSeed(value: unknown): AuthorityObservationSeedV2 {
|
|
217
|
+
const record = asRecord(value);
|
|
218
|
+
const expectedKeys = [
|
|
219
|
+
"committed_at",
|
|
220
|
+
"committed_bytes_sha256",
|
|
221
|
+
"committed_revision",
|
|
222
|
+
"contract",
|
|
223
|
+
"divergence",
|
|
224
|
+
"observer_version",
|
|
225
|
+
"plan_path",
|
|
226
|
+
"plan_signature",
|
|
227
|
+
"shadow",
|
|
228
|
+
"source_events",
|
|
229
|
+
"source_kind",
|
|
230
|
+
"source_ref",
|
|
231
|
+
"state_path_identity",
|
|
232
|
+
].sort();
|
|
233
|
+
if (Object.keys(record).sort().join(",") !== expectedKeys.join(",")) {
|
|
234
|
+
throw new Error("authority observation seed has unsupported fields");
|
|
235
|
+
}
|
|
236
|
+
if (
|
|
237
|
+
record.contract !== "assurance_kernel/authority_observation_seed/v2" ||
|
|
238
|
+
record.observer_version !== AUTHORITY_OBSERVER_VERSION_V2 ||
|
|
239
|
+
(record.source_kind !== "state_mutation" &&
|
|
240
|
+
record.source_kind !== "project_migration") ||
|
|
241
|
+
typeof record.source_ref !== "string" ||
|
|
242
|
+
!record.source_ref ||
|
|
243
|
+
typeof record.state_path_identity !== "string" ||
|
|
244
|
+
!SHA256_RE.test(record.state_path_identity) ||
|
|
245
|
+
typeof record.committed_bytes_sha256 !== "string" ||
|
|
246
|
+
!SHA256_RE.test(record.committed_bytes_sha256) ||
|
|
247
|
+
typeof record.committed_revision !== "string" ||
|
|
248
|
+
!record.committed_revision ||
|
|
249
|
+
typeof record.committed_at !== "string" ||
|
|
250
|
+
!Number.isFinite(Date.parse(record.committed_at)) ||
|
|
251
|
+
(record.plan_path !== null && typeof record.plan_path !== "string") ||
|
|
252
|
+
(record.plan_signature !== null && typeof record.plan_signature !== "string") ||
|
|
253
|
+
!Array.isArray(record.source_events) ||
|
|
254
|
+
record.source_events.length === 0 ||
|
|
255
|
+
record.source_events.length > 256
|
|
256
|
+
) {
|
|
257
|
+
throw new Error("authority observation seed fields are invalid");
|
|
258
|
+
}
|
|
259
|
+
const sourceEvents = record.source_events.map((value) => {
|
|
260
|
+
const event = asRecord(value);
|
|
261
|
+
if (
|
|
262
|
+
Object.keys(event).sort().join(",") !== "action,at,id" ||
|
|
263
|
+
typeof event.id !== "string" ||
|
|
264
|
+
!event.id ||
|
|
265
|
+
typeof event.action !== "string" ||
|
|
266
|
+
!event.action ||
|
|
267
|
+
(event.at !== null &&
|
|
268
|
+
(typeof event.at !== "string" || !Number.isFinite(Date.parse(event.at))))
|
|
269
|
+
) {
|
|
270
|
+
throw new Error("authority observation seed source event is invalid");
|
|
271
|
+
}
|
|
272
|
+
return { id: event.id, action: event.action, at: event.at as string | null };
|
|
273
|
+
});
|
|
274
|
+
const shadow = asRecord(record.shadow);
|
|
275
|
+
const divergence = asRecord(record.divergence);
|
|
276
|
+
if (
|
|
277
|
+
Object.keys(shadow).sort().join(",") !==
|
|
278
|
+
"ambiguous,phase,reason,source_states" ||
|
|
279
|
+
(shadow.phase !== null &&
|
|
280
|
+
!['working', 'review', 'done', 'stopped'].includes(String(shadow.phase))) ||
|
|
281
|
+
typeof shadow.reason !== "string" ||
|
|
282
|
+
typeof shadow.ambiguous !== "boolean" ||
|
|
283
|
+
!Array.isArray(shadow.source_states) ||
|
|
284
|
+
shadow.source_states.length > 128 ||
|
|
285
|
+
shadow.source_states.some((state) => typeof state !== "string") ||
|
|
286
|
+
Object.keys(divergence).sort().join(",") !== "detected,fields" ||
|
|
287
|
+
typeof divergence.detected !== "boolean" ||
|
|
288
|
+
!Array.isArray(divergence.fields) ||
|
|
289
|
+
divergence.fields.length > 128 ||
|
|
290
|
+
divergence.fields.some((field) => typeof field !== "string")
|
|
291
|
+
) {
|
|
292
|
+
throw new Error("authority observation seed projection is invalid");
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
contract: "assurance_kernel/authority_observation_seed/v2",
|
|
296
|
+
observer_version: AUTHORITY_OBSERVER_VERSION_V2,
|
|
297
|
+
source_kind: record.source_kind,
|
|
298
|
+
source_ref: record.source_ref,
|
|
299
|
+
state_path_identity: record.state_path_identity,
|
|
300
|
+
committed_bytes_sha256: record.committed_bytes_sha256,
|
|
301
|
+
committed_revision: record.committed_revision,
|
|
302
|
+
committed_at: record.committed_at,
|
|
303
|
+
plan_path: record.plan_path as string | null,
|
|
304
|
+
plan_signature: record.plan_signature as string | null,
|
|
305
|
+
source_events: sourceEvents,
|
|
306
|
+
shadow: {
|
|
307
|
+
phase: shadow.phase as AuthorityObservationSeedV2["shadow"]["phase"],
|
|
308
|
+
reason: shadow.reason,
|
|
309
|
+
ambiguous: shadow.ambiguous,
|
|
310
|
+
source_states: shadow.source_states as string[],
|
|
311
|
+
},
|
|
312
|
+
divergence: {
|
|
313
|
+
detected: divergence.detected,
|
|
314
|
+
fields: divergence.fields as string[],
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function parseReceipt(value: unknown): AuthorityCommitReceipt {
|
|
320
|
+
const record = asRecord(value);
|
|
321
|
+
const v2 = record.contract === AUTHORITY_COMMIT_RECEIPT_V2_CONTRACT;
|
|
322
|
+
const expectedKeys = [
|
|
323
|
+
"after_sha256",
|
|
324
|
+
"attempt_id",
|
|
325
|
+
"before_sha256",
|
|
326
|
+
"contract",
|
|
327
|
+
"ledger_revision",
|
|
328
|
+
...(v2 ? ["observation_generation", "observation_seed"] : []),
|
|
329
|
+
"previous_record_hash",
|
|
330
|
+
"record_id",
|
|
331
|
+
"recorded_at",
|
|
332
|
+
"source_kind",
|
|
333
|
+
"source_ref",
|
|
334
|
+
"state_path_identity",
|
|
335
|
+
"status",
|
|
336
|
+
"targets",
|
|
337
|
+
].sort();
|
|
338
|
+
if (Object.keys(record).sort().join(",") !== expectedKeys.join(",")) {
|
|
339
|
+
throw new Error("authority receipt has unsupported fields");
|
|
340
|
+
}
|
|
341
|
+
if (
|
|
342
|
+
record.contract !== AUTHORITY_COMMIT_RECEIPT_CONTRACT &&
|
|
343
|
+
record.contract !== AUTHORITY_COMMIT_RECEIPT_V2_CONTRACT
|
|
344
|
+
) {
|
|
345
|
+
throw new Error("authority receipt contract is unsupported");
|
|
346
|
+
}
|
|
347
|
+
if (typeof record.record_id !== "string" || !SHA256_RE.test(record.record_id)) {
|
|
348
|
+
throw new Error("authority receipt record_id is invalid");
|
|
349
|
+
}
|
|
350
|
+
if (typeof record.attempt_id !== "string" || !ATTEMPT_ID_RE.test(record.attempt_id)) {
|
|
351
|
+
throw new Error("authority receipt attempt_id is invalid");
|
|
352
|
+
}
|
|
353
|
+
if (
|
|
354
|
+
record.source_kind !== "state_mutation" &&
|
|
355
|
+
record.source_kind !== "project_migration"
|
|
356
|
+
) {
|
|
357
|
+
throw new Error("authority receipt source_kind is invalid");
|
|
358
|
+
}
|
|
359
|
+
if (
|
|
360
|
+
record.status !== "prepared" &&
|
|
361
|
+
record.status !== "committed" &&
|
|
362
|
+
record.status !== "aborted" &&
|
|
363
|
+
record.status !== "recovered_committed" &&
|
|
364
|
+
record.status !== "recovered_aborted"
|
|
365
|
+
) {
|
|
366
|
+
throw new Error("authority receipt status is invalid");
|
|
367
|
+
}
|
|
368
|
+
if (
|
|
369
|
+
typeof record.state_path_identity !== "string" ||
|
|
370
|
+
!SHA256_RE.test(record.state_path_identity) ||
|
|
371
|
+
!Array.isArray(record.targets) ||
|
|
372
|
+
record.targets.length === 0 ||
|
|
373
|
+
(record.before_sha256 !== null &&
|
|
374
|
+
(typeof record.before_sha256 !== "string" ||
|
|
375
|
+
!SHA256_RE.test(record.before_sha256))) ||
|
|
376
|
+
typeof record.after_sha256 !== "string" ||
|
|
377
|
+
!SHA256_RE.test(record.after_sha256) ||
|
|
378
|
+
typeof record.ledger_revision !== "string" ||
|
|
379
|
+
!record.ledger_revision ||
|
|
380
|
+
typeof record.source_ref !== "string" ||
|
|
381
|
+
!record.source_ref ||
|
|
382
|
+
(record.previous_record_hash !== null &&
|
|
383
|
+
(typeof record.previous_record_hash !== "string" ||
|
|
384
|
+
!SHA256_RE.test(record.previous_record_hash))) ||
|
|
385
|
+
typeof record.recorded_at !== "string" ||
|
|
386
|
+
!Number.isFinite(Date.parse(record.recorded_at))
|
|
387
|
+
) {
|
|
388
|
+
throw new Error("authority receipt fields are invalid");
|
|
389
|
+
}
|
|
390
|
+
const targets = record.targets.map(parseTarget);
|
|
391
|
+
if (
|
|
392
|
+
record.before_sha256 !== targets[0].before_sha256 ||
|
|
393
|
+
record.after_sha256 !== targets[0].after_sha256
|
|
394
|
+
) {
|
|
395
|
+
throw new Error("authority receipt primary hashes do not match targets");
|
|
396
|
+
}
|
|
397
|
+
const observationSeed = v2
|
|
398
|
+
? parseObservationSeed(record.observation_seed)
|
|
399
|
+
: undefined;
|
|
400
|
+
if (
|
|
401
|
+
v2 &&
|
|
402
|
+
(record.observation_generation !== AUTHORITY_OBSERVATION_GENERATION_V2 ||
|
|
403
|
+
observationSeed!.observer_version !== AUTHORITY_OBSERVER_VERSION_V2 ||
|
|
404
|
+
observationSeed!.committed_bytes_sha256 !== targets[0].after_sha256 ||
|
|
405
|
+
observationSeed!.committed_revision !== record.ledger_revision)
|
|
406
|
+
) {
|
|
407
|
+
throw new Error("authority receipt observation seed binding is invalid");
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
contract: record.contract,
|
|
411
|
+
record_id: record.record_id,
|
|
412
|
+
attempt_id: record.attempt_id,
|
|
413
|
+
source_kind: record.source_kind,
|
|
414
|
+
status: record.status,
|
|
415
|
+
state_path_identity: record.state_path_identity,
|
|
416
|
+
targets,
|
|
417
|
+
before_sha256: record.before_sha256 as string | null,
|
|
418
|
+
after_sha256: record.after_sha256,
|
|
419
|
+
ledger_revision: record.ledger_revision,
|
|
420
|
+
source_ref: record.source_ref,
|
|
421
|
+
previous_record_hash: record.previous_record_hash as string | null,
|
|
422
|
+
recorded_at: record.recorded_at,
|
|
423
|
+
...(v2
|
|
424
|
+
? {
|
|
425
|
+
observation_generation: AUTHORITY_OBSERVATION_GENERATION_V2,
|
|
426
|
+
observation_seed: observationSeed,
|
|
427
|
+
}
|
|
428
|
+
: {}),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function withoutRecordId(
|
|
433
|
+
record: AuthorityCommitReceipt,
|
|
434
|
+
): Omit<AuthorityCommitReceipt, "record_id"> {
|
|
435
|
+
const { record_id: _recordId, ...body } = record;
|
|
436
|
+
return body;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function sameAttemptContract(
|
|
440
|
+
prepared: AuthorityCommitReceipt,
|
|
441
|
+
terminal: AuthorityCommitReceipt,
|
|
442
|
+
): boolean {
|
|
443
|
+
return (
|
|
444
|
+
prepared.contract === terminal.contract &&
|
|
445
|
+
prepared.attempt_id === terminal.attempt_id &&
|
|
446
|
+
prepared.source_kind === terminal.source_kind &&
|
|
447
|
+
prepared.state_path_identity === terminal.state_path_identity &&
|
|
448
|
+
stableStringify(prepared.targets) === stableStringify(terminal.targets) &&
|
|
449
|
+
prepared.ledger_revision === terminal.ledger_revision &&
|
|
450
|
+
prepared.source_ref === terminal.source_ref &&
|
|
451
|
+
prepared.observation_generation === terminal.observation_generation &&
|
|
452
|
+
stableStringify(prepared.observation_seed ?? null) ===
|
|
453
|
+
stableStringify(terminal.observation_seed ?? null)
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function readAuthorityCommitReceipts(
|
|
458
|
+
statePath: string,
|
|
459
|
+
): AuthorityCommitReceipt[] {
|
|
460
|
+
const journal = receiptJournalPath(statePath);
|
|
461
|
+
if (!existsSync(journal)) return [];
|
|
462
|
+
const stat = lstatSync(journal);
|
|
463
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
464
|
+
throw new Error("authority receipt journal must be a regular file");
|
|
465
|
+
}
|
|
466
|
+
const content = readFileSync(journal, "utf8");
|
|
467
|
+
if (!content) return [];
|
|
468
|
+
if (!content.endsWith("\n")) {
|
|
469
|
+
throw new Error("authority receipt journal has a partial record");
|
|
470
|
+
}
|
|
471
|
+
const records = content
|
|
472
|
+
.trimEnd()
|
|
473
|
+
.split("\n")
|
|
474
|
+
.map((line, index) => {
|
|
475
|
+
try {
|
|
476
|
+
return parseReceipt(JSON.parse(line));
|
|
477
|
+
} catch (error) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`authority receipt line ${index + 1} is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
const attempts = new Map<
|
|
484
|
+
string,
|
|
485
|
+
{ prepared: AuthorityCommitReceipt; terminal: AuthorityCommitReceipt | null }
|
|
486
|
+
>();
|
|
487
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
488
|
+
const record = records[index];
|
|
489
|
+
const expectedPrevious = index === 0 ? null : records[index - 1].record_id;
|
|
490
|
+
if (record.previous_record_hash !== expectedPrevious) {
|
|
491
|
+
throw new Error(`authority receipt line ${index + 1} chain predecessor mismatch`);
|
|
492
|
+
}
|
|
493
|
+
if (recordHash(withoutRecordId(record)) !== record.record_id) {
|
|
494
|
+
throw new Error(`authority receipt line ${index + 1} record hash mismatch`);
|
|
495
|
+
}
|
|
496
|
+
const current = attempts.get(record.attempt_id);
|
|
497
|
+
if (record.status === "prepared") {
|
|
498
|
+
if (current) throw new Error("authority receipt attempt_id was reused");
|
|
499
|
+
attempts.set(record.attempt_id, { prepared: record, terminal: null });
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (!current || current.terminal) {
|
|
503
|
+
throw new Error("authority receipt terminal record has no unique prepared record");
|
|
504
|
+
}
|
|
505
|
+
if (!sameAttemptContract(current.prepared, record)) {
|
|
506
|
+
throw new Error("authority receipt terminal record changed attempt identity");
|
|
507
|
+
}
|
|
508
|
+
current.terminal = record;
|
|
509
|
+
}
|
|
510
|
+
return records;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function appendReceipt(
|
|
514
|
+
statePath: string,
|
|
515
|
+
body: Omit<AuthorityCommitReceipt, "record_id" | "previous_record_hash">,
|
|
516
|
+
): AuthorityCommitReceipt {
|
|
517
|
+
const records = readAuthorityCommitReceipts(statePath);
|
|
518
|
+
const recordWithoutId: Omit<AuthorityCommitReceipt, "record_id"> = {
|
|
519
|
+
...body,
|
|
520
|
+
previous_record_hash:
|
|
521
|
+
records.length === 0 ? null : records[records.length - 1].record_id,
|
|
522
|
+
};
|
|
523
|
+
beforeAppendForTest?.(recordWithoutId);
|
|
524
|
+
const record: AuthorityCommitReceipt = {
|
|
525
|
+
...recordWithoutId,
|
|
526
|
+
record_id: recordHash(recordWithoutId),
|
|
527
|
+
};
|
|
528
|
+
const journal = receiptJournalPath(statePath);
|
|
529
|
+
mkdirSync(dirname(journal), { recursive: true, mode: 0o700 });
|
|
530
|
+
if (existsSync(journal) && lstatSync(journal).isSymbolicLink()) {
|
|
531
|
+
throw new Error("authority receipt journal must not be a symlink");
|
|
532
|
+
}
|
|
533
|
+
const descriptor = openSync(
|
|
534
|
+
journal,
|
|
535
|
+
constants.O_WRONLY |
|
|
536
|
+
constants.O_CREAT |
|
|
537
|
+
constants.O_APPEND |
|
|
538
|
+
(constants.O_NOFOLLOW ?? 0),
|
|
539
|
+
0o600,
|
|
540
|
+
);
|
|
541
|
+
try {
|
|
542
|
+
writeSync(descriptor, `${JSON.stringify(record)}\n`, undefined, "utf8");
|
|
543
|
+
fsyncSync(descriptor);
|
|
544
|
+
} finally {
|
|
545
|
+
closeSync(descriptor);
|
|
546
|
+
}
|
|
547
|
+
fsyncDirectory(dirname(journal));
|
|
548
|
+
return record;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function pendingPrepared(records: AuthorityCommitReceipt[]): PreparedAuthorityCommit[] {
|
|
552
|
+
const pending = new Map<string, PreparedAuthorityCommit>();
|
|
553
|
+
for (const record of records) {
|
|
554
|
+
if (record.status === "prepared") {
|
|
555
|
+
pending.set(record.attempt_id, record as PreparedAuthorityCommit);
|
|
556
|
+
} else {
|
|
557
|
+
pending.delete(record.attempt_id);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return [...pending.values()];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function prepareAuthorityCommit(
|
|
564
|
+
statePath: string,
|
|
565
|
+
input: PrepareAuthorityCommitInput,
|
|
566
|
+
): PreparedAuthorityCommit {
|
|
567
|
+
const records = readAuthorityCommitReceipts(statePath);
|
|
568
|
+
if (pendingPrepared(records).length > 0) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
"authority receipt has a pending attempt; recover it before another commit",
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
if (!input.targets.length) throw new Error("authority commit requires targets");
|
|
574
|
+
if (!input.ledger_revision.trim() || !input.source_ref.trim()) {
|
|
575
|
+
throw new Error("authority commit revision and source_ref are required");
|
|
576
|
+
}
|
|
577
|
+
const root = projectRootForStatePath(statePath);
|
|
578
|
+
const targets = input.targets.map((target) => ({
|
|
579
|
+
path: targetPath(root, target.absolute_path),
|
|
580
|
+
before_sha256:
|
|
581
|
+
target.before_bytes === null ? null : sha256(target.before_bytes),
|
|
582
|
+
after_sha256: sha256(target.after_bytes),
|
|
583
|
+
}));
|
|
584
|
+
const attemptId = input.attempt_id ?? randomUUID();
|
|
585
|
+
if (!ATTEMPT_ID_RE.test(attemptId)) {
|
|
586
|
+
throw new Error("authority commit attempt_id must be a random UUID v4");
|
|
587
|
+
}
|
|
588
|
+
const wantsV2 =
|
|
589
|
+
input.observation_generation !== undefined ||
|
|
590
|
+
input.observation_seed !== undefined;
|
|
591
|
+
if (
|
|
592
|
+
wantsV2 &&
|
|
593
|
+
(input.observation_generation !== AUTHORITY_OBSERVATION_GENERATION_V2 ||
|
|
594
|
+
input.observation_seed === undefined)
|
|
595
|
+
) {
|
|
596
|
+
throw new Error(
|
|
597
|
+
"authority receipt v2 requires the supported observation generation and seed",
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
if (
|
|
601
|
+
wantsV2 &&
|
|
602
|
+
(input.observation_seed!.committed_bytes_sha256 !== targets[0].after_sha256 ||
|
|
603
|
+
input.observation_seed!.committed_revision !== input.ledger_revision)
|
|
604
|
+
) {
|
|
605
|
+
throw new Error("authority receipt v2 seed does not bind the committed target");
|
|
606
|
+
}
|
|
607
|
+
return appendReceipt(statePath, {
|
|
608
|
+
contract: wantsV2
|
|
609
|
+
? AUTHORITY_COMMIT_RECEIPT_V2_CONTRACT
|
|
610
|
+
: AUTHORITY_COMMIT_RECEIPT_CONTRACT,
|
|
611
|
+
attempt_id: attemptId,
|
|
612
|
+
source_kind: input.source_kind,
|
|
613
|
+
status: "prepared",
|
|
614
|
+
state_path_identity: authorityStatePathIdentity(statePath),
|
|
615
|
+
targets,
|
|
616
|
+
before_sha256: targets[0].before_sha256,
|
|
617
|
+
after_sha256: targets[0].after_sha256,
|
|
618
|
+
ledger_revision: input.ledger_revision,
|
|
619
|
+
source_ref: input.source_ref,
|
|
620
|
+
recorded_at: new Date().toISOString(),
|
|
621
|
+
...(wantsV2
|
|
622
|
+
? {
|
|
623
|
+
observation_generation: AUTHORITY_OBSERVATION_GENERATION_V2,
|
|
624
|
+
observation_seed: input.observation_seed,
|
|
625
|
+
}
|
|
626
|
+
: {}),
|
|
627
|
+
}) as PreparedAuthorityCommit;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export function terminalizeAuthorityCommit(
|
|
631
|
+
statePath: string,
|
|
632
|
+
prepared: PreparedAuthorityCommit,
|
|
633
|
+
status: Exclude<AuthorityCommitReceiptStatus, "prepared">,
|
|
634
|
+
): AuthorityCommitReceipt {
|
|
635
|
+
if (!TERMINAL_STATUSES.has(status)) {
|
|
636
|
+
throw new Error("authority commit terminal status is invalid");
|
|
637
|
+
}
|
|
638
|
+
const records = readAuthorityCommitReceipts(statePath);
|
|
639
|
+
const pending = pendingPrepared(records);
|
|
640
|
+
if (
|
|
641
|
+
pending.length !== 1 ||
|
|
642
|
+
pending[0].attempt_id !== prepared.attempt_id ||
|
|
643
|
+
!sameAttemptContract(pending[0], prepared)
|
|
644
|
+
) {
|
|
645
|
+
throw new Error("authority commit is not the unique pending attempt");
|
|
646
|
+
}
|
|
647
|
+
return appendReceipt(statePath, {
|
|
648
|
+
contract: prepared.contract,
|
|
649
|
+
attempt_id: prepared.attempt_id,
|
|
650
|
+
source_kind: prepared.source_kind,
|
|
651
|
+
status,
|
|
652
|
+
state_path_identity: prepared.state_path_identity,
|
|
653
|
+
targets: prepared.targets,
|
|
654
|
+
before_sha256: prepared.before_sha256,
|
|
655
|
+
after_sha256: prepared.after_sha256,
|
|
656
|
+
ledger_revision: prepared.ledger_revision,
|
|
657
|
+
source_ref: prepared.source_ref,
|
|
658
|
+
recorded_at: new Date().toISOString(),
|
|
659
|
+
...(prepared.contract === AUTHORITY_COMMIT_RECEIPT_V2_CONTRACT
|
|
660
|
+
? {
|
|
661
|
+
observation_generation: prepared.observation_generation,
|
|
662
|
+
observation_seed: prepared.observation_seed,
|
|
663
|
+
}
|
|
664
|
+
: {}),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export function recoverAuthorityCommitReceipts(
|
|
669
|
+
statePath: string,
|
|
670
|
+
): AuthorityCommitReceipt[] {
|
|
671
|
+
const root = projectRootForStatePath(statePath);
|
|
672
|
+
const pending = pendingPrepared(readAuthorityCommitReceipts(statePath));
|
|
673
|
+
const recovered: AuthorityCommitReceipt[] = [];
|
|
674
|
+
for (const prepared of pending) {
|
|
675
|
+
if (prepared.state_path_identity !== authorityStatePathIdentity(statePath)) {
|
|
676
|
+
throw new Error("authority receipt recovery state path identity mismatch");
|
|
677
|
+
}
|
|
678
|
+
const current = prepared.targets.map((target) =>
|
|
679
|
+
currentTargetHash(root, target),
|
|
680
|
+
);
|
|
681
|
+
const matchesBefore = current.every(
|
|
682
|
+
(hash, index) => hash === prepared.targets[index].before_sha256,
|
|
683
|
+
);
|
|
684
|
+
const matchesAfter = current.every(
|
|
685
|
+
(hash, index) => hash === prepared.targets[index].after_sha256,
|
|
686
|
+
);
|
|
687
|
+
if (matchesAfter) {
|
|
688
|
+
recovered.push(
|
|
689
|
+
terminalizeAuthorityCommit(
|
|
690
|
+
statePath,
|
|
691
|
+
prepared,
|
|
692
|
+
"recovered_committed",
|
|
693
|
+
),
|
|
694
|
+
);
|
|
695
|
+
} else if (matchesBefore) {
|
|
696
|
+
recovered.push(
|
|
697
|
+
terminalizeAuthorityCommit(
|
|
698
|
+
statePath,
|
|
699
|
+
prepared,
|
|
700
|
+
"recovered_aborted",
|
|
701
|
+
),
|
|
702
|
+
);
|
|
703
|
+
} else {
|
|
704
|
+
throw new Error(
|
|
705
|
+
`authority receipt recovery is ambiguous for attempt ${prepared.attempt_id}`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return recovered;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export function setBeforeAuthorityReceiptAppendForTest(
|
|
713
|
+
hook: ((record: Omit<AuthorityCommitReceipt, "record_id">) => void) | null,
|
|
714
|
+
): void {
|
|
715
|
+
beforeAppendForTest = hook;
|
|
716
|
+
}
|