From 9312811be8f0e79b8f05ce7e0d09a2626063b8ea Mon Sep 17 00:00:00 2001 From: Daniel Strader Date: Sat, 5 Sep 2026 19:27:59 -0700 Subject: [PATCH 1/3] Stream framed checkpoints from the shared engine --- CHANGELOG.md | 1 + benchmarks/checkpoint-v2-promotion-v1.json | 125 ++ docs/checkpoint-v2-promotion-evaluation.md | 155 ++ docs/local-checkpoints.md | 23 +- docs/shared-checkpoint-artifact-v2.md | 56 +- docs/shared-checkpoint-schema-v1.md | 6 +- package.json | 3 + src/checkpoint-artifact-v2.ts | 10 +- src/checkpoint-v2-evaluation-cli.ts | 1660 ++++++++++++++++++++ src/checkpoints.ts | 830 +++++++++- src/explore.ts | 502 +++++- test/checkpoint-read-accounting.test.js | 2 + test/checkpoint-source-isolation.test.js | 223 +++ test/checkpoint-source-streaming.test.js | 856 ++++++++++ test/checkpoint-v2-evaluation.test.js | 394 +++++ test/checkpoint-v2.test.js | 103 +- test/inkcheck.test.js | 22 +- 17 files changed, 4832 insertions(+), 139 deletions(-) create mode 100644 benchmarks/checkpoint-v2-promotion-v1.json create mode 100644 docs/checkpoint-v2-promotion-evaluation.md create mode 100644 src/checkpoint-v2-evaluation-cli.ts create mode 100644 test/checkpoint-source-isolation.test.js create mode 100644 test/checkpoint-source-streaming.test.js create mode 100644 test/checkpoint-v2-evaluation.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e3572e3..ffbddfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Add an opt-in engine-native producer for framed checkpoint v2. A shared-search engine can remain quiescent at its exact resumable boundary while a callback-scoped, repeatable source streams the unchanged canonical schema-v1 identity and ordered frame records; the source yields detached values, snapshots mutable caller inputs, prevents evidence callbacks from aliasing retained findings, invalidates at synchronous or awaited callback settlement, and never constructs or recursively clones a complete checkpoint graph. Write receipts distinguish zero materialized graphs, one identity pass, and separate zero-or-one candidate-encoding and reuse-verification record passes. Same-ID framed reuse bounded-stream-compares canonical records with the live source instead of trusting a recomputed manifest/payload pair; legacy reuse and sidecar-free retention inventory fail closed on this graph-free route while remaining available to the graph API. Framed read receipts expose their actual effective codec limits, including cumulative decoded bytes independently of the legacy whole-value string ceiling, and restore structurally clones the validated graph instead of recreating one artifact-sized JSON string, while stored, frame, record, depth, and runtime bounds remain enforced. Windows recovery-slot cleaning treats ordinary `EPERM`/`EBUSY`/`EACCES` namespace contention as failure to acquire—not ownership—without weakening durable claim or manifest writes. Existing synchronous and graph APIs, stable IDs, default legacy-v1 CLI/MCP writes, search order, and resource policy remain unchanged. This is still a partial #156 foundation pending matched Intercept and Heresy II evidence; it does not promote a default, stream provisional read records into a live engine, restart InkBench, or publish a release. - Add an opt-in framed checkpoint artifact-v2 codec around the unchanged logical shared-checkpoint schema v1. Ordered configuration, scheduler, frontier, witness-ancestry, dedupe, semantic-index, finding, and metadata records are independently bounded and compressed; exact frame/component checksums and a terminal index support incremental readback without a whole stored artifact, decompressed artifact, or raw artifact string. Cross-format writers share one neutral no-clobber commit point, so a stable ID reuses exactly one verified `.json.gz` or `.inkcp` layout under the existing crash-recovery and retention protocol. Observed-before-work, preflight-without-output, and successfully encoded no-clobber losers reuse the winner; a private candidate that emitted bytes and then failed preserves its error for a later retry. Stable checkpoint IDs, exact resume state, default gzip writes, default CLI/MCP search behavior, search order, and resource policy are unchanged; storage listing and search-session disk accounting now recognize, reuse, and attribute either verified layout. This is a codec foundation only: framed v2 remains an explicit library opt-in pending Intercept plus second-family promotion evidence, and it does not complete #156, activate checkpoint/epoch/pressure policy, restart InkBench, or publish a release. - Extend the partial owner ledger across checkpoint reopen and report enrichment/finalization without adding another payload read, decode, parse, or stable-ID traversal. Successful checkpoint opens and resumes expose bounded stage-by-stage receipts, while typed corrupt/unsupported/resource-limit failures retain the work reached before failure. The opt-in accounted report builder measures the source exploration graph, finding-identity strings already materialized by enrichment, and the returned report; report saves separately distinguish created envelope graphs from reuse-parsed artifact graphs. These compact-JSON serialized-view proxies and their conservative potential sums are not exclusive retained-owner bytes or observed heap peaks. Runtime receipts remain outside saved report/checkpoint payloads and IDs, and bounded JSON streaming still avoids the monolithic report graph. This adds no checkpoint v2/framed readback, allocation, eviction, compaction, stopping policy, benchmark restart, or release; #156 and #216 remain open. - Add versioned retained-owner and finalization receipts without changing search policy or the checksum-bound observability-v1/v2 shapes. Shared pass telemetry now accounts for the bounded observability ledger through a non-recursive canonical UTF-8 projection and preserves its known high-water state across resume; older checkpoints remain readable and explicitly report incomplete owner history. Checkpoint saves report logical graph bytes, streamed artifact/chunk bytes, compressed output, configured compression capacity, durable payload/manifest bytes, and whether encode/compress work was applied or an existing stable ID was reused. Report saves similarly expose canonical identity-string, artifact-serialization, conservative potential string, and durable bytes outside the content-derived report. Configured finalization headroom is labeled unallocated capacity and remains excluded from retained/process totals. Stable-ID algorithms are unchanged: deterministic owner state participates in newly emitted checkpoint content, while post-write receipts never feed back into the IDs or payloads they measure. These fields are observational/accounting inputs only: they do not emit the reserved checkpoint/epoch/pressure reasons, change frontier order, activate a budget or stopping policy, add checkpoint v2, or publish a release; #156 and #216 remain open. diff --git a/benchmarks/checkpoint-v2-promotion-v1.json b/benchmarks/checkpoint-v2-promotion-v1.json new file mode 100644 index 0000000..63b5f9d --- /dev/null +++ b/benchmarks/checkpoint-v2-promotion-v1.json @@ -0,0 +1,125 @@ +{ + "schemaVersion": 1, + "kind": "checkpoint_v2_promotion_evaluation", + "id": "checkpoint-v2-promotion-v1", + "compiler": { + "name": "inklecate", + "version": "1.2.1" + }, + "controls": { + "cellOrder": [ + "heresy2-legacy-split", + "heresy2-framed-split", + "heresy2-uninterrupted", + "intercept-uninterrupted", + "intercept-framed-split", + "intercept-legacy-split" + ], + "maxDepth": 100, + "searchSeed": 7, + "storySeed": 1, + "concurrency": 1, + "minimizeRepros": false, + "stateSensitivity": "source_semantics", + "loopRiskDetection": "only_without_turns_randomness_visit_counts_or_externals", + "maxMemoryMb": 4096, + "maxTimeMs": 840000, + "workerTimeoutMs": 900000, + "coordinatorMaxOldSpaceMb": 6144, + "storage": { + "maxCheckpointBytes": 536870912, + "maxProjectBytes": 2147483648, + "framedV2": { + "maxTotalDecodedBytes": 2147483648 + } + } + }, + "sources": [ + { + "id": "heresy2", + "story": "authored/heresy2/heresy2.ink", + "entrypointSha256": "49db768479e523f39ae9eb838f6daa0798244b7bbe266d686a7a967ccf66f7ea", + "compiledStorySha256": "5c65cf077478ad70985341e564695cb8ab35af20ec574d12b517d1e18f9a4bb7", + "closure": { + "includeCount": 20, + "fileCount": 21, + "bundleSha256": "2991d13fbf2d036c9bfb5cf4207c16509dfdf77dae24fa410ace1a0d3a36b626" + }, + "provenance": { + "license": "CC-BY-4.0", + "licenseFile": "authored/heresy2/LICENSE", + "licenseSha256": "7e7170e3cebf88a9f60c7b8421418323c09304da1af4d5e90f4da1dc1c8a2661", + "upstream": "randall-frank/heresy2-assets", + "commit": "37b8a7804217bb40a9f69f6fd9c173f2017d550e" + } + }, + { + "id": "intercept", + "story": "authored/the-intercept/TheIntercept.ink", + "entrypointSha256": "1d7b5653b689a9da6801bbfaaa3fbd20b0cc47113b28bb1b1774f6cd11e9eea4", + "compiledStorySha256": "2a441a97a345cfc7d3c944e758949d6c3b37ccfe88a75ddad06cf3ac83a637c6", + "closure": { + "includeCount": 0, + "fileCount": 1, + "bundleSha256": "c9865a7d2d334e39b68f788f790d202a7c1fbd9acb2bf2fc54fa1a4d549db6c9" + }, + "provenance": { + "license": "MIT", + "licenseFile": "authored/the-intercept/LICENSE-AND-PROVENANCE.md", + "licenseSha256": "4b0d7e6b2ccac7794d0705ee40a6728d664ff1cd80994b5d31077a4cde38f3da", + "upstream": "inkle/the-intercept", + "commit": "2a816b56e61ce4bf02bec1c638074645bdd871e3" + } + } + ], + "cells": [ + { + "id": "heresy2-legacy-split", + "sourceId": "heresy2", + "mode": "legacy-split", + "baseStates": 100000, + "targetStates": 150000, + "expected": "resume_exact" + }, + { + "id": "heresy2-framed-split", + "sourceId": "heresy2", + "mode": "framed-split", + "baseStates": 100000, + "targetStates": 150000, + "expected": "resume_exact" + }, + { + "id": "heresy2-uninterrupted", + "sourceId": "heresy2", + "mode": "uninterrupted", + "baseStates": 100000, + "targetStates": 150000, + "expected": "endpoint" + }, + { + "id": "intercept-uninterrupted", + "sourceId": "intercept", + "mode": "uninterrupted", + "baseStates": 600000, + "targetStates": 650000, + "expected": "endpoint" + }, + { + "id": "intercept-framed-split", + "sourceId": "intercept", + "mode": "framed-split", + "baseStates": 600000, + "targetStates": 650000, + "expected": "resume_exact" + }, + { + "id": "intercept-legacy-split", + "sourceId": "intercept", + "mode": "legacy-split", + "baseStates": 600000, + "targetStates": 650000, + "expected": "legacy_readback_resource_limit" + } + ] +} diff --git a/docs/checkpoint-v2-promotion-evaluation.md b/docs/checkpoint-v2-promotion-evaluation.md new file mode 100644 index 0000000..d6d8a66 --- /dev/null +++ b/docs/checkpoint-v2-promotion-evaluation.md @@ -0,0 +1,155 @@ +# Checkpoint-v2 promotion evaluation + +This evaluation is the checked, preregistered proof for the engine-native +framed-v2 checkpoint path. It asks two narrow questions under matched search +controls: + +1. Does a framed checkpoint written directly from the paused shared engine + resume to the byte-identical canonical result of an uninterrupted run? +2. At the previously observed *The Intercept* schema-v1 readback boundary, + does framed v2 reopen and resume while the legacy reader returns its typed, + non-destructive resource limit? + +It does not make framed v2 the default, change search allocation, claim story +coverage, or establish an InkBench or portable performance improvement. + +## Preregistered matrix + +`benchmarks/checkpoint-v2-promotion-v1.json` fixes six serial cells in mirrored +order: + +| Order | Cell | Base | Target | Expected observation | +| ---: | --- | ---: | ---: | --- | +| 1 | Heresy II legacy split | 100,000 | 150,000 | reopen and exact resume | +| 2 | Heresy II framed split | 100,000 | 150,000 | engine-native write, reopen, exact resume | +| 3 | Heresy II uninterrupted | — | 150,000 | matched endpoint | +| 4 | The Intercept uninterrupted | — | 650,000 | matched endpoint | +| 5 | The Intercept framed split | 600,000 | 650,000 | engine-native write, reopen, exact resume | +| 6 | The Intercept legacy split | 600,000 | 650,000 | typed schema-v1 readback resource limit | + +All cells use maximum depth 100, search seed 7, story seed 1, one shared-search +lane, no repro minimization, a 4 GiB search guard, a 14-minute graceful time +guard, and a 15-minute hard worker boundary. Turn and random state sensitivity +comes from `scanStorySemantics`. Forced-loop detection is enabled only when +the source uses no turns, randomness, visit counts, or externals. + +Every cell runs in a fresh temporary project and a fresh child process. On +Unix, the worker starts a process group. A hard boundary first terminates that +group, then retains a referenced escalation timer and sends `SIGKILL` to the +group even if its leader already exited; a resistant descendant therefore +cannot evade cleanup by outliving the leader. Windows force-terminates the +captured process tree while its leader still exists and repeats that tree +cleanup at the escalation boundary as a best effort. Cells execute serially so +one story's retained frontier cannot overlap another cell's resource +observation. + +The complete checkpoint pair is capped at 512 MiB, and checkpoint storage in +the isolated project is capped at 2 GiB. Framed v2 changes only its cumulative +decoded-record ceiling to 2 GiB; the ordinary per-frame, record, count, +checksum, and durable-byte limits remain in force. + +## Source and compiler pins + +The runner inventories each entrypoint with `inspectProject`, rejects a +truncated INCLUDE list, adds the entrypoint to that closure, and hashes sorted +project-relative path, NUL, raw bytes, NUL tuples. It validates the entrypoint, +complete closure, and license/provenance file before starting a worker. Each +worker repeats the closure check after its isolated copy and validates the +compiled story JSON after normalizing the compiler artifact to exactly one +final LF, matching the pinned InkBench corpus packaging. + +| Story | Upstream pin | Source files | Bundle SHA-256 | Compiled JSON SHA-256 | +| --- | --- | ---: | --- | --- | +| Heresy II | `randall-frank/heresy2-assets@37b8a7804217bb40a9f69f6fd9c173f2017d550e` | 21 | `2991d13fbf2d036c9bfb5cf4207c16509dfdf77dae24fa410ace1a0d3a36b626` | `5c65cf077478ad70985341e564695cb8ab35af20ec574d12b517d1e18f9a4bb7` | +| The Intercept | `inkle/the-intercept@2a816b56e61ce4bf02bec1c638074645bdd871e3` | 1 | `c9865a7d2d334e39b68f788f790d202a7c1fbd9acb2bf2fc54fa1a4d549db6c9` | `2a441a97a345cfc7d3c944e758949d6c3b37ccfe88a75ddad06cf3ac83a637c6` | + +The compiler contract is inklecate 1.2.1. The report records the resolved +executable's SHA-256 and whether it came from `INKLECATE_PATH`, Inkcheck's +managed cache, or `PATH`; it never emits the executable's local path. + +## Run + +Build and run the full matrix from a clean candidate checkout: + +```sh +npm run --silent evaluate-checkpoint-v2 -- \ + benchmarks/checkpoint-v2-promotion-v1.json \ + --output benchmarks/results/checkpoint-v2-promotion-v1.json +``` + +The package script runs a fresh TypeScript build immediately before starting +the coordinator with `--max-old-space-size=6144`. This is the supported proof +entrypoint: it binds the otherwise ignored `dist` executable to the candidate +source immediately before the runner fingerprints and executes it. Invoking +the compiled JavaScript directly is not a qualifying proof procedure. + +The report freezes the raw manifest hash, Git commit and tree, clean/dirty +count, package and lockfile hashes, compiled `dist` bundle hash, +Node/V8/platform/CPU/RAM facts, compiler fingerprint, selection, controls, +cases, progress, and verdict. With `--output`, the coordinator writes a private, +fsynced, atomically renamed `in_progress` snapshot after initialization, before +every cell, and after every cell. The pre-cell snapshot names the active cell; +the post-cell snapshot retains every recorded terminal case. A coordinator +crash or cancellation therefore leaves all completed evidence and the exact +unfinished boundary in the output file. In-progress snapshots are always +inconclusive with no allowed claims, including the narrow interval after the +sixth case is recorded but before the explicit `completed` snapshot. Omitting +`--output` emits only the final stdout report and does not provide this durable +interruption record. Progress on stderr contains only cell IDs and statuses. + +`--case ID` may be repeated for a diagnostic rerun. A filtered run always has +`completeMatrix: false` and an `inconclusive` verdict; it cannot become +promotion evidence by itself. + +Exit status 0 means every complete-matrix gate passed and the final snapshot is +`completed`. Status 1 means a +completed gate contradicted the contract. Status 2 means the matrix was +inconclusive, unavailable, filtered, or could not safely start. A timeout, +hard-killed worker, dirty candidate, missing 6 GiB coordinator flag, source or +compiler drift, or unexpected legacy-readback success is never converted into +a pass. + +## Exactness and resource-limit rules + +The framed base uses `exploreSharedResumableWithCheckpointSource` and writes +with `saveCheckpointArtifactFromSource`. Its receipt must say that zero full +checkpoint graphs were materialized, one identity traversal and one frame +traversal ran, and legacy whole-artifact serialization/compression did not run. +The legacy base deliberately omits `format`, proving that the public library +default remains legacy v1. + +Successful split cells reopen their artifact and resume with the same +source-scoped engine API to the declared target. The evaluator computes the +SHA-256 and UTF-8 byte count of the exact `JSON.stringify` token stream without +building a report-sized string. It requires: + +- matched legacy/framed base result digests; +- identical stable checkpoint IDs and logical checkpoint byte counts; +- identical loaded checkpoint digests where both formats can reopen; +- split/resumed target digests equal to the uninterrupted target digest; and +- exact state horizons with `maxStates` true and memory, time, frontier, beam, + loop, and worker stops false. `maxDepth` may also be true. + +The Intercept legacy cell passes its narrow boundary gate only when reopening +returns `CheckpointReadError` with kind `resource_limit`, stage +`decompression`, and unit `bytes`. The runner hashes the stored payload before +and after the failed read and compares the public listing before and after, but +does not publish the private payload hash. If legacy readback succeeds on a +different runtime, the observation is retained as `inconclusive`; it is not +silently redefined as a pass. + +## Privacy and interpretation + +The machine report contains numeric counts, booleans, stable IDs, public source +and candidate hashes, and canonical result/checkpoint digests. It excludes +absolute paths, story prose, choices, variables, witnesses, serialized runtime +state, private framed-component digests, and checkpoint payload hashes. +Checkpoint files stay inside their isolated temporary cell and are deleted +after that worker; do not publish or attach them. + +A passing report permits claims only about exact resume in these declared +cells, the observed zero-materialized-graph framed write receipt, the typed +legacy boundary, and non-destructive failure. It forbids universal or portable +performance claims, coverage or defect-absence claims, allocation-policy +promotion, a default-format change, and any InkBench improvement claim. Timing +and peak RSS are single-machine observations retained for audit, not promises. diff --git a/docs/local-checkpoints.md b/docs/local-checkpoints.md index 329f0e6..9808a37 100644 --- a/docs/local-checkpoints.md +++ b/docs/local-checkpoints.md @@ -50,9 +50,9 @@ Resource-limit errors add generic `observed`, `limit`, and `unit` (`bytes`, `cou The low-level `readCheckpointArtifactV2` / `writeCheckpointArtifactV2` codec API reports cancellation as `CheckpointArtifactV2Error` with `kind: "cancelled"` and can promptly interrupt a stalled source or record iterator mid-I/O. High-level `saveCheckpointArtifact`, `openCheckpointArtifact`, and `loadCheckpointForResume` normalize that condition to the ordinary `AbortError` contract. Only publication after the neutral no-clobber commit becomes non-cancellable so recovery can finish one unambiguous pair. -`listCheckpointArtifacts`, `openCheckpointArtifact`, and `loadCheckpointForResume` accept optional `maxStoredBytes` and `maxDecompressedBytes` read limits. Defaults cap stored input at 512 MiB and schema-v1 decompression at the smaller of 512 MiB and the runtime's maximum string length. Framed-v2 library reads also accept explicit per-header, frame, record, JSON-depth, count, total-stored, and total-decoded `framedV2Limits`; runtime Buffer/string ceilings and the enclosing stored/decompressed bounds remain authoritative. Listing uses those limits only for a sidecar-free compatibility fallback. +`listCheckpointArtifacts`, `openCheckpointArtifact`, and `loadCheckpointForResume` accept optional `maxStoredBytes` and `maxDecompressedBytes` read limits. Defaults cap stored input at 512 MiB and schema-v1 decompression at the smaller of 512 MiB and the runtime's maximum string length. Framed-v2 library reads also accept explicit per-header, frame, record, JSON-depth, count, total-stored, and total-decoded `framedV2Limits`. The enclosing `maxStoredBytes` bound remains authoritative for either layout. `maxDecompressedBytes` is the legacy schema-v1 whole-value/string ceiling; framed v2 instead applies its own `maxTotalDecodedBytes` cumulative ceiling plus per-frame, per-record, JSON-depth, and runtime Buffer/string bounds. Listing uses those limits only for a sidecar-free compatibility fallback. -Successful `openCheckpointArtifact` and `loadCheckpointForResume` calls also return an additive `CheckpointReadAccountingV1` receipt. Typed failures attach the partial receipt reached before failure as `CheckpointReadError.accounting`; constructing it does not retry the failed operation. The receipt records effective configured limits (which are bounds, not allocated capacity), the existing manifest buffer/string/parsed source graph and canonical checksum-validation string when present, the one stored payload buffer, the gzip output buffer or a legacy-JSON `not_applied` marker, the one raw artifact string, the parsed artifact source graph, and the exact logical checkpoint bytes, largest source chunk, and two configuration-comparison strings from the existing stable-ID/envelope validation. Plain legacy JSON and gzip therefore have distinct truthful owner shapes without changing their accepted bytes. +Successful `openCheckpointArtifact` and `loadCheckpointForResume` calls also return an additive `CheckpointReadAccountingV1` receipt. Typed failures attach the partial receipt reached before failure as `CheckpointReadError.accounting`; constructing it does not retry the failed operation. The receipt records effective configured limits (which are bounds, not allocated capacity), the existing manifest buffer/string/parsed source graph and canonical checksum-validation string when present, the one stored payload buffer, the gzip output buffer or a legacy-JSON `not_applied` marker, the one raw artifact string, the parsed artifact source graph, and the exact logical checkpoint bytes, largest source chunk, and two configuration-comparison strings from the existing stable-ID/envelope validation. Framed reads add `configuredLimits.framedV2` with the actual effective header, frame, record, depth, count, total-stored, and total-decoded codec bounds; the outer legacy `maxDecompressedBytes` field remains visible for receipt compatibility but is not presented as the framed cumulative decoded bound. Plain legacy JSON and gzip therefore retain their existing configured-limit shape and have distinct truthful owner shapes without changing their accepted bytes. `conservativePotential.totalBytes` sums the reported logical owners that were reached plus the largest validation chunk. It is deliberately not an observed simultaneous peak, V8 heap measurement, or claim about unreported runtime/library internals such as fixed race probes, hashing state, gzip/JSON-parser workspace, or source-freshness compilation. The parsed artifact already owns its nested checkpoint; `currentReturnedGraph` transfers that same graph rather than charging it twice. It is zero after metadata-only `openCheckpointArtifact` and one with the exact logical checkpoint bytes after a successful `loadCheckpointForResume`. For framed v2 the monolithic stored/decompressed/raw-string/parsed-artifact owners are `not_applied`; after a successful terminal-index/EOF validation, an additive `framedV2` section reports the fixed privacy-safe component/frame/index totals while the assembled returned graph remains explicit. A typed mid-stream v2 failure retains manifest and failure-stage accounting but currently omits `framedV2`, because the codec has not returned verified aggregate component totals; it does not present partial frames as a completed receipt. A stale or path-changed resume retains its existing plain source-binding error after the artifact read completed, so it is outside the typed read-failure receipt contract; callers that need accounting alongside stale/path-changed metadata can use `openCheckpointArtifact` first. @@ -62,9 +62,24 @@ Default checkpoints live under `.inkcheck/checkpoints/checkpoint-.json.gz` The default writer emits compact JSON through gzip directly into a private same-directory temporary file while computing the stored-byte digest in the same pass. It never constructs a second artifact-sized JSON string in memory, and it checks the final payload-plus-sidecar bytes before publication. Its reader bounds stored input and gzip output before parsing and does not create a second decompressed string. That schema-v1 storage layout is nevertheless memory-heavy: the compressed buffer, decompressed buffer, one JSON string, and parsed frontier graph can overlap until garbage collection. -The opt-in v2 writer instead emits ordered bounded records into independently compressed frames and a terminal index. Its reader incrementally validates and assembles those records without a whole artifact Buffer, whole decompressed Buffer, or whole raw JSON string. It still retains the reconstructed logical resume graph, and one frame's stored/decoded/record data plus runtime/parser workspace can overlap. Both layouts pass the same logical checkpoint schema, stable-ID, source, configuration, and freshness checks. See [shared checkpoint artifact v2](shared-checkpoint-artifact-v2.md) for the wire contract and deliberate limits. +The opt-in v2 writer instead emits ordered bounded records into independently compressed frames and a terminal index. Its reader incrementally validates and assembles those records without a whole artifact Buffer, whole decompressed Buffer, or whole raw JSON string. Resume structurally clones the validated flat graph without round-tripping it through another whole JSON string. It still retains the reconstructed logical resume graph, and that ownership clone plus one frame's stored/decoded/record data and runtime/parser workspace can overlap. Both layouts pass the same logical checkpoint schema, stable-ID, source, configuration, and freshness checks. See [shared checkpoint artifact v2](shared-checkpoint-artifact-v2.md) for the wire contract and deliberate limits. -`saveCheckpointArtifact` returns an additive schema-v1 `accounting` receipt outside the checkpoint object and stable ID. It reports the logical checkpoint graph's canonical UTF-8 bytes and largest source chunk from the existing ID traversal; whether legacy artifact serialization/compression was applied; and the durable payload, manifest, and pair bytes. Configured compression capacity is explicitly not observed heap. A framed write marks those legacy monolithic stages `not_applied` and adds fixed component/frame/index totals under `framedV2`. A fast same-ID reuse reports all encode work as `not_applied`, while a concurrent loser that encoded a candidate before finding the winner truthfully reports applied work with outcome `reused` in the layout it attempted. Neither path opens or decodes the payload an extra time merely to construct this receipt. +Library callers that own the live shared-search engine can also avoid constructing and recursively cloning the complete logical checkpoint before a framed write: + +```ts +const run = await exploreSharedResumableWithCheckpointSource( + storyJson, + knots, + externals, + options, + (source) => saveCheckpointArtifactFromSource(projectRoot, entrypoint, source), + priorCheckpoint, +); +``` + +This is an opt-in framed-v2 producer. The search engine is paused at the exact resumable boundary while the asynchronous callback runs. Its callback-scoped source is repeatable and read-only; each record yielded to the consumer is detached, and the source and any outstanding iterator become invalid as soon as the callback settles. The writer makes one streaming pass over the canonical logical schema-v1 bytes for the unchanged stable ID and, when it must encode a new candidate, one ordered framed-record pass. It never calls the complete-checkpoint materializer, builds the full checkpoint graph, or constructs a monolithic JSON string. Same-ID framed reuse validates and rehashes the canonical pair, then bounded-stream-decodes every record and compares its canonical value and position directly with the paused live source. This prevents a deliberately recomputed manifest/payload checksum pair from standing in for logical stable-ID validation without constructing the graph. Legacy same-ID reuse, whether manifested or sidecar-free, remains available through the graph API but is explicitly unsupported through the engine-native route. Source-native retention likewise requires manifests for every existing artifact and fails before publication when it encounters a sidecar-free legacy artifact, rather than inflating it behind a zero-graph receipt or omitting it from quota accounting. The existing synchronous resumable API, graph-based save API, and default legacy-v1 CLI/MCP writes are unchanged. + +`saveCheckpointArtifact` returns an additive schema-v1 `accounting` receipt outside the checkpoint object and stable ID. It reports the logical checkpoint graph's canonical UTF-8 bytes and largest source chunk from the existing ID traversal; whether legacy artifact serialization/compression was applied; and the durable payload, manifest, and pair bytes. Configured compression capacity is explicitly not observed heap. A framed write marks those legacy monolithic stages `not_applied` and adds fixed component/frame/index totals under `framedV2`. A source-native write additionally reports `checkpointGraph.count: 0` and an `engineSource` receipt with zero materialized checkpoint graphs, one identity pass, `framePasses` for candidate encoding, and `reuseVerificationPasses` for the bounded live-source comparison. Each latter counter is zero or one: a created candidate reports `1/0`, an observed-before-work reuse reports `0/1`, and a no-clobber loser that encoded before verifying its winner reports `1/1`. A same-ID framed reuse reports serialization/compression encode work as `not_applied`, while a concurrent loser truthfully reports its applied candidate work with outcome `reused`. Receipt construction itself does not add another pass beyond the integrity work required by that operation. Cross-format requests reuse committed same-ID bytes when they observe them before candidate work, when format preflight fails without emitting candidate bytes, or after a candidate encoded successfully but loses no-clobber publication. If a private candidate emits bytes and then fails, that request preserves its own error instead of returning an untruthful reuse receipt; a later retry can reuse the durable winner. diff --git a/docs/shared-checkpoint-artifact-v2.md b/docs/shared-checkpoint-artifact-v2.md index 0f8316a..7be771d 100644 --- a/docs/shared-checkpoint-artifact-v2.md +++ b/docs/shared-checkpoint-artifact-v2.md @@ -15,6 +15,41 @@ await saveCheckpointArtifact(projectRoot, entrypoint, checkpoint, { }); ``` +Or they can keep the shared engine paused at its exact checkpoint boundary and +stream the same logical checkpoint directly into the codec: + +```ts +await exploreSharedResumableWithCheckpointSource( + storyJson, + knots, + externals, + options, + (source) => saveCheckpointArtifactFromSource(projectRoot, entrypoint, source), + priorCheckpoint, +); +``` + +The callback-scoped source is repeatable only while that callback is active. +It yields detached records, is invalidated in `finally`, and keeps the engine +quiescent across asynchronous backpressure. Plain callback results invalidate +the source synchronously on return; promise/thenable results keep it live only +until their single adopted settlement. The engine snapshots mutable knot, +external, and option inputs before work, and evidence callbacks receive +detached findings, so caller mutation cannot split the identity and frame +passes. The producer performs one +canonical logical-identity pass and, only when candidate encoding is needed, +one framed-record pass. It does not materialize or recursively clone the full +`SharedSearchCheckpoint` graph. The graph and source producers retain the same +logical bytes, stable ID, component order, framed payload, and resume contract. +Manifested same-ID reuse revalidates the bounded sidecar and complete payload +digest without decoding a graph. For framed-v2 reuse it also bounded-stream- +decodes each record and compares the canonical value and position with the live +source, so a self-consistent rewrite cannot bypass logical stable-ID validation. +Legacy artifacts, manifested or sidecar-free, must first be opened/migrated +through the graph API; source-native reuse rejects them rather than hiding a +materialized graph behind its receipt. Source-native retention similarly fails +before publication if any retained legacy artifact lacks its manifest. + The resulting payload is `.inkcheck/checkpoints/checkpoint-.inkcp`, with a schema-v2 canonical `.meta.json` sidecar. Readers dispatch from that verified sidecar and continue to accept manifested and legacy sidecar-free schema-v1 @@ -121,14 +156,19 @@ limitations in [local resumable checkpoints](local-checkpoints.md). ## Deliberate scope -This is a codec foundation, not format promotion. It does not make framed v2 the -default, add an engine-native streaming checkpoint producer, change allocation, -compaction, eviction, stopping, or epoch policy, publish a release, or establish -an InkBench improvement claim. Promotion still requires exact split-versus- -uninterrupted resume evidence, the observed Intercept schema-v1 readback-limit -cell under identical ceilings, and a second public story family such as Heresy -II, together with the adversarial truncation, checksum, bounds, cancellation, -crash, and mixed-layout gates. +This remains an opt-in foundation, not format promotion. The engine-native +producer removes the write-side full-checkpoint materialization, but the framed +reader still reconstructs a private complete schema-v1 graph before handing it +to the existing resume engine. That handoff uses a structural ownership clone, +not a whole-checkpoint JSON string, but both graphs may overlap until the caller +releases its loaded value. This work does not make framed v2 the default, +stream provisional read callbacks directly into a live engine, change +allocation, compaction, eviction, stopping, or epoch policy, publish a release, +or establish an InkBench improvement claim. Promotion still requires exact +split-versus-uninterrupted resume evidence, the observed Intercept schema-v1 +readback-limit cell under identical ceilings, and a second public story family +such as Heresy II, together with the adversarial truncation, checksum, bounds, +cancellation, crash, and mixed-layout gates. Checkpoint records can contain authored text, variables, serialized runtime state, findings, and witness paths. Treat `.inkcp` files as sensitive project diff --git a/docs/shared-checkpoint-schema-v1.md b/docs/shared-checkpoint-schema-v1.md index 8089d30..c872b9c 100644 --- a/docs/shared-checkpoint-schema-v1.md +++ b/docs/shared-checkpoint-schema-v1.md @@ -50,15 +50,15 @@ inkcheck resume checkpoint-0123456789abcdef01234567 --max-states 1000000 --json See [local resumable checkpoints](local-checkpoints.md) for freshness, privacy, atomic-write, quota, and retention behavior. -The logical checkpoint remains schema v1. The default local artifact still streams that one JSON value through gzip as `.json.gz`, while library callers may explicitly choose the framed-v2 `.inkcp` storage codec. Both layouts reconstruct the same property-ordered logical checkpoint before the unchanged stable-ID check; readers continue to accept earlier plain `.json` artifacts. Compression or framing therefore changes neither the checkpoint ID, deterministic frontier order, nor split-run equivalence. A verified artifact in either layout wins same-ID reuse, and new writers never publish both layouts for one ID. +The logical checkpoint remains schema v1. The default local artifact still streams that one JSON value through gzip as `.json.gz`, while library callers may explicitly choose the framed-v2 `.inkcp` storage codec. They may also stream a callback-scoped, repeatable view of the paused engine directly into that codec, avoiding construction and recursive cloning of the complete checkpoint graph while retaining the same canonical logical bytes. Both layouts reconstruct the same property-ordered logical checkpoint before the unchanged stable-ID check; readers continue to accept earlier plain `.json` artifacts. Compression, framing, or the producer path therefore changes neither the checkpoint ID, deterministic frontier order, nor split-run equivalence. Graph-based writers reuse a verified artifact in either layout, while the graph-free source route reuses only framed-v2 after bounded record-by-record comparison with the live source; legacy reuse/migration remains on the graph path. New writers never publish both layouts for one ID. New artifacts have a small canonical self-checksummed metadata sidecar, allowing list and retention operations to read bounded metadata plus payload file size without opening the frontier. A framed-v2 sidecar additionally binds the exact private component/frame/index summary; its public projection contains only fixed component names and numeric counts/bytes. Those checksums detect isolated field corruption but are not authentication against a hostile local writer who can recompute them. Opening and resuming remain full-integrity boundaries: manifest validation and the full stored-payload digest are the external stored-file corruption boundary, framed records or the legacy one-value payload are decoded under explicit bounds, the nested observability `integritySha256` is checked as part of logical checkpoint validation, and the original stable-ID, envelope, configuration, and freshness checks follow. Read failures explicitly distinguish corruption, unsupported schemas, and an artifact that cannot fit its configured readback envelope. -The library reopen APIs return a separate schema-v1 read-accounting receipt outside this envelope; typed `CheckpointReadError` failures carry the partial receipt reached before failure. For legacy artifacts it distinguishes the manifest and its checksum-validation string, stored payload, gzip output versus plain JSON, raw string, parsed source graph, existing stable-ID traversal and configuration-comparison strings, effective limits, conservative potential, and checkpoint graph retained on return. Framed v2 marks the monolithic payload/string/parsed-artifact owners `not_applied` and adds fixed component/frame/index totals. Metadata-only open retains no checkpoint graph; successful resume transfers one. Conservative potential is not an observed peak or full V8/runtime heap. These counters change neither bytes, stable identity, validation order, source-freshness behavior, nor split-run trajectory. +The library reopen APIs return a separate schema-v1 read-accounting receipt outside this envelope; typed `CheckpointReadError` failures carry the partial receipt reached before failure. For legacy artifacts it distinguishes the manifest and its checksum-validation string, stored payload, gzip output versus plain JSON, raw string, parsed source graph, existing stable-ID traversal and configuration-comparison strings, effective limits, conservative potential, and checkpoint graph retained on return. Framed v2 marks the monolithic payload/string/parsed-artifact owners `not_applied`, adds fixed component/frame/index totals, and exposes the actual effective codec limits separately from the retained legacy whole-string limit field. Metadata-only open retains no checkpoint graph; successful resume transfers one. Conservative potential is not an observed peak or full V8/runtime heap. These counters change neither bytes, stable identity, validation order, source-freshness behavior, nor split-run trajectory. ## Deliberate limits -Schema v1 supports only base `shared:deep-novelty-v1`; assertions, goals, variable-aware steering, goal-aware steering, and the default portfolio are rejected rather than resumed approximately. Hosted-job resume and frontier partitioning remain future work; MCP continuation is available through cooperative result-window sessions. The default gzip layout is still one JSON value and cannot safely reopen a payload above the runtime's maximum string length even when gzip keeps the stored file below quota. Its compressed buffer, decompressed buffer, JSON string, and parsed graph may overlap in memory. The reader reports an unsafe boundary as a resource limit and preserves the artifact. The opt-in framed-v2 codec removes those whole-value readback materializations by delivering bounded records into the same logical schema-v1 resume graph; that graph and bounded frame/runtime workspace still consume memory. +Schema v1 supports only base `shared:deep-novelty-v1`; assertions, goals, variable-aware steering, goal-aware steering, and the default portfolio are rejected rather than resumed approximately. Hosted-job resume and frontier partitioning remain future work; MCP continuation is available through cooperative result-window sessions. The default gzip layout is still one JSON value and cannot safely reopen a payload above the runtime's maximum string length even when gzip keeps the stored file below quota. Its compressed buffer, decompressed buffer, JSON string, and parsed graph may overlap in memory. The reader reports an unsafe boundary as a resource limit and preserves the artifact. The opt-in framed-v2 codec removes those whole-value readback materializations by delivering bounded records into the same logical schema-v1 resume graph. Engine restore structurally clones that validated flat graph rather than serializing it into another whole JSON string; the source and ownership clone plus bounded frame/runtime workspace can still overlap in memory. The observability reason vocabulary reserves `checkpoint`, `epoch`, and `pressure`, but this slice emits none of those boundaries. The optional artifact-v2 storage encoding adds no resource policy, owner ceiling, epoch behavior, default-format promotion, or claim that issue #156 or #216 is complete. diff --git a/package.json b/package.json index bb11fa9..a2a408f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "evaluate-promotion": "node dist/promotion-benchmark-cli.js", "evaluate-campaign-children": "node dist/campaign-child-evaluation-cli.js", "evaluate-long-tail": "node dist/long-tail-evaluation-cli.js", + "evaluate-checkpoint-v2": "npm run --silent build && node --max-old-space-size=6144 dist/checkpoint-v2-evaluation-cli.js", "evaluate-agent-readiness": "node dist/agent-readiness-benchmark-cli.js", "evaluate-loop-specialist": "node scripts/loop-specialist-evaluation.js", "evaluate-gate-probe": "node scripts/gate-probe-evaluation.js", @@ -55,6 +56,7 @@ "benchmarks/results/concurrency-activation-handoff-v3.json", "benchmarks/long-tail-partition-evaluation-v1.json", "benchmarks/long-tail-promotion-v2.json", + "benchmarks/checkpoint-v2-promotion-v1.json", "benchmarks/results/long-tail-partition-intercept-5m-v1.json", "benchmarks/results/long-tail-partition-synthetic-control-v1.json", "benchmarks/results/long-tail-promotion-v2.json", @@ -81,6 +83,7 @@ "docs/campaign-policy-contract.md", "docs/concurrency-evaluation.md", "docs/long-tail-partition-evaluation.md", + "docs/checkpoint-v2-promotion-evaluation.md", "docs/product-engineering-scorecard.md", "docs/rules-that-matter-0.7.md", "docs/loop-specialist-evaluation.md", diff --git a/src/checkpoint-artifact-v2.ts b/src/checkpoint-artifact-v2.ts index 4abed02..ad9a9bf 100644 --- a/src/checkpoint-artifact-v2.ts +++ b/src/checkpoint-artifact-v2.ts @@ -288,7 +288,9 @@ const CHECKPOINT_ARTIFACT_V2_LIMIT_METRICS: Readonly< maxTotalDecodedBytes: "bytes", }); -function effectiveLimits(input: Partial | undefined): CheckpointArtifactV2Limits { +export function resolveCheckpointArtifactV2Limits( + input: Partial | undefined +): CheckpointArtifactV2Limits { const limits = { ...DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS, ...input }; for (const [name, value] of Object.entries(limits)) { const metric = CHECKPOINT_ARTIFACT_V2_LIMIT_METRICS[name as keyof CheckpointArtifactV2Limits]; @@ -298,7 +300,7 @@ function effectiveLimits(input: Partial | undefined) "resource_limit", "header", `${name} must be a positive 32-bit safe integer`, - value, + typeof value === "number" && Number.isFinite(value) ? value : undefined, 0xffffffff, metric ); @@ -610,7 +612,7 @@ export async function writeCheckpointArtifactV2( let limits: CheckpointArtifactV2Limits; let gzipLevel: number; try { - limits = effectiveLimits(options.limits); + limits = resolveCheckpointArtifactV2Limits(options.limits); gzipLevel = options.gzipLevel ?? 1; if (!Number.isInteger(gzipLevel) || gzipLevel < 0 || gzipLevel > 9) { throw error("unsupported", "payload", "gzipLevel must be an integer from 0 through 9"); @@ -1179,7 +1181,7 @@ export async function readCheckpointArtifactV2( ): Promise { const reader = new BoundedReadable(source, options.signal); try { - const limits = effectiveLimits(options.limits); + const limits = resolveCheckpointArtifactV2Limits(options.limits); const frames: CheckpointArtifactV2FrameSummary[] = []; const components = emptyComponents(); const expectedStarts = new Map(); diff --git a/src/checkpoint-v2-evaluation-cli.ts b/src/checkpoint-v2-evaluation-cli.ts new file mode 100644 index 0000000..6ac5488 --- /dev/null +++ b/src/checkpoint-v2-evaluation-cli.ts @@ -0,0 +1,1660 @@ +#!/usr/bin/env node +import { spawn, spawnSync } from "child_process"; +import { createHash, randomUUID } from "crypto"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as v8 from "v8"; +import { + CheckpointReadError, + listCheckpointArtifacts, + loadCheckpointForResume, + saveCheckpointArtifact, + saveCheckpointArtifactFromSource, + type CheckpointArtifactReference, +} from "./checkpoints"; +import { inspectProject } from "./discovery"; +import { + exploreShared, + exploreSharedResumable, + exploreSharedResumableWithCheckpointSource, + type ExploreOptions, + type ExploreResult, + type SharedSearchCheckpoint, +} from "./explore"; +import { + compile, + resolveInklecate, + scanExternals, + scanKnots, + scanStorySemantics, +} from "./inklecate"; +import { createResourceGuards } from "./resource-guards"; +import { VERSION } from "./version"; + +const EVALUATION_KIND = "checkpoint_v2_promotion_evaluation"; +const EVALUATION_ID = "checkpoint-v2-promotion-v1"; +const INKLECATE_VERSION = "1.2.1"; +const MAX_MANIFEST_BYTES = 1024 * 1024; +const MAX_WORKER_OUTPUT_BYTES = 4 * 1024 * 1024; +const MAX_WORKER_DIAGNOSTIC_BYTES = 64 * 1024; +const WORKER_KILL_GRACE_MS = 2_000; + +const EXPECTED_CELL_CONTRACT = [ + ["heresy2-legacy-split", "heresy2", "legacy-split", 100_000, 150_000, "resume_exact"], + ["heresy2-framed-split", "heresy2", "framed-split", 100_000, 150_000, "resume_exact"], + ["heresy2-uninterrupted", "heresy2", "uninterrupted", 100_000, 150_000, "endpoint"], + ["intercept-uninterrupted", "intercept", "uninterrupted", 600_000, 650_000, "endpoint"], + ["intercept-framed-split", "intercept", "framed-split", 600_000, 650_000, "resume_exact"], + ["intercept-legacy-split", "intercept", "legacy-split", 600_000, 650_000, "legacy_readback_resource_limit"], +] as const; + +type CellMode = "legacy-split" | "framed-split" | "uninterrupted"; +type CellExpectation = "resume_exact" | "endpoint" | "legacy_readback_resource_limit"; + +export interface CheckpointV2EvaluationSource { + id: string; + story: string; + entrypointSha256: string; + compiledStorySha256: string; + closure: { + includeCount: number; + fileCount: number; + bundleSha256: string; + }; + provenance: { + license: string; + licenseFile: string; + licenseSha256: string; + upstream: string; + commit: string; + }; +} + +export interface CheckpointV2EvaluationCell { + id: string; + sourceId: string; + mode: CellMode; + baseStates: number; + targetStates: number; + expected: CellExpectation; +} + +export interface CheckpointV2EvaluationControls { + cellOrder: string[]; + maxDepth: number; + searchSeed: number; + storySeed: number; + concurrency: number; + minimizeRepros: boolean; + stateSensitivity: "source_semantics"; + loopRiskDetection: "only_without_turns_randomness_visit_counts_or_externals"; + maxMemoryMb: number; + maxTimeMs: number; + workerTimeoutMs: number; + coordinatorMaxOldSpaceMb: number; + storage: { + maxCheckpointBytes: number; + maxProjectBytes: number; + framedV2: { maxTotalDecodedBytes: number }; + }; +} + +export interface CheckpointV2EvaluationManifest { + schemaVersion: 1; + kind: typeof EVALUATION_KIND; + id: typeof EVALUATION_ID; + compiler: { name: "inklecate"; version: "1.2.1" }; + controls: CheckpointV2EvaluationControls; + sources: CheckpointV2EvaluationSource[]; + cells: CheckpointV2EvaluationCell[]; +} + +interface Digest { + sha256: string; + utf8Bytes: number; +} + +interface CompilerFingerprint { + name: "inklecate"; + version: "1.2.1"; + executableSha256: string; + resolutionClass: "environment_override" | "managed_cache" | "path"; +} + +interface ResultObservation { + digest: Digest; + statesExplored: number; + exhaustive: boolean; + truncated: boolean; + truncatedBy: { + maxDepth: boolean; + maxStates: boolean; + beamWidth: boolean; + frontier: boolean; + memory: boolean; + time: boolean; + loop: boolean; + worker: boolean; + }; + counts: { + endings: number; + visibleOutcomes: number; + runtimeErrors: number; + runtimeWarnings: number; + visitedKnots: number; + unvisitedKnots: number; + }; +} + +interface CheckpointObservation { + requestedFormat: "legacy-v1" | "framed-v2"; + storageEncoding: "gzip" | "framed-v2"; + id: string; + logicalCheckpointUtf8Bytes: number; + payloadSizeBytes: number; + durableSizeBytes: number; + write: { + outcome: "created" | "reused"; + materializedCheckpointGraphs: number; + engineSourceMaterializedGraphs: number | null; + engineSourceIdentityPasses: number | null; + engineSourceFramePasses: number | null; + legacySerializationApplied: boolean; + legacyCompressionApplied: boolean; + framedV2Applied: boolean; + }; + logicalCheckpoint?: Digest; + readback: { + status: "completed" | "resource_limit"; + storageEncoding?: "gzip" | "framed-v2"; + kind?: "resource_limit"; + stage?: "decompression"; + unit?: "bytes"; + observed?: number; + limit?: number; + payloadVerified?: boolean; + }; + preservation: { + payloadBytesUnchanged: boolean; + listingUnchanged: boolean; + listedBefore: number; + listedAfter: number; + }; +} + +export interface CompletedCheckpointV2EvaluationCase { + schemaVersion: 1; + id: string; + sourceId: string; + mode: CellMode; + expected: CellExpectation; + status: "completed" | "inconclusive"; + reason?: "legacy_readback_succeeded"; + configuration: { + baseStates: number; + targetStates: number; + maxDepth: number; + searchSeed: number; + storySeed: number; + concurrency: 1; + preserveTurnState: boolean; + preserveRandomState: boolean; + detectLoopRisks: boolean; + }; + compiler: CompilerFingerprint; + source: { + fileCount: number; + bundleSha256: string; + compiledStorySha256: string; + }; + elapsedMs: number; + peakRssBytes: number; + base?: ResultObservation; + checkpoint?: CheckpointObservation; + resumed?: ResultObservation; + uninterrupted?: ResultObservation; +} + +interface FailedEvaluationCase { + schemaVersion: 1; + id: string; + sourceId: string; + mode: CellMode; + expected: CellExpectation; + status: "failed" | "unavailable"; + reason: string; + timeoutMs?: number; + cleanupScope?: ProcessCleanupScope; +} + +type EvaluationCaseResult = CompletedCheckpointV2EvaluationCase | FailedEvaluationCase; + +interface ValidatedSource { + entrypoint: string; + root: string; + relativeFiles: string[]; + entrypointRelative: string; + bundleSha256: string; +} + +interface WorkerRequest { + schemaVersion: 1; + manifestRoot: string; + cellRoot: string; + source: CheckpointV2EvaluationSource; + cell: CheckpointV2EvaluationCell; + controls: CheckpointV2EvaluationControls; +} + +interface WorkerError { + schemaVersion: 1; + kind: "checkpoint_v2_evaluation_worker_error"; + code: string; +} + +type ProcessCleanupScope = "process_group" | "process_tree" | "exact_process"; + +export interface BoundedProcessResult { + status: "completed" | "timeout" | "output_limit" | "spawn_error"; + exitCode: number | null; + signal: NodeJS.Signals | null; + stdout: string; + stderr: string; + cleanupScope?: ProcessCleanupScope; +} + +export interface EvaluationProgressSnapshot { + status: "in_progress" | "completed"; + selectedCellCount: number; + recordedCellCount: number; + recordedCellIds: string[]; + activeCellId?: string; +} + +class EvaluationProtocolError extends Error { + constructor(readonly code: string) { + super(code); + this.name = "EvaluationProtocolError"; + } +} + +function protocol(code: string): never { + throw new EvaluationProtocolError(code); +} + +function isObject(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function exactKeys(value: Record, keys: readonly string[], code: string): void { + const actual = Object.keys(value).sort(); + const expected = [...keys].sort(); + if (JSON.stringify(actual) !== JSON.stringify(expected)) protocol(code); +} + +function integer(value: unknown, min: number, max: number, code: string): asserts value is number { + if (!Number.isSafeInteger(value) || (value as number) < min || (value as number) > max) protocol(code); +} + +function sha256(value: string | Buffer): string { + return createHash("sha256").update(value).digest("hex"); +} + +/** Match the checked InkBench corpus packaging: compiler JSON plus one final LF. */ +function compiledStorySha256(value: string): string { + const withoutFinalNewline = value.endsWith("\r\n") + ? value.slice(0, -2) + : value.endsWith("\n") ? value.slice(0, -1) : value; + return sha256(`${withoutFinalNewline}\n`); +} + +function regularFile(file: string, code: string): void { + let stat: fs.Stats; + try { + stat = fs.lstatSync(file); + } catch { + protocol(code); + } + if (!stat.isFile() || stat.isSymbolicLink()) protocol(code); +} + +function safeFile(root: string, relative: string, code: string): string { + if (!relative || path.isAbsolute(relative) || relative.includes("\0")) protocol(code); + const resolvedRoot = path.resolve(root); + const resolved = path.resolve(resolvedRoot, relative); + const relation = path.relative(resolvedRoot, resolved); + if (relation === "" || relation === ".." || relation.startsWith(`..${path.sep}`) || path.isAbsolute(relation)) { + protocol(code); + } + regularFile(resolved, code); + let realRoot: string; + let realResolved: string; + try { + realRoot = fs.realpathSync(resolvedRoot); + realResolved = fs.realpathSync(resolved); + } catch { + protocol(code); + } + const realRelation = path.relative(realRoot, realResolved); + if (realRelation === "" || realRelation === ".." || realRelation.startsWith(`..${path.sep}`) + || path.isAbsolute(realRelation)) { + protocol(code); + } + return resolved; +} + +function normalizeRelative(value: string): string { + return value.split(path.sep).join("/"); +} + +function *jsonChunks(value: unknown, ancestors = new Set()): Generator { + if (value === null) { + yield "null"; + return; + } + if (typeof value === "string") { + yield JSON.stringify(value); + return; + } + if (typeof value === "number") { + yield Number.isFinite(value) ? String(value) : "null"; + return; + } + if (typeof value === "boolean") { + yield value ? "true" : "false"; + return; + } + if (typeof value !== "object") protocol("non_json_value"); + if (ancestors.has(value)) protocol("circular_json_value"); + ancestors.add(value); + try { + if (Array.isArray(value)) { + yield "["; + for (let index = 0; index < value.length; index++) { + if (index > 0) yield ","; + const item = value[index]; + if (item === undefined || typeof item === "function" || typeof item === "symbol") yield "null"; + else yield *jsonChunks(item, ancestors); + } + yield "]"; + return; + } + yield "{"; + let first = true; + for (const key of Object.keys(value)) { + const item = (value as Record)[key]; + if (item === undefined || typeof item === "function" || typeof item === "symbol") continue; + if (!first) yield ","; + first = false; + yield JSON.stringify(key); + yield ":"; + yield *jsonChunks(item, ancestors); + } + yield "}"; + } finally { + ancestors.delete(value); + } +} + +export function digestJson(value: unknown): Digest { + const hash = createHash("sha256"); + let utf8Bytes = 0; + for (const chunk of jsonChunks(value)) { + hash.update(chunk); + utf8Bytes += Buffer.byteLength(chunk); + if (!Number.isSafeInteger(utf8Bytes)) protocol("json_digest_size_overflow"); + } + return { sha256: hash.digest("hex"), utf8Bytes }; +} + +async function digestFile(file: string): Promise<{ sha256: string; bytes: number }> { + regularFile(file, "digest_file_invalid"); + const hash = createHash("sha256"); + let bytes = 0; + await new Promise((resolve, reject) => { + const input = fs.createReadStream(file); + input.on("data", (chunk: Buffer | string) => { + const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk; + bytes += buffer.length; + if (!Number.isSafeInteger(bytes)) { + input.destroy(new RangeError("file size exceeds safe integer range")); + return; + } + hash.update(buffer); + }); + input.on("error", reject); + input.on("end", resolve); + }); + return { sha256: hash.digest("hex"), bytes }; +} + +function sourceClosure(entrypoint: string): ValidatedSource { + let inspection: ReturnType; + try { + inspection = inspectProject(entrypoint); + } catch { + protocol("source_inspection_failed"); + } + if (inspection.truncation.includes) protocol("source_include_closure_truncated"); + const root = path.dirname(path.resolve(entrypoint)); + const entrypointRelative = normalizeRelative(path.relative(root, path.resolve(entrypoint))); + const relativeFiles = [...new Set([entrypointRelative, ...inspection.includes.map(normalizeRelative)])].sort(); + const hash = createHash("sha256"); + for (const relative of relativeFiles) { + const file = safeFile(root, relative, "source_closure_file_invalid"); + hash.update(relative); + hash.update("\0"); + hash.update(fs.readFileSync(file)); + hash.update("\0"); + } + return { + entrypoint: path.resolve(entrypoint), + root, + relativeFiles, + entrypointRelative, + bundleSha256: hash.digest("hex"), + }; +} + +export async function validateEvaluationSource( + manifestRoot: string, + source: CheckpointV2EvaluationSource +): Promise { + const entrypoint = safeFile(manifestRoot, source.story, "source_entrypoint_invalid"); + const entrypointDigest = await digestFile(entrypoint); + if (entrypointDigest.sha256 !== source.entrypointSha256) protocol("source_entrypoint_sha256_drift"); + const license = safeFile(manifestRoot, source.provenance.licenseFile, "source_license_invalid"); + if ((await digestFile(license)).sha256 !== source.provenance.licenseSha256) { + protocol("source_license_sha256_drift"); + } + const closure = sourceClosure(entrypoint); + if (closure.relativeFiles.length !== source.closure.fileCount) protocol("source_closure_file_count_drift"); + if (closure.relativeFiles.length - 1 !== source.closure.includeCount) protocol("source_include_count_drift"); + if (closure.bundleSha256 !== source.closure.bundleSha256) protocol("source_bundle_sha256_drift"); + return closure; +} + +export function validateCheckpointV2EvaluationManifest( + value: unknown +): asserts value is CheckpointV2EvaluationManifest { + if (!isObject(value)) protocol("manifest_not_object"); + exactKeys(value, ["schemaVersion", "kind", "id", "compiler", "controls", "sources", "cells"], "manifest_keys"); + if (value.schemaVersion !== 1 || value.kind !== EVALUATION_KIND || value.id !== EVALUATION_ID) { + protocol("manifest_identity"); + } + if (!isObject(value.compiler)) protocol("manifest_compiler"); + exactKeys(value.compiler, ["name", "version"], "manifest_compiler_keys"); + if (value.compiler.name !== "inklecate" || value.compiler.version !== INKLECATE_VERSION) { + protocol("manifest_compiler_pin"); + } + if (!isObject(value.controls)) protocol("manifest_controls"); + const controls = value.controls; + exactKeys(controls, [ + "cellOrder", "maxDepth", "searchSeed", "storySeed", "concurrency", "minimizeRepros", + "stateSensitivity", "loopRiskDetection", "maxMemoryMb", "maxTimeMs", "workerTimeoutMs", + "coordinatorMaxOldSpaceMb", "storage", + ], "manifest_control_keys"); + const expectedOrder = EXPECTED_CELL_CONTRACT.map(([id]) => id); + if (!Array.isArray(controls.cellOrder) || JSON.stringify(controls.cellOrder) !== JSON.stringify(expectedOrder)) { + protocol("manifest_cell_order"); + } + const exactControls: Array<[string, unknown]> = [ + ["maxDepth", 100], ["searchSeed", 7], ["storySeed", 1], ["concurrency", 1], + ["minimizeRepros", false], ["stateSensitivity", "source_semantics"], + ["loopRiskDetection", "only_without_turns_randomness_visit_counts_or_externals"], + ["maxMemoryMb", 4096], ["maxTimeMs", 840_000], ["workerTimeoutMs", 900_000], + ["coordinatorMaxOldSpaceMb", 6144], + ]; + for (const [key, expected] of exactControls) if (controls[key] !== expected) protocol(`manifest_control_${key}`); + if (!isObject(controls.storage)) protocol("manifest_storage"); + exactKeys(controls.storage, ["maxCheckpointBytes", "maxProjectBytes", "framedV2"], "manifest_storage_keys"); + if (controls.storage.maxCheckpointBytes !== 536_870_912 || controls.storage.maxProjectBytes !== 2_147_483_648) { + protocol("manifest_storage_limits"); + } + if (!isObject(controls.storage.framedV2)) protocol("manifest_framed_limits"); + exactKeys(controls.storage.framedV2, ["maxTotalDecodedBytes"], "manifest_framed_limit_keys"); + if (controls.storage.framedV2.maxTotalDecodedBytes !== 2_147_483_648) protocol("manifest_framed_limit"); + + if (!Array.isArray(value.sources) || value.sources.length !== 2) protocol("manifest_sources"); + const sourceIds = new Set(); + for (const raw of value.sources) { + if (!isObject(raw)) protocol("manifest_source_object"); + exactKeys(raw, ["id", "story", "entrypointSha256", "compiledStorySha256", "closure", "provenance"], "manifest_source_keys"); + if (typeof raw.id !== "string" || !["heresy2", "intercept"].includes(raw.id) || sourceIds.has(raw.id)) { + protocol("manifest_source_id"); + } + sourceIds.add(raw.id); + if (typeof raw.story !== "string" || !raw.story.endsWith(".ink")) protocol("manifest_source_story"); + for (const key of ["entrypointSha256", "compiledStorySha256"] as const) { + if (typeof raw[key] !== "string" || !/^[0-9a-f]{64}$/.test(raw[key] as string)) protocol("manifest_source_sha256"); + } + if (!isObject(raw.closure)) protocol("manifest_source_closure"); + exactKeys(raw.closure, ["includeCount", "fileCount", "bundleSha256"], "manifest_source_closure_keys"); + integer(raw.closure.includeCount, 0, 10_000, "manifest_source_include_count"); + integer(raw.closure.fileCount, 1, 10_001, "manifest_source_file_count"); + if (raw.closure.fileCount !== raw.closure.includeCount + 1 + || typeof raw.closure.bundleSha256 !== "string" || !/^[0-9a-f]{64}$/.test(raw.closure.bundleSha256)) { + protocol("manifest_source_closure_values"); + } + if (!isObject(raw.provenance)) protocol("manifest_source_provenance"); + exactKeys(raw.provenance, ["license", "licenseFile", "licenseSha256", "upstream", "commit"], "manifest_source_provenance_keys"); + if (typeof raw.provenance.license !== "string" || typeof raw.provenance.licenseFile !== "string" + || typeof raw.provenance.upstream !== "string" || !/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(raw.provenance.upstream) + || typeof raw.provenance.commit !== "string" || !/^[0-9a-f]{40}$/.test(raw.provenance.commit) + || typeof raw.provenance.licenseSha256 !== "string" || !/^[0-9a-f]{64}$/.test(raw.provenance.licenseSha256)) { + protocol("manifest_source_provenance_values"); + } + } + if (JSON.stringify(value.sources.map((source) => source.id)) !== JSON.stringify(["heresy2", "intercept"])) { + protocol("manifest_source_order"); + } + + if (!Array.isArray(value.cells) || value.cells.length !== EXPECTED_CELL_CONTRACT.length) protocol("manifest_cells"); + value.cells.forEach((raw, index) => { + if (!isObject(raw)) protocol("manifest_cell_object"); + exactKeys(raw, ["id", "sourceId", "mode", "baseStates", "targetStates", "expected"], "manifest_cell_keys"); + const expected = EXPECTED_CELL_CONTRACT[index]; + const actual = [raw.id, raw.sourceId, raw.mode, raw.baseStates, raw.targetStates, raw.expected]; + if (JSON.stringify(actual) !== JSON.stringify(expected)) protocol(`manifest_cell_contract_${index}`); + }); +} + +function copySource(closure: ValidatedSource, destination: string): string { + for (const relative of closure.relativeFiles) { + const source = safeFile(closure.root, relative, "source_copy_input_invalid"); + const target = path.resolve(destination, relative); + const relation = path.relative(path.resolve(destination), target); + if (relation === ".." || relation.startsWith(`..${path.sep}`) || path.isAbsolute(relation)) { + protocol("source_copy_escape"); + } + fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 }); + fs.copyFileSync(source, target); + } + return path.resolve(destination, closure.entrypointRelative); +} + +function safeTemporaryRoot(root: string): string { + const resolved = path.resolve(root); + const relative = path.relative(path.resolve(os.tmpdir()), resolved); + if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) { + protocol("cell_root_not_isolated_temp"); + } + return resolved; +} + +function compilerResolutionClass(executable: string): CompilerFingerprint["resolutionClass"] { + const env = process.env.INKLECATE_PATH; + if (env && path.resolve(env) === path.resolve(executable)) return "environment_override"; + const managed = path.resolve(os.homedir(), ".cache", "inkcheck", `inklecate-${INKLECATE_VERSION}`); + const relative = path.relative(managed, path.resolve(executable)); + if (relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))) { + return "managed_cache"; + } + return "path"; +} + +async function compilerFingerprint(): Promise { + const executable = await resolveInklecate(); + return { + name: "inklecate", + version: INKLECATE_VERSION, + executableSha256: (await digestFile(executable)).sha256, + resolutionClass: compilerResolutionClass(executable), + }; +} + +function resultObservation(result: ExploreResult, expectedStates: number, controls: CheckpointV2EvaluationControls): ResultObservation { + const causes = result.truncatedBy; + const loopStopped = causes.loop === true; + const workerStopped = causes.worker === true; + if (result.statesExplored !== expectedStates || result.limits.maxStates !== expectedStates + || result.limits.maxDepth !== controls.maxDepth || result.limits.seed !== controls.searchSeed + || result.limits.storySeed !== controls.storySeed || result.truncated !== true + || causes.maxStates !== true || causes.beamWidth !== false || causes.frontier !== false + || causes.memory !== false || causes.time !== false || loopStopped || workerStopped) { + protocol("unexpected_search_horizon"); + } + const visibleOutcomes = new Set(result.endingsFound.map((ending) => ending.finalText.trim().replace(/\s+/g, " "))).size; + return { + digest: digestJson(result), + statesExplored: result.statesExplored, + exhaustive: result.exhaustive, + truncated: result.truncated, + truncatedBy: { + maxDepth: causes.maxDepth, + maxStates: causes.maxStates, + beamWidth: causes.beamWidth, + frontier: causes.frontier, + memory: causes.memory, + time: causes.time, + loop: loopStopped, + worker: workerStopped, + }, + counts: { + endings: result.endingsFound.length, + visibleOutcomes, + runtimeErrors: result.runtimeErrors.length, + runtimeWarnings: result.runtimeWarnings.length, + visitedKnots: result.visitedKnots.length, + unvisitedKnots: result.unvisitedKnots.length, + }, + }; +} + +function checkpointDigest(checkpoint: SharedSearchCheckpoint): Digest { + return digestJson(checkpoint); +} + +function checkpointSummary( + projectRoot: string, + reference: CheckpointArtifactReference, + expectedEncoding: "gzip" | "framed-v2" +) { + const records = listCheckpointArtifacts(projectRoot); + const record = records.find((candidate) => candidate.id === reference.id); + if (!record || record.storageEncoding !== expectedEncoding || records.length !== 1) { + protocol("checkpoint_listing_mismatch"); + } + return { records, record }; +} + +function searchOptions( + states: number, + controls: CheckpointV2EvaluationControls, + semantics: ReturnType, + externals: string[], + guards: ReturnType +): ExploreOptions { + return { + maxStates: states, + maxDepth: controls.maxDepth, + seed: controls.searchSeed, + storySeed: controls.storySeed, + preserveTurnState: semantics.usesTurns, + preserveRandomState: semantics.usesRandomness, + randomnessDetected: semantics.usesRandomness, + detectLoopRisks: !semantics.usesTurns && !semantics.usesRandomness + && !semantics.usesVisitCounts && externals.length === 0, + memoryGuard: guards.memoryGuard, + timeGuard: guards.timeGuard, + }; +} + +async function resumeFrom( + storyJson: string, + knots: ReturnType, + externals: string[], + options: ExploreOptions, + checkpoint: SharedSearchCheckpoint +): Promise { + const resumed = await exploreSharedResumableWithCheckpointSource( + storyJson, + knots, + externals, + options, + () => undefined, + checkpoint + ); + return resumed.result; +} + +function commonCase( + source: CheckpointV2EvaluationSource, + cell: CheckpointV2EvaluationCell, + controls: CheckpointV2EvaluationControls, + semantics: ReturnType, + externals: string[], + compiler: CompilerFingerprint, + closure: ValidatedSource, + startedAt: number +) { + return { + schemaVersion: 1 as const, + id: cell.id, + sourceId: source.id, + mode: cell.mode, + expected: cell.expected, + configuration: { + baseStates: cell.baseStates, + targetStates: cell.targetStates, + maxDepth: controls.maxDepth, + searchSeed: controls.searchSeed, + storySeed: controls.storySeed, + concurrency: 1 as const, + preserveTurnState: semantics.usesTurns, + preserveRandomState: semantics.usesRandomness, + detectLoopRisks: !semantics.usesTurns && !semantics.usesRandomness + && !semantics.usesVisitCounts && externals.length === 0, + }, + compiler, + source: { + fileCount: closure.relativeFiles.length, + bundleSha256: closure.bundleSha256, + compiledStorySha256: source.compiledStorySha256, + }, + elapsedMs: Date.now() - startedAt, + peakRssBytes: process.resourceUsage().maxRSS * 1024, + }; +} + +async function runLegacySplit( + projectRoot: string, + entrypoint: string, + storyJson: string, + knots: ReturnType, + externals: string[], + baseOptions: ExploreOptions, + targetOptions: ExploreOptions, + controls: CheckpointV2EvaluationControls, + cell: CheckpointV2EvaluationCell +): Promise<{ + status: "completed" | "inconclusive"; + reason?: "legacy_readback_succeeded"; + base: ResultObservation; + checkpoint: CheckpointObservation; + resumed?: ResultObservation; +}> { + const baseRun = exploreSharedResumable(storyJson, knots, externals, baseOptions); + if (!baseRun.checkpoint) protocol("base_checkpoint_missing"); + const base = resultObservation(baseRun.result, cell.baseStates, controls); + const originalCheckpoint = checkpointDigest(baseRun.checkpoint); + const reference = await saveCheckpointArtifact(projectRoot, entrypoint, baseRun.checkpoint, { + maxCheckpointBytes: controls.storage.maxCheckpointBytes, + maxProjectBytes: controls.storage.maxProjectBytes, + }); + if (reference.accounting.checkpointGraph.count !== 1 || reference.accounting.outcome !== "created" + || reference.accounting.serialization.status !== "applied" || reference.accounting.compression.status !== "applied") { + protocol("legacy_write_receipt_mismatch"); + } + const before = checkpointSummary(projectRoot, reference, "gzip"); + const payloadFile = path.resolve(projectRoot, reference.path); + const payloadBefore = await digestFile(payloadFile); + const listingBefore = digestJson(before.records); + const checkpoint: CheckpointObservation = { + requestedFormat: "legacy-v1", + storageEncoding: "gzip", + id: reference.id, + logicalCheckpointUtf8Bytes: reference.accounting.checkpointGraph.logicalUtf8Bytes, + payloadSizeBytes: before.record.payloadSizeBytes, + durableSizeBytes: before.record.sizeBytes, + write: { + outcome: reference.accounting.outcome, + materializedCheckpointGraphs: reference.accounting.checkpointGraph.count, + engineSourceMaterializedGraphs: null, + engineSourceIdentityPasses: null, + engineSourceFramePasses: null, + legacySerializationApplied: true, + legacyCompressionApplied: true, + framedV2Applied: false, + }, + logicalCheckpoint: originalCheckpoint, + readback: { status: "completed" }, + preservation: { + payloadBytesUnchanged: true, + listingUnchanged: true, + listedBefore: before.records.length, + listedAfter: before.records.length, + }, + }; + let loaded: Awaited>; + try { + loaded = await loadCheckpointForResume(projectRoot, reference.id, { + maxStoredBytes: controls.storage.maxCheckpointBytes, + maxDecompressedBytes: controls.storage.maxCheckpointBytes, + }); + } catch (error) { + if (cell.expected !== "legacy_readback_resource_limit") protocol("legacy_readback_unexpected_failure"); + if (!(error instanceof CheckpointReadError) || error.kind !== "resource_limit" + || error.stage !== "decompression" || error.unit !== "bytes") { + protocol("legacy_readback_wrong_failure_type"); + } + const afterRecords = listCheckpointArtifacts(projectRoot); + const payloadAfter = await digestFile(payloadFile); + const payloadBytesUnchanged = payloadBefore.bytes === payloadAfter.bytes && payloadBefore.sha256 === payloadAfter.sha256; + const listingUnchanged = listingBefore.sha256 === digestJson(afterRecords).sha256; + if (!payloadBytesUnchanged || !listingUnchanged || afterRecords.length !== before.records.length) { + protocol("legacy_readback_mutated_artifact"); + } + checkpoint.readback = { + status: "resource_limit", + kind: "resource_limit", + stage: "decompression", + unit: "bytes", + ...(error.observed === undefined ? {} : { observed: error.observed }), + ...(error.limit === undefined ? {} : { limit: error.limit }), + payloadVerified: error.payloadVerified, + }; + checkpoint.preservation = { + payloadBytesUnchanged, + listingUnchanged, + listedBefore: before.records.length, + listedAfter: afterRecords.length, + }; + return { status: "completed", base, checkpoint }; + } + const loadedDigest = checkpointDigest(loaded.checkpoint); + if (loadedDigest.sha256 !== originalCheckpoint.sha256 || loadedDigest.utf8Bytes !== originalCheckpoint.utf8Bytes) { + protocol("legacy_loaded_checkpoint_mismatch"); + } + checkpoint.logicalCheckpoint = loadedDigest; + checkpoint.readback = { status: "completed", storageEncoding: loaded.artifact.storageEncoding as "gzip" }; + const resumed = resultObservation( + await resumeFrom(storyJson, knots, externals, targetOptions, loaded.checkpoint), + cell.targetStates, + controls + ); + return cell.expected === "legacy_readback_resource_limit" + ? { status: "inconclusive", reason: "legacy_readback_succeeded", base, checkpoint, resumed } + : { status: "completed", base, checkpoint, resumed }; +} + +async function runFramedSplit( + projectRoot: string, + entrypoint: string, + storyJson: string, + knots: ReturnType, + externals: string[], + baseOptions: ExploreOptions, + targetOptions: ExploreOptions, + controls: CheckpointV2EvaluationControls, + cell: CheckpointV2EvaluationCell +): Promise<{ + base: ResultObservation; + checkpoint: CheckpointObservation; + resumed: ResultObservation; +}> { + const baseRun = await exploreSharedResumableWithCheckpointSource( + storyJson, + knots, + externals, + baseOptions, + (source) => saveCheckpointArtifactFromSource(projectRoot, entrypoint, source, { + format: "framed-v2", + maxCheckpointBytes: controls.storage.maxCheckpointBytes, + maxProjectBytes: controls.storage.maxProjectBytes, + framedV2Limits: { + maxTotalDecodedBytes: controls.storage.framedV2.maxTotalDecodedBytes, + }, + }) + ); + if (!baseRun.checkpoint) protocol("framed_base_checkpoint_missing"); + const reference = baseRun.checkpoint; + const engine = reference.accounting.engineSource; + if (reference.accounting.outcome !== "created" || reference.accounting.checkpointGraph.count !== 0 + || !engine || engine.materializedCheckpointGraphs !== 0 || engine.identityPasses !== 1 || engine.framePasses !== 1 + || reference.accounting.serialization.status !== "not_applied" + || reference.accounting.compression.status !== "not_applied" + || reference.accounting.framedV2?.status !== "applied") { + protocol("framed_write_receipt_mismatch"); + } + const base = resultObservation(baseRun.result, cell.baseStates, controls); + const before = checkpointSummary(projectRoot, reference, "framed-v2"); + const payloadFile = path.resolve(projectRoot, reference.path); + const payloadBefore = await digestFile(payloadFile); + const listingBefore = digestJson(before.records); + const loaded = await loadCheckpointForResume(projectRoot, reference.id, { + maxStoredBytes: controls.storage.maxCheckpointBytes, + maxDecompressedBytes: controls.storage.maxCheckpointBytes, + framedV2Limits: { + maxTotalDecodedBytes: controls.storage.framedV2.maxTotalDecodedBytes, + }, + }); + if (loaded.artifact.storageEncoding !== "framed-v2" + || loaded.accounting.storedPayload.status !== "not_applied" + || loaded.accounting.decompressedPayload.status !== "not_applied" + || loaded.accounting.rawArtifactString.status !== "not_applied" + || loaded.accounting.parsedArtifactGraph.status !== "not_applied" + || loaded.accounting.framedV2?.status !== "applied") { + protocol("framed_read_receipt_mismatch"); + } + const afterRecords = listCheckpointArtifacts(projectRoot); + const payloadAfter = await digestFile(payloadFile); + const preservation = { + payloadBytesUnchanged: payloadBefore.bytes === payloadAfter.bytes && payloadBefore.sha256 === payloadAfter.sha256, + listingUnchanged: listingBefore.sha256 === digestJson(afterRecords).sha256, + listedBefore: before.records.length, + listedAfter: afterRecords.length, + }; + if (!preservation.payloadBytesUnchanged || !preservation.listingUnchanged) protocol("framed_readback_mutated_artifact"); + const checkpoint: CheckpointObservation = { + requestedFormat: "framed-v2", + storageEncoding: "framed-v2", + id: reference.id, + logicalCheckpointUtf8Bytes: engine.logicalCheckpointUtf8BytesVisited, + payloadSizeBytes: before.record.payloadSizeBytes, + durableSizeBytes: before.record.sizeBytes, + write: { + outcome: reference.accounting.outcome, + materializedCheckpointGraphs: 0, + engineSourceMaterializedGraphs: engine.materializedCheckpointGraphs, + engineSourceIdentityPasses: engine.identityPasses, + engineSourceFramePasses: engine.framePasses, + legacySerializationApplied: false, + legacyCompressionApplied: false, + framedV2Applied: true, + }, + logicalCheckpoint: checkpointDigest(loaded.checkpoint), + readback: { status: "completed", storageEncoding: "framed-v2" }, + preservation, + }; + const resumed = resultObservation( + await resumeFrom(storyJson, knots, externals, targetOptions, loaded.checkpoint), + cell.targetStates, + controls + ); + return { base, checkpoint, resumed }; +} + +export async function runCheckpointV2EvaluationCell( + request: WorkerRequest +): Promise { + const cellRoot = safeTemporaryRoot(request.cellRoot); + if (fs.existsSync(cellRoot) && fs.readdirSync(cellRoot).length > 0) protocol("cell_root_not_empty"); + fs.mkdirSync(cellRoot, { recursive: true, mode: 0o700 }); + const startedAt = Date.now(); + try { + const closure = await validateEvaluationSource(request.manifestRoot, request.source); + const entrypoint = copySource(closure, cellRoot); + const copiedClosure = sourceClosure(entrypoint); + if (copiedClosure.bundleSha256 !== request.source.closure.bundleSha256 + || JSON.stringify(copiedClosure.relativeFiles) !== JSON.stringify(closure.relativeFiles)) { + protocol("isolated_source_copy_drift"); + } + const inspection = inspectProject(entrypoint); + if (inspection.truncation.includes) protocol("isolated_source_include_closure_truncated"); + const semantics = scanStorySemantics(entrypoint); + const knots = scanKnots(entrypoint); + const externals = scanExternals(entrypoint); + const compiler = await compilerFingerprint(); + const guards = createResourceGuards({ + maxMemoryMb: request.controls.maxMemoryMb, + maxTimeMs: request.controls.maxTimeMs, + startedAtMs: startedAt, + }); + const compiled = await compile(entrypoint); + if (!compiled.success || !compiled.storyJson) protocol("compile_failed"); + if (compiledStorySha256(compiled.storyJson) !== request.source.compiledStorySha256) { + protocol("compiled_story_sha256_drift"); + } + const baseOptions = searchOptions(request.cell.baseStates, request.controls, semantics, externals, guards); + const targetOptions = searchOptions(request.cell.targetStates, request.controls, semantics, externals, guards); + const common = () => commonCase( + request.source, + request.cell, + request.controls, + semantics, + externals, + compiler, + copiedClosure, + startedAt + ); + if (request.cell.mode === "uninterrupted") { + const uninterrupted = resultObservation( + exploreShared(compiled.storyJson, knots, externals, targetOptions), + request.cell.targetStates, + request.controls + ); + return { ...common(), status: "completed", uninterrupted }; + } + if (request.cell.mode === "framed-split") { + const split = await runFramedSplit( + cellRoot, entrypoint, compiled.storyJson, knots, externals, + baseOptions, targetOptions, request.controls, request.cell + ); + return { ...common(), status: "completed", ...split }; + } + const split = await runLegacySplit( + cellRoot, entrypoint, compiled.storyJson, knots, externals, + baseOptions, targetOptions, request.controls, request.cell + ); + return { ...common(), ...split }; + } finally { + fs.rmSync(cellRoot, { recursive: true, force: true }); + } +} + +function killProcessTree(child: ReturnType, signal: NodeJS.Signals): ProcessCleanupScope { + if (!child.pid) return "exact_process"; + if (process.platform === "win32") { + // Windows has no detached POSIX process group that can receive SIGTERM. + // Kill the captured tree while its leader still exists, then repeat at the + // forced-cleanup boundary below. Waiting to use taskkill until after the + // leader closes can orphan descendants that outlive that leader. + spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }); + return "process_tree"; + } + try { + process.kill(-child.pid, signal); + return "process_group"; + } catch { + child.kill(signal); + return "exact_process"; + } +} + +export function runBoundedProcess( + executable: string, + args: string[], + options: { + cwd: string; + timeoutMs: number; + stdoutLimitBytes?: number; + stderrLimitBytes?: number; + /** Testable grace boundary; production workers use WORKER_KILL_GRACE_MS. */ + killGraceMs?: number; + } +): Promise { + return new Promise((resolve) => { + const child = spawn(executable, args, { + cwd: options.cwd, + stdio: ["ignore", "pipe", "pipe"], + detached: process.platform !== "win32", + }); + const stdout: Buffer[] = []; + const stderr: Buffer[] = []; + let stdoutBytes = 0; + let stderrBytes = 0; + let termination: "timeout" | "output_limit" | undefined; + let cleanupScope: ProcessCleanupScope | undefined; + let killTimer: NodeJS.Timeout | undefined; + let resolved = false; + let forceAttempted = false; + let closed: { code: number | null; signal: NodeJS.Signals | null } | undefined; + const finish = (code: number | null, signal: NodeJS.Signals | null) => { + if (resolved) return; + resolved = true; + clearTimeout(timeout); + resolve({ + status: termination ?? "completed", + exitCode: code, + signal, + stdout: Buffer.concat(stdout).toString("utf8"), + stderr: Buffer.concat(stderr).toString("utf8"), + ...(cleanupScope ? { cleanupScope } : {}), + }); + }; + const terminate = (reason: "timeout" | "output_limit") => { + if (termination) return; + termination = reason; + cleanupScope = killProcessTree(child, "SIGTERM"); + killTimer = setTimeout(() => { + const forcedScope = killProcessTree(child, "SIGKILL"); + if (cleanupScope === undefined || cleanupScope === "exact_process") cleanupScope = forcedScope; + forceAttempted = true; + if (closed) finish(closed.code, closed.signal); + }, process.platform === "win32" ? 0 : (options.killGraceMs ?? WORKER_KILL_GRACE_MS)); + // This timer deliberately remains referenced. Once a leader exits after + // SIGTERM, the coordinator must stay alive long enough to SIGKILL its + // captured process group/tree rather than resolving and orphaning it. + }; + const timeout = setTimeout(() => terminate("timeout"), options.timeoutMs); + timeout.unref(); + child.stdout.on("data", (chunk: Buffer) => { + stdoutBytes += chunk.length; + if (stdoutBytes <= (options.stdoutLimitBytes ?? MAX_WORKER_OUTPUT_BYTES)) stdout.push(chunk); + else terminate("output_limit"); + }); + child.stderr.on("data", (chunk: Buffer) => { + stderrBytes += chunk.length; + if (stderrBytes <= (options.stderrLimitBytes ?? MAX_WORKER_DIAGNOSTIC_BYTES)) stderr.push(chunk); + else terminate("output_limit"); + }); + child.on("error", () => { + if (resolved) return; + if (termination && child.pid) { + // A failed graceful signal must not cancel the scheduled force pass. + return; + } + resolved = true; + clearTimeout(timeout); + if (killTimer) clearTimeout(killTimer); + resolve({ + status: "spawn_error", + exitCode: null, + signal: null, + stdout: Buffer.concat(stdout).toString("utf8"), + stderr: Buffer.concat(stderr).toString("utf8"), + }); + }); + child.on("close", (code, signal) => { + if (resolved) return; + clearTimeout(timeout); + closed = { code, signal }; + if (!termination || forceAttempted) { + if (killTimer) clearTimeout(killTimer); + finish(code, signal); + } + }); + }); +} + +function workerRuntimeArgs(): string[] { + const result: string[] = []; + for (let index = 0; index < process.execArgv.length; index++) { + const argument = process.execArgv[index]; + if (/^--max[-_]old[-_]space[-_]size=\d+$/.test(argument)) result.push(argument); + else if (/^--max[-_]old[-_]space[-_]size$/.test(argument) && /^\d+$/.test(process.execArgv[index + 1] ?? "")) { + result.push(argument, process.execArgv[++index]); + } + } + return result; +} + +function maxOldSpaceSizeMb(): number | null { + const args = workerRuntimeArgs(); + for (let index = 0; index < args.length; index++) { + const inline = args[index].match(/=(\d+)$/); + if (inline) return Number(inline[1]); + if (/^--max[-_]old[-_]space[-_]size$/.test(args[index])) return Number(args[index + 1]); + } + return null; +} + +function safeWorkerError(error: unknown): string { + if (error instanceof EvaluationProtocolError) return error.code; + if (error instanceof CheckpointReadError) return `checkpoint_${error.kind}_${error.stage}`; + if (error instanceof RangeError) return "range_error"; + if (error instanceof Error && error.name === "AbortError") return "cancelled"; + return "worker_error"; +} + +export function writeEvaluationOutputAtomic(file: string, value: string): void { + const destination = path.resolve(file); + fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 }); + const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.${process.pid}.${randomUUID()}.tmp`); + let descriptor: number | undefined; + try { + descriptor = fs.openSync(temporary, "wx", 0o600); + fs.writeFileSync(descriptor, value, "utf8"); + fs.fsyncSync(descriptor); + fs.closeSync(descriptor); + descriptor = undefined; + fs.renameSync(temporary, destination); + if (process.platform !== "win32") { + const directory = fs.openSync(path.dirname(destination), "r"); + try { + fs.fsyncSync(directory); + } finally { + fs.closeSync(directory); + } + } + } finally { + if (descriptor !== undefined) fs.closeSync(descriptor); + fs.rmSync(temporary, { force: true }); + } +} + +function gitOutput(root: string, args: string[]): string { + const result = spawnSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }); + if (result.status !== 0) protocol("candidate_git_unavailable"); + return result.stdout.trim(); +} + +function regularFiles(root: string, relative = ""): string[] { + const directory = path.resolve(root, relative); + if (!fs.existsSync(directory)) protocol("candidate_dist_missing"); + const result: string[] = []; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const child = relative ? path.join(relative, entry.name) : entry.name; + if (entry.isSymbolicLink()) protocol("candidate_dist_symlink"); + if (entry.isDirectory()) result.push(...regularFiles(root, child)); + else if (entry.isFile()) result.push(normalizeRelative(child)); + } + return result.sort(); +} + +function bundleDigest(root: string, files: string[]): string { + const hash = createHash("sha256"); + for (const relative of files) { + hash.update(relative); + hash.update("\0"); + hash.update(fs.readFileSync(path.resolve(root, relative))); + hash.update("\0"); + } + return hash.digest("hex"); +} + +function candidateFingerprint(manifestFile: string, manifestSha256: string) { + const repositoryRoot = gitOutput(path.dirname(manifestFile), ["rev-parse", "--show-toplevel"]); + const dirty = gitOutput(repositoryRoot, ["status", "--porcelain=v1", "--untracked-files=all"]) + .split(/\r?\n/).filter(Boolean); + const packageFile = safeFile(repositoryRoot, "package.json", "candidate_package_missing"); + const packageValue = JSON.parse(fs.readFileSync(packageFile, "utf8")) as { version?: unknown }; + if (packageValue.version !== VERSION) protocol("candidate_version_mismatch"); + const lock = safeFile(repositoryRoot, "package-lock.json", "candidate_lock_missing"); + const distRoot = path.resolve(repositoryRoot, "dist"); + const distFiles = regularFiles(distRoot); + return { + manifestSha256, + git: { + head: gitOutput(repositoryRoot, ["rev-parse", "HEAD"]), + tree: gitOutput(repositoryRoot, ["rev-parse", "HEAD^{tree}"]), + clean: dirty.length === 0, + dirtyEntryCount: dirty.length, + }, + packageVersion: VERSION, + dependencyLockSha256: sha256(fs.readFileSync(lock)), + dist: { + fileCount: distFiles.length, + bundleSha256: bundleDigest(distRoot, distFiles), + }, + }; +} + +function runtimeFingerprint() { + const cpus = os.cpus(); + return { + nodeVersion: process.version, + v8Version: process.versions.v8, + platform: process.platform, + architecture: process.arch, + logicalCpuCount: cpus.length, + cpuModel: cpus[0]?.model ?? "unknown", + totalMemoryBytes: os.totalmem(), + v8HeapLimitBytes: v8.getHeapStatistics().heap_size_limit, + maxOldSpaceSizeMb: maxOldSpaceSizeMb(), + }; +} + +function sameDigest(left: Digest | undefined, right: Digest | undefined): boolean { + return Boolean(left && right && left.sha256 === right.sha256 && left.utf8Bytes === right.utf8Bytes); +} + +export function evaluationProgressSnapshot( + selectedCellIds: string[], + recordedCellIds: string[], + activeCellId: string | undefined, + status: EvaluationProgressSnapshot["status"] +): EvaluationProgressSnapshot { + if (recordedCellIds.length > selectedCellIds.length + || recordedCellIds.some((id, index) => id !== selectedCellIds[index])) { + protocol("evaluation_progress_order"); + } + if (activeCellId !== undefined && activeCellId !== selectedCellIds[recordedCellIds.length]) { + protocol("evaluation_progress_active_cell"); + } + if (status === "completed" + && (activeCellId !== undefined || recordedCellIds.length !== selectedCellIds.length)) { + protocol("evaluation_progress_incomplete"); + } + return { + status, + selectedCellCount: selectedCellIds.length, + recordedCellCount: recordedCellIds.length, + recordedCellIds: [...recordedCellIds], + ...(activeCellId === undefined ? {} : { activeCellId }), + }; +} + +interface VerdictGate { + id: string; + status: "passed" | "failed" | "not_evaluated"; + checks: Record; +} + +export function evaluateCheckpointV2Verdict( + manifest: CheckpointV2EvaluationManifest, + cases: EvaluationCaseResult[], + selectedCellIds: string[], + candidateClean: boolean, + coordinatorHeapQualified: boolean +) { + const completeMatrix = JSON.stringify(selectedCellIds) === JSON.stringify(manifest.controls.cellOrder) + && cases.length === manifest.cells.length; + const violations: string[] = []; + const uncertainties: string[] = []; + for (const cell of cases) { + if (cell.status === "failed") violations.push(`${cell.id}:${cell.reason}`); + else if (cell.status === "unavailable") uncertainties.push(`${cell.id}:${cell.reason}`); + else if (cell.status === "inconclusive") uncertainties.push(`${cell.id}:${cell.reason ?? "inconclusive"}`); + } + if (!completeMatrix) uncertainties.push("filtered_or_incomplete_matrix"); + if (!candidateClean) uncertainties.push("candidate_worktree_dirty"); + if (!coordinatorHeapQualified) uncertainties.push("coordinator_heap_ceiling_unverified"); + const completed = new Map(cases + .filter((cell): cell is CompletedCheckpointV2EvaluationCase => cell.status === "completed") + .map((cell) => [cell.id, cell])); + const gates: VerdictGate[] = []; + const compare = (id: string, checks: Record | undefined) => { + if (!checks) { + gates.push({ id, status: "not_evaluated", checks: {} }); + return; + } + const passed = Object.values(checks).every(Boolean); + gates.push({ id, status: passed ? "passed" : "failed", checks }); + if (!passed) violations.push(id); + }; + const heresyLegacy = completed.get("heresy2-legacy-split"); + const heresyFramed = completed.get("heresy2-framed-split"); + const heresyWhole = completed.get("heresy2-uninterrupted"); + compare("heresy2_base_storage_parity", heresyLegacy && heresyFramed ? { + baseResult: sameDigest(heresyLegacy.base?.digest, heresyFramed.base?.digest), + checkpointId: heresyLegacy.checkpoint?.id === heresyFramed.checkpoint?.id, + logicalCheckpointBytes: heresyLegacy.checkpoint?.logicalCheckpointUtf8Bytes === heresyFramed.checkpoint?.logicalCheckpointUtf8Bytes, + loadedCheckpoint: sameDigest(heresyLegacy.checkpoint?.logicalCheckpoint, heresyFramed.checkpoint?.logicalCheckpoint), + } : undefined); + compare("heresy2_exact_resume", heresyLegacy && heresyFramed && heresyWhole ? { + legacyVsUninterrupted: sameDigest(heresyLegacy.resumed?.digest, heresyWhole.uninterrupted?.digest), + framedVsUninterrupted: sameDigest(heresyFramed.resumed?.digest, heresyWhole.uninterrupted?.digest), + legacyVsFramed: sameDigest(heresyLegacy.resumed?.digest, heresyFramed.resumed?.digest), + } : undefined); + const interceptWhole = completed.get("intercept-uninterrupted"); + const interceptFramed = completed.get("intercept-framed-split"); + const interceptLegacy = completed.get("intercept-legacy-split"); + compare("intercept_base_storage_parity", interceptLegacy && interceptFramed ? { + baseResult: sameDigest(interceptLegacy.base?.digest, interceptFramed.base?.digest), + checkpointId: interceptLegacy.checkpoint?.id === interceptFramed.checkpoint?.id, + logicalCheckpointBytes: interceptLegacy.checkpoint?.logicalCheckpointUtf8Bytes === interceptFramed.checkpoint?.logicalCheckpointUtf8Bytes, + } : undefined); + compare("intercept_framed_exact_resume", interceptFramed && interceptWhole ? { + framedVsUninterrupted: sameDigest(interceptFramed.resumed?.digest, interceptWhole.uninterrupted?.digest), + engineNativeNoGraph: interceptFramed.checkpoint?.write.materializedCheckpointGraphs === 0 + && interceptFramed.checkpoint.write.engineSourceMaterializedGraphs === 0, + legacyStagesNotApplied: interceptFramed.checkpoint?.write.legacySerializationApplied === false + && interceptFramed.checkpoint.write.legacyCompressionApplied === false, + } : undefined); + compare("intercept_legacy_readback_boundary", interceptLegacy ? { + typedResourceLimit: interceptLegacy.checkpoint?.readback.status === "resource_limit" + && interceptLegacy.checkpoint.readback.kind === "resource_limit" + && interceptLegacy.checkpoint.readback.stage === "decompression" + && interceptLegacy.checkpoint.readback.unit === "bytes", + payloadPreserved: interceptLegacy.checkpoint?.preservation.payloadBytesUnchanged === true, + listingPreserved: interceptLegacy.checkpoint?.preservation.listingUnchanged === true, + } : undefined); + const unevaluated = gates.filter((gate) => gate.status === "not_evaluated").length; + if (completeMatrix && unevaluated > 0) uncertainties.push("required_gate_not_evaluated"); + const status = violations.length > 0 ? "failed" : uncertainties.length > 0 ? "inconclusive" : "passed"; + const promotionClaims = [ + "declared_cell_exact_resume", + "engine_native_zero_materialized_checkpoint_graph", + "typed_legacy_readback_boundary", + "artifact_preservation_after_readback_limit", + ]; + return { + status, + completeMatrix, + gates, + violations, + uncertainties, + allowedClaims: status === "passed" ? promotionClaims : [], + forbiddenClaims: [ + "universal_performance_improvement", + "portable_memory_improvement", + "story_coverage", + "defect_absence", + "allocation_policy_promotion", + "checkpoint_default_format_change", + "inkbench_improvement", + ], + }; +} + +export function evaluationSnapshotVerdict( + verdict: ReturnType, + status: EvaluationProgressSnapshot["status"] +): ReturnType { + if (status === "completed" || verdict.status === "failed") return verdict; + return { + ...verdict, + status: "inconclusive", + uncertainties: [...verdict.uncertainties, "evaluation_in_progress"], + allowedClaims: [], + }; +} + +function assertPrivacySafe(value: unknown): void { + const visit = (item: unknown): void => { + if (typeof item === "string") { + if (path.isAbsolute(item) || /^[A-Za-z]:[\\/]/.test(item) || item.includes("\n")) protocol("report_privacy_violation"); + return; + } + if (Array.isArray(item)) { + item.forEach(visit); + return; + } + if (isObject(item)) Object.values(item).forEach(visit); + }; + visit(value); +} + +function parseWorkerError(stdout: string): WorkerError | undefined { + try { + const value = JSON.parse(stdout) as WorkerError; + return value?.schemaVersion === 1 && value.kind === "checkpoint_v2_evaluation_worker_error" + && typeof value.code === "string" ? value : undefined; + } catch { + return undefined; + } +} + +async function runWorkerCell( + manifestRoot: string, + scratch: string, + source: CheckpointV2EvaluationSource, + cell: CheckpointV2EvaluationCell, + controls: CheckpointV2EvaluationControls, + sequence: number +): Promise { + const requestFile = path.join(scratch, `request-${sequence}.json`); + const cellRoot = path.join(scratch, `cell-${sequence}`); + const request: WorkerRequest = { schemaVersion: 1, manifestRoot, cellRoot, source, cell, controls }; + fs.writeFileSync(requestFile, JSON.stringify(request), { mode: 0o600 }); + try { + const child = await runBoundedProcess( + process.execPath, + [...workerRuntimeArgs(), __filename, "--worker", requestFile], + { cwd: process.cwd(), timeoutMs: controls.workerTimeoutMs } + ); + if (child.status === "timeout") { + return { schemaVersion: 1, id: cell.id, sourceId: source.id, mode: cell.mode, expected: cell.expected, + status: "unavailable", reason: "worker_timeout", timeoutMs: controls.workerTimeoutMs, + cleanupScope: child.cleanupScope }; + } + if (child.status !== "completed") { + return { schemaVersion: 1, id: cell.id, sourceId: source.id, mode: cell.mode, expected: cell.expected, + status: "unavailable", reason: child.status, ...(child.cleanupScope ? { cleanupScope: child.cleanupScope } : {}) }; + } + if (child.exitCode !== 0) { + const error = parseWorkerError(child.stdout); + return { schemaVersion: 1, id: cell.id, sourceId: source.id, mode: cell.mode, expected: cell.expected, + status: error?.code === "worker_error" ? "unavailable" : "failed", + reason: error?.code ?? "worker_exit" }; + } + let result: CompletedCheckpointV2EvaluationCase; + try { + result = JSON.parse(child.stdout) as CompletedCheckpointV2EvaluationCase; + } catch { + return { schemaVersion: 1, id: cell.id, sourceId: source.id, mode: cell.mode, expected: cell.expected, + status: "failed", reason: "worker_output_invalid" }; + } + if (result.schemaVersion !== 1 || result.id !== cell.id || result.sourceId !== source.id + || result.mode !== cell.mode || !["completed", "inconclusive"].includes(result.status)) { + return { schemaVersion: 1, id: cell.id, sourceId: source.id, mode: cell.mode, expected: cell.expected, + status: "failed", reason: "worker_output_contract" }; + } + assertPrivacySafe(result); + return result; + } finally { + fs.rmSync(requestFile, { force: true }); + fs.rmSync(cellRoot, { recursive: true, force: true }); + } +} + +async function workerMain(requestFile: string): Promise { + let request: WorkerRequest; + try { + const raw = fs.readFileSync(requestFile, "utf8"); + request = JSON.parse(raw) as WorkerRequest; + if (request.schemaVersion !== 1 || !request.source || !request.cell || !request.controls) { + protocol("worker_request_invalid"); + } + const result = await runCheckpointV2EvaluationCell(request); + assertPrivacySafe(result); + process.stdout.write(JSON.stringify(result)); + } catch (error) { + const result: WorkerError = { + schemaVersion: 1, + kind: "checkpoint_v2_evaluation_worker_error", + code: safeWorkerError(error), + }; + process.stdout.write(JSON.stringify(result)); + process.exitCode = 1; + } +} + +function cliArguments(args: string[]): { manifestFile: string; outputFile?: string; requestedCells: string[] } { + const positional: string[] = []; + const requestedCells: string[] = []; + let outputFile: string | undefined; + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + if (argument === "--case" || argument === "--output") { + const value = args[++index]; + if (!value || value.startsWith("--")) protocol("cli_option_value_missing"); + if (argument === "--case") requestedCells.push(value); + else outputFile = value; + continue; + } + if (argument.startsWith("--")) protocol("cli_option_unknown"); + positional.push(argument); + } + if (positional.length !== 1) protocol("cli_usage"); + return { manifestFile: path.resolve(positional[0]), ...(outputFile ? { outputFile: path.resolve(outputFile) } : {}), requestedCells }; +} + +async function coordinatorMain(args: string[]): Promise { + const parsed = cliArguments(args); + regularFile(parsed.manifestFile, "manifest_file_invalid"); + const raw = fs.readFileSync(parsed.manifestFile); + if (raw.length > MAX_MANIFEST_BYTES) protocol("manifest_too_large"); + let value: unknown; + try { + value = JSON.parse(raw.toString("utf8")); + } catch { + protocol("manifest_json_invalid"); + } + validateCheckpointV2EvaluationManifest(value); + const manifest = value; + const manifestRoot = path.dirname(parsed.manifestFile); + for (const source of manifest.sources) await validateEvaluationSource(manifestRoot, source); + const manifestSha256 = sha256(raw); + const candidate = candidateFingerprint(parsed.manifestFile, manifestSha256); + const runtime = runtimeFingerprint(); + const compiler = await compilerFingerprint(); + const requested = new Set(parsed.requestedCells); + for (const id of requested) if (!manifest.controls.cellOrder.includes(id)) protocol("unknown_selected_cell"); + const selectedCells = requested.size === 0 + ? manifest.cells + : manifest.cells.filter((cell) => requested.has(cell.id)); + if (selectedCells.length === 0) protocol("no_selected_cells"); + const selectedCellIds = selectedCells.map((cell) => cell.id); + const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "inkcheck-checkpoint-v2-evaluation-")); + const cases: EvaluationCaseResult[] = []; + const sourceSummaries = manifest.sources.map((source) => ({ + id: source.id, + upstream: source.provenance.upstream, + commit: source.provenance.commit, + license: source.provenance.license, + entrypointSha256: source.entrypointSha256, + licenseSha256: source.provenance.licenseSha256, + bundleSha256: source.closure.bundleSha256, + fileCount: source.closure.fileCount, + compiledStorySha256: source.compiledStorySha256, + })); + const writeReportSnapshot = ( + status: EvaluationProgressSnapshot["status"], + activeCellId?: string + ) => { + const evaluatedVerdict = evaluateCheckpointV2Verdict( + manifest, + cases, + selectedCellIds, + candidate.git.clean, + runtime.maxOldSpaceSizeMb !== null && runtime.maxOldSpaceSizeMb >= manifest.controls.coordinatorMaxOldSpaceMb + ); + const verdict = evaluationSnapshotVerdict(evaluatedVerdict, status); + const progress = evaluationProgressSnapshot( + selectedCellIds, + cases.map((cell) => cell.id), + activeCellId, + status + ); + const report = { + schemaVersion: 1, + kind: EVALUATION_KIND, + evaluationId: manifest.id, + generatedAt: new Date().toISOString(), + candidate, + runtime, + compiler, + selection: { + serial: true, + requestedCellIds: parsed.requestedCells, + selectedCellIds, + completeMatrix: verdict.completeMatrix, + }, + progress, + controls: manifest.controls, + sources: sourceSummaries, + cases, + verdict, + }; + assertPrivacySafe(report); + const output = `${JSON.stringify(report, null, 2)}\n`; + if (parsed.outputFile) writeEvaluationOutputAtomic(parsed.outputFile, output); + else if (status === "completed") process.stdout.write(output); + return verdict; + }; + if (parsed.outputFile) writeReportSnapshot("in_progress"); + try { + for (let index = 0; index < selectedCells.length; index++) { + const cell = selectedCells[index]; + const source = manifest.sources.find((candidateSource) => candidateSource.id === cell.sourceId); + if (!source) protocol("selected_source_missing"); + // Persist the active cell before it can allocate or spawn. If the + // coordinator is cancelled or crashes, all earlier cases plus the exact + // unfinished boundary remain in the atomic report. + if (parsed.outputFile) writeReportSnapshot("in_progress", cell.id); + process.stderr.write(`${JSON.stringify({ schemaVersion: 1, kind: "checkpoint_v2_evaluation_progress", cell: cell.id, status: "started" })}\n`); + let result = await runWorkerCell(manifestRoot, scratch, source, cell, manifest.controls, index); + if ((result.status === "completed" || result.status === "inconclusive") + && JSON.stringify(result.compiler) !== JSON.stringify(compiler)) { + result = { schemaVersion: 1, id: cell.id, sourceId: source.id, mode: cell.mode, expected: cell.expected, + status: "failed", reason: "compiler_fingerprint_drift" }; + } + cases.push(result); + // Record every terminal cell outcome before starting the next cell. This + // snapshot remains non-promotional until the coordinator writes the + // explicit completed snapshot below. + if (parsed.outputFile) writeReportSnapshot("in_progress"); + process.stderr.write(`${JSON.stringify({ schemaVersion: 1, kind: "checkpoint_v2_evaluation_progress", cell: cell.id, status: result.status })}\n`); + } + } finally { + fs.rmSync(scratch, { recursive: true, force: true }); + } + const verdict = writeReportSnapshot("completed"); + if (verdict.status === "failed") process.exitCode = 1; + else if (verdict.status === "inconclusive") process.exitCode = 2; +} + +async function main(): Promise { + const args = process.argv.slice(2); + if (args[0] === "--worker" && args.length === 2) { + await workerMain(path.resolve(args[1])); + return; + } + await coordinatorMain(args); +} + +if (require.main === module) { + main().catch((error) => { + const result = { + schemaVersion: 1, + kind: "checkpoint_v2_evaluation_error", + code: safeWorkerError(error), + }; + process.stderr.write(`${JSON.stringify(result)}\n`); + process.exitCode = 2; + }); +} diff --git a/src/checkpoints.ts b/src/checkpoints.ts index 0544861..129189c 100644 --- a/src/checkpoints.ts +++ b/src/checkpoints.ts @@ -13,12 +13,15 @@ import { type CheckpointArtifactV2ReadResult, type CheckpointArtifactV2WriteResult, readCheckpointArtifactV2, + resolveCheckpointArtifactV2Limits, writeCheckpointArtifactV2, } from "./checkpoint-artifact-v2"; import { compile, scanKnots } from "./inklecate"; import { + assertLiveSharedSearchCheckpointSourceV1, SHARED_SEARCH_CHECKPOINT_SCHEMA_VERSION, type SharedSearchCheckpoint, + type SharedSearchCheckpointSourceV1, } from "./explore"; import { VERSION } from "./version"; @@ -47,10 +50,23 @@ export interface CheckpointWriteAccountingV1 { schemaVersion: 1; outcome: "created" | "reused"; checkpointGraph: { - count: 1; + count: 0 | 1; logicalUtf8Bytes: number; peakSourceChunkUtf8Bytes: number; }; + /** Present only when a framed-v2 operation uses a paused engine source. */ + engineSource?: { + schemaVersion: 1; + status: "applied"; + materializedCheckpointGraphs: 0; + identityPasses: 1; + /** Ordered source-record traversals used to encode a private candidate. */ + framePasses: 0 | 1; + /** Ordered source-record traversals used to verify a durable framed reuse. */ + reuseVerificationPasses: 0 | 1; + logicalCheckpointUtf8BytesVisited: number; + peakSourceChunkUtf8Bytes: number; + }; serialization: { status: "applied"; logicalArtifactUtf8BytesEmitted: number; @@ -159,6 +175,20 @@ export interface CheckpointReadAccountingV1 { maxManifestBytes: number; maxStoredBytes: number; maxDecompressedBytes: number; + /** Present only for framed-v2 reads; these are the actual codec bounds. */ + framedV2?: { + basis: "effective_codec_limits_not_allocated_capacity"; + maxHeaderBytes: number; + maxStoredFrameBytes: number; + maxDecodedFrameBytes: number; + maxRecordBytes: number; + maxJsonDepth: number; + maxRecordsPerFrame: number; + maxFrames: number; + maxTotalRecords: number; + maxTotalStoredBytes: number; + maxTotalDecodedBytes: number; + }; }; manifest: { status: "applied" | "not_present" | "not_completed"; @@ -261,7 +291,7 @@ export class CheckpointReadError extends Error { } } -interface CheckpointArtifact { +interface CheckpointArtifactMetadata { artifactSchemaVersion: 1 | 2; artifactType: "shared-search-checkpoint"; id: string; @@ -272,9 +302,44 @@ interface CheckpointArtifact { storySha256: string; knotsSha256: string; configuration: SharedSearchCheckpoint["configuration"]; +} + +interface CheckpointArtifact extends CheckpointArtifactMetadata { checkpoint: SharedSearchCheckpoint; } +interface ExpectedCheckpointSummary { + engine: SharedSearchCheckpoint["engine"]; + configuration: SharedSearchCheckpoint["configuration"]; + totalGranted: number; + statesExplored: number; +} + +type CheckpointSaveInput = { + kind: "checkpoint-graph"; + checkpoint: SharedSearchCheckpoint; +} | { + kind: "engine-source"; + source: SharedSearchCheckpointSourceV1; +}; + +function expectedCheckpointSummary(input: CheckpointSaveInput): ExpectedCheckpointSummary { + if (input.kind === "checkpoint-graph") { + return { + engine: input.checkpoint.engine, + configuration: input.checkpoint.configuration, + totalGranted: input.checkpoint.state.totalGranted, + statesExplored: input.checkpoint.state.statesExplored, + }; + } + return { + engine: input.source.engine, + configuration: { ...input.source.configuration, externals: [...input.source.configuration.externals] }, + totalGranted: input.source.totalGranted, + statesExplored: input.source.statesExplored, + }; +} + const CHECKPOINT_V2_COMPONENT_ORDER = [ "configuration", "scheduler", "frontier", "witnessAncestry", "dedupe", "semanticIndexes", "findings", "metadata", @@ -410,6 +475,27 @@ const CHECKPOINT_V2_ARRAY_STATE_FIELDS = new Set([ + ...CHECKPOINT_V2_ARRAY_STATE_FIELDS, + "nodes", +]); + function *checkpointV2NodePayloadRecords( nodes: SharedSearchCheckpoint["state"]["nodes"] ): Generator { @@ -519,6 +605,112 @@ function checkpointArtifactV2Frames( }); } +function checkpointArtifactV2FramesFromSource( + artifact: CheckpointArtifactMetadata, + source: SharedSearchCheckpointSourceV1 +): CheckpointArtifactV2FrameInput[] { + const frames: CheckpointArtifactV2FrameInput[] = [{ + component: "configuration", + field: "checkpoint", + start: 0, + records: [{ + schemaVersion: source.checkpointSchemaVersion, + engine: source.engine, + configuration: source.configuration, + }], + }]; + for (const spec of CHECKPOINT_V2_STATE_COMPONENT_FIELDS) { + for (const field of spec.fields) { + frames.push({ + component: spec.component, + field, + start: 0, + records: source.stateRecords(field), + }); + } + if (spec.component === "frontier") { + frames.push({ + component: "frontier", + field: "nodePayload", + start: 0, + records: source.nodePayloadRecords(), + }); + } + if (spec.component === "witnessAncestry") { + frames.push({ + component: "witnessAncestry", + field: "nodeSlots", + start: 0, + records: [source.nodeSlots], + }, { + component: "witnessAncestry", + field: "nodes", + start: 0, + records: source.nodeAncestryRecords(), + }); + } + if (spec.component === "metadata") { + frames.push({ + component: "metadata", + field: "artifact", + start: 0, + records: [{ + artifactSchemaVersion: artifact.artifactSchemaVersion, + artifactType: artifact.artifactType, + id: artifact.id, + createdAt: artifact.createdAt, + inkcheckVersion: artifact.inkcheckVersion, + checkpointSchemaVersion: artifact.checkpointSchemaVersion, + source: artifact.source, + storySha256: artifact.storySha256, + knotsSha256: artifact.knotsSha256, + }], + }); + } + } + return frames.sort((left, right) => { + return CHECKPOINT_V2_COMPONENT_ORDER.indexOf(left.component) + - CHECKPOINT_V2_COMPONENT_ORDER.indexOf(right.component) + || (left.field < right.field ? -1 : left.field > right.field ? 1 : 0); + }); +} + +function *checkpointArtifactV2RecordsFromSource( + artifact: CheckpointArtifactMetadata, + source: SharedSearchCheckpointSourceV1 +): Generator { + for (const frame of checkpointArtifactV2FramesFromSource(artifact, source)) { + let index = frame.start; + for (const value of frame.records as Iterable) { + yield { + component: frame.component, + field: frame.field, + index, + value, + }; + index += 1; + } + } +} + +function canonicalJsonValuesMatch(left: unknown, right: unknown): boolean { + const leftChunks = jsonChunks(left); + const rightChunks = jsonChunks(right); + try { + while (true) { + const leftChunk = leftChunks.next(); + const rightChunk = rightChunks.next(); + if (leftChunk.done || rightChunk.done) { + return leftChunk.done === rightChunk.done; + } + if (leftChunk.value !== rightChunk.value) return false; + } + } finally { + leftChunks.return(undefined); + rightChunks.return(undefined); + } +} + type CheckpointV2Record = { component: CheckpointArtifactV2FrameInput["component"]; field: string; @@ -1043,6 +1235,62 @@ function *jsonChunks(value: unknown, ancestors = new Set()): Generator): Generator { + yield "["; + let first = true; + for (const record of records) { + if (!first) yield ","; + first = false; + yield *jsonChunks(record); + } + yield "]"; +} + +function *checkpointSourceJsonChunks( + source: SharedSearchCheckpointSourceV1 +): Generator { + if (source.sourceSchemaVersion !== 1 + || source.checkpointSchemaVersion !== SHARED_SEARCH_CHECKPOINT_SCHEMA_VERSION + || source.engine !== "shared:deep-novelty-v1") { + throw new RangeError("Unsupported shared checkpoint source schema"); + } + yield `{"schemaVersion":${source.checkpointSchemaVersion},"engine":`; + yield *jsonChunks(source.engine); + yield `,"configuration":`; + yield *jsonChunks(source.configuration); + yield `,"state":{`; + let firstField = true; + for (const field of CHECKPOINT_V1_STATE_FIELD_ORDER) { + const records = source.stateRecords(field); + if (CHECKPOINT_V1_ARRAY_STATE_FIELDS.has(field)) { + if (!firstField) yield ","; + firstField = false; + yield JSON.stringify(field); + yield ":"; + yield *jsonArrayFromRecords(records); + continue; + } + const iterator = records[Symbol.iterator](); + let first: IteratorResult; + try { + first = iterator.next(); + if (first.done) continue; + const extra = iterator.next(); + if (!extra.done) { + throw new Error(`Shared checkpoint source emitted multiple scalar records for ${String(field)}`); + } + } finally { + iterator.return?.(); + } + if (!firstField) yield ","; + firstField = false; + yield JSON.stringify(field); + yield ":"; + yield *jsonChunks(first.value); + } + yield "}}"; +} + interface JsonChunkAccounting { logicalUtf8Bytes: number; peakSourceChunkUtf8Bytes: number; @@ -1051,6 +1299,7 @@ interface JsonChunkAccounting { interface CheckpointIdentity { id: string; checkpointGraph: CheckpointWriteAccountingV1["checkpointGraph"]; + engineSource?: NonNullable; } function exactByteAdd(description: string, total: number, value: number): number { @@ -1199,6 +1448,51 @@ function checkpointIdentity(entrypoint: string, checkpoint: SharedSearchCheckpoi }; } +async function checkpointIdentityFromSource( + entrypoint: string, + source: SharedSearchCheckpointSourceV1, + signal?: AbortSignal +): Promise { + const hash = createHash("sha256").update(entrypoint).update("\0"); + const accounting: JsonChunkAccounting = { + logicalUtf8Bytes: 0, + peakSourceChunkUtf8Bytes: 0, + }; + let chunksSinceYield = 0; + let bytesSinceYield = 0; + for (const chunk of checkpointSourceJsonChunks(source)) { + signal?.throwIfAborted(); + accountJsonChunk(accounting, chunk); + hash.update(chunk); + chunksSinceYield++; + bytesSinceYield += Buffer.byteLength(chunk); + if (chunksSinceYield >= 1_024 || bytesSinceYield >= 1024 * 1024) { + chunksSinceYield = 0; + bytesSinceYield = 0; + await new Promise((resolve) => setImmediate(resolve)); + } + } + signal?.throwIfAborted(); + return { + id: `checkpoint-${hash.digest("hex").slice(0, 24)}`, + checkpointGraph: { + count: 0, + logicalUtf8Bytes: 0, + peakSourceChunkUtf8Bytes: 0, + }, + engineSource: { + schemaVersion: 1, + status: "applied", + materializedCheckpointGraphs: 0, + identityPasses: 1, + framePasses: 0, + reuseVerificationPasses: 0, + logicalCheckpointUtf8BytesVisited: accounting.logicalUtf8Bytes, + peakSourceChunkUtf8Bytes: accounting.peakSourceChunkUtf8Bytes, + }, + }; +} + function checkpointId(entrypoint: string, checkpoint: SharedSearchCheckpoint): string { return checkpointIdentity(entrypoint, checkpoint).id; } @@ -1724,6 +2018,24 @@ async function loadFramedArtifactDetailed( const limits = checkpointReadLimits(inputLimits); const accounting = checkpointReadAccounting("framed-v2", limits); try { + const requestedFramedV2Limits = resolveCheckpointArtifactV2Limits(inputLimits.framedV2Limits); + const effectiveFramedV2Limits: CheckpointArtifactV2Limits = { + ...requestedFramedV2Limits, + maxTotalStoredBytes: Math.min(requestedFramedV2Limits.maxTotalStoredBytes, limits.maxStoredBytes), + }; + accounting.configuredLimits.framedV2 = { + basis: "effective_codec_limits_not_allocated_capacity", + maxHeaderBytes: effectiveFramedV2Limits.maxHeaderBytes, + maxStoredFrameBytes: effectiveFramedV2Limits.maxStoredFrameBytes, + maxDecodedFrameBytes: effectiveFramedV2Limits.maxDecodedFrameBytes, + maxRecordBytes: effectiveFramedV2Limits.maxRecordBytes, + maxJsonDepth: effectiveFramedV2Limits.maxJsonDepth, + maxRecordsPerFrame: effectiveFramedV2Limits.maxRecordsPerFrame, + maxFrames: effectiveFramedV2Limits.maxFrames, + maxTotalRecords: effectiveFramedV2Limits.maxTotalRecords, + maxTotalStoredBytes: effectiveFramedV2Limits.maxTotalStoredBytes, + maxTotalDecodedBytes: effectiveFramedV2Limits.maxTotalDecodedBytes, + }; inputLimits.signal?.throwIfAborted(); const manifestFile = checkpointManifestFile(projectRoot, id); if (!fs.existsSync(manifestFile)) { @@ -1736,13 +2048,8 @@ async function loadFramedArtifactDetailed( throw corrupt("manifest", "framed-v2 checkpoint metadata is missing its component summary"); } inputLimits.signal?.throwIfAborted(); - const maxNodeSlots = inputLimits.framedV2Limits?.maxTotalRecords - ?? DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS.maxTotalRecords; + const maxNodeSlots = effectiveFramedV2Limits.maxTotalRecords; const assembly = new CheckpointArtifactV2Assembly(maxNodeSlots); - const requestedStored = inputLimits.framedV2Limits?.maxTotalStoredBytes - ?? DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS.maxTotalStoredBytes; - const requestedDecoded = inputLimits.framedV2Limits?.maxTotalDecodedBytes - ?? DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS.maxTotalDecodedBytes; let codec: CheckpointArtifactV2ReadResult; let payloadSizeBytes = 0; let payloadSha256 = ""; @@ -1764,18 +2071,20 @@ async function loadFramedArtifactDetailed( const source = fs.createReadStream(file, { fd, autoClose: false, start: 0 }); const digest = new CheckpointReadDigestTransform(limits.maxStoredBytes); try { - [codec] = await Promise.all([ + const [codecSettlement, pipelineSettlement] = await Promise.allSettled([ readCheckpointArtifactV2(digest, { - limits: { - ...inputLimits.framedV2Limits, - maxTotalStoredBytes: Math.min(requestedStored, limits.maxStoredBytes), - maxTotalDecodedBytes: Math.min(requestedDecoded, limits.maxDecompressedBytes), - }, + // maxDecompressedBytes is the schema-v1 whole-string ceiling. A + // framed reader never constructs that string, so its explicitly + // bounded cumulative decoded budget is independent. + limits: effectiveFramedV2Limits, signal: inputLimits.signal, onRecord: (record) => assembly.add(record), }), pipeline(source, digest), ]); + if (codecSettlement.status === "rejected") throw codecSettlement.reason; + if (pipelineSettlement.status === "rejected") throw pipelineSettlement.reason; + codec = codecSettlement.value; const after = fs.fstatSync(fd); let visible: fs.Stats; try { @@ -1834,6 +2143,13 @@ async function loadFramedArtifactDetailed( finalizeCheckpointReadAccounting(accounting); return loaded; } catch (error) { + if (error instanceof CheckpointArtifactV2Error) { + const translated = checkpointReadErrorFromV2(error); + if (translated instanceof CheckpointReadError) { + translated.accounting = finalizeCheckpointReadAccounting(accounting, translated); + } + throw translated; + } if (error instanceof CheckpointReadError) { error.accounting = finalizeCheckpointReadAccounting(accounting, error); } @@ -1883,10 +2199,11 @@ function summary(projectRoot: string, loaded: LoadedCheckpointArtifact): Checkpo } function manifestForArtifact( - artifact: CheckpointArtifact, + artifact: CheckpointArtifactMetadata, storageEncoding: CheckpointStorageEncoding, artifactSizeBytes: number, artifactSha256: string, + expected: ExpectedCheckpointSummary, framedV2?: CheckpointArtifactV2ManifestSummary ): CheckpointArtifactManifest { const framed = storageEncoding === "framed-v2"; @@ -1907,9 +2224,9 @@ function manifestForArtifact( inkcheckVersion: artifact.inkcheckVersion, checkpointSchemaVersion: artifact.checkpointSchemaVersion, entrypoint: artifact.source.entrypoint, - engine: artifact.checkpoint.engine, - totalGranted: artifact.checkpoint.state.totalGranted, - statesExplored: artifact.checkpoint.state.statesExplored, + engine: expected.engine, + totalGranted: expected.totalGranted, + statesExplored: expected.statesExplored, storageEncoding, artifactSizeBytes, artifactSha256, @@ -2166,6 +2483,7 @@ function readCheckpointManifest( ): { manifest: CheckpointArtifactManifest; sizeBytes: number; + raw: string; } { const manifestFile = checkpointManifestFile(projectRoot, id); const stored = readBoundedBuffer( @@ -2188,7 +2506,7 @@ function readCheckpointManifest( if (accounting) { accounting.manifest.status = "applied"; } - return { manifest, sizeBytes: stored.length }; + return { manifest, sizeBytes: stored.length, raw }; } function validateManifestStorage( @@ -2289,7 +2607,8 @@ function recordFromManifest(projectRoot: string, id: string, file: string): Chec function checkpointRecords( projectRoot: string, - inputLimits: CheckpointReadLimits = {} + inputLimits: CheckpointReadLimits = {}, + requireManifests = false ): CheckpointRecord[] { const directory = checkpointsDirectory(projectRoot); if (!fs.existsSync(directory)) return []; @@ -2308,6 +2627,12 @@ function checkpointRecords( if (checkpointStorageEncoding(file) === "framed-v2") { throw corrupt("manifest", `framed checkpoint ${id} is missing its required versioned metadata manifest`); } + if (requireManifests) { + throw unsupported( + "manifest", + `engine-native checkpoint storage requires a versioned metadata manifest for retained artifact ${id}; reopen or migrate it through the graph API first` + ); + } return { ...summary(projectRoot, loadLegacyArtifactDetailed(projectRoot, id, inputLimits)), file }; }); } @@ -2490,7 +2815,8 @@ async function writeCompressedArtifact( async function writeFramedArtifact( temporary: string, - artifact: CheckpointArtifact, + inputs: Iterable, + nodeSlots: number, limits: CheckpointByteStorageLimits, framedV2Limits: Partial | undefined, signal: AbortSignal | undefined @@ -2542,12 +2868,12 @@ async function writeFramedArtifact( const maxNodeSlots = framedV2Limits?.maxTotalRecords ?? DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS.maxTotalRecords; if (Number.isSafeInteger(maxNodeSlots) && maxNodeSlots > 0 - && artifact.checkpoint.state.nodes.length > maxNodeSlots) { + && nodeSlots > maxNodeSlots) { throw new CheckpointFramedPreflightError( "resource_limit", "record", - `framed checkpoint node-slot count ${artifact.checkpoint.state.nodes.length} exceeds maxTotalRecords`, - artifact.checkpoint.state.nodes.length, + `framed checkpoint node-slot count ${nodeSlots} exceeds maxTotalRecords`, + nodeSlots, maxNodeSlots, "count" ); @@ -2561,7 +2887,7 @@ async function writeFramedArtifact( let codec: CheckpointArtifactV2WriteResult; try { [codec] = await Promise.all([ - writeCheckpointArtifactV2(limiter, checkpointArtifactV2Frames(artifact), { + writeCheckpointArtifactV2(limiter, inputs, { limits: codecLimits, signal, }), @@ -2599,9 +2925,10 @@ function oldestFirst(a: T, b: T): n function pruneCheckpoints( projectRoot: string, protectedId: string, - limits: CheckpointByteStorageLimits + limits: CheckpointByteStorageLimits, + requireManifests = false ): string[] { - let records = checkpointRecords(projectRoot); + let records = checkpointRecords(projectRoot, {}, requireManifests); const removed: string[] = []; const remove = (record: CheckpointRecord) => { fs.rmSync(record.file, { force: true }); @@ -2843,7 +3170,14 @@ function acquireRecoverySlotCleaning( syncDirectory(checkpointsDirectory(projectRoot)); return file; } catch (error) { - if ((error as NodeJS.ErrnoException).code === "EEXIST") return undefined; + const code = (error as NodeJS.ErrnoException).code; + // Windows can surface ERROR_ACCESS_DENIED/SHARING_VIOLATION as EPERM, + // EBUSY, or EACCES while another process removes or still holds this + // fixed lock pathname. None proves ownership, so fail closed at this slot + // and let the bounded reservation scan continue. + if (code === "EEXIST" || code === "EPERM" || code === "EBUSY" || code === "EACCES") { + return undefined; + } throw error; } } @@ -2981,7 +3315,7 @@ function manifestMatchesVisiblePayload( manifest: CheckpointArtifactManifest, payload: { sizeBytes: number; sha256: string }, relative: string, - checkpoint: SharedSearchCheckpoint + expected: ExpectedCheckpointSummary ): boolean { const storageEncoding = checkpointStorageEncoding(file); return manifestMatchesCheckpointPayload( @@ -2990,7 +3324,7 @@ function manifestMatchesVisiblePayload( manifest, payload, relative, - checkpoint + expected ) && manifest.storageEncoding === storageEncoding; } @@ -3000,7 +3334,7 @@ function manifestMatchesCheckpointPayload( manifest: CheckpointArtifactManifest, payload: { sizeBytes: number; sha256: string }, relative: string, - checkpoint: SharedSearchCheckpoint + expected: ExpectedCheckpointSummary ): boolean { try { validateStoredEntrypoint(projectRoot, manifest.entrypoint, "manifest"); @@ -3010,7 +3344,7 @@ function manifestMatchesCheckpointPayload( return manifest.id === id && manifest.artifactSizeBytes === payload.sizeBytes && manifest.artifactSha256 === payload.sha256 - && expectedManifestMatchesCheckpoint(manifest, relative, checkpoint); + && expectedManifestMatchesCheckpoint(manifest, relative, expected); } function matchingRecoveryManifest( @@ -3019,7 +3353,7 @@ function matchingRecoveryManifest( file: string, payload: { sizeBytes: number; sha256: string }, relative: string, - checkpoint: SharedSearchCheckpoint + expected: ExpectedCheckpointSummary ): RecoveryManifest | undefined { for (const candidate of recoveryManifestSourceFiles(projectRoot, id)) { try { @@ -3031,7 +3365,7 @@ function matchingRecoveryManifest( recovery.manifest, payload, relative, - checkpoint + expected )) return recovery; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT" || error instanceof CheckpointReadError) continue; @@ -3046,7 +3380,7 @@ function matchingRecoveryManifestForNeutralPayload( id: string, payload: { sizeBytes: number; sha256: string }, relative: string, - checkpoint: SharedSearchCheckpoint + expected: ExpectedCheckpointSummary ): RecoveryManifest | undefined { let match: RecoveryManifest | undefined; for (const candidate of recoveryManifestSourceFiles(projectRoot, id)) { @@ -3058,7 +3392,7 @@ function matchingRecoveryManifestForNeutralPayload( recovery.manifest, payload, relative, - checkpoint + expected )) continue; if (match && match.raw !== recovery.raw) { throw corrupt("manifest", `checkpoint ${id} has ambiguous recovery metadata for its committed payload`); @@ -3079,7 +3413,7 @@ function readMatchingCanonicalManifest( file: string, payload: { sizeBytes: number; sha256: string }, relative: string, - checkpoint: SharedSearchCheckpoint, + expected: ExpectedCheckpointSummary, expectedRaw?: string ): StoredCheckpointManifest | undefined { const manifestFile = checkpointManifestFile(projectRoot, id); @@ -3103,7 +3437,7 @@ function readMatchingCanonicalManifest( manifest, payload, relative, - checkpoint + expected )) return undefined; return { file: manifestFile, raw, manifest, sizeBytes: stored.length }; } catch (error) { @@ -3122,7 +3456,7 @@ function promoteRecoveryManifest( file: string, payload: { sizeBytes: number; sha256: string }, relative: string, - checkpoint: SharedSearchCheckpoint, + expected: ExpectedCheckpointSummary, recovery: RecoveryManifest ): StoredCheckpointManifest { const directory = checkpointsDirectory(projectRoot); @@ -3135,7 +3469,7 @@ function promoteRecoveryManifest( file, payload, relative, - checkpoint, + expected, recovery.raw ); const alreadyPublished = matches(); @@ -3432,7 +3766,7 @@ function recoverPublishedCheckpoint( projectRoot: string, relative: string, id: string, - checkpoint: SharedSearchCheckpoint, + expected: ExpectedCheckpointSummary, limits: CheckpointByteStorageLimits, expectedTransaction?: CheckpointTransaction ): RecoveredCheckpointPair | undefined { @@ -3466,7 +3800,7 @@ function recoverPublishedCheckpoint( file, payload, relative, - checkpoint + expected ); if (canonical) { expectedManifestRaw = canonical.raw; @@ -3477,7 +3811,7 @@ function recoverPublishedCheckpoint( file, payload, relative, - checkpoint + expected ); if (!recovery) return undefined; promoteRecoveryManifest( @@ -3486,7 +3820,7 @@ function recoverPublishedCheckpoint( file, payload, relative, - checkpoint, + expected, recovery ); expectedManifestRaw = recovery.raw; @@ -3500,7 +3834,7 @@ function recoverPublishedCheckpoint( projectRoot, relative, id, - checkpoint, + expected, limits, expectedTransaction ); @@ -3513,7 +3847,7 @@ function recoverPublishedCheckpoint( id, committed, relative, - checkpoint + expected ); } catch (error) { // A public winner can appear while a later, larger neutral is being @@ -3526,7 +3860,7 @@ function recoverPublishedCheckpoint( projectRoot, relative, id, - checkpoint, + expected, limits, expectedTransaction ); @@ -3539,7 +3873,7 @@ function recoverPublishedCheckpoint( projectRoot, relative, id, - checkpoint, + expected, limits, expectedTransaction ); @@ -3571,7 +3905,7 @@ function recoverPublishedCheckpoint( file, payload, relative, - checkpoint, + expected, recovery ); expectedManifestRaw = recovery.raw; @@ -3595,7 +3929,7 @@ function recoverPublishedCheckpoint( file, verifiedPayload, relative, - checkpoint, + expected, expectedManifestRaw ); if (!verifiedManifest) { @@ -3721,12 +4055,12 @@ function enforceDurableCheckpointLimits( function expectedManifestMatchesCheckpoint( manifest: CheckpointArtifactManifest, relative: string, - checkpoint: SharedSearchCheckpoint + expected: ExpectedCheckpointSummary ): boolean { return manifest.entrypoint === relative - && manifest.engine === checkpoint.engine - && manifest.totalGranted === checkpoint.state.totalGranted - && manifest.statesExplored === checkpoint.state.statesExplored; + && manifest.engine === expected.engine + && manifest.totalGranted === expected.totalGranted + && manifest.statesExplored === expected.statesExplored; } function checkpointWriteAccounting( @@ -3736,6 +4070,7 @@ function checkpointWriteAccounting( operation?: { outcome: CheckpointWriteAccountingV1["outcome"]; written?: CheckpointArtifactWriteAccounting; + reuseVerificationPasses?: 0 | 1; } ): CheckpointWriteAccountingV1 { const written = operation?.written; @@ -3747,6 +4082,13 @@ function checkpointWriteAccounting( schemaVersion: 1, outcome: operation?.outcome ?? "reused", checkpointGraph: identity.checkpointGraph, + ...(identity.engineSource ? { + engineSource: { + ...identity.engineSource, + framePasses: written?.framedV2 ? 1 : 0, + reuseVerificationPasses: operation?.reuseVerificationPasses ?? 0, + } satisfies NonNullable, + } : {}), serialization: written?.serialization ?? { status: "not_applied", logicalArtifactUtf8BytesEmitted: 0, @@ -3766,28 +4108,256 @@ function checkpointWriteAccounting( }; } +interface SourceNativeReuseContext { + source: SharedSearchCheckpointSourceV1; + signal?: AbortSignal; + framedV2Limits?: Partial; +} + +async function verifyCanonicalPublishedCheckpointForSource( + root: string, + relative: string, + id: string, + expected: ExpectedCheckpointSummary, + limits: CheckpointByteStorageLimits, + context: SourceNativeReuseContext +): Promise { + const file = checkpointFile(root, id); + if (!fs.existsSync(checkpointManifestFile(root, id))) { + throw unsupported( + "manifest", + "engine-native checkpoint reuse requires a versioned metadata manifest" + ); + } + if (checkpointStorageEncoding(file) !== "framed-v2") { + throw unsupported( + "storage", + "engine-native checkpoint reuse requires framed-v2 storage; reopen or migrate the legacy artifact through the graph API first" + ); + } + const stored = readCheckpointManifest(root, id); + validateManifestStorage(root, id, file, stored.manifest); + if (!expectedManifestMatchesCheckpoint(stored.manifest, relative, expected)) { + throw corrupt("manifest", "checkpoint metadata manifest does not match the requested saved frontier"); + } + const pairBytes = exactByteSum( + "durable checkpoint pair byte count", + [stored.manifest.artifactSizeBytes, stored.sizeBytes] + ); + enforceDurableCheckpointLimits(pairBytes, limits); + const digestLimit = Math.min(limits.maxCheckpointBytes, limits.maxProjectBytes); + if (!stored.manifest.framedV2) { + throw corrupt("manifest", "framed-v2 checkpoint metadata is missing its component summary"); + } + + const artifactMetadata: CheckpointArtifactMetadata = { + artifactSchemaVersion: CHECKPOINT_ARTIFACT_V2_SCHEMA_VERSION, + artifactType: "shared-search-checkpoint", + id, + createdAt: stored.manifest.createdAt, + inkcheckVersion: stored.manifest.inkcheckVersion, + checkpointSchemaVersion: stored.manifest.checkpointSchemaVersion, + source: { entrypoint: relative }, + storySha256: expected.configuration.storySha256, + knotsSha256: expected.configuration.knotsSha256, + configuration: expected.configuration, + }; + const expectedRecords = checkpointArtifactV2RecordsFromSource( + artifactMetadata, + context.source + ); + const requestedStored = context.framedV2Limits?.maxTotalStoredBytes + ?? DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS.maxTotalStoredBytes; + const requestedDecoded = context.framedV2Limits?.maxTotalDecodedBytes + ?? DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS.maxTotalDecodedBytes; + const fd = fs.openSync(file, "r"); + const opened = fs.fstatSync(fd); + if (!opened.isFile()) { + expectedRecords.return(undefined); + fs.closeSync(fd); + throw corrupt("storage", "stored checkpoint artifact must be a regular file"); + } + if (opened.size > digestLimit) { + expectedRecords.return(undefined); + fs.closeSync(fd); + throw resourceLimit( + "storage", + `stored checkpoint artifact is ${opened.size} bytes, above the ${digestLimit}-byte source-native verification limit`, + opened.size, + digestLimit + ); + } + const source = fs.createReadStream(file, { fd, autoClose: false, start: 0 }); + const digest = new CheckpointReadDigestTransform(digestLimit); + let comparisonError: CheckpointReadError | undefined; + let codec: CheckpointArtifactV2ReadResult; + let payload: CheckpointPayloadDigest; + try { + context.signal?.throwIfAborted(); + const [codecSettlement, pipelineSettlement] = await Promise.allSettled([ + readCheckpointArtifactV2(digest, { + limits: { + ...context.framedV2Limits, + maxTotalStoredBytes: Math.min(requestedStored, digestLimit), + maxTotalDecodedBytes: requestedDecoded, + }, + signal: context.signal, + onRecord: (record) => { + const expectedRecord = expectedRecords.next(); + if (expectedRecord.done + || expectedRecord.value.component !== record.component + || expectedRecord.value.field !== record.field + || expectedRecord.value.index !== record.index + || !canonicalJsonValuesMatch(expectedRecord.value.value, record.value)) { + comparisonError = corrupt( + "envelope", + "checkpoint artifact content does not match its live source or stable ID; restore or regenerate the artifact" + ); + throw comparisonError; + } + }, + }), + pipeline(source, digest), + ]); + if (codecSettlement.status === "rejected") throw codecSettlement.reason; + if (pipelineSettlement.status === "rejected") throw pipelineSettlement.reason; + codec = codecSettlement.value; + const extra = expectedRecords.next(); + if (!extra.done) { + throw corrupt( + "envelope", + "checkpoint artifact content does not match its live source or stable ID; restore or regenerate the artifact" + ); + } + const after = fs.fstatSync(fd); + let visible: fs.Stats; + try { + visible = fs.statSync(file); + } catch { + throw corrupt("storage", "checkpoint artifact path changed during source-native verification"); + } + const openedIdentityChanged = opened.dev !== 0 && after.dev !== 0 + && (opened.dev !== after.dev || opened.ino !== after.ino); + const pathIdentityChanged = opened.dev !== 0 && visible.dev !== 0 + && (opened.dev !== visible.dev || opened.ino !== visible.ino); + if (openedIdentityChanged || pathIdentityChanged || after.size !== opened.size + || visible.size !== opened.size || digest.bytes !== opened.size) { + throw corrupt("storage", "checkpoint artifact changed during source-native verification"); + } + payload = { + sizeBytes: digest.bytes, + sha256: digest.digest(), + device: after.dev, + inode: after.ino, + }; + } catch (error) { + if (!source.destroyed) source.destroy(); + if (comparisonError) throw comparisonError; + if (context.signal?.aborted) throw checkpointAbortError(); + if (error instanceof CheckpointArtifactV2Error) throw checkpointReadErrorFromV2(error); + throw error; + } finally { + expectedRecords.return(undefined); + try { + fs.closeSync(fd); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EBADF") throw error; + } + } + if (payload.sha256 !== stored.manifest.artifactSha256) { + throw corrupt( + "storage", + "checkpoint artifact bytes do not match its metadata checksum; content does not match its stable ID or was modified" + ); + } + const framedV2 = checkpointArtifactV2Summary(codec); + if (JSON.stringify(framedV2) !== JSON.stringify(stored.manifest.framedV2)) { + throw corrupt("manifest", "framed checkpoint component summary does not match its streamed payload"); + } + const canonical = readMatchingCanonicalManifest( + root, + id, + file, + payload, + relative, + expected, + stored.raw + ); + if (!canonical) { + throw corrupt("manifest", `checkpoint ${id} metadata changed during bounded source-native reuse`); + } + context.signal?.throwIfAborted(); + const verifiedPayload = fileDigest(file, digestLimit); + if (!samePayloadDigest(payload, verifiedPayload)) { + throw corrupt("storage", `checkpoint ${id} payload changed during bounded source-native reuse`); + } + const verifiedManifest = readMatchingCanonicalManifest( + root, + id, + file, + verifiedPayload, + relative, + expected, + canonical.raw + ); + if (!verifiedManifest) { + throw corrupt("manifest", `checkpoint ${id} metadata changed during bounded source-native reuse`); + } + return { + manifest: verifiedManifest.manifest, + metadataSizeBytes: verifiedManifest.sizeBytes, + payloadSizeBytes: verifiedPayload.sizeBytes, + payloadSha256: verifiedPayload.sha256, + }; +} + async function reuseCheckpointArtifact( root: string, relative: string, identity: CheckpointIdentity, - checkpoint: SharedSearchCheckpoint, + expected: ExpectedCheckpointSummary, limits: CheckpointByteStorageLimits, expectedTransaction?: CheckpointTransaction, operation?: { outcome: CheckpointWriteAccountingV1["outcome"]; written?: CheckpointArtifactWriteAccounting; - } + reuseVerificationPasses?: 0 | 1; + }, + sourceContext?: SourceNativeReuseContext ): Promise { const { id } = identity; const directory = checkpointsDirectory(root); let existing = checkpointFile(root, id); let payloadBytes: number; let manifestBytes: number; - const recovered = recoverPublishedCheckpoint(root, relative, id, checkpoint, limits, expectedTransaction); - if (recovered) { + let reuseVerificationPasses: 0 | 1 = 0; + const recovered = recoverPublishedCheckpoint(root, relative, id, expected, limits, expectedTransaction); + if (identity.engineSource && (!recovered || operation?.outcome !== "created")) { + if (!sourceContext) { + throw new Error("engine-native checkpoint reuse requires its live source context"); + } + // A manifest/payload checksum pair authenticates storage bytes, not the + // logical frontier behind this stable ID. Re-read framed records under + // bounded codec limits and compare them directly with the paused source. + const verified = await verifyCanonicalPublishedCheckpointForSource( + root, + relative, + id, + expected, + limits, + sourceContext + ); + existing = checkpointFile(root, id); + payloadBytes = verified.payloadSizeBytes; + manifestBytes = verified.metadataSizeBytes; + reuseVerificationPasses = 1; + cleanupRecoveryManifests(root, id, expectedTransaction); + } else if (recovered) { existing = checkpointFile(root, id); payloadBytes = recovered.payloadSizeBytes; manifestBytes = recovered.metadataSizeBytes; + } else if (identity.engineSource) { + throw new Error("new engine-native checkpoint publication lost its recovery metadata"); } else { try { const loaded = await loadArtifactDetailed(root, id); @@ -3797,7 +4367,13 @@ async function reuseCheckpointArtifact( loaded.artifact, loaded.storageEncoding, loaded.payloadSizeBytes, - loaded.payloadSha256 + loaded.payloadSha256, + { + engine: loaded.artifact.checkpoint.engine, + configuration: loaded.artifact.checkpoint.configuration, + totalGranted: loaded.artifact.checkpoint.state.totalGranted, + statesExplored: loaded.artifact.checkpoint.state.statesExplored, + } ); const raw = serializedManifest(manifest); enforceDurableCheckpointLimits(exactByteSum( @@ -3819,7 +4395,7 @@ async function reuseCheckpointArtifact( if (!fs.existsSync(checkpointManifestFile(root, id))) throw error; const { manifest, sizeBytes: metadataSizeBytes } = readCheckpointManifest(root, id); validateManifestStorage(root, id, existing, manifest); - if (!expectedManifestMatchesCheckpoint(manifest, relative, checkpoint)) { + if (!expectedManifestMatchesCheckpoint(manifest, relative, expected)) { throw corrupt("manifest", "checkpoint metadata manifest does not match the requested saved frontier"); } payloadBytes = manifest.artifactSizeBytes; @@ -3853,14 +4429,21 @@ async function reuseCheckpointArtifact( const manifestFile = checkpointManifestFile(root, id); if (fs.existsSync(manifestFile)) fs.chmodSync(manifestFile, 0o600); } - const pruned = pruneCheckpoints(root, id, limits); + const pruned = pruneCheckpoints(root, id, limits, identity.engineSource !== undefined); if (pruned.length > 0) syncDirectory(directory); const encoding = checkpointStorageEncoding(checkpointFile(root, id)); return { id, path: checkpointRelativePath(id, encoding), pruned, - accounting: checkpointWriteAccounting(identity, payloadBytes, manifestBytes, operation), + accounting: checkpointWriteAccounting( + identity, + payloadBytes, + manifestBytes, + reuseVerificationPasses === 1 + ? { ...(operation ?? { outcome: "reused" }), reuseVerificationPasses } + : operation + ), }; } @@ -3868,13 +4451,17 @@ async function saveCheckpointArtifactExclusive( root: string, relative: string, identity: CheckpointIdentity, - checkpoint: SharedSearchCheckpoint, + input: CheckpointSaveInput, limits: CheckpointByteStorageLimits, format: CheckpointArtifactFormat, signal?: AbortSignal, framedV2Limits?: Partial ): Promise { const { id } = identity; + const expected = expectedCheckpointSummary(input); + const sourceContext: SourceNativeReuseContext | undefined = input.kind === "engine-source" + ? { source: input.source, signal, framedV2Limits } + : undefined; const directory = checkpointsDirectory(root); const destination = checkpointDestination(root, id, format); const neutral = checkpointPublicationPayloadFile(root, id); @@ -3882,27 +4469,33 @@ async function saveCheckpointArtifactExclusive( || checkpointCandidateFiles(root, id).some((candidate) => fs.existsSync(candidate)); if (hasCommittedCandidate()) { signal?.throwIfAborted(); - return reuseCheckpointArtifact(root, relative, identity, checkpoint, limits); + return reuseCheckpointArtifact( + root, relative, identity, expected, limits, undefined, undefined, sourceContext + ); } // Validate the existing retention set before creating a new durable file. // Corrupt old state must not turn a successful write into a partial cleanup. try { - checkpointRecords(root); + checkpointRecords(root, {}, input.kind === "engine-source"); } catch (error) { // A same-ID writer can publish between the initial existence check and // retention validation. Reopen its durable transaction instead of making // validation inflate a sidecar-free (and potentially huge) frontier. if (hasCommittedCandidate()) { signal?.throwIfAborted(); - return reuseCheckpointArtifact(root, relative, identity, checkpoint, limits); + return reuseCheckpointArtifact( + root, relative, identity, expected, limits, undefined, undefined, sourceContext + ); } throw error; } if (hasCommittedCandidate()) { signal?.throwIfAborted(); - return reuseCheckpointArtifact(root, relative, identity, checkpoint, limits); + return reuseCheckpointArtifact( + root, relative, identity, expected, limits, undefined, undefined, sourceContext + ); } - const artifact: CheckpointArtifact = { + const artifactMetadata: CheckpointArtifactMetadata = { artifactSchemaVersion: format === "framed-v2" ? CHECKPOINT_ARTIFACT_V2_SCHEMA_VERSION : CHECKPOINT_ARTIFACT_SCHEMA_VERSION, @@ -3912,11 +4505,13 @@ async function saveCheckpointArtifactExclusive( inkcheckVersion: VERSION, checkpointSchemaVersion: SHARED_SEARCH_CHECKPOINT_SCHEMA_VERSION, source: { entrypoint: relative }, - storySha256: checkpoint.configuration.storySha256, - knotsSha256: checkpoint.configuration.knotsSha256, - configuration: checkpoint.configuration, - checkpoint, + storySha256: expected.configuration.storySha256, + knotsSha256: expected.configuration.knotsSha256, + configuration: expected.configuration, }; + const graphArtifact: CheckpointArtifact | undefined = input.kind === "checkpoint-graph" + ? { ...artifactMetadata, checkpoint: input.checkpoint } + : undefined; checkpointTestBarrier("before-reserve-transaction"); let transaction: CheckpointTransaction; try { @@ -3931,7 +4526,9 @@ async function saveCheckpointArtifactExclusive( // this loser is scanning the bounded recovery slots. Its verified durable // pair makes a slot/preflight failure irrelevant to this logical ID. if (hasCommittedCandidate()) { - return reuseCheckpointArtifact(root, relative, identity, checkpoint, limits); + return reuseCheckpointArtifact( + root, relative, identity, expected, limits, undefined, undefined, sourceContext + ); } throw error; } @@ -3950,17 +4547,37 @@ async function saveCheckpointArtifactExclusive( root, relative, identity, - checkpoint, + expected, limits, - transaction + transaction, + undefined, + sourceContext ); completedPair = true; return reference; } try { - written = format === "framed-v2" - ? await writeFramedArtifact(temporary, artifact, limits, framedV2Limits, signal) - : await writeCompressedArtifact(temporary, artifact, limits, true, signal); + if (format === "framed-v2") { + const inputs = input.kind === "checkpoint-graph" + ? checkpointArtifactV2Frames(graphArtifact!) + : checkpointArtifactV2FramesFromSource(artifactMetadata, input.source); + const nodeSlots = input.kind === "checkpoint-graph" + ? input.checkpoint.state.nodes.length + : input.source.nodeSlots; + written = await writeFramedArtifact( + temporary, + inputs, + nodeSlots, + limits, + framedV2Limits, + signal + ); + } else { + if (!graphArtifact) { + throw new RangeError("Engine-native checkpoint sources require framed-v2 storage"); + } + written = await writeCompressedArtifact(temporary, graphArtifact, limits, true, signal); + } } catch (error) { // Cancellation wins even if another writer committed while this request // was validating its format-specific limits. @@ -3976,19 +4593,22 @@ async function saveCheckpointArtifactExclusive( root, relative, identity, - checkpoint, + expected, limits, - transaction + transaction, + undefined, + sourceContext ); completedPair = true; return reference; } signal?.throwIfAborted(); const manifest = manifestForArtifact( - artifact, + artifactMetadata, format === "framed-v2" ? "framed-v2" : "gzip", written.sizeBytes, written.sha256, + expected, written.framedV2Manifest ); const rawManifest = serializedManifest(manifest); @@ -4076,13 +4696,14 @@ async function saveCheckpointArtifactExclusive( root, relative, identity, - checkpoint, + expected, limits, transaction, { outcome: publishedPayload ? "created" : "reused", written, - } + }, + sourceContext ); completedPair = true; return reference; @@ -4103,13 +4724,14 @@ async function saveCheckpointArtifactExclusive( root, relative, identity, - checkpoint, + expected, limits, transaction, written ? { outcome: publishedPayload ? "created" : "reused", written, - } : undefined + } : undefined, + sourceContext ); completedPair = true; return reference; @@ -4152,7 +4774,7 @@ export async function saveCheckpointArtifact( root, relative, identity, - checkpoint, + { kind: "checkpoint-graph", checkpoint }, limits, format, inputLimits.signal, @@ -4160,6 +4782,42 @@ export async function saveCheckpointArtifact( ); } +/** + * Persist a callback-scoped engine source directly through the framed-v2 + * codec. The logical schema and stable ID are identical to the materialized + * checkpoint path, but no complete checkpoint graph is constructed. + */ +export async function saveCheckpointArtifactFromSource( + projectRoot: string, + entrypoint: string, + source: SharedSearchCheckpointSourceV1, + inputLimits: CheckpointStorageLimits = {} +): Promise { + assertLiveSharedSearchCheckpointSourceV1(source); + if (inputLimits.format !== undefined && inputLimits.format !== "framed-v2") { + throw new RangeError("Engine-native checkpoint sources require framed-v2 storage"); + } + const root = path.resolve(projectRoot); + const relative = relativeEntrypoint(root, entrypoint); + const limits = storageLimits(inputLimits); + inputLimits.signal?.throwIfAborted(); + const identity = await checkpointIdentityFromSource(relative, source, inputLimits.signal); + const directory = checkpointsDirectory(root); + inputLimits.signal?.throwIfAborted(); + fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); + if (process.platform !== "win32") fs.chmodSync(directory, 0o700); + return saveCheckpointArtifactExclusive( + root, + relative, + identity, + { kind: "engine-source", source }, + limits, + "framed-v2", + inputLimits.signal, + inputLimits.framedV2Limits + ); +} + export function listCheckpointArtifacts( projectRoot: string, readLimits: CheckpointReadLimits = {} diff --git a/src/explore.ts b/src/explore.ts index b234516..717706d 100644 --- a/src/explore.ts +++ b/src/explore.ts @@ -1649,7 +1649,11 @@ function recordEnding( const retained = !endings.has(key); endings.set(key, ending); visibleOutcomes.add(visibleOutcomeKey(ending.finalText)); - if (retained) onEvidence?.({ kind: "ending", finding: ending }); + // The retained finding is part of the live checkpoint graph. Never hand the + // same object to caller code: an evidence callback may keep and mutate it + // while an async checkpoint-source consumer is between its identity and + // frame passes. + if (retained) onEvidence?.({ kind: "ending", finding: cloneJsonValue(ending) }); } function recordRuntimeError( @@ -1660,7 +1664,9 @@ function recordRuntimeError( ): void { if (runtimeErrors.has(key)) return; runtimeErrors.set(key, error); - onEvidence?.({ kind: "runtime-error", finding: error }); + // Keep callback ownership disjoint from the engine's retained checkpoint + // state for the same reason as endings above. + onEvidence?.({ kind: "runtime-error", finding: cloneJsonValue(error) }); } /** @@ -2293,8 +2299,184 @@ export interface SharedSearchCheckpoint { }; } +/** + * Callback-scoped, repeatable view of an engine checkpoint. Large retained + * collections are exposed as record iterables so a framed writer never needs + * to construct the complete SharedSearchCheckpoint graph. + * + * Every iterator returns detached JSON values and becomes invalid as soon as + * the callback supplied to exploreSharedResumableWithCheckpointSource returns. + */ +export interface SharedSearchCheckpointSourceV1 { + sourceSchemaVersion: 1; + checkpointSchemaVersion: 1; + engine: SharedSearchCheckpoint["engine"]; + configuration: Readonly; + statesExplored: number; + totalGranted: number; + nodeSlots: number; + stateRecords(field: keyof SharedSearchCheckpoint["state"]): Iterable; + nodePayloadRecords(): Iterable; + nodeAncestryRecords(): Iterable; +} + +interface SharedSearchCheckpointSourceRegistrationV1 { + active: boolean; +} + +const sharedSearchCheckpointSourceRegistrationsV1 = new WeakMap< + SharedSearchCheckpointSourceV1, + SharedSearchCheckpointSourceRegistrationV1 +>(); + +/** + * Fail closed unless a source was minted by a live shared-search engine and is + * still inside its callback lifetime. Storage callers use this runtime brand + * rather than accepting a structurally fabricated record source. + */ +export function assertLiveSharedSearchCheckpointSourceV1( + value: unknown +): asserts value is SharedSearchCheckpointSourceV1 { + if (!value || typeof value !== "object") { + throw new TypeError("Shared checkpoint source must be minted by a live shared-search engine"); + } + const registration = sharedSearchCheckpointSourceRegistrationsV1.get( + value as SharedSearchCheckpointSourceV1 + ); + if (!registration) { + throw new TypeError("Shared checkpoint source must be minted by a live shared-search engine"); + } + if (!registration.active) { + throw new Error("Shared checkpoint source is no longer active outside its callback"); + } +} + +interface SharedSearchCheckpointSourceLeaseV1 { + source: SharedSearchCheckpointSourceV1; + release(): void; +} + +interface SharedSearchCheckpointSourceBackingV1 { + stateRecords(field: keyof SharedSearchCheckpoint["state"]): Iterable; + nodePayloadRecords(): Iterable; + nodeAncestryRecords(): Iterable; +} + +interface SharedSearchCheckpointSourceIteratorSlotV1 { + iterator?: Iterator; +} + +interface SharedSearchCheckpointSourceCellV1 { + backing?: SharedSearchCheckpointSourceBackingV1; + registration: SharedSearchCheckpointSourceRegistrationV1; + activeIterators: Set; +} + +type SharedSearchCheckpointSourceMetadataV1 = Omit< + SharedSearchCheckpointSourceV1, + "stateRecords" | "nodePayloadRecords" | "nodeAncestryRecords" +>; + +/** + * Construct public wrappers in a lexical scope that owns only a mutable cell, + * never the engine collections captured by the backing methods. Releasing the + * lease can therefore sever both the backing object and every started iterator + * even when user code retains the public source forever. + */ +function createSharedSearchCheckpointSourceLeaseV1( + metadata: SharedSearchCheckpointSourceMetadataV1, + cell: SharedSearchCheckpointSourceCellV1 +): SharedSearchCheckpointSourceLeaseV1 { + const liveBacking = (): SharedSearchCheckpointSourceBackingV1 => { + if (!cell.registration.active || !cell.backing) { + throw new Error("Shared checkpoint source is no longer active outside its callback"); + } + return cell.backing; + }; + const leasedRecords = ( + kind: "state" | "nodePayload" | "nodeAncestry", + field?: keyof SharedSearchCheckpoint["state"] + ): Iterable => { + liveBacking(); + return { + [Symbol.iterator](): Iterator { + const selected = liveBacking(); + const records = kind === "state" + ? selected.stateRecords(field as keyof SharedSearchCheckpoint["state"]) + : kind === "nodePayload" + ? selected.nodePayloadRecords() + : selected.nodeAncestryRecords(); + const slot: SharedSearchCheckpointSourceIteratorSlotV1 = { + iterator: records[Symbol.iterator](), + }; + cell.activeIterators.add(slot); + return { + next(): IteratorResult { + liveBacking(); + const iterator = slot.iterator; + if (!iterator) { + throw new Error("Shared checkpoint source is no longer active outside its callback"); + } + let result: IteratorResult; + try { + result = iterator.next(); + } catch (error) { + slot.iterator = undefined; + cell.activeIterators.delete(slot); + throw error; + } + if (result.done) { + slot.iterator = undefined; + cell.activeIterators.delete(slot); + return { done: true, value: undefined }; + } + return { done: false, value: cloneJsonValue(result.value) }; + }, + return(value?: unknown): IteratorResult { + const iterator = slot.iterator; + slot.iterator = undefined; + cell.activeIterators.delete(slot); + if (!iterator?.return) return { done: true, value }; + const result = iterator.return(value); + return result.done + ? result + : { done: false, value: cloneJsonValue(result.value) }; + }, + }; + }, + }; + }; + const source: SharedSearchCheckpointSourceV1 = Object.freeze({ + ...metadata, + stateRecords: (field: keyof SharedSearchCheckpoint["state"]) => leasedRecords("state", field), + nodePayloadRecords: () => leasedRecords("nodePayload"), + nodeAncestryRecords: () => leasedRecords("nodeAncestry"), + }); + sharedSearchCheckpointSourceRegistrationsV1.set(source, cell.registration); + return { + source, + release(): void { + if (!cell.registration.active) return; + cell.registration.active = false; + cell.backing = undefined; + for (const slot of cell.activeIterators) { + const iterator = slot.iterator; + slot.iterator = undefined; + try { + iterator?.return?.(); + } catch { + // The source is already invalid. Always sever engine ownership even + // if an iterator's cleanup path itself fails. + } + } + cell.activeIterators.clear(); + }, + }; +} + interface SharedPassEngine extends PassEngine { checkpoint(): SharedSearchCheckpoint; + checkpointSource(): SharedSearchCheckpointSourceLeaseV1; } class SharedMaxHeap { @@ -2356,6 +2538,10 @@ class SharedMaxHeap { checkpoint(): SharedCheckpointHeapItem[] { return this.items.map((item) => ({ ...item })); } + + *checkpointRecords(): Generator { + for (const item of this.items) yield { ...item }; + } } class CheckpointMulberry32 { @@ -3585,6 +3771,17 @@ function createSharedEngine( opts: ExploreOptions, checkpoint?: SharedSearchCheckpoint ): SharedPassEngine { + // The source API deliberately awaits caller code while the engine is + // paused. Detach every mutable configuration input up front so reentrant or + // async caller mutation cannot change later findings, checkpoint records, + // final result fields, or callback selection. + knots = knots.map(({ name, isFunction, file, line }) => ({ name, isFunction, file, line })); + externals = [...externals]; + opts = { + ...opts, + ...(opts.assertions ? { assertions: cloneJsonValue(opts.assertions) } : {}), + ...(opts.goals ? { goals: cloneJsonValue(opts.goals) } : {}), + }; const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH; if (!Number.isSafeInteger(maxDepth) || maxDepth < 1 || maxDepth > 1_000) { throw new RangeError("maxDepth must be an integer from 1 to 1000"); @@ -3609,6 +3806,7 @@ function createSharedEngine( } const variableAware = opts.sharedVariableAware ?? false; const goalAware = opts.sharedGoalAware ?? false; + const reportGoalResults = (opts.goals?.length ?? 0) > 0; if (checkpoint && (variableAware || goalAware || opts.assertions?.length || opts.goals?.length)) { throw new RangeError("Invalid shared checkpoint: this schema supports only base shared search without assertions or goals"); } @@ -3639,8 +3837,11 @@ function createSharedEngine( && checkpoint.state.sharedObservability.sampleIntervalStates !== observabilityIntervalStates) { throw new RangeError("Invalid shared checkpoint: observability interval changed"); } + // Preserve caller ownership without constructing one artifact-sized JSON + // string. Framed readback can exceed the runtime's single-string ceiling; + // the validated flat checkpoint graph is therefore cloned structurally. const restored = checkpoint - ? JSON.parse(JSON.stringify(checkpoint.state)) as SharedSearchCheckpoint["state"] + ? cloneJsonValue(checkpoint.state) : undefined; const endings = new Map(restored?.endings ?? []); @@ -3653,7 +3854,11 @@ function createSharedEngine( const variableStateCounts = new Map(restored?.variableStateCounts ?? []); const variableTransitionCounts = new Map(restored?.variableTransitionCounts ?? []); let meaningfulVariableTransitions = restored?.meaningfulVariableTransitions ?? 0; - const nonFunctionKnots = knots.filter((k) => !k.isFunction); + // Snapshot caller-owned discovery metadata before any async checkpoint + // consumer can mutate the inputs used to build the final result. + const nonFunctionKnots = knots + .filter((k) => !k.isFunction) + .map(({ name, isFunction, file, line }) => ({ name, isFunction, file, line })); const nodes: Array = restored?.nodes.map((node) => node ?? undefined) ?? []; const deep: number[] = restored?.deep ?? []; const random: number[] = restored?.random ?? []; @@ -4785,25 +4990,31 @@ function createSharedEngine( endingsFound: [...endings.values()], runtimeErrors: [...runtimeErrors.values()], assertionResults: assertions.results(exhaustive), - ...(opts.goals?.length ? { goalResults: goals.results(exhaustive) } : {}), + ...(reportGoalResults ? { goalResults: goals.results(exhaustive) } : {}), runtimeWarnings: [...runtimeWarnings], unvisitedKnots: nonFunctionKnots .filter((knot) => !visitedKnots.has(knot.name)) .map(({ name, file, line }) => ({ name, file, line })), visitedKnots: [...visitedKnots], - externalFunctionsStubbed: [...externals], - randomnessDetected: opts.randomnessDetected ?? false, + externalFunctionsStubbed: [...checkpointConfiguration.externals], + randomnessDetected: checkpointConfiguration.randomnessDetected, truncated, truncatedBy, exhaustive, - limits: { maxDepth, maxStates: totalGranted, seed, storySeed: normalizeStorySeed(opts.storySeed) }, + limits: { maxDepth, maxStates: totalGranted, seed, storySeed: checkpointConfiguration.storySeed }, }); }; - const buildCheckpoint = (): SharedSearchCheckpoint => { + const assertCheckpointable = (): void => { if (done() || memoryStopped || timeStopped) { throw new RangeError("Shared search cannot checkpoint after completion or a resource stop"); } + }; + + const checkpointOwnerState = (): Pick< + SharedSearchCheckpoint["state"], + "sharedObservability" | "ownerAccounting" + > => { const sharedObservabilityBody = sharedObservabilityCheckpointBody(); const orderedSharedObservabilityBody = orderedSharedObservabilityCheckpointV2Body( sharedObservabilityBody, @@ -4818,6 +5029,15 @@ function createSharedEngine( if (!orderedSharedObservabilityBody || !integritySha256 || !emittedLedger) { throw new Error("Shared observability v2 checkpoint could not be canonicalized"); } + return { + sharedObservability: { ...orderedSharedObservabilityBody, integritySha256 }, + ownerAccounting: sharedOwnerAccountingCheckpoint(emittedLedger), + }; + }; + + const buildCheckpoint = (): SharedSearchCheckpoint => { + assertCheckpointable(); + const ownerState = checkpointOwnerState(); const value: SharedSearchCheckpoint = { schemaVersion: SHARED_SEARCH_CHECKPOINT_SCHEMA_VERSION, engine: "shared:deep-novelty-v1", @@ -4867,13 +5087,135 @@ function createSharedEngine( findingBytes, ancestryPayloadBytes, peakRetainedMemory: { ...peakRetainedMemory }, - sharedObservability: { ...orderedSharedObservabilityBody, integritySha256 }, - ownerAccounting: sharedOwnerAccountingCheckpoint(emittedLedger), + sharedObservability: ownerState.sharedObservability, + ownerAccounting: ownerState.ownerAccounting, }, }; return cloneJsonValue(value); }; + const checkpointSource = (): SharedSearchCheckpointSourceLeaseV1 => { + assertCheckpointable(); + const ownerState = checkpointOwnerState(); + const configuration = cloneJsonValue(checkpointConfiguration); + Object.freeze(configuration.externals); + Object.freeze(configuration); + const scalarState: Partial = { + meaningfulVariableTransitions, + rngState: rng.checkpoint(), + policyCursor, + insertionOrder, + current: cloneJsonValue(current), + pendingStates, + pendingBytes, + pendingVariableBytes, + activeStateBytes, + activeVariableBytes, + peakPendingStates, + peakPendingBytes, + retainedNodes, + dedupeBytes, + semanticIndexBytes, + releasedNodes, + frontierCompactions, + guardChecksSinceLast: sinceGuard, + statesExplored, + totalGranted, + dedupeHits, + maxDepthReached, + deepestStateDiscovered, + lastDiscoveryAtState, + discoveryCurve: discoveryCurve.checkpoint(), + truncated, + truncatedBy: { ...truncatedBy }, + finished, + findingBytes, + ancestryPayloadBytes, + peakRetainedMemory: { ...peakRetainedMemory }, + sharedObservability: ownerState.sharedObservability, + ownerAccounting: ownerState.ownerAccounting, + }; + const registration: SharedSearchCheckpointSourceRegistrationV1 = { active: true }; + const nodeRecords = function *(): Generator { + for (const node of nodes) yield node ?? null; + }; + const nodePayloadRecords = function *(): Generator { + for (let index = 0; index < nodes.length; index++) { + const node = nodes[index]; + if (!node) continue; + yield { + index, + ...(node.stateJson === undefined ? {} : { stateJson: node.stateJson }), + ...(node.variables === undefined ? {} : { variables: node.variables }), + }; + } + }; + const nodeAncestryRecords = function *(): Generator { + for (let index = 0; index < nodes.length; index++) { + const node = nodes[index]; + if (!node) continue; + yield { + index, + parent: node.parent, + ...(node.choiceText === undefined ? {} : { choiceText: node.choiceText }), + ...(node.choiceIndex === undefined ? {} : { choiceIndex: node.choiceIndex }), + depth: node.depth, + active: node.active, + childRefs: node.childRefs, + stateBytes: node.stateBytes, + variableBytes: node.variableBytes, + ancestryBytes: node.ancestryBytes, + }; + } + }; + const rawRecordsForField = ( + field: keyof SharedSearchCheckpoint["state"] + ): Iterable => { + switch (field) { + case "endings": return endings.entries(); + case "visibleOutcomes": return visibleOutcomes.values(); + case "runtimeErrors": return runtimeErrors.entries(); + case "runtimeWarnings": return runtimeWarnings.values(); + case "visitedKnots": return visitedKnots.values(); + case "seenStates": return seenStates.values(); + case "seenChoiceSets": return seenChoiceSets.values(); + case "variableStateCounts": return variableStateCounts.entries(); + case "variableTransitionCounts": return variableTransitionCounts.entries(); + case "nodes": return nodeRecords(); + case "deep": return deep.values(); + case "random": return random.values(); + case "novelty": return novelty.checkpointRecords(); + default: { + if (!Object.prototype.hasOwnProperty.call(scalarState, field)) { + throw new Error(`Shared checkpoint source does not recognize state field ${String(field)}`); + } + const value = scalarState[field]; + return value === undefined ? [] : [value]; + } + } + }; + return createSharedSearchCheckpointSourceLeaseV1( + { + sourceSchemaVersion: 1, + checkpointSchemaVersion: SHARED_SEARCH_CHECKPOINT_SCHEMA_VERSION, + engine: "shared:deep-novelty-v1", + configuration, + statesExplored, + totalGranted, + nodeSlots: nodes.length, + }, + { + backing: { + stateRecords: rawRecordsForField, + nodePayloadRecords, + nodeAncestryRecords, + }, + registration, + activeIterators: new Set(), + } + ); + }; + return { label: foundBy, systematic: true, @@ -4913,6 +5255,7 @@ function createSharedEngine( stoppedForTime: () => timeStopped, snapshot: buildResult, checkpoint: buildCheckpoint, + checkpointSource, finalize(): ExploreResult { if (memoryStopped) { truncated = true; @@ -4989,22 +5332,23 @@ export interface SharedResumableRun { checkpoint?: SharedSearchCheckpoint; } -/** - * Run the base shared search to a total state grant and preserve its exact live - * frontier when work remains. A resumed run receives a new total grant, not an - * additional budget, so callers can safely grow 100k -> 1m without double-counting. - * - * This first checkpoint schema intentionally excludes assertions, goals, and - * experimental frontier scoring. Those modes need their own explicit state - * contracts before they can make the same exact-resume promise. - */ -export function exploreSharedResumable( +export interface SharedCheckpointSourceRun { + result: ExploreResult; + /** Result returned by the scoped source consumer while resumable work remains. */ + checkpoint?: T; +} + +function runSharedToCheckpointBoundary( storyJson: string, knots: KnotInfo[], - externals: string[] = [], - opts: ExploreOptions = {}, - checkpoint?: SharedSearchCheckpoint -): SharedResumableRun { + externals: string[], + opts: ExploreOptions, + checkpoint?: SharedSearchCheckpoint, + onProgress: ExploreOptions["onProgress"] = opts.onProgress +): SharedPassEngine { + // Keep cadence and validation inputs stable even if an early evidence + // callback mutates the caller-owned options object during engine creation. + opts = { ...opts }; if (opts.sharedVariableAware || opts.sharedGoalAware || opts.assertions?.length || opts.goals?.length) { throw new RangeError("Shared resumable search supports only the base shared strategy without assertions or goals"); } @@ -5033,7 +5377,7 @@ export function exploreSharedResumable( const consumed = engine.run(1); remaining--; statesExplored += consumed; - if (opts.onProgress) { + if (onProgress) { grantsSinceClock++; const stateDue = statesExplored - lastStates >= stateInterval; if (stateDue || grantsSinceClock >= 64 || timeInterval === 0) { @@ -5043,22 +5387,120 @@ export function exploreSharedResumable( const snapshot = engine.snapshot(); lastStates = statesExplored; lastAt = now; - opts.onProgress(progressFromSnapshot(engine.label, statesExplored, snapshot)); + onProgress(progressFromSnapshot(engine.label, statesExplored, snapshot)); } } } if (consumed === 0) break; } + return engine; +} + +function finalizeSharedResumableEngine( + engine: SharedPassEngine, + onProgress: ExploreOptions["onProgress"] +): ExploreResult { + const result = engine.finalize(); + result.passes = [engine.telemetry()]; + onProgress?.(progressFromSnapshot(engine.label, result.statesExplored, result)); + return result; +} + +/** + * Run the base shared search to a total state grant and preserve its exact live + * frontier when work remains. A resumed run receives a new total grant, not an + * additional budget, so callers can safely grow 100k -> 1m without double-counting. + * + * This first checkpoint schema intentionally excludes assertions, goals, and + * experimental frontier scoring. Those modes need their own explicit state + * contracts before they can make the same exact-resume promise. + */ +export function exploreSharedResumable( + storyJson: string, + knots: KnotInfo[], + externals: string[] = [], + opts: ExploreOptions = {}, + checkpoint?: SharedSearchCheckpoint +): SharedResumableRun { + const onProgress = opts.onProgress; + const engine = runSharedToCheckpointBoundary(storyJson, knots, externals, opts, checkpoint, onProgress); const nextCheckpoint = !engine.done() && !engine.stoppedForMemory() && !engine.stoppedForTime() ? engine.checkpoint() : undefined; - const result = engine.finalize(); - result.passes = [engine.telemetry()]; - opts.onProgress?.(progressFromSnapshot(engine.label, result.statesExplored, result)); + const result = finalizeSharedResumableEngine(engine, onProgress); return nextCheckpoint ? { result, checkpoint: nextCheckpoint } : { result }; } +/** + * Run the same exact resumable search, but hand a live, read-only checkpoint + * source to an async consumer instead of materializing the complete checkpoint + * graph. The engine remains quiescent until the callback settles; every source + * iterator is then invalidated before finalization mutates termination state. + */ +export async function exploreSharedResumableWithCheckpointSource( + storyJson: string, + knots: KnotInfo[], + externals: string[] = [], + opts: ExploreOptions = {}, + consume: (source: SharedSearchCheckpointSourceV1) => T | Promise, + checkpoint?: SharedSearchCheckpoint +): Promise> { + const onProgress = opts.onProgress; + const engine = runSharedToCheckpointBoundary(storyJson, knots, externals, opts, checkpoint, onProgress); + let checkpointResult: T | undefined; + if (!engine.done() && !engine.stoppedForMemory() && !engine.stoppedForTime()) { + const lease = engine.checkpointSource(); + let consumed: T | Promise; + try { + consumed = consume(lease.source); + } catch (error) { + lease.release(); + throw error; + } + let then: unknown; + try { + then = consumed !== null + && (typeof consumed === "object" || typeof consumed === "function") + ? (consumed as PromiseLike).then + : undefined; + } catch (error) { + lease.release(); + throw error; + } + if (typeof then === "function") { + try { + // We already performed the one PromiseResolve-style Get of `then` to + // distinguish a plain synchronous value. Adopt that exact method in a + // job instead of `await consumed`, which would read a stateful getter + // a second time. Native resolving functions provide once-only + // fulfillment/rejection if a hostile thenable calls both or throws. + checkpointResult = await new Promise((resolve, reject) => { + queueMicrotask(() => { + try { + Reflect.apply(then, consumed, [resolve, reject]); + } catch (error) { + reject(error); + } + }); + }); + } finally { + lease.release(); + } + } else { + // A synchronous callback has already settled. Release before the async + // function reaches its first suspension point so the source cannot be + // used in the microtask gap outside the callback. + checkpointResult = consumed as T; + lease.release(); + } + } + const result = finalizeSharedResumableEngine(engine, onProgress); + return checkpointResult === undefined + ? { result } + : { result, checkpoint: checkpointResult }; +} + export function exploreSharedVariableAware( storyJson: string, knots: KnotInfo[], diff --git a/test/checkpoint-read-accounting.test.js b/test/checkpoint-read-accounting.test.js index b6b91bd..fd7a6d8 100644 --- a/test/checkpoint-read-accounting.test.js +++ b/test/checkpoint-read-accounting.test.js @@ -97,6 +97,8 @@ test("gzip checkpoint reopen accounts existing owners without an extra payload r assert.strictEqual(accounting.storageEncoding, "gzip"); assert.strictEqual(accounting.configuredLimits.basis, "configured_limits_not_allocated_capacity"); assert.ok(accounting.configuredLimits.maxManifestBytes >= manifestBytes); + assert.strictEqual(Object.hasOwn(accounting.configuredLimits, "framedV2"), false, + "legacy receipt shape does not gain framed codec limits"); assert.deepStrictEqual(accounting.manifest.storedBuffer, { count: 1, bytes: manifestBytes }); assert.deepStrictEqual(accounting.manifest.rawString, { count: 1, diff --git a/test/checkpoint-source-isolation.test.js b/test/checkpoint-source-isolation.test.js new file mode 100644 index 0000000..1f3cf8b --- /dev/null +++ b/test/checkpoint-source-isolation.test.js @@ -0,0 +1,223 @@ +const { test } = require("node:test"); +const assert = require("node:assert"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const { compile, scanKnots } = require("../dist/inklecate"); +const { + exploreSharedResumable, + exploreSharedResumableWithCheckpointSource, +} = require("../dist/explore"); +const { + loadCheckpointForResume, + saveCheckpointArtifactFromSource, +} = require("../dist/checkpoints"); + +const FIXTURE = path.join(__dirname, "fixtures", "search", "low-dedup-wide.ink"); + +async function isolatedFixture() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "inkcheck-source-isolation-")); + const story = path.join(root, "story.ink"); + fs.copyFileSync(FIXTURE, story); + const compiled = await compile(story); + assert.strictEqual(compiled.success, true); + return { root, story, storyJson: compiled.storyJson }; +} + +test("checkpoint-source callbacks cannot mutate retained evidence or final-result inputs", async () => { + const fixture = await isolatedFixture(); + const baselineOptions = { + maxDepth: 150, + maxStates: 1_001, + seed: 7, + storySeed: 1, + preserveTurnState: false, + preserveRandomState: false, + }; + try { + const baselineKnots = scanKnots(fixture.story); + const baseline = exploreSharedResumable( + fixture.storyJson, + baselineKnots, + [], + baselineOptions + ); + assert.ok(baseline.checkpoint); + + const mutableKnots = scanKnots(fixture.story); + const mutableExternals = []; + let capturedFinding; + let originalProgressCalls = 0; + let replacementProgressCalls = 0; + const mutableOptions = { + ...baselineOptions, + onEvidence(evidence) { + if (!capturedFinding && evidence.kind === "ending") capturedFinding = evidence.finding; + }, + onProgress() { + originalProgressCalls++; + }, + }; + const marker = " [caller mutation after identity began]"; + const streamed = await exploreSharedResumableWithCheckpointSource( + fixture.storyJson, + mutableKnots, + mutableExternals, + mutableOptions, + async (source) => { + assert.ok(capturedFinding, "the fixture emits retained evidence before its checkpoint boundary"); + setImmediate(() => { + capturedFinding.finalText += marker; + capturedFinding.path.push("caller-owned mutation"); + mutableExternals.push("late_external_never_bound"); + mutableOptions.randomnessDetected = true; + mutableOptions.storySeed = 99; + mutableOptions.goals = [{ id: "late-goal", condition: { kind: "ending" } }]; + mutableOptions.onProgress = () => { + replacementProgressCalls++; + throw new Error("late progress callback must not replace the captured callback"); + }; + mutableKnots[0].name = "late_mutated_knot"; + mutableKnots[0].file = "late-private-path.ink"; + }); + return saveCheckpointArtifactFromSource(fixture.root, fixture.story, source); + } + ); + + assert.ok(streamed.checkpoint); + assert.match(capturedFinding.finalText, /caller mutation after identity began/, + "the adversarial mutation actually ran while the source callback was pending"); + assert.deepStrictEqual(streamed.result, baseline.result); + assert.strictEqual(Object.hasOwn(streamed.result, "goalResults"), false); + assert.deepStrictEqual(streamed.result.externalFunctionsStubbed, []); + assert.strictEqual(streamed.result.randomnessDetected, false); + assert.strictEqual(streamed.result.limits.storySeed, 1); + assert.strictEqual(originalProgressCalls, 1); + assert.strictEqual(replacementProgressCalls, 0); + + const loaded = await loadCheckpointForResume(fixture.root, streamed.checkpoint.id); + assert.deepStrictEqual(loaded.checkpoint, baseline.checkpoint, + "identity and frame passes retain the same detached finding bytes"); + } finally { + fs.rmSync(fixture.root, { recursive: true, force: true }); + } +}); + +test("checkpoint-source lease ends synchronously for plain results and spans a pending promise", async () => { + const fixture = await isolatedFixture(); + const options = { + maxDepth: 150, + maxStates: 73, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }; + try { + const knots = scanKnots(fixture.story); + let synchronousSource; + const synchronousRun = exploreSharedResumableWithCheckpointSource( + fixture.storyJson, + knots, + [], + options, + (source) => { + synchronousSource = source; + return "synchronous"; + } + ); + assert.throws( + () => synchronousSource.stateRecords("nodes"), + /no longer active outside its callback/, + "a plain callback result releases before the async wrapper's first suspension" + ); + assert.strictEqual((await synchronousRun).checkpoint, "synchronous"); + + let pendingSource; + let settle; + const pendingRun = exploreSharedResumableWithCheckpointSource( + fixture.storyJson, + knots, + [], + options, + (source) => { + pendingSource = source; + return new Promise((resolve) => { settle = resolve; }); + } + ); + const iterator = pendingSource.stateRecords("nodes")[Symbol.iterator](); + assert.strictEqual(iterator.next().done, false, + "a source remains live while its callback promise is unsettled"); + settle("asynchronous"); + assert.strictEqual((await pendingRun).checkpoint, "asynchronous"); + assert.throws( + () => pendingSource.stateRecords("nodes"), + /no longer active outside its callback/ + ); + assert.throws(() => iterator.next(), /no longer active outside its callback/); + } finally { + fs.rmSync(fixture.root, { recursive: true, force: true }); + } +}); + +test("checkpoint-source lease adopts hostile thenables with one getter read and always releases", async () => { + const fixture = await isolatedFixture(); + const options = { + maxDepth: 150, + maxStates: 73, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }; + try { + const knots = scanKnots(fixture.story); + let source; + let getterReads = 0; + const adopted = {}; + Object.defineProperty(adopted, "then", { + get() { + getterReads++; + if (getterReads > 1) throw new Error("then getter was read twice"); + return function (resolve) { + assert.strictEqual(source.stateRecords("nodes")[Symbol.iterator]().next().done, false); + resolve("adopted-once"); + }; + }, + }); + const adoptedRun = exploreSharedResumableWithCheckpointSource( + fixture.storyJson, knots, [], options, + (value) => { source = value; return adopted; } + ); + assert.strictEqual(source.stateRecords("nodes")[Symbol.iterator]().next().done, false, + "thenable invocation is a later job, so its callback lifetime remains active"); + assert.strictEqual((await adoptedRun).checkpoint, "adopted-once"); + assert.strictEqual(getterReads, 1); + assert.throws(() => source.stateRecords("nodes"), /no longer active outside its callback/); + + const getterError = new Error("then getter failed"); + let getterSource; + const throwingGetter = {}; + Object.defineProperty(throwingGetter, "then", { get() { throw getterError; } }); + const getterRun = exploreSharedResumableWithCheckpointSource( + fixture.storyJson, knots, [], options, + (value) => { getterSource = value; return throwingGetter; } + ); + assert.throws(() => getterSource.stateRecords("nodes"), /no longer active outside its callback/); + await assert.rejects(() => getterRun, (error) => error === getterError); + + const invocationError = new Error("then invocation failed"); + let invocationSource; + const throwingThen = { + then() { throw invocationError; }, + }; + const invocationRun = exploreSharedResumableWithCheckpointSource( + fixture.storyJson, knots, [], options, + (value) => { invocationSource = value; return throwingThen; } + ); + assert.strictEqual(invocationSource.stateRecords("nodes")[Symbol.iterator]().next().done, false); + await assert.rejects(() => invocationRun, (error) => error === invocationError); + assert.throws(() => invocationSource.stateRecords("nodes"), /no longer active outside its callback/); + } finally { + fs.rmSync(fixture.root, { recursive: true, force: true }); + } +}); diff --git a/test/checkpoint-source-streaming.test.js b/test/checkpoint-source-streaming.test.js new file mode 100644 index 0000000..05e68f8 --- /dev/null +++ b/test/checkpoint-source-streaming.test.js @@ -0,0 +1,856 @@ +const { test } = require("node:test"); +const assert = require("node:assert"); +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const { compile, scanKnots } = require("../dist/inklecate"); +const { + exploreSharedResumable, + exploreSharedResumableWithCheckpointSource, +} = require("../dist/explore"); +const { + loadCheckpointForResume, + saveCheckpointArtifact, + saveCheckpointArtifactFromSource, +} = require("../dist/checkpoints"); +const { + CHECKPOINT_ARTIFACT_V2_COMPONENT_ORDER, + readCheckpointArtifactV2, + writeCheckpointArtifactV2, +} = require("../dist/checkpoint-artifact-v2"); + +const FIXTURE = path.join(__dirname, "fixtures", "search", "low-dedup-wide.ink"); +const DEEP_FIXTURE = path.join(__dirname, "fixtures", "search", "deep-branching.ink"); + +async function fixtureRoots(fixturePath = FIXTURE) { + const graphRoot = fs.mkdtempSync(path.join(os.tmpdir(), "inkcheck-source-graph-")); + const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "inkcheck-source-live-")); + const graphStory = path.join(graphRoot, "story.ink"); + const sourceStory = path.join(sourceRoot, "story.ink"); + fs.copyFileSync(fixturePath, graphStory); + fs.copyFileSync(fixturePath, sourceStory); + const compiled = await compile(graphStory); + assert.strictEqual(compiled.success, true); + return { + graphRoot, + sourceRoot, + graphStory, + sourceStory, + compiled, + knots: scanKnots(graphStory), + }; +} + +async function withFixedDate(iso, callback) { + const NativeDate = Date; + const fixed = NativeDate.parse(iso); + global.Date = class extends NativeDate { + constructor(...values) { + super(...(values.length > 0 ? values : [fixed])); + } + static now() { return fixed; } + }; + try { + return await callback(); + } finally { + global.Date = NativeDate; + } +} + +async function rewriteFramedArtifact(root, reference, mutateRecord) { + const payload = path.join(root, ...reference.path.split("/")); + const manifestFile = path.join(path.dirname(payload), `${reference.id}.meta.json`); + const inputs = []; + let current; + await readCheckpointArtifactV2(fs.createReadStream(payload), { + onRecord(record) { + const key = `${record.component}\0${record.field}`; + if (!current || current.key !== key) { + current = { + key, + component: record.component, + field: record.field, + start: record.index, + records: [], + }; + inputs.push(current); + } + current.records.push(mutateRecord(record)); + }, + }); + const replacement = `${payload}.replacement`; + const codec = await writeCheckpointArtifactV2( + fs.createWriteStream(replacement, { flags: "wx", mode: 0o600 }), + inputs.map(({ key: _key, ...input }) => input) + ); + fs.renameSync(replacement, payload); + + const manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8")); + const bytes = fs.readFileSync(payload); + manifest.artifactSizeBytes = bytes.length; + manifest.artifactSha256 = crypto.createHash("sha256").update(bytes).digest("hex"); + manifest.framedV2 = { + schemaVersion: 2, + componentOrder: CHECKPOINT_ARTIFACT_V2_COMPONENT_ORDER, + artifactBytes: codec.artifactBytes, + artifactOverheadBytes: codec.artifactBytes - codec.data.storedBytes - codec.index.storedBytes, + data: codec.data, + components: codec.components, + index: codec.index, + }; + const body = { + manifestSchemaVersion: manifest.manifestSchemaVersion, + artifactSchemaVersion: manifest.artifactSchemaVersion, + artifactType: manifest.artifactType, + id: manifest.id, + createdAt: manifest.createdAt, + inkcheckVersion: manifest.inkcheckVersion, + checkpointSchemaVersion: manifest.checkpointSchemaVersion, + entrypoint: manifest.entrypoint, + engine: manifest.engine, + totalGranted: manifest.totalGranted, + statesExplored: manifest.statesExplored, + storageEncoding: manifest.storageEncoding, + artifactSizeBytes: manifest.artifactSizeBytes, + artifactSha256: manifest.artifactSha256, + framedV2: manifest.framedV2, + }; + manifest.manifestSha256 = crypto.createHash("sha256") + .update(JSON.stringify(body)).digest("hex"); + fs.writeFileSync(manifestFile, JSON.stringify(manifest)); +} + +test("engine-native source preserves logical ID, framed bytes, and exact resume without a checkpoint graph", async () => { + const fixture = await fixtureRoots(); + const options = { + maxDepth: 150, + maxStates: 1_001, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + sharedObservabilityIntervalStates: 25, + }; + try { + const materialized = exploreSharedResumable( + fixture.compiled.storyJson, + fixture.knots, + [], + options + ); + assert.ok(materialized.checkpoint); + + let capturedSource; + let danglingNodeIterator; + const [graphReference, streamed] = await withFixedDate( + "2026-09-06T12:34:56.000Z", + async () => { + const graphReference = await saveCheckpointArtifact( + fixture.graphRoot, + fixture.graphStory, + materialized.checkpoint, + { format: "framed-v2" } + ); + const streamed = await exploreSharedResumableWithCheckpointSource( + fixture.compiled.storyJson, + fixture.knots, + [], + options, + async (source) => { + capturedSource = source; + danglingNodeIterator = source.stateRecords("nodes")[Symbol.iterator](); + let detachedNode = danglingNodeIterator.next(); + while (!detachedNode.done && detachedNode.value === null) { + detachedNode = danglingNodeIterator.next(); + } + assert.strictEqual(detachedNode.done, false); + detachedNode.value.parent = 987_654_321; + await new Promise((resolve) => setImmediate(resolve)); + return saveCheckpointArtifactFromSource( + fixture.sourceRoot, + fixture.sourceStory, + source + ); + } + ); + return [graphReference, streamed]; + } + ); + assert.ok(streamed.checkpoint); + assert.deepStrictEqual(streamed.result, materialized.result); + assert.strictEqual(streamed.checkpoint.id, graphReference.id); + assert.deepStrictEqual(streamed.checkpoint.accounting.checkpointGraph, { + count: 0, + logicalUtf8Bytes: 0, + peakSourceChunkUtf8Bytes: 0, + }); + assert.deepStrictEqual(streamed.checkpoint.accounting.engineSource, { + schemaVersion: 1, + status: "applied", + materializedCheckpointGraphs: 0, + identityPasses: 1, + framePasses: 1, + reuseVerificationPasses: 0, + logicalCheckpointUtf8BytesVisited: + graphReference.accounting.checkpointGraph.logicalUtf8Bytes, + peakSourceChunkUtf8Bytes: + graphReference.accounting.checkpointGraph.peakSourceChunkUtf8Bytes, + }); + assert.deepStrictEqual(streamed.checkpoint.accounting.serialization, { + status: "not_applied", + logicalArtifactUtf8BytesEmitted: 0, + peakSourceChunkUtf8Bytes: 0, + }); + assert.deepStrictEqual(streamed.checkpoint.accounting.compression, { + status: "not_applied", + storedBytesEmitted: 0, + peakOutputChunkBytes: 0, + }); + assert.strictEqual(streamed.checkpoint.accounting.framedV2.status, "applied"); + + const graphPayload = fs.readFileSync(path.join(fixture.graphRoot, graphReference.path)); + const sourcePayload = fs.readFileSync(path.join(fixture.sourceRoot, streamed.checkpoint.path)); + assert.strictEqual(sourcePayload.equals(graphPayload), true, + "fixed-time graph and engine-native producers emit identical framed bytes"); + assert.strictEqual( + crypto.createHash("sha256").update(sourcePayload).digest("hex"), + crypto.createHash("sha256").update(graphPayload).digest("hex") + ); + + const loaded = await loadCheckpointForResume(fixture.sourceRoot, streamed.checkpoint.id); + assert.deepStrictEqual(loaded.checkpoint, materialized.checkpoint); + assert.throws( + () => capturedSource.stateRecords("nodes"), + /no longer active outside its callback/ + ); + assert.deepStrictEqual(danglingNodeIterator.return(), { done: true, value: undefined }, + "release severs a paused iterator before user code asks it to unwind"); + assert.throws( + () => danglingNodeIterator.next(), + /no longer active outside its callback/ + ); + await assert.rejects( + () => saveCheckpointArtifactFromSource( + fixture.sourceRoot, + fixture.sourceStory, + capturedSource + ), + /no longer active outside its callback/ + ); + + const endpointOptions = { ...options, maxStates: 1_200 }; + const nativeStringify = JSON.stringify; + let serializedWholeCheckpointState = 0; + JSON.stringify = (value, ...args) => { + if (value === loaded.checkpoint.state) { + serializedWholeCheckpointState++; + throw new Error("whole checkpoint-state serialization is forbidden during restore"); + } + return nativeStringify(value, ...args); + }; + let resumed; + try { + resumed = exploreSharedResumable( + fixture.compiled.storyJson, + fixture.knots, + [], + endpointOptions, + loaded.checkpoint + ); + } finally { + JSON.stringify = nativeStringify; + } + assert.strictEqual(serializedWholeCheckpointState, 0); + const uninterrupted = exploreSharedResumable( + fixture.compiled.storyJson, + fixture.knots, + [], + endpointOptions + ); + assert.deepStrictEqual(resumed, uninterrupted); + } finally { + fs.rmSync(fixture.graphRoot, { recursive: true, force: true }); + fs.rmSync(fixture.sourceRoot, { recursive: true, force: true }); + } +}); + +test("engine-native save rejects fabricated sources before filesystem work", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "inkcheck-source-fabricated-")); + try { + const fabricated = { + sourceSchemaVersion: 1, + checkpointSchemaVersion: 1, + engine: "shared:deep-novelty-v1", + configuration: {}, + statesExplored: 1, + totalGranted: 1, + nodeSlots: 0, + stateRecords: () => [], + nodePayloadRecords: () => [], + nodeAncestryRecords: () => [], + }; + await assert.rejects( + () => saveCheckpointArtifactFromSource(root, path.join(root, "story.ink"), fabricated), + /must be minted by a live shared-search engine/ + ); + assert.deepStrictEqual(fs.readdirSync(root), [], + "runtime branding fails before creating the checkpoint directory"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("manifested framed same-ID source reuse compares records without parsing a complete checkpoint", async () => { + const fixture = await fixtureRoots(); + const options = { + maxDepth: 150, + maxStates: 73, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }; + try { + const materialized = exploreSharedResumable( + fixture.compiled.storyJson, + fixture.knots, + [], + options + ); + assert.ok(materialized.checkpoint); + const existing = await saveCheckpointArtifact( + fixture.graphRoot, + fixture.graphStory, + materialized.checkpoint, + { format: "framed-v2" } + ); + const nativeParse = JSON.parse; + let parsedCompleteCheckpoint = 0; + const streamed = await exploreSharedResumableWithCheckpointSource( + fixture.compiled.storyJson, + fixture.knots, + [], + options, + async (source) => { + JSON.parse = (text, ...args) => { + if (typeof text === "string" && text.includes('"checkpoint":')) { + parsedCompleteCheckpoint++; + throw new Error("complete checkpoint parsing is forbidden during source-native reuse"); + } + return nativeParse(text, ...args); + }; + try { + return await saveCheckpointArtifactFromSource( + fixture.graphRoot, + fixture.graphStory, + source + ); + } finally { + JSON.parse = nativeParse; + } + } + ); + assert.ok(streamed.checkpoint); + assert.strictEqual(streamed.checkpoint.id, existing.id); + assert.strictEqual(streamed.checkpoint.path, existing.path); + assert.strictEqual(streamed.checkpoint.accounting.outcome, "reused"); + assert.strictEqual(streamed.checkpoint.accounting.checkpointGraph.count, 0); + assert.strictEqual(streamed.checkpoint.accounting.engineSource.materializedCheckpointGraphs, 0); + assert.strictEqual(streamed.checkpoint.accounting.engineSource.framePasses, 0); + assert.strictEqual(streamed.checkpoint.accounting.engineSource.reuseVerificationPasses, 1); + assert.strictEqual(parsedCompleteCheckpoint, 0); + assert.deepStrictEqual(streamed.result, materialized.result); + } finally { + fs.rmSync(fixture.graphRoot, { recursive: true, force: true }); + fs.rmSync(fixture.sourceRoot, { recursive: true, force: true }); + } +}); + +test("sequential engine-native saves create once and verify the same framed ID on reuse", async () => { + const fixture = await fixtureRoots(); + const options = { + maxDepth: 150, + maxStates: 73, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }; + const save = () => exploreSharedResumableWithCheckpointSource( + fixture.compiled.storyJson, + fixture.knots, + [], + options, + (source) => saveCheckpointArtifactFromSource( + fixture.graphRoot, + fixture.graphStory, + source + ) + ); + try { + const first = await save(); + const second = await save(); + assert.ok(first.checkpoint); + assert.ok(second.checkpoint); + assert.strictEqual(first.checkpoint.accounting.outcome, "created"); + assert.strictEqual(second.checkpoint.accounting.outcome, "reused"); + assert.strictEqual(second.checkpoint.id, first.checkpoint.id); + assert.strictEqual(second.checkpoint.path, first.checkpoint.path); + assert.strictEqual(second.checkpoint.accounting.engineSource.materializedCheckpointGraphs, 0); + assert.strictEqual(second.checkpoint.accounting.engineSource.reuseVerificationPasses, 1); + assert.deepStrictEqual(second.result, first.result); + } finally { + fs.rmSync(fixture.graphRoot, { recursive: true, force: true }); + fs.rmSync(fixture.sourceRoot, { recursive: true, force: true }); + } +}); + +test("concurrent engine-native same-ID writers keep live source context through no-clobber reuse", async () => { + const fixture = await fixtureRoots(); + const options = { + maxDepth: 150, + maxStates: 1_001, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }; + let ready = 0; + let release; + const gate = new Promise((resolve) => { release = resolve; }); + const save = () => exploreSharedResumableWithCheckpointSource( + fixture.compiled.storyJson, + fixture.knots, + [], + options, + async (source) => { + ready += 1; + if (ready === 2) release(); + await gate; + return saveCheckpointArtifactFromSource( + fixture.graphRoot, + fixture.graphStory, + source + ); + } + ); + try { + const saves = await Promise.all([save(), save()]); + assert.ok(saves[0].checkpoint); + assert.ok(saves[1].checkpoint); + assert.strictEqual(saves[0].checkpoint.id, saves[1].checkpoint.id); + assert.strictEqual(saves[0].checkpoint.path, saves[1].checkpoint.path); + assert.deepStrictEqual( + saves.map((run) => run.checkpoint.accounting.outcome).sort(), + ["created", "reused"] + ); + for (const run of saves) { + assert.strictEqual(run.checkpoint.accounting.engineSource.materializedCheckpointGraphs, 0); + assert.strictEqual( + run.checkpoint.accounting.engineSource.reuseVerificationPasses, + run.checkpoint.accounting.outcome === "reused" ? 1 : 0 + ); + } + const loaded = await loadCheckpointForResume(fixture.graphRoot, saves[0].checkpoint.id); + assert.strictEqual(loaded.checkpoint.state.statesExplored, 1_001); + } finally { + fs.rmSync(fixture.graphRoot, { recursive: true, force: true }); + fs.rmSync(fixture.sourceRoot, { recursive: true, force: true }); + } +}); + +test("source reuse rejects a self-consistent framed payload whose logical frontier changed", async () => { + const fixture = await fixtureRoots(); + const options = { + maxDepth: 150, + maxStates: 73, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }; + try { + const materialized = exploreSharedResumable( + fixture.compiled.storyJson, + fixture.knots, + [], + options + ); + assert.ok(materialized.checkpoint); + const existing = await saveCheckpointArtifact( + fixture.graphRoot, + fixture.graphStory, + materialized.checkpoint, + { format: "framed-v2" } + ); + let changed = 0; + await rewriteFramedArtifact(fixture.graphRoot, existing, (record) => { + if (record.component === "dedupe" && record.field === "dedupeHits") { + changed += 1; + return record.value + 1; + } + return record.value; + }); + assert.strictEqual(changed, 1); + const payload = path.join(fixture.graphRoot, ...existing.path.split("/")); + const before = fs.readFileSync(payload); + + await assert.rejects( + () => exploreSharedResumableWithCheckpointSource( + fixture.compiled.storyJson, + fixture.knots, + [], + options, + (source) => saveCheckpointArtifactFromSource( + fixture.graphRoot, + fixture.graphStory, + source + ) + ), + (error) => error?.kind === "corrupt" && error?.stage === "envelope" + && /live source or stable ID/.test(error.message) + ); + const nextCheckpoint = structuredClone(materialized.checkpoint); + nextCheckpoint.state.dedupeHits += 2; + const immediateWrite = await saveCheckpointArtifact( + fixture.graphRoot, + fixture.graphStory, + nextCheckpoint, + { format: "framed-v2" } + ); + assert.notStrictEqual(immediateWrite.id, existing.id, + "failed source-verification teardown settles before the next writer reuses its descriptor"); + assert.strictEqual(fs.readFileSync(payload).equals(before), true, + "failed logical verification never replaces the suspect canonical pair"); + await assert.rejects( + () => loadCheckpointForResume(fixture.graphRoot, existing.id), + /content does not match its stable ID/ + ); + } finally { + fs.rmSync(fixture.graphRoot, { recursive: true, force: true }); + fs.rmSync(fixture.sourceRoot, { recursive: true, force: true }); + } +}); + +test("manifested legacy reuse is explicit unsupported on the graph-free source path", async () => { + const fixture = await fixtureRoots(); + const options = { + maxDepth: 150, + maxStates: 73, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }; + try { + const materialized = exploreSharedResumable( + fixture.compiled.storyJson, + fixture.knots, + [], + options + ); + assert.ok(materialized.checkpoint); + const existing = await saveCheckpointArtifact( + fixture.graphRoot, + fixture.graphStory, + materialized.checkpoint, + { format: "legacy-v1" } + ); + const payload = path.join(fixture.graphRoot, ...existing.path.split("/")); + const before = fs.readFileSync(payload); + const nativeParse = JSON.parse; + let parsedCompleteCheckpoint = 0; + JSON.parse = (text, ...args) => { + if (typeof text === "string" && text.includes('"checkpoint":')) { + parsedCompleteCheckpoint++; + throw new Error("complete checkpoint parsing is forbidden during source-native reuse"); + } + return nativeParse(text, ...args); + }; + try { + await assert.rejects( + () => exploreSharedResumableWithCheckpointSource( + fixture.compiled.storyJson, + fixture.knots, + [], + options, + (source) => saveCheckpointArtifactFromSource( + fixture.graphRoot, + fixture.graphStory, + source + ) + ), + (error) => error?.kind === "unsupported" && error?.stage === "storage" + ); + } finally { + JSON.parse = nativeParse; + } + assert.strictEqual(parsedCompleteCheckpoint, 0); + assert.strictEqual(fs.readFileSync(payload).equals(before), true); + } finally { + fs.rmSync(fixture.graphRoot, { recursive: true, force: true }); + fs.rmSync(fixture.sourceRoot, { recursive: true, force: true }); + } +}); + +test("sidecar-free legacy reuse is explicit unsupported on the graph-free source path", async () => { + const fixture = await fixtureRoots(); + const options = { + maxDepth: 150, + maxStates: 73, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }; + try { + const materialized = exploreSharedResumable( + fixture.compiled.storyJson, + fixture.knots, + [], + options + ); + assert.ok(materialized.checkpoint); + const existing = await saveCheckpointArtifact( + fixture.graphRoot, + fixture.graphStory, + materialized.checkpoint, + { format: "legacy-v1" } + ); + const payload = path.join(fixture.graphRoot, existing.path); + const before = fs.readFileSync(payload); + fs.rmSync(path.join(path.dirname(payload), `${existing.id}.meta.json`)); + await assert.rejects( + () => exploreSharedResumableWithCheckpointSource( + fixture.compiled.storyJson, + fixture.knots, + [], + options, + (source) => saveCheckpointArtifactFromSource( + fixture.graphRoot, + fixture.graphStory, + source + ) + ), + (error) => error?.kind === "unsupported" && error?.stage === "manifest" + ); + assert.strictEqual(fs.readFileSync(payload).equals(before), true, + "unsupported source-native reuse preserves the sidecar-free legacy bytes"); + } finally { + fs.rmSync(fixture.graphRoot, { recursive: true, force: true }); + fs.rmSync(fixture.sourceRoot, { recursive: true, force: true }); + } +}); + +test("source retention fails closed on an unrelated sidecar-free legacy artifact without parsing it", async () => { + const fixture = await fixtureRoots(); + const legacyOptions = { + maxDepth: 150, + maxStates: 71, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }; + const sourceOptions = { ...legacyOptions, maxStates: 73 }; + try { + const legacy = exploreSharedResumable( + fixture.compiled.storyJson, + fixture.knots, + [], + legacyOptions + ); + assert.ok(legacy.checkpoint); + const existing = await saveCheckpointArtifact( + fixture.graphRoot, + fixture.graphStory, + legacy.checkpoint, + { format: "legacy-v1" } + ); + const directory = path.dirname(path.join(fixture.graphRoot, ...existing.path.split("/"))); + fs.rmSync(path.join(directory, `${existing.id}.meta.json`)); + const beforeNames = fs.readdirSync(directory).sort(); + const nativeParse = JSON.parse; + let parsedCompleteCheckpoint = 0; + JSON.parse = (text, ...args) => { + if (typeof text === "string" && text.includes('"checkpoint":')) { + parsedCompleteCheckpoint++; + throw new Error("retention must not parse a sidecar-free checkpoint"); + } + return nativeParse(text, ...args); + }; + try { + await assert.rejects( + () => exploreSharedResumableWithCheckpointSource( + fixture.compiled.storyJson, + fixture.knots, + [], + sourceOptions, + (source) => saveCheckpointArtifactFromSource( + fixture.graphRoot, + fixture.graphStory, + source, + { maxGenerationsPerEntrypoint: 10 } + ) + ), + (error) => error?.kind === "unsupported" && error?.stage === "manifest" + ); + } finally { + JSON.parse = nativeParse; + } + assert.strictEqual(parsedCompleteCheckpoint, 0); + assert.deepStrictEqual(fs.readdirSync(directory).sort(), beforeNames, + "manifest-required preflight rejects before reserving or publishing a source candidate"); + } finally { + fs.rmSync(fixture.graphRoot, { recursive: true, force: true }); + fs.rmSync(fixture.sourceRoot, { recursive: true, force: true }); + } +}); + +test("engine-native source preserves deep-frontier parity while migrating a pre-ledger checkpoint", async () => { + const fixture = await fixtureRoots(DEEP_FIXTURE); + const initialOptions = { + maxDepth: 500, + maxStates: 73, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }; + const resumedOptions = { ...initialOptions, maxStates: 401 }; + try { + const initial = exploreSharedResumable( + fixture.compiled.storyJson, + fixture.knots, + [], + initialOptions + ); + assert.ok(initial.checkpoint); + const preLedger = JSON.parse(JSON.stringify(initial.checkpoint)); + delete preLedger.state.sharedObservability; + delete preLedger.state.ownerAccounting; + + const materialized = exploreSharedResumable( + fixture.compiled.storyJson, + fixture.knots, + [], + resumedOptions, + preLedger + ); + assert.ok(materialized.checkpoint); + const [graphReference, streamed] = await withFixedDate( + "2026-09-06T12:35:56.000Z", + async () => { + const graphReference = await saveCheckpointArtifact( + fixture.graphRoot, + fixture.graphStory, + materialized.checkpoint, + { format: "framed-v2" } + ); + const streamed = await exploreSharedResumableWithCheckpointSource( + fixture.compiled.storyJson, + fixture.knots, + [], + resumedOptions, + (source) => saveCheckpointArtifactFromSource( + fixture.sourceRoot, + fixture.sourceStory, + source + ), + preLedger + ); + return [graphReference, streamed]; + } + ); + assert.ok(streamed.checkpoint); + assert.deepStrictEqual(streamed.result, materialized.result); + assert.strictEqual(streamed.checkpoint.id, graphReference.id); + const graphPayload = fs.readFileSync(path.join(fixture.graphRoot, graphReference.path)); + const sourcePayload = fs.readFileSync(path.join(fixture.sourceRoot, streamed.checkpoint.path)); + assert.strictEqual(sourcePayload.equals(graphPayload), true); + const loaded = await loadCheckpointForResume(fixture.sourceRoot, streamed.checkpoint.id); + assert.deepStrictEqual(loaded.checkpoint, materialized.checkpoint); + } finally { + fs.rmSync(fixture.graphRoot, { recursive: true, force: true }); + fs.rmSync(fixture.sourceRoot, { recursive: true, force: true }); + } +}); + +test("callback rejection invalidates and unwinds every live engine-source iterator", async () => { + const fixture = await fixtureRoots(); + const expected = new Error("consumer rejected checkpoint source"); + let capturedSource; + let capturedIterator; + try { + await assert.rejects( + () => exploreSharedResumableWithCheckpointSource( + fixture.compiled.storyJson, + fixture.knots, + [], + { + maxDepth: 150, + maxStates: 73, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }, + async (source) => { + capturedSource = source; + capturedIterator = source.stateRecords("nodes")[Symbol.iterator](); + assert.strictEqual(capturedIterator.next().done, false); + await new Promise((resolve) => setImmediate(resolve)); + throw expected; + } + ), + (error) => error === expected + ); + assert.throws( + () => capturedSource.nodePayloadRecords(), + /no longer active outside its callback/ + ); + assert.deepStrictEqual(capturedIterator.return(), { done: true, value: undefined }); + assert.throws( + () => capturedIterator.next(), + /no longer active outside its callback/ + ); + } finally { + fs.rmSync(fixture.graphRoot, { recursive: true, force: true }); + fs.rmSync(fixture.sourceRoot, { recursive: true, force: true }); + } +}); + +test("engine-native identity traversal is cancellable and always invalidates the source", async () => { + const fixture = await fixtureRoots(); + let capturedSource; + const controller = new AbortController(); + try { + await assert.rejects( + () => exploreSharedResumableWithCheckpointSource( + fixture.compiled.storyJson, + fixture.knots, + [], + { + maxDepth: 150, + maxStates: 2_000, + seed: 7, + preserveTurnState: false, + preserveRandomState: false, + }, + async (source) => { + capturedSource = source; + setImmediate(() => controller.abort()); + return saveCheckpointArtifactFromSource( + fixture.sourceRoot, + fixture.sourceStory, + source, + { signal: controller.signal } + ); + } + ), + (error) => error?.name === "AbortError" + ); + assert.throws( + () => capturedSource.stateRecords("seenStates"), + /no longer active outside its callback/ + ); + const directory = path.join(fixture.sourceRoot, ".inkcheck", "checkpoints"); + assert.deepStrictEqual(fs.existsSync(directory) ? fs.readdirSync(directory) : [], [], + "cancellation before publication leaves no artifact or private transaction files"); + } finally { + fs.rmSync(fixture.graphRoot, { recursive: true, force: true }); + fs.rmSync(fixture.sourceRoot, { recursive: true, force: true }); + } +}); diff --git a/test/checkpoint-v2-evaluation.test.js b/test/checkpoint-v2-evaluation.test.js new file mode 100644 index 0000000..09604c9 --- /dev/null +++ b/test/checkpoint-v2-evaluation.test.js @@ -0,0 +1,394 @@ +const { test } = require("node:test"); +const assert = require("node:assert"); +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const { compile } = require("../dist/inklecate"); +const { + digestJson, + evaluationProgressSnapshot, + evaluationSnapshotVerdict, + evaluateCheckpointV2Verdict, + runBoundedProcess, + runCheckpointV2EvaluationCell, + validateCheckpointV2EvaluationManifest, + validateEvaluationSource, + writeEvaluationOutputAtomic, +} = require("../dist/checkpoint-v2-evaluation-cli"); + +const REPOSITORY = path.join(__dirname, ".."); +const MANIFEST_FILE = path.join(REPOSITORY, "benchmarks", "checkpoint-v2-promotion-v1.json"); +const SYNTHETIC_STORY = path.join(REPOSITORY, "test", "fixtures", "search", "low-dedup-wide.ink"); + +function sha256(value) { + return crypto.createHash("sha256").update(value).digest("hex"); +} + +function manifest() { + return JSON.parse(fs.readFileSync(MANIFEST_FILE, "utf8")); +} + +async function syntheticSource() { + const story = fs.readFileSync(SYNTHETIC_STORY); + const compiled = await compile(SYNTHETIC_STORY); + assert.strictEqual(compiled.success, true); + const relative = path.relative(REPOSITORY, SYNTHETIC_STORY).split(path.sep).join("/"); + const name = path.basename(SYNTHETIC_STORY); + return { + id: "synthetic-wide", + story: relative, + entrypointSha256: sha256(story), + compiledStorySha256: sha256(`${compiled.storyJson.replace(/\r?\n$/, "")}\n`), + closure: { + includeCount: 0, + fileCount: 1, + bundleSha256: sha256(Buffer.concat([Buffer.from(`${name}\0`), story, Buffer.from("\0")])), + }, + provenance: { + license: "MIT", + licenseFile: "LICENSE", + licenseSha256: sha256(fs.readFileSync(path.join(REPOSITORY, "LICENSE"))), + upstream: "fixture/inkcheck", + commit: "0000000000000000000000000000000000000000", + }, + }; +} + +function syntheticControls(cellOrder) { + return { + cellOrder, + maxDepth: 150, + searchSeed: 7, + storySeed: 1, + concurrency: 1, + minimizeRepros: false, + stateSensitivity: "source_semantics", + loopRiskDetection: "only_without_turns_randomness_visit_counts_or_externals", + maxMemoryMb: 512, + maxTimeMs: 30_000, + workerTimeoutMs: 40_000, + coordinatorMaxOldSpaceMb: 256, + storage: { + maxCheckpointBytes: 64 * 1024 * 1024, + maxProjectBytes: 128 * 1024 * 1024, + framedV2: { maxTotalDecodedBytes: 128 * 1024 * 1024 }, + }, + }; +} + +function cell(id, mode, expected = mode === "uninterrupted" ? "endpoint" : "resume_exact") { + return { + id, + sourceId: "synthetic-wide", + mode, + baseStates: 73, + targetStates: 100, + expected, + }; +} + +async function runSynthetic(source, spec, controls) { + return runCheckpointV2EvaluationCell({ + schemaVersion: 1, + manifestRoot: REPOSITORY, + cellRoot: fs.mkdtempSync(path.join(os.tmpdir(), "inkcheck-checkpoint-v2-eval-test-")), + source, + cell: spec, + controls, + }); +} + +function resultObservation(digest = "a") { + return { digest: { sha256: digest.repeat(64).slice(0, 64), utf8Bytes: 10 } }; +} + +function completedCase(id, mode) { + const sourceId = id.startsWith("heresy2") ? "heresy2" : "intercept"; + const expected = id === "intercept-legacy-split" + ? "legacy_readback_resource_limit" + : mode === "uninterrupted" ? "endpoint" : "resume_exact"; + const result = { + schemaVersion: 1, + id, + sourceId, + mode, + expected, + status: "completed", + configuration: {}, + compiler: {}, + source: {}, + elapsedMs: 1, + peakRssBytes: 1, + }; + if (mode === "uninterrupted") return { ...result, uninterrupted: resultObservation("b") }; + const legacyBoundary = id === "intercept-legacy-split"; + return { + ...result, + base: resultObservation("a"), + ...(!legacyBoundary ? { resumed: resultObservation("b") } : {}), + checkpoint: { + id: `checkpoint-${sourceId}`, + logicalCheckpointUtf8Bytes: 100, + logicalCheckpoint: resultObservation("c").digest, + write: { + materializedCheckpointGraphs: mode === "framed-split" ? 0 : 1, + engineSourceMaterializedGraphs: mode === "framed-split" ? 0 : null, + legacySerializationApplied: mode !== "framed-split", + legacyCompressionApplied: mode !== "framed-split", + }, + readback: legacyBoundary + ? { status: "resource_limit", kind: "resource_limit", stage: "decompression", unit: "bytes" } + : { status: "completed" }, + preservation: { payloadBytesUnchanged: true, listingUnchanged: true }, + }, + }; +} + +test("checkpoint-v2 promotion manifest freezes the mirrored six-cell order and vendored source provenance", async () => { + const value = manifest(); + validateCheckpointV2EvaluationManifest(value); + assert.deepStrictEqual(value.controls.cellOrder, value.cells.map((entry) => entry.id)); + assert.deepStrictEqual(value.cells.map((entry) => entry.mode), [ + "legacy-split", "framed-split", "uninterrupted", + "uninterrupted", "framed-split", "legacy-split", + ]); + const validated = await Promise.all(value.sources.map((source) => validateEvaluationSource( + path.dirname(MANIFEST_FILE), + source + ))); + assert.deepStrictEqual(validated.map((source) => source.relativeFiles.length), [21, 1]); + for (const source of value.sources) { + const compiled = await compile(path.resolve(path.dirname(MANIFEST_FILE), source.story)); + assert.strictEqual(compiled.success, true); + assert.strictEqual( + sha256(`${compiled.storyJson.replace(/\r?\n$/, "")}\n`), + source.compiledStorySha256 + ); + } + + const drifted = structuredClone(value.sources[0]); + drifted.entrypointSha256 = "0".repeat(64); + await assert.rejects( + () => validateEvaluationSource(path.dirname(MANIFEST_FILE), drifted), + /source_entrypoint_sha256_drift/ + ); +}); + +test("streaming JSON digests exactly match JSON.stringify without constructing a report-sized string", () => { + const value = { + finite: 1.25, + nonFinite: Number.POSITIVE_INFINITY, + text: "quote: \" and newline\n", + array: [1, undefined, true, null], + nested: { kept: "yes", omitted: undefined }, + }; + const serialized = JSON.stringify(value); + assert.deepStrictEqual(digestJson(value), { + sha256: sha256(serialized), + utf8Bytes: Buffer.byteLength(serialized), + }); +}); + +test("fast synthetic legacy, engine-native framed, and uninterrupted cells retain exact endpoints", async () => { + const source = await syntheticSource(); + const specs = [ + cell("synthetic-legacy", "legacy-split"), + cell("synthetic-framed", "framed-split"), + cell("synthetic-uninterrupted", "uninterrupted"), + ]; + const controls = syntheticControls(specs.map((entry) => entry.id)); + const legacy = await runSynthetic(source, specs[0], controls); + const framed = await runSynthetic(source, specs[1], controls); + const uninterrupted = await runSynthetic(source, specs[2], controls); + + assert.strictEqual(legacy.checkpoint.storageEncoding, "gzip", "omitting format retains the legacy default"); + assert.strictEqual(legacy.checkpoint.write.legacySerializationApplied, true); + assert.deepStrictEqual(framed.checkpoint.write, { + outcome: "created", + materializedCheckpointGraphs: 0, + engineSourceMaterializedGraphs: 0, + engineSourceIdentityPasses: 1, + engineSourceFramePasses: 1, + legacySerializationApplied: false, + legacyCompressionApplied: false, + framedV2Applied: true, + }); + assert.deepStrictEqual(legacy.base.digest, framed.base.digest); + assert.strictEqual(legacy.checkpoint.id, framed.checkpoint.id); + assert.strictEqual(legacy.checkpoint.logicalCheckpointUtf8Bytes, framed.checkpoint.logicalCheckpointUtf8Bytes); + assert.deepStrictEqual(legacy.checkpoint.logicalCheckpoint, framed.checkpoint.logicalCheckpoint); + assert.deepStrictEqual(legacy.resumed.digest, uninterrupted.uninterrupted.digest); + assert.deepStrictEqual(framed.resumed.digest, uninterrupted.uninterrupted.digest); + + const output = JSON.stringify({ legacy, framed, uninterrupted }); + assert.strictEqual(output.includes(REPOSITORY), false); + assert.strictEqual(output.includes("Wide tree leaf."), false); + assert.strictEqual(output.includes("[Left]"), false); +}); + +test("hard timeout kills the isolated Unix process group and returns typed cleanup scope", async (context) => { + if (process.platform === "win32") { + context.skip("Unix process-group assertion has a separate process-tree implementation on Windows"); + return; + } + const script = [ + "const {spawn}=require('node:child_process')", + "const child=spawn(process.execPath,['-e','setInterval(()=>{},1000)'])", + "process.stdout.write(String(child.pid)+'\\n')", + "setInterval(()=>{},1000)", + ].join(";"); + const result = await runBoundedProcess(process.execPath, ["-e", script], { + cwd: REPOSITORY, + timeoutMs: 150, + stdoutLimitBytes: 1024, + stderrLimitBytes: 1024, + killGraceMs: 50, + }); + assert.strictEqual(result.status, "timeout"); + assert.strictEqual(result.cleanupScope, "process_group"); + const descendant = Number(result.stdout.trim()); + assert.ok(Number.isSafeInteger(descendant)); + await new Promise((resolve) => setTimeout(resolve, 50)); + assert.throws(() => process.kill(descendant, 0), /ESRCH/); +}); + +test("hard timeout still force-kills a SIGTERM-resistant descendant after its leader exits", async () => { + const descendantScript = [ + "process.on('SIGTERM',()=>{})", + "process.stdout.write('ready')", + "setInterval(()=>{},1000)", + ].join(";"); + const leaderScript = [ + "const {spawn}=require('node:child_process')", + "process.on('SIGTERM',()=>process.exit(0))", + `const child=spawn(process.execPath,['-e',${JSON.stringify(descendantScript)}],{stdio:['ignore','pipe','ignore']})`, + "child.stdout.once('data',()=>process.stdout.write(String(child.pid)+'\\n'))", + "setInterval(()=>{},1000)", + ].join(";"); + const result = await runBoundedProcess(process.execPath, ["-e", leaderScript], { + cwd: REPOSITORY, + timeoutMs: 250, + stdoutLimitBytes: 1024, + stderrLimitBytes: 1024, + killGraceMs: 75, + }); + assert.strictEqual(result.status, "timeout"); + assert.strictEqual(result.cleanupScope, process.platform === "win32" ? "process_tree" : "process_group"); + const descendant = Number(result.stdout.trim()); + assert.ok(Number.isSafeInteger(descendant)); + const deadline = Date.now() + 2_000; + while (true) { + try { + process.kill(descendant, 0); + } catch (error) { + assert.match(String(error), /ESRCH/); + break; + } + if (Date.now() >= deadline) assert.fail(`descendant ${descendant} survived forced cleanup`); + await new Promise((resolve) => setTimeout(resolve, 25)); + } +}); + +test("filtered and unexpectedly successful legacy boundary evidence can never pass", () => { + const value = manifest(); + validateCheckpointV2EvaluationManifest(value); + const all = [ + completedCase("heresy2-legacy-split", "legacy-split"), + completedCase("heresy2-framed-split", "framed-split"), + completedCase("heresy2-uninterrupted", "uninterrupted"), + completedCase("intercept-uninterrupted", "uninterrupted"), + completedCase("intercept-framed-split", "framed-split"), + completedCase("intercept-legacy-split", "legacy-split"), + ]; + assert.strictEqual( + evaluateCheckpointV2Verdict(value, all, value.controls.cellOrder, true, true).status, + "passed" + ); + const filtered = evaluateCheckpointV2Verdict(value, [all[0]], [all[0].id], true, true); + assert.strictEqual(filtered.completeMatrix, false); + assert.strictEqual(filtered.status, "inconclusive"); + assert.deepStrictEqual(filtered.allowedClaims, []); + + all[5] = { ...all[5], status: "inconclusive", reason: "legacy_readback_succeeded" }; + const unexpected = evaluateCheckpointV2Verdict(value, all, value.controls.cellOrder, true, true); + assert.strictEqual(unexpected.status, "inconclusive"); + assert.deepStrictEqual(unexpected.allowedClaims, []); + assert.ok(unexpected.uncertainties.includes("intercept-legacy-split:legacy_readback_succeeded")); +}); + +test("evaluation output replacement is atomic and leaves no private temporary", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "inkcheck-checkpoint-v2-output-")); + const output = path.join(root, "result.json"); + try { + fs.writeFileSync(output, "old\n"); + writeEvaluationOutputAtomic(output, "new\n"); + assert.strictEqual(fs.readFileSync(output, "utf8"), "new\n"); + assert.deepStrictEqual(fs.readdirSync(root), ["result.json"]); + if (process.platform !== "win32") assert.strictEqual(fs.statSync(output).mode & 0o777, 0o600); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test("atomic progress snapshots retain prior cases and never authorize an in-progress proof", () => { + const selected = ["cell-a", "cell-b"]; + assert.deepStrictEqual(evaluationProgressSnapshot(selected, [], undefined, "in_progress"), { + status: "in_progress", + selectedCellCount: 2, + recordedCellCount: 0, + recordedCellIds: [], + }); + assert.deepStrictEqual(evaluationProgressSnapshot(selected, [], "cell-a", "in_progress"), { + status: "in_progress", + selectedCellCount: 2, + recordedCellCount: 0, + recordedCellIds: [], + activeCellId: "cell-a", + }); + assert.deepStrictEqual(evaluationProgressSnapshot(selected, ["cell-a"], "cell-b", "in_progress"), { + status: "in_progress", + selectedCellCount: 2, + recordedCellCount: 1, + recordedCellIds: ["cell-a"], + activeCellId: "cell-b", + }); + assert.deepStrictEqual(evaluationProgressSnapshot(selected, selected, undefined, "completed"), { + status: "completed", + selectedCellCount: 2, + recordedCellCount: 2, + recordedCellIds: selected, + }); + assert.throws( + () => evaluationProgressSnapshot(selected, ["cell-b"], undefined, "in_progress"), + /evaluation_progress_order/ + ); + assert.throws( + () => evaluationProgressSnapshot(selected, ["cell-a"], undefined, "completed"), + /evaluation_progress_incomplete/ + ); + const passing = { + status: "passed", + completeMatrix: true, + gates: [], + violations: [], + uncertainties: [], + allowedClaims: ["would_be_terminal_only"], + forbiddenClaims: [], + }; + const partial = evaluationSnapshotVerdict(passing, "in_progress"); + assert.strictEqual(partial.status, "inconclusive"); + assert.deepStrictEqual(partial.allowedClaims, []); + assert.ok(partial.uncertainties.includes("evaluation_in_progress")); +}); + +test("the proof package entrypoint rebuilds ignored dist and ships its frozen contract", () => { + const packageValue = JSON.parse(fs.readFileSync(path.join(REPOSITORY, "package.json"), "utf8")); + assert.strictEqual( + packageValue.scripts["evaluate-checkpoint-v2"], + "npm run --silent build && node --max-old-space-size=6144 dist/checkpoint-v2-evaluation-cli.js" + ); + assert.ok(packageValue.files.includes("benchmarks/checkpoint-v2-promotion-v1.json")); + assert.ok(packageValue.files.includes("docs/checkpoint-v2-promotion-evaluation.md")); +}); diff --git a/test/checkpoint-v2.test.js b/test/checkpoint-v2.test.js index add9d88..dd7679d 100644 --- a/test/checkpoint-v2.test.js +++ b/test/checkpoint-v2.test.js @@ -302,7 +302,7 @@ test("outer byte caps do not silently raise framed codec defaults", async () => maxCheckpointBytes: raisedOuterCap, maxProjectBytes: raisedOuterCap, }); - await openCheckpointArtifact(fixture.root, saved.id, { + const opened = await openCheckpointArtifact(fixture.root, saved.id, { maxStoredBytes: raisedOuterCap, maxDecompressedBytes: raisedOuterCap, }); @@ -314,9 +314,106 @@ test("outer byte caps do not silently raise framed codec defaults", async () => readLimits.maxTotalStoredBytes, DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS.maxTotalStoredBytes ); - assert.ok( - readLimits.maxTotalDecodedBytes <= DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS.maxTotalDecodedBytes + assert.strictEqual( + readLimits.maxTotalDecodedBytes, + DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS.maxTotalDecodedBytes + ); + assert.strictEqual( + opened.accounting.configuredLimits.framedV2.maxTotalStoredBytes, + DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS.maxTotalStoredBytes + ); + assert.strictEqual( + opened.accounting.configuredLimits.framedV2.maxTotalDecodedBytes, + DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS.maxTotalDecodedBytes + ); + const aboveWholeStringLimit = require("node:buffer").constants.MAX_STRING_LENGTH + 1024; + const independentlyBounded = await openCheckpointArtifact(fixture.root, saved.id, { + maxStoredBytes: raisedOuterCap, + maxDecompressedBytes: 1, + framedV2Limits: { maxTotalDecodedBytes: aboveWholeStringLimit }, + }); + assert.strictEqual(readLimits.maxTotalDecodedBytes, aboveWholeStringLimit, + "framed cumulative decoded bytes are independent of the schema-v1 whole-string cap"); + assert.strictEqual(independentlyBounded.accounting.configuredLimits.maxDecompressedBytes, 1); + assert.deepStrictEqual(independentlyBounded.accounting.configuredLimits.framedV2, { + basis: "effective_codec_limits_not_allocated_capacity", + ...DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS, + maxTotalDecodedBytes: aboveWholeStringLimit, + }); + assert.deepStrictEqual( + Object.keys(independentlyBounded.accounting.configuredLimits.framedV2), + [ + "basis", + "maxHeaderBytes", + "maxStoredFrameBytes", + "maxDecodedFrameBytes", + "maxRecordBytes", + "maxJsonDepth", + "maxRecordsPerFrame", + "maxFrames", + "maxTotalRecords", + "maxTotalStoredBytes", + "maxTotalDecodedBytes", + ], + "the public receipt has one exact allowlisted limit shape" + ); + assert.ok(independentlyBounded.accounting.framedV2.codec.data.decodedBytes > 1, + "success proves the legacy one-byte string cap was not applied to framed decoded data"); + await assert.rejects( + () => openCheckpointArtifact(fixture.root, saved.id, { + maxStoredBytes: raisedOuterCap, + maxDecompressedBytes: 1, + framedV2Limits: { maxTotalDecodedBytes: 1 }, + }), + (error) => { + assert.ok(error instanceof CheckpointReadError); + assert.strictEqual(error.kind, "resource_limit"); + assert.strictEqual(error.accounting.configuredLimits.maxDecompressedBytes, 1); + assert.deepStrictEqual(error.accounting.configuredLimits.framedV2, { + basis: "effective_codec_limits_not_allocated_capacity", + ...DEFAULT_CHECKPOINT_ARTIFACT_V2_LIMITS, + maxTotalDecodedBytes: 1, + }); + return true; + } + ); + await assert.rejects( + () => openCheckpointArtifact(fixture.root, saved.id, { + framedV2Limits: { surprise: 1 }, + }), + (error) => { + assert.ok(error instanceof CheckpointReadError); + assert.strictEqual(error.kind, "unsupported"); + assert.strictEqual(error.accounting.configuredLimits.framedV2, undefined); + assert.deepStrictEqual(Object.keys(error.accounting.configuredLimits), [ + "basis", "maxManifestBytes", "maxStoredBytes", "maxDecompressedBytes", + ]); + return true; + } + ); + await assert.rejects( + () => openCheckpointArtifact(fixture.root, saved.id, { + framedV2Limits: { maxFrames: "not-a-number" }, + }), + (error) => { + assert.ok(error instanceof CheckpointReadError); + assert.strictEqual(error.kind, "resource_limit"); + assert.strictEqual(error.unit, "count"); + assert.strictEqual(error.observed, undefined); + assert.strictEqual(error.accounting.configuredLimits.framedV2, undefined); + return true; + } + ); + const nextCheckpoint = structuredClone(fixture.checkpoint); + nextCheckpoint.state.dedupeHits += 1; + const immediateWrite = await saveCheckpointArtifact( + fixture.root, + fixture.story, + nextCheckpoint, + { format: "framed-v2" } ); + assert.notStrictEqual(immediateWrite.id, saved.id, + "failed framed-read teardown settles before the next writer can reuse its descriptor"); } finally { checkpointV2Codec.writeCheckpointArtifactV2 = originalWrite; checkpointV2Codec.readCheckpointArtifactV2 = originalRead; diff --git a/test/inkcheck.test.js b/test/inkcheck.test.js index e63e50b..0fd0015 100644 --- a/test/inkcheck.test.js +++ b/test/inkcheck.test.js @@ -2945,7 +2945,27 @@ test("checkpoint publication serializes cross-process same-ID writers and recove })); fs.rmSync(artifactFile); fs.rmSync(manifestFile); - await saveCheckpointArtifact(tmp, story, checkpoint); + const originalCleaningWrite = fs.writeFileSync; + let injectedWindowsCleaningContention = false; + fs.writeFileSync = (candidate, ...args) => { + const options = args[1]; + if (!injectedWindowsCleaningContention + && path.resolve(String(candidate)) === path.resolve(staleCleaning) + && options?.flag === "wx") { + injectedWindowsCleaningContention = true; + const error = new Error("simulated Windows delete-pending cleaning claim"); + error.code = "EPERM"; + throw error; + } + return originalCleaningWrite(candidate, ...args); + }; + try { + await saveCheckpointArtifact(tmp, story, checkpoint); + } finally { + fs.writeFileSync = originalCleaningWrite; + } + assert.strictEqual(injectedWindowsCleaningContention, true, + "Windows EPERM while acquiring a cleaning claim is exercised as fail-closed contention"); assert.strictEqual(fs.existsSync(staleCleaning), true, "a crashed cleaning owner consumes one bounded slot instead of permitting pathname reuse"); assert.strictEqual((await openCheckpointArtifact(tmp, id)).artifact.totalGranted, 20); From 416305c7a49e0670914dcc9116005890616c3e4a Mon Sep 17 00:00:00 2001 From: Daniel Strader Date: Sat, 5 Sep 2026 20:01:15 -0700 Subject: [PATCH 2/3] Record matched checkpoint v2 evidence --- CHANGELOG.md | 4 +- .../results/checkpoint-v2-promotion-v1.json | 754 ++++++++++++++++++ docs/checkpoint-v2-promotion-evaluation.md | 27 + docs/local-checkpoints.md | 2 +- docs/shared-checkpoint-artifact-v2.md | 11 +- package.json | 1 + test/checkpoint-v2-evaluation.test.js | 1 + 7 files changed, 792 insertions(+), 8 deletions(-) create mode 100644 benchmarks/results/checkpoint-v2-promotion-v1.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ffbddfe..cfbcb6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ ## Unreleased -- Add an opt-in engine-native producer for framed checkpoint v2. A shared-search engine can remain quiescent at its exact resumable boundary while a callback-scoped, repeatable source streams the unchanged canonical schema-v1 identity and ordered frame records; the source yields detached values, snapshots mutable caller inputs, prevents evidence callbacks from aliasing retained findings, invalidates at synchronous or awaited callback settlement, and never constructs or recursively clones a complete checkpoint graph. Write receipts distinguish zero materialized graphs, one identity pass, and separate zero-or-one candidate-encoding and reuse-verification record passes. Same-ID framed reuse bounded-stream-compares canonical records with the live source instead of trusting a recomputed manifest/payload pair; legacy reuse and sidecar-free retention inventory fail closed on this graph-free route while remaining available to the graph API. Framed read receipts expose their actual effective codec limits, including cumulative decoded bytes independently of the legacy whole-value string ceiling, and restore structurally clones the validated graph instead of recreating one artifact-sized JSON string, while stored, frame, record, depth, and runtime bounds remain enforced. Windows recovery-slot cleaning treats ordinary `EPERM`/`EBUSY`/`EACCES` namespace contention as failure to acquire—not ownership—without weakening durable claim or manifest writes. Existing synchronous and graph APIs, stable IDs, default legacy-v1 CLI/MCP writes, search order, and resource policy remain unchanged. This is still a partial #156 foundation pending matched Intercept and Heresy II evidence; it does not promote a default, stream provisional read records into a live engine, restart InkBench, or publish a release. -- Add an opt-in framed checkpoint artifact-v2 codec around the unchanged logical shared-checkpoint schema v1. Ordered configuration, scheduler, frontier, witness-ancestry, dedupe, semantic-index, finding, and metadata records are independently bounded and compressed; exact frame/component checksums and a terminal index support incremental readback without a whole stored artifact, decompressed artifact, or raw artifact string. Cross-format writers share one neutral no-clobber commit point, so a stable ID reuses exactly one verified `.json.gz` or `.inkcp` layout under the existing crash-recovery and retention protocol. Observed-before-work, preflight-without-output, and successfully encoded no-clobber losers reuse the winner; a private candidate that emitted bytes and then failed preserves its error for a later retry. Stable checkpoint IDs, exact resume state, default gzip writes, default CLI/MCP search behavior, search order, and resource policy are unchanged; storage listing and search-session disk accounting now recognize, reuse, and attribute either verified layout. This is a codec foundation only: framed v2 remains an explicit library opt-in pending Intercept plus second-family promotion evidence, and it does not complete #156, activate checkpoint/epoch/pressure policy, restart InkBench, or publish a release. +- Add an opt-in engine-native producer for framed checkpoint v2. A shared-search engine can remain quiescent at its exact resumable boundary while a callback-scoped, repeatable source streams the unchanged canonical schema-v1 identity and ordered frame records; the source yields detached values, snapshots mutable caller inputs, prevents evidence callbacks from aliasing retained findings, invalidates at synchronous or awaited callback settlement, and never constructs or recursively clones a complete checkpoint graph. Write receipts distinguish zero materialized graphs, one identity pass, and separate zero-or-one candidate-encoding and reuse-verification record passes. Same-ID framed reuse bounded-stream-compares canonical records with the live source instead of trusting a recomputed manifest/payload pair; legacy reuse and sidecar-free retention inventory fail closed on this graph-free route while remaining available to the graph API. Framed read receipts expose their actual effective codec limits, including cumulative decoded bytes independently of the legacy whole-value string ceiling, and restore structurally clones the validated graph instead of recreating one artifact-sized JSON string, while stored, frame, record, depth, and runtime bounds remain enforced. Windows recovery-slot cleaning treats ordinary `EPERM`/`EBUSY`/`EACCES` namespace contention as failure to acquire—not ownership—without weakening durable claim or manifest writes. Existing synchronous and graph APIs, stable IDs, default legacy-v1 CLI/MCP writes, search order, and resource policy remain unchanged. The checked six-cell evaluation on candidate `9312811` records exact Heresy II 100K→150K and Intercept 600K→650K framed resumes, zero materialized checkpoint graphs on both framed writes, and the matched Intercept legacy decompression limit at 536,870,888 bytes without artifact mutation. This is still a partial #156 foundation: the evidence does not promote a default, stream provisional read records into a live engine, restart InkBench, establish portable performance or memory improvement, or publish a release. +- Add an opt-in framed checkpoint artifact-v2 codec around the unchanged logical shared-checkpoint schema v1. Ordered configuration, scheduler, frontier, witness-ancestry, dedupe, semantic-index, finding, and metadata records are independently bounded and compressed; exact frame/component checksums and a terminal index support incremental readback without a whole stored artifact, decompressed artifact, or raw artifact string. Cross-format writers share one neutral no-clobber commit point, so a stable ID reuses exactly one verified `.json.gz` or `.inkcp` layout under the existing crash-recovery and retention protocol. Observed-before-work, preflight-without-output, and successfully encoded no-clobber losers reuse the winner; a private candidate that emitted bytes and then failed preserves its error for a later retry. Stable checkpoint IDs, exact resume state, default gzip writes, default CLI/MCP search behavior, search order, and resource policy are unchanged; storage listing and search-session disk accounting now recognize, reuse, and attribute either verified layout. This is a codec foundation only: the later engine-native evaluation supplies its declared Intercept plus second-family functional evidence, but framed v2 remains an explicit library opt-in and the work does not complete #156, activate checkpoint/epoch/pressure policy, restart InkBench, or publish a release. - Extend the partial owner ledger across checkpoint reopen and report enrichment/finalization without adding another payload read, decode, parse, or stable-ID traversal. Successful checkpoint opens and resumes expose bounded stage-by-stage receipts, while typed corrupt/unsupported/resource-limit failures retain the work reached before failure. The opt-in accounted report builder measures the source exploration graph, finding-identity strings already materialized by enrichment, and the returned report; report saves separately distinguish created envelope graphs from reuse-parsed artifact graphs. These compact-JSON serialized-view proxies and their conservative potential sums are not exclusive retained-owner bytes or observed heap peaks. Runtime receipts remain outside saved report/checkpoint payloads and IDs, and bounded JSON streaming still avoids the monolithic report graph. This adds no checkpoint v2/framed readback, allocation, eviction, compaction, stopping policy, benchmark restart, or release; #156 and #216 remain open. - Add versioned retained-owner and finalization receipts without changing search policy or the checksum-bound observability-v1/v2 shapes. Shared pass telemetry now accounts for the bounded observability ledger through a non-recursive canonical UTF-8 projection and preserves its known high-water state across resume; older checkpoints remain readable and explicitly report incomplete owner history. Checkpoint saves report logical graph bytes, streamed artifact/chunk bytes, compressed output, configured compression capacity, durable payload/manifest bytes, and whether encode/compress work was applied or an existing stable ID was reused. Report saves similarly expose canonical identity-string, artifact-serialization, conservative potential string, and durable bytes outside the content-derived report. Configured finalization headroom is labeled unallocated capacity and remains excluded from retained/process totals. Stable-ID algorithms are unchanged: deterministic owner state participates in newly emitted checkpoint content, while post-write receipts never feed back into the IDs or payloads they measure. These fields are observational/accounting inputs only: they do not emit the reserved checkpoint/epoch/pressure reasons, change frontier order, activate a budget or stopping policy, add checkpoint v2, or publish a release; #156 and #216 remain open. - Extend shared-search observability to nested schema v2 with monotonic sample sequences, canonical coalesced reason vectors, an exact 15-bit trigger mask, a fixed eight-count boundary-local trigger-yield tuple, category/frontier/cadence/termination triggers, and category-specific first/last discovery, current/longest dry-state, and identities-per-million-transition facts. The redundant vector/mask/tuple preserves exact retained-boundary triggers across compaction while aggregate interval deltas are rebuilt: adjacent complete-history samples require the tuple to equal the semantic interval delta, while genuine compaction gaps or incomplete v1 history allow only a componentwise-bounded tuple. Observability-v1 migration synthesizes masks only from explicit historical boundaries and uses a zero tuple to mean no v1 trigger was recorded, not that an interval was event-free. Sequence validation applies monotonic and feasible record/state/cadence bounds without claiming exact replay of discarded compaction history. Emitted v2 checkpoints add a domain-separated, key-order-independent canonical SHA-256 self-check binding every fixed known ledger field; it detects stale or accidental mutation but is not authentication because a writer can recompute it, and persisted payload/manifest digests remain the external artifact-corruption boundary. V1 and pre-ledger inputs gain the nested checksum on their first emitted v2 checkpoint. Live process sampling and progress output remain bounded to cadence and final termination while the compacted deterministic ledger retains event boundaries. Checkpoint construction remains side-effect-free and deterministic: the persisted ledger participates in stable checkpoint identity, while observability-v1 checkpoints migrate explicitly with incomplete event history and nullable facts rather than invented history. This remains an independent partial #216 slice: `checkpoint`, `epoch`, and generic `pressure` reasons are reserved but not emitted; retained-owner accounting remains incomplete; no resource, allocation, or stopping policy is activated; #216 remains open; and the earlier V1 overhead study is not an exact-head overhead claim for this V2 implementation. diff --git a/benchmarks/results/checkpoint-v2-promotion-v1.json b/benchmarks/results/checkpoint-v2-promotion-v1.json new file mode 100644 index 0000000..c38893d --- /dev/null +++ b/benchmarks/results/checkpoint-v2-promotion-v1.json @@ -0,0 +1,754 @@ +{ + "schemaVersion": 1, + "kind": "checkpoint_v2_promotion_evaluation", + "evaluationId": "checkpoint-v2-promotion-v1", + "generatedAt": "2026-09-06T02:55:15.731Z", + "candidate": { + "manifestSha256": "7f28003f7eaae71f4805450d22b62f29a248b0d632a8ce7b33e2e76699e85988", + "git": { + "head": "9312811be8f0e79b8f05ce7e0d09a2626063b8ea", + "tree": "966ef5aece7d00974b6c8c9e870d4013762c596b", + "clean": true, + "dirtyEntryCount": 0 + }, + "packageVersion": "0.7.2", + "dependencyLockSha256": "118098bc49f332257dfa566496148d89d9141cfffa4971009982fa5c333c605e", + "dist": { + "fileCount": 48, + "bundleSha256": "dd52f43f9b438990f5dde7b10257afdc0e2139324e807403c026117c45d36c19" + } + }, + "runtime": { + "nodeVersion": "v22.13.1", + "v8Version": "12.4.254.21-node.22", + "platform": "darwin", + "architecture": "arm64", + "logicalCpuCount": 8, + "cpuModel": "Apple M1", + "totalMemoryBytes": 8589934592, + "v8HeapLimitBytes": 6492782592, + "maxOldSpaceSizeMb": 6144 + }, + "compiler": { + "name": "inklecate", + "version": "1.2.1", + "executableSha256": "df0f53f321203bb5f7ccfe7debb3f0f9abb3f328a56363bef4dde605baf17e5e", + "resolutionClass": "managed_cache" + }, + "selection": { + "serial": true, + "requestedCellIds": [], + "selectedCellIds": [ + "heresy2-legacy-split", + "heresy2-framed-split", + "heresy2-uninterrupted", + "intercept-uninterrupted", + "intercept-framed-split", + "intercept-legacy-split" + ], + "completeMatrix": true + }, + "progress": { + "status": "completed", + "selectedCellCount": 6, + "recordedCellCount": 6, + "recordedCellIds": [ + "heresy2-legacy-split", + "heresy2-framed-split", + "heresy2-uninterrupted", + "intercept-uninterrupted", + "intercept-framed-split", + "intercept-legacy-split" + ] + }, + "controls": { + "cellOrder": [ + "heresy2-legacy-split", + "heresy2-framed-split", + "heresy2-uninterrupted", + "intercept-uninterrupted", + "intercept-framed-split", + "intercept-legacy-split" + ], + "maxDepth": 100, + "searchSeed": 7, + "storySeed": 1, + "concurrency": 1, + "minimizeRepros": false, + "stateSensitivity": "source_semantics", + "loopRiskDetection": "only_without_turns_randomness_visit_counts_or_externals", + "maxMemoryMb": 4096, + "maxTimeMs": 840000, + "workerTimeoutMs": 900000, + "coordinatorMaxOldSpaceMb": 6144, + "storage": { + "maxCheckpointBytes": 536870912, + "maxProjectBytes": 2147483648, + "framedV2": { + "maxTotalDecodedBytes": 2147483648 + } + } + }, + "sources": [ + { + "id": "heresy2", + "upstream": "randall-frank/heresy2-assets", + "commit": "37b8a7804217bb40a9f69f6fd9c173f2017d550e", + "license": "CC-BY-4.0", + "entrypointSha256": "49db768479e523f39ae9eb838f6daa0798244b7bbe266d686a7a967ccf66f7ea", + "licenseSha256": "7e7170e3cebf88a9f60c7b8421418323c09304da1af4d5e90f4da1dc1c8a2661", + "bundleSha256": "2991d13fbf2d036c9bfb5cf4207c16509dfdf77dae24fa410ace1a0d3a36b626", + "fileCount": 21, + "compiledStorySha256": "5c65cf077478ad70985341e564695cb8ab35af20ec574d12b517d1e18f9a4bb7" + }, + { + "id": "intercept", + "upstream": "inkle/the-intercept", + "commit": "2a816b56e61ce4bf02bec1c638074645bdd871e3", + "license": "MIT", + "entrypointSha256": "1d7b5653b689a9da6801bbfaaa3fbd20b0cc47113b28bb1b1774f6cd11e9eea4", + "licenseSha256": "4b0d7e6b2ccac7794d0705ee40a6728d664ff1cd80994b5d31077a4cde38f3da", + "bundleSha256": "c9865a7d2d334e39b68f788f790d202a7c1fbd9acb2bf2fc54fa1a4d549db6c9", + "fileCount": 1, + "compiledStorySha256": "2a441a97a345cfc7d3c944e758949d6c3b37ccfe88a75ddad06cf3ac83a637c6" + } + ], + "cases": [ + { + "schemaVersion": 1, + "id": "heresy2-legacy-split", + "sourceId": "heresy2", + "mode": "legacy-split", + "expected": "resume_exact", + "configuration": { + "baseStates": 100000, + "targetStates": 150000, + "maxDepth": 100, + "searchSeed": 7, + "storySeed": 1, + "concurrency": 1, + "preserveTurnState": false, + "preserveRandomState": true, + "detectLoopRisks": false + }, + "compiler": { + "name": "inklecate", + "version": "1.2.1", + "executableSha256": "df0f53f321203bb5f7ccfe7debb3f0f9abb3f328a56363bef4dde605baf17e5e", + "resolutionClass": "managed_cache" + }, + "source": { + "fileCount": 21, + "bundleSha256": "2991d13fbf2d036c9bfb5cf4207c16509dfdf77dae24fa410ace1a0d3a36b626", + "compiledStorySha256": "5c65cf077478ad70985341e564695cb8ab35af20ec574d12b517d1e18f9a4bb7" + }, + "elapsedMs": 205456, + "peakRssBytes": 2349662208, + "status": "completed", + "base": { + "digest": { + "sha256": "b9d81a35ead806b2cb742a38515dbdcf61df3177c548ad7a2fe6b3194f66e3b2", + "utf8Bytes": 70116 + }, + "statesExplored": 100000, + "exhaustive": false, + "truncated": true, + "truncatedBy": { + "maxDepth": true, + "maxStates": true, + "beamWidth": false, + "frontier": false, + "memory": false, + "time": false, + "loop": false, + "worker": false + }, + "counts": { + "endings": 1, + "visibleOutcomes": 1, + "runtimeErrors": 0, + "runtimeWarnings": 0, + "visitedKnots": 14, + "unvisitedKnots": 16 + } + }, + "checkpoint": { + "requestedFormat": "legacy-v1", + "storageEncoding": "gzip", + "id": "checkpoint-5358f1fd60cd22200b9db873", + "logicalCheckpointUtf8Bytes": 370262984, + "payloadSizeBytes": 39126345, + "durableSizeBytes": 39126905, + "write": { + "outcome": "created", + "materializedCheckpointGraphs": 1, + "engineSourceMaterializedGraphs": null, + "engineSourceIdentityPasses": null, + "engineSourceFramePasses": null, + "legacySerializationApplied": true, + "legacyCompressionApplied": true, + "framedV2Applied": false + }, + "logicalCheckpoint": { + "sha256": "cfb4640deb4acda8cf2c617dee55c428f8941e8c5e65de3cb6b1bad3467c2fa4", + "utf8Bytes": 370262984 + }, + "readback": { + "status": "completed", + "storageEncoding": "gzip" + }, + "preservation": { + "payloadBytesUnchanged": true, + "listingUnchanged": true, + "listedBefore": 1, + "listedAfter": 1 + } + }, + "resumed": { + "digest": { + "sha256": "7e4eef738f7ae2c34a2abf031d3bb16b781a9f1eba8dcc6949f53aa42d526571", + "utf8Bytes": 78083 + }, + "statesExplored": 150000, + "exhaustive": false, + "truncated": true, + "truncatedBy": { + "maxDepth": true, + "maxStates": true, + "beamWidth": false, + "frontier": false, + "memory": false, + "time": false, + "loop": false, + "worker": false + }, + "counts": { + "endings": 1, + "visibleOutcomes": 1, + "runtimeErrors": 0, + "runtimeWarnings": 0, + "visitedKnots": 14, + "unvisitedKnots": 16 + } + } + }, + { + "schemaVersion": 1, + "id": "heresy2-framed-split", + "sourceId": "heresy2", + "mode": "framed-split", + "expected": "resume_exact", + "configuration": { + "baseStates": 100000, + "targetStates": 150000, + "maxDepth": 100, + "searchSeed": 7, + "storySeed": 1, + "concurrency": 1, + "preserveTurnState": false, + "preserveRandomState": true, + "detectLoopRisks": false + }, + "compiler": { + "name": "inklecate", + "version": "1.2.1", + "executableSha256": "df0f53f321203bb5f7ccfe7debb3f0f9abb3f328a56363bef4dde605baf17e5e", + "resolutionClass": "managed_cache" + }, + "source": { + "fileCount": 21, + "bundleSha256": "2991d13fbf2d036c9bfb5cf4207c16509dfdf77dae24fa410ace1a0d3a36b626", + "compiledStorySha256": "5c65cf077478ad70985341e564695cb8ab35af20ec574d12b517d1e18f9a4bb7" + }, + "elapsedMs": 88327, + "peakRssBytes": 1752760320, + "status": "completed", + "base": { + "digest": { + "sha256": "b9d81a35ead806b2cb742a38515dbdcf61df3177c548ad7a2fe6b3194f66e3b2", + "utf8Bytes": 70116 + }, + "statesExplored": 100000, + "exhaustive": false, + "truncated": true, + "truncatedBy": { + "maxDepth": true, + "maxStates": true, + "beamWidth": false, + "frontier": false, + "memory": false, + "time": false, + "loop": false, + "worker": false + }, + "counts": { + "endings": 1, + "visibleOutcomes": 1, + "runtimeErrors": 0, + "runtimeWarnings": 0, + "visitedKnots": 14, + "unvisitedKnots": 16 + } + }, + "checkpoint": { + "requestedFormat": "framed-v2", + "storageEncoding": "framed-v2", + "id": "checkpoint-5358f1fd60cd22200b9db873", + "logicalCheckpointUtf8Bytes": 370262984, + "payloadSizeBytes": 38472302, + "durableSizeBytes": 38475807, + "write": { + "outcome": "created", + "materializedCheckpointGraphs": 0, + "engineSourceMaterializedGraphs": 0, + "engineSourceIdentityPasses": 1, + "engineSourceFramePasses": 1, + "legacySerializationApplied": false, + "legacyCompressionApplied": false, + "framedV2Applied": true + }, + "logicalCheckpoint": { + "sha256": "cfb4640deb4acda8cf2c617dee55c428f8941e8c5e65de3cb6b1bad3467c2fa4", + "utf8Bytes": 370262984 + }, + "readback": { + "status": "completed", + "storageEncoding": "framed-v2" + }, + "preservation": { + "payloadBytesUnchanged": true, + "listingUnchanged": true, + "listedBefore": 1, + "listedAfter": 1 + } + }, + "resumed": { + "digest": { + "sha256": "7e4eef738f7ae2c34a2abf031d3bb16b781a9f1eba8dcc6949f53aa42d526571", + "utf8Bytes": 78083 + }, + "statesExplored": 150000, + "exhaustive": false, + "truncated": true, + "truncatedBy": { + "maxDepth": true, + "maxStates": true, + "beamWidth": false, + "frontier": false, + "memory": false, + "time": false, + "loop": false, + "worker": false + }, + "counts": { + "endings": 1, + "visibleOutcomes": 1, + "runtimeErrors": 0, + "runtimeWarnings": 0, + "visitedKnots": 14, + "unvisitedKnots": 16 + } + } + }, + { + "schemaVersion": 1, + "id": "heresy2-uninterrupted", + "sourceId": "heresy2", + "mode": "uninterrupted", + "expected": "endpoint", + "configuration": { + "baseStates": 100000, + "targetStates": 150000, + "maxDepth": 100, + "searchSeed": 7, + "storySeed": 1, + "concurrency": 1, + "preserveTurnState": false, + "preserveRandomState": true, + "detectLoopRisks": false + }, + "compiler": { + "name": "inklecate", + "version": "1.2.1", + "executableSha256": "df0f53f321203bb5f7ccfe7debb3f0f9abb3f328a56363bef4dde605baf17e5e", + "resolutionClass": "managed_cache" + }, + "source": { + "fileCount": 21, + "bundleSha256": "2991d13fbf2d036c9bfb5cf4207c16509dfdf77dae24fa410ace1a0d3a36b626", + "compiledStorySha256": "5c65cf077478ad70985341e564695cb8ab35af20ec574d12b517d1e18f9a4bb7" + }, + "elapsedMs": 58327, + "peakRssBytes": 1373896704, + "status": "completed", + "uninterrupted": { + "digest": { + "sha256": "7e4eef738f7ae2c34a2abf031d3bb16b781a9f1eba8dcc6949f53aa42d526571", + "utf8Bytes": 78083 + }, + "statesExplored": 150000, + "exhaustive": false, + "truncated": true, + "truncatedBy": { + "maxDepth": true, + "maxStates": true, + "beamWidth": false, + "frontier": false, + "memory": false, + "time": false, + "loop": false, + "worker": false + }, + "counts": { + "endings": 1, + "visibleOutcomes": 1, + "runtimeErrors": 0, + "runtimeWarnings": 0, + "visitedKnots": 14, + "unvisitedKnots": 16 + } + } + }, + { + "schemaVersion": 1, + "id": "intercept-uninterrupted", + "sourceId": "intercept", + "mode": "uninterrupted", + "expected": "endpoint", + "configuration": { + "baseStates": 600000, + "targetStates": 650000, + "maxDepth": 100, + "searchSeed": 7, + "storySeed": 1, + "concurrency": 1, + "preserveTurnState": false, + "preserveRandomState": false, + "detectLoopRisks": true + }, + "compiler": { + "name": "inklecate", + "version": "1.2.1", + "executableSha256": "df0f53f321203bb5f7ccfe7debb3f0f9abb3f328a56363bef4dde605baf17e5e", + "resolutionClass": "managed_cache" + }, + "source": { + "fileCount": 1, + "bundleSha256": "c9865a7d2d334e39b68f788f790d202a7c1fbd9acb2bf2fc54fa1a4d549db6c9", + "compiledStorySha256": "2a441a97a345cfc7d3c944e758949d6c3b37ccfe88a75ddad06cf3ac83a637c6" + }, + "elapsedMs": 213395, + "peakRssBytes": 1841954816, + "status": "completed", + "uninterrupted": { + "digest": { + "sha256": "fec8965d6d4ea44c7466313474a753840ab6801587451d8abc0ffad2d1097346", + "utf8Bytes": 1983483 + }, + "statesExplored": 650000, + "exhaustive": false, + "truncated": true, + "truncatedBy": { + "maxDepth": false, + "maxStates": true, + "beamWidth": false, + "frontier": false, + "memory": false, + "time": false, + "loop": false, + "worker": false + }, + "counts": { + "endings": 1375, + "visibleOutcomes": 12, + "runtimeErrors": 0, + "runtimeWarnings": 0, + "visitedKnots": 29, + "unvisitedKnots": 1 + } + } + }, + { + "schemaVersion": 1, + "id": "intercept-framed-split", + "sourceId": "intercept", + "mode": "framed-split", + "expected": "resume_exact", + "configuration": { + "baseStates": 600000, + "targetStates": 650000, + "maxDepth": 100, + "searchSeed": 7, + "storySeed": 1, + "concurrency": 1, + "preserveTurnState": false, + "preserveRandomState": false, + "detectLoopRisks": true + }, + "compiler": { + "name": "inklecate", + "version": "1.2.1", + "executableSha256": "df0f53f321203bb5f7ccfe7debb3f0f9abb3f328a56363bef4dde605baf17e5e", + "resolutionClass": "managed_cache" + }, + "source": { + "fileCount": 1, + "bundleSha256": "c9865a7d2d334e39b68f788f790d202a7c1fbd9acb2bf2fc54fa1a4d549db6c9", + "compiledStorySha256": "2a441a97a345cfc7d3c944e758949d6c3b37ccfe88a75ddad06cf3ac83a637c6" + }, + "elapsedMs": 273913, + "peakRssBytes": 2092597248, + "status": "completed", + "base": { + "digest": { + "sha256": "93fa7fe49e1fe8a882c04c5b4ac181825e040b8879681b6a8977256e93c5746f", + "utf8Bytes": 1914724 + }, + "statesExplored": 600000, + "exhaustive": false, + "truncated": true, + "truncatedBy": { + "maxDepth": false, + "maxStates": true, + "beamWidth": false, + "frontier": false, + "memory": false, + "time": false, + "loop": false, + "worker": false + }, + "counts": { + "endings": 1290, + "visibleOutcomes": 12, + "runtimeErrors": 0, + "runtimeWarnings": 0, + "visitedKnots": 29, + "unvisitedKnots": 1 + } + }, + "checkpoint": { + "requestedFormat": "framed-v2", + "storageEncoding": "framed-v2", + "id": "checkpoint-27fe6780e1c0ac80fbcbbbe5", + "logicalCheckpointUtf8Bytes": 624765446, + "payloadSizeBytes": 67737494, + "durableSizeBytes": 67741016, + "write": { + "outcome": "created", + "materializedCheckpointGraphs": 0, + "engineSourceMaterializedGraphs": 0, + "engineSourceIdentityPasses": 1, + "engineSourceFramePasses": 1, + "legacySerializationApplied": false, + "legacyCompressionApplied": false, + "framedV2Applied": true + }, + "logicalCheckpoint": { + "sha256": "a99fd85b123691421981f289c82e49afeb3a6ee5b32b717c20c3a84b9f5de682", + "utf8Bytes": 624765446 + }, + "readback": { + "status": "completed", + "storageEncoding": "framed-v2" + }, + "preservation": { + "payloadBytesUnchanged": true, + "listingUnchanged": true, + "listedBefore": 1, + "listedAfter": 1 + } + }, + "resumed": { + "digest": { + "sha256": "fec8965d6d4ea44c7466313474a753840ab6801587451d8abc0ffad2d1097346", + "utf8Bytes": 1983483 + }, + "statesExplored": 650000, + "exhaustive": false, + "truncated": true, + "truncatedBy": { + "maxDepth": false, + "maxStates": true, + "beamWidth": false, + "frontier": false, + "memory": false, + "time": false, + "loop": false, + "worker": false + }, + "counts": { + "endings": 1375, + "visibleOutcomes": 12, + "runtimeErrors": 0, + "runtimeWarnings": 0, + "visitedKnots": 29, + "unvisitedKnots": 1 + } + } + }, + { + "schemaVersion": 1, + "id": "intercept-legacy-split", + "sourceId": "intercept", + "mode": "legacy-split", + "expected": "legacy_readback_resource_limit", + "configuration": { + "baseStates": 600000, + "targetStates": 650000, + "maxDepth": 100, + "searchSeed": 7, + "storySeed": 1, + "concurrency": 1, + "preserveTurnState": false, + "preserveRandomState": false, + "detectLoopRisks": true + }, + "compiler": { + "name": "inklecate", + "version": "1.2.1", + "executableSha256": "df0f53f321203bb5f7ccfe7debb3f0f9abb3f328a56363bef4dde605baf17e5e", + "resolutionClass": "managed_cache" + }, + "source": { + "fileCount": 1, + "bundleSha256": "c9865a7d2d334e39b68f788f790d202a7c1fbd9acb2bf2fc54fa1a4d549db6c9", + "compiledStorySha256": "2a441a97a345cfc7d3c944e758949d6c3b37ccfe88a75ddad06cf3ac83a637c6" + }, + "elapsedMs": 360631, + "peakRssBytes": 1957625856, + "status": "completed", + "base": { + "digest": { + "sha256": "93fa7fe49e1fe8a882c04c5b4ac181825e040b8879681b6a8977256e93c5746f", + "utf8Bytes": 1914724 + }, + "statesExplored": 600000, + "exhaustive": false, + "truncated": true, + "truncatedBy": { + "maxDepth": false, + "maxStates": true, + "beamWidth": false, + "frontier": false, + "memory": false, + "time": false, + "loop": false, + "worker": false + }, + "counts": { + "endings": 1290, + "visibleOutcomes": 12, + "runtimeErrors": 0, + "runtimeWarnings": 0, + "visitedKnots": 29, + "unvisitedKnots": 1 + } + }, + "checkpoint": { + "requestedFormat": "legacy-v1", + "storageEncoding": "gzip", + "id": "checkpoint-27fe6780e1c0ac80fbcbbbe5", + "logicalCheckpointUtf8Bytes": 624765446, + "payloadSizeBytes": 67731526, + "durableSizeBytes": 67732091, + "write": { + "outcome": "created", + "materializedCheckpointGraphs": 1, + "engineSourceMaterializedGraphs": null, + "engineSourceIdentityPasses": null, + "engineSourceFramePasses": null, + "legacySerializationApplied": true, + "legacyCompressionApplied": true, + "framedV2Applied": false + }, + "logicalCheckpoint": { + "sha256": "a99fd85b123691421981f289c82e49afeb3a6ee5b32b717c20c3a84b9f5de682", + "utf8Bytes": 624765446 + }, + "readback": { + "status": "resource_limit", + "kind": "resource_limit", + "stage": "decompression", + "unit": "bytes", + "limit": 536870888, + "payloadVerified": true + }, + "preservation": { + "payloadBytesUnchanged": true, + "listingUnchanged": true, + "listedBefore": 1, + "listedAfter": 1 + } + } + } + ], + "verdict": { + "status": "passed", + "completeMatrix": true, + "gates": [ + { + "id": "heresy2_base_storage_parity", + "status": "passed", + "checks": { + "baseResult": true, + "checkpointId": true, + "logicalCheckpointBytes": true, + "loadedCheckpoint": true + } + }, + { + "id": "heresy2_exact_resume", + "status": "passed", + "checks": { + "legacyVsUninterrupted": true, + "framedVsUninterrupted": true, + "legacyVsFramed": true + } + }, + { + "id": "intercept_base_storage_parity", + "status": "passed", + "checks": { + "baseResult": true, + "checkpointId": true, + "logicalCheckpointBytes": true + } + }, + { + "id": "intercept_framed_exact_resume", + "status": "passed", + "checks": { + "framedVsUninterrupted": true, + "engineNativeNoGraph": true, + "legacyStagesNotApplied": true + } + }, + { + "id": "intercept_legacy_readback_boundary", + "status": "passed", + "checks": { + "typedResourceLimit": true, + "payloadPreserved": true, + "listingPreserved": true + } + } + ], + "violations": [], + "uncertainties": [], + "allowedClaims": [ + "declared_cell_exact_resume", + "engine_native_zero_materialized_checkpoint_graph", + "typed_legacy_readback_boundary", + "artifact_preservation_after_readback_limit" + ], + "forbiddenClaims": [ + "universal_performance_improvement", + "portable_memory_improvement", + "story_coverage", + "defect_absence", + "allocation_policy_promotion", + "checkpoint_default_format_change", + "inkbench_improvement" + ] + } +} diff --git a/docs/checkpoint-v2-promotion-evaluation.md b/docs/checkpoint-v2-promotion-evaluation.md index d6d8a66..dbcdad5 100644 --- a/docs/checkpoint-v2-promotion-evaluation.md +++ b/docs/checkpoint-v2-promotion-evaluation.md @@ -109,6 +109,33 @@ hard-killed worker, dirty candidate, missing 6 GiB coordinator flag, source or compiler drift, or unexpected legacy-readback success is never converted into a pass. +## Checked result + +The complete matrix finished at `2026-09-06T02:55:15.731Z` on implementation +commit `9312811be8f0e79b8f05ce7e0d09a2626063b8ea` (tree +`966ef5aece7d00974b6c8c9e870d4013762c596b`). The checked +[machine result](../benchmarks/results/checkpoint-v2-promotion-v1.json) has +SHA-256 `0443af81a3aa3312101ccbbf14161e7162d3332a613e14b5d27cbb94ee880088`. +All six cells completed, all five declared gates passed, and the report records +no violations or uncertainties. + +- Heresy II legacy and framed checkpoints shared the same stable ID and + 370,262,984-byte logical checkpoint. Both 100,000-state splits resumed to the + exact uninterrupted 150,000-state result. +- The Intercept framed checkpoint at 600,000 states reported zero materialized + checkpoint graphs, one identity pass, and one frame pass. Its + 624,765,446-byte logical checkpoint reopened and resumed to the exact + uninterrupted 650,000-state result. +- The matched Intercept legacy checkpoint shared the same stable ID and logical + byte count, then returned the expected typed `resource_limit` at the + runtime-reported 536,870,888-byte decompression ceiling. Its payload bytes and + public listing remained unchanged. + +The result permits only the four claims enumerated in its `allowedClaims`. +Single-run elapsed time and peak RSS remain descriptive observations; they are +not comparative evidence and do not support any forbidden default-format, +InkBench, portable-memory, or universal-performance claim. + ## Exactness and resource-limit rules The framed base uses `exploreSharedResumableWithCheckpointSource` and writes diff --git a/docs/local-checkpoints.md b/docs/local-checkpoints.md index 9808a37..f9c8c11 100644 --- a/docs/local-checkpoints.md +++ b/docs/local-checkpoints.md @@ -101,7 +101,7 @@ An individually oversized payload-plus-sidecar pair is rejected. Once a new gene This is the safe foundation for the observed 600,000-state boundary, not a claim that every such checkpoint can now resume. A gzip payload can be within the durable disk quota while its single logical JSON value is larger than V8 can represent. Inkcheck now returns `resource_limit` at that boundary, keeps the known-good bytes intact, and can still list/prune a manifested artifact without inflation. It does not misreport the file as corrupt or retry an unsafe allocation. A repeated same-ID save may recognize such an artifact only after its canonical manifest matches the requested checkpoint summary and its full stored-byte digest verifies; that preserves known bytes but does not claim the logical payload was decoded or resumable. -The opt-in framed artifact v2 foundation supplies independently bounded records/frames, checksums, a terminal index, incremental assembly, mixed-layout compatibility, and stable schema-v1 IDs. It is not yet the default and does not by itself prove that the observed 600,000-state cell now completes or that memory use improved. Default promotion requires exact split-versus-uninterrupted equality, adversarial truncation/oversize/checksum/cancellation/crash/race gates, the same Intercept boundary under identical ceilings, and a second public family such as Heresy II. No allocation, compaction, eviction, epoch, or stopping policy is activated by selecting the storage codec. +The opt-in framed artifact v2 foundation supplies independently bounded records/frames, checksums, a terminal index, incremental assembly, mixed-layout compatibility, and stable schema-v1 IDs. The checked [checkpoint-v2 promotion evaluation](checkpoint-v2-promotion-evaluation.md) now records exact split-versus-uninterrupted equality on Heresy II and on the 600,000→650,000-state Intercept boundary, while the matched legacy Intercept artifact returns its typed decompression limit and remains unchanged. Both engine-native framed writes report zero materialized checkpoint graphs. That single-machine functional evidence does not establish a portable memory or performance improvement and does not make framed v2 the default. No allocation, compaction, eviction, epoch, or stopping policy is activated by selecting the storage codec. ## Privacy diff --git a/docs/shared-checkpoint-artifact-v2.md b/docs/shared-checkpoint-artifact-v2.md index 7be771d..ef8f549 100644 --- a/docs/shared-checkpoint-artifact-v2.md +++ b/docs/shared-checkpoint-artifact-v2.md @@ -164,11 +164,12 @@ not a whole-checkpoint JSON string, but both graphs may overlap until the caller releases its loaded value. This work does not make framed v2 the default, stream provisional read callbacks directly into a live engine, change allocation, compaction, eviction, stopping, or epoch policy, publish a release, -or establish an InkBench improvement claim. Promotion still requires exact -split-versus-uninterrupted resume evidence, the observed Intercept schema-v1 -readback-limit cell under identical ceilings, and a second public story family -such as Heresy II, together with the adversarial truncation, checksum, bounds, -cancellation, crash, and mixed-layout gates. +or establish an InkBench improvement claim. The checked +[checkpoint-v2 promotion evaluation](checkpoint-v2-promotion-evaluation.md) +now supplies exact split-versus-uninterrupted resume evidence for the matched +Intercept readback-limit cell and Heresy II, alongside the codec's adversarial +truncation, checksum, bounds, cancellation, crash, and mixed-layout gates. +That functional result does not itself select or promote a default format. Checkpoint records can contain authored text, variables, serialized runtime state, findings, and witness paths. Treat `.inkcp` files as sensitive project diff --git a/package.json b/package.json index a2a408f..2508c7f 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "benchmarks/results/long-tail-promotion-v2.json", "benchmarks/results/loop-specialist-intercept-100k-v1.json", "benchmarks/results/gate-probe-intercept-5m-v1.json", + "benchmarks/results/checkpoint-v2-promotion-v1.json", "skills/inkcheck", "docs/inkjam-qa-guide.md", "docs/hosted-checker.md", diff --git a/test/checkpoint-v2-evaluation.test.js b/test/checkpoint-v2-evaluation.test.js index 09604c9..2c9a637 100644 --- a/test/checkpoint-v2-evaluation.test.js +++ b/test/checkpoint-v2-evaluation.test.js @@ -390,5 +390,6 @@ test("the proof package entrypoint rebuilds ignored dist and ships its frozen co "npm run --silent build && node --max-old-space-size=6144 dist/checkpoint-v2-evaluation-cli.js" ); assert.ok(packageValue.files.includes("benchmarks/checkpoint-v2-promotion-v1.json")); + assert.ok(packageValue.files.includes("benchmarks/results/checkpoint-v2-promotion-v1.json")); assert.ok(packageValue.files.includes("docs/checkpoint-v2-promotion-evaluation.md")); }); From 861415648f3561fe9c8b363abef7ee2fc9fe37ab Mon Sep 17 00:00:00 2001 From: Daniel Strader Date: Sat, 5 Sep 2026 21:40:58 -0700 Subject: [PATCH 3/3] Keep authored benchmark bytes stable across platforms --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0c238b4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +benchmarks/authored/**/*.ink text eol=lf +benchmarks/authored/**/LICENSE text eol=lf +benchmarks/authored/**/LICENSE*.md text eol=lf