From 5a927638c74da92f977d125a1647e36bd5164841 Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 03:46:03 +0200 Subject: [PATCH 01/14] test(rfc64): define selective coverage evidence contract --- devnet/rfc64-m1-selective-coverage/README.md | 44 + .../rfc64-m1-selective-coverage/manifest.ts | 267 ++++++ .../rfc64-m1-selective-coverage/package.json | 11 + .../rfc64-m1-selective-coverage/tsconfig.json | 14 + .../verifier.test.ts | 594 ++++++++++++ .../rfc64-m1-selective-coverage/verifier.ts | 854 ++++++++++++++++++ package.json | 2 + pnpm-lock.yaml | 2 + pnpm-workspace.yaml | 1 + 9 files changed, 1789 insertions(+) create mode 100644 devnet/rfc64-m1-selective-coverage/README.md create mode 100644 devnet/rfc64-m1-selective-coverage/manifest.ts create mode 100644 devnet/rfc64-m1-selective-coverage/package.json create mode 100644 devnet/rfc64-m1-selective-coverage/tsconfig.json create mode 100644 devnet/rfc64-m1-selective-coverage/verifier.test.ts create mode 100644 devnet/rfc64-m1-selective-coverage/verifier.ts diff --git a/devnet/rfc64-m1-selective-coverage/README.md b/devnet/rfc64-m1-selective-coverage/README.md new file mode 100644 index 0000000000..67007a5ca1 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/README.md @@ -0,0 +1,44 @@ +# RFC-64 M1 selective coverage evidence + +This directory defines the closed, deterministic evidence boundary for the M1 +three-process devnet harness. The process launcher is intentionally separate; +it will populate this contract from real Publisher, Edge, and Core processes. + +The verifier passes only when all of these user-visible outcomes are proven: + +- the corpus digest, network, exact Git/runtime manifests, and three distinct + process peer IDs match a trust anchor supplied outside the evidence artifact; +- Publisher-owned source snapshots match the anchored corpus, so a receiver + cannot redefine the VM or SWM state it is expected to receive; +- an Edge has no VM or SWM payload before the user selects a graph; +- an on-demand selection receives an exact point-in-time VM and SWM snapshot + but does not advance after restart without another request; +- an always-on selection receives the first exact snapshot and advances to the + second exact snapshot after restart; +- unselected public and private graphs remain payload-free on the Edge; +- automatic Core rounds never exceed the configured batch and never include a + private graph; +- every public graph is eventually scheduled and converges to exact final VM + and SWM heads, inventory digests, asset counts, and payload triple counts. + +Edge results are bound to runtime subscription modes and distinct operation job +IDs whose completion records carry the exact resulting snapshot. After restart, +always-on work must come from the reconciler; on-demand payload remains at its +first snapshot but its process-local mode is absent until a second explicit user +request reactivates and advances it. Core results are +bound to scheduler-issued automatic jobs with an empty explicit selection list +and exact final-wave per-graph completion records. Every public graph must first +appear within the anchored coverage-round limit. Manual catch-up cannot be +relabeled as automatic evidence. + +Metadata is allowed to exist for an excluded graph because chain and discovery +metadata are not corpus payload. Metadata-only responses can never satisfy a +required plane: `reportedComplete` is treated as an assertion, and the verifier +independently requires exact nonzero data, counts, heads, and inventory roots. + +Run the bounded contract checks with: + +```sh +pnpm test:m1:rfc64-selective-coverage:unit +pnpm typecheck:m1:rfc64-selective-coverage +``` diff --git a/devnet/rfc64-m1-selective-coverage/manifest.ts b/devnet/rfc64-m1-selective-coverage/manifest.ts new file mode 100644 index 0000000000..122c3c0f7d --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/manifest.ts @@ -0,0 +1,267 @@ +import { createHash } from 'node:crypto'; + +export const SELECTIVE_COVERAGE_CORPUS_SCHEMA = + 'dkg-rfc64-m1-selective-coverage-corpus-v1' as const; +export const SELECTIVE_COVERAGE_EVIDENCE_SCHEMA = + 'dkg-rfc64-m1-selective-coverage-evidence-v1' as const; +export const SELECTIVE_COVERAGE_VERDICT_SCHEMA = + 'dkg-rfc64-m1-selective-coverage-verdict-v1' as const; + +export const MAX_SELECTIVE_COVERAGE_GRAPHS = 64; +export const MAX_SELECTIVE_COVERAGE_ROUNDS = 256; + +export type EdgeCoveragePolicy = 'always-on' | 'on-demand' | 'unselected'; + +export interface PlaneExpectationV1 { + readonly headDigest: string; + readonly inventoryDigest: string; + readonly assetCount: number; + readonly dataTripleCount: number; +} + +export interface GraphSnapshotExpectationV1 { + readonly vm: PlaneExpectationV1; + readonly swm: PlaneExpectationV1; +} + +export interface SelectiveCoverageGraphV1 { + readonly contextGraphId: string; + readonly accessPolicy: 0 | 1; + readonly publishPolicy: 0 | 1; + readonly edgePolicy: EdgeCoveragePolicy; + /** Snapshot present when the Edge selection is first exercised. */ + readonly selectedSnapshot: GraphSnapshotExpectationV1; + /** Snapshot after a second publication wave and an Edge restart. */ + readonly finalSnapshot: GraphSnapshotExpectationV1; +} + +export interface SelectiveCoverageCorpusV1 { + readonly schema: typeof SELECTIVE_COVERAGE_CORPUS_SCHEMA; + readonly networkId: string; + readonly coreAutomaticBatchSize: number; + /** Maximum automatic rounds allowed before every public graph is first admitted. */ + readonly coreCoverageRoundLimit: number; + /** Lexicographically ordered by contextGraphId. */ + readonly graphs: readonly SelectiveCoverageGraphV1[]; + /** SHA-256 of the closed corpus payload, excluding this field. */ + readonly manifestDigest: string; +} + +export interface PlaneObservationV1 { + readonly reportedComplete: boolean; + readonly headDigest: string | null; + readonly inventoryDigest: string | null; + readonly assetCount: number; + readonly metadataTripleCount: number; + readonly dataTripleCount: number; +} + +export interface GraphObservationV1 { + readonly contextGraphId: string; + readonly vm: PlaneObservationV1; + readonly swm: PlaneObservationV1; +} + +export interface EdgeGraphObservationV1 extends GraphObservationV1 { + /** Mode read from the running Edge, not copied from the corpus manifest. */ + readonly runtimeSyncMode: 'always-on' | 'on-demand' | null; + /** Operation that produced the observed payload; null when payload is absent. */ + readonly producingJobId: string | null; +} + +export interface EdgeSyncOperationV1 { + readonly sequence: number; + readonly phase: 'selection' | 'post-restart-auto' | 'post-restart-explicit'; + readonly source: 'reconciler' | 'user'; + readonly syncMode: 'always-on' | 'on-demand'; + readonly contextGraphId: string; + readonly jobId: string; + readonly completedWave: 'selected' | 'final'; + readonly completedSnapshot: GraphSnapshotExpectationV1; +} + +export interface CoreAutomaticCompletionV1 { + readonly contextGraphId: string; + readonly completedWave: 'final'; + readonly completedSnapshot: GraphSnapshotExpectationV1; +} + +export interface CoreAutomaticRoundV1 { + readonly round: number; + readonly jobId: string; + readonly planningLane: string; + readonly source: 'automatic-core-public'; + readonly configuredBatchSize: number; + /** Must remain empty: selected Core work is outside automatic coverage evidence. */ + readonly explicitSelectedContextGraphIds: readonly string[]; + /** Automatic coverage only. Explicit selections are deliberately outside this cap. */ + readonly contextGraphIds: readonly string[]; + /** Exact terminal states produced by this automatic job after the final wave. */ + readonly completions: readonly CoreAutomaticCompletionV1[]; +} + +export interface CoreFinalObservationV1 extends GraphObservationV1 { + /** Scheduler-issued automatic job IDs that produced this graph's final state. */ + readonly automaticJobIds: readonly string[]; +} + +export interface SelectiveCoverageProvenanceV1 { + readonly networkId: string; + readonly testedHeadCommit: string; + readonly runtimeManifestDigest: string; + readonly publisherPeerId: string; + readonly edgePeerId: string; + readonly corePeerId: string; +} + +export interface ExpectedSelectiveCoverageProvenanceV1 + extends SelectiveCoverageProvenanceV1 { + readonly corpusManifestDigest: string; +} + +export interface SelectiveCoverageEvidenceV1 { + readonly schema: typeof SELECTIVE_COVERAGE_EVIDENCE_SCHEMA; + readonly provenance: SelectiveCoverageProvenanceV1; + readonly corpus: SelectiveCoverageCorpusV1; + /** Publisher-owned source snapshots; receivers cannot define their expectations. */ + readonly publisher: { + readonly selected: readonly GraphObservationV1[]; + readonly final: readonly GraphObservationV1[]; + }; + readonly edge: { + readonly beforeSelection: readonly EdgeGraphObservationV1[]; + readonly afterSelection: readonly EdgeGraphObservationV1[]; + readonly afterRestart: readonly EdgeGraphObservationV1[]; + readonly afterSecondOnDemand: readonly EdgeGraphObservationV1[]; + readonly operations: readonly EdgeSyncOperationV1[]; + }; + readonly core: { + readonly automaticBatchSize: number; + readonly rounds: readonly CoreAutomaticRoundV1[]; + readonly final: readonly CoreFinalObservationV1[]; + }; +} + +export interface SelectiveCoverageChecksV1 { + readonly schemaWellFormed: boolean; + readonly provenanceMatches: boolean; + readonly corpusDigestMatches: boolean; + readonly corpusCanonicalOrder: boolean; + readonly requiredPolicyCellsPresent: boolean; + readonly publisherSnapshotsExact: boolean; + readonly publicSecondWaveAdvances: boolean; + readonly edgePassiveBeforeSelection: boolean; + readonly edgeSelectedSnapshotsExact: boolean; + readonly edgeOnDemandRemainsPointInTime: boolean; + readonly edgeAlwaysOnRefreshesAfterRestart: boolean; + readonly edgeOperationProvenance: boolean; + readonly edgeSecondOnDemandConverges: boolean; + readonly edgeUnselectedExcluded: boolean; + readonly edgePrivateExcluded: boolean; + readonly coreBatchMatchesManifest: boolean; + readonly coreBatchWithinBound: boolean; + readonly coreRoundsPublicOnly: boolean; + readonly coreAutomaticProvenance: boolean; + readonly coreEveryPublicScheduled: boolean; + readonly coreCoverageWithinWindow: boolean; + readonly coreFinalPublicExact: boolean; + readonly corePrivateExcluded: boolean; + readonly noMetadataOnlyCompletion: boolean; +} + +export interface SelectiveCoverageVerdictV1 { + readonly schema: typeof SELECTIVE_COVERAGE_VERDICT_SCHEMA; + readonly pass: boolean; + readonly checks: SelectiveCoverageChecksV1; + readonly missingCoreContextGraphIds: readonly string[]; + readonly rejectReasons: readonly string[]; + readonly recomputedCorpusDigest: string; +} + +type CorpusPayload = Omit; + +/** Construct a byte-deterministic manifest and normalize graph order once. */ +export function createSelectiveCoverageCorpus(input: { + networkId: string; + coreAutomaticBatchSize: number; + coreCoverageRoundLimit: number; + graphs: readonly SelectiveCoverageGraphV1[]; +}): SelectiveCoverageCorpusV1 { + const payload: CorpusPayload = { + schema: SELECTIVE_COVERAGE_CORPUS_SCHEMA, + networkId: input.networkId, + coreAutomaticBatchSize: input.coreAutomaticBatchSize, + coreCoverageRoundLimit: input.coreCoverageRoundLimit, + graphs: [...input.graphs].sort((left, right) => + compareCodeUnits(left.contextGraphId, right.contextGraphId)), + }; + return Object.freeze({ + ...payload, + manifestDigest: computeSelectiveCoverageCorpusDigest(payload), + }); +} + +export function computeSelectiveCoverageCorpusDigest( + corpus: CorpusPayload | SelectiveCoverageCorpusV1, +): string { + const { manifestDigest: _ignored, ...payload } = corpus as SelectiveCoverageCorpusV1; + return `sha256:${createHash('sha256').update(canonicalJson(payload)).digest('hex')}`; +} + +/** Stable JSON is also used by the future process launcher when publishing artifacts. */ +export function canonicalJson(value: unknown): string { + return JSON.stringify(normalizeJson(value, '$', new WeakSet())); +} + +function normalizeJson(value: unknown, path: string, seen: WeakSet): unknown { + if (value === null || typeof value === 'string' || typeof value === 'boolean') { + return value; + } + if (typeof value === 'number') { + if (!Number.isFinite(value) || !Number.isSafeInteger(value) || Object.is(value, -0)) { + throw new TypeError(`${path} must be a lossless JSON integer`); + } + return value; + } + if (typeof value !== 'object') throw new TypeError(`${path} is not JSON data`); + if (seen.has(value)) throw new TypeError(`${path} repeats an object reference`); + seen.add(value); + if (Array.isArray(value)) { + if (Object.getPrototypeOf(value) !== Array.prototype) { + throw new TypeError(`${path} must be a plain array`); + } + const ownKeys = Reflect.ownKeys(value); + const expected = new Set(['length']); + for (let index = 0; index < value.length; index += 1) expected.add(String(index)); + if (ownKeys.length !== expected.size || ownKeys.some((key) => !expected.has(key))) { + throw new TypeError(`${path} must be a dense unextended array`); + } + return value.map((_, index) => { + const descriptor = Object.getOwnPropertyDescriptor(value, String(index)); + if (!descriptor?.enumerable || !('value' in descriptor)) { + throw new TypeError(`${path}[${index}] must be an enumerable data property`); + } + return normalizeJson(descriptor.value, `${path}[${index}]`, seen); + }); + } + if (Object.getPrototypeOf(value) !== Object.prototype) { + throw new TypeError(`${path} must be a plain object`); + } + const result: Record = {}; + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.some((key) => typeof key !== 'string')) { + throw new TypeError(`${path} must not contain symbol keys`); + } + for (const key of (ownKeys as string[]).sort()) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (!descriptor?.enumerable || !('value' in descriptor)) { + throw new TypeError(`${path}.${key} must be an enumerable data property`); + } + result[key] = normalizeJson(descriptor.value, `${path}.${key}`, seen); + } + return result; +} + +function compareCodeUnits(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} diff --git a/devnet/rfc64-m1-selective-coverage/package.json b/devnet/rfc64-m1-selective-coverage/package.json new file mode 100644 index 0000000000..e8e563ac43 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/package.json @@ -0,0 +1,11 @@ +{ + "name": "@devnet/rfc64-m1-selective-coverage", + "private": true, + "version": "0.0.0", + "type": "module", + "description": "Fail-closed evidence contract for RFC-64 M1 Edge selection and bounded Core public coverage.", + "scripts": { + "typecheck": "tsc --noEmit -p tsconfig.json", + "test": "node --experimental-strip-types --test verifier.test.ts" + } +} diff --git a/devnet/rfc64-m1-selective-coverage/tsconfig.json b/devnet/rfc64-m1-selective-coverage/tsconfig.json new file mode 100644 index 0000000000..75eea5acb8 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "declaration": false, + "declarationMap": false, + "noEmit": true, + "rootDir": ".", + "skipLibCheck": true, + "sourceMap": false, + "types": ["node"] + }, + "include": ["*.ts"] +} diff --git a/devnet/rfc64-m1-selective-coverage/verifier.test.ts b/devnet/rfc64-m1-selective-coverage/verifier.test.ts new file mode 100644 index 0000000000..a1458c1ff9 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/verifier.test.ts @@ -0,0 +1,594 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import test from 'node:test'; + +import { + SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, + canonicalJson, + createSelectiveCoverageCorpus, + type GraphObservationV1, + type GraphSnapshotExpectationV1, + type ExpectedSelectiveCoverageProvenanceV1, + type SelectiveCoverageEvidenceV1, + type SelectiveCoverageGraphV1, +} from './manifest.ts'; +import { verifySelectiveCoverage as verifyWithProvenance } from './verifier.ts'; + +const id = (name: string) => `0x1111111111111111111111111111111111111111/${name}`; +const hash = (value: string) => `sha256:${createHash('sha256').update(value).digest('hex')}`; + +function snapshot(name: string, wave: 'selected' | 'final'): GraphSnapshotExpectationV1 { + const offset = wave === 'selected' ? 1 : 2; + return { + vm: { + headDigest: hash(`${name}:${wave}:vm:head`), + inventoryDigest: hash(`${name}:${wave}:vm:inventory`), + assetCount: offset, + dataTripleCount: 100 * offset, + }, + swm: { + headDigest: hash(`${name}:${wave}:swm:head`), + inventoryDigest: hash(`${name}:${wave}:swm:inventory`), + assetCount: offset, + dataTripleCount: 80 * offset, + }, + }; +} + +const graphs: readonly SelectiveCoverageGraphV1[] = [ + { name: '01-public-open-on-demand', accessPolicy: 0 as const, publishPolicy: 1 as const, edgePolicy: 'on-demand' as const }, + { name: '02-public-curated-always-on', accessPolicy: 0 as const, publishPolicy: 0 as const, edgePolicy: 'always-on' as const }, + { name: '03-public-open-unselected', accessPolicy: 0 as const, publishPolicy: 1 as const, edgePolicy: 'unselected' as const }, + { name: '04-private-open', accessPolicy: 1 as const, publishPolicy: 1 as const, edgePolicy: 'unselected' as const }, + { name: '05-private-curated', accessPolicy: 1 as const, publishPolicy: 0 as const, edgePolicy: 'unselected' as const }, +].map((cell) => ({ + contextGraphId: id(cell.name), + accessPolicy: cell.accessPolicy, + publishPolicy: cell.publishPolicy, + edgePolicy: cell.edgePolicy, + selectedSnapshot: snapshot(cell.name, 'selected'), + finalSnapshot: snapshot(cell.name, 'final'), +})); + +const corpus = createSelectiveCoverageCorpus({ + networkId: 'otp:20430', + coreAutomaticBatchSize: 2, + coreCoverageRoundLimit: 2, + graphs, +}); + +const PROVENANCE = Object.freeze({ + networkId: corpus.networkId, + testedHeadCommit: 'a'.repeat(40), + runtimeManifestDigest: hash('runtime-manifest'), + publisherPeerId: 'publisher-peer', + edgePeerId: 'edge-peer', + corePeerId: 'core-peer', +}); +const EXPECTED_PROVENANCE: ExpectedSelectiveCoverageProvenanceV1 = Object.freeze({ + ...PROVENANCE, + corpusManifestDigest: corpus.manifestDigest, +}); + +function verifySelectiveCoverage(input: unknown) { + return verifyWithProvenance(input, EXPECTED_PROVENANCE); +} + +function absent(contextGraphId: string): GraphObservationV1 { + const plane = { + reportedComplete: false, + headDigest: null, + inventoryDigest: null, + assetCount: 0, + metadataTripleCount: 0, + dataTripleCount: 0, + } as const; + return { contextGraphId, vm: { ...plane }, swm: { ...plane } }; +} + +function exact( + graph: SelectiveCoverageGraphV1, + expected: GraphSnapshotExpectationV1, +): GraphObservationV1 { + const plane = (value: GraphSnapshotExpectationV1['vm']) => ({ + reportedComplete: true, + headDigest: value.headDigest, + inventoryDigest: value.inventoryDigest, + assetCount: value.assetCount, + metadataTripleCount: 5, + dataTripleCount: value.dataTripleCount, + }); + return { + contextGraphId: graph.contextGraphId, + vm: plane(expected.vm), + swm: plane(expected.swm), + }; +} + +function edgeAbsent(contextGraphId: string) { + return { + ...absent(contextGraphId), + runtimeSyncMode: null, + producingJobId: null, + } as const; +} + +function edgeExact( + graph: SelectiveCoverageGraphV1, + expected: GraphSnapshotExpectationV1, + producingJobId: string, +) { + return { + ...exact(graph, expected), + runtimeSyncMode: graph.edgePolicy as 'on-demand' | 'always-on', + producingJobId, + }; +} + +function fixture(): SelectiveCoverageEvidenceV1 { + return { + schema: SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, + provenance: PROVENANCE, + corpus, + publisher: { + selected: corpus.graphs.map((graph) => exact(graph, graph.selectedSnapshot)), + final: corpus.graphs.map((graph) => exact(graph, graph.finalSnapshot)), + }, + edge: { + beforeSelection: corpus.graphs.map((graph) => edgeAbsent(graph.contextGraphId)), + afterSelection: corpus.graphs.map((graph) => + graph.accessPolicy === 0 && graph.edgePolicy !== 'unselected' + ? edgeExact( + graph, + graph.selectedSnapshot, + graph.edgePolicy === 'on-demand' + ? 'edge-select-on-demand' + : 'edge-select-always-on', + ) + : edgeAbsent(graph.contextGraphId)), + afterRestart: corpus.graphs.map((graph) => { + if (graph.accessPolicy !== 0 || graph.edgePolicy === 'unselected') { + return edgeAbsent(graph.contextGraphId); + } + const alwaysOn = graph.edgePolicy === 'always-on'; + const observed = edgeExact( + graph, + alwaysOn ? graph.finalSnapshot : graph.selectedSnapshot, + alwaysOn ? 'edge-auto-always-on' : 'edge-select-on-demand', + ); + return alwaysOn ? observed : { ...observed, runtimeSyncMode: null }; + }), + afterSecondOnDemand: corpus.graphs.map((graph) => + graph.accessPolicy === 0 && graph.edgePolicy !== 'unselected' + ? edgeExact( + graph, + graph.finalSnapshot, + graph.edgePolicy === 'on-demand' + ? 'edge-second-on-demand' + : 'edge-auto-always-on', + ) + : edgeAbsent(graph.contextGraphId)), + operations: [ + { + sequence: 0, + phase: 'selection', + source: 'user', + syncMode: 'on-demand', + contextGraphId: graphs[0]!.contextGraphId, + jobId: 'edge-select-on-demand', + completedWave: 'selected', + completedSnapshot: graphs[0]!.selectedSnapshot, + }, + { + sequence: 1, + phase: 'selection', + source: 'user', + syncMode: 'always-on', + contextGraphId: graphs[1]!.contextGraphId, + jobId: 'edge-select-always-on', + completedWave: 'selected', + completedSnapshot: graphs[1]!.selectedSnapshot, + }, + { + sequence: 2, + phase: 'post-restart-auto', + source: 'reconciler', + syncMode: 'always-on', + contextGraphId: graphs[1]!.contextGraphId, + jobId: 'edge-auto-always-on', + completedWave: 'final', + completedSnapshot: graphs[1]!.finalSnapshot, + }, + { + sequence: 3, + phase: 'post-restart-explicit', + source: 'user', + syncMode: 'on-demand', + contextGraphId: graphs[0]!.contextGraphId, + jobId: 'edge-second-on-demand', + completedWave: 'final', + completedSnapshot: graphs[0]!.finalSnapshot, + }, + ], + }, + core: { + automaticBatchSize: 2, + rounds: [ + { + round: 0, + jobId: 'core-auto-0', + planningLane: 'publisher-peer', + source: 'automatic-core-public', + configuredBatchSize: 2, + explicitSelectedContextGraphIds: [], + contextGraphIds: [graphs[0]!.contextGraphId, graphs[1]!.contextGraphId], + completions: [graphs[0]!, graphs[1]!].map((graph) => ({ + contextGraphId: graph.contextGraphId, + completedWave: 'final' as const, + completedSnapshot: graph.finalSnapshot, + })), + }, + { + round: 1, + jobId: 'core-auto-1', + planningLane: 'publisher-peer', + source: 'automatic-core-public', + configuredBatchSize: 2, + explicitSelectedContextGraphIds: [], + contextGraphIds: [graphs[2]!.contextGraphId], + completions: [{ + contextGraphId: graphs[2]!.contextGraphId, + completedWave: 'final', + completedSnapshot: graphs[2]!.finalSnapshot, + }], + }, + ], + final: corpus.graphs.map((graph, index) => ({ + ...(graph.accessPolicy === 0 + ? exact(graph, graph.finalSnapshot) + : absent(graph.contextGraphId)), + automaticJobIds: graph.accessPolicy === 0 + ? [index < 2 ? 'core-auto-0' : 'core-auto-1'] + : [], + })), + }, + }; +} + +function clone(): any { + return JSON.parse(JSON.stringify(fixture())); +} + +test('accepts exact Edge selection and bounded Core public convergence evidence', () => { + const verdict = verifySelectiveCoverage(fixture()); + assert.equal(verdict.pass, true); + assert.deepEqual(verdict.rejectReasons, []); + assert.deepEqual(verdict.missingCoreContextGraphIds, []); + for (const [name, value] of Object.entries(verdict.checks)) { + assert.equal(value, true, name); + } +}); + +test('corpus and evidence serialization is deterministic', () => { + assert.equal( + canonicalJson({ z: { second: 2, first: 1 }, a: ['@', ':', '/'] }), + canonicalJson({ a: ['@', ':', '/'], z: { first: 1, second: 2 } }), + ); + const rebuilt = createSelectiveCoverageCorpus({ + networkId: corpus.networkId, + coreAutomaticBatchSize: corpus.coreAutomaticBatchSize, + coreCoverageRoundLimit: corpus.coreCoverageRoundLimit, + graphs: [...graphs].reverse(), + }); + assert.equal(rebuilt.manifestDigest, corpus.manifestDigest); + assert.equal(canonicalJson(rebuilt), canonicalJson(corpus)); +}); + +test('corpus, runtime, network, and peer roles require an external trust anchor', () => { + const substituted = clone(); + substituted.corpus.graphs[0].finalSnapshot.vm.assetCount += 1; + substituted.corpus = createSelectiveCoverageCorpus({ + networkId: substituted.corpus.networkId, + coreAutomaticBatchSize: substituted.corpus.coreAutomaticBatchSize, + coreCoverageRoundLimit: substituted.corpus.coreCoverageRoundLimit, + graphs: substituted.corpus.graphs, + }); + let verdict = verifyWithProvenance(substituted, EXPECTED_PROVENANCE); + assert.equal(verdict.checks.corpusDigestMatches, true); + assert.equal(verdict.checks.provenanceMatches, false); + + const wrongRole = clone(); + wrongRole.provenance.corePeerId = 'manual-receiver-peer'; + verdict = verifyWithProvenance(wrongRole, EXPECTED_PROVENANCE); + assert.equal(verdict.checks.provenanceMatches, false); +}); + +test('every public graph must have a real larger second publication wave', () => { + const noOp = clone(); + const graph = noOp.corpus.graphs[2]; + graph.finalSnapshot = structuredClone(graph.selectedSnapshot); + noOp.corpus = createSelectiveCoverageCorpus({ + networkId: noOp.corpus.networkId, + coreAutomaticBatchSize: noOp.corpus.coreAutomaticBatchSize, + coreCoverageRoundLimit: noOp.corpus.coreCoverageRoundLimit, + graphs: noOp.corpus.graphs, + }); + noOp.publisher.final[2] = exact(graph, graph.selectedSnapshot); + noOp.core.final[2] = { + ...exact(graph, graph.selectedSnapshot), + automaticJobIds: ['core-auto-1'], + }; + noOp.core.rounds[1].completions[0].completedSnapshot = graph.selectedSnapshot; + const verdict = verifyWithProvenance(noOp, { + ...EXPECTED_PROVENANCE, + corpusManifestDigest: noOp.corpus.manifestDigest, + }); + assert.equal(verdict.checks.publisherSnapshotsExact, true); + assert.equal(verdict.checks.coreFinalPublicExact, true); + assert.equal(verdict.checks.coreAutomaticProvenance, true); + assert.equal(verdict.checks.publicSecondWaveAdvances, false); +}); + +test('metadata-only responses cannot claim Edge or Core completion', () => { + const edgeMetadataOnly = clone(); + edgeMetadataOnly.edge.afterSelection[0].vm.reportedComplete = true; + edgeMetadataOnly.edge.afterSelection[0].vm.assetCount = 0; + edgeMetadataOnly.edge.afterSelection[0].vm.dataTripleCount = 0; + let verdict = verifySelectiveCoverage(edgeMetadataOnly); + assert.equal(verdict.pass, false); + assert.equal(verdict.checks.edgeSelectedSnapshotsExact, false); + assert.equal(verdict.checks.noMetadataOnlyCompletion, false); + + const coreMetadataOnly = clone(); + coreMetadataOnly.core.final[2].swm.reportedComplete = true; + coreMetadataOnly.core.final[2].swm.assetCount = 0; + coreMetadataOnly.core.final[2].swm.dataTripleCount = 0; + verdict = verifySelectiveCoverage(coreMetadataOnly); + assert.equal(verdict.checks.coreFinalPublicExact, false); + assert.equal(verdict.checks.noMetadataOnlyCompletion, false); +}); + +test('private graphs are excluded from Core rounds and final payload', () => { + const scheduledPrivate = clone(); + scheduledPrivate.core.rounds[1].contextGraphIds.push(graphs[3]!.contextGraphId); + let verdict = verifySelectiveCoverage(scheduledPrivate); + assert.equal(verdict.pass, false); + assert.equal(verdict.checks.coreRoundsPublicOnly, false); + assert.equal(verdict.checks.corePrivateExcluded, false); + + const acquiredPrivate = clone(); + acquiredPrivate.core.final[3] = { + ...exact(graphs[3]!, graphs[3]!.finalSnapshot), + automaticJobIds: ['core-auto-1'], + }; + verdict = verifySelectiveCoverage(acquiredPrivate); + assert.equal(verdict.checks.corePrivateExcluded, false); +}); + +test('automatic Core batch overflow fails even when final convergence is exact', () => { + const raw = clone(); + raw.core.rounds[0].contextGraphIds.push(graphs[2]!.contextGraphId); + const verdict = verifySelectiveCoverage(raw); + assert.equal(verdict.pass, false); + assert.equal(verdict.checks.coreBatchWithinBound, false); + assert.equal(verdict.checks.coreFinalPublicExact, true); +}); + +test('VM or SWM digest mismatch fails exact convergence', () => { + const edgeMismatch = clone(); + edgeMismatch.edge.afterRestart[1].swm.inventoryDigest = hash('wrong-edge-swm'); + let verdict = verifySelectiveCoverage(edgeMismatch); + assert.equal(verdict.checks.edgeAlwaysOnRefreshesAfterRestart, false); + assert.equal(verdict.checks.noMetadataOnlyCompletion, false); + + const coreMismatch = clone(); + coreMismatch.core.final[2].vm.headDigest = hash('wrong-core-vm'); + verdict = verifySelectiveCoverage(coreMismatch); + assert.equal(verdict.checks.coreFinalPublicExact, false); +}); + +test('on-demand does not refresh automatically while always-on must refresh after restart', () => { + const onDemandAdvanced = clone(); + onDemandAdvanced.edge.afterRestart[0] = edgeExact( + graphs[0]!, + graphs[0]!.finalSnapshot, + 'edge-select-on-demand', + ); + assert.equal( + verifySelectiveCoverage(onDemandAdvanced).checks.edgeOnDemandRemainsPointInTime, + false, + ); + + const incorrectlyPersistedOnDemand = clone(); + incorrectlyPersistedOnDemand.edge.afterRestart[0].runtimeSyncMode = 'on-demand'; + assert.equal( + verifySelectiveCoverage(incorrectlyPersistedOnDemand).checks.edgeOnDemandRemainsPointInTime, + false, + ); + + const alwaysOnStale = clone(); + alwaysOnStale.edge.afterRestart[1] = edgeExact( + graphs[1]!, + graphs[1]!.selectedSnapshot, + 'edge-auto-always-on', + ); + assert.equal( + verifySelectiveCoverage(alwaysOnStale).checks.edgeAlwaysOnRefreshesAfterRestart, + false, + ); + + const manualAlwaysOn = clone(); + manualAlwaysOn.edge.operations[2].source = 'user'; + assert.equal( + verifySelectiveCoverage(manualAlwaysOn).checks.edgeOperationProvenance, + false, + ); + + const hiddenOnDemandRefresh = clone(); + hiddenOnDemandRefresh.edge.operations[2] = { + ...hiddenOnDemandRefresh.edge.operations[2], + contextGraphId: graphs[0]!.contextGraphId, + syncMode: 'on-demand', + }; + assert.equal( + verifySelectiveCoverage(hiddenOnDemandRefresh).checks.edgeOperationProvenance, + false, + ); +}); + +test('unselected Edge public payload and missing Core scheduling fail independently', () => { + const edgeLeak = clone(); + edgeLeak.edge.afterRestart[2] = { + ...exact(graphs[2]!, graphs[2]!.finalSnapshot), + runtimeSyncMode: null, + producingJobId: null, + }; + assert.equal(verifySelectiveCoverage(edgeLeak).checks.edgeUnselectedExcluded, false); + + const missingCore = clone(); + missingCore.core.rounds.splice(1, 1); + const verdict = verifySelectiveCoverage(missingCore); + assert.equal(verdict.checks.coreEveryPublicScheduled, false); + assert.deepEqual(verdict.missingCoreContextGraphIds, [graphs[2]!.contextGraphId]); + + const leakedSubscription = clone(); + leakedSubscription.edge.afterRestart[2].runtimeSyncMode = 'always-on'; + assert.equal( + verifySelectiveCoverage(leakedSubscription).checks.edgeUnselectedExcluded, + false, + ); + + const leakedPrivateSubscription = clone(); + leakedPrivateSubscription.edge.afterRestart[3].runtimeSyncMode = 'always-on'; + assert.equal( + verifySelectiveCoverage(leakedPrivateSubscription).checks.edgePrivateExcluded, + false, + ); +}); + +test('Core convergence must bind to automatic jobs with no explicit selections', () => { + const explicit = clone(); + explicit.core.rounds[0].explicitSelectedContextGraphIds = [graphs[0]!.contextGraphId]; + let verdict = verifySelectiveCoverage(explicit); + assert.equal(verdict.checks.coreAutomaticProvenance, false); + + const wrongJob = clone(); + wrongJob.core.final[2].automaticJobIds = ['core-auto-0']; + verdict = verifySelectiveCoverage(wrongJob); + assert.equal(verdict.checks.coreAutomaticProvenance, false); + assert.equal(verdict.checks.coreFinalPublicExact, true); + + const staleCompletion = clone(); + staleCompletion.core.rounds[1].completions[0].completedSnapshot = + graphs[2]!.selectedSnapshot; + verdict = verifySelectiveCoverage(staleCompletion); + assert.equal(verdict.checks.coreAutomaticProvenance, false); + assert.equal(verdict.checks.coreFinalPublicExact, true); +}); + +test('Edge checkpoints bind their exact state to the operation that produced it', () => { + const wrongSelectionJob = clone(); + wrongSelectionJob.edge.afterSelection[0].producingJobId = 'edge-auto-always-on'; + let verdict = verifySelectiveCoverage(wrongSelectionJob); + assert.equal(verdict.checks.edgeOperationProvenance, true); + assert.equal(verdict.checks.edgeSelectedSnapshotsExact, false); + + const falseAutomaticCompletion = clone(); + falseAutomaticCompletion.edge.operations[2].completedSnapshot = + graphs[1]!.selectedSnapshot; + verdict = verifySelectiveCoverage(falseAutomaticCompletion); + assert.equal(verdict.checks.edgeAlwaysOnRefreshesAfterRestart, true); + assert.equal(verdict.checks.edgeOperationProvenance, false); +}); + +test('late eventual coverage fails the deterministic first-admission window', () => { + const starved = clone(); + starved.core.rounds = [ + { + ...starved.core.rounds[0], + contextGraphIds: [graphs[0]!.contextGraphId], + completions: [starved.core.rounds[0].completions[0]], + }, + { + ...starved.core.rounds[1], + contextGraphIds: [graphs[0]!.contextGraphId], + completions: [{ + contextGraphId: graphs[0]!.contextGraphId, + completedWave: 'final', + completedSnapshot: graphs[0]!.finalSnapshot, + }], + }, + { + ...starved.core.rounds[1], + round: 2, + jobId: 'core-auto-2', + contextGraphIds: [graphs[1]!.contextGraphId, graphs[2]!.contextGraphId], + completions: [graphs[1]!, graphs[2]!].map((graph) => ({ + contextGraphId: graph.contextGraphId, + completedWave: 'final', + completedSnapshot: graph.finalSnapshot, + })), + }, + ]; + starved.core.final[0].automaticJobIds = ['core-auto-0']; + starved.core.final[1].automaticJobIds = ['core-auto-2']; + starved.core.final[2].automaticJobIds = ['core-auto-2']; + const verdict = verifySelectiveCoverage(starved); + assert.equal(verdict.checks.coreEveryPublicScheduled, true); + assert.equal(verdict.checks.coreCoverageWithinWindow, false); + assert.equal(verdict.checks.coreBatchWithinBound, true); +}); + +test('unknown keys, duplicate rows, and stale manifest digest fail closed', () => { + const extra = clone(); + extra.edge.afterSelection[0].unverified = true; + assert.equal(verifySelectiveCoverage(extra).checks.schemaWellFormed, false); + + const duplicate = clone(); + duplicate.edge.afterRestart[1].contextGraphId = duplicate.edge.afterRestart[0].contextGraphId; + assert.equal(verifySelectiveCoverage(duplicate).checks.corpusCanonicalOrder, false); + + const staleDigest = clone(); + staleDigest.corpus.graphs[0].finalSnapshot.vm.assetCount += 1; + assert.equal(verifySelectiveCoverage(staleDigest).checks.corpusDigestMatches, false); +}); + +test('non-JSON object topology fails closed without throwing', () => { + const hidden = clone(); + Object.defineProperty(hidden.core, 'hidden', { value: true, enumerable: false }); + assert.equal(verifySelectiveCoverage(hidden).checks.schemaWellFormed, false); + + const extendedArray = clone(); + extendedArray.core.rounds.unverified = true; + assert.equal(verifySelectiveCoverage(extendedArray).checks.schemaWellFormed, false); + + const proxy = Proxy.revocable(fixture(), {}); + proxy.revoke(); + assert.doesNotThrow(() => verifySelectiveCoverage(proxy.proxy)); + assert.equal(verifySelectiveCoverage(proxy.proxy).checks.schemaWellFormed, false); +}); + +test('canonical JSON rejects ambiguous or lossy JavaScript values', () => { + const sparse = new Array(1); + const extended: unknown[] & { extra?: boolean } = [1]; + extended.extra = true; + const symbolKeyed = { visible: true } as Record; + symbolKeyed[Symbol('hidden')] = true; + const hidden = { visible: true }; + Object.defineProperty(hidden, 'hidden', { value: true, enumerable: false }); + const accessor = {}; + Object.defineProperty(accessor, 'value', { enumerable: true, get: () => 1 }); + const shared = { value: true }; + for (const value of [ + sparse, + extended, + symbolKeyed, + hidden, + accessor, + [shared, shared], + Number.MAX_SAFE_INTEGER + 1, + -0, + ]) { + assert.throws(() => canonicalJson(value)); + } +}); diff --git a/devnet/rfc64-m1-selective-coverage/verifier.ts b/devnet/rfc64-m1-selective-coverage/verifier.ts new file mode 100644 index 0000000000..e28634b761 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/verifier.ts @@ -0,0 +1,854 @@ +import { + MAX_SELECTIVE_COVERAGE_GRAPHS, + MAX_SELECTIVE_COVERAGE_ROUNDS, + SELECTIVE_COVERAGE_CORPUS_SCHEMA, + SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, + SELECTIVE_COVERAGE_VERDICT_SCHEMA, + computeSelectiveCoverageCorpusDigest, + type CoreAutomaticRoundV1, + type CoreFinalObservationV1, + type EdgeCoveragePolicy, + type EdgeGraphObservationV1, + type EdgeSyncOperationV1, + type ExpectedSelectiveCoverageProvenanceV1, + type GraphObservationV1, + type GraphSnapshotExpectationV1, + type PlaneExpectationV1, + type PlaneObservationV1, + type SelectiveCoverageChecksV1, + type SelectiveCoverageCorpusV1, + type SelectiveCoverageEvidenceV1, + type SelectiveCoverageGraphV1, + type SelectiveCoverageVerdictV1, +} from './manifest.ts'; + +const DIGEST = /^(?:0x|sha256:)[0-9a-f]{64}$/u; +const ID = /^[A-Za-z0-9._:/@-]+$/u; + +const CHECK_NAMES: readonly (keyof SelectiveCoverageChecksV1)[] = Object.freeze([ + 'schemaWellFormed', + 'provenanceMatches', + 'corpusDigestMatches', + 'corpusCanonicalOrder', + 'requiredPolicyCellsPresent', + 'publisherSnapshotsExact', + 'publicSecondWaveAdvances', + 'edgePassiveBeforeSelection', + 'edgeSelectedSnapshotsExact', + 'edgeOnDemandRemainsPointInTime', + 'edgeAlwaysOnRefreshesAfterRestart', + 'edgeOperationProvenance', + 'edgeSecondOnDemandConverges', + 'edgeUnselectedExcluded', + 'edgePrivateExcluded', + 'coreBatchMatchesManifest', + 'coreBatchWithinBound', + 'coreRoundsPublicOnly', + 'coreAutomaticProvenance', + 'coreEveryPublicScheduled', + 'coreCoverageWithinWindow', + 'coreFinalPublicExact', + 'corePrivateExcluded', + 'noMetadataOnlyCompletion', +]); + +const REASONS: Readonly> = Object.freeze({ + schemaWellFormed: 'evidence failed closed structural validation', + provenanceMatches: 'repository, runtime, network, or process-role provenance differs from the external trust anchor', + corpusDigestMatches: 'corpus manifest digest does not match its closed payload', + corpusCanonicalOrder: 'corpus or observation rows are not canonical and unique', + requiredPolicyCellsPresent: 'corpus must include public on-demand, public always-on, public unselected, private open, and private curated cells', + publisherSnapshotsExact: 'Publisher-owned VM or SWM source snapshots differ from the anchored corpus', + publicSecondWaveAdvances: 'one or more public graphs did not produce a distinct, larger second publication wave', + edgePassiveBeforeSelection: 'Edge acquired VM or SWM payload before any user selection', + edgeSelectedSnapshotsExact: 'Edge selection did not produce exact VM and SWM snapshot evidence', + edgeOnDemandRemainsPointInTime: 'on-demand Edge state changed without another explicit request', + edgeAlwaysOnRefreshesAfterRestart: 'always-on Edge state did not reach the final post-restart snapshot', + edgeOperationProvenance: 'Edge operations do not prove explicit selection, automatic always-on restart work, and no hidden on-demand refresh', + edgeSecondOnDemandConverges: 'a second explicit on-demand request did not reach the final exact snapshot', + edgeUnselectedExcluded: 'Edge acquired an unselected public context graph', + edgePrivateExcluded: 'Edge acquired an unselected private context graph', + coreBatchMatchesManifest: 'Core evidence used a different automatic batch bound', + coreBatchWithinBound: 'a Core automatic round exceeded its configured batch bound', + coreRoundsPublicOnly: 'a Core automatic round admitted a private, duplicate, or unknown context graph', + coreAutomaticProvenance: 'Core rounds or final observations are not bound to scheduler-issued automatic jobs with no explicit selections', + coreEveryPublicScheduled: 'one or more public context graphs never entered a Core automatic round', + coreCoverageWithinWindow: 'Core did not first admit every public graph within the anchored coverage-round limit', + coreFinalPublicExact: 'Core did not converge every public VM and SWM plane exactly', + corePrivateExcluded: 'Core automatic coverage acquired private VM or SWM payload', + noMetadataOnlyCompletion: 'a required plane reported completion without exact nonzero payload', +}); + +/** Verify untrusted harness JSON without trusting summary or synced flags. */ +export function verifySelectiveCoverage( + input: unknown, + expected: ExpectedSelectiveCoverageProvenanceV1, +): SelectiveCoverageVerdictV1 { + try { + if (!parseExpectedProvenance(expected)) return schemaReject(); + const evidence = parseEvidence(input); + if (!evidence) return schemaReject(); + return verifyParsed(evidence, expected); + } catch { + return schemaReject(); + } +} + +function verifyParsed( + evidence: SelectiveCoverageEvidenceV1, + expected: ExpectedSelectiveCoverageProvenanceV1, +): SelectiveCoverageVerdictV1 { + const { corpus } = evidence; + const graphIds = corpus.graphs.map((graph) => graph.contextGraphId); + const publicGraphs = corpus.graphs.filter((graph) => graph.accessPolicy === 0); + const privateGraphs = corpus.graphs.filter((graph) => graph.accessPolicy === 1); + const byId = new Map(corpus.graphs.map((graph) => [graph.contextGraphId, graph])); + const publisherSelected = byObservationId(evidence.publisher.selected); + const publisherFinal = byObservationId(evidence.publisher.final); + const edgeBefore = byObservationId(evidence.edge.beforeSelection); + const edgeSelected = byObservationId(evidence.edge.afterSelection); + const edgeRestarted = byObservationId(evidence.edge.afterRestart); + const edgeSecondOnDemand = byObservationId(evidence.edge.afterSecondOnDemand); + const coreFinal = byObservationId(evidence.core.final); + + const observationsCanonical = [ + evidence.publisher.selected, + evidence.publisher.final, + evidence.edge.beforeSelection, + evidence.edge.afterSelection, + evidence.edge.afterRestart, + evidence.edge.afterSecondOnDemand, + evidence.core.final, + ].every((rows) => exactCanonicalIds(rows.map((row) => row.contextGraphId), graphIds)); + const corpusCanonicalOrder = strictlyIncreasing(graphIds) && observationsCanonical; + const provenanceMatches = evidence.provenance.networkId === expected.networkId + && evidence.provenance.testedHeadCommit === expected.testedHeadCommit + && evidence.provenance.runtimeManifestDigest === expected.runtimeManifestDigest + && evidence.provenance.publisherPeerId === expected.publisherPeerId + && evidence.provenance.edgePeerId === expected.edgePeerId + && evidence.provenance.corePeerId === expected.corePeerId + && corpus.networkId === expected.networkId + && corpus.manifestDigest === expected.corpusManifestDigest; + const corpusDigestMatches = computeSelectiveCoverageCorpusDigest(corpus) + === corpus.manifestDigest; + const requiredPolicyCellsPresent = hasRequiredPolicyCells(corpus.graphs); + const publisherSnapshotsExact = corpus.graphs.every((graph) => + exactGraph(publisherSelected.get(graph.contextGraphId), graph.selectedSnapshot) + && exactGraph(publisherFinal.get(graph.contextGraphId), graph.finalSnapshot)); + const publicSecondWaveAdvances = publicGraphs.every((graph) => + snapshotsAdvance(graph.selectedSnapshot, graph.finalSnapshot)); + + const edgePassiveBeforeSelection = corpus.graphs.every((graph) => + absentGraph(edgeBefore.get(graph.contextGraphId)) + && edgeBefore.get(graph.contextGraphId)?.runtimeSyncMode === null + && edgeBefore.get(graph.contextGraphId)?.producingJobId === null); + const edgeSelectedSnapshotsExact = corpus.graphs.every((graph) => { + const observed = edgeSelected.get(graph.contextGraphId); + return graph.accessPolicy === 0 && graph.edgePolicy !== 'unselected' + ? exactGraph(observed, graph.selectedSnapshot) + && observed?.runtimeSyncMode === graph.edgePolicy + && observed.producingJobId === edgeJobId(evidence.edge.operations, graph.contextGraphId, 'selection') + : absentGraph(observed) && observed?.runtimeSyncMode === null + && observed.producingJobId === null; + }); + const edgeOnDemandRemainsPointInTime = corpus.graphs + .filter((graph) => graph.edgePolicy === 'on-demand') + .every((graph) => exactGraph(edgeRestarted.get(graph.contextGraphId), graph.selectedSnapshot) + && edgeRestarted.get(graph.contextGraphId)?.runtimeSyncMode === null + && edgeRestarted.get(graph.contextGraphId)?.producingJobId + === edgeJobId(evidence.edge.operations, graph.contextGraphId, 'selection')); + const edgeAlwaysOnRefreshesAfterRestart = corpus.graphs + .filter((graph) => graph.edgePolicy === 'always-on') + .every((graph) => ( + snapshotsAdvance(graph.selectedSnapshot, graph.finalSnapshot) + && exactGraph(edgeRestarted.get(graph.contextGraphId), graph.finalSnapshot) + && edgeRestarted.get(graph.contextGraphId)?.runtimeSyncMode === 'always-on' + && edgeRestarted.get(graph.contextGraphId)?.producingJobId + === edgeJobId(evidence.edge.operations, graph.contextGraphId, 'post-restart-auto') + )); + const edgeOperationProvenance = verifyEdgeOperations(evidence.edge.operations, corpus.graphs); + const edgeSecondOnDemandConverges = corpus.graphs.every((graph) => { + const observed = edgeSecondOnDemand.get(graph.contextGraphId); + if (graph.accessPolicy !== 0 || graph.edgePolicy === 'unselected') { + return absentGraph(observed) && observed?.runtimeSyncMode === null + && observed.producingJobId === null; + } + const phase = graph.edgePolicy === 'always-on' + ? 'post-restart-auto' + : 'post-restart-explicit'; + return snapshotsAdvance(graph.selectedSnapshot, graph.finalSnapshot) + && exactGraph(observed, graph.finalSnapshot) + && observed?.runtimeSyncMode === graph.edgePolicy + && observed.producingJobId + === edgeJobId(evidence.edge.operations, graph.contextGraphId, phase); + }); + const edgeUnselectedExcluded = publicGraphs + .filter((graph) => graph.edgePolicy === 'unselected') + .every((graph) => absentGraph(edgeSelected.get(graph.contextGraphId)) + && absentGraph(edgeRestarted.get(graph.contextGraphId)) + && absentGraph(edgeSecondOnDemand.get(graph.contextGraphId)) + && [edgeSelected, edgeRestarted, edgeSecondOnDemand].every((phase) => + phase.get(graph.contextGraphId)?.runtimeSyncMode === null + && phase.get(graph.contextGraphId)?.producingJobId === null)); + const edgePrivateExcluded = privateGraphs.every((graph) => + absentGraph(edgeSelected.get(graph.contextGraphId)) + && absentGraph(edgeRestarted.get(graph.contextGraphId)) + && absentGraph(edgeSecondOnDemand.get(graph.contextGraphId)) + && [edgeSelected, edgeRestarted, edgeSecondOnDemand].every((phase) => + phase.get(graph.contextGraphId)?.runtimeSyncMode === null + && phase.get(graph.contextGraphId)?.producingJobId === null)); + + const coreBatchMatchesManifest = evidence.core.automaticBatchSize + === corpus.coreAutomaticBatchSize; + const coreBatchWithinBound = evidence.core.rounds.every((round) => + round.contextGraphIds.length <= corpus.coreAutomaticBatchSize); + const coreRoundsPublicOnly = evidence.core.rounds.every((round) => { + const unique = new Set(round.contextGraphIds); + return unique.size === round.contextGraphIds.length + && round.contextGraphIds.every((contextGraphId) => byId.get(contextGraphId)?.accessPolicy === 0); + }); + const automaticJobs = new Map(evidence.core.rounds.map((round) => [round.jobId, round])); + const coreAutomaticProvenance = automaticJobs.size === evidence.core.rounds.length + && evidence.core.rounds.every((round) => + round.source === 'automatic-core-public' + && round.planningLane === expected.publisherPeerId + && round.configuredBatchSize === corpus.coreAutomaticBatchSize + && round.explicitSelectedContextGraphIds.length === 0 + && new Set(round.completions.map((completion) => completion.contextGraphId)).size + === round.completions.length + && round.completions.every((completion) => { + const graph = byId.get(completion.contextGraphId); + return round.contextGraphIds.includes(completion.contextGraphId) + && completion.completedWave === 'final' + && graph?.accessPolicy === 0 + && exactSnapshot(completion.completedSnapshot, graph.finalSnapshot); + })) + && evidence.core.final.every((observation) => { + const graph = byId.get(observation.contextGraphId); + if (graph?.accessPolicy !== 0) return observation.automaticJobIds.length === 0; + return observation.automaticJobIds.length > 0 + && observation.automaticJobIds.every((jobId) => + automaticJobs.get(jobId)?.contextGraphIds.includes(observation.contextGraphId) === true) + && observation.automaticJobIds.some((jobId) => + automaticJobs.get(jobId)?.completions.some((completion) => + completion.contextGraphId === observation.contextGraphId + && exactSnapshot(completion.completedSnapshot, graph.finalSnapshot))); + }); + const scheduled = new Set(evidence.core.rounds.flatMap((round) => round.contextGraphIds)); + const missingCoreContextGraphIds = publicGraphs + .map((graph) => graph.contextGraphId) + .filter((contextGraphId) => !scheduled.has(contextGraphId)); + const coreEveryPublicScheduled = missingCoreContextGraphIds.length === 0; + const scheduledWithinWindow = new Set( + evidence.core.rounds + .slice(0, corpus.coreCoverageRoundLimit) + .flatMap((round) => round.contextGraphIds), + ); + const coreCoverageWithinWindow = publicGraphs.every((graph) => + scheduledWithinWindow.has(graph.contextGraphId)); + const coreFinalPublicExact = publicGraphs.every((graph) => + exactGraph(coreFinal.get(graph.contextGraphId), graph.finalSnapshot)); + const corePrivateExcluded = privateGraphs.every((graph) => + absentGraph(coreFinal.get(graph.contextGraphId)) && !scheduled.has(graph.contextGraphId)); + + const requiredExactPlanes: Array = []; + for (const graph of corpus.graphs) { + if (graph.accessPolicy === 0 && graph.edgePolicy !== 'unselected') { + const selected = edgeSelected.get(graph.contextGraphId); + requiredExactPlanes.push([selected?.vm, graph.selectedSnapshot.vm], [selected?.swm, graph.selectedSnapshot.swm]); + const restarted = edgeRestarted.get(graph.contextGraphId); + const expected = graph.edgePolicy === 'always-on' ? graph.finalSnapshot : graph.selectedSnapshot; + requiredExactPlanes.push([restarted?.vm, expected.vm], [restarted?.swm, expected.swm]); + const secondOnDemand = edgeSecondOnDemand.get(graph.contextGraphId); + requiredExactPlanes.push( + [secondOnDemand?.vm, graph.finalSnapshot.vm], + [secondOnDemand?.swm, graph.finalSnapshot.swm], + ); + } + if (graph.accessPolicy === 0) { + const final = coreFinal.get(graph.contextGraphId); + requiredExactPlanes.push([final?.vm, graph.finalSnapshot.vm], [final?.swm, graph.finalSnapshot.swm]); + } + } + const noMetadataOnlyCompletion = requiredExactPlanes.every(([observed, expected]) => + exactPlane(observed, expected) && (observed?.dataTripleCount ?? 0) > 0); + + const checks = Object.freeze({ + schemaWellFormed: true, + provenanceMatches, + corpusDigestMatches, + corpusCanonicalOrder, + requiredPolicyCellsPresent, + publisherSnapshotsExact, + publicSecondWaveAdvances, + edgePassiveBeforeSelection, + edgeSelectedSnapshotsExact, + edgeOnDemandRemainsPointInTime, + edgeAlwaysOnRefreshesAfterRestart, + edgeOperationProvenance, + edgeSecondOnDemandConverges, + edgeUnselectedExcluded, + edgePrivateExcluded, + coreBatchMatchesManifest, + coreBatchWithinBound, + coreRoundsPublicOnly, + coreAutomaticProvenance, + coreEveryPublicScheduled, + coreCoverageWithinWindow, + coreFinalPublicExact, + corePrivateExcluded, + noMetadataOnlyCompletion, + }) satisfies SelectiveCoverageChecksV1; + const rejectReasons = CHECK_NAMES.filter((name) => !checks[name]).map((name) => REASONS[name]); + return Object.freeze({ + schema: SELECTIVE_COVERAGE_VERDICT_SCHEMA, + pass: CHECK_NAMES.every((name) => checks[name]), + checks, + missingCoreContextGraphIds: Object.freeze(missingCoreContextGraphIds), + rejectReasons: Object.freeze(rejectReasons), + recomputedCorpusDigest: computeSelectiveCoverageCorpusDigest(corpus), + }); +} + +function parseEvidence(input: unknown): SelectiveCoverageEvidenceV1 | undefined { + const root = closedRecord(input, [ + 'schema', 'provenance', 'corpus', 'publisher', 'edge', 'core', + ]); + if (!root || root.schema !== SELECTIVE_COVERAGE_EVIDENCE_SCHEMA) return undefined; + const provenance = parseProvenance(root.provenance); + const corpus = parseCorpus(root.corpus); + const publisher = closedRecord(root.publisher, ['selected', 'final']); + const edge = closedRecord(root.edge, [ + 'beforeSelection', 'afterSelection', 'afterRestart', 'afterSecondOnDemand', 'operations', + ]); + const core = closedRecord(root.core, ['automaticBatchSize', 'rounds', 'final']); + if (!provenance || !corpus || !publisher || !edge || !core) return undefined; + const publisherSelected = parseObservations(publisher.selected); + const publisherFinal = parseObservations(publisher.final); + const beforeSelection = parseEdgeObservations(edge.beforeSelection); + const afterSelection = parseEdgeObservations(edge.afterSelection); + const afterRestart = parseEdgeObservations(edge.afterRestart); + const afterSecondOnDemand = parseEdgeObservations(edge.afterSecondOnDemand); + const operations = parseEdgeOperations(edge.operations); + const final = parseCoreFinalObservations(core.final); + if (!publisherSelected || !publisherFinal || !beforeSelection || !afterSelection + || !afterRestart || !afterSecondOnDemand || !operations || !final) return undefined; + if (!nonNegativeInteger(core.automaticBatchSize)) return undefined; + if (!closedArray(core.rounds, 1, MAX_SELECTIVE_COVERAGE_ROUNDS)) return undefined; + const rounds: CoreAutomaticRoundV1[] = []; + for (let index = 0; index < core.rounds.length; index += 1) { + const row = closedRecord(core.rounds[index], [ + 'round', 'jobId', 'planningLane', 'source', 'configuredBatchSize', + 'explicitSelectedContextGraphIds', 'contextGraphIds', 'completions', + ]); + if (!row || row.round !== index + || row.source !== 'automatic-core-public' + || !positiveInteger(row.configuredBatchSize) + || !closedArray(row.explicitSelectedContextGraphIds, 0, MAX_SELECTIVE_COVERAGE_GRAPHS) + || !closedArray(row.contextGraphIds, 0, MAX_SELECTIVE_COVERAGE_GRAPHS) + || !closedArray(row.completions, 0, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; + const jobId = identifier(row.jobId); + const planningLane = identifier(row.planningLane); + if (!jobId || !planningLane) return undefined; + const explicitSelectedContextGraphIds: string[] = []; + for (const id of row.explicitSelectedContextGraphIds) { + const parsed = identifier(id); + if (!parsed) return undefined; + explicitSelectedContextGraphIds.push(parsed); + } + const ids: string[] = []; + for (const id of row.contextGraphIds) { + const parsed = identifier(id); + if (!parsed) return undefined; + ids.push(parsed); + } + const completions = []; + for (const inputCompletion of row.completions) { + const completion = closedRecord(inputCompletion, [ + 'contextGraphId', 'completedWave', 'completedSnapshot', + ]); + if (!completion || completion.completedWave !== 'final') return undefined; + const contextGraphId = identifier(completion.contextGraphId); + const completedSnapshot = parseSnapshot(completion.completedSnapshot); + if (!contextGraphId || !completedSnapshot) return undefined; + completions.push({ + contextGraphId, + completedWave: 'final' as const, + completedSnapshot, + }); + } + rounds.push({ + round: index, + jobId, + planningLane, + source: 'automatic-core-public', + configuredBatchSize: row.configuredBatchSize as number, + explicitSelectedContextGraphIds: Object.freeze(explicitSelectedContextGraphIds), + contextGraphIds: Object.freeze(ids), + completions: Object.freeze(completions), + }); + } + return { + schema: SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, + provenance, + corpus, + publisher: { selected: publisherSelected, final: publisherFinal }, + edge: { + beforeSelection, + afterSelection, + afterRestart, + afterSecondOnDemand, + operations, + }, + core: { + automaticBatchSize: core.automaticBatchSize as number, + rounds: Object.freeze(rounds), + final, + }, + }; +} + +function parseCorpus(input: unknown): SelectiveCoverageCorpusV1 | undefined { + const root = closedRecord(input, [ + 'schema', 'networkId', 'coreAutomaticBatchSize', 'coreCoverageRoundLimit', + 'graphs', 'manifestDigest', + ]); + if (!root || root.schema !== SELECTIVE_COVERAGE_CORPUS_SCHEMA) return undefined; + const networkId = identifier(root.networkId); + const manifestDigest = digest(root.manifestDigest); + if (!networkId || !manifestDigest || !positiveInteger(root.coreAutomaticBatchSize) + || !positiveInteger(root.coreCoverageRoundLimit) + || (root.coreCoverageRoundLimit as number) > MAX_SELECTIVE_COVERAGE_ROUNDS + || !closedArray(root.graphs, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; + const graphs: SelectiveCoverageGraphV1[] = []; + for (const inputGraph of root.graphs) { + const graph = closedRecord(inputGraph, [ + 'contextGraphId', 'accessPolicy', 'publishPolicy', 'edgePolicy', + 'selectedSnapshot', 'finalSnapshot', + ]); + if (!graph) return undefined; + const contextGraphId = identifier(graph.contextGraphId); + const accessPolicy = binaryPolicy(graph.accessPolicy); + const publishPolicy = binaryPolicy(graph.publishPolicy); + const edgePolicy = parseEdgePolicy(graph.edgePolicy); + const selectedSnapshot = parseSnapshot(graph.selectedSnapshot); + const finalSnapshot = parseSnapshot(graph.finalSnapshot); + if (!contextGraphId || accessPolicy === undefined || publishPolicy === undefined + || !edgePolicy || !selectedSnapshot || !finalSnapshot) return undefined; + if (accessPolicy === 1 && edgePolicy !== 'unselected') return undefined; + graphs.push({ + contextGraphId, + accessPolicy, + publishPolicy, + edgePolicy, + selectedSnapshot, + finalSnapshot, + }); + } + return { + schema: SELECTIVE_COVERAGE_CORPUS_SCHEMA, + networkId, + coreAutomaticBatchSize: root.coreAutomaticBatchSize as number, + coreCoverageRoundLimit: root.coreCoverageRoundLimit as number, + graphs: Object.freeze(graphs), + manifestDigest, + }; +} + +function parseProvenance(input: unknown): SelectiveCoverageEvidenceV1['provenance'] | undefined { + const root = closedRecord(input, [ + 'networkId', 'testedHeadCommit', 'runtimeManifestDigest', + 'publisherPeerId', 'edgePeerId', 'corePeerId', + ]); + if (!root) return undefined; + const networkId = identifier(root.networkId); + const runtimeManifestDigest = digest(root.runtimeManifestDigest); + const publisherPeerId = identifier(root.publisherPeerId); + const edgePeerId = identifier(root.edgePeerId); + const corePeerId = identifier(root.corePeerId); + if (!networkId || typeof root.testedHeadCommit !== 'string' + || !/^[0-9a-f]{40,64}$/u.test(root.testedHeadCommit) + || !runtimeManifestDigest || !publisherPeerId || !edgePeerId || !corePeerId + || new Set([publisherPeerId, edgePeerId, corePeerId]).size !== 3) return undefined; + return { + networkId, + testedHeadCommit: root.testedHeadCommit, + runtimeManifestDigest, + publisherPeerId, + edgePeerId, + corePeerId, + }; +} + +function parseExpectedProvenance( + input: unknown, +): ExpectedSelectiveCoverageProvenanceV1 | undefined { + const root = closedRecord(input, [ + 'networkId', 'testedHeadCommit', 'runtimeManifestDigest', 'corpusManifestDigest', + 'publisherPeerId', 'edgePeerId', 'corePeerId', + ]); + if (!root) return undefined; + const { corpusManifestDigest: _omitted, ...provenanceInput } = root; + const provenance = parseProvenance(provenanceInput); + const corpusManifestDigest = digest(root.corpusManifestDigest); + return provenance && corpusManifestDigest + ? { ...provenance, corpusManifestDigest } + : undefined; +} + +function parseSnapshot(input: unknown): GraphSnapshotExpectationV1 | undefined { + const root = closedRecord(input, ['vm', 'swm']); + if (!root) return undefined; + const vm = parseExpectation(root.vm); + const swm = parseExpectation(root.swm); + return vm && swm ? { vm, swm } : undefined; +} + +function parseExpectation(input: unknown): PlaneExpectationV1 | undefined { + const root = closedRecord(input, ['headDigest', 'inventoryDigest', 'assetCount', 'dataTripleCount']); + if (!root) return undefined; + const headDigest = digest(root.headDigest); + const inventoryDigest = digest(root.inventoryDigest); + if (!headDigest || !inventoryDigest || !positiveInteger(root.assetCount) + || !positiveInteger(root.dataTripleCount)) return undefined; + return { + headDigest, + inventoryDigest, + assetCount: root.assetCount as number, + dataTripleCount: root.dataTripleCount as number, + }; +} + +function parseObservations(input: unknown): readonly GraphObservationV1[] | undefined { + if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; + const result: GraphObservationV1[] = []; + for (const inputRow of input) { + const row = closedRecord(inputRow, ['contextGraphId', 'vm', 'swm']); + if (!row) return undefined; + const contextGraphId = identifier(row.contextGraphId); + const vm = parseObservation(row.vm); + const swm = parseObservation(row.swm); + if (!contextGraphId || !vm || !swm) return undefined; + result.push({ contextGraphId, vm, swm }); + } + return Object.freeze(result); +} + +function parseEdgeObservations(input: unknown): readonly EdgeGraphObservationV1[] | undefined { + if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; + const result: EdgeGraphObservationV1[] = []; + for (const inputRow of input) { + const row = closedRecord(inputRow, [ + 'contextGraphId', 'runtimeSyncMode', 'producingJobId', 'vm', 'swm', + ]); + if (!row) return undefined; + const contextGraphId = identifier(row.contextGraphId); + const vm = parseObservation(row.vm); + const swm = parseObservation(row.swm); + const runtimeSyncMode = row.runtimeSyncMode; + const producingJobId = row.producingJobId === null ? null : identifier(row.producingJobId); + if (!contextGraphId || !vm || !swm + || producingJobId === undefined + || (runtimeSyncMode !== null && runtimeSyncMode !== 'on-demand' + && runtimeSyncMode !== 'always-on')) return undefined; + result.push({ contextGraphId, runtimeSyncMode, producingJobId, vm, swm }); + } + return Object.freeze(result); +} + +function parseEdgeOperations(input: unknown): readonly EdgeSyncOperationV1[] | undefined { + if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS * 2)) return undefined; + const result: EdgeSyncOperationV1[] = []; + for (let index = 0; index < input.length; index += 1) { + const row = closedRecord(input[index], [ + 'sequence', 'phase', 'source', 'syncMode', 'contextGraphId', 'jobId', + 'completedWave', 'completedSnapshot', + ]); + if (!row || row.sequence !== index) return undefined; + const phase = row.phase; + const source = row.source; + const syncMode = row.syncMode; + const contextGraphId = identifier(row.contextGraphId); + const jobId = identifier(row.jobId); + const completedWave = row.completedWave; + const completedSnapshot = parseSnapshot(row.completedSnapshot); + if ((phase !== 'selection' && phase !== 'post-restart-auto' + && phase !== 'post-restart-explicit') + || (source !== 'reconciler' && source !== 'user') + || (syncMode !== 'always-on' && syncMode !== 'on-demand') + || (completedWave !== 'selected' && completedWave !== 'final') + || !completedSnapshot || !contextGraphId || !jobId) return undefined; + result.push({ + sequence: index, + phase, + source, + syncMode, + contextGraphId, + jobId, + completedWave, + completedSnapshot, + }); + } + return Object.freeze(result); +} + +function parseCoreFinalObservations( + input: unknown, +): readonly CoreFinalObservationV1[] | undefined { + if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; + const result: CoreFinalObservationV1[] = []; + for (const inputRow of input) { + const row = closedRecord(inputRow, ['contextGraphId', 'automaticJobIds', 'vm', 'swm']); + if (!row || !closedArray(row.automaticJobIds, 0, MAX_SELECTIVE_COVERAGE_ROUNDS)) { + return undefined; + } + const contextGraphId = identifier(row.contextGraphId); + const vm = parseObservation(row.vm); + const swm = parseObservation(row.swm); + const automaticJobIds: string[] = []; + for (const inputJobId of row.automaticJobIds) { + const jobId = identifier(inputJobId); + if (!jobId) return undefined; + automaticJobIds.push(jobId); + } + if (!contextGraphId || !vm || !swm + || new Set(automaticJobIds).size !== automaticJobIds.length) return undefined; + result.push({ contextGraphId, automaticJobIds: Object.freeze(automaticJobIds), vm, swm }); + } + return Object.freeze(result); +} + +function parseObservation(input: unknown): PlaneObservationV1 | undefined { + const root = closedRecord(input, [ + 'reportedComplete', 'headDigest', 'inventoryDigest', 'assetCount', + 'metadataTripleCount', 'dataTripleCount', + ]); + if (!root || typeof root.reportedComplete !== 'boolean' + || !nonNegativeInteger(root.assetCount) + || !nonNegativeInteger(root.metadataTripleCount) + || !nonNegativeInteger(root.dataTripleCount)) return undefined; + const headDigest = root.headDigest === null ? null : digest(root.headDigest); + const inventoryDigest = root.inventoryDigest === null ? null : digest(root.inventoryDigest); + if (headDigest === undefined || inventoryDigest === undefined) return undefined; + return { + reportedComplete: root.reportedComplete, + headDigest, + inventoryDigest, + assetCount: root.assetCount as number, + metadataTripleCount: root.metadataTripleCount as number, + dataTripleCount: root.dataTripleCount as number, + }; +} + +function hasRequiredPolicyCells(graphs: readonly SelectiveCoverageGraphV1[]): boolean { + return graphs.some((graph) => graph.accessPolicy === 0 && graph.edgePolicy === 'on-demand') + && graphs.some((graph) => graph.accessPolicy === 0 && graph.edgePolicy === 'always-on') + && graphs.some((graph) => graph.accessPolicy === 0 && graph.edgePolicy === 'unselected') + && graphs.some((graph) => graph.accessPolicy === 0 && graph.publishPolicy === 1) + && graphs.some((graph) => graph.accessPolicy === 0 && graph.publishPolicy === 0) + && graphs.some((graph) => graph.accessPolicy === 1 && graph.publishPolicy === 1) + && graphs.some((graph) => graph.accessPolicy === 1 && graph.publishPolicy === 0); +} + +function verifyEdgeOperations( + operations: readonly EdgeSyncOperationV1[], + graphs: readonly SelectiveCoverageGraphV1[], +): boolean { + const selected = graphs.filter((graph) => + graph.accessPolicy === 0 && graph.edgePolicy !== 'unselected'); + if (operations.length !== selected.length * 2 + || new Set(operations.map((operation) => operation.jobId)).size !== operations.length) { + return false; + } + const selectionSequences = operations + .filter((operation) => operation.phase === 'selection') + .map((operation) => operation.sequence); + const automaticSequences = operations + .filter((operation) => operation.phase === 'post-restart-auto') + .map((operation) => operation.sequence); + const secondOnDemandSequences = operations + .filter((operation) => operation.phase === 'post-restart-explicit') + .map((operation) => operation.sequence); + if (selectionSequences.length === 0 + || Math.max(...selectionSequences) >= Math.min(...automaticSequences) + || Math.max(...automaticSequences) >= Math.min(...secondOnDemandSequences)) return false; + return selected.every((graph) => { + const graphOperations = operations.filter((operation) => + operation.contextGraphId === graph.contextGraphId); + if (graphOperations.length !== 2) return false; + const selection = graphOperations.find((operation) => operation.phase === 'selection'); + if (selection?.source !== 'user' || selection.syncMode !== graph.edgePolicy + || selection.completedWave !== 'selected' + || !exactSnapshot(selection.completedSnapshot, graph.selectedSnapshot)) return false; + if (graph.edgePolicy === 'on-demand') { + const refresh = graphOperations.find((operation) => + operation.phase === 'post-restart-explicit'); + return refresh?.source === 'user' && refresh.syncMode === 'on-demand' + && refresh.completedWave === 'final' + && exactSnapshot(refresh.completedSnapshot, graph.finalSnapshot); + } + const refresh = graphOperations.find((operation) => + operation.phase === 'post-restart-auto'); + return refresh?.source === 'reconciler' && refresh.syncMode === 'always-on' + && refresh.completedWave === 'final' + && exactSnapshot(refresh.completedSnapshot, graph.finalSnapshot); + }) && operations.every((operation) => { + const graph = graphs.find((candidate) => + candidate.contextGraphId === operation.contextGraphId); + return graph?.accessPolicy === 0 && graph.edgePolicy !== 'unselected'; + }); +} + +function edgeJobId( + operations: readonly EdgeSyncOperationV1[], + contextGraphId: string, + phase: EdgeSyncOperationV1['phase'], +): string | undefined { + return operations.find((operation) => + operation.contextGraphId === contextGraphId && operation.phase === phase)?.jobId; +} + +function exactGraph( + observed: GraphObservationV1 | undefined, + expected: GraphSnapshotExpectationV1, +): boolean { + return exactPlane(observed?.vm, expected.vm) && exactPlane(observed?.swm, expected.swm); +} + +function exactPlane( + observed: PlaneObservationV1 | undefined, + expected: PlaneExpectationV1, +): boolean { + return observed?.reportedComplete === true + && observed.headDigest === expected.headDigest + && observed.inventoryDigest === expected.inventoryDigest + && observed.assetCount === expected.assetCount + && observed.metadataTripleCount > 0 + && observed.dataTripleCount === expected.dataTripleCount; +} + +function absentGraph(observed: GraphObservationV1 | undefined): boolean { + return absentPlane(observed?.vm) && absentPlane(observed?.swm); +} + +function absentPlane(observed: PlaneObservationV1 | undefined): boolean { + return observed?.reportedComplete === false + && observed.headDigest === null + && observed.inventoryDigest === null + && observed.assetCount === 0 + && observed.dataTripleCount === 0; +} + +function exactSnapshot( + left: GraphSnapshotExpectationV1, + right: GraphSnapshotExpectationV1, +): boolean { + return exactExpectation(left.vm, right.vm) && exactExpectation(left.swm, right.swm); +} + +function exactExpectation(left: PlaneExpectationV1, right: PlaneExpectationV1): boolean { + return left.headDigest === right.headDigest + && left.inventoryDigest === right.inventoryDigest + && left.assetCount === right.assetCount + && left.dataTripleCount === right.dataTripleCount; +} + +function snapshotsAdvance( + left: GraphSnapshotExpectationV1, + right: GraphSnapshotExpectationV1, +): boolean { + return left.vm.headDigest !== right.vm.headDigest + && left.vm.inventoryDigest !== right.vm.inventoryDigest + && left.swm.headDigest !== right.swm.headDigest + && left.swm.inventoryDigest !== right.swm.inventoryDigest + && right.vm.assetCount > left.vm.assetCount + && right.vm.dataTripleCount > left.vm.dataTripleCount + && right.swm.assetCount > left.swm.assetCount + && right.swm.dataTripleCount > left.swm.dataTripleCount; +} + +function byObservationId(rows: readonly T[]): Map { + return new Map(rows.map((row) => [row.contextGraphId, row])); +} + +function exactCanonicalIds(actual: readonly string[], expected: readonly string[]): boolean { + return actual.length === expected.length && actual.every((id, index) => id === expected[index]); +} + +function strictlyIncreasing(values: readonly string[]): boolean { + return values.every((value, index) => index === 0 || values[index - 1]! < value); +} + +function closedRecord( + value: unknown, + keys: readonly string[], +): Record | undefined { + if (value === null || typeof value !== 'object' || Array.isArray(value) + || Object.getPrototypeOf(value) !== Object.prototype) return undefined; + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.some((key) => typeof key !== 'string')) return undefined; + const actual = (ownKeys as string[]).sort(); + const expected = [...keys].sort(); + if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { + return undefined; + } + if (actual.some((key) => { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + return !descriptor?.enumerable || !('value' in descriptor); + })) return undefined; + return value as Record; +} + +function closedArray(value: unknown, minimum: number, maximum: number): value is unknown[] { + if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype + || value.length < minimum || value.length > maximum) return false; + const expected = new Set(['length']); + for (let index = 0; index < value.length; index += 1) expected.add(String(index)); + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.length !== expected.size || ownKeys.some((key) => !expected.has(key))) return false; + for (let index = 0; index < value.length; index += 1) { + const descriptor = Object.getOwnPropertyDescriptor(value, String(index)); + if (!descriptor?.enumerable || !('value' in descriptor)) return false; + } + return true; +} + +function identifier(value: unknown): string | undefined { + return typeof value === 'string' && value.length <= 256 && ID.test(value) ? value : undefined; +} + +function digest(value: unknown): string | undefined { + return typeof value === 'string' && DIGEST.test(value) ? value : undefined; +} + +function binaryPolicy(value: unknown): 0 | 1 | undefined { + return value === 0 || value === 1 ? value : undefined; +} + +function parseEdgePolicy(value: unknown): EdgeCoveragePolicy | undefined { + return value === 'on-demand' || value === 'always-on' || value === 'unselected' + ? value + : undefined; +} + +function nonNegativeInteger(value: unknown): value is number { + return Number.isSafeInteger(value) && (value as number) >= 0; +} + +function positiveInteger(value: unknown): value is number { + return Number.isSafeInteger(value) && (value as number) > 0; +} + +function schemaReject(): SelectiveCoverageVerdictV1 { + const checks = Object.freeze(Object.fromEntries( + CHECK_NAMES.map((name) => [name, false]), + )) as Readonly; + return Object.freeze({ + schema: SELECTIVE_COVERAGE_VERDICT_SCHEMA, + pass: false, + checks, + missingCoreContextGraphIds: Object.freeze([]), + rejectReasons: Object.freeze([REASONS.schemaWellFormed]), + recomputedCorpusDigest: '', + }); +} diff --git a/package.json b/package.json index c898db7014..b8bad70a96 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,8 @@ "test:m1:rfc64-public-swm-parity:verify": "node --import tsx devnet/rfc64-cp1-public-swm-parity/verify-live.ts", "test:m1:rfc64-public-swm-parity:unit": "node --import tsx --test devnet/rfc64-cp1-public-swm-parity/verifier.test.ts", "typecheck:m1:rfc64-public-swm-parity": "tsc --project devnet/rfc64-cp1-public-swm-parity/tsconfig.json", + "test:m1:rfc64-selective-coverage:unit": "node --experimental-strip-types --test devnet/rfc64-m1-selective-coverage/verifier.test.ts", + "typecheck:m1:rfc64-selective-coverage": "tsc --project devnet/rfc64-m1-selective-coverage/tsconfig.json", "test:devnet:v10-core-flows": "vitest run --config devnet/v10-core-flows/vitest.config.ts", "test:devnet:v10-e2e": "vitest run --config devnet/v10-end-to-end/vitest.config.ts", "test:devnet:v10-stress": "vitest run --config devnet/v10-stress/vitest.config.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b40b2e451..07e1e795a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -351,6 +351,8 @@ importers: devnet/rfc64-m0-public-baseline: {} + devnet/rfc64-m1-selective-coverage: {} + devnet/rfc64-persistence-lifecycle: dependencies: '@origintrail-official/dkg-agent': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index aba8272f79..5d9f97e88a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,6 +24,7 @@ packages: - "devnet/rfc64-gate1-public-open" - "devnet/rfc64-gate2-multi-asset-completeness" - "devnet/rfc64-cp1-public-swm-parity" + - "devnet/rfc64-m1-selective-coverage" - "devnet/rfc64-m0-public-baseline" - "devnet/pr1386-term-canon" - "devnet/pr1385-subgraph-rs" From 7bda7fc86a55dff2b9c025d1c876ccdcb11530a3 Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 04:09:18 +0200 Subject: [PATCH 02/14] test(rfc64): launch selective coverage evidence --- devnet/rfc64-m1-selective-coverage/README.md | 119 +++- .../launch-live.ts | 122 ++++ .../live-runner.ts | 35 ++ .../rfc64-m1-selective-coverage/package.json | 4 +- .../process-runtime-fixture.mjs | 39 ++ .../process-runtime.test.ts | 63 ++ .../process-runtime.ts | 289 +++++++++ .../runtime.test.ts | 561 ++++++++++++++++++ devnet/rfc64-m1-selective-coverage/runtime.ts | 418 +++++++++++++ .../sync-coverage-journal.ts | 159 +++++ .../rfc64-m1-selective-coverage/tsconfig.json | 2 +- .../verify-live.ts | 38 ++ package.json | 5 +- 13 files changed, 1848 insertions(+), 6 deletions(-) create mode 100644 devnet/rfc64-m1-selective-coverage/launch-live.ts create mode 100644 devnet/rfc64-m1-selective-coverage/live-runner.ts create mode 100644 devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs create mode 100644 devnet/rfc64-m1-selective-coverage/process-runtime.test.ts create mode 100644 devnet/rfc64-m1-selective-coverage/process-runtime.ts create mode 100644 devnet/rfc64-m1-selective-coverage/runtime.test.ts create mode 100644 devnet/rfc64-m1-selective-coverage/runtime.ts create mode 100644 devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts create mode 100644 devnet/rfc64-m1-selective-coverage/verify-live.ts diff --git a/devnet/rfc64-m1-selective-coverage/README.md b/devnet/rfc64-m1-selective-coverage/README.md index 67007a5ca1..c1cf7217c7 100644 --- a/devnet/rfc64-m1-selective-coverage/README.md +++ b/devnet/rfc64-m1-selective-coverage/README.md @@ -1,8 +1,10 @@ # RFC-64 M1 selective coverage evidence -This directory defines the closed, deterministic evidence boundary for the M1 -three-process devnet harness. The process launcher is intentionally separate; -it will populate this contract from real Publisher, Edge, and Core processes. +This directory defines and launches the closed, deterministic evidence boundary +for the M1 three-process devnet harness. An operator-owned adapter controls real +Publisher, Edge, and Core processes; the repository-owned collector controls +the phase order, checks process identity, and refuses to publish an artifact +until the fail-closed verifier accepts every observation. The verifier passes only when all of these user-visible outcomes are proven: @@ -36,6 +38,117 @@ metadata are not corpus payload. Metadata-only responses can never satisfy a required plane: `reportedComplete` is treated as an assertion, and the verifier independently requires exact nonzero data, counts, heads, and inventory roots. +## Runtime sequence + +The launcher deliberately starts Core cold, after the second publication wave: + +1. Start Publisher and Edge as distinct OS processes and verify their peer, + network, commit, and loaded-runtime identities against the trust anchor. +2. Publish the selected wave and read exact Publisher VM and SWM snapshots. +3. Prove the Edge has no payload, then issue only the anchored on-demand and + always-on user selections and retain their returned job IDs. +4. Publish the final wave, restart the Edge into a new OS process, and require + its actual reconciler record for always-on refresh. Observe on-demand data + still at the selected snapshot with no active runtime mode. +5. Issue the second explicit on-demand request and require exact final state. +6. Start Core cold, consume scheduler-issued automatic rounds only, and require + every public graph to enter the bounded window and reach exact final state. + +The collector always stops every role it attempted to start. If collection, +node cleanup, adapter shutdown, or adapter exit fails, it does not write a +passing evidence artifact. Artifact publication therefore happens only after +the controlling adapter and all controlled node processes have shut down. + +## Exact plane observations + +For each VM and SWM plane the adapter queries the complete scoped KA inventory +and builds `Rfc64SemanticSnapshotV1` using +`devnet/_bootstrap/rfc64-evidence.ts`. The mapping is fixed: + +- `headDigest` = `ualsSha256`; +- `inventoryDigest` = `semanticNQuadsSha256`; +- `assetCount` = `kaCount`; +- `dataTripleCount` = `quadCount`; +- `metadataTripleCount` = the separately queried scoped metadata row count. + +An empty or metadata-only plane must use the absent representation; it must not +set `reportedComplete=true`. The adapter may not derive the expected snapshot +from Edge or Core. Expected snapshots come from the immutable corpus file and +must match Publisher-owned observations. + +## Operator adapter protocol + +Set `DKG_RFC64_M1_ADAPTER_COMMAND` and, optionally, +`DKG_RFC64_M1_ADAPTER_ARGS_JSON` to an executable that reads one JSON command +per stdin line. It may write ordinary logs, but evidence results use exactly: + +```text +DKG_RFC64_M1_RESULT {"schema":"dkg-rfc64-m1-selective-coverage-runtime-result-v1",...} +``` + +Every command carries the runtime protocol, a launcher-generated 256-bit +`sessionNonce`, a monotonic `sequence`, a command name, and a payload. The +adapter must return the same nonce and sequence with either +`{"ok":true,"value":...}` or `{"ok":false,"error":"..."}`. A result or log +line larger than 1 MiB is rejected, preventing an unbounded stdout buffer. +Commands are: + +| Command | Required runtime action/evidence | +| --- | --- | +| `start` | Start the named role; independently return PID, process instance/start/wave IDs, durable-directory identity, peer ID, network, commit, and loaded-runtime digest. The command contains only the role, never the trust anchor. | +| `publish-wave` | Publish the named anchored wave; return exact Publisher VM/SWM observations for every graph. | +| `observe-edge` | Return exact Edge VM/SWM observations, effective runtime mode, and the actual producing job ID. | +| `synchronize-edge` | Issue only the named explicit user selection; return its real job ID and terminal exact snapshot. | +| `restart-edge` | Stop and restart Edge from the same durable data directory; return a receipt binding the old process instance/PID and observed exit to the new process instance/PID, stable directory identity, and peer identity. | +| `wait-edge-reconciler` | Wait/read only; return source, mode, job ID, completed snapshot, and the post-restart journal reference without receiving those conclusions in the command. | +| `core-automatic-round` | Return `round` with the frozen scheduler plan and a journal snapshot/reference proving its actual planned IDs, lane, job ID, empty explicit selection, and per-CG terminal completions. | +| `observe-core-final` | Return exact final VM/SWM observations and the automatic job IDs that produced each graph. | +| `stop` / `shutdown` | Stop one role / close the controlling adapter. Acknowledgement means all controlled node processes have exited; the launcher waits for adapter exit, then escalates from `SIGTERM` to `SIGKILL` on timeout. | + +The adapter reads automatic provenance from the node-admin-only endpoint +`GET /api/diagnostics/sync-coverage-evidence?afterSequence=N`. The launcher +requires schema version 1, an in-window terminal entry, `evidenceTruncated=false`, +and all metadata/durable/shared-memory verification bits. It binds: + +- `edge-reconciler-job` entries to the actual job ID, context graph, + `source=reconciler`, `trigger=periodic-reconciler`, and + `syncMode=always-on`; +- `core-automatic-round` entries to the actual job ID, planning lane, + configured batch, frozen explicit/automatic ID lists, and every terminal + per-CG completion. + +`droppedBeforeSequence` and `nextSequence` prove the selected entry was not +overwritten. Any truncated, missing, nonterminal, or mismatched record fails the +run. Synthetic job IDs or inference from final store contents are not acceptable +substitutes. The admin subscriptions response supplies the effective Edge +`syncMode`; omission retains the legacy `always-on` interpretation only inside +the node, never as evidence for a requested on-demand selection. + +This repository supplies the fail-closed orchestrator and framed adapter +protocol, not a deployment-specific adapter executable. The live command is +therefore intentionally blocked unless `DKG_RFC64_M1_ADAPTER_COMMAND` names an +operator-reviewed implementation. The launcher removes corpus, trust-anchor, +and output paths from the adapter environment; the adapter must report runtime +identity from the processes it launched, not echo expected values. + +## Running the live gate + +The corpus and trust anchor are separate, pre-existing operator inputs. The +launcher cleans and rebuilds the runtime closure, recomputes its manifest, and +requires it to match the trust anchor before starting any node: + +```sh +export DKG_RFC64_M1_CORPUS_FILE=/secure/operator/m1-corpus.json +export DKG_RFC64_M1_TRUST_ANCHOR_FILE=/secure/operator/m1-trust-anchor.json +export DKG_RFC64_M1_ADAPTER_COMMAND=/secure/operator/dkg-m1-adapter +export DKG_RFC64_M1_ADAPTER_ARGS_JSON='[]' +pnpm test:m1:rfc64-selective-coverage +``` + +The trust anchor contains `networkId`, exact `testedHeadCommit`, computed +`runtimeManifestDigest`, `corpusManifestDigest`, and the three expected peer +IDs. Do not generate or overwrite it from receiver output during the run. + Run the bounded contract checks with: ```sh diff --git a/devnet/rfc64-m1-selective-coverage/launch-live.ts b/devnet/rfc64-m1-selective-coverage/launch-live.ts new file mode 100644 index 0000000000..84c9bf3e3d --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/launch-live.ts @@ -0,0 +1,122 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { + atomicWriteExactBytes, + readCleanRepositoryHead, +} from '../rfc64-persistence-lifecycle/evidence.ts'; +import { + buildGate2RuntimeManifestV1, + runGate2CleanRuntimeBuildV1, +} from '../rfc64-gate2-multi-asset-completeness/runtime-provenance.ts'; +import { + canonicalJson, + type ExpectedSelectiveCoverageProvenanceV1, + type SelectiveCoverageCorpusV1, +} from './manifest.ts'; +import { ProcessSelectiveCoverageRuntimeV1 } from './process-runtime.ts'; +import { collectSelectiveCoverageEvidenceV1 } from './runtime.ts'; +import { runSelectiveCoverageLiveV1 } from './live-runner.ts'; + +const repoRoot = resolve(import.meta.dirname, '../..'); +const corpusPath = resolveRequiredPath('DKG_RFC64_M1_CORPUS_FILE'); +const trustAnchorPath = resolveRequiredPath('DKG_RFC64_M1_TRUST_ANCHOR_FILE'); +const artifactPath = resolve( + process.env['DKG_RFC64_M1_ARTIFACT'] + ?? resolve(import.meta.dirname, 'artifacts/selective-coverage-evidence.json'), +); +const adapterCommand = requiredEnvironment('DKG_RFC64_M1_ADAPTER_COMMAND'); +const adapterArgs = parseStringArray( + process.env['DKG_RFC64_M1_ADAPTER_ARGS_JSON'] ?? '[]', + 'DKG_RFC64_M1_ADAPTER_ARGS_JSON', +); +const adapterCwd = resolve(process.env['DKG_RFC64_M1_ADAPTER_CWD'] ?? repoRoot); +const timeoutMs = parseTimeout(process.env['DKG_RFC64_M1_ADAPTER_TIMEOUT_MS']); + +const corpus = readJson(corpusPath) as SelectiveCoverageCorpusV1; +const expectedProvenance = readJson(trustAnchorPath) as ExpectedSelectiveCoverageProvenanceV1; +const sourceCommit = readCleanRepositoryHead(repoRoot); +if (sourceCommit !== expectedProvenance.testedHeadCommit) { + throw new Error('M1 trust anchor names a different checked-out source commit'); +} +runGate2CleanRuntimeBuildV1(repoRoot); +if (readCleanRepositoryHead(repoRoot) !== sourceCommit) { + throw new Error('M1 source HEAD changed during the clean runtime build'); +} +const runtimeManifest = buildGate2RuntimeManifestV1(repoRoot, sourceCommit); +if (runtimeManifest.manifestDigest !== expectedProvenance.runtimeManifestDigest) { + throw new Error('M1 trust anchor names a different clean runtime manifest'); +} + +const runtime = new ProcessSelectiveCoverageRuntimeV1({ + command: adapterCommand, + args: adapterArgs, + cwd: adapterCwd, + timeoutMs, + env: adapterEnvironment(), +}); +await runSelectiveCoverageLiveV1({ + collect: () => collectSelectiveCoverageEvidenceV1({ + corpus, + expectedProvenance, + runtime, + }), + close: () => runtime.close(), + publish: (evidence) => { + const bytes = Buffer.from(`${canonicalJson(evidence)}\n`, 'utf8'); + const published = atomicWriteExactBytes(artifactPath, bytes); + process.stdout.write( + `[rfc64-m1] PASS ${artifactPath} sha256:${published.sha256}\n`, + ); + }, +}); + +function requiredEnvironment(name: string): string { + const value = process.env[name]?.trim(); + if (!value) throw new Error(`${name} is required`); + return value; +} + +function resolveRequiredPath(name: string): string { + return resolve(requiredEnvironment(name)); +} + +function readJson(path: string): unknown { + try { + return JSON.parse(readFileSync(path, 'utf8')); + } catch (error) { + throw new Error(`Could not read M1 JSON input: ${path}`, { cause: error }); + } +} + +function parseStringArray(value: string, label: string): string[] { + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch (error) { + throw new Error(`${label} must be a JSON string array`, { cause: error }); + } + if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== 'string')) { + throw new TypeError(`${label} must be a JSON string array`); + } + return parsed; +} + +function parseTimeout(value: string | undefined): number | undefined { + if (value === undefined || value.trim() === '') return undefined; + const parsed = Number(value); + if (!Number.isSafeInteger(parsed)) { + throw new TypeError('DKG_RFC64_M1_ADAPTER_TIMEOUT_MS must be an integer'); + } + return parsed; +} + +function adapterEnvironment(): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { ...process.env, NODE_ENV: 'production' }; + for (const name of [ + 'DKG_RFC64_M1_CORPUS_FILE', + 'DKG_RFC64_M1_TRUST_ANCHOR_FILE', + 'DKG_RFC64_M1_ARTIFACT', + ]) delete env[name]; + return env; +} diff --git a/devnet/rfc64-m1-selective-coverage/live-runner.ts b/devnet/rfc64-m1-selective-coverage/live-runner.ts new file mode 100644 index 0000000000..f2787e9bc9 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/live-runner.ts @@ -0,0 +1,35 @@ +/** + * Publish only after both collection and controller shutdown succeed. This + * keeps a fresh PASS artifact from surviving a failed process cleanup. + */ +export async function runSelectiveCoverageLiveV1(input: { + readonly collect: () => Promise; + readonly close: () => Promise; + readonly publish: (value: T) => Promise | void; +}): Promise { + let value: T | undefined; + let collectionFailure: unknown; + try { + value = await input.collect(); + } catch (error) { + collectionFailure = error; + } + + let closeFailure: unknown; + try { + await input.close(); + } catch (error) { + closeFailure = error; + } + + if (collectionFailure !== undefined && closeFailure !== undefined) { + throw new AggregateError( + [collectionFailure, closeFailure], + 'M1 collection and runtime shutdown both failed', + ); + } + if (collectionFailure !== undefined) throw collectionFailure; + if (closeFailure !== undefined) throw closeFailure; + if (value === undefined) throw new Error('M1 collection returned no evidence'); + await input.publish(value); +} diff --git a/devnet/rfc64-m1-selective-coverage/package.json b/devnet/rfc64-m1-selective-coverage/package.json index e8e563ac43..6802356548 100644 --- a/devnet/rfc64-m1-selective-coverage/package.json +++ b/devnet/rfc64-m1-selective-coverage/package.json @@ -6,6 +6,8 @@ "description": "Fail-closed evidence contract for RFC-64 M1 Edge selection and bounded Core public coverage.", "scripts": { "typecheck": "tsc --noEmit -p tsconfig.json", - "test": "node --experimental-strip-types --test verifier.test.ts" + "test": "node --experimental-strip-types --test verifier.test.ts runtime.test.ts process-runtime.test.ts", + "live": "node --import tsx launch-live.ts", + "verify-live": "node --import tsx verify-live.ts" } } diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs b/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs new file mode 100644 index 0000000000..12b3b1bce8 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs @@ -0,0 +1,39 @@ +import { createInterface } from 'node:readline'; + +const commandSchema = 'dkg-rfc64-m1-selective-coverage-runtime-command-v1'; +const resultSchema = 'dkg-rfc64-m1-selective-coverage-runtime-result-v1'; +const protocol = 'dkg-rfc64-m1-selective-coverage-runtime-v1'; +const prefix = 'DKG_RFC64_M1_RESULT '; +const lines = createInterface({ input: process.stdin, crlfDelay: Infinity }); + +lines.on('line', (line) => { + const input = JSON.parse(line); + if (input.schema !== commandSchema || input.protocol !== protocol) process.exit(12); + let value = null; + if (input.command === 'start') { + if (Object.keys(input.payload).sort().join(',') !== 'role') process.exit(13); + const role = input.payload.role; + value = { + protocol, + role, + pid: process.pid, + peerId: `${role}-peer`, + networkId: process.env.FIXTURE_NETWORK_ID, + testedHeadCommit: process.env.FIXTURE_SOURCE_COMMIT, + runtimeManifestDigest: process.env.FIXTURE_RUNTIME_MANIFEST, + processStartedAt: 100, + processInstanceId: `${role}-instance`, + dataDirectoryIdentity: `${role}-data`, + evidenceWaveId: `${role}-wave`, + }; + } + process.stdout.write(`${prefix}${JSON.stringify({ + schema: resultSchema, + protocol, + sessionNonce: input.sessionNonce, + sequence: input.sequence, + ok: true, + value, + })}\n`); + if (input.command === 'shutdown') process.exit(0); +}); diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts new file mode 100644 index 0000000000..2f71b5a330 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts @@ -0,0 +1,63 @@ +import assert from 'node:assert/strict'; +import { resolve } from 'node:path'; +import test from 'node:test'; + +import { + createSelectiveCoverageCorpus, + type ExpectedSelectiveCoverageProvenanceV1, +} from './manifest.ts'; +import { ProcessSelectiveCoverageRuntimeV1 } from './process-runtime.ts'; +import { SELECTIVE_COVERAGE_RUNTIME_PROTOCOL } from './runtime.ts'; + +test('exchanges sequence-bound JSON without sending the trust anchor to the adapter', async () => { + const runtime = new ProcessSelectiveCoverageRuntimeV1({ + command: process.execPath, + args: [resolve(import.meta.dirname, 'process-runtime-fixture.mjs')], + cwd: resolve(import.meta.dirname, '../..'), + timeoutMs: 5_000, + env: { + ...process.env, + FIXTURE_NETWORK_ID: 'otp:20430', + FIXTURE_SOURCE_COMMIT: 'a'.repeat(40), + FIXTURE_RUNTIME_MANIFEST: `sha256:${'b'.repeat(64)}`, + }, + }); + const corpus = createSelectiveCoverageCorpus({ + networkId: 'otp:20430', + coreAutomaticBatchSize: 1, + coreCoverageRoundLimit: 1, + graphs: [{ + contextGraphId: '0x1111111111111111111111111111111111111111/public', + accessPolicy: 0, + publishPolicy: 1, + edgePolicy: 'on-demand', + selectedSnapshot: { + vm: { headDigest: `sha256:${'1'.repeat(64)}`, inventoryDigest: `sha256:${'2'.repeat(64)}`, assetCount: 1, dataTripleCount: 1 }, + swm: { headDigest: `sha256:${'3'.repeat(64)}`, inventoryDigest: `sha256:${'4'.repeat(64)}`, assetCount: 1, dataTripleCount: 1 }, + }, + finalSnapshot: { + vm: { headDigest: `sha256:${'5'.repeat(64)}`, inventoryDigest: `sha256:${'6'.repeat(64)}`, assetCount: 2, dataTripleCount: 2 }, + swm: { headDigest: `sha256:${'7'.repeat(64)}`, inventoryDigest: `sha256:${'8'.repeat(64)}`, assetCount: 2, dataTripleCount: 2 }, + }, + }], + }); + const expected: ExpectedSelectiveCoverageProvenanceV1 = { + networkId: corpus.networkId, + testedHeadCommit: 'a'.repeat(40), + runtimeManifestDigest: `sha256:${'b'.repeat(64)}`, + corpusManifestDigest: corpus.manifestDigest, + publisherPeerId: 'publisher-peer', + edgePeerId: 'edge-peer', + corePeerId: 'core-peer', + }; + try { + const ready = await runtime.start('publisher'); + assert.equal(ready.protocol, SELECTIVE_COVERAGE_RUNTIME_PROTOCOL); + assert.equal(ready.role, 'publisher'); + assert.equal(ready.peerId, 'publisher-peer'); + assert.ok(ready.pid > 0); + await runtime.stop('publisher'); + } finally { + await runtime.close(); + } +}); diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime.ts b/devnet/rfc64-m1-selective-coverage/process-runtime.ts new file mode 100644 index 0000000000..a7dc1325d6 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/process-runtime.ts @@ -0,0 +1,289 @@ +import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'; +import { randomBytes } from 'node:crypto'; + +import { + SELECTIVE_COVERAGE_RUNTIME_PROTOCOL, + type SelectiveCoverageEdgeRestartReceiptV1, + type SelectiveCoverageRuntimeReadyV1, + type SelectiveCoverageRuntimeRole, + type SelectiveCoverageRuntimeV1, +} from './runtime.ts'; +import { + type CoreAutomaticRoundV1, + type CoreFinalObservationV1, + type EdgeGraphObservationV1, + type EdgeSyncOperationV1, + type GraphObservationV1, +} from './manifest.ts'; +import type { SyncCoverageJournalReferenceV1 } from './sync-coverage-journal.ts'; + +export const SELECTIVE_COVERAGE_RUNTIME_COMMAND_SCHEMA = + 'dkg-rfc64-m1-selective-coverage-runtime-command-v1' as const; +export const SELECTIVE_COVERAGE_RUNTIME_RESULT_SCHEMA = + 'dkg-rfc64-m1-selective-coverage-runtime-result-v1' as const; +export const SELECTIVE_COVERAGE_RUNTIME_RESULT_PREFIX = 'DKG_RFC64_M1_RESULT '; +const MAX_RESULT_LINE_BYTES = 1024 * 1024; +const CLOSE_GRACE_MS = 5_000; + +interface PendingRequest { + readonly resolve: (value: unknown) => void; + readonly reject: (error: Error) => void; + readonly timer: ReturnType; +} + +/** + * JSON-lines bridge to an operator-owned adapter that controls three real DKG + * processes. Ordinary adapter logs may use stdout; only prefixed result lines + * are parsed as evidence-bearing responses. + */ +export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRuntimeV1 { + private readonly child: ChildProcessWithoutNullStreams; + private readonly pending = new Map(); + private sequence = 0; + private closed = false; + private closing = false; + private exitError: Error | undefined; + private stdoutBuffer = Buffer.alloc(0); + private readonly sessionNonce = randomBytes(32).toString('hex'); + private readonly exited: Promise; + + constructor(input: { + readonly command: string; + readonly args?: readonly string[]; + readonly cwd: string; + readonly env?: NodeJS.ProcessEnv; + readonly timeoutMs?: number; + }) { + if (!input.command.trim()) throw new TypeError('M1 runtime adapter command is empty'); + const timeoutMs = input.timeoutMs ?? 120_000; + if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > 3_600_000) { + throw new RangeError('M1 runtime adapter timeout is outside 1s..1h'); + } + this.timeoutMs = timeoutMs; + this.child = spawn(input.command, [...(input.args ?? [])], { + cwd: input.cwd, + env: input.env, + stdio: ['pipe', 'pipe', 'pipe'], + }); + this.child.stderr.pipe(process.stderr); + this.child.stdout.on('data', (chunk: Buffer) => this.consumeChunk(chunk)); + let resolveExited!: () => void; + this.exited = new Promise((resolveExit) => { + resolveExited = resolveExit; + }); + this.child.once('error', (error) => this.failAll( + new Error('M1 runtime adapter process failed', { cause: error }), + )); + this.child.once('exit', (code, signal) => { + resolveExited(); + if (!this.closing || this.pending.size > 0) { + this.failAll(new Error( + `M1 runtime adapter exited before shutdown acknowledgement ` + + `(code=${String(code)} signal=${String(signal)})`, + )); + } + }); + } + + private readonly timeoutMs: number; + + async start(role: SelectiveCoverageRuntimeRole): Promise { + return await this.request('start', { role }) as SelectiveCoverageRuntimeReadyV1; + } + + async stop(role: SelectiveCoverageRuntimeRole): Promise { + await this.request('stop', { role }); + } + + async publishWave(wave: 'selected' | 'final'): Promise { + return await this.request('publish-wave', { wave }) as readonly GraphObservationV1[]; + } + + async observeEdge( + checkpoint: 'before-selection' | 'after-selection' | 'after-restart' + | 'after-second-on-demand', + ): Promise { + return (await this.request('observe-edge', { checkpoint })) as readonly EdgeGraphObservationV1[]; + } + + async synchronizeEdge(input: { + readonly contextGraphId: string; + readonly phase: 'selection' | 'post-restart-explicit'; + readonly syncMode: 'always-on' | 'on-demand'; + readonly wave: EdgeSyncOperationV1['completedWave']; + }): Promise<{ + readonly operation: Omit; + readonly journal?: SyncCoverageJournalReferenceV1; + }> { + return (await this.request('synchronize-edge', input)) as { + readonly operation: Omit; + readonly journal?: SyncCoverageJournalReferenceV1; + }; + } + + async restartEdge(): Promise { + return await this.request('restart-edge', {}) as SelectiveCoverageEdgeRestartReceiptV1; + } + + async waitForEdgeReconciler(input: { + readonly contextGraphId: string; + }): Promise<{ + readonly operation: Omit; + readonly journal: SyncCoverageJournalReferenceV1; + }> { + return await this.request('wait-edge-reconciler', input) as { + readonly operation: Omit; + readonly journal: SyncCoverageJournalReferenceV1; + }; + } + + async runCoreAutomaticRound(round: number): Promise<{ + readonly round: CoreAutomaticRoundV1; + readonly journal: SyncCoverageJournalReferenceV1; + }> { + return await this.request('core-automatic-round', { round }) as { + readonly round: CoreAutomaticRoundV1; + readonly journal: SyncCoverageJournalReferenceV1; + }; + } + + async observeCoreFinal(): Promise { + return await this.request('observe-core-final', {}) as readonly CoreFinalObservationV1[]; + } + + async close(): Promise { + if (this.closed) return; + this.closing = true; + let shutdownFailure: unknown; + try { + await this.request('shutdown', {}); + } catch (error) { + shutdownFailure = error; + } + this.closed = true; + this.child.stdin.end(); + if (!await this.waitForExit(CLOSE_GRACE_MS)) { + this.child.kill('SIGTERM'); + if (!await this.waitForExit(CLOSE_GRACE_MS)) { + this.child.kill('SIGKILL'); + await this.exited; + } + } + if (shutdownFailure !== undefined) throw shutdownFailure; + } + + private request(command: string, payload: unknown): Promise { + if (this.closed) return Promise.reject(new Error('M1 runtime adapter is closed')); + if (this.exitError) return Promise.reject(this.exitError); + const sequence = this.sequence; + this.sequence += 1; + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(sequence); + reject(new Error(`M1 runtime adapter command timed out: ${command}`)); + }, this.timeoutMs); + timer.unref(); + this.pending.set(sequence, { resolve, reject, timer }); + const envelope = JSON.stringify({ + schema: SELECTIVE_COVERAGE_RUNTIME_COMMAND_SCHEMA, + protocol: SELECTIVE_COVERAGE_RUNTIME_PROTOCOL, + sessionNonce: this.sessionNonce, + sequence, + command, + payload, + }); + this.child.stdin.write(`${envelope}\n`, (error) => { + if (!error) return; + const request = this.pending.get(sequence); + if (!request) return; + clearTimeout(request.timer); + this.pending.delete(sequence); + request.reject(new Error(`M1 runtime adapter command write failed: ${command}`, { + cause: error, + })); + }); + }); + } + + private consumeChunk(chunk: Buffer): void { + this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, chunk]); + let newline = this.stdoutBuffer.indexOf(0x0a); + while (newline >= 0) { + const line = this.stdoutBuffer.subarray(0, newline); + this.stdoutBuffer = this.stdoutBuffer.subarray(newline + 1); + if (line.byteLength > MAX_RESULT_LINE_BYTES) { + this.failAll(new Error('M1 runtime adapter result/log line exceeds 1 MiB')); + return; + } + this.consumeLine(line.toString('utf8').replace(/\r$/u, '')); + newline = this.stdoutBuffer.indexOf(0x0a); + } + if (this.stdoutBuffer.byteLength > MAX_RESULT_LINE_BYTES) { + this.failAll(new Error('M1 runtime adapter result/log line exceeds 1 MiB')); + } + } + + private consumeLine(line: string): void { + if (!line.startsWith(SELECTIVE_COVERAGE_RUNTIME_RESULT_PREFIX)) return; + let value: unknown; + try { + value = JSON.parse(line.slice(SELECTIVE_COVERAGE_RUNTIME_RESULT_PREFIX.length)); + } catch (error) { + this.failAll(new Error('M1 runtime adapter emitted malformed result JSON', { cause: error })); + return; + } + if (!isPlainRecord(value) + || value['schema'] !== SELECTIVE_COVERAGE_RUNTIME_RESULT_SCHEMA + || value['protocol'] !== SELECTIVE_COVERAGE_RUNTIME_PROTOCOL + || value['sessionNonce'] !== this.sessionNonce + || !Number.isSafeInteger(value['sequence'])) { + this.failAll(new Error('M1 runtime adapter emitted an invalid result envelope')); + return; + } + const sequence = value['sequence'] as number; + const request = this.pending.get(sequence); + if (!request) { + this.failAll(new Error(`M1 runtime adapter emitted an unknown result sequence: ${sequence}`)); + return; + } + clearTimeout(request.timer); + this.pending.delete(sequence); + if (value['ok'] === true && Object.hasOwn(value, 'value')) { + request.resolve(value['value']); + return; + } + const message = typeof value['error'] === 'string' && value['error'] + ? value['error'] + : 'runtime adapter command failed without an error message'; + request.reject(new Error(message)); + } + + private failAll(error: Error): void { + if (!this.exitError) this.exitError = error; + for (const request of this.pending.values()) { + clearTimeout(request.timer); + request.reject(error); + } + this.pending.clear(); + } + + private async waitForExit(timeoutMs: number): Promise { + if (this.child.exitCode !== null || this.child.signalCode !== null) return true; + let timer: ReturnType | undefined; + const timedOut = new Promise((resolveTimeout) => { + timer = setTimeout(() => resolveTimeout(false), timeoutMs); + timer.unref(); + }); + const exited = this.exited.then(() => true as const); + const result = await Promise.race([exited, timedOut]); + if (timer) clearTimeout(timer); + return result; + } +} + +function isPlainRecord(value: unknown): value is Record { + return value !== null + && typeof value === 'object' + && !Array.isArray(value) + && Object.getPrototypeOf(value) === Object.prototype; +} diff --git a/devnet/rfc64-m1-selective-coverage/runtime.test.ts b/devnet/rfc64-m1-selective-coverage/runtime.test.ts new file mode 100644 index 0000000000..08a8bc8eb3 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/runtime.test.ts @@ -0,0 +1,561 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import test from 'node:test'; + +import { + canonicalJson, + createSelectiveCoverageCorpus, + type CoreAutomaticRoundV1, + type CoreFinalObservationV1, + type EdgeGraphObservationV1, + type EdgeSyncOperationV1, + type ExpectedSelectiveCoverageProvenanceV1, + type GraphObservationV1, + type GraphSnapshotExpectationV1, + type SelectiveCoverageGraphV1, +} from './manifest.ts'; +import { + collectSelectiveCoverageEvidenceV1, + SELECTIVE_COVERAGE_RUNTIME_PROTOCOL, + type SelectiveCoverageEdgeRestartReceiptV1, + type SelectiveCoverageRuntimeReadyV1, + type SelectiveCoverageRuntimeRole, + type SelectiveCoverageRuntimeV1, +} from './runtime.ts'; +import type { SyncCoverageJournalReferenceV1 } from './sync-coverage-journal.ts'; +import { runSelectiveCoverageLiveV1 } from './live-runner.ts'; + +const digest = (value: string) => + `sha256:${createHash('sha256').update(value).digest('hex')}`; +const graphId = (name: string) => `0x1111111111111111111111111111111111111111/${name}`; + +function snapshot(name: string, wave: 'selected' | 'final'): GraphSnapshotExpectationV1 { + const count = wave === 'selected' ? 1 : 2; + return { + vm: { + headDigest: digest(`${name}:${wave}:vm:head`), + inventoryDigest: digest(`${name}:${wave}:vm:inventory`), + assetCount: count, + dataTripleCount: count * 20, + }, + swm: { + headDigest: digest(`${name}:${wave}:swm:head`), + inventoryDigest: digest(`${name}:${wave}:swm:inventory`), + assetCount: count, + dataTripleCount: count * 15, + }, + }; +} + +const graphInputs = [ + ['01-public-open-on-demand', 0, 1, 'on-demand'], + ['02-public-curated-always-on', 0, 0, 'always-on'], + ['03-public-open-unselected', 0, 1, 'unselected'], + ['04-private-open', 1, 1, 'unselected'], + ['05-private-curated', 1, 0, 'unselected'], +] as const; +const graphs: readonly SelectiveCoverageGraphV1[] = graphInputs.map((row) => ({ + contextGraphId: graphId(row[0]), + accessPolicy: row[1], + publishPolicy: row[2], + edgePolicy: row[3], + selectedSnapshot: snapshot(row[0], 'selected'), + finalSnapshot: snapshot(row[0], 'final'), +})); +const corpus = createSelectiveCoverageCorpus({ + networkId: 'otp:20430', + coreAutomaticBatchSize: 2, + coreCoverageRoundLimit: 2, + graphs, +}); +const expected: ExpectedSelectiveCoverageProvenanceV1 = { + networkId: corpus.networkId, + testedHeadCommit: 'a'.repeat(40), + runtimeManifestDigest: digest('runtime'), + corpusManifestDigest: corpus.manifestDigest, + publisherPeerId: 'publisher-peer', + edgePeerId: 'edge-peer', + corePeerId: 'core-peer', +}; + +function exactObservation( + graph: SelectiveCoverageGraphV1, + expectedSnapshot: GraphSnapshotExpectationV1, +): GraphObservationV1 { + const plane = (expectedPlane: GraphSnapshotExpectationV1['vm']) => ({ + reportedComplete: true, + headDigest: expectedPlane.headDigest, + inventoryDigest: expectedPlane.inventoryDigest, + assetCount: expectedPlane.assetCount, + metadataTripleCount: 4, + dataTripleCount: expectedPlane.dataTripleCount, + }); + return { + contextGraphId: graph.contextGraphId, + vm: plane(expectedSnapshot.vm), + swm: plane(expectedSnapshot.swm), + }; +} + +function absentObservation(contextGraphId: string): GraphObservationV1 { + const plane = { + reportedComplete: false, + headDigest: null, + inventoryDigest: null, + assetCount: 0, + metadataTripleCount: 0, + dataTripleCount: 0, + } as const; + return { contextGraphId, vm: { ...plane }, swm: { ...plane } }; +} + +class ScriptedRuntime implements SelectiveCoverageRuntimeV1 { + readonly calls: string[] = []; + readonly stopped: SelectiveCoverageRuntimeRole[] = []; + readyMutation?: (ready: SelectiveCoverageRuntimeReadyV1) => SelectiveCoverageRuntimeReadyV1; + selectedPublisherMutation?: (rows: GraphObservationV1[]) => GraphObservationV1[]; + operationMutation?: ( + operation: Omit, + ) => Omit; + coreRoundMutation?: (round: CoreAutomaticRoundV1) => CoreAutomaticRoundV1; + edgeJournalMutation?: ( + journal: SyncCoverageJournalReferenceV1, + ) => SyncCoverageJournalReferenceV1; + coreJournalMutation?: ( + journal: SyncCoverageJournalReferenceV1, + ) => SyncCoverageJournalReferenceV1; + restartReceiptMutation?: ( + receipt: SelectiveCoverageEdgeRestartReceiptV1, + ) => SelectiveCoverageEdgeRestartReceiptV1; + + async start(role: SelectiveCoverageRuntimeRole): Promise { + this.calls.push(`start:${role}`); + const pid = role === 'publisher' ? 101 : role === 'edge' ? 102 : 104; + const peerId = role === 'publisher' + ? expected.publisherPeerId + : role === 'edge' + ? expected.edgePeerId + : expected.corePeerId; + const ready: SelectiveCoverageRuntimeReadyV1 = { + protocol: SELECTIVE_COVERAGE_RUNTIME_PROTOCOL, + role, + pid, + peerId, + networkId: expected.networkId, + testedHeadCommit: expected.testedHeadCommit, + runtimeManifestDigest: expected.runtimeManifestDigest, + processStartedAt: role === 'edge' ? 0 : 1, + processInstanceId: `${role}-instance-before`, + dataDirectoryIdentity: `${role}-data`, + evidenceWaveId: role === 'core' ? 'core-wave' : `${role}-wave-before`, + }; + return this.readyMutation?.(ready) ?? ready; + } + + async stop(role: SelectiveCoverageRuntimeRole): Promise { + this.calls.push(`stop:${role}`); + this.stopped.push(role); + } + + async publishWave(wave: 'selected' | 'final'): Promise { + this.calls.push(`publish:${wave}`); + const rows = corpus.graphs.map((graph) => + exactObservation(graph, wave === 'selected' ? graph.selectedSnapshot : graph.finalSnapshot)); + return wave === 'selected' ? this.selectedPublisherMutation?.(rows) ?? rows : rows; + } + + async observeEdge( + checkpoint: 'before-selection' | 'after-selection' | 'after-restart' + | 'after-second-on-demand', + ): Promise { + this.calls.push(`observe-edge:${checkpoint}`); + return corpus.graphs.map((graph): EdgeGraphObservationV1 => { + if (checkpoint === 'before-selection' + || graph.accessPolicy !== 0 + || graph.edgePolicy === 'unselected') { + return { + ...absentObservation(graph.contextGraphId), + runtimeSyncMode: null, + producingJobId: null, + }; + } + const alwaysOn = graph.edgePolicy === 'always-on'; + const afterFinal = checkpoint === 'after-second-on-demand' + || (checkpoint === 'after-restart' && alwaysOn); + const selectionJob = alwaysOn ? 'edge-select-always-on' : 'edge-select-on-demand'; + const producingJobId = afterFinal + ? (alwaysOn ? 'edge-auto-always-on' : 'edge-second-on-demand') + : selectionJob; + return { + ...exactObservation( + graph, + afterFinal ? graph.finalSnapshot : graph.selectedSnapshot, + ), + runtimeSyncMode: checkpoint === 'after-restart' && !alwaysOn + ? null + : graph.edgePolicy, + producingJobId, + }; + }); + } + + async synchronizeEdge(input: { + readonly contextGraphId: string; + readonly phase: 'selection' | 'post-restart-explicit'; + readonly syncMode: 'always-on' | 'on-demand'; + readonly wave: EdgeSyncOperationV1['completedWave']; + }): Promise<{ + readonly operation: Omit; + readonly journal?: SyncCoverageJournalReferenceV1; + }> { + this.calls.push(`edge-sync:${input.phase}:${input.contextGraphId}`); + const graph = corpus.graphs.find((candidate) => + candidate.contextGraphId === input.contextGraphId)!; + const jobId = input.phase === 'post-restart-explicit' + ? 'edge-second-on-demand' + : input.syncMode === 'always-on' + ? 'edge-select-always-on' + : 'edge-select-on-demand'; + const operation: Omit = { + phase: input.phase, + source: 'user', + syncMode: input.syncMode, + contextGraphId: input.contextGraphId, + jobId, + completedWave: input.wave, + completedSnapshot: input.wave === 'selected' + ? graph.selectedSnapshot + : graph.finalSnapshot, + }; + const result = this.operationMutation?.(operation) ?? operation; + return { operation: result }; + } + + async restartEdge(): Promise { + this.calls.push('restart:edge'); + const ready: SelectiveCoverageRuntimeReadyV1 = { + protocol: SELECTIVE_COVERAGE_RUNTIME_PROTOCOL, + role: 'edge', + pid: 103, + peerId: expected.edgePeerId, + networkId: expected.networkId, + testedHeadCommit: expected.testedHeadCommit, + runtimeManifestDigest: expected.runtimeManifestDigest, + processStartedAt: 1, + processInstanceId: 'edge-instance-after', + dataDirectoryIdentity: 'edge-data', + evidenceWaveId: 'edge-wave', + }; + const receipt: SelectiveCoverageEdgeRestartReceiptV1 = { + previous: { + pid: 102, + processInstanceId: 'edge-instance-before', + exitedAt: 1, + }, + current: this.readyMutation?.(ready) ?? ready, + }; + return this.restartReceiptMutation?.(receipt) ?? receipt; + } + + async waitForEdgeReconciler(input: { + readonly contextGraphId: string; + }): Promise<{ + readonly operation: Omit; + readonly journal: SyncCoverageJournalReferenceV1; + }> { + this.calls.push(`edge-sync:post-restart-auto:${input.contextGraphId}`); + const graph = corpus.graphs.find((candidate) => + candidate.contextGraphId === input.contextGraphId)!; + const base: Omit = { + phase: 'post-restart-auto', + source: 'reconciler', + syncMode: 'always-on', + contextGraphId: input.contextGraphId, + jobId: 'edge-auto-always-on', + completedWave: 'final', + completedSnapshot: graph.finalSnapshot, + }; + const operation = this.operationMutation?.(base) ?? base; + const journal = journalReference({ + kind: 'edge-reconciler-job', + sequence: 1, + waveId: 'edge-wave', + jobId: operation.jobId, + contextGraphId: operation.contextGraphId, + source: 'reconciler', + trigger: 'periodic-reconciler', + syncMode: 'always-on', + rehydratedSelectionCount: 1, + evidenceTruncated: false, + state: 'complete', + verified: { metadata: true, durable: true, sharedMemory: true }, + startedAt: 10, + finishedAt: 11, + }); + return { + operation, + journal: this.edgeJournalMutation?.(journal) ?? journal, + }; + } + + async runCoreAutomaticRound(round: number): Promise<{ + readonly round: CoreAutomaticRoundV1; + readonly journal: SyncCoverageJournalReferenceV1; + }> { + this.calls.push(`core-round:${round}`); + const publicGraphs = corpus.graphs.filter((graph) => graph.accessPolicy === 0); + const selected = round === 0 ? publicGraphs.slice(0, 2) : publicGraphs.slice(2); + const result: CoreAutomaticRoundV1 = { + round, + jobId: `core-auto-${round}`, + planningLane: expected.publisherPeerId, + source: 'automatic-core-public', + configuredBatchSize: corpus.coreAutomaticBatchSize, + explicitSelectedContextGraphIds: [], + contextGraphIds: selected.map((graph) => graph.contextGraphId), + completions: selected.map((graph) => ({ + contextGraphId: graph.contextGraphId, + completedWave: 'final', + completedSnapshot: graph.finalSnapshot, + })), + }; + const observed = this.coreRoundMutation?.(result) ?? result; + return { + round: observed, + journal: this.coreJournalMutation?.(journalReference({ + kind: 'core-automatic-round', + sequence: round + 1, + waveId: 'core-wave', + jobId: observed.jobId, + planningLane: observed.planningLane, + source: 'automatic-core-public', + trigger: 'peer-sync', + configuredBatchSize: observed.configuredBatchSize, + effectiveBatchSize: observed.configuredBatchSize, + explicitSelectedContextGraphIds: observed.explicitSelectedContextGraphIds, + explicitSelectedContextGraphCount: observed.explicitSelectedContextGraphIds.length, + automaticContextGraphIds: observed.contextGraphIds, + automaticContextGraphCount: observed.contextGraphIds.length, + evidenceTruncated: false, + state: 'complete', + startedAt: 20 + round, + finishedAt: 21 + round, + completions: observed.completions.map((completion) => ({ + jobId: observed.jobId, + contextGraphId: completion.contextGraphId, + state: 'complete', + verified: { metadata: true, durable: true, sharedMemory: true }, + finishedAt: 21 + round, + })), + })) ?? journalReference({ + kind: 'core-automatic-round', + sequence: round + 1, + waveId: 'core-wave', + jobId: observed.jobId, + planningLane: observed.planningLane, + source: 'automatic-core-public', + trigger: 'peer-sync', + configuredBatchSize: observed.configuredBatchSize, + effectiveBatchSize: observed.configuredBatchSize, + explicitSelectedContextGraphIds: observed.explicitSelectedContextGraphIds, + explicitSelectedContextGraphCount: observed.explicitSelectedContextGraphIds.length, + automaticContextGraphIds: observed.contextGraphIds, + automaticContextGraphCount: observed.contextGraphIds.length, + evidenceTruncated: false, + state: 'complete', + startedAt: 20 + round, + finishedAt: 21 + round, + completions: observed.completions.map((completion) => ({ + jobId: observed.jobId, + contextGraphId: completion.contextGraphId, + state: 'complete', + verified: { metadata: true, durable: true, sharedMemory: true }, + finishedAt: 21 + round, + })), + }), + }; + } + + async observeCoreFinal(): Promise { + this.calls.push('observe-core-final'); + return corpus.graphs.map((graph, index) => ({ + ...(graph.accessPolicy === 0 + ? exactObservation(graph, graph.finalSnapshot) + : absentObservation(graph.contextGraphId)), + automaticJobIds: graph.accessPolicy === 0 + ? [index < 2 ? 'core-auto-0' : 'core-auto-1'] + : [], + })); + } +} + +function journalReference( + entry: Record, +): SyncCoverageJournalReferenceV1 { + const sequence = entry['sequence'] as number; + return { + sequence, + snapshot: { + schemaVersion: 1, + processStartedAt: 1, + waveId: entry['waveId'], + capacity: 256, + nextSequence: sequence + 1, + droppedBeforeSequence: 0, + entries: [entry], + }, + }; +} + +test('collects the anchored three-process Edge/Core sequence and cleans up', async () => { + const runtime = new ScriptedRuntime(); + const evidence = await collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }); + + assert.equal(evidence.provenance.edgePeerId, expected.edgePeerId); + assert.doesNotThrow(() => canonicalJson(evidence)); + assert.deepEqual(evidence.core.rounds.map((round) => round.contextGraphIds.length), [2, 1]); + assert.deepEqual(runtime.stopped, ['core', 'edge', 'publisher']); + assert.ok( + runtime.calls.indexOf('publish:final') < runtime.calls.indexOf('start:core'), + 'Core must join cold after final publication', + ); + assert.ok( + runtime.calls.indexOf('observe-edge:after-restart') + < runtime.calls.findIndex((call) => call.startsWith('edge-sync:post-restart-explicit')), + 'on-demand state must be observed stale before the second user request', + ); +}); + +test('rejects metadata-only runtime output and does not skip cleanup', async () => { + const runtime = new ScriptedRuntime(); + runtime.selectedPublisherMutation = (rows) => { + rows[0] = { + ...rows[0]!, + vm: { + ...rows[0]!.vm, + assetCount: 0, + dataTripleCount: 0, + }, + }; + return rows; + }; + await assert.rejects( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + /failed closed/, + ); + assert.deepEqual(runtime.stopped, ['core', 'edge', 'publisher']); +}); + +test('rejects a runtime identity that is not externally anchored', async () => { + const runtime = new ScriptedRuntime(); + runtime.readyMutation = (ready) => ready.role === 'edge' + ? { ...ready, peerId: 'unexpected-edge' } + : ready; + await assert.rejects( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + /edge runtime identity differs/, + ); + assert.deepEqual(runtime.stopped, ['edge', 'publisher']); + assert.equal(runtime.calls.includes('publish:selected'), false); +}); + +test('rejects relabelled Edge reconciler work', async () => { + const runtime = new ScriptedRuntime(); + runtime.operationMutation = (operation) => operation.phase === 'post-restart-auto' + ? { ...operation, source: 'user' } + : operation; + await assert.rejects( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + /failed closed/, + ); +}); + +test('rejects truncated automatic Edge journal evidence', async () => { + const runtime = new ScriptedRuntime(); + runtime.edgeJournalMutation = (journal) => { + const copy = structuredClone(journal) as any; + copy.snapshot.entries[0].evidenceTruncated = true; + return copy; + }; + await assert.rejects( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + /missing, truncated, or incomplete/, + ); +}); + +test('rejects Core work that is not an automatic scheduler round', async () => { + const runtime = new ScriptedRuntime(); + runtime.coreRoundMutation = (round) => ({ + ...round, + explicitSelectedContextGraphIds: [graphs[0]!.contextGraphId], + }); + await assert.rejects( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + /not scheduler-issued automatic coverage/, + ); +}); + +test('rejects a Core journal entry overwritten before collection', async () => { + const runtime = new ScriptedRuntime(); + runtime.coreJournalMutation = (journal) => { + const copy = structuredClone(journal) as any; + copy.snapshot.droppedBeforeSequence = journal.sequence + 1; + return copy; + }; + await assert.rejects( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + /no longer retains/, + ); +}); + +test('requires restart to cross an OS process boundary', async () => { + const runtime = new ScriptedRuntime(); + runtime.readyMutation = (ready) => ready.role === 'edge' && ready.pid === 103 + ? { ...ready, pid: 102 } + : ready; + await assert.rejects( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + /does not prove old-process exit/, + ); +}); + +test('requires positive proof that the previous Edge process exited', async () => { + const runtime = new ScriptedRuntime(); + runtime.restartReceiptMutation = (receipt) => ({ + ...receipt, + previous: { ...receipt.previous, exitedAt: -1 }, + }); + await assert.rejects( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + /does not prove old-process exit/, + ); +}); + +test('requires the replacement Edge process to start after the prior exit', async () => { + const runtime = new ScriptedRuntime(); + runtime.restartReceiptMutation = (receipt) => ({ + ...receipt, + previous: { ...receipt.previous, exitedAt: receipt.current.processStartedAt + 1 }, + }); + await assert.rejects( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + /does not prove old-process exit/, + ); +}); + +test('does not publish a PASS artifact when controller shutdown fails', async () => { + let published = false; + await assert.rejects( + runSelectiveCoverageLiveV1({ + collect: async () => ({ pass: true }), + close: async () => { + throw new Error('shutdown timeout'); + }, + publish: async () => { + published = true; + }, + }), + /shutdown timeout/, + ); + assert.equal(published, false); +}); diff --git a/devnet/rfc64-m1-selective-coverage/runtime.ts b/devnet/rfc64-m1-selective-coverage/runtime.ts new file mode 100644 index 0000000000..64ef8304bf --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/runtime.ts @@ -0,0 +1,418 @@ +import { + type CoreAutomaticRoundV1, + type CoreFinalObservationV1, + type EdgeGraphObservationV1, + type EdgeSyncOperationV1, + type ExpectedSelectiveCoverageProvenanceV1, + type GraphObservationV1, + type SelectiveCoverageCorpusV1, + type SelectiveCoverageEvidenceV1, + SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, + computeSelectiveCoverageCorpusDigest, +} from './manifest.ts'; +import { verifySelectiveCoverage } from './verifier.ts'; +import { + assertCoreAutomaticRoundJournalV1, + assertEdgeReconcilerJournalV1, + type SyncCoverageJournalReferenceV1, +} from './sync-coverage-journal.ts'; + +export const SELECTIVE_COVERAGE_RUNTIME_PROTOCOL = + 'dkg-rfc64-m1-selective-coverage-runtime-v1' as const; + +export type SelectiveCoverageRuntimeRole = 'publisher' | 'edge' | 'core'; + +export interface SelectiveCoverageRuntimeReadyV1 { + readonly protocol: typeof SELECTIVE_COVERAGE_RUNTIME_PROTOCOL; + readonly role: SelectiveCoverageRuntimeRole; + readonly pid: number; + readonly peerId: string; + readonly networkId: string; + readonly testedHeadCommit: string; + readonly runtimeManifestDigest: string; + /** Adapter-observed process start instant, also present in the node journal. */ + readonly processStartedAt: number; + /** Per-process unguessable instance identity, not the stable DKG peer ID. */ + readonly processInstanceId: string; + /** Stable identity for the durable directory reused across an Edge restart. */ + readonly dataDirectoryIdentity: string; + /** Process-local sync evidence wave emitted by the node journal. */ + readonly evidenceWaveId: string; +} + +export interface SelectiveCoverageEdgeRestartReceiptV1 { + readonly previous: { + readonly pid: number; + readonly processInstanceId: string; + readonly exitedAt: number; + }; + readonly current: SelectiveCoverageRuntimeReadyV1; +} + +export interface SelectiveCoverageRuntimeV1 { + start(role: SelectiveCoverageRuntimeRole): Promise; + stop(role: SelectiveCoverageRuntimeRole): Promise; + publishWave(wave: 'selected' | 'final'): Promise; + observeEdge( + checkpoint: 'before-selection' | 'after-selection' | 'after-restart' + | 'after-second-on-demand', + ): Promise; + synchronizeEdge(input: { + readonly contextGraphId: string; + readonly phase: 'selection' | 'post-restart-explicit'; + readonly syncMode: 'always-on' | 'on-demand'; + readonly wave: EdgeSyncOperationV1['completedWave']; + }): Promise<{ + readonly operation: Omit; + /** Required only for post-restart automatic reconciler work. */ + readonly journal?: SyncCoverageJournalReferenceV1; + }>; + restartEdge(): Promise; + waitForEdgeReconciler(input: { + readonly contextGraphId: string; + }): Promise<{ + readonly operation: Omit; + readonly journal: SyncCoverageJournalReferenceV1; + }>; + runCoreAutomaticRound(round: number): Promise<{ + readonly round: CoreAutomaticRoundV1; + readonly journal: SyncCoverageJournalReferenceV1; + }>; + observeCoreFinal(): Promise; +} + +/** + * Run the user-visible M1 sequence without deriving expectations from a receiver. + * + * The corpus and provenance are immutable operator inputs. Runtime observations + * can satisfy them, but can never redefine them. The final verifier is invoked + * before evidence is returned, so callers cannot accidentally publish a + * metadata-only or otherwise incomplete artifact as a passing run. + */ +export async function collectSelectiveCoverageEvidenceV1(input: { + readonly corpus: SelectiveCoverageCorpusV1; + readonly expectedProvenance: ExpectedSelectiveCoverageProvenanceV1; + readonly runtime: SelectiveCoverageRuntimeV1; +}): Promise { + assertAnchoredCorpus(input.corpus, input.expectedProvenance); + const attempted = new Set(); + let primaryFailure: unknown; + try { + attempted.add('publisher'); + const publisher = await input.runtime.start('publisher'); + assertReady(publisher, 'publisher', input.expectedProvenance); + + attempted.add('edge'); + const edgeBeforeRestart = await input.runtime.start('edge'); + assertReady(edgeBeforeRestart, 'edge', input.expectedProvenance); + assertDistinctProcesses([publisher, edgeBeforeRestart]); + + const publisherSelected = canonicalGraphObservations( + await input.runtime.publishWave('selected'), + input.corpus, + 'Publisher selected wave', + ); + const edgeBeforeSelection = canonicalEdgeObservations( + await input.runtime.observeEdge('before-selection'), + input.corpus, + 'Edge before selection', + ); + + const edgeOperations: EdgeSyncOperationV1[] = []; + for (const graph of selectedPublicGraphs(input.corpus)) { + const result = await input.runtime.synchronizeEdge({ + contextGraphId: graph.contextGraphId, + phase: 'selection', + syncMode: graph.edgePolicy as 'always-on' | 'on-demand', + wave: 'selected', + }); + edgeOperations.push(withSequence(result.operation, edgeOperations.length)); + } + const edgeAfterSelection = canonicalEdgeObservations( + await input.runtime.observeEdge('after-selection'), + input.corpus, + 'Edge after selection', + ); + + const publisherFinal = canonicalGraphObservations( + await input.runtime.publishWave('final'), + input.corpus, + 'Publisher final wave', + ); + + const edgeRestart = await input.runtime.restartEdge(); + assertEdgeRestartReceipt(edgeRestart, edgeBeforeRestart); + const edgeAfterRestartReady = edgeRestart.current; + assertReady(edgeAfterRestartReady, 'edge', input.expectedProvenance); + assertDistinctProcesses([publisher, edgeBeforeRestart, edgeAfterRestartReady]); + + for (const graph of selectedPublicGraphs(input.corpus) + .filter((candidate) => candidate.edgePolicy === 'always-on')) { + const result = await input.runtime.waitForEdgeReconciler({ + contextGraphId: graph.contextGraphId, + }); + assertEdgeReconcilerJournalV1( + result.journal, + result.operation, + edgeAfterRestartReady, + ); + edgeOperations.push(withSequence(result.operation, edgeOperations.length)); + } + const edgeAfterRestart = canonicalEdgeObservations( + await input.runtime.observeEdge('after-restart'), + input.corpus, + 'Edge after restart', + ); + + for (const graph of selectedPublicGraphs(input.corpus) + .filter((candidate) => candidate.edgePolicy === 'on-demand')) { + const result = await input.runtime.synchronizeEdge({ + contextGraphId: graph.contextGraphId, + phase: 'post-restart-explicit', + syncMode: 'on-demand', + wave: 'final', + }); + edgeOperations.push(withSequence(result.operation, edgeOperations.length)); + } + const edgeAfterSecondOnDemand = canonicalEdgeObservations( + await input.runtime.observeEdge('after-second-on-demand'), + input.corpus, + 'Edge after second on-demand request', + ); + + attempted.add('core'); + const core = await input.runtime.start('core'); + assertReady(core, 'core', input.expectedProvenance); + assertDistinctProcesses([publisher, edgeAfterRestartReady, core]); + + const rounds: CoreAutomaticRoundV1[] = []; + const scheduled = new Set(); + const publicIds = new Set( + input.corpus.graphs + .filter((graph) => graph.accessPolicy === 0) + .map((graph) => graph.contextGraphId), + ); + for (let round = 0; round < input.corpus.coreCoverageRoundLimit; round += 1) { + const result = await input.runtime.runCoreAutomaticRound(round); + const observed = result.round; + assertCoreRoundEnvelope(observed, round, input.corpus, publisher.peerId); + assertCoreAutomaticRoundJournalV1(result.journal, observed, core); + rounds.push(observed); + for (const contextGraphId of observed.contextGraphIds) scheduled.add(contextGraphId); + if ([...publicIds].every((contextGraphId) => scheduled.has(contextGraphId))) break; + } + const coreFinal = canonicalCoreObservations( + await input.runtime.observeCoreFinal(), + input.corpus, + 'Core final', + ); + + const evidence: SelectiveCoverageEvidenceV1 = { + schema: SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, + provenance: { + networkId: publisher.networkId, + testedHeadCommit: publisher.testedHeadCommit, + runtimeManifestDigest: publisher.runtimeManifestDigest, + publisherPeerId: publisher.peerId, + edgePeerId: edgeAfterRestartReady.peerId, + corePeerId: core.peerId, + }, + corpus: input.corpus, + publisher: { + selected: publisherSelected, + final: publisherFinal, + }, + edge: { + beforeSelection: edgeBeforeSelection, + afterSelection: edgeAfterSelection, + afterRestart: edgeAfterRestart, + afterSecondOnDemand: edgeAfterSecondOnDemand, + operations: Object.freeze(edgeOperations), + }, + core: { + automaticBatchSize: input.corpus.coreAutomaticBatchSize, + rounds: Object.freeze(rounds), + final: coreFinal, + }, + }; + const detachedEvidence = detachJsonEvidence(evidence); + const verdict = verifySelectiveCoverage(detachedEvidence, input.expectedProvenance); + if (!verdict.pass) { + throw new Error( + `M1 runtime evidence failed closed: ${verdict.rejectReasons.join('; ')}`, + ); + } + return detachedEvidence; + } catch (error) { + primaryFailure = error; + throw error; + } finally { + const cleanupFailures: unknown[] = []; + for (const role of [...attempted].reverse()) { + try { + await input.runtime.stop(role); + } catch (error) { + cleanupFailures.push(error); + } + } + if (primaryFailure === undefined && cleanupFailures.length > 0) { + throw new AggregateError(cleanupFailures, 'M1 runtime cleanup failed'); + } + } +} + +function assertEdgeRestartReceipt( + receipt: SelectiveCoverageEdgeRestartReceiptV1, + previous: SelectiveCoverageRuntimeReadyV1, +): void { + if (receipt.previous.pid !== previous.pid + || receipt.previous.processInstanceId !== previous.processInstanceId + || !Number.isSafeInteger(receipt.previous.exitedAt) + || receipt.previous.exitedAt < previous.processStartedAt + || receipt.current.processStartedAt < receipt.previous.exitedAt + || receipt.current.pid === previous.pid + || receipt.current.processInstanceId === previous.processInstanceId + || receipt.current.dataDirectoryIdentity !== previous.dataDirectoryIdentity) { + throw new Error('Edge restart receipt does not prove old-process exit and durable reuse'); + } +} + +function detachJsonEvidence( + evidence: SelectiveCoverageEvidenceV1, +): SelectiveCoverageEvidenceV1 { + try { + // Runtime responses cross JSON in production. Detaching here gives an + // injected/in-process adapter the same boundary and prevents shared object + // identities from making the final canonical artifact ambiguous. + return JSON.parse(JSON.stringify(evidence)) as SelectiveCoverageEvidenceV1; + } catch (error) { + throw new Error('M1 runtime evidence is not lossless JSON', { cause: error }); + } +} + +function selectedPublicGraphs(corpus: SelectiveCoverageCorpusV1) { + return corpus.graphs.filter((graph) => + graph.accessPolicy === 0 && graph.edgePolicy !== 'unselected'); +} + +function withSequence( + operation: Omit, + sequence: number, +): EdgeSyncOperationV1 { + return { sequence, ...operation }; +} + +function assertAnchoredCorpus( + corpus: SelectiveCoverageCorpusV1, + expected: ExpectedSelectiveCoverageProvenanceV1, +): void { + if (computeSelectiveCoverageCorpusDigest(corpus) !== corpus.manifestDigest) { + throw new Error('M1 corpus manifest digest does not match its payload'); + } + if (corpus.manifestDigest !== expected.corpusManifestDigest) { + throw new Error('M1 corpus differs from the external trust anchor'); + } + if (corpus.networkId !== expected.networkId) { + throw new Error('M1 corpus network differs from the external trust anchor'); + } + for (const graph of corpus.graphs) { + if (graph.accessPolicy === 1 && graph.edgePolicy !== 'unselected') { + throw new Error('M1 private graphs must remain unselected in the Edge slice'); + } + } +} + +function assertReady( + actual: SelectiveCoverageRuntimeReadyV1, + role: SelectiveCoverageRuntimeRole, + expected: ExpectedSelectiveCoverageProvenanceV1, +): void { + const expectedPeerId = role === 'publisher' + ? expected.publisherPeerId + : role === 'edge' + ? expected.edgePeerId + : expected.corePeerId; + if (actual.protocol !== SELECTIVE_COVERAGE_RUNTIME_PROTOCOL + || actual.role !== role + || !Number.isSafeInteger(actual.pid) + || actual.pid <= 0 + || actual.peerId !== expectedPeerId + || actual.networkId !== expected.networkId + || actual.testedHeadCommit !== expected.testedHeadCommit + || actual.runtimeManifestDigest !== expected.runtimeManifestDigest) { + throw new Error(`${role} runtime identity differs from the external trust anchor`); + } + for (const [label, value] of [ + ['processInstanceId', actual.processInstanceId], + ['dataDirectoryIdentity', actual.dataDirectoryIdentity], + ['evidenceWaveId', actual.evidenceWaveId], + ] as const) { + if (typeof value !== 'string' || value.length < 1 || value.length > 256) { + throw new Error(`${role} runtime ${label} is invalid`); + } + } + if (!Number.isSafeInteger(actual.processStartedAt) || actual.processStartedAt < 0) { + throw new Error(`${role} runtime processStartedAt is invalid`); + } +} + +function assertDistinctProcesses( + processes: readonly SelectiveCoverageRuntimeReadyV1[], +): void { + if (new Set(processes.map((entry) => entry.pid)).size !== processes.length) { + throw new Error('M1 roles did not cross distinct OS process boundaries'); + } + const peerByRole = new Map(); + for (const process of processes) peerByRole.set(process.role, process.peerId); + if (new Set(peerByRole.values()).size !== peerByRole.size) { + throw new Error('M1 roles did not use distinct DKG peer identities'); + } +} + +function canonicalGraphObservations( + observations: readonly T[], + corpus: SelectiveCoverageCorpusV1, + label: string, +): readonly T[] { + const expectedIds = corpus.graphs.map((graph) => graph.contextGraphId); + const byId = new Map(observations.map((row) => [row.contextGraphId, row])); + if (observations.length !== expectedIds.length || byId.size !== expectedIds.length) { + throw new Error(`${label} did not return exactly one row per anchored graph`); + } + const canonical = expectedIds.map((contextGraphId) => byId.get(contextGraphId)); + if (canonical.some((row) => row === undefined)) { + throw new Error(`${label} omitted an anchored graph`); + } + return Object.freeze(canonical as T[]); +} + +function canonicalEdgeObservations( + observations: readonly EdgeGraphObservationV1[], + corpus: SelectiveCoverageCorpusV1, + label: string, +): readonly EdgeGraphObservationV1[] { + return canonicalGraphObservations(observations, corpus, label); +} + +function canonicalCoreObservations( + observations: readonly CoreFinalObservationV1[], + corpus: SelectiveCoverageCorpusV1, + label: string, +): readonly CoreFinalObservationV1[] { + return canonicalGraphObservations(observations, corpus, label); +} + +function assertCoreRoundEnvelope( + observed: CoreAutomaticRoundV1, + round: number, + corpus: SelectiveCoverageCorpusV1, + publisherPeerId: string, +): void { + if (observed.round !== round + || observed.source !== 'automatic-core-public' + || observed.planningLane !== publisherPeerId + || observed.configuredBatchSize !== corpus.coreAutomaticBatchSize + || observed.explicitSelectedContextGraphIds.length !== 0) { + throw new Error(`Core round ${round} is not scheduler-issued automatic coverage`); + } +} diff --git a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts new file mode 100644 index 0000000000..ebab9c6ee9 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts @@ -0,0 +1,159 @@ +import type { + CoreAutomaticRoundV1, + EdgeSyncOperationV1, +} from './manifest.ts'; + +const MAX_JOURNAL_CAPACITY = 4_096; +const MAX_CONTEXT_GRAPH_IDS = 256; + +export interface SyncCoverageJournalReferenceV1 { + /** Raw node-admin response from /api/diagnostics/sync-coverage-evidence. */ + readonly snapshot: unknown; + /** Exact terminal entry selected by the adapter. */ + readonly sequence: number; +} + +export interface SyncCoverageJournalProcessIdentityV1 { + readonly processStartedAt: number; + readonly evidenceWaveId: string; +} + +/** + * Bind an automatic Edge completion to the immutable operator journal. The + * exact VM/SWM snapshot remains independently queried by the harness. + */ +export function assertEdgeReconcilerJournalV1( + reference: SyncCoverageJournalReferenceV1 | undefined, + operation: Omit, + process: SyncCoverageJournalProcessIdentityV1, +): void { + const entry = terminalEntry(reference, 'edge-reconciler-job', process); + if (entry['jobId'] !== operation.jobId + || entry['contextGraphId'] !== operation.contextGraphId + || entry['source'] !== 'reconciler' + || entry['trigger'] !== 'periodic-reconciler' + || entry['syncMode'] !== 'always-on' + || !positiveInteger(entry['rehydratedSelectionCount']) + || !verifiedPlanes(entry['verified'])) { + throw new Error('Edge automatic completion lacks exact reconciler journal provenance'); + } +} + +/** Bind one claimed Core round to its frozen scheduler plan and completions. */ +export function assertCoreAutomaticRoundJournalV1( + reference: SyncCoverageJournalReferenceV1 | undefined, + round: CoreAutomaticRoundV1, + process: SyncCoverageJournalProcessIdentityV1, +): void { + const entry = terminalEntry(reference, 'core-automatic-round', process); + const automaticIds = stringArray(entry['automaticContextGraphIds']); + const explicitIds = stringArray(entry['explicitSelectedContextGraphIds']); + const completions = plainArray(entry['completions']); + if (entry['jobId'] !== round.jobId + || entry['planningLane'] !== round.planningLane + || entry['source'] !== 'automatic-core-public' + || entry['configuredBatchSize'] !== round.configuredBatchSize + || !nonNegativeInteger(entry['effectiveBatchSize']) + || entry['automaticContextGraphCount'] !== automaticIds.length + || entry['explicitSelectedContextGraphCount'] !== explicitIds.length + || !sameStrings(automaticIds, round.contextGraphIds) + || !sameStrings(explicitIds, round.explicitSelectedContextGraphIds) + || completions.length !== round.completions.length) { + throw new Error('Core round differs from its immutable scheduler journal plan'); + } + for (const expected of round.completions) { + const completion = completions.find((candidate) => + isPlainRecord(candidate) && candidate['contextGraphId'] === expected.contextGraphId); + if (!isPlainRecord(completion) + || completion['jobId'] !== round.jobId + || completion['state'] !== 'complete' + || !verifiedPlanes(completion['verified']) + || !nonNegativeInteger(completion['finishedAt'])) { + throw new Error('Core round lacks a terminal verified completion for a planned graph'); + } + } +} + +function terminalEntry( + reference: SyncCoverageJournalReferenceV1 | undefined, + kind: 'edge-reconciler-job' | 'core-automatic-round', + process: SyncCoverageJournalProcessIdentityV1, +): Record { + if (!reference || !nonNegativeInteger(reference.sequence)) { + throw new Error(`${kind} requires an operator-journal terminal entry`); + } + const snapshot = reference.snapshot; + if (!isPlainRecord(snapshot) + || snapshot['schemaVersion'] !== 1 + || snapshot['processStartedAt'] !== process.processStartedAt + || snapshot['waveId'] !== process.evidenceWaveId + || !positiveInteger(snapshot['capacity']) + || (snapshot['capacity'] as number) > MAX_JOURNAL_CAPACITY + || !nonNegativeInteger(snapshot['nextSequence']) + || !nonNegativeInteger(snapshot['droppedBeforeSequence'])) { + throw new Error('Sync coverage journal snapshot is malformed'); + } + if ((snapshot['droppedBeforeSequence'] as number) > reference.sequence + || (snapshot['nextSequence'] as number) <= reference.sequence) { + throw new Error('Sync coverage journal no longer retains the referenced evidence'); + } + const entries = plainArray(snapshot['entries']); + if (entries.length > (snapshot['capacity'] as number)) { + throw new Error('Sync coverage journal exceeds its declared capacity'); + } + const candidate = entries.find((entry) => + isPlainRecord(entry) && entry['sequence'] === reference.sequence); + if (!isPlainRecord(candidate) + || candidate['kind'] !== kind + || candidate['waveId'] !== snapshot['waveId'] + || candidate['evidenceTruncated'] !== false + || candidate['state'] !== 'complete' + || !nonNegativeInteger(candidate['startedAt']) + || !nonNegativeInteger(candidate['finishedAt'])) { + throw new Error(`${kind} terminal journal entry is missing, truncated, or incomplete`); + } + return candidate; +} + +function verifiedPlanes(value: unknown): boolean { + return isPlainRecord(value) + && value['metadata'] === true + && value['durable'] === true + && value['sharedMemory'] === true; +} + +function stringArray(value: unknown): string[] { + const values = plainArray(value); + if (values.length > MAX_CONTEXT_GRAPH_IDS + || values.some((entry) => typeof entry !== 'string' || entry.length === 0) + || new Set(values).size !== values.length) { + throw new Error('Sync coverage journal contains invalid context graph IDs'); + } + return values as string[]; +} + +function plainArray(value: unknown): unknown[] { + if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) { + throw new Error('Sync coverage journal field is not a plain array'); + } + return value; +} + +function sameStrings(left: readonly string[], right: readonly string[]): boolean { + return left.length === right.length && left.every((value, index) => value === right[index]); +} + +function positiveInteger(value: unknown): value is number { + return Number.isSafeInteger(value) && (value as number) > 0; +} + +function nonNegativeInteger(value: unknown): value is number { + return Number.isSafeInteger(value) && (value as number) >= 0; +} + +function isPlainRecord(value: unknown): value is Record { + return value !== null + && typeof value === 'object' + && !Array.isArray(value) + && Object.getPrototypeOf(value) === Object.prototype; +} diff --git a/devnet/rfc64-m1-selective-coverage/tsconfig.json b/devnet/rfc64-m1-selective-coverage/tsconfig.json index 75eea5acb8..37f50de209 100644 --- a/devnet/rfc64-m1-selective-coverage/tsconfig.json +++ b/devnet/rfc64-m1-selective-coverage/tsconfig.json @@ -5,7 +5,7 @@ "declaration": false, "declarationMap": false, "noEmit": true, - "rootDir": ".", + "rootDir": "../..", "skipLibCheck": true, "sourceMap": false, "types": ["node"] diff --git a/devnet/rfc64-m1-selective-coverage/verify-live.ts b/devnet/rfc64-m1-selective-coverage/verify-live.ts new file mode 100644 index 0000000000..ae18757236 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/verify-live.ts @@ -0,0 +1,38 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { readCleanRepositoryHead } from '../rfc64-persistence-lifecycle/evidence.ts'; +import { buildGate2RuntimeManifestV1 } from + '../rfc64-gate2-multi-asset-completeness/runtime-provenance.ts'; +import type { ExpectedSelectiveCoverageProvenanceV1 } from './manifest.ts'; +import { verifySelectiveCoverage } from './verifier.ts'; + +const repoRoot = resolve(import.meta.dirname, '../..'); +const trustAnchorPath = resolve(requiredEnvironment('DKG_RFC64_M1_TRUST_ANCHOR_FILE')); +const artifactPath = resolve( + process.env['DKG_RFC64_M1_ARTIFACT'] + ?? resolve(import.meta.dirname, 'artifacts/selective-coverage-evidence.json'), +); +const expected = JSON.parse( + readFileSync(trustAnchorPath, 'utf8'), +) as ExpectedSelectiveCoverageProvenanceV1; +const sourceCommit = readCleanRepositoryHead(repoRoot); +if (sourceCommit !== expected.testedHeadCommit) { + throw new Error('M1 trust anchor names a different checked-out source commit'); +} +const runtimeManifest = buildGate2RuntimeManifestV1(repoRoot, sourceCommit); +if (runtimeManifest.manifestDigest !== expected.runtimeManifestDigest) { + throw new Error('M1 trust anchor names a different built runtime manifest'); +} +const evidence = JSON.parse(readFileSync(artifactPath, 'utf8')); +const verdict = verifySelectiveCoverage(evidence, expected); +if (!verdict.pass) { + throw new Error(`M1 evidence rejected: ${verdict.rejectReasons.join('; ')}`); +} +process.stdout.write(`[rfc64-m1] VERIFIED ${artifactPath}\n`); + +function requiredEnvironment(name: string): string { + const value = process.env[name]?.trim(); + if (!value) throw new Error(`${name} is required`); + return value; +} diff --git a/package.json b/package.json index b8bad70a96..4667f7a531 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,10 @@ "test:m1:rfc64-public-swm-parity:verify": "node --import tsx devnet/rfc64-cp1-public-swm-parity/verify-live.ts", "test:m1:rfc64-public-swm-parity:unit": "node --import tsx --test devnet/rfc64-cp1-public-swm-parity/verifier.test.ts", "typecheck:m1:rfc64-public-swm-parity": "tsc --project devnet/rfc64-cp1-public-swm-parity/tsconfig.json", - "test:m1:rfc64-selective-coverage:unit": "node --experimental-strip-types --test devnet/rfc64-m1-selective-coverage/verifier.test.ts", + "test:m1:rfc64-selective-coverage": "pnpm run test:m1:rfc64-selective-coverage:generate && pnpm run test:m1:rfc64-selective-coverage:verify", + "test:m1:rfc64-selective-coverage:generate": "node --import tsx devnet/rfc64-m1-selective-coverage/launch-live.ts", + "test:m1:rfc64-selective-coverage:verify": "node --import tsx devnet/rfc64-m1-selective-coverage/verify-live.ts", + "test:m1:rfc64-selective-coverage:unit": "node --experimental-strip-types --test devnet/rfc64-m1-selective-coverage/verifier.test.ts devnet/rfc64-m1-selective-coverage/runtime.test.ts devnet/rfc64-m1-selective-coverage/process-runtime.test.ts", "typecheck:m1:rfc64-selective-coverage": "tsc --project devnet/rfc64-m1-selective-coverage/tsconfig.json", "test:devnet:v10-core-flows": "vitest run --config devnet/v10-core-flows/vitest.config.ts", "test:devnet:v10-e2e": "vitest run --config devnet/v10-end-to-end/vitest.config.ts", From 4895e01ef8757750fe8fce94f055d8b98f3a2523 Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 04:12:29 +0200 Subject: [PATCH 03/14] test(rfc64): bind core journal contract --- devnet/rfc64-m1-selective-coverage/README.md | 6 +++-- .../runtime.test.ts | 26 +++++++++++++++++++ .../sync-coverage-journal.ts | 12 +++++---- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/devnet/rfc64-m1-selective-coverage/README.md b/devnet/rfc64-m1-selective-coverage/README.md index c1cf7217c7..4709faad2b 100644 --- a/devnet/rfc64-m1-selective-coverage/README.md +++ b/devnet/rfc64-m1-selective-coverage/README.md @@ -107,7 +107,8 @@ Commands are: The adapter reads automatic provenance from the node-admin-only endpoint `GET /api/diagnostics/sync-coverage-evidence?afterSequence=N`. The launcher -requires schema version 1, an in-window terminal entry, `evidenceTruncated=false`, +requires schema version 1, the exact 256-entry journal capacity, an in-window +terminal entry, `evidenceTruncated=false`, and all metadata/durable/shared-memory verification bits. It binds: - `edge-reconciler-job` entries to the actual job ID, context graph, @@ -115,7 +116,8 @@ and all metadata/durable/shared-memory verification bits. It binds: `syncMode=always-on`; - `core-automatic-round` entries to the actual job ID, planning lane, configured batch, frozen explicit/automatic ID lists, and every terminal - per-CG completion. + per-CG completion. Every completion carries the same real scheduler-round + job ID; a detached or synthetic per-CG ID is rejected. `droppedBeforeSequence` and `nextSequence` prove the selected entry was not overwritten. Any truncated, missing, nonterminal, or mismatched record fails the diff --git a/devnet/rfc64-m1-selective-coverage/runtime.test.ts b/devnet/rfc64-m1-selective-coverage/runtime.test.ts index 08a8bc8eb3..42030686ff 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime.test.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime.test.ts @@ -508,6 +508,32 @@ test('rejects a Core journal entry overwritten before collection', async () => { ); }); +test('binds every Core completion to the actual scheduler round job ID', async () => { + const runtime = new ScriptedRuntime(); + runtime.coreJournalMutation = (journal) => { + const copy = structuredClone(journal) as any; + copy.snapshot.entries[0].completions[0].jobId = 'unbound-child-job'; + return copy; + }; + await assert.rejects( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + /lacks a terminal verified completion/, + ); +}); + +test('requires the exact runtime journal capacity from the node-admin contract', async () => { + const runtime = new ScriptedRuntime(); + runtime.coreJournalMutation = (journal) => { + const copy = structuredClone(journal) as any; + copy.snapshot.capacity = 257; + return copy; + }; + await assert.rejects( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + /journal snapshot is malformed/, + ); +}); + test('requires restart to cross an OS process boundary', async () => { const runtime = new ScriptedRuntime(); runtime.readyMutation = (ready) => ready.role === 'edge' && ready.pid === 103 diff --git a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts index ebab9c6ee9..4e19b1ad27 100644 --- a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts +++ b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts @@ -3,8 +3,9 @@ import type { EdgeSyncOperationV1, } from './manifest.ts'; -const MAX_JOURNAL_CAPACITY = 4_096; -const MAX_CONTEXT_GRAPH_IDS = 256; +const JOURNAL_CAPACITY = 256; +const MAX_CONTEXT_GRAPH_IDS = 32; +const MAX_CONTEXT_GRAPH_ID_LENGTH = 256; export interface SyncCoverageJournalReferenceV1 { /** Raw node-admin response from /api/diagnostics/sync-coverage-evidence. */ @@ -87,8 +88,7 @@ function terminalEntry( || snapshot['schemaVersion'] !== 1 || snapshot['processStartedAt'] !== process.processStartedAt || snapshot['waveId'] !== process.evidenceWaveId - || !positiveInteger(snapshot['capacity']) - || (snapshot['capacity'] as number) > MAX_JOURNAL_CAPACITY + || snapshot['capacity'] !== JOURNAL_CAPACITY || !nonNegativeInteger(snapshot['nextSequence']) || !nonNegativeInteger(snapshot['droppedBeforeSequence'])) { throw new Error('Sync coverage journal snapshot is malformed'); @@ -125,7 +125,9 @@ function verifiedPlanes(value: unknown): boolean { function stringArray(value: unknown): string[] { const values = plainArray(value); if (values.length > MAX_CONTEXT_GRAPH_IDS - || values.some((entry) => typeof entry !== 'string' || entry.length === 0) + || values.some((entry) => typeof entry !== 'string' + || entry.length === 0 + || entry.length > MAX_CONTEXT_GRAPH_ID_LENGTH) || new Set(values).size !== values.length) { throw new Error('Sync coverage journal contains invalid context graph IDs'); } From ad4707f46704dbd95c4182532ad1d5309ed11c25 Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 04:16:21 +0200 Subject: [PATCH 04/14] test(rfc64): bind journal process start epoch --- devnet/rfc64-m1-selective-coverage/README.md | 2 +- .../rfc64-m1-selective-coverage/runtime.test.ts | 16 ++++++++++++++++ devnet/rfc64-m1-selective-coverage/runtime.ts | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/devnet/rfc64-m1-selective-coverage/README.md b/devnet/rfc64-m1-selective-coverage/README.md index 4709faad2b..655985511f 100644 --- a/devnet/rfc64-m1-selective-coverage/README.md +++ b/devnet/rfc64-m1-selective-coverage/README.md @@ -95,7 +95,7 @@ Commands are: | Command | Required runtime action/evidence | | --- | --- | -| `start` | Start the named role; independently return PID, process instance/start/wave IDs, durable-directory identity, peer ID, network, commit, and loaded-runtime digest. The command contains only the role, never the trust anchor. | +| `start` | Start the named role; independently return PID, process instance/wave IDs, `processStartedAt` as integer epoch milliseconds sourced from `Math.floor(performance.timeOrigin)`, durable-directory identity, peer ID, network, commit, and loaded-runtime digest. The command contains only the role, never the trust anchor. | | `publish-wave` | Publish the named anchored wave; return exact Publisher VM/SWM observations for every graph. | | `observe-edge` | Return exact Edge VM/SWM observations, effective runtime mode, and the actual producing job ID. | | `synchronize-edge` | Issue only the named explicit user selection; return its real job ID and terminal exact snapshot. | diff --git a/devnet/rfc64-m1-selective-coverage/runtime.test.ts b/devnet/rfc64-m1-selective-coverage/runtime.test.ts index 42030686ff..96e128e5a3 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime.test.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime.test.ts @@ -534,6 +534,22 @@ test('requires the exact runtime journal capacity from the node-admin contract', ); }); +test('accepts the truthful Node process-start epoch used by the runtime journal', async () => { + const runtime = new ScriptedRuntime(); + const processStartedAt = 1_753_000_000_123; + runtime.readyMutation = (ready) => ready.role === 'core' + ? { ...ready, processStartedAt } + : ready; + runtime.coreJournalMutation = (journal) => { + const copy = structuredClone(journal) as any; + copy.snapshot.processStartedAt = processStartedAt; + return copy; + }; + await assert.doesNotReject( + collectSelectiveCoverageEvidenceV1({ corpus, expectedProvenance: expected, runtime }), + ); +}); + test('requires restart to cross an OS process boundary', async () => { const runtime = new ScriptedRuntime(); runtime.readyMutation = (ready) => ready.role === 'edge' && ready.pid === 103 diff --git a/devnet/rfc64-m1-selective-coverage/runtime.ts b/devnet/rfc64-m1-selective-coverage/runtime.ts index 64ef8304bf..26287327fb 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime.ts @@ -30,7 +30,7 @@ export interface SelectiveCoverageRuntimeReadyV1 { readonly networkId: string; readonly testedHeadCommit: string; readonly runtimeManifestDigest: string; - /** Adapter-observed process start instant, also present in the node journal. */ + /** Node process-start epoch milliseconds, sourced from performance.timeOrigin. */ readonly processStartedAt: number; /** Per-process unguessable instance identity, not the stable DKG peer ID. */ readonly processInstanceId: string; From c74a408ffa4869f66a421dcfbbc07296200b18e0 Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 04:54:33 +0200 Subject: [PATCH 05/14] test(rfc64): retain automatic journal proof --- .../rfc64-m1-selective-coverage/manifest.ts | 22 ++ devnet/rfc64-m1-selective-coverage/runtime.ts | 16 + .../sync-coverage-journal.ts | 25 +- .../verifier.test.ts | 96 +++++ .../rfc64-m1-selective-coverage/verifier.ts | 362 ++++++++++++------ 5 files changed, 388 insertions(+), 133 deletions(-) diff --git a/devnet/rfc64-m1-selective-coverage/manifest.ts b/devnet/rfc64-m1-selective-coverage/manifest.ts index 122c3c0f7d..3c2ac3e53c 100644 --- a/devnet/rfc64-m1-selective-coverage/manifest.ts +++ b/devnet/rfc64-m1-selective-coverage/manifest.ts @@ -105,6 +105,26 @@ export interface CoreFinalObservationV1 extends GraphObservationV1 { readonly automaticJobIds: readonly string[]; } +/** Bounded raw node-admin journal response retained in the published artifact. */ +export interface SyncCoverageJournalReferenceV1 { + readonly snapshot: unknown; + readonly sequence: number; +} + +export interface SyncCoverageJournalProcessIdentityV1 { + readonly processStartedAt: number; + readonly evidenceWaveId: string; +} + +export interface SelectiveCoverageAutomaticJournalEvidenceV1 { + readonly edgeProcess: SyncCoverageJournalProcessIdentityV1; + /** Ordered exactly like post-restart automatic Edge operations. */ + readonly edgeReconciler: readonly SyncCoverageJournalReferenceV1[]; + readonly coreProcess: SyncCoverageJournalProcessIdentityV1; + /** Ordered exactly like Core automatic rounds. */ + readonly coreRounds: readonly SyncCoverageJournalReferenceV1[]; +} + export interface SelectiveCoverageProvenanceV1 { readonly networkId: string; readonly testedHeadCommit: string; @@ -122,6 +142,8 @@ export interface ExpectedSelectiveCoverageProvenanceV1 export interface SelectiveCoverageEvidenceV1 { readonly schema: typeof SELECTIVE_COVERAGE_EVIDENCE_SCHEMA; readonly provenance: SelectiveCoverageProvenanceV1; + /** Raw automatic-work proof required for independent artifact verification. */ + readonly automaticJournalEvidence: SelectiveCoverageAutomaticJournalEvidenceV1; readonly corpus: SelectiveCoverageCorpusV1; /** Publisher-owned source snapshots; receivers cannot define their expectations. */ readonly publisher: { diff --git a/devnet/rfc64-m1-selective-coverage/runtime.ts b/devnet/rfc64-m1-selective-coverage/runtime.ts index 26287327fb..2677c598e5 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime.ts @@ -119,6 +119,7 @@ export async function collectSelectiveCoverageEvidenceV1(input: { ); const edgeOperations: EdgeSyncOperationV1[] = []; + const edgeReconcilerJournals: SyncCoverageJournalReferenceV1[] = []; for (const graph of selectedPublicGraphs(input.corpus)) { const result = await input.runtime.synchronizeEdge({ contextGraphId: graph.contextGraphId, @@ -156,6 +157,7 @@ export async function collectSelectiveCoverageEvidenceV1(input: { result.operation, edgeAfterRestartReady, ); + edgeReconcilerJournals.push(result.journal); edgeOperations.push(withSequence(result.operation, edgeOperations.length)); } const edgeAfterRestart = canonicalEdgeObservations( @@ -186,6 +188,7 @@ export async function collectSelectiveCoverageEvidenceV1(input: { assertDistinctProcesses([publisher, edgeAfterRestartReady, core]); const rounds: CoreAutomaticRoundV1[] = []; + const coreRoundJournals: SyncCoverageJournalReferenceV1[] = []; const scheduled = new Set(); const publicIds = new Set( input.corpus.graphs @@ -198,6 +201,7 @@ export async function collectSelectiveCoverageEvidenceV1(input: { assertCoreRoundEnvelope(observed, round, input.corpus, publisher.peerId); assertCoreAutomaticRoundJournalV1(result.journal, observed, core); rounds.push(observed); + coreRoundJournals.push(result.journal); for (const contextGraphId of observed.contextGraphIds) scheduled.add(contextGraphId); if ([...publicIds].every((contextGraphId) => scheduled.has(contextGraphId))) break; } @@ -217,6 +221,18 @@ export async function collectSelectiveCoverageEvidenceV1(input: { edgePeerId: edgeAfterRestartReady.peerId, corePeerId: core.peerId, }, + automaticJournalEvidence: { + edgeProcess: { + processStartedAt: edgeAfterRestartReady.processStartedAt, + evidenceWaveId: edgeAfterRestartReady.evidenceWaveId, + }, + edgeReconciler: Object.freeze(edgeReconcilerJournals), + coreProcess: { + processStartedAt: core.processStartedAt, + evidenceWaveId: core.evidenceWaveId, + }, + coreRounds: Object.freeze(coreRoundJournals), + }, corpus: input.corpus, publisher: { selected: publisherSelected, diff --git a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts index 4e19b1ad27..53e803d2e1 100644 --- a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts +++ b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts @@ -1,22 +1,29 @@ import type { CoreAutomaticRoundV1, EdgeSyncOperationV1, + SyncCoverageJournalProcessIdentityV1, + SyncCoverageJournalReferenceV1, } from './manifest.ts'; const JOURNAL_CAPACITY = 256; const MAX_CONTEXT_GRAPH_IDS = 32; const MAX_CONTEXT_GRAPH_ID_LENGTH = 256; -export interface SyncCoverageJournalReferenceV1 { - /** Raw node-admin response from /api/diagnostics/sync-coverage-evidence. */ - readonly snapshot: unknown; - /** Exact terminal entry selected by the adapter. */ - readonly sequence: number; -} +export type { + SyncCoverageJournalProcessIdentityV1, + SyncCoverageJournalReferenceV1, +} from './manifest.ts'; -export interface SyncCoverageJournalProcessIdentityV1 { - readonly processStartedAt: number; - readonly evidenceWaveId: string; +/** Parse the closed outer reference before retaining untrusted journal JSON. */ +export function parseSyncCoverageJournalReferenceV1( + input: unknown, +): SyncCoverageJournalReferenceV1 | undefined { + if (!isPlainRecord(input) + || Reflect.ownKeys(input).length !== 2 + || !Object.hasOwn(input, 'snapshot') + || !Object.hasOwn(input, 'sequence') + || !nonNegativeInteger(input['sequence'])) return undefined; + return { snapshot: input['snapshot'], sequence: input['sequence'] }; } /** diff --git a/devnet/rfc64-m1-selective-coverage/verifier.test.ts b/devnet/rfc64-m1-selective-coverage/verifier.test.ts index a1458c1ff9..e8c5e7cd51 100644 --- a/devnet/rfc64-m1-selective-coverage/verifier.test.ts +++ b/devnet/rfc64-m1-selective-coverage/verifier.test.ts @@ -125,10 +125,86 @@ function edgeExact( }; } +function journalReference( + entry: Record, + processStartedAt: number, +) { + const sequence = entry.sequence as number; + return { + sequence, + snapshot: { + schemaVersion: 1, + processStartedAt, + waveId: entry.waveId, + capacity: 256, + nextSequence: sequence + 1, + droppedBeforeSequence: 0, + entries: [entry], + }, + }; +} + +function coreJournal( + round: number, + jobId: string, + contextGraphIds: readonly string[], +) { + return journalReference({ + kind: 'core-automatic-round', + sequence: round + 1, + waveId: 'core-wave', + jobId, + planningLane: 'publisher-peer', + source: 'automatic-core-public', + trigger: 'peer-sync', + configuredBatchSize: 2, + effectiveBatchSize: 2, + explicitSelectedContextGraphIds: [], + explicitSelectedContextGraphCount: 0, + automaticContextGraphIds: contextGraphIds, + automaticContextGraphCount: contextGraphIds.length, + evidenceTruncated: false, + state: 'complete', + startedAt: 20 + round, + finishedAt: 21 + round, + completions: contextGraphIds.map((contextGraphId) => ({ + jobId, + contextGraphId, + state: 'complete', + verified: { metadata: true, durable: true, sharedMemory: true }, + finishedAt: 21 + round, + })), + }, 2); +} + function fixture(): SelectiveCoverageEvidenceV1 { return { schema: SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, provenance: PROVENANCE, + automaticJournalEvidence: { + edgeProcess: { processStartedAt: 1, evidenceWaveId: 'edge-wave' }, + edgeReconciler: [journalReference({ + kind: 'edge-reconciler-job', + sequence: 1, + waveId: 'edge-wave', + jobId: 'edge-auto-always-on', + contextGraphId: graphs[1]!.contextGraphId, + source: 'reconciler', + trigger: 'periodic-reconciler', + syncMode: 'always-on', + rehydratedSelectionCount: 1, + evidenceTruncated: false, + state: 'complete', + verified: { metadata: true, durable: true, sharedMemory: true }, + startedAt: 10, + finishedAt: 11, + }, 1)], + coreProcess: { processStartedAt: 2, evidenceWaveId: 'core-wave' }, + coreRounds: [ + coreJournal(0, 'core-auto-0', [graphs[0]!.contextGraphId, graphs[1]!.contextGraphId]), + coreJournal(1, 'core-auto-1', [graphs[2]!.contextGraphId]), + ], + }, corpus, publisher: { selected: corpus.graphs.map((graph) => exact(graph, graph.selectedSnapshot)), @@ -269,6 +345,26 @@ test('accepts exact Edge selection and bounded Core public convergence evidence' } }); +test('published artifact must retain matching automatic journal proof', () => { + const missing = clone(); + delete missing.automaticJournalEvidence; + assert.equal(verifySelectiveCoverage(missing).checks.schemaWellFormed, false); + + const relabelled = clone(); + relabelled.edge.operations[2].jobId = 'synthetic-reconciler-job'; + relabelled.edge.afterRestart[1].producingJobId = 'synthetic-reconciler-job'; + relabelled.edge.afterSecondOnDemand[1].producingJobId = 'synthetic-reconciler-job'; + const edgeVerdict = verifySelectiveCoverage(relabelled); + assert.equal(edgeVerdict.checks.edgeOperationProvenance, false); + + const syntheticCore = clone(); + syntheticCore.core.rounds[0].jobId = 'synthetic-core-round'; + syntheticCore.core.final[0].automaticJobIds = ['synthetic-core-round']; + syntheticCore.core.final[1].automaticJobIds = ['synthetic-core-round']; + const coreVerdict = verifySelectiveCoverage(syntheticCore); + assert.equal(coreVerdict.checks.coreAutomaticProvenance, false); +}); + test('corpus and evidence serialization is deterministic', () => { assert.equal( canonicalJson({ z: { second: 2, first: 1 }, a: ['@', ':', '/'] }), diff --git a/devnet/rfc64-m1-selective-coverage/verifier.ts b/devnet/rfc64-m1-selective-coverage/verifier.ts index e28634b761..8c53e0994a 100644 --- a/devnet/rfc64-m1-selective-coverage/verifier.ts +++ b/devnet/rfc64-m1-selective-coverage/verifier.ts @@ -20,7 +20,14 @@ import { type SelectiveCoverageEvidenceV1, type SelectiveCoverageGraphV1, type SelectiveCoverageVerdictV1, + type SyncCoverageJournalProcessIdentityV1, + type SyncCoverageJournalReferenceV1, } from './manifest.ts'; +import { + assertCoreAutomaticRoundJournalV1, + assertEdgeReconcilerJournalV1, + parseSyncCoverageJournalReferenceV1, +} from './sync-coverage-journal.ts'; const DIGEST = /^(?:0x|sha256:)[0-9a-f]{64}$/u; const ID = /^[A-Za-z0-9._:/@-]+$/u; @@ -98,19 +105,72 @@ function verifyParsed( evidence: SelectiveCoverageEvidenceV1, expected: ExpectedSelectiveCoverageProvenanceV1, ): SelectiveCoverageVerdictV1 { - const { corpus } = evidence; - const graphIds = corpus.graphs.map((graph) => graph.contextGraphId); - const publicGraphs = corpus.graphs.filter((graph) => graph.accessPolicy === 0); - const privateGraphs = corpus.graphs.filter((graph) => graph.accessPolicy === 1); - const byId = new Map(corpus.graphs.map((graph) => [graph.contextGraphId, graph])); - const publisherSelected = byObservationId(evidence.publisher.selected); - const publisherFinal = byObservationId(evidence.publisher.final); - const edgeBefore = byObservationId(evidence.edge.beforeSelection); - const edgeSelected = byObservationId(evidence.edge.afterSelection); - const edgeRestarted = byObservationId(evidence.edge.afterRestart); - const edgeSecondOnDemand = byObservationId(evidence.edge.afterSecondOnDemand); - const coreFinal = byObservationId(evidence.core.final); + const context = buildVerificationContext(evidence, expected); + const envelope = verifyEnvelope(context); + const publisher = verifyPublisher(context); + const edge = verifyEdge(context); + const core = verifyCore(context); + const checks = Object.freeze({ + schemaWellFormed: true, + ...envelope, + ...publisher, + ...edge, + ...core.checks, + noMetadataOnlyCompletion: verifyExactPayloads(context), + }) satisfies SelectiveCoverageChecksV1; + const rejectReasons = CHECK_NAMES.filter((name) => !checks[name]).map((name) => REASONS[name]); + return Object.freeze({ + schema: SELECTIVE_COVERAGE_VERDICT_SCHEMA, + pass: CHECK_NAMES.every((name) => checks[name]), + checks, + missingCoreContextGraphIds: core.missingContextGraphIds, + rejectReasons: Object.freeze(rejectReasons), + recomputedCorpusDigest: computeSelectiveCoverageCorpusDigest(context.corpus), + }); +} +interface VerificationContext { + readonly evidence: SelectiveCoverageEvidenceV1; + readonly expected: ExpectedSelectiveCoverageProvenanceV1; + readonly corpus: SelectiveCoverageCorpusV1; + readonly graphIds: readonly string[]; + readonly publicGraphs: readonly SelectiveCoverageGraphV1[]; + readonly privateGraphs: readonly SelectiveCoverageGraphV1[]; + readonly byId: ReadonlyMap; + readonly publisherSelected: ReadonlyMap; + readonly publisherFinal: ReadonlyMap; + readonly edgeBefore: ReadonlyMap; + readonly edgeSelected: ReadonlyMap; + readonly edgeRestarted: ReadonlyMap; + readonly edgeSecondOnDemand: ReadonlyMap; + readonly coreFinal: ReadonlyMap; +} + +function buildVerificationContext( + evidence: SelectiveCoverageEvidenceV1, + expected: ExpectedSelectiveCoverageProvenanceV1, +): VerificationContext { + const corpus = evidence.corpus; + return { + evidence, + expected, + corpus, + graphIds: corpus.graphs.map((graph) => graph.contextGraphId), + publicGraphs: corpus.graphs.filter((graph) => graph.accessPolicy === 0), + privateGraphs: corpus.graphs.filter((graph) => graph.accessPolicy === 1), + byId: new Map(corpus.graphs.map((graph) => [graph.contextGraphId, graph])), + publisherSelected: byObservationId(evidence.publisher.selected), + publisherFinal: byObservationId(evidence.publisher.final), + edgeBefore: byObservationId(evidence.edge.beforeSelection), + edgeSelected: byObservationId(evidence.edge.afterSelection), + edgeRestarted: byObservationId(evidence.edge.afterRestart), + edgeSecondOnDemand: byObservationId(evidence.edge.afterSecondOnDemand), + coreFinal: byObservationId(evidence.core.final), + }; +} + +function verifyEnvelope(context: VerificationContext) { + const { evidence, expected, corpus, graphIds } = context; const observationsCanonical = [ evidence.publisher.selected, evidence.publisher.final, @@ -120,24 +180,34 @@ function verifyParsed( evidence.edge.afterSecondOnDemand, evidence.core.final, ].every((rows) => exactCanonicalIds(rows.map((row) => row.contextGraphId), graphIds)); - const corpusCanonicalOrder = strictlyIncreasing(graphIds) && observationsCanonical; - const provenanceMatches = evidence.provenance.networkId === expected.networkId - && evidence.provenance.testedHeadCommit === expected.testedHeadCommit - && evidence.provenance.runtimeManifestDigest === expected.runtimeManifestDigest - && evidence.provenance.publisherPeerId === expected.publisherPeerId - && evidence.provenance.edgePeerId === expected.edgePeerId - && evidence.provenance.corePeerId === expected.corePeerId - && corpus.networkId === expected.networkId - && corpus.manifestDigest === expected.corpusManifestDigest; - const corpusDigestMatches = computeSelectiveCoverageCorpusDigest(corpus) - === corpus.manifestDigest; - const requiredPolicyCellsPresent = hasRequiredPolicyCells(corpus.graphs); - const publisherSnapshotsExact = corpus.graphs.every((graph) => - exactGraph(publisherSelected.get(graph.contextGraphId), graph.selectedSnapshot) - && exactGraph(publisherFinal.get(graph.contextGraphId), graph.finalSnapshot)); - const publicSecondWaveAdvances = publicGraphs.every((graph) => - snapshotsAdvance(graph.selectedSnapshot, graph.finalSnapshot)); + return { + provenanceMatches: evidence.provenance.networkId === expected.networkId + && evidence.provenance.testedHeadCommit === expected.testedHeadCommit + && evidence.provenance.runtimeManifestDigest === expected.runtimeManifestDigest + && evidence.provenance.publisherPeerId === expected.publisherPeerId + && evidence.provenance.edgePeerId === expected.edgePeerId + && evidence.provenance.corePeerId === expected.corePeerId + && corpus.networkId === expected.networkId + && corpus.manifestDigest === expected.corpusManifestDigest, + corpusDigestMatches: computeSelectiveCoverageCorpusDigest(corpus) === corpus.manifestDigest, + corpusCanonicalOrder: strictlyIncreasing(graphIds) && observationsCanonical, + requiredPolicyCellsPresent: hasRequiredPolicyCells(corpus.graphs), + }; +} +function verifyPublisher(context: VerificationContext) { + return { + publisherSnapshotsExact: context.corpus.graphs.every((graph) => + exactGraph(context.publisherSelected.get(graph.contextGraphId), graph.selectedSnapshot) + && exactGraph(context.publisherFinal.get(graph.contextGraphId), graph.finalSnapshot)), + publicSecondWaveAdvances: context.publicGraphs.every((graph) => + snapshotsAdvance(graph.selectedSnapshot, graph.finalSnapshot)), + }; +} + +function verifyEdge(context: VerificationContext) { + const { corpus, evidence, edgeBefore, edgeSelected, edgeRestarted, + edgeSecondOnDemand } = context; const edgePassiveBeforeSelection = corpus.graphs.every((graph) => absentGraph(edgeBefore.get(graph.contextGraphId)) && edgeBefore.get(graph.contextGraphId)?.runtimeSyncMode === null @@ -147,7 +217,8 @@ function verifyParsed( return graph.accessPolicy === 0 && graph.edgePolicy !== 'unselected' ? exactGraph(observed, graph.selectedSnapshot) && observed?.runtimeSyncMode === graph.edgePolicy - && observed.producingJobId === edgeJobId(evidence.edge.operations, graph.contextGraphId, 'selection') + && observed.producingJobId + === edgeJobId(evidence.edge.operations, graph.contextGraphId, 'selection') : absentGraph(observed) && observed?.runtimeSyncMode === null && observed.producingJobId === null; }); @@ -159,14 +230,11 @@ function verifyParsed( === edgeJobId(evidence.edge.operations, graph.contextGraphId, 'selection')); const edgeAlwaysOnRefreshesAfterRestart = corpus.graphs .filter((graph) => graph.edgePolicy === 'always-on') - .every((graph) => ( - snapshotsAdvance(graph.selectedSnapshot, graph.finalSnapshot) + .every((graph) => snapshotsAdvance(graph.selectedSnapshot, graph.finalSnapshot) && exactGraph(edgeRestarted.get(graph.contextGraphId), graph.finalSnapshot) && edgeRestarted.get(graph.contextGraphId)?.runtimeSyncMode === 'always-on' && edgeRestarted.get(graph.contextGraphId)?.producingJobId - === edgeJobId(evidence.edge.operations, graph.contextGraphId, 'post-restart-auto') - )); - const edgeOperationProvenance = verifyEdgeOperations(evidence.edge.operations, corpus.graphs); + === edgeJobId(evidence.edge.operations, graph.contextGraphId, 'post-restart-auto')); const edgeSecondOnDemandConverges = corpus.graphs.every((graph) => { const observed = edgeSecondOnDemand.get(graph.contextGraphId); if (graph.accessPolicy !== 0 || graph.edgePolicy === 'unselected') { @@ -182,33 +250,39 @@ function verifyParsed( && observed.producingJobId === edgeJobId(evidence.edge.operations, graph.contextGraphId, phase); }); - const edgeUnselectedExcluded = publicGraphs - .filter((graph) => graph.edgePolicy === 'unselected') - .every((graph) => absentGraph(edgeSelected.get(graph.contextGraphId)) - && absentGraph(edgeRestarted.get(graph.contextGraphId)) - && absentGraph(edgeSecondOnDemand.get(graph.contextGraphId)) - && [edgeSelected, edgeRestarted, edgeSecondOnDemand].every((phase) => - phase.get(graph.contextGraphId)?.runtimeSyncMode === null - && phase.get(graph.contextGraphId)?.producingJobId === null)); - const edgePrivateExcluded = privateGraphs.every((graph) => + const excluded = (graph: SelectiveCoverageGraphV1) => absentGraph(edgeSelected.get(graph.contextGraphId)) && absentGraph(edgeRestarted.get(graph.contextGraphId)) && absentGraph(edgeSecondOnDemand.get(graph.contextGraphId)) && [edgeSelected, edgeRestarted, edgeSecondOnDemand].every((phase) => phase.get(graph.contextGraphId)?.runtimeSyncMode === null - && phase.get(graph.contextGraphId)?.producingJobId === null)); - - const coreBatchMatchesManifest = evidence.core.automaticBatchSize - === corpus.coreAutomaticBatchSize; - const coreBatchWithinBound = evidence.core.rounds.every((round) => - round.contextGraphIds.length <= corpus.coreAutomaticBatchSize); - const coreRoundsPublicOnly = evidence.core.rounds.every((round) => { - const unique = new Set(round.contextGraphIds); - return unique.size === round.contextGraphIds.length - && round.contextGraphIds.every((contextGraphId) => byId.get(contextGraphId)?.accessPolicy === 0); - }); + && phase.get(graph.contextGraphId)?.producingJobId === null); + return { + edgePassiveBeforeSelection, + edgeSelectedSnapshotsExact, + edgeOnDemandRemainsPointInTime, + edgeAlwaysOnRefreshesAfterRestart, + edgeOperationProvenance: verifyEdgeOperations(evidence.edge.operations, corpus.graphs) + && verifyAutomaticEdgeJournals(evidence), + edgeSecondOnDemandConverges, + edgeUnselectedExcluded: context.publicGraphs + .filter((graph) => graph.edgePolicy === 'unselected').every(excluded), + edgePrivateExcluded: context.privateGraphs.every(excluded), + }; +} + +function verifyCore(context: VerificationContext) { + const { evidence, expected, corpus, byId, coreFinal } = context; const automaticJobs = new Map(evidence.core.rounds.map((round) => [round.jobId, round])); + const scheduled = new Set(evidence.core.rounds.flatMap((round) => round.contextGraphIds)); + const missingContextGraphIds = Object.freeze(context.publicGraphs + .map((graph) => graph.contextGraphId) + .filter((contextGraphId) => !scheduled.has(contextGraphId))); + const scheduledWithinWindow = new Set(evidence.core.rounds + .slice(0, corpus.coreCoverageRoundLimit) + .flatMap((round) => round.contextGraphIds)); const coreAutomaticProvenance = automaticJobs.size === evidence.core.rounds.length + && verifyAutomaticCoreJournals(evidence) && evidence.core.rounds.every((round) => round.source === 'automatic-core-public' && round.planningLane === expected.publisherPeerId @@ -234,95 +308,67 @@ function verifyParsed( completion.contextGraphId === observation.contextGraphId && exactSnapshot(completion.completedSnapshot, graph.finalSnapshot))); }); - const scheduled = new Set(evidence.core.rounds.flatMap((round) => round.contextGraphIds)); - const missingCoreContextGraphIds = publicGraphs - .map((graph) => graph.contextGraphId) - .filter((contextGraphId) => !scheduled.has(contextGraphId)); - const coreEveryPublicScheduled = missingCoreContextGraphIds.length === 0; - const scheduledWithinWindow = new Set( - evidence.core.rounds - .slice(0, corpus.coreCoverageRoundLimit) - .flatMap((round) => round.contextGraphIds), - ); - const coreCoverageWithinWindow = publicGraphs.every((graph) => - scheduledWithinWindow.has(graph.contextGraphId)); - const coreFinalPublicExact = publicGraphs.every((graph) => - exactGraph(coreFinal.get(graph.contextGraphId), graph.finalSnapshot)); - const corePrivateExcluded = privateGraphs.every((graph) => - absentGraph(coreFinal.get(graph.contextGraphId)) && !scheduled.has(graph.contextGraphId)); - - const requiredExactPlanes: Array = []; - for (const graph of corpus.graphs) { + return { + checks: { + coreBatchMatchesManifest: evidence.core.automaticBatchSize + === corpus.coreAutomaticBatchSize, + coreBatchWithinBound: evidence.core.rounds.every((round) => + round.contextGraphIds.length <= corpus.coreAutomaticBatchSize), + coreRoundsPublicOnly: evidence.core.rounds.every((round) => + new Set(round.contextGraphIds).size === round.contextGraphIds.length + && round.contextGraphIds.every((id) => byId.get(id)?.accessPolicy === 0)), + coreAutomaticProvenance, + coreEveryPublicScheduled: missingContextGraphIds.length === 0, + coreCoverageWithinWindow: context.publicGraphs.every((graph) => + scheduledWithinWindow.has(graph.contextGraphId)), + coreFinalPublicExact: context.publicGraphs.every((graph) => + exactGraph(coreFinal.get(graph.contextGraphId), graph.finalSnapshot)), + corePrivateExcluded: context.privateGraphs.every((graph) => + absentGraph(coreFinal.get(graph.contextGraphId)) && !scheduled.has(graph.contextGraphId)), + }, + missingContextGraphIds, + }; +} + +function verifyExactPayloads(context: VerificationContext): boolean { + const required: Array = []; + for (const graph of context.corpus.graphs) { if (graph.accessPolicy === 0 && graph.edgePolicy !== 'unselected') { - const selected = edgeSelected.get(graph.contextGraphId); - requiredExactPlanes.push([selected?.vm, graph.selectedSnapshot.vm], [selected?.swm, graph.selectedSnapshot.swm]); - const restarted = edgeRestarted.get(graph.contextGraphId); + const selected = context.edgeSelected.get(graph.contextGraphId); + required.push([selected?.vm, graph.selectedSnapshot.vm], [selected?.swm, graph.selectedSnapshot.swm]); + const restarted = context.edgeRestarted.get(graph.contextGraphId); const expected = graph.edgePolicy === 'always-on' ? graph.finalSnapshot : graph.selectedSnapshot; - requiredExactPlanes.push([restarted?.vm, expected.vm], [restarted?.swm, expected.swm]); - const secondOnDemand = edgeSecondOnDemand.get(graph.contextGraphId); - requiredExactPlanes.push( - [secondOnDemand?.vm, graph.finalSnapshot.vm], - [secondOnDemand?.swm, graph.finalSnapshot.swm], - ); + required.push([restarted?.vm, expected.vm], [restarted?.swm, expected.swm]); + const final = context.edgeSecondOnDemand.get(graph.contextGraphId); + required.push([final?.vm, graph.finalSnapshot.vm], [final?.swm, graph.finalSnapshot.swm]); } if (graph.accessPolicy === 0) { - const final = coreFinal.get(graph.contextGraphId); - requiredExactPlanes.push([final?.vm, graph.finalSnapshot.vm], [final?.swm, graph.finalSnapshot.swm]); + const final = context.coreFinal.get(graph.contextGraphId); + required.push([final?.vm, graph.finalSnapshot.vm], [final?.swm, graph.finalSnapshot.swm]); } } - const noMetadataOnlyCompletion = requiredExactPlanes.every(([observed, expected]) => + return required.every(([observed, expected]) => exactPlane(observed, expected) && (observed?.dataTripleCount ?? 0) > 0); - - const checks = Object.freeze({ - schemaWellFormed: true, - provenanceMatches, - corpusDigestMatches, - corpusCanonicalOrder, - requiredPolicyCellsPresent, - publisherSnapshotsExact, - publicSecondWaveAdvances, - edgePassiveBeforeSelection, - edgeSelectedSnapshotsExact, - edgeOnDemandRemainsPointInTime, - edgeAlwaysOnRefreshesAfterRestart, - edgeOperationProvenance, - edgeSecondOnDemandConverges, - edgeUnselectedExcluded, - edgePrivateExcluded, - coreBatchMatchesManifest, - coreBatchWithinBound, - coreRoundsPublicOnly, - coreAutomaticProvenance, - coreEveryPublicScheduled, - coreCoverageWithinWindow, - coreFinalPublicExact, - corePrivateExcluded, - noMetadataOnlyCompletion, - }) satisfies SelectiveCoverageChecksV1; - const rejectReasons = CHECK_NAMES.filter((name) => !checks[name]).map((name) => REASONS[name]); - return Object.freeze({ - schema: SELECTIVE_COVERAGE_VERDICT_SCHEMA, - pass: CHECK_NAMES.every((name) => checks[name]), - checks, - missingCoreContextGraphIds: Object.freeze(missingCoreContextGraphIds), - rejectReasons: Object.freeze(rejectReasons), - recomputedCorpusDigest: computeSelectiveCoverageCorpusDigest(corpus), - }); } function parseEvidence(input: unknown): SelectiveCoverageEvidenceV1 | undefined { const root = closedRecord(input, [ - 'schema', 'provenance', 'corpus', 'publisher', 'edge', 'core', + 'schema', 'provenance', 'automaticJournalEvidence', 'corpus', 'publisher', 'edge', 'core', ]); if (!root || root.schema !== SELECTIVE_COVERAGE_EVIDENCE_SCHEMA) return undefined; const provenance = parseProvenance(root.provenance); + const automaticJournalEvidence = parseAutomaticJournalEvidence( + root.automaticJournalEvidence, + ); const corpus = parseCorpus(root.corpus); const publisher = closedRecord(root.publisher, ['selected', 'final']); const edge = closedRecord(root.edge, [ 'beforeSelection', 'afterSelection', 'afterRestart', 'afterSecondOnDemand', 'operations', ]); const core = closedRecord(root.core, ['automaticBatchSize', 'rounds', 'final']); - if (!provenance || !corpus || !publisher || !edge || !core) return undefined; + if (!provenance || !automaticJournalEvidence || !corpus || !publisher || !edge || !core) { + return undefined; + } const publisherSelected = parseObservations(publisher.selected); const publisherFinal = parseObservations(publisher.final); const beforeSelection = parseEdgeObservations(edge.beforeSelection); @@ -391,6 +437,7 @@ function parseEvidence(input: unknown): SelectiveCoverageEvidenceV1 | undefined return { schema: SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, provenance, + automaticJournalEvidence, corpus, publisher: { selected: publisherSelected, final: publisherFinal }, edge: { @@ -408,6 +455,39 @@ function parseEvidence(input: unknown): SelectiveCoverageEvidenceV1 | undefined }; } +function parseAutomaticJournalEvidence( + input: unknown, +): SelectiveCoverageEvidenceV1['automaticJournalEvidence'] | undefined { + const root = closedRecord(input, [ + 'edgeProcess', 'edgeReconciler', 'coreProcess', 'coreRounds', + ]); + if (!root + || !closedArray(root.edgeReconciler, 0, MAX_SELECTIVE_COVERAGE_GRAPHS) + || !closedArray(root.coreRounds, 1, MAX_SELECTIVE_COVERAGE_ROUNDS)) return undefined; + const edgeProcess = parseJournalProcessIdentity(root.edgeProcess); + const coreProcess = parseJournalProcessIdentity(root.coreProcess); + const edgeReconciler = root.edgeReconciler.map(parseSyncCoverageJournalReferenceV1); + const coreRounds = root.coreRounds.map(parseSyncCoverageJournalReferenceV1); + if (!edgeProcess || !coreProcess + || edgeReconciler.some((entry) => entry === undefined) + || coreRounds.some((entry) => entry === undefined)) return undefined; + return { + edgeProcess, + edgeReconciler: Object.freeze(edgeReconciler as SyncCoverageJournalReferenceV1[]), + coreProcess, + coreRounds: Object.freeze(coreRounds as SyncCoverageJournalReferenceV1[]), + }; +} + +function parseJournalProcessIdentity( + input: unknown, +): SyncCoverageJournalProcessIdentityV1 | undefined { + const root = closedRecord(input, ['processStartedAt', 'evidenceWaveId']); + const evidenceWaveId = root && identifier(root.evidenceWaveId); + if (!root || !nonNegativeInteger(root.processStartedAt) || !evidenceWaveId) return undefined; + return { processStartedAt: root.processStartedAt as number, evidenceWaveId }; +} + function parseCorpus(input: unknown): SelectiveCoverageCorpusV1 | undefined { const root = closedRecord(input, [ 'schema', 'networkId', 'coreAutomaticBatchSize', 'coreCoverageRoundLimit', @@ -650,6 +730,40 @@ function hasRequiredPolicyCells(graphs: readonly SelectiveCoverageGraphV1[]): bo && graphs.some((graph) => graph.accessPolicy === 1 && graph.publishPolicy === 0); } +function verifyAutomaticEdgeJournals(evidence: SelectiveCoverageEvidenceV1): boolean { + const operations = evidence.edge.operations.filter((operation) => + operation.phase === 'post-restart-auto'); + if (operations.length !== evidence.automaticJournalEvidence.edgeReconciler.length) { + return false; + } + try { + operations.forEach((operation, index) => assertEdgeReconcilerJournalV1( + evidence.automaticJournalEvidence.edgeReconciler[index], + operation, + evidence.automaticJournalEvidence.edgeProcess, + )); + return true; + } catch { + return false; + } +} + +function verifyAutomaticCoreJournals(evidence: SelectiveCoverageEvidenceV1): boolean { + if (evidence.core.rounds.length !== evidence.automaticJournalEvidence.coreRounds.length) { + return false; + } + try { + evidence.core.rounds.forEach((round, index) => assertCoreAutomaticRoundJournalV1( + evidence.automaticJournalEvidence.coreRounds[index], + round, + evidence.automaticJournalEvidence.coreProcess, + )); + return true; + } catch { + return false; + } +} + function verifyEdgeOperations( operations: readonly EdgeSyncOperationV1[], graphs: readonly SelectiveCoverageGraphV1[], From 383d82ae7381fdf1abcb98d8098521cf85d91388 Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 04:54:38 +0200 Subject: [PATCH 06/14] test(rfc64): decode adapter output fail closed --- devnet/rfc64-m1-selective-coverage/README.md | 13 + .../adapter-environment.test.ts | 22 ++ .../adapter-environment.ts | 14 + .../launch-live.ts | 13 +- .../rfc64-m1-selective-coverage/package.json | 2 +- .../process-runtime-fixture.mjs | 31 +- .../process-runtime.test.ts | 48 +++ .../process-runtime.ts | 60 ++-- .../runtime-wire.ts | 305 ++++++++++++++++++ package.json | 2 +- 10 files changed, 474 insertions(+), 36 deletions(-) create mode 100644 devnet/rfc64-m1-selective-coverage/adapter-environment.test.ts create mode 100644 devnet/rfc64-m1-selective-coverage/adapter-environment.ts create mode 100644 devnet/rfc64-m1-selective-coverage/runtime-wire.ts diff --git a/devnet/rfc64-m1-selective-coverage/README.md b/devnet/rfc64-m1-selective-coverage/README.md index 655985511f..97a422977d 100644 --- a/devnet/rfc64-m1-selective-coverage/README.md +++ b/devnet/rfc64-m1-selective-coverage/README.md @@ -126,6 +126,19 @@ substitutes. The admin subscriptions response supplies the effective Edge `syncMode`; omission retains the legacy `always-on` interpretation only inside the node, never as evidence for a requested on-demand selection. +The exact bounded journal snapshots and selected sequence numbers are retained +in the canonical artifact alongside the Edge/Core process-start and wave +identities. `verify-live` parses and revalidates those raw records, so an artifact +with relabeled automatic fields, synthetic job IDs, missing journal proof, or a +mismatched process wave is rejected independently of the collection process. + +Every adapter response is decoded through a closed per-command schema at the +process boundary before orchestration can consume it. Wrong session/protocol, +unknown sequences, malformed JSON, oversized lines, unexpected keys, and +malformed command values all fail closed. The launcher also has a direct +regression test proving that corpus, trust-anchor, and artifact paths are absent +from the spawned adapter environment. + This repository supplies the fail-closed orchestrator and framed adapter protocol, not a deployment-specific adapter executable. The live command is therefore intentionally blocked unless `DKG_RFC64_M1_ADAPTER_COMMAND` names an diff --git a/devnet/rfc64-m1-selective-coverage/adapter-environment.test.ts b/devnet/rfc64-m1-selective-coverage/adapter-environment.test.ts new file mode 100644 index 0000000000..d03789a500 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/adapter-environment.test.ts @@ -0,0 +1,22 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { buildSelectiveCoverageAdapterEnvironment } from './adapter-environment.ts'; + +test('strips immutable operator inputs from the spawned adapter environment', () => { + const parent = { + NODE_ENV: 'development', + DKG_RFC64_M1_CORPUS_FILE: '/secure/corpus.json', + DKG_RFC64_M1_TRUST_ANCHOR_FILE: '/secure/trust-anchor.json', + DKG_RFC64_M1_ARTIFACT: '/secure/pass-artifact.json', + PRESERVED_ADAPTER_SETTING: 'present', + }; + const result = buildSelectiveCoverageAdapterEnvironment(parent); + + assert.equal(result.NODE_ENV, 'production'); + assert.equal(result.PRESERVED_ADAPTER_SETTING, 'present'); + assert.equal(Object.hasOwn(result, 'DKG_RFC64_M1_CORPUS_FILE'), false); + assert.equal(Object.hasOwn(result, 'DKG_RFC64_M1_TRUST_ANCHOR_FILE'), false); + assert.equal(Object.hasOwn(result, 'DKG_RFC64_M1_ARTIFACT'), false); + assert.equal(parent.DKG_RFC64_M1_TRUST_ANCHOR_FILE, '/secure/trust-anchor.json'); +}); diff --git a/devnet/rfc64-m1-selective-coverage/adapter-environment.ts b/devnet/rfc64-m1-selective-coverage/adapter-environment.ts new file mode 100644 index 0000000000..509be441a1 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/adapter-environment.ts @@ -0,0 +1,14 @@ +const PRIVATE_LAUNCHER_INPUTS = Object.freeze([ + 'DKG_RFC64_M1_CORPUS_FILE', + 'DKG_RFC64_M1_TRUST_ANCHOR_FILE', + 'DKG_RFC64_M1_ARTIFACT', +]); + +/** Keep immutable expectations outside the operator adapter's process boundary. */ +export function buildSelectiveCoverageAdapterEnvironment( + parent: NodeJS.ProcessEnv, +): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { ...parent, NODE_ENV: 'production' }; + for (const name of PRIVATE_LAUNCHER_INPUTS) delete env[name]; + return env; +} diff --git a/devnet/rfc64-m1-selective-coverage/launch-live.ts b/devnet/rfc64-m1-selective-coverage/launch-live.ts index 84c9bf3e3d..c3e11f9ed0 100644 --- a/devnet/rfc64-m1-selective-coverage/launch-live.ts +++ b/devnet/rfc64-m1-selective-coverage/launch-live.ts @@ -17,6 +17,7 @@ import { import { ProcessSelectiveCoverageRuntimeV1 } from './process-runtime.ts'; import { collectSelectiveCoverageEvidenceV1 } from './runtime.ts'; import { runSelectiveCoverageLiveV1 } from './live-runner.ts'; +import { buildSelectiveCoverageAdapterEnvironment } from './adapter-environment.ts'; const repoRoot = resolve(import.meta.dirname, '../..'); const corpusPath = resolveRequiredPath('DKG_RFC64_M1_CORPUS_FILE'); @@ -53,7 +54,7 @@ const runtime = new ProcessSelectiveCoverageRuntimeV1({ args: adapterArgs, cwd: adapterCwd, timeoutMs, - env: adapterEnvironment(), + env: buildSelectiveCoverageAdapterEnvironment(process.env), }); await runSelectiveCoverageLiveV1({ collect: () => collectSelectiveCoverageEvidenceV1({ @@ -110,13 +111,3 @@ function parseTimeout(value: string | undefined): number | undefined { } return parsed; } - -function adapterEnvironment(): NodeJS.ProcessEnv { - const env: NodeJS.ProcessEnv = { ...process.env, NODE_ENV: 'production' }; - for (const name of [ - 'DKG_RFC64_M1_CORPUS_FILE', - 'DKG_RFC64_M1_TRUST_ANCHOR_FILE', - 'DKG_RFC64_M1_ARTIFACT', - ]) delete env[name]; - return env; -} diff --git a/devnet/rfc64-m1-selective-coverage/package.json b/devnet/rfc64-m1-selective-coverage/package.json index 6802356548..7c49732e6f 100644 --- a/devnet/rfc64-m1-selective-coverage/package.json +++ b/devnet/rfc64-m1-selective-coverage/package.json @@ -6,7 +6,7 @@ "description": "Fail-closed evidence contract for RFC-64 M1 Edge selection and bounded Core public coverage.", "scripts": { "typecheck": "tsc --noEmit -p tsconfig.json", - "test": "node --experimental-strip-types --test verifier.test.ts runtime.test.ts process-runtime.test.ts", + "test": "node --experimental-strip-types --test verifier.test.ts runtime.test.ts process-runtime.test.ts adapter-environment.test.ts", "live": "node --import tsx launch-live.ts", "verify-live": "node --import tsx verify-live.ts" } diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs b/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs index 12b3b1bce8..0785516b59 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs +++ b/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs @@ -27,13 +27,40 @@ lines.on('line', (line) => { evidenceWaveId: `${role}-wave`, }; } - process.stdout.write(`${prefix}${JSON.stringify({ + const result = { schema: resultSchema, protocol, sessionNonce: input.sessionNonce, sequence: input.sequence, ok: true, value, - })}\n`); + }; + const mode = process.env.FIXTURE_MODE; + if (input.command === 'publish-wave' && mode === 'malformed-publish') { + process.stdout.write(`${prefix}${JSON.stringify(result)}\n`, () => process.exit(0)); + return; + } + if (input.command === 'start' && mode) { + if (mode === 'malformed-publish') { + process.stdout.write(`${prefix}${JSON.stringify(result)}\n`); + return; + } + if (mode === 'malformed-value') result.value = { role: input.payload.role }; + if (mode === 'wrong-nonce') result.sessionNonce = 'wrong-session'; + if (mode === 'wrong-protocol') result.protocol = 'wrong-protocol'; + if (mode === 'wrong-schema') result.schema = 'wrong-schema'; + if (mode === 'unknown-sequence') result.sequence += 1; + if (mode === 'malformed-json') { + process.stdout.write(`${prefix}{not-json\n`, () => process.exit(0)); + return; + } + if (mode === 'oversized-line') { + process.stdout.write(`${'x'.repeat(1024 * 1024 + 1)}\n`, () => process.exit(0)); + return; + } + process.stdout.write(`${prefix}${JSON.stringify(result)}\n`, () => process.exit(0)); + return; + } + process.stdout.write(`${prefix}${JSON.stringify(result)}\n`); if (input.command === 'shutdown') process.exit(0); }); diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts index 2f71b5a330..0bf9b35037 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts +++ b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts @@ -61,3 +61,51 @@ test('exchanges sequence-bound JSON without sending the trust anchor to the adap await runtime.close(); } }); + +for (const [mode, message] of [ + ['malformed-value', /response failed decoding/], + ['wrong-nonce', /invalid result envelope/], + ['wrong-protocol', /invalid result envelope/], + ['wrong-schema', /invalid result envelope/], + ['unknown-sequence', /unknown result sequence/], + ['malformed-json', /malformed result JSON/], + ['oversized-line', /exceeds 1 MiB/], +] as const) { + test(`rejects fail-closed adapter output: ${mode}`, async () => { + const runtime = fixtureRuntime(mode); + try { + await assert.rejects(runtime.start('publisher'), message); + } finally { + await runtime.close().catch(() => undefined); + } + }); +} + +test('decodes non-start command results at the adapter boundary', async () => { + const runtime = fixtureRuntime('malformed-publish'); + try { + await runtime.start('publisher'); + await assert.rejects( + runtime.publishWave('selected'), + /response failed decoding: publish-wave/, + ); + } finally { + await runtime.close().catch(() => undefined); + } +}); + +function fixtureRuntime(mode?: string): ProcessSelectiveCoverageRuntimeV1 { + return new ProcessSelectiveCoverageRuntimeV1({ + command: process.execPath, + args: [resolve(import.meta.dirname, 'process-runtime-fixture.mjs')], + cwd: resolve(import.meta.dirname, '../..'), + timeoutMs: 5_000, + env: { + ...process.env, + ...(mode ? { FIXTURE_MODE: mode } : {}), + FIXTURE_NETWORK_ID: 'otp:20430', + FIXTURE_SOURCE_COMMIT: 'a'.repeat(40), + FIXTURE_RUNTIME_MANIFEST: `sha256:${'b'.repeat(64)}`, + }, + }); +} diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime.ts b/devnet/rfc64-m1-selective-coverage/process-runtime.ts index a7dc1325d6..635d5e4a1f 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime.ts +++ b/devnet/rfc64-m1-selective-coverage/process-runtime.ts @@ -16,6 +16,17 @@ import { type GraphObservationV1, } from './manifest.ts'; import type { SyncCoverageJournalReferenceV1 } from './sync-coverage-journal.ts'; +import { + decodeCoreFinalObservations, + decodeCoreRoundResult, + decodeEdgeObservations, + decodeEdgeReconcilerResult, + decodeEdgeSyncResult, + decodeGraphObservations, + decodeNull, + decodeRestartReceipt, + decodeRuntimeReady, +} from './runtime-wire.ts'; export const SELECTIVE_COVERAGE_RUNTIME_COMMAND_SCHEMA = 'dkg-rfc64-m1-selective-coverage-runtime-command-v1' as const; @@ -88,22 +99,22 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti private readonly timeoutMs: number; async start(role: SelectiveCoverageRuntimeRole): Promise { - return await this.request('start', { role }) as SelectiveCoverageRuntimeReadyV1; + return await this.request('start', { role }, decodeRuntimeReady); } async stop(role: SelectiveCoverageRuntimeRole): Promise { - await this.request('stop', { role }); + await this.request('stop', { role }, decodeNull); } async publishWave(wave: 'selected' | 'final'): Promise { - return await this.request('publish-wave', { wave }) as readonly GraphObservationV1[]; + return await this.request('publish-wave', { wave }, decodeGraphObservations); } async observeEdge( checkpoint: 'before-selection' | 'after-selection' | 'after-restart' | 'after-second-on-demand', ): Promise { - return (await this.request('observe-edge', { checkpoint })) as readonly EdgeGraphObservationV1[]; + return await this.request('observe-edge', { checkpoint }, decodeEdgeObservations); } async synchronizeEdge(input: { @@ -115,14 +126,11 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti readonly operation: Omit; readonly journal?: SyncCoverageJournalReferenceV1; }> { - return (await this.request('synchronize-edge', input)) as { - readonly operation: Omit; - readonly journal?: SyncCoverageJournalReferenceV1; - }; + return await this.request('synchronize-edge', input, decodeEdgeSyncResult); } async restartEdge(): Promise { - return await this.request('restart-edge', {}) as SelectiveCoverageEdgeRestartReceiptV1; + return await this.request('restart-edge', {}, decodeRestartReceipt); } async waitForEdgeReconciler(input: { @@ -131,24 +139,18 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti readonly operation: Omit; readonly journal: SyncCoverageJournalReferenceV1; }> { - return await this.request('wait-edge-reconciler', input) as { - readonly operation: Omit; - readonly journal: SyncCoverageJournalReferenceV1; - }; + return await this.request('wait-edge-reconciler', input, decodeEdgeReconcilerResult); } async runCoreAutomaticRound(round: number): Promise<{ readonly round: CoreAutomaticRoundV1; readonly journal: SyncCoverageJournalReferenceV1; }> { - return await this.request('core-automatic-round', { round }) as { - readonly round: CoreAutomaticRoundV1; - readonly journal: SyncCoverageJournalReferenceV1; - }; + return await this.request('core-automatic-round', { round }, decodeCoreRoundResult); } async observeCoreFinal(): Promise { - return await this.request('observe-core-final', {}) as readonly CoreFinalObservationV1[]; + return await this.request('observe-core-final', {}, decodeCoreFinalObservations); } async close(): Promise { @@ -156,7 +158,7 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti this.closing = true; let shutdownFailure: unknown; try { - await this.request('shutdown', {}); + await this.request('shutdown', {}, decodeNull); } catch (error) { shutdownFailure = error; } @@ -172,7 +174,11 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti if (shutdownFailure !== undefined) throw shutdownFailure; } - private request(command: string, payload: unknown): Promise { + private request( + command: string, + payload: unknown, + decode: (input: unknown) => T, + ): Promise { if (this.closed) return Promise.reject(new Error('M1 runtime adapter is closed')); if (this.exitError) return Promise.reject(this.exitError); const sequence = this.sequence; @@ -183,7 +189,19 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti reject(new Error(`M1 runtime adapter command timed out: ${command}`)); }, this.timeoutMs); timer.unref(); - this.pending.set(sequence, { resolve, reject, timer }); + this.pending.set(sequence, { + resolve: (value) => { + try { + resolve(decode(value)); + } catch (error) { + reject(new Error(`M1 runtime adapter response failed decoding: ${command}`, { + cause: error, + })); + } + }, + reject, + timer, + }); const envelope = JSON.stringify({ schema: SELECTIVE_COVERAGE_RUNTIME_COMMAND_SCHEMA, protocol: SELECTIVE_COVERAGE_RUNTIME_PROTOCOL, diff --git a/devnet/rfc64-m1-selective-coverage/runtime-wire.ts b/devnet/rfc64-m1-selective-coverage/runtime-wire.ts new file mode 100644 index 0000000000..5646a48f47 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/runtime-wire.ts @@ -0,0 +1,305 @@ +import { + MAX_SELECTIVE_COVERAGE_GRAPHS, + MAX_SELECTIVE_COVERAGE_ROUNDS, + type CoreAutomaticRoundV1, + type CoreFinalObservationV1, + type EdgeGraphObservationV1, + type EdgeSyncOperationV1, + type GraphObservationV1, + type GraphSnapshotExpectationV1, +} from './manifest.ts'; +import { + SELECTIVE_COVERAGE_RUNTIME_PROTOCOL, + type SelectiveCoverageEdgeRestartReceiptV1, + type SelectiveCoverageRuntimeReadyV1, +} from './runtime.ts'; +import { + parseSyncCoverageJournalReferenceV1, + type SyncCoverageJournalReferenceV1, +} from './sync-coverage-journal.ts'; + +type Decoder = (input: unknown) => T; + +export function decodeRuntimeReady(input: unknown): SelectiveCoverageRuntimeReadyV1 { + const row = record(input, [ + 'protocol', 'role', 'pid', 'peerId', 'networkId', 'testedHeadCommit', + 'runtimeManifestDigest', 'processStartedAt', 'processInstanceId', + 'dataDirectoryIdentity', 'evidenceWaveId', + ]); + const role = row.role; + if (row.protocol !== SELECTIVE_COVERAGE_RUNTIME_PROTOCOL + || (role !== 'publisher' && role !== 'edge' && role !== 'core') + || !positiveInteger(row.pid) + || !nonNegativeInteger(row.processStartedAt)) fail('runtime ready'); + return { + protocol: SELECTIVE_COVERAGE_RUNTIME_PROTOCOL, + role, + pid: row.pid as number, + peerId: text(row.peerId, 'peerId'), + networkId: text(row.networkId, 'networkId'), + testedHeadCommit: text(row.testedHeadCommit, 'testedHeadCommit'), + runtimeManifestDigest: text(row.runtimeManifestDigest, 'runtimeManifestDigest'), + processStartedAt: row.processStartedAt as number, + processInstanceId: text(row.processInstanceId, 'processInstanceId'), + dataDirectoryIdentity: text(row.dataDirectoryIdentity, 'dataDirectoryIdentity'), + evidenceWaveId: text(row.evidenceWaveId, 'evidenceWaveId'), + }; +} + +export function decodeRestartReceipt(input: unknown): SelectiveCoverageEdgeRestartReceiptV1 { + const row = record(input, ['previous', 'current']); + const previous = record(row.previous, ['pid', 'processInstanceId', 'exitedAt']); + if (!positiveInteger(previous.pid) || !nonNegativeInteger(previous.exitedAt)) { + fail('restart receipt'); + } + return { + previous: { + pid: previous.pid as number, + processInstanceId: text(previous.processInstanceId, 'processInstanceId'), + exitedAt: previous.exitedAt as number, + }, + current: decodeRuntimeReady(row.current), + }; +} + +export function decodeGraphObservations(input: unknown): readonly GraphObservationV1[] { + return array(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS, decodeGraphObservation); +} + +export function decodeEdgeObservations(input: unknown): readonly EdgeGraphObservationV1[] { + return array(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS, decodeEdgeObservation); +} + +export function decodeCoreFinalObservations(input: unknown): readonly CoreFinalObservationV1[] { + return array(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS, (value) => { + const row = record(value, ['contextGraphId', 'automaticJobIds', 'vm', 'swm']); + return { + contextGraphId: text(row.contextGraphId, 'contextGraphId'), + vm: decodePlaneObservation(row.vm), + swm: decodePlaneObservation(row.swm), + automaticJobIds: array(row.automaticJobIds, 0, MAX_SELECTIVE_COVERAGE_ROUNDS, + (entry) => text(entry, 'automaticJobId')), + }; + }); +} + +export function decodeEdgeSyncResult(input: unknown): { + readonly operation: Omit; + readonly journal?: SyncCoverageJournalReferenceV1; +} { + const row = optionalRecord(input, ['operation'], ['journal']); + const journal = Object.hasOwn(row, 'journal') + ? requiredJournal(row.journal) + : undefined; + return { operation: decodeEdgeOperation(row.operation), ...(journal ? { journal } : {}) }; +} + +export function decodeEdgeReconcilerResult(input: unknown): { + readonly operation: Omit; + readonly journal: SyncCoverageJournalReferenceV1; +} { + const row = record(input, ['operation', 'journal']); + return { + operation: decodeEdgeOperation(row.operation), + journal: requiredJournal(row.journal), + }; +} + +export function decodeCoreRoundResult(input: unknown): { + readonly round: CoreAutomaticRoundV1; + readonly journal: SyncCoverageJournalReferenceV1; +} { + const row = record(input, ['round', 'journal']); + return { round: decodeCoreRound(row.round), journal: requiredJournal(row.journal) }; +} + +export function decodeNull(input: unknown): null { + if (input !== null) fail('null acknowledgement'); + return null; +} + +function decodeGraphObservation(input: unknown): GraphObservationV1 { + const row = record(input, ['contextGraphId', 'vm', 'swm']); + return { + contextGraphId: text(row.contextGraphId, 'contextGraphId'), + vm: decodePlaneObservation(row.vm), + swm: decodePlaneObservation(row.swm), + }; +} + +function decodeEdgeObservation(input: unknown): EdgeGraphObservationV1 { + const row = record(input, [ + 'contextGraphId', 'runtimeSyncMode', 'producingJobId', 'vm', 'swm', + ]); + const runtimeSyncMode = row.runtimeSyncMode; + if (runtimeSyncMode !== null + && runtimeSyncMode !== 'always-on' + && runtimeSyncMode !== 'on-demand') fail('Edge runtime sync mode'); + return { + contextGraphId: text(row.contextGraphId, 'contextGraphId'), + runtimeSyncMode, + producingJobId: row.producingJobId === null + ? null + : text(row.producingJobId, 'producingJobId'), + vm: decodePlaneObservation(row.vm), + swm: decodePlaneObservation(row.swm), + }; +} + +function decodePlaneObservation(input: unknown) { + const row = record(input, [ + 'reportedComplete', 'headDigest', 'inventoryDigest', 'assetCount', + 'metadataTripleCount', 'dataTripleCount', + ]); + if (typeof row.reportedComplete !== 'boolean' + || (row.headDigest !== null && typeof row.headDigest !== 'string') + || (row.inventoryDigest !== null && typeof row.inventoryDigest !== 'string') + || !nonNegativeInteger(row.assetCount) + || !nonNegativeInteger(row.metadataTripleCount) + || !nonNegativeInteger(row.dataTripleCount)) fail('plane observation'); + return { + reportedComplete: row.reportedComplete, + headDigest: row.headDigest, + inventoryDigest: row.inventoryDigest, + assetCount: row.assetCount as number, + metadataTripleCount: row.metadataTripleCount as number, + dataTripleCount: row.dataTripleCount as number, + }; +} + +function decodeEdgeOperation(input: unknown): Omit { + const row = record(input, [ + 'phase', 'source', 'syncMode', 'contextGraphId', 'jobId', + 'completedWave', 'completedSnapshot', + ]); + if ((row.phase !== 'selection' && row.phase !== 'post-restart-auto' + && row.phase !== 'post-restart-explicit') + || (row.source !== 'user' && row.source !== 'reconciler') + || (row.syncMode !== 'always-on' && row.syncMode !== 'on-demand') + || (row.completedWave !== 'selected' && row.completedWave !== 'final')) { + fail('Edge operation'); + } + return { + phase: row.phase, + source: row.source, + syncMode: row.syncMode, + contextGraphId: text(row.contextGraphId, 'contextGraphId'), + jobId: text(row.jobId, 'jobId'), + completedWave: row.completedWave, + completedSnapshot: decodeSnapshot(row.completedSnapshot), + }; +} + +function decodeCoreRound(input: unknown): CoreAutomaticRoundV1 { + const row = record(input, [ + 'round', 'jobId', 'planningLane', 'source', 'configuredBatchSize', + 'explicitSelectedContextGraphIds', 'contextGraphIds', 'completions', + ]); + if (!nonNegativeInteger(row.round) + || row.source !== 'automatic-core-public' + || !positiveInteger(row.configuredBatchSize)) fail('Core round'); + return { + round: row.round as number, + jobId: text(row.jobId, 'jobId'), + planningLane: text(row.planningLane, 'planningLane'), + source: 'automatic-core-public', + configuredBatchSize: row.configuredBatchSize as number, + explicitSelectedContextGraphIds: array( + row.explicitSelectedContextGraphIds, + 0, + MAX_SELECTIVE_COVERAGE_GRAPHS, + (entry) => text(entry, 'contextGraphId'), + ), + contextGraphIds: array(row.contextGraphIds, 0, MAX_SELECTIVE_COVERAGE_GRAPHS, + (entry) => text(entry, 'contextGraphId')), + completions: array(row.completions, 0, MAX_SELECTIVE_COVERAGE_GRAPHS, (entry) => { + const completion = record(entry, [ + 'contextGraphId', 'completedWave', 'completedSnapshot', + ]); + if (completion.completedWave !== 'final') fail('Core completion'); + return { + contextGraphId: text(completion.contextGraphId, 'contextGraphId'), + completedWave: 'final' as const, + completedSnapshot: decodeSnapshot(completion.completedSnapshot), + }; + }), + }; +} + +function decodeSnapshot(input: unknown): GraphSnapshotExpectationV1 { + const row = record(input, ['vm', 'swm']); + const plane = (value: unknown) => { + const item = record(value, [ + 'headDigest', 'inventoryDigest', 'assetCount', 'dataTripleCount', + ]); + if (!positiveInteger(item.assetCount) || !positiveInteger(item.dataTripleCount)) { + fail('snapshot plane'); + } + return { + headDigest: text(item.headDigest, 'headDigest'), + inventoryDigest: text(item.inventoryDigest, 'inventoryDigest'), + assetCount: item.assetCount as number, + dataTripleCount: item.dataTripleCount as number, + }; + }; + return { vm: plane(row.vm), swm: plane(row.swm) }; +} + +function requiredJournal(input: unknown): SyncCoverageJournalReferenceV1 { + const parsed = parseSyncCoverageJournalReferenceV1(input); + if (!parsed) fail('journal reference'); + return parsed; +} + +function array( + input: unknown, + minimum: number, + maximum: number, + decode: Decoder, +): readonly T[] { + if (!Array.isArray(input) + || Object.getPrototypeOf(input) !== Array.prototype + || input.length < minimum + || input.length > maximum) fail('array'); + return Object.freeze(input.map(decode)); +} + +function record(input: unknown, keys: readonly string[]): Record { + return optionalRecord(input, keys, []); +} + +function optionalRecord( + input: unknown, + requiredKeys: readonly string[], + optionalKeys: readonly string[], +): Record { + if (!isPlainRecord(input)) fail('record'); + const allowed = new Set([...requiredKeys, ...optionalKeys]); + if (Reflect.ownKeys(input).some((key) => typeof key !== 'string' || !allowed.has(key)) + || requiredKeys.some((key) => !Object.hasOwn(input, key))) fail('record'); + return input; +} + +function text(input: unknown, label: string): string { + if (typeof input !== 'string' || input.length < 1 || input.length > 4_096) fail(label); + return input; +} + +function positiveInteger(input: unknown): boolean { + return Number.isSafeInteger(input) && (input as number) > 0; +} + +function nonNegativeInteger(input: unknown): boolean { + return Number.isSafeInteger(input) && (input as number) >= 0; +} + +function isPlainRecord(input: unknown): input is Record { + return input !== null + && typeof input === 'object' + && !Array.isArray(input) + && Object.getPrototypeOf(input) === Object.prototype; +} + +function fail(label: string): never { + throw new TypeError(`M1 runtime adapter returned invalid ${label}`); +} diff --git a/package.json b/package.json index 4667f7a531..10d3d14e80 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "test:m1:rfc64-selective-coverage": "pnpm run test:m1:rfc64-selective-coverage:generate && pnpm run test:m1:rfc64-selective-coverage:verify", "test:m1:rfc64-selective-coverage:generate": "node --import tsx devnet/rfc64-m1-selective-coverage/launch-live.ts", "test:m1:rfc64-selective-coverage:verify": "node --import tsx devnet/rfc64-m1-selective-coverage/verify-live.ts", - "test:m1:rfc64-selective-coverage:unit": "node --experimental-strip-types --test devnet/rfc64-m1-selective-coverage/verifier.test.ts devnet/rfc64-m1-selective-coverage/runtime.test.ts devnet/rfc64-m1-selective-coverage/process-runtime.test.ts", + "test:m1:rfc64-selective-coverage:unit": "node --experimental-strip-types --test devnet/rfc64-m1-selective-coverage/verifier.test.ts devnet/rfc64-m1-selective-coverage/runtime.test.ts devnet/rfc64-m1-selective-coverage/process-runtime.test.ts devnet/rfc64-m1-selective-coverage/adapter-environment.test.ts", "typecheck:m1:rfc64-selective-coverage": "tsc --project devnet/rfc64-m1-selective-coverage/tsconfig.json", "test:devnet:v10-core-flows": "vitest run --config devnet/v10-core-flows/vitest.config.ts", "test:devnet:v10-e2e": "vitest run --config devnet/v10-end-to-end/vitest.config.ts", From 4fcb8133c2e43d4aae75412c9c859816f906e4fc Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 13:25:57 +0200 Subject: [PATCH 07/14] test(rfc64): require complete bounded rounds --- devnet/rfc64-m1-selective-coverage/README.md | 9 +- .../evidence-codec.ts | 459 ++++++++++++++++++ .../rfc64-m1-selective-coverage/manifest.ts | 7 + .../process-runtime-fixture.mjs | 5 + .../process-runtime.test.ts | 38 +- .../runtime-wire.ts | 157 +----- .../runtime.test.ts | 14 + devnet/rfc64-m1-selective-coverage/runtime.ts | 51 +- .../sync-coverage-journal.ts | 15 +- .../verifier.test.ts | 135 +++++- .../rfc64-m1-selective-coverage/verifier.ts | 452 +---------------- 11 files changed, 742 insertions(+), 600 deletions(-) create mode 100644 devnet/rfc64-m1-selective-coverage/evidence-codec.ts diff --git a/devnet/rfc64-m1-selective-coverage/README.md b/devnet/rfc64-m1-selective-coverage/README.md index 97a422977d..558fd20fe7 100644 --- a/devnet/rfc64-m1-selective-coverage/README.md +++ b/devnet/rfc64-m1-selective-coverage/README.md @@ -23,6 +23,11 @@ The verifier passes only when all of these user-visible outcomes are proven: - every public graph is eventually scheduled and converges to exact final VM and SWM heads, inventory digests, asset counts, and payload triple counts. +The corpus may contain up to 64 graphs, while one automatic journal entry may +contain at most 32 graph IDs. Accordingly, `coreAutomaticBatchSize` is capped at +32 and a 33-64 graph corpus must converge through multiple bounded rounds. A +truncated single-round claim cannot satisfy the gate. + Edge results are bound to runtime subscription modes and distinct operation job IDs whose completion records carry the exact resulting snapshot. After restart, always-on work must come from the reconciler; on-demand payload remains at its @@ -117,7 +122,9 @@ and all metadata/durable/shared-memory verification bits. It binds: - `core-automatic-round` entries to the actual job ID, planning lane, configured batch, frozen explicit/automatic ID lists, and every terminal per-CG completion. Every completion carries the same real scheduler-round - job ID; a detached or synthetic per-CG ID is rejected. + job ID; a detached or synthetic per-CG ID is rejected. Planned IDs and + completion IDs must match exactly in count and order, so later completion + cannot retroactively validate an incomplete earlier admission round. `droppedBeforeSequence` and `nextSequence` prove the selected entry was not overwritten. Any truncated, missing, nonterminal, or mismatched record fails the diff --git a/devnet/rfc64-m1-selective-coverage/evidence-codec.ts b/devnet/rfc64-m1-selective-coverage/evidence-codec.ts new file mode 100644 index 0000000000..2d3f3c13f0 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/evidence-codec.ts @@ -0,0 +1,459 @@ +import { + MAX_SELECTIVE_COVERAGE_GRAPHS, + MAX_SELECTIVE_COVERAGE_ROUNDS, + MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY, + SELECTIVE_COVERAGE_CORPUS_SCHEMA, + SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, + type CoreAutomaticRoundV1, + type CoreFinalObservationV1, + type EdgeCoveragePolicy, + type EdgeGraphObservationV1, + type EdgeSyncOperationV1, + type ExpectedSelectiveCoverageProvenanceV1, + type GraphObservationV1, + type GraphSnapshotExpectationV1, + type PlaneExpectationV1, + type PlaneObservationV1, + type SelectiveCoverageCorpusV1, + type SelectiveCoverageEvidenceV1, + type SelectiveCoverageGraphV1, + type SyncCoverageJournalProcessIdentityV1, + type SyncCoverageJournalReferenceV1, +} from './manifest.ts'; +import { parseSyncCoverageJournalReferenceV1 } from './sync-coverage-journal.ts'; + +const DIGEST = /^(?:0x|sha256:)[0-9a-f]{64}$/u; +const ID = /^[A-Za-z0-9._:/@-]+$/u; + +/** Canonical closed-schema decoder shared by artifact and process boundaries. */ +export function decodeSelectiveCoverageEvidence( + input: unknown, +): SelectiveCoverageEvidenceV1 | undefined { + const root = closedRecord(input, [ + 'schema', 'provenance', 'automaticJournalEvidence', 'corpus', 'publisher', 'edge', 'core', + ]); + if (!root || root.schema !== SELECTIVE_COVERAGE_EVIDENCE_SCHEMA) return undefined; + const provenance = parseProvenance(root.provenance); + const automaticJournalEvidence = parseAutomaticJournalEvidence(root.automaticJournalEvidence); + const corpus = parseCorpus(root.corpus); + const publisher = closedRecord(root.publisher, ['selected', 'final']); + const edge = closedRecord(root.edge, [ + 'beforeSelection', 'afterSelection', 'afterRestart', 'afterSecondOnDemand', 'operations', + ]); + const core = closedRecord(root.core, ['automaticBatchSize', 'rounds', 'final']); + if (!provenance || !automaticJournalEvidence || !corpus || !publisher || !edge || !core + || !nonNegativeInteger(core.automaticBatchSize) + || !closedArray(core.rounds, 1, MAX_SELECTIVE_COVERAGE_ROUNDS)) return undefined; + const publisherSelected = decodeGraphObservations(publisher.selected); + const publisherFinal = decodeGraphObservations(publisher.final); + const beforeSelection = decodeEdgeObservations(edge.beforeSelection); + const afterSelection = decodeEdgeObservations(edge.afterSelection); + const afterRestart = decodeEdgeObservations(edge.afterRestart); + const afterSecondOnDemand = decodeEdgeObservations(edge.afterSecondOnDemand); + const operations = parseEdgeOperations(edge.operations); + const final = decodeCoreFinalObservations(core.final); + if (!publisherSelected || !publisherFinal || !beforeSelection || !afterSelection + || !afterRestart || !afterSecondOnDemand || !operations || !final) return undefined; + const rounds: CoreAutomaticRoundV1[] = []; + for (let index = 0; index < core.rounds.length; index += 1) { + const round = decodeCoreAutomaticRound(core.rounds[index]); + if (!round || round.round !== index) return undefined; + rounds.push(round); + } + return { + schema: SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, + provenance, + automaticJournalEvidence, + corpus, + publisher: { selected: publisherSelected, final: publisherFinal }, + edge: { + beforeSelection, + afterSelection, + afterRestart, + afterSecondOnDemand, + operations, + }, + core: { + automaticBatchSize: core.automaticBatchSize as number, + rounds: Object.freeze(rounds), + final, + }, + }; +} + +export function decodeExpectedSelectiveCoverageProvenance( + input: unknown, +): ExpectedSelectiveCoverageProvenanceV1 | undefined { + const root = closedRecord(input, [ + 'networkId', 'testedHeadCommit', 'runtimeManifestDigest', 'corpusManifestDigest', + 'publisherPeerId', 'edgePeerId', 'corePeerId', + ]); + if (!root) return undefined; + const { corpusManifestDigest: _omitted, ...provenanceInput } = root; + const provenance = parseProvenance(provenanceInput); + const corpusManifestDigest = digest(root.corpusManifestDigest); + return provenance && corpusManifestDigest + ? { ...provenance, corpusManifestDigest } + : undefined; +} + +export function decodeGraphObservations( + input: unknown, +): readonly GraphObservationV1[] | undefined { + if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; + const result: GraphObservationV1[] = []; + for (const inputRow of input) { + const row = closedRecord(inputRow, ['contextGraphId', 'vm', 'swm']); + const contextGraphId = row && identifier(row.contextGraphId); + const vm = row && parseObservation(row.vm); + const swm = row && parseObservation(row.swm); + if (!contextGraphId || !vm || !swm) return undefined; + result.push({ contextGraphId, vm, swm }); + } + return Object.freeze(result); +} + +export function decodeEdgeObservations( + input: unknown, +): readonly EdgeGraphObservationV1[] | undefined { + if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; + const result: EdgeGraphObservationV1[] = []; + for (const inputRow of input) { + const row = closedRecord(inputRow, [ + 'contextGraphId', 'runtimeSyncMode', 'producingJobId', 'vm', 'swm', + ]); + if (!row) return undefined; + const contextGraphId = identifier(row.contextGraphId); + const vm = parseObservation(row.vm); + const swm = parseObservation(row.swm); + const runtimeSyncMode = row.runtimeSyncMode; + const producingJobId = row.producingJobId === null ? null : identifier(row.producingJobId); + if (!contextGraphId || !vm || !swm || producingJobId === undefined + || (runtimeSyncMode !== null && runtimeSyncMode !== 'on-demand' + && runtimeSyncMode !== 'always-on')) return undefined; + result.push({ contextGraphId, runtimeSyncMode, producingJobId, vm, swm }); + } + return Object.freeze(result); +} + +export function decodeCoreFinalObservations( + input: unknown, +): readonly CoreFinalObservationV1[] | undefined { + if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; + const result: CoreFinalObservationV1[] = []; + for (const inputRow of input) { + const row = closedRecord(inputRow, ['contextGraphId', 'automaticJobIds', 'vm', 'swm']); + if (!row || !closedArray(row.automaticJobIds, 0, MAX_SELECTIVE_COVERAGE_ROUNDS)) { + return undefined; + } + const contextGraphId = identifier(row.contextGraphId); + const vm = parseObservation(row.vm); + const swm = parseObservation(row.swm); + const automaticJobIds = row.automaticJobIds.map(identifier); + if (!contextGraphId || !vm || !swm || automaticJobIds.some((value) => !value) + || new Set(automaticJobIds).size !== automaticJobIds.length) return undefined; + result.push({ + contextGraphId, + automaticJobIds: Object.freeze(automaticJobIds as string[]), + vm, + swm, + }); + } + return Object.freeze(result); +} + +export function decodeGraphSnapshot( + input: unknown, +): GraphSnapshotExpectationV1 | undefined { + const root = closedRecord(input, ['vm', 'swm']); + if (!root) return undefined; + const vm = parseExpectation(root.vm); + const swm = parseExpectation(root.swm); + return vm && swm ? { vm, swm } : undefined; +} + +export function decodeCoreAutomaticRound(input: unknown): CoreAutomaticRoundV1 | undefined { + const row = closedRecord(input, [ + 'round', 'jobId', 'planningLane', 'source', 'configuredBatchSize', + 'explicitSelectedContextGraphIds', 'contextGraphIds', 'completions', + ]); + if (!row || !nonNegativeInteger(row.round) + || row.source !== 'automatic-core-public' + || !positiveInteger(row.configuredBatchSize) + || !closedArray(row.explicitSelectedContextGraphIds, 0, MAX_SELECTIVE_COVERAGE_GRAPHS) + || !closedArray(row.contextGraphIds, 0, MAX_SELECTIVE_COVERAGE_GRAPHS) + || !closedArray(row.completions, 0, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; + const jobId = identifier(row.jobId); + const planningLane = identifier(row.planningLane); + const explicitSelectedContextGraphIds = row.explicitSelectedContextGraphIds.map(identifier); + const contextGraphIds = row.contextGraphIds.map(identifier); + if (!jobId || !planningLane + || explicitSelectedContextGraphIds.some((value) => !value) + || contextGraphIds.some((value) => !value)) return undefined; + const completions = []; + for (const inputCompletion of row.completions) { + const completion = closedRecord(inputCompletion, [ + 'contextGraphId', 'completedWave', 'completedSnapshot', + ]); + const contextGraphId = completion && identifier(completion.contextGraphId); + const completedSnapshot = completion && decodeGraphSnapshot(completion.completedSnapshot); + if (!completion || completion.completedWave !== 'final' + || !contextGraphId || !completedSnapshot) return undefined; + completions.push({ contextGraphId, completedWave: 'final' as const, completedSnapshot }); + } + return { + round: row.round, + jobId, + planningLane, + source: 'automatic-core-public', + configuredBatchSize: row.configuredBatchSize, + explicitSelectedContextGraphIds: Object.freeze(explicitSelectedContextGraphIds as string[]), + contextGraphIds: Object.freeze(contextGraphIds as string[]), + completions: Object.freeze(completions), + }; +} + +function parseAutomaticJournalEvidence( + input: unknown, +): SelectiveCoverageEvidenceV1['automaticJournalEvidence'] | undefined { + const root = closedRecord(input, [ + 'edgeProcess', 'edgeReconciler', 'coreProcess', 'coreRounds', + ]); + if (!root + || !closedArray(root.edgeReconciler, 0, MAX_SELECTIVE_COVERAGE_GRAPHS) + || !closedArray(root.coreRounds, 1, MAX_SELECTIVE_COVERAGE_ROUNDS)) return undefined; + const edgeProcess = parseJournalProcessIdentity(root.edgeProcess); + const coreProcess = parseJournalProcessIdentity(root.coreProcess); + const edgeReconciler = root.edgeReconciler.map(parseSyncCoverageJournalReferenceV1); + const coreRounds = root.coreRounds.map(parseSyncCoverageJournalReferenceV1); + if (!edgeProcess || !coreProcess + || edgeReconciler.some((entry) => entry === undefined) + || coreRounds.some((entry) => entry === undefined)) return undefined; + return { + edgeProcess, + edgeReconciler: Object.freeze(edgeReconciler as SyncCoverageJournalReferenceV1[]), + coreProcess, + coreRounds: Object.freeze(coreRounds as SyncCoverageJournalReferenceV1[]), + }; +} + +function parseJournalProcessIdentity( + input: unknown, +): SyncCoverageJournalProcessIdentityV1 | undefined { + const root = closedRecord(input, ['processStartedAt', 'evidenceWaveId']); + const evidenceWaveId = root && identifier(root.evidenceWaveId); + if (!root || !nonNegativeInteger(root.processStartedAt) || !evidenceWaveId) return undefined; + return { processStartedAt: root.processStartedAt, evidenceWaveId }; +} + +function parseCorpus(input: unknown): SelectiveCoverageCorpusV1 | undefined { + const root = closedRecord(input, [ + 'schema', 'networkId', 'coreAutomaticBatchSize', 'coreCoverageRoundLimit', + 'graphs', 'manifestDigest', + ]); + if (!root || root.schema !== SELECTIVE_COVERAGE_CORPUS_SCHEMA) return undefined; + const networkId = identifier(root.networkId); + const manifestDigest = digest(root.manifestDigest); + if (!networkId || !manifestDigest || !positiveInteger(root.coreAutomaticBatchSize) + || root.coreAutomaticBatchSize > MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY + || !positiveInteger(root.coreCoverageRoundLimit) + || root.coreCoverageRoundLimit > MAX_SELECTIVE_COVERAGE_ROUNDS + || !closedArray(root.graphs, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; + const graphs: SelectiveCoverageGraphV1[] = []; + for (const inputGraph of root.graphs) { + const graph = closedRecord(inputGraph, [ + 'contextGraphId', 'accessPolicy', 'publishPolicy', 'edgePolicy', + 'selectedSnapshot', 'finalSnapshot', + ]); + if (!graph) return undefined; + const contextGraphId = identifier(graph.contextGraphId); + const accessPolicy = binaryPolicy(graph.accessPolicy); + const publishPolicy = binaryPolicy(graph.publishPolicy); + const edgePolicy = parseEdgePolicy(graph.edgePolicy); + const selectedSnapshot = decodeGraphSnapshot(graph.selectedSnapshot); + const finalSnapshot = decodeGraphSnapshot(graph.finalSnapshot); + if (!contextGraphId || accessPolicy === undefined || publishPolicy === undefined + || !edgePolicy || !selectedSnapshot || !finalSnapshot + || (accessPolicy === 1 && edgePolicy !== 'unselected')) return undefined; + graphs.push({ + contextGraphId, + accessPolicy, + publishPolicy, + edgePolicy, + selectedSnapshot, + finalSnapshot, + }); + } + return { + schema: SELECTIVE_COVERAGE_CORPUS_SCHEMA, + networkId, + coreAutomaticBatchSize: root.coreAutomaticBatchSize, + coreCoverageRoundLimit: root.coreCoverageRoundLimit, + graphs: Object.freeze(graphs), + manifestDigest, + }; +} + +function parseProvenance(input: unknown): SelectiveCoverageEvidenceV1['provenance'] | undefined { + const root = closedRecord(input, [ + 'networkId', 'testedHeadCommit', 'runtimeManifestDigest', + 'publisherPeerId', 'edgePeerId', 'corePeerId', + ]); + if (!root) return undefined; + const networkId = identifier(root.networkId); + const runtimeManifestDigest = digest(root.runtimeManifestDigest); + const publisherPeerId = identifier(root.publisherPeerId); + const edgePeerId = identifier(root.edgePeerId); + const corePeerId = identifier(root.corePeerId); + if (!networkId || typeof root.testedHeadCommit !== 'string' + || !/^[0-9a-f]{40,64}$/u.test(root.testedHeadCommit) + || !runtimeManifestDigest || !publisherPeerId || !edgePeerId || !corePeerId + || new Set([publisherPeerId, edgePeerId, corePeerId]).size !== 3) return undefined; + return { + networkId, + testedHeadCommit: root.testedHeadCommit, + runtimeManifestDigest, + publisherPeerId, + edgePeerId, + corePeerId, + }; +} + +function parseEdgeOperations(input: unknown): readonly EdgeSyncOperationV1[] | undefined { + if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS * 2)) return undefined; + const result: EdgeSyncOperationV1[] = []; + for (let index = 0; index < input.length; index += 1) { + const row = closedRecord(input[index], [ + 'sequence', 'phase', 'source', 'syncMode', 'contextGraphId', 'jobId', + 'completedWave', 'completedSnapshot', + ]); + if (!row || row.sequence !== index) return undefined; + const phase = row.phase; + const source = row.source; + const syncMode = row.syncMode; + const contextGraphId = identifier(row.contextGraphId); + const jobId = identifier(row.jobId); + const completedWave = row.completedWave; + const completedSnapshot = decodeGraphSnapshot(row.completedSnapshot); + if ((phase !== 'selection' && phase !== 'post-restart-auto' + && phase !== 'post-restart-explicit') + || (source !== 'reconciler' && source !== 'user') + || (syncMode !== 'always-on' && syncMode !== 'on-demand') + || (completedWave !== 'selected' && completedWave !== 'final') + || !completedSnapshot || !contextGraphId || !jobId) return undefined; + result.push({ + sequence: index, + phase, + source, + syncMode, + contextGraphId, + jobId, + completedWave, + completedSnapshot, + }); + } + return Object.freeze(result); +} + +function parseExpectation(input: unknown): PlaneExpectationV1 | undefined { + const root = closedRecord(input, [ + 'headDigest', 'inventoryDigest', 'assetCount', 'dataTripleCount', + ]); + if (!root) return undefined; + const headDigest = digest(root.headDigest); + const inventoryDigest = digest(root.inventoryDigest); + if (!headDigest || !inventoryDigest || !positiveInteger(root.assetCount) + || !positiveInteger(root.dataTripleCount)) return undefined; + return { + headDigest, + inventoryDigest, + assetCount: root.assetCount, + dataTripleCount: root.dataTripleCount, + }; +} + +function parseObservation(input: unknown): PlaneObservationV1 | undefined { + const root = closedRecord(input, [ + 'reportedComplete', 'headDigest', 'inventoryDigest', 'assetCount', + 'metadataTripleCount', 'dataTripleCount', + ]); + if (!root || typeof root.reportedComplete !== 'boolean' + || !nonNegativeInteger(root.assetCount) + || !nonNegativeInteger(root.metadataTripleCount) + || !nonNegativeInteger(root.dataTripleCount)) return undefined; + const headDigest = root.headDigest === null ? null : digest(root.headDigest); + const inventoryDigest = root.inventoryDigest === null ? null : digest(root.inventoryDigest); + if (headDigest === undefined || inventoryDigest === undefined) return undefined; + return { + reportedComplete: root.reportedComplete, + headDigest, + inventoryDigest, + assetCount: root.assetCount, + metadataTripleCount: root.metadataTripleCount, + dataTripleCount: root.dataTripleCount, + }; +} + +export function closedRecord( + value: unknown, + keys: readonly string[], +): Record | undefined { + if (value === null || typeof value !== 'object' || Array.isArray(value) + || Object.getPrototypeOf(value) !== Object.prototype) return undefined; + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.some((key) => typeof key !== 'string')) return undefined; + const actual = (ownKeys as string[]).sort(); + const expected = [...keys].sort(); + if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { + return undefined; + } + if (actual.some((key) => { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + return !descriptor?.enumerable || !('value' in descriptor); + })) return undefined; + return value as Record; +} + +export function closedArray( + value: unknown, + minimum: number, + maximum: number, +): value is unknown[] { + if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype + || value.length < minimum || value.length > maximum) return false; + const expected = new Set(['length']); + for (let index = 0; index < value.length; index += 1) expected.add(String(index)); + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.length !== expected.size || ownKeys.some((key) => !expected.has(key))) return false; + for (let index = 0; index < value.length; index += 1) { + const descriptor = Object.getOwnPropertyDescriptor(value, String(index)); + if (!descriptor?.enumerable || !('value' in descriptor)) return false; + } + return true; +} + +export function identifier(value: unknown): string | undefined { + return typeof value === 'string' && value.length <= 256 && ID.test(value) ? value : undefined; +} + +function digest(value: unknown): string | undefined { + return typeof value === 'string' && DIGEST.test(value) ? value : undefined; +} + +function binaryPolicy(value: unknown): 0 | 1 | undefined { + return value === 0 || value === 1 ? value : undefined; +} + +function parseEdgePolicy(value: unknown): EdgeCoveragePolicy | undefined { + return value === 'on-demand' || value === 'always-on' || value === 'unselected' + ? value + : undefined; +} + +export function nonNegativeInteger(value: unknown): value is number { + return Number.isSafeInteger(value) && (value as number) >= 0; +} + +export function positiveInteger(value: unknown): value is number { + return Number.isSafeInteger(value) && (value as number) > 0; +} diff --git a/devnet/rfc64-m1-selective-coverage/manifest.ts b/devnet/rfc64-m1-selective-coverage/manifest.ts index 3c2ac3e53c..3ac1794a31 100644 --- a/devnet/rfc64-m1-selective-coverage/manifest.ts +++ b/devnet/rfc64-m1-selective-coverage/manifest.ts @@ -9,6 +9,8 @@ export const SELECTIVE_COVERAGE_VERDICT_SCHEMA = export const MAX_SELECTIVE_COVERAGE_GRAPHS = 64; export const MAX_SELECTIVE_COVERAGE_ROUNDS = 256; +/** Matches the bounded producer journal; larger corpora span multiple rounds. */ +export const MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY = 32; export type EdgeCoveragePolicy = 'always-on' | 'on-demand' | 'unselected'; @@ -209,6 +211,11 @@ export function createSelectiveCoverageCorpus(input: { coreCoverageRoundLimit: number; graphs: readonly SelectiveCoverageGraphV1[]; }): SelectiveCoverageCorpusV1 { + if (!Number.isSafeInteger(input.coreAutomaticBatchSize) + || input.coreAutomaticBatchSize < 1 + || input.coreAutomaticBatchSize > MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY) { + throw new RangeError('Core automatic batch exceeds one bounded journal entry'); + } const payload: CorpusPayload = { schema: SELECTIVE_COVERAGE_CORPUS_SCHEMA, networkId: input.networkId, diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs b/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs index 0785516b59..4b2005ed4a 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs +++ b/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs @@ -36,6 +36,11 @@ lines.on('line', (line) => { value, }; const mode = process.env.FIXTURE_MODE; + if (input.command === process.env.FIXTURE_MALFORM_COMMAND) { + result.value = { unexpected: true }; + process.stdout.write(`${prefix}${JSON.stringify(result)}\n`, () => process.exit(0)); + return; + } if (input.command === 'publish-wave' && mode === 'malformed-publish') { process.stdout.write(`${prefix}${JSON.stringify(result)}\n`, () => process.exit(0)); return; diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts index 0bf9b35037..c5d15dc0b9 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts +++ b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts @@ -94,7 +94,42 @@ test('decodes non-start command results at the adapter boundary', async () => { } }); -function fixtureRuntime(mode?: string): ProcessSelectiveCoverageRuntimeV1 { +for (const [command, invoke] of [ + ['observe-edge', (runtime: ProcessSelectiveCoverageRuntimeV1) => + runtime.observeEdge('before-selection')], + ['synchronize-edge', (runtime: ProcessSelectiveCoverageRuntimeV1) => + runtime.synchronizeEdge({ + contextGraphId: '0x1111111111111111111111111111111111111111/public', + phase: 'selection', + syncMode: 'on-demand', + wave: 'selected', + })], + ['restart-edge', (runtime: ProcessSelectiveCoverageRuntimeV1) => runtime.restartEdge()], + ['wait-edge-reconciler', (runtime: ProcessSelectiveCoverageRuntimeV1) => + runtime.waitForEdgeReconciler({ + contextGraphId: '0x1111111111111111111111111111111111111111/public', + })], + ['core-automatic-round', (runtime: ProcessSelectiveCoverageRuntimeV1) => + runtime.runCoreAutomaticRound(0)], + ['observe-core-final', (runtime: ProcessSelectiveCoverageRuntimeV1) => + runtime.observeCoreFinal()], + ['shutdown', (runtime: ProcessSelectiveCoverageRuntimeV1) => runtime.close()], +] as const) { + test(`rejects malformed ${command} values at the adapter boundary`, async () => { + const runtime = fixtureRuntime(undefined, command); + try { + await runtime.start('publisher'); + await assert.rejects(invoke(runtime), /response failed decoding/); + } finally { + await runtime.close().catch(() => undefined); + } + }); +} + +function fixtureRuntime( + mode?: string, + malformedCommand?: string, +): ProcessSelectiveCoverageRuntimeV1 { return new ProcessSelectiveCoverageRuntimeV1({ command: process.execPath, args: [resolve(import.meta.dirname, 'process-runtime-fixture.mjs')], @@ -103,6 +138,7 @@ function fixtureRuntime(mode?: string): ProcessSelectiveCoverageRuntimeV1 { env: { ...process.env, ...(mode ? { FIXTURE_MODE: mode } : {}), + ...(malformedCommand ? { FIXTURE_MALFORM_COMMAND: malformedCommand } : {}), FIXTURE_NETWORK_ID: 'otp:20430', FIXTURE_SOURCE_COMMIT: 'a'.repeat(40), FIXTURE_RUNTIME_MANIFEST: `sha256:${'b'.repeat(64)}`, diff --git a/devnet/rfc64-m1-selective-coverage/runtime-wire.ts b/devnet/rfc64-m1-selective-coverage/runtime-wire.ts index 5646a48f47..be624eac3d 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime-wire.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime-wire.ts @@ -1,13 +1,17 @@ import { - MAX_SELECTIVE_COVERAGE_GRAPHS, - MAX_SELECTIVE_COVERAGE_ROUNDS, type CoreAutomaticRoundV1, type CoreFinalObservationV1, type EdgeGraphObservationV1, type EdgeSyncOperationV1, type GraphObservationV1, - type GraphSnapshotExpectationV1, } from './manifest.ts'; +import { + decodeCoreAutomaticRound, + decodeCoreFinalObservations as parseCoreFinalObservations, + decodeEdgeObservations as parseEdgeObservations, + decodeGraphObservations as parseGraphObservations, + decodeGraphSnapshot, +} from './evidence-codec.ts'; import { SELECTIVE_COVERAGE_RUNTIME_PROTOCOL, type SelectiveCoverageEdgeRestartReceiptV1, @@ -18,8 +22,6 @@ import { type SyncCoverageJournalReferenceV1, } from './sync-coverage-journal.ts'; -type Decoder = (input: unknown) => T; - export function decodeRuntimeReady(input: unknown): SelectiveCoverageRuntimeReadyV1 { const row = record(input, [ 'protocol', 'role', 'pid', 'peerId', 'networkId', 'testedHeadCommit', @@ -63,24 +65,15 @@ export function decodeRestartReceipt(input: unknown): SelectiveCoverageEdgeResta } export function decodeGraphObservations(input: unknown): readonly GraphObservationV1[] { - return array(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS, decodeGraphObservation); + return requiredCodec(parseGraphObservations(input), 'graph observations'); } export function decodeEdgeObservations(input: unknown): readonly EdgeGraphObservationV1[] { - return array(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS, decodeEdgeObservation); + return requiredCodec(parseEdgeObservations(input), 'Edge observations'); } export function decodeCoreFinalObservations(input: unknown): readonly CoreFinalObservationV1[] { - return array(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS, (value) => { - const row = record(value, ['contextGraphId', 'automaticJobIds', 'vm', 'swm']); - return { - contextGraphId: text(row.contextGraphId, 'contextGraphId'), - vm: decodePlaneObservation(row.vm), - swm: decodePlaneObservation(row.swm), - automaticJobIds: array(row.automaticJobIds, 0, MAX_SELECTIVE_COVERAGE_ROUNDS, - (entry) => text(entry, 'automaticJobId')), - }; - }); + return requiredCodec(parseCoreFinalObservations(input), 'Core final observations'); } export function decodeEdgeSyncResult(input: unknown): { @@ -110,7 +103,10 @@ export function decodeCoreRoundResult(input: unknown): { readonly journal: SyncCoverageJournalReferenceV1; } { const row = record(input, ['round', 'journal']); - return { round: decodeCoreRound(row.round), journal: requiredJournal(row.journal) }; + return { + round: requiredCodec(decodeCoreAutomaticRound(row.round), 'Core round'), + journal: requiredJournal(row.journal), + }; } export function decodeNull(input: unknown): null { @@ -118,55 +114,6 @@ export function decodeNull(input: unknown): null { return null; } -function decodeGraphObservation(input: unknown): GraphObservationV1 { - const row = record(input, ['contextGraphId', 'vm', 'swm']); - return { - contextGraphId: text(row.contextGraphId, 'contextGraphId'), - vm: decodePlaneObservation(row.vm), - swm: decodePlaneObservation(row.swm), - }; -} - -function decodeEdgeObservation(input: unknown): EdgeGraphObservationV1 { - const row = record(input, [ - 'contextGraphId', 'runtimeSyncMode', 'producingJobId', 'vm', 'swm', - ]); - const runtimeSyncMode = row.runtimeSyncMode; - if (runtimeSyncMode !== null - && runtimeSyncMode !== 'always-on' - && runtimeSyncMode !== 'on-demand') fail('Edge runtime sync mode'); - return { - contextGraphId: text(row.contextGraphId, 'contextGraphId'), - runtimeSyncMode, - producingJobId: row.producingJobId === null - ? null - : text(row.producingJobId, 'producingJobId'), - vm: decodePlaneObservation(row.vm), - swm: decodePlaneObservation(row.swm), - }; -} - -function decodePlaneObservation(input: unknown) { - const row = record(input, [ - 'reportedComplete', 'headDigest', 'inventoryDigest', 'assetCount', - 'metadataTripleCount', 'dataTripleCount', - ]); - if (typeof row.reportedComplete !== 'boolean' - || (row.headDigest !== null && typeof row.headDigest !== 'string') - || (row.inventoryDigest !== null && typeof row.inventoryDigest !== 'string') - || !nonNegativeInteger(row.assetCount) - || !nonNegativeInteger(row.metadataTripleCount) - || !nonNegativeInteger(row.dataTripleCount)) fail('plane observation'); - return { - reportedComplete: row.reportedComplete, - headDigest: row.headDigest, - inventoryDigest: row.inventoryDigest, - assetCount: row.assetCount as number, - metadataTripleCount: row.metadataTripleCount as number, - dataTripleCount: row.dataTripleCount as number, - }; -} - function decodeEdgeOperation(input: unknown): Omit { const row = record(input, [ 'phase', 'source', 'syncMode', 'contextGraphId', 'jobId', @@ -186,84 +133,19 @@ function decodeEdgeOperation(input: unknown): Omit text(entry, 'contextGraphId'), + completedSnapshot: requiredCodec( + decodeGraphSnapshot(row.completedSnapshot), + 'graph snapshot', ), - contextGraphIds: array(row.contextGraphIds, 0, MAX_SELECTIVE_COVERAGE_GRAPHS, - (entry) => text(entry, 'contextGraphId')), - completions: array(row.completions, 0, MAX_SELECTIVE_COVERAGE_GRAPHS, (entry) => { - const completion = record(entry, [ - 'contextGraphId', 'completedWave', 'completedSnapshot', - ]); - if (completion.completedWave !== 'final') fail('Core completion'); - return { - contextGraphId: text(completion.contextGraphId, 'contextGraphId'), - completedWave: 'final' as const, - completedSnapshot: decodeSnapshot(completion.completedSnapshot), - }; - }), }; } -function decodeSnapshot(input: unknown): GraphSnapshotExpectationV1 { - const row = record(input, ['vm', 'swm']); - const plane = (value: unknown) => { - const item = record(value, [ - 'headDigest', 'inventoryDigest', 'assetCount', 'dataTripleCount', - ]); - if (!positiveInteger(item.assetCount) || !positiveInteger(item.dataTripleCount)) { - fail('snapshot plane'); - } - return { - headDigest: text(item.headDigest, 'headDigest'), - inventoryDigest: text(item.inventoryDigest, 'inventoryDigest'), - assetCount: item.assetCount as number, - dataTripleCount: item.dataTripleCount as number, - }; - }; - return { vm: plane(row.vm), swm: plane(row.swm) }; -} - function requiredJournal(input: unknown): SyncCoverageJournalReferenceV1 { const parsed = parseSyncCoverageJournalReferenceV1(input); if (!parsed) fail('journal reference'); return parsed; } -function array( - input: unknown, - minimum: number, - maximum: number, - decode: Decoder, -): readonly T[] { - if (!Array.isArray(input) - || Object.getPrototypeOf(input) !== Array.prototype - || input.length < minimum - || input.length > maximum) fail('array'); - return Object.freeze(input.map(decode)); -} - function record(input: unknown, keys: readonly string[]): Record { return optionalRecord(input, keys, []); } @@ -285,6 +167,11 @@ function text(input: unknown, label: string): string { return input; } +function requiredCodec(input: T | undefined, label: string): T { + if (input === undefined) fail(label); + return input; +} + function positiveInteger(input: unknown): boolean { return Number.isSafeInteger(input) && (input as number) > 0; } diff --git a/devnet/rfc64-m1-selective-coverage/runtime.test.ts b/devnet/rfc64-m1-selective-coverage/runtime.test.ts index 96e128e5a3..7a4f645973 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime.test.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime.test.ts @@ -414,6 +414,20 @@ test('collects the anchored three-process Edge/Core sequence and cleans up', asy assert.equal(evidence.provenance.edgePeerId, expected.edgePeerId); assert.doesNotThrow(() => canonicalJson(evidence)); assert.deepEqual(evidence.core.rounds.map((round) => round.contextGraphIds.length), [2, 1]); + assert.deepEqual( + evidence.edge.operations.map((operation) => [ + operation.sequence, + operation.phase, + operation.syncMode, + operation.contextGraphId, + ]), + [ + [0, 'selection', 'on-demand', graphs[0]!.contextGraphId], + [1, 'selection', 'always-on', graphs[1]!.contextGraphId], + [2, 'post-restart-auto', 'always-on', graphs[1]!.contextGraphId], + [3, 'post-restart-explicit', 'on-demand', graphs[0]!.contextGraphId], + ], + ); assert.deepEqual(runtime.stopped, ['core', 'edge', 'publisher']); assert.ok( runtime.calls.indexOf('publish:final') < runtime.calls.indexOf('start:core'), diff --git a/devnet/rfc64-m1-selective-coverage/runtime.ts b/devnet/rfc64-m1-selective-coverage/runtime.ts index 2677c598e5..79f8815832 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime.ts @@ -8,6 +8,7 @@ import { type SelectiveCoverageCorpusV1, type SelectiveCoverageEvidenceV1, SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, + MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY, computeSelectiveCoverageCorpusDigest, } from './manifest.ts'; import { verifySelectiveCoverage } from './verifier.ts'; @@ -95,6 +96,7 @@ export async function collectSelectiveCoverageEvidenceV1(input: { readonly runtime: SelectiveCoverageRuntimeV1; }): Promise { assertAnchoredCorpus(input.corpus, input.expectedProvenance); + const edgePlan = buildEdgePhasePlan(input.corpus); const attempted = new Set(); let primaryFailure: unknown; try { @@ -120,11 +122,11 @@ export async function collectSelectiveCoverageEvidenceV1(input: { const edgeOperations: EdgeSyncOperationV1[] = []; const edgeReconcilerJournals: SyncCoverageJournalReferenceV1[] = []; - for (const graph of selectedPublicGraphs(input.corpus)) { + for (const step of edgePlan.selection) { const result = await input.runtime.synchronizeEdge({ - contextGraphId: graph.contextGraphId, + contextGraphId: step.contextGraphId, phase: 'selection', - syncMode: graph.edgePolicy as 'always-on' | 'on-demand', + syncMode: step.syncMode, wave: 'selected', }); edgeOperations.push(withSequence(result.operation, edgeOperations.length)); @@ -147,10 +149,9 @@ export async function collectSelectiveCoverageEvidenceV1(input: { assertReady(edgeAfterRestartReady, 'edge', input.expectedProvenance); assertDistinctProcesses([publisher, edgeBeforeRestart, edgeAfterRestartReady]); - for (const graph of selectedPublicGraphs(input.corpus) - .filter((candidate) => candidate.edgePolicy === 'always-on')) { + for (const step of edgePlan.reconciler) { const result = await input.runtime.waitForEdgeReconciler({ - contextGraphId: graph.contextGraphId, + contextGraphId: step.contextGraphId, }); assertEdgeReconcilerJournalV1( result.journal, @@ -166,10 +167,9 @@ export async function collectSelectiveCoverageEvidenceV1(input: { 'Edge after restart', ); - for (const graph of selectedPublicGraphs(input.corpus) - .filter((candidate) => candidate.edgePolicy === 'on-demand')) { + for (const step of edgePlan.secondOnDemand) { const result = await input.runtime.synchronizeEdge({ - contextGraphId: graph.contextGraphId, + contextGraphId: step.contextGraphId, phase: 'post-restart-explicit', syncMode: 'on-demand', wave: 'final', @@ -306,9 +306,33 @@ function detachJsonEvidence( } } -function selectedPublicGraphs(corpus: SelectiveCoverageCorpusV1) { - return corpus.graphs.filter((graph) => - graph.accessPolicy === 0 && graph.edgePolicy !== 'unselected'); +interface EdgePhasePlanV1 { + readonly selection: readonly { + readonly contextGraphId: string; + readonly syncMode: 'always-on' | 'on-demand'; + }[]; + readonly reconciler: readonly { readonly contextGraphId: string }[]; + readonly secondOnDemand: readonly { readonly contextGraphId: string }[]; +} + +function buildEdgePhasePlan(corpus: SelectiveCoverageCorpusV1): EdgePhasePlanV1 { + const selection: Array = []; + const reconciler: Array = []; + const secondOnDemand: Array = []; + for (const graph of corpus.graphs) { + if (graph.accessPolicy !== 0 || graph.edgePolicy === 'unselected') continue; + selection.push({ contextGraphId: graph.contextGraphId, syncMode: graph.edgePolicy }); + if (graph.edgePolicy === 'always-on') { + reconciler.push({ contextGraphId: graph.contextGraphId }); + } else { + secondOnDemand.push({ contextGraphId: graph.contextGraphId }); + } + } + return { + selection: Object.freeze(selection), + reconciler: Object.freeze(reconciler), + secondOnDemand: Object.freeze(secondOnDemand), + }; } function withSequence( @@ -331,6 +355,9 @@ function assertAnchoredCorpus( if (corpus.networkId !== expected.networkId) { throw new Error('M1 corpus network differs from the external trust anchor'); } + if (corpus.coreAutomaticBatchSize > MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY) { + throw new Error('M1 Core batch exceeds the bounded journal entry capacity'); + } for (const graph of corpus.graphs) { if (graph.accessPolicy === 1 && graph.edgePolicy !== 'unselected') { throw new Error('M1 private graphs must remain unselected in the Edge slice'); diff --git a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts index 53e803d2e1..5f0c57292d 100644 --- a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts +++ b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts @@ -4,9 +4,9 @@ import type { SyncCoverageJournalProcessIdentityV1, SyncCoverageJournalReferenceV1, } from './manifest.ts'; +import { MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY } from './manifest.ts'; const JOURNAL_CAPACITY = 256; -const MAX_CONTEXT_GRAPH_IDS = 32; const MAX_CONTEXT_GRAPH_ID_LENGTH = 256; export type { @@ -66,13 +66,16 @@ export function assertCoreAutomaticRoundJournalV1( || entry['explicitSelectedContextGraphCount'] !== explicitIds.length || !sameStrings(automaticIds, round.contextGraphIds) || !sameStrings(explicitIds, round.explicitSelectedContextGraphIds) - || completions.length !== round.completions.length) { + || round.completions.length !== round.contextGraphIds.length + || completions.length !== automaticIds.length) { throw new Error('Core round differs from its immutable scheduler journal plan'); } - for (const expected of round.completions) { - const completion = completions.find((candidate) => - isPlainRecord(candidate) && candidate['contextGraphId'] === expected.contextGraphId); + for (let index = 0; index < round.completions.length; index += 1) { + const expected = round.completions[index]!; + const completion = completions[index]; if (!isPlainRecord(completion) + || expected.contextGraphId !== round.contextGraphIds[index] + || completion['contextGraphId'] !== automaticIds[index] || completion['jobId'] !== round.jobId || completion['state'] !== 'complete' || !verifiedPlanes(completion['verified']) @@ -131,7 +134,7 @@ function verifiedPlanes(value: unknown): boolean { function stringArray(value: unknown): string[] { const values = plainArray(value); - if (values.length > MAX_CONTEXT_GRAPH_IDS + if (values.length > MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY || values.some((entry) => typeof entry !== 'string' || entry.length === 0 || entry.length > MAX_CONTEXT_GRAPH_ID_LENGTH) diff --git a/devnet/rfc64-m1-selective-coverage/verifier.test.ts b/devnet/rfc64-m1-selective-coverage/verifier.test.ts index e8c5e7cd51..49aff0508e 100644 --- a/devnet/rfc64-m1-selective-coverage/verifier.test.ts +++ b/devnet/rfc64-m1-selective-coverage/verifier.test.ts @@ -148,6 +148,7 @@ function coreJournal( round: number, jobId: string, contextGraphIds: readonly string[], + configuredBatchSize = 2, ) { return journalReference({ kind: 'core-automatic-round', @@ -157,8 +158,8 @@ function coreJournal( planningLane: 'publisher-peer', source: 'automatic-core-public', trigger: 'peer-sync', - configuredBatchSize: 2, - effectiveBatchSize: 2, + configuredBatchSize, + effectiveBatchSize: configuredBatchSize, explicitSelectedContextGraphIds: [], explicitSelectedContextGraphCount: 0, automaticContextGraphIds: contextGraphIds, @@ -365,6 +366,136 @@ test('published artifact must retain matching automatic journal proof', () => { assert.equal(coreVerdict.checks.coreAutomaticProvenance, false); }); +test('supports 33 public graphs through multiple bounded journal rounds', () => { + const extra = Array.from({ length: 30 }, (_, index): SelectiveCoverageGraphV1 => { + const name = `03-public-unselected-${String(index).padStart(2, '0')}`; + return { + contextGraphId: id(name), + accessPolicy: 0, + publishPolicy: 1, + edgePolicy: 'unselected', + selectedSnapshot: snapshot(name, 'selected'), + finalSnapshot: snapshot(name, 'final'), + }; + }); + const largeCorpus = createSelectiveCoverageCorpus({ + networkId: corpus.networkId, + coreAutomaticBatchSize: 32, + coreCoverageRoundLimit: 2, + graphs: [...graphs, ...extra], + }); + const evidence = clone(); + evidence.corpus = largeCorpus; + evidence.publisher.selected = largeCorpus.graphs.map((graph) => + exact(graph, graph.selectedSnapshot)); + evidence.publisher.final = largeCorpus.graphs.map((graph) => exact(graph, graph.finalSnapshot)); + evidence.edge.beforeSelection = largeCorpus.graphs.map((graph) => edgeAbsent(graph.contextGraphId)); + evidence.edge.afterSelection = largeCorpus.graphs.map((graph) => { + if (graph.contextGraphId === graphs[0]!.contextGraphId) { + return edgeExact(graph, graph.selectedSnapshot, 'edge-select-on-demand'); + } + if (graph.contextGraphId === graphs[1]!.contextGraphId) { + return edgeExact(graph, graph.selectedSnapshot, 'edge-select-always-on'); + } + return edgeAbsent(graph.contextGraphId); + }); + evidence.edge.afterRestart = largeCorpus.graphs.map((graph) => { + if (graph.contextGraphId === graphs[0]!.contextGraphId) { + return { + ...edgeExact(graph, graph.selectedSnapshot, 'edge-select-on-demand'), + runtimeSyncMode: null, + }; + } + if (graph.contextGraphId === graphs[1]!.contextGraphId) { + return edgeExact(graph, graph.finalSnapshot, 'edge-auto-always-on'); + } + return edgeAbsent(graph.contextGraphId); + }); + evidence.edge.afterSecondOnDemand = largeCorpus.graphs.map((graph) => { + if (graph.contextGraphId === graphs[0]!.contextGraphId) { + return edgeExact(graph, graph.finalSnapshot, 'edge-second-on-demand'); + } + if (graph.contextGraphId === graphs[1]!.contextGraphId) { + return edgeExact(graph, graph.finalSnapshot, 'edge-auto-always-on'); + } + return edgeAbsent(graph.contextGraphId); + }); + const publicGraphs = largeCorpus.graphs.filter((graph) => graph.accessPolicy === 0); + const chunks = [publicGraphs.slice(0, 32), publicGraphs.slice(32)]; + evidence.core.automaticBatchSize = 32; + evidence.core.rounds = chunks.map((chunk, round) => ({ + round, + jobId: `core-large-${round}`, + planningLane: 'publisher-peer', + source: 'automatic-core-public', + configuredBatchSize: 32, + explicitSelectedContextGraphIds: [], + contextGraphIds: chunk.map((graph) => graph.contextGraphId), + completions: chunk.map((graph) => ({ + contextGraphId: graph.contextGraphId, + completedWave: 'final', + completedSnapshot: graph.finalSnapshot, + })), + })); + evidence.automaticJournalEvidence.coreRounds = chunks.map((chunk, round) => + coreJournal( + round, + `core-large-${round}`, + chunk.map((graph) => graph.contextGraphId), + 32, + )); + const jobByGraph = new Map(chunks.flatMap((chunk, round) => + chunk.map((graph) => [graph.contextGraphId, `core-large-${round}`] as const))); + evidence.core.final = largeCorpus.graphs.map((graph) => ({ + ...(graph.accessPolicy === 0 ? exact(graph, graph.finalSnapshot) : absent(graph.contextGraphId)), + automaticJobIds: graph.accessPolicy === 0 ? [jobByGraph.get(graph.contextGraphId)!] : [], + })); + + const verdict = verifyWithProvenance(evidence, { + ...EXPECTED_PROVENANCE, + corpusManifestDigest: largeCorpus.manifestDigest, + }); + assert.equal(verdict.pass, true, verdict.rejectReasons.join('; ')); +}); + +test('rejects a batch that cannot fit one untruncated journal entry', () => { + assert.throws(() => createSelectiveCoverageCorpus({ + networkId: corpus.networkId, + coreAutomaticBatchSize: 33, + coreCoverageRoundLimit: 1, + graphs, + }), /exceeds one bounded journal entry/); +}); + +test('scheduled Core IDs require exact same-round terminal completions', () => { + const evidence = clone(); + evidence.core.rounds[0].completions.pop(); + evidence.automaticJournalEvidence.coreRounds[0].snapshot.entries[0].completions.pop(); + + const deferred = graphs[1]!; + evidence.core.rounds[1].contextGraphIds.unshift(deferred.contextGraphId); + evidence.core.rounds[1].completions.unshift({ + contextGraphId: deferred.contextGraphId, + completedWave: 'final', + completedSnapshot: deferred.finalSnapshot, + }); + const later = evidence.automaticJournalEvidence.coreRounds[1].snapshot.entries[0]; + later.automaticContextGraphIds.unshift(deferred.contextGraphId); + later.automaticContextGraphCount += 1; + later.completions.unshift({ + jobId: 'core-auto-1', + contextGraphId: deferred.contextGraphId, + state: 'complete', + verified: { metadata: true, durable: true, sharedMemory: true }, + finishedAt: 22, + }); + evidence.core.final[1].automaticJobIds = ['core-auto-1']; + + const verdict = verifySelectiveCoverage(evidence); + assert.equal(verdict.checks.coreAutomaticProvenance, false); + assert.equal(verdict.pass, false); +}); + test('corpus and evidence serialization is deterministic', () => { assert.equal( canonicalJson({ z: { second: 2, first: 1 }, a: ['@', ':', '/'] }), diff --git a/devnet/rfc64-m1-selective-coverage/verifier.ts b/devnet/rfc64-m1-selective-coverage/verifier.ts index 8c53e0994a..bbbc9cf448 100644 --- a/devnet/rfc64-m1-selective-coverage/verifier.ts +++ b/devnet/rfc64-m1-selective-coverage/verifier.ts @@ -1,13 +1,8 @@ import { - MAX_SELECTIVE_COVERAGE_GRAPHS, - MAX_SELECTIVE_COVERAGE_ROUNDS, - SELECTIVE_COVERAGE_CORPUS_SCHEMA, - SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, SELECTIVE_COVERAGE_VERDICT_SCHEMA, computeSelectiveCoverageCorpusDigest, type CoreAutomaticRoundV1, type CoreFinalObservationV1, - type EdgeCoveragePolicy, type EdgeGraphObservationV1, type EdgeSyncOperationV1, type ExpectedSelectiveCoverageProvenanceV1, @@ -20,17 +15,16 @@ import { type SelectiveCoverageEvidenceV1, type SelectiveCoverageGraphV1, type SelectiveCoverageVerdictV1, - type SyncCoverageJournalProcessIdentityV1, - type SyncCoverageJournalReferenceV1, } from './manifest.ts'; import { assertCoreAutomaticRoundJournalV1, assertEdgeReconcilerJournalV1, - parseSyncCoverageJournalReferenceV1, } from './sync-coverage-journal.ts'; -const DIGEST = /^(?:0x|sha256:)[0-9a-f]{64}$/u; -const ID = /^[A-Za-z0-9._:/@-]+$/u; +import { + decodeExpectedSelectiveCoverageProvenance, + decodeSelectiveCoverageEvidence, +} from './evidence-codec.ts'; const CHECK_NAMES: readonly (keyof SelectiveCoverageChecksV1)[] = Object.freeze([ 'schemaWellFormed', @@ -92,8 +86,8 @@ export function verifySelectiveCoverage( expected: ExpectedSelectiveCoverageProvenanceV1, ): SelectiveCoverageVerdictV1 { try { - if (!parseExpectedProvenance(expected)) return schemaReject(); - const evidence = parseEvidence(input); + if (!decodeExpectedSelectiveCoverageProvenance(expected)) return schemaReject(); + const evidence = decodeSelectiveCoverageEvidence(input); if (!evidence) return schemaReject(); return verifyParsed(evidence, expected); } catch { @@ -288,11 +282,12 @@ function verifyCore(context: VerificationContext) { && round.planningLane === expected.publisherPeerId && round.configuredBatchSize === corpus.coreAutomaticBatchSize && round.explicitSelectedContextGraphIds.length === 0 + && round.completions.length === round.contextGraphIds.length && new Set(round.completions.map((completion) => completion.contextGraphId)).size === round.completions.length - && round.completions.every((completion) => { + && round.completions.every((completion, index) => { const graph = byId.get(completion.contextGraphId); - return round.contextGraphIds.includes(completion.contextGraphId) + return round.contextGraphIds[index] === completion.contextGraphId && completion.completedWave === 'final' && graph?.accessPolicy === 0 && exactSnapshot(completion.completedSnapshot, graph.finalSnapshot); @@ -351,375 +346,6 @@ function verifyExactPayloads(context: VerificationContext): boolean { exactPlane(observed, expected) && (observed?.dataTripleCount ?? 0) > 0); } -function parseEvidence(input: unknown): SelectiveCoverageEvidenceV1 | undefined { - const root = closedRecord(input, [ - 'schema', 'provenance', 'automaticJournalEvidence', 'corpus', 'publisher', 'edge', 'core', - ]); - if (!root || root.schema !== SELECTIVE_COVERAGE_EVIDENCE_SCHEMA) return undefined; - const provenance = parseProvenance(root.provenance); - const automaticJournalEvidence = parseAutomaticJournalEvidence( - root.automaticJournalEvidence, - ); - const corpus = parseCorpus(root.corpus); - const publisher = closedRecord(root.publisher, ['selected', 'final']); - const edge = closedRecord(root.edge, [ - 'beforeSelection', 'afterSelection', 'afterRestart', 'afterSecondOnDemand', 'operations', - ]); - const core = closedRecord(root.core, ['automaticBatchSize', 'rounds', 'final']); - if (!provenance || !automaticJournalEvidence || !corpus || !publisher || !edge || !core) { - return undefined; - } - const publisherSelected = parseObservations(publisher.selected); - const publisherFinal = parseObservations(publisher.final); - const beforeSelection = parseEdgeObservations(edge.beforeSelection); - const afterSelection = parseEdgeObservations(edge.afterSelection); - const afterRestart = parseEdgeObservations(edge.afterRestart); - const afterSecondOnDemand = parseEdgeObservations(edge.afterSecondOnDemand); - const operations = parseEdgeOperations(edge.operations); - const final = parseCoreFinalObservations(core.final); - if (!publisherSelected || !publisherFinal || !beforeSelection || !afterSelection - || !afterRestart || !afterSecondOnDemand || !operations || !final) return undefined; - if (!nonNegativeInteger(core.automaticBatchSize)) return undefined; - if (!closedArray(core.rounds, 1, MAX_SELECTIVE_COVERAGE_ROUNDS)) return undefined; - const rounds: CoreAutomaticRoundV1[] = []; - for (let index = 0; index < core.rounds.length; index += 1) { - const row = closedRecord(core.rounds[index], [ - 'round', 'jobId', 'planningLane', 'source', 'configuredBatchSize', - 'explicitSelectedContextGraphIds', 'contextGraphIds', 'completions', - ]); - if (!row || row.round !== index - || row.source !== 'automatic-core-public' - || !positiveInteger(row.configuredBatchSize) - || !closedArray(row.explicitSelectedContextGraphIds, 0, MAX_SELECTIVE_COVERAGE_GRAPHS) - || !closedArray(row.contextGraphIds, 0, MAX_SELECTIVE_COVERAGE_GRAPHS) - || !closedArray(row.completions, 0, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; - const jobId = identifier(row.jobId); - const planningLane = identifier(row.planningLane); - if (!jobId || !planningLane) return undefined; - const explicitSelectedContextGraphIds: string[] = []; - for (const id of row.explicitSelectedContextGraphIds) { - const parsed = identifier(id); - if (!parsed) return undefined; - explicitSelectedContextGraphIds.push(parsed); - } - const ids: string[] = []; - for (const id of row.contextGraphIds) { - const parsed = identifier(id); - if (!parsed) return undefined; - ids.push(parsed); - } - const completions = []; - for (const inputCompletion of row.completions) { - const completion = closedRecord(inputCompletion, [ - 'contextGraphId', 'completedWave', 'completedSnapshot', - ]); - if (!completion || completion.completedWave !== 'final') return undefined; - const contextGraphId = identifier(completion.contextGraphId); - const completedSnapshot = parseSnapshot(completion.completedSnapshot); - if (!contextGraphId || !completedSnapshot) return undefined; - completions.push({ - contextGraphId, - completedWave: 'final' as const, - completedSnapshot, - }); - } - rounds.push({ - round: index, - jobId, - planningLane, - source: 'automatic-core-public', - configuredBatchSize: row.configuredBatchSize as number, - explicitSelectedContextGraphIds: Object.freeze(explicitSelectedContextGraphIds), - contextGraphIds: Object.freeze(ids), - completions: Object.freeze(completions), - }); - } - return { - schema: SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, - provenance, - automaticJournalEvidence, - corpus, - publisher: { selected: publisherSelected, final: publisherFinal }, - edge: { - beforeSelection, - afterSelection, - afterRestart, - afterSecondOnDemand, - operations, - }, - core: { - automaticBatchSize: core.automaticBatchSize as number, - rounds: Object.freeze(rounds), - final, - }, - }; -} - -function parseAutomaticJournalEvidence( - input: unknown, -): SelectiveCoverageEvidenceV1['automaticJournalEvidence'] | undefined { - const root = closedRecord(input, [ - 'edgeProcess', 'edgeReconciler', 'coreProcess', 'coreRounds', - ]); - if (!root - || !closedArray(root.edgeReconciler, 0, MAX_SELECTIVE_COVERAGE_GRAPHS) - || !closedArray(root.coreRounds, 1, MAX_SELECTIVE_COVERAGE_ROUNDS)) return undefined; - const edgeProcess = parseJournalProcessIdentity(root.edgeProcess); - const coreProcess = parseJournalProcessIdentity(root.coreProcess); - const edgeReconciler = root.edgeReconciler.map(parseSyncCoverageJournalReferenceV1); - const coreRounds = root.coreRounds.map(parseSyncCoverageJournalReferenceV1); - if (!edgeProcess || !coreProcess - || edgeReconciler.some((entry) => entry === undefined) - || coreRounds.some((entry) => entry === undefined)) return undefined; - return { - edgeProcess, - edgeReconciler: Object.freeze(edgeReconciler as SyncCoverageJournalReferenceV1[]), - coreProcess, - coreRounds: Object.freeze(coreRounds as SyncCoverageJournalReferenceV1[]), - }; -} - -function parseJournalProcessIdentity( - input: unknown, -): SyncCoverageJournalProcessIdentityV1 | undefined { - const root = closedRecord(input, ['processStartedAt', 'evidenceWaveId']); - const evidenceWaveId = root && identifier(root.evidenceWaveId); - if (!root || !nonNegativeInteger(root.processStartedAt) || !evidenceWaveId) return undefined; - return { processStartedAt: root.processStartedAt as number, evidenceWaveId }; -} - -function parseCorpus(input: unknown): SelectiveCoverageCorpusV1 | undefined { - const root = closedRecord(input, [ - 'schema', 'networkId', 'coreAutomaticBatchSize', 'coreCoverageRoundLimit', - 'graphs', 'manifestDigest', - ]); - if (!root || root.schema !== SELECTIVE_COVERAGE_CORPUS_SCHEMA) return undefined; - const networkId = identifier(root.networkId); - const manifestDigest = digest(root.manifestDigest); - if (!networkId || !manifestDigest || !positiveInteger(root.coreAutomaticBatchSize) - || !positiveInteger(root.coreCoverageRoundLimit) - || (root.coreCoverageRoundLimit as number) > MAX_SELECTIVE_COVERAGE_ROUNDS - || !closedArray(root.graphs, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; - const graphs: SelectiveCoverageGraphV1[] = []; - for (const inputGraph of root.graphs) { - const graph = closedRecord(inputGraph, [ - 'contextGraphId', 'accessPolicy', 'publishPolicy', 'edgePolicy', - 'selectedSnapshot', 'finalSnapshot', - ]); - if (!graph) return undefined; - const contextGraphId = identifier(graph.contextGraphId); - const accessPolicy = binaryPolicy(graph.accessPolicy); - const publishPolicy = binaryPolicy(graph.publishPolicy); - const edgePolicy = parseEdgePolicy(graph.edgePolicy); - const selectedSnapshot = parseSnapshot(graph.selectedSnapshot); - const finalSnapshot = parseSnapshot(graph.finalSnapshot); - if (!contextGraphId || accessPolicy === undefined || publishPolicy === undefined - || !edgePolicy || !selectedSnapshot || !finalSnapshot) return undefined; - if (accessPolicy === 1 && edgePolicy !== 'unselected') return undefined; - graphs.push({ - contextGraphId, - accessPolicy, - publishPolicy, - edgePolicy, - selectedSnapshot, - finalSnapshot, - }); - } - return { - schema: SELECTIVE_COVERAGE_CORPUS_SCHEMA, - networkId, - coreAutomaticBatchSize: root.coreAutomaticBatchSize as number, - coreCoverageRoundLimit: root.coreCoverageRoundLimit as number, - graphs: Object.freeze(graphs), - manifestDigest, - }; -} - -function parseProvenance(input: unknown): SelectiveCoverageEvidenceV1['provenance'] | undefined { - const root = closedRecord(input, [ - 'networkId', 'testedHeadCommit', 'runtimeManifestDigest', - 'publisherPeerId', 'edgePeerId', 'corePeerId', - ]); - if (!root) return undefined; - const networkId = identifier(root.networkId); - const runtimeManifestDigest = digest(root.runtimeManifestDigest); - const publisherPeerId = identifier(root.publisherPeerId); - const edgePeerId = identifier(root.edgePeerId); - const corePeerId = identifier(root.corePeerId); - if (!networkId || typeof root.testedHeadCommit !== 'string' - || !/^[0-9a-f]{40,64}$/u.test(root.testedHeadCommit) - || !runtimeManifestDigest || !publisherPeerId || !edgePeerId || !corePeerId - || new Set([publisherPeerId, edgePeerId, corePeerId]).size !== 3) return undefined; - return { - networkId, - testedHeadCommit: root.testedHeadCommit, - runtimeManifestDigest, - publisherPeerId, - edgePeerId, - corePeerId, - }; -} - -function parseExpectedProvenance( - input: unknown, -): ExpectedSelectiveCoverageProvenanceV1 | undefined { - const root = closedRecord(input, [ - 'networkId', 'testedHeadCommit', 'runtimeManifestDigest', 'corpusManifestDigest', - 'publisherPeerId', 'edgePeerId', 'corePeerId', - ]); - if (!root) return undefined; - const { corpusManifestDigest: _omitted, ...provenanceInput } = root; - const provenance = parseProvenance(provenanceInput); - const corpusManifestDigest = digest(root.corpusManifestDigest); - return provenance && corpusManifestDigest - ? { ...provenance, corpusManifestDigest } - : undefined; -} - -function parseSnapshot(input: unknown): GraphSnapshotExpectationV1 | undefined { - const root = closedRecord(input, ['vm', 'swm']); - if (!root) return undefined; - const vm = parseExpectation(root.vm); - const swm = parseExpectation(root.swm); - return vm && swm ? { vm, swm } : undefined; -} - -function parseExpectation(input: unknown): PlaneExpectationV1 | undefined { - const root = closedRecord(input, ['headDigest', 'inventoryDigest', 'assetCount', 'dataTripleCount']); - if (!root) return undefined; - const headDigest = digest(root.headDigest); - const inventoryDigest = digest(root.inventoryDigest); - if (!headDigest || !inventoryDigest || !positiveInteger(root.assetCount) - || !positiveInteger(root.dataTripleCount)) return undefined; - return { - headDigest, - inventoryDigest, - assetCount: root.assetCount as number, - dataTripleCount: root.dataTripleCount as number, - }; -} - -function parseObservations(input: unknown): readonly GraphObservationV1[] | undefined { - if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; - const result: GraphObservationV1[] = []; - for (const inputRow of input) { - const row = closedRecord(inputRow, ['contextGraphId', 'vm', 'swm']); - if (!row) return undefined; - const contextGraphId = identifier(row.contextGraphId); - const vm = parseObservation(row.vm); - const swm = parseObservation(row.swm); - if (!contextGraphId || !vm || !swm) return undefined; - result.push({ contextGraphId, vm, swm }); - } - return Object.freeze(result); -} - -function parseEdgeObservations(input: unknown): readonly EdgeGraphObservationV1[] | undefined { - if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; - const result: EdgeGraphObservationV1[] = []; - for (const inputRow of input) { - const row = closedRecord(inputRow, [ - 'contextGraphId', 'runtimeSyncMode', 'producingJobId', 'vm', 'swm', - ]); - if (!row) return undefined; - const contextGraphId = identifier(row.contextGraphId); - const vm = parseObservation(row.vm); - const swm = parseObservation(row.swm); - const runtimeSyncMode = row.runtimeSyncMode; - const producingJobId = row.producingJobId === null ? null : identifier(row.producingJobId); - if (!contextGraphId || !vm || !swm - || producingJobId === undefined - || (runtimeSyncMode !== null && runtimeSyncMode !== 'on-demand' - && runtimeSyncMode !== 'always-on')) return undefined; - result.push({ contextGraphId, runtimeSyncMode, producingJobId, vm, swm }); - } - return Object.freeze(result); -} - -function parseEdgeOperations(input: unknown): readonly EdgeSyncOperationV1[] | undefined { - if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS * 2)) return undefined; - const result: EdgeSyncOperationV1[] = []; - for (let index = 0; index < input.length; index += 1) { - const row = closedRecord(input[index], [ - 'sequence', 'phase', 'source', 'syncMode', 'contextGraphId', 'jobId', - 'completedWave', 'completedSnapshot', - ]); - if (!row || row.sequence !== index) return undefined; - const phase = row.phase; - const source = row.source; - const syncMode = row.syncMode; - const contextGraphId = identifier(row.contextGraphId); - const jobId = identifier(row.jobId); - const completedWave = row.completedWave; - const completedSnapshot = parseSnapshot(row.completedSnapshot); - if ((phase !== 'selection' && phase !== 'post-restart-auto' - && phase !== 'post-restart-explicit') - || (source !== 'reconciler' && source !== 'user') - || (syncMode !== 'always-on' && syncMode !== 'on-demand') - || (completedWave !== 'selected' && completedWave !== 'final') - || !completedSnapshot || !contextGraphId || !jobId) return undefined; - result.push({ - sequence: index, - phase, - source, - syncMode, - contextGraphId, - jobId, - completedWave, - completedSnapshot, - }); - } - return Object.freeze(result); -} - -function parseCoreFinalObservations( - input: unknown, -): readonly CoreFinalObservationV1[] | undefined { - if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; - const result: CoreFinalObservationV1[] = []; - for (const inputRow of input) { - const row = closedRecord(inputRow, ['contextGraphId', 'automaticJobIds', 'vm', 'swm']); - if (!row || !closedArray(row.automaticJobIds, 0, MAX_SELECTIVE_COVERAGE_ROUNDS)) { - return undefined; - } - const contextGraphId = identifier(row.contextGraphId); - const vm = parseObservation(row.vm); - const swm = parseObservation(row.swm); - const automaticJobIds: string[] = []; - for (const inputJobId of row.automaticJobIds) { - const jobId = identifier(inputJobId); - if (!jobId) return undefined; - automaticJobIds.push(jobId); - } - if (!contextGraphId || !vm || !swm - || new Set(automaticJobIds).size !== automaticJobIds.length) return undefined; - result.push({ contextGraphId, automaticJobIds: Object.freeze(automaticJobIds), vm, swm }); - } - return Object.freeze(result); -} - -function parseObservation(input: unknown): PlaneObservationV1 | undefined { - const root = closedRecord(input, [ - 'reportedComplete', 'headDigest', 'inventoryDigest', 'assetCount', - 'metadataTripleCount', 'dataTripleCount', - ]); - if (!root || typeof root.reportedComplete !== 'boolean' - || !nonNegativeInteger(root.assetCount) - || !nonNegativeInteger(root.metadataTripleCount) - || !nonNegativeInteger(root.dataTripleCount)) return undefined; - const headDigest = root.headDigest === null ? null : digest(root.headDigest); - const inventoryDigest = root.inventoryDigest === null ? null : digest(root.inventoryDigest); - if (headDigest === undefined || inventoryDigest === undefined) return undefined; - return { - reportedComplete: root.reportedComplete, - headDigest, - inventoryDigest, - assetCount: root.assetCount as number, - metadataTripleCount: root.metadataTripleCount as number, - dataTripleCount: root.dataTripleCount as number, - }; -} - function hasRequiredPolicyCells(graphs: readonly SelectiveCoverageGraphV1[]): boolean { return graphs.some((graph) => graph.accessPolicy === 0 && graph.edgePolicy === 'on-demand') && graphs.some((graph) => graph.accessPolicy === 0 && graph.edgePolicy === 'always-on') @@ -893,66 +519,6 @@ function strictlyIncreasing(values: readonly string[]): boolean { return values.every((value, index) => index === 0 || values[index - 1]! < value); } -function closedRecord( - value: unknown, - keys: readonly string[], -): Record | undefined { - if (value === null || typeof value !== 'object' || Array.isArray(value) - || Object.getPrototypeOf(value) !== Object.prototype) return undefined; - const ownKeys = Reflect.ownKeys(value); - if (ownKeys.some((key) => typeof key !== 'string')) return undefined; - const actual = (ownKeys as string[]).sort(); - const expected = [...keys].sort(); - if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { - return undefined; - } - if (actual.some((key) => { - const descriptor = Object.getOwnPropertyDescriptor(value, key); - return !descriptor?.enumerable || !('value' in descriptor); - })) return undefined; - return value as Record; -} - -function closedArray(value: unknown, minimum: number, maximum: number): value is unknown[] { - if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype - || value.length < minimum || value.length > maximum) return false; - const expected = new Set(['length']); - for (let index = 0; index < value.length; index += 1) expected.add(String(index)); - const ownKeys = Reflect.ownKeys(value); - if (ownKeys.length !== expected.size || ownKeys.some((key) => !expected.has(key))) return false; - for (let index = 0; index < value.length; index += 1) { - const descriptor = Object.getOwnPropertyDescriptor(value, String(index)); - if (!descriptor?.enumerable || !('value' in descriptor)) return false; - } - return true; -} - -function identifier(value: unknown): string | undefined { - return typeof value === 'string' && value.length <= 256 && ID.test(value) ? value : undefined; -} - -function digest(value: unknown): string | undefined { - return typeof value === 'string' && DIGEST.test(value) ? value : undefined; -} - -function binaryPolicy(value: unknown): 0 | 1 | undefined { - return value === 0 || value === 1 ? value : undefined; -} - -function parseEdgePolicy(value: unknown): EdgeCoveragePolicy | undefined { - return value === 'on-demand' || value === 'always-on' || value === 'unselected' - ? value - : undefined; -} - -function nonNegativeInteger(value: unknown): value is number { - return Number.isSafeInteger(value) && (value as number) >= 0; -} - -function positiveInteger(value: unknown): value is number { - return Number.isSafeInteger(value) && (value as number) > 0; -} - function schemaReject(): SelectiveCoverageVerdictV1 { const checks = Object.freeze(Object.fromEntries( CHECK_NAMES.map((name) => [name, false]), From 0c00ca11830843d1455ac395ef0410fb7f5ba730 Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 14:49:13 +0200 Subject: [PATCH 08/14] test(rfc64): close M1 input boundaries --- .../boundary-codec.test.ts | 57 ++++++++ .../boundary-codec.ts | 84 ++++++++++++ .../evidence-codec.ts | 64 ++------- .../launch-live.ts | 19 +-- .../rfc64-m1-selective-coverage/manifest.ts | 56 +------- .../operator-input.test.ts | 72 +++++++++++ .../operator-input.ts | 48 +++++++ .../rfc64-m1-selective-coverage/package.json | 2 +- .../runtime-wire.ts | 56 ++++---- .../sync-coverage-journal.ts | 122 +++++++++--------- .../verifier.test.ts | 4 +- .../verify-live.ts | 6 +- .../rfc64-persistence-lifecycle/evidence.ts | 30 ++++- package.json | 2 +- 14 files changed, 399 insertions(+), 223 deletions(-) create mode 100644 devnet/rfc64-m1-selective-coverage/boundary-codec.test.ts create mode 100644 devnet/rfc64-m1-selective-coverage/boundary-codec.ts create mode 100644 devnet/rfc64-m1-selective-coverage/operator-input.test.ts create mode 100644 devnet/rfc64-m1-selective-coverage/operator-input.ts diff --git a/devnet/rfc64-m1-selective-coverage/boundary-codec.test.ts b/devnet/rfc64-m1-selective-coverage/boundary-codec.test.ts new file mode 100644 index 0000000000..4355a032e2 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/boundary-codec.test.ts @@ -0,0 +1,57 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + boundedString, + closedArray, + closedRecord, + identifier, + nonNegativeInteger, + plainRecord, + positiveInteger, + requireDecoded, +} from './boundary-codec.ts'; + +test('shared record primitives keep open and closed JSON boundaries distinct', () => { + assert.deepEqual(plainRecord({ required: 1, future: true }), { required: 1, future: true }); + assert.deepEqual(closedRecord({ required: 1, optional: 2 }, ['required'], ['optional']), { + required: 1, + optional: 2, + }); + assert.equal(closedRecord({ required: 1, future: true }, ['required']), undefined); + + const hidden = { required: 1 }; + Object.defineProperty(hidden, 'hidden', { value: true, enumerable: false }); + const accessor = {}; + Object.defineProperty(accessor, 'required', { enumerable: true, get: () => 1 }); + const symbol = { required: 1 } as Record; + symbol[Symbol('hidden')] = true; + for (const value of [hidden, accessor, symbol, Object.create(null)]) { + assert.equal(plainRecord(value), undefined); + } +}); + +test('shared array, string, identifier, and integer primitives are fail closed', () => { + assert.equal(closedArray([1, 2], 1, 2), true); + const sparse = new Array(1); + const extended: unknown[] & { extra?: boolean } = [1]; + extended.extra = true; + const accessor = [1]; + Object.defineProperty(accessor, '0', { enumerable: true, get: () => 1 }); + for (const value of [sparse, extended, accessor]) { + assert.equal(closedArray(value, 0, 2), false); + } + + assert.equal(boundedString('runtime text', 1, 4_096), 'runtime text'); + assert.equal(identifier('peer:@/id'), 'peer:@/id'); + assert.equal(identifier('contains space'), undefined); + assert.equal(nonNegativeInteger(0), true); + assert.equal(nonNegativeInteger(-1), false); + assert.equal(positiveInteger(1), true); + assert.equal(positiveInteger(0), false); +}); + +test('shared throwing adapter preserves its labeled boundary', () => { + assert.equal(requireDecoded('value', 'test boundary'), 'value'); + assert.throws(() => requireDecoded(undefined, 'test boundary'), /Invalid test boundary/u); +}); diff --git a/devnet/rfc64-m1-selective-coverage/boundary-codec.ts b/devnet/rfc64-m1-selective-coverage/boundary-codec.ts new file mode 100644 index 0000000000..debb4af2e8 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/boundary-codec.ts @@ -0,0 +1,84 @@ +/** Shared fail-closed primitives for every M1 JSON trust boundary. */ + +export type PlainDataRecord = Record; + +const IDENTIFIER = /^[A-Za-z0-9._:/@-]+$/u; + +/** Accept an ordinary object whose own fields are enumerable data properties. */ +export function plainRecord(value: unknown): PlainDataRecord | undefined { + if (value === null || typeof value !== 'object' || Array.isArray(value) + || Object.getPrototypeOf(value) !== Object.prototype) return undefined; + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.some((key) => typeof key !== 'string')) return undefined; + if ((ownKeys as string[]).some((key) => { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + return !descriptor?.enumerable || !('value' in descriptor); + })) return undefined; + return value as PlainDataRecord; +} + +/** Accept a closed ordinary object with required and explicitly optional keys. */ +export function closedRecord( + value: unknown, + requiredKeys: readonly string[], + optionalKeys: readonly string[] = [], +): PlainDataRecord | undefined { + const row = plainRecord(value); + if (!row) return undefined; + const allowed = new Set([...requiredKeys, ...optionalKeys]); + const actual = Object.keys(row); + if (actual.some((key) => !allowed.has(key)) + || requiredKeys.some((key) => !Object.hasOwn(row, key))) return undefined; + return row; +} + +/** Accept a dense ordinary array with no custom or accessor properties. */ +export function closedArray( + value: unknown, + minimum: number, + maximum: number, +): value is unknown[] { + if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype + || value.length < minimum || value.length > maximum) return false; + const expected = new Set(['length']); + for (let index = 0; index < value.length; index += 1) expected.add(String(index)); + const ownKeys = Reflect.ownKeys(value); + if (ownKeys.length !== expected.size || ownKeys.some((key) => !expected.has(key))) return false; + for (let index = 0; index < value.length; index += 1) { + const descriptor = Object.getOwnPropertyDescriptor(value, String(index)); + if (!descriptor?.enumerable || !('value' in descriptor)) return false; + } + return true; +} + +export function boundedString( + value: unknown, + minimum: number, + maximum: number, + pattern?: RegExp, +): string | undefined { + return typeof value === 'string' + && value.length >= minimum + && value.length <= maximum + && (pattern === undefined || pattern.test(value)) + ? value + : undefined; +} + +export function identifier(value: unknown): string | undefined { + return boundedString(value, 1, 256, IDENTIFIER); +} + +export function nonNegativeInteger(value: unknown): value is number { + return Number.isSafeInteger(value) && (value as number) >= 0; +} + +export function positiveInteger(value: unknown): value is number { + return Number.isSafeInteger(value) && (value as number) > 0; +} + +/** Translate an undefined-returning decoder into a labeled throwing boundary. */ +export function requireDecoded(value: T | undefined, label: string): T { + if (value === undefined) throw new TypeError(`Invalid ${label}`); + return value; +} diff --git a/devnet/rfc64-m1-selective-coverage/evidence-codec.ts b/devnet/rfc64-m1-selective-coverage/evidence-codec.ts index 2d3f3c13f0..34d9bd1470 100644 --- a/devnet/rfc64-m1-selective-coverage/evidence-codec.ts +++ b/devnet/rfc64-m1-selective-coverage/evidence-codec.ts @@ -21,9 +21,15 @@ import { type SyncCoverageJournalReferenceV1, } from './manifest.ts'; import { parseSyncCoverageJournalReferenceV1 } from './sync-coverage-journal.ts'; +import { + closedArray, + closedRecord, + identifier, + nonNegativeInteger, + positiveInteger, +} from './boundary-codec.ts'; const DIGEST = /^(?:0x|sha256:)[0-9a-f]{64}$/u; -const ID = /^[A-Za-z0-9._:/@-]+$/u; /** Canonical closed-schema decoder shared by artifact and process boundaries. */ export function decodeSelectiveCoverageEvidence( @@ -35,7 +41,7 @@ export function decodeSelectiveCoverageEvidence( if (!root || root.schema !== SELECTIVE_COVERAGE_EVIDENCE_SCHEMA) return undefined; const provenance = parseProvenance(root.provenance); const automaticJournalEvidence = parseAutomaticJournalEvidence(root.automaticJournalEvidence); - const corpus = parseCorpus(root.corpus); + const corpus = decodeSelectiveCoverageCorpus(root.corpus); const publisher = closedRecord(root.publisher, ['selected', 'final']); const edge = closedRecord(root.edge, [ 'beforeSelection', 'afterSelection', 'afterRestart', 'afterSecondOnDemand', 'operations', @@ -246,7 +252,9 @@ function parseJournalProcessIdentity( return { processStartedAt: root.processStartedAt, evidenceWaveId }; } -function parseCorpus(input: unknown): SelectiveCoverageCorpusV1 | undefined { +export function decodeSelectiveCoverageCorpus( + input: unknown, +): SelectiveCoverageCorpusV1 | undefined { const root = closedRecord(input, [ 'schema', 'networkId', 'coreAutomaticBatchSize', 'coreCoverageRoundLimit', 'graphs', 'manifestDigest', @@ -394,48 +402,6 @@ function parseObservation(input: unknown): PlaneObservationV1 | undefined { }; } -export function closedRecord( - value: unknown, - keys: readonly string[], -): Record | undefined { - if (value === null || typeof value !== 'object' || Array.isArray(value) - || Object.getPrototypeOf(value) !== Object.prototype) return undefined; - const ownKeys = Reflect.ownKeys(value); - if (ownKeys.some((key) => typeof key !== 'string')) return undefined; - const actual = (ownKeys as string[]).sort(); - const expected = [...keys].sort(); - if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { - return undefined; - } - if (actual.some((key) => { - const descriptor = Object.getOwnPropertyDescriptor(value, key); - return !descriptor?.enumerable || !('value' in descriptor); - })) return undefined; - return value as Record; -} - -export function closedArray( - value: unknown, - minimum: number, - maximum: number, -): value is unknown[] { - if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype - || value.length < minimum || value.length > maximum) return false; - const expected = new Set(['length']); - for (let index = 0; index < value.length; index += 1) expected.add(String(index)); - const ownKeys = Reflect.ownKeys(value); - if (ownKeys.length !== expected.size || ownKeys.some((key) => !expected.has(key))) return false; - for (let index = 0; index < value.length; index += 1) { - const descriptor = Object.getOwnPropertyDescriptor(value, String(index)); - if (!descriptor?.enumerable || !('value' in descriptor)) return false; - } - return true; -} - -export function identifier(value: unknown): string | undefined { - return typeof value === 'string' && value.length <= 256 && ID.test(value) ? value : undefined; -} - function digest(value: unknown): string | undefined { return typeof value === 'string' && DIGEST.test(value) ? value : undefined; } @@ -449,11 +415,3 @@ function parseEdgePolicy(value: unknown): EdgeCoveragePolicy | undefined { ? value : undefined; } - -export function nonNegativeInteger(value: unknown): value is number { - return Number.isSafeInteger(value) && (value as number) >= 0; -} - -export function positiveInteger(value: unknown): value is number { - return Number.isSafeInteger(value) && (value as number) > 0; -} diff --git a/devnet/rfc64-m1-selective-coverage/launch-live.ts b/devnet/rfc64-m1-selective-coverage/launch-live.ts index c3e11f9ed0..c765e3f688 100644 --- a/devnet/rfc64-m1-selective-coverage/launch-live.ts +++ b/devnet/rfc64-m1-selective-coverage/launch-live.ts @@ -1,4 +1,3 @@ -import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { @@ -11,13 +10,15 @@ import { } from '../rfc64-gate2-multi-asset-completeness/runtime-provenance.ts'; import { canonicalJson, - type ExpectedSelectiveCoverageProvenanceV1, - type SelectiveCoverageCorpusV1, } from './manifest.ts'; import { ProcessSelectiveCoverageRuntimeV1 } from './process-runtime.ts'; import { collectSelectiveCoverageEvidenceV1 } from './runtime.ts'; import { runSelectiveCoverageLiveV1 } from './live-runner.ts'; import { buildSelectiveCoverageAdapterEnvironment } from './adapter-environment.ts'; +import { + readExpectedSelectiveCoverageProvenance, + readSelectiveCoverageCorpus, +} from './operator-input.ts'; const repoRoot = resolve(import.meta.dirname, '../..'); const corpusPath = resolveRequiredPath('DKG_RFC64_M1_CORPUS_FILE'); @@ -34,8 +35,8 @@ const adapterArgs = parseStringArray( const adapterCwd = resolve(process.env['DKG_RFC64_M1_ADAPTER_CWD'] ?? repoRoot); const timeoutMs = parseTimeout(process.env['DKG_RFC64_M1_ADAPTER_TIMEOUT_MS']); -const corpus = readJson(corpusPath) as SelectiveCoverageCorpusV1; -const expectedProvenance = readJson(trustAnchorPath) as ExpectedSelectiveCoverageProvenanceV1; +const corpus = readSelectiveCoverageCorpus(corpusPath); +const expectedProvenance = readExpectedSelectiveCoverageProvenance(trustAnchorPath); const sourceCommit = readCleanRepositoryHead(repoRoot); if (sourceCommit !== expectedProvenance.testedHeadCommit) { throw new Error('M1 trust anchor names a different checked-out source commit'); @@ -82,14 +83,6 @@ function resolveRequiredPath(name: string): string { return resolve(requiredEnvironment(name)); } -function readJson(path: string): unknown { - try { - return JSON.parse(readFileSync(path, 'utf8')); - } catch (error) { - throw new Error(`Could not read M1 JSON input: ${path}`, { cause: error }); - } -} - function parseStringArray(value: string, label: string): string[] { let parsed: unknown; try { diff --git a/devnet/rfc64-m1-selective-coverage/manifest.ts b/devnet/rfc64-m1-selective-coverage/manifest.ts index 3ac1794a31..da5382ffe6 100644 --- a/devnet/rfc64-m1-selective-coverage/manifest.ts +++ b/devnet/rfc64-m1-selective-coverage/manifest.ts @@ -1,4 +1,5 @@ import { createHash } from 'node:crypto'; +import { stableJson } from '../rfc64-persistence-lifecycle/evidence.ts'; export const SELECTIVE_COVERAGE_CORPUS_SCHEMA = 'dkg-rfc64-m1-selective-coverage-corpus-v1' as const; @@ -239,56 +240,11 @@ export function computeSelectiveCoverageCorpusDigest( /** Stable JSON is also used by the future process launcher when publishing artifacts. */ export function canonicalJson(value: unknown): string { - return JSON.stringify(normalizeJson(value, '$', new WeakSet())); -} - -function normalizeJson(value: unknown, path: string, seen: WeakSet): unknown { - if (value === null || typeof value === 'string' || typeof value === 'boolean') { - return value; - } - if (typeof value === 'number') { - if (!Number.isFinite(value) || !Number.isSafeInteger(value) || Object.is(value, -0)) { - throw new TypeError(`${path} must be a lossless JSON integer`); - } - return value; - } - if (typeof value !== 'object') throw new TypeError(`${path} is not JSON data`); - if (seen.has(value)) throw new TypeError(`${path} repeats an object reference`); - seen.add(value); - if (Array.isArray(value)) { - if (Object.getPrototypeOf(value) !== Array.prototype) { - throw new TypeError(`${path} must be a plain array`); - } - const ownKeys = Reflect.ownKeys(value); - const expected = new Set(['length']); - for (let index = 0; index < value.length; index += 1) expected.add(String(index)); - if (ownKeys.length !== expected.size || ownKeys.some((key) => !expected.has(key))) { - throw new TypeError(`${path} must be a dense unextended array`); - } - return value.map((_, index) => { - const descriptor = Object.getOwnPropertyDescriptor(value, String(index)); - if (!descriptor?.enumerable || !('value' in descriptor)) { - throw new TypeError(`${path}[${index}] must be an enumerable data property`); - } - return normalizeJson(descriptor.value, `${path}[${index}]`, seen); - }); - } - if (Object.getPrototypeOf(value) !== Object.prototype) { - throw new TypeError(`${path} must be a plain object`); - } - const result: Record = {}; - const ownKeys = Reflect.ownKeys(value); - if (ownKeys.some((key) => typeof key !== 'string')) { - throw new TypeError(`${path} must not contain symbol keys`); - } - for (const key of (ownKeys as string[]).sort()) { - const descriptor = Object.getOwnPropertyDescriptor(value, key); - if (!descriptor?.enumerable || !('value' in descriptor)) { - throw new TypeError(`${path}.${key} must be an enumerable data property`); - } - result[key] = normalizeJson(descriptor.value, `${path}.${key}`, seen); - } - return result; + return stableJson(value, { + format: 'compact', + trailingLf: false, + numbers: 'safe-integer', + }); } function compareCodeUnits(left: string, right: string): number { diff --git a/devnet/rfc64-m1-selective-coverage/operator-input.test.ts b/devnet/rfc64-m1-selective-coverage/operator-input.test.ts new file mode 100644 index 0000000000..2327b13d6e --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/operator-input.test.ts @@ -0,0 +1,72 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; + +import { + createSelectiveCoverageCorpus, + type ExpectedSelectiveCoverageProvenanceV1, +} from './manifest.ts'; +import { + readExpectedSelectiveCoverageProvenance, + readSelectiveCoverageCorpus, +} from './operator-input.ts'; + +const digest = `sha256:${'a'.repeat(64)}`; +const snapshot = { + vm: { headDigest: digest, inventoryDigest: digest, assetCount: 1, dataTripleCount: 1 }, + swm: { headDigest: digest, inventoryDigest: digest, assetCount: 1, dataTripleCount: 1 }, +}; +const corpus = createSelectiveCoverageCorpus({ + networkId: 'otp:20430', + coreAutomaticBatchSize: 1, + coreCoverageRoundLimit: 1, + graphs: [{ + contextGraphId: '0x1111111111111111111111111111111111111111/public-open', + accessPolicy: 0, + publishPolicy: 1, + edgePolicy: 'on-demand', + selectedSnapshot: snapshot, + finalSnapshot: { vm: { ...snapshot.vm }, swm: { ...snapshot.swm } }, + }], +}); +const provenance: ExpectedSelectiveCoverageProvenanceV1 = { + networkId: corpus.networkId, + testedHeadCommit: 'b'.repeat(40), + runtimeManifestDigest: digest, + corpusManifestDigest: corpus.manifestDigest, + publisherPeerId: 'publisher-peer', + edgePeerId: 'edge-peer', + corePeerId: 'core-peer', +}; + +test('operator input readers return only closed decoded corpus and provenance', (context) => { + const directory = mkdtempSync(join(tmpdir(), 'rfc64-m1-input-')); + context.after(() => rmSync(directory, { recursive: true, force: true })); + const corpusPath = join(directory, 'corpus.json'); + const provenancePath = join(directory, 'provenance.json'); + writeFileSync(corpusPath, JSON.stringify(corpus)); + writeFileSync(provenancePath, JSON.stringify(provenance)); + + assert.deepEqual(readSelectiveCoverageCorpus(corpusPath), corpus); + assert.deepEqual(readExpectedSelectiveCoverageProvenance(provenancePath), provenance); +}); + +test('operator input readers label malformed JSON and schema failures', (context) => { + const directory = mkdtempSync(join(tmpdir(), 'rfc64-m1-input-')); + context.after(() => rmSync(directory, { recursive: true, force: true })); + const corpusPath = join(directory, 'corpus.json'); + const provenancePath = join(directory, 'provenance.json'); + writeFileSync(corpusPath, JSON.stringify({ ...corpus, unexpected: true })); + writeFileSync(provenancePath, '{'); + + assert.throws( + () => readSelectiveCoverageCorpus(corpusPath), + /M1 selective-coverage corpus failed closed-schema validation/u, + ); + assert.throws( + () => readExpectedSelectiveCoverageProvenance(provenancePath), + /Could not read M1 selective-coverage trust anchor/u, + ); +}); diff --git a/devnet/rfc64-m1-selective-coverage/operator-input.ts b/devnet/rfc64-m1-selective-coverage/operator-input.ts new file mode 100644 index 0000000000..00a6b75f6d --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/operator-input.ts @@ -0,0 +1,48 @@ +import { readFileSync } from 'node:fs'; + +import { + decodeExpectedSelectiveCoverageProvenance, + decodeSelectiveCoverageCorpus, +} from './evidence-codec.ts'; +import type { + ExpectedSelectiveCoverageProvenanceV1, + SelectiveCoverageCorpusV1, +} from './manifest.ts'; + +export function readSelectiveCoverageCorpus( + path: string, +): SelectiveCoverageCorpusV1 { + return readDecodedJson( + path, + 'M1 selective-coverage corpus', + decodeSelectiveCoverageCorpus, + ); +} + +export function readExpectedSelectiveCoverageProvenance( + path: string, +): ExpectedSelectiveCoverageProvenanceV1 { + return readDecodedJson( + path, + 'M1 selective-coverage trust anchor', + decodeExpectedSelectiveCoverageProvenance, + ); +} + +function readDecodedJson( + path: string, + label: string, + decoder: (input: unknown) => T | undefined, +): T { + let input: unknown; + try { + input = JSON.parse(readFileSync(path, 'utf8')); + } catch (error) { + throw new Error(`Could not read ${label}: ${path}`, { cause: error }); + } + const decoded = decoder(input); + if (decoded === undefined) { + throw new TypeError(`${label} failed closed-schema validation: ${path}`); + } + return decoded; +} diff --git a/devnet/rfc64-m1-selective-coverage/package.json b/devnet/rfc64-m1-selective-coverage/package.json index 7c49732e6f..04dccb7040 100644 --- a/devnet/rfc64-m1-selective-coverage/package.json +++ b/devnet/rfc64-m1-selective-coverage/package.json @@ -6,7 +6,7 @@ "description": "Fail-closed evidence contract for RFC-64 M1 Edge selection and bounded Core public coverage.", "scripts": { "typecheck": "tsc --noEmit -p tsconfig.json", - "test": "node --experimental-strip-types --test verifier.test.ts runtime.test.ts process-runtime.test.ts adapter-environment.test.ts", + "test": "node --experimental-strip-types --test verifier.test.ts runtime.test.ts process-runtime.test.ts adapter-environment.test.ts operator-input.test.ts boundary-codec.test.ts", "live": "node --import tsx launch-live.ts", "verify-live": "node --import tsx verify-live.ts" } diff --git a/devnet/rfc64-m1-selective-coverage/runtime-wire.ts b/devnet/rfc64-m1-selective-coverage/runtime-wire.ts index be624eac3d..fa5ae2ae88 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime-wire.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime-wire.ts @@ -21,6 +21,13 @@ import { parseSyncCoverageJournalReferenceV1, type SyncCoverageJournalReferenceV1, } from './sync-coverage-journal.ts'; +import { + boundedString, + closedRecord, + nonNegativeInteger, + positiveInteger, + requireDecoded, +} from './boundary-codec.ts'; export function decodeRuntimeReady(input: unknown): SelectiveCoverageRuntimeReadyV1 { const row = record(input, [ @@ -65,15 +72,18 @@ export function decodeRestartReceipt(input: unknown): SelectiveCoverageEdgeResta } export function decodeGraphObservations(input: unknown): readonly GraphObservationV1[] { - return requiredCodec(parseGraphObservations(input), 'graph observations'); + return requireDecoded(parseGraphObservations(input), 'M1 runtime adapter graph observations'); } export function decodeEdgeObservations(input: unknown): readonly EdgeGraphObservationV1[] { - return requiredCodec(parseEdgeObservations(input), 'Edge observations'); + return requireDecoded(parseEdgeObservations(input), 'M1 runtime adapter Edge observations'); } export function decodeCoreFinalObservations(input: unknown): readonly CoreFinalObservationV1[] { - return requiredCodec(parseCoreFinalObservations(input), 'Core final observations'); + return requireDecoded( + parseCoreFinalObservations(input), + 'M1 runtime adapter Core final observations', + ); } export function decodeEdgeSyncResult(input: unknown): { @@ -104,7 +114,7 @@ export function decodeCoreRoundResult(input: unknown): { } { const row = record(input, ['round', 'journal']); return { - round: requiredCodec(decodeCoreAutomaticRound(row.round), 'Core round'), + round: requireDecoded(decodeCoreAutomaticRound(row.round), 'M1 runtime adapter Core round'), journal: requiredJournal(row.journal), }; } @@ -133,9 +143,9 @@ function decodeEdgeOperation(input: unknown): Omit { - return optionalRecord(input, keys, []); + return requireDecoded(closedRecord(input, keys), 'M1 runtime adapter record'); } function optionalRecord( @@ -155,36 +165,14 @@ function optionalRecord( requiredKeys: readonly string[], optionalKeys: readonly string[], ): Record { - if (!isPlainRecord(input)) fail('record'); - const allowed = new Set([...requiredKeys, ...optionalKeys]); - if (Reflect.ownKeys(input).some((key) => typeof key !== 'string' || !allowed.has(key)) - || requiredKeys.some((key) => !Object.hasOwn(input, key))) fail('record'); - return input; + return requireDecoded( + closedRecord(input, requiredKeys, optionalKeys), + 'M1 runtime adapter record', + ); } function text(input: unknown, label: string): string { - if (typeof input !== 'string' || input.length < 1 || input.length > 4_096) fail(label); - return input; -} - -function requiredCodec(input: T | undefined, label: string): T { - if (input === undefined) fail(label); - return input; -} - -function positiveInteger(input: unknown): boolean { - return Number.isSafeInteger(input) && (input as number) > 0; -} - -function nonNegativeInteger(input: unknown): boolean { - return Number.isSafeInteger(input) && (input as number) >= 0; -} - -function isPlainRecord(input: unknown): input is Record { - return input !== null - && typeof input === 'object' - && !Array.isArray(input) - && Object.getPrototypeOf(input) === Object.prototype; + return requireDecoded(boundedString(input, 1, 4_096), `M1 runtime adapter ${label}`); } function fail(label: string): never { diff --git a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts index 5f0c57292d..f24ed4f410 100644 --- a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts +++ b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts @@ -5,6 +5,14 @@ import type { SyncCoverageJournalReferenceV1, } from './manifest.ts'; import { MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY } from './manifest.ts'; +import { + boundedString, + closedArray, + closedRecord, + nonNegativeInteger, + plainRecord, + positiveInteger, +} from './boundary-codec.ts'; const JOURNAL_CAPACITY = 256; const MAX_CONTEXT_GRAPH_ID_LENGTH = 256; @@ -18,12 +26,9 @@ export type { export function parseSyncCoverageJournalReferenceV1( input: unknown, ): SyncCoverageJournalReferenceV1 | undefined { - if (!isPlainRecord(input) - || Reflect.ownKeys(input).length !== 2 - || !Object.hasOwn(input, 'snapshot') - || !Object.hasOwn(input, 'sequence') - || !nonNegativeInteger(input['sequence'])) return undefined; - return { snapshot: input['snapshot'], sequence: input['sequence'] }; + const row = closedRecord(input, ['snapshot', 'sequence']); + if (!row || !nonNegativeInteger(row['sequence'])) return undefined; + return { snapshot: row['snapshot'], sequence: row['sequence'] }; } /** @@ -56,7 +61,11 @@ export function assertCoreAutomaticRoundJournalV1( const entry = terminalEntry(reference, 'core-automatic-round', process); const automaticIds = stringArray(entry['automaticContextGraphIds']); const explicitIds = stringArray(entry['explicitSelectedContextGraphIds']); - const completions = plainArray(entry['completions']); + const completions = requiredArray( + entry['completions'], + MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY, + 'Core completions', + ); if (entry['jobId'] !== round.jobId || entry['planningLane'] !== round.planningLane || entry['source'] !== 'automatic-core-public' @@ -73,13 +82,14 @@ export function assertCoreAutomaticRoundJournalV1( for (let index = 0; index < round.completions.length; index += 1) { const expected = round.completions[index]!; const completion = completions[index]; - if (!isPlainRecord(completion) + const completionRow = plainRecord(completion); + if (!completionRow || expected.contextGraphId !== round.contextGraphIds[index] - || completion['contextGraphId'] !== automaticIds[index] - || completion['jobId'] !== round.jobId - || completion['state'] !== 'complete' - || !verifiedPlanes(completion['verified']) - || !nonNegativeInteger(completion['finishedAt'])) { + || completionRow['contextGraphId'] !== automaticIds[index] + || completionRow['jobId'] !== round.jobId + || completionRow['state'] !== 'complete' + || !verifiedPlanes(completionRow['verified']) + || !nonNegativeInteger(completionRow['finishedAt'])) { throw new Error('Core round lacks a terminal verified completion for a planned graph'); } } @@ -94,59 +104,66 @@ function terminalEntry( throw new Error(`${kind} requires an operator-journal terminal entry`); } const snapshot = reference.snapshot; - if (!isPlainRecord(snapshot) - || snapshot['schemaVersion'] !== 1 - || snapshot['processStartedAt'] !== process.processStartedAt - || snapshot['waveId'] !== process.evidenceWaveId - || snapshot['capacity'] !== JOURNAL_CAPACITY - || !nonNegativeInteger(snapshot['nextSequence']) - || !nonNegativeInteger(snapshot['droppedBeforeSequence'])) { + const snapshotRow = plainRecord(snapshot); + if (!snapshotRow + || snapshotRow['schemaVersion'] !== 1 + || snapshotRow['processStartedAt'] !== process.processStartedAt + || snapshotRow['waveId'] !== process.evidenceWaveId + || snapshotRow['capacity'] !== JOURNAL_CAPACITY + || !nonNegativeInteger(snapshotRow['nextSequence']) + || !nonNegativeInteger(snapshotRow['droppedBeforeSequence'])) { throw new Error('Sync coverage journal snapshot is malformed'); } - if ((snapshot['droppedBeforeSequence'] as number) > reference.sequence - || (snapshot['nextSequence'] as number) <= reference.sequence) { + if ((snapshotRow['droppedBeforeSequence'] as number) > reference.sequence + || (snapshotRow['nextSequence'] as number) <= reference.sequence) { throw new Error('Sync coverage journal no longer retains the referenced evidence'); } - const entries = plainArray(snapshot['entries']); - if (entries.length > (snapshot['capacity'] as number)) { + const entries = requiredArray(snapshotRow['entries'], JOURNAL_CAPACITY, 'journal entries'); + if (entries.length > (snapshotRow['capacity'] as number)) { throw new Error('Sync coverage journal exceeds its declared capacity'); } - const candidate = entries.find((entry) => - isPlainRecord(entry) && entry['sequence'] === reference.sequence); - if (!isPlainRecord(candidate) - || candidate['kind'] !== kind - || candidate['waveId'] !== snapshot['waveId'] - || candidate['evidenceTruncated'] !== false - || candidate['state'] !== 'complete' - || !nonNegativeInteger(candidate['startedAt']) - || !nonNegativeInteger(candidate['finishedAt'])) { + const candidate = entries.find((entry) => { + const row = plainRecord(entry); + return row !== undefined && row['sequence'] === reference.sequence; + }); + const candidateRow = plainRecord(candidate); + if (!candidateRow + || candidateRow['kind'] !== kind + || candidateRow['waveId'] !== snapshotRow['waveId'] + || candidateRow['evidenceTruncated'] !== false + || candidateRow['state'] !== 'complete' + || !nonNegativeInteger(candidateRow['startedAt']) + || !nonNegativeInteger(candidateRow['finishedAt'])) { throw new Error(`${kind} terminal journal entry is missing, truncated, or incomplete`); } - return candidate; + return candidateRow; } function verifiedPlanes(value: unknown): boolean { - return isPlainRecord(value) - && value['metadata'] === true - && value['durable'] === true - && value['sharedMemory'] === true; + const row = plainRecord(value); + return row !== undefined + && row['metadata'] === true + && row['durable'] === true + && row['sharedMemory'] === true; } function stringArray(value: unknown): string[] { - const values = plainArray(value); + const values = requiredArray( + value, + MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY, + 'context graph IDs', + ); if (values.length > MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY - || values.some((entry) => typeof entry !== 'string' - || entry.length === 0 - || entry.length > MAX_CONTEXT_GRAPH_ID_LENGTH) + || values.some((entry) => boundedString(entry, 1, MAX_CONTEXT_GRAPH_ID_LENGTH) === undefined) || new Set(values).size !== values.length) { throw new Error('Sync coverage journal contains invalid context graph IDs'); } return values as string[]; } -function plainArray(value: unknown): unknown[] { - if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) { - throw new Error('Sync coverage journal field is not a plain array'); +function requiredArray(value: unknown, maximum: number, label: string): unknown[] { + if (!closedArray(value, 0, maximum)) { + throw new Error(`Sync coverage journal ${label} is not a bounded plain array`); } return value; } @@ -154,18 +171,3 @@ function plainArray(value: unknown): unknown[] { function sameStrings(left: readonly string[], right: readonly string[]): boolean { return left.length === right.length && left.every((value, index) => value === right[index]); } - -function positiveInteger(value: unknown): value is number { - return Number.isSafeInteger(value) && (value as number) > 0; -} - -function nonNegativeInteger(value: unknown): value is number { - return Number.isSafeInteger(value) && (value as number) >= 0; -} - -function isPlainRecord(value: unknown): value is Record { - return value !== null - && typeof value === 'object' - && !Array.isArray(value) - && Object.getPrototypeOf(value) === Object.prototype; -} diff --git a/devnet/rfc64-m1-selective-coverage/verifier.test.ts b/devnet/rfc64-m1-selective-coverage/verifier.test.ts index 49aff0508e..3ec124b5a2 100644 --- a/devnet/rfc64-m1-selective-coverage/verifier.test.ts +++ b/devnet/rfc64-m1-selective-coverage/verifier.test.ts @@ -497,10 +497,12 @@ test('scheduled Core IDs require exact same-round terminal completions', () => { }); test('corpus and evidence serialization is deterministic', () => { + const canonical = canonicalJson({ z: { second: 2, first: 1 }, a: ['@', ':', '/'] }); assert.equal( - canonicalJson({ z: { second: 2, first: 1 }, a: ['@', ':', '/'] }), + canonical, canonicalJson({ a: ['@', ':', '/'], z: { first: 1, second: 2 } }), ); + assert.equal(canonical, '{"a":["@",":","/"],"z":{"first":1,"second":2}}'); const rebuilt = createSelectiveCoverageCorpus({ networkId: corpus.networkId, coreAutomaticBatchSize: corpus.coreAutomaticBatchSize, diff --git a/devnet/rfc64-m1-selective-coverage/verify-live.ts b/devnet/rfc64-m1-selective-coverage/verify-live.ts index ae18757236..dfa66583b1 100644 --- a/devnet/rfc64-m1-selective-coverage/verify-live.ts +++ b/devnet/rfc64-m1-selective-coverage/verify-live.ts @@ -4,7 +4,7 @@ import { resolve } from 'node:path'; import { readCleanRepositoryHead } from '../rfc64-persistence-lifecycle/evidence.ts'; import { buildGate2RuntimeManifestV1 } from '../rfc64-gate2-multi-asset-completeness/runtime-provenance.ts'; -import type { ExpectedSelectiveCoverageProvenanceV1 } from './manifest.ts'; +import { readExpectedSelectiveCoverageProvenance } from './operator-input.ts'; import { verifySelectiveCoverage } from './verifier.ts'; const repoRoot = resolve(import.meta.dirname, '../..'); @@ -13,9 +13,7 @@ const artifactPath = resolve( process.env['DKG_RFC64_M1_ARTIFACT'] ?? resolve(import.meta.dirname, 'artifacts/selective-coverage-evidence.json'), ); -const expected = JSON.parse( - readFileSync(trustAnchorPath, 'utf8'), -) as ExpectedSelectiveCoverageProvenanceV1; +const expected = readExpectedSelectiveCoverageProvenance(trustAnchorPath); const sourceCommit = readCleanRepositoryHead(repoRoot); if (sourceCommit !== expected.testedHeadCommit) { throw new Error('M1 trust anchor names a different checked-out source commit'); diff --git a/devnet/rfc64-persistence-lifecycle/evidence.ts b/devnet/rfc64-persistence-lifecycle/evidence.ts index 74d513f463..6dda8bae9d 100644 --- a/devnet/rfc64-persistence-lifecycle/evidence.ts +++ b/devnet/rfc64-persistence-lifecycle/evidence.ts @@ -57,9 +57,25 @@ export function readCleanRepositoryHead(repoRootInput: string): string { return head; } -export function stableJson(value: unknown): string { - const normalized = normalizePlainJsonValue(value, '$', new WeakSet()); - return `${JSON.stringify(normalized, null, 2)}\n`; +export interface StableJsonOptions { + readonly format?: 'pretty' | 'compact'; + readonly trailingLf?: boolean; + readonly numbers?: 'finite' | 'safe-integer'; +} + +export function stableJson(value: unknown, options: StableJsonOptions = {}): string { + const normalized = normalizePlainJsonValue( + value, + '$', + new WeakSet(), + options.numbers ?? 'finite', + ); + const encoded = JSON.stringify( + normalized, + null, + options.format === 'compact' ? undefined : 2, + ); + return options.trailingLf === false ? encoded : `${encoded}\n`; } export function atomicWriteStableJson( @@ -143,12 +159,14 @@ function normalizePlainJsonValue( value: unknown, path: string, seen: WeakSet, + numbers: NonNullable, ): unknown { if (value === null || typeof value === 'string' || typeof value === 'boolean') { return value; } if (typeof value === 'number') { - if (!Number.isFinite(value) || Object.is(value, -0)) { + if (!Number.isFinite(value) || Object.is(value, -0) + || (numbers === 'safe-integer' && !Number.isSafeInteger(value))) { throw new TypeError(`${path} contains a non-lossless JSON number`); } return value; @@ -184,7 +202,7 @@ function normalizePlainJsonValue( if (descriptor === undefined || !descriptor.enumerable || !('value' in descriptor)) { throw new TypeError(`${path}[${index}] is not an enumerable data property`); } - return normalizePlainJsonValue(descriptor.value, `${path}[${index}]`, seen); + return normalizePlainJsonValue(descriptor.value, `${path}[${index}]`, seen, numbers); }); } @@ -202,7 +220,7 @@ function normalizePlainJsonValue( if (descriptor === undefined || !descriptor.enumerable || !('value' in descriptor)) { throw new TypeError(`${path}.${key} is not an enumerable data property`); } - return [key, normalizePlainJsonValue(descriptor.value, `${path}.${key}`, seen)]; + return [key, normalizePlainJsonValue(descriptor.value, `${path}.${key}`, seen, numbers)]; }) .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0); return Object.fromEntries(entries); diff --git a/package.json b/package.json index 10d3d14e80..5a68f4e6d2 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "test:m1:rfc64-selective-coverage": "pnpm run test:m1:rfc64-selective-coverage:generate && pnpm run test:m1:rfc64-selective-coverage:verify", "test:m1:rfc64-selective-coverage:generate": "node --import tsx devnet/rfc64-m1-selective-coverage/launch-live.ts", "test:m1:rfc64-selective-coverage:verify": "node --import tsx devnet/rfc64-m1-selective-coverage/verify-live.ts", - "test:m1:rfc64-selective-coverage:unit": "node --experimental-strip-types --test devnet/rfc64-m1-selective-coverage/verifier.test.ts devnet/rfc64-m1-selective-coverage/runtime.test.ts devnet/rfc64-m1-selective-coverage/process-runtime.test.ts devnet/rfc64-m1-selective-coverage/adapter-environment.test.ts", + "test:m1:rfc64-selective-coverage:unit": "node --experimental-strip-types --test devnet/rfc64-m1-selective-coverage/verifier.test.ts devnet/rfc64-m1-selective-coverage/runtime.test.ts devnet/rfc64-m1-selective-coverage/process-runtime.test.ts devnet/rfc64-m1-selective-coverage/adapter-environment.test.ts devnet/rfc64-m1-selective-coverage/operator-input.test.ts devnet/rfc64-m1-selective-coverage/boundary-codec.test.ts", "typecheck:m1:rfc64-selective-coverage": "tsc --project devnet/rfc64-m1-selective-coverage/tsconfig.json", "test:devnet:v10-core-flows": "vitest run --config devnet/v10-core-flows/vitest.config.ts", "test:devnet:v10-e2e": "vitest run --config devnet/v10-end-to-end/vitest.config.ts", From a315a0c5dfd5813f4f6577793ee8ba34ef25e9ed Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 16:31:56 +0200 Subject: [PATCH 09/14] test(rfc64): bind Core evidence to planned capacity --- .../sync-coverage-journal.ts | 5 ++++- .../rfc64-m1-selective-coverage/verifier.test.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts index f24ed4f410..5e5e9bdb18 100644 --- a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts +++ b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts @@ -66,11 +66,14 @@ export function assertCoreAutomaticRoundJournalV1( MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY, 'Core completions', ); + const effectiveBatchSize = entry['effectiveBatchSize']; if (entry['jobId'] !== round.jobId || entry['planningLane'] !== round.planningLane || entry['source'] !== 'automatic-core-public' || entry['configuredBatchSize'] !== round.configuredBatchSize - || !nonNegativeInteger(entry['effectiveBatchSize']) + || !nonNegativeInteger(effectiveBatchSize) + || effectiveBatchSize > round.configuredBatchSize + || automaticIds.length > effectiveBatchSize || entry['automaticContextGraphCount'] !== automaticIds.length || entry['explicitSelectedContextGraphCount'] !== explicitIds.length || !sameStrings(automaticIds, round.contextGraphIds) diff --git a/devnet/rfc64-m1-selective-coverage/verifier.test.ts b/devnet/rfc64-m1-selective-coverage/verifier.test.ts index 3ec124b5a2..0d75bf4d62 100644 --- a/devnet/rfc64-m1-selective-coverage/verifier.test.ts +++ b/devnet/rfc64-m1-selective-coverage/verifier.test.ts @@ -364,6 +364,22 @@ test('published artifact must retain matching automatic journal proof', () => { syntheticCore.core.final[1].automaticJobIds = ['synthetic-core-round']; const coreVerdict = verifySelectiveCoverage(syntheticCore); assert.equal(coreVerdict.checks.coreAutomaticProvenance, false); + + const exceedsPlannedCapacity = clone(); + exceedsPlannedCapacity.automaticJournalEvidence.coreRounds[0] + .snapshot.entries[0].effectiveBatchSize = 1; + assert.equal( + verifySelectiveCoverage(exceedsPlannedCapacity).checks.coreAutomaticProvenance, + false, + ); + + const exceedsConfiguredCapacity = clone(); + exceedsConfiguredCapacity.automaticJournalEvidence.coreRounds[0] + .snapshot.entries[0].effectiveBatchSize = 3; + assert.equal( + verifySelectiveCoverage(exceedsConfiguredCapacity).checks.coreAutomaticProvenance, + false, + ); }); test('supports 33 public graphs through multiple bounded journal rounds', () => { From dba9a978ddf6fa3a2ed878799d62192a3ecc1eb1 Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 16:41:51 +0200 Subject: [PATCH 10/14] test(rfc64): close final M1 harness feedback --- .../boundary-codec.ts | 10 + .../edge-operation-plan.ts | 149 ++++++++++ .../evidence-codec.ts | 270 +++++++++++++----- .../rfc64-m1-selective-coverage/manifest.ts | 8 +- .../process-runtime.test.ts | 46 +++ .../process-runtime.ts | 31 +- .../runtime-wire.ts | 115 ++++---- .../runtime.test.ts | 23 ++ devnet/rfc64-m1-selective-coverage/runtime.ts | 46 +-- .../verifier.test.ts | 10 + .../rfc64-m1-selective-coverage/verifier.ts | 78 ++--- .../evidence.test.ts | 12 +- .../rfc64-persistence-lifecycle/evidence.ts | 25 +- 13 files changed, 593 insertions(+), 230 deletions(-) create mode 100644 devnet/rfc64-m1-selective-coverage/edge-operation-plan.ts diff --git a/devnet/rfc64-m1-selective-coverage/boundary-codec.ts b/devnet/rfc64-m1-selective-coverage/boundary-codec.ts index debb4af2e8..18c4b55a00 100644 --- a/devnet/rfc64-m1-selective-coverage/boundary-codec.ts +++ b/devnet/rfc64-m1-selective-coverage/boundary-codec.ts @@ -2,6 +2,16 @@ export type PlainDataRecord = Record; +/** + * Declare every string field of a model exactly once. Type-checking fails when + * the model gains a field until the closed boundary descriptor is updated. + */ +export function defineRecordKeys() { + return []>( + ...keys: Exclude, Keys[number]> extends never ? Keys : never + ): Keys => keys; +} + const IDENTIFIER = /^[A-Za-z0-9._:/@-]+$/u; /** Accept an ordinary object whose own fields are enumerable data properties. */ diff --git a/devnet/rfc64-m1-selective-coverage/edge-operation-plan.ts b/devnet/rfc64-m1-selective-coverage/edge-operation-plan.ts new file mode 100644 index 0000000000..1c061dc362 --- /dev/null +++ b/devnet/rfc64-m1-selective-coverage/edge-operation-plan.ts @@ -0,0 +1,149 @@ +import { + type EdgeSyncOperationV1, + type GraphSnapshotExpectationV1, + type SelectiveCoverageCorpusV1, +} from './manifest.ts'; + +export type EdgeOperationPhaseV1 = EdgeSyncOperationV1['phase']; +export type EdgeSnapshotSelectorV1 = 'selectedSnapshot' | 'finalSnapshot'; + +export interface EdgeOperationPlanStepV1< + Phase extends EdgeOperationPhaseV1 = EdgeOperationPhaseV1, +> { + readonly sequence: number; + readonly phase: Phase; + readonly source: EdgeSyncOperationV1['source']; + readonly syncMode: EdgeSyncOperationV1['syncMode']; + readonly contextGraphId: string; + readonly completedWave: EdgeSyncOperationV1['completedWave']; + readonly snapshotSelector: EdgeSnapshotSelectorV1; + readonly completedSnapshot: GraphSnapshotExpectationV1; +} + +export interface EdgeOperationPlanV1 { + readonly ordered: readonly EdgeOperationPlanStepV1[]; + readonly selection: readonly EdgeOperationPlanStepV1<'selection'>[]; + readonly postRestartAutomatic: readonly EdgeOperationPlanStepV1<'post-restart-auto'>[]; + readonly postRestartExplicit: readonly EdgeOperationPlanStepV1<'post-restart-explicit'>[]; +} + +/** + * Derive the one canonical Edge state-machine plan from the anchored corpus. + * + * Corpus order is preserved within each phase. The collector executes these + * phase slices, while the verifier compares evidence against `ordered`, so a + * new phase or policy branch cannot silently drift between the two. + */ +export function buildEdgeOperationPlan( + corpus: SelectiveCoverageCorpusV1, +): EdgeOperationPlanV1 { + const selection: EdgeOperationPlanStepV1<'selection'>[] = []; + const postRestartAutomatic: EdgeOperationPlanStepV1<'post-restart-auto'>[] = []; + const postRestartExplicit: EdgeOperationPlanStepV1<'post-restart-explicit'>[] = []; + + for (const graph of corpus.graphs) { + if (graph.accessPolicy !== 0 || graph.edgePolicy === 'unselected') continue; + selection.push({ + sequence: selection.length, + phase: 'selection', + source: 'user', + syncMode: graph.edgePolicy, + contextGraphId: graph.contextGraphId, + completedWave: 'selected', + snapshotSelector: 'selectedSnapshot', + completedSnapshot: graph.selectedSnapshot, + }); + } + + for (const graph of corpus.graphs) { + if (graph.accessPolicy === 0 && graph.edgePolicy === 'always-on') { + postRestartAutomatic.push({ + sequence: selection.length + postRestartAutomatic.length, + phase: 'post-restart-auto', + source: 'reconciler', + syncMode: 'always-on', + contextGraphId: graph.contextGraphId, + completedWave: 'final', + snapshotSelector: 'finalSnapshot', + completedSnapshot: graph.finalSnapshot, + }); + } + } + + for (const graph of corpus.graphs) { + if (graph.accessPolicy === 0 && graph.edgePolicy === 'on-demand') { + postRestartExplicit.push({ + sequence: selection.length + + postRestartAutomatic.length + + postRestartExplicit.length, + phase: 'post-restart-explicit', + source: 'user', + syncMode: 'on-demand', + contextGraphId: graph.contextGraphId, + completedWave: 'final', + snapshotSelector: 'finalSnapshot', + completedSnapshot: graph.finalSnapshot, + }); + } + } + + const frozenSelection = freezeSteps(selection); + const frozenAutomatic = freezeSteps(postRestartAutomatic); + const frozenExplicit = freezeSteps(postRestartExplicit); + return Object.freeze({ + ordered: Object.freeze([ + ...frozenSelection, + ...frozenAutomatic, + ...frozenExplicit, + ]), + selection: frozenSelection, + postRestartAutomatic: frozenAutomatic, + postRestartExplicit: frozenExplicit, + }); +} + +/** Compare an observed operation to a plan step, excluding its runtime job ID. */ +export function matchesEdgeOperationPlanStep( + operation: EdgeSyncOperationV1 | undefined, + step: EdgeOperationPlanStepV1, +): boolean { + return operation?.sequence === step.sequence + && operation.phase === step.phase + && operation.source === step.source + && operation.syncMode === step.syncMode + && operation.contextGraphId === step.contextGraphId + && operation.completedWave === step.completedWave + && exactSnapshot(operation.completedSnapshot, step.completedSnapshot); +} + +export function findEdgeOperationPlanStep( + plan: EdgeOperationPlanV1, + contextGraphId: string, + phase: EdgeOperationPhaseV1, +): EdgeOperationPlanStepV1 | undefined { + return plan.ordered.find((step) => + step.contextGraphId === contextGraphId && step.phase === phase); +} + +function freezeSteps( + steps: readonly EdgeOperationPlanStepV1[], +): readonly EdgeOperationPlanStepV1[] { + return Object.freeze(steps.map((step) => Object.freeze(step))); +} + +function exactSnapshot( + left: GraphSnapshotExpectationV1, + right: GraphSnapshotExpectationV1, +): boolean { + return exactPlane(left.vm, right.vm) && exactPlane(left.swm, right.swm); +} + +function exactPlane( + left: GraphSnapshotExpectationV1['vm'], + right: GraphSnapshotExpectationV1['vm'], +): boolean { + return left.headDigest === right.headDigest + && left.inventoryDigest === right.inventoryDigest + && left.assetCount === right.assetCount + && left.dataTripleCount === right.dataTripleCount; +} diff --git a/devnet/rfc64-m1-selective-coverage/evidence-codec.ts b/devnet/rfc64-m1-selective-coverage/evidence-codec.ts index 34d9bd1470..90bd6d2c92 100644 --- a/devnet/rfc64-m1-selective-coverage/evidence-codec.ts +++ b/devnet/rfc64-m1-selective-coverage/evidence-codec.ts @@ -4,6 +4,7 @@ import { MAX_SYNC_COVERAGE_IDS_PER_JOURNAL_ENTRY, SELECTIVE_COVERAGE_CORPUS_SCHEMA, SELECTIVE_COVERAGE_EVIDENCE_SCHEMA, + type CoreAutomaticCompletionV1, type CoreAutomaticRoundV1, type CoreFinalObservationV1, type EdgeCoveragePolicy, @@ -15,8 +16,10 @@ import { type PlaneExpectationV1, type PlaneObservationV1, type SelectiveCoverageCorpusV1, + type SelectiveCoverageAutomaticJournalEvidenceV1, type SelectiveCoverageEvidenceV1, type SelectiveCoverageGraphV1, + type SelectiveCoverageProvenanceV1, type SyncCoverageJournalProcessIdentityV1, type SyncCoverageJournalReferenceV1, } from './manifest.ts'; @@ -24,6 +27,7 @@ import { parseSyncCoverageJournalReferenceV1 } from './sync-coverage-journal.ts' import { closedArray, closedRecord, + defineRecordKeys, identifier, nonNegativeInteger, positiveInteger, @@ -31,22 +35,154 @@ import { const DIGEST = /^(?:0x|sha256:)[0-9a-f]{64}$/u; +type PublisherEvidenceV1 = SelectiveCoverageEvidenceV1['publisher']; +type EdgeEvidenceV1 = SelectiveCoverageEvidenceV1['edge']; +type CoreEvidenceV1 = SelectiveCoverageEvidenceV1['core']; + +const SELECTIVE_COVERAGE_EVIDENCE_KEYS = defineRecordKeys()( + 'schema', + 'provenance', + 'automaticJournalEvidence', + 'corpus', + 'publisher', + 'edge', + 'core', +); +const SELECTIVE_COVERAGE_PROVENANCE_KEYS = defineRecordKeys< + SelectiveCoverageProvenanceV1 +>()( + 'networkId', + 'testedHeadCommit', + 'runtimeManifestDigest', + 'publisherPeerId', + 'edgePeerId', + 'corePeerId', +); +const EXPECTED_SELECTIVE_COVERAGE_PROVENANCE_KEYS = defineRecordKeys< + ExpectedSelectiveCoverageProvenanceV1 +>()( + ...SELECTIVE_COVERAGE_PROVENANCE_KEYS, + 'corpusManifestDigest', +); +const AUTOMATIC_JOURNAL_EVIDENCE_KEYS = defineRecordKeys< + SelectiveCoverageAutomaticJournalEvidenceV1 +>()( + 'edgeProcess', + 'edgeReconciler', + 'coreProcess', + 'coreRounds', +); +const JOURNAL_PROCESS_IDENTITY_KEYS = defineRecordKeys< + SyncCoverageJournalProcessIdentityV1 +>()('processStartedAt', 'evidenceWaveId'); +const SELECTIVE_COVERAGE_CORPUS_KEYS = defineRecordKeys()( + 'schema', + 'networkId', + 'coreAutomaticBatchSize', + 'coreCoverageRoundLimit', + 'graphs', + 'manifestDigest', +); +const SELECTIVE_COVERAGE_GRAPH_KEYS = defineRecordKeys()( + 'contextGraphId', + 'accessPolicy', + 'publishPolicy', + 'edgePolicy', + 'selectedSnapshot', + 'finalSnapshot', +); +const PUBLISHER_EVIDENCE_KEYS = defineRecordKeys()('selected', 'final'); +const EDGE_EVIDENCE_KEYS = defineRecordKeys()( + 'beforeSelection', + 'afterSelection', + 'afterRestart', + 'afterSecondOnDemand', + 'operations', +); +const CORE_EVIDENCE_KEYS = defineRecordKeys()( + 'automaticBatchSize', + 'rounds', + 'final', +); +const GRAPH_OBSERVATION_KEYS = defineRecordKeys()( + 'contextGraphId', + 'vm', + 'swm', +); +const EDGE_GRAPH_OBSERVATION_KEYS = defineRecordKeys()( + 'contextGraphId', + 'runtimeSyncMode', + 'producingJobId', + 'vm', + 'swm', +); +const CORE_FINAL_OBSERVATION_KEYS = defineRecordKeys()( + 'contextGraphId', + 'automaticJobIds', + 'vm', + 'swm', +); +const GRAPH_SNAPSHOT_EXPECTATION_KEYS = defineRecordKeys()( + 'vm', + 'swm', +); +const EDGE_SYNC_OPERATION_PAYLOAD_KEYS = defineRecordKeys< + Omit +>()( + 'phase', + 'source', + 'syncMode', + 'contextGraphId', + 'jobId', + 'completedWave', + 'completedSnapshot', +); +const EDGE_SYNC_OPERATION_KEYS = defineRecordKeys()( + 'sequence', + ...EDGE_SYNC_OPERATION_PAYLOAD_KEYS, +); +const CORE_AUTOMATIC_ROUND_KEYS = defineRecordKeys()( + 'round', + 'jobId', + 'planningLane', + 'source', + 'configuredBatchSize', + 'explicitSelectedContextGraphIds', + 'contextGraphIds', + 'completions', +); +const CORE_AUTOMATIC_COMPLETION_KEYS = defineRecordKeys()( + 'contextGraphId', + 'completedWave', + 'completedSnapshot', +); +const PLANE_EXPECTATION_KEYS = defineRecordKeys()( + 'headDigest', + 'inventoryDigest', + 'assetCount', + 'dataTripleCount', +); +const PLANE_OBSERVATION_KEYS = defineRecordKeys()( + 'reportedComplete', + 'headDigest', + 'inventoryDigest', + 'assetCount', + 'metadataTripleCount', + 'dataTripleCount', +); + /** Canonical closed-schema decoder shared by artifact and process boundaries. */ export function decodeSelectiveCoverageEvidence( input: unknown, ): SelectiveCoverageEvidenceV1 | undefined { - const root = closedRecord(input, [ - 'schema', 'provenance', 'automaticJournalEvidence', 'corpus', 'publisher', 'edge', 'core', - ]); + const root = closedRecord(input, SELECTIVE_COVERAGE_EVIDENCE_KEYS); if (!root || root.schema !== SELECTIVE_COVERAGE_EVIDENCE_SCHEMA) return undefined; const provenance = parseProvenance(root.provenance); const automaticJournalEvidence = parseAutomaticJournalEvidence(root.automaticJournalEvidence); const corpus = decodeSelectiveCoverageCorpus(root.corpus); - const publisher = closedRecord(root.publisher, ['selected', 'final']); - const edge = closedRecord(root.edge, [ - 'beforeSelection', 'afterSelection', 'afterRestart', 'afterSecondOnDemand', 'operations', - ]); - const core = closedRecord(root.core, ['automaticBatchSize', 'rounds', 'final']); + const publisher = closedRecord(root.publisher, PUBLISHER_EVIDENCE_KEYS); + const edge = closedRecord(root.edge, EDGE_EVIDENCE_KEYS); + const core = closedRecord(root.core, CORE_EVIDENCE_KEYS); if (!provenance || !automaticJournalEvidence || !corpus || !publisher || !edge || !core || !nonNegativeInteger(core.automaticBatchSize) || !closedArray(core.rounds, 1, MAX_SELECTIVE_COVERAGE_ROUNDS)) return undefined; @@ -90,10 +226,7 @@ export function decodeSelectiveCoverageEvidence( export function decodeExpectedSelectiveCoverageProvenance( input: unknown, ): ExpectedSelectiveCoverageProvenanceV1 | undefined { - const root = closedRecord(input, [ - 'networkId', 'testedHeadCommit', 'runtimeManifestDigest', 'corpusManifestDigest', - 'publisherPeerId', 'edgePeerId', 'corePeerId', - ]); + const root = closedRecord(input, EXPECTED_SELECTIVE_COVERAGE_PROVENANCE_KEYS); if (!root) return undefined; const { corpusManifestDigest: _omitted, ...provenanceInput } = root; const provenance = parseProvenance(provenanceInput); @@ -109,7 +242,7 @@ export function decodeGraphObservations( if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; const result: GraphObservationV1[] = []; for (const inputRow of input) { - const row = closedRecord(inputRow, ['contextGraphId', 'vm', 'swm']); + const row = closedRecord(inputRow, GRAPH_OBSERVATION_KEYS); const contextGraphId = row && identifier(row.contextGraphId); const vm = row && parseObservation(row.vm); const swm = row && parseObservation(row.swm); @@ -125,9 +258,7 @@ export function decodeEdgeObservations( if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; const result: EdgeGraphObservationV1[] = []; for (const inputRow of input) { - const row = closedRecord(inputRow, [ - 'contextGraphId', 'runtimeSyncMode', 'producingJobId', 'vm', 'swm', - ]); + const row = closedRecord(inputRow, EDGE_GRAPH_OBSERVATION_KEYS); if (!row) return undefined; const contextGraphId = identifier(row.contextGraphId); const vm = parseObservation(row.vm); @@ -148,7 +279,7 @@ export function decodeCoreFinalObservations( if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; const result: CoreFinalObservationV1[] = []; for (const inputRow of input) { - const row = closedRecord(inputRow, ['contextGraphId', 'automaticJobIds', 'vm', 'swm']); + const row = closedRecord(inputRow, CORE_FINAL_OBSERVATION_KEYS); if (!row || !closedArray(row.automaticJobIds, 0, MAX_SELECTIVE_COVERAGE_ROUNDS)) { return undefined; } @@ -171,18 +302,23 @@ export function decodeCoreFinalObservations( export function decodeGraphSnapshot( input: unknown, ): GraphSnapshotExpectationV1 | undefined { - const root = closedRecord(input, ['vm', 'swm']); + const root = closedRecord(input, GRAPH_SNAPSHOT_EXPECTATION_KEYS); if (!root) return undefined; const vm = parseExpectation(root.vm); const swm = parseExpectation(root.swm); return vm && swm ? { vm, swm } : undefined; } +/** Shared closed decoder for the Edge operation artifact and runtime adapter. */ +export function decodeEdgeSyncOperationPayload( + input: unknown, +): Omit | undefined { + const row = closedRecord(input, EDGE_SYNC_OPERATION_PAYLOAD_KEYS); + return row ? parseEdgeSyncOperationRecord(row) : undefined; +} + export function decodeCoreAutomaticRound(input: unknown): CoreAutomaticRoundV1 | undefined { - const row = closedRecord(input, [ - 'round', 'jobId', 'planningLane', 'source', 'configuredBatchSize', - 'explicitSelectedContextGraphIds', 'contextGraphIds', 'completions', - ]); + const row = closedRecord(input, CORE_AUTOMATIC_ROUND_KEYS); if (!row || !nonNegativeInteger(row.round) || row.source !== 'automatic-core-public' || !positiveInteger(row.configuredBatchSize) @@ -198,9 +334,7 @@ export function decodeCoreAutomaticRound(input: unknown): CoreAutomaticRoundV1 | || contextGraphIds.some((value) => !value)) return undefined; const completions = []; for (const inputCompletion of row.completions) { - const completion = closedRecord(inputCompletion, [ - 'contextGraphId', 'completedWave', 'completedSnapshot', - ]); + const completion = closedRecord(inputCompletion, CORE_AUTOMATIC_COMPLETION_KEYS); const contextGraphId = completion && identifier(completion.contextGraphId); const completedSnapshot = completion && decodeGraphSnapshot(completion.completedSnapshot); if (!completion || completion.completedWave !== 'final' @@ -222,9 +356,7 @@ export function decodeCoreAutomaticRound(input: unknown): CoreAutomaticRoundV1 | function parseAutomaticJournalEvidence( input: unknown, ): SelectiveCoverageEvidenceV1['automaticJournalEvidence'] | undefined { - const root = closedRecord(input, [ - 'edgeProcess', 'edgeReconciler', 'coreProcess', 'coreRounds', - ]); + const root = closedRecord(input, AUTOMATIC_JOURNAL_EVIDENCE_KEYS); if (!root || !closedArray(root.edgeReconciler, 0, MAX_SELECTIVE_COVERAGE_GRAPHS) || !closedArray(root.coreRounds, 1, MAX_SELECTIVE_COVERAGE_ROUNDS)) return undefined; @@ -246,7 +378,7 @@ function parseAutomaticJournalEvidence( function parseJournalProcessIdentity( input: unknown, ): SyncCoverageJournalProcessIdentityV1 | undefined { - const root = closedRecord(input, ['processStartedAt', 'evidenceWaveId']); + const root = closedRecord(input, JOURNAL_PROCESS_IDENTITY_KEYS); const evidenceWaveId = root && identifier(root.evidenceWaveId); if (!root || !nonNegativeInteger(root.processStartedAt) || !evidenceWaveId) return undefined; return { processStartedAt: root.processStartedAt, evidenceWaveId }; @@ -255,10 +387,7 @@ function parseJournalProcessIdentity( export function decodeSelectiveCoverageCorpus( input: unknown, ): SelectiveCoverageCorpusV1 | undefined { - const root = closedRecord(input, [ - 'schema', 'networkId', 'coreAutomaticBatchSize', 'coreCoverageRoundLimit', - 'graphs', 'manifestDigest', - ]); + const root = closedRecord(input, SELECTIVE_COVERAGE_CORPUS_KEYS); if (!root || root.schema !== SELECTIVE_COVERAGE_CORPUS_SCHEMA) return undefined; const networkId = identifier(root.networkId); const manifestDigest = digest(root.manifestDigest); @@ -269,10 +398,7 @@ export function decodeSelectiveCoverageCorpus( || !closedArray(root.graphs, 1, MAX_SELECTIVE_COVERAGE_GRAPHS)) return undefined; const graphs: SelectiveCoverageGraphV1[] = []; for (const inputGraph of root.graphs) { - const graph = closedRecord(inputGraph, [ - 'contextGraphId', 'accessPolicy', 'publishPolicy', 'edgePolicy', - 'selectedSnapshot', 'finalSnapshot', - ]); + const graph = closedRecord(inputGraph, SELECTIVE_COVERAGE_GRAPH_KEYS); if (!graph) return undefined; const contextGraphId = identifier(graph.contextGraphId); const accessPolicy = binaryPolicy(graph.accessPolicy); @@ -303,10 +429,7 @@ export function decodeSelectiveCoverageCorpus( } function parseProvenance(input: unknown): SelectiveCoverageEvidenceV1['provenance'] | undefined { - const root = closedRecord(input, [ - 'networkId', 'testedHeadCommit', 'runtimeManifestDigest', - 'publisherPeerId', 'edgePeerId', 'corePeerId', - ]); + const root = closedRecord(input, SELECTIVE_COVERAGE_PROVENANCE_KEYS); if (!root) return undefined; const networkId = identifier(root.networkId); const runtimeManifestDigest = digest(root.runtimeManifestDigest); @@ -331,42 +454,46 @@ function parseEdgeOperations(input: unknown): readonly EdgeSyncOperationV1[] | u if (!closedArray(input, 1, MAX_SELECTIVE_COVERAGE_GRAPHS * 2)) return undefined; const result: EdgeSyncOperationV1[] = []; for (let index = 0; index < input.length; index += 1) { - const row = closedRecord(input[index], [ - 'sequence', 'phase', 'source', 'syncMode', 'contextGraphId', 'jobId', - 'completedWave', 'completedSnapshot', - ]); - if (!row || row.sequence !== index) return undefined; - const phase = row.phase; - const source = row.source; - const syncMode = row.syncMode; - const contextGraphId = identifier(row.contextGraphId); - const jobId = identifier(row.jobId); - const completedWave = row.completedWave; - const completedSnapshot = decodeGraphSnapshot(row.completedSnapshot); - if ((phase !== 'selection' && phase !== 'post-restart-auto' - && phase !== 'post-restart-explicit') - || (source !== 'reconciler' && source !== 'user') - || (syncMode !== 'always-on' && syncMode !== 'on-demand') - || (completedWave !== 'selected' && completedWave !== 'final') - || !completedSnapshot || !contextGraphId || !jobId) return undefined; + const row = closedRecord(input[index], EDGE_SYNC_OPERATION_KEYS); + const operation = row && parseEdgeSyncOperationRecord(row); + if (!row || row.sequence !== index || !operation) return undefined; result.push({ sequence: index, - phase, - source, - syncMode, - contextGraphId, - jobId, - completedWave, - completedSnapshot, + ...operation, }); } return Object.freeze(result); } +function parseEdgeSyncOperationRecord( + row: Record, +): Omit | undefined { + const phase = row.phase; + const source = row.source; + const syncMode = row.syncMode; + const contextGraphId = identifier(row.contextGraphId); + const jobId = identifier(row.jobId); + const completedWave = row.completedWave; + const completedSnapshot = decodeGraphSnapshot(row.completedSnapshot); + if ((phase !== 'selection' && phase !== 'post-restart-auto' + && phase !== 'post-restart-explicit') + || (source !== 'reconciler' && source !== 'user') + || (syncMode !== 'always-on' && syncMode !== 'on-demand') + || (completedWave !== 'selected' && completedWave !== 'final') + || !completedSnapshot || !contextGraphId || !jobId) return undefined; + return { + phase, + source, + syncMode, + contextGraphId, + jobId, + completedWave, + completedSnapshot, + }; +} + function parseExpectation(input: unknown): PlaneExpectationV1 | undefined { - const root = closedRecord(input, [ - 'headDigest', 'inventoryDigest', 'assetCount', 'dataTripleCount', - ]); + const root = closedRecord(input, PLANE_EXPECTATION_KEYS); if (!root) return undefined; const headDigest = digest(root.headDigest); const inventoryDigest = digest(root.inventoryDigest); @@ -381,10 +508,7 @@ function parseExpectation(input: unknown): PlaneExpectationV1 | undefined { } function parseObservation(input: unknown): PlaneObservationV1 | undefined { - const root = closedRecord(input, [ - 'reportedComplete', 'headDigest', 'inventoryDigest', 'assetCount', - 'metadataTripleCount', 'dataTripleCount', - ]); + const root = closedRecord(input, PLANE_OBSERVATION_KEYS); if (!root || typeof root.reportedComplete !== 'boolean' || !nonNegativeInteger(root.assetCount) || !nonNegativeInteger(root.metadataTripleCount) diff --git a/devnet/rfc64-m1-selective-coverage/manifest.ts b/devnet/rfc64-m1-selective-coverage/manifest.ts index da5382ffe6..4aca28eea9 100644 --- a/devnet/rfc64-m1-selective-coverage/manifest.ts +++ b/devnet/rfc64-m1-selective-coverage/manifest.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto'; -import { stableJson } from '../rfc64-persistence-lifecycle/evidence.ts'; +import { compactSafeIntegerJson } from '../rfc64-persistence-lifecycle/evidence.ts'; export const SELECTIVE_COVERAGE_CORPUS_SCHEMA = 'dkg-rfc64-m1-selective-coverage-corpus-v1' as const; @@ -240,11 +240,7 @@ export function computeSelectiveCoverageCorpusDigest( /** Stable JSON is also used by the future process launcher when publishing artifacts. */ export function canonicalJson(value: unknown): string { - return stableJson(value, { - format: 'compact', - trailingLf: false, - numbers: 'safe-integer', - }); + return compactSafeIntegerJson(value); } function compareCodeUnits(left: string, right: string): number { diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts index c5d15dc0b9..a845b02aef 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts +++ b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts @@ -94,6 +94,52 @@ test('decodes non-start command results at the adapter boundary', async () => { } }); +test('missing adapter executable fails startup and close without hanging', async () => { + const runtime = new ProcessSelectiveCoverageRuntimeV1({ + command: resolve(import.meta.dirname, `missing-adapter-${process.pid}`), + cwd: resolve(import.meta.dirname, '../..'), + timeoutMs: 5_000, + }); + await assert.rejects(runtime.start('publisher'), /runtime adapter process failed/); + + let timeout: ReturnType | undefined; + const closeTimeout = new Promise((_resolve, reject) => { + timeout = setTimeout(() => reject(new Error('runtime close timed out')), 1_000); + }); + try { + await assert.rejects( + Promise.race([runtime.close(), closeTimeout]), + /runtime adapter process failed/, + ); + } finally { + if (timeout) clearTimeout(timeout); + } +}); + +test('a non-terminal child error cannot satisfy the process-exit proof', async () => { + const runtime = fixtureRuntime(); + const internals = runtime as unknown as { + readonly child: { emit(event: 'error', error: Error): boolean }; + readonly exited: Promise; + }; + try { + await runtime.start('publisher'); + internals.child.emit('error', new Error('synthetic live-child kill failure')); + assert.doesNotThrow(() => { + internals.child.emit('error', new Error('second synthetic live-child error')); + }); + const terminalState = await Promise.race([ + internals.exited.then(() => 'exited' as const), + new Promise<'still-running'>((resolveState) => { + setTimeout(() => resolveState('still-running'), 50); + }), + ]); + assert.equal(terminalState, 'still-running'); + } finally { + await runtime.close().catch(() => undefined); + } +}); + for (const [command, invoke] of [ ['observe-edge', (runtime: ProcessSelectiveCoverageRuntimeV1) => runtime.observeEdge('before-selection')], diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime.ts b/devnet/rfc64-m1-selective-coverage/process-runtime.ts index 635d5e4a1f..7780ff6efa 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime.ts +++ b/devnet/rfc64-m1-selective-coverage/process-runtime.ts @@ -82,11 +82,25 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti this.exited = new Promise((resolveExit) => { resolveExited = resolveExit; }); - this.child.once('error', (error) => this.failAll( - new Error('M1 runtime adapter process failed', { cause: error }), - )); - this.child.once('exit', (code, signal) => { + let terminal = false; + const resolveTerminal = () => { + if (terminal) return; + terminal = true; resolveExited(); + }; + this.child.on('error', (error) => { + // Spawn failures are terminal but ChildProcess also emits `error` for + // failures such as an unsuccessful kill while the child is still live. + // Do not let those later errors falsely satisfy cleanup. + if (this.child.pid === undefined + || this.child.exitCode !== null + || this.child.signalCode !== null) { + resolveTerminal(); + } + this.failAll(new Error('M1 runtime adapter process failed', { cause: error })); + }); + this.child.once('exit', (code, signal) => { + resolveTerminal(); if (!this.closing || this.pending.size > 0) { this.failAll(new Error( `M1 runtime adapter exited before shutdown acknowledgement ` @@ -94,6 +108,15 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti )); } }); + this.child.once('close', (code, signal) => { + resolveTerminal(); + if (!this.closing || this.pending.size > 0) { + this.failAll(new Error( + `M1 runtime adapter closed before shutdown acknowledgement ` + + `(code=${String(code)} signal=${String(signal)})`, + )); + } + }); } private readonly timeoutMs: number; diff --git a/devnet/rfc64-m1-selective-coverage/runtime-wire.ts b/devnet/rfc64-m1-selective-coverage/runtime-wire.ts index fa5ae2ae88..33805862d5 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime-wire.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime-wire.ts @@ -9,8 +9,8 @@ import { decodeCoreAutomaticRound, decodeCoreFinalObservations as parseCoreFinalObservations, decodeEdgeObservations as parseEdgeObservations, + decodeEdgeSyncOperationPayload, decodeGraphObservations as parseGraphObservations, - decodeGraphSnapshot, } from './evidence-codec.ts'; import { SELECTIVE_COVERAGE_RUNTIME_PROTOCOL, @@ -24,17 +24,63 @@ import { import { boundedString, closedRecord, + defineRecordKeys, nonNegativeInteger, positiveInteger, requireDecoded, } from './boundary-codec.ts'; +type EdgeRestartPreviousV1 = SelectiveCoverageEdgeRestartReceiptV1['previous']; +type EdgeSyncResultV1 = { + readonly operation: Omit; + readonly journal?: SyncCoverageJournalReferenceV1; +}; +type EdgeReconcilerResultV1 = { + readonly operation: Omit; + readonly journal: SyncCoverageJournalReferenceV1; +}; +type CoreRoundResultV1 = { + readonly round: CoreAutomaticRoundV1; + readonly journal: SyncCoverageJournalReferenceV1; +}; +type OptionalPropertyKeys = { + [Key in keyof T]-?: object extends Pick ? Key : never; +}[keyof T]; + +const RUNTIME_READY_KEYS = defineRecordKeys()( + 'protocol', + 'role', + 'pid', + 'peerId', + 'networkId', + 'testedHeadCommit', + 'runtimeManifestDigest', + 'processStartedAt', + 'processInstanceId', + 'dataDirectoryIdentity', + 'evidenceWaveId', +); +const EDGE_RESTART_RECEIPT_KEYS = defineRecordKeys()( + 'previous', + 'current', +); +const EDGE_RESTART_PREVIOUS_KEYS = defineRecordKeys()( + 'pid', + 'processInstanceId', + 'exitedAt', +); +const EDGE_SYNC_RESULT_KEYS = defineRecordKeys()('operation', 'journal'); +const EDGE_SYNC_RESULT_OPTIONAL_KEYS = defineRecordKeys< + Pick> +>()('journal'); +const EDGE_RECONCILER_RESULT_KEYS = defineRecordKeys()( + 'operation', + 'journal', +); +const CORE_ROUND_RESULT_KEYS = defineRecordKeys()('round', 'journal'); + export function decodeRuntimeReady(input: unknown): SelectiveCoverageRuntimeReadyV1 { - const row = record(input, [ - 'protocol', 'role', 'pid', 'peerId', 'networkId', 'testedHeadCommit', - 'runtimeManifestDigest', 'processStartedAt', 'processInstanceId', - 'dataDirectoryIdentity', 'evidenceWaveId', - ]); + const row = record(input, RUNTIME_READY_KEYS); const role = row.role; if (row.protocol !== SELECTIVE_COVERAGE_RUNTIME_PROTOCOL || (role !== 'publisher' && role !== 'edge' && role !== 'core') @@ -56,8 +102,8 @@ export function decodeRuntimeReady(input: unknown): SelectiveCoverageRuntimeRead } export function decodeRestartReceipt(input: unknown): SelectiveCoverageEdgeRestartReceiptV1 { - const row = record(input, ['previous', 'current']); - const previous = record(row.previous, ['pid', 'processInstanceId', 'exitedAt']); + const row = record(input, EDGE_RESTART_RECEIPT_KEYS); + const previous = record(row.previous, EDGE_RESTART_PREVIOUS_KEYS); if (!positiveInteger(previous.pid) || !nonNegativeInteger(previous.exitedAt)) { fail('restart receipt'); } @@ -86,33 +132,24 @@ export function decodeCoreFinalObservations(input: unknown): readonly CoreFinalO ); } -export function decodeEdgeSyncResult(input: unknown): { - readonly operation: Omit; - readonly journal?: SyncCoverageJournalReferenceV1; -} { - const row = optionalRecord(input, ['operation'], ['journal']); +export function decodeEdgeSyncResult(input: unknown): EdgeSyncResultV1 { + const row = optionalRecord(input, EDGE_SYNC_RESULT_KEYS, EDGE_SYNC_RESULT_OPTIONAL_KEYS); const journal = Object.hasOwn(row, 'journal') ? requiredJournal(row.journal) : undefined; return { operation: decodeEdgeOperation(row.operation), ...(journal ? { journal } : {}) }; } -export function decodeEdgeReconcilerResult(input: unknown): { - readonly operation: Omit; - readonly journal: SyncCoverageJournalReferenceV1; -} { - const row = record(input, ['operation', 'journal']); +export function decodeEdgeReconcilerResult(input: unknown): EdgeReconcilerResultV1 { + const row = record(input, EDGE_RECONCILER_RESULT_KEYS); return { operation: decodeEdgeOperation(row.operation), journal: requiredJournal(row.journal), }; } -export function decodeCoreRoundResult(input: unknown): { - readonly round: CoreAutomaticRoundV1; - readonly journal: SyncCoverageJournalReferenceV1; -} { - const row = record(input, ['round', 'journal']); +export function decodeCoreRoundResult(input: unknown): CoreRoundResultV1 { + const row = record(input, CORE_ROUND_RESULT_KEYS); return { round: requireDecoded(decodeCoreAutomaticRound(row.round), 'M1 runtime adapter Core round'), journal: requiredJournal(row.journal), @@ -125,29 +162,10 @@ export function decodeNull(input: unknown): null { } function decodeEdgeOperation(input: unknown): Omit { - const row = record(input, [ - 'phase', 'source', 'syncMode', 'contextGraphId', 'jobId', - 'completedWave', 'completedSnapshot', - ]); - if ((row.phase !== 'selection' && row.phase !== 'post-restart-auto' - && row.phase !== 'post-restart-explicit') - || (row.source !== 'user' && row.source !== 'reconciler') - || (row.syncMode !== 'always-on' && row.syncMode !== 'on-demand') - || (row.completedWave !== 'selected' && row.completedWave !== 'final')) { - fail('Edge operation'); - } - return { - phase: row.phase, - source: row.source, - syncMode: row.syncMode, - contextGraphId: text(row.contextGraphId, 'contextGraphId'), - jobId: text(row.jobId, 'jobId'), - completedWave: row.completedWave, - completedSnapshot: requireDecoded( - decodeGraphSnapshot(row.completedSnapshot), - 'M1 runtime adapter graph snapshot', - ), - }; + return requireDecoded( + decodeEdgeSyncOperationPayload(input), + 'M1 runtime adapter Edge operation', + ); } function requiredJournal(input: unknown): SyncCoverageJournalReferenceV1 { @@ -162,11 +180,12 @@ function record(input: unknown, keys: readonly string[]): Record { + const optional = new Set(optionalKeys); return requireDecoded( - closedRecord(input, requiredKeys, optionalKeys), + closedRecord(input, keys.filter((key) => !optional.has(key)), optionalKeys), 'M1 runtime adapter record', ); } diff --git a/devnet/rfc64-m1-selective-coverage/runtime.test.ts b/devnet/rfc64-m1-selective-coverage/runtime.test.ts index 7a4f645973..c2c238d8c0 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime.test.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime.test.ts @@ -22,6 +22,8 @@ import { type SelectiveCoverageRuntimeRole, type SelectiveCoverageRuntimeV1, } from './runtime.ts'; +import { decodeEdgeSyncOperationPayload } from './evidence-codec.ts'; +import { decodeEdgeSyncResult } from './runtime-wire.ts'; import type { SyncCoverageJournalReferenceV1 } from './sync-coverage-journal.ts'; import { runSelectiveCoverageLiveV1 } from './live-runner.ts'; @@ -109,6 +111,27 @@ function absentObservation(contextGraphId: string): GraphObservationV1 { return { contextGraphId, vm: { ...plane }, swm: { ...plane } }; } +test('keeps artifact and adapter Edge-operation boundaries in lockstep', () => { + const operation: Omit = { + phase: 'selection', + source: 'user', + syncMode: 'on-demand', + contextGraphId: graphs[0].contextGraphId, + jobId: 'edge-select-on-demand', + completedWave: 'selected', + completedSnapshot: graphs[0].selectedSnapshot, + }; + assert.deepEqual(decodeEdgeSyncOperationPayload(operation), operation); + assert.deepEqual(decodeEdgeSyncResult({ operation }).operation, operation); + + const unknownField = { ...operation, futureField: true }; + assert.equal(decodeEdgeSyncOperationPayload(unknownField), undefined); + assert.throws( + () => decodeEdgeSyncResult({ operation: unknownField }), + /Invalid M1 runtime adapter Edge operation/, + ); +}); + class ScriptedRuntime implements SelectiveCoverageRuntimeV1 { readonly calls: string[] = []; readonly stopped: SelectiveCoverageRuntimeRole[] = []; diff --git a/devnet/rfc64-m1-selective-coverage/runtime.ts b/devnet/rfc64-m1-selective-coverage/runtime.ts index 79f8815832..cf3084d444 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime.ts @@ -12,6 +12,7 @@ import { computeSelectiveCoverageCorpusDigest, } from './manifest.ts'; import { verifySelectiveCoverage } from './verifier.ts'; +import { buildEdgeOperationPlan } from './edge-operation-plan.ts'; import { assertCoreAutomaticRoundJournalV1, assertEdgeReconcilerJournalV1, @@ -96,7 +97,7 @@ export async function collectSelectiveCoverageEvidenceV1(input: { readonly runtime: SelectiveCoverageRuntimeV1; }): Promise { assertAnchoredCorpus(input.corpus, input.expectedProvenance); - const edgePlan = buildEdgePhasePlan(input.corpus); + const edgePlan = buildEdgeOperationPlan(input.corpus); const attempted = new Set(); let primaryFailure: unknown; try { @@ -125,9 +126,9 @@ export async function collectSelectiveCoverageEvidenceV1(input: { for (const step of edgePlan.selection) { const result = await input.runtime.synchronizeEdge({ contextGraphId: step.contextGraphId, - phase: 'selection', + phase: step.phase, syncMode: step.syncMode, - wave: 'selected', + wave: step.completedWave, }); edgeOperations.push(withSequence(result.operation, edgeOperations.length)); } @@ -149,7 +150,7 @@ export async function collectSelectiveCoverageEvidenceV1(input: { assertReady(edgeAfterRestartReady, 'edge', input.expectedProvenance); assertDistinctProcesses([publisher, edgeBeforeRestart, edgeAfterRestartReady]); - for (const step of edgePlan.reconciler) { + for (const step of edgePlan.postRestartAutomatic) { const result = await input.runtime.waitForEdgeReconciler({ contextGraphId: step.contextGraphId, }); @@ -167,12 +168,12 @@ export async function collectSelectiveCoverageEvidenceV1(input: { 'Edge after restart', ); - for (const step of edgePlan.secondOnDemand) { + for (const step of edgePlan.postRestartExplicit) { const result = await input.runtime.synchronizeEdge({ contextGraphId: step.contextGraphId, - phase: 'post-restart-explicit', - syncMode: 'on-demand', - wave: 'final', + phase: step.phase, + syncMode: step.syncMode, + wave: step.completedWave, }); edgeOperations.push(withSequence(result.operation, edgeOperations.length)); } @@ -306,35 +307,6 @@ function detachJsonEvidence( } } -interface EdgePhasePlanV1 { - readonly selection: readonly { - readonly contextGraphId: string; - readonly syncMode: 'always-on' | 'on-demand'; - }[]; - readonly reconciler: readonly { readonly contextGraphId: string }[]; - readonly secondOnDemand: readonly { readonly contextGraphId: string }[]; -} - -function buildEdgePhasePlan(corpus: SelectiveCoverageCorpusV1): EdgePhasePlanV1 { - const selection: Array = []; - const reconciler: Array = []; - const secondOnDemand: Array = []; - for (const graph of corpus.graphs) { - if (graph.accessPolicy !== 0 || graph.edgePolicy === 'unselected') continue; - selection.push({ contextGraphId: graph.contextGraphId, syncMode: graph.edgePolicy }); - if (graph.edgePolicy === 'always-on') { - reconciler.push({ contextGraphId: graph.contextGraphId }); - } else { - secondOnDemand.push({ contextGraphId: graph.contextGraphId }); - } - } - return { - selection: Object.freeze(selection), - reconciler: Object.freeze(reconciler), - secondOnDemand: Object.freeze(secondOnDemand), - }; -} - function withSequence( operation: Omit, sequence: number, diff --git a/devnet/rfc64-m1-selective-coverage/verifier.test.ts b/devnet/rfc64-m1-selective-coverage/verifier.test.ts index 0d75bf4d62..ab1b815091 100644 --- a/devnet/rfc64-m1-selective-coverage/verifier.test.ts +++ b/devnet/rfc64-m1-selective-coverage/verifier.test.ts @@ -746,6 +746,16 @@ test('Edge checkpoints bind their exact state to the operation that produced it' assert.equal(verdict.checks.edgeOperationProvenance, false); }); +test('Edge operations preserve canonical corpus order within each phase', () => { + const reordered = clone(); + [reordered.edge.operations[0], reordered.edge.operations[1]] = + [reordered.edge.operations[1], reordered.edge.operations[0]]; + + const verdict = verifySelectiveCoverage(reordered); + assert.equal(verdict.checks.edgeOperationProvenance, false); + assert.equal(verdict.pass, false); +}); + test('late eventual coverage fails the deterministic first-admission window', () => { const starved = clone(); starved.core.rounds = [ diff --git a/devnet/rfc64-m1-selective-coverage/verifier.ts b/devnet/rfc64-m1-selective-coverage/verifier.ts index bbbc9cf448..b024490550 100644 --- a/devnet/rfc64-m1-selective-coverage/verifier.ts +++ b/devnet/rfc64-m1-selective-coverage/verifier.ts @@ -20,6 +20,12 @@ import { assertCoreAutomaticRoundJournalV1, assertEdgeReconcilerJournalV1, } from './sync-coverage-journal.ts'; +import { + buildEdgeOperationPlan, + findEdgeOperationPlanStep, + matchesEdgeOperationPlanStep, + type EdgeOperationPlanV1, +} from './edge-operation-plan.ts'; import { decodeExpectedSelectiveCoverageProvenance, @@ -202,6 +208,7 @@ function verifyPublisher(context: VerificationContext) { function verifyEdge(context: VerificationContext) { const { corpus, evidence, edgeBefore, edgeSelected, edgeRestarted, edgeSecondOnDemand } = context; + const operationPlan = buildEdgeOperationPlan(corpus); const edgePassiveBeforeSelection = corpus.graphs.every((graph) => absentGraph(edgeBefore.get(graph.contextGraphId)) && edgeBefore.get(graph.contextGraphId)?.runtimeSyncMode === null @@ -212,7 +219,7 @@ function verifyEdge(context: VerificationContext) { ? exactGraph(observed, graph.selectedSnapshot) && observed?.runtimeSyncMode === graph.edgePolicy && observed.producingJobId - === edgeJobId(evidence.edge.operations, graph.contextGraphId, 'selection') + === edgeJobId(operationPlan, evidence.edge.operations, graph.contextGraphId, 'selection') : absentGraph(observed) && observed?.runtimeSyncMode === null && observed.producingJobId === null; }); @@ -221,14 +228,19 @@ function verifyEdge(context: VerificationContext) { .every((graph) => exactGraph(edgeRestarted.get(graph.contextGraphId), graph.selectedSnapshot) && edgeRestarted.get(graph.contextGraphId)?.runtimeSyncMode === null && edgeRestarted.get(graph.contextGraphId)?.producingJobId - === edgeJobId(evidence.edge.operations, graph.contextGraphId, 'selection')); + === edgeJobId(operationPlan, evidence.edge.operations, graph.contextGraphId, 'selection')); const edgeAlwaysOnRefreshesAfterRestart = corpus.graphs .filter((graph) => graph.edgePolicy === 'always-on') .every((graph) => snapshotsAdvance(graph.selectedSnapshot, graph.finalSnapshot) && exactGraph(edgeRestarted.get(graph.contextGraphId), graph.finalSnapshot) && edgeRestarted.get(graph.contextGraphId)?.runtimeSyncMode === 'always-on' && edgeRestarted.get(graph.contextGraphId)?.producingJobId - === edgeJobId(evidence.edge.operations, graph.contextGraphId, 'post-restart-auto')); + === edgeJobId( + operationPlan, + evidence.edge.operations, + graph.contextGraphId, + 'post-restart-auto', + )); const edgeSecondOnDemandConverges = corpus.graphs.every((graph) => { const observed = edgeSecondOnDemand.get(graph.contextGraphId); if (graph.accessPolicy !== 0 || graph.edgePolicy === 'unselected') { @@ -242,7 +254,7 @@ function verifyEdge(context: VerificationContext) { && exactGraph(observed, graph.finalSnapshot) && observed?.runtimeSyncMode === graph.edgePolicy && observed.producingJobId - === edgeJobId(evidence.edge.operations, graph.contextGraphId, phase); + === edgeJobId(operationPlan, evidence.edge.operations, graph.contextGraphId, phase); }); const excluded = (graph: SelectiveCoverageGraphV1) => absentGraph(edgeSelected.get(graph.contextGraphId)) @@ -256,7 +268,7 @@ function verifyEdge(context: VerificationContext) { edgeSelectedSnapshotsExact, edgeOnDemandRemainsPointInTime, edgeAlwaysOnRefreshesAfterRestart, - edgeOperationProvenance: verifyEdgeOperations(evidence.edge.operations, corpus.graphs) + edgeOperationProvenance: verifyEdgeOperations(evidence.edge.operations, operationPlan) && verifyAutomaticEdgeJournals(evidence), edgeSecondOnDemandConverges, edgeUnselectedExcluded: context.publicGraphs @@ -392,60 +404,30 @@ function verifyAutomaticCoreJournals(evidence: SelectiveCoverageEvidenceV1): boo function verifyEdgeOperations( operations: readonly EdgeSyncOperationV1[], - graphs: readonly SelectiveCoverageGraphV1[], + plan: EdgeOperationPlanV1, ): boolean { - const selected = graphs.filter((graph) => - graph.accessPolicy === 0 && graph.edgePolicy !== 'unselected'); - if (operations.length !== selected.length * 2 + if (operations.length !== plan.ordered.length || new Set(operations.map((operation) => operation.jobId)).size !== operations.length) { return false; } - const selectionSequences = operations - .filter((operation) => operation.phase === 'selection') - .map((operation) => operation.sequence); - const automaticSequences = operations - .filter((operation) => operation.phase === 'post-restart-auto') - .map((operation) => operation.sequence); - const secondOnDemandSequences = operations - .filter((operation) => operation.phase === 'post-restart-explicit') - .map((operation) => operation.sequence); - if (selectionSequences.length === 0 - || Math.max(...selectionSequences) >= Math.min(...automaticSequences) - || Math.max(...automaticSequences) >= Math.min(...secondOnDemandSequences)) return false; - return selected.every((graph) => { - const graphOperations = operations.filter((operation) => - operation.contextGraphId === graph.contextGraphId); - if (graphOperations.length !== 2) return false; - const selection = graphOperations.find((operation) => operation.phase === 'selection'); - if (selection?.source !== 'user' || selection.syncMode !== graph.edgePolicy - || selection.completedWave !== 'selected' - || !exactSnapshot(selection.completedSnapshot, graph.selectedSnapshot)) return false; - if (graph.edgePolicy === 'on-demand') { - const refresh = graphOperations.find((operation) => - operation.phase === 'post-restart-explicit'); - return refresh?.source === 'user' && refresh.syncMode === 'on-demand' - && refresh.completedWave === 'final' - && exactSnapshot(refresh.completedSnapshot, graph.finalSnapshot); - } - const refresh = graphOperations.find((operation) => - operation.phase === 'post-restart-auto'); - return refresh?.source === 'reconciler' && refresh.syncMode === 'always-on' - && refresh.completedWave === 'final' - && exactSnapshot(refresh.completedSnapshot, graph.finalSnapshot); - }) && operations.every((operation) => { - const graph = graphs.find((candidate) => - candidate.contextGraphId === operation.contextGraphId); - return graph?.accessPolicy === 0 && graph.edgePolicy !== 'unselected'; - }); + return plan.ordered.every((step, index) => + matchesEdgeOperationPlanStep(operations[index], step)); } function edgeJobId( + plan: EdgeOperationPlanV1, operations: readonly EdgeSyncOperationV1[], contextGraphId: string, phase: EdgeSyncOperationV1['phase'], ): string | undefined { - return operations.find((operation) => - operation.contextGraphId === contextGraphId && operation.phase === phase)?.jobId; + const step = findEdgeOperationPlanStep(plan, contextGraphId, phase); + if (!step) return undefined; + const operation = operations[step.sequence]; + return operation?.sequence === step.sequence + && operation.phase === step.phase + && operation.contextGraphId === step.contextGraphId + ? operation.jobId + : undefined; } function exactGraph( diff --git a/devnet/rfc64-persistence-lifecycle/evidence.test.ts b/devnet/rfc64-persistence-lifecycle/evidence.test.ts index 5b83914278..7993042b6b 100644 --- a/devnet/rfc64-persistence-lifecycle/evidence.test.ts +++ b/devnet/rfc64-persistence-lifecycle/evidence.test.ts @@ -19,6 +19,7 @@ import test from 'node:test'; import { atomicWriteStableJson, + compactSafeIntegerJson, readCleanRepositoryHead, stableJson, } from './evidence.js'; @@ -31,7 +32,7 @@ test.afterEach(() => { } }); -test('stableJson sorts keys and preserves supported plain data exactly', () => { +test('stable JSON encoders preserve their distinct exact policies', () => { const value = { z: [true, null, 17.25, 'plain'], a: { second: 2, first: 1 }, @@ -42,6 +43,15 @@ test('stableJson sorts keys and preserves supported plain data exactly', () => { '{\n "a": {\n "first": 1,\n "second": 2\n },\n "z": [\n true,\n null,\n 17.25,\n "plain"\n ]\n}\n', ); assert.deepEqual(JSON.parse(encoded), value); + assert.equal( + compactSafeIntegerJson({ z: [true, null, 17], a: { second: 2, first: 1 } }), + '{"a":{"first":1,"second":2},"z":[true,null,17]}', + ); + assert.throws(() => compactSafeIntegerJson({ value: 17.25 }), /non-lossless JSON number/u); + assert.throws( + () => compactSafeIntegerJson({ value: Number.MAX_SAFE_INTEGER + 1 }), + /non-lossless JSON number/u, + ); }); test('stableJson rejects values that JSON would omit, coerce, or reshape', () => { diff --git a/devnet/rfc64-persistence-lifecycle/evidence.ts b/devnet/rfc64-persistence-lifecycle/evidence.ts index 6dda8bae9d..04ee0c5cc0 100644 --- a/devnet/rfc64-persistence-lifecycle/evidence.ts +++ b/devnet/rfc64-persistence-lifecycle/evidence.ts @@ -57,25 +57,24 @@ export function readCleanRepositoryHead(repoRootInput: string): string { return head; } -export interface StableJsonOptions { - readonly format?: 'pretty' | 'compact'; - readonly trailingLf?: boolean; - readonly numbers?: 'finite' | 'safe-integer'; -} +type JsonNumberPolicy = 'finite' | 'safe-integer'; -export function stableJson(value: unknown, options: StableJsonOptions = {}): string { +/** Gate 0 canonical JSON: sorted keys, two-space indentation, and one trailing LF. */ +export function stableJson(value: unknown): string { const normalized = normalizePlainJsonValue( value, '$', new WeakSet(), - options.numbers ?? 'finite', + 'finite', ); - const encoded = JSON.stringify( - normalized, - null, - options.format === 'compact' ? undefined : 2, + return `${JSON.stringify(normalized, null, 2)}\n`; +} + +/** M1 canonical JSON: sorted keys, compact bytes, and lossless integer numbers only. */ +export function compactSafeIntegerJson(value: unknown): string { + return JSON.stringify( + normalizePlainJsonValue(value, '$', new WeakSet(), 'safe-integer'), ); - return options.trailingLf === false ? encoded : `${encoded}\n`; } export function atomicWriteStableJson( @@ -159,7 +158,7 @@ function normalizePlainJsonValue( value: unknown, path: string, seen: WeakSet, - numbers: NonNullable, + numbers: JsonNumberPolicy, ): unknown { if (value === null || typeof value === 'string' || typeof value === 'boolean') { return value; From 1a3809a542e37bf838ad09abcaca4a7e3261f07a Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 16:46:29 +0200 Subject: [PATCH 11/14] test(rfc64): close adapter result envelopes --- .../process-runtime-fixture.mjs | 12 +++ .../process-runtime.test.ts | 13 ++++ .../process-runtime.ts | 75 ++++++++++++++----- .../sync-coverage-journal.ts | 6 +- 4 files changed, 86 insertions(+), 20 deletions(-) diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs b/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs index 4b2005ed4a..06a954e0ae 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs +++ b/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs @@ -55,6 +55,18 @@ lines.on('line', (line) => { if (mode === 'wrong-protocol') result.protocol = 'wrong-protocol'; if (mode === 'wrong-schema') result.schema = 'wrong-schema'; if (mode === 'unknown-sequence') result.sequence += 1; + if (mode === 'extra-envelope') result.unexpected = true; + if (mode === 'mixed-success-envelope') result.error = 'contradictory failure'; + if (mode === 'mixed-failure-envelope') { + result.ok = false; + result.error = 'fixture failure'; + } + if (mode === 'nonboolean-ok') result.ok = 'yes'; + if (mode === 'failure-envelope') { + result.ok = false; + delete result.value; + result.error = 'fixture failure'; + } if (mode === 'malformed-json') { process.stdout.write(`${prefix}{not-json\n`, () => process.exit(0)); return; diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts index a845b02aef..94161e3e3a 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts +++ b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts @@ -68,6 +68,10 @@ for (const [mode, message] of [ ['wrong-protocol', /invalid result envelope/], ['wrong-schema', /invalid result envelope/], ['unknown-sequence', /unknown result sequence/], + ['extra-envelope', /invalid result envelope/], + ['mixed-success-envelope', /invalid result envelope/], + ['mixed-failure-envelope', /invalid result envelope/], + ['nonboolean-ok', /invalid result envelope/], ['malformed-json', /malformed result JSON/], ['oversized-line', /exceeds 1 MiB/], ] as const) { @@ -81,6 +85,15 @@ for (const [mode, message] of [ }); } +test('accepts the exact closed failure result envelope', async () => { + const runtime = fixtureRuntime('failure-envelope'); + try { + await assert.rejects(runtime.start('publisher'), /fixture failure/); + } finally { + await runtime.close().catch(() => undefined); + } +}); + test('decodes non-start command results at the adapter boundary', async () => { const runtime = fixtureRuntime('malformed-publish'); try { diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime.ts b/devnet/rfc64-m1-selective-coverage/process-runtime.ts index 7780ff6efa..ccd21a22ba 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime.ts +++ b/devnet/rfc64-m1-selective-coverage/process-runtime.ts @@ -1,6 +1,11 @@ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'; import { randomBytes } from 'node:crypto'; +import { + closedRecord, + defineRecordKeys, + plainRecord, +} from './boundary-codec.ts'; import { SELECTIVE_COVERAGE_RUNTIME_PROTOCOL, type SelectiveCoverageEdgeRestartReceiptV1, @@ -36,6 +41,41 @@ export const SELECTIVE_COVERAGE_RUNTIME_RESULT_PREFIX = 'DKG_RFC64_M1_RESULT '; const MAX_RESULT_LINE_BYTES = 1024 * 1024; const CLOSE_GRACE_MS = 5_000; +interface RuntimeSuccessResultEnvelopeV1 { + readonly schema: typeof SELECTIVE_COVERAGE_RUNTIME_RESULT_SCHEMA; + readonly protocol: typeof SELECTIVE_COVERAGE_RUNTIME_PROTOCOL; + readonly sessionNonce: string; + readonly sequence: number; + readonly ok: true; + readonly value: unknown; +} + +interface RuntimeFailureResultEnvelopeV1 { + readonly schema: typeof SELECTIVE_COVERAGE_RUNTIME_RESULT_SCHEMA; + readonly protocol: typeof SELECTIVE_COVERAGE_RUNTIME_PROTOCOL; + readonly sessionNonce: string; + readonly sequence: number; + readonly ok: false; + readonly error: string; +} + +const RUNTIME_SUCCESS_RESULT_KEYS = defineRecordKeys()( + 'schema', + 'protocol', + 'sessionNonce', + 'sequence', + 'ok', + 'value', +); +const RUNTIME_FAILURE_RESULT_KEYS = defineRecordKeys()( + 'schema', + 'protocol', + 'sessionNonce', + 'sequence', + 'ok', + 'error', +); + interface PendingRequest { readonly resolve: (value: unknown) => void; readonly reject: (error: Error) => void; @@ -273,15 +313,22 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti this.failAll(new Error('M1 runtime adapter emitted malformed result JSON', { cause: error })); return; } - if (!isPlainRecord(value) - || value['schema'] !== SELECTIVE_COVERAGE_RUNTIME_RESULT_SCHEMA - || value['protocol'] !== SELECTIVE_COVERAGE_RUNTIME_PROTOCOL - || value['sessionNonce'] !== this.sessionNonce - || !Number.isSafeInteger(value['sequence'])) { + const probe = plainRecord(value); + const row = probe?.['ok'] === true + ? closedRecord(value, RUNTIME_SUCCESS_RESULT_KEYS) + : probe?.['ok'] === false + ? closedRecord(value, RUNTIME_FAILURE_RESULT_KEYS) + : undefined; + if (!row + || row['schema'] !== SELECTIVE_COVERAGE_RUNTIME_RESULT_SCHEMA + || row['protocol'] !== SELECTIVE_COVERAGE_RUNTIME_PROTOCOL + || row['sessionNonce'] !== this.sessionNonce + || !Number.isSafeInteger(row['sequence']) + || (row['ok'] === false && (typeof row['error'] !== 'string' || !row['error']))) { this.failAll(new Error('M1 runtime adapter emitted an invalid result envelope')); return; } - const sequence = value['sequence'] as number; + const sequence = row['sequence'] as number; const request = this.pending.get(sequence); if (!request) { this.failAll(new Error(`M1 runtime adapter emitted an unknown result sequence: ${sequence}`)); @@ -289,14 +336,11 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti } clearTimeout(request.timer); this.pending.delete(sequence); - if (value['ok'] === true && Object.hasOwn(value, 'value')) { - request.resolve(value['value']); + if (row['ok'] === true) { + request.resolve(row['value']); return; } - const message = typeof value['error'] === 'string' && value['error'] - ? value['error'] - : 'runtime adapter command failed without an error message'; - request.reject(new Error(message)); + request.reject(new Error(row['error'] as string)); } private failAll(error: Error): void { @@ -321,10 +365,3 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti return result; } } - -function isPlainRecord(value: unknown): value is Record { - return value !== null - && typeof value === 'object' - && !Array.isArray(value) - && Object.getPrototypeOf(value) === Object.prototype; -} diff --git a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts index 5e5e9bdb18..0b68e3abe8 100644 --- a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts +++ b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts @@ -9,6 +9,7 @@ import { boundedString, closedArray, closedRecord, + defineRecordKeys, nonNegativeInteger, plainRecord, positiveInteger, @@ -16,6 +17,9 @@ import { const JOURNAL_CAPACITY = 256; const MAX_CONTEXT_GRAPH_ID_LENGTH = 256; +const SYNC_COVERAGE_JOURNAL_REFERENCE_KEYS = defineRecordKeys< + SyncCoverageJournalReferenceV1 +>()('snapshot', 'sequence'); export type { SyncCoverageJournalProcessIdentityV1, @@ -26,7 +30,7 @@ export type { export function parseSyncCoverageJournalReferenceV1( input: unknown, ): SyncCoverageJournalReferenceV1 | undefined { - const row = closedRecord(input, ['snapshot', 'sequence']); + const row = closedRecord(input, SYNC_COVERAGE_JOURNAL_REFERENCE_KEYS); if (!row || !nonNegativeInteger(row['sequence'])) return undefined; return { snapshot: row['snapshot'], sequence: row['sequence'] }; } From 7427437c865c2ac11c8de63d063c08a0c20e3609 Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 16:52:14 +0200 Subject: [PATCH 12/14] test(rfc64): fail closed on adapter exit --- .../process-runtime-fixture.mjs | 11 +++- .../process-runtime.test.ts | 18 ++++++ .../process-runtime.ts | 59 ++++++++++++++----- 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs b/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs index 06a954e0ae..3065bef34a 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs +++ b/devnet/rfc64-m1-selective-coverage/process-runtime-fixture.mjs @@ -45,7 +45,16 @@ lines.on('line', (line) => { process.stdout.write(`${prefix}${JSON.stringify(result)}\n`, () => process.exit(0)); return; } - if (input.command === 'start' && mode) { + if (input.command === 'shutdown' && mode === 'shutdown-nonzero') { + process.stdout.write(`${prefix}${JSON.stringify(result)}\n`, () => process.exit(17)); + return; + } + if (input.command === 'shutdown' && mode === 'shutdown-hang') { + process.stdout.write(`${prefix}${JSON.stringify(result)}\n`); + setInterval(() => undefined, 1_000); + return; + } + if (input.command === 'start' && mode && !mode.startsWith('shutdown-')) { if (mode === 'malformed-publish') { process.stdout.write(`${prefix}${JSON.stringify(result)}\n`); return; diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts index 94161e3e3a..6ec313f42d 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts +++ b/devnet/rfc64-m1-selective-coverage/process-runtime.test.ts @@ -153,6 +153,24 @@ test('a non-terminal child error cannot satisfy the process-exit proof', async ( } }); +test('rejects a non-zero adapter exit after shutdown acknowledgement', async () => { + const runtime = fixtureRuntime('shutdown-nonzero'); + await runtime.start('publisher'); + await assert.rejects( + runtime.close(), + /runtime adapter exited abnormally after shutdown \(code=17 signal=null\)/, + ); +}); + +test('rejects forced termination after shutdown acknowledgement', async () => { + const runtime = fixtureRuntime('shutdown-hang'); + await runtime.start('publisher'); + await assert.rejects( + runtime.close(), + /runtime adapter required forced SIGTERM during shutdown/, + ); +}); + for (const [command, invoke] of [ ['observe-edge', (runtime: ProcessSelectiveCoverageRuntimeV1) => runtime.observeEdge('before-selection')], diff --git a/devnet/rfc64-m1-selective-coverage/process-runtime.ts b/devnet/rfc64-m1-selective-coverage/process-runtime.ts index ccd21a22ba..7c50bdb662 100644 --- a/devnet/rfc64-m1-selective-coverage/process-runtime.ts +++ b/devnet/rfc64-m1-selective-coverage/process-runtime.ts @@ -82,6 +82,11 @@ interface PendingRequest { readonly timer: ReturnType; } +interface ProcessExitOutcome { + readonly code: number | null; + readonly signal: NodeJS.Signals | null; +} + /** * JSON-lines bridge to an operator-owned adapter that controls three real DKG * processes. Ordinary adapter logs may use stdout; only prefixed result lines @@ -96,7 +101,8 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti private exitError: Error | undefined; private stdoutBuffer = Buffer.alloc(0); private readonly sessionNonce = randomBytes(32).toString('hex'); - private readonly exited: Promise; + private readonly exited: Promise; + private exitOutcome: ProcessExitOutcome | undefined; constructor(input: { readonly command: string; @@ -118,15 +124,16 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti }); this.child.stderr.pipe(process.stderr); this.child.stdout.on('data', (chunk: Buffer) => this.consumeChunk(chunk)); - let resolveExited!: () => void; + let resolveExited!: (outcome: ProcessExitOutcome) => void; this.exited = new Promise((resolveExit) => { resolveExited = resolveExit; }); let terminal = false; - const resolveTerminal = () => { + const resolveTerminal = (outcome: ProcessExitOutcome) => { if (terminal) return; terminal = true; - resolveExited(); + this.exitOutcome = outcome; + resolveExited(outcome); }; this.child.on('error', (error) => { // Spawn failures are terminal but ChildProcess also emits `error` for @@ -135,12 +142,12 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti if (this.child.pid === undefined || this.child.exitCode !== null || this.child.signalCode !== null) { - resolveTerminal(); + resolveTerminal({ code: this.child.exitCode, signal: this.child.signalCode }); } this.failAll(new Error('M1 runtime adapter process failed', { cause: error })); }); this.child.once('exit', (code, signal) => { - resolveTerminal(); + resolveTerminal({ code, signal }); if (!this.closing || this.pending.size > 0) { this.failAll(new Error( `M1 runtime adapter exited before shutdown acknowledgement ` @@ -149,7 +156,7 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti } }); this.child.once('close', (code, signal) => { - resolveTerminal(); + resolveTerminal({ code, signal }); if (!this.closing || this.pending.size > 0) { this.failAll(new Error( `M1 runtime adapter closed before shutdown acknowledgement ` @@ -227,14 +234,32 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti } this.closed = true; this.child.stdin.end(); - if (!await this.waitForExit(CLOSE_GRACE_MS)) { + let forcedSignal: NodeJS.Signals | undefined; + let exit = await this.waitForExit(CLOSE_GRACE_MS); + if (!exit) { + forcedSignal = 'SIGTERM'; this.child.kill('SIGTERM'); - if (!await this.waitForExit(CLOSE_GRACE_MS)) { + exit = await this.waitForExit(CLOSE_GRACE_MS); + if (!exit) { + forcedSignal = 'SIGKILL'; this.child.kill('SIGKILL'); - await this.exited; + exit = await this.waitForExit(CLOSE_GRACE_MS); } } if (shutdownFailure !== undefined) throw shutdownFailure; + if (this.exitError) throw this.exitError; + if (!exit) { + throw new Error('M1 runtime adapter did not exit after forced SIGKILL'); + } + if (forcedSignal) { + throw new Error(`M1 runtime adapter required forced ${forcedSignal} during shutdown`); + } + if (exit.code !== 0 || exit.signal !== null) { + throw new Error( + `M1 runtime adapter exited abnormally after shutdown ` + + `(code=${String(exit.code)} signal=${String(exit.signal)})`, + ); + } } private request( @@ -352,15 +377,17 @@ export class ProcessSelectiveCoverageRuntimeV1 implements SelectiveCoverageRunti this.pending.clear(); } - private async waitForExit(timeoutMs: number): Promise { - if (this.child.exitCode !== null || this.child.signalCode !== null) return true; + private async waitForExit(timeoutMs: number): Promise { + if (this.exitOutcome) return this.exitOutcome; + if (this.child.exitCode !== null || this.child.signalCode !== null) { + return { code: this.child.exitCode, signal: this.child.signalCode }; + } let timer: ReturnType | undefined; - const timedOut = new Promise((resolveTimeout) => { - timer = setTimeout(() => resolveTimeout(false), timeoutMs); + const timedOut = new Promise((resolveTimeout) => { + timer = setTimeout(() => resolveTimeout(undefined), timeoutMs); timer.unref(); }); - const exited = this.exited.then(() => true as const); - const result = await Promise.race([exited, timedOut]); + const result = await Promise.race([this.exited, timedOut]); if (timer) clearTimeout(timer); return result; } From 48ae964c3920f34e93988e3cba52df7b9c991516 Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 17:29:59 +0200 Subject: [PATCH 13/14] test(rfc64): cover pre-selection Edge exclusion --- .../verifier.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/devnet/rfc64-m1-selective-coverage/verifier.test.ts b/devnet/rfc64-m1-selective-coverage/verifier.test.ts index ab1b815091..b6a97ba977 100644 --- a/devnet/rfc64-m1-selective-coverage/verifier.test.ts +++ b/devnet/rfc64-m1-selective-coverage/verifier.test.ts @@ -346,6 +346,30 @@ test('accepts exact Edge selection and bounded Core public convergence evidence' } }); +test('rejects Edge payload, subscription, or job evidence before selection', () => { + const payloadLeak = clone(); + payloadLeak.edge.beforeSelection[0] = { + ...exact(graphs[0]!, graphs[0]!.selectedSnapshot), + runtimeSyncMode: null, + producingJobId: null, + }; + let verdict = verifySelectiveCoverage(payloadLeak); + assert.equal(verdict.checks.edgePassiveBeforeSelection, false); + assert.equal(verdict.pass, false); + + const subscriptionLeak = clone(); + subscriptionLeak.edge.beforeSelection[0].runtimeSyncMode = 'on-demand'; + verdict = verifySelectiveCoverage(subscriptionLeak); + assert.equal(verdict.checks.edgePassiveBeforeSelection, false); + assert.equal(verdict.pass, false); + + const jobLeak = clone(); + jobLeak.edge.beforeSelection[0].producingJobId = 'premature-edge-job'; + verdict = verifySelectiveCoverage(jobLeak); + assert.equal(verdict.checks.edgePassiveBeforeSelection, false); + assert.equal(verdict.pass, false); +}); + test('published artifact must retain matching automatic journal proof', () => { const missing = clone(); delete missing.automaticJournalEvidence; From cb28be01c0759270cdbc396bb75841636d78ac0c Mon Sep 17 00:00:00 2001 From: Branimir Rakic Date: Sun, 2 Aug 2026 17:37:00 +0200 Subject: [PATCH 14/14] test(rfc64): bind Core evidence to automatic triggers --- devnet/rfc64-m1-selective-coverage/runtime.test.ts | 4 ++-- .../sync-coverage-journal.ts | 7 +++++++ devnet/rfc64-m1-selective-coverage/verifier.test.ts | 12 +++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/devnet/rfc64-m1-selective-coverage/runtime.test.ts b/devnet/rfc64-m1-selective-coverage/runtime.test.ts index c2c238d8c0..79906b3595 100644 --- a/devnet/rfc64-m1-selective-coverage/runtime.test.ts +++ b/devnet/rfc64-m1-selective-coverage/runtime.test.ts @@ -352,7 +352,7 @@ class ScriptedRuntime implements SelectiveCoverageRuntimeV1 { jobId: observed.jobId, planningLane: observed.planningLane, source: 'automatic-core-public', - trigger: 'peer-sync', + trigger: 'connection-open', configuredBatchSize: observed.configuredBatchSize, effectiveBatchSize: observed.configuredBatchSize, explicitSelectedContextGraphIds: observed.explicitSelectedContextGraphIds, @@ -377,7 +377,7 @@ class ScriptedRuntime implements SelectiveCoverageRuntimeV1 { jobId: observed.jobId, planningLane: observed.planningLane, source: 'automatic-core-public', - trigger: 'peer-sync', + trigger: 'connection-open', configuredBatchSize: observed.configuredBatchSize, effectiveBatchSize: observed.configuredBatchSize, explicitSelectedContextGraphIds: observed.explicitSelectedContextGraphIds, diff --git a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts index 0b68e3abe8..8e4cad988b 100644 --- a/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts +++ b/devnet/rfc64-m1-selective-coverage/sync-coverage-journal.ts @@ -74,6 +74,7 @@ export function assertCoreAutomaticRoundJournalV1( if (entry['jobId'] !== round.jobId || entry['planningLane'] !== round.planningLane || entry['source'] !== 'automatic-core-public' + || !isCoreAutomaticTriggerV1(entry['trigger']) || entry['configuredBatchSize'] !== round.configuredBatchSize || !nonNegativeInteger(effectiveBatchSize) || effectiveBatchSize > round.configuredBatchSize @@ -102,6 +103,12 @@ export function assertCoreAutomaticRoundJournalV1( } } +function isCoreAutomaticTriggerV1(value: unknown): boolean { + return value === 'connection-open' + || value === 'peer-update' + || value === 'periodic-reconciler'; +} + function terminalEntry( reference: SyncCoverageJournalReferenceV1 | undefined, kind: 'edge-reconciler-job' | 'core-automatic-round', diff --git a/devnet/rfc64-m1-selective-coverage/verifier.test.ts b/devnet/rfc64-m1-selective-coverage/verifier.test.ts index b6a97ba977..5a9cd86809 100644 --- a/devnet/rfc64-m1-selective-coverage/verifier.test.ts +++ b/devnet/rfc64-m1-selective-coverage/verifier.test.ts @@ -157,7 +157,7 @@ function coreJournal( jobId, planningLane: 'publisher-peer', source: 'automatic-core-public', - trigger: 'peer-sync', + trigger: 'connection-open', configuredBatchSize, effectiveBatchSize: configuredBatchSize, explicitSelectedContextGraphIds: [], @@ -406,6 +406,16 @@ test('published artifact must retain matching automatic journal proof', () => { ); }); +test('manual catch-up cannot satisfy Core automatic provenance', () => { + const manualCatchup = clone(); + manualCatchup.automaticJournalEvidence.coreRounds[0] + .snapshot.entries[0].trigger = 'manual-catchup'; + + const verdict = verifySelectiveCoverage(manualCatchup); + assert.equal(verdict.checks.coreAutomaticProvenance, false); + assert.equal(verdict.pass, false); +}); + test('supports 33 public graphs through multiple bounded journal rounds', () => { const extra = Array.from({ length: 30 }, (_, index): SelectiveCoverageGraphV1 => { const name = `03-public-unselected-${String(index).padStart(2, '0')}`;