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,395 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
|
+
import { stableStringify } from "../canonical_json";
|
|
4
|
+
import { inspectLegacyAggregate, mapLegacyState } from "./legacy";
|
|
5
|
+
import {
|
|
6
|
+
appendObservationJournalEntry,
|
|
7
|
+
type JournalEntry,
|
|
8
|
+
} from "./storage";
|
|
9
|
+
import {
|
|
10
|
+
appendAutomaticObservationV2,
|
|
11
|
+
readAutomaticObservationsV2,
|
|
12
|
+
type V3AuthorityObservationV2,
|
|
13
|
+
} from "./automatic_observations";
|
|
14
|
+
import {
|
|
15
|
+
AUTHORITY_OBSERVATION_GENERATION_V2,
|
|
16
|
+
AUTHORITY_OBSERVER_VERSION_V2,
|
|
17
|
+
readAuthorityCommitReceipts,
|
|
18
|
+
type AuthorityCommitReceipt,
|
|
19
|
+
type AuthorityObservationSeedV2,
|
|
20
|
+
} from "../authority_commit_receipts";
|
|
21
|
+
import type {
|
|
22
|
+
V3AuthorityObservation,
|
|
23
|
+
V3CommitSourceEvent,
|
|
24
|
+
} from "./types";
|
|
25
|
+
|
|
26
|
+
export const V3_OBSERVER_VERSION = "assurance-kernel-p2a-observer/v1";
|
|
27
|
+
|
|
28
|
+
export interface CommittedLedgerReceipt {
|
|
29
|
+
attempt_id: string;
|
|
30
|
+
source_kind: "state_mutation" | "project_migration";
|
|
31
|
+
source_ref: string;
|
|
32
|
+
receipt_status:
|
|
33
|
+
| "committed"
|
|
34
|
+
| "prepared"
|
|
35
|
+
| "recovered_committed";
|
|
36
|
+
previous_state: Record<string, unknown>;
|
|
37
|
+
proposed_state: Record<string, unknown>;
|
|
38
|
+
committed_state: Record<string, unknown>;
|
|
39
|
+
committed_bytes: string;
|
|
40
|
+
ledger_revision: string;
|
|
41
|
+
committed_at: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ObservationAppender = (
|
|
45
|
+
root: string,
|
|
46
|
+
entry: JournalEntry & { observation: V3AuthorityObservation },
|
|
47
|
+
) => "appended" | "duplicate";
|
|
48
|
+
|
|
49
|
+
type ObservationV2Appender = (
|
|
50
|
+
root: string,
|
|
51
|
+
observation: V3AuthorityObservationV2,
|
|
52
|
+
) => "appended" | "duplicate";
|
|
53
|
+
|
|
54
|
+
let observationAppenderForTest: ObservationAppender | null = null;
|
|
55
|
+
let observationV2AppenderForTest: ObservationV2Appender | null = null;
|
|
56
|
+
|
|
57
|
+
/** Test-only fault seam; production always appends through secure kernel storage. */
|
|
58
|
+
export function setObservationAppenderForTest(
|
|
59
|
+
appender: ObservationAppender | null,
|
|
60
|
+
): void {
|
|
61
|
+
observationAppenderForTest = appender;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Test-only fault seam for the additive automatic-observation v2 journal. */
|
|
65
|
+
export function setObservationV2AppenderForTest(
|
|
66
|
+
appender: ObservationV2Appender | null,
|
|
67
|
+
): void {
|
|
68
|
+
observationV2AppenderForTest = appender;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
72
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
73
|
+
? (value as Record<string, unknown>)
|
|
74
|
+
: {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function stringOrNull(value: unknown): string | null {
|
|
78
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function digest(domain: string, value: string): string {
|
|
82
|
+
return `sha256:${createHash("sha256").update(`${domain}\0${value}`).digest("hex")}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function projectRootForLedgerPath(statePath: string): string | null {
|
|
86
|
+
if (basename(statePath) !== "current_iteration.json") return null;
|
|
87
|
+
const memoryDirectory = dirname(statePath);
|
|
88
|
+
if (basename(memoryDirectory) !== "memory") return null;
|
|
89
|
+
const immDirectory = dirname(memoryDirectory);
|
|
90
|
+
if (basename(immDirectory) !== ".imm") return null;
|
|
91
|
+
return resolve(dirname(immDirectory));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function newHistoryEvents(
|
|
95
|
+
previous: Record<string, unknown>,
|
|
96
|
+
proposed: Record<string, unknown>,
|
|
97
|
+
receipt: Pick<
|
|
98
|
+
CommittedLedgerReceipt,
|
|
99
|
+
| "attempt_id"
|
|
100
|
+
| "source_kind"
|
|
101
|
+
| "source_ref"
|
|
102
|
+
| "receipt_status"
|
|
103
|
+
| "committed_at"
|
|
104
|
+
>,
|
|
105
|
+
): V3CommitSourceEvent[] {
|
|
106
|
+
const before = Array.isArray(previous.history) ? previous.history : [];
|
|
107
|
+
const after = Array.isArray(proposed.history) ? proposed.history : [];
|
|
108
|
+
if (after.length < before.length)
|
|
109
|
+
throw new Error("authority commit removed history events");
|
|
110
|
+
for (let index = 0; index < before.length; index += 1) {
|
|
111
|
+
if (stableStringify(before[index]) !== stableStringify(after[index]))
|
|
112
|
+
throw new Error("authority commit rewrote existing history");
|
|
113
|
+
}
|
|
114
|
+
const appended = after.slice(before.length).map((raw) => {
|
|
115
|
+
const entry = asRecord(raw);
|
|
116
|
+
const canonical = stableStringify(entry);
|
|
117
|
+
return {
|
|
118
|
+
id: digest("assurance-kernel-v3-history-event/v1", canonical),
|
|
119
|
+
action: stringOrNull(entry.action) ?? "unknown",
|
|
120
|
+
at: stringOrNull(entry.at),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
if (appended.length > 0) return appended;
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
id: receipt.attempt_id,
|
|
127
|
+
action:
|
|
128
|
+
receipt.source_kind === "project_migration"
|
|
129
|
+
? `project_migration:${receipt.source_ref}`
|
|
130
|
+
: receipt.receipt_status === "recovered_committed"
|
|
131
|
+
? `recovered_state_mutation:${receipt.source_ref}`
|
|
132
|
+
: `state_mutation:${receipt.source_ref}`,
|
|
133
|
+
at: receipt.committed_at,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function divergenceFields(state: Record<string, unknown>): string[] {
|
|
139
|
+
const aggregate = inspectLegacyAggregate(state);
|
|
140
|
+
const fields: string[] = [];
|
|
141
|
+
if (aggregate.replan_mismatch || aggregate.replan_type_invalid)
|
|
142
|
+
fields.push("requires_replan");
|
|
143
|
+
if (aggregate.active_step_mismatch) fields.push("active_step");
|
|
144
|
+
if (aggregate.ownership_conflict) fields.push("ownership");
|
|
145
|
+
if (aggregate.follow_up_malformed || aggregate.follow_up_state === "closed")
|
|
146
|
+
fields.push("pending_follow_up");
|
|
147
|
+
if (aggregate.steps_malformed) fields.push("steps");
|
|
148
|
+
if (["invalid", "conflict"].includes(aggregate.terminal))
|
|
149
|
+
fields.push("plan_terminal");
|
|
150
|
+
const nextAction = asRecord(state.next_action);
|
|
151
|
+
if (
|
|
152
|
+
aggregate.replan_signal &&
|
|
153
|
+
(nextAction.action === "activate" ||
|
|
154
|
+
(typeof nextAction.command === "string" &&
|
|
155
|
+
nextAction.command.includes("imm-work activate")))
|
|
156
|
+
)
|
|
157
|
+
fields.push("next_action");
|
|
158
|
+
return fields;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function legacyTaskId(state: Record<string, unknown>): string {
|
|
162
|
+
const identity = [state.plan_path, state.plan_signature]
|
|
163
|
+
.map((value) => (typeof value === "string" ? value : ""))
|
|
164
|
+
.join("\0");
|
|
165
|
+
return `legacy-${createHash("sha256").update(identity).digest("hex").slice(0, 16)}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function buildV3AuthorityObservation(
|
|
169
|
+
receipt: CommittedLedgerReceipt,
|
|
170
|
+
): V3AuthorityObservation {
|
|
171
|
+
const sourceEvents = newHistoryEvents(
|
|
172
|
+
receipt.previous_state,
|
|
173
|
+
receipt.proposed_state,
|
|
174
|
+
receipt,
|
|
175
|
+
);
|
|
176
|
+
const shadow = mapLegacyState(receipt.committed_state);
|
|
177
|
+
const fields = divergenceFields(receipt.committed_state);
|
|
178
|
+
const core = {
|
|
179
|
+
contract: "assurance_kernel/v3_authority_observation/v1" as const,
|
|
180
|
+
observer_version: V3_OBSERVER_VERSION,
|
|
181
|
+
commit_id: receipt.attempt_id,
|
|
182
|
+
ledger_revision: receipt.ledger_revision,
|
|
183
|
+
committed_at: receipt.committed_at,
|
|
184
|
+
plan_path: stringOrNull(receipt.committed_state.plan_path),
|
|
185
|
+
plan_signature: stringOrNull(receipt.committed_state.plan_signature),
|
|
186
|
+
source_events: sourceEvents,
|
|
187
|
+
shadow,
|
|
188
|
+
divergence: { detected: fields.length > 0, fields },
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
...core,
|
|
192
|
+
observation_id: digest(
|
|
193
|
+
"assurance-kernel-v3-observation/v1",
|
|
194
|
+
stableStringify(core),
|
|
195
|
+
),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function buildAuthorityObservationSeedV2(
|
|
200
|
+
statePathIdentity: string,
|
|
201
|
+
receipt: CommittedLedgerReceipt,
|
|
202
|
+
): AuthorityObservationSeedV2 {
|
|
203
|
+
const sourceEvents = newHistoryEvents(
|
|
204
|
+
receipt.previous_state,
|
|
205
|
+
receipt.proposed_state,
|
|
206
|
+
receipt,
|
|
207
|
+
);
|
|
208
|
+
const shadow = mapLegacyState(receipt.committed_state);
|
|
209
|
+
const fields = divergenceFields(receipt.committed_state);
|
|
210
|
+
return {
|
|
211
|
+
contract: "assurance_kernel/authority_observation_seed/v2",
|
|
212
|
+
observer_version: AUTHORITY_OBSERVER_VERSION_V2,
|
|
213
|
+
source_kind: receipt.source_kind,
|
|
214
|
+
source_ref: receipt.source_ref,
|
|
215
|
+
state_path_identity: statePathIdentity,
|
|
216
|
+
committed_bytes_sha256: `sha256:${createHash("sha256")
|
|
217
|
+
.update(receipt.committed_bytes)
|
|
218
|
+
.digest("hex")}`,
|
|
219
|
+
committed_revision: receipt.ledger_revision,
|
|
220
|
+
plan_path: stringOrNull(receipt.committed_state.plan_path),
|
|
221
|
+
plan_signature: stringOrNull(receipt.committed_state.plan_signature),
|
|
222
|
+
source_events: sourceEvents,
|
|
223
|
+
shadow,
|
|
224
|
+
divergence: { detected: fields.length > 0, fields },
|
|
225
|
+
committed_at: receipt.committed_at,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function committedReceiptV2(
|
|
230
|
+
receipt: AuthorityCommitReceipt,
|
|
231
|
+
): receipt is AuthorityCommitReceipt & {
|
|
232
|
+
status: "committed" | "recovered_committed";
|
|
233
|
+
observation_seed: AuthorityObservationSeedV2;
|
|
234
|
+
} {
|
|
235
|
+
return (
|
|
236
|
+
(receipt.status === "committed" ||
|
|
237
|
+
receipt.status === "recovered_committed") &&
|
|
238
|
+
receipt.observation_seed !== undefined
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function buildAutomaticObservationV2(
|
|
243
|
+
receipt: AuthorityCommitReceipt,
|
|
244
|
+
): V3AuthorityObservationV2 {
|
|
245
|
+
if (!committedReceiptV2(receipt))
|
|
246
|
+
throw new Error("automatic observation requires a committed receipt-v2 seed");
|
|
247
|
+
const seed = receipt.observation_seed;
|
|
248
|
+
const core = {
|
|
249
|
+
contract: "assurance_kernel/v3_authority_observation/v2" as const,
|
|
250
|
+
receipt_record_id: receipt.record_id,
|
|
251
|
+
receipt_attempt_id: receipt.attempt_id,
|
|
252
|
+
receipt_protocol: receipt.contract,
|
|
253
|
+
receipt_status: receipt.status,
|
|
254
|
+
source_kind: seed.source_kind,
|
|
255
|
+
source_ref: seed.source_ref,
|
|
256
|
+
state_path_identity: seed.state_path_identity,
|
|
257
|
+
committed_bytes_sha256: seed.committed_bytes_sha256,
|
|
258
|
+
ledger_revision: seed.committed_revision,
|
|
259
|
+
plan_path: seed.plan_path,
|
|
260
|
+
plan_signature: seed.plan_signature,
|
|
261
|
+
source_events: seed.source_events,
|
|
262
|
+
shadow: seed.shadow,
|
|
263
|
+
divergence: seed.divergence,
|
|
264
|
+
observer_generation: AUTHORITY_OBSERVATION_GENERATION_V2,
|
|
265
|
+
observer_version: seed.observer_version,
|
|
266
|
+
committed_at: seed.committed_at,
|
|
267
|
+
observed_at: receipt.recorded_at,
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
...core,
|
|
271
|
+
observation_id: digest(
|
|
272
|
+
"assurance-kernel-v3-observation/v2",
|
|
273
|
+
stableStringify(core),
|
|
274
|
+
),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function setAutomaticObservationV2AppenderForTest(
|
|
279
|
+
appender:
|
|
280
|
+
| ((
|
|
281
|
+
root: string,
|
|
282
|
+
observation: V3AuthorityObservationV2,
|
|
283
|
+
) => "appended" | "duplicate")
|
|
284
|
+
| null,
|
|
285
|
+
): void {
|
|
286
|
+
observationV2AppenderForTest = appender;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function observeTerminalReceiptV2(
|
|
290
|
+
statePath: string,
|
|
291
|
+
receipt: AuthorityCommitReceipt,
|
|
292
|
+
): "appended" | "duplicate" | "not_production_ledger" {
|
|
293
|
+
const root = projectRootForLedgerPath(statePath);
|
|
294
|
+
if (!root) return "not_production_ledger";
|
|
295
|
+
return (observationV2AppenderForTest ?? appendAutomaticObservationV2)(
|
|
296
|
+
root,
|
|
297
|
+
buildAutomaticObservationV2(receipt),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function replayMissingAutomaticObservationsV2(statePath: string): number {
|
|
302
|
+
const root = projectRootForLedgerPath(statePath);
|
|
303
|
+
if (!root) return 0;
|
|
304
|
+
const receipts = readAuthorityCommitReceipts(statePath);
|
|
305
|
+
const existing = new Set(
|
|
306
|
+
readAutomaticObservationsV2(root).map((entry) => entry.receipt_record_id),
|
|
307
|
+
);
|
|
308
|
+
let appended = 0;
|
|
309
|
+
for (const receipt of receipts) {
|
|
310
|
+
if (receipt.contract !== "assurance_kernel/authority_commit_receipt/v2")
|
|
311
|
+
continue;
|
|
312
|
+
if (!committedReceiptV2(receipt) || existing.has(receipt.record_id)) continue;
|
|
313
|
+
observeTerminalReceiptV2(statePath, receipt);
|
|
314
|
+
existing.add(receipt.record_id);
|
|
315
|
+
appended += 1;
|
|
316
|
+
}
|
|
317
|
+
return appended;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function observeTerminalReceiptV2BestEffort(
|
|
321
|
+
statePath: string,
|
|
322
|
+
receipt: AuthorityCommitReceipt,
|
|
323
|
+
): void {
|
|
324
|
+
try {
|
|
325
|
+
observeTerminalReceiptV2(statePath, receipt);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error(
|
|
328
|
+
`warning: authority receipt v2 persisted but automatic observation failed: ${
|
|
329
|
+
error instanceof Error ? error.message : error
|
|
330
|
+
}`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function replayMissingAutomaticObservationsV2BestEffort(
|
|
336
|
+
statePath: string,
|
|
337
|
+
): void {
|
|
338
|
+
try {
|
|
339
|
+
replayMissingAutomaticObservationsV2(statePath);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error(
|
|
342
|
+
`warning: automatic observation recovery failed: ${
|
|
343
|
+
error instanceof Error ? error.message : error
|
|
344
|
+
}`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function observeCommittedLedger(
|
|
350
|
+
statePath: string,
|
|
351
|
+
receipt: CommittedLedgerReceipt,
|
|
352
|
+
): "appended" | "duplicate" | "not_production_ledger" {
|
|
353
|
+
const root = projectRootForLedgerPath(statePath);
|
|
354
|
+
if (!root) return "not_production_ledger";
|
|
355
|
+
const observation = buildV3AuthorityObservation(receipt);
|
|
356
|
+
const entry: JournalEntry & { observation: V3AuthorityObservation } = {
|
|
357
|
+
contract: "assurance_kernel/journal/v1",
|
|
358
|
+
timestamp: new Date().toISOString(),
|
|
359
|
+
task_id: legacyTaskId(receipt.committed_state),
|
|
360
|
+
command: "observe_v3_authority_commit",
|
|
361
|
+
entry_phase: observation.shadow.phase,
|
|
362
|
+
result:
|
|
363
|
+
observation.shadow.ambiguous || observation.divergence.detected
|
|
364
|
+
? "escalated"
|
|
365
|
+
: "ok",
|
|
366
|
+
reason_code: observation.divergence.detected
|
|
367
|
+
? "shadow_divergence"
|
|
368
|
+
: "authority_commit_observed",
|
|
369
|
+
recovery_hint: observation.divergence.detected
|
|
370
|
+
? "Inspect the committed v3 authority fields; the observer never repairs them."
|
|
371
|
+
: null,
|
|
372
|
+
planner_reentry: false,
|
|
373
|
+
user_intervention: false,
|
|
374
|
+
observation,
|
|
375
|
+
};
|
|
376
|
+
return (observationAppenderForTest ?? appendObservationJournalEntry)(
|
|
377
|
+
root,
|
|
378
|
+
entry,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function observeCommittedLedgerBestEffort(
|
|
383
|
+
statePath: string,
|
|
384
|
+
receipt: CommittedLedgerReceipt,
|
|
385
|
+
): void {
|
|
386
|
+
try {
|
|
387
|
+
observeCommittedLedger(statePath, receipt);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error(
|
|
390
|
+
`warning: v3 authority commit was persisted but shadow observation failed: ${
|
|
391
|
+
error instanceof Error ? error.message : error
|
|
392
|
+
}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// P2B1 U1: read-only canary preparation. NOT exported from kernel/index.ts.
|
|
2
|
+
// Aggregates every authoritative owner into one immutable preview + digest.
|
|
3
|
+
// Contains NO confirm callback, authority issuer, capability, rehearsal,
|
|
4
|
+
// enrollment call, or writer. Direct import cannot authorize or mutate.
|
|
5
|
+
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { readBackendClaim, readTaskTombstone } from "./backend_claim";
|
|
9
|
+
import { readTaskIntent } from "./intent";
|
|
10
|
+
import {
|
|
11
|
+
readTaskRecordRaw,
|
|
12
|
+
readWorkspaceStateRaw,
|
|
13
|
+
} from "./storage";
|
|
14
|
+
|
|
15
|
+
const SOURCE_PATH = ".imm/state/workspace.json";
|
|
16
|
+
|
|
17
|
+
export interface PiCanaryPrepareInput {
|
|
18
|
+
task_id: string;
|
|
19
|
+
now: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PiCanaryPreparation {
|
|
23
|
+
contract: "assurance_kernel/pi_canary_preparation/v1";
|
|
24
|
+
task_id: string;
|
|
25
|
+
generated_at: string;
|
|
26
|
+
root_state_path: string;
|
|
27
|
+
intent: {
|
|
28
|
+
path: string;
|
|
29
|
+
revision: number;
|
|
30
|
+
content_hash: string;
|
|
31
|
+
} | null;
|
|
32
|
+
backend_claim: {
|
|
33
|
+
present: boolean;
|
|
34
|
+
task_id: string | null;
|
|
35
|
+
lifecycle_status: string | null;
|
|
36
|
+
};
|
|
37
|
+
task_tombstone: {
|
|
38
|
+
present: boolean;
|
|
39
|
+
terminal_lifecycle: string | null;
|
|
40
|
+
};
|
|
41
|
+
task_record_v3: {
|
|
42
|
+
present: boolean;
|
|
43
|
+
lifecycle: string | null;
|
|
44
|
+
artifact_state: string | null;
|
|
45
|
+
} | null;
|
|
46
|
+
workspace: {
|
|
47
|
+
current_working: string | null;
|
|
48
|
+
};
|
|
49
|
+
digest: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sha256Hex(bytes: string | Buffer): string {
|
|
53
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function stableStringify(value: unknown): string {
|
|
57
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
58
|
+
if (Array.isArray(value)) return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
59
|
+
const record = value as Record<string, unknown>;
|
|
60
|
+
return `{${Object.keys(record)
|
|
61
|
+
.sort()
|
|
62
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
63
|
+
.join(",")}}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build the immutable preparation identity for one exact canary task.
|
|
68
|
+
* Read-only: no writes, no capability, no confirmation callback.
|
|
69
|
+
*/
|
|
70
|
+
export function preparePiCanary(root: string, input: PiCanaryPrepareInput): PiCanaryPreparation {
|
|
71
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(input.task_id))
|
|
72
|
+
throw new Error("task id must match [A-Za-z0-9][A-Za-z0-9._-]{0,127}");
|
|
73
|
+
const canonicalRoot = resolve(root);
|
|
74
|
+
const statePath = resolve(canonicalRoot, SOURCE_PATH);
|
|
75
|
+
|
|
76
|
+
let intent: PiCanaryPreparation["intent"] = null;
|
|
77
|
+
try {
|
|
78
|
+
const read = readTaskIntent(canonicalRoot, input.task_id);
|
|
79
|
+
intent = {
|
|
80
|
+
path: read.intent_ref.path,
|
|
81
|
+
revision: read.intent_ref.revision,
|
|
82
|
+
content_hash: read.content_hash,
|
|
83
|
+
};
|
|
84
|
+
} catch {
|
|
85
|
+
intent = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fail-closed owner reads: malformed, unreadable, symlinked, or
|
|
89
|
+
// contradictory claim/record/workspace/tombstone state is a structured
|
|
90
|
+
// rejection and is never normalized to absence. Only ENOENT means absent.
|
|
91
|
+
const claim = readBackendClaim(canonicalRoot);
|
|
92
|
+
const backend: PiCanaryPreparation["backend_claim"] = claim
|
|
93
|
+
? {
|
|
94
|
+
present: true,
|
|
95
|
+
task_id: claim.task_id,
|
|
96
|
+
lifecycle_status: claim.lifecycle_status,
|
|
97
|
+
}
|
|
98
|
+
: { present: false, task_id: null, lifecycle_status: null };
|
|
99
|
+
if (claim && claim.task_id !== input.task_id)
|
|
100
|
+
throw new Error(
|
|
101
|
+
`backend claim belongs to task ${claim.task_id}, not ${input.task_id}`,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const tombstone = readTaskTombstone(canonicalRoot, input.task_id);
|
|
105
|
+
const taskTombstone: PiCanaryPreparation["task_tombstone"] = tombstone
|
|
106
|
+
? {
|
|
107
|
+
present: true,
|
|
108
|
+
terminal_lifecycle: tombstone.terminal_lifecycle,
|
|
109
|
+
}
|
|
110
|
+
: { present: false, terminal_lifecycle: null };
|
|
111
|
+
if (tombstone && claim)
|
|
112
|
+
throw new Error(
|
|
113
|
+
`task ${input.task_id} has both an active backend claim and a terminal tombstone`,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const current = readTaskRecordRaw(canonicalRoot, input.task_id);
|
|
117
|
+
const record: PiCanaryPreparation["task_record_v3"] = current.record
|
|
118
|
+
? { present: true, lifecycle: current.record.lifecycle, artifact_state: current.record.artifact_state }
|
|
119
|
+
: { present: false, lifecycle: null, artifact_state: null };
|
|
120
|
+
if (claim && !current.record)
|
|
121
|
+
throw new Error(
|
|
122
|
+
`backend claim exists for task ${input.task_id} but its TaskRecord v3 is absent`,
|
|
123
|
+
);
|
|
124
|
+
if (claim && !intent)
|
|
125
|
+
throw new Error(
|
|
126
|
+
`backend claim exists for task ${input.task_id} but its intent sidecar is unreadable`,
|
|
127
|
+
);
|
|
128
|
+
if (current.record && current.record.task_id !== input.task_id)
|
|
129
|
+
throw new Error(
|
|
130
|
+
`task record identity is inconsistent for ${input.task_id}`,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const state = readWorkspaceStateRaw(canonicalRoot);
|
|
134
|
+
const workspace: PiCanaryPreparation["workspace"] = {
|
|
135
|
+
current_working: state.state.current_working,
|
|
136
|
+
};
|
|
137
|
+
if (claim && state.state.current_working !== claim.task_id)
|
|
138
|
+
throw new Error(
|
|
139
|
+
`workspace owner ${state.state.current_working} contradicts backend claim task ${claim.task_id}`,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const preparation: PiCanaryPreparation = {
|
|
143
|
+
contract: "assurance_kernel/pi_canary_preparation/v1",
|
|
144
|
+
task_id: input.task_id,
|
|
145
|
+
generated_at: input.now,
|
|
146
|
+
root_state_path: statePath,
|
|
147
|
+
intent,
|
|
148
|
+
backend_claim: backend,
|
|
149
|
+
task_tombstone: taskTombstone,
|
|
150
|
+
task_record_v3: record,
|
|
151
|
+
workspace,
|
|
152
|
+
digest: "",
|
|
153
|
+
};
|
|
154
|
+
preparation.digest = `sha256:${sha256Hex(stableStringify(preparation))}`;
|
|
155
|
+
return preparation;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Revalidate an immutable preparation: recompute from live owners and assert
|
|
160
|
+
* the digest is unchanged. Read-only.
|
|
161
|
+
*/
|
|
162
|
+
export function revalidatePiCanary(
|
|
163
|
+
root: string,
|
|
164
|
+
input: PiCanaryPrepareInput,
|
|
165
|
+
previous: PiCanaryPreparation,
|
|
166
|
+
): { unchanged: boolean; current: PiCanaryPreparation } {
|
|
167
|
+
const current = preparePiCanary(root, input);
|
|
168
|
+
return { unchanged: current.digest === previous.digest, current };
|
|
169
|
+
}
|