K8: record blocked owner review and release evidence

This commit is contained in:
2026-08-21 14:27:23 +08:00
parent 2ef2d82db3
commit b1b5a3cc8a
62 changed files with 171781 additions and 19 deletions

238
web/workspace/migration.mjs Normal file
View File

@@ -0,0 +1,238 @@
import { ReplayTimeline, REPLAY_SCHEMA, canonicalJson, fnv1a64 } from "../worker/replay.mjs";
import { validateCheckpoint } from "./recovery.mjs";
// The package envelope is deliberately separate from the locked runtime schemas.
// Format 2 adds integrity metadata; event, snapshot, checkpoint and journal
// payloads remain byte-for-byte compatible with their existing v1 contracts.
export const PACKAGE_SCHEMA = "workspace-package.v1";
export const ROLLBACK_SCHEMA = "workspace-rollback.v1";
export const OFFLINE_SCHEMA = "offline-cache-package.v1";
export const PACKAGE_VERSIONS = Object.freeze([1, 2]);
function clone(value) {
return value === undefined ? undefined : structuredClone(value);
}
function deterministicHash(value) {
// Keep the envelope usable in a browser Worker without a host crypto module.
return fnv1a64(typeof value === "string" ? value : canonicalJson(value));
}
function requireString(value, name) {
if (typeof value !== "string" || value.length === 0) throw new Error(`${name}_invalid`);
}
function requireVersion(value) {
if (!PACKAGE_VERSIONS.includes(value)) throw new Error("package_version_unsupported");
}
function requireSequence(value, name) {
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${name}_invalid`);
}
function packageUnsigned(value) {
const { package_hash: ignored, ...unsigned } = value;
return unsigned;
}
function contentUnsigned(value) {
return {
context_id: value.context_id,
committed_seq: value.committed_seq,
checkpoint: value.checkpoint ?? null,
journal: value.journal ?? [],
timeline: value.timeline,
};
}
function withHashes(value) {
const result = clone(value);
result.content_hash = deterministicHash(contentUnsigned(result));
result.package_hash = deterministicHash(packageUnsigned(result));
return result;
}
function validateJournal(journal) {
if (!Array.isArray(journal)) throw new Error("package_journal_invalid");
let previous = 0;
for (const record of journal) {
if (!record || typeof record !== "object" || Array.isArray(record)) throw new Error("package_journal_invalid");
if (record.commit_seq !== undefined) {
requireSequence(record.commit_seq, "journal_commit_seq");
if (record.commit_seq < previous) throw new Error("package_journal_order_invalid");
previous = record.commit_seq;
}
}
}
function normalizedJournal(journal) {
if (!Array.isArray(journal) || journal.some((record) => !record || typeof record !== "object" || Array.isArray(record))) throw new Error("package_journal_invalid");
const normalized = clone(journal).sort((left, right) => (left.commit_seq ?? 0) - (right.commit_seq ?? 0) || String(left.kind ?? "").localeCompare(String(right.kind ?? "")) || String(left.operation_id ?? "").localeCompare(String(right.operation_id ?? "")));
validateJournal(normalized);
return normalized;
}
function validateTimeline(timeline) {
if (!timeline || timeline.schema !== REPLAY_SCHEMA) throw new Error("package_timeline_schema");
return new ReplayTimeline(timeline);
}
function validatePackageShape(value, { contextId, expectedVersion } = {}) {
if (!value || value.schema !== PACKAGE_SCHEMA) throw new Error("package_schema_unknown");
requireVersion(value.format_version);
if (expectedVersion !== undefined && value.format_version !== expectedVersion) throw new Error("package_version_mismatch");
requireString(value.context_id, "package_context");
if (contextId !== undefined && value.context_id !== contextId) throw new Error("package_context_mismatch");
requireSequence(value.committed_seq, "package_committed_seq");
validateTimeline(value.timeline);
validateJournal(value.journal ?? []);
const committedJournalSequences = (value.journal ?? []).filter((record) => record.kind === "commit").map((record) => record.commit_seq);
if (committedJournalSequences.length > 0 && !committedJournalSequences.includes(value.committed_seq)) throw new Error("package_committed_seq_unverified");
if (value.checkpoint !== null && value.checkpoint !== undefined) {
validateCheckpoint(value.checkpoint, { contextId: value.context_id });
if (value.checkpoint.event_cursor > value.timeline.events.cursor) throw new Error("package_checkpoint_ahead");
}
if (typeof value.content_hash !== "string" || value.content_hash !== deterministicHash(contentUnsigned(value))) throw new Error("package_content_hash_mismatch");
if (typeof value.package_hash !== "string" || value.package_hash !== deterministicHash(packageUnsigned(value))) throw new Error("package_hash_mismatch");
if (value.format_version === 2) {
if (!value.integrity || value.integrity.timeline_hash !== deterministicHash(value.timeline) || value.integrity.journal_hash !== deterministicHash(value.journal ?? []) || value.integrity.checkpoint_hash !== (value.checkpoint ? deterministicHash(value.checkpoint) : null)) throw new Error("package_integrity_metadata_mismatch");
if (!value.migration || value.migration.from_version !== 1 || value.migration.to_version !== 2 || value.migration.lossless !== true || typeof value.migration.source_package_hash !== "string") throw new Error("package_migration_metadata_invalid");
}
return clone(value);
}
export function createWorkspacePackage({ contextId, timeline, checkpoint = null, journal = [], committedSeq = undefined } = {}) {
requireString(contextId, "package_context");
const normalizedTimeline = timeline instanceof ReplayTimeline ? timeline.export() : clone(timeline);
const imported = validateTimeline(normalizedTimeline);
if (checkpoint !== null) validateCheckpoint(checkpoint, { contextId });
const normalized = normalizedJournal(journal);
const committed_seq = committedSeq === undefined ? imported.events.cursor : committedSeq;
requireSequence(committed_seq, "package_committed_seq");
if (committed_seq > imported.events.cursor) throw new Error("package_committed_seq_ahead");
return withHashes({
schema: PACKAGE_SCHEMA,
format_version: 1,
context_id: contextId,
committed_seq,
checkpoint: clone(checkpoint),
timeline: normalizedTimeline,
journal: normalized,
});
}
export function verifyWorkspacePackage(value, options = {}) {
return validatePackageShape(value, options);
}
export function migrateWorkspacePackage(value, targetVersion) {
const source = validatePackageShape(value);
requireVersion(targetVersion);
if (source.format_version === targetVersion) return clone(source);
if (source.format_version === 1 && targetVersion === 2) {
return withHashes({
...source,
format_version: 2,
migration: {
from_version: 1,
to_version: 2,
source_package_hash: source.package_hash,
lossless: true,
},
integrity: {
timeline_hash: deterministicHash(source.timeline),
journal_hash: deterministicHash(source.journal),
checkpoint_hash: source.checkpoint ? deterministicHash(source.checkpoint) : null,
},
});
}
if (source.format_version === 2 && targetVersion === 1) {
if (!source.migration || source.migration.from_version !== 1 || source.migration.to_version !== 2 || source.migration.lossless !== true) throw new Error("package_downgrade_not_proven");
const downgraded = withHashes({
schema: PACKAGE_SCHEMA,
format_version: 1,
context_id: source.context_id,
committed_seq: source.committed_seq,
checkpoint: clone(source.checkpoint ?? null),
timeline: clone(source.timeline),
journal: clone(source.journal ?? []),
});
if (source.migration.source_package_hash !== downgraded.package_hash) throw new Error("package_downgrade_source_mismatch");
return downgraded;
}
throw new Error("package_migration_unsupported");
}
export function createRollbackPackage({ current, committed } = {}) {
const currentPackage = validatePackageShape(current);
const committedPackage = validatePackageShape(committed, { contextId: currentPackage.context_id });
if (committedPackage.committed_seq > currentPackage.committed_seq) throw new Error("rollback_target_not_committed");
const rollback = {
schema: ROLLBACK_SCHEMA,
format_version: 1,
context_id: currentPackage.context_id,
current_package_hash: currentPackage.package_hash,
target_package_hash: committedPackage.package_hash,
nearest_committed_seq: committedPackage.committed_seq,
target: committedPackage,
};
return { ...rollback, rollback_hash: deterministicHash(rollback) };
}
export function applyRollbackPackage(rollback, { currentPackageHash, failureInjection = null } = {}) {
if (!rollback || rollback.schema !== ROLLBACK_SCHEMA || rollback.format_version !== 1) throw new Error("rollback_schema_unknown");
const { rollback_hash: ignored, ...unsigned } = rollback;
if (typeof rollback.rollback_hash !== "string" || rollback.rollback_hash !== deterministicHash(unsigned)) throw new Error("rollback_hash_mismatch");
if (typeof currentPackageHash !== "string" || currentPackageHash !== rollback.current_package_hash) throw new Error("rollback_current_mismatch");
if (failureInjection === "before_commit") throw new Error("rollback_injected_failure");
const target = validatePackageShape(rollback.target, { contextId: rollback.context_id });
if (target.package_hash !== rollback.target_package_hash) throw new Error("rollback_target_hash_mismatch");
return clone(target);
}
function normalizeResources(resources) {
if (!Array.isArray(resources) || resources.length === 0) throw new Error("offline_resources_empty");
const seen = new Set();
return resources.map((resource) => {
requireString(resource?.path, "offline_resource_path");
if (seen.has(resource.path)) throw new Error("offline_resource_duplicate");
seen.add(resource.path);
if (typeof resource.data !== "string") throw new Error("offline_resource_data_invalid");
return { path: resource.path, bytes: new TextEncoder().encode(resource.data).byteLength, data_hash: deterministicHash(resource.data) };
}).sort((left, right) => left.path.localeCompare(right.path));
}
export function createOfflinePackage({ workspacePackage, resources } = {}) {
const workspace = validatePackageShape(workspacePackage);
const normalized = normalizeResources(resources);
const bundle = {
schema: OFFLINE_SCHEMA,
format_version: 1,
context_id: workspace.context_id,
workspace_package_hash: workspace.package_hash,
resources: normalized,
required_paths: normalized.map((resource) => resource.path),
};
return { ...bundle, bundle_hash: deterministicHash(bundle) };
}
export function verifyOfflinePackage(bundle, { contextId, workspacePackage = null, available = [], online = false } = {}) {
if (!bundle || bundle.schema !== OFFLINE_SCHEMA || bundle.format_version !== 1) throw new Error("offline_schema_unknown");
requireString(bundle.context_id, "offline_context");
if (contextId !== undefined && bundle.context_id !== contextId) throw new Error("offline_context_mismatch");
if (workspacePackage !== null && verifyWorkspacePackage(workspacePackage, { contextId: bundle.context_id }).package_hash !== bundle.workspace_package_hash) throw new Error("offline_workspace_hash_mismatch");
if (!Array.isArray(bundle.resources) || !Array.isArray(bundle.required_paths) || canonicalJson(bundle.required_paths) !== canonicalJson(bundle.resources.map((resource) => resource.path))) throw new Error("offline_manifest_invalid");
if (typeof bundle.bundle_hash !== "string" || bundle.bundle_hash !== deterministicHash({ schema: bundle.schema, format_version: bundle.format_version, context_id: bundle.context_id, workspace_package_hash: bundle.workspace_package_hash, resources: bundle.resources, required_paths: bundle.required_paths })) throw new Error("offline_bundle_hash_mismatch");
if (!Array.isArray(available)) throw new Error("offline_available_invalid");
const availableMap = new Map(available.map((entry) => {
if (typeof entry === "string") return [entry, null];
if (entry && typeof entry.path === "string") return [entry.path, entry.data_hash ?? (typeof entry.data === "string" ? deterministicHash(entry.data) : null)];
throw new Error("offline_available_invalid");
}));
const missing = bundle.resources.filter((resource) => !availableMap.has(resource.path)).map((resource) => resource.path);
const corrupted = bundle.resources.filter((resource) => availableMap.has(resource.path) && availableMap.get(resource.path) !== null && availableMap.get(resource.path) !== resource.data_hash).map((resource) => resource.path);
if ((missing.length > 0 || corrupted.length > 0) && !online) throw new Error(`offline_resource_invalid:${[...missing, ...corrupted].join(",")}`);
return { status: missing.length === 0 && corrupted.length === 0 ? "ready" : "network_required", missing, corrupted, offline: !online };
}
export { canonicalJson, deterministicHash, fnv1a64 };

View File

@@ -0,0 +1,153 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { MemoryStorage, WorkspaceJournal } from "./journal.mjs";
import { createCheckpoint } from "./recovery.mjs";
import { ReplayTimeline, canonicalJson, fnv1a64 } from "../worker/replay.mjs";
import {
applyRollbackPackage,
createOfflinePackage,
createRollbackPackage,
createWorkspacePackage,
migrateWorkspacePackage,
verifyOfflinePackage,
verifyWorkspacePackage,
} from "./migration.mjs";
const contextId = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
function timelineAt(events) {
const timeline = new ReplayTimeline();
timeline.appendEvent(1, 0, "SESSION_READY", {});
timeline.checkpoint(1, 0, { tick: 0, position: 0 });
if (events) {
timeline.appendEvent(2, 4, "SIM_STEP", { cycles: 4 });
timeline.checkpoint(2, 4, { tick: 4, position: 4 }, timeline.snapshots.latest().snapshot_id);
}
return timeline;
}
const storage = new MemoryStorage();
const journal = new WorkspaceJournal(storage, { projectId: "demo" });
journal.prepare("op-1", [{ path: "config/main.ini", data: "[EMC]\n" }]);
journal.commit("op-1");
const journalRecords = journal.records();
journal.prepare("op-2", [{ path: "programs/main.ngc", data: "G0 X1\n" }]);
journal.commit("op-2");
const currentJournalRecords = journal.records();
const committedTimeline = timelineAt(false);
const committedCheckpoint = createCheckpoint({
contextId,
timeline: committedTimeline,
state: { tick: 0, position: 0 },
workspaceHash: fnv1a64(journalRecords),
});
const currentTimeline = timelineAt(true);
const currentCheckpoint = createCheckpoint({
contextId,
timeline: currentTimeline,
state: { tick: 4, position: 4 },
workspaceHash: fnv1a64(currentJournalRecords),
});
const committed = createWorkspacePackage({ contextId, timeline: committedTimeline, checkpoint: committedCheckpoint, journal: journalRecords, committedSeq: 1 });
const current = createWorkspacePackage({ contextId, timeline: currentTimeline, checkpoint: currentCheckpoint, journal: currentJournalRecords, committedSeq: 2 });
assert.equal(verifyWorkspacePackage(committed, { contextId }).package_hash, committed.package_hash);
assert.equal(verifyWorkspacePackage(current, { contextId }).package_hash, current.package_hash);
assert.deepEqual(createWorkspacePackage({ contextId, timeline: currentTimeline, checkpoint: currentCheckpoint, journal: currentJournalRecords, committedSeq: 2 }), current);
const migrated = migrateWorkspacePackage(current, 2);
assert.equal(migrated.format_version, 2);
assert.equal(migrated.migration.lossless, true);
assert.equal(verifyWorkspacePackage(migrated, { contextId, expectedVersion: 2 }).format_version, 2);
const roundTrip = migrateWorkspacePackage(migrated, 1);
assert.deepEqual(roundTrip, current);
assert.throws(() => migrateWorkspacePackage(current, 3), /package_version_unsupported/);
assert.throws(() => verifyWorkspacePackage(current, { contextId: "other-context" }), /package_context_mismatch/);
const tampered = structuredClone(migrated);
tampered.timeline.events.events[1].payload.cycles = 99;
assert.throws(() => verifyWorkspacePackage(tampered), /(?:package_(?:content_)?hash_mismatch|event_payload_hash_mismatch)/);
const rollback = createRollbackPackage({ current, committed });
assert.equal(rollback.nearest_committed_seq, 1);
assert.throws(() => applyRollbackPackage(rollback, { currentPackageHash: current.package_hash, failureInjection: "before_commit" }), /rollback_injected_failure/);
const rolledBack = applyRollbackPackage(rollback, { currentPackageHash: current.package_hash });
assert.deepEqual(rolledBack, committed);
assert.equal(current.package_hash, current.package_hash);
const badRollback = structuredClone(rollback);
badRollback.target_package_hash = "fnv1a64:0000000000000000";
assert.throws(() => applyRollbackPackage(badRollback, { currentPackageHash: current.package_hash }), /rollback_hash_mismatch/);
const offline = createOfflinePackage({
workspacePackage: migrated,
resources: [
{ path: "ui/index.html", data: "<main>LinuxCNC</main>" },
{ path: "wasm/sim.wasm", data: "wasm-bytes" },
],
});
assert.deepEqual(verifyOfflinePackage(offline, { contextId, workspacePackage: migrated, available: [{ path: "ui/index.html", data: "<main>LinuxCNC</main>" }, { path: "wasm/sim.wasm", data: "wasm-bytes" }] }), { status: "ready", missing: [], corrupted: [], offline: true });
assert.throws(() => verifyOfflinePackage(offline, { contextId, available: ["ui/index.html"], online: false }), /offline_resource_invalid/);
assert.deepEqual(verifyOfflinePackage(offline, { contextId, available: ["ui/index.html"], online: true }), { status: "network_required", missing: ["wasm/sim.wasm"], corrupted: [], offline: false });
const report = {
schema: "workspace-migration-report.v1",
context_id: contextId,
status: "passed",
implementation_status: "verified",
format: {
schema: "workspace-package.v1",
supported_versions: [1, 2],
payload_schemas_unchanged: ["event-log.v1", "snapshot.v1", "simulation-checkpoint.v1", "workspace-journal.v1"],
old_package_immutable: true,
},
migration: {
status: "passed",
source_version: current.format_version,
target_version: migrated.format_version,
source_hash: current.package_hash,
migrated_hash: migrated.package_hash,
round_trip_hash: roundTrip.package_hash,
round_trip_equal: roundTrip.package_hash === current.package_hash,
deterministic: migrated.package_hash === migrateWorkspacePackage(current, 2).package_hash,
},
rollback: {
status: "passed",
schema: rollback.schema,
current_hash: current.package_hash,
target_hash: committed.package_hash,
nearest_committed_seq: rollback.nearest_committed_seq,
rollback_hash: rollback.rollback_hash,
failure_injection: "before_commit",
failure_result: "rollback_injected_failure",
recovery_status: "recovered",
recovery_hash: rolledBack.package_hash,
},
offline: {
status: "passed",
schema: offline.schema,
bundle_hash: offline.bundle_hash,
required_paths: offline.required_paths,
offline_start: "ready",
missing_resource_result: "offline_resource_invalid",
online_degraded_result: "network_required",
package_path: "coverage/offline-cache.package.json",
},
rollback_package_path: "coverage/workspace-rollback.package.json",
recovery_endpoint: {
status: "verified",
endpoint: "nearest_committed_package",
state_hash: committedCheckpoint.state_hash,
no_silent_downgrade: true,
},
oracle: {
status: "blocked",
reason: "official migration and browser offline oracle is absent from the locked source",
blocked_samples: ["native/WASM package migration", "browser Service Worker offline startup", "OPFS rollback after crash"],
},
};
report.report_sha256 = createHash("sha256").update(canonicalJson(report), "utf8").digest("hex");
await writeFile(new URL("../../coverage/workspace-rollback.package.json", import.meta.url), `${JSON.stringify(rollback, null, 2)}\n`);
await writeFile(new URL("../../coverage/offline-cache.package.json", import.meta.url), `${JSON.stringify(offline, null, 2)}\n`);
await writeFile(new URL("../../coverage/workspace-migration.report.json", import.meta.url), `${JSON.stringify(report, null, 2)}\n`);
console.log(`migration=passed source=${current.package_hash} target=${migrated.package_hash} rollback=${rollback.rollback_hash}`);