From 9567186252f7ebb60794911baeeabce0daec5140 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 14:17:48 -0700 Subject: [PATCH 01/65] fix(tooling): reconcile decision traceability with owned documentation --- .github/workflows/quality-gates.yml | 9 + docs/development.md | 4 +- .../reports/2026-10-02-traceability-gate.md | 140 ++++++++ package-lock.json | 49 +++ package.json | 1 + scripts/README.md | 94 +++++- scripts/check-decision-traceability.mjs | 235 +++++++++++++ scripts/check-decision-traceability.sh | 271 +-------------- scripts/check-decision-traceability.test.mjs | 312 ++++++++++++++++++ scripts/decision-traceability-map.json | 50 +++ 10 files changed, 884 insertions(+), 281 deletions(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-traceability-gate.md create mode 100644 scripts/check-decision-traceability.mjs create mode 100644 scripts/check-decision-traceability.test.mjs create mode 100644 scripts/decision-traceability-map.json diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml index 13f299463..774f1503d 100644 --- a/.github/workflows/quality-gates.yml +++ b/.github/workflows/quality-gates.yml @@ -20,6 +20,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install Node toolchain uses: actions/setup-node@v4 @@ -30,7 +32,14 @@ jobs: - name: Install npm dependencies run: npm ci --include=optional + - name: Test decision traceability gate + run: node --test scripts/check-decision-traceability.test.mjs + - name: Run no-new-debt lint gate + env: + TRACEABILITY_MODE: range + TRACEABILITY_BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before }} + TRACEABILITY_HEAD_REF: ${{ github.event.pull_request.head.sha || github.sha }} run: npm run lint:no-new typecheck: diff --git a/docs/development.md b/docs/development.md index f42060b0c..a5d46569b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -73,7 +73,9 @@ directories do not require inventories or fixed headings. - Strict workspace Clippy and the current frontend lint/accessibility gates are not green. - Release smoke does not yet prove the packaged artifact. -- Decision traceability still implements a retired README-per-directory model. +- Decision traceability checks declared decision-to-guide impacts and local + references; semantic documentation coverage still requires review. See + [gate operation](../scripts/README.md#decision-traceability-operation). Those limitations are active work, not setup exceptions. See the [current remediation portfolio](plans/current-standards-remediation/plan.md). diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-traceability-gate.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-traceability-gate.md new file mode 100644 index 000000000..5e6db3df6 --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-traceability-gate.md @@ -0,0 +1,140 @@ +# Decision Traceability Gate Reconciliation + +Date: 2026-10-02 + +Plan authority: [domain architecture and multimodal workflows](../plan.md). +Admitted base: `4938e405c7f656365eefdca492774ccae110c90d`. +Standards inspected: `dcc56f26e884ade260770beceba2501d3746200d` of Coding-Standards. +This later inspected revision is explicit; it is not a silent replacement of the +plan's historical planning baseline. + +## Finding And Policy Decision + +The previous checker required a README at every changed source directory, eleven +universal headings, path-list-selected host/producer headings, and a README or +any ADR edit. This contradicted the current Documentation workflow, +[repository documentation rules](../../../README.md), and the active plan's +Documentation Proportionality. Its process-substitution diff collection also +hid failed Git reads as empty successful checks. + +Replace that mechanism with declared decision-to-guide impact checking. The +[project map](../../../../scripts/decision-traceability-map.json) and its +[owning guide](../../../../scripts/README.md#decision-traceability-operation) +select exact durable decision sources, affected knowledge and documentation +profiles. The decision is scoped gate reconciliation, not an exception to +contract documentation or permission to disable verification. + +The admitted rows cover host/service and scheduler-session authority +(ADR-001/011), runtime lifecycle/readiness/observability (ADR-002/003/007), +canonical/composed-node and migration ownership (ADR-006/009), contributor +workspace policy (ADR-017), and the map's own coverage. Each decision was read +against its canonical guide before selecting the row. The gate validates an +update to that owner only; an unrelated README, plan or ADR cannot substitute. +Changed unmapped ADRs produce an unresolved mapping diagnostic. + +Mixed implementation files are intentionally not triggers. A local repair that +preserves a contract needs no guide churn. Source changes that alter consumer +semantics still require canonical documentation, review and contract tests; +path comparison cannot decide those semantics. Existing scheduler-only, +host/binding, worker-protocol and structured-producer validation remains intact. +The removed heading checks never proved those contracts. + +## Change Input And Migration + +- The existing hook explicitly selects staged mode. All evaluated map, guide, + decision and reference content comes from HEAD and the index tree. An unstaged + repair cannot satisfy a staged obligation. +- Range mode requires explicit base and head commit refs, reports resolved IDs, + and reads both snapshots. The affected CI job fetches history and supplies + event base/head IDs; it does not guess a branch or adjacent revision. +- Both prior and current maps contribute obligations. Row removal, trigger + deletion/rename and owner migration cannot erase the previous obligation. +- Initial map adoption is supported only from the exact audited no-map base + above, with the repository-selected map path. Its decision sources and owners + must resolve in that base. There is no override to skip an arbitrary absent + prior map. Retire the migration case when supported bases all carry the map. +- Missing modes, revisions, maps, owners and referenced files fail explicitly; + malformed and unreadable Git input cannot become a successful empty diff. + +Local file links in current mapped owners and changed Markdown are checked. +Unchanged documents are checked when a referenced ADR or prior/current canonical +guide target changes, preserving rename/deletion protection. Remote URLs and heading fragments are not checked. +Untouched historical links remain outside scope. One superseded security plan +already names a never-created proposed trust/isolation ADR; the gate does not +turn that historical proposal into current authority or silently repair it. + +## Write Set And Ownership + +- `scripts/check-decision-traceability.sh`: retained shell entrypoint +- `scripts/check-decision-traceability.mjs`: Git-snapshot gate +- `scripts/check-decision-traceability.test.mjs`: isolated regression cases +- `scripts/decision-traceability-map.json`: bounded project impact coverage +- `package.json` / `package-lock.json`: development-only CommonMark parser and its locked dependency closure +- `scripts/README.md`: current gate operation and coverage owner +- `docs/development.md`: replace the now-retired-gate limitation +- `.github/workflows/quality-gates.yml`: only the affected job's history, + explicit inputs and focused test invocation +- This report: dated deciding evidence + +No runtime implementation, other CI job, standards checkout, active plan, +issues or execution ledger is edited here. The integration coordinator owns +master-plan reconciliation and downstream runtime branch dependencies. + +## Independent Review Repair + +Independent review found false positives for balanced-parenthesis destinations, +tracked directories and fenced examples, mapping-key ordering churn, and a +retained-prior-owner migration gap. The revised candidate uses the established +CommonMark parser instead of owning a regex Markdown grammar, treats tracked +trees as valid link targets, compares normalized maps, and requires a retained +old owner to explain migration. Deleted old owners remain permitted only with a +current replacement and repaired readers. Three regression cases cover these +findings. The parser is a pinned development dependency, BSD-2-Clause licensed; +no production dependency version or existing override changes. + +Revised focused verification passes (24 cases). The integration owner independently +reviewed staged tree `90f286152a59c27bff775e73ceda2b8386dbd575`, including the +complete gate, regression additions, package/lock delta, wrapper and CI inputs, +and reproduced all 24 cases. That narrow review accepted the four repairs +without another finding. This report-only update records the result; unrelated +baseline CI and final remote-head qualification remain unsatisfied. `npm ci --ignore-scripts` installed the locked dependency closure. +CommonMark 0.31.2 and its new entities 3.0.1, mdurl 1.0.1 and minimist 1.2.8 +entries are development-only; all prior version/integrity identities are unchanged. +The installed package retains its copyright/license notices. No third-party +source is vendored or copied into the product. The gate owner maintains the +parser dependency and retires it if the gate no longer consumes Markdown. +Node/npm here are 24.19.0/11.9.0 rather than the pinned 24.12.0/11.6.2; exact +pinned-toolchain qualification remains a hosted-CI obligation. + +## Verification And Limits + +- Node's ordinary test runner: 24 focused cases pass, covering code-only changes, + required guide updates, unrelated documents, index/range isolation, missing and + contradictory inputs, missing maps, malformed maps, broken references, + removed/moved rows and paths, nonregular files, spaces/newlines, and failed or + malformed Git diffs. Test commits have explicitly synthetic identities and + live only in disposable temporary repositories. +- Real staged adoption from the audited base passes using the repository map. +- Repository tooling lint and focused ESLint over both new JavaScript files pass; Bash and Node syntax + checks pass. The checkout uses its isolated locked dependency tree; + no global configuration is changed. The reviewed repair adds the development-only + parser dependency described above. +- `npm run lint:no-new` remains blocked before traceability by the existing + `IoInspectorPage.svelte:520` DOM-mutation finding. This slice does not waive + that gate or claim repository-wide lint success. The a11y check independently + reports the same three existing findings as the clean base (Inspector and + RunGraphSnapshot reviewed-ignore annotations; SavedGraphInspectionSnapshot + keyboard activation). +- Passing this gate establishes selected traceability and file-reference + integrity, not semantic prose completeness, all standards compliance, runtime + correctness or release readiness. Independent narrow review and integration-owner + approval permit draft publication. Merge remains conditional on exact-head + CI and applicable hosted code review; neither is claimed complete here. + +The candidate branch is `fix/decision-traceability-ownership-2026-10-02`, targeting +main. Its isolated checkout is retained for review and downstream dependency +coordination; it is not an accepted integration or cleanup instruction. + +The existing pre-push `npm test` also passed on 2026-10-02: node-engine +258 passed / 1 ignored, workflow-nodes 168 passed. This proves its declared +library scope, not the unrelated failing aggregate/desktop gates. diff --git a/package-lock.json b/package-lock.json index 61ac3ad64..d1f5f8729 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@wdio/mocha-framework": "^9.29.0", "@wdio/spec-reporter": "^9.29.0", "autoprefixer": "^10.4.19", + "commonmark": "0.31.2", "es-module-lexer": "^2.0.0", "eslint": "^9.0.0", "eslint-plugin-svelte": "^3.0.0", @@ -4383,6 +4384,44 @@ "node": ">= 6" } }, + "node_modules/commonmark": { + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/commonmark/-/commonmark-0.31.2.tgz", + "integrity": "sha512-2fRLTyb9r/2835k5cwcAwOj0DEc44FARnMp5veGsJ+mEAZdi52sNopLu07ZyElQUz058H43whzlERDIaaSw4rg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "entities": "~3.0.1", + "mdurl": "~1.0.1", + "minimist": "~1.2.8" + }, + "bin": { + "commonmark": "bin/commonmark" + }, + "engines": { + "node": "*" + } + }, + "node_modules/commonmark/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/commonmark/node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true, + "license": "MIT" + }, "node_modules/compress-commons": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", @@ -7150,6 +7189,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", diff --git a/package.json b/package.json index 49032ab1e..d80bd6c4d 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@wdio/mocha-framework": "^9.29.0", "@wdio/spec-reporter": "^9.29.0", "autoprefixer": "^10.4.19", + "commonmark": "0.31.2", "es-module-lexer": "^2.0.0", "eslint": "^9.0.0", "eslint-plugin-svelte": "^3.0.0", diff --git a/scripts/README.md b/scripts/README.md index 6d67c3bb0..6074ffba6 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -10,7 +10,9 @@ main app entrypoint. | ----------- | ----------- | | `check-runtime-redistributables-smoke.sh` | Verifies a built Pantograph release artifact exists, then runs the bounded release contract smoke that covers managed-runtime view projection, runtime diagnostics projection, current image workflow shape, Pumas resolution, stale graph diagnostics, and image artifact retention. | | `check-current-image-workflow-smoke.mjs` | Validates the bundled current image workflow template and tracked Juggernaut workflow still use canonical `puma-lib -> llm-inference -> image-output` graph shape without retired executable inference nodes. | -| `check-decision-traceability.sh` | Enforces source-directory README/ADR decision traceability for changed source directories, with repo-specific host-facing and structured-producer paths plus a grep fallback when `ripgrep` is unavailable. | +| `check-decision-traceability.sh` | Runs the decision-to-guide traceability gate over explicit Git snapshots using the reviewed impact map. | +| `check-decision-traceability.mjs` | Validates mapped decision impacts, canonical guides, and local ADR references without per-directory documentation rules. | +| `check-decision-traceability.test.mjs` | Exercises Git snapshot isolation, impact ownership, missing inputs, broken references, and path transitions. | | `check-no-python-linkage.sh` | Verifies the runtime-separation guarantee that Pantograph no longer links Python in-process. | | `check-scheduler-only-workflow-execution.sh` | Fails when public Rust, Tauri, binding, or frontend source reintroduces direct workflow execution APIs outside scheduler session execution. | | `check-rustler-beam-smoke.sh` | Builds `pantograph_rustler`, verifies the local BEAM toolchain exists, and runs the Mix smoke harness under `bindings/beam/pantograph_native_smoke/`. | @@ -52,14 +54,12 @@ can be debugged without the full app UI in the loop. ## Invariants - Scripts run relative to the repository root. -- Decision traceability defaults cover `src`, `src-tauri/src`, `crates`, - `packages/svelte-graph/src`, `scripts`, and `.pantograph`; use environment - overrides for temporary focused audits. The default host-facing and - structured-producer lists include UniFFI binding generator helpers and active - inference managed-runtime producers only. -- Decision traceability must preserve equivalent README/ADR checks when `rg` is - unavailable by falling back to standard shell tooling instead of failing on - clean CI runners. +- Decision traceability checks only declared decision-source impacts and local + references. It does not infer semantic contract changes from source-directory + names, require fixed headings, or accept an unrelated ADR as evidence. +- The gate reads the selected Git snapshots, including the map and guides. It + fails on unavailable or unreadable inputs rather than guessing a branch or + treating a failed diff as an empty change. - Smoke tests target real Pantograph worker/runtime modules, not forks of that logic. - Release contract smoke is headless: it validates the built artifact and @@ -79,7 +79,10 @@ can be debugged without the full app UI in the loop. and repo-local build configuration. **External:** Bash, Node.js, Python, and any runtime libraries required by the -specific script being executed. +specific script being executed. Decision traceability additionally uses the +locked development-only `commonmark` package (BSD-2-Clause); run `npm ci` before +invoking it. The package owns Markdown parsing, avoiding a parallel regex +grammar; it does not render or execute document content. ## Related ADRs - `docs/adr/ADR-011-scheduler-only-workflow-execution.md` @@ -93,11 +96,12 @@ specific script being executed. python3 -m py_compile scripts/diffusion_cli_smoketest.py ./.venv/bin/python scripts/diffusion_cli_smoketest.py --model-path /path/to/tiny-sd-turbo node scripts/check-current-image-workflow-smoke.mjs -npm run lint:no-new +TRACEABILITY_STAGED_ONLY=1 npm run lint:no-new npm run lint:a11y npm run format:check npm run release:sbom -- 0.1.0 -./scripts/check-decision-traceability.sh +TRACEABILITY_STAGED_ONLY=1 ./scripts/check-decision-traceability.sh +node --test scripts/check-decision-traceability.test.mjs ./scripts/check-no-python-linkage.sh ./scripts/check-scheduler-only-workflow-execution.sh ./scripts/check-rustler-beam-smoke.sh @@ -135,3 +139,69 @@ Reason: script stdout/stderr is diagnostic and may change unless a future script is explicitly documented as machine-consumed. Revisit trigger: CI, external tooling, or another repo begins parsing a script's output structurally. + +## Decision Traceability Operation + +The [impact map](decision-traceability-map.json) is owned by this guide. It maps +exact accepted decision documents to current documentation owners: + +- ADR-001/011: host/service ownership and scheduler-session consumer lifecycle, + owned by [headless integration](../docs/headless-workflow.md) +- ADR-002/003/007: runtime lifecycle, readiness, observability and recovery, + owned by [runtime operations](../docs/runtime-operations.md) +- ADR-006/009: canonical/composed-node and migration responsibility, + owned by [architecture](../ARCHITECTURE.md) +- ADR-017: contributor workspace policy and verification procedure, + owned by [development](../docs/development.md) +- The map itself: the gate's declared coverage, owned by this guide + +Changing a mapped decision or its mapping requires updating that boundary's +canonical guide. New or changed unmapped ADRs fail as unresolved coverage until +an owner and the affected knowledge are declared. Removing a row or moving a +trigger still evaluates its prior obligation. A moved guide must be replaced in +the current map and affected links; if the old guide remains, update it to +explain the migration. A retired row still requires its prior guide +to explain the change. The map admits exact decision-document paths only, not +whole source trees or mixed implementation files. + +Staged mode (`TRACEABILITY_STAGED_ONLY=1`) compares HEAD with the index tree and +reads all content from those snapshots. Unstaged documents cannot repair the +index. Range mode requires both revisions explicitly: + +```bash +TRACEABILITY_MODE=range TRACEABILITY_BASE_REF= \ + TRACEABILITY_HEAD_REF= npm run traceability +``` + +The gate reports the resolved input IDs. CI supplies event base/head commits +and fetches their history. Missing modes, refs, maps, owners or local targets +fail; unreadable Git state also fails. The old source-root/host/producer path +list overrides are rejected. The shell entrypoint explicitly selects this +repository's map; the Node implementation accepts `--map` for isolated tests or +an explicitly reviewed alternative. + +Both prior and current maps are read. The sole migration case is the audited +pre-map commit `4938e405c7f656365eefdca492774ccae110c90d` with this exact map path. +At that base, current decision rows must also resolve in the prior snapshot; +there is no general missing-map opt-out. Remove that transition once supported +comparison bases all contain the map. + +Local links in mapped guides and changed Markdown are checked, including ADR +paths in code spans. Unchanged readers are checked when an ADR or prior/current +canonical guide target changes, so moving/deleting owned documentation cannot +leave stale references. Untouched historical links are outside this change-scoped +gate. The development-only CommonMark parser owns Markdown syntax, including balanced +link destinations, used reference links and images. Fenced/indented examples +are not live links; inline code-span ADR paths remain the repository convention. +Tracked directory targets are valid. Object-key, boundary-row and trigger +ordering do not change mapped knowledge or create guide-edit obligations. +URL fragments and remote URLs are outside this file-existence check. A guide +edit proves traceability, not that its prose is substantively correct. + +This gate does not prove semantic contract completeness. A code change that +alters consumer behavior, responsibility, invariants or procedures still needs +its canonical documentation under [documentation rules](../docs/README.md), +with review and applicable contract tests. Routine repairs under unchanged +contracts need no documentation churn. Existing host/binding, scheduler-only, +worker-protocol and structured-producer checks remain required; the removed +universal README headings never proved those contracts. diff --git a/scripts/check-decision-traceability.mjs b/scripts/check-decision-traceability.mjs new file mode 100644 index 000000000..50200b029 --- /dev/null +++ b/scripts/check-decision-traceability.mjs @@ -0,0 +1,235 @@ +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { Parser } from 'commonmark'; + +const markdown = new Parser(); + +// Audited transition from the retired directory-template gate. This is not a +// caller override: every other prior revision must contain the selected map. +const LEGACY_BASE = '4938e405c7f656365eefdca492774ccae110c90d'; +const PROJECT_MAP = 'scripts/decision-traceability-map.json'; +const ADR_PATH = /^docs\/adr\/ADR-\d+[^/]*\.md$/; +const PROFILES = new Set(['boundary-readme', 'contract-readme', 'adr', 'runbook']); + +function fail(kind, message) { + throw new Error(`${kind}: ${message}`); +} + +function git(...args) { + const result = spawnSync('git', args, { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 }); + if (result.error || result.status !== 0) { + fail('unsupported', `cannot read Git input (${args[0]}): ${result.error?.message || result.stderr.trim() || result.signal || result.status}`); + } + return result.stdout; +} + +function repositoryPath(value, label) { + if (typeof value !== 'string' || !value || value.includes('\\') || value.includes('\0') || + path.posix.isAbsolute(value) || path.posix.normalize(value) !== value || + value === '..' || value.startsWith('../')) { + fail('invalid', `${label} must be an exact repository-relative file path`); + } + return value; +} + +function revision(ref, label) { + if (!ref) fail('unavailable', `${label} is required in range mode`); + return git('rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`).trim(); +} + +function snapshot(rev) { + const files = new Map(); + const output = git('ls-tree', '-rtz', '--full-tree', rev); + if (output && !output.endsWith('\0')) fail('invalid', 'malformed Git tree input'); + for (const record of output.split('\0').filter(Boolean)) { + const match = /^(\d{6}) (\w+) ([0-9a-f]+)\t([\s\S]+)$/.exec(record); + if (!match) fail('invalid', 'malformed Git tree entry'); + files.set(match[4], { mode: match[1], type: match[2], oid: match[3] }); + } + files.set('.', { mode: '040000', type: 'tree', oid: rev }); + const cache = new Map(); + return { + rev, + files, + read(file) { + const entry = files.get(file); + if (!entry) fail('unavailable', `${file} is missing at ${rev}`); + if (entry.type !== 'blob' || !['100644', '100755'].includes(entry.mode)) { + fail('unsupported', `${file} must be a regular tracked file at ${rev}`); + } + if (!cache.has(file)) cache.set(file, git('cat-file', 'blob', entry.oid)); + return cache.get(file); + }, + }; +} + +function changedPaths(base, head) { + // No rename inference: both the removed and added paths remain obligations. + const output = git('diff', '--no-ext-diff', '--no-textconv', '--name-status', '-z', '--no-renames', base, head, '--'); + if (output && !output.endsWith('\0')) fail('invalid', 'malformed Git diff: missing NUL terminator'); + const fields = output ? output.slice(0, -1).split('\0') : []; + if (fields.length % 2) fail('invalid', 'malformed Git diff: incomplete status/path pair'); + const changed = new Set(); + for (let i = 0; i < fields.length; i += 2) { + if (!/^[AMDT]$/.test(fields[i]) || !fields[i + 1]) { + fail('invalid', `malformed Git diff: unsupported status/path pair ${JSON.stringify(fields[i])}`); + } + if (changed.has(fields[i + 1])) fail('invalid', 'malformed Git diff: duplicate path'); + changed.add(fields[i + 1]); + } + return changed; +} + +function readMap(state, mapPath) { + let value; + try { value = JSON.parse(state.read(mapPath)); } + catch (error) { + if (/^(unavailable|unsupported):/.test(error.message)) throw error; + fail('invalid', `cannot parse ${mapPath} at ${state.rev}: ${error.message}`); + } + if (value?.version !== 1 || !Array.isArray(value.boundaries) || !value.boundaries.length || + Object.keys(value).some(key => !['version', 'boundaries'].includes(key))) { + fail('invalid', `${mapPath} must declare version 1 and a nonempty boundaries array`); + } + const rows = new Map(); + const owners = new Map(); + for (const row of value.boundaries) { + if (!row || typeof row.id !== 'string' || !row.id.trim() || rows.has(row.id) || + !PROFILES.has(row.profile) || typeof row.knowledge !== 'string' || !row.knowledge.trim() || + !Array.isArray(row.triggers) || !row.triggers.length || + Object.keys(row).some(key => !['id', 'profile', 'knowledge', 'artifact', 'triggers'].includes(key))) { + fail('invalid', `${mapPath} contains an invalid or duplicate boundary`); + } + repositoryPath(row.artifact, `artifact for ${row.id}`); + if (!row.artifact.endsWith('.md')) fail('invalid', `artifact for ${row.id} must be Markdown`); + if (!state.read(row.artifact).trim()) fail('invalid', `empty canonical artifact ${row.artifact}`); + for (const trigger of row.triggers) { + repositoryPath(trigger, `trigger for ${row.id}`); + if (trigger === row.artifact || owners.has(trigger)) fail('invalid', `contradictory trigger ownership: ${trigger}`); + // Decision documents and the map are the entire admitted trigger surface. + // Mixed implementation files would require semantic review, not path rules. + if (!ADR_PATH.test(trigger) && trigger !== mapPath) fail('unsupported', `undeclared decision-source trigger: ${trigger}`); + state.read(trigger); + owners.set(trigger, row.id); + } + rows.set(row.id, { id: row.id, profile: row.profile, knowledge: row.knowledge, artifact: row.artifact, triggers: [...row.triggers].sort() }); + } + if (owners.get(mapPath) === undefined) fail('invalid', `${mapPath} must map its own coverage changes to an owning guide`); + return rows; +} + +function localTarget(owner, reference, rootRelative = false) { + if (/^[a-z][a-z\d+.-]*:/i.test(reference) || reference.startsWith('//')) return null; + const file = reference.split(/[?#]/, 1)[0]; + if (!file) return null; + let decoded; + try { decoded = decodeURIComponent(file); } + catch { fail('invalid', `malformed local reference in ${owner}: ${reference}`); } + const target = decoded.startsWith('/') ? decoded.slice(1) : + rootRelative && decoded.startsWith('docs/') ? decoded : path.posix.join(path.posix.dirname(owner), decoded); + return repositoryPath(path.posix.normalize(target).replace(/\/$/, ''), `local reference in ${owner}`); +} + +function documentReferences(state, file, allLocalLinks, changed, mappedArtifacts) { + const text = state.read(file); + const refs = []; + // Use CommonMark's syntax owner: examples in fenced/indented blocks are not + // live links, and balanced/escaped destinations and references stay intact. + const walker = markdown.parse(text).walker(); + let event; + while ((event = walker.next())) { + if (!event.entering) continue; + const node = event.node; + if (node.type === 'link' || node.type === 'image') refs.push([node.destination, false]); + if (node.type === 'code' && /ADR-\d+[^\n]*\.md(?:#[^\n]*)?$/.test(node.literal)) { + refs.push([node.literal, true]); + } + } + for (const [reference, rootRelative] of refs) { + const target = localTarget(file, reference, rootRelative); + if (!target || (!allLocalLinks && (!changed.has(target) || (!ADR_PATH.test(target) && !mappedArtifacts.has(target))))) continue; + if (!state.files.has(target)) fail('unavailable', `broken local reference in ${file}: ${reference} -> ${target}`); + if (ADR_PATH.test(target) && !state.read(target).trim()) fail('invalid', `empty ADR target in ${file}: ${target}`); + } +} + +function main() { + if (process.argv.length !== 4 || process.argv[2] !== '--map') fail('unavailable', 'invoke with --map '); + const mapPath = repositoryPath(process.argv[3], 'map'); + const env = process.env; + for (const retired of ['TRACEABILITY_SOURCE_ROOTS', 'TRACEABILITY_HOST_FACING_DIRS', 'TRACEABILITY_STRUCTURED_PRODUCER_DIRS', 'ADR_DIR']) { + if (env[retired] !== undefined) fail('invalid', `${retired} is retired; use the reviewed decision-source map`); + } + if (env.TRACEABILITY_STAGED_ONLY !== undefined && env.TRACEABILITY_STAGED_ONLY !== '1') { + fail('invalid', 'TRACEABILITY_STAGED_ONLY must be 1 when supplied'); + } + const mode = env.TRACEABILITY_STAGED_ONLY === '1' ? 'staged' : env.TRACEABILITY_MODE; + if (!['staged', 'range'].includes(mode)) fail('unavailable', 'select TRACEABILITY_STAGED_ONLY=1 or TRACEABILITY_MODE=range with explicit base/head refs'); + if (env.TRACEABILITY_STAGED_ONLY === '1' && env.TRACEABILITY_MODE && env.TRACEABILITY_MODE !== 'staged') { + fail('invalid', 'contradictory traceability modes'); + } + if (mode === 'staged' && (env.TRACEABILITY_BASE_REF || env.TRACEABILITY_HEAD_REF)) fail('invalid', 'staged mode cannot use range refs'); + const root = git('rev-parse', '--show-toplevel').trim(); + process.chdir(root); + const baseRev = mode === 'staged' ? revision('HEAD', 'HEAD') : revision(env.TRACEABILITY_BASE_REF, 'TRACEABILITY_BASE_REF'); + const headRev = mode === 'staged' ? git('write-tree').trim() : revision(env.TRACEABILITY_HEAD_REF, 'TRACEABILITY_HEAD_REF'); + console.log(`Decision traceability input: ${mode} ${baseRev} -> ${headRev}; map ${mapPath}`); + const base = snapshot(baseRev); + const head = snapshot(headRev); + const changed = changedPaths(baseRev, headRev); + const current = readMap(head, mapPath); + let prior; + if (!base.files.has(mapPath) && baseRev === LEGACY_BASE && mapPath === PROJECT_MAP) { + // The audited legacy tree had no impact map. Validate current decisions at + // the prior snapshot too; only the map's own addition is new at adoption. + prior = new Map(); + for (const [id, row] of current) { + if (row.triggers.includes(mapPath)) continue; + base.read(row.artifact); + for (const trigger of row.triggers) base.read(trigger); + prior.set(id, row); + } + console.log(`Explicit legacy-map adoption from ${LEGACY_BASE}`); + } else { + prior = readMap(base, mapPath); + } + const canonicalMap = rows => JSON.stringify([...rows].sort(([a], [b]) => a.localeCompare(b))); + const mapChanged = canonicalMap(prior) !== canonicalMap(current); + const affected = new Set(); + for (const [id, row] of [...prior, ...current]) { + if (row.triggers.some(trigger => trigger === mapPath ? mapChanged : changed.has(trigger)) || + JSON.stringify(prior.get(id)) !== JSON.stringify(current.get(id))) affected.add(id); + } + for (const file of changed) { + if (ADR_PATH.test(file) && ![...prior.values(), ...current.values()].some(row => row.triggers.includes(file))) { + fail('unavailable', `changed ADR has no declared impact mapping: ${file}`); + } + } + for (const id of affected) { + const old = prior.get(id); + const next = current.get(id); + for (const row of [old, next].filter(Boolean)) { + // A moved owner may be removed only when its replacement exists in the + // current map. A retained old owner must explain retirement/migration. + const movedAndRemoved = row === old && next && old.artifact !== next.artifact && !head.files.has(old.artifact); + if (!movedAndRemoved) head.read(row.artifact); + if (!changed.has(row.artifact)) fail('unavailable', `boundary ${id} requires an update to ${row.artifact} (${row.knowledge}); unrelated documents cannot satisfy it`); + } + } + const owners = new Set([...current.values()].map(row => row.artifact)); + const mappedArtifacts = new Set([...prior.values(), ...current.values()].map(row => row.artifact)); + for (const [file, entry] of head.files) { + if (!file.endsWith('.md') || entry.type !== 'blob') continue; + // Preserve readers of changed decisions and prior/current canonical guides. + // Other references are checked in owners and changed documents; untouched + // historical targets are not promoted into current documentation authority. + documentReferences(head, file, owners.has(file) || changed.has(file), changed, mappedArtifacts); + } + console.log(`Decision traceability passed: ${changed.size} changed path(s), ${affected.size} mapped impact(s). Semantic contract review remains required.`); +} + +try { main(); } +catch (error) { + console.error(`Decision traceability failed: ${error.message}`); + process.exitCode = 1; +} diff --git a/scripts/check-decision-traceability.sh b/scripts/check-decision-traceability.sh index 1da750570..3a6effa51 100755 --- a/scripts/check-decision-traceability.sh +++ b/scripts/check-decision-traceability.sh @@ -1,271 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -SOURCE_ROOTS="${TRACEABILITY_SOURCE_ROOTS:-src,src-tauri/src,crates,packages/svelte-graph/src,scripts,.pantograph}" -ADR_DIR="${ADR_DIR:-docs/adr}" -BASE_REF="${TRACEABILITY_BASE_REF:-origin/main}" -STAGED_ONLY="${TRACEABILITY_STAGED_ONLY:-0}" -HOST_FACING_DIRS="${TRACEABILITY_HOST_FACING_DIRS:-src,src-tauri/src/workflow,src-tauri/src/llm/commands,src-tauri/src/llm/commands/registry,crates/pantograph-workflow-service,crates/pantograph-workflow-service/src,crates/pantograph-workflow-service/src/workflow,crates/pantograph-workflow-service/tests,crates/pantograph-workflow-service/examples,crates/pantograph-embedded-runtime,crates/pantograph-uniffi,crates/pantograph-uniffi/src,crates/pantograph-uniffi/src/bin,crates/pantograph-rustler,crates/pantograph-rustler/src,crates/pantograph-frontend-http-adapter,crates/inference/torch,crates/inference/depth,crates/inference/audio}" -STRUCTURED_PRODUCER_DIRS="${TRACEABILITY_STRUCTURED_PRODUCER_DIRS:-src,src/generated,src-tauri/src/workflow,crates/pantograph-workflow-service,crates/pantograph-workflow-service/src,crates/pantograph-workflow-service/src/workflow,crates/pantograph-workflow-service/tests,crates/pantograph-embedded-runtime,crates/pantograph-uniffi,crates/pantograph-uniffi/src,crates/pantograph-uniffi/src/bin,crates/pantograph-rustler,crates/pantograph-rustler/src,crates/inference/torch,crates/inference/depth,crates/inference/audio,crates/inference/src/managed_runtime,crates/inference/src/managed_runtime/llama_cpp_platform,.pantograph,.pantograph/workflows,.pantograph/orchestrations}" - -required_headers=( - "## Purpose" - "## Contents" - "## Problem" - "## Constraints" - "## Decision" - "## Alternatives Rejected" - "## Invariants" - "## Revisit Triggers" - "## Dependencies" - "## Related ADRs" - "## Usage Examples" -) - -host_facing_headers=("## API Consumer Contract") -structured_producer_headers=("## Structured Producer Contract") - -banned_placeholders=( - "Source file used by modules in this directory." - "Subdirectory containing related implementation details." - "Keep files in this directory scoped to a single responsibility boundary." - "import { value } from './module';" -) - -have_rg=false -if command -v rg >/dev/null 2>&1; then - have_rg=true -fi - -contains_fixed_exact_line() { - local pattern="$1" - local file="$2" - - if [ "$have_rg" = true ]; then - rg -F -x -q "$pattern" "$file" - else - grep -F -x -q -- "$pattern" "$file" - fi -} - -contains_fixed_text() { - local pattern="$1" - local file="$2" - - if [ "$have_rg" = true ]; then - rg -F -q "$pattern" "$file" - else - grep -F -q -- "$pattern" "$file" - fi -} - -stdin_matches_regex() { - local pattern="$1" - - if [ "$have_rg" = true ]; then - rg -q "$pattern" - else - grep -E -q -- "$pattern" - fi -} - -stdin_matches_regex_i() { - local pattern="$1" - - if [ "$have_rg" = true ]; then - rg -i -q "$pattern" - else - grep -E -i -q -- "$pattern" - fi -} - -trim_dir() { - local dir_path="$1" - dir_path="${dir_path#./}" - dir_path="${dir_path%/}" - printf '%s\n' "$dir_path" -} - -csv_contains_dir() { - local needle - local haystack="$2" - local item - needle="$(trim_dir "$1")" - - [ -n "$haystack" ] || return 1 - - IFS=',' read -ra items <<< "$haystack" - for item in "${items[@]}"; do - if [ "$(trim_dir "$item")" = "$needle" ]; then - return 0 - fi - done - - return 1 -} - -extract_section_body() { - local header="$1" - local file="$2" - - awk -v header="$header" ' - $0 == header { in_section = 1; next } - in_section && /^## / { exit } - in_section { print } - ' "$file" -} - -file_in_source_root() { - local file="$1" - local root - - IFS=',' read -ra roots <<< "$SOURCE_ROOTS" - for root in "${roots[@]}"; do - root="$(trim_dir "$root")" - if [ "$file" = "$root" ] || [[ "$file" == "$root/"* ]]; then - return 0 - fi - done - - return 1 -} - -resolve_diff_range() { - if git rev-parse --verify "$BASE_REF" >/dev/null 2>&1; then - printf '%s...HEAD\n' "$BASE_REF" - elif git rev-parse --verify "origin/master" >/dev/null 2>&1; then - printf 'origin/master...HEAD\n' - elif git rev-parse --verify "main" >/dev/null 2>&1; then - printf 'main...HEAD\n' - elif git rev-parse --verify "master" >/dev/null 2>&1; then - printf 'master...HEAD\n' - elif git rev-parse --verify "HEAD~1" >/dev/null 2>&1; then - printf 'HEAD~1...HEAD\n' - else - return 1 - fi -} - -changed_files_for_mode() { - local diff_range - - if [ "$STAGED_ONLY" = "1" ]; then - git diff --cached --name-only --diff-filter=ACMR - return 0 - fi - - diff_range="$(resolve_diff_range)" || return 1 - git diff --name-only --diff-filter=ACMR "$diff_range" -} - -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "Not inside a git repository." - exit 1 -fi - -if ! mapfile -t changed_files < <(changed_files_for_mode); then - echo "Skipping decision traceability check: unable to resolve changed files." - exit 0 -fi - -if [ "${#changed_files[@]}" -eq 0 ]; then - echo "No changed files detected for decision traceability check." - exit 0 -fi - -declare -A changed_lookup=() -for file in "${changed_files[@]}"; do - changed_lookup["$file"]=1 -done - -adr_changed=false -for file in "${changed_files[@]}"; do - if [[ "$file" == "$ADR_DIR/"*.md ]]; then - adr_changed=true - break - fi -done - -declare -A changed_dirs=() -for file in "${changed_files[@]}"; do - if file_in_source_root "$file"; then - changed_dirs["$(dirname "$file")"]=1 - fi -done - -if [ "${#changed_dirs[@]}" -eq 0 ]; then - echo "No source-directory changes detected for decision traceability check." - exit 0 -fi - -failures=0 -for module_dir in "${!changed_dirs[@]}"; do - readme_path="$module_dir/README.md" - required_headers_for_dir=("${required_headers[@]}") - - if csv_contains_dir "$module_dir" "$HOST_FACING_DIRS"; then - required_headers_for_dir+=("${host_facing_headers[@]}") - fi - if csv_contains_dir "$module_dir" "$STRUCTURED_PRODUCER_DIRS"; then - required_headers_for_dir+=("${structured_producer_headers[@]}") - fi - - if [ ! -f "$readme_path" ]; then - echo "Missing README.md for changed directory: $module_dir" - failures=$((failures + 1)) - continue - fi - - missing_header=false - for header in "${required_headers_for_dir[@]}"; do - if ! contains_fixed_exact_line "$header" "$readme_path"; then - echo "Missing required heading in $readme_path: $header" - missing_header=true - fi - done - if [ "$missing_header" = true ]; then - failures=$((failures + 1)) - fi - - none_format_invalid=false - for header in "${required_headers_for_dir[@]}"; do - section_body="$(extract_section_body "$header" "$readme_path")" - if printf '%s\n' "$section_body" | stdin_matches_regex_i '(^|[^[:alnum:]_])none([^[:alnum:]_]|$)'; then - if ! printf '%s\n' "$section_body" | stdin_matches_regex_i 'reason:'; then - echo "Section with None is missing Reason in $readme_path: $header" - none_format_invalid=true - fi - if ! printf '%s\n' "$section_body" | stdin_matches_regex_i 'revisit trigger:'; then - echo "Section with None is missing Revisit trigger in $readme_path: $header" - none_format_invalid=true - fi - fi - done - if [ "$none_format_invalid" = true ]; then - failures=$((failures + 1)) - fi - - placeholder_found=false - for phrase in "${banned_placeholders[@]}"; do - if contains_fixed_text "$phrase" "$readme_path"; then - echo "Banned placeholder phrase in $readme_path: $phrase" - placeholder_found=true - fi - done - if [ "$placeholder_found" = true ]; then - failures=$((failures + 1)) - fi - - if [ -z "${changed_lookup["$readme_path"]+set}" ] && [ "$adr_changed" = false ]; then - echo "Changed directory without decision traceability update: $module_dir" - echo "Update $readme_path or add/update an ADR under $ADR_DIR/." - failures=$((failures + 1)) - fi -done - -if [ "$failures" -gt 0 ]; then - echo "Decision traceability check failed ($failures issue(s))." - exit 1 -fi - -echo "Decision traceability check passed." +# The repository selects its map here; the caller must select the change input. +exec node "$(dirname "${BASH_SOURCE[0]}")/check-decision-traceability.mjs" \ + --map scripts/decision-traceability-map.json diff --git a/scripts/check-decision-traceability.test.mjs b/scripts/check-decision-traceability.test.mjs new file mode 100644 index 000000000..7cbbb62b1 --- /dev/null +++ b/scripts/check-decision-traceability.test.mjs @@ -0,0 +1,312 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; + +const checker = fileURLToPath(new URL('./check-decision-traceability.sh', import.meta.url)); +const mapPath = 'scripts/decision-traceability-map.json'; +const decision = 'docs/adr/ADR-001-example.md'; +const guide = 'docs/consumer.md'; +const realGit = spawnSync('which', ['git'], { encoding: 'utf8' }).stdout.trim(); + +function fixture(t, options = {}) { + const root = mkdtempSync(path.join(tmpdir(), 'pantograph-traceability-')); + t.after(() => rmSync(root, { recursive: true, force: true })); + function git(...args) { + const result = spawnSync(realGit, args, { + cwd: root, encoding: 'utf8', + // Synthetic identities belong only to disposable test commits. + env: { ...process.env, GIT_AUTHOR_NAME: 'Test Fixture', GIT_AUTHOR_EMAIL: 'fixture@example.invalid', GIT_COMMITTER_NAME: 'Test Fixture', GIT_COMMITTER_EMAIL: 'fixture@example.invalid' }, + }); + assert.equal(result.status, 0, result.stderr); + return result.stdout.trim(); + } + function write(file, text) { + mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + writeFileSync(path.join(root, file), text); + } + function map(rows = []) { + return { + version: 1, + boundaries: [ + { id: 'consumer', triggers: [decision], artifact: guide, profile: 'contract-readme', knowledge: 'Consumer lifecycle' }, + { id: 'gate', triggers: [mapPath], artifact: 'scripts/README.md', profile: 'boundary-readme', knowledge: 'Coverage of the gate' }, + ...rows, + ], + }; + } + function writeMap(value = map()) { write(mapPath, `${JSON.stringify(value, null, 2)}\n`); } + function commit() { + git('add', '-A'); + const tree = git('write-tree'); + const previous = spawnSync(realGit, ['rev-parse', '--verify', 'HEAD'], { cwd: root, encoding: 'utf8' }); + const sha = git('commit-tree', tree, ...(previous.status === 0 ? ['-p', previous.stdout.trim()] : []), '-m', 'test: fixture snapshot'); + git('update-ref', 'HEAD', sha); + return sha; + } + function run(env = {}) { + const cleanEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('TRACEABILITY_') && key !== 'ADR_DIR')); + const result = spawnSync('bash', [checker], { cwd: root, encoding: 'utf8', env: { ...cleanEnv, TRACEABILITY_STAGED_ONLY: '1', ...env } }); + return { status: result.status, output: result.stdout + result.stderr }; + } + git('init', '-q'); + write('src/internal.rs', 'fn internal() {}\n'); + write(decision, '# Decision\n\nThe service owns the consumer lifecycle.\n'); + write(guide, '# Consumer\n\n[Decision](adr/ADR-001-example.md)\n'); + write('scripts/README.md', '# Gate\n\nCurrent impact coverage.\n'); + if (!options.withoutMap) writeMap(); + const base = commit(); + return { root, git, write, map, writeMap, commit, run, base }; +} + +function passed(result) { assert.equal(result.status, 0, result.output); assert.match(result.output, /Decision traceability passed:/); } +function failed(result, diagnostic) { assert.equal(result.status, 1, result.output); assert.match(result.output, diagnostic); assert.doesNotMatch(result.output, /Decision traceability passed:/); } + +test('unchanged contracts allow code-only changes, nested paths, spaces and newlines without README churn', t => { + const f = fixture(t); + f.write('src/new owner/nested\nmodule.rs', 'fn implementation() {}\n'); + f.git('add', '-A'); + passed(f.run()); +}); + +test('explicit valid input with no changes passes only after validating the map', t => { + const f = fixture(t); + passed(f.run()); + f.writeMap({ version: 1, boundaries: [] }); + f.git('add', mapPath); + failed(f.run(), /invalid: .*nonempty boundaries/); +}); + +test('a changed decision requires its canonical guide; an unrelated README or ADR does not count', t => { + const f = fixture(t); + f.write(decision, '# Decision\n\nConsumer shutdown must be awaited.\n'); + f.write('README.md', '# Unrelated documentation\n'); + f.git('add', '-A'); + failed(f.run(), /unavailable: boundary consumer requires an update to docs\/consumer.md/); + f.write(guide, '# Consumer\n\nAwait shutdown. [Decision](adr/ADR-001-example.md)\n'); + f.git('add', guide); + passed(f.run()); +}); + +test('staged mode reads guide and map from index, never unstaged repairs', t => { + const f = fixture(t); + f.write(decision, '# Changed decision\n'); + f.git('add', decision); + f.write(guide, '# Unstaged guide repair\n'); + failed(f.run(), /boundary consumer requires an update/); + f.git('add', guide); + f.write(guide, '[Unstaged broken link](missing.md)\n'); + f.writeMap({ invalid: true }); + passed(f.run()); +}); + +test('explicit range reads exact commits even when worktree and index differ', t => { + const f = fixture(t); + f.write('src/internal.rs', 'fn repaired() {}\n'); + const head = f.commit(); + f.writeMap({ invalid: true }); + f.git('add', mapPath); + passed(f.run({ TRACEABILITY_STAGED_ONLY: undefined, TRACEABILITY_MODE: 'range', TRACEABILITY_BASE_REF: f.base, TRACEABILITY_HEAD_REF: head })); +}); + +test('missing modes, revisions, invalid refs and contradictory modes fail clearly', t => { + const f = fixture(t); + failed(f.run({ TRACEABILITY_STAGED_ONLY: undefined }), /unavailable: select TRACEABILITY/); + failed(f.run({ TRACEABILITY_STAGED_ONLY: undefined, TRACEABILITY_MODE: 'range', TRACEABILITY_BASE_REF: f.base }), /unavailable: TRACEABILITY_HEAD_REF/); + failed(f.run({ TRACEABILITY_STAGED_ONLY: undefined, TRACEABILITY_MODE: 'range', TRACEABILITY_BASE_REF: 'missing-base', TRACEABILITY_HEAD_REF: 'HEAD' }), /unsupported: cannot read Git input \(rev-parse\)/); + failed(f.run({ TRACEABILITY_MODE: 'range' }), /invalid: contradictory traceability modes/); + failed(f.run({ TRACEABILITY_BASE_REF: f.base }), /invalid: staged mode cannot use range refs/); + failed(f.run({ TRACEABILITY_SOURCE_ROOTS: 'unrelated' }), /invalid: TRACEABILITY_SOURCE_ROOTS is retired/); +}); + +test('missing current or arbitrary prior map never silently succeeds', t => { + const f = fixture(t, { withoutMap: true }); + failed(f.run(), /unavailable: scripts\/decision-traceability-map.json is missing/); + f.writeMap(); + f.write('scripts/README.md', '# Gate adoption\n'); + f.git('add', '-A'); + failed(f.run(), new RegExp(`unavailable: scripts/decision-traceability-map.json is missing at ${f.base}`)); +}); + +test('missing canonical artifact and broken local ADR or ordinary local links fail', t => { + const f = fixture(t); + f.git('rm', guide); + failed(f.run(), /unavailable: docs\/consumer.md is missing/); + f.write(guide, '# Consumer\n\n[Decision](adr/ADR-099-missing.md)\n'); + f.git('add', guide); + failed(f.run(), /broken local reference in docs\/consumer.md: adr\/ADR-099-missing.md/); + f.write(guide, '# Consumer\n\n[Instructions](missing.md)\n'); + f.git('add', guide); + failed(f.run(), /broken local reference in docs\/consumer.md: missing.md/); +}); + +test('root-relative code-span ADR references are checked; external URLs are not fetched', t => { + const f = fixture(t); + f.write('docs/review.md', '# Review\n\n`docs/adr/ADR-099-missing.md`\n'); + f.git('add', '-A'); + failed(f.run(), /broken local reference in docs\/review.md: docs\/adr\/ADR-099-missing.md/); + f.write('docs/review.md', '# Review\n\n[External](https://example.invalid/ADR-099-remote.md)\n'); + f.git('add', '-A'); + passed(f.run()); +}); + +test('an unmapped added or changed ADR is unresolved, even with unrelated guide edits', t => { + const f = fixture(t); + f.write('docs/adr/ADR-002-new.md', '# New durable decision\n'); + f.write(guide, '# Updated consumer\n'); + f.git('add', '-A'); + failed(f.run(), /unavailable: changed ADR has no declared impact mapping: docs\/adr\/ADR-002-new.md/); +}); + +test('deleting a map row cannot hide the prior obligation', t => { + const f = fixture(t); + const next = f.map(); + next.boundaries = next.boundaries.filter(row => row.id !== 'consumer'); + f.writeMap(next); + f.write('scripts/README.md', '# Removed consumer map row\n'); + f.git('add', '-A'); + failed(f.run(), /boundary consumer requires an update to docs\/consumer.md/); +}); + +test('renaming a mapped ADR requires its guide and cannot leave an unchanged reader dangling', t => { + const f = fixture(t); + f.write('docs/reader.md', '# Reader\n\n[Decision](adr/ADR-001-example.md)\n'); + f.commit(); + const moved = 'docs/adr/ADR-003-renamed.md'; + f.git('mv', decision, moved); + const next = f.map(); + next.boundaries[0].triggers = [moved]; + f.writeMap(next); + f.write('scripts/README.md', '# Gate\n\nDecision path migrated.\n'); + f.git('add', '-A'); + failed(f.run(), /boundary consumer requires an update/); + f.write(guide, '# Consumer\n\n[Decision](adr/ADR-003-renamed.md)\n'); + f.git('add', guide); + failed(f.run(), /broken local reference in docs\/reader.md/); + f.write('docs/reader.md', '# Reader\n\n[Decision](adr/ADR-003-renamed.md)\n'); + f.git('add', '-A'); + passed(f.run()); +}); + +test('moving the canonical guide cannot leave unchanged readers dangling', t => { + const f = fixture(t); + f.write('docs/reader.md', '# Reader\n\n[Consumer](consumer.md)\n'); + f.commit(); + f.git('mv', guide, 'docs/new consumer.md'); + const next = f.map(); + next.boundaries[0].artifact = 'docs/new consumer.md'; + f.writeMap(next); + f.write('scripts/README.md', '# Gate\n\nConsumer guide moved.\n'); + f.git('add', '-A'); + failed(f.run(), /broken local reference in docs\/reader.md: consumer.md/); + f.write('docs/reader.md', '# Reader\n\n[Consumer]()\n'); + f.git('add', 'docs/reader.md'); + passed(f.run()); +}); + +test('contradictory trigger ownership, malformed maps and nonregular owners fail', t => { + const f = fixture(t); + const next = f.map(); + next.boundaries.push({ ...next.boundaries[0], id: 'duplicate' }); + f.writeMap(next); + f.git('add', '-A'); + failed(f.run(), /invalid: contradictory trigger ownership/); + f.write(mapPath, '{ invalid JSON'); + f.git('add', mapPath); + failed(f.run(), /invalid: cannot parse/); + f.writeMap(); + f.git('add', mapPath); + f.git('update-index', '--cacheinfo', `120000,${f.git('rev-parse', `HEAD:${guide}`)},${guide}`); + failed(f.run(), /unsupported: docs\/consumer.md must be a regular tracked file/); +}); + +for (const [name, body, expected] of [ + ['unreadable', 'echo "diff read denied" >&2; exit 42', /unsupported: cannot read Git input \(diff\): diff read denied/], + ['unterminated', "printf 'M\\0src/internal.rs'", /invalid: malformed Git diff: missing NUL terminator/], + ['malformed', "printf 'M\\0'", /invalid: malformed Git diff: incomplete status\/path pair/], + ['unsupported status', "printf 'U\\0src/internal.rs\\0'", /invalid: malformed Git diff: unsupported status\/path pair/], +]) { + test(`${name} Git diff fails instead of reporting no changed files`, t => { + const f = fixture(t); + const bin = path.join(f.root, 'test-bin'); + mkdirSync(bin); + writeFileSync(path.join(bin, 'git'), `#!/usr/bin/env bash\nif [ "$1" = diff ]; then ${body}; fi\nexec "${realGit}" "$@"\n`, { mode: 0o755 }); + failed(f.run({ PATH: `${bin}:${process.env.PATH}` }), expected); + }); +} + +test('untouched historical missing links do not expand the gate beyond affected authority', t => { + const f = fixture(t); + f.write('docs/old-plan.md', '# Historical proposal\n\n`docs/adr/ADR-099-not-adopted.md`\n'); + f.commit(); + f.write('src/internal.rs', 'fn repaired() {}\n'); + f.git('add', '-A'); + passed(f.run()); +}); + +test('a deleted mapped decision still requires its guide after removing the trigger row', t => { + const f = fixture(t); + f.git('rm', decision); + const next = f.map(); + next.boundaries = next.boundaries.filter(row => row.id !== 'consumer'); + f.writeMap(next); + f.write('scripts/README.md', '# Gate\n\nDecision retired.\n'); + f.git('add', '-A'); + failed(f.run(), /boundary consumer requires an update/); + f.write(guide, '# Consumer\n\nDecision retired; no remaining lifecycle promise.\n'); + f.git('add', guide); + passed(f.run()); +}); + +test('Markdown paths are document-relative and support encoded or angle-bracket spaces', t => { + const f = fixture(t); + f.write('docs/extra guide.md', '# Extra guide\n'); + f.write(guide, '# Consumer\n\n[Extra]() and [Encoded](extra%20guide.md)\n'); + f.git('add', '-A'); + passed(f.run()); + f.write(guide, '# Consumer\n\n[Broken]()\n'); + f.git('add', guide); + failed(f.run(), /broken local reference in docs\/consumer.md: missing(?:%20| )guide.md/); + f.write(guide, '# Consumer\n\n[Relative](docs/adr/ADR-001-example.md)\n'); + f.git('add', guide); + failed(f.run(), /docs\/docs\/adr\/ADR-001-example.md/); +}); + +test('CommonMark destinations, references and directory links resolve without parsing examples', t => { + const f = fixture(t); + f.write('docs/consumer(v2).md', '# Second consumer\n'); + f.write('docs/examples/README.md', '# Examples\n'); + f.write(guide, '# Consumer\n\n[Balanced](consumer(v2).md)\n[Directory](examples/)\n[Reference][next]\n\n[next]: consumer(v2).md\n\n```markdown\n[Example](missing.md)\n`docs/adr/ADR-999-example.md`\n```\n\n [Indented](missing.md)\n'); + f.git('add', '-A'); + passed(f.run()); + f.write(guide, '# Consumer\n\n[Missing directory](missing/)\n'); + f.git('add', guide); + failed(f.run(), /broken local reference.*missing\//); +}); + +test('map object and boundary ordering do not change declared knowledge', t => { + const f = fixture(t); + const next = f.map(); + next.boundaries = next.boundaries.reverse().map(row => Object.fromEntries(Object.entries(row).reverse())); + f.writeMap(next); + f.git('add', mapPath); + passed(f.run()); +}); + +test('changing the canonical owner requires disposition of the retained prior guide', t => { + const f = fixture(t); + const next = f.map(); + next.boundaries[0].artifact = 'docs/new-consumer.md'; + f.writeMap(next); + f.write('docs/new-consumer.md', '# Consumer\n\nUpdated lifecycle owner.\n'); + f.write('scripts/README.md', '# Gate\n\nConsumer ownership migrated.\n'); + f.write(decision, '# Decision\n\nNew consumer lifecycle.\n'); + f.git('add', '-A'); + failed(f.run(), /boundary consumer requires an update to docs\/consumer.md/); + f.write(guide, '# Consumer moved\n\nSee [current owner](new-consumer.md).\n'); + f.git('add', guide); + passed(f.run()); +}); diff --git a/scripts/decision-traceability-map.json b/scripts/decision-traceability-map.json new file mode 100644 index 000000000..53d9ea868 --- /dev/null +++ b/scripts/decision-traceability-map.json @@ -0,0 +1,50 @@ +{ + "version": 1, + "boundaries": [ + { + "id": "headless-execution", + "triggers": [ + "docs/adr/ADR-001-headless-embedding-service-boundary.md", + "docs/adr/ADR-011-scheduler-only-workflow-execution.md" + ], + "artifact": "docs/headless-workflow.md", + "profile": "contract-readme", + "knowledge": "Host adapter ownership and the scheduler-session consumer lifecycle" + }, + { + "id": "runtime-operations", + "triggers": [ + "docs/adr/ADR-002-runtime-registry-ownership-and-lifecycle.md", + "docs/adr/ADR-003-runtime-redistributables-manager-boundary.md", + "docs/adr/ADR-007-managed-runtime-observability-ownership.md" + ], + "artifact": "docs/runtime-operations.md", + "profile": "runbook", + "knowledge": "Backend runtime lifecycle, readiness, observability, and recovery ownership" + }, + { + "id": "node-contract-ownership", + "triggers": [ + "docs/adr/ADR-006-canonical-node-contract-ownership.md", + "docs/adr/ADR-009-composed-node-contracts-and-migration.md" + ], + "artifact": "ARCHITECTURE.md", + "profile": "boundary-readme", + "knowledge": "Canonical primitive/composed node contracts and migration responsibility" + }, + { + "id": "rust-workspace-policy", + "triggers": ["docs/adr/ADR-017-rust-workspace-policy.md"], + "artifact": "docs/development.md", + "profile": "runbook", + "knowledge": "Contributor-facing workspace policy and verification procedure" + }, + { + "id": "traceability-policy", + "triggers": ["scripts/decision-traceability-map.json"], + "artifact": "scripts/README.md", + "profile": "boundary-readme", + "knowledge": "Declared impact coverage and operation of the traceability gate" + } + ] +} From e22ebb2fe52efc8cfcaac6b23c4a902c7bc6a571 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 15:03:56 -0700 Subject: [PATCH 02/65] fix(ci): select explicit fork-point ranges for pull requests --- .github/workflows/quality-gates.yml | 26 +++++++++++++++++-- .../reports/2026-10-02-traceability-gate.md | 21 +++++++++++++++ scripts/README.md | 8 ++++-- scripts/check-decision-traceability.test.mjs | 21 +++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml index 774f1503d..a33987d48 100644 --- a/.github/workflows/quality-gates.yml +++ b/.github/workflows/quality-gates.yml @@ -35,11 +35,33 @@ jobs: - name: Test decision traceability gate run: node --test scripts/check-decision-traceability.test.mjs + - name: Resolve explicit traceability range + id: traceability-range + env: + EVENT_NAME: ${{ github.event_name }} + EVENT_BASE: ${{ github.event.pull_request.base.sha || github.event.before }} + EVENT_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} + shell: bash + run: | + set -euo pipefail + base="$(git rev-parse --verify --end-of-options "${EVENT_BASE}^{commit}")" + head="$(git rev-parse --verify --end-of-options "${EVENT_HEAD}^{commit}")" + if [[ "$EVENT_NAME" == pull_request ]]; then + # The PR owns changes since its fork point, not reverse changes + # from an independently advanced target branch. + base="$(git merge-base --all "$base" "$head")" + if [[ -z "$base" || "$base" == *$'\n'* ]]; then + echo "Traceability requires one unambiguous PR merge base" >&2 + exit 1 + fi + fi + printf 'base=%s\nhead=%s\n' "$base" "$head" >> "$GITHUB_OUTPUT" + - name: Run no-new-debt lint gate env: TRACEABILITY_MODE: range - TRACEABILITY_BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before }} - TRACEABILITY_HEAD_REF: ${{ github.event.pull_request.head.sha || github.sha }} + TRACEABILITY_BASE_REF: ${{ steps.traceability-range.outputs.base }} + TRACEABILITY_HEAD_REF: ${{ steps.traceability-range.outputs.head }} run: npm run lint:no-new typecheck: diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-traceability-gate.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-traceability-gate.md index 5e6db3df6..6d650a5a7 100644 --- a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-traceability-gate.md +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-traceability-gate.md @@ -138,3 +138,24 @@ coordination; it is not an accepted integration or cleanup instruction. The existing pre-push `npm test` also passed on 2026-10-02: node-engine 258 passed / 1 ignored, workflow-nodes 168 passed. This proves its declared library scope, not the unrelated failing aggregate/desktop gates. + +## Hosted Review: Pull-request Range Selection + +CodeRabbit reviewed exact head `9567186252f7ebb60794911baeeabce0daec5140` and +identified the PR target-tip versus fork-point mismatch. The workflow now +resolves a unique PR merge base explicitly, while push events retain their +before/head range. The gate continues to compare exactly the supplied snapshots; +it does not silently reinterpret an explicit range. Ambiguous/no common bases +fail. A diverged-branch regression covers unrelated target-only map additions. +This correction is separate from unchanged baseline CI failures. + +Correction verification: 25 focused tests pass. The actual workflow shell was +extracted and executed against a disposable divergent Git fixture: PR mode +selected its fork point, and push mode preserved the exact before/head input. +Workflow YAML, Bash syntax, scoped ESLint and whitespace checks pass. The +integration owner independently reviewed staged tree +`85ab1fe64e85c7f299d53f99e5ebb74625730607`, read the actual workflow delta, +reproduced the divergent-PR regression and passed staged whitespace checks. +That bounded review found no new source issue and accepted publication. +This report-only addition records the review; exact-head hosted CI and hosted +review qualification remain outstanding. diff --git a/scripts/README.md b/scripts/README.md index 6074ffba6..c90ca0066 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -173,8 +173,12 @@ TRACEABILITY_MODE=range TRACEABILITY_BASE_REF= \ TRACEABILITY_HEAD_REF= npm run traceability ``` -The gate reports the resolved input IDs. CI supplies event base/head commits -and fetches their history. Missing modes, refs, maps, owners or local targets +The gate reports and compares the exact supplied input IDs. CI fetches history +and, for pull requests, explicitly selects the unique merge base of the event +base/head commits as the comparison base. This excludes unrelated target-branch +changes from the PR delta. Push events compare the event before/head commits +directly. Missing or multiple PR merge bases fail; the gate itself never silently +substitutes a caller-provided revision. Missing modes, refs, maps, owners or local targets fail; unreadable Git state also fails. The old source-root/host/producer path list overrides are rejected. The shell entrypoint explicitly selects this repository's map; the Node implementation accepts `--map` for isolated tests or diff --git a/scripts/check-decision-traceability.test.mjs b/scripts/check-decision-traceability.test.mjs index 7cbbb62b1..104f91296 100644 --- a/scripts/check-decision-traceability.test.mjs +++ b/scripts/check-decision-traceability.test.mjs @@ -310,3 +310,24 @@ test('changing the canonical owner requires disposition of the retained prior gu f.git('add', guide); passed(f.run()); }); + +test('PR callers select the fork point explicitly without changing exact range semantics', t => { + const f = fixture(t); + f.write('src/internal.rs', 'fn pr_repair() {}\n'); + const head = f.commit(); + f.git('checkout', '-b', 'target', f.base); + const next = f.map([{ id: 'target-only', triggers: ['docs/adr/ADR-002-target.md'], artifact: 'docs/target-guide.md', profile: 'contract-readme', knowledge: 'Target-only contract' }]); + f.writeMap(next); + f.write('docs/adr/ADR-002-target.md', '# Target decision\n'); + f.write('docs/target-guide.md', '# Target owner\n'); + f.write('scripts/README.md', '# Gate\n\nTarget-only contract added.\n'); + const target = f.commit(); + const env = { TRACEABILITY_STAGED_ONLY: undefined, TRACEABILITY_MODE: 'range', TRACEABILITY_BASE_REF: target, TRACEABILITY_HEAD_REF: head }; + // Exact range is intentionally not silently changed by the checker. + failed(f.run(env), /unavailable: docs\/target-guide.md is missing/); + const base = f.git('merge-base', '--all', target, head); + assert.equal(base, f.base); + const result = f.run({ ...env, TRACEABILITY_BASE_REF: base }); + passed(result); + assert.match(result.output, /1 changed path\(s\), 0 mapped impact\(s\)/); +}); From 4cf2317b5d541e534cd953dddb9a3aa919fceb52 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 15:59:45 -0700 Subject: [PATCH 03/65] fix(ci): provision native prerequisites and pin C# generator --- .../workflows/headless-embedding-contract.yml | 7 +- .github/workflows/quality-gates.yml | 34 +------- .../workflows/runtime-separation-check.yml | 3 + bindings/csharp/README.md | 20 +++++ .../reports/2026-10-02-ci-prerequisites.md | 78 +++++++++++++++++++ scripts/README.md | 10 +++ scripts/check-uniffi-csharp-smoke.sh | 2 +- scripts/install-ubuntu-build-dependencies.sh | 19 +++++ scripts/package-uniffi-csharp-artifacts.sh | 2 +- 9 files changed, 142 insertions(+), 33 deletions(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-ci-prerequisites.md create mode 100755 scripts/install-ubuntu-build-dependencies.sh diff --git a/.github/workflows/headless-embedding-contract.yml b/.github/workflows/headless-embedding-contract.yml index 943faad8e..fb53a8088 100644 --- a/.github/workflows/headless-embedding-contract.yml +++ b/.github/workflows/headless-embedding-contract.yml @@ -25,7 +25,12 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Install C# UniFFI generator - run: cargo install uniffi-bindgen-cs --version 0.9.0 --locked + # Official v0.9.0+v0.28.3 source; this version is not on crates.io. + run: >- + cargo install uniffi-bindgen-cs + --git https://github.com/NordSecurity/uniffi-bindgen-cs + --rev 2f4880f03ed08d960ad0ee14d11cf95444eee540 + --locked - name: Run workflow service tests run: cargo test -p pantograph-workflow-service diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml index a33987d48..882259565 100644 --- a/.github/workflows/quality-gates.yml +++ b/.github/workflows/quality-gates.yml @@ -128,21 +128,8 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install Ubuntu native desktop dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential \ - libayatana-appindicator3-dev \ - libglib2.0-dev \ - libgtk-3-dev \ - libjavascriptcoregtk-4.1-dev \ - librsvg2-dev \ - libsoup-3.0-dev \ - libssl-dev \ - libwebkit2gtk-4.1-dev \ - libxdo-dev \ - pkg-config + - name: Install Ubuntu native build dependencies + run: ./scripts/install-ubuntu-build-dependencies.sh - name: Checkout Pumas-Library uses: actions/checkout@v4 @@ -323,21 +310,8 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install Ubuntu native desktop dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential \ - libayatana-appindicator3-dev \ - libglib2.0-dev \ - libgtk-3-dev \ - libjavascriptcoregtk-4.1-dev \ - librsvg2-dev \ - libsoup-3.0-dev \ - libssl-dev \ - libwebkit2gtk-4.1-dev \ - libxdo-dev \ - pkg-config + - name: Install Ubuntu native build dependencies + run: ./scripts/install-ubuntu-build-dependencies.sh - name: Checkout Pumas-Library uses: actions/checkout@v4 diff --git a/.github/workflows/runtime-separation-check.yml b/.github/workflows/runtime-separation-check.yml index 5907ec0a6..97611c2dc 100644 --- a/.github/workflows/runtime-separation-check.yml +++ b/.github/workflows/runtime-separation-check.yml @@ -13,6 +13,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install Ubuntu native build dependencies + run: ./scripts/install-ubuntu-build-dependencies.sh + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@1.92.0 diff --git a/bindings/csharp/README.md b/bindings/csharp/README.md index 2abdfa44a..ff8566ece 100644 --- a/bindings/csharp/README.md +++ b/bindings/csharp/README.md @@ -10,6 +10,26 @@ Runtime smoke coverage for generated Pantograph C# bindings. | `Pantograph.DirectRuntimeQuickstart/` | Artifact-ready quickstart showing native save/list/load/edit-session/workflow-session usage from C#. | | `PACKAGE-README.md` | README copied to the generated C# binding artifact. | +## Generator prerequisite + +The maintained workflow uses UniFFI 0.28.3 and the official +[uniffi-bindgen-cs v0.9.0+v0.28.3 source](https://github.com/NordSecurity/uniffi-bindgen-cs/tree/2f4880f03ed08d960ad0ee14d11cf95444eee540). +The generator is not published as crates.io version 0.9.0. Install its exact +reviewed Git revision with the upstream lockfile: + +```bash +cargo install uniffi-bindgen-cs \ + --git https://github.com/NordSecurity/uniffi-bindgen-cs \ + --rev 2f4880f03ed08d960ad0ee14d11cf95444eee540 --locked +``` + +This provisions a development/CI tool; it does not change the application's +UniFFI dependency or hand-edit generated bindings. The headless workflow owns +its pin and generated-artifact qualification. Keep the upstream MPL-2.0 notices +with any redistributed generator source/binary; this repository does not vendor +or package the generator itself. Existing generated-binding/artifact licensing +obligations remain unchanged. + ## Usage Run the repository-level smoke script: diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-ci-prerequisites.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-ci-prerequisites.md new file mode 100644 index 000000000..1ae424056 --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-ci-prerequisites.md @@ -0,0 +1,78 @@ +# CI Build Prerequisite Repair + +Date: 2026-10-02. Status: `Verifying`. +Parent authority: [active domain architecture plan](../plan.md), CI-01 baseline +repair scope. Branch: `fix/ci-build-prerequisites-2026-10-02`, admitted from gate +`e22ebb2fe52efc8cfcaac6b23c4a902c7bc6a571`. Initial PR target is the gate branch; +retarget `main` after its prerequisite qualifies. The tooling owner implements +this isolated proposal; the domain integrator owns review and integration. +The checkout is retained for review until integrated, rejected or superseded. +Standards inspected: `MrScripty/Coding-Standards@dcc56f26e884ade260770beceba2501d3746200d`, +Core/Router, Implementation, Verification, Commit, Documentation, Tooling, Build, +Dependencies/Licensing and applicable Rust tooling guidance. + +## Diagnosed Obstructions + +- The headless workflow asks crates.io for generator `uniffi-bindgen-cs` 0.9.0, + which is unavailable there. This reproduces on docs-only PR #1 and gate PR #2 + before any workflow-service or C# smoke runs. Source: + [headless run](https://github.com/MrScripty/Pantograph/actions/runs/37066045057). +- Runtime-separation CI builds the actual Tauri binary but never provisions the + GLib/GTK/WebKit native prerequisites, stopping in `glib-sys`. Source: + [separation run](https://github.com/MrScripty/Pantograph/actions/runs/37066045102). +- Workspace check already provisions desktop libraries but Lance's build needs + `protoc`; the runner has no compiler. Source: + [baseline quality run](https://github.com/MrScripty/Pantograph/actions/runs/37059945912). + +These are build prerequisites, not permission to skip checks, reduce features, +relax warnings, or accept the runtime source. Other failing baseline contracts, +Clippy, frontend lint and dependency-audit findings remain separate work. + +## Coherent Repair + +Use the official generator source rather than an unavailable registry release. +NordSecurity's [tag](https://github.com/NordSecurity/uniffi-bindgen-cs/tree/v0.9.0%2Bv0.28.3) +resolves to `2f4880f03ed08d960ad0ee14d11cf95444eee540`; its manifest and lockfile +identify generator `0.9.0+v0.28.3` and UniFFI 0.28.3, matching this repository's +0.28 consumer. Its README documents Git installation and Rust 1.81 minimum. +Pin that exact commit with `--locked`, retaining the intended generator version. +No application Cargo declaration, Cargo lockfile, generated C# or Pumas pin changes. +The C# binding guide owns local installation instructions; missing-tool messages +link that owner instead of repeating a registry/version guess. The generator is +an unchanged development executable, not vendored or packaged in this proposal; +its [MPL-2.0 source notices](https://github.com/NordSecurity/uniffi-bindgen-cs/blob/2f4880f03ed08d960ad0ee14d11cf95444eee540/LICENSE) +remain upstream. Existing binding/release obligations are not waived. + +Consolidate the two existing identical Ubuntu native package lists into +`scripts/install-ubuntu-build-dependencies.sh`, add `protobuf-compiler`, and use +that same owned list for the runtime-separation build. The only permitted system +mutation is the existing hosted Ubuntu provisioning step; no local or user +computer system packages were installed. Failed package installation fails the +step. The existing Rust checks, features, ordering and failure behavior remain. + +Write set: three affected workflow YAML files, the shared Ubuntu installer, +the scripts and C# guides, two C# scripts' missing-tool messages, and this report. +No runtime source, test fixture, severity, broad dependency upgrade, published +branch history or presentation artifact changes. + +## Evidence And Remaining Claims + +- Official tag resolution, source manifest, lockfile and README checked through + GitHub at the exact generator revision. +- All workflow YAML parses; all three affected shell scripts pass `bash -n`. +- A disposable `sudo` substitute verified the exact apt update/install argument + sequence, retained GLib/Desktop packages, added protobuf compiler and fail-fast + propagation of an installation error. This proves orchestration only; it does + not establish successful apt provisioning. +- Staged whitespace, staged traceability (nine paths, no ownership impact) and + tooling lint passed before publication. +- Actual generator installation, produced binding compatibility, native compile + and all downstream CI claims remain pending on an Ubuntu hosted runner. +- Independent integrator review accepted staged tree + `a7567c4563dc09a145ee897983c31c8a8b3ec36f`: all nine files inspected, + Bash syntax and whitespace independently checked, and upstream generator and + workspace manifests verified at the exact pinned revision. This bounded source + review found no blocker; actual provisioning and native/generator builds still + require hosted qualification. This report records that acceptance after review. +- This proposal remains a draft, not a green-CI or release claim. No merge until + required exact-head CI and applicable hosted review qualify it and its prerequisite. diff --git a/scripts/README.md b/scripts/README.md index c90ca0066..c4d431ff6 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -209,3 +209,13 @@ with review and applicable contract tests. Routine repairs under unchanged contracts need no documentation churn. Existing host/binding, scheduler-only, worker-protocol and structured-producer checks remain required; the removed universal README headings never proved those contracts. + +## Ubuntu CI Build Prerequisites + +`install-ubuntu-build-dependencies.sh` owns the shared native package list for +workspace check, warning-deny Clippy and the desktop linkage build on Ubuntu +hosted runners. It retains the existing GTK/WebKit/libsoup packages and installs +`protobuf-compiler` for the Lance build's `protoc` requirement. This command +uses `sudo apt-get` and changes the runner's system packages; do not treat it as +a read-only check or run it on another computer without authorization. A failed +install fails the step. It changes no Rust features, pins or verification gates. diff --git a/scripts/check-uniffi-csharp-smoke.sh b/scripts/check-uniffi-csharp-smoke.sh index 0ec573a19..0142ba24a 100755 --- a/scripts/check-uniffi-csharp-smoke.sh +++ b/scripts/check-uniffi-csharp-smoke.sh @@ -6,7 +6,7 @@ cd "$repo_root" if ! command -v uniffi-bindgen-cs >/dev/null 2>&1; then echo "Missing required generator: uniffi-bindgen-cs" >&2 - echo "Install a UniFFI 0.28-compatible C# generator, for example uniffi-bindgen-cs 0.9.x." >&2 + echo "See bindings/csharp/README.md for the pinned UniFFI 0.28-compatible generator install." >&2 exit 1 fi diff --git a/scripts/install-ubuntu-build-dependencies.sh b/scripts/install-ubuntu-build-dependencies.sh new file mode 100755 index 000000000..dfb5d3492 --- /dev/null +++ b/scripts/install-ubuntu-build-dependencies.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Shared prerequisites for the existing Ubuntu desktop-build CI consumers. +# This intentionally installs system packages; run only on the selected runner. +sudo apt-get update +sudo apt-get install -y \ + build-essential \ + libayatana-appindicator3-dev \ + libglib2.0-dev \ + libgtk-3-dev \ + libjavascriptcoregtk-4.1-dev \ + librsvg2-dev \ + libsoup-3.0-dev \ + libssl-dev \ + libwebkit2gtk-4.1-dev \ + libxdo-dev \ + pkg-config \ + protobuf-compiler diff --git a/scripts/package-uniffi-csharp-artifacts.sh b/scripts/package-uniffi-csharp-artifacts.sh index 006b6a592..0213f4c30 100755 --- a/scripts/package-uniffi-csharp-artifacts.sh +++ b/scripts/package-uniffi-csharp-artifacts.sh @@ -6,7 +6,7 @@ cd "$repo_root" if ! command -v uniffi-bindgen-cs >/dev/null 2>&1; then echo "Missing required generator: uniffi-bindgen-cs" >&2 - echo "Install a UniFFI 0.28-compatible C# generator, for example uniffi-bindgen-cs 0.9.x." >&2 + echo "See bindings/csharp/README.md for the pinned UniFFI 0.28-compatible generator install." >&2 exit 1 fi From 74723b5249ad35404506c6c01ab6c087c8db3b58 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 16:22:08 -0700 Subject: [PATCH 04/65] test(workflow): restore canonical contract fixture preconditions --- .../src/graph/session_tests.rs | 21 ++++-- .../src/technical_fit.rs | 4 +- .../workflow/task_execution_classification.rs | 10 ++- .../2026-10-02-workflow-contract-fixtures.md | 65 +++++++++++++++++++ 4 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-workflow-contract-fixtures.md diff --git a/crates/pantograph-workflow-service/src/graph/session_tests.rs b/crates/pantograph-workflow-service/src/graph/session_tests.rs index 13a755377..c6c3d12ea 100644 --- a/crates/pantograph-workflow-service/src/graph/session_tests.rs +++ b/crates/pantograph-workflow-service/src/graph/session_tests.rs @@ -1324,11 +1324,16 @@ async fn refresh_current_validation_summary_rejects_stale_requested_revision() { async fn refresh_current_validation_summary_rejects_revision_changed_during_fact_lookup() { let entered = Arc::new(Notify::new()); let release = Arc::new(Notify::new()); + let second_entered = Arc::new(Notify::new()); + let second_release = Arc::new(Notify::new()); let store = Arc::new(GraphSessionStore::with_inference_interface_facts_provider( - Arc::new(BlockingInferenceFactsProvider { + Arc::new(SequencedBlockingInferenceFactsProvider { facts: BTreeMap::from([("infer".to_string(), ready_inference_facts())]), - entered: Arc::clone(&entered), - release: Arc::clone(&release), + calls: Mutex::new(0), + first_entered: Arc::clone(&entered), + first_release: Arc::clone(&release), + second_entered: Arc::clone(&second_entered), + second_release: Arc::clone(&second_release), }), )); let session = store @@ -1357,7 +1362,10 @@ async fn refresh_current_validation_summary_rejects_revision_changed_during_fact }) .await .expect("mutate graph while validation facts are pending"); + second_entered.notified().await; release.notify_one(); + second_release.notify_one(); + store.drain_validation_tasks_for_tests().await; let response = refresh .await .expect("refresh task should not panic") @@ -3086,7 +3094,12 @@ fn inference_graph() -> WorkflowGraph { position: Position { x: 200.0, y: 0.0 }, data: serde_json::json!({ "task_kind": "image_generation", - "runtime": "pytorch" + "runtime": "pytorch", + "runtime_source_context": { + "operation_type": "image_generation", + "context_shape_key": "image.1024.square", + "cancellation_mode": "run_scoped" + } }), }, ], diff --git a/crates/pantograph-workflow-service/src/technical_fit.rs b/crates/pantograph-workflow-service/src/technical_fit.rs index a4f5763db..8565861f0 100644 --- a/crates/pantograph-workflow-service/src/technical_fit.rs +++ b/crates/pantograph-workflow-service/src/technical_fit.rs @@ -1636,9 +1636,9 @@ mod tests { let assessment = workflow_runtime_preflight_from_decision( &decision, - &[], + &["candle".to_string()], &["llm/gen-verse/trado-8b-instruct".to_string()], - &[unavailable_candle_runtime()], + &[], ); assert_eq!(assessment.runtime_warnings.len(), 1); diff --git a/crates/pantograph-workflow-service/src/workflow/task_execution_classification.rs b/crates/pantograph-workflow-service/src/workflow/task_execution_classification.rs index b1d0a5f64..5740e7f8c 100644 --- a/crates/pantograph-workflow-service/src/workflow/task_execution_classification.rs +++ b/crates/pantograph-workflow-service/src/workflow/task_execution_classification.rs @@ -108,7 +108,7 @@ mod tests { #[test] fn classifier_rejects_excluded_and_unknown_nodes() { - for node_type in ["model-provider", "expand-settings", "image-output"] { + for node_type in ["model-provider", "image-output"] { let contract = contract(node_type); assert_eq!( @@ -118,6 +118,14 @@ mod tests { ); } + assert!(!workflow_nodes::builtin_node_contracts() + .expect("built-in node contracts") + .iter() + .any(|contract| contract.node_type.as_str() == "expand-settings")); + assert_eq!( + classify_workflow_scheduler_task("expand-settings", None), + WorkflowSchedulerTaskExecutionClass::Unsupported + ); assert_eq!( classify_workflow_scheduler_task("not-registered", None), WorkflowSchedulerTaskExecutionClass::Unsupported diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-workflow-contract-fixtures.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-workflow-contract-fixtures.md new file mode 100644 index 000000000..5998666dd --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-workflow-contract-fixtures.md @@ -0,0 +1,65 @@ +# Workflow Contract Fixture Repair + +Date: 2026-10-02. Status: `Verifying`. +Authority: [domain architecture plan](../plan.md), CI-01 baseline qualification. +Branch: `fix/workflow-contract-fixtures-2026-10-02`, based on prerequisite +`4cf2317b5d541e534cd953dddb9a3aa919fceb52` ([PR #6](https://github.com/MrScripty/Pantograph/pull/6)). +The workflow-service test owner implements this slice; the domain integrator owns +independent review and integration. Retain the branch until integrated, rejected +or superseded. Coding standards inspected at +`MrScripty/Coding-Standards@dcc56f26e884ade260770beceba2501d3746200d` include +Implementation, Verification, Commit, Documentation and Rust tooling guidance. + +## Reproduction + +The prerequisite branch installed the pinned C# generator successfully. Its +[headless run](https://github.com/MrScripty/Pantograph/actions/runs/37075520823) +then stopped at `cargo test -p pantograph-workflow-service`: 852 library tests +passed and 20 failed. All 20 failure identities match the earlier base-4938e40 +library run. The earlier aggregate of 23 also included three integration-contract +failures reached with `--no-fail-fast`. Comparing the prerequisite head with base +shows no workflow-service, Cargo manifest or lockfile source changes. +The original assertion logs were retained during diagnosis. + +PR #6 also passed the runtime-separation workflow and workspace checks with both +no default features and all features. Those receipts qualify provisioning and +native compilation, not the skipped generated-binding smoke or whole CI. + +## Corrected Preconditions + +The shared inference graph fixture omitted required `runtime_source_context`, +so publication rejected its request before lifecycle and dependency assertions. +Supply the same explicit operation, context shape and cancellation facts as the +existing publication tests. No production fallback or validation relaxation is +introduced. The stale-revision test also raced the new automatic validation +started by graph mutation. Use its existing sequenced facts provider to distinguish +both requests, then drain background validation before asserting the new revision +is Current. All original stale-response and current-summary assertions remain. + +The classifier fixture demanded a built-in contract for retired `expand-settings`. +Assert that the registry omits that type and that a missing contract is rejected; +retain rejection checks for existing excluded and unknown node types. + +The missing-candidate test supplied neither a runtime requirement nor a selected +model, making readiness irrelevant, and supplied a differently failing runtime. +Declare its required backend and omit candidate capabilities so the unchanged +incomplete-candidate warning and blocking assertions exercise their intended path. +Existing unavailable-runtime and irrelevant-runtime tests remain intact. + +## Verification And Open Work + +- Fresh Cargo compilation and complete library run: 867 passed, five failed, + zero ignored. All 15 repaired failures passed; only the unchanged capacity + tests remain. This is not a green aggregate claim. +- The five capacity fixtures execute a source/output graph without inputs and + expect runtime admission; non-runtime execution intentionally does not load a + runtime. Their admission/lifecycle setup requires a separate bounded repair. +- Three earlier integration-contract failures and other CI lint, audit and Rustler + failures remain separate. No workflow check or test has been disabled. +- Focused library run excluding the unchanged nine-test capacity module: 863 + passed. The synchronized race passed ten isolated repetitions. Cargo format, + staged whitespace and staged traceability passed. +- Independent integrator review accepted staged tree + `0650a9620edeff2c943f649147e5e2be06b460f8` after inspecting the changes, + surrounding source, report and whitespace. This report records that bounded + acceptance afterward. Exact-head hosted qualification remains pending. From 333e53db2b60c85c0190fd6ba34b75d3d5ecce46 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 17:10:30 -0700 Subject: [PATCH 05/65] fix(workflow): preserve session capacity state and diagnostics --- .../src/event.rs | 77 +++- .../src/tests.rs | 81 +++++ .../src/workflow/diagnostic_errors.rs | 45 +++ .../src/workflow/session_runtime.rs | 187 +++++++--- .../src/workflow/tests.rs | 1 + .../src/workflow/tests/diagnostics.rs | 19 +- .../src/workflow/tests/session_capacity.rs | 248 ++++++++++--- .../workflow/tests/session_capacity_faults.rs | 337 ++++++++++++++++++ docs/headless-workflow.md | 29 ++ .../plan.md | 37 ++ ...26-10-02-session-capacity-observability.md | 98 +++++ 11 files changed, 1052 insertions(+), 107 deletions(-) create mode 100644 crates/pantograph-workflow-service/src/workflow/tests/session_capacity_faults.rs create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-session-capacity-observability.md diff --git a/crates/pantograph-diagnostics-ledger/src/event.rs b/crates/pantograph-diagnostics-ledger/src/event.rs index a017ae9ba..a608c9c9d 100644 --- a/crates/pantograph-diagnostics-ledger/src/event.rs +++ b/crates/pantograph-diagnostics-ledger/src/event.rs @@ -1062,6 +1062,10 @@ impl SchedulerModelCacheState { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct SchedulerModelLifecycleChangedPayload { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workflow_execution_session_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub unloaded_workflow_execution_session_id: Option, pub transition: SchedulerModelLifecycleTransition, #[serde(default)] pub cache_state: Option, @@ -1080,6 +1084,25 @@ pub struct SchedulerModelLifecycleChangedPayload { impl SchedulerModelLifecycleChangedPayload { fn validate(&self) -> Result<(), DiagnosticsLedgerError> { + match ( + &self.workflow_execution_session_id, + &self.unloaded_workflow_execution_session_id, + ) { + (Some(target), Some(unloaded)) => { + validate_required_text("workflow_execution_session_id", target, MAX_ID_LEN)?; + validate_required_text( + "unloaded_workflow_execution_session_id", + unloaded, + MAX_ID_LEN, + )?; + } + (None, None) => {} + _ => { + return Err(DiagnosticsLedgerError::MissingField { + field: "capacity_session_identity_pair", + }) + } + } if let Some(summary) = self.execution_plan_summary.as_ref() { summary.validate()?; } @@ -2316,6 +2339,7 @@ impl InferenceOptionDiagnosticSummary { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DiagnosticErrorScopeKind { + SessionRuntime, Run, Node, RuntimeModel, @@ -2397,6 +2421,8 @@ impl DiagnosticErrorLocation { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct DiagnosticErrorOccurredPayload { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workflow_execution_session_id: Option, pub phase: String, pub scope: DiagnosticErrorScopeKind, pub severity: DiagnosticErrorSeverity, @@ -2415,6 +2441,18 @@ pub struct DiagnosticErrorOccurredPayload { impl DiagnosticErrorOccurredPayload { fn validate(&self) -> Result<(), DiagnosticsLedgerError> { + if self.scope == DiagnosticErrorScopeKind::SessionRuntime { + let session_id = self.workflow_execution_session_id.as_deref().ok_or( + DiagnosticsLedgerError::MissingField { + field: "workflow_execution_session_id", + }, + )?; + validate_required_text("workflow_execution_session_id", session_id, MAX_ID_LEN)?; + } else if self.workflow_execution_session_id.is_some() { + return Err(DiagnosticsLedgerError::InvalidField { + field: "workflow_execution_session_id", + }); + } validate_required_text("error_phase", &self.phase, MAX_ID_LEN)?; validate_required_text("error_code", &self.code, MAX_ID_LEN)?; validate_required_text( @@ -3465,7 +3503,6 @@ fn validate_event_scope( | DiagnosticEventKind::SchedulerQueuePlacement | DiagnosticEventKind::SchedulerQueueControl | DiagnosticEventKind::SchedulerRunDelayed - | DiagnosticEventKind::SchedulerModelLifecycleChanged | DiagnosticEventKind::SchedulerRunAdmitted | DiagnosticEventKind::SchedulerReservationChanged | DiagnosticEventKind::SchedulerTaskAttemptLifecycleChanged @@ -3501,9 +3538,26 @@ fn validate_event_scope( { return Err(DiagnosticsLedgerError::MissingField { field: "node_id" }); } - if request.payload.event_kind() == DiagnosticEventKind::SchedulerModelLifecycleChanged - && request.model_id.is_none() + } + DiagnosticEventKind::SchedulerModelLifecycleChanged => { + let DiagnosticEventPayload::SchedulerModelLifecycleChanged(payload) = &request.payload + else { + return Err(DiagnosticsLedgerError::InvalidField { + field: "event_kind", + }); + }; + if request.workflow_run_id.is_none() && payload.workflow_execution_session_id.is_none() { + return Err(DiagnosticsLedgerError::MissingField { + field: "workflow_run_id_or_execution_session_id", + }); + } + if request.workflow_id.is_none() { + return Err(DiagnosticsLedgerError::MissingField { + field: "workflow_id", + }); + } + if request.model_id.is_none() { return Err(DiagnosticsLedgerError::MissingField { field: "model_id" }); } } @@ -3515,7 +3569,9 @@ fn validate_event_scope( }; if !matches!( payload.scope, - DiagnosticErrorScopeKind::Transport | DiagnosticErrorScopeKind::Projection + DiagnosticErrorScopeKind::Transport + | DiagnosticErrorScopeKind::Projection + | DiagnosticErrorScopeKind::SessionRuntime ) { if request.workflow_run_id.is_none() { return Err(DiagnosticsLedgerError::MissingField { @@ -3569,6 +3625,19 @@ fn validate_diagnostic_error_scope( } Ok(()) } + DiagnosticErrorScopeKind::SessionRuntime => { + if request.workflow_id.is_none() { + return Err(DiagnosticsLedgerError::MissingField { + field: "workflow_id", + }); + } + if request.scheduler_policy_id.is_none() { + return Err(DiagnosticsLedgerError::MissingField { + field: "scheduler_policy_id", + }); + } + Ok(()) + } DiagnosticErrorScopeKind::Scheduler => { if request.scheduler_policy_id.is_none() { return Err(DiagnosticsLedgerError::MissingField { diff --git a/crates/pantograph-diagnostics-ledger/src/tests.rs b/crates/pantograph-diagnostics-ledger/src/tests.rs index 68d435032..4ea640319 100644 --- a/crates/pantograph-diagnostics-ledger/src/tests.rs +++ b/crates/pantograph-diagnostics-ledger/src/tests.rs @@ -1173,6 +1173,8 @@ fn model_lifecycle_projects_canonical_error_link_without_counting_new_error() { let mut lifecycle = sample_scheduler_model_lifecycle_event("workflow_run_alpha"); lifecycle.payload = DiagnosticEventPayload::SchedulerModelLifecycleChanged( SchedulerModelLifecycleChangedPayload { + workflow_execution_session_id: None, + unloaded_workflow_execution_session_id: None, transition: SchedulerModelLifecycleTransition::LoadFailed, cache_state: Some(SchedulerModelCacheState::Failed), execution_plan_summary: Some(SchedulerExecutionPlanSummary { @@ -5872,6 +5874,8 @@ fn sample_scheduler_model_lifecycle_event(workflow_run_id: &str) -> DiagnosticEv payload_ref: None, payload: DiagnosticEventPayload::SchedulerModelLifecycleChanged( SchedulerModelLifecycleChangedPayload { + workflow_execution_session_id: None, + unloaded_workflow_execution_session_id: None, transition: SchedulerModelLifecycleTransition::LoadRequested, cache_state: Some(SchedulerModelCacheState::CacheMiss), execution_plan_summary: None, @@ -5911,6 +5915,7 @@ fn sample_diagnostic_error_event(workflow_run_id: &str) -> DiagnosticEventAppend retention_class: DiagnosticEventRetentionClass::AuditMetadata, payload_ref: None, payload: DiagnosticEventPayload::DiagnosticErrorOccurred(DiagnosticErrorOccurredPayload { + workflow_execution_session_id: None, phase: "runtime_model_load".to_string(), scope: DiagnosticErrorScopeKind::RuntimeModel, severity: DiagnosticErrorSeverity::Fatal, @@ -6581,3 +6586,79 @@ fn sample_event( correlation_id: Some("corr-1".to_string()), } } + +#[test] +fn session_capacity_events_preserve_identity_without_creating_a_run() { + let mut ledger = SqliteDiagnosticsLedger::open_in_memory().expect("ledger"); + let mut event = sample_scheduler_model_lifecycle_event("unused"); + event.workflow_run_id = None; + event.workflow_version_id = None; + event.workflow_semantic_version = None; + let DiagnosticEventPayload::SchedulerModelLifecycleChanged(payload) = &mut event.payload else { + panic!("model lifecycle payload"); + }; + payload.workflow_execution_session_id = Some("session-target".to_string()); + payload.unloaded_workflow_execution_session_id = Some("session-victim".to_string()); + payload.transition = SchedulerModelLifecycleTransition::UnloadCompleted; + payload.duration_ms = Some(7); + let recorded = ledger + .append_diagnostic_event(event) + .expect("session event"); + assert!(recorded.workflow_run_id.is_none()); + let payload: serde_json::Value = serde_json::from_str(&recorded.payload_json).expect("payload"); + assert_eq!(payload["workflow_execution_session_id"], "session-target"); + ledger + .drain_run_list_projection(100) + .expect("list projection"); + ledger + .drain_run_detail_projection(100) + .expect("detail projection"); + assert!(ledger + .query_run_list_projection(RunListProjectionQuery::default()) + .expect("runs") + .is_empty()); +} + +#[test] +fn session_capacity_identity_does_not_relax_legacy_run_event_requirements() { + let mut event = sample_scheduler_model_lifecycle_event("run-real"); + let original = serde_json::to_value(&event.payload).expect("legacy payload"); + let decoded: DiagnosticEventPayload = + serde_json::from_value(original.clone()).expect("old payload parses"); + assert_eq!(serde_json::to_value(decoded).expect("round trip"), original); + event.workflow_run_id = None; + assert!(event.validate().is_err()); + let DiagnosticEventPayload::SchedulerModelLifecycleChanged(payload) = &mut event.payload else { + panic!("model lifecycle payload"); + }; + payload.workflow_execution_session_id = Some("session-target".to_string()); + assert!(event.validate().is_err()); + let DiagnosticEventPayload::SchedulerModelLifecycleChanged(payload) = &mut event.payload else { + panic!("model lifecycle payload"); + }; + payload.unloaded_workflow_execution_session_id = Some(" ".to_string()); + assert!(event.validate().is_err()); + let mut run_event = sample_run_started_event("run-real"); + run_event.workflow_run_id = None; + assert!(run_event.validate().is_err()); +} + +#[test] +fn session_runtime_error_requires_genuine_session_and_workflow_identity() { + let mut event = sample_diagnostic_error_event("unused"); + event.workflow_run_id = None; + event.source_component = DiagnosticEventSourceComponent::Scheduler; + let DiagnosticEventPayload::DiagnosticErrorOccurred(payload) = &mut event.payload else { + panic!("error payload"); + }; + payload.scope = DiagnosticErrorScopeKind::SessionRuntime; + payload.severity = DiagnosticErrorSeverity::Error; + assert!(event.validate().is_err()); + let DiagnosticEventPayload::DiagnosticErrorOccurred(payload) = &mut event.payload else { + panic!("error payload"); + }; + payload.workflow_execution_session_id = Some("session-target".to_string()); + event.validate().expect("session-scoped error"); + event.workflow_id = None; + assert!(event.validate().is_err()); +} diff --git a/crates/pantograph-workflow-service/src/workflow/diagnostic_errors.rs b/crates/pantograph-workflow-service/src/workflow/diagnostic_errors.rs index 3b7213c48..9499b2eff 100644 --- a/crates/pantograph-workflow-service/src/workflow/diagnostic_errors.rs +++ b/crates/pantograph-workflow-service/src/workflow/diagnostic_errors.rs @@ -20,6 +20,7 @@ use super::{WorkflowErrorDiagnosticsLink, WorkflowService, WorkflowServiceError} pub(crate) enum WorkflowDiagnosticErrorPhase { RunSnapshot, SchedulerAdmission, + SessionRuntimeAdmission, RuntimePreflight, RuntimeModelLoad, RuntimeLaunch, @@ -77,6 +78,18 @@ const WORKFLOW_SERVICE_SOURCE: &[DiagnosticEventSourceComponent] = &[DiagnosticEventSourceComponent::WorkflowService]; const WORKFLOW_DIAGNOSTIC_ERROR_REGISTRY: &[WorkflowDiagnosticErrorRegistryEntry] = &[ + WorkflowDiagnosticErrorRegistryEntry { + phase: WorkflowDiagnosticErrorPhase::SessionRuntimeAdmission, + phase_id: "session_runtime_admission", + code: "session_runtime_admission_failed", + scope_kind: DiagnosticErrorScopeKind::SessionRuntime, + default_source: DiagnosticEventSourceComponent::Scheduler, + allowed_sources: SCHEDULER_SOURCE, + default_severity: DiagnosticErrorSeverity::Error, + default_recoverability: DiagnosticErrorRecoverability::Retryable, + causality_policy: WorkflowDiagnosticCausalityPolicy::DirectProducerKnowledgeOnly, + projection_effect: WorkflowDiagnosticProjectionEffect::DiagnosticsOnly, + }, WorkflowDiagnosticErrorRegistryEntry { phase: WorkflowDiagnosticErrorPhase::RunSnapshot, phase_id: "run_snapshot", @@ -304,8 +317,16 @@ pub(crate) struct WorkflowDiagnosticTransportScope { pub workflow_id: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct WorkflowDiagnosticSessionRuntimeScope { + pub session_id: String, + pub workflow_id: WorkflowId, + pub workflow_run_id: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum WorkflowDiagnosticErrorScope { + SessionRuntime(WorkflowDiagnosticSessionRuntimeScope), Run(WorkflowDiagnosticRunScope), Node(WorkflowDiagnosticNodeScope), RuntimeModel(WorkflowDiagnosticRuntimeModelScope), @@ -318,6 +339,7 @@ pub(crate) enum WorkflowDiagnosticErrorScope { impl WorkflowDiagnosticErrorScope { fn kind(&self) -> DiagnosticErrorScopeKind { match self { + Self::SessionRuntime(_) => DiagnosticErrorScopeKind::SessionRuntime, Self::Run(_) => DiagnosticErrorScopeKind::Run, Self::Node(_) => DiagnosticErrorScopeKind::Node, Self::RuntimeModel(_) => DiagnosticErrorScopeKind::RuntimeModel, @@ -330,6 +352,16 @@ impl WorkflowDiagnosticErrorScope { fn append_request_fields(&self, request: &mut DiagnosticEventAppendRequest) { match self { + Self::SessionRuntime(scope) => { + request.workflow_id = Some(scope.workflow_id.clone()); + request.workflow_run_id = scope.workflow_run_id.clone(); + request.scheduler_policy_id = Some("priority_then_fifo".to_string()); + if let DiagnosticEventPayload::DiagnosticErrorOccurred(payload) = + &mut request.payload + { + payload.workflow_execution_session_id = Some(scope.session_id.clone()); + } + } Self::Run(scope) => apply_run_context(request, &scope.run), Self::Node(scope) => { apply_run_context(request, &scope.run); @@ -420,6 +452,18 @@ pub(crate) struct WorkflowDiagnosticErrorRecordRequest { } impl WorkflowDiagnosticErrorRecordRequest { + #[track_caller] + pub(crate) fn session_runtime_admission_failed( + scope: WorkflowDiagnosticSessionRuntimeScope, + error: &WorkflowServiceError, + ) -> Self { + Self::from_error( + WorkflowDiagnosticErrorPhase::SessionRuntimeAdmission, + WorkflowDiagnosticErrorScope::SessionRuntime(scope), + error, + ) + } + #[track_caller] pub(crate) fn run_snapshot_failed( scope: WorkflowDiagnosticRunScope, @@ -693,6 +737,7 @@ impl WorkflowService { payload_ref: None, payload: DiagnosticEventPayload::DiagnosticErrorOccurred( DiagnosticErrorOccurredPayload { + workflow_execution_session_id: None, phase: registry_entry.phase_id.to_string(), scope: registry_entry.scope_kind, severity: request.severity.unwrap_or(registry_entry.default_severity), diff --git a/crates/pantograph-workflow-service/src/workflow/session_runtime.rs b/crates/pantograph-workflow-service/src/workflow/session_runtime.rs index 5a5db1fd1..e99db3310 100644 --- a/crates/pantograph-workflow-service/src/workflow/session_runtime.rs +++ b/crates/pantograph-workflow-service/src/workflow/session_runtime.rs @@ -1,16 +1,20 @@ use pantograph_diagnostics_ledger::{ - DiagnosticEventAppendRequest, DiagnosticEventPayload, DiagnosticEventPrivacyClass, - DiagnosticEventRetentionClass, DiagnosticEventSourceComponent, SchedulerModelCacheState, - SchedulerModelLifecycleChangedPayload, SchedulerModelLifecycleTransition, + sanitize_diagnostic_error_text, DiagnosticEventAppendRequest, DiagnosticEventPayload, + DiagnosticEventPrivacyClass, DiagnosticEventRetentionClass, DiagnosticEventSourceComponent, + SchedulerModelCacheState, SchedulerModelLifecycleChangedPayload, + SchedulerModelLifecycleTransition, MAX_DIAGNOSTIC_ERROR_TEXT_LEN, }; use pantograph_runtime_attribution::{ - BucketId, ClientId, ClientSessionId, WorkflowId, WorkflowRunId, WorkflowRunSnapshotRecord, + BucketId, ClientId, ClientSessionId, WorkflowId, WorkflowRunSnapshotRecord, }; use pantograph_timing_contracts::{checked_timing_duration_ms, WorkflowTimingAttemptId}; use crate::scheduler::WorkflowExecutionSessionPreflightCache; use crate::technical_fit::WorkflowTechnicalFitOverride; +use super::diagnostic_errors::{ + WorkflowDiagnosticErrorRecordRequest, WorkflowDiagnosticSessionRuntimeScope, +}; use super::{ WorkflowExecutionSessionRetentionHint, WorkflowExecutionSessionRuntimeSelectionTarget, WorkflowExecutionSessionRuntimeUnloadCandidate, WorkflowExecutionSessionSummary, @@ -49,8 +53,6 @@ fn workflow_timing_duration_ms( pub(super) struct WorkflowSessionRuntimeAdmissionDiagnosticContext<'a> { pub(super) session: &'a WorkflowExecutionSessionSummary, pub(super) snapshot: Option<&'a WorkflowRunSnapshotRecord>, - pub(super) workflow_run_id: &'a str, - pub(super) workflow_semantic_version: &'a str, } struct CapacityRebalanceModelLifecycleEventRequest<'a> { @@ -162,7 +164,8 @@ impl WorkflowService { duration_ms: None, error: None, }, - )?; + ) + .map_err(lifecycle_diagnostic_failure)?; self.record_capacity_rebalance_model_lifecycle_events_if_configured( CapacityRebalanceModelLifecycleEventRequest { context, @@ -173,7 +176,8 @@ impl WorkflowService { duration_ms: None, error: None, }, - )?; + ) + .map_err(lifecycle_diagnostic_failure)?; } let unload_started_at_ms = crate::scheduler::unix_timestamp_ms(); let unload_result = host @@ -183,49 +187,55 @@ impl WorkflowService { WorkflowExecutionSessionUnloadReason::CapacityRebalance, ) .await; + // Host residency is authoritative even if subsequent telemetry fails. + if unload_result.is_ok() { + self.session_store_guard()? + .mark_runtime_loaded(&candidate.session_id, false)?; + } let unload_duration_ms = workflow_timing_duration_ms( &unload_timing_attempt_id, unload_started_at_ms, crate::scheduler::unix_timestamp_ms(), )?; - if let Some(context) = diagnostics_context.as_ref() { - match &unload_result { - Ok(()) => { - self.record_capacity_rebalance_model_lifecycle_events_if_configured( - CapacityRebalanceModelLifecycleEventRequest { - context, - candidate: &candidate, - transition: - SchedulerModelLifecycleTransition::UnloadCompleted, - timing_attempt_id: Some(unload_timing_attempt_id.as_str()), - reason: "capacity rebalance unloaded selected session", - duration_ms: Some(unload_duration_ms), - error: None, - }, - )?; - } - Err(error) => { - let error_text = error.to_string(); - let _diagnostic_result = self - .record_capacity_rebalance_model_lifecycle_events_if_configured( - CapacityRebalanceModelLifecycleEventRequest { - context, - candidate: &candidate, - transition: SchedulerModelLifecycleTransition::UnloadFailed, - timing_attempt_id: Some(unload_timing_attempt_id.as_str()), - reason: - "capacity rebalance failed to unload selected session", - duration_ms: Some(unload_duration_ms), - error: Some(error_text.as_str()), - }, - ); - } + let terminal_diagnostics = if let Some(context) = diagnostics_context.as_ref() { + let error_text = unload_result.as_ref().err().map(|error| { + sanitize_diagnostic_error_text( + &error.to_string(), + MAX_DIAGNOSTIC_ERROR_TEXT_LEN, + ) + }); + self.record_capacity_rebalance_model_lifecycle_events_if_configured( + CapacityRebalanceModelLifecycleEventRequest { + context, + candidate: &candidate, + transition: if unload_result.is_ok() { + SchedulerModelLifecycleTransition::UnloadCompleted + } else { + SchedulerModelLifecycleTransition::UnloadFailed + }, + timing_attempt_id: Some(unload_timing_attempt_id.as_str()), + reason: if unload_result.is_ok() { + "capacity rebalance unloaded selected session" + } else { + "capacity rebalance failed to unload selected session" + }, + duration_ms: Some(unload_duration_ms), + error: error_text.as_deref(), + }, + ) + } else { + Ok(()) + }; + match unload_result { + Ok(()) => terminal_diagnostics.map_err(lifecycle_diagnostic_failure)?, + Err(error) => { + return Err(match terminal_diagnostics { + Ok(()) => error, + Err(recording_error) => error + .with_diagnostics(lifecycle_diagnostic_link(&recording_error)), + }) } } - unload_result?; - if let Ok(mut store) = self.session_store.lock() { - let _ = store.mark_runtime_loaded(&candidate.session_id, false); - } } RuntimeDecision::LoadTarget { workflow_id, @@ -260,7 +270,10 @@ impl WorkflowService { return Ok(()); } - let workflow_run_id = WorkflowRunId::try_from(request.context.workflow_run_id.to_string())?; + let workflow_run_id = request + .context + .snapshot + .map(|snapshot| snapshot.workflow_run_id.clone()); let workflow_id = workflow_id_for_runtime_admission_event( request.context.session, request.context.snapshot, @@ -276,21 +289,16 @@ impl WorkflowService { source_component: DiagnosticEventSourceComponent::Scheduler, source_instance_id: Some("workflow-session-scheduler".to_string()), occurred_at_ms: crate::scheduler::unix_timestamp_ms() as i64, - workflow_run_id: Some(workflow_run_id.clone()), + workflow_run_id: workflow_run_id.clone(), workflow_id: Some(workflow_id.clone()), workflow_version_id: request .context .snapshot .map(|snapshot| snapshot.workflow_version_id.clone()), - workflow_semantic_version: Some( - request - .context - .snapshot - .map(|snapshot| snapshot.workflow_semantic_version.clone()) - .unwrap_or_else(|| { - request.context.workflow_semantic_version.to_string() - }), - ), + workflow_semantic_version: request + .context + .snapshot + .map(|snapshot| snapshot.workflow_semantic_version.clone()), node_id: None, node_type: None, node_version: None, @@ -320,6 +328,12 @@ impl WorkflowService { payload_ref: None, payload: DiagnosticEventPayload::SchedulerModelLifecycleChanged( SchedulerModelLifecycleChangedPayload { + workflow_execution_session_id: Some( + request.context.session.session_id.clone(), + ), + unloaded_workflow_execution_session_id: Some( + request.candidate.session_id.clone(), + ), transition: request.transition, cache_state: Some(SchedulerModelCacheState::for_lifecycle_transition( request.transition, @@ -407,8 +421,51 @@ impl WorkflowService { super::format_runtime_not_ready_message(&cache.blocking_runtime_issues), )); } - self.ensure_session_runtime_loaded(host, session_id, None) - .await + let session = self.session_store_guard()?.session_summary(session_id)?; + let result = self + .ensure_session_runtime_loaded( + host, + session_id, + Some(WorkflowSessionRuntimeAdmissionDiagnosticContext { + session: &session, + snapshot: None, + }), + ) + .await; + match result { + Ok(()) => Ok(()), + Err(error) => { + let outcome = self.record_workflow_diagnostic_error_if_configured( + WorkflowDiagnosticErrorRecordRequest::session_runtime_admission_failed( + WorkflowDiagnosticSessionRuntimeScope { + session_id: session.session_id.clone(), + workflow_id: WorkflowId::try_from(session.workflow_id.clone())?, + workflow_run_id: None, + }, + &error, + ), + ); + let prior_unavailable = error + .diagnostics() + .and_then(|link| link.diagnostics_unavailable.clone()); + let mut diagnostics = match outcome { + Ok(outcome) => outcome.into_error_link(None::), + Err(recording_error) => super::WorkflowErrorDiagnosticsLink { + workflow_run_id: None, + diagnostic_event_id: None, + diagnostics_unavailable: Some(recording_error.message().to_string()), + }, + }; + if let Some(prior) = prior_unavailable { + diagnostics.diagnostics_unavailable = + Some(match diagnostics.diagnostics_unavailable { + Some(current) if current != prior => format!("{prior}; {current}"), + _ => prior, + }); + } + Err(error.with_diagnostics(diagnostics)) + } + } } pub(super) async fn refresh_session_runtime_affinity_basis( @@ -486,3 +543,19 @@ fn runtime_event_bucket_id( .map_err(WorkflowServiceError::from), } } + +fn lifecycle_diagnostic_link(error: &WorkflowServiceError) -> super::WorkflowErrorDiagnosticsLink { + super::WorkflowErrorDiagnosticsLink { + workflow_run_id: None, + diagnostic_event_id: None, + diagnostics_unavailable: Some(format!( + "capacity lifecycle event unavailable: {}", + sanitize_diagnostic_error_text(error.message(), MAX_DIAGNOSTIC_ERROR_TEXT_LEN) + )), + } +} + +fn lifecycle_diagnostic_failure(error: WorkflowServiceError) -> WorkflowServiceError { + let link = lifecycle_diagnostic_link(&error); + error.with_diagnostics(link) +} diff --git a/crates/pantograph-workflow-service/src/workflow/tests.rs b/crates/pantograph-workflow-service/src/workflow/tests.rs index ac76e59c2..d3a6fecbe 100644 --- a/crates/pantograph-workflow-service/src/workflow/tests.rs +++ b/crates/pantograph-workflow-service/src/workflow/tests.rs @@ -23,6 +23,7 @@ mod scheduler_snapshot; mod scheduler_snapshot_diagnostics; mod session_admission; mod session_capacity; +mod session_capacity_faults; mod session_capacity_limits; mod session_execution; mod session_queue; diff --git a/crates/pantograph-workflow-service/src/workflow/tests/diagnostics.rs b/crates/pantograph-workflow-service/src/workflow/tests/diagnostics.rs index 588aa7aa3..19b542165 100644 --- a/crates/pantograph-workflow-service/src/workflow/tests/diagnostics.rs +++ b/crates/pantograph-workflow-service/src/workflow/tests/diagnostics.rs @@ -408,12 +408,13 @@ fn workflow_diagnostic_error_recorder_validates_registered_scope() { fn workflow_diagnostic_error_registry_declares_phase_contracts() { let registry = registered_workflow_diagnostic_error_phases(); - assert_eq!(registry.len(), 13); + assert_eq!(registry.len(), 14); for entry in registry { assert!(!entry.phase_id.starts_with("inference")); assert!(!entry.code.starts_with("inference")); } for phase_id in [ + "session_runtime_admission", "runtime_preflight", "runtime_model_load", "runtime_launch", @@ -426,6 +427,22 @@ fn workflow_diagnostic_error_registry_declares_phase_contracts() { "missing canonical diagnostic error phase {phase_id}" ); } + let session_runtime = registry + .iter() + .find(|entry| entry.phase == WorkflowDiagnosticErrorPhase::SessionRuntimeAdmission) + .expect("session runtime phase"); + assert_eq!( + session_runtime.scope_kind, + pantograph_diagnostics_ledger::DiagnosticErrorScopeKind::SessionRuntime + ); + assert_eq!( + session_runtime.projection_effect, + WorkflowDiagnosticProjectionEffect::DiagnosticsOnly + ); + assert_eq!( + session_runtime.default_severity, + pantograph_diagnostics_ledger::DiagnosticErrorSeverity::Error + ); let runtime_model_load = registry .iter() .find(|entry| entry.phase == WorkflowDiagnosticErrorPhase::RuntimeModelLoad) diff --git a/crates/pantograph-workflow-service/src/workflow/tests/session_capacity.rs b/crates/pantograph-workflow-service/src/workflow/tests/session_capacity.rs index ba220a464..db7001255 100644 --- a/crates/pantograph-workflow-service/src/workflow/tests/session_capacity.rs +++ b/crates/pantograph-workflow-service/src/workflow/tests/session_capacity.rs @@ -43,20 +43,26 @@ async fn workflow_execution_session_capacity_rebalance_uses_host_selected_candid let selecting_host = SelectingRuntimeHost::new(second.session_id.clone(), unloads.clone()); service - .run_workflow_execution_session( + .workflow_set_execution_session_keep_alive( &selecting_host, - WorkflowExecutionSessionRunRequest { + WorkflowExecutionSessionKeepAliveRequest { session_id: third_session_id.clone(), - workflow_semantic_version: "0.1.0".to_string(), - inputs: Vec::new(), - output_targets: None, - override_selection: None, - timeout_ms: None, - priority: None, + keep_alive: true, }, ) .await - .expect("run third session"); + .expect("admit target keep-alive runtime"); + + service + .workflow_set_execution_session_keep_alive( + &selecting_host, + WorkflowExecutionSessionKeepAliveRequest { + session_id: third_session_id.clone(), + keep_alive: false, + }, + ) + .await + .expect("disable target keep-alive and unload runtime"); let unloads = unloads.lock().expect("unloads lock poisoned"); assert_eq!( @@ -121,27 +127,37 @@ async fn workflow_execution_session_capacity_rebalance_preserves_unload_error_wh .expect("create target session"); let error = service - .run_workflow_execution_session( + .workflow_set_execution_session_keep_alive( &host, - WorkflowExecutionSessionRunRequest { - session_id: target.session_id, - workflow_semantic_version: "0.1.0".to_string(), - inputs: Vec::new(), - output_targets: None, - override_selection: None, - timeout_ms: None, - priority: None, + WorkflowExecutionSessionKeepAliveRequest { + session_id: target.session_id.clone(), + keep_alive: true, }, ) .await .expect_err("capacity rebalance should preserve runtime unload failure"); + let state = service + .session_store_guard() + .expect("session store") + .session_summary(&target.session_id) + .expect("target still exists"); + assert!(!state.keep_alive); + assert_eq!(state.state, WorkflowExecutionSessionState::IdleUnloaded); + assert_eq!( + service + .session_store_guard() + .expect("store") + .loaded_session_count(), + 2 + ); assert_eq!(error.code(), WorkflowErrorCode::RuntimeNotReady); assert!(error.message().contains("runtime unload failed")); assert!(!error.message().contains("diagnostics ledger lock poisoned")); let diagnostics = error .diagnostics() .expect("diagnostics unavailable link should be attached"); + assert!(diagnostics.workflow_run_id.is_none()); assert!(diagnostics.diagnostic_event_id.is_none()); assert!(diagnostics .diagnostics_unavailable @@ -191,20 +207,26 @@ async fn workflow_execution_session_capacity_rebalance_preserves_affine_idle_run .expect("create target session"); service - .run_workflow_execution_session( + .workflow_set_execution_session_keep_alive( &host, - WorkflowExecutionSessionRunRequest { + WorkflowExecutionSessionKeepAliveRequest { session_id: target.session_id.clone(), - workflow_semantic_version: "0.1.0".to_string(), - inputs: Vec::new(), - output_targets: None, - override_selection: None, - timeout_ms: None, - priority: None, + keep_alive: true, + }, + ) + .await + .expect("admit target keep-alive runtime"); + + service + .workflow_set_execution_session_keep_alive( + &host, + WorkflowExecutionSessionKeepAliveRequest { + session_id: target.session_id.clone(), + keep_alive: false, }, ) .await - .expect("run target session"); + .expect("disable target keep-alive and unload runtime"); let unloads = unloads.lock().expect("unloads lock poisoned"); assert_eq!( @@ -273,20 +295,26 @@ async fn workflow_execution_session_capacity_rebalance_preserves_shared_model_id .expect("create target session"); service - .run_workflow_execution_session( + .workflow_set_execution_session_keep_alive( &host, - WorkflowExecutionSessionRunRequest { + WorkflowExecutionSessionKeepAliveRequest { session_id: target.session_id.clone(), - workflow_semantic_version: "0.1.0".to_string(), - inputs: Vec::new(), - output_targets: None, - override_selection: None, - timeout_ms: None, - priority: None, + keep_alive: true, }, ) .await - .expect("run target session"); + .expect("admit target keep-alive runtime"); + + service + .workflow_set_execution_session_keep_alive( + &host, + WorkflowExecutionSessionKeepAliveRequest { + session_id: target.session_id.clone(), + keep_alive: false, + }, + ) + .await + .expect("disable target keep-alive and unload runtime"); let unloads = unloads.lock().expect("unloads lock poisoned"); assert_eq!( @@ -341,6 +369,17 @@ async fn workflow_execution_session_capacity_rebalance_preserves_shared_model_id .as_str() .expect("capacity unload timing attempt id"); assert!(timing_attempt_id.starts_with("timing_attempt_")); + for event in &capacity_rebalance_events { + assert!(event.workflow_run_id.is_none()); + let payload: serde_json::Value = + serde_json::from_str(&event.payload_json).expect("payload"); + assert_eq!(payload["workflow_execution_session_id"], target.session_id); + assert_eq!( + payload["unloaded_workflow_execution_session_id"], + other_model.session_id + ); + } + assert!(unload_completed_payload["duration_ms"].as_u64().is_some()); assert!(capacity_rebalance_events[0] .payload_json .contains("\"cache_state\":\"unload_requested\"")); @@ -427,20 +466,26 @@ async fn workflow_execution_session_capacity_rebalance_preserves_shared_backend_ .expect("create target session"); service - .run_workflow_execution_session( + .workflow_set_execution_session_keep_alive( &host, - WorkflowExecutionSessionRunRequest { + WorkflowExecutionSessionKeepAliveRequest { session_id: target.session_id.clone(), - workflow_semantic_version: "0.1.0".to_string(), - inputs: Vec::new(), - output_targets: None, - override_selection: None, - timeout_ms: None, - priority: None, + keep_alive: true, }, ) .await - .expect("run target session"); + .expect("admit target keep-alive runtime"); + + service + .workflow_set_execution_session_keep_alive( + &host, + WorkflowExecutionSessionKeepAliveRequest { + session_id: target.session_id.clone(), + keep_alive: false, + }, + ) + .await + .expect("disable target keep-alive and unload runtime"); let unloads = unloads.lock().expect("unloads lock poisoned"); assert_eq!( @@ -454,3 +499,116 @@ async fn workflow_execution_session_capacity_rebalance_preserves_shared_backend_ .iter() .any(|session_id| session_id == &shared_backend.session_id)); } + +#[tokio::test] +async fn keep_alive_capacity_failure_records_session_error_and_rolls_back() { + let service = WorkflowService::with_capacity_limits(3, 2) + .with_diagnostics_ledger(SqliteDiagnosticsLedger::open_in_memory().expect("ledger")); + let host = SelectingRuntimeHost::new(String::new(), Arc::new(Mutex::new(Vec::new()))); + for workflow_id in ["loaded-a", "loaded-b"] { + service + .create_workflow_execution_session( + &host, + WorkflowExecutionSessionCreateRequest { + workflow_id: workflow_id.to_string(), + usage_profile: None, + keep_alive: true, + }, + ) + .await + .expect("loaded session"); + } + let target = service + .create_workflow_execution_session( + &host, + WorkflowExecutionSessionCreateRequest { + workflow_id: "target".to_string(), + usage_profile: None, + keep_alive: false, + }, + ) + .await + .expect("target session"); + let error = service + .workflow_set_execution_session_keep_alive( + &host, + WorkflowExecutionSessionKeepAliveRequest { + session_id: target.session_id.clone(), + keep_alive: true, + }, + ) + .await + .expect_err("host declined every unload candidate"); + assert_eq!(error.code(), WorkflowErrorCode::SchedulerBusy); + let link = error.diagnostics().expect("diagnostic link"); + assert!(link.workflow_run_id.is_none()); + assert!(link.diagnostic_event_id.is_some()); + assert!(link.diagnostics_unavailable.is_none()); + let state = service + .session_store_guard() + .expect("store") + .session_summary(&target.session_id) + .expect("target retained"); + assert!(!state.keep_alive); + assert_eq!(state.state, WorkflowExecutionSessionState::IdleUnloaded); + let events = { + let ledger = service.diagnostics_ledger_guard().expect("ledger"); + pantograph_diagnostics_ledger::DiagnosticsLedgerRepository::diagnostic_events_after( + &*ledger, 0, 20, + ) + .expect("events") + }; + let event = events + .iter() + .find(|event| Some(&event.event_id) == link.diagnostic_event_id.as_ref()) + .expect("recorded error"); + assert!(event.workflow_run_id.is_none()); + let payload: serde_json::Value = serde_json::from_str(&event.payload_json).expect("payload"); + assert_eq!(payload["phase"], "session_runtime_admission"); + assert_eq!(payload["workflow_execution_session_id"], target.session_id); + assert_eq!(payload["scope"], "session_runtime"); +} + +#[tokio::test] +async fn failed_keep_alive_creation_removes_only_the_new_session() { + let service = WorkflowService::with_capacity_limits(2, 1) + .with_diagnostics_ledger(SqliteDiagnosticsLedger::open_in_memory().expect("ledger")); + let host = SelectingRuntimeHost::new(String::new(), Arc::new(Mutex::new(Vec::new()))); + let existing = service + .create_workflow_execution_session( + &host, + WorkflowExecutionSessionCreateRequest { + workflow_id: "existing".to_string(), + usage_profile: None, + keep_alive: true, + }, + ) + .await + .expect("existing loaded session"); + let error = service + .create_workflow_execution_session( + &host, + WorkflowExecutionSessionCreateRequest { + workflow_id: "rejected".to_string(), + usage_profile: None, + keep_alive: true, + }, + ) + .await + .expect_err("capacity admission rejected"); + assert_eq!(error.code(), WorkflowErrorCode::SchedulerBusy); + assert!(error + .diagnostics() + .expect("error link") + .workflow_run_id + .is_none()); + let store = service.session_store_guard().expect("store"); + assert_eq!(store.active.len(), 1); + assert_eq!(store.loaded_session_count(), 1); + assert!( + store + .session_summary(&existing.session_id) + .expect("existing retained") + .keep_alive + ); +} diff --git a/crates/pantograph-workflow-service/src/workflow/tests/session_capacity_faults.rs b/crates/pantograph-workflow-service/src/workflow/tests/session_capacity_faults.rs new file mode 100644 index 000000000..fc0bde245 --- /dev/null +++ b/crates/pantograph-workflow-service/src/workflow/tests/session_capacity_faults.rs @@ -0,0 +1,337 @@ +use super::*; +use async_trait::async_trait; + +struct CapacityFaultHost { + inner: MockWorkflowHost, + loads: Mutex>, + unloads: Mutex>, + unload_error: Option, +} + +impl CapacityFaultHost { + fn new(unload_error: Option) -> Self { + Self { + inner: MockWorkflowHost::new(16, 4096), + loads: Mutex::new(Vec::new()), + unloads: Mutex::new(Vec::new()), + unload_error, + } + } +} + +#[async_trait] +impl WorkflowHost for CapacityFaultHost { + async fn validate_workflow(&self, id: &str) -> Result<(), WorkflowServiceError> { + self.inner.validate_workflow(id).await + } + async fn workflow_graph_fingerprint(&self, id: &str) -> Result { + self.inner.workflow_graph_fingerprint(id).await + } + async fn workflow_capabilities( + &self, + id: &str, + ) -> Result { + self.inner.workflow_capabilities(id).await + } + async fn runtime_capabilities( + &self, + ) -> Result, WorkflowServiceError> { + self.inner.runtime_capabilities().await + } + async fn load_session_runtime( + &self, + session_id: &str, + _id: &str, + _usage: Option<&str>, + _hint: WorkflowExecutionSessionRetentionHint, + ) -> Result<(), WorkflowServiceError> { + self.loads + .lock() + .expect("loads") + .push(session_id.to_string()); + Ok(()) + } + async fn unload_session_runtime( + &self, + session_id: &str, + _id: &str, + _reason: WorkflowExecutionSessionUnloadReason, + ) -> Result<(), WorkflowServiceError> { + if let Some(error) = &self.unload_error { + return Err(WorkflowServiceError::RuntimeNotReady(error.clone())); + } + self.unloads + .lock() + .expect("unloads") + .push(session_id.to_string()); + Ok(()) + } + async fn run_workflow( + &self, + _id: &str, + _inputs: &[WorkflowPortBinding], + _targets: Option<&[WorkflowOutputTarget]>, + _options: WorkflowRunOptions, + _handle: WorkflowRunHandle, + ) -> Result, WorkflowServiceError> { + panic!("explicit keep-alive must not execute a workflow") + } +} + +async fn assert_completed_append_failure_preserves_residency(enable_existing: bool) { + let temp = tempfile::tempdir().expect("temp ledger"); + let path = temp.path().join("diagnostics.sqlite"); + let service = WorkflowService::with_capacity_limits(2, 1) + .with_diagnostics_ledger(SqliteDiagnosticsLedger::open(&path).expect("ledger")); + let host = CapacityFaultHost::new(None); + let victim = service + .create_workflow_execution_session( + &host, + WorkflowExecutionSessionCreateRequest { + workflow_id: "wf-1".to_string(), + usage_profile: None, + keep_alive: true, + }, + ) + .await + .expect("loaded victim"); + let connection = rusqlite::Connection::open(&path).expect("fault connection"); + connection + .execute_batch( + "CREATE TRIGGER reject_terminal BEFORE INSERT ON diagnostic_events + WHEN json_extract(NEW.payload_json, '$.transition') = 'unload_completed' + BEGIN SELECT RAISE(FAIL, 'injected terminal append failure'); END;", + ) + .expect("fault trigger"); + let target_id; + let error = if enable_existing { + let target = service + .create_workflow_execution_session( + &host, + WorkflowExecutionSessionCreateRequest { + workflow_id: "wf-1".to_string(), + usage_profile: None, + keep_alive: false, + }, + ) + .await + .expect("unloaded target"); + target_id = Some(target.session_id.clone()); + service + .workflow_set_execution_session_keep_alive( + &host, + WorkflowExecutionSessionKeepAliveRequest { + session_id: target.session_id, + keep_alive: true, + }, + ) + .await + .expect_err("terminal telemetry failed") + } else { + target_id = None; + service + .create_workflow_execution_session( + &host, + WorkflowExecutionSessionCreateRequest { + workflow_id: "wf-1".to_string(), + usage_profile: None, + keep_alive: true, + }, + ) + .await + .expect_err("terminal telemetry failed") + }; + let link = error.diagnostics().expect("explicit telemetry failure"); + assert!( + link.diagnostic_event_id.is_some(), + "canonical error is independently recorded" + ); + assert!(link + .diagnostics_unavailable + .as_deref() + .expect("lifecycle gap") + .contains("injected terminal append failure")); + assert!(link.workflow_run_id.is_none()); + { + let store = service.session_store_guard().expect("store"); + assert_eq!(store.loaded_session_count(), 0); + assert_eq!( + store + .session_summary(&victim.session_id) + .expect("victim") + .state, + WorkflowExecutionSessionState::IdleUnloaded + ); + assert_eq!(store.active.len(), if enable_existing { 2 } else { 1 }); + if let Some(id) = target_id { + let target = store.session_summary(&id).expect("target"); + assert!(!target.keep_alive); + assert_eq!(target.state, WorkflowExecutionSessionState::IdleUnloaded); + } + } + assert_eq!( + *host.unloads.lock().expect("unloads"), + vec![victim.session_id.clone()] + ); + connection + .execute_batch("DROP TRIGGER reject_terminal") + .expect("remove fault"); + service + .workflow_set_execution_session_keep_alive( + &host, + WorkflowExecutionSessionKeepAliveRequest { + session_id: victim.session_id.clone(), + keep_alive: true, + }, + ) + .await + .expect("recovery reloads unloaded victim"); + assert_eq!( + *host.loads.lock().expect("loads"), + vec![victim.session_id.clone(), victim.session_id] + ); + assert_eq!( + service + .session_store_guard() + .expect("store") + .loaded_session_count(), + 1 + ); +} + +#[tokio::test] +async fn keep_alive_creation_terminal_append_failure_preserves_residency_and_recovery() { + assert_completed_append_failure_preserves_residency(false).await; +} + +#[tokio::test] +async fn keep_alive_enablement_terminal_append_failure_preserves_residency_and_recovery() { + assert_completed_append_failure_preserves_residency(true).await; +} + +async fn assert_failed_unload_reports_terminal_diagnostics(reject_terminal: bool) { + let temp = tempfile::tempdir().expect("temp ledger"); + let path = temp.path().join("diagnostics.sqlite"); + let service = WorkflowService::with_capacity_limits(2, 1) + .with_diagnostics_ledger(SqliteDiagnosticsLedger::open(&path).expect("ledger")); + let message = "unload refused\nworker still busy"; + let host = CapacityFaultHost::new(Some(message.to_string())); + let victim = service + .create_workflow_execution_session( + &host, + WorkflowExecutionSessionCreateRequest { + workflow_id: "wf-1".to_string(), + usage_profile: None, + keep_alive: true, + }, + ) + .await + .expect("loaded victim"); + let target = service + .create_workflow_execution_session( + &host, + WorkflowExecutionSessionCreateRequest { + workflow_id: "wf-1".to_string(), + usage_profile: None, + keep_alive: false, + }, + ) + .await + .expect("unloaded target"); + if reject_terminal { + rusqlite::Connection::open(&path) + .expect("fault connection") + .execute_batch( + "CREATE TRIGGER reject_failed BEFORE INSERT ON diagnostic_events + WHEN json_extract(NEW.payload_json, '$.transition') = 'unload_failed' + BEGIN SELECT RAISE(FAIL, 'injected failed-event append failure'); END;", + ) + .expect("fault trigger"); + } + let error = service + .workflow_set_execution_session_keep_alive( + &host, + WorkflowExecutionSessionKeepAliveRequest { + session_id: target.session_id.clone(), + keep_alive: true, + }, + ) + .await + .expect_err("host unload failure"); + assert_eq!(error.code(), WorkflowErrorCode::RuntimeNotReady); + assert_eq!(error.message(), message); + let link = error.diagnostics().expect("canonical error link"); + assert!(link.workflow_run_id.is_none()); + assert!(link.diagnostic_event_id.is_some()); + if reject_terminal { + assert!(link + .diagnostics_unavailable + .as_deref() + .expect("terminal event gap") + .contains("injected failed-event append failure")); + } else { + assert!(link.diagnostics_unavailable.is_none()); + } + let store = service.session_store_guard().expect("store"); + assert_eq!(store.loaded_session_count(), 1); + assert_eq!( + store + .session_summary(&victim.session_id) + .expect("victim") + .state, + WorkflowExecutionSessionState::IdleLoaded + ); + assert!( + !store + .session_summary(&target.session_id) + .expect("target") + .keep_alive + ); + drop(store); + let ledger = service.diagnostics_ledger_guard().expect("ledger"); + let events = + pantograph_diagnostics_ledger::DiagnosticsLedgerRepository::diagnostic_events_after( + &*ledger, 0, 20, + ) + .expect("events"); + let lifecycle = events.iter().filter(|event| event.event_kind == pantograph_diagnostics_ledger::DiagnosticEventKind::SchedulerModelLifecycleChanged).collect::>(); + assert_eq!(lifecycle.len(), if reject_terminal { 2 } else { 3 }); + let payloads = lifecycle + .iter() + .map(|event| { + assert!(event.workflow_run_id.is_none()); + serde_json::from_str::(&event.payload_json).expect("payload") + }) + .collect::>(); + assert_eq!(payloads[0]["transition"], "unload_scheduled"); + assert_eq!(payloads[1]["transition"], "unload_started"); + assert_eq!( + payloads[0]["timing_attempt_id"], + payloads[1]["timing_attempt_id"] + ); + if !reject_terminal { + assert_eq!(payloads[2]["transition"], "unload_failed"); + assert_eq!( + payloads[2]["timing_attempt_id"], + payloads[0]["timing_attempt_id"] + ); + assert!(!payloads[2]["error"] + .as_str() + .expect("sanitized host error") + .contains('\n')); + assert!(payloads[2]["error"] + .as_str() + .expect("error") + .contains("worker still busy")); + } +} + +#[tokio::test] +async fn multiline_unload_error_records_complete_lifecycle_with_healthy_ledger() { + assert_failed_unload_reports_terminal_diagnostics(false).await; +} + +#[tokio::test] +async fn terminal_unload_failure_is_reported_even_when_canonical_error_records() { + assert_failed_unload_reports_terminal_diagnostics(true).await; +} diff --git a/docs/headless-workflow.md b/docs/headless-workflow.md index 30f45cff0..59ba95d05 100644 --- a/docs/headless-workflow.md +++ b/docs/headless-workflow.md @@ -76,3 +76,32 @@ must decode runtime input rather than relying on static-language assertions. Packaging, platform support, and binding compatibility are not yet established as release-grade claims. See [Release](release.md) and the current audit. + +### Session capacity and keep-alive diagnostics + +Explicit keep-alive creation and enablement own session-count capacity eviction. +The host may choose an idle victim; the default policy preserves workflow, +model and backend affinity. Disabling keep-alive or closing the session performs +explicit cleanup. Ordinary non-runtime runs do not load a session runtime, and +runtime-task resource reservations remain a separate admission contract. + +Capacity eviction events carry `workflow_execution_session_id` for the target +and `unloaded_workflow_execution_session_id` for the victim, plus the existing +model/runtime facts and shared timing-attempt ID. Session-only operations leave +run attribution absent and do not create run projections. Historical run-scoped +payloads retain their existing decoding and required run identity. + +A failed session-runtime admission records the `session_runtime_admission` error +phase under `session_runtime` scope. The returned error retains the operational +code and message; its diagnostic link identifies the recorded error or explains +why diagnostics were unavailable. No workflow-run ID is synthesized. Failed +keep-alive enablement rolls back the flag; failed keep-alive creation removes +the new session. A failed victim unload does not mark that victim unloaded. + +Once the host confirms an eviction, session residency is updated before terminal +telemetry is recorded. If that recording fails, admission reports the diagnostic +failure and the caller rolls back its target request, but the evicted session +remains correctly unloaded and can be reloaded on retry. An unload failure leaves +the victim loaded. Missing lifecycle telemetry is reported even when recording +the separate canonical error succeeds; lifecycle error text is sanitized without +changing the original operational error returned to the caller. diff --git a/docs/plans/domain-architecture-and-multimodal/plan.md b/docs/plans/domain-architecture-and-multimodal/plan.md index 1cd409f3c..2b7c5755a 100644 --- a/docs/plans/domain-architecture-and-multimodal/plan.md +++ b/docs/plans/domain-architecture-and-multimodal/plan.md @@ -616,6 +616,43 @@ No arbitrary file-size target, new crate quota, or mandatory universal abstracti **Status:** `Active` +### CI-01 — Session Capacity Observability Repair + +**Decision (2026-10-02):** Explicit keep-alive creation/enablement owns the +existing session-count capacity limit, host-selected eviction and affinity. +Runtime-task dispatch continues to own dependency admission and resource +reservations; non-runtime runs must not acquire a session runtime. The five +legacy capacity tests reached the wrong owner after this split. Moving only +their trigger would lose previously asserted eviction diagnostics and timing: +`session_runtime.rs` currently receives no diagnostic context from its sole +keep-alive caller, while that context requires a run that does not exist. + +**Admitted repair:** Emit eviction lifecycle facts with genuine target and +unloaded execution-session identities and optional real run attribution. Extend +the existing typed diagnostic payloads additively, retaining old payload decoding +and validation of run-only events. Add a registered session-runtime error scope +so an unload failure retains its original code/message and reports diagnostic +unavailability without inventing a workflow run. Preserve host selection, +rollback, cleanup, event order and a shared timing-attempt identity. Session-only +facts belong in the ledger; they must not create fabricated run projections. + +**Write set:** workflow-service `session_runtime.rs`, `diagnostic_errors.rs`, +`tests/session_capacity.rs`, `tests/session_capacity_faults.rs`, test module +registration and focused diagnostic tests if needed; diagnostics +ledger `event.rs` and `tests.rs`; this plan, its scoped report, and the owning +`docs/headless-workflow.md` guide. No runtime-task admission, resource reservation, +non-runtime execution, public run facade, or unrelated baseline repair changes. +The integrator owns this decision and independent review. Implementation is on +`fix/session-capacity-observability-2026-10-02`, separate from fixture PR #7. + +**Gate:** Direct keep-alive lifecycle tests preserve victim selection, all three +affinity cases, explicit target cleanup, rollback and original unload failure; +available and unavailable diagnostics are tested. Ledger tests accept valid +session-only events, reject missing/blank identity, preserve old run-event +contracts, and demonstrate no fabricated run projection. The non-runtime no-load +regression and focused runtime-dispatch tests remain green. Full CI gaps remain +explicit; independent review precedes integration. + ### M5 — Integrated acceptance and maintainer handoff **Goal:** One honest, current product and compliance result. diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-session-capacity-observability.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-session-capacity-observability.md new file mode 100644 index 000000000..42904028f --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-02-session-capacity-observability.md @@ -0,0 +1,98 @@ +# Session Capacity Observability + +Date: 2026-10-02. Status: `Verifying`. +Authority: [CI-01 session-capacity decision](../plan.md#ci-01--session-capacity-observability-repair). +Branch: `fix/session-capacity-observability-2026-10-02`, based on fixture +PR #7 at `74723b5249ad35404506c6c01ab6c087c8db3b58`. Retain until integrated, +rejected or superseded. The workflow-service owner implements; the domain +integrator independently reviews the admitted decision and final patch. +Coding standards inspected at `MrScripty/Coding-Standards@dcc56f26e884ade260770beceba2501d3746200d`. + +## Source-Grounded Decision + +The five old capacity tests invoked a non-runtime graph and expected session +runtime eviction. Current non-runtime execution deliberately performs no runtime +load. Session-count eviction is owned by explicit keep-alive creation/enablement: +`ensure_keep_alive_session_runtime_ready` calls the existing capacity loader, +which consults the host victim selector and default affinity policy. Runtime-task +dependency admission and resource reservations have separate owners. + +The loader's optional diagnostic context previously required a workflow run, +but its sole caller passed no context. Moving tests to keep-alive without repairing +observability would lose their ordered eviction, timing and failure-diagnostic +assertions. The integrator accepted the session-scoped design before implementation; +the plan was updated first. No obsolete non-runtime loading path is restored. + +## Resulting Contract + +Existing capacity selection, host unload/load calls and affinity remain unchanged. +Keep-alive admission supplies the genuine execution-session summary. Model +lifecycle payloads now optionally identify the target and unloaded session as a +validated pair. Real snapshot run attribution remains optional; session-only +operations have no run ID or semantic run version. Old payloads omit new fields +and retain their former round trip. Other run-only event validation remains strict. + +The registered `session_runtime_admission` error phase uses `session_runtime` +scope and diagnostics-only projection semantics. Error payloads contain the real +execution-session ID. Original operational errors retain their code/message; +recording failures become diagnostic-unavailability detail instead of replacing +the unload error. Existing caller rollback removes a failed newly created session +or reverses failed keep-alive enablement. A failed victim unload leaves its loaded +state intact. Session-only ledger records do not invent run projections. + +The five tests now exercise explicit keep-alive enablement and disablement, +preserving victim choice, workflow/model/backend affinity, target cleanup, +unload error and ordered timing/event assertions. Additional lifecycle tests +cover recorded capacity rejection and rollback. Ledger regressions cover session +identity, strict legacy validation and absent fabricated run projections. + +## Evidence And Remaining Qualification + +- Final fresh Cargo compilation and full library runs: workflow-service 874/874 + and diagnostics-ledger 103/103 passed, zero ignored. This includes all former + capacity assertions, the non-runtime no-load regression, runtime-dispatch tests, + new creation/enablement rollback cases and three ledger scope regressions. +- The diagnostic registry check includes its admitted fourteenth phase and asserts + session scope, error severity and diagnostics-only projection behavior. +- Cargo format, staged whitespace and staged traceability passed. Independent + source review and exact-head hosted qualification remain pending. +- Baseline integration-contract, Rustler, frontend lint, audit and Clippy failures + remain separate. Hosted exact-head CI and applicable review are required; + these focused results do not establish whole-repository merge qualification. + +## Independent Review Repair — 2026-10-03 + +Review of frozen tree `363367538bf09d950e4661903253cc6676459d18` requested two +changes. Successful host unload could be followed by a failed terminal append +before residency changed, leaving a falsely loaded victim. Failed-unload lifecycle +append errors were discarded, including validation failures caused by multiline +host errors, even when separate canonical error recording succeeded. + +The repair commits a successful host residency transition immediately, before +fallible timing/terminal reporting. The existing failed-admission response policy +is retained: target creation/enablement rolls back after a terminal telemetry +failure, while the genuinely unloaded victim stays unloaded and can be reloaded. +Lifecycle text is sanitized; missing lifecycle-event detail survives canonical +error recording and does not replace the original failed-unload code/message. + +Four fault regressions exercise SQLite triggers rejecting only terminal lifecycle +writes, with canonical error recording still healthy: creation and enablement +both verify victim residency and actual reload recovery; a healthy-ledger multiline +host error verifies a complete ordered lifecycle; a selectively rejected failed +terminal event verifies explicit unavailable detail alongside a canonical error ID. +The earlier frozen tree remains available for comparison. Narrow re-review is +required before publication; verification for this repair is recorded below. + +Repair verification: fresh Cargo compilation and 15/15 capacity tests passed. +The freshly built repair binary then passed all 878 workflow-service library +tests, including non-runtime no-load and runtime-dispatch regressions. Ledger +source is unchanged from the previously passing 103/103 run. Formatting, +whitespace and staged traceability passed. The three earlier integration-contract +failures remain unchanged; no new integrated or hosted pass is claimed. + +Independent narrow re-review accepted tree +`260422c5c67332280527781ed13c312683b0143d`: both P1/P2 repairs and their four +fault regressions were inspected; all 15 capacity tests were independently rerun +from the repair binary and passed. No remaining narrow source finding was raised. +This report-only addition records acceptance after that frozen review. Hosted +exact-head qualification, unrelated integration failures and whole CI remain open. From 6a5b134b5a0089c07f2a5a049f58ee24084b7308 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 22:06:27 -0700 Subject: [PATCH 06/65] test(workflow): align headless contracts with scheduler execution --- .../tests/contract.rs | 240 +++++++++++++++--- .../fixtures/run_projection_contract.json | 6 + ...26-10-03-headless-integration-contracts.md | 50 ++++ 3 files changed, 261 insertions(+), 35 deletions(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-headless-integration-contracts.md diff --git a/crates/pantograph-workflow-service/tests/contract.rs b/crates/pantograph-workflow-service/tests/contract.rs index 23b5b6e5f..dc10fffc3 100644 --- a/crates/pantograph-workflow-service/tests/contract.rs +++ b/crates/pantograph-workflow-service/tests/contract.rs @@ -224,32 +224,6 @@ impl WorkflowHost for ContractHost { Ok("contract-graph".to_string()) } - async fn run_workflow( - &self, - _workflow_id: &str, - _inputs: &[WorkflowPortBinding], - output_targets: Option<&[WorkflowOutputTarget]>, - _run_options: pantograph_workflow_service::WorkflowRunOptions, - _run_handle: pantograph_workflow_service::WorkflowRunHandle, - ) -> Result, WorkflowServiceError> { - if let Some(targets) = output_targets { - return Ok(targets - .iter() - .map(|target| WorkflowPortBinding { - node_id: target.node_id.clone(), - port_id: target.port_id.clone(), - value: serde_json::json!([0.1, 0.2, 0.3]), - }) - .collect()); - } - - Ok(vec![WorkflowPortBinding { - node_id: "vector-output-1".to_string(), - port_id: "vector".to_string(), - value: serde_json::json!([0.1, 0.2, 0.3]), - }]) - } - async fn workflow_io( &self, _workflow_id: &str, @@ -287,10 +261,139 @@ impl WorkflowHost for ContractHost { } } +// Execution contracts use supported scheduler tasks and a real graph. The +// metadata-only ContractHost above retains its embedding capability snapshots. +#[derive(Default)] +struct SchedulerContractHost { + graph_reads: std::sync::atomic::AtomicUsize, + io_reads: std::sync::atomic::AtomicUsize, +} + +impl SchedulerContractHost { + fn assert_admission_callbacks(&self) { + use std::sync::atomic::Ordering; + assert!( + self.graph_reads.load(Ordering::SeqCst) > 0, + "graph admission must consult the host" + ); + assert!( + self.io_reads.load(Ordering::SeqCst) > 0, + "I/O admission must consult the host" + ); + } +} + +#[async_trait] +impl WorkflowHost for SchedulerContractHost { + async fn validate_workflow(&self, workflow_id: &str) -> Result<(), WorkflowServiceError> { + assert_eq!(workflow_id, "wf-1"); + Ok(()) + } + + async fn workflow_graph_fingerprint( + &self, + _workflow_id: &str, + ) -> Result { + Ok("contract-text-graph".to_string()) + } + + async fn workflow_graph( + &self, + _workflow_id: &str, + ) -> Result { + self.graph_reads + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + use pantograph_workflow_service::{GraphEdge, GraphNode, Position, WorkflowGraph}; + Ok(WorkflowGraph { + nodes: vec![ + GraphNode { + id: "text-input-1".to_string(), + node_type: "text-input".to_string(), + position: Position { x: 0.0, y: 0.0 }, + data: serde_json::json!({}), + }, + GraphNode { + id: "text-output-1".to_string(), + node_type: "text-output".to_string(), + position: Position { x: 100.0, y: 0.0 }, + data: serde_json::json!({}), + }, + ], + edges: vec![GraphEdge { + id: "edge".to_string(), + source: "text-input-1".to_string(), + source_handle: "text".to_string(), + target: "text-output-1".to_string(), + target_handle: "text".to_string(), + }], + derived_graph: None, + }) + } + + async fn workflow_capabilities( + &self, + _workflow_id: &str, + ) -> Result { + Ok(WorkflowHostCapabilities { + max_input_bindings: 32, + max_output_targets: 8, + max_value_bytes: 4096, + runtime_requirements: WorkflowRuntimeRequirements::default(), + models: Vec::new(), + runtime_capabilities: Vec::new(), + }) + } + + async fn workflow_io( + &self, + workflow_id: &str, + ) -> Result { + self.io_reads + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + let mut io = ContractHost.workflow_io(workflow_id).await?; + io.outputs = vec![WorkflowIoNode { + node_id: "text-output-1".to_string(), + node_type: "text-output".to_string(), + name: Some("Text Output".to_string()), + description: None, + ports: vec![WorkflowIoPort { + port_id: "text".to_string(), + name: Some("Text".to_string()), + description: None, + data_type: Some("string".to_string()), + required: Some(false), + multiple: Some(false), + }], + }]; + Ok(io) + } +} + +#[test] +fn workflow_run_vector_response_contract_round_trip() { + // Vector serialization remains a public DTO contract even though this + // integration fixture executes only the scheduler's supported text tasks. + let expected = serde_json::json!({ + "workflow_run_id": "run-vector-contract", + "outputs": [{ + "node_id": "vector-output-1", + "port_id": "vector", + "value": [0.1, 0.2, 0.3] + }], + "timing_ms": 42 + }); + let response: pantograph_workflow_service::WorkflowRunResponse = + serde_json::from_value(expected.clone()).expect("deserialize vector run response"); + assert_eq!( + serde_json::to_value(response).expect("serialize vector run response"), + expected + ); +} + #[tokio::test] async fn workflow_run_contract_snapshot() { let service = WorkflowService::new(); - let host = ContractHost; + let host = SchedulerContractHost::default(); let session = service .create_workflow_execution_session( &host, @@ -315,8 +418,8 @@ async fn workflow_run_contract_snapshot() { value: serde_json::json!("hello world"), }], output_targets: Some(vec![WorkflowOutputTarget { - node_id: "vector-output-1".to_string(), - port_id: "vector".to_string(), + node_id: "text-output-1".to_string(), + port_id: "text".to_string(), }]), override_selection: None, timeout_ms: None, @@ -325,6 +428,7 @@ async fn workflow_run_contract_snapshot() { ) .await .expect("session workflow run response"); + host.assert_admission_callbacks(); let value = serde_json::to_value(response).expect("serialize response"); assert!(value["workflow_run_id"] @@ -334,9 +438,9 @@ async fn workflow_run_contract_snapshot() { "workflow_run_id": value["workflow_run_id"], "outputs": [ { - "node_id": "vector-output-1", - "port_id": "vector", - "value": [0.1, 0.2, 0.3] + "node_id": "text-output-1", + "port_id": "text", + "value": "hello world" } ], "timing_ms": value["timing_ms"] @@ -345,6 +449,55 @@ async fn workflow_run_contract_snapshot() { assert_eq!(value, expected); } +#[tokio::test] +async fn workflow_run_missing_graph_preserves_admission_diagnostics_contract() { + let service = WorkflowService::new(); + let host = ContractHost; + let session = service + .create_workflow_execution_session( + &host, + WorkflowExecutionSessionCreateRequest { + workflow_id: "wf-1".to_string(), + usage_profile: None, + keep_alive: false, + }, + ) + .await + .expect("create metadata-only session"); + let error = service + .run_workflow_execution_session( + &host, + WorkflowExecutionSessionRunRequest { + session_id: session.session_id, + workflow_semantic_version: "0.1.0".to_string(), + inputs: Vec::new(), + output_targets: None, + override_selection: None, + timeout_ms: None, + priority: None, + }, + ) + .await + .expect_err("execution requires a stored graph"); + assert_eq!( + error.code(), + pantograph_workflow_service::WorkflowErrorCode::WorkflowNotFound + ); + assert_eq!(error.message(), "workflow 'wf-1' not found"); + let diagnostics = error + .diagnostics() + .expect("admission error retains diagnostics attribution"); + assert!(diagnostics + .workflow_run_id + .as_deref() + .is_some_and(|id| !id.is_empty())); + assert_eq!(diagnostics.diagnostic_event_id, None); + assert_eq!( + diagnostics.diagnostics_unavailable.as_deref(), + Some("diagnostics ledger is not configured") + ); +} + #[tokio::test] async fn workflow_capabilities_contract_snapshot() { let service = WorkflowService::new(); @@ -2398,7 +2551,7 @@ fn workflow_trace_snapshot_request_rejects_blank_contract_filter() { #[tokio::test] async fn workflow_run_rejects_non_discovered_output_target_contract() { let service = WorkflowService::new(); - let host = ContractHost; + let host = SchedulerContractHost::default(); let session = service .create_workflow_execution_session( &host, @@ -2417,9 +2570,13 @@ async fn workflow_run_rejects_non_discovered_output_target_contract() { WorkflowExecutionSessionRunRequest { session_id: session.session_id, workflow_semantic_version: "0.1.0".to_string(), - inputs: Vec::new(), + inputs: vec![WorkflowPortBinding { + node_id: "text-input-1".to_string(), + port_id: "text".to_string(), + value: serde_json::json!("hello world"), + }], output_targets: Some(vec![WorkflowOutputTarget { - node_id: "vector-output-1".to_string(), + node_id: "text-output-1".to_string(), port_id: "stream".to_string(), }]), override_selection: None, @@ -2429,8 +2586,21 @@ async fn workflow_run_rejects_non_discovered_output_target_contract() { ) .await .expect_err("expected invalid request for non-discovered target"); + host.assert_admission_callbacks(); + assert_eq!( + err.code(), + pantograph_workflow_service::WorkflowErrorCode::InvalidRequest + ); + assert_eq!( + err.message(), + "output_targets.0 references non-discoverable output 'text-output-1.stream'" + ); assert!(matches!(err, WorkflowServiceError::InvalidRequest(_))); + assert!( + err.diagnostics().is_none(), + "unconfigured terminal ledger preserves the original error" + ); } #[tokio::test] diff --git a/crates/pantograph-workflow-service/tests/fixtures/run_projection_contract.json b/crates/pantograph-workflow-service/tests/fixtures/run_projection_contract.json index d77f354cc..18ebcd67d 100644 --- a/crates/pantograph-workflow-service/tests/fixtures/run_projection_contract.json +++ b/crates/pantograph-workflow-service/tests/fixtures/run_projection_contract.json @@ -34,6 +34,9 @@ "output_artifact_count": 1, "output_artifact_total_size_bytes": 1024, "model_cache_state": "cache_hit", + "memory_failure_kind": null, + "observed_peak_ram_bytes": null, + "observed_peak_vram_bytes": null, "scheduler_reason": "queued behind one run", "latest_error_event_id": "error-event-1", "latest_error_severity": "fatal", @@ -119,6 +122,9 @@ "output_artifact_count": 1, "output_artifact_total_size_bytes": 1024, "model_cache_state": "cache_hit", + "memory_failure_kind": null, + "observed_peak_ram_bytes": null, + "observed_peak_vram_bytes": null, "scheduler_reason": "queued behind one run", "latest_error_event_id": "error-event-1", "latest_error_severity": "fatal", diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-headless-integration-contracts.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-headless-integration-contracts.md new file mode 100644 index 000000000..08512603a --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-headless-integration-contracts.md @@ -0,0 +1,50 @@ +# Headless Integration Contract Fixtures + +## Source-grounded scope + +PR #8 head `333e53db2b60c85c0190fd6ba34b75d3d5ecce46` passed all 878 +workflow-service library tests in hosted job 111081103534, then failed three of +29 integration contracts. Runtime Separation passed; aggregate Quality Gates +remains red. This repair changes test fixtures only and is stacked on PR #8. + +The shared run projection fixture omitted `memory_failure_kind`, +`observed_peak_ram_bytes` and `observed_peak_vram_bytes`, which the canonical +ledger run-list/detail records serialize as null when unavailable. Add those +fields to both rows, preserving the exact Rust round-trip assertions and the +TypeScript projection consumer's shared fixture. + +The execution fixture supplied canned vector outputs through the obsolete host +`run_workflow` hook, but current session execution reads `workflow_graph` and +runs scheduler tasks. Its missing graph failed before its output assertions. +Use a dedicated text-input to text-output graph host, with explicit non-runtime +capabilities and I/O, for the two execution contracts. Assert that graph and I/O +callbacks occurred and that the scheduler returns the supplied text. Remove the +unused canned run hook. Preserve vector response serialization and deserialization +as a separate exact DTO round-trip test; no vector execution support is claimed. + +The invalid-output fixture must also supply the text input: target discovery is +validated after all tasks complete, so missing inputs would instead leave a task +AwaitingInputs. Assert the exact non-discoverable output message and original +InvalidRequest variant. With no ledger, terminal finalization preserves that +error without a diagnostics wrapper. The previous missing-graph admission error +had a wrapper because admission records canonical errors separately. Retain this +actual admission/error-link contract in a dedicated missing-graph regression, +including real nonblank run attribution and the unavailable-ledger explanation. +Production scheduling, diagnostics and host APIs are unchanged. + +## Verification and qualification + +- Focused TypeScript projection tests: 12 passed, zero failed. +- Rustfmt and whitespace checks passed. No local Cargo build was attempted: + generated intermediates were reclaimed for the approved Android emulator and + current disk capacity does not support a fresh Rust build. +- Fresh hosted Rust integration qualification is required after publication. + Preserved old executables are not evidence for this changed source. +- Independent integrator source review accepted staged tree + `2c4b27a6fcccc182d90e597df4f275984cbe4fa3` after reading all three changed + files and the relevant host, validation and diagnostics sources. This is source + review acceptance, not Rust execution evidence; this report update follows it. +- Existing frontend lint, scheduler Clippy, + dependency audit and native Rustler failures remain separate. C# full binding + and native packaging qualification previously remained blocked downstream of + the failing integration contracts. No checks or assertions were waived. From e1bf2d8d4426566308fb9ce72f4c4f1895512568 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 22:14:49 -0700 Subject: [PATCH 07/65] test(workflow): guard required legacy host compatibility hook --- .../tests/contract.rs | 22 +++++++++++++++++++ ...26-10-03-headless-integration-contracts.md | 13 +++++++++++ 2 files changed, 35 insertions(+) diff --git a/crates/pantograph-workflow-service/tests/contract.rs b/crates/pantograph-workflow-service/tests/contract.rs index dc10fffc3..053763329 100644 --- a/crates/pantograph-workflow-service/tests/contract.rs +++ b/crates/pantograph-workflow-service/tests/contract.rs @@ -71,6 +71,17 @@ struct ContractHost; #[async_trait] impl WorkflowHost for ContractHost { + async fn run_workflow( + &self, + _workflow_id: &str, + _inputs: &[WorkflowPortBinding], + _output_targets: Option<&[WorkflowOutputTarget]>, + _run_options: pantograph_workflow_service::WorkflowRunOptions, + _run_handle: pantograph_workflow_service::WorkflowRunHandle, + ) -> Result, WorkflowServiceError> { + panic!("session contract tests must execute scheduler tasks, not the legacy host hook"); + } + async fn validate_workflow(&self, _workflow_id: &str) -> Result<(), WorkflowServiceError> { Ok(()) } @@ -285,6 +296,17 @@ impl SchedulerContractHost { #[async_trait] impl WorkflowHost for SchedulerContractHost { + async fn run_workflow( + &self, + _workflow_id: &str, + _inputs: &[WorkflowPortBinding], + _output_targets: Option<&[WorkflowOutputTarget]>, + _run_options: pantograph_workflow_service::WorkflowRunOptions, + _run_handle: pantograph_workflow_service::WorkflowRunHandle, + ) -> Result, WorkflowServiceError> { + panic!("session contract tests must execute scheduler tasks, not the legacy host hook"); + } + async fn validate_workflow(&self, workflow_id: &str) -> Result<(), WorkflowServiceError> { assert_eq!(workflow_id, "wf-1"); Ok(()) diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-headless-integration-contracts.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-headless-integration-contracts.md index 08512603a..e8a4b245f 100644 --- a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-headless-integration-contracts.md +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-headless-integration-contracts.md @@ -48,3 +48,16 @@ Production scheduling, diagnostics and host APIs are unchanged. dependency audit and native Rustler failures remain separate. C# full binding and native packaging qualification previously remained blocked downstream of the failing integration contracts. No checks or assertions were waived. + +## Hosted compilation correction + +Initial exact-head hosted run 37098781826 failed compilation with E0046 before +executing contracts: `WorkflowHost::run_workflow` remains a required compatibility +trait method. Both fixture hosts now implement it as an explicit panic guard, +so accidental execution through that hook fails instead of returning canned +outputs. This preserves the real scheduler path and all assertions. Rustfmt and +staged whitespace pass; fresh hosted compilation and execution remain required. +Independent integrator review accepted the exact two-hook correction at staged +`08b64cd03e0ce645b3aae6e0a5363ff3674397b3`; it satisfies the required trait +signature while retaining fail-on-legacy-bypass behavior. This acceptance is +source-only and does not replace hosted Rust execution. From 2c5f22b09025c557585a010076af75bed5b0859b Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 22:24:49 -0700 Subject: [PATCH 08/65] fix(beam): align smoke stubs with default native exports --- .../beam/pantograph_native_smoke/README.md | 5 +++ .../lib/pantograph/native.ex | 3 ++ .../2026-10-03-beam-smoke-export-stubs.md | 34 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-beam-smoke-export-stubs.md diff --git a/bindings/beam/pantograph_native_smoke/README.md b/bindings/beam/pantograph_native_smoke/README.md index caa987685..b13dc7b5a 100644 --- a/bindings/beam/pantograph_native_smoke/README.md +++ b/bindings/beam/pantograph_native_smoke/README.md @@ -38,6 +38,11 @@ The same runner is used by the required `Rustler BEAM smoke` job in - The local `Pantograph.Native` shim defines generated NIF stubs for the full default Rustler export surface so `:erlang.load_nif/2` can load the compiled library before individual smoke tests call the narrower contract under test. + This includes the package-facts snapshot (arity 3), individual summary (arity 2), + and update-cursor query (arity 3) exports. Their stubs permit native loading; + this smoke harness does not claim model-library query behavior coverage. + The seven `frontend-http` exports are feature-gated and outside this default + build's export surface. - This harness now also pins a backend-owned validation error path by asserting that unknown edge endpoints round-trip back to BEAM as validation strings through `workflow_validate/1`. diff --git a/bindings/beam/pantograph_native_smoke/lib/pantograph/native.ex b/bindings/beam/pantograph_native_smoke/lib/pantograph/native.ex index f1b302ac9..88f3796d0 100644 --- a/bindings/beam/pantograph_native_smoke/lib/pantograph/native.ex +++ b/bindings/beam/pantograph_native_smoke/lib/pantograph/native.ex @@ -58,6 +58,9 @@ defmodule Pantograph.Native do {:pumas_list_models, 1}, {:pumas_search_models, 4}, {:pumas_get_model, 2}, + {:pumas_model_package_facts_summary_snapshot, 3}, + {:pumas_resolve_model_package_facts_summary, 2}, + {:pumas_list_model_library_updates_since, 3}, {:pumas_rebuild_index, 1}, {:pumas_search_hf, 4}, {:pumas_get_repo_files, 2}, diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-beam-smoke-export-stubs.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-beam-smoke-export-stubs.md new file mode 100644 index 000000000..ca983a95b --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-beam-smoke-export-stubs.md @@ -0,0 +1,34 @@ +# BEAM Smoke Export Stub Repair + +Hosted Quality run 37099224216 at PR #9 head +`e1bf2d8d4426566308fb9ce72f4c4f1895512568` builds the Rustler NIF successfully, +then job 111135294915 fails all five BEAM smoke tests. The loader reports +`Function not found 'Elixir.Pantograph.Native':pumas_model_package_facts_summary_snapshot/3`. + +The default Rust NIF surface has added three exports since the smoke shim was +written: `pumas_model_package_facts_summary_snapshot/3`, +`pumas_resolve_model_package_facts_summary/2`, and +`pumas_list_model_library_updates_since/3`. Add their matching generated Elixir +stub declarations so the BEAM loader can replace them with the native functions. +No native implementation, feature selection, loader error handling, or assertions +change. The existing five tests exercise real loading and workflow/registry +round trips and will fail if export registration remains inconsistent. + +Source inspection finds 67 default Rust exports and 67 matching shim names after +this change, with the three new arities checked directly against their Rust +signatures. Seven additional Rust exports are guarded by `frontend-http`; the +canonical smoke runner uses the default feature set and does not include them. +This is bounded source evidence, not an ABI execution claim or a new permanent +source-parser gate. No broader model-library query behavior is claimed. + +Staged whitespace passes. Elixir/Mix are unavailable locally and no Rust rebuild +was attempted under the current disk constraint. Fresh hosted Rustler BEAM smoke +qualification and independent source review are pending. PR #9's workflow-service +tests already pass, while its downstream Headless qualification is still running. +Existing lint, Clippy and dependency audit failures remain separate; no merge is +qualified by this repair alone. + +Independent integrator source review accepted staged tree +`ad33e068e8057e70e8552fdb6e9f122d3ac82667` after comparing all three new +names/arities against the Rust NIF definitions and reading the unchanged generated +stub behavior. Actual BEAM loading remains a hosted qualification requirement. From 7b6dc58809e00a542533aece4959e09ba4068865 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 22:48:29 -0700 Subject: [PATCH 09/65] test(beam): preserve canonical graph validation and selector contracts --- .../test/pantograph_native_smoke_test.exs | 12 +++++++---- .../2026-10-03-beam-smoke-export-stubs.md | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/bindings/beam/pantograph_native_smoke/test/pantograph_native_smoke_test.exs b/bindings/beam/pantograph_native_smoke/test/pantograph_native_smoke_test.exs index 6d7b0619e..e1b7f3f1d 100644 --- a/bindings/beam/pantograph_native_smoke/test/pantograph_native_smoke_test.exs +++ b/bindings/beam/pantograph_native_smoke/test/pantograph_native_smoke_test.exs @@ -20,13 +20,16 @@ defmodule PantographNativeSmokeTest do test "workflow_validate returns edge-reference errors for unknown nodes" do graph_json = Pantograph.Native.workflow_new("beam-invalid", "BEAM Invalid") - |> Pantograph.Native.workflow_add_edge("missing-source", "out", "missing-target", "in") + |> Pantograph.Native.workflow_add_node("known-source", "text-input", 0.0, 0.0, "{}") + |> Pantograph.Native.workflow_add_node("known-target", "text-output", 100.0, 0.0, "{}") + |> Pantograph.Native.workflow_add_edge("missing-source", "text", "known-target", "text") + |> Pantograph.Native.workflow_add_edge("known-source", "text", "missing-target", "text") errors = Pantograph.Native.workflow_validate(graph_json) assert is_list(errors) - assert Enum.any?(errors, &String.contains?(&1, "unknown node 'missing-source'")) - assert Enum.any?(errors, &String.contains?(&1, "unknown node 'missing-target'")) + assert Enum.any?(errors, &String.contains?(&1, "unknown source node 'missing-source'")) + assert Enum.any?(errors, &String.contains?(&1, "unknown target node 'missing-target'")) end test "workflow_from_json returns parse errors as BEAM tuples" do @@ -57,6 +60,7 @@ defmodule PantographNativeSmokeTest do queryable = Pantograph.Native.node_registry_queryable_ports(registry) assert String.contains?(queryable, ~s("node_type":"puma-lib")) - assert String.contains?(queryable, ~s("port_id":"model_path")) + assert String.contains?(queryable, ~s("port_id":"pumas_model_ref")) + refute String.contains?(queryable, ~s("port_id":"model_path")) end end diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-beam-smoke-export-stubs.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-beam-smoke-export-stubs.md index ca983a95b..18248eaf8 100644 --- a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-beam-smoke-export-stubs.md +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-beam-smoke-export-stubs.md @@ -32,3 +32,23 @@ Independent integrator source review accepted staged tree `ad33e068e8057e70e8552fdb6e9f122d3ac82667` after comparing all three new names/arities against the Rust NIF definitions and reading the unchanged generated stub behavior. Actual BEAM loading remains a hosted qualification requirement. + +## Hosted loading result and newly reachable fixture repairs + +At head `2c5f22b09025c557585a010076af75bed5b0859b`, hosted job 111136852470 +built and loaded the native library successfully. Three smoke tests passed and +two reached outdated assertions. This qualifies native loading, not the full +smoke suite. + +Canonical graph contract validation reports an unknown source first and returns +for that edge before checking the target. Give the fixture two edges, each with +one known endpoint, to preserve both unknown-source and unknown-target checks. +Assert the current role-specific messages. Canonical `puma-lib` discovery exposes +`pumas_model_ref`; assert that port and reject the retired `model_path` port. +These changes preserve the validation/discovery coverage at the actual public +BEAM boundary. Production code and all other assertions remain unchanged. +Whitespace passes; independent source review and hosted rerun are pending. +Independent integrator source review accepted follow-up tree +`421f3be33e9504040c6e93c00f11bad7f8b8ec77` against the canonical edge validator +and current path-free model selector port. Fresh hosted smoke execution remains +required for these fixture assertions. From 8ac413c52b2501bc3c69310a38eb6f13e81bb2d4 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 22:50:07 -0700 Subject: [PATCH 10/65] test(http): exercise transport contracts with bounded server waits --- .../src/lib.rs | 335 +++++++++--------- ...-10-03-http-adapter-transport-contracts.md | 57 +++ 2 files changed, 215 insertions(+), 177 deletions(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-http-adapter-transport-contracts.md diff --git a/crates/pantograph-frontend-http-adapter/src/lib.rs b/crates/pantograph-frontend-http-adapter/src/lib.rs index cbd9cb44a..ff924c8f6 100644 --- a/crates/pantograph-frontend-http-adapter/src/lib.rs +++ b/crates/pantograph-frontend-http-adapter/src/lib.rs @@ -356,56 +356,11 @@ fn normalize_base_url(raw_base_url: String) -> Result Result; - } - - #[async_trait::async_trait] - impl WorkflowServiceSessionRunExt for WorkflowService { - async fn workflow_run( - &self, - host: &FrontendHttpWorkflowHost, - request: WorkflowRunRequest, - ) -> Result { - let session = self - .create_workflow_execution_session( - host, - WorkflowExecutionSessionCreateRequest { - workflow_id: request.workflow_id, - usage_profile: None, - keep_alive: false, - }, - ) - .await?; - - self.run_workflow_execution_session( - host, - WorkflowExecutionSessionRunRequest { - session_id: session.session_id, - workflow_semantic_version: request.workflow_semantic_version, - inputs: request.inputs, - output_targets: request.output_targets, - override_selection: request.override_selection, - timeout_ms: request.timeout_ms, - priority: None, - }, - ) - .await - } - } + use std::time::{Duration, Instant}; #[test] fn parse_workflow_outputs_payload_rejects_missing_fields() { @@ -463,71 +418,62 @@ mod tests { ); } - fn create_temp_workflow_root_with_output(workflow_id: &str) -> std::path::PathBuf { - let suffix = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock") - .as_nanos(); - let root = std::env::temp_dir().join(format!("pantograph-frontend-http-tests-{suffix}")); - let workflows_dir = root.join(".pantograph").join("workflows"); - std::fs::create_dir_all(&workflows_dir).expect("create workflows dir"); - - let workflow_json = serde_json::json!({ - "version": "1.0", - "metadata": { - "name": "Output Workflow" - }, - "graph": { - "nodes": [ - { - "id": "vector-output-1", - "node_type": "vector-output", - "data": { - "definition": { - "category": "output", - "io_binding_origin": "client_session", - "outputs": [ - { - "id": "vector", - "data_type": "embedding", - "required": false, - "multiple": false - } - ] - } - }, - "position": { "x": 0.0, "y": 0.0 } - } - ], - "edges": [] - } - }); - - let file_path = workflows_dir.join(format!("{}.json", workflow_id)); - std::fs::write( - file_path, - serde_json::to_vec(&workflow_json).expect("serialize workflow"), - ) - .expect("write workflow"); - root - } - fn spawn_single_workflow_server( status_code: u16, body: serde_json::Value, - ) -> (String, std::thread::JoinHandle<()>) { + ) -> (String, std::thread::JoinHandle) { let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server"); + listener + .set_nonblocking(true) + .expect("set nonblocking accept"); let addr = listener.local_addr().expect("local addr"); let body_text = body.to_string(); let reason = if status_code == 200 { "OK" } else { "ERROR" }; let handle = std::thread::spawn(move || { - let (mut stream, _) = listener.accept().expect("accept"); + let deadline = Instant::now() + Duration::from_secs(5); + let mut stream = loop { + match listener.accept() { + Ok((stream, _)) => break stream, + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + assert!( + Instant::now() < deadline, + "HTTP transport was never called before the test-server deadline" + ); + std::thread::sleep(Duration::from_millis(10)); + } + Err(error) => panic!("test server accept failed: {error}"), + } + }; + stream.set_nonblocking(false).expect("set blocking read"); stream .set_read_timeout(Some(Duration::from_secs(2))) .expect("set timeout"); - let mut request_buf = [0_u8; 8192]; - let _ = stream.read(&mut request_buf); + let mut reader = BufReader::new(&stream); + let mut line = String::new(); + reader.read_line(&mut line).expect("read request line"); + assert_eq!(line, "POST /v1/workflow/run HTTP/1.1\r\n"); + let mut content_length = None; + loop { + line.clear(); + assert!(reader.read_line(&mut line).expect("read request header") > 0); + if line == "\r\n" { + break; + } + if let Some((name, value)) = line.split_once(':') { + if name.eq_ignore_ascii_case("content-length") { + content_length = + Some(value.trim().parse::().expect("content length")); + } + } + } + let content_length = content_length.expect("JSON request has content length"); + assert!(content_length <= 8192, "unexpected test request size"); + let mut request_body = vec![0_u8; content_length]; + reader + .read_exact(&mut request_body) + .expect("read complete JSON request"); + drop(reader); let response = format!( "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", @@ -539,17 +485,15 @@ mod tests { stream .write_all(response.as_bytes()) .expect("write response"); + serde_json::from_slice(&request_body).expect("request body is JSON") }); (format!("http://{}", addr), handle) } #[tokio::test] - async fn workflow_run_returns_output_not_produced_for_missing_target_output() { + async fn transport_preserves_empty_outputs_for_a_requested_target() { let workflow_id = "wf-output-not-produced"; - let workflow_root = create_temp_workflow_root_with_output(workflow_id) - .join(".pantograph") - .join("workflows"); let payload = serde_json::json!({ "workflow_run_id": "adapter-run-1", @@ -561,7 +505,56 @@ mod tests { let host = FrontendHttpWorkflowHost::new( base_url, None, - vec![workflow_root], + Vec::new(), + DEFAULT_MAX_INPUT_BINDINGS, + DEFAULT_MAX_OUTPUT_TARGETS, + DEFAULT_MAX_VALUE_BYTES, + DEFAULT_BACKEND_NAME.to_string(), + ) + .expect("build frontend host"); + + let outputs = host + .run_workflow( + workflow_id, + &[], + Some(&[WorkflowOutputTarget { + node_id: "vector-output-1".to_string(), + port_id: "vector".to_string(), + }]), + WorkflowRunOptions { + timeout_ms: Some(2_000), + ..WorkflowRunOptions::default() + }, + WorkflowRunHandle::new(), + ) + .await + .expect("transport preserves a valid empty outputs array"); + + let request = server_thread.join().expect("join server"); + assert_eq!( + request, + serde_json::json!({ + "workflow_id": workflow_id, + "inputs": [], + "output_targets": [{"node_id": "vector-output-1", "port_id": "vector"}], + }) + ); + assert!(outputs.is_empty()); + } + + #[tokio::test] + async fn transport_maps_output_not_produced_error_envelope() { + let workflow_id = "wf-output-not-produced"; + let payload = serde_json::json!({ + "code": "output_not_produced", + "message": "requested output target 'vector-output-1.vector' was not produced" + }); + let (base_url, server_thread) = spawn_single_workflow_server(422, payload); + + let host = FrontendHttpWorkflowHost::new( + base_url, + None, + Vec::new(), DEFAULT_MAX_INPUT_BINDINGS, DEFAULT_MAX_OUTPUT_TARGETS, DEFAULT_MAX_VALUE_BYTES, @@ -569,34 +562,35 @@ mod tests { ) .expect("build frontend host"); - let err = WorkflowService::new() - .workflow_run( - &host, - WorkflowRunRequest { - workflow_id: workflow_id.to_string(), - workflow_semantic_version: "0.1.0".to_string(), - inputs: Vec::new(), - output_targets: Some(vec![WorkflowOutputTarget { - node_id: "vector-output-1".to_string(), - port_id: "vector".to_string(), - }]), - override_selection: None, - timeout_ms: None, + let err = host + .run_workflow( + workflow_id, + &[], + None, + WorkflowRunOptions { + timeout_ms: Some(2_000), + ..WorkflowRunOptions::default() }, + WorkflowRunHandle::new(), ) .await - .expect_err("missing output should return output_not_produced"); + .expect_err("422 envelope should map to output_not_produced"); server_thread.join().expect("join server"); - assert!(matches!(err, WorkflowServiceError::OutputNotProduced(_))); + match err { + WorkflowServiceError::OutputNotProduced(message) => { + assert_eq!( + message, + "requested output target 'vector-output-1.vector' was not produced" + ); + } + other => panic!("expected output_not_produced, got {}", other), + } } #[tokio::test] async fn workflow_run_maps_non_2xx_error_envelope_to_service_error() { let workflow_id = "wf-runtime-not-ready"; - let workflow_root = create_temp_workflow_root_with_output(workflow_id) - .join(".pantograph") - .join("workflows"); let payload = serde_json::json!({ "code": "runtime_not_ready", "message": "backend unavailable" @@ -606,7 +600,7 @@ mod tests { let host = FrontendHttpWorkflowHost::new( base_url, None, - vec![workflow_root], + Vec::new(), DEFAULT_MAX_INPUT_BINDINGS, DEFAULT_MAX_OUTPUT_TARGETS, DEFAULT_MAX_VALUE_BYTES, @@ -614,17 +608,16 @@ mod tests { ) .expect("build frontend host"); - let err = WorkflowService::new() - .workflow_run( - &host, - WorkflowRunRequest { - workflow_id: workflow_id.to_string(), - workflow_semantic_version: "0.1.0".to_string(), - inputs: Vec::new(), - output_targets: None, - override_selection: None, - timeout_ms: None, + let err = host + .run_workflow( + workflow_id, + &[], + None, + WorkflowRunOptions { + timeout_ms: Some(2_000), + ..WorkflowRunOptions::default() }, + WorkflowRunHandle::new(), ) .await .expect_err("503 envelope should map to runtime_not_ready"); @@ -641,9 +634,6 @@ mod tests { #[tokio::test] async fn workflow_run_maps_cancelled_error_envelope_to_service_error() { let workflow_id = "wf-cancelled"; - let workflow_root = create_temp_workflow_root_with_output(workflow_id) - .join(".pantograph") - .join("workflows"); let payload = serde_json::json!({ "code": "cancelled", "message": "workflow run cancelled" @@ -653,7 +643,7 @@ mod tests { let host = FrontendHttpWorkflowHost::new( base_url, None, - vec![workflow_root], + Vec::new(), DEFAULT_MAX_INPUT_BINDINGS, DEFAULT_MAX_OUTPUT_TARGETS, DEFAULT_MAX_VALUE_BYTES, @@ -661,17 +651,16 @@ mod tests { ) .expect("build frontend host"); - let err = WorkflowService::new() - .workflow_run( - &host, - WorkflowRunRequest { - workflow_id: workflow_id.to_string(), - workflow_semantic_version: "0.1.0".to_string(), - inputs: Vec::new(), - output_targets: None, - override_selection: None, - timeout_ms: None, + let err = host + .run_workflow( + workflow_id, + &[], + None, + WorkflowRunOptions { + timeout_ms: Some(2_000), + ..WorkflowRunOptions::default() }, + WorkflowRunHandle::new(), ) .await .expect_err("409 cancelled envelope should map to cancelled"); @@ -688,9 +677,6 @@ mod tests { #[tokio::test] async fn workflow_run_maps_invalid_request_error_envelope_to_service_error() { let workflow_id = "wf-invalid-request"; - let workflow_root = create_temp_workflow_root_with_output(workflow_id) - .join(".pantograph") - .join("workflows"); let payload = serde_json::json!({ "code": "invalid_request", "message": "workflow requires interactive input" @@ -700,7 +686,7 @@ mod tests { let host = FrontendHttpWorkflowHost::new( base_url, None, - vec![workflow_root], + Vec::new(), DEFAULT_MAX_INPUT_BINDINGS, DEFAULT_MAX_OUTPUT_TARGETS, DEFAULT_MAX_VALUE_BYTES, @@ -708,17 +694,16 @@ mod tests { ) .expect("build frontend host"); - let err = WorkflowService::new() - .workflow_run( - &host, - WorkflowRunRequest { - workflow_id: workflow_id.to_string(), - workflow_semantic_version: "0.1.0".to_string(), - inputs: Vec::new(), - output_targets: None, - override_selection: None, - timeout_ms: None, + let err = host + .run_workflow( + workflow_id, + &[], + None, + WorkflowRunOptions { + timeout_ms: Some(2_000), + ..WorkflowRunOptions::default() }, + WorkflowRunHandle::new(), ) .await .expect_err("400 invalid_request envelope should map to invalid request"); @@ -735,9 +720,6 @@ mod tests { #[tokio::test] async fn workflow_run_rejects_non_envelope_non_2xx_error_payload() { let workflow_id = "wf-malformed-error"; - let workflow_root = create_temp_workflow_root_with_output(workflow_id) - .join(".pantograph") - .join("workflows"); let payload = serde_json::json!({ "error": "backend unavailable" }); @@ -746,7 +728,7 @@ mod tests { let host = FrontendHttpWorkflowHost::new( base_url, None, - vec![workflow_root], + Vec::new(), DEFAULT_MAX_INPUT_BINDINGS, DEFAULT_MAX_OUTPUT_TARGETS, DEFAULT_MAX_VALUE_BYTES, @@ -754,17 +736,16 @@ mod tests { ) .expect("build frontend host"); - let err = WorkflowService::new() - .workflow_run( - &host, - WorkflowRunRequest { - workflow_id: workflow_id.to_string(), - workflow_semantic_version: "0.1.0".to_string(), - inputs: Vec::new(), - output_targets: None, - override_selection: None, - timeout_ms: None, + let err = host + .run_workflow( + workflow_id, + &[], + None, + WorkflowRunOptions { + timeout_ms: Some(2_000), + ..WorkflowRunOptions::default() }, + WorkflowRunHandle::new(), ) .await .expect_err("non-envelope errors must not be silently remapped"); diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-http-adapter-transport-contracts.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-http-adapter-transport-contracts.md new file mode 100644 index 000000000..1fd5cfaa1 --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-http-adapter-transport-contracts.md @@ -0,0 +1,57 @@ +# Frontend HTTP Adapter Transport Contracts + +## Diagnosis and owner + +PR #9 Headless run 37099224225 passed workflow-service tests and then remained +in the frontend HTTP adapter step. Completed job logs are unavailable while it +runs. Source inspection identifies an unbounded wait: five tests use a test-only +session wrapper around a vector-output graph, which the current scheduler does +not execute. The service returns before the HTTP host hook is called, and each +test then joins a server blocked in `TcpListener::accept`. Its existing socket +read timeout only applies after acceptance. PR #10 inherits the same test path. + +The four HTTP error-envelope tests belong to the transport implementation in +`FrontendHttpWorkflowHost::run_workflow`. Invoke that port directly, retain the +original exact error kinds/messages, and require a real POST to `/v1/workflow/run`. +Remove the test-only session wrapper and disk graph fixtures, which obscured +that boundary. This does not reintroduce a public direct-run service API or make +session scheduling dispatch through the compatibility hook. + +The old missing-output fixture mixed two owners: it returned HTTP 200 with an +empty output list but expected the local service to validate a requested target. +The adapter only parses that response. Preserve its empty-output behavior in a +transport test and assert remote `output_not_produced` envelope mapping separately. +Canonical scheduler missing-result rejection remains covered by +`task_result_output_projection::tests::rejects_missing_requested_output`; this +slice makes no new claim about the scheduler's public error code. + +## Bounded server and evidence + +The test server now uses a five-second accept deadline and retains a two-second +read timeout. Transport test requests have a two-second timeout. Read the complete +request line, headers and declared body before replying, avoiding partial-read +assumptions; assert the actual HTTP method/path. Missing dispatch becomes a clear +finite server failure rather than a CI job waiting indefinitely. + +Changes are confined to the adapter's test module and this report. Rustfmt and +whitespace pass. Local Cargo execution is unavailable under the shared disk +constraint; existing binaries do not qualify changed source. Independent review +and fresh hosted adapter tests are required. Running earlier jobs cannot consume +this repair automatically and need cancellation or their existing timeout before +their final logs become available. Broader lint, Clippy, audit and native binding +qualification remain separately open. + +## Terminal evidence and review + +Both earlier stalled runs were cancelled through the authenticated GitHub UI. +Completed PR #9 logs now confirm 878 library tests and all 31 integration contracts +passed; the adapter compiled, passed nine of fourteen tests, then blocked in its +remaining HTTP fixtures until cancellation at 05:31:39 UTC. Native/C# stages did +not run. This corroborates the source-grounded hang diagnosis. + +Independent integrator source review accepted staged tree +`85d9372bd91786187719042bc778446485d553fd`, including transport/target-validation +ownership and bounded socket lifetimes. At the reviewer's suggestion, the empty +response transport test additionally checks the exact received JSON workflow ID, +inputs and output targets. This is source review only; hosted compilation and +execution remain required. Production module bytes remain unchanged. From f4fec1f3eb9e4c632bfab60fa324f0c3b877efee Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 23:02:02 -0700 Subject: [PATCH 11/65] test(uniffi): assert canonical typed memory estimate --- crates/pantograph-uniffi/src/lib_tests.rs | 13 ++++++++- ...0-03-uniffi-capability-estimate-fixture.md | 28 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-capability-estimate-fixture.md diff --git a/crates/pantograph-uniffi/src/lib_tests.rs b/crates/pantograph-uniffi/src/lib_tests.rs index 1e822f621..a7235a0c1 100644 --- a/crates/pantograph-uniffi/src/lib_tests.rs +++ b/crates/pantograph-uniffi/src/lib_tests.rs @@ -317,7 +317,18 @@ fn test_workflow_get_capabilities_contract_success() { assert_eq!(response.max_input_bindings, DEFAULT_MAX_INPUT_BINDINGS); assert_eq!(response.max_value_bytes, DEFAULT_MAX_VALUE_BYTES); assert_eq!(response.runtime_requirements.required_models.len(), 0); - assert_eq!(response.runtime_requirements.estimated_peak_ram_mb, Some(0)); + let peak_ram = response.runtime_requirements.resource_estimates.iter().find(|estimate| { + estimate.kind() == pantograph_workflow_service::WorkflowTechnicalFitResourceEstimateKind::PeakRamBytes + }); + assert_eq!( + peak_ram, + Some( + &pantograph_workflow_service::WorkflowTechnicalFitResourceEstimate::available( + pantograph_workflow_service::WorkflowTechnicalFitResourceEstimateKind::PeakRamBytes, + 0, + ) + ), + ); } #[test] diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-capability-estimate-fixture.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-capability-estimate-fixture.md new file mode 100644 index 000000000..759af6f5b --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-capability-estimate-fixture.md @@ -0,0 +1,28 @@ +# UniFFI Capability Estimate Fixture + +Headless run 37101137330 at PR #11 head +`8ac413c52b2501bc3c69310a38eb6f13e81bb2d4` passed workflow-service and frontend +HTTP adapter tests, then reached UniFFI adapter-mode qualification. The +frontend-http test configuration failed compilation at `lib_tests.rs:320`: +`WorkflowRuntimeRequirements::estimated_peak_ram_mb` no longer exists. + +The canonical contract publishes typed `resource_estimates`. Host capabilities +call `estimate_memory_requirements`, which emits available zero-byte RAM/VRAM +estimates when the graph has no required models. Preserve the old zero-RAM +assertion by locating PeakRamBytes and comparing the complete available estimate, +including its zero value and empty diagnostics. An absent, unavailable, or +nonzero estimate fails the assertion. Existing limits and no-model assertions +remain unchanged; no production, feature, or memory policy change is needed. + +Rustfmt and staged whitespace pass. No local Cargo build was attempted under the +shared disk constraint. Independent source review and fresh hosted UniFFI test +execution remain required. The separate unused shutdown Result warning in +`runtime.rs:209` is recorded for lifecycle/error-contract review rather than +suppressed. Native metadata, C# generation/package/quickstart and aggregate lint, +Clippy/audit qualification remain open. + +Independent integrator source review accepted staged tree +`1910522f10c8fddcbe156635e0db2663e220f5fc` against capabilities.rs:313–370. +This preserves full available zero-byte RAM semantics; exact-head hosted +frontend-http feature compilation and test execution remain required. The +preceding no-default UniFFI configuration already passed all 12 tests. From ebbffc8a6bca39da7a92809ca997e3394da0c2d1 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 23:26:43 -0700 Subject: [PATCH 12/65] test(uniffi): preserve canonical model selector identity --- crates/pantograph-uniffi/src/runtime_tests.rs | 18 ++++++++++-- .../2026-10-03-uniffi-selector-contracts.md | 28 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-selector-contracts.md diff --git a/crates/pantograph-uniffi/src/runtime_tests.rs b/crates/pantograph-uniffi/src/runtime_tests.rs index 05c001124..9c4485f7b 100644 --- a/crates/pantograph-uniffi/src/runtime_tests.rs +++ b/crates/pantograph-uniffi/src/runtime_tests.rs @@ -669,7 +669,12 @@ async fn direct_runtime_exposes_backend_owned_graph_authoring_discovery() { .as_array() .expect("queryable ports") .iter() - .any(|port| port["node_type"] == "puma-lib" && port["port_id"] == "model_path")); + .any(|port| port["node_type"] == "puma-lib" && port["port_id"] == "pumas_model_ref")); + assert!(!queryable + .as_array() + .expect("queryable ports") + .iter() + .any(|port| { port["node_type"] == "puma-lib" && port["port_id"] == "model_path" })); let missing = runtime .workflow_graph_get_node_definition("missing-node".to_string()) @@ -732,7 +737,7 @@ async fn direct_runtime_puma_lib_options_use_selector_access_from_pumas_api() { let options_json = runtime .workflow_graph_query_port_options( "puma-lib".to_string(), - "model_path".to_string(), + "pumas_model_ref".to_string(), serde_json::json!({ "limit": 10, "context": { @@ -757,8 +762,15 @@ async fn direct_runtime_puma_lib_options_use_selector_access_from_pumas_api() { .find(|option| option["metadata"]["id"] == "llm/imported/uniffi-test-gguf") .expect("selector option should be present"); + assert!( + option["value"].is_object(), + "selection must carry a model reference, not a display path" + ); + assert_eq!(option["value"]["model_ref_contract_version"], 1); + assert_eq!(option["value"]["model_id"], "llm/imported/uniffi-test-gguf"); + assert_eq!(option["value"], option["metadata"]["pumas_model_ref"]); assert_eq!( - option["value"], + option["metadata"]["display_entry_path"], serde_json::json!(model_file.display().to_string()) ); assert!(result["metadata"]["package_facts_summary_cursor"] diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-selector-contracts.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-selector-contracts.md new file mode 100644 index 000000000..17e71c629 --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-selector-contracts.md @@ -0,0 +1,28 @@ +# UniFFI Selector Contract Fixtures + +PR #12 Headless run 37101754525 compiled the repaired capability contract and +ran the frontend-http UniFFI configuration: 18 passed and five failed. Two +failures were stale selector fixtures referring to `puma-lib:model_path`, which +has been replaced by `pumas_model_ref` in the canonical node and options provider. + +Update discovery to require `pumas_model_ref` and explicitly reject the retired +queryable port. Query options through the canonical port and require an object +with model-ref contract version 1 and the exact indexed model ID. Verify that the +value matches the canonical reference included in selector metadata; preserve +the old fixture's exact file association assertion against `display_entry_path`, +where display filesystem paths now belong. Cursor and model-presence assertions +remain unchanged. The provider source is workflow-nodes/src/input/puma_lib.rs: +selector_row_value, selector_row_option_metadata and port_option_from_selector_row. +These tests do not claim inference readiness or model execution. + +Only two test functions and this report change. Rustfmt and whitespace pass; +independent source review and hosted tests remain required. No local build was +attempted. The three other runtime-test failures are separate: two require a +legitimate embedding validation/publication API bridge, and managed-runtime +readiness requires current installation evidence rather than legacy empty files. +Neither admission checks nor readiness semantics are weakened here. + +Independent integrator source review accepted staged tree +`f106e00bcb9b43a7903baab5403c88a38ee9b7a8`: canonical reference identity, +version and metadata equality remain asserted, while display path and cursor +checks are preserved. Hosted qualification is still required. From 33018efe2f2e0f331eb4f03301bc937acc437ac5 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 23:36:25 -0700 Subject: [PATCH 13/65] test(uniffi): report missing managed runtime from actual fixture evidence --- crates/pantograph-uniffi/src/runtime_tests.rs | 21 +++++++++++---- ...26-10-03-uniffi-missing-runtime-fixture.md | 27 +++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-missing-runtime-fixture.md diff --git a/crates/pantograph-uniffi/src/runtime_tests.rs b/crates/pantograph-uniffi/src/runtime_tests.rs index 9c4485f7b..4bd9a3c92 100644 --- a/crates/pantograph-uniffi/src/runtime_tests.rs +++ b/crates/pantograph-uniffi/src/runtime_tests.rs @@ -1024,11 +1024,22 @@ async fn direct_runtime_exposes_managed_media_dependency_statuses_and_actions_js let neutral_statuses: serde_json::Value = serde_json::from_str(&neutral_statuses_json) .expect("parse neutral managed dependency statuses"); let neutral_statuses = neutral_statuses.as_array().expect("neutral statuses array"); - assert!(neutral_statuses.iter().any(|status| { - status["key"]["runtime_sidecar"] == serde_json::json!("llama_cpp") - && status["category"] == serde_json::json!("runtime_sidecar") - && status["readiness_state"] == serde_json::json!("ready") - })); + let runtime_status = neutral_statuses + .iter() + .find(|status| { + status["key"]["runtime_sidecar"] == serde_json::json!("llama_cpp") + && status["category"] == serde_json::json!("runtime_sidecar") + }) + .expect("neutral runtime sidecar status"); + // Legacy files under app-data/runtimes are not a managed installation. + // The current owner resolves app-data/third-party/runtimes instead. + assert_eq!(runtime_status["install_state"], "missing"); + assert_eq!(runtime_status["readiness_state"], "missing"); + assert_eq!(runtime_status["available"], false); + assert!(!runtime_status["missing_files"] + .as_array() + .expect("missing runtime files") + .is_empty()); assert!(neutral_statuses.iter().any(|status| { status["key"]["media_tool"] == serde_json::json!("ffmpeg") && status["category"] == serde_json::json!("media_tool") diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-missing-runtime-fixture.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-missing-runtime-fixture.md new file mode 100644 index 000000000..9a562af1d --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-missing-runtime-fixture.md @@ -0,0 +1,27 @@ +# UniFFI Managed Runtime Readiness Fixture + +The current hosted UniFFI runtime suite exposes a false readiness expectation: +`create_temp_root` writes legacy fixture files under `app-data/runtimes/llama-cpp`, +but the managed-runtime owner resolves `app-data/third-party/runtimes/llama-cpp` +or a selected managed version. See inference/src/managed_runtime/paths.rs and +operations/state_transitions.rs:runtime_install_dir_for_projection. On the +supported test platforms the current installation validator reports required +files missing; operations/projection.rs and neutral_contracts.rs preserve that +Missing state rather than marking a runtime Ready. + +Keep the fixture's legacy files and assert the honest neutral projection: +llama_cpp/runtime_sidecar exists, install and readiness states are Missing, +available is false, and missing_files is nonempty. All media dependency listing, +staging/install/remove and conversion-capability assertions remain. This repairs +the test's evidence claim without fabricating a managed installation or weakening +production readiness rules. + +Rustfmt and whitespace pass. Independent source review and hosted execution are +pending; no local Rust build was attempted. The validation/publication API bridge +is separately reviewed, and native/C# plus aggregate lint/Clippy/audit qualification +remain open. + +Independent integrator source review accepted staged tree +`2e98f08e0c780bd5becc8a74925dac9bbd252ec6` as an explicit regression that +legacy-location files cannot establish a current managed installation. Hosted +execution remains required. From 8a8dcbcc0a58aafc0f8e09d7853078856ad78834 Mon Sep 17 00:00:00 2001 From: Puma Date: Fri, 2 Oct 2026 23:55:03 -0700 Subject: [PATCH 14/65] fix(uniffi): publish owner-derived validation before native execution --- .../Program.cs | 51 ++++ .../README.md | 8 +- .../csharp/Pantograph.NativeSmoke/Program.cs | 54 ++++ .../src/embedded_workflow_graph_api.rs | 36 ++- .../src/workflow_service_composition.rs | 2 +- crates/pantograph-uniffi/src/runtime.rs | 47 +++- crates/pantograph-uniffi/src/runtime_tests.rs | 22 +- .../src/runtime_validation_tests.rs | 240 ++++++++++++++++++ .../src/graph/inference_interface_request.rs | 14 +- .../src/graph/mod.rs | 1 + .../src/workflow/attribution_api.rs | 1 + .../executable_validation_snapshot.rs | 144 ++++++++++- .../src/workflow/session_execution_api.rs | 2 +- .../workflow/task_execution_classification.rs | 11 + .../src/workflow/tests/session_execution.rs | 4 +- .../src/workflow/tests/workflow_version.rs | 11 +- docs/headless-workflow.md | 36 +++ .../plan.md | 35 +++ ...03-uniffi-validation-publication-bridge.md | 83 ++++++ scripts/check-uniffi-csharp-smoke.sh | 2 + .../check-uniffi-embedded-runtime-surface.sh | 2 + 21 files changed, 768 insertions(+), 38 deletions(-) create mode 100644 crates/pantograph-uniffi/src/runtime_validation_tests.rs create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-validation-publication-bridge.md diff --git a/bindings/csharp/Pantograph.DirectRuntimeQuickstart/Program.cs b/bindings/csharp/Pantograph.DirectRuntimeQuickstart/Program.cs index 3d0c78e9a..a3f544739 100644 --- a/bindings/csharp/Pantograph.DirectRuntimeQuickstart/Program.cs +++ b/bindings/csharp/Pantograph.DirectRuntimeQuickstart/Program.cs @@ -29,6 +29,7 @@ if (args.Contains("--run-session", StringComparer.Ordinal)) { + await PublishSavedWorkflow(runtime, workflowId, savedPath); await RunWorkflowSession(runtime, workflowId); } else @@ -173,6 +174,56 @@ await runtime.WorkflowGraphCloseEditSession( $$"""{"session_id":{{JsonSerializer.Serialize(editSessionId)}}}"""); } +static async Task PublishSavedWorkflow( + FfiPantographRuntime runtime, string workflowId, string savedPath) +{ + string fileJson = runtime.WorkflowGraphLoad(JsonSerializer.Serialize(new { path = savedPath })); + using JsonDocument file = JsonDocument.Parse(fileJson); + string editJson = await runtime.WorkflowGraphCreateEditSession(JsonSerializer.Serialize(new + { + workflow_id = workflowId, + graph = file.RootElement.GetProperty("graph"), + })); + string editSessionId = ReadString(editJson, "session_id"); + string graphRevision = ReadString(editJson, "graph_revision"); + try + { + string validationJson = await runtime.WorkflowGraphRefreshCurrentValidationSummary( + JsonSerializer.Serialize(new + { + graph_session_id = editSessionId, + graph_revision = graphRevision, + })); + using JsonDocument validation = JsonDocument.Parse(validationJson); + JsonElement summary = validation.RootElement.GetProperty("summary"); + if (summary.GetProperty("state").GetString() != "current" + || !summary.GetProperty("submit_gate").GetProperty("allowed").GetBoolean()) + { + throw new InvalidOperationException($"Workflow validation is not executable: {validationJson}"); + } + string validationSessionId = summary.GetProperty("validation_session_id").GetString() + ?? throw new InvalidOperationException("Missing validation session identity"); + string publicationJson = await runtime.PublishGraphSessionExecutableValidationSnapshot( + JsonSerializer.Serialize(new + { + workflow_id = workflowId, + workflow_semantic_version = "0.1.0", + graph_session_id = editSessionId, + validation_session_id = validationSessionId, + })); + if (ReadString(publicationJson, "validation_session_id") != validationSessionId + || string.IsNullOrWhiteSpace(ReadString(publicationJson, "validation_snapshot_id"))) + { + throw new InvalidOperationException($"Unexpected publication identity: {publicationJson}"); + } + } + finally + { + await runtime.WorkflowGraphCloseEditSession( + JsonSerializer.Serialize(new { session_id = editSessionId })); + } +} + static async Task RunWorkflowSession(FfiPantographRuntime runtime, string workflowId) { string createResponse = await runtime.WorkflowCreateSession( diff --git a/bindings/csharp/Pantograph.DirectRuntimeQuickstart/README.md b/bindings/csharp/Pantograph.DirectRuntimeQuickstart/README.md index e9b35c826..ca8bc7575 100644 --- a/bindings/csharp/Pantograph.DirectRuntimeQuickstart/README.md +++ b/bindings/csharp/Pantograph.DirectRuntimeQuickstart/README.md @@ -36,5 +36,11 @@ your workflow and append `--run-session`. - Loads the saved workflow through `WorkflowGraphLoad`. - Opens a graph edit session, mutates node data, saves the edited graph, and closes the graph edit session. -- When `--run-session` is passed: opens a workflow execution session, submits a +- When `--run-session` is passed: refreshes owner validation for the saved graph, + publishes its current executable snapshot, closes that edit session, opens a + workflow execution session, submits a session run, prints outputs, and closes the workflow execution session. + +The native runtime's attribution store is ephemeral. The example publishes again +each time it opens the runtime. Missing or stale validation is an error; the +example does not construct snapshot content or bypass admission. diff --git a/bindings/csharp/Pantograph.NativeSmoke/Program.cs b/bindings/csharp/Pantograph.NativeSmoke/Program.cs index a46504dec..6b32a1055 100644 --- a/bindings/csharp/Pantograph.NativeSmoke/Program.cs +++ b/bindings/csharp/Pantograph.NativeSmoke/Program.cs @@ -46,6 +46,8 @@ private static async Task RunTextSmoke(FfiPantographRuntime runtime, string proj { WriteTextWorkflow(projectRoot, TextWorkflowId); ExerciseGraphAuthoringDiscovery(runtime); + await PublishSavedWorkflow(runtime, TextWorkflowId, + Path.Combine(projectRoot, ".pantograph", "workflows", $"{TextWorkflowId}.json")); string createResponse = await runtime.WorkflowCreateSession( $$"""{"workflow_id":"{{TextWorkflowId}}","keep_alive":true}"""); @@ -113,6 +115,56 @@ private static void ExerciseGraphAuthoringDiscovery(FfiPantographRuntime runtime } } + private static async Task PublishSavedWorkflow( + FfiPantographRuntime runtime, string workflowId, string savedPath) + { + string fileJson = runtime.WorkflowGraphLoad(JsonSerializer.Serialize(new { path = savedPath })); + using JsonDocument file = JsonDocument.Parse(fileJson); + string editJson = await runtime.WorkflowGraphCreateEditSession(JsonSerializer.Serialize(new + { + workflow_id = workflowId, + graph = file.RootElement.GetProperty("graph"), + })); + string editSessionId = ReadString(editJson, "session_id"); + string graphRevision = ReadString(editJson, "graph_revision"); + try + { + string validationJson = await runtime.WorkflowGraphRefreshCurrentValidationSummary( + JsonSerializer.Serialize(new + { + graph_session_id = editSessionId, + graph_revision = graphRevision, + })); + using JsonDocument validation = JsonDocument.Parse(validationJson); + JsonElement summary = validation.RootElement.GetProperty("summary"); + if (summary.GetProperty("state").GetString() != "current" + || !summary.GetProperty("submit_gate").GetProperty("allowed").GetBoolean()) + { + throw new InvalidOperationException($"Workflow validation is not executable: {validationJson}"); + } + string validationSessionId = summary.GetProperty("validation_session_id").GetString() + ?? throw new InvalidOperationException("Missing validation session identity"); + string publicationJson = await runtime.PublishGraphSessionExecutableValidationSnapshot( + JsonSerializer.Serialize(new + { + workflow_id = workflowId, + workflow_semantic_version = "0.1.0", + graph_session_id = editSessionId, + validation_session_id = validationSessionId, + })); + if (ReadString(publicationJson, "validation_session_id") != validationSessionId + || string.IsNullOrWhiteSpace(ReadString(publicationJson, "validation_snapshot_id"))) + { + throw new InvalidOperationException($"Unexpected publication identity: {publicationJson}"); + } + } + finally + { + await runtime.WorkflowGraphCloseEditSession( + JsonSerializer.Serialize(new { session_id = editSessionId })); + } + } + private static async Task RunDiffusionSmoke(FfiPantographRuntime runtime, string projectRoot) { string modelId = RequireEnv("PANTOGRAPH_DIFFUSION_SMOKE_PUMAS_MODEL_ID"); @@ -123,6 +175,8 @@ private static async Task RunDiffusionSmoke(FfiPantographRuntime runtime, string string outputPath = Environment.GetEnvironmentVariable("PANTOGRAPH_DIFFUSION_SMOKE_OUTPUT") ?? ""; WriteDiffusionWorkflow(projectRoot, DiffusionWorkflowId, modelId, pumasArtifactId); + await PublishSavedWorkflow(runtime, DiffusionWorkflowId, + Path.Combine(projectRoot, ".pantograph", "workflows", $"{DiffusionWorkflowId}.json")); string createResponse = await runtime.WorkflowCreateSession( $$"""{"workflow_id":"{{DiffusionWorkflowId}}","keep_alive":true}"""); diff --git a/crates/pantograph-embedded-runtime/src/embedded_workflow_graph_api.rs b/crates/pantograph-embedded-runtime/src/embedded_workflow_graph_api.rs index a70395234..162466fe6 100644 --- a/crates/pantograph-embedded-runtime/src/embedded_workflow_graph_api.rs +++ b/crates/pantograph-embedded-runtime/src/embedded_workflow_graph_api.rs @@ -1,15 +1,17 @@ use pantograph_workflow_service::{ ConnectionCandidatesResponse, ConnectionCommitResponse, EdgeInsertionPreviewResponse, FileSystemWorkflowGraphStore, InsertNodeConnectionResponse, InsertNodeOnEdgeResponse, - WorkflowFile, WorkflowGraphAddEdgeRequest, WorkflowGraphAddNodeRequest, - WorkflowGraphConnectRequest, WorkflowGraphEditSessionCloseRequest, - WorkflowGraphEditSessionCloseResponse, WorkflowGraphEditSessionCreateRequest, - WorkflowGraphEditSessionCreateResponse, WorkflowGraphEditSessionGraphRequest, - WorkflowGraphEditSessionGraphResponse, WorkflowGraphGetConnectionCandidatesRequest, - WorkflowGraphInsertNodeAndConnectRequest, WorkflowGraphInsertNodeOnEdgeRequest, - WorkflowGraphListResponse, WorkflowGraphLoadRequest, + ValidatedWorkflowExecutableValidationSnapshotRecord, WorkflowFile, WorkflowGraphAddEdgeRequest, + WorkflowGraphAddNodeRequest, WorkflowGraphConnectRequest, + WorkflowGraphCurrentValidationRefreshRequest, WorkflowGraphCurrentValidationRefreshResponse, + WorkflowGraphEditSessionCloseRequest, WorkflowGraphEditSessionCloseResponse, + WorkflowGraphEditSessionCreateRequest, WorkflowGraphEditSessionCreateResponse, + WorkflowGraphEditSessionGraphRequest, WorkflowGraphEditSessionGraphResponse, + WorkflowGraphGetConnectionCandidatesRequest, WorkflowGraphInsertNodeAndConnectRequest, + WorkflowGraphInsertNodeOnEdgeRequest, WorkflowGraphListResponse, WorkflowGraphLoadRequest, WorkflowGraphPreviewNodeInsertOnEdgeRequest, WorkflowGraphRemoveEdgeRequest, WorkflowGraphRemoveNodeRequest, WorkflowGraphSaveRequest, WorkflowGraphSaveResponse, + WorkflowGraphSessionExecutableValidationSnapshotPublishRequest, WorkflowGraphUndoRedoStateRequest, WorkflowGraphUndoRedoStateResponse, WorkflowGraphUpdateNodeDataRequest, WorkflowGraphUpdateNodePositionRequest, WorkflowServiceError, @@ -18,6 +20,26 @@ use pantograph_workflow_service::{ use crate::EmbeddedRuntime; impl EmbeddedRuntime { + /// Refresh validation from the current server-owned graph edit session. + pub async fn workflow_graph_refresh_current_validation_summary( + &self, + request: WorkflowGraphCurrentValidationRefreshRequest, + ) -> Result { + self.workflow_service + .workflow_graph_refresh_current_validation_summary(request) + .await + } + + /// Publish executable validation from current graph-session provenance. + pub async fn publish_graph_session_executable_validation_snapshot( + &self, + request: WorkflowGraphSessionExecutableValidationSnapshotPublishRequest, + ) -> Result { + self.workflow_service + .publish_graph_session_executable_validation_snapshot(request) + .await + } + pub(crate) fn graph_store(&self) -> FileSystemWorkflowGraphStore { FileSystemWorkflowGraphStore::new(self.config.project_root.clone()) } diff --git a/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs b/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs index f47e520d9..87b076248 100644 --- a/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs +++ b/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs @@ -1012,7 +1012,7 @@ mod tests { })); let projections = snapshot - .scheduler_inference_task_projections() + .scheduler_inference_task_projections(&connected_dependency_inference_graph(model_id)) .expect("scheduler projections"); let scheduler_node_id = "infer".parse().expect("scheduler node id"); let projection = projections diff --git a/crates/pantograph-uniffi/src/runtime.rs b/crates/pantograph-uniffi/src/runtime.rs index 39aff8266..1e4ab0b11 100644 --- a/crates/pantograph-uniffi/src/runtime.rs +++ b/crates/pantograph-uniffi/src/runtime.rs @@ -17,14 +17,17 @@ use pantograph_workflow_service::{ WorkflowExecutionSessionQueueListRequest, WorkflowExecutionSessionQueuePushFrontRequest, WorkflowExecutionSessionQueueReprioritizeRequest, WorkflowExecutionSessionRunRequest, WorkflowExecutionSessionStatusRequest, WorkflowGraphAddEdgeRequest, - WorkflowGraphAddNodeRequest, WorkflowGraphConnectRequest, WorkflowGraphEditSessionCloseRequest, + WorkflowGraphAddNodeRequest, WorkflowGraphConnectRequest, + WorkflowGraphCurrentValidationRefreshRequest, WorkflowGraphEditSessionCloseRequest, WorkflowGraphEditSessionCreateRequest, WorkflowGraphEditSessionGraphRequest, WorkflowGraphGetConnectionCandidatesRequest, WorkflowGraphInsertNodeAndConnectRequest, WorkflowGraphInsertNodeOnEdgeRequest, WorkflowGraphLoadRequest, WorkflowGraphPreviewNodeInsertOnEdgeRequest, WorkflowGraphRemoveEdgeRequest, - WorkflowGraphRemoveNodeRequest, WorkflowGraphSaveRequest, WorkflowGraphUndoRedoStateRequest, - WorkflowGraphUpdateNodeDataRequest, WorkflowGraphUpdateNodePositionRequest, WorkflowIoRequest, - WorkflowPreflightRequest, WorkflowService, WorkflowServiceError, + WorkflowGraphRemoveNodeRequest, WorkflowGraphSaveRequest, + WorkflowGraphSessionExecutableValidationSnapshotPublishRequest, + WorkflowGraphUndoRedoStateRequest, WorkflowGraphUpdateNodeDataRequest, + WorkflowGraphUpdateNodePositionRequest, WorkflowIoRequest, WorkflowPreflightRequest, + WorkflowService, WorkflowServiceError, }; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tokio::sync::RwLock; @@ -761,6 +764,38 @@ impl FfiPantographRuntime { serialize_response(&response) } + /// Refresh owner-derived graph validation and return WorkflowGraphCurrentValidationRefreshResponse JSON. + pub async fn workflow_graph_refresh_current_validation_summary( + &self, + request_json: String, + ) -> Result { + let request: WorkflowGraphCurrentValidationRefreshRequest = parse_request(request_json)?; + let response = self + .runtime + .workflow_graph_refresh_current_validation_summary(request) + .await + .map_err(map_workflow_service_error)?; + serialize_response(&response) + } + + /// Publish the current graph-session validation as WorkflowExecutableValidationSnapshotRecord JSON. + /// + /// Snapshot provenance is derived by the workflow service, never supplied by + /// the caller. This runtime's attribution store is ephemeral: republish after reopening it. + pub async fn publish_graph_session_executable_validation_snapshot( + &self, + request_json: String, + ) -> Result { + let request: WorkflowGraphSessionExecutableValidationSnapshotPublishRequest = + parse_request(request_json)?; + let response = self + .runtime + .publish_graph_session_executable_validation_snapshot(request) + .await + .map_err(map_workflow_service_error)?; + serialize_response(response.as_record()) + } + /// Close a graph edit session and return WorkflowGraphEditSessionCloseResponse JSON. pub async fn workflow_graph_close_edit_session( &self, @@ -1077,3 +1112,7 @@ fn workflow_adapter_error(code: WorkflowErrorCode, message: impl Into) - #[cfg(test)] #[path = "runtime_tests.rs"] mod runtime_tests; + +#[cfg(test)] +#[path = "runtime_validation_tests.rs"] +mod runtime_validation_tests; diff --git a/crates/pantograph-uniffi/src/runtime_tests.rs b/crates/pantograph-uniffi/src/runtime_tests.rs index 4bd9a3c92..9b3492f5e 100644 --- a/crates/pantograph-uniffi/src/runtime_tests.rs +++ b/crates/pantograph-uniffi/src/runtime_tests.rs @@ -10,7 +10,7 @@ use pantograph_workflow_service::{WorkflowErrorCode, WorkflowErrorEnvelope}; use super::{FfiEmbeddedRuntimeConfig, FfiPantographRuntime}; use crate::FfiError; -fn create_temp_root(workflow_id: &str) -> PathBuf { +pub(super) fn create_temp_root(workflow_id: &str) -> PathBuf { let suffix = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("clock") @@ -270,7 +270,7 @@ fn write_human_input_workflow(root: &Path, workflow_id: &str) { .expect("write workflow"); } -fn workflow_error_envelope(err: FfiError) -> WorkflowErrorEnvelope { +pub(super) fn workflow_error_envelope(err: FfiError) -> WorkflowErrorEnvelope { let message = match err { FfiError::Other { message } => message, other => panic!("expected FfiError::Other with envelope JSON, got {other:?}"), @@ -295,6 +295,8 @@ async fn direct_runtime_runs_workflow_session_from_json() { .await .expect("runtime"); + super::runtime_validation_tests::publish_saved_workflow(&runtime, &root, workflow_id).await; + let create_session_json = runtime .workflow_create_session( serde_json::json!({ @@ -375,7 +377,7 @@ async fn direct_runtime_runs_workflow_session_from_json() { } #[tokio::test] -async fn direct_runtime_workflow_session_run_preserves_invalid_request_envelope() { +async fn direct_runtime_rejects_interactive_graph_at_scheduler_boundary() { let workflow_id = "uniffi-runtime-interactive-run"; let root = create_temp_root(workflow_id); write_human_input_workflow(&root, workflow_id); @@ -392,6 +394,8 @@ async fn direct_runtime_workflow_session_run_preserves_invalid_request_envelope( .await .expect("runtime"); + super::runtime_validation_tests::publish_saved_workflow(&runtime, &root, workflow_id).await; + let create_session_json = runtime .workflow_create_session( serde_json::json!({ @@ -421,14 +425,14 @@ async fn direct_runtime_workflow_session_run_preserves_invalid_request_envelope( .to_string(), ) .await - .expect_err("interactive workflow session run should preserve invalid-request envelope"); + .expect_err("interactive workflow must remain unsupported by the scheduler"); let envelope = workflow_error_envelope(err); - assert_eq!(envelope.code, WorkflowErrorCode::InvalidRequest); - assert_eq!( - envelope.message, - "workflow 'uniffi-runtime-interactive-run' requires interactive input at node 'human-input-1'" - ); + assert_eq!(envelope.code, WorkflowErrorCode::CapabilityViolation); + assert!(envelope + .message + .contains("scheduler task session runner has no execution path")); + assert!(envelope.message.contains("unsupported=1")); runtime.shutdown().await; let _ = std::fs::remove_dir_all(root); diff --git a/crates/pantograph-uniffi/src/runtime_validation_tests.rs b/crates/pantograph-uniffi/src/runtime_validation_tests.rs new file mode 100644 index 000000000..b1c09bfa7 --- /dev/null +++ b/crates/pantograph-uniffi/src/runtime_validation_tests.rs @@ -0,0 +1,240 @@ +use std::path::Path; +use std::sync::Arc; + +use pantograph_workflow_service::WorkflowErrorCode; +use serde_json::{json, Value}; + +use super::runtime_tests::{create_temp_root, workflow_error_envelope}; +use super::{FfiEmbeddedRuntimeConfig, FfiPantographRuntime}; + +async fn open_runtime(root: &Path) -> Arc { + FfiPantographRuntime::new( + FfiEmbeddedRuntimeConfig { + app_data_dir: root.join("app-data").to_string_lossy().into_owned(), + project_root: root.to_string_lossy().into_owned(), + workflow_roots: Vec::new(), + max_loaded_sessions: None, + }, + None, + ) + .await + .expect("open runtime") +} + +fn parse(response: String) -> Value { + serde_json::from_str(&response).expect("public response JSON") +} + +async fn refresh_saved_graph( + runtime: &FfiPantographRuntime, + root: &Path, + workflow_id: &str, +) -> (Value, Value) { + let file = parse( + runtime + .workflow_graph_load( + json!({ + "path": root.join(".pantograph/workflows").join(format!("{workflow_id}.json")) + }) + .to_string(), + ) + .expect("load saved graph"), + ); + let edit = parse( + runtime + .workflow_graph_create_edit_session( + json!({ + "workflow_id": workflow_id, + "graph": file["graph"], + }) + .to_string(), + ) + .await + .expect("create edit session"), + ); + let validation = parse( + runtime + .workflow_graph_refresh_current_validation_summary( + json!({ + "graph_session_id": edit["session_id"], + "graph_revision": edit["graph_revision"], + }) + .to_string(), + ) + .await + .expect("refresh owner validation"), + ); + assert_eq!(validation["summary"]["state"], "current"); + assert_eq!(validation["summary"]["submit_gate"]["allowed"], true); + assert!(validation["summary"]["validation_session_id"] + .as_str() + .is_some()); + (edit, validation) +} + +fn publication_request(workflow_id: &str, edit: &Value, validation: &Value) -> Value { + json!({ + "workflow_id": workflow_id, + "workflow_semantic_version": "0.1.0", + "graph_session_id": edit["session_id"], + "validation_session_id": validation["summary"]["validation_session_id"], + }) +} + +pub(super) async fn publish_saved_workflow( + runtime: &FfiPantographRuntime, + root: &Path, + workflow_id: &str, +) -> Value { + let (edit, validation) = refresh_saved_graph(runtime, root, workflow_id).await; + let published = parse( + runtime + .publish_graph_session_executable_validation_snapshot( + publication_request(workflow_id, &edit, &validation).to_string(), + ) + .await + .expect("publish owner-derived executable snapshot"), + ); + assert_eq!(published["workflow_id"], workflow_id); + assert_eq!( + published["validation_session_id"], + validation["summary"]["validation_session_id"] + ); + assert!(published["validation_snapshot_id"] + .as_str() + .is_some_and(|id| !id.is_empty())); + runtime + .workflow_graph_close_edit_session(json!({"session_id": edit["session_id"]}).to_string()) + .await + .expect("close published edit session"); + published +} + +async fn run_text( + runtime: &FfiPantographRuntime, + workflow_id: &str, +) -> Result { + let session = parse( + runtime + .workflow_create_session( + json!({ + "workflow_id": workflow_id, + "keep_alive": false, + }) + .to_string(), + ) + .await + .expect("create execution session"), + ); + let result = runtime.workflow_run_session(json!({ + "session_id": session["session_id"], + "workflow_semantic_version": "0.1.0", + "inputs": [{"node_id": "text-input-1", "port_id": "text", "value": "published text"}], + "output_targets": [{"node_id": "text-output-1", "port_id": "text"}], + }).to_string()).await; + runtime + .workflow_close_session(json!({"session_id": session["session_id"]}).to_string()) + .await + .expect("close execution session"); + result +} + +fn assert_missing_publication(error: crate::FfiError) { + let envelope = workflow_error_envelope(error); + assert_eq!(envelope.code, WorkflowErrorCode::InvalidRequest); + assert_eq!( + envelope.message, + "saved executable validation snapshot was not found for workflow version" + ); + let diagnostics = envelope.diagnostics.expect("run admission diagnostics"); + assert!(diagnostics + .workflow_run_id + .as_deref() + .is_some_and(|id| !id.is_empty())); +} + +#[tokio::test] +async fn public_validation_publication_is_required_again_after_runtime_reopen() { + let workflow_id = "uniffi-publication-reopen"; + let root = create_temp_root(workflow_id); + let runtime = open_runtime(&root).await; + assert_missing_publication( + run_text(&runtime, workflow_id) + .await + .expect_err("missing publication"), + ); + publish_saved_workflow(&runtime, &root, workflow_id).await; + let response = parse( + run_text(&runtime, workflow_id) + .await + .expect("published text runs"), + ); + assert_eq!(response["outputs"][0]["value"], "published text"); + runtime.shutdown().await; + drop(runtime); + + let reopened = open_runtime(&root).await; + assert_missing_publication( + run_text(&reopened, workflow_id) + .await + .expect_err("ephemeral snapshot is absent"), + ); + publish_saved_workflow(&reopened, &root, workflow_id).await; + let response = parse( + run_text(&reopened, workflow_id) + .await + .expect("republished text runs"), + ); + assert_eq!(response["outputs"][0]["value"], "published text"); + reopened.shutdown().await; + drop(reopened); + std::fs::remove_dir_all(root).expect("remove fixture"); +} + +#[tokio::test] +async fn public_publication_rejects_stale_validation_and_caller_supplied_proof() { + let workflow_id = "uniffi-publication-stale"; + let root = create_temp_root(workflow_id); + let runtime = open_runtime(&root).await; + let (edit, validation) = refresh_saved_graph(&runtime, &root, workflow_id).await; + let request = publication_request(workflow_id, &edit, &validation); + let mut forged_request = request.clone(); + forged_request["validation_publication"] = json!({"status": "executable"}); + let error = runtime + .publish_graph_session_executable_validation_snapshot(forged_request.to_string()) + .await + .expect_err("caller proof is not accepted"); + let envelope = workflow_error_envelope(error); + assert_eq!(envelope.code, WorkflowErrorCode::InvalidRequest); + assert!(envelope.message.contains("unknown field")); + + let changed = parse( + runtime + .workflow_graph_update_node_data( + json!({ + "session_id": edit["session_id"], + "node_id": "text-input-1", + "data": {"text": "changed after validation"}, + }) + .to_string(), + ) + .await + .expect("mutate graph"), + ); + assert_ne!(changed["graph_revision"], edit["graph_revision"]); + let error = runtime + .publish_graph_session_executable_validation_snapshot(request.to_string()) + .await + .expect_err("stale validation cannot be published"); + let envelope = workflow_error_envelope(error); + assert_eq!(envelope.code, WorkflowErrorCode::InvalidRequest); + assert!(!envelope.message.is_empty()); + assert_missing_publication( + run_text(&runtime, workflow_id) + .await + .expect_err("rejected publication stores no executable snapshot"), + ); + runtime.shutdown().await; + drop(runtime); + std::fs::remove_dir_all(root).expect("remove fixture"); +} diff --git a/crates/pantograph-workflow-service/src/graph/inference_interface_request.rs b/crates/pantograph-workflow-service/src/graph/inference_interface_request.rs index ef991f7b2..527db7027 100644 --- a/crates/pantograph-workflow-service/src/graph/inference_interface_request.rs +++ b/crates/pantograph-workflow-service/src/graph/inference_interface_request.rs @@ -73,6 +73,14 @@ pub enum InferenceInterfaceGraphResolutionDiagnosticCode { InvalidRuntimeSourceContext, } +/// Canonical graph nodes requiring inference-interface and dependency proofs. +pub(crate) fn inference_nodes_in_graph(graph: &WorkflowGraph) -> impl Iterator { + graph + .nodes + .iter() + .filter(|node| node.node_type == NODE_TYPE_GENERIC_INFERENCE) +} + pub fn inference_interface_resolution_inputs_from_graph( graph: &WorkflowGraph, ) -> InferenceInterfaceGraphResolutionInputs { @@ -85,11 +93,7 @@ pub fn inference_interface_resolution_inputs_from_graph( let mut requests = Vec::new(); let mut diagnostics = Vec::new(); - for node in graph - .nodes - .iter() - .filter(|node| node.node_type == NODE_TYPE_GENERIC_INFERENCE) - { + for node in inference_nodes_in_graph(graph) { let Some(model_ref) = model_ref_for_inference_node( &node.id, &node.data, diff --git a/crates/pantograph-workflow-service/src/graph/mod.rs b/crates/pantograph-workflow-service/src/graph/mod.rs index 764c5c7b5..4dad3ac73 100644 --- a/crates/pantograph-workflow-service/src/graph/mod.rs +++ b/crates/pantograph-workflow-service/src/graph/mod.rs @@ -83,6 +83,7 @@ pub use inference_interface_publication::{ InferenceInterfaceNodeProjectionRecord, InferenceInterfacePublicationError, WorkflowGraphInferenceValidationPublication, }; +pub(crate) use inference_interface_request::inference_nodes_in_graph; pub use inference_interface_request::{ inference_interface_resolution_inputs_from_graph, InferenceInterfaceGraphResolutionDiagnostic, InferenceInterfaceGraphResolutionDiagnosticCode, InferenceInterfaceGraphResolutionInput, diff --git a/crates/pantograph-workflow-service/src/workflow/attribution_api.rs b/crates/pantograph-workflow-service/src/workflow/attribution_api.rs index cadd1d78a..26c9f626e 100644 --- a/crates/pantograph-workflow-service/src/workflow/attribution_api.rs +++ b/crates/pantograph-workflow-service/src/workflow/attribution_api.rs @@ -277,6 +277,7 @@ impl WorkflowService { &workflow_version, snapshot_id, &source, + &graph, ) .map_err(workflow_executable_validation_snapshot_service_error)?; self.reject_changed_dependency_proof_freshness(&snapshot)?; diff --git a/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs b/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs index b72ec9b1e..1dc4b8b5b 100644 --- a/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs +++ b/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs @@ -204,6 +204,7 @@ impl WorkflowExecutableValidationSnapshotRecord { workflow_version: &WorkflowVersionRecord, validation_snapshot_id: WorkflowExecutableValidationSnapshotId, source: &CurrentExecutableValidationSnapshotSource, + graph: &WorkflowGraph, ) -> Result { let nodes = source .nodes @@ -223,7 +224,7 @@ impl WorkflowExecutableValidationSnapshotRecord { validation_summary: source.validation_summary.clone(), nodes, }; - snapshot.validate()?; + snapshot.validate_graph_inference_coverage(graph)?; Ok(snapshot) } @@ -263,10 +264,6 @@ impl WorkflowExecutableValidationSnapshotRecord { self.nodes.len(), WORKFLOW_EXECUTABLE_VALIDATION_SNAPSHOT_MAX_NODES, )?; - if self.nodes.is_empty() { - return Err(WorkflowExecutableValidationSnapshotError::MissingNodes); - } - let mut seen_nodes = BTreeSet::new(); for node in &self.nodes { node.validate()?; @@ -279,6 +276,45 @@ impl WorkflowExecutableValidationSnapshotRecord { Ok(()) } + /// Validate contextual coverage before publication or executable projection. + /// Empty inference coverage is valid for a graph with no inference nodes. + fn validate_graph_inference_coverage( + &self, + graph: &WorkflowGraph, + ) -> Result<(), WorkflowExecutableValidationSnapshotError> { + self.validate()?; + let mut expected = BTreeSet::new(); + for node in crate::graph::inference_nodes_in_graph(graph) { + if !expected.insert(node.id.as_str()) { + return Err( + WorkflowExecutableValidationSnapshotError::DuplicateGraphInferenceNode { + node_id: node.id.clone(), + }, + ); + } + } + let actual = self + .nodes + .iter() + .map(|node| node.node_id.as_str()) + .collect::>(); + if actual != expected { + return Err( + WorkflowExecutableValidationSnapshotError::InferenceNodeCoverageMismatch { + missing_node_ids: expected + .difference(&actual) + .map(|id| (*id).to_string()) + .collect(), + unexpected_node_ids: actual + .difference(&expected) + .map(|id| (*id).to_string()) + .collect(), + }, + ); + } + Ok(()) + } + pub fn to_attribution_store_request( &self, ) -> Result< @@ -574,8 +610,10 @@ impl ValidatedWorkflowExecutableValidationSnapshotRecord { pub fn scheduler_inference_task_projections( &self, + graph: &WorkflowGraph, ) -> Result { + self.0.validate_graph_inference_coverage(graph)?; let records = self .0 .nodes @@ -764,6 +802,13 @@ pub enum WorkflowExecutableValidationSnapshotError { NonExecutableSummary { status: DraftGraphValidationStatus }, #[error("executable validation snapshot must contain at least one inference node")] MissingNodes, + #[error("duplicate graph inference node '{node_id}'")] + DuplicateGraphInferenceNode { node_id: String }, + #[error("inference snapshot coverage mismatch: missing {missing_node_ids:?}, unexpected {unexpected_node_ids:?}")] + InferenceNodeCoverageMismatch { + missing_node_ids: Vec, + unexpected_node_ids: Vec, + }, #[error("duplicate executable validation snapshot node '{node_id}'")] DuplicateNode { node_id: WorkflowNodeId }, #[error("node '{node_id}' model ref is invalid: {message}")] @@ -1093,6 +1138,92 @@ mod tests { use super::*; + fn inference_graph_fixture() -> WorkflowGraph { + WorkflowGraph { + nodes: vec![crate::graph::GraphNode { + id: "infer_node".to_string(), + node_type: "llm-inference".to_string(), + position: crate::graph::Position { x: 0.0, y: 0.0 }, + data: serde_json::json!({}), + }], + edges: Vec::new(), + derived_graph: None, + } + } + + #[test] + fn empty_snapshot_projects_only_for_zero_inference_graph() { + let mut snapshot = snapshot_fixture(); + snapshot.nodes.clear(); + let imported: WorkflowExecutableValidationSnapshotRecord = serde_json::from_str( + &serde_json::to_string(&snapshot).expect("serialize empty snapshot"), + ) + .expect("deserialize empty snapshot"); + let validated = ValidatedWorkflowExecutableValidationSnapshotRecord::try_from(imported) + .expect("empty structurally executable record"); + assert!(validated + .scheduler_inference_task_projections(&WorkflowGraph::new()) + .is_ok()); + assert!(matches!( + validated.scheduler_inference_task_projections(&inference_graph_fixture()), + Err(WorkflowExecutableValidationSnapshotError::InferenceNodeCoverageMismatch { missing_node_ids, unexpected_node_ids }) + if missing_node_ids == vec!["infer_node"] && unexpected_node_ids.is_empty() + )); + } + + #[test] + fn projection_rejects_extra_and_duplicate_inference_nodes() { + let snapshot = snapshot_fixture(); + let validated = + ValidatedWorkflowExecutableValidationSnapshotRecord::try_from(snapshot.clone()) + .expect("valid runtime snapshot"); + assert!(matches!( + validated.scheduler_inference_task_projections(&WorkflowGraph::new()), + Err(WorkflowExecutableValidationSnapshotError::InferenceNodeCoverageMismatch { missing_node_ids, unexpected_node_ids }) + if missing_node_ids.is_empty() && unexpected_node_ids == vec!["infer_node"] + )); + let mut duplicate_graph = inference_graph_fixture(); + duplicate_graph.nodes.push(duplicate_graph.nodes[0].clone()); + assert!(matches!( + validated.scheduler_inference_task_projections(&duplicate_graph), + Err(WorkflowExecutableValidationSnapshotError::DuplicateGraphInferenceNode { .. }) + )); + let mut duplicate_snapshot = snapshot; + duplicate_snapshot + .nodes + .push(duplicate_snapshot.nodes[0].clone()); + assert!(matches!( + ValidatedWorkflowExecutableValidationSnapshotRecord::try_from(duplicate_snapshot), + Err(WorkflowExecutableValidationSnapshotError::DuplicateNode { .. }) + )); + } + + #[test] + fn owner_publication_requires_exact_graph_inference_coverage() { + let version = workflow_version_fixture(); + let mut source = snapshot_source_fixture(); + source.nodes.clear(); + let id = WorkflowExecutableValidationSnapshotId::generate(); + assert!( + WorkflowExecutableValidationSnapshotRecord::from_snapshot_source( + &version, + id.clone(), + &source, + &WorkflowGraph::new() + ) + .is_ok() + ); + assert!(matches!( + WorkflowExecutableValidationSnapshotRecord::from_snapshot_source( + &version, + id, + &source, + &inference_graph_fixture() + ), + Err(WorkflowExecutableValidationSnapshotError::InferenceNodeCoverageMismatch { .. }) + )); + } + #[test] fn validation_publication_without_dependency_proof_fails_closed() { let workflow_version = workflow_version_fixture(); @@ -1128,6 +1259,7 @@ mod tests { &workflow_version, snapshot_id.clone(), &source, + &inference_graph_fixture(), ) .expect("source should compact to snapshot"); @@ -1163,7 +1295,7 @@ mod tests { ValidatedWorkflowExecutableValidationSnapshotRecord::try_from(snapshot.clone()) .expect("snapshot should validate"); let projections = validated - .scheduler_inference_task_projections() + .scheduler_inference_task_projections(&inference_graph_fixture()) .expect("snapshot should project scheduler tasks"); let node_id = SchedulerNodeId::parse("infer_node").expect("valid scheduler node id"); diff --git a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs index d8ec105d0..b282e59c3 100644 --- a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs +++ b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs @@ -777,7 +777,7 @@ impl WorkflowService { }, )?; let projections = snapshot - .scheduler_inference_task_projections() + .scheduler_inference_task_projections(&graph) .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?; return workflow_scheduler_task_graph_with_inference_projections( &workflow_id, diff --git a/crates/pantograph-workflow-service/src/workflow/task_execution_classification.rs b/crates/pantograph-workflow-service/src/workflow/task_execution_classification.rs index 5740e7f8c..e722d8699 100644 --- a/crates/pantograph-workflow-service/src/workflow/task_execution_classification.rs +++ b/crates/pantograph-workflow-service/src/workflow/task_execution_classification.rs @@ -132,6 +132,17 @@ mod tests { ); } + #[test] + fn other_inference_bearing_node_types_remain_unsupported() { + let mut other = contract("llm-inference"); + other.node_type = "future-inference".parse().expect("valid node type"); + assert!(!other.inference_tasks.is_empty()); + assert_eq!( + classify_workflow_scheduler_task("future-inference", Some(&other)), + WorkflowSchedulerTaskExecutionClass::Unsupported, + ); + } + #[test] fn classifier_requires_matching_contract_facts() { let text_contract = contract("text-input"); diff --git a/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs b/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs index 2560ad48f..16411087d 100644 --- a/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs +++ b/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs @@ -1697,7 +1697,7 @@ async fn workflow_execution_session_bootstrap_recovery_applies_progress_loop_bef ) .expect("validated executable snapshot"); let projections = snapshot - .scheduler_inference_task_projections() + .scheduler_inference_task_projections(&graph) .expect("scheduler inference task projections"); let first_created = service @@ -1876,7 +1876,7 @@ async fn workflow_execution_session_bootstrap_recovery_redispatches_ready_runtim ) .expect("validated executable snapshot"); let projections = snapshot - .scheduler_inference_task_projections() + .scheduler_inference_task_projections(&graph) .expect("scheduler inference task projections"); let first_created = service diff --git a/crates/pantograph-workflow-service/src/workflow/tests/workflow_version.rs b/crates/pantograph-workflow-service/src/workflow/tests/workflow_version.rs index f203283b5..762ff0cb0 100644 --- a/crates/pantograph-workflow-service/src/workflow/tests/workflow_version.rs +++ b/crates/pantograph-workflow-service/src/workflow/tests/workflow_version.rs @@ -182,8 +182,15 @@ fn workflow_run_graph_query_returns_none_for_unknown_run() { #[test] fn workflow_executable_validation_snapshot_round_trips_through_attribution() { let service = WorkflowService::with_ephemeral_attribution_store().expect("service"); + let mut graph = graph(); + graph.nodes.push(GraphNode { + id: "infer_node".to_string(), + node_type: "llm-inference".to_string(), + position: Position { x: 0.0, y: 0.0 }, + data: serde_json::json!({}), + }); let version = service - .resolve_workflow_graph_version("workflow-versioned", "1.0.0", &graph()) + .resolve_workflow_graph_version("workflow-versioned", "1.0.0", &graph) .expect("version"); let snapshot = executable_validation_snapshot(&version); @@ -202,7 +209,7 @@ fn workflow_executable_validation_snapshot_round_trips_through_attribution() { assert_eq!(stored.as_record(), &snapshot); assert_eq!(loaded.as_record(), &snapshot); - assert!(loaded.scheduler_inference_task_projections().is_ok()); + assert!(loaded.scheduler_inference_task_projections(&graph).is_ok()); } #[test] diff --git a/docs/headless-workflow.md b/docs/headless-workflow.md index 59ba95d05..618e97806 100644 --- a/docs/headless-workflow.md +++ b/docs/headless-workflow.md @@ -105,3 +105,39 @@ remains correctly unloaded and can be reloaded on retry. An unload failure leave the victim loaded. Missing lifecycle telemetry is reported even when recording the separate canonical error succeeds; lifecycle error text is sanitized without changing the original operational error returned to the caller. + +## Publishing validation before native session execution + +The UniFFI runtime owns an ephemeral attribution store. Before running a saved +workflow version, an embedding client must publish validation through the public +graph-session API: + +1. Load the saved graph with `workflow_graph_load`, or save it with + `workflow_graph_save`, and create a graph edit session for that workflow ID. +2. Call `workflow_graph_refresh_current_validation_summary` with the returned + `graph_session_id` and `graph_revision`. Inspect the returned summary and + require its submit gate to allow execution; unavailable facts remain blocking. +3. Call `publish_graph_session_executable_validation_snapshot` with workflow ID, + semantic version, graph-session ID and the current validation-session ID. + The service derives the graph, descriptor facts and dependency proof from its + own session state. The request accepts identifiers, not a caller-authored proof + or snapshot body, and rejects stale publication. +4. Create an execution session and run the same saved workflow/version through + `workflow_run_session`. A missing snapshot still fails closed. Closing the + graph edit session after successful publication does not erase the published + validation record. + +Reopening `FfiPantographRuntime` starts a fresh attribution store, even when its +app-data directory is reused. Repeat validation/publication before execution; +filesystem persistence of the workflow alone is not evidence of current +executable validation. The bridge does not add support for interactive scheduler +tasks or fabricate inference/runtime readiness. Errors retain the existing JSON +workflow error envelopes. + +Executable snapshots contain inference proofs, so a graph with no inference nodes +has an empty proof list. Publication and executable projection both compare that +list against the canonical graph's complete inference-node set. Empty records +cannot stand in for missing runtime proofs, and extra/duplicate node identities +are rejected. Rust callers of `scheduler_inference_task_projections` must supply +the actual graph for this coverage check; normal run admission also retains its +workflow-version and fingerprint checks. diff --git a/docs/plans/domain-architecture-and-multimodal/plan.md b/docs/plans/domain-architecture-and-multimodal/plan.md index 2b7c5755a..2a3d07860 100644 --- a/docs/plans/domain-architecture-and-multimodal/plan.md +++ b/docs/plans/domain-architecture-and-multimodal/plan.md @@ -877,6 +877,41 @@ appropriate to its recorded lifecycle; the next-slice field is not independent execution authority. The user has now authorized starting the plan with subagents and the bounded implementation pilot. +## UniFFI Validation Publication Bridge (2026-10-03) + +Hosted headless qualification now reaches the UniFFI runtime tests. The FFI +constructor owns an ephemeral attribution store, and execution correctly requires +an owner-published executable validation snapshot. The embedding API exposes +edit sessions but omits validation refresh and snapshot publication, so a valid +text graph cannot currently complete the public create/run flow. + +Accepted bounded repair: expose typed EmbeddedRuntime delegates and strict JSON +UniFFI entrypoints for the existing workflow-service current-validation refresh +and graph-session snapshot publication methods. Keep graph/proof construction, +revision checks, descriptor compatibility and dependency-proof freshness at their +existing owners. A client supplies session/version identifiers, never trusted +snapshot content. It loads/saves a graph, creates an edit session, refreshes the +returned graph revision, publishes its current executable validation session, +then creates/runs an execution session. Reopening the ephemeral FFI runtime +requires publication again. Missing, stale and unavailable validation still fail +closed; runtime inference facts and readiness are not fabricated. + +Required evidence: actual public JSON text execution; missing/stale/reopened +runtime rejection; existing error-envelope fidelity; UniFFI metadata and generated +C# surface checks. Independent read-only review is required before publication. +Canonical selector fixtures and managed-runtime readiness evidence stay separate. + +Independent review found that inference-only snapshots rejected every legitimate +zero-inference graph and that real C# callers had not been migrated. The accepted +correction keeps publication mandatory and checks exact canonical inference-node +coverage at owner publication and executable projection, including imported +records. Empty coverage is valid only for a graph with no inference nodes. Missing, +extra and duplicate runtime projections fail closed; per-node proofs and existing +freshness checks remain. The canonical inference request owner supplies the shared +classification; unsupported inference-bearing task types remain rejected. Both +NativeSmoke and the packaged DirectRuntimeQuickstart must perform the real public +load/edit/refresh/publish lifecycle before execution. + ## Final Acceptance - Acceptance status: `blocked` diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-validation-publication-bridge.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-validation-publication-bridge.md new file mode 100644 index 000000000..758674a94 --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-validation-publication-bridge.md @@ -0,0 +1,83 @@ +# UniFFI Owner-Derived Validation Publication + +## Observed gap and accepted decision + +PR #12 hosted UniFFI tests compiled and reached execution, but saved text and +interactive fixtures failed on missing executable validation snapshots. The FFI +constructor configures ephemeral attribution; the scheduler requires a matching +saved validation snapshot. The API exposed editing but no path to obtain/publish +that validation. This is an embedding API gap, not permission to fabricate a test +snapshot or weaken admission. + +The integrator accepted a thin bridge before implementation; the canonical plan +records it. EmbeddedRuntime delegates typed requests to the existing service +refresh/publication methods. UniFFI accepts their strict JSON DTOs, maps errors +through the existing workflow envelope, and serializes the validated snapshot +record returned by the owner. Publication derives graph and proof provenance from +server-owned session state. Revision, descriptor and dependency-proof freshness +checks remain unchanged. No parallel validator or snapshot constructor is added. + +The public client flow loads a saved graph, creates an edit session, refreshes the +returned revision, checks the submit gate, publishes the returned validation +session for the workflow/version, then runs an execution session. The documented +attribution lifecycle is ephemeral; reopening the runtime requires republishing. +No inference readiness or interactive task execution support is introduced. + +## Evidence and remaining qualification + +The existing text execution test now obtains its snapshot through that public +flow. New tests cover execution after publication and closing the edit session, +missing publication, loss of publication on runtime reopen followed by successful +republishing, stale graph validation, and rejection of caller-supplied proof +fields. The interactive graph retains an explicit rejection regression at the +current unsupported scheduler-task boundary rather than asserting the obsolete +legacy executor's waiting-input message. UniFFI metadata and generated C# checks +require both new methods. + +Rustfmt, shell syntax and staged whitespace are supporting checks only. No local +Rust compilation has been attempted under the shared disk constraint. Independent +read-only source review is required before publication, then exact-head hosted +Rust and generated-binding execution must qualify the bridge. Selector fixtures +are a separate preceding milestone. The managed-runtime readiness fixture still +requires current evidence; aggregate lint, Clippy and audit remain unresolved. + +## Independent review correction + +Historical checkpoint `0e5496ce362002d11379d3771b952beb02d0eaa6` remains frozen +in its original checkout. Review found two blocking gaps: inference-only records +unconditionally required a node, so pure text could not be published; and actual +C# callers still attempted execution without publication. No wrapper ownership, +strict DTO or newly introduced race bypass was found. + +The integrator approved a domain correction: structural validation permits zero +inference nodes, while publication and scheduler projection require exact equality +with the canonical graph's inference-node set. The inference request owner exports +a shared iterator for its existing llm-inference category. Other inference-bearing +node types remain Unsupported in the scheduler classifier, with a regression for +that fail-closed behavior. Duplicate snapshot and graph inference IDs are rejected; +missing/extra projection IDs are reported. Runtime-node proof validation and all +revision/fingerprint/version/dependency freshness checks stay in force. + +The public scheduler projection method now requires a graph argument and enforces +coverage itself, including for imported/deserialized records. The execution owner +passes its actual loaded graph; publication passes the server-owned edit graph. +No new caller flag, alternate snapshot constructor or schema escape is added. +Regressions cover empty owner publication, valid runtime coverage, missing/extra/ +duplicate projections and deserialized empty records presented for runtime graphs. +The stored-attribution fixture now actually includes its claimed inference node. + +NativeSmoke and DirectRuntimeQuickstart now load/edit/refresh/publish through the +new generated methods, check submit permission and publication identity, and close +the edit session before execution. Symbol checks remain supporting gates, not a +claim that the C# workflow ran. Both real programs require hosted generation, +compilation and execution. This correction is based on the separately published +selector/readiness milestones; their tests have passed in hosted runs. Independent +re-review and all fresh Rust/C# qualification remain pending. + +Independent narrow re-review accepted correction tree +`80666d31216c3286eec923bd4ee96e8a58bab05e`: exact graph coverage at owner +construction and graph-required public projection resolves the zero-inference +blocker, and both actual C# callers use publication before execution. Missing, +extra, duplicate and deserialized-empty regressions were inspected; unsupported +inference-bearing kinds remain fail-closed. No new source blocker was found. +This report records bounded source acceptance, not hosted execution evidence. diff --git a/scripts/check-uniffi-csharp-smoke.sh b/scripts/check-uniffi-csharp-smoke.sh index 0142ba24a..51fe2cb9c 100755 --- a/scripts/check-uniffi-csharp-smoke.sh +++ b/scripts/check-uniffi-csharp-smoke.sh @@ -72,6 +72,8 @@ require_generated_text 'public record FfiEmbeddedRuntimeConfig' require_generated_text 'namespace uniffi.pantograph_headless;' reject_generated_text 'Task WorkflowRun(String @requestJson)' require_generated_text 'Task WorkflowCreateSession(String @requestJson)' +require_generated_text 'Task WorkflowGraphRefreshCurrentValidationSummary(String @requestJson)' +require_generated_text 'Task PublishGraphSessionExecutableValidationSnapshot(String @requestJson)' dotnet_root="$(dirname "$(readlink -f "$(command -v dotnet)")")" sdk_dir="$dotnet_root/sdk" diff --git a/scripts/check-uniffi-embedded-runtime-surface.sh b/scripts/check-uniffi-embedded-runtime-surface.sh index 0a27bbb3e..aa8823864 100755 --- a/scripts/check-uniffi-embedded-runtime-surface.sh +++ b/scripts/check-uniffi-embedded-runtime-surface.sh @@ -62,6 +62,8 @@ require_metadata 'name: "workflow_list_session_queue"' require_metadata 'name: "workflow_cancel_session_queue_item"' require_metadata 'name: "workflow_reprioritize_session_queue_item"' require_metadata 'name: "workflow_set_session_keep_alive"' +require_metadata 'name: "workflow_graph_refresh_current_validation_summary"' +require_metadata 'name: "publish_graph_session_executable_validation_snapshot"' require_metadata 'name: "workflow_graph_list_node_definitions"' require_metadata 'name: "workflow_graph_get_node_definition"' require_metadata 'name: "workflow_graph_get_node_definitions_by_category"' From b7b4da4875ab50061e026e6e0b4e0ddf0a909041 Mon Sep 17 00:00:00 2001 From: Puma Date: Sat, 3 Oct 2026 00:09:10 -0700 Subject: [PATCH 15/65] test(workflow): publish real validation in text attribution fixtures --- .../src/workflow/tests/session_execution.rs | 63 ++++++++++++++----- ...03-uniffi-validation-publication-bridge.md | 16 +++++ 2 files changed, 63 insertions(+), 16 deletions(-) diff --git a/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs b/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs index 16411087d..fa52cdfee 100644 --- a/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs +++ b/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs @@ -3252,6 +3252,49 @@ async fn workflow_execution_session_run_rejects_stale_graph_before_queue_admissi assert_eq!(host.run_attempts.load(Ordering::SeqCst), 0); } +async fn publish_non_runtime_execution_snapshot( + service: &WorkflowService, + workflow_id: &str, + semantic_version: &str, + graph: WorkflowGraph, +) { + let edit = service + .workflow_graph_create_edit_session(crate::WorkflowGraphEditSessionCreateRequest { + graph, + workflow_id: Some(workflow_id.to_string()), + }) + .await + .expect("create non-runtime graph session"); + let validation = service + .workflow_graph_refresh_current_validation_summary( + crate::WorkflowGraphCurrentValidationRefreshRequest { + graph_session_id: edit.session_id.clone(), + graph_revision: edit.graph_revision.parse().expect("graph revision"), + }, + ) + .await + .expect("validate non-runtime graph"); + assert!(validation.summary.submit_gate.allowed); + service + .publish_graph_session_executable_validation_snapshot( + crate::WorkflowGraphSessionExecutableValidationSnapshotPublishRequest { + workflow_id: workflow_id.to_string(), + workflow_semantic_version: semantic_version.to_string(), + graph_session_id: edit.session_id.clone(), + validation_session_id: validation.summary.validation_session_id, + validation_snapshot_id: None, + }, + ) + .await + .expect("publish non-runtime executable snapshot"); + service + .workflow_graph_close_edit_session(crate::WorkflowGraphEditSessionCloseRequest { + session_id: edit.session_id, + }) + .await + .expect("close published graph session"); +} + #[tokio::test] async fn workflow_execution_session_run_records_snapshot_before_execution() { let host = MockWorkflowHost::with_technical_fit_decision( @@ -3286,14 +3329,8 @@ async fn workflow_execution_session_run_records_snapshot_before_execution() { let workflow_id = "wf-snapshot"; let workflow_semantic_version = "1.2.3"; let graph = mock_workflow_graph(); - let version = service - .resolve_workflow_graph_version(workflow_id, workflow_semantic_version, &graph) - .expect("resolve workflow version"); - service - .store_workflow_executable_validation_snapshot(runtime_executable_validation_snapshot( - &version, &graph, - )) - .expect("store executable validation snapshot"); + publish_non_runtime_execution_snapshot(&service, workflow_id, workflow_semantic_version, graph) + .await; let created = service .create_workflow_execution_session( @@ -3700,14 +3737,8 @@ async fn attributed_workflow_execution_session_carries_client_bucket_into_run_ev let workflow_id = "wf-attributed"; let workflow_semantic_version = "1.2.3"; let graph = mock_workflow_graph(); - let version = service - .resolve_workflow_graph_version(workflow_id, workflow_semantic_version, &graph) - .expect("resolve workflow version"); - service - .store_workflow_executable_validation_snapshot(runtime_executable_validation_snapshot( - &version, &graph, - )) - .expect("store executable validation snapshot"); + publish_non_runtime_execution_snapshot(&service, workflow_id, workflow_semantic_version, graph) + .await; let registered = service .register_attribution_client(ClientRegistrationRequest { display_name: Some("local gui".to_string()), diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-validation-publication-bridge.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-validation-publication-bridge.md index 758674a94..c732c2732 100644 --- a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-validation-publication-bridge.md +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-validation-publication-bridge.md @@ -81,3 +81,19 @@ blocker, and both actual C# callers use publication before execution. Missing, extra, duplicate and deserialized-empty regressions were inspected; unsupported inference-bearing kinds remain fail-closed. No new source blocker was found. This report records bounded source acceptance, not hosted execution evidence. + +## Hosted attribution-fixture follow-up + +Initial PR #15 head `8a8dcbcc0a58aafc0f8e09d7853078856ad78834` compiled and +ran the workflow library suite: 880 passed, two failed. Exact coverage rejected +pre-existing synthetic `infer` records attached to pure text graphs in the +snapshot-ordering and client/bucket attribution fixtures. Their behavioral +assertions remain unchanged. Both fixtures now create a graph session, refresh +owner validation and publish through the real service API, then close the edit +session; they no longer invent an inference node to satisfy the old nonempty +snapshot rule. Runtime-inference fixtures retain their actual runtime proofs. +Rustfmt/whitespace pass; narrow review and fresh hosted qualification are pending. +Independent narrow source review accepted follow-up tree +`255079840fae85689cc6ff5b508fe3bc66be0e8b`: both fixtures now use the public +owner validation flow, with all behavioral assertions unchanged and no production +change. Fresh hosted full-library and downstream C# qualification remain required. From 21045742659c90fd5bd2ecd48fdc5895ad9b7f8a Mon Sep 17 00:00:00 2001 From: Puma Date: Sat, 3 Oct 2026 00:46:18 -0700 Subject: [PATCH 16/65] fix(ui): derive model selector state without synchronization effect --- .../2026-10-03-selector-derived-state.md | 22 +++++++++++++++++++ .../nodes/workflow/PumaLibNode.svelte | 6 +---- 2 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-selector-derived-state.md diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-selector-derived-state.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-selector-derived-state.md new file mode 100644 index 000000000..48815bd3e --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-selector-derived-state.md @@ -0,0 +1,22 @@ +# Selector State Derivation + +The current Full lint job reports one error in PumaLibNode.svelte: a `$state` +value is copied from `data.model_id` by an effect. Replace that pair with writable +`$derived` state. The existing selection handler can still update the value +optimistically, and incoming node data remains authoritative. No selection, +hydration, cache or error-handling code changes. + +The locked Svelte 5.55.4 supports writable derived values; official documentation +covers this optimistic-update pattern from 5.25 onward: +https://svelte.dev/docs/svelte/$derived#Overriding-derived-values + +Verification: existing selector-state and model-options-cache tests pass 15/15; +full ESLint, TypeScript typecheck, direct Svelte client compilation and whitespace +checks pass. No dependencies or Rust sources change. No browser interaction claim +is made. Independent review and exact-head hosted qualification remain pending. +The separately bot-reviewed/native-qualified PR #15 head remains unchanged; +critical/a11y lint, Clippy and dependency audit still have separate work. +Independent integrator source review accepted tree +`0435e312fab157a8d9e3d6b5492e3f0197aa44e6` after checking the component and +selection handler. Optimistic writes and source-driven resynchronization remain; +no broader error/hydration behavior change is implied. Hosted gates remain required. diff --git a/src/components/nodes/workflow/PumaLibNode.svelte b/src/components/nodes/workflow/PumaLibNode.svelte index 9d52b2382..1399b4783 100644 --- a/src/components/nodes/workflow/PumaLibNode.svelte +++ b/src/components/nodes/workflow/PumaLibNode.svelte @@ -31,17 +31,13 @@ let { id, data, selected = false }: Props = $props(); - let modelId = $state(undefined); + let modelId = $derived(typeof data.model_id === 'string' ? data.model_id : undefined); let availableModels: PortOption[] = $state([]); let isLoading = $state(false); let loadError = $state(null); let selectionError = $state(null); let searchQuery = $state(''); - $effect(() => { - modelId = typeof data.model_id === 'string' ? data.model_id : undefined; - }); - let filteredModels = $derived( searchQuery ? availableModels.filter((model) => { From 88888780c0fe2ae28a37263bfd84de5a51db9bbf Mon Sep 17 00:00:00 2001 From: Puma Date: Sat, 3 Oct 2026 00:55:14 -0700 Subject: [PATCH 17/65] fix(ci): parse Svelte role-button attributes structurally --- .../reports/2026-10-03-a11y-role-parser.md | 16 ++++++ package.json | 2 +- scripts/check-svelte-a11y.mjs | 40 +------------ scripts/svelte-role-button-check.mjs | 57 +++++++++++++++++++ scripts/svelte-role-button-check.test.mjs | 42 ++++++++++++++ 5 files changed, 117 insertions(+), 40 deletions(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-a11y-role-parser.md create mode 100644 scripts/svelte-role-button-check.mjs create mode 100644 scripts/svelte-role-button-check.test.mjs diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-a11y-role-parser.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-a11y-role-parser.md new file mode 100644 index 000000000..db5f963cd --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-a11y-role-parser.md @@ -0,0 +1,16 @@ +# Svelte role-button scanner correction + +## Decision and scope + +The role-button scanner truncated opening tags at JavaScript arrow operators and quoted greater-than signs. SavedGraphInspectionSnapshot therefore failed keyboard activation checks despite declaring onkeydown. Parse Svelte with the existing locked compiler and inspect complete attributes. Keep native-control exemptions and require explicit tabindex, a keyboard handler, and accessible-name evidence. Rendered descendant text/expressions count as name evidence; callback source and comments do not. Preserve the existing native-button and reviewed-ignore policies. + +No dependency change or accessibility suppression is included. Dynamic role expressions remain outside this literal-role check. This is a static guard, not a complete runtime accessibility audit. Malformed Svelte stops the gate. + +## Verification + +- Eight parser regression tests pass: modern arrow handlers, quoted greater-than signs, legacy directives, real missing keyboard activation, independent required attributes and source lines, nested markup, comments/scripts, malformed syntax, and descendant names versus callback source. +- The lint:a11y entry point runs these regressions before scanning. +- Full repository scan now reports only the two existing reviewed-a11y-ignore findings in IoInspectorPage and RunGraphSnapshot. Those remain unresolved rather than waived. +- Independent review pending. No hosted qualification yet. + +Independent source review accepted tree 9ac66e91ff8ee2e5e1e9bc0393b22df5b27a6e00. Full ESLint also passes. Name evidence is intentionally static rather than a complete accessible-name computation: conditional or hidden content and dynamically empty labels still require UI/accessibility validation. Hosted qualification remains pending publication. diff --git a/package.json b/package.json index d80bd6c4d..6f3b5f18e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "lint": "eslint scripts eslint.config.mjs vite.config.ts --max-warnings 0", "lint:full": "eslint src packages scripts eslint.config.mjs vite.config.ts --max-warnings 0", "lint:critical": "node scripts/check-critical-antipatterns.mjs", - "lint:a11y": "node scripts/check-svelte-a11y.mjs", + "lint:a11y": "node --test scripts/svelte-role-button-check.test.mjs && node scripts/check-svelte-a11y.mjs", "lint:no-new": "npm run lint:critical && npm run lint:a11y && npm run traceability", "format:check": "cargo fmt --all -- --check", "test:frontend": "node scripts/run-frontend-tests.mjs", diff --git a/scripts/check-svelte-a11y.mjs b/scripts/check-svelte-a11y.mjs index e4537e80d..97b78cd1f 100755 --- a/scripts/check-svelte-a11y.mjs +++ b/scripts/check-svelte-a11y.mjs @@ -1,11 +1,11 @@ #!/usr/bin/env node import { readdir, readFile } from 'node:fs/promises'; import { extname, join, relative } from 'node:path'; +import { collectRoleButtonViolations } from './svelte-role-button-check.mjs'; const ROOT = process.cwd(); const SCAN_ROOTS = ['src', 'packages/svelte-graph/src']; const IGNORE_DIRS = new Set(['node_modules', 'dist', 'target', '.git', '.svelte-kit']); -const INTERACTIVE_SEMANTIC_TAGS = new Set(['button', 'a', 'input', 'select', 'textarea']); async function listSvelteFiles(dir, out = []) { const entries = await readdir(dir, { withFileTypes: true }); @@ -65,44 +65,6 @@ function collectButtonViolations(text, relPath) { return violations; } -function collectRoleButtonViolations(text, relPath) { - const violations = []; - const roleRegex = /<([\w:-]+)\b(?=[^>]*\srole\s*=\s*["']button["'])[^>]*>/g; - let match; - while ((match = roleRegex.exec(text)) !== null) { - const tag = match[1]; - if (INTERACTIVE_SEMANTIC_TAGS.has(tag)) continue; - - const open = match[0]; - const line = lineForIndex(text, match.index); - if (!/\stabindex\s*=/.test(open)) { - violations.push({ - file: relPath, - line, - rule: 'role-button-tabindex', - message: 'Generic role="button" element must declare tabindex.', - }); - } - if (!/\son:keydown\s*=|\sonkeydown\s*=/.test(open)) { - violations.push({ - file: relPath, - line, - rule: 'role-button-keydown', - message: 'Generic role="button" element must handle keyboard activation.', - }); - } - if (!hasAccessibleName(open)) { - violations.push({ - file: relPath, - line, - rule: 'role-button-accessible-name', - message: 'Generic role="button" element must include an accessible name.', - }); - } - } - return violations; -} - function collectIgnoreViolations(text, relPath) { const violations = []; const lines = text.split('\n'); diff --git a/scripts/svelte-role-button-check.mjs b/scripts/svelte-role-button-check.mjs new file mode 100644 index 000000000..7b0e9fc17 --- /dev/null +++ b/scripts/svelte-role-button-check.mjs @@ -0,0 +1,57 @@ +import { parse } from 'svelte/compiler'; + +const INTERACTIVE_SEMANTIC_TAGS = new Set(['button', 'a', 'input', 'select', 'textarea']); + +// A rendered text expression is name evidence, as in the existing button check. +// Attribute expressions (including event handlers) are never name evidence. +function hasRenderedName(node) { + if (!node || typeof node !== 'object') return false; + if (node.type === 'Text') return /[A-Za-z0-9]/.test(node.data); + if (node.type === 'ExpressionTag') return true; + if (node.type === 'Comment') return false; + if (node.type === 'RegularElement' && ['script', 'style'].includes(node.name)) return false; + return ['nodes', 'fragment', 'consequent', 'alternate', 'body', 'fallback'].some((key) => { + const value = node[key]; + return Array.isArray(value) ? value.some(hasRenderedName) : hasRenderedName(value); + }); +} + +/** Inspect parsed attributes so JavaScript arrows and quoted > cannot end a tag. */ +export function collectRoleButtonViolations(text, relPath) { + const ast = parse(text, { modern: true, filename: relPath }); + const violations = []; + + function visit(node) { + if (!node || typeof node !== 'object') return; + if (node.type === 'RegularElement' && !INTERACTIVE_SEMANTIC_TAGS.has(node.name)) { + const attributes = node.attributes; + const role = attributes.find((attribute) => attribute.type === 'Attribute' && attribute.name === 'role'); + const isButton = Array.isArray(role?.value) + && role.value.length === 1 + && role.value[0].type === 'Text' + && role.value[0].data === 'button'; + if (isButton) { + const hasAttribute = (name) => attributes.some((attribute) => attribute.type === 'Attribute' && attribute.name === name); + const hasKeydown = hasAttribute('onkeydown') || attributes.some((attribute) => attribute.type === 'OnDirective' && attribute.name === 'keydown'); + const line = text.slice(0, node.start).split('\n').length; + const add = (rule, message) => violations.push({ file: relPath, line, rule, message }); + if (!hasAttribute('tabindex')) { + add('role-button-tabindex', 'Generic role="button" element must declare tabindex.'); + } + if (!hasKeydown) { + add('role-button-keydown', 'Generic role="button" element must handle keyboard activation.'); + } + if (!hasAttribute('aria-label') && !hasAttribute('aria-labelledby') && !hasRenderedName(node.fragment)) { + add('role-button-accessible-name', 'Generic role="button" element must include an accessible name.'); + } + } + } + for (const value of Object.values(node)) { + if (Array.isArray(value)) value.forEach(visit); + else if (value && typeof value === 'object') visit(value); + } + } + + visit(ast.fragment); + return violations; +} diff --git a/scripts/svelte-role-button-check.test.mjs b/scripts/svelte-role-button-check.test.mjs new file mode 100644 index 000000000..6b65e31cc --- /dev/null +++ b/scripts/svelte-role-button-check.test.mjs @@ -0,0 +1,42 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { collectRoleButtonViolations } from './svelte-role-button-check.mjs'; + +const check = (source) => collectRoleButtonViolations(source, 'Fixture.svelte'); +const rules = (source) => check(source).map((violation) => violation.rule); + +test('arrow callbacks do not hide later keyboard and name attributes', () => { + assert.deepEqual(check(' select()} onkeydown={(event) => activate(event)} aria-label="Select" />'), []); +}); + +test('quoted greater-than signs and legacy event directives retain complete attributes', () => { + assert.deepEqual(check('
select()} on:keydown={activate} aria-labelledby="label" />'), []); +}); + +test('missing keyboard activation remains a violation after an arrow callback', () => { + assert.deepEqual(rules(' select()} aria-label="Select" />'), ['role-button-keydown']); +}); + +test('all required attributes are checked independently with exact location', () => { + const violations = check('\n
'); + assert.deepEqual(violations.map(({ rule }) => rule), ['role-button-tabindex', 'role-button-keydown', 'role-button-accessible-name']); + assert.ok(violations.every(({ file, line }) => file === 'Fixture.svelte' && line === 2)); +}); + +test('comments and script strings are not rendered elements', () => { + assert.deepEqual(check(''), []); +}); + +test('nested control-flow elements are inspected and native controls remain exempt', () => { + assert.deepEqual(rules('{#if visible}
{/if}'), ['role-button-keydown']); +}); + +test('malformed Svelte fails closed instead of silently skipping checks', () => { + assert.throws(() => check('
} />')); +}); + + +test('visible descendant text supplies a name, but handler expressions do not', () => { + assert.deepEqual(check('
{label}
'), []); + assert.deepEqual(rules('
activate()}>
'), ['role-button-accessible-name']); +}); From e5e2bdfa7acb8fa8fdaa55501b3a98834b2f2352 Mon Sep 17 00:00:00 2001 From: Puma Date: Sat, 3 Oct 2026 01:07:21 -0700 Subject: [PATCH 18/65] fix(ui): own artifact download anchors and URL release lifecycle --- .../2026-10-03-artifact-download-lifecycle.md | 22 ++++ .../workbench/IoInspectorPage.svelte | 48 +++++--- .../artifactDownloadLifecycle.test.ts | 107 ++++++++++++++++++ .../workbench/artifactDownloadLifecycle.ts | 91 +++++++++++++++ 4 files changed, 253 insertions(+), 15 deletions(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-artifact-download-lifecycle.md create mode 100644 src/components/workbench/artifactDownloadLifecycle.test.ts create mode 100644 src/components/workbench/artifactDownloadLifecycle.ts diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-artifact-download-lifecycle.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-artifact-download-lifecycle.md new file mode 100644 index 000000000..70dabe050 --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-artifact-download-lifecycle.md @@ -0,0 +1,22 @@ +# Artifact download anchor and URL lifecycle + +## Decision + +The critical anti-pattern gate identified imperative insertion of a temporary download anchor in IoInspectorPage. Render pending anchors through Svelte and activate them after mounting. A small download lifecycle owner retains each object URL until its own 30-second release or activation failure. Successful activations retain their grace timer after component disposal; unactivated URLs are reclaimed immediately. Pending completion promises settle on disposal, late URL admission is rejected and reclaimed, and activation after disposal cannot click a detached anchor. The original click error reaches the existing artifact error formatter. + +Concurrent requests retain distinct IDs, URLs, and filenames. Existing filename/media derivation, read authorization, loading state, and rel protection remain. Lifecycle-generation checks prevent late read completion from creating a download after unmount. The scheduler and artifact backend are unchanged. + +## Verification + +- Six direct lifecycle tests cover concurrent downloads and independent delayed release; original activation error; unmount with pending and active downloads; late admission; and unmount during activation. +- The lifecycle and existing inspector presenter suite pass 32 tests; all frontend tests pass 546/546. +- TypeScript, full ESLint, critical anti-pattern gate, and Svelte client compilation (zero warnings) pass locally. +- This is controller execution plus component compilation, not a real-browser download receipt. Browser download policy and actual saved bytes remain outside this local qualification. +- Independent review and exact-head hosted qualification are pending. The two a11y suppression findings, Clippy, and dependency audit remain separate. + + +## Independent review repair + +Review of initial tree a367c62b8b30729e1d5fa4a95d6e247337ca6756 found that immediate revocation of already-clicked URLs on unmount shortened the pre-existing browser grace period. The revised owner keeps those timers after disposal and clears component publication/completion callbacks. Tests distinguish clicked-then-unmount and successful versus failed activation that synchronously unmounts. Browser timer/revocation defaults live in the controller, so retained timers do not need component callbacks. Narrow re-review is pending. + +Independent narrow re-review accepted repair tree 1521cf361523f32ab5a127955cfed3fb87f8ee0c. The grace timer confirms URL release only; it cannot confirm that the browser saved a file. Hosted and real-browser qualification remain distinct. diff --git a/src/components/workbench/IoInspectorPage.svelte b/src/components/workbench/IoInspectorPage.svelte index 1d6c1a516..234b96653 100644 --- a/src/components/workbench/IoInspectorPage.svelte +++ b/src/components/workbench/IoInspectorPage.svelte @@ -1,5 +1,6 @@ +{#each pendingDownloads as request (request.id)} + +{/each} +
diff --git a/src/components/workbench/artifactDownloadLifecycle.test.ts b/src/components/workbench/artifactDownloadLifecycle.test.ts new file mode 100644 index 000000000..b68104f22 --- /dev/null +++ b/src/components/workbench/artifactDownloadLifecycle.test.ts @@ -0,0 +1,107 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createArtifactDownloadLifecycle, type PendingArtifactDownload } from './artifactDownloadLifecycle.ts'; + +function setup() { + let pending: PendingArtifactDownload[] = []; + const revoked: string[] = []; + const timers = new Map void>(); + const delays: number[] = []; + let timerId = 0; + const lifecycle = createArtifactDownloadLifecycle({ + publish: (value) => { pending = value; }, + revoke: (url) => { revoked.push(url); }, + schedule: (callback, delay) => { delays.push(delay); timers.set(++timerId, callback); return timerId; }, + cancel: (id) => { timers.delete(id); }, + }); + return { lifecycle, revoked, timers, delays, pending: () => pending }; +} + +test('concurrent downloads keep distinct filenames and URLs until their own delayed release', async () => { + const h = setup(); + const first = h.lifecycle.enqueue('blob:first', 'first.png'); + const second = h.lifecycle.enqueue('blob:second', 'second.wav'); + const [a, b] = h.pending(); + assert.deepEqual([a.filename, b.filename], ['first.png', 'second.wav']); + let clicks = 0; + h.lifecycle.activate(b.id, () => { clicks += 1; }); + await second; + assert.deepEqual(h.pending(), [a]); + h.lifecycle.activate(a.id, () => { clicks += 1; }); + await first; + h.lifecycle.activate(a.id, () => { clicks += 1; }); + assert.equal(clicks, 2); + assert.deepEqual(h.pending(), []); + assert.deepEqual(h.revoked, []); + assert.deepEqual(h.delays, [30_000, 30_000]); + h.timers.get(1)!(); + assert.deepEqual(h.revoked, ['blob:second']); + h.timers.get(2)!(); + assert.deepEqual(h.revoked, ['blob:second', 'blob:first']); + h.lifecycle.dispose(); + assert.equal(h.revoked.length, 2); +}); + +test('click failure preserves the original error and immediately releases its URL', async () => { + const h = setup(); + const result = h.lifecycle.enqueue('blob:failed', 'failed.bin'); + const original = new Error('activation rejected'); + const rejection = assert.rejects(result, (error) => error === original); + h.lifecycle.activate(h.pending()[0].id, () => { throw original; }); + await rejection; + assert.deepEqual(h.revoked, ['blob:failed']); + assert.equal(h.timers.size, 0); + assert.deepEqual(h.pending(), []); +}); + +test('unmount settles pending work but preserves the clicked URL grace period', async () => { + const h = setup(); + const active = h.lifecycle.enqueue('blob:active', 'active.bin'); + h.lifecycle.activate(h.pending()[0].id, () => {}); + await active; + const pending = h.lifecycle.enqueue('blob:pending', 'pending.bin'); + const id = h.pending()[0].id; + const rejection = assert.rejects(pending, /disposed before activation/); + h.lifecycle.dispose(); + await rejection; + h.lifecycle.activate(id, () => assert.fail('disposed download clicked')); + h.lifecycle.dispose(); + assert.deepEqual(h.revoked, ['blob:pending']); + assert.equal(h.timers.size, 1); + h.timers.get(1)!(); + assert.deepEqual(h.revoked, ['blob:pending', 'blob:active']); + assert.equal(h.timers.size, 0); + assert.deepEqual(h.pending(), []); +}); + +test('a late URL handed to an unmounted owner is reclaimed and cannot hang', async () => { + const h = setup(); + h.lifecycle.dispose(); + await assert.rejects(h.lifecycle.enqueue('blob:late', 'late.bin'), /disposed/); + assert.deepEqual(h.revoked, ['blob:late']); + assert.deepEqual(h.pending(), []); +}); + + +test('successful click that unmounts still retains its URL for the grace period', async () => { + const h = setup(); + const result = h.lifecycle.enqueue('blob:unmount', 'unmount.bin'); + h.lifecycle.activate(h.pending()[0].id, () => h.lifecycle.dispose()); + await result; + assert.deepEqual(h.revoked, []); + assert.deepEqual(h.delays, [30_000]); + h.timers.get(1)!(); + assert.deepEqual(h.revoked, ['blob:unmount']); + assert.equal(h.timers.size, 0); +}); + +test('failed click that unmounts preserves the error and reclaims immediately', async () => { + const h = setup(); + const result = h.lifecycle.enqueue('blob:failed-unmount', 'failed.bin'); + const original = new Error('click failed after navigation'); + const rejection = assert.rejects(result, (error) => error === original); + h.lifecycle.activate(h.pending()[0].id, () => { h.lifecycle.dispose(); throw original; }); + await rejection; + assert.deepEqual(h.revoked, ['blob:failed-unmount']); + assert.equal(h.timers.size, 0); +}); diff --git a/src/components/workbench/artifactDownloadLifecycle.ts b/src/components/workbench/artifactDownloadLifecycle.ts new file mode 100644 index 000000000..8d8c7555f --- /dev/null +++ b/src/components/workbench/artifactDownloadLifecycle.ts @@ -0,0 +1,91 @@ +export interface PendingArtifactDownload { + id: number; + objectUrl: string; + filename: string; +} + +interface DownloadEntry { + request: PendingArtifactDownload; + resolve?: () => void; + reject?: (error: unknown) => void; + timer?: number; + activated: boolean; +} + +interface DownloadLifecycleOptions { + publish: (pending: PendingArtifactDownload[]) => void; + revoke?: (objectUrl: string) => void; + schedule?: (callback: () => void, delay: number) => number; + cancel?: (timer: number) => void; +} + +const REVOKE_DELAY_MS = 30_000; + +/** Own each download URL from admission through click, delayed release, or disposal. */ +export function createArtifactDownloadLifecycle(options: DownloadLifecycleOptions) { + const { + revoke = (objectUrl: string) => URL.revokeObjectURL(objectUrl), + schedule = (callback: () => void, delay: number) => window.setTimeout(callback, delay), + cancel = (timer: number) => window.clearTimeout(timer), + } = options; + let publishPending: DownloadLifecycleOptions['publish'] | undefined = options.publish; + const entries = new Map(); + let serial = 0; + let disposed = false; + + function publish(): void { + publishPending?.([...entries.values()].filter((entry) => !entry.activated).map((entry) => entry.request)); + } + + function release(entry: DownloadEntry): void { + if (!entries.delete(entry.request.id)) return; + if (entry.timer !== undefined) cancel(entry.timer); + revoke(entry.request.objectUrl); + } + + return { + enqueue(objectUrl: string, filename: string): Promise { + if (disposed) { + revoke(objectUrl); + return Promise.reject(new Error('Artifact download owner is disposed.')); + } + return new Promise((resolve, reject) => { + const request = { id: ++serial, objectUrl, filename }; + entries.set(request.id, { request, resolve, reject, activated: false }); + publish(); + }); + }, + activate(id: number, click: () => void): void { + const entry = entries.get(id); + if (!entry || entry.activated || disposed) return; + entry.activated = true; + try { + click(); + entry.timer = schedule(() => release(entry), REVOKE_DELAY_MS); + entry.resolve?.(); + } catch (error) { + release(entry); + entry.reject?.(error); + } + entry.resolve = undefined; + entry.reject = undefined; + publish(); + }, + dispose(): void { + if (disposed) return; + disposed = true; + for (const entry of entries.values()) { + // A successful click keeps its grace timer even when the UI goes away. + // In-progress activation finishes its success/error path after click returns. + if (!entry.activated) { + release(entry); + entry.reject?.(new Error('Artifact download owner was disposed before activation.')); + entry.resolve = undefined; + entry.reject = undefined; + } + } + publishPending?.([]); + publishPending = undefined; + }, + }; +} From fef75dddd7d23e1fd1841c133e31e4fd03923692 Mon Sep 17 00:00:00 2001 From: Puma Date: Sat, 3 Oct 2026 01:11:32 -0700 Subject: [PATCH 19/65] fix(scheduler): implement standard AsRef for validated contracts --- .github/workflows/quality-gates.yml | 8 ++ crates/pantograph-scheduler/src/batching.rs | 7 +- crates/pantograph-scheduler/src/capability.rs | 7 +- crates/pantograph-scheduler/src/dispatch.rs | 7 +- .../src/dispatch_selection.rs | 14 +-- crates/pantograph-scheduler/src/handoff.rs | 7 +- crates/pantograph-scheduler/src/intent.rs | 7 +- crates/pantograph-scheduler/src/lifecycle.rs | 9 +- crates/pantograph-scheduler/src/queue.rs | 14 +-- crates/pantograph-scheduler/src/readiness.rs | 14 +-- crates/pantograph-scheduler/src/resource.rs | 7 +- .../pantograph-scheduler/src/supervision.rs | 7 +- .../tests/as_ref_compatibility.rs | 85 +++++++++++++++++++ .../2026-10-03-scheduler-asref-contracts.md | 17 ++++ 14 files changed, 168 insertions(+), 42 deletions(-) create mode 100644 crates/pantograph-scheduler/tests/as_ref_compatibility.rs create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-scheduler-asref-contracts.md diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml index 882259565..6481af342 100644 --- a/.github/workflows/quality-gates.yml +++ b/.github/workflows/quality-gates.yml @@ -178,10 +178,18 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@1.92.0 + with: + components: clippy - name: Cache Cargo uses: Swatinem/rust-cache@v2 + - name: Run scheduler contract and accessor compatibility tests + run: cargo test -p pantograph-scheduler + + - name: Check scheduler standard-trait accessor lint + run: cargo clippy -p pantograph-scheduler --all-targets -- -D clippy::should_implement_trait + - name: Run node-engine tests run: cargo test -p node-engine --lib diff --git a/crates/pantograph-scheduler/src/batching.rs b/crates/pantograph-scheduler/src/batching.rs index 55d7bfcfc..a5bd7c4cd 100644 --- a/crates/pantograph-scheduler/src/batching.rs +++ b/crates/pantograph-scheduler/src/batching.rs @@ -180,12 +180,13 @@ impl SchedulerBatchPolicyDecision { #[must_use] pub struct ValidatedSchedulerBatchPolicyDecision(SchedulerBatchPolicyDecision); -impl ValidatedSchedulerBatchPolicyDecision { - #[must_use] - pub fn as_ref(&self) -> &SchedulerBatchPolicyDecision { +impl AsRef for ValidatedSchedulerBatchPolicyDecision { + fn as_ref(&self) -> &SchedulerBatchPolicyDecision { &self.0 } +} +impl ValidatedSchedulerBatchPolicyDecision { #[must_use] pub fn into_inner(self) -> SchedulerBatchPolicyDecision { self.0 diff --git a/crates/pantograph-scheduler/src/capability.rs b/crates/pantograph-scheduler/src/capability.rs index 8ff807fe7..d627a0d56 100644 --- a/crates/pantograph-scheduler/src/capability.rs +++ b/crates/pantograph-scheduler/src/capability.rs @@ -202,12 +202,13 @@ fn default_scheduler_capability_hint_contract_version() -> u16 { #[must_use] pub struct ValidatedSchedulerCapabilityHintSnapshot(SchedulerCapabilityHintSnapshot); -impl ValidatedSchedulerCapabilityHintSnapshot { - #[must_use] - pub fn as_ref(&self) -> &SchedulerCapabilityHintSnapshot { +impl AsRef for ValidatedSchedulerCapabilityHintSnapshot { + fn as_ref(&self) -> &SchedulerCapabilityHintSnapshot { &self.0 } +} +impl ValidatedSchedulerCapabilityHintSnapshot { #[must_use] pub fn into_inner(self) -> SchedulerCapabilityHintSnapshot { self.0 diff --git a/crates/pantograph-scheduler/src/dispatch.rs b/crates/pantograph-scheduler/src/dispatch.rs index 7b9655d24..3c45624fc 100644 --- a/crates/pantograph-scheduler/src/dispatch.rs +++ b/crates/pantograph-scheduler/src/dispatch.rs @@ -201,12 +201,13 @@ impl SchedulerDispatchDecision { #[must_use] pub struct ValidatedSchedulerDispatchDecision(SchedulerDispatchDecision); -impl ValidatedSchedulerDispatchDecision { - #[must_use] - pub fn as_ref(&self) -> &SchedulerDispatchDecision { +impl AsRef for ValidatedSchedulerDispatchDecision { + fn as_ref(&self) -> &SchedulerDispatchDecision { &self.0 } +} +impl ValidatedSchedulerDispatchDecision { #[must_use] pub fn into_inner(self) -> SchedulerDispatchDecision { self.0 diff --git a/crates/pantograph-scheduler/src/dispatch_selection.rs b/crates/pantograph-scheduler/src/dispatch_selection.rs index eaa74197d..2b368089f 100644 --- a/crates/pantograph-scheduler/src/dispatch_selection.rs +++ b/crates/pantograph-scheduler/src/dispatch_selection.rs @@ -262,12 +262,13 @@ impl SchedulerDispatchSelectionRequest { #[must_use] pub struct ValidatedSchedulerDispatchSelectionRequest(SchedulerDispatchSelectionRequest); -impl ValidatedSchedulerDispatchSelectionRequest { - #[must_use] - pub fn as_ref(&self) -> &SchedulerDispatchSelectionRequest { +impl AsRef for ValidatedSchedulerDispatchSelectionRequest { + fn as_ref(&self) -> &SchedulerDispatchSelectionRequest { &self.0 } +} +impl ValidatedSchedulerDispatchSelectionRequest { #[must_use] pub fn into_inner(self) -> SchedulerDispatchSelectionRequest { self.0 @@ -341,12 +342,13 @@ impl SchedulerDispatchSelectionDecision { #[must_use] pub struct ValidatedSchedulerDispatchSelectionDecision(SchedulerDispatchSelectionDecision); -impl ValidatedSchedulerDispatchSelectionDecision { - #[must_use] - pub fn as_ref(&self) -> &SchedulerDispatchSelectionDecision { +impl AsRef for ValidatedSchedulerDispatchSelectionDecision { + fn as_ref(&self) -> &SchedulerDispatchSelectionDecision { &self.0 } +} +impl ValidatedSchedulerDispatchSelectionDecision { #[must_use] pub fn into_inner(self) -> SchedulerDispatchSelectionDecision { self.0 diff --git a/crates/pantograph-scheduler/src/handoff.rs b/crates/pantograph-scheduler/src/handoff.rs index 3de212226..4bd1bbb0c 100644 --- a/crates/pantograph-scheduler/src/handoff.rs +++ b/crates/pantograph-scheduler/src/handoff.rs @@ -160,12 +160,13 @@ impl SchedulerRuntimeHandoff { #[must_use] pub struct ValidatedSchedulerRuntimeHandoff(SchedulerRuntimeHandoff); -impl ValidatedSchedulerRuntimeHandoff { - #[must_use] - pub fn as_ref(&self) -> &SchedulerRuntimeHandoff { +impl AsRef for ValidatedSchedulerRuntimeHandoff { + fn as_ref(&self) -> &SchedulerRuntimeHandoff { &self.0 } +} +impl ValidatedSchedulerRuntimeHandoff { #[must_use] pub fn into_inner(self) -> SchedulerRuntimeHandoff { self.0 diff --git a/crates/pantograph-scheduler/src/intent.rs b/crates/pantograph-scheduler/src/intent.rs index 3b8b45dca..c3a3d686f 100644 --- a/crates/pantograph-scheduler/src/intent.rs +++ b/crates/pantograph-scheduler/src/intent.rs @@ -245,12 +245,13 @@ fn default_schedulable_task_intent_contract_version() -> u16 { #[must_use] pub struct ValidatedSchedulableTaskIntent(SchedulableTaskIntent); -impl ValidatedSchedulableTaskIntent { - #[must_use] - pub fn as_ref(&self) -> &SchedulableTaskIntent { +impl AsRef for ValidatedSchedulableTaskIntent { + fn as_ref(&self) -> &SchedulableTaskIntent { &self.0 } +} +impl ValidatedSchedulableTaskIntent { #[must_use] pub fn into_inner(self) -> SchedulableTaskIntent { self.0 diff --git a/crates/pantograph-scheduler/src/lifecycle.rs b/crates/pantograph-scheduler/src/lifecycle.rs index 094750aa5..c8afae468 100644 --- a/crates/pantograph-scheduler/src/lifecycle.rs +++ b/crates/pantograph-scheduler/src/lifecycle.rs @@ -179,12 +179,15 @@ pub struct ValidatedSchedulerTaskLifecycleDiagnosticSnapshot( SchedulerTaskLifecycleDiagnosticSnapshot, ); -impl ValidatedSchedulerTaskLifecycleDiagnosticSnapshot { - #[must_use] - pub fn as_ref(&self) -> &SchedulerTaskLifecycleDiagnosticSnapshot { +impl AsRef + for ValidatedSchedulerTaskLifecycleDiagnosticSnapshot +{ + fn as_ref(&self) -> &SchedulerTaskLifecycleDiagnosticSnapshot { &self.0 } +} +impl ValidatedSchedulerTaskLifecycleDiagnosticSnapshot { #[must_use] pub fn into_inner(self) -> SchedulerTaskLifecycleDiagnosticSnapshot { self.0 diff --git a/crates/pantograph-scheduler/src/queue.rs b/crates/pantograph-scheduler/src/queue.rs index 8b6fbea74..53f5c3a7c 100644 --- a/crates/pantograph-scheduler/src/queue.rs +++ b/crates/pantograph-scheduler/src/queue.rs @@ -576,12 +576,13 @@ pub fn apply_scheduler_task_state_transition( #[must_use] pub struct ValidatedSchedulerTaskStateRecord(SchedulerTaskStateRecord); -impl ValidatedSchedulerTaskStateRecord { - #[must_use] - pub fn as_ref(&self) -> &SchedulerTaskStateRecord { +impl AsRef for ValidatedSchedulerTaskStateRecord { + fn as_ref(&self) -> &SchedulerTaskStateRecord { &self.0 } +} +impl ValidatedSchedulerTaskStateRecord { #[must_use] pub fn into_inner(self) -> SchedulerTaskStateRecord { self.0 @@ -601,12 +602,13 @@ impl TryFrom for ValidatedSchedulerTaskStateRecord { #[must_use] pub struct ValidatedSchedulerTaskStateTransition(SchedulerTaskStateTransition); -impl ValidatedSchedulerTaskStateTransition { - #[must_use] - pub fn as_ref(&self) -> &SchedulerTaskStateTransition { +impl AsRef for ValidatedSchedulerTaskStateTransition { + fn as_ref(&self) -> &SchedulerTaskStateTransition { &self.0 } +} +impl ValidatedSchedulerTaskStateTransition { #[must_use] pub fn into_inner(self) -> SchedulerTaskStateTransition { self.0 diff --git a/crates/pantograph-scheduler/src/readiness.rs b/crates/pantograph-scheduler/src/readiness.rs index f4dc238b6..9d2a625bc 100644 --- a/crates/pantograph-scheduler/src/readiness.rs +++ b/crates/pantograph-scheduler/src/readiness.rs @@ -248,12 +248,13 @@ pub(crate) fn validate_ready_proof_for_intent( #[must_use] pub struct ValidatedSchedulerReadinessAdmissionRequest(SchedulerReadinessAdmissionRequest); -impl ValidatedSchedulerReadinessAdmissionRequest { - #[must_use] - pub fn as_ref(&self) -> &SchedulerReadinessAdmissionRequest { +impl AsRef for ValidatedSchedulerReadinessAdmissionRequest { + fn as_ref(&self) -> &SchedulerReadinessAdmissionRequest { &self.0 } +} +impl ValidatedSchedulerReadinessAdmissionRequest { #[must_use] pub fn into_inner(self) -> SchedulerReadinessAdmissionRequest { self.0 @@ -274,12 +275,13 @@ impl TryFrom for ValidatedSchedulerReadiness #[must_use] pub struct ValidatedSchedulerReadinessAdmissionDecision(SchedulerReadinessAdmissionDecision); -impl ValidatedSchedulerReadinessAdmissionDecision { - #[must_use] - pub fn as_ref(&self) -> &SchedulerReadinessAdmissionDecision { +impl AsRef for ValidatedSchedulerReadinessAdmissionDecision { + fn as_ref(&self) -> &SchedulerReadinessAdmissionDecision { &self.0 } +} +impl ValidatedSchedulerReadinessAdmissionDecision { #[must_use] pub fn into_inner(self) -> SchedulerReadinessAdmissionDecision { self.0 diff --git a/crates/pantograph-scheduler/src/resource.rs b/crates/pantograph-scheduler/src/resource.rs index 8ce65ae1b..255128945 100644 --- a/crates/pantograph-scheduler/src/resource.rs +++ b/crates/pantograph-scheduler/src/resource.rs @@ -290,12 +290,13 @@ impl SchedulerResourceResidencySnapshot { #[must_use] pub struct ValidatedSchedulerResourceResidencySnapshot(SchedulerResourceResidencySnapshot); -impl ValidatedSchedulerResourceResidencySnapshot { - #[must_use] - pub fn as_ref(&self) -> &SchedulerResourceResidencySnapshot { +impl AsRef for ValidatedSchedulerResourceResidencySnapshot { + fn as_ref(&self) -> &SchedulerResourceResidencySnapshot { &self.0 } +} +impl ValidatedSchedulerResourceResidencySnapshot { #[must_use] pub fn into_inner(self) -> SchedulerResourceResidencySnapshot { self.0 diff --git a/crates/pantograph-scheduler/src/supervision.rs b/crates/pantograph-scheduler/src/supervision.rs index 6b08fe4a9..c99db0bcb 100644 --- a/crates/pantograph-scheduler/src/supervision.rs +++ b/crates/pantograph-scheduler/src/supervision.rs @@ -284,12 +284,13 @@ impl SchedulerLifecycleOwnerSnapshot { #[must_use] pub struct ValidatedSchedulerLifecycleOwnerSnapshot(SchedulerLifecycleOwnerSnapshot); -impl ValidatedSchedulerLifecycleOwnerSnapshot { - #[must_use] - pub fn as_ref(&self) -> &SchedulerLifecycleOwnerSnapshot { +impl AsRef for ValidatedSchedulerLifecycleOwnerSnapshot { + fn as_ref(&self) -> &SchedulerLifecycleOwnerSnapshot { &self.0 } +} +impl ValidatedSchedulerLifecycleOwnerSnapshot { #[must_use] pub fn into_inner(self) -> SchedulerLifecycleOwnerSnapshot { self.0 diff --git a/crates/pantograph-scheduler/tests/as_ref_compatibility.rs b/crates/pantograph-scheduler/tests/as_ref_compatibility.rs new file mode 100644 index 000000000..2e7dc1d26 --- /dev/null +++ b/crates/pantograph-scheduler/tests/as_ref_compatibility.rs @@ -0,0 +1,85 @@ +//! Compile coverage of public accessor call forms after migration to AsRef. +use pantograph_scheduler::*; + +macro_rules! accessor_call_forms { + ($name:ident, $wrapper:ty, $raw:ty) => { + #[test] + fn $name() { + let _: fn(&$wrapper) -> &$raw = |value| value.as_ref(); + let _: fn(&$wrapper) -> &$raw = |value| <$wrapper>::as_ref(value); + let _: fn(&$wrapper) -> &$raw = <$wrapper>::as_ref; + let _: fn(&$wrapper) -> &$raw = |value| <$wrapper as AsRef<$raw>>::as_ref(value); + } + }; +} + +accessor_call_forms!( + validated_scheduler_batch_policy_decision, + ValidatedSchedulerBatchPolicyDecision, + SchedulerBatchPolicyDecision +); +accessor_call_forms!( + validated_scheduler_capability_hint_snapshot, + ValidatedSchedulerCapabilityHintSnapshot, + SchedulerCapabilityHintSnapshot +); +accessor_call_forms!( + validated_scheduler_dispatch_decision, + ValidatedSchedulerDispatchDecision, + SchedulerDispatchDecision +); +accessor_call_forms!( + validated_scheduler_dispatch_selection_request, + ValidatedSchedulerDispatchSelectionRequest, + SchedulerDispatchSelectionRequest +); +accessor_call_forms!( + validated_scheduler_dispatch_selection_decision, + ValidatedSchedulerDispatchSelectionDecision, + SchedulerDispatchSelectionDecision +); +accessor_call_forms!( + validated_scheduler_runtime_handoff, + ValidatedSchedulerRuntimeHandoff, + SchedulerRuntimeHandoff +); +accessor_call_forms!( + validated_schedulable_task_intent, + ValidatedSchedulableTaskIntent, + SchedulableTaskIntent +); +accessor_call_forms!( + validated_scheduler_task_lifecycle_diagnostic_snapshot, + ValidatedSchedulerTaskLifecycleDiagnosticSnapshot, + SchedulerTaskLifecycleDiagnosticSnapshot +); +accessor_call_forms!( + validated_scheduler_task_state_record, + ValidatedSchedulerTaskStateRecord, + SchedulerTaskStateRecord +); +accessor_call_forms!( + validated_scheduler_task_state_transition, + ValidatedSchedulerTaskStateTransition, + SchedulerTaskStateTransition +); +accessor_call_forms!( + validated_scheduler_readiness_admission_request, + ValidatedSchedulerReadinessAdmissionRequest, + SchedulerReadinessAdmissionRequest +); +accessor_call_forms!( + validated_scheduler_readiness_admission_decision, + ValidatedSchedulerReadinessAdmissionDecision, + SchedulerReadinessAdmissionDecision +); +accessor_call_forms!( + validated_scheduler_resource_residency_snapshot, + ValidatedSchedulerResourceResidencySnapshot, + SchedulerResourceResidencySnapshot +); +accessor_call_forms!( + validated_scheduler_lifecycle_owner_snapshot, + ValidatedSchedulerLifecycleOwnerSnapshot, + SchedulerLifecycleOwnerSnapshot +); diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-scheduler-asref-contracts.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-scheduler-asref-contracts.md new file mode 100644 index 000000000..ac37383f2 --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-scheduler-asref-contracts.md @@ -0,0 +1,17 @@ +# Scheduler validated-wrapper AsRef compatibility + +## Decision and public surface + +Move the fourteen validated scheduler wrapper accessors from inherent methods into standard AsRef implementations, preserving exported wrapper types, borrowed raw targets, lifetime elision, and direct `&self.0` references. Constructors, validation, ownership, serialization, and into_inner remain unchanged. This addresses should_implement_trait without suppressing the lint. + +A Rust 1.92 standalone Clippy probe showed that retaining an inherent accessor alongside the trait still triggers the lint. A trait-only probe compiled and executed method, type-qualified, fully qualified, and typed function-pointer forms. Repository inspection found no no_implicit_prelude usage or explicit validated scheduler UFCS references. The crate is publish=false but its types are publicly re-exported, so external Git/path consumers remain relevant: a niche consumer disabling the Rust prelude must import AsRef explicitly. Trait-only calls rely on the standard prelude. No claim is made about unseen consumers. + +The integration compile fixtures cover all fourteen exported wrapper/raw pairs with method calls, type-qualified calls, typed function pointers, and explicitly trait-qualified calls. The Quality workflow runs the scheduler suite and a targeted should_implement_trait Clippy check, while retaining the existing aggregate warning-deny audit unchanged. + +## Verification and remaining scope + +The small standalone probe passed locally; formatting and whitespace checks pass. No local workspace Rust build was run due to disk constraints. Actual scheduler tests and targeted Clippy qualification require fresh hosted execution. Independent source review is pending. + +The runtime execution enum size/construction API, redundant must_use annotation, managed dependency needless borrow, and later aggregate Clippy findings remain separate. This is an accessor-contract repair, not a scheduler redesign. + +Independent source review accepted tree ea62f92c5bfed9225adcf753a77c5be55d23475e. Accessor must_use annotations follow the standard trait method semantics; the validated wrapper types remain must_use. This acceptance covers the source/API migration and hosted checks, not an unperformed local workspace compilation. From 8a08b6a010ee62fbd3b29e193c65922c0769b928 Mon Sep 17 00:00:00 2001 From: Puma Date: Sat, 3 Oct 2026 01:13:33 -0700 Subject: [PATCH 20/65] fix(ci): remove redundant path borrow and Result annotation --- .../src/redistributables/state.rs | 2 +- .../pantograph-scheduler/src/dispatch_selection_policy.rs | 1 - .../reports/2026-10-03-clippy-borrow-result-annotations.md | 7 +++++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-clippy-borrow-result-annotations.md diff --git a/crates/pantograph-managed-dependencies/src/redistributables/state.rs b/crates/pantograph-managed-dependencies/src/redistributables/state.rs index 3912e9024..5673aed1b 100644 --- a/crates/pantograph-managed-dependencies/src/redistributables/state.rs +++ b/crates/pantograph-managed-dependencies/src/redistributables/state.rs @@ -29,7 +29,7 @@ pub fn load_managed_redistributable_state( } fn load_state_file(path: &Path) -> Result { - let contents = fs::read_to_string(&path).map_err(|e| { + let contents = fs::read_to_string(path).map_err(|e| { format!( "Failed to read managed redistributable state {:?}: {}", path, e diff --git a/crates/pantograph-scheduler/src/dispatch_selection_policy.rs b/crates/pantograph-scheduler/src/dispatch_selection_policy.rs index c8ec9a09b..91f64a41d 100644 --- a/crates/pantograph-scheduler/src/dispatch_selection_policy.rs +++ b/crates/pantograph-scheduler/src/dispatch_selection_policy.rs @@ -12,7 +12,6 @@ use crate::error::SchedulerContractError; use crate::intent::SchedulableTaskIntent; use crate::resource_types::SchedulerResourceFitState; -#[must_use] pub fn select_scheduler_dispatch( request: ValidatedSchedulerDispatchSelectionRequest, ) -> Result { diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-clippy-borrow-result-annotations.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-clippy-borrow-result-annotations.md new file mode 100644 index 000000000..0b2a4003a --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-clippy-borrow-result-annotations.md @@ -0,0 +1,7 @@ +# Redundant borrow and Result annotation repair + +Two independent warning-deny Clippy findings require no behavioral change. Pass the existing &Path directly to fs::read_to_string instead of borrowing it again. Remove a redundant bare must_use annotation from select_scheduler_dispatch because its Result return type already carries that obligation; the validated successful payload also remains must_use. + +Error formatting, deserialization, dispatch policy, signatures, and tests are unchanged. Whitespace validation passes. No local Rust workspace compilation was performed; exact-head hosted qualification and independent source review are pending. These two changes do not address the separate AsRef migration, public runtime enum layout, or other aggregate findings. + +Independent source review accepted the two-line repair at tree b35f32903f85288265a68ffbe57e9d61bf021b80. Publication is stacked on the separately reviewed AsRef milestone to retain cumulative hosted qualification; this report does not imply that aggregate Clippy already passes. From e6160c068164c878c95fe148aea6714df08681d4 Mon Sep 17 00:00:00 2001 From: Puma Date: Sat, 3 Oct 2026 01:22:42 -0700 Subject: [PATCH 21/65] fix(a11y): disclose video caption limits and document graph activation --- .../reports/2026-10-03-preview-a11y-limitations.md | 9 +++++++++ src/components/workbench/IoInspectorPage.svelte | 6 ++++++ src/components/workbench/RunGraphSnapshot.svelte | 1 + .../workbench/ioInspectorPresenters.test.ts | 12 ++++++++++++ src/components/workbench/ioInspectorPresenters.ts | 4 +++- 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-preview-a11y-limitations.md diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-preview-a11y-limitations.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-preview-a11y-limitations.md new file mode 100644 index 000000000..e8805a559 --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-preview-a11y-limitations.md @@ -0,0 +1,9 @@ +# Preview accessibility review and disclosure + +The run graph's conditional button role and tabindex share the same onSelectNode guard. Existing click and Enter/Space activation remain unchanged; the adjacent review reason explains the static compiler limitation without weakening keyboard behavior. + +The artifact projection currently has no caption-track association. Video renderer summaries now disclose that no caption track is provided for this preview; the video preview displays that notice. This does not assert that arbitrary source media has no embedded or burned-in captions, or claim full accessibility. No dummy track is created. Tests cover explicit and inferred video media and non-video families. Current track-association state is absent by contract; adding supported association and validating real accessible playback remain future work. + +Local qualification: 27 presenter tests and 547 full frontend tests pass; the eight parser regressions and full accessibility scan pass; TypeScript and full ESLint pass; both changed Svelte components compile without warnings. This is static/controller evidence, not a real-browser accessibility audit. Independent source review and hosted qualification are pending. + +Independent source review accepted tree bc2ae922fb12ec0776d7bc6be0a52ff3f6297861. The notice describes this preview's absent track association only, and the role/tabindex rationale matches existing activation behavior. This is not a blanket WCAG conformance claim. diff --git a/src/components/workbench/IoInspectorPage.svelte b/src/components/workbench/IoInspectorPage.svelte index 234b96653..6c84f3bf4 100644 --- a/src/components/workbench/IoInspectorPage.svelte +++ b/src/components/workbench/IoInspectorPage.svelte @@ -1049,6 +1049,7 @@ aria-label={`Audio preview of ${artifact.artifact_id}`} > {:else if renderer.family === 'video'} + + {#if renderer.captionNotice} +

+ {renderer.captionNotice} +

+ {/if} {:else if bodyPreview.text !== null && bodyPreview.text !== undefined}
             {/each}
             {#each canvas.nodes as node (node.id)}
+              
               
                {
   assert.equal(rows.find((row) => row.label === 'Last Event Seq')?.value, '44');
   assert.deepEqual(buildRetentionCleanupDetailRows(null), []);
 });
+
+
+test('only video previews disclose the currently absent caption-track association', () => {
+  const video = buildIoArtifactRendererSummary({ media_type: 'video/mp4', payload_ref: 'artifact://video' });
+  assert.equal(video.family, 'video');
+  assert.equal(video.captionNotice, 'No caption track is provided for this preview.');
+  const inferredVideo = buildIoArtifactRendererSummary({ media_type: null, payload_kind: 'video', payload_ref: 'artifact://video' });
+  assert.equal(inferredVideo.captionNotice, video.captionNotice);
+  for (const media_type of ['audio/wav', 'image/png', 'text/plain', 'application/json', 'application/octet-stream']) {
+    assert.equal(buildIoArtifactRendererSummary({ media_type, payload_ref: 'artifact://other' }).captionNotice, undefined);
+  }
+});
diff --git a/src/components/workbench/ioInspectorPresenters.ts b/src/components/workbench/ioInspectorPresenters.ts
index e1d7b1392..d496ae17e 100644
--- a/src/components/workbench/ioInspectorPresenters.ts
+++ b/src/components/workbench/ioInspectorPresenters.ts
@@ -40,6 +40,7 @@ export interface IoArtifactRendererSummary {
   family: IoArtifactMediaFamily;
   title: string;
   detail: string;
+  captionNotice?: string;
 }
 
 export interface IoArtifactDescriptorMetadataRow {
@@ -467,7 +468,8 @@ export function buildIoArtifactRendererSummary(
     case 'audio':
       return { family, title: 'Audio', detail };
     case 'video':
-      return { family, title: 'Video', detail };
+      // The artifact projection has no caption-track association to supply to this preview.
+      return { family, title: 'Video', detail, captionNotice: 'No caption track is provided for this preview.' };
     case '3d':
       return { family, title: '3D asset', detail };
     case 'table':

From 0b5c0df663d76dc97f2ba3836d33ea2404ca79df Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 03:12:13 -0700
Subject: [PATCH 22/65] fix(deps): update frontend security dependency cohort

---
 ...2026-10-03-frontend-dependency-security.md |  35 ++
 package-lock.json                             | 552 ++++++------------
 package.json                                  |  13 +-
 3 files changed, 233 insertions(+), 367 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md

diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md
new file mode 100644
index 000000000..4bb99fe80
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md
@@ -0,0 +1,35 @@
+# Frontend dependency security repair (2026-10-03)
+
+Status: implemented and source-reviewed; exact-head hosted and GUI qualification remain pending. Parent: PR #20, `8a08b6a010ee62fbd3b29e193c65922c0769b928`. Standards reviewed at Coding-Standards `dcc56f26e884ade260770beceba2501d3746200d`; this is newer than the active plan's planning baseline, without changing plan or release authority.
+
+## Resolution and rationale
+
+- Upgrade the four direct Tiptap requirements from `^3.15.3` to `^3.30.5`; all 26 locked Tiptap packages resolve to `3.30.5`, satisfying upstream's exact cohort peers.
+- Upgrade Svelte `5.55.4` to `5.55.7` (requirement `^5.55.7`), and resolve its transitive devalue from `5.7.1` to `5.9.3`. No direct devalue dependency or permanent override is added.
+- [Upstream pm 3.30.5](https://github.com/ueberdosis/tiptap/blob/v3.30.5/packages/pm/package.json) removes prosemirror-markdown. The regenerated graph prunes prosemirror-markdown, markdown-it, linkify-it and their unneeded dependencies; remove the obsolete markdown-it 14.1.1 override. The separate linkifyjs dependency remains and resolves to 4.3.3 through Tiptap.
+- Preserve Vite 6.4.1 and Svelte Vite plugin 5.1.1. Other version changes are confined to Tiptap's ProseMirror requirements. No new package entries appear. Resolver-only peer-flag normalization affects unchanged packages; argparse, entities and escape-string-regexp become dev-only after the production chain is pruned.
+- Root manifest/root lock remain the only npm resolution authority. Registry metadata selected exact candidates; temporary generation constraints prevented caret ranges selecting later releases, then were removed and the lock regenerated. Recovery is restoration of the prior manifest/lock pair, not a second active resolution.
+
+[Official Svelte release](https://github.com/sveltejs/svelte/releases/tag/svelte@5.55.7) and [devalue release](https://github.com/sveltejs/devalue/releases/tag/v5.9.3) describe the security corrections. No source-search result is treated as proof of non-exposure. npm audit and resolver evidence prove their stated graph/advisory properties, not complete application security or release readiness.
+
+## Advisory disposition
+
+A fresh production audit of the parent lock reported five affected package nodes (three high, two moderate). The following advisory IDs are captured from that audit; remediation is the selected upgrade, or graph removal for markdown-it/linkify-it:
+
+- @tiptap/core: [GHSA-cp6q-959q-f8rh](https://github.com/advisories/GHSA-cp6q-959q-f8rh), [GHSA-j95f-988m-3j2f](https://github.com/advisories/GHSA-j95f-988m-3j2f).
+- devalue: [GHSA-77vg-94rm-hx3p](https://github.com/advisories/GHSA-77vg-94rm-hx3p), [GHSA-9rgm-9g3h-6x36](https://github.com/advisories/GHSA-9rgm-9g3h-6x36), [GHSA-j22f-vq7h-c4qm](https://github.com/advisories/GHSA-j22f-vq7h-c4qm), [GHSA-hx4r-w6wj-j8fg](https://github.com/advisories/GHSA-hx4r-w6wj-j8fg), [GHSA-mcm9-63f2-9j32](https://github.com/advisories/GHSA-mcm9-63f2-9j32), [GHSA-wf3x-273g-mvxv](https://github.com/advisories/GHSA-wf3x-273g-mvxv), [GHSA-4q55-j62x-fr9h](https://github.com/advisories/GHSA-4q55-j62x-fr9h).
+- linkify-it: [GHSA-22p9-wv53-3rq4](https://github.com/advisories/GHSA-22p9-wv53-3rq4), [GHSA-v245-v573-v5vm](https://github.com/advisories/GHSA-v245-v573-v5vm).
+- markdown-it: [GHSA-6v5v-wf23-fmfq](https://github.com/advisories/GHSA-6v5v-wf23-fmfq), [GHSA-253c-mchw-3w2r](https://github.com/advisories/GHSA-253c-mchw-3w2r).
+- svelte: [GHSA-f3cj-j4f6-wq85](https://github.com/advisories/GHSA-f3cj-j4f6-wq85), [GHSA-rcqx-6q8c-2c42](https://github.com/advisories/GHSA-rcqx-6q8c-2c42), [GHSA-9rmh-mm8f-r9h6](https://github.com/advisories/GHSA-9rmh-mm8f-r9h6), [GHSA-pr6f-5x2q-rwfp](https://github.com/advisories/GHSA-pr6f-5x2q-rwfp).
+
+## Verification and limits
+
+Local generation and clean install used Node 24.19.0/npm 11.9.0 because the pinned Node 24.12.0/npm 11.6.2 were unavailable. No global toolchain was changed. A clean hosted install and applicable checks using both declared versions are required before qualification; the local results do not waive this requirement.
+
+Passed against the reviewed manifest/lock: `npm ci --include=optional --no-fund --no-audit`, `npm ls --all`, `npm audit --omit=dev --audit-level=high` (zero findings), `npm run typecheck`, `npm run test:frontend` (540/540), `npm run build`, `npm run lint`, `npm run lint:full`, staged decision traceability, and whitespace validation. Existing resolver and audit checks provide the dependency regression evidence; no duplicate version-assertion harness is added.
+
+`npm run lint:no-new` stops at the existing IoInspectorPage.svelte:520 DOM-mutation finding. Separately, `npm run lint:a11y` passes its eight parser tests but reports existing reviewed-ignore annotations at IoInspectorPage.svelte:1034 and RunGraphSnapshot.svelte:156. These unchanged UI issues belong to the parallel UI repair.
+
+`npm run test:workflow-editor-image-gui` exits 2 at preflight because PANTOGRAPH_DIFFUSION_SMOKE_PUMAS_MODEL_ID is unavailable. Real Pumas model/artifact IDs, saved workflow, Python runtime, desktop drivers/display and built host remain prerequisites; no GUI success is claimed. The integration owner must obtain that qualified evidence. Rust/desktop compilation and release publication are outside this dependency slice.
+
+The source manifest/lock pair was independently reviewed before publication at staged tree `b75bac90dbbda40c3006e3228f0ce5eab8ed8f56`. Subsequent documentation adds this report without changing those files. Keep the task worktree protected while hosted qualification or integration is pending; the integration owner controls final merge and terminal disposition.
diff --git a/package-lock.json b/package-lock.json
index d1f5f8729..a7ecc0235 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,13 +13,13 @@
       "dependencies": {
         "@tauri-apps/api": "^2.9.0",
         "@tauri-apps/plugin-dialog": "^2.6.0",
-        "@tiptap/core": "^3.15.3",
-        "@tiptap/extension-placeholder": "^3.15.3",
-        "@tiptap/pm": "^3.15.3",
-        "@tiptap/starter-kit": "^3.15.3",
+        "@tiptap/core": "^3.30.5",
+        "@tiptap/extension-placeholder": "^3.30.5",
+        "@tiptap/pm": "^3.30.5",
+        "@tiptap/starter-kit": "^3.30.5",
         "@xyflow/svelte": "^1.5.0",
         "lucide-svelte": "^0.562.0",
-        "svelte": "^5.55.4",
+        "svelte": "^5.55.7",
         "three": "^0.183.1"
       },
       "devDependencies": {
@@ -1377,12 +1377,6 @@
         "node": ">=18"
       }
     },
-    "node_modules/@remirror/core-constants": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
-      "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
-      "license": "MIT"
-    },
     "node_modules/@rollup/rollup-android-arm-eabi": {
       "version": "4.55.1",
       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
@@ -1784,7 +1778,6 @@
       "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
         "debug": "^4.4.1",
@@ -2056,353 +2049,345 @@
       }
     },
     "node_modules/@tiptap/core": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.15.3.tgz",
-      "integrity": "sha512-bmXydIHfm2rEtGju39FiQNfzkFx9CDvJe+xem1dgEZ2P6Dj7nQX9LnA1ZscW7TuzbBRkL5p3dwuBIi3f62A66A==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.30.5.tgz",
+      "integrity": "sha512-3O7N0FyKIfuLV+xrdWyDM3V5eUY/q2CgLjhhMwOAbM1Pu7VPp9VP+TpEYOdH8aRyB+h1vj5hX5A747D8ZrPfHA==",
       "license": "MIT",
-      "peer": true,
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/pm": "^3.15.3"
+        "@tiptap/pm": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-blockquote": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.15.3.tgz",
-      "integrity": "sha512-13x5UsQXtttFpoS/n1q173OeurNxppsdWgP3JfsshzyxIghhC141uL3H6SGYQLPU31AizgDs2OEzt6cSUevaZg==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.30.5.tgz",
+      "integrity": "sha512-8pf1ZDrl6XlVPUee/2YlWZPFSF7yqsJEX6WLW41x06MT1dapG3Qudcns0znj6kYsX8JXTJ+Mhegy4z19bvTtRg==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3"
+        "@tiptap/core": "3.30.5",
+        "@tiptap/pm": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-bold": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.15.3.tgz",
-      "integrity": "sha512-I8JYbkkUTNUXbHd/wCse2bR0QhQtJD7+0/lgrKOmGfv5ioLxcki079Nzuqqay3PjgYoJLIJQvm3RAGxT+4X91w==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.30.5.tgz",
+      "integrity": "sha512-MLZS+s/BJiJbv2C2M3G4FQGn43kPwYUUr2TiW3afSn+dRkEZlDxx5CwZYbe1PEt2+VX/SIifn945Oqr3s0axSA==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3"
+        "@tiptap/core": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-bullet-list": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.15.3.tgz",
-      "integrity": "sha512-MGwEkNT7ltst6XaWf0ObNgpKQ4PvuuV3igkBrdYnQS+qaAx9IF4isygVPqUc9DvjYC306jpyKsNqNrENIXcosA==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.30.5.tgz",
+      "integrity": "sha512-66OGw4suO0Gr/4QAEiSvVi0eSvEqStvu1moHUSvUlwvEqnuB0ME2w9HuRogPElzWdrYLzbBA47mlCi9Veva0ew==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/extension-list": "^3.15.3"
+        "@tiptap/extension-list": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-code": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.15.3.tgz",
-      "integrity": "sha512-x6LFt3Og6MFINYpsMzrJnz7vaT9Yk1t4oXkbJsJRSavdIWBEBcoRudKZ4sSe/AnsYlRJs8FY2uR76mt9e+7xAQ==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.30.5.tgz",
+      "integrity": "sha512-0dCt8eBo4sMtw+LjUu+0GF0JrTlREROdWoy8TM5kFPxJ5ZU2LKDiROXDPwXStaaoFFR/aStLH9xSpdz7cHUiUA==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3"
+        "@tiptap/core": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-code-block": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.15.3.tgz",
-      "integrity": "sha512-q1UB9icNfdJppTqMIUWfoRKkx5SSdWIpwZoL2NeOI5Ah3E20/dQKVttIgLhsE521chyvxCYCRaHD5tMNGKfhyw==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.30.5.tgz",
+      "integrity": "sha512-SvkNoOio2xBy9AtSMyFKMp88MTUvyIXsGCnuz71INV2wHdH4VfRPoQLT6e077LpTCa9w6LHyTKtbZA4HYOp7wg==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3",
-        "@tiptap/pm": "^3.15.3"
+        "@tiptap/core": "3.30.5",
+        "@tiptap/pm": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-document": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.15.3.tgz",
-      "integrity": "sha512-AC72nI2gnogBuETCKbZekn+h6t5FGGcZG2abPGKbz/x9rwpb6qV2hcbAQ30t6M7H6cTOh2/Ut8bEV2MtMB15sw==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.30.5.tgz",
+      "integrity": "sha512-4mKoD3bBr5W2AQ2Y/7amOqcVw0eqJbUwRtwvxypeo5Hi0PB9O5Ev8P05c3EUTo10bllqdKYXGeCU5AcgdQsHWQ==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3"
+        "@tiptap/core": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-dropcursor": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.15.3.tgz",
-      "integrity": "sha512-jGI5XZpdo8GSYQFj7HY15/oEwC2m2TqZz0/Fln5qIhY32XlZhWrsMuMI6WbUJrTH16es7xO6jmRlDsc6g+vJWg==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.30.5.tgz",
+      "integrity": "sha512-gJxn9PMUee8zazQBYqbOZiI7zTFXPVJ15AzTnLfUu4eDtPieMwtpgcjhCt4bZlp37drLm+Li76wWLirb7iLxsw==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/extensions": "^3.15.3"
+        "@tiptap/extensions": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-gapcursor": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.15.3.tgz",
-      "integrity": "sha512-Kaw0sNzP0bQI/xEAMSfIpja6xhsu9WqqAK/puzOIS1RKWO47Wps/tzqdSJ9gfslPIb5uY5mKCfy8UR8Xgiia8w==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.30.5.tgz",
+      "integrity": "sha512-h3m2ZA1XXLAfdHwdtG0MZnf0KWbNyP8xTI3RUlTDR5apmKmYVBaeocOJwTcFFZ92gPKqxYKyhteZqYtHOCZGJA==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/extensions": "^3.15.3"
+        "@tiptap/extensions": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-hard-break": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.15.3.tgz",
-      "integrity": "sha512-8HjxmeRbBiXW+7JKemAJtZtHlmXQ9iji398CPQ0yYde68WbIvUhHXjmbJE5pxFvvQTJ/zJv1aISeEOZN2bKBaw==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.30.5.tgz",
+      "integrity": "sha512-PcL4Z8l/DlauwZUJg0jf6SXKk6/YOXJtd8yYzqHrmCLRoY4kAa7+TxUhc/lWhuitUiOZ8+nB8V/NNM+WR6mRaw==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3"
+        "@tiptap/core": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-heading": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.15.3.tgz",
-      "integrity": "sha512-G1GG6iN1YXPS+75arDpo+bYRzhr3dNDw99c7D7na3aDawa9Qp7sZ/bVrzFUUcVEce0cD6h83yY7AooBxEc67hA==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.30.5.tgz",
+      "integrity": "sha512-x7e7+p1bWXvwz6vqONPP1SVB73V0gZP7LIDL/5KMnxWLjahMTabgVPgjV30kz7GsOEgUYtEf7eCyGVGNNWL5Dg==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3"
+        "@tiptap/core": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-horizontal-rule": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.15.3.tgz",
-      "integrity": "sha512-FYkN7L6JsfwwNEntmLklCVKvgL0B0N47OXMacRk6kYKQmVQ4Nvc7q/VJLpD9sk4wh4KT1aiCBfhKEBTu5pv1fg==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.30.5.tgz",
+      "integrity": "sha512-s5t2xM6wPYRJl2cqYplc7Ze8m8xddk4DP2v0aLFcUuD100D1B5eJbxsiwLK6wBAzNwiuVjd486RnmUpcswaXhQ==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3",
-        "@tiptap/pm": "^3.15.3"
+        "@tiptap/core": "3.30.5",
+        "@tiptap/pm": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-italic": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.15.3.tgz",
-      "integrity": "sha512-6XeuPjcWy7OBxpkgOV7bD6PATO5jhIxc8SEK4m8xn8nelGTBIbHGqK37evRv+QkC7E0MUryLtzwnmmiaxcKL0Q==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.30.5.tgz",
+      "integrity": "sha512-Fu9EuSRlHQNGES7UhY4mQod3yUKnWwxsGlPuDJURfwju/Ug04Jz8iRuERZBPcRgLNkwKDmbDsWUZF72RRIL2Uw==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3"
+        "@tiptap/core": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-link": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.15.3.tgz",
-      "integrity": "sha512-PdDXyBF9Wco9U1x6e+b7tKBWG+kqBDXDmaYXHkFm/gYuQCQafVJ5mdrDdKgkHDWVnJzMWZXBcZjT9r57qtlLWg==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.30.5.tgz",
+      "integrity": "sha512-zsQ+q83HpCYOMTNzdjb12dggE7sZMp0aqQJhArdavSTl8hLbVY/u3qJN88t25IEwAKVK0pcTJfWWA2QFl8bjAQ==",
       "license": "MIT",
       "dependencies": {
-        "linkifyjs": "^4.3.2"
+        "linkifyjs": "^4.3.3"
       },
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3",
-        "@tiptap/pm": "^3.15.3"
+        "@tiptap/core": "3.30.5",
+        "@tiptap/pm": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-list": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.15.3.tgz",
-      "integrity": "sha512-n7y/MF9lAM5qlpuH5IR4/uq+kJPEJpe9NrEiH+NmkO/5KJ6cXzpJ6F4U17sMLf2SNCq+TWN9QK8QzoKxIn50VQ==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.30.5.tgz",
+      "integrity": "sha512-CHThihH+7TA0TfwtmA+eTXP1MAsA/+p011olJHg8Rilj5xA+L+0IFkjhN16JnZkBgSA4MnfEv15UMNfRaNpuVg==",
       "license": "MIT",
-      "peer": true,
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3",
-        "@tiptap/pm": "^3.15.3"
+        "@tiptap/core": "3.30.5",
+        "@tiptap/pm": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-list-item": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.15.3.tgz",
-      "integrity": "sha512-CCxL5ek1p0lO5e8aqhnPzIySldXRSigBFk2fP9OLgdl5qKFLs2MGc19jFlx5+/kjXnEsdQTFbGY1Sizzt0TVDw==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.30.5.tgz",
+      "integrity": "sha512-N0fKUyQkPQBvVB48imjfIfdexU9VsAX1RydajYP2xAt+QOJ8KHVmXv1EGTp+v7pGdZMLdls1cSZ2f352vW3aWQ==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/extension-list": "^3.15.3"
+        "@tiptap/extension-list": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-list-keymap": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.15.3.tgz",
-      "integrity": "sha512-UxqnTEEAKrL+wFQeSyC9z0mgyUUVRS2WTcVFoLZCE6/Xus9F53S4bl7VKFadjmqI4GpDk5Oe2IOUc72o129jWg==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.30.5.tgz",
+      "integrity": "sha512-3pLR2yo29uhowaGMbD8v71XpgdNmqotBg1NXlyTMVFzxELj+VjjeLLTqSguR/iWDUbHXNzIsfMEgpRnNMO//8w==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/extension-list": "^3.15.3"
+        "@tiptap/extension-list": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-ordered-list": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.15.3.tgz",
-      "integrity": "sha512-/8uhw528Iy0c9wF6tHCiIn0ToM0Ml6Ll2c/3iPRnKr4IjXwx2Lr994stUFihb+oqGZwV1J8CPcZJ4Ufpdqi4Dw==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.30.5.tgz",
+      "integrity": "sha512-bUGUnSAgjZhoUWBtr+1wRJHF3NnNvuKQr6sQ8le1tJ2yvLq448ZsSZjZGvTNeLsy3GDKBoMJ0rqLUK34+nM3hg==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/extension-list": "^3.15.3"
+        "@tiptap/extension-list": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-paragraph": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.15.3.tgz",
-      "integrity": "sha512-lc0Qu/1AgzcEfS67NJMj5tSHHhH6NtA6uUpvppEKGsvJwgE2wKG1onE4isrVXmcGRdxSMiCtyTDemPNMu6/ozQ==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.30.5.tgz",
+      "integrity": "sha512-GrNNlAImfQhYRtGE95YNAjcTclUUMPHs9JeE9vMgfcoKcOmZ6GsbWUdN0hA55xqwGaUjnroOVtf77K9z1EbkJQ==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3"
+        "@tiptap/core": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-placeholder": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.15.3.tgz",
-      "integrity": "sha512-XcHHnojT186hKIoOgcPBesXk89+caNGVUdMtc171Vcr/5s0dpnr4q5LfE+YRC+S85CpCxCRRnh84Ou+XRtOqrw==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.30.5.tgz",
+      "integrity": "sha512-WgkX4ImAAeV+Qe7ATJ+Gio2MmKWON1CKpCKPHWgCObaoMEaSmdoxXPmI9wXMuM+AJydJepp4n7U/V4TVts14rg==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/extensions": "^3.15.3"
+        "@tiptap/extensions": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-strike": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.15.3.tgz",
-      "integrity": "sha512-Y1P3eGNY7RxQs2BcR6NfLo9VfEOplXXHAqkOM88oowWWOE7dMNeFFZM9H8HNxoQgXJ7H0aWW9B7ZTWM9hWli2Q==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.30.5.tgz",
+      "integrity": "sha512-r8IbWUm4YXNCkmc6h6dzuREQIGbO0x9z9l3GfQPUR3LxFtGssDLngE4dFXHRt/ejLWKEauy6clklf7LawtvYzw==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3"
+        "@tiptap/core": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-text": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.15.3.tgz",
-      "integrity": "sha512-MhkBz8ZvrqOKtKNp+ZWISKkLUlTrDR7tbKZc2OnNcUTttL9dz0HwT+cg91GGz19fuo7ttDcfsPV6eVmflvGToA==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.30.5.tgz",
+      "integrity": "sha512-pOgj4mIGFlw4NUdA6PCTFP/MHrYWnOYiZRYeu0I3/Yo3iN4Am/CgYBLl8PWsNh+YPO2hA2DWs7ZMke9D2j5R+g==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3"
+        "@tiptap/core": "3.30.5"
       }
     },
     "node_modules/@tiptap/extension-underline": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.15.3.tgz",
-      "integrity": "sha512-r/IwcNN0W366jGu4Y0n2MiFq9jGa4aopOwtfWO4d+J0DyeS2m7Go3+KwoUqi0wQTiVU74yfi4DF6eRsMQ9/iHQ==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.30.5.tgz",
+      "integrity": "sha512-u12G/WW2uFRY95BzrSAU9RW002K+7lFpSCL6fb7Puh8yLhek3lddOtAx0P+NI/PyMKtcoVMtBxoi+0mwVM7NPQ==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3"
+        "@tiptap/core": "3.30.5"
       }
     },
     "node_modules/@tiptap/extensions": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.15.3.tgz",
-      "integrity": "sha512-ycx/BgxR4rc9tf3ZyTdI98Z19yKLFfqM3UN+v42ChuIwkzyr9zyp7kG8dB9xN2lNqrD+5y/HyJobz/VJ7T90gA==",
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.30.5.tgz",
+      "integrity": "sha512-5x3OiCYBvXz0G5OGM8f7ka++1dh9EXdNRZ8IcMcEd+QwW4RPwvmJ2EjheA1eRRyMlcbNU7T+4IRlBXURlTetEA==",
       "license": "MIT",
-      "peer": true,
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
       },
       "peerDependencies": {
-        "@tiptap/core": "^3.15.3",
-        "@tiptap/pm": "^3.15.3"
+        "@tiptap/core": "3.30.5",
+        "@tiptap/pm": "3.30.5"
       }
     },
     "node_modules/@tiptap/pm": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.15.3.tgz",
-      "integrity": "sha512-Zm1BaU1TwFi3CQiisxjgnzzIus+q40bBKWLqXf6WEaus8Z6+vo1MT2pU52dBCMIRaW9XNDq3E5cmGtMc1AlveA==",
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "prosemirror-changeset": "^2.3.0",
-        "prosemirror-collab": "^1.3.1",
-        "prosemirror-commands": "^1.6.2",
-        "prosemirror-dropcursor": "^1.8.1",
-        "prosemirror-gapcursor": "^1.3.2",
-        "prosemirror-history": "^1.4.1",
-        "prosemirror-inputrules": "^1.4.0",
-        "prosemirror-keymap": "^1.2.2",
-        "prosemirror-markdown": "^1.13.1",
-        "prosemirror-menu": "^1.2.4",
-        "prosemirror-model": "^1.24.1",
-        "prosemirror-schema-basic": "^1.2.3",
-        "prosemirror-schema-list": "^1.5.0",
-        "prosemirror-state": "^1.4.3",
-        "prosemirror-tables": "^1.6.4",
-        "prosemirror-trailing-node": "^3.0.0",
-        "prosemirror-transform": "^1.10.2",
-        "prosemirror-view": "^1.38.1"
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.30.5.tgz",
+      "integrity": "sha512-gufkLkW2tA6PZPjivYxDiGzTIIftwqhmYI6lvvKu2S4FbhcysJgMAe/GXVSywzCRVVex9SrvCe6RFYrqnwRitQ==",
+      "license": "MIT",
+      "dependencies": {
+        "prosemirror-changeset": "^2.4.1",
+        "prosemirror-commands": "^1.7.1",
+        "prosemirror-dropcursor": "^1.8.2",
+        "prosemirror-gapcursor": "^1.4.1",
+        "prosemirror-history": "^1.5.0",
+        "prosemirror-inputrules": "^1.5.1",
+        "prosemirror-keymap": "^1.2.3",
+        "prosemirror-model": "^1.25.11",
+        "prosemirror-schema-list": "^1.5.1",
+        "prosemirror-state": "^1.4.4",
+        "prosemirror-tables": "^1.8.5",
+        "prosemirror-transform": "^1.12.0",
+        "prosemirror-view": "^1.41.9"
       },
       "funding": {
         "type": "github",
@@ -2410,35 +2395,35 @@
       }
     },
     "node_modules/@tiptap/starter-kit": {
-      "version": "3.15.3",
-      "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.15.3.tgz",
-      "integrity": "sha512-ia+eQr9Mt1ln2UO+kK4kFTJOrZK4GhvZXFjpCCYuHtco3rhr2fZAIxEEY4cl/vo5VO5WWyPqxhkFeLcoWmNjSw==",
-      "license": "MIT",
-      "dependencies": {
-        "@tiptap/core": "^3.15.3",
-        "@tiptap/extension-blockquote": "^3.15.3",
-        "@tiptap/extension-bold": "^3.15.3",
-        "@tiptap/extension-bullet-list": "^3.15.3",
-        "@tiptap/extension-code": "^3.15.3",
-        "@tiptap/extension-code-block": "^3.15.3",
-        "@tiptap/extension-document": "^3.15.3",
-        "@tiptap/extension-dropcursor": "^3.15.3",
-        "@tiptap/extension-gapcursor": "^3.15.3",
-        "@tiptap/extension-hard-break": "^3.15.3",
-        "@tiptap/extension-heading": "^3.15.3",
-        "@tiptap/extension-horizontal-rule": "^3.15.3",
-        "@tiptap/extension-italic": "^3.15.3",
-        "@tiptap/extension-link": "^3.15.3",
-        "@tiptap/extension-list": "^3.15.3",
-        "@tiptap/extension-list-item": "^3.15.3",
-        "@tiptap/extension-list-keymap": "^3.15.3",
-        "@tiptap/extension-ordered-list": "^3.15.3",
-        "@tiptap/extension-paragraph": "^3.15.3",
-        "@tiptap/extension-strike": "^3.15.3",
-        "@tiptap/extension-text": "^3.15.3",
-        "@tiptap/extension-underline": "^3.15.3",
-        "@tiptap/extensions": "^3.15.3",
-        "@tiptap/pm": "^3.15.3"
+      "version": "3.30.5",
+      "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.30.5.tgz",
+      "integrity": "sha512-cmDRvukpoqjQjhE96q+l3tCLNZZcJKehxK+00Sp9F4XbKecfM2QpZ2TRHh3vi8qhCeDQNyMoCqfNaqznIjLjIQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tiptap/core": "3.30.5",
+        "@tiptap/extension-blockquote": "3.30.5",
+        "@tiptap/extension-bold": "3.30.5",
+        "@tiptap/extension-bullet-list": "3.30.5",
+        "@tiptap/extension-code": "3.30.5",
+        "@tiptap/extension-code-block": "3.30.5",
+        "@tiptap/extension-document": "3.30.5",
+        "@tiptap/extension-dropcursor": "3.30.5",
+        "@tiptap/extension-gapcursor": "3.30.5",
+        "@tiptap/extension-hard-break": "3.30.5",
+        "@tiptap/extension-heading": "3.30.5",
+        "@tiptap/extension-horizontal-rule": "3.30.5",
+        "@tiptap/extension-italic": "3.30.5",
+        "@tiptap/extension-link": "3.30.5",
+        "@tiptap/extension-list": "3.30.5",
+        "@tiptap/extension-list-item": "3.30.5",
+        "@tiptap/extension-list-keymap": "3.30.5",
+        "@tiptap/extension-ordered-list": "3.30.5",
+        "@tiptap/extension-paragraph": "3.30.5",
+        "@tiptap/extension-strike": "3.30.5",
+        "@tiptap/extension-text": "3.30.5",
+        "@tiptap/extension-underline": "3.30.5",
+        "@tiptap/extensions": "3.30.5",
+        "@tiptap/pm": "3.30.5"
       },
       "funding": {
         "type": "github",
@@ -2548,28 +2533,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/@types/linkify-it": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
-      "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
-      "license": "MIT"
-    },
-    "node_modules/@types/markdown-it": {
-      "version": "14.1.2",
-      "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
-      "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/linkify-it": "^5",
-        "@types/mdurl": "^2"
-      }
-    },
-    "node_modules/@types/mdurl": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
-      "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
-      "license": "MIT"
-    },
     "node_modules/@types/mocha": {
       "version": "10.0.10",
       "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
@@ -2583,7 +2546,6 @@
       "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "undici-types": "~6.21.0"
       }
@@ -2735,7 +2697,6 @@
       "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@typescript-eslint/scope-manager": "8.53.0",
         "@typescript-eslint/types": "8.53.0",
@@ -2843,7 +2804,6 @@
       "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==",
       "devOptional": true,
       "license": "MIT",
-      "peer": true,
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
@@ -3155,7 +3115,6 @@
       "integrity": "sha512-RNvk07rQFEizO30a0m5snaWDY4EF0vUZcUDV/aK7bs5RIao5RQwMr+cPNVu9CbvizAATGVRlQz02ppHF7U2Ypg==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "engines": {
         "node": ">=18.20.0"
       },
@@ -3210,7 +3169,6 @@
       "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "chalk": "^5.1.2",
         "loglevel": "^1.6.0",
@@ -3468,7 +3426,6 @@
       "resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-1.5.0.tgz",
       "integrity": "sha512-kHJVwMtabN7xthDJqLvx5wGT5qjOAVV90J9a26geDS51lrGsGb7CCxNRiZCMuVDlRkczGVIy9dkqdph6FL4slQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@svelte-put/shortcut": "^4.1.0",
         "@xyflow/system": "0.0.74"
@@ -3524,7 +3481,6 @@
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
       "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
       "license": "MIT",
-      "peer": true,
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -3701,6 +3657,7 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true,
       "license": "Python-2.0"
     },
     "node_modules/aria-query": {
@@ -4020,7 +3977,6 @@
         }
       ],
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "baseline-browser-mapping": "^2.9.0",
         "caniuse-lite": "^1.0.30001759",
@@ -4551,12 +4507,6 @@
         "node": ">=20"
       }
     },
-    "node_modules/crelt": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
-      "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
-      "license": "MIT"
-    },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4685,7 +4635,6 @@
       "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
       "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
       "license": "ISC",
-      "peer": true,
       "engines": {
         "node": ">=12"
       }
@@ -4842,9 +4791,9 @@
       }
     },
     "node_modules/devalue": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz",
-      "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==",
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.3.tgz",
+      "integrity": "sha512-xRumYOCUZN/EesqHEU3WOXanOZNvfZFZ/o1AHVFDX1yI0UAkZkOgDXt341CzKoBVIkgQba55/+DjGBKrIoKcHw==",
       "license": "MIT"
     },
     "node_modules/didyoumean": {
@@ -5101,6 +5050,7 @@
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
       "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "dev": true,
       "license": "BSD-2-Clause",
       "engines": {
         "node": ">=0.12"
@@ -5182,6 +5132,7 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
       "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=10"
@@ -5218,7 +5169,6 @@
       "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.8.0",
         "@eslint-community/regexpp": "^4.12.1",
@@ -5532,7 +5482,6 @@
       "integrity": "sha512-VfLC9o84B40LEw+zX7UykUptKkscX1rPYY4jaAsQ6KyKL0X0ltDkWzKIUiY9g/u0ApNerrkhM/QEY0TDT8pJOQ==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@vitest/snapshot": "^4.1.7",
         "deep-eql": "^5.0.2",
@@ -6709,7 +6658,6 @@
       "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "bin": {
         "jiti": "bin/jiti.js"
       }
@@ -6925,19 +6873,10 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/linkify-it": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
-      "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
-      "license": "MIT",
-      "dependencies": {
-        "uc.micro": "^2.0.0"
-      }
-    },
     "node_modules/linkifyjs": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
-      "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
+      "version": "4.3.3",
+      "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
+      "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
       "license": "MIT"
     },
     "node_modules/locate-app": {
@@ -7122,29 +7061,6 @@
         "@jridgewell/sourcemap-codec": "^1.5.5"
       }
     },
-    "node_modules/markdown-it": {
-      "version": "14.1.1",
-      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
-      "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
-      "license": "MIT",
-      "dependencies": {
-        "argparse": "^2.0.1",
-        "entities": "^4.4.0",
-        "linkify-it": "^5.0.0",
-        "mdurl": "^2.0.0",
-        "punycode.js": "^2.3.1",
-        "uc.micro": "^2.1.0"
-      },
-      "bin": {
-        "markdown-it": "bin/markdown-it.mjs"
-      }
-    },
-    "node_modules/mdurl": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
-      "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
-      "license": "MIT"
-    },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -7959,7 +7875,6 @@
         }
       ],
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "nanoid": "^3.3.11",
         "picocolors": "^1.1.1",
@@ -8251,27 +8166,18 @@
       }
     },
     "node_modules/prosemirror-changeset": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz",
-      "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==",
+      "version": "2.4.4",
+      "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.4.tgz",
+      "integrity": "sha512-Gyuz9jYdrAeA9I7AholyOes9KUmv/lEwsC5O9s5xF5VB3EgTPsXMGUxz4+H1B+wKVPdZbvRcg1Cp3x8qX+8ofA==",
       "license": "MIT",
       "dependencies": {
         "prosemirror-transform": "^1.0.0"
       }
     },
-    "node_modules/prosemirror-collab": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
-      "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
-      "license": "MIT",
-      "dependencies": {
-        "prosemirror-state": "^1.0.0"
-      }
-    },
     "node_modules/prosemirror-commands": {
-      "version": "1.7.1",
-      "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
-      "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz",
+      "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==",
       "license": "MIT",
       "dependencies": {
         "prosemirror-model": "^1.0.0",
@@ -8280,9 +8186,9 @@
       }
     },
     "node_modules/prosemirror-dropcursor": {
-      "version": "1.8.2",
-      "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
-      "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
+      "version": "1.8.4",
+      "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.4.tgz",
+      "integrity": "sha512-VmKxhcA6a+h5oPSYmfF1ZNeQqPbyYaIxqZsKqeYCyTlPtrjUgNaHL3P8vVaO19oqAV0zuaM/axkowZHrB1P3Tg==",
       "license": "MIT",
       "dependencies": {
         "prosemirror-state": "^1.0.0",
@@ -8291,9 +8197,9 @@
       }
     },
     "node_modules/prosemirror-gapcursor": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz",
-      "integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==",
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
+      "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
       "license": "MIT",
       "dependencies": {
         "prosemirror-keymap": "^1.0.0",
@@ -8303,9 +8209,9 @@
       }
     },
     "node_modules/prosemirror-history": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
-      "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.1.tgz",
+      "integrity": "sha512-onlwnqKvgtFg6srZOmDOAhlAy1FIHQpOExORybX0S+DvJepE5tPALrB9iGet/2sqqqrelSnB10OoiOJqAvu2DA==",
       "license": "MIT",
       "dependencies": {
         "prosemirror-state": "^1.2.2",
@@ -8334,48 +8240,15 @@
         "w3c-keyname": "^2.2.0"
       }
     },
-    "node_modules/prosemirror-markdown": {
-      "version": "1.13.2",
-      "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz",
-      "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/markdown-it": "^14.0.0",
-        "markdown-it": "^14.0.0",
-        "prosemirror-model": "^1.25.0"
-      }
-    },
-    "node_modules/prosemirror-menu": {
-      "version": "1.2.5",
-      "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz",
-      "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==",
-      "license": "MIT",
-      "dependencies": {
-        "crelt": "^1.0.0",
-        "prosemirror-commands": "^1.0.0",
-        "prosemirror-history": "^1.0.0",
-        "prosemirror-state": "^1.0.0"
-      }
-    },
     "node_modules/prosemirror-model": {
-      "version": "1.25.4",
-      "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
-      "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
+      "version": "1.25.12",
+      "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.12.tgz",
+      "integrity": "sha512-Ue2gTmXMa7EhpLNhC7J+h4+ykD8ha12K6rrZFFKKJHBForfIStw5gJ6Zrf1mqqAa9NmpmEB4wp9bKmA4eUVjWg==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "orderedmap": "^2.0.0"
       }
     },
-    "node_modules/prosemirror-schema-basic": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
-      "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
-      "license": "MIT",
-      "dependencies": {
-        "prosemirror-model": "^1.25.0"
-      }
-    },
     "node_modules/prosemirror-schema-list": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
@@ -8392,7 +8265,6 @@
       "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
       "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "prosemirror-model": "^1.0.0",
         "prosemirror-transform": "^1.0.0",
@@ -8412,38 +8284,22 @@
         "prosemirror-view": "^1.41.4"
       }
     },
-    "node_modules/prosemirror-trailing-node": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
-      "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@remirror/core-constants": "3.0.0",
-        "escape-string-regexp": "^4.0.0"
-      },
-      "peerDependencies": {
-        "prosemirror-model": "^1.22.1",
-        "prosemirror-state": "^1.4.2",
-        "prosemirror-view": "^1.33.8"
-      }
-    },
     "node_modules/prosemirror-transform": {
-      "version": "1.10.5",
-      "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz",
-      "integrity": "sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==",
+      "version": "1.12.2",
+      "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.2.tgz",
+      "integrity": "sha512-PE/aY0HEY4zczvmqrilgkUK/WautF0chvMqkmM/iN5/aPHvwrCWaQZjS6CIZp+K84YrvPFqrL+YArrfJX8xX7g==",
       "license": "MIT",
       "dependencies": {
         "prosemirror-model": "^1.21.0"
       }
     },
     "node_modules/prosemirror-view": {
-      "version": "1.41.5",
-      "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.5.tgz",
-      "integrity": "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==",
+      "version": "1.42.6",
+      "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.6.tgz",
+      "integrity": "sha512-3g25f6hKV2bBxo3puH3EcYWmmVNSH0iWmNsMVT7sugDSpA025jgpOLarcDWr39SdzPjYMaiYtRdNtXialPSjEQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
-        "prosemirror-model": "^1.20.0",
+        "prosemirror-model": "^1.25.12",
         "prosemirror-state": "^1.0.0",
         "prosemirror-transform": "^1.1.0"
       }
@@ -8506,15 +8362,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/punycode.js": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
-      "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/query-selector-shadow-dom": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
@@ -9511,11 +9358,10 @@
       }
     },
     "node_modules/svelte": {
-      "version": "5.55.4",
-      "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.4.tgz",
-      "integrity": "sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ==",
+      "version": "5.55.7",
+      "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.7.tgz",
+      "integrity": "sha512-ymI5ykLPwIHW839E053FQbI1G+jnRFJEw3Kv5Y4njixVWywQBx+NUFpkkKyk5LIb36Fg9DVXSYpqiGekLD0hyw==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@jridgewell/remapping": "^2.3.4",
         "@jridgewell/sourcemap-codec": "^1.5.0",
@@ -9526,7 +9372,7 @@
         "aria-query": "5.3.1",
         "axobject-query": "^4.1.0",
         "clsx": "^2.1.1",
-        "devalue": "^5.6.4",
+        "devalue": "^5.8.1",
         "esm-env": "^1.2.1",
         "esrap": "^2.2.4",
         "is-reference": "^3.0.3",
@@ -9794,7 +9640,6 @@
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -9858,7 +9703,6 @@
       "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "esbuild": "~0.28.0"
       },
@@ -10388,7 +10232,6 @@
       "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
       "dev": true,
       "license": "Apache-2.0",
-      "peer": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -10421,12 +10264,6 @@
         "typescript": ">=4.8.4 <6.0.0"
       }
     },
-    "node_modules/uc.micro": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
-      "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
-      "license": "MIT"
-    },
     "node_modules/undici": {
       "version": "6.27.0",
       "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
@@ -10539,7 +10376,6 @@
       "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "esbuild": "^0.25.0",
         "fdir": "^6.4.4",
@@ -10633,7 +10469,6 @@
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -10745,7 +10580,6 @@
       "integrity": "sha512-OoKlyOHhqUGIT1kL5mYbkjDI3bMf5xkBl4FsBwElOkU3aeUAKRe4aW8LzoQ1aNfut8POpjqcadQ4L5Lc4tnUwg==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@types/node": "^20.11.30",
         "@types/sinonjs__fake-timers": "^8.1.5",
diff --git a/package.json b/package.json
index 6f3b5f18e..7667024ce 100644
--- a/package.json
+++ b/package.json
@@ -38,18 +38,15 @@
   "dependencies": {
     "@tauri-apps/api": "^2.9.0",
     "@tauri-apps/plugin-dialog": "^2.6.0",
-    "@tiptap/core": "^3.15.3",
-    "@tiptap/extension-placeholder": "^3.15.3",
-    "@tiptap/pm": "^3.15.3",
-    "@tiptap/starter-kit": "^3.15.3",
+    "@tiptap/core": "^3.30.5",
+    "@tiptap/extension-placeholder": "^3.30.5",
+    "@tiptap/pm": "^3.30.5",
+    "@tiptap/starter-kit": "^3.30.5",
     "@xyflow/svelte": "^1.5.0",
     "lucide-svelte": "^0.562.0",
-    "svelte": "^5.55.4",
+    "svelte": "^5.55.7",
     "three": "^0.183.1"
   },
-  "overrides": {
-    "markdown-it": "14.1.1"
-  },
   "devDependencies": {
     "@eslint/js": "^9.0.0",
     "@sveltejs/vite-plugin-svelte": "^5.0.0",

From f6d03a0ea4588f23aa9533c95835c92e42e64810 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 03:19:50 -0700
Subject: [PATCH 23/65] ci: qualify frontend dependency tree and build

---
 .github/workflows/quality-gates.yml                       | 6 ++++++
 .../reports/2026-10-03-frontend-dependency-security.md    | 8 ++++++++
 2 files changed, 14 insertions(+)

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 6481af342..a06d4a195 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -121,6 +121,12 @@ jobs:
       - name: Block high severity production vulnerabilities
         run: npm audit --omit=dev --audit-level=high
 
+      - name: Verify installed dependency tree
+        run: npm ls --all
+
+      - name: Build frontend assets
+        run: npm run build
+
   rust-check:
     name: Rust workspace check
     runs-on: ubuntu-latest
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md
index 4bb99fe80..81477aa9e 100644
--- a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md
@@ -33,3 +33,11 @@ Passed against the reviewed manifest/lock: `npm ci --include=optional --no-fund
 `npm run test:workflow-editor-image-gui` exits 2 at preflight because PANTOGRAPH_DIFFUSION_SMOKE_PUMAS_MODEL_ID is unavailable. Real Pumas model/artifact IDs, saved workflow, Python runtime, desktop drivers/display and built host remain prerequisites; no GUI success is claimed. The integration owner must obtain that qualified evidence. Rust/desktop compilation and release publication are outside this dependency slice.
 
 The source manifest/lock pair was independently reviewed before publication at staged tree `b75bac90dbbda40c3006e3228f0ce5eab8ed8f56`. Subsequent documentation adds this report without changing those files. Keep the task worktree protected while hosted qualification or integration is pending; the integration owner controls final merge and terminal disposition.
+
+## Hosted qualification follow-up
+
+[Quality Gates run 37115665903](https://github.com/MrScripty/Pantograph/actions/runs/37115665903) confirms Node 24.12.0/npm 11.6.2, clean npm ci, zero production-audit findings, 540/540 frontend tests, typecheck and full lint. Its synthetic merge `e66f54bc7b3f2963956918a6a781fc3a2ecf47ca` has the same tree `1c51bc4dcce332846e550ecf6374864521a11099` as published source commit `0b5c0df663d76dc97f2ba3836d33ea2404ca79df`. The unchanged critical-lint failure remains visible.
+
+That workflow lacked installed-tree and frontend-build checks. Add `npm ls --all` and `npm run build` to the existing dependency-audit job after clean installation and the production audit, preserving every existing command and aggregate gate. These checks prove resolver consistency and frontend asset compilation on the declared toolchain; they do not establish GUI or packaged-release acceptance. Exact-head hosted results for this workflow change remain pending.
+
+The hosted development-inclusive install audit reports 41 affected package nodes (1 low, 5 moderate, 35 high). Dependency maintainers must triage that separate follow-up before any broader security claim; this slice does not upgrade unrelated development tools or claim zero vulnerabilities across the complete graph. Headless/Runtime Separation results remain under observation, and the real-model GUI prerequisites above remain unavailable.

From 7e0dd20dd31c8b0f6616501eb81758783c687f8b Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 04:05:10 -0700
Subject: [PATCH 24/65] fix(scheduler): box runtime execution intent payload

---
 CHANGELOG.md                                  |  4 ++
 .../runtime_dispatch_candidate_provider.rs    |  6 +--
 crates/pantograph-scheduler/src/queue.rs      | 10 +++-
 .../pantograph-scheduler/tests/queue_state.rs | 46 ++++++++++++++++++-
 .../src/scheduler/store_task_results.rs       |  5 +-
 .../src/scheduler/store_tests.rs              |  2 +-
 .../src/scheduler/task_orchestrator.rs        |  4 +-
 .../runtime_branch_batch_execution.rs         |  4 +-
 .../src/workflow/session_execution_api.rs     |  6 +--
 .../src/workflow/task_execution_worker.rs     |  6 +--
 .../workflow/tests/task_state_read_model.rs   |  2 +-
 .../2026-10-03-runtime-intent-layout.md       | 15 ++++++
 12 files changed, 89 insertions(+), 21 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-intent-layout.md

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a7c1d64a..21f7f2295 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@ The format is based on Keep a Changelog.
   canonicalization records.
 
 ### Changed
+- Rust source API: `SchedulerTaskExecutionIntent::Runtime.task_intent` is now boxed.
+  Construct runtime variants with `SchedulerTaskExecutionIntent::runtime(intent)`;
+  direct struct-variant callers must pass `Box::new(intent)`. Serialized JSON and
+  borrowed `runtime_task_intent()` access remain unchanged.
 - Root project `README.md` reorganized around install, usage, development, and contribution workflows.
 - Documentation consolidated around current guides, accepted decisions, audits,
   and active plans; superseded narration remains available in Git history.
diff --git a/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs b/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs
index 53e74907e..a00456cce 100644
--- a/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs
+++ b/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs
@@ -1636,9 +1636,9 @@ mod tests {
             node_id: intent.node_id,
             task_id: intent.task_id,
             state: SchedulerTaskState::Ready {
-                execution_intent: pantograph_scheduler::SchedulerTaskExecutionIntent::Runtime {
-                    task_intent: schedulable_intent(Some("cuda:0")),
-                },
+                execution_intent: pantograph_scheduler::SchedulerTaskExecutionIntent::runtime(
+                    schedulable_intent(Some("cuda:0")),
+                ),
             },
             state_version: 1,
             last_transition_id: SchedulerTaskStateTransitionId::parse("transition.ready")
diff --git a/crates/pantograph-scheduler/src/queue.rs b/crates/pantograph-scheduler/src/queue.rs
index 53f5c3a7c..2cc890f44 100644
--- a/crates/pantograph-scheduler/src/queue.rs
+++ b/crates/pantograph-scheduler/src/queue.rs
@@ -274,7 +274,7 @@ impl SchedulerNonRuntimeTaskIntent {
 #[non_exhaustive]
 pub enum SchedulerTaskExecutionIntent {
     Runtime {
-        task_intent: SchedulableTaskIntent,
+        task_intent: Box,
     },
     SourceInput {
         task_intent: SchedulerSourceInputTaskIntent,
@@ -285,6 +285,14 @@ pub enum SchedulerTaskExecutionIntent {
 }
 
 impl SchedulerTaskExecutionIntent {
+    /// Wraps a runtime intent without changing its validation or wire representation.
+    #[must_use]
+    pub fn runtime(task_intent: SchedulableTaskIntent) -> Self {
+        Self::Runtime {
+            task_intent: Box::new(task_intent),
+        }
+    }
+
     #[must_use]
     pub fn runtime_task_intent(&self) -> Option<&SchedulableTaskIntent> {
         match self {
diff --git a/crates/pantograph-scheduler/tests/queue_state.rs b/crates/pantograph-scheduler/tests/queue_state.rs
index dbe2904b3..766d2814e 100644
--- a/crates/pantograph-scheduler/tests/queue_state.rs
+++ b/crates/pantograph-scheduler/tests/queue_state.rs
@@ -12,6 +12,50 @@ use pantograph_scheduler::{
     SCHEDULABLE_TASK_INTENT_CONTRACT_VERSION, SCHEDULER_TASK_STATE_CONTRACT_VERSION,
 };
 
+#[test]
+fn boxed_runtime_intent_preserves_exact_json_and_borrowed_payload() {
+    let raw = task_intent("run.001", "task.001");
+    let execution = SchedulerTaskExecutionIntent::runtime(raw.clone());
+    let expected = serde_json::json!({ "execution_kind": "runtime", "task_intent": raw });
+    assert_eq!(
+        serde_json::to_value(&execution).expect("serialize runtime intent"),
+        expected
+    );
+    let decoded: SchedulerTaskExecutionIntent =
+        serde_json::from_value(expected).expect("decode existing wire shape");
+    assert_eq!(decoded, execution);
+    assert_eq!(decoded.runtime_task_intent(), Some(&raw));
+    assert!(
+        std::mem::size_of::()
+            < std::mem::size_of::()
+    );
+}
+
+#[test]
+fn boxed_runtime_intent_retains_validation_and_task_correlation() {
+    let valid = task_intent("run.001", "task.001");
+    ValidatedSchedulerTaskStateRecord::try_from(task_record_with_state(ready_state(valid.clone())))
+        .expect("valid runtime intent remains accepted");
+    let mut invalid = valid;
+    invalid.contract_version = 0;
+    let expected = invalid.validate().expect_err("invalid raw version");
+    assert_eq!(
+        task_record_with_state(ready_state(invalid))
+            .validate()
+            .expect_err("boxed invalid version"),
+        expected
+    );
+    assert_eq!(
+        task_record_with_state(ready_state(task_intent("run.other", "task.001")))
+            .validate()
+            .expect_err("mismatched run"),
+        SchedulerContractError::InvalidField {
+            field: "workflow_run_id",
+            reason: "task state workflow run id must match task intent",
+        },
+    );
+}
+
 #[test]
 fn valid_task_state_transition_fixture_decodes_validates_and_applies() {
     let transition: SchedulerTaskStateTransition =
@@ -643,7 +687,7 @@ fn completed_source_input_state(task_kind: &str) -> SchedulerTaskState {
 }
 
 fn runtime_execution_intent(task_intent: SchedulableTaskIntent) -> SchedulerTaskExecutionIntent {
-    SchedulerTaskExecutionIntent::Runtime { task_intent }
+    SchedulerTaskExecutionIntent::runtime(task_intent)
 }
 
 fn source_input_execution_intent(task_kind: &str) -> SchedulerTaskExecutionIntent {
diff --git a/crates/pantograph-workflow-service/src/scheduler/store_task_results.rs b/crates/pantograph-workflow-service/src/scheduler/store_task_results.rs
index 8bfc9a3b6..e48f8a169 100644
--- a/crates/pantograph-workflow-service/src/scheduler/store_task_results.rs
+++ b/crates/pantograph-workflow-service/src/scheduler/store_task_results.rs
@@ -1176,9 +1176,8 @@ mod tests {
         workflow_run_id: &str,
         task_id: &str,
     ) -> SchedulerTaskState {
-        let execution_intent = SchedulerTaskExecutionIntent::Runtime {
-            task_intent: task_intent(workflow_run_id, task_id),
-        };
+        let execution_intent =
+            SchedulerTaskExecutionIntent::runtime(task_intent(workflow_run_id, task_id));
         match state {
             SchedulerTaskStateKind::AwaitingInputs => SchedulerTaskState::AwaitingInputs {
                 diagnostics: Vec::new(),
diff --git a/crates/pantograph-workflow-service/src/scheduler/store_tests.rs b/crates/pantograph-workflow-service/src/scheduler/store_tests.rs
index e9d8ef635..3bcdccfcb 100644
--- a/crates/pantograph-workflow-service/src/scheduler/store_tests.rs
+++ b/crates/pantograph-workflow-service/src/scheduler/store_tests.rs
@@ -176,7 +176,7 @@ fn scheduler_state(
 }
 
 fn runtime_execution_intent(task_intent: SchedulableTaskIntent) -> SchedulerTaskExecutionIntent {
-    SchedulerTaskExecutionIntent::Runtime { task_intent }
+    SchedulerTaskExecutionIntent::runtime(task_intent)
 }
 
 fn scheduler_state_diagnostics() -> Vec {
diff --git a/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs b/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
index 3a4aa2386..2f6217dc2 100644
--- a/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
+++ b/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
@@ -2718,7 +2718,7 @@ fn initial_task_state(
             }
             if let Some(task_intent) = task.schedulable_intent.clone() {
                 Ok(SchedulerTaskState::WaitingDependencyReadiness {
-                    execution_intent: SchedulerTaskExecutionIntent::Runtime { task_intent },
+                    execution_intent: SchedulerTaskExecutionIntent::runtime(task_intent),
                 })
             } else {
                 Ok(awaiting_inputs_state())
@@ -3601,7 +3601,7 @@ fn runtime_execution_intent(
             )),
         ));
     };
-    Ok(SchedulerTaskExecutionIntent::Runtime { task_intent })
+    Ok(SchedulerTaskExecutionIntent::runtime(task_intent))
 }
 
 enum MaterializedBindingValue<'a> {
diff --git a/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs b/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs
index d31c6580a..bb2cf341f 100644
--- a/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs
+++ b/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs
@@ -3224,9 +3224,7 @@ mod tests {
     }
 
     fn runtime_execution_intent(workflow_run_id: &str) -> SchedulerTaskExecutionIntent {
-        SchedulerTaskExecutionIntent::Runtime {
-            task_intent: task_intent_for_run(workflow_run_id),
-        }
+        SchedulerTaskExecutionIntent::runtime(task_intent_for_run(workflow_run_id))
     }
 
     fn prompt_task_result(workflow_run_id: &str, prompt_text: &str) -> WorkflowSchedulerTaskResult {
diff --git a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
index b282e59c3..e8bd263f2 100644
--- a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
+++ b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
@@ -2643,8 +2643,8 @@ mod tests {
                 node_id: task.node_id.clone(),
                 task_id: task.task_id.clone(),
                 state: SchedulerTaskState::Completed {
-                    execution_intent: SchedulerTaskExecutionIntent::Runtime {
-                        task_intent: pantograph_scheduler::SchedulableTaskIntent {
+                    execution_intent: SchedulerTaskExecutionIntent::runtime(
+                        pantograph_scheduler::SchedulableTaskIntent {
                             contract_version:
                                 pantograph_scheduler::SCHEDULABLE_TASK_INTENT_CONTRACT_VERSION,
                             workflow_id: task.workflow_id.clone(),
@@ -2668,7 +2668,7 @@ mod tests {
                             dependency_override_patches: Vec::new(),
                             estimate_hints: Vec::new(),
                         },
-                    },
+                    ),
                 },
                 state_version: 1,
                 last_transition_id: "transition.completed".parse().expect("transition id"),
diff --git a/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs b/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs
index 6c71c09e9..5a15d2c97 100644
--- a/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs
+++ b/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs
@@ -3992,9 +3992,9 @@ mod tests {
             node_id: SchedulerNodeId::parse(WORKER_BATCH_NODE_ID).expect("node id"),
             task_id: SchedulerTaskId::parse(WORKER_BATCH_TASK_ID).expect("task id"),
             state: SchedulerTaskState::Ready {
-                execution_intent: SchedulerTaskExecutionIntent::Runtime {
-                    task_intent: task_intent_for_run(workflow_run_id),
-                },
+                execution_intent: SchedulerTaskExecutionIntent::runtime(task_intent_for_run(
+                    workflow_run_id,
+                )),
             },
             state_version: 1,
             last_transition_id: SchedulerTaskStateTransitionId::parse(format!(
diff --git a/crates/pantograph-workflow-service/src/workflow/tests/task_state_read_model.rs b/crates/pantograph-workflow-service/src/workflow/tests/task_state_read_model.rs
index e95be212d..f82af0582 100644
--- a/crates/pantograph-workflow-service/src/workflow/tests/task_state_read_model.rs
+++ b/crates/pantograph-workflow-service/src/workflow/tests/task_state_read_model.rs
@@ -85,7 +85,7 @@ fn state_with_intent(
 }
 
 fn runtime_execution_intent(task_intent: SchedulableTaskIntent) -> SchedulerTaskExecutionIntent {
-    SchedulerTaskExecutionIntent::Runtime { task_intent }
+    SchedulerTaskExecutionIntent::runtime(task_intent)
 }
 
 fn scheduler_task_graph(workflow_run_id: &str) -> WorkflowSchedulerTaskGraph {
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-intent-layout.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-intent-layout.md
new file mode 100644
index 000000000..4f148cb66
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-intent-layout.md
@@ -0,0 +1,15 @@
+# Boxed runtime intent payload
+
+## Accepted source-API decision
+
+Box only the large Runtime.task_intent field in SchedulerTaskExecutionIntent and add runtime(intent) as the preferred constructor. Migrate all ten repository construction sites; the remaining explicit Runtime match only inspects the variant. Borrowed runtime_task_intent access, validation/correlation, and tagged Serde representation remain unchanged. Source consumers directly constructing the public struct variant must now supply Box::new(intent); the changelog records this source-breaking change. Unseen Git/path consumers are not claimed compatible.
+
+The scheduler crate is publish=false and inherits workspace development version 0.1.0. docs/release.md says no accepted release workflow currently exists; it distinguishes development packaging from release acceptance. This milestone therefore does not invent a crate release/version bump or publish a release artifact.
+
+## Verification plan and evidence
+
+New regressions compare exact tagged JSON, deserialize the old wire shape, verify borrowed payload equality, check reduced inline layout, and retain raw validation errors plus exact task/run correlation failure. Existing scheduler suites and workflow-service/embedded-runtime callers remain in hosted qualification.
+
+Rust formatting and whitespace checks pass locally. No local workspace compilation or executed new Rust test is claimed; source review and fresh hosted scheduler/Clippy/workflow tests are required. Existing aggregate gates are unchanged, and this is an internal layout/source-API repair rather than the future research scheduler redesign.
+
+Independent source review accepted tree e3f247a05873d68632044d1078b7490b76529fbf. Fresh hosted compilation/runtime qualification remains required. The layout assertion checks inline size only and is not evidence of measured execution-performance improvement.

From 5218c1ace1cde44efbfb9f6202b910ea8462c08c Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 04:16:46 -0700
Subject: [PATCH 25/65] fix(media): accept named conversion result input

---
 .github/workflows/quality-gates.yml           |   3 +
 CHANGELOG.md                                  |   3 +
 crates/pantograph-media-conversion/src/lib.rs | 207 ++++++++++++++----
 .../reports/2026-10-03-media-result-input.md  |  11 +
 4 files changed, 185 insertions(+), 39 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-media-result-input.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 6481af342..9c4851e4c 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -190,6 +190,9 @@ jobs:
       - name: Check scheduler standard-trait accessor lint
         run: cargo clippy -p pantograph-scheduler --all-targets -- -D clippy::should_implement_trait
 
+      - name: Run media conversion contract tests
+        run: cargo test -p pantograph-media-conversion
+
       - name: Run node-engine tests
         run: cargo test -p node-engine --lib
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21f7f2295..e293441d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,9 @@ The format is based on Keep a Changelog.
   canonicalization records.
 
 ### Changed
+- Rust source API: `MediaConversionResult::try_new` now accepts one
+  `MediaConversionResultInput` with the same eight named fields instead of eight
+  positional arguments. Result fields and validation behavior are unchanged.
 - Rust source API: `SchedulerTaskExecutionIntent::Runtime.task_intent` is now boxed.
   Construct runtime variants with `SchedulerTaskExecutionIntent::runtime(intent)`;
   direct struct-variant callers must pass `Box::new(intent)`. Serialized JSON and
diff --git a/crates/pantograph-media-conversion/src/lib.rs b/crates/pantograph-media-conversion/src/lib.rs
index 70449e236..8ca4089d2 100644
--- a/crates/pantograph-media-conversion/src/lib.rs
+++ b/crates/pantograph-media-conversion/src/lib.rs
@@ -1299,16 +1299,16 @@ where
         }
 
         let command_id = request.target.format_id.as_str().to_string();
-        MediaConversionResult::try_new(
-            request.conversion_id,
-            MediaConversionStatus::Converted,
-            self.converter.target_media_type.clone(),
-            request.target,
+        MediaConversionResult::try_new(MediaConversionResultInput {
+            conversion_id: request.conversion_id,
+            status: MediaConversionStatus::Converted,
+            media_type: self.converter.target_media_type.clone(),
+            target: request.target,
             command_id,
-            output.stdout,
-            vec![self.converter.dependency.clone()],
-            output.stderr_summary,
-        )
+            body: output.stdout,
+            dependencies: vec![self.converter.dependency.clone()],
+            stderr_summary: output.stderr_summary,
+        })
     }
 }
 
@@ -1324,17 +1324,33 @@ pub struct MediaConversionResult {
     pub stderr_summary: Option,
 }
 
+/// Named, unvalidated input to the result constructor.
+///
+/// This preserves the result field representation while making construction explicit.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct MediaConversionResultInput {
+    pub conversion_id: MediaConversionId,
+    pub status: MediaConversionStatus,
+    pub media_type: MediaType,
+    pub target: MediaConversionTarget,
+    pub command_id: String,
+    pub body: Vec,
+    pub dependencies: Vec,
+    pub stderr_summary: Option,
+}
+
 impl MediaConversionResult {
-    pub fn try_new(
-        conversion_id: MediaConversionId,
-        status: MediaConversionStatus,
-        media_type: MediaType,
-        target: MediaConversionTarget,
-        command_id: String,
-        body: Vec,
-        dependencies: Vec,
-        stderr_summary: Option,
-    ) -> Result {
+    pub fn try_new(input: MediaConversionResultInput) -> Result {
+        let MediaConversionResultInput {
+            conversion_id,
+            status,
+            media_type,
+            target,
+            command_id,
+            body,
+            dependencies,
+            stderr_summary,
+        } = input;
         if body.is_empty() {
             return Err(MediaConversionError::MissingField { field: "body" });
         }
@@ -2027,22 +2043,135 @@ mod tests {
                 .to_string(),
         };
 
-        let result = MediaConversionResult::try_new(
-            id("conversion-a"),
-            MediaConversionStatus::Converted,
-            "image/jpeg".parse().expect("media type"),
-            target(),
-            "oiiotool_jpg".to_string(),
-            vec![1, 2, 3],
-            vec![dependency.clone()],
-            Some("bounded stderr".to_string()),
-        )
+        let result = MediaConversionResult::try_new(MediaConversionResultInput {
+            conversion_id: id("conversion-a"),
+            status: MediaConversionStatus::Converted,
+            media_type: "image/jpeg".parse().expect("media type"),
+            target: target(),
+            command_id: "oiiotool_jpg".to_string(),
+            body: vec![1, 2, 3],
+            dependencies: vec![dependency.clone()],
+            stderr_summary: Some("bounded stderr".to_string()),
+        })
         .expect("result");
 
         assert_eq!(result.dependencies, vec![dependency]);
         assert_eq!(result.status, MediaConversionStatus::Converted);
     }
 
+    fn valid_result_input() -> MediaConversionResultInput {
+        MediaConversionResultInput {
+            conversion_id: id("conversion-a"),
+            status: MediaConversionStatus::Converted,
+            media_type: "image/jpeg".parse().expect("media type"),
+            target: target(),
+            command_id: "oiiotool_jpg".to_string(),
+            body: vec![1, 2, 3],
+            dependencies: vec![MediaConversionDependencyAttribution {
+                dependency_id: ManagedMediaDependencyId::Oiiotool,
+                version: id("2.5.18"),
+                lease_id: id("lease-1"),
+                lease_holder: "lease holder".to_string(),
+            }],
+            stderr_summary: Some("bounded stderr".to_string()),
+        }
+    }
+
+    #[test]
+    fn named_result_input_preserves_all_fields_and_existing_normalization() {
+        let mut input = valid_result_input();
+        input.command_id = "  oiiotool_jpg  ".to_string();
+        input.stderr_summary = Some("  bounded stderr  ".to_string());
+        input.dependencies[0].lease_holder = "  lease holder  ".to_string();
+        let result = MediaConversionResult::try_new(input.clone()).expect("valid named input");
+        assert_eq!(
+            result,
+            MediaConversionResult {
+                conversion_id: input.conversion_id,
+                status: input.status,
+                media_type: input.media_type,
+                target: input.target,
+                command_id: "oiiotool_jpg".to_string(),
+                body: input.body,
+                dependencies: input.dependencies,
+                stderr_summary: input.stderr_summary,
+            }
+        );
+    }
+
+    #[test]
+    fn result_fields_round_trip_through_named_input() {
+        let result = MediaConversionResult::try_new(valid_result_input()).expect("result");
+        let round_trip = MediaConversionResult::try_new(MediaConversionResultInput {
+            conversion_id: result.conversion_id.clone(),
+            status: result.status,
+            media_type: result.media_type.clone(),
+            target: result.target.clone(),
+            command_id: result.command_id.clone(),
+            body: result.body.clone(),
+            dependencies: result.dependencies.clone(),
+            stderr_summary: result.stderr_summary.clone(),
+        })
+        .expect("result round trip");
+        assert_eq!(round_trip, result);
+    }
+
+    #[test]
+    fn named_result_input_preserves_validation_order_and_errors() {
+        let mut input = valid_result_input();
+        input.body.clear();
+        input.command_id = "bad command".to_string();
+        input.stderr_summary = Some("bad\nsummary".to_string());
+        input.dependencies[0].lease_holder = "bad\nlease".to_string();
+        assert_eq!(
+            MediaConversionResult::try_new(input.clone()).expect_err("body first"),
+            MediaConversionError::MissingField { field: "body" }
+        );
+        input.body.push(1);
+        assert_eq!(
+            MediaConversionResult::try_new(input.clone()).expect_err("command second"),
+            MediaConversionError::InvalidIdentifier {
+                field: "command_id"
+            }
+        );
+        input.command_id = "valid".to_string();
+        assert_eq!(
+            MediaConversionResult::try_new(input.clone()).expect_err("stderr third"),
+            MediaConversionError::InvalidText {
+                field: "stderr_summary"
+            }
+        );
+        input.stderr_summary = None;
+        assert_eq!(
+            MediaConversionResult::try_new(input).expect_err("lease last"),
+            MediaConversionError::InvalidText {
+                field: "dependency_lease_holder"
+            }
+        );
+    }
+
+    #[test]
+    fn named_result_input_preserves_text_bounds() {
+        let mut input = valid_result_input();
+        input.stderr_summary = Some("x".repeat(MAX_ERROR_SUMMARY_LEN + 1));
+        assert_eq!(
+            MediaConversionResult::try_new(input.clone()).expect_err("stderr bound"),
+            MediaConversionError::FieldTooLong {
+                field: "stderr_summary",
+                max_len: MAX_ERROR_SUMMARY_LEN
+            }
+        );
+        input.stderr_summary = None;
+        input.dependencies[0].lease_holder = "x".repeat(MAX_LEASE_HOLDER_LEN + 1);
+        assert_eq!(
+            MediaConversionResult::try_new(input).expect_err("lease bound"),
+            MediaConversionError::FieldTooLong {
+                field: "dependency_lease_holder",
+                max_len: MAX_LEASE_HOLDER_LEN
+            }
+        );
+    }
+
     #[test]
     fn source_and_result_reject_empty_bodies() {
         let source_error = MediaConversionSource::try_new(
@@ -2056,16 +2185,16 @@ mod tests {
             MediaConversionError::MissingField { field: "body" }
         ));
 
-        let result_error = MediaConversionResult::try_new(
-            id("conversion-a"),
-            MediaConversionStatus::Converted,
-            "image/jpeg".parse().expect("media type"),
-            target(),
-            "oiiotool_jpg".to_string(),
-            Vec::new(),
-            Vec::new(),
-            None,
-        )
+        let result_error = MediaConversionResult::try_new(MediaConversionResultInput {
+            conversion_id: id("conversion-a"),
+            status: MediaConversionStatus::Converted,
+            media_type: "image/jpeg".parse().expect("media type"),
+            target: target(),
+            command_id: "oiiotool_jpg".to_string(),
+            body: Vec::new(),
+            dependencies: Vec::new(),
+            stderr_summary: None,
+        })
         .expect_err("empty result body");
         assert!(matches!(
             result_error,
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-media-result-input.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-media-result-input.md
new file mode 100644
index 000000000..858775f23
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-media-result-input.md
@@ -0,0 +1,11 @@
+# Named media conversion result input
+
+The next aggregate warning-deny Clippy failure after the scheduler layout repair was the eight-argument MediaConversionResult::try_new constructor. Replace its positional arguments with MediaConversionResultInput carrying exactly the existing eight fields. The constructor destructures that input and retains the existing validation statements, ordering, normalization, errors, and result representation. Migrate all three repository callers: the managed executor and two existing tests.
+
+This is an explicit public Rust construction change despite the crate being publish=false at workspace development version 0.1.0. The changelog records the new call shape; unseen external Git/path consumers are not assumed compatible. No serialized DTO, conversion routing, process execution, or dependency policy changes are included.
+
+Four regressions cover all successful fields and existing command-only normalization, result-field round-trip, ordered failures for body/command/stderr/dependency holder, and text bounds. Existing executor success, failure, timeout and cancellation tests remain. The focused hosted Rust job now runs the media conversion crate tests explicitly.
+
+Formatting and whitespace checks pass locally. No local workspace compilation or executed new Rust tests are claimed. Independent source review and fresh hosted media/scheduler/aggregate Clippy qualification are pending. Existing gates remain intact, with no lint suppression.
+
+Independent source review accepted final tree 443829480fb96b4164eb068adfa51ee9e8d14d92. This confirms the bounded source/API migration and regression coverage; actual Rust execution remains hosted qualification.

From cac290b6b5087c494a41a1945a591aacf55c7f65 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 04:29:59 -0700
Subject: [PATCH 26/65] fix(inference): preserve behavior while clearing
 annotation and style lints

---
 .github/workflows/quality-gates.yml           |  6 ++
 crates/inference/src/backend/compatibility.rs | 49 +++++++++------
 crates/inference/src/execution_telemetry.rs   |  1 -
 crates/inference/src/gateway.rs               |  4 +-
 .../inference/src/image_generation_planner.rs |  1 -
 crates/inference/src/model_contracts.rs       | 19 +++---
 crates/inference/src/server.rs                |  6 +-
 crates/inference/src/server_tests.rs          | 62 +++++++++++++++++++
 crates/inference/src/types.rs                 | 26 --------
 .../2026-10-03-inference-lint-basics.md       |  9 +++
 10 files changed, 123 insertions(+), 60 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-lint-basics.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 9c4851e4c..4aef59bdb 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -193,6 +193,12 @@ jobs:
       - name: Run media conversion contract tests
         run: cargo test -p pantograph-media-conversion
 
+      - name: Run inference compatibility and contract tests
+        run: |
+          cargo test -p inference --lib backend::compatibility::tests
+          cargo test -p inference --lib model_contracts::tests
+          cargo test -p inference --lib server::tests::runtime_matchers_preserve_path_component_comparison
+
       - name: Run node-engine tests
         run: cargo test -p node-engine --lib
 
diff --git a/crates/inference/src/backend/compatibility.rs b/crates/inference/src/backend/compatibility.rs
index da7be876d..f879a94d8 100644
--- a/crates/inference/src/backend/compatibility.rs
+++ b/crates/inference/src/backend/compatibility.rs
@@ -181,21 +181,25 @@ impl BackendCapabilities {
             &mut issues,
         );
         let option_diagnostics = self.option_compatibility_diagnostics(backend_key, &request);
-        issues.extend(option_diagnostics.iter().filter_map(|diagnostic| {
-            matches!(
-                &diagnostic.state,
-                OptionSupportState::Unsupported | OptionSupportState::Rejected
-            )
-            .then(|| BackendCompatibilityIssue {
-                kind: BackendCompatibilityIssueKind::UnsupportedOption,
-                phase: InferenceLifecyclePhase::TaskValidation,
-                message: diagnostic.message.clone().unwrap_or_else(|| {
-                    format!("option {} is not supported", diagnostic.option_path)
+        issues.extend(
+            option_diagnostics
+                .iter()
+                .filter(|diagnostic| {
+                    matches!(
+                        &diagnostic.state,
+                        OptionSupportState::Unsupported | OptionSupportState::Rejected
+                    )
+                })
+                .map(|diagnostic| BackendCompatibilityIssue {
+                    kind: BackendCompatibilityIssueKind::UnsupportedOption,
+                    phase: InferenceLifecyclePhase::TaskValidation,
+                    message: diagnostic.message.clone().unwrap_or_else(|| {
+                        format!("option {} is not supported", diagnostic.option_path)
+                    }),
+                    model_id: Some(request.package_facts.model_ref.model_id.clone()),
+                    path: None,
                 }),
-                model_id: Some(request.package_facts.model_ref.model_id.clone()),
-                path: None,
-            })
-        }));
+        );
         let compatible = [task, model_source, preprocessing, postprocessing]
             .into_iter()
             .all(|status| status == BackendCompatibilityStatus::Supported)
@@ -452,10 +456,7 @@ fn compatibility_report_status_label(report: &BackendCompatibilityReport) -> &'s
         report.preprocessing,
         report.postprocessing,
     ];
-    if statuses
-        .iter()
-        .any(|status| *status == BackendCompatibilityStatus::Unsupported)
-    {
+    if statuses.contains(&BackendCompatibilityStatus::Unsupported) {
         "rejected"
     } else {
         "degraded"
@@ -909,6 +910,7 @@ mod tests {
             Some("llama_cpp"),
             BackendCompatibilityRequest::new(&task, &package).with_options(
                 BackendCompatibilityOptions {
+                    streaming: true,
                     cache: CacheGenerationOptions {
                         use_cache: Some(true),
                         kv_cache_checkpoint_requested: Some(true),
@@ -919,6 +921,17 @@ mod tests {
         );
 
         assert!(!report.compatible);
+        assert_eq!(
+            report
+                .issues
+                .iter()
+                .map(|issue| issue.message.as_str())
+                .collect::>(),
+            vec![
+                "option cache.use_cache is not supported by this backend",
+                "option cache.kv_cache_checkpoint_requested is not supported by this backend",
+            ],
+        );
         assert!(report.option_diagnostics.iter().any(|diagnostic| {
             diagnostic.option_path == "cache.use_cache"
                 && matches!(&diagnostic.state, OptionSupportState::Unsupported)
diff --git a/crates/inference/src/execution_telemetry.rs b/crates/inference/src/execution_telemetry.rs
index d0691c038..0523b0741 100644
--- a/crates/inference/src/execution_telemetry.rs
+++ b/crates/inference/src/execution_telemetry.rs
@@ -24,7 +24,6 @@ pub struct InferenceExecutionTelemetryScope {
 }
 
 impl InferenceExecutionTelemetryScope {
-    #[must_use]
     pub fn new() -> Self {
         Self::default()
     }
diff --git a/crates/inference/src/gateway.rs b/crates/inference/src/gateway.rs
index c8bc03509..fea9f01a4 100644
--- a/crates/inference/src/gateway.rs
+++ b/crates/inference/src/gateway.rs
@@ -3497,9 +3497,7 @@ fn start_runtime_resource_monitor_for_process(
 fn finish_runtime_resource_monitor(
     guard: Option,
 ) -> Option {
-    let Some(guard) = guard else {
-        return None;
-    };
+    let guard = guard?;
     match guard.finish() {
         Ok(observation) => Some(observation),
         Err(error) => {
diff --git a/crates/inference/src/image_generation_planner.rs b/crates/inference/src/image_generation_planner.rs
index 773afb52f..7008fe941 100644
--- a/crates/inference/src/image_generation_planner.rs
+++ b/crates/inference/src/image_generation_planner.rs
@@ -101,7 +101,6 @@ impl PlannedImageGenerationLaunchHandoff {
         &self.artifact_load_target
     }
 
-    #[must_use]
     pub fn backend_decision(&self) -> &BackendExecutionDecision {
         &self.backend_decision
     }
diff --git a/crates/inference/src/model_contracts.rs b/crates/inference/src/model_contracts.rs
index 795056401..c88a1e4cb 100644
--- a/crates/inference/src/model_contracts.rs
+++ b/crates/inference/src/model_contracts.rs
@@ -343,22 +343,17 @@ impl TaskModalitySignature {
 }
 
 /// Support tier for a task or backend mapping.
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum SupportTier {
     Stable,
     Experimental,
     Roadmap,
     Unsupported,
+    #[default]
     Unknown,
 }
 
-impl Default for SupportTier {
-    fn default() -> Self {
-        Self::Unknown
-    }
-}
-
 /// Broad task family used for compatibility diagnostics without choosing a
 /// runtime or scheduler policy.
 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
@@ -2136,7 +2131,6 @@ impl ResolvedModelSource {
     ///
     /// This enforces model-source shape invariants without selecting a backend
     /// or deciding runtime placement.
-    #[must_use]
     pub fn validate_for_backend_load(&self) -> Result<(), Vec> {
         let mut diagnostics = Vec::new();
 
@@ -2423,6 +2417,15 @@ mod tests {
             .unwrap_or_else(|| panic!("missing request contract for {task_id:?}"))
     }
 
+    #[test]
+    fn support_tier_default_retains_unknown_wire_value() {
+        assert_eq!(SupportTier::default(), SupportTier::Unknown);
+        assert_eq!(
+            serde_json::to_value(SupportTier::default()).expect("default serializes"),
+            serde_json::json!("unknown")
+        );
+    }
+
     #[test]
     fn task_registry_entries_publish_typed_request_contracts() {
         let text = registry_contract(InferenceTaskId::TextGeneration);
diff --git a/crates/inference/src/server.rs b/crates/inference/src/server.rs
index 8925cf983..2de053845 100644
--- a/crates/inference/src/server.rs
+++ b/crates/inference/src/server.rs
@@ -648,7 +648,7 @@ impl LlamaServer {
             return false;
         };
         active.mode == LlamaCppRuntimeMode::Inference
-            && active.model_path == PathBuf::from(model_path)
+            && active.model_path.as_path() == Path::new(model_path)
             && active.mmproj_path == mmproj_path.map(PathBuf::from)
             && active.device == *device
             && active.context_size == Some(context_size)
@@ -669,7 +669,7 @@ impl LlamaServer {
             return false;
         };
         active.mode == LlamaCppRuntimeMode::Embedding
-            && active.model_path == PathBuf::from(model_path)
+            && active.model_path.as_path() == Path::new(model_path)
             && active.device == *device
             && active.port == expected_port
     }
@@ -685,7 +685,7 @@ impl LlamaServer {
             return false;
         };
         active.mode == LlamaCppRuntimeMode::Reranking
-            && active.model_path == PathBuf::from(model_path)
+            && active.model_path.as_path() == Path::new(model_path)
             && active.device == *device
             && active.port == expected_port
     }
diff --git a/crates/inference/src/server_tests.rs b/crates/inference/src/server_tests.rs
index eed60e3ce..08e13b685 100644
--- a/crates/inference/src/server_tests.rs
+++ b/crates/inference/src/server_tests.rs
@@ -424,3 +424,65 @@ fn assert_arg_pair(args: &[String], name: &str, value: &str) {
         "expected arg pair {name} {value} in {args:?}"
     );
 }
+
+#[test]
+fn runtime_matchers_preserve_path_component_comparison() {
+    let device = DeviceConfig {
+        device: DeviceBackend::Auto,
+        gpu_layers: -1,
+    };
+    let mut server = LlamaServer::new();
+    server.set_test_runtime_state(
+        ServerMode::SidecarInference {
+            port: 11434,
+            model_path: "models/main.gguf".to_string(),
+            mmproj_path: None,
+            device: device.clone(),
+            context_size: 4096,
+            cpu_threads: None,
+            batch_size: None,
+            ubatch_size: None,
+        },
+        true,
+    );
+    assert!(server.matches_inference_runtime(
+        "models/./main.gguf",
+        None,
+        &device,
+        4096,
+        None,
+        None,
+        None,
+        Some(11434)
+    ));
+    assert!(!server.matches_inference_runtime(
+        "models/other.gguf",
+        None,
+        &device,
+        4096,
+        None,
+        None,
+        None,
+        Some(11434)
+    ));
+    server.set_test_runtime_state(
+        ServerMode::SidecarEmbedding {
+            port: 11434,
+            model_path: "models/main.gguf".to_string(),
+            device: device.clone(),
+        },
+        true,
+    );
+    assert!(server.matches_embedding_runtime("models/./main.gguf", &device, Some(11434)));
+    assert!(!server.matches_embedding_runtime("models/other.gguf", &device, Some(11434)));
+    server.set_test_runtime_state(
+        ServerMode::SidecarReranking {
+            port: 11434,
+            model_path: "models/main.gguf".to_string(),
+            device: device.clone(),
+        },
+        true,
+    );
+    assert!(server.matches_reranking_runtime("models/./main.gguf", &device, Some(11434)));
+    assert!(!server.matches_reranking_runtime("models/other.gguf", &device, Some(11434)));
+}
diff --git a/crates/inference/src/types.rs b/crates/inference/src/types.rs
index c2451f46b..e91c5dd96 100644
--- a/crates/inference/src/types.rs
+++ b/crates/inference/src/types.rs
@@ -922,7 +922,6 @@ pub struct InferenceRequestLifecycleEvent {
 }
 
 impl InferenceRequestLifecycleEvent {
-    #[must_use]
     pub fn builder(
         phase: InferenceLifecyclePhase,
         kind: InferenceRequestLifecycleEventKind,
@@ -950,7 +949,6 @@ pub struct InferenceRequestLifecycleEventContext {
 }
 
 impl InferenceRequestLifecycleEventContext {
-    #[must_use]
     pub fn builder(
         &self,
         phase: InferenceLifecyclePhase,
@@ -980,7 +978,6 @@ pub struct InferenceRequestLifecycleEventBuilder {
 }
 
 impl InferenceRequestLifecycleEventBuilder {
-    #[must_use]
     pub fn new(
         phase: InferenceLifecyclePhase,
         kind: InferenceRequestLifecycleEventKind,
@@ -1015,49 +1012,41 @@ impl InferenceRequestLifecycleEventBuilder {
         }
     }
 
-    #[must_use]
     pub fn with_request_id(mut self, request_id: Option) -> Self {
         self.event.request_id = request_id;
         self
     }
 
-    #[must_use]
     pub fn with_phase(mut self, phase: InferenceLifecyclePhase) -> Self {
         self.event.phase = phase;
         self
     }
 
-    #[must_use]
     pub fn with_kind(mut self, kind: InferenceRequestLifecycleEventKind) -> Self {
         self.event.kind = kind;
         self
     }
 
-    #[must_use]
     pub fn with_occurred_at_ms(mut self, occurred_at_ms: u64) -> Self {
         self.event.occurred_at_ms = occurred_at_ms;
         self
     }
 
-    #[must_use]
     pub fn with_task_id(mut self, task_id: Option) -> Self {
         self.event.task_id = task_id;
         self
     }
 
-    #[must_use]
     pub fn with_backend_key(mut self, backend_key: Option) -> Self {
         self.event.backend_key = backend_key;
         self
     }
 
-    #[must_use]
     pub fn with_runtime_id(mut self, runtime_id: Option) -> Self {
         self.event.runtime_id = runtime_id;
         self
     }
 
-    #[must_use]
     pub fn with_selected_runtime_variant_id(
         mut self,
         selected_runtime_variant_id: Option,
@@ -1066,13 +1055,11 @@ impl InferenceRequestLifecycleEventBuilder {
         self
     }
 
-    #[must_use]
     pub fn with_runtime_instance_id(mut self, runtime_instance_id: Option) -> Self {
         self.event.runtime_instance_id = runtime_instance_id;
         self
     }
 
-    #[must_use]
     pub fn with_selected_device_class(
         mut self,
         selected_device_class: Option,
@@ -1081,7 +1068,6 @@ impl InferenceRequestLifecycleEventBuilder {
         self
     }
 
-    #[must_use]
     pub fn with_selected_device_id(
         mut self,
         selected_device_id: Option,
@@ -1090,7 +1076,6 @@ impl InferenceRequestLifecycleEventBuilder {
         self
     }
 
-    #[must_use]
     pub fn with_selected_network_node_id(
         mut self,
         selected_network_node_id: Option,
@@ -1099,37 +1084,31 @@ impl InferenceRequestLifecycleEventBuilder {
         self
     }
 
-    #[must_use]
     pub fn with_model_id(mut self, model_id: Option) -> Self {
         self.event.model_id = model_id;
         self
     }
 
-    #[must_use]
     pub fn with_resolved_artifact_kind(mut self, resolved_artifact_kind: Option) -> Self {
         self.event.resolved_artifact_kind = resolved_artifact_kind;
         self
     }
 
-    #[must_use]
     pub fn with_usage(mut self, usage: Option) -> Self {
         self.event.usage = usage;
         self
     }
 
-    #[must_use]
     pub fn with_cache_handle_id(mut self, cache_handle_id: Option) -> Self {
         self.event.cache_handle_id = cache_handle_id;
         self
     }
 
-    #[must_use]
     pub fn with_artifact_refs(mut self, artifact_refs: Vec) -> Self {
         self.event.artifact_refs = artifact_refs;
         self
     }
 
-    #[must_use]
     pub fn with_resource_observation(
         mut self,
         resource_observation: Option,
@@ -1138,13 +1117,11 @@ impl InferenceRequestLifecycleEventBuilder {
         self
     }
 
-    #[must_use]
     pub fn with_detail(mut self, detail: Option) -> Self {
         self.event.detail = detail;
         self
     }
 
-    #[must_use]
     pub fn with_canonical_error_event_id(
         mut self,
         canonical_error_event_id: Option,
@@ -1153,7 +1130,6 @@ impl InferenceRequestLifecycleEventBuilder {
         self
     }
 
-    #[must_use]
     pub fn with_compatibility_report(
         mut self,
         compatibility_report: Option,
@@ -1162,7 +1138,6 @@ impl InferenceRequestLifecycleEventBuilder {
         self
     }
 
-    #[must_use]
     pub fn with_compatibility_issues(
         mut self,
         compatibility_issues: Vec,
@@ -1171,7 +1146,6 @@ impl InferenceRequestLifecycleEventBuilder {
         self
     }
 
-    #[must_use]
     pub fn with_option_diagnostics(
         mut self,
         option_diagnostics: Vec,
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-lint-basics.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-lint-basics.md
new file mode 100644
index 000000000..bfb4c5b7f
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-lint-basics.md
@@ -0,0 +1,9 @@
+# Behavior-preserving inference lint corrections
+
+Fresh PR #24 Clippy job 111191777174 reached the inference crate and reported 54 findings. This bounded slice addresses 36 annotation/style findings: 29 redundant method must_use attributes whose return types already carry that obligation; equivalent ordered filter/map and contains checks; Option early return via ?; derived SupportTier default with Unknown retained; and three borrowed Path comparisons instead of temporary PathBuf allocations. Public signatures, type-level must_use, conversion/error policy, and runtime identity semantics remain unchanged.
+
+The option regression now checks exact ordered unsupported diagnostics while a supported streaming option emits no issue. New tests retain Unknown's default serialized value and Path-component equality across inference/embedding/reranking matchers, including mismatched model rejection. Hosted checks explicitly run compatibility/model-contract suites and the matcher regression. Existing lifecycle builder/telemetry behavior is unchanged by removing redundant annotations.
+
+Formatting and whitespace pass locally. No local inference compilation or executed new Rust tests are claimed. Independent review and fresh hosted qualification remain pending. Nine high-arity functions, the public image-planning enum, and eight large-error returns are intentionally separate structural/API proposals, not suppressed or silently modified.
+
+Independent source review accepted tree cfc7168c7afd6904d00ea9818e67b8f3f17424cc, including diagnostic order/content, Unknown default, borrowed Path equivalence and retained type/Result must-use obligations. Fresh hosted tests and Clippy remain required.

From 58df81689306debaced0e224e16576742376381d Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 04:37:00 -0700
Subject: [PATCH 27/65] fix(inference): box managed diagnostic and planned
 image payloads

---
 .github/workflows/quality-gates.yml           |  6 +++++
 CHANGELOG.md                                  |  4 +++
 .../pytorch_worker_image_contract_tests.rs    |  2 +-
 crates/inference/src/gateway.rs               |  4 +--
 .../inference/src/image_generation_planner.rs |  6 ++---
 .../src/image_generation_planner_tests.rs     | 12 +++++++++
 .../src/managed_runtime/contracts.rs          | 27 ++++++++++++++++---
 .../src/runtime_host_execution_port.rs        |  2 +-
 .../2026-10-03-inference-payload-layout.md    | 13 +++++++++
 9 files changed, 66 insertions(+), 10 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-payload-layout.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 4aef59bdb..eceec701c 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -199,6 +199,12 @@ jobs:
           cargo test -p inference --lib model_contracts::tests
           cargo test -p inference --lib server::tests::runtime_matchers_preserve_path_component_comparison
 
+      - name: Run inference payload serialization and planner tests
+        run: |
+          cargo test -p inference --lib managed_runtime::contracts::tests
+          cargo test -p inference --lib image_generation_planner_tests
+          cargo test -p inference --lib pytorch_worker_image_contract_tests
+
       - name: Run node-engine tests
         run: cargo test -p node-engine --lib
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e293441d3..fa776e383 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@ The format is based on Keep a Changelog.
   canonicalization records.
 
 ### Changed
+- Rust source API: `ManagedRuntimeCommandResolutionError::MissingRuntimeVariant.diagnostic`
+  and `ImageGenerationPlanningOutcome::Planned.plan` now hold boxed payloads.
+  Direct constructors must box their values and consuming plan callers must unbox;
+  tagged JSON, diagnostic fields, and error Display text are unchanged.
 - Rust source API: `MediaConversionResult::try_new` now accepts one
   `MediaConversionResultInput` with the same eight named fields instead of eight
   positional arguments. Result fields and validation behavior are unchanged.
diff --git a/crates/inference/src/backend/pytorch_worker_image_contract_tests.rs b/crates/inference/src/backend/pytorch_worker_image_contract_tests.rs
index e46a74c4a..410d80dc2 100644
--- a/crates/inference/src/backend/pytorch_worker_image_contract_tests.rs
+++ b/crates/inference/src/backend/pytorch_worker_image_contract_tests.rs
@@ -262,7 +262,7 @@ fn test_pytorch_worker_generate_image_request_maps_from_validated_plan() {
         panic!("expected validated image plan");
     };
 
-    let worker_request = PyTorchGenerateImageRequest::from(&plan);
+    let worker_request = PyTorchGenerateImageRequest::from(plan.as_ref());
 
     assert_eq!(
         worker_request.model_ref.model_id,
diff --git a/crates/inference/src/gateway.rs b/crates/inference/src/gateway.rs
index fea9f01a4..79333d5d3 100644
--- a/crates/inference/src/gateway.rs
+++ b/crates/inference/src/gateway.rs
@@ -1613,7 +1613,7 @@ impl InferenceGateway {
         reject_cancelled_execution_handle("image generation planning", &cancellation)?;
         match plan_image_generation_execution(input) {
             ImageGenerationPlanningOutcome::Planned { plan } => {
-                self.generate_image_from_plan_with_cancellation(plan, cancellation)
+                self.generate_image_from_plan_with_cancellation(*plan, cancellation)
                     .await
             }
             ImageGenerationPlanningOutcome::Rejected { diagnostics } => {
@@ -1699,7 +1699,7 @@ impl InferenceGateway {
                 let execution_telemetry = self.start_execution_telemetry().await;
                 let context = execution_telemetry.backend_execution_context();
                 let result = self
-                    .generate_image_from_plan_with_context(plan, context)
+                    .generate_image_from_plan_with_context(*plan, context)
                     .await;
                 let resource_observation = finish_execution_telemetry(execution_telemetry);
                 record_planned_image_generation_lifecycle_result(
diff --git a/crates/inference/src/image_generation_planner.rs b/crates/inference/src/image_generation_planner.rs
index 7008fe941..0f9461cf0 100644
--- a/crates/inference/src/image_generation_planner.rs
+++ b/crates/inference/src/image_generation_planner.rs
@@ -142,7 +142,7 @@ pub enum ImageGenerationPlanningOutcome {
     /// The request has one canonical PyTorch/Diffusers execution plan.
     Planned {
         /// Validated execution plan.
-        plan: ImageGenerationExecutionPlan,
+        plan: Box,
     },
     /// Planning failed closed with typed diagnostics.
     Rejected {
@@ -493,7 +493,7 @@ pub fn plan_image_generation_execution(
         )]);
     };
     ImageGenerationPlanningOutcome::Planned {
-        plan: ImageGenerationExecutionPlan {
+        plan: Box::new(ImageGenerationExecutionPlan {
             model_ref: input.package_facts.model_ref.clone(),
             artifact_entry_path,
             artifact_load_target: input.artifact_load_target.clone(),
@@ -515,7 +515,7 @@ pub fn plan_image_generation_execution(
             denoising_scheduler,
             num_images_per_prompt: input.request.num_images_per_prompt,
             resource_estimates,
-        },
+        }),
     }
 }
 
diff --git a/crates/inference/src/image_generation_planner_tests.rs b/crates/inference/src/image_generation_planner_tests.rs
index 7211a2b96..b754fc12c 100644
--- a/crates/inference/src/image_generation_planner_tests.rs
+++ b/crates/inference/src/image_generation_planner_tests.rs
@@ -126,9 +126,21 @@ fn planner_accepts_pumas_diffusers_stable_diffusion_facts() {
         backend_decision: &decision,
     });
 
+    let encoded = serde_json::to_value(&outcome).expect("serialize planned outcome");
+    let round_trip: ImageGenerationPlanningOutcome =
+        serde_json::from_value(encoded.clone()).expect("decode planned outcome");
+    assert_eq!(round_trip, outcome);
     let ImageGenerationPlanningOutcome::Planned { plan } = outcome else {
         panic!("expected valid image-generation plan");
     };
+    assert_eq!(
+        encoded,
+        serde_json::json!({ "status": "planned", "plan": plan.as_ref() })
+    );
+    assert!(
+        std::mem::size_of::()
+            < std::mem::size_of::()
+    );
 
     assert_eq!(plan.model_ref.model_id, "image/stable-diffusion/tiny-sd");
     assert_eq!(plan.backend_id.as_str(), "pytorch");
diff --git a/crates/inference/src/managed_runtime/contracts.rs b/crates/inference/src/managed_runtime/contracts.rs
index fac717e7b..5e13c0009 100644
--- a/crates/inference/src/managed_runtime/contracts.rs
+++ b/crates/inference/src/managed_runtime/contracts.rs
@@ -240,7 +240,7 @@ pub enum ManagedRuntimeCommandResolutionError {
         missing_file: String,
     },
     MissingRuntimeVariant {
-        diagnostic: DeviceResolutionDiagnostic,
+        diagnostic: Box,
         requested_device: Option,
         missing_path: PathBuf,
     },
@@ -293,7 +293,7 @@ impl ManagedRuntimeCommandResolutionError {
             _ => None,
         };
         Self::MissingRuntimeVariant {
-            diagnostic: DeviceResolutionDiagnostic {
+            diagnostic: Box::new(DeviceResolutionDiagnostic {
                 code: DeviceResolutionDiagnosticCode::MissingRuntimeVariant,
                 severity: DeviceResolutionDiagnosticSeverity::Error,
                 message: format!(
@@ -305,7 +305,7 @@ impl ManagedRuntimeCommandResolutionError {
                 device_id: None,
                 runtime_variant_id: Some(runtime_variant_id.clone()),
                 backend_id: Some(backend_id),
-            },
+            }),
             requested_device: None,
             missing_path,
         }
@@ -370,6 +370,27 @@ mod tests {
         );
 
         let encoded = serde_json::to_value(&error).expect("serialize command error");
+        let message = "llama.cpp runtime variant 'llama_cpp.cuda' is selected but server binary is missing at /tmp/runtime/cuda/llama-server";
+        let expected = serde_json::json!({
+            "kind": "missing_runtime_variant",
+            "diagnostic": {
+                "code": "missing_runtime_variant",
+                "severity": "error",
+                "message": message,
+                "device_class": "cuda",
+                "runtime_variant_id": "llama_cpp.cuda",
+                "backend_id": "llama_cpp",
+            },
+            "requested_device": null,
+            "missing_path": "/tmp/runtime/cuda/llama-server",
+        });
+        assert_eq!(encoded, expected);
+        assert_eq!(error.to_string(), message);
+        let round_trip: ManagedRuntimeCommandResolutionError =
+            serde_json::from_value(expected).expect("existing error wire shape");
+        assert_eq!(round_trip, error);
+        assert_eq!(round_trip.to_string(), message);
+        assert!(std::mem::size_of::() < 128);
 
         assert_eq!(
             encoded["kind"],
diff --git a/crates/pantograph-embedded-runtime/src/runtime_host_execution_port.rs b/crates/pantograph-embedded-runtime/src/runtime_host_execution_port.rs
index 7710dc220..306fb79a8 100644
--- a/crates/pantograph-embedded-runtime/src/runtime_host_execution_port.rs
+++ b/crates/pantograph-embedded-runtime/src/runtime_host_execution_port.rs
@@ -631,7 +631,7 @@ impl RuntimeHostBatchExecutionPort for EmbeddedRuntimeHostExecutionPort {
             };
             let plan = match inference::plan_image_generation_execution(projection.planning_input())
             {
-                ImageGenerationPlanningOutcome::Planned { plan } => plan,
+                ImageGenerationPlanningOutcome::Planned { plan } => *plan,
                 ImageGenerationPlanningOutcome::Rejected { diagnostics } => {
                     return Ok(rejected_batch_member_response(
                         request,
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-payload-layout.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-payload-layout.md
new file mode 100644
index 000000000..e331a69ba
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-payload-layout.md
@@ -0,0 +1,13 @@
+# Inference diagnostic and planning payload layout
+
+Fresh aggregate diagnostics identify two large payload owners: ManagedRuntimeCommandResolutionError::MissingRuntimeVariant embeds DeviceResolutionDiagnostic, and ImageGenerationPlanningOutcome::Planned embeds ImageGenerationExecutionPlan. Box only those payload fields. Leave all other error variants, request validation, planner decisions, facade context, registry resolution return types, and high-arity functions unchanged.
+
+Caller inventory: the managed-runtime diagnostic has one constructor and a Display arm reading its existing message; platform and serialization tests retain diagnostic access. The planning outcome has one constructor, two consuming gateway branches and one consuming embedded batch branch that unbox at their existing boundary, plus a worker projection test that explicitly borrows the raw plan. Inspection-only matches retain their behavior.
+
+These are explicit public Rust field-type changes, documented in the changelog despite inference being publish=false at workspace development version 0.1.0. Direct constructors must box payloads; owned plan consumers must unbox. Tagged JSON, diagnostic fields, Display messages, and underlying plan/result types remain unchanged. No release/version bump or unseen-consumer compatibility claim is made.
+
+The managed-runtime test checks exact full JSON, old-shape decoding, equality and exact Display text. The successful planner fixture retains its detailed plan assertions and adds exact outer tagged shape/round-trip/layout checks. Hosted steps run managed-runtime contracts, all existing planner acceptance/rejection tests, and worker image projection tests. The inline size assertions are not execution-performance measurements.
+
+Formatting and whitespace pass locally. No local workspace compilation/new Rust execution is claimed. Independent source review and fresh hosted inference/embedded/workflow qualification are pending.
+
+Independent source review accepted tree d8784bd001db2ffef90b0c97465b167bf1c51472, including the bounded two-field layout change, consuming caller adaptations, exact JSON/Display assertions and retained planner suites. Fresh hosted execution is still required.

From 1cae42ec9ff5a904ffe780a2ae129c2c3975fcdf Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 04:47:05 -0700
Subject: [PATCH 28/65] fix(ci): execute feature-gated PyTorch projection tests

---
 .github/workflows/quality-gates.yml                        | 4 +++-
 .../reports/2026-10-03-inference-payload-layout.md         | 7 +++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index eceec701c..65bde6762 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -203,7 +203,9 @@ jobs:
         run: |
           cargo test -p inference --lib managed_runtime::contracts::tests
           cargo test -p inference --lib image_generation_planner_tests
-          cargo test -p inference --lib pytorch_worker_image_contract_tests
+          cargo test -p inference --features backend-pytorch --lib pytorch_worker_image_contract_tests -- --list > "$RUNNER_TEMP/pytorch-image-contract-tests.list"
+          python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith("backend::pytorch::pytorch_worker_image_contract_tests::") and line.endswith(": test")]; print("PyTorch image contract tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/pytorch-image-contract-tests.list"
+          cargo test -p inference --features backend-pytorch --lib pytorch_worker_image_contract_tests
 
       - name: Run node-engine tests
         run: cargo test -p node-engine --lib
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-payload-layout.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-payload-layout.md
index e331a69ba..24f2eee87 100644
--- a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-payload-layout.md
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-payload-layout.md
@@ -11,3 +11,10 @@ The managed-runtime test checks exact full JSON, old-shape decoding, equality an
 Formatting and whitespace pass locally. No local workspace compilation/new Rust execution is claimed. Independent source review and fresh hosted inference/embedded/workflow qualification are pending.
 
 Independent source review accepted tree d8784bd001db2ffef90b0c97465b167bf1c51472, including the bounded two-field layout change, consuming caller adaptations, exact JSON/Display assertions and retained planner suites. Fresh hosted execution is still required.
+
+
+## Hosted qualification correction
+
+At head 58df81689306debaced0e224e16576742376381d, the exact managed-runtime error test and all 25 planner tests pass. The worker projection command ran zero tests because inference's default feature is backend-llamacpp. The corrected command explicitly enables backend-pytorch, checks that the filtered suite lists at least one test, and then runs that same feature/filter selection. Worker projection execution remains unqualified until the corrected hosted step succeeds. Aggregate inference Clippy is down to 11 remaining findings; those are separate scope.
+
+Independent source review accepted correction tree 6e3942251a33f311f829aba75b10859bb46482f6: explicit feature selection and nonzero exact-module discovery close the false-green gap. The final receipt must report the actual executed count; the prior zero-test step remains unqualified.

From 4bbf55eda8c095914b5288a59a47765a540fcf39 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 04:49:14 -0700
Subject: [PATCH 29/65] refactor(inference): group private gateway lifecycle
 contexts

---
 .github/workflows/quality-gates.yml           |   6 +
 crates/inference/src/gateway.rs               | 334 ++++++++++--------
 crates/inference/src/gateway_tests.rs         |  79 +++++
 .../2026-10-03-gateway-private-contexts.md    |  11 +
 4 files changed, 279 insertions(+), 151 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-gateway-private-contexts.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 65bde6762..4d6123532 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -207,6 +207,12 @@ jobs:
           python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith("backend::pytorch::pytorch_worker_image_contract_tests::") and line.endswith(": test")]; print("PyTorch image contract tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/pytorch-image-contract-tests.list"
           cargo test -p inference --features backend-pytorch --lib pytorch_worker_image_contract_tests
 
+      - name: Run gateway attribution and warmup regression tests
+        run: |
+          cargo test -p inference --lib gateway::tests::private_lifecycle_context
+          cargo test -p inference --lib gateway::tests::test_runtime_lifecycle_snapshot
+          cargo test -p inference --lib gateway::tests::test_failed_restart_clears_active_runtime_config_and_attempted_modes
+
       - name: Run node-engine tests
         run: cargo test -p node-engine --lib
 
diff --git a/crates/inference/src/gateway.rs b/crates/inference/src/gateway.rs
index 79333d5d3..540d1fea3 100644
--- a/crates/inference/src/gateway.rs
+++ b/crates/inference/src/gateway.rs
@@ -46,9 +46,9 @@ use crate::types::{
     InferenceCompatibilityIssueSummary, InferenceCompatibilityReportSummary,
     InferenceEmbeddingResult, InferenceExecutionInput, InferenceExecutionRequest,
     InferenceExecutionRequestValidationError, InferenceExecutionResult,
-    InferenceRequestLifecycleEvent, InferenceRequestLifecycleEventKind,
-    InferenceRequestLifecycleEventSink, InferenceUsage, RerankRequest, RerankResponse,
-    RuntimeLifecycleSnapshot, ServerModeInfo,
+    InferenceRequestLifecycleEvent, InferenceRequestLifecycleEventContext,
+    InferenceRequestLifecycleEventKind, InferenceRequestLifecycleEventSink, InferenceUsage,
+    RerankRequest, RerankResponse, RuntimeLifecycleSnapshot, ServerModeInfo,
 };
 use crate::{
     BackendExecutionContext, InferenceExecutionCancellationHandle,
@@ -155,6 +155,15 @@ pub struct InferenceGateway {
     runtime_instance_sequence: Arc,
 }
 
+struct RuntimeWarmupStartContext<'a> {
+    config: &'a BackendConfig,
+    previous_last_inference_config: Option,
+    previous_runtime_instance_id: Option,
+    runtime_id: String,
+    warmup_started_at_ms: u64,
+    warmup_timing_attempt_id: pantograph_timing_contracts::WorkflowTimingAttemptId,
+}
+
 fn config_model_target(config: &BackendConfig) -> Option {
     config
         .model_path
@@ -657,12 +666,14 @@ impl InferenceGateway {
         }
 
         self.record_start_result(
-            config,
-            previous_last_inference_config,
-            previous_runtime_instance_id,
-            runtime_id,
-            warmup_started_at_ms,
-            warmup_timing_attempt_id,
+            RuntimeWarmupStartContext {
+                config,
+                previous_last_inference_config,
+                previous_runtime_instance_id,
+                runtime_id,
+                warmup_started_at_ms,
+                warmup_timing_attempt_id,
+            },
             start_result,
         )
         .await
@@ -670,14 +681,17 @@ impl InferenceGateway {
 
     async fn record_start_result(
         &self,
-        config: &BackendConfig,
-        previous_last_inference_config: Option,
-        previous_runtime_instance_id: Option,
-        runtime_id: String,
-        warmup_started_at_ms: u64,
-        warmup_timing_attempt_id: pantograph_timing_contracts::WorkflowTimingAttemptId,
+        context: RuntimeWarmupStartContext<'_>,
         start_result: Result,
     ) -> Result<(), GatewayError> {
+        let RuntimeWarmupStartContext {
+            config,
+            previous_last_inference_config,
+            previous_runtime_instance_id,
+            runtime_id,
+            warmup_started_at_ms,
+            warmup_timing_attempt_id,
+        } = context;
         match start_result {
             Ok(start_outcome) => {
                 let mut current_runtime_config = self.current_runtime_config.write().await;
@@ -1014,14 +1028,17 @@ impl InferenceGateway {
 
         record_inference_lifecycle_event(
             lifecycle_sink.as_ref(),
-            request_id.clone(),
-            task_id.clone(),
-            backend_key.clone(),
-            runtime_id.clone(),
-            runtime_instance_id.clone(),
-            selected_device_class,
-            selected_device_id.clone(),
-            model_id.clone(),
+            InferenceRequestLifecycleEventContext {
+                request_id: request_id.clone(),
+                task_id: task_id.clone(),
+                backend_key: backend_key.clone(),
+                runtime_id: runtime_id.clone(),
+                runtime_instance_id: runtime_instance_id.clone(),
+                selected_device_class,
+                selected_device_id: selected_device_id.clone(),
+                model_id: model_id.clone(),
+                ..Default::default()
+            },
             InferenceRequestLifecycleEventKind::Started,
             None,
         );
@@ -1065,14 +1082,17 @@ impl InferenceGateway {
                 );
                 record_inference_lifecycle_event(
                     lifecycle_sink.as_ref(),
-                    request_id,
-                    task_id,
-                    backend_key,
-                    runtime_id,
-                    runtime_instance_id,
-                    selected_device_class,
-                    selected_device_id,
-                    model_id,
+                    InferenceRequestLifecycleEventContext {
+                        request_id,
+                        task_id,
+                        backend_key,
+                        runtime_id,
+                        runtime_instance_id,
+                        selected_device_class,
+                        selected_device_id,
+                        model_id,
+                        ..Default::default()
+                    },
                     InferenceRequestLifecycleEventKind::CleanupCompleted,
                     None,
                 );
@@ -1219,14 +1239,17 @@ impl InferenceGateway {
         record_inference_lifecycle_phase_event(
             lifecycle_sink.as_ref(),
             InferenceLifecyclePhase::TaskValidation,
-            request_id.clone(),
-            task_id.clone(),
-            backend_key.clone(),
-            runtime_id.clone(),
-            runtime_instance_id.clone(),
-            selected_device_class,
-            selected_device_id.clone(),
-            model_id.clone(),
+            InferenceRequestLifecycleEventContext {
+                request_id: request_id.clone(),
+                task_id: task_id.clone(),
+                backend_key: backend_key.clone(),
+                runtime_id: runtime_id.clone(),
+                runtime_instance_id: runtime_instance_id.clone(),
+                selected_device_class,
+                selected_device_id: selected_device_id.clone(),
+                model_id: model_id.clone(),
+                ..Default::default()
+            },
             InferenceRequestLifecycleEventKind::Started,
             None,
         );
@@ -1279,14 +1302,17 @@ impl InferenceGateway {
         record_inference_lifecycle_phase_event(
             lifecycle_sink.as_ref(),
             InferenceLifecyclePhase::Preprocessing,
-            request_id.clone(),
-            task_id.clone(),
-            backend_key.clone(),
-            runtime_id.clone(),
-            runtime_instance_id.clone(),
-            selected_device_class,
-            selected_device_id.clone(),
-            model_id.clone(),
+            InferenceRequestLifecycleEventContext {
+                request_id: request_id.clone(),
+                task_id: task_id.clone(),
+                backend_key: backend_key.clone(),
+                runtime_id: runtime_id.clone(),
+                runtime_instance_id: runtime_instance_id.clone(),
+                selected_device_class,
+                selected_device_id: selected_device_id.clone(),
+                model_id: model_id.clone(),
+                ..Default::default()
+            },
             InferenceRequestLifecycleEventKind::Started,
             None,
         );
@@ -1352,14 +1378,17 @@ impl InferenceGateway {
         let model_id = non_empty_model_id(model);
         record_inference_lifecycle_event(
             lifecycle_sink.as_ref(),
-            request_id.clone(),
-            Some("embedding".to_string()),
-            backend_key.clone(),
-            runtime_id.clone(),
-            runtime_instance_id.clone(),
-            selected_device_class,
-            selected_device_id.clone(),
-            model_id.clone(),
+            InferenceRequestLifecycleEventContext {
+                request_id: request_id.clone(),
+                task_id: Some("embedding".to_string()),
+                backend_key: backend_key.clone(),
+                runtime_id: runtime_id.clone(),
+                runtime_instance_id: runtime_instance_id.clone(),
+                selected_device_class,
+                selected_device_id: selected_device_id.clone(),
+                model_id: model_id.clone(),
+                ..Default::default()
+            },
             InferenceRequestLifecycleEventKind::Started,
             None,
         );
@@ -1444,14 +1473,17 @@ impl InferenceGateway {
         let model_id = non_empty_model_id(&request.model);
         record_inference_lifecycle_event(
             lifecycle_sink.as_ref(),
-            request_id.clone(),
-            Some("rerank".to_string()),
-            backend_key.clone(),
-            runtime_id.clone(),
-            runtime_instance_id.clone(),
-            selected_device_class,
-            selected_device_id.clone(),
-            model_id.clone(),
+            InferenceRequestLifecycleEventContext {
+                request_id: request_id.clone(),
+                task_id: Some("rerank".to_string()),
+                backend_key: backend_key.clone(),
+                runtime_id: runtime_id.clone(),
+                runtime_instance_id: runtime_instance_id.clone(),
+                selected_device_class,
+                selected_device_id: selected_device_id.clone(),
+                model_id: model_id.clone(),
+                ..Default::default()
+            },
             InferenceRequestLifecycleEventKind::Started,
             None,
         );
@@ -1781,14 +1813,17 @@ impl InferenceGateway {
         let model_id = non_empty_model_id(&request.model);
         record_inference_lifecycle_event(
             lifecycle_sink.as_ref(),
-            request_id.clone(),
-            Some("image_generation".to_string()),
-            backend_key.clone(),
-            runtime_id.clone(),
-            runtime_instance_id.clone(),
-            selected_device_class,
-            selected_device_id.clone(),
-            model_id.clone(),
+            InferenceRequestLifecycleEventContext {
+                request_id: request_id.clone(),
+                task_id: Some("image_generation".to_string()),
+                backend_key: backend_key.clone(),
+                runtime_id: runtime_id.clone(),
+                runtime_instance_id: runtime_instance_id.clone(),
+                selected_device_class,
+                selected_device_id: selected_device_id.clone(),
+                model_id: model_id.clone(),
+                ..Default::default()
+            },
             InferenceRequestLifecycleEventKind::Started,
             None,
         );
@@ -1865,14 +1900,17 @@ impl InferenceGateway {
         record_inference_lifecycle_phase_event(
             lifecycle_sink.as_ref(),
             InferenceLifecyclePhase::TaskValidation,
-            request_id.clone(),
-            task_id.clone(),
-            backend_key.clone(),
-            runtime_id.clone(),
-            runtime_instance_id.clone(),
-            selected_device_class,
-            selected_device_id.clone(),
-            model_id.clone(),
+            InferenceRequestLifecycleEventContext {
+                request_id: request_id.clone(),
+                task_id: task_id.clone(),
+                backend_key: backend_key.clone(),
+                runtime_id: runtime_id.clone(),
+                runtime_instance_id: runtime_instance_id.clone(),
+                selected_device_class,
+                selected_device_id: selected_device_id.clone(),
+                model_id: model_id.clone(),
+                ..Default::default()
+            },
             InferenceRequestLifecycleEventKind::Started,
             None,
         );
@@ -1948,14 +1986,17 @@ impl InferenceGateway {
         }
         record_inference_lifecycle_event(
             lifecycle_sink.as_ref(),
-            request_id.clone(),
-            task_id.clone(),
-            backend_key.clone(),
-            runtime_id.clone(),
-            runtime_instance_id.clone(),
-            selected_device_class,
-            selected_device_id.clone(),
-            model_id.clone(),
+            InferenceRequestLifecycleEventContext {
+                request_id: request_id.clone(),
+                task_id: task_id.clone(),
+                backend_key: backend_key.clone(),
+                runtime_id: runtime_id.clone(),
+                runtime_instance_id: runtime_instance_id.clone(),
+                selected_device_class,
+                selected_device_id: selected_device_id.clone(),
+                model_id: model_id.clone(),
+                ..Default::default()
+            },
             InferenceRequestLifecycleEventKind::Started,
             None,
         );
@@ -2307,14 +2348,17 @@ impl LifecycleStream {
     fn record(&self, kind: InferenceRequestLifecycleEventKind, detail: Option) {
         record_inference_lifecycle_event(
             self.lifecycle_sink.as_ref(),
-            self.request_id.clone(),
-            self.task_id.clone(),
-            self.backend_key.clone(),
-            self.runtime_id.clone(),
-            self.runtime_instance_id.clone(),
-            self.selected_device_class,
-            self.selected_device_id.clone(),
-            self.model_id.clone(),
+            InferenceRequestLifecycleEventContext {
+                request_id: self.request_id.clone(),
+                task_id: self.task_id.clone(),
+                backend_key: self.backend_key.clone(),
+                runtime_id: self.runtime_id.clone(),
+                runtime_instance_id: self.runtime_instance_id.clone(),
+                selected_device_class: self.selected_device_class,
+                selected_device_id: self.selected_device_id.clone(),
+                model_id: self.model_id.clone(),
+                ..Default::default()
+            },
             kind,
             detail,
         );
@@ -3118,28 +3162,14 @@ fn typed_text_generation_stream_request_json(
 
 fn record_inference_lifecycle_event(
     sink: &dyn InferenceRequestLifecycleEventSink,
-    request_id: Option,
-    task_id: Option,
-    backend_key: Option,
-    runtime_id: Option,
-    runtime_instance_id: Option,
-    selected_device_class: Option,
-    selected_device_id: Option,
-    model_id: Option,
+    context: InferenceRequestLifecycleEventContext,
     kind: InferenceRequestLifecycleEventKind,
     detail: Option,
 ) {
     record_inference_lifecycle_phase_event(
         sink,
         InferenceLifecyclePhase::BackendExecution,
-        request_id,
-        task_id,
-        backend_key,
-        runtime_id,
-        runtime_instance_id,
-        selected_device_class,
-        selected_device_id,
-        model_id,
+        context,
         kind,
         detail,
     );
@@ -3148,28 +3178,21 @@ fn record_inference_lifecycle_event(
 fn record_inference_lifecycle_phase_event(
     sink: &dyn InferenceRequestLifecycleEventSink,
     phase: InferenceLifecyclePhase,
-    request_id: Option,
-    task_id: Option,
-    backend_key: Option,
-    runtime_id: Option,
-    runtime_instance_id: Option,
-    selected_device_class: Option,
-    selected_device_id: Option,
-    model_id: Option,
+    context: InferenceRequestLifecycleEventContext,
     kind: InferenceRequestLifecycleEventKind,
     detail: Option,
 ) {
     record_inference_lifecycle_phase_event_with_option_diagnostics(
         sink,
         phase,
-        request_id,
-        task_id,
-        backend_key,
-        runtime_id,
-        runtime_instance_id,
-        selected_device_class,
-        selected_device_id,
-        model_id,
+        context.request_id,
+        context.task_id,
+        context.backend_key,
+        context.runtime_id,
+        context.runtime_instance_id,
+        context.selected_device_class,
+        context.selected_device_id,
+        context.model_id,
         kind,
         detail,
         Vec::new(),
@@ -3778,14 +3801,17 @@ fn record_typed_lifecycle_result_with_option_diagnostics(
     record_inference_lifecycle_phase_event(
         sink,
         InferenceLifecyclePhase::BackendExecution,
-        request_id,
-        task_id,
-        backend_key,
-        runtime_id,
-        runtime_instance_id,
-        selected_device_class,
-        selected_device_id,
-        model_id,
+        InferenceRequestLifecycleEventContext {
+            request_id,
+            task_id,
+            backend_key,
+            runtime_id,
+            runtime_instance_id,
+            selected_device_class,
+            selected_device_id,
+            model_id,
+            ..Default::default()
+        },
         InferenceRequestLifecycleEventKind::CleanupCompleted,
         None,
     );
@@ -3867,14 +3893,17 @@ fn record_successful_non_streaming_lifecycle_phase(
     record_inference_lifecycle_phase_event(
         sink,
         phase.clone(),
-        request_id.clone(),
-        task_id.clone(),
-        backend_key.clone(),
-        runtime_id.clone(),
-        runtime_instance_id.clone(),
-        selected_device_class,
-        selected_device_id.clone(),
-        model_id.clone(),
+        InferenceRequestLifecycleEventContext {
+            request_id: request_id.clone(),
+            task_id: task_id.clone(),
+            backend_key: backend_key.clone(),
+            runtime_id: runtime_id.clone(),
+            runtime_instance_id: runtime_instance_id.clone(),
+            selected_device_class,
+            selected_device_id: selected_device_id.clone(),
+            model_id: model_id.clone(),
+            ..Default::default()
+        },
         InferenceRequestLifecycleEventKind::Started,
         None,
     );
@@ -4037,14 +4066,17 @@ fn record_non_streaming_lifecycle_phase_result_with_references(
     record_inference_lifecycle_phase_event(
         sink,
         phase,
-        request_id,
-        task_id,
-        backend_key,
-        runtime_id,
-        runtime_instance_id,
-        selected_device_class,
-        selected_device_id,
-        model_id,
+        InferenceRequestLifecycleEventContext {
+            request_id,
+            task_id,
+            backend_key,
+            runtime_id,
+            runtime_instance_id,
+            selected_device_class,
+            selected_device_id,
+            model_id,
+            ..Default::default()
+        },
         InferenceRequestLifecycleEventKind::CleanupCompleted,
         None,
     );
diff --git a/crates/inference/src/gateway_tests.rs b/crates/inference/src/gateway_tests.rs
index 91e5d298a..094354ade 100644
--- a/crates/inference/src/gateway_tests.rs
+++ b/crates/inference/src/gateway_tests.rs
@@ -5071,3 +5071,82 @@ async fn selected_text_invalid_handoffs_have_no_backend_effects() {
         .is_err());
     assert!(effects.lock().unwrap().is_empty());
 }
+
+#[test]
+fn private_lifecycle_context_preserves_complete_attribution_and_absence() {
+    let sink = RecordingLifecycleSink::default();
+    let device_id = InferenceDeviceId::parse("cuda:0").expect("device id");
+    let context = InferenceRequestLifecycleEventContext {
+        request_id: Some("request-1".to_string()),
+        task_id: Some("text_generation".to_string()),
+        backend_key: Some("llama_cpp".to_string()),
+        runtime_id: Some("llama_cpp.cuda".to_string()),
+        runtime_instance_id: Some("instance-1".to_string()),
+        selected_device_class: Some(InferenceDeviceClass::Cuda),
+        selected_device_id: Some(device_id.clone()),
+        model_id: Some("pumas://models/example".to_string()),
+        ..Default::default()
+    };
+    record_inference_lifecycle_event(
+        &sink,
+        context.clone(),
+        InferenceRequestLifecycleEventKind::Started,
+        Some("start detail".to_string()),
+    );
+    record_inference_lifecycle_phase_event(
+        &sink,
+        InferenceLifecyclePhase::Preprocessing,
+        context,
+        InferenceRequestLifecycleEventKind::Completed,
+        None,
+    );
+    record_inference_lifecycle_event(
+        &sink,
+        InferenceRequestLifecycleEventContext::default(),
+        InferenceRequestLifecycleEventKind::CleanupCompleted,
+        None,
+    );
+    let events = sink.events();
+    assert_eq!(events.len(), 3);
+    for (index, (phase, kind, detail)) in [
+        (
+            InferenceLifecyclePhase::BackendExecution,
+            InferenceRequestLifecycleEventKind::Started,
+            Some("start detail".to_string()),
+        ),
+        (
+            InferenceLifecyclePhase::Preprocessing,
+            InferenceRequestLifecycleEventKind::Completed,
+            None,
+        ),
+    ]
+    .into_iter()
+    .enumerate()
+    {
+        let expected =
+            InferenceRequestLifecycleEvent::builder(phase, kind, events[index].occurred_at_ms)
+                .with_request_id(Some("request-1".to_string()))
+                .with_task_id(Some("text_generation".to_string()))
+                .with_backend_key(Some("llama_cpp".to_string()))
+                .with_runtime_id(Some("llama_cpp.cuda".to_string()))
+                .with_runtime_instance_id(Some("instance-1".to_string()))
+                .with_selected_device_class(Some(InferenceDeviceClass::Cuda))
+                .with_selected_device_id(Some(device_id.clone()))
+                .with_model_id(Some("pumas://models/example".to_string()))
+                .with_detail(detail)
+                .build();
+        assert_eq!(events[index], expected);
+        assert_eq!(events[index].selected_runtime_variant_id, None);
+        assert_eq!(events[index].selected_network_node_id, None);
+        assert_eq!(events[index].resolved_artifact_kind, None);
+    }
+    assert_eq!(
+        events[2],
+        InferenceRequestLifecycleEvent::builder(
+            InferenceLifecyclePhase::BackendExecution,
+            InferenceRequestLifecycleEventKind::CleanupCompleted,
+            events[2].occurred_at_ms,
+        )
+        .build()
+    );
+}
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-gateway-private-contexts.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-gateway-private-contexts.md
new file mode 100644
index 000000000..c577ea8e6
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-gateway-private-contexts.md
@@ -0,0 +1,11 @@
+# Private gateway lifecycle and warmup context grouping
+
+Replace eight repeated attribution parameters in two private lifecycle helpers with the existing InferenceRequestLifecycleEventContext. All thirteen existing call sites populate the same eight values; previously absent fields remain None via Default. The lower diagnostic recorder is unchanged. The context is moved through the helper, avoiding extra attribution clones.
+
+Group the one-caller private record_start_result inputs into RuntimeWarmupStartContext while leaving its Result separate. Destructuring restores the same local names before the byte-identical success/rollback/timing body. No public generation/server signature, lock scope, timestamp source, runtime identity, lifecycle selection, or error behavior changes.
+
+A complete-event regression checks both helper routes, all attribution values, detail, phase/kind, and explicit absence of variant/network/artifact fields; a default-context event confirms no invented attribution. Hosted steps also run existing warmup success, normalized failure and failed-restart rollback tests.
+
+Formatting/whitespace checks and a source comparison of the unchanged warmup behavior block pass locally. No local Rust execution is claimed. Independent source review and fresh hosted tests/Clippy remain pending. Existing unrelated high-arity helpers and public APIs are untouched; no new lint suppression is added.
+
+Independent source review accepted tree 03e481cd24fd8765c6b11c865ec73ff8619dd1e8. The reviewed PR #26 feature-gated projection-test correction was merged cleanly as the publication base; gateway source/test contents are unchanged by that inheritance. Actual hosted lifecycle/warmup execution remains required.

From 23c0c141c4620e3f2d6f462648098f399689b9b0 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 05:00:44 -0700
Subject: [PATCH 30/65] fix(inference): box two concrete error return
 boundaries

---
 .github/workflows/quality-gates.yml           |  5 ++
 CHANGELOG.md                                  |  4 +
 crates/inference/src/managed_binaries.rs      | 74 +++++++++++++++++--
 crates/inference/src/model_contracts.rs       | 23 +++---
 crates/inference/tests/model_contracts.rs     | 28 ++++++-
 .../2026-10-03-inference-error-boundaries.md  | 11 +++
 6 files changed, 124 insertions(+), 21 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-error-boundaries.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 4d6123532..548da9f1e 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -213,6 +213,11 @@ jobs:
           cargo test -p inference --lib gateway::tests::test_runtime_lifecycle_snapshot
           cargo test -p inference --lib gateway::tests::test_failed_restart_clears_active_runtime_config_and_attempted_modes
 
+      - name: Run typed error boundary regression tests
+        run: |
+          cargo test -p inference --lib managed_binaries::tests
+          cargo test -p inference --test model_contracts task_registry_resolution_
+
       - name: Run node-engine tests
         run: cargo test -p node-engine --lib
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa776e383..d74a50be3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@ The format is based on Keep a Changelog.
   canonicalization records.
 
 ### Changed
+- Rust source API: `resolve_managed_binary_command` and
+  `resolve_task_registry_entry_from_evidence` now return boxed concrete errors.
+  Error variants, diagnostic JSON and Display text remain unchanged; callers
+  matching an owned error must dereference the box.
 - Rust source API: `ManagedRuntimeCommandResolutionError::MissingRuntimeVariant.diagnostic`
   and `ImageGenerationPlanningOutcome::Planned.plan` now hold boxed payloads.
   Direct constructors must box their values and consuming plan callers must unbox;
diff --git a/crates/inference/src/managed_binaries.rs b/crates/inference/src/managed_binaries.rs
index 6dae960d5..81fa2a374 100644
--- a/crates/inference/src/managed_binaries.rs
+++ b/crates/inference/src/managed_binaries.rs
@@ -112,13 +112,13 @@ pub fn resolve_managed_binary_command(
     app_data_dir: &Path,
     id: ManagedBinaryId,
     args: &[&str],
-) -> Result {
+) -> Result> {
     let snapshot = managed_runtime_snapshot(app_data_dir, id)
         .map_err(ManagedBinaryFacadeError::RuntimeStatus)?;
 
     if !snapshot.available || snapshot.readiness_state != ManagedRuntimeReadinessState::Ready {
         let install_root = selected_install_root(&snapshot);
-        return Err(ManagedBinaryFacadeError::RuntimeNotReady {
+        return Err(Box::new(ManagedBinaryFacadeError::RuntimeNotReady {
             key: ManagedBinaryKey::runtime(snapshot.id),
             display_name: snapshot.display_name,
             readiness_state: snapshot.readiness_state,
@@ -126,7 +126,7 @@ pub fn resolve_managed_binary_command(
             install_root,
             missing_files: snapshot.missing_files,
             unavailable_reason: snapshot.unavailable_reason,
-        });
+        }));
     }
 
     let key = managed_runtime_dependency_key(id).ok_or_else(|| {
@@ -140,13 +140,13 @@ pub fn resolve_managed_binary_command(
         .map(resolved_command_from_dependency_command)
         .map_err(|source| {
             let install_root = selected_install_root(&snapshot);
-            ManagedBinaryFacadeError::RuntimeCommandResolution {
+            Box::new(ManagedBinaryFacadeError::RuntimeCommandResolution {
                 key: ManagedBinaryKey::runtime(snapshot.id),
                 display_name: snapshot.display_name,
                 selected_version: snapshot.selection.selected_version,
                 install_root,
                 source,
-            }
+            })
         })
 }
 
@@ -196,14 +196,14 @@ mod tests {
     fn resolve_command_reports_facade_not_ready_context() {
         let temp = tempfile::tempdir().expect("temp dir");
 
-        let error = resolve_managed_binary_command(
+        let error: Box = resolve_managed_binary_command(
             temp.path(),
             ManagedBinaryId::LlamaCpp,
             &["--port", "0"],
         )
         .expect_err("missing llama.cpp should fail before command resolution");
 
-        match error {
+        match *error {
             ManagedBinaryFacadeError::RuntimeNotReady {
                 key,
                 readiness_state,
@@ -217,4 +217,64 @@ mod tests {
             other => panic!("unexpected error: {other}"),
         }
     }
+
+    #[test]
+    fn boxed_facade_errors_preserve_variants_and_exact_display() {
+        let not_ready = Box::new(ManagedBinaryFacadeError::RuntimeNotReady {
+            key: ManagedBinaryKey::runtime(ManagedBinaryId::LlamaCpp),
+            display_name: "llama.cpp".to_string(),
+            readiness_state: ManagedRuntimeReadinessState::Missing,
+            selected_version: Some("v1".to_string()),
+            install_root: Some("/runtime".to_string()),
+            missing_files: vec!["server".to_string(), "library".to_string()],
+            unavailable_reason: Some("not installed".to_string()),
+        });
+        assert_eq!(not_ready.to_string(), "llama.cpp is not ready for launch (Missing, selected version v1, install root /runtime, missing server, library: not installed)");
+        assert!(matches!(
+            *not_ready,
+            ManagedBinaryFacadeError::RuntimeNotReady { .. }
+        ));
+        let command = Box::new(ManagedBinaryFacadeError::RuntimeCommandResolution {
+            key: ManagedBinaryKey::runtime(ManagedBinaryId::LlamaCpp),
+            display_name: "llama.cpp".to_string(),
+            selected_version: Some("v1".to_string()),
+            install_root: Some("/runtime".to_string()),
+            source: "denied".to_string(),
+        });
+        assert_eq!(command.to_string(), "failed to resolve llama.cpp launch command for selected version v1 at /runtime: denied");
+        assert!(matches!(
+            *command,
+            ManagedBinaryFacadeError::RuntimeCommandResolution { .. }
+        ));
+        let status = Box::new(ManagedBinaryFacadeError::RuntimeStatus(
+            "unreadable".to_string(),
+        ));
+        assert_eq!(
+            status.to_string(),
+            "failed to read managed runtime status: unreadable"
+        );
+    }
+
+    #[test]
+    fn successful_command_projection_preserves_launch_fields_and_argument_boundaries() {
+        let command = resolved_command_from_dependency_command(ResolvedManagedDependencyCommand {
+            key: ManagedDependencyKey::RuntimeSidecar(RuntimeSidecarDependencyId::LlamaCpp),
+            executable_path: "/runtime/server".to_string(),
+            working_directory: "/runtime".to_string(),
+            args: vec!["--model".to_string(), "models/a b.gguf".to_string()],
+            env_overrides: vec![("RUNTIME_MODE".to_string(), "test".to_string())],
+            pid_file: Some("/runtime/server.pid".to_string()),
+        });
+        assert_eq!(command.executable_path, PathBuf::from("/runtime/server"));
+        assert_eq!(command.working_directory, PathBuf::from("/runtime"));
+        assert_eq!(
+            command.args,
+            vec![OsString::from("--model"), OsString::from("models/a b.gguf")]
+        );
+        assert_eq!(
+            command.env_overrides,
+            vec![(OsString::from("RUNTIME_MODE"), OsString::from("test"))]
+        );
+        assert_eq!(command.pid_file, Some(PathBuf::from("/runtime/server.pid")));
+    }
 }
diff --git a/crates/inference/src/model_contracts.rs b/crates/inference/src/model_contracts.rs
index c88a1e4cb..03b125c54 100644
--- a/crates/inference/src/model_contracts.rs
+++ b/crates/inference/src/model_contracts.rs
@@ -800,10 +800,12 @@ pub fn resolve_task_registry_entry(value: &str) -> Option {
 /// task signature.
 pub fn resolve_task_registry_entry_from_evidence(
     evidence: &TaskEvidence,
-) -> Result {
+) -> Result> {
     let labels = task_evidence_labels(evidence);
     if labels.is_empty() {
-        return Err(TaskRegistryResolutionDiagnostic::missing_task_evidence());
+        return Err(Box::new(
+            TaskRegistryResolutionDiagnostic::missing_task_evidence(),
+        ));
     }
 
     let mut resolved_entries = Vec::new();
@@ -814,8 +816,8 @@ pub fn resolve_task_registry_entry_from_evidence(
     }
 
     let Some(first) = resolved_entries.first().cloned() else {
-        return Err(TaskRegistryResolutionDiagnostic::unsupported_task_label(
-            labels,
+        return Err(Box::new(
+            TaskRegistryResolutionDiagnostic::unsupported_task_label(labels),
         ));
     };
 
@@ -830,21 +832,20 @@ pub fn resolve_task_registry_entry_from_evidence(
     }
 
     if canonical_task_ids.len() > 1 {
-        return Err(TaskRegistryResolutionDiagnostic::conflicting_task_evidence(
-            labels,
-            canonical_task_ids,
+        return Err(Box::new(
+            TaskRegistryResolutionDiagnostic::conflicting_task_evidence(labels, canonical_task_ids),
         ));
     }
 
     if !first.matches_task_evidence(evidence) {
-        return Err(TaskRegistryResolutionDiagnostic::unsupported_task_label(
-            labels,
+        return Err(Box::new(
+            TaskRegistryResolutionDiagnostic::unsupported_task_label(labels),
         ));
     }
 
     if !first.matches_modality_evidence(evidence) {
-        return Err(TaskRegistryResolutionDiagnostic::modality_mismatch(
-            &first, evidence,
+        return Err(Box::new(
+            TaskRegistryResolutionDiagnostic::modality_mismatch(&first, evidence),
         ));
     }
 
diff --git a/crates/inference/tests/model_contracts.rs b/crates/inference/tests/model_contracts.rs
index a8412597b..87340781f 100644
--- a/crates/inference/tests/model_contracts.rs
+++ b/crates/inference/tests/model_contracts.rs
@@ -19,8 +19,8 @@ use inference::{
     PackageSizeRole, ProcessorComponentKind, PumasArtifactLoadPathKind, PumasArtifactLoadTarget,
     PumasModelRef, ResolvedModelPackageFacts, ResolvedModelSource, ResolvedModelSourceKind,
     RuntimeLifecycleSnapshot, SupportTier, TaskEvidence, TaskExecutionBehavior, TaskFamily,
-    TaskRegistryEntry, TaskRegistryResolutionDiagnosticKind, TaskRequestContract,
-    TaskStreamingSupport, MODEL_PACKAGE_FACTS_CONTRACT_VERSION,
+    TaskRegistryEntry, TaskRegistryResolutionDiagnostic, TaskRegistryResolutionDiagnosticKind,
+    TaskRequestContract, TaskStreamingSupport, MODEL_PACKAGE_FACTS_CONTRACT_VERSION,
 };
 
 const PACKAGE_FACT_FIXTURES: &[(&str, &str)] = &[
@@ -1156,6 +1156,11 @@ fn task_registry_resolution_returns_validated_entry_from_package_evidence() {
     .expect("text generation evidence should resolve");
 
     assert_eq!(entry.task_id, InferenceTaskId::TextGeneration);
+    let expected = resolve_task_registry_entry("text_generation").expect("canonical entry");
+    assert_eq!(
+        serde_json::to_value(&entry).expect("resolved entry"),
+        serde_json::to_value(expected).expect("canonical entry")
+    );
 }
 
 #[test]
@@ -1181,7 +1186,17 @@ fn task_registry_resolution_reports_unsupported_task_evidence() {
     );
 
     let encoded = serde_json::to_value(&diagnostic).expect("encode diagnostic");
-    assert_eq!(encoded["kind"], serde_json::json!("unsupported_task_label"));
+    assert_eq!(
+        encoded,
+        serde_json::json!({
+            "kind": "unsupported_task_label",
+            "message": "package task evidence does not match a canonical inference task registry entry",
+            "labels": ["object_detection", "object-detection"],
+        })
+    );
+    let decoded: TaskRegistryResolutionDiagnostic =
+        serde_json::from_value(encoded).expect("decode existing diagnostic shape");
+    assert_eq!(&decoded, diagnostic.as_ref());
 }
 
 #[test]
@@ -1198,6 +1213,13 @@ fn task_registry_resolution_reports_missing_task_evidence() {
         TaskRegistryResolutionDiagnosticKind::MissingTaskEvidence
     );
     assert!(diagnostic.labels.is_empty());
+    assert_eq!(
+        serde_json::to_value(&diagnostic).expect("missing diagnostic"),
+        serde_json::json!({
+            "kind": "missing_task_evidence",
+            "message": "package task evidence does not include a task label",
+        })
+    );
 }
 
 #[test]
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-error-boundaries.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-error-boundaries.md
new file mode 100644
index 000000000..f4b2559d7
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-inference-error-boundaries.md
@@ -0,0 +1,11 @@
+# Two typed error return boundaries
+
+Fresh inference diagnostics identify only two remaining oversized error return owners after the managed diagnostic payload repair. Return Box from resolve_managed_binary_command and Box from resolve_task_registry_entry_from_evidence. Keep the concrete error enums/DTOs, all fields, Display/Debug messages, diagnostic JSON, successful result types, and validation/branch order unchanged. No other Result is boxed.
+
+This explicitly changes two public Rust return types and adds allocation on their failure paths. Direct owned pattern matching needs dereferencing; the existing facade test is migrated. Repository consumers otherwise inspect fields, format diagnostics, or discard errors and retain that behavior. The changelog records the source change despite inference being publish=false; unseen consumers are not claimed compatible.
+
+Tests cover actual missing-runtime facade resolution, all facade variants/exact Display, successful command field/argument-boundary projection, full successful canonical registry output, and exact diagnostic JSON/raw-shape decoding. Existing conflict/modality error tests remain. Command projection is not a claim that a real runtime was installed/launched. Hosted checks explicitly run the facade and registry resolution tests.
+
+Formatting and whitespace pass locally. No local Rust execution is claimed. Independent source review and fresh hosted boundary/Clippy qualification are pending; public high-arity APIs remain separate.
+
+Independent source review accepted tree 29515000a6205a212139bcc53a6c65acd465e9d7. This covers only the two boxed concrete error boundaries and preserved payloads/branch order. Pure successful-command projection remains distinct from real runtime installation/readiness qualification; fresh hosted tests are required.

From fb61592a2b1c14b9dedb13d5bdb1872ffcf192cf Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 05:13:21 -0700
Subject: [PATCH 31/65] refactor(inference): name PyTorch text generation
 inputs

---
 .github/workflows/quality-gates.yml           |   7 +
 CHANGELOG.md                                  |   4 +
 crates/inference/src/backend/mod.rs           |   2 +-
 crates/inference/src/backend/pytorch.rs       |  99 ++++------
 crates/inference/src/backend/pytorch_tests.rs | 179 +++++++++++++++---
 crates/inference/src/lib.rs                   |   2 +-
 ...6-10-03-pytorch-text-generation-request.md |  11 ++
 7 files changed, 217 insertions(+), 87 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pytorch-text-generation-request.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 548da9f1e..1954ded40 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -218,6 +218,13 @@ jobs:
           cargo test -p inference --lib managed_binaries::tests
           cargo test -p inference --test model_contracts task_registry_resolution_
 
+      - name: Run named PyTorch text request contract tests
+        run: |
+          cargo test -p inference --features backend-pytorch --lib pytorch_named_text_request -- --list > "$RUNNER_TEMP/pytorch-named-text-tests.list"
+          python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith("backend::pytorch::tests::pytorch_named_text_request") and line.endswith(": test")]; print("Named PyTorch text tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/pytorch-named-text-tests.list"
+          cargo test -p inference --features backend-pytorch --lib pytorch_named_text_request
+          cargo test -p inference --features backend-pytorch --lib test_pytorch_generate_text_
+
       - name: Run node-engine tests
         run: cargo test -p node-engine --lib
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d74a50be3..81822497d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@ The format is based on Keep a Changelog.
   canonicalization records.
 
 ### Changed
+- Rust source API: PyTorch `generate_with_top_k` and `generate_stream_with_top_k`
+  now accept `PyTorchTextGenerationRequest` with the same seven named inputs.
+  Legacy `generate`/`generate_stream` signatures and the worker wire contract
+  remain unchanged.
 - Rust source API: `resolve_managed_binary_command` and
   `resolve_task_registry_entry_from_evidence` now return boxed concrete errors.
   Error variants, diagnostic JSON and Display text remain unchanged; callers
diff --git a/crates/inference/src/backend/mod.rs b/crates/inference/src/backend/mod.rs
index 6da38cd6b..2b26c39e0 100644
--- a/crates/inference/src/backend/mod.rs
+++ b/crates/inference/src/backend/mod.rs
@@ -63,7 +63,7 @@ pub use llamacpp::LlamaCppBackend;
 pub use candle::CandleBackend;
 
 #[cfg(feature = "backend-pytorch")]
-pub use pytorch::PyTorchBackend;
+pub use pytorch::{PyTorchBackend, PyTorchTextGenerationRequest};
 
 pub use compatibility::{
     BackendCompatibilityIssue, BackendCompatibilityIssueKind, BackendCompatibilityOptions,
diff --git a/crates/inference/src/backend/pytorch.rs b/crates/inference/src/backend/pytorch.rs
index 52dae7fbe..55b455063 100644
--- a/crates/inference/src/backend/pytorch.rs
+++ b/crates/inference/src/backend/pytorch.rs
@@ -100,6 +100,19 @@ impl PyTorchDeviceProbeSnapshot {
     }
 }
 
+/// Owned scalar inputs accepted by the PyTorch text generation entry points.
+/// Worker-specific policy fields remain private to the worker contract.
+#[derive(Debug, Clone, PartialEq)]
+pub struct PyTorchTextGenerationRequest {
+    pub prompt: String,
+    pub system_prompt: Option,
+    pub max_tokens: i64,
+    pub temperature: f64,
+    pub top_p: f64,
+    pub top_k: Option,
+    pub masked_prompt_json: Option,
+}
+
 /// PyTorch backend using in-process PyO3 embedded Python.
 ///
 /// Loads models via HuggingFace transformers with `trust_remote_code=True`,
@@ -2109,25 +2122,19 @@ impl PyTorchBackend {
     fn generate_text_envelope(
         request_id: impl Into,
         operation: PyTorchWorkerOperation,
-        prompt: String,
-        system_prompt: Option,
-        max_tokens: i64,
-        temperature: f64,
-        top_p: f64,
-        top_k: Option,
-        masked_prompt_json: Option,
+        request: PyTorchTextGenerationRequest,
     ) -> PyTorchWorkerEnvelope {
         PyTorchWorkerEnvelope::new(
             request_id,
             operation,
             Self::generate_text_request(
-                prompt,
-                system_prompt,
-                max_tokens,
-                temperature,
-                top_p,
-                top_k,
-                masked_prompt_json,
+                request.prompt,
+                request.system_prompt,
+                request.max_tokens,
+                request.temperature,
+                request.top_p,
+                request.top_k,
+                request.masked_prompt_json,
             ),
         )
     }
@@ -2492,39 +2499,27 @@ impl PyTorchBackend {
         top_p: f64,
         masked_prompt_json: Option,
     ) -> Result {
-        self.generate_with_top_k(
+        self.generate_with_top_k(PyTorchTextGenerationRequest {
             prompt,
             system_prompt,
             max_tokens,
             temperature,
             top_p,
-            None,
+            top_k: None,
             masked_prompt_json,
-        )
+        })
         .await
     }
 
     pub async fn generate_with_top_k(
         &self,
-        prompt: String,
-        system_prompt: Option,
-        max_tokens: i64,
-        temperature: f64,
-        top_p: f64,
-        top_k: Option,
-        masked_prompt_json: Option,
+        request: PyTorchTextGenerationRequest,
     ) -> Result {
         let request_id = format!("pytorch-generate-text-{}", Uuid::new_v4().simple());
         let envelope = Self::generate_text_envelope(
             request_id.clone(),
             PyTorchWorkerOperation::GenerateText,
-            prompt,
-            system_prompt,
-            max_tokens,
-            temperature,
-            top_p,
-            top_k,
-            masked_prompt_json,
+            request,
         );
         Self::validate_generate_text_envelope(&envelope)?;
         let envelope_json = serde_json::to_string(&envelope).map_err(|error| {
@@ -2601,39 +2596,27 @@ impl PyTorchBackend {
         top_p: f64,
         masked_prompt_json: Option,
     ) -> Pin> + Send>> {
-        self.generate_stream_with_top_k(
+        self.generate_stream_with_top_k(PyTorchTextGenerationRequest {
             prompt,
             system_prompt,
             max_tokens,
             temperature,
             top_p,
-            None,
+            top_k: None,
             masked_prompt_json,
-        )
+        })
     }
 
     pub fn generate_stream_with_top_k(
         &self,
-        prompt: String,
-        system_prompt: Option,
-        max_tokens: i64,
-        temperature: f64,
-        top_p: f64,
-        top_k: Option,
-        masked_prompt_json: Option,
+        request: PyTorchTextGenerationRequest,
     ) -> Pin> + Send>> {
         let (tx, rx) = tokio::sync::mpsc::channel::>(32);
         let request_id = format!("pytorch-generate-text-stream-{}", Uuid::new_v4().simple());
         let envelope = Self::generate_text_envelope(
             request_id.clone(),
             PyTorchWorkerOperation::GenerateTextStream,
-            prompt,
-            system_prompt,
-            max_tokens,
-            temperature,
-            top_p,
-            top_k,
-            masked_prompt_json,
+            request,
         );
 
         if let Err(error) = Self::validate_generate_text_stream_envelope(&envelope) {
@@ -2887,15 +2870,17 @@ impl InferenceBackend for PyTorchBackend {
             .and_then(|value| value.as_u64())
             .and_then(|value| u32::try_from(value).ok());
 
-        Ok(self.generate_stream_with_top_k(
-            prompt,
-            system_prompt,
-            max_tokens,
-            temperature,
-            top_p,
-            top_k,
-            None,
-        ))
+        Ok(
+            self.generate_stream_with_top_k(PyTorchTextGenerationRequest {
+                prompt,
+                system_prompt,
+                max_tokens,
+                temperature,
+                top_p,
+                top_k,
+                masked_prompt_json: None,
+            }),
+        )
     }
 
     async fn embeddings(
diff --git a/crates/inference/src/backend/pytorch_tests.rs b/crates/inference/src/backend/pytorch_tests.rs
index 178f2f7ca..ca751e16a 100644
--- a/crates/inference/src/backend/pytorch_tests.rs
+++ b/crates/inference/src/backend/pytorch_tests.rs
@@ -2650,24 +2650,28 @@ fn test_pytorch_generate_text_envelopes_thread_top_k_for_generate_and_stream() {
     let generate_envelope = PyTorchBackend::generate_text_envelope(
         "req-generate-top-k",
         PyTorchWorkerOperation::GenerateText,
-        "Explain adapters.".to_string(),
-        Some("Be precise.".to_string()),
-        48,
-        0.3,
-        0.9,
-        Some(33),
-        None,
+        PyTorchTextGenerationRequest {
+            prompt: "Explain adapters.".to_string(),
+            system_prompt: Some("Be precise.".to_string()),
+            max_tokens: 48,
+            temperature: 0.3,
+            top_p: 0.9,
+            top_k: Some(33),
+            masked_prompt_json: None,
+        },
     );
     let stream_envelope = PyTorchBackend::generate_text_envelope(
         "req-stream-top-k",
         PyTorchWorkerOperation::GenerateTextStream,
-        "Explain adapters.".to_string(),
-        Some("Be precise.".to_string()),
-        48,
-        0.3,
-        0.9,
-        Some(33),
-        None,
+        PyTorchTextGenerationRequest {
+            prompt: "Explain adapters.".to_string(),
+            system_prompt: Some("Be precise.".to_string()),
+            max_tokens: 48,
+            temperature: 0.3,
+            top_p: 0.9,
+            top_k: Some(33),
+            masked_prompt_json: None,
+        },
     );
 
     PyTorchBackend::validate_generate_text_envelope(&generate_envelope)
@@ -2689,13 +2693,15 @@ fn test_pytorch_generate_text_envelope_rejects_unscoped_transformers_kwargs() {
     let mut generate_envelope = PyTorchBackend::generate_text_envelope(
         "req-generate-raw-kwarg",
         PyTorchWorkerOperation::GenerateText,
-        "Explain adapters.".to_string(),
-        None,
-        48,
-        0.3,
-        0.9,
-        None,
-        None,
+        PyTorchTextGenerationRequest {
+            prompt: "Explain adapters.".to_string(),
+            system_prompt: None,
+            max_tokens: 48,
+            temperature: 0.3,
+            top_p: 0.9,
+            top_k: None,
+            masked_prompt_json: None,
+        },
     );
     generate_envelope
         .payload
@@ -2716,13 +2722,15 @@ fn test_pytorch_generate_text_stream_envelope_rejects_policy_transformers_kwargs
     let mut stream_envelope = PyTorchBackend::generate_text_envelope(
         "req-stream-policy-kwarg",
         PyTorchWorkerOperation::GenerateTextStream,
-        "Explain adapters.".to_string(),
-        None,
-        48,
-        0.3,
-        0.9,
-        None,
-        None,
+        PyTorchTextGenerationRequest {
+            prompt: "Explain adapters.".to_string(),
+            system_prompt: None,
+            max_tokens: 48,
+            temperature: 0.3,
+            top_p: 0.9,
+            top_k: None,
+            masked_prompt_json: None,
+        },
     );
     stream_envelope
         .payload
@@ -6398,3 +6406,118 @@ fn selected_text_adapter_preserves_text_parts_and_rejects_nontext_parts() {
     ]}]});
     assert!(extract_prompt_from_messages(&request).is_err());
 }
+
+fn named_text_request(prompt: &str) -> crate::PyTorchTextGenerationRequest {
+    crate::PyTorchTextGenerationRequest {
+        prompt: prompt.to_string(),
+        system_prompt: Some("Be concise.".to_string()),
+        max_tokens: 64,
+        temperature: 0.2,
+        top_p: 0.95,
+        top_k: Some(40),
+        masked_prompt_json: Some("{\"prompt\":\"masked\"}".to_string()),
+    }
+}
+
+#[test]
+fn pytorch_named_text_request_preserves_exact_worker_envelopes() {
+    for (operation, label) in [
+        (PyTorchWorkerOperation::GenerateText, "generate_text"),
+        (
+            PyTorchWorkerOperation::GenerateTextStream,
+            "generate_text_stream",
+        ),
+    ] {
+        let envelope = PyTorchBackend::generate_text_envelope(
+            "request-named",
+            operation,
+            named_text_request("Explain adapters."),
+        );
+        PyTorchBackend::validate_generate_text_envelope_operation(&envelope, operation)
+            .expect("same worker validation");
+        assert_eq!(
+            serde_json::to_value(&envelope).expect("worker envelope"),
+            serde_json::json!({
+                "contract_version": 1,
+                "request_id": "request-named",
+                "operation": label,
+                "cancellation": { "drop_stream_cancels": false },
+                "payload": {
+                    "prompt": "Explain adapters.",
+                    "system_prompt": "Be concise.",
+                    "max_tokens": 64,
+                    "temperature": 0.2,
+                    "top_p": 0.95,
+                    "masked_prompt_json": "{\"prompt\":\"masked\"}",
+                    "transformers_kwargs": { "top_k": 40 },
+                },
+            })
+        );
+    }
+}
+
+#[tokio::test]
+async fn pytorch_named_text_request_preserves_legacy_validation_paths() {
+    use futures_util::StreamExt;
+    use std::time::Duration;
+
+    let backend = PyTorchBackend::new();
+    let mut request = named_text_request("  ");
+    request.top_k = None;
+    request.masked_prompt_json = None;
+    let legacy = backend
+        .generate(
+            "  ".to_string(),
+            Some("Be concise.".to_string()),
+            64,
+            0.2,
+            0.95,
+            None,
+        )
+        .await
+        .expect_err("blank legacy prompt");
+    let named = backend
+        .generate_with_top_k(request.clone())
+        .await
+        .expect_err("blank named prompt");
+    assert_eq!(legacy.to_string(), named.to_string());
+    assert!(
+        matches!(named, BackendError::Config(ref message) if message == "PyTorch worker generate_text envelope requires a prompt")
+    );
+
+    let mut legacy_stream = backend.generate_stream(
+        "  ".to_string(),
+        Some("Be concise.".to_string()),
+        64,
+        0.2,
+        0.95,
+        None,
+    );
+    let mut named_stream = backend.generate_stream_with_top_k(request);
+    let legacy_error = tokio::time::timeout(Duration::from_secs(2), legacy_stream.next())
+        .await
+        .expect("bounded legacy validation")
+        .expect("legacy error item")
+        .expect_err("legacy validation failure");
+    let named_error = tokio::time::timeout(Duration::from_secs(2), named_stream.next())
+        .await
+        .expect("bounded named validation")
+        .expect("named error item")
+        .expect_err("named validation failure");
+    assert_eq!(legacy_error.to_string(), named_error.to_string());
+    assert!(
+        matches!(named_error, BackendError::Config(ref message) if message == "PyTorch worker generate_text envelope requires a prompt")
+    );
+    assert!(
+        tokio::time::timeout(Duration::from_secs(2), legacy_stream.next())
+            .await
+            .expect("legacy stream closes")
+            .is_none()
+    );
+    assert!(
+        tokio::time::timeout(Duration::from_secs(2), named_stream.next())
+            .await
+            .expect("named stream closes")
+            .is_none()
+    );
+}
diff --git a/crates/inference/src/lib.rs b/crates/inference/src/lib.rs
index a10466048..961841a4a 100644
--- a/crates/inference/src/lib.rs
+++ b/crates/inference/src/lib.rs
@@ -79,7 +79,7 @@ pub use backend::LlamaCppBackend;
 pub use backend::CandleBackend;
 
 #[cfg(feature = "backend-pytorch")]
-pub use backend::PyTorchBackend;
+pub use backend::{PyTorchBackend, PyTorchTextGenerationRequest};
 
 pub use config::{DeviceConfig, EmbeddingMemoryMode};
 pub use dependency_requirements::{
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pytorch-text-generation-request.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pytorch-text-generation-request.md
new file mode 100644
index 000000000..0ba10cc8d
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pytorch-text-generation-request.md
@@ -0,0 +1,11 @@
+# Named PyTorch text generation inputs
+
+The existing seven scalar inputs are grouped in public PyTorchTextGenerationRequest for the two top_k entry points and the private envelope helper. No existing request type represented exactly these inputs: the private worker DTO includes additional denoising/block controls and arbitrary kwargs, and general generation options carry broader policy. The new input type has no defaults or serialization contract and is re-exported beside PyTorchBackend under the existing feature gate.
+
+All three repository callers of the two public methods are migrated. Legacy generate/generate_stream signatures remain unchanged and wrap the same values. Strings move through the new container without added production clones; top_k still maps only to transformers_kwargs and absent values remain absent. The private worker DTO, request IDs, cancellation defaults, validation, stream/job behavior and Python worker are unchanged. The two changed public Rust call shapes are explicitly recorded in the changelog.
+
+Tests assert exact generated and streaming wire envelopes for all seven inputs, including the existing cancellation default, and compare legacy/named early validation errors and bounded stream closure. Existing top_k/unknown-policy envelope tests remain. Hosted commands explicitly enable backend-pytorch and require nonzero new-test discovery; no actual model inference is claimed by the validation-only tests.
+
+Formatting/whitespace pass locally. No local Rust execution is claimed. Independent source review and fresh hosted PyTorch contract/Clippy qualification remain pending. Public llama-server requests and other error owners are outside this slice.
+
+Independent source review accepted tree 6f40ff092f2b12780fb2152d0755ae3b7c531c23: exact owned inputs, unchanged legacy defaults/worker mapping, and bounded validation/stream tests. Fresh hosted execution remains required. The existing remote-code documentation/policy wording is outside this arity slice.

From 9458fac38ad2d6bbbe4c17a565a9066d613a7fe9 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 05:33:26 -0700
Subject: [PATCH 32/65] refactor(inference): reuse effective llama runtime
 settings

---
 .github/workflows/quality-gates.yml           |   8 ++
 CHANGELOG.md                                  |   4 +
 crates/inference/src/backend/llamacpp.rs      |  13 +--
 crates/inference/src/server.rs                | 100 +++++++++++-------
 crates/inference/src/server_tests.rs          | 100 ++++++++----------
 .../2026-10-03-llama-settings-input.md        |  11 ++
 6 files changed, 131 insertions(+), 105 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-llama-settings-input.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 1954ded40..14752d008 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -225,6 +225,14 @@ jobs:
           cargo test -p inference --features backend-pytorch --lib pytorch_named_text_request
           cargo test -p inference --features backend-pytorch --lib test_pytorch_generate_text_
 
+      - name: Run llama server startup and reuse contract tests
+        run: |
+          for test_filter in server::tests backend::llamacpp::tests; do
+            cargo test -p inference --features backend-llamacpp --lib "$test_filter" -- --list > "$RUNNER_TEMP/llama-settings-tests.list"
+            python3 -c 'import pathlib, sys; prefix = sys.argv[2] + "::"; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith(prefix) and line.endswith(": test")]; print(prefix, "tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/llama-settings-tests.list" "$test_filter"
+            cargo test -p inference --features backend-llamacpp --lib "$test_filter"
+          done
+
       - name: Run node-engine tests
         run: cargo test -p node-engine --lib
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 81822497d..f7039893a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,10 @@ The format is based on Keep a Changelog.
   canonicalization records.
 
 ### Changed
+- Rust source API: `LlamaServer::start_sidecar_inference` and
+  `matches_inference_runtime` now borrow the existing `LlamaCppRuntimeSettings`
+  for effective device/context/thread/batch values. Model/mmproj paths, process
+  spawner and port override retain their existing roles and ownership.
 - Rust source API: PyTorch `generate_with_top_k` and `generate_stream_with_top_k`
   now accept `PyTorchTextGenerationRequest` with the same seven named inputs.
   Legacy `generate`/`generate_stream` signatures and the worker wire contract
diff --git a/crates/inference/src/backend/llamacpp.rs b/crates/inference/src/backend/llamacpp.rs
index 783487742..99c19ee42 100644
--- a/crates/inference/src/backend/llamacpp.rs
+++ b/crates/inference/src/backend/llamacpp.rs
@@ -212,7 +212,6 @@ impl InferenceBackend for LlamaCppBackend {
 
         let runtime_settings = LlamaCppRuntimeSettings::try_from_backend_config(config)?;
         let device_config = runtime_settings.device_config();
-        let context_size = runtime_settings.context_size;
 
         if config.embedding_mode {
             // Start in embedding mode
@@ -289,11 +288,7 @@ impl InferenceBackend for LlamaCppBackend {
             if self.server.matches_inference_runtime(
                 &model_path.to_string_lossy(),
                 mmproj_path.as_deref(),
-                &device_config,
-                context_size,
-                runtime_settings.cpu_threads,
-                runtime_settings.batch_size,
-                runtime_settings.ubatch_size,
+                &runtime_settings,
                 config.port_override,
             ) {
                 return Ok(BackendStartOutcome {
@@ -307,11 +302,7 @@ impl InferenceBackend for LlamaCppBackend {
                     spawner,
                     &model_path.to_string_lossy(),
                     mmproj_path.as_deref(),
-                    &device_config,
-                    context_size,
-                    runtime_settings.cpu_threads,
-                    runtime_settings.batch_size,
-                    runtime_settings.ubatch_size,
+                    &runtime_settings,
                     config.port_override,
                 )
                 .await
diff --git a/crates/inference/src/server.rs b/crates/inference/src/server.rs
index 2de053845..50cb2cfbb 100644
--- a/crates/inference/src/server.rs
+++ b/crates/inference/src/server.rs
@@ -12,6 +12,7 @@ use std::sync::Arc;
 use sysinfo::{Pid, ProcessesToUpdate, Signal, System};
 use tokio::sync::RwLock;
 
+use crate::backend::LlamaCppRuntimeSettings;
 use crate::config::DeviceConfig;
 use crate::constants::{hosts, ports, timeouts};
 use crate::llamacpp_sidecar_events::{
@@ -54,17 +55,32 @@ fn parse_sidecar_pid(raw: &str) -> Option {
         .map(|record| record.pid)
 }
 
-fn active_runtime_descriptor(
+struct RuntimeDescriptorInput<'a> {
     mode: LlamaCppRuntimeMode,
     port: u16,
-    model_path: &str,
-    mmproj_path: Option<&str>,
-    device: &DeviceConfig,
+    model_path: &'a str,
+    mmproj_path: Option<&'a str>,
+    device: &'a DeviceConfig,
     context_size: Option,
     cpu_threads: Option,
     batch_size: Option,
     ubatch_size: Option,
+}
+
+fn active_runtime_descriptor(
+    input: RuntimeDescriptorInput<'_>,
 ) -> Option {
+    let RuntimeDescriptorInput {
+        mode,
+        port,
+        model_path,
+        mmproj_path,
+        device,
+        context_size,
+        cpu_threads,
+        batch_size,
+        ubatch_size,
+    } = input;
     let selected_device = selected_contract_device(device)?;
     let (selected_device_class, selected_device_id) = selected_device
         .map(|(device_class, device_id)| (Some(device_class), Some(device_id)))
@@ -250,13 +266,14 @@ impl LlamaServer {
         spawner: Arc,
         model_path: &str,
         mmproj_path: Option<&str>,
-        device: &DeviceConfig,
-        context_size: u32,
-        cpu_threads: Option,
-        batch_size: Option,
-        ubatch_size: Option,
+        settings: &LlamaCppRuntimeSettings,
         port_override: Option,
     ) -> Result<(), LlamaCppSidecarStartupError> {
+        let device = &settings.device_config();
+        let context_size = settings.context_size;
+        let cpu_threads = settings.cpu_threads;
+        let batch_size = settings.batch_size;
+        let ubatch_size = settings.ubatch_size;
         // Stop any existing connection
         self.stop();
 
@@ -636,13 +653,14 @@ impl LlamaServer {
         &self,
         model_path: &str,
         mmproj_path: Option<&str>,
-        device: &DeviceConfig,
-        context_size: u32,
-        cpu_threads: Option,
-        batch_size: Option,
-        ubatch_size: Option,
+        settings: &LlamaCppRuntimeSettings,
         port_override: Option,
     ) -> bool {
+        let device = &settings.device_config();
+        let context_size = settings.context_size;
+        let cpu_threads = settings.cpu_threads;
+        let batch_size = settings.batch_size;
+        let ubatch_size = settings.ubatch_size;
         let expected_port = port_override.unwrap_or(ports::SERVER);
         let Some(active) = self.active_runtime_descriptor() else {
             return false;
@@ -719,47 +737,47 @@ impl LlamaServer {
                 cpu_threads,
                 batch_size,
                 ubatch_size,
-            } => active_runtime_descriptor(
-                LlamaCppRuntimeMode::Inference,
-                *port,
+            } => active_runtime_descriptor(RuntimeDescriptorInput {
+                mode: LlamaCppRuntimeMode::Inference,
+                port: *port,
                 model_path,
-                mmproj_path.as_deref(),
+                mmproj_path: mmproj_path.as_deref(),
                 device,
-                Some(*context_size),
-                *cpu_threads,
-                *batch_size,
-                *ubatch_size,
-            ),
+                context_size: Some(*context_size),
+                cpu_threads: *cpu_threads,
+                batch_size: *batch_size,
+                ubatch_size: *ubatch_size,
+            }),
             ServerMode::SidecarEmbedding {
                 port,
                 model_path,
                 device,
-            } => active_runtime_descriptor(
-                LlamaCppRuntimeMode::Embedding,
-                *port,
+            } => active_runtime_descriptor(RuntimeDescriptorInput {
+                mode: LlamaCppRuntimeMode::Embedding,
+                port: *port,
                 model_path,
-                None,
+                mmproj_path: None,
                 device,
-                None,
-                None,
-                None,
-                None,
-            ),
+                context_size: None,
+                cpu_threads: None,
+                batch_size: None,
+                ubatch_size: None,
+            }),
             ServerMode::SidecarReranking {
                 port,
                 model_path,
                 device,
-            } => active_runtime_descriptor(
-                LlamaCppRuntimeMode::Reranking,
-                *port,
+            } => active_runtime_descriptor(RuntimeDescriptorInput {
+                mode: LlamaCppRuntimeMode::Reranking,
+                port: *port,
                 model_path,
-                None,
+                mmproj_path: None,
                 device,
-                None,
-                None,
-                None,
-                None,
-            ),
+                context_size: None,
+                cpu_threads: None,
+                batch_size: None,
+                ubatch_size: None,
+            }),
             ServerMode::None | ServerMode::External { .. } => None,
         }
     }
diff --git a/crates/inference/src/server_tests.rs b/crates/inference/src/server_tests.rs
index 08e13b685..a4c03d619 100644
--- a/crates/inference/src/server_tests.rs
+++ b/crates/inference/src/server_tests.rs
@@ -1,4 +1,5 @@
 use super::{parse_sidecar_pid, LlamaServer, ServerMode};
+use crate::backend::LlamaCppRuntimeSettings;
 use crate::config::DeviceConfig;
 use crate::device::DeviceBackend;
 use crate::llamacpp_sidecar_events::LlamaCppSidecarStartupError;
@@ -89,21 +90,13 @@ fn inference_runtime_matcher_requires_matching_port() {
     assert!(server.matches_inference_runtime(
         "/models/main.gguf",
         Some("/models/vision.mmproj"),
-        &device,
-        4096,
-        Some(8),
-        Some(512),
-        Some(128),
+        &inference_settings(&device, 4096, Some(8), Some(512), Some(128)),
         Some(11434),
     ));
     assert!(!server.matches_inference_runtime(
         "/models/other.gguf",
         Some("/models/vision.mmproj"),
-        &device,
-        4096,
-        Some(8),
-        Some(512),
-        Some(128),
+        &inference_settings(&device, 4096, Some(8), Some(512), Some(128)),
         Some(11434),
     ));
     assert!(!server.matches_embedding_runtime("/models/main.gguf", &device, Some(11434),));
@@ -111,31 +104,19 @@ fn inference_runtime_matcher_requires_matching_port() {
     assert!(!server.matches_inference_runtime(
         "/models/main.gguf",
         Some("/models/vision.mmproj"),
-        &device,
-        4096,
-        Some(8),
-        Some(512),
-        Some(128),
+        &inference_settings(&device, 4096, Some(8), Some(512), Some(128)),
         Some(18080),
     ));
     assert!(!server.matches_inference_runtime(
         "/models/main.gguf",
         Some("/models/vision.mmproj"),
-        &device,
-        8192,
-        Some(8),
-        Some(512),
-        Some(128),
+        &inference_settings(&device, 8192, Some(8), Some(512), Some(128)),
         Some(11434),
     ));
     assert!(!server.matches_inference_runtime(
         "/models/main.gguf",
         Some("/models/vision.mmproj"),
-        &device,
-        4096,
-        Some(16),
-        Some(512),
-        Some(128),
+        &inference_settings(&device, 4096, Some(16), Some(512), Some(128)),
         Some(11434),
     ));
 }
@@ -356,14 +337,16 @@ async fn start_sidecar_inference_cleans_process_and_pid_file_on_start_error() {
             }),
             "/models/main.gguf",
             None,
-            &DeviceConfig {
-                device: DeviceBackend::Auto,
-                gpu_layers: -1,
-            },
-            4096,
-            None,
-            None,
-            None,
+            &inference_settings(
+                &DeviceConfig {
+                    device: DeviceBackend::Auto,
+                    gpu_layers: -1,
+                },
+                4096,
+                None,
+                None,
+                None,
+            ),
             Some(18080),
         )
         .await;
@@ -394,14 +377,16 @@ async fn start_sidecar_inference_applies_runtime_settings_to_llama_server_args()
             }),
             "/models/main.gguf",
             Some("/models/mmproj.gguf"),
-            &DeviceConfig {
-                device: DeviceBackend::Vulkan(0),
-                gpu_layers: 12,
-            },
-            16384,
-            Some(8),
-            Some(512),
-            Some(128),
+            &inference_settings(
+                &DeviceConfig {
+                    device: DeviceBackend::Vulkan(0),
+                    gpu_layers: 12,
+                },
+                16384,
+                Some(8),
+                Some(512),
+                Some(128),
+            ),
             Some(18080),
         )
         .await;
@@ -448,22 +433,14 @@ fn runtime_matchers_preserve_path_component_comparison() {
     assert!(server.matches_inference_runtime(
         "models/./main.gguf",
         None,
-        &device,
-        4096,
-        None,
-        None,
-        None,
-        Some(11434)
+        &inference_settings(&device, 4096, None, None, None),
+        Some(11434),
     ));
     assert!(!server.matches_inference_runtime(
         "models/other.gguf",
         None,
-        &device,
-        4096,
-        None,
-        None,
-        None,
-        Some(11434)
+        &inference_settings(&device, 4096, None, None, None),
+        Some(11434),
     ));
     server.set_test_runtime_state(
         ServerMode::SidecarEmbedding {
@@ -486,3 +463,20 @@ fn runtime_matchers_preserve_path_component_comparison() {
     assert!(server.matches_reranking_runtime("models/./main.gguf", &device, Some(11434)));
     assert!(!server.matches_reranking_runtime("models/other.gguf", &device, Some(11434)));
 }
+
+fn inference_settings(
+    device: &DeviceConfig,
+    context_size: u32,
+    cpu_threads: Option,
+    batch_size: Option,
+    ubatch_size: Option,
+) -> LlamaCppRuntimeSettings {
+    LlamaCppRuntimeSettings {
+        device: device.device.clone(),
+        gpu_layers: device.gpu_layers,
+        context_size,
+        cpu_threads,
+        batch_size,
+        ubatch_size,
+    }
+}
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-llama-settings-input.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-llama-settings-input.md
new file mode 100644
index 000000000..9c4a17f1b
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-llama-settings-input.md
@@ -0,0 +1,11 @@
+# Llama server effective settings input
+
+Reuse existing backend-owned LlamaCppRuntimeSettings in the two public inference startup/reuse methods instead of repeating device/context/thread/batch parameters. The backend passes the same normalized settings object it already owns; the server projects the existing DeviceConfig and scalar values without new normalization or validation. Model/mmproj borrows, process spawner, port override/default, startup cleanup, and reuse semantics remain unchanged. Embedding/reranking public APIs are untouched.
+
+A private RuntimeDescriptorInput groups the existing nine snapshot parameters at all three callers. The descriptor construction block, startup behavior block and reuse comparison block are byte-identical after input destructuring/projection. Known call sites are one backend startup/reuse path and the server tests; Tauri's same-named command goes through the gateway. The two public Rust signature changes are recorded explicitly in the changelog.
+
+Existing tests retain exact launch settings/argument boundaries, startup process/PID cleanup, readiness/descriptor identities, port/context/device reuse distinctions and Path-component equality. Hosted commands enable backend-llamacpp, require nonzero discovery, and run both server and backend test modules. The tests use existing mock process infrastructure, not a claim of real model loading.
+
+Formatting, whitespace and unchanged-body comparisons pass locally. No local Rust execution is claimed. Independent source review and fresh hosted startup/reuse/Clippy qualification remain pending. This adds no runtime selection policy or scheduler algorithm.
+
+Independent source review accepted exact tree 47ac0a3557868b5d0847c84e151798c061ca2d8f with no blockers. It verified all callers and unchanged behavior blocks, plus 12 server and 11 backend test attributes in source and feature-enabled nonzero discovery. Those source counts are not execution receipts; fresh hosted tests remain required.

From c277a0b26d94e6ad1b1d25cb7ff6aa54db6f79b7 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 05:39:11 -0700
Subject: [PATCH 33/65] docs(inference): clarify default-deny remote code
 policy

---
 crates/inference/src/backend/pytorch.rs                  | 5 +++--
 .../reports/2026-10-03-pytorch-trust-policy-docs.md      | 9 +++++++++
 2 files changed, 12 insertions(+), 2 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pytorch-trust-policy-docs.md

diff --git a/crates/inference/src/backend/pytorch.rs b/crates/inference/src/backend/pytorch.rs
index 55b455063..48271cdb3 100644
--- a/crates/inference/src/backend/pytorch.rs
+++ b/crates/inference/src/backend/pytorch.rs
@@ -115,8 +115,9 @@ pub struct PyTorchTextGenerationRequest {
 
 /// PyTorch backend using in-process PyO3 embedded Python.
 ///
-/// Loads models via HuggingFace transformers with `trust_remote_code=True`,
-/// supporting standard models, dLLM architectures, and Sherry quantised models.
+/// Loads models via HuggingFace transformers using explicit model-load security
+/// policy; custom remote code is denied by default. Supports standard models,
+/// dLLM architectures, and Sherry quantised models.
 pub struct PyTorchBackend {
     /// Whether the backend has been initialised and is ready
     ready: bool,
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pytorch-trust-policy-docs.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pytorch-trust-policy-docs.md
new file mode 100644
index 000000000..0a9cbff16
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pytorch-trust-policy-docs.md
@@ -0,0 +1,9 @@
+# PyTorch trust-policy documentation correction
+
+The PyTorchBackend class comment incorrectly described unconditional trust_remote_code=True. The actual loader chain is policy-driven and defaults closed: ModelRemoteCodePolicy defaults to Deny; PyTorchTransformersTrustPolicy derives allow_remote_code only from explicit Allow; worker_contract.py defaults the missing flag to false; worker.py defaults trust_remote_code=False and rejects required custom code when it remains closed.
+
+Correct only the class documentation to describe that existing behavior. No runtime, policy default, validation, accepted-source, worker, or credential behavior changes. This is a source-grounded documentation correction, not a comprehensive security audit or authorization to load custom code.
+
+Whitespace/formatting checks pass. Runtime tests were not rerun for this comment-only change. Publication receipt pending.
+
+Source review accepted the exact documentation-only checkpoint 52e511f44a5de9113b7ec32c5f2df4f4935cbc34. The review confirms documentation accuracy only; no additional runtime execution is claimed. Publication follows the separately reviewed llama milestone; no runtime implementation changed in this slice.

From 92ca20540422bbc74fb888c810893d3ce2121dbd Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 05:56:06 -0700
Subject: [PATCH 34/65] refactor(node-engine): group validation lifecycle
 attribution

---
 .github/workflows/quality-gates.yml           |  6 +++
 crates/node-engine/src/core_executor.rs       | 45 +++++++++++++------
 .../2026-10-03-node-validation-context.md     |  7 +++
 3 files changed, 44 insertions(+), 14 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-validation-context.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 14752d008..aa09628bd 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -233,6 +233,12 @@ jobs:
             cargo test -p inference --features backend-llamacpp --lib "$test_filter"
           done
 
+      - name: Run contract-only node lifecycle tests
+        run: |
+          cargo test -p node-engine --features inference-nodes --lib rejects_contract_only_with_lifecycle -- --list > "$RUNNER_TEMP/node-validation-lifecycle-tests.list"
+          python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith("core_executor::tests::inference_tests::") and "rejects_contract_only_with_lifecycle" in line and line.endswith(": test")]; print("Contract-only lifecycle tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/node-validation-lifecycle-tests.list"
+          cargo test -p node-engine --features inference-nodes --lib rejects_contract_only_with_lifecycle
+
       - name: Run node-engine tests
         run: cargo test -p node-engine --lib
 
diff --git a/crates/node-engine/src/core_executor.rs b/crates/node-engine/src/core_executor.rs
index ef2192afb..2a79bac79 100644
--- a/crates/node-engine/src/core_executor.rs
+++ b/crates/node-engine/src/core_executor.rs
@@ -434,14 +434,16 @@ fn reject_contract_only_inference_task(
     let artifact_refs = contract_only_task_artifact_refs(inputs);
     record_task_validation_failure_lifecycle(
         extensions,
-        task_id,
-        execution_id,
-        entry.canonical_label(),
-        backend_key,
-        inference_model_id_from_inputs(inputs),
-        message.clone(),
-        option_diagnostics,
-        artifact_refs,
+        TaskValidationFailureContext {
+            task_id,
+            execution_id,
+            task_label: entry.canonical_label(),
+            backend_key,
+            model_id: inference_model_id_from_inputs(inputs),
+            detail: message.clone(),
+            option_diagnostics,
+            artifact_refs,
+        },
     );
 
     Err(NodeEngineError::ExecutionFailed(message))
@@ -570,17 +572,32 @@ fn task_option_present(inputs: &HashMap, aliases: &[&
 }
 
 #[cfg(feature = "inference-nodes")]
-fn record_task_validation_failure_lifecycle(
-    extensions: &ExecutorExtensions,
-    task_id: &str,
-    execution_id: &str,
-    task_label: &str,
-    backend_key: Option<&str>,
+struct TaskValidationFailureContext<'a> {
+    task_id: &'a str,
+    execution_id: &'a str,
+    task_label: &'a str,
+    backend_key: Option<&'a str>,
     model_id: Option,
     detail: String,
     option_diagnostics: Vec,
     artifact_refs: Vec,
+}
+
+#[cfg(feature = "inference-nodes")]
+fn record_task_validation_failure_lifecycle(
+    extensions: &ExecutorExtensions,
+    context: TaskValidationFailureContext<'_>,
 ) {
+    let TaskValidationFailureContext {
+        task_id,
+        execution_id,
+        task_label,
+        backend_key,
+        model_id,
+        detail,
+        option_diagnostics,
+        artifact_refs,
+    } = context;
     let Some(sink) = extensions
         .get::>(
             extension_keys::INFERENCE_LIFECYCLE_SINK,
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-validation-context.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-validation-context.md
new file mode 100644
index 000000000..a14e36be1
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-validation-context.md
@@ -0,0 +1,7 @@
+# Private node validation lifecycle context
+
+Fresh PR #30 aggregate Clippy cleared inference and stopped at node-engine's private record_task_validation_failure_lifecycle helper. Group its existing eight values into TaskValidationFailureContext, keep ExecutorExtensions separate, and migrate the single caller. Both the new private type and helper retain the inference-nodes feature gate. The sink lookup and complete event loop are byte-identical after destructuring.
+
+The two existing contract-only depth/video tests already assert complete request/task/backend/runtime/model attribution, Started/Failed/Cleanup ordering, and failure-only option diagnostics and artifact references. Hosted commands now explicitly enable inference-nodes, require nonzero exact-module discovery, and execute these tests. Public API, task support policy, generated request IDs, diagnostics and sink failure handling are unchanged.
+
+Formatting, whitespace and unchanged event-body comparison pass locally. No local Rust execution is claimed. Root source review accepted frozen tree f20d0e683c0057dfa7337abe8b0e56f6e2a6effa after inspecting the exact three-file diff, unchanged eight-value grouping, feature gates, single caller and nonzero test discovery. This is source review, not Rust execution or independent-agent review. Fresh hosted tests/Clippy qualification and the full combined external review remain pending; no lint suppression is added.

From a6fa0755f2b2f25b25d556e00bcb8f2da60ae234 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 06:07:55 -0700
Subject: [PATCH 35/65] test(node-engine): disambiguate mock backend result
 type

---
 crates/node-engine/src/core_executor/kv_cache_test_support.rs | 2 +-
 .../reports/2026-10-03-node-validation-context.md             | 4 ++++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/crates/node-engine/src/core_executor/kv_cache_test_support.rs b/crates/node-engine/src/core_executor/kv_cache_test_support.rs
index eb2f56b59..8b90349fd 100644
--- a/crates/node-engine/src/core_executor/kv_cache_test_support.rs
+++ b/crates/node-engine/src/core_executor/kv_cache_test_support.rs
@@ -81,7 +81,7 @@ impl InferenceBackend for MockKvBackend {
         Ok(BackendStartOutcome::default())
     }
 
-    async fn stop(&mut self) -> Result<(), BackendError> {
+    async fn stop(&mut self) -> std::result::Result<(), BackendError> {
         Ok(())
     }
 
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-validation-context.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-validation-context.md
index a14e36be1..995569d94 100644
--- a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-validation-context.md
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-validation-context.md
@@ -5,3 +5,7 @@ Fresh PR #30 aggregate Clippy cleared inference and stopped at node-engine's pri
 The two existing contract-only depth/video tests already assert complete request/task/backend/runtime/model attribution, Started/Failed/Cleanup ordering, and failure-only option diagnostics and artifact references. Hosted commands now explicitly enable inference-nodes, require nonzero exact-module discovery, and execute these tests. Public API, task support policy, generated request IDs, diagnostics and sink failure handling are unchanged.
 
 Formatting, whitespace and unchanged event-body comparison pass locally. No local Rust execution is claimed. Root source review accepted frozen tree f20d0e683c0057dfa7337abe8b0e56f6e2a6effa after inspecting the exact three-file diff, unchanged eight-value grouping, feature gates, single caller and nonzero test discovery. This is source review, not Rust execution or independent-agent review. Fresh hosted tests/Clippy qualification and the full combined external review remain pending; no lint suppression is added.
+
+## Feature-enabled fixture compilation correction
+
+The initial exact-head focused job 111207010012 at 92ca205 failed before lifecycle test discovery: MockKvBackend::stop resolved Result through super::* to node-engine's one-parameter NodeEngineError alias (E0107/E0053). This fixture file is unchanged from main 4938e405. Qualify std::result::Result explicitly, matching InferenceBackend::stop and neighboring mock methods, without changing the successful mock response. The two lifecycle tests have not yet executed; the feature/nonzero gate remains in place. Root source review accepted correction tree 39a1d45aaf80fe05264f5f26357e3cb2d00a9875. Corrected hosted execution remains pending.

From 3c4dbbd331a83ffd5f249392eda5dfd47ee3142c Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 06:12:33 -0700
Subject: [PATCH 36/65] refactor(runtime-host): implement standard borrowed
 accessors

---
 .github/workflows/quality-gates.yml           |  4 ++
 CHANGELOG.md                                  |  4 ++
 .../src/reservation_lifecycle.rs              | 14 +++--
 .../src/runtime_host_execution.rs             | 28 ++++++----
 .../src/runtime_session_load.rs               |  7 ++-
 .../tests/as_ref_compatibility.rs             | 56 +++++++++++++++++++
 .../reports/2026-10-03-runtime-host-asref.md  |  7 +++
 7 files changed, 99 insertions(+), 21 deletions(-)
 create mode 100644 crates/pantograph-runtime-host-contracts/tests/as_ref_compatibility.rs
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-host-asref.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index aa09628bd..9b767666e 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -190,6 +190,10 @@ jobs:
       - name: Check scheduler standard-trait accessor lint
         run: cargo clippy -p pantograph-scheduler --all-targets -- -D clippy::should_implement_trait
 
+      - name: Run runtime-host contract and accessor compatibility tests
+        run: cargo test -p pantograph-runtime-host-contracts
+      - name: Check runtime-host standard-trait accessor lint
+        run: cargo clippy -p pantograph-runtime-host-contracts --all-targets -- -D clippy::should_implement_trait
       - name: Run media conversion contract tests
         run: cargo test -p pantograph-media-conversion
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7039893a..67e487d99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -47,3 +47,7 @@ The format is based on Keep a Changelog.
 
 ### Security
 - Canonical path validation enforced at file-boundary entry points to block traversal and symlink escape paths.
+
+### Runtime-host borrowed accessor compatibility
+
+The seven validated runtime-host contract wrappers now implement standard `AsRef` in place of inherent `as_ref` methods. Borrowed values and lifetimes, wrapper types, validation and `into_inner` are unchanged. Ordinary method syntax, qualified calls and function pointers continue to resolve through the standard prelude. Rust callers using `no_implicit_prelude` must explicitly import `std::convert::AsRef`. The workspace crate remains unpublished (`publish = false`).
diff --git a/crates/pantograph-runtime-host-contracts/src/reservation_lifecycle.rs b/crates/pantograph-runtime-host-contracts/src/reservation_lifecycle.rs
index 68876e834..c22409ae2 100644
--- a/crates/pantograph-runtime-host-contracts/src/reservation_lifecycle.rs
+++ b/crates/pantograph-runtime-host-contracts/src/reservation_lifecycle.rs
@@ -133,12 +133,13 @@ impl ReservationLifecycleEvent {
 #[must_use]
 pub struct ValidatedReservationLifecycleEvent(ReservationLifecycleEvent);
 
-impl ValidatedReservationLifecycleEvent {
-    #[must_use]
-    pub fn as_ref(&self) -> &ReservationLifecycleEvent {
+impl AsRef for ValidatedReservationLifecycleEvent {
+    fn as_ref(&self) -> &ReservationLifecycleEvent {
         &self.0
     }
+}
 
+impl ValidatedReservationLifecycleEvent {
     #[must_use]
     pub fn into_inner(self) -> ReservationLifecycleEvent {
         self.0
@@ -195,12 +196,13 @@ impl ReservationLifecycleApplication {
 #[must_use]
 pub struct ValidatedReservationLifecycleApplication(ReservationLifecycleApplication);
 
-impl ValidatedReservationLifecycleApplication {
-    #[must_use]
-    pub fn as_ref(&self) -> &ReservationLifecycleApplication {
+impl AsRef for ValidatedReservationLifecycleApplication {
+    fn as_ref(&self) -> &ReservationLifecycleApplication {
         &self.0
     }
+}
 
+impl ValidatedReservationLifecycleApplication {
     #[must_use]
     pub fn into_inner(self) -> ReservationLifecycleApplication {
         self.0
diff --git a/crates/pantograph-runtime-host-contracts/src/runtime_host_execution.rs b/crates/pantograph-runtime-host-contracts/src/runtime_host_execution.rs
index ae267ba72..74506beff 100644
--- a/crates/pantograph-runtime-host-contracts/src/runtime_host_execution.rs
+++ b/crates/pantograph-runtime-host-contracts/src/runtime_host_execution.rs
@@ -167,12 +167,13 @@ impl RuntimeHostExecutionInputValue {
 #[must_use]
 pub struct ValidatedRuntimeHostExecutionRequest(RuntimeHostExecutionRequest);
 
-impl ValidatedRuntimeHostExecutionRequest {
-    #[must_use]
-    pub fn as_ref(&self) -> &RuntimeHostExecutionRequest {
+impl AsRef for ValidatedRuntimeHostExecutionRequest {
+    fn as_ref(&self) -> &RuntimeHostExecutionRequest {
         &self.0
     }
+}
 
+impl ValidatedRuntimeHostExecutionRequest {
     #[must_use]
     pub fn into_inner(self) -> RuntimeHostExecutionRequest {
         self.0
@@ -388,12 +389,13 @@ impl RuntimeHostExecutionResponse {
 #[must_use]
 pub struct ValidatedRuntimeHostExecutionResponse(RuntimeHostExecutionResponse);
 
-impl ValidatedRuntimeHostExecutionResponse {
-    #[must_use]
-    pub fn as_ref(&self) -> &RuntimeHostExecutionResponse {
+impl AsRef for ValidatedRuntimeHostExecutionResponse {
+    fn as_ref(&self) -> &RuntimeHostExecutionResponse {
         &self.0
     }
+}
 
+impl ValidatedRuntimeHostExecutionResponse {
     #[must_use]
     pub fn into_inner(self) -> RuntimeHostExecutionResponse {
         self.0
@@ -724,12 +726,13 @@ pub enum RuntimeHostBatchMemberReservationDisposition {
 #[must_use]
 pub struct ValidatedRuntimeHostBatchExecutionRequest(RuntimeHostBatchExecutionRequest);
 
-impl ValidatedRuntimeHostBatchExecutionRequest {
-    #[must_use]
-    pub fn as_ref(&self) -> &RuntimeHostBatchExecutionRequest {
+impl AsRef for ValidatedRuntimeHostBatchExecutionRequest {
+    fn as_ref(&self) -> &RuntimeHostBatchExecutionRequest {
         &self.0
     }
+}
 
+impl ValidatedRuntimeHostBatchExecutionRequest {
     #[must_use]
     pub fn into_inner(self) -> RuntimeHostBatchExecutionRequest {
         self.0
@@ -749,12 +752,13 @@ impl TryFrom for ValidatedRuntimeHostBatchExec
 #[must_use]
 pub struct ValidatedRuntimeHostBatchExecutionResponse(RuntimeHostBatchExecutionResponse);
 
-impl ValidatedRuntimeHostBatchExecutionResponse {
-    #[must_use]
-    pub fn as_ref(&self) -> &RuntimeHostBatchExecutionResponse {
+impl AsRef for ValidatedRuntimeHostBatchExecutionResponse {
+    fn as_ref(&self) -> &RuntimeHostBatchExecutionResponse {
         &self.0
     }
+}
 
+impl ValidatedRuntimeHostBatchExecutionResponse {
     #[must_use]
     pub fn into_inner(self) -> RuntimeHostBatchExecutionResponse {
         self.0
diff --git a/crates/pantograph-runtime-host-contracts/src/runtime_session_load.rs b/crates/pantograph-runtime-host-contracts/src/runtime_session_load.rs
index 30a5a48c1..3005ab079 100644
--- a/crates/pantograph-runtime-host-contracts/src/runtime_session_load.rs
+++ b/crates/pantograph-runtime-host-contracts/src/runtime_session_load.rs
@@ -89,12 +89,13 @@ pub enum WorkflowSessionRuntimeLoadProofDiagnosticPhase {
 #[must_use]
 pub struct ValidatedWorkflowSessionRuntimeLoadProof(WorkflowSessionRuntimeLoadProof);
 
-impl ValidatedWorkflowSessionRuntimeLoadProof {
-    #[must_use]
-    pub fn as_ref(&self) -> &WorkflowSessionRuntimeLoadProof {
+impl AsRef for ValidatedWorkflowSessionRuntimeLoadProof {
+    fn as_ref(&self) -> &WorkflowSessionRuntimeLoadProof {
         &self.0
     }
+}
 
+impl ValidatedWorkflowSessionRuntimeLoadProof {
     #[must_use]
     pub fn into_inner(self) -> WorkflowSessionRuntimeLoadProof {
         self.0
diff --git a/crates/pantograph-runtime-host-contracts/tests/as_ref_compatibility.rs b/crates/pantograph-runtime-host-contracts/tests/as_ref_compatibility.rs
new file mode 100644
index 000000000..4cd23823c
--- /dev/null
+++ b/crates/pantograph-runtime-host-contracts/tests/as_ref_compatibility.rs
@@ -0,0 +1,56 @@
+//! Compile coverage of public accessor call forms after migration to AsRef.
+use pantograph_runtime_host_contracts::*;
+
+macro_rules! accessor_call_forms {
+    ($name:ident, $wrapper:ty, $raw:ty) => {
+        #[test]
+        fn $name() {
+            let _: fn(&$wrapper) -> &$raw = |value| value.as_ref();
+            let _: fn(&$wrapper) -> &$raw = |value| <$wrapper>::as_ref(value);
+            let _: fn(&$wrapper) -> &$raw = <$wrapper>::as_ref;
+            let _: fn(&$wrapper) -> &$raw = |value| <$wrapper as AsRef<$raw>>::as_ref(value);
+        }
+    };
+}
+
+accessor_call_forms!(
+    validated_reservation_lifecycle_event,
+    ValidatedReservationLifecycleEvent,
+    ReservationLifecycleEvent
+);
+
+accessor_call_forms!(
+    validated_reservation_lifecycle_application,
+    ValidatedReservationLifecycleApplication,
+    ReservationLifecycleApplication
+);
+
+accessor_call_forms!(
+    validated_runtime_host_execution_request,
+    ValidatedRuntimeHostExecutionRequest,
+    RuntimeHostExecutionRequest
+);
+
+accessor_call_forms!(
+    validated_runtime_host_execution_response,
+    ValidatedRuntimeHostExecutionResponse,
+    RuntimeHostExecutionResponse
+);
+
+accessor_call_forms!(
+    validated_runtime_host_batch_execution_request,
+    ValidatedRuntimeHostBatchExecutionRequest,
+    RuntimeHostBatchExecutionRequest
+);
+
+accessor_call_forms!(
+    validated_runtime_host_batch_execution_response,
+    ValidatedRuntimeHostBatchExecutionResponse,
+    RuntimeHostBatchExecutionResponse
+);
+
+accessor_call_forms!(
+    validated_workflow_session_runtime_load_proof,
+    ValidatedWorkflowSessionRuntimeLoadProof,
+    WorkflowSessionRuntimeLoadProof
+);
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-host-asref.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-host-asref.md
new file mode 100644
index 000000000..638f26128
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-host-asref.md
@@ -0,0 +1,7 @@
+# Runtime-host standard borrowed accessors
+
+Fresh PR #32 Clippy job 111207010086 reports seven should_implement_trait errors in runtime-host-contracts. Move exactly those borrowed accessors into AsRef implementations, preserving the seven exported wrapper/raw pairs, validation, constructors, into_inner and raw reference lifetimes. Retain wrapper must_use; the redundant inherent accessor annotation disappears with the method migration.
+
+The source inventory covers runtime-host-contracts, embedded-runtime and workflow-service callers. No existing qualified accessor/function-pointer or no_implicit_prelude callers were found. Seven compile tests cover method syntax, qualified syntax, a typed function pointer and fully qualified trait syntax. Existing contract behavior tests remain, and hosted CI adds the complete crate suite plus the targeted trait lint. PR #19 already established with a Rust 1.92 probe that adding the trait while retaining the inherent method does not satisfy this lint.
+
+The public compatibility note records the explicit trait import needed for no_implicit_prelude consumers. This publish=false crate has no version bump. Formatting and staged whitespace are checked locally; root source review accepted frozen tree a8fc84c89cf8c97198ac2e2e4fb0ee437782a228 after inspecting all seven files; fresh hosted execution remains pending. No local workspace Rust qualification is claimed.

From 395596fb0daf91b07d0075d0df51f22061597be1 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 06:20:35 -0700
Subject: [PATCH 37/65] refactor(diagnostics): reuse projection state input

---
 .github/workflows/quality-gates.yml           |   2 +
 .../src/sqlite/event_sqlite.rs                | 110 +++++++++++-------
 .../src/tests.rs                              |   9 ++
 ...6-10-03-ledger-private-projection-input.md |   7 ++
 4 files changed, 87 insertions(+), 41 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-private-projection-input.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 9b767666e..9963b1131 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -194,6 +194,8 @@ jobs:
         run: cargo test -p pantograph-runtime-host-contracts
       - name: Check runtime-host standard-trait accessor lint
         run: cargo clippy -p pantograph-runtime-host-contracts --all-targets -- -D clippy::should_implement_trait
+      - name: Run diagnostics ledger contract tests
+        run: cargo test -p pantograph-diagnostics-ledger
       - name: Run media conversion contract tests
         run: cargo test -p pantograph-media-conversion
 
diff --git a/crates/pantograph-diagnostics-ledger/src/sqlite/event_sqlite.rs b/crates/pantograph-diagnostics-ledger/src/sqlite/event_sqlite.rs
index 30e6c2d0d..528a0850d 100644
--- a/crates/pantograph-diagnostics-ledger/src/sqlite/event_sqlite.rs
+++ b/crates/pantograph-diagnostics-ledger/src/sqlite/event_sqlite.rs
@@ -182,22 +182,31 @@ pub(super) fn upsert_projection_state(
     update.validate()?;
     let tx = ledger.conn.transaction()?;
     let record = if update.status == ProjectionStatus::Failed {
-        write_projection_failure_state(
+        write_projection_state(
             &tx,
-            update.projection_name.as_str(),
-            update.projection_version,
-            update.last_applied_event_seq,
-            update.rebuilt_at_ms,
-            update
-                .last_error
-                .as_deref()
-                .expect("failed projection state has an error"),
-            update
-                .last_error_at_ms
-                .expect("failed projection state has an error timestamp"),
-            update
-                .last_failed_event_seq
-                .expect("failed projection state has a failed event cursor"),
+            ProjectionStateWrite {
+                projection_name: update.projection_name.as_str(),
+                projection_version: update.projection_version,
+                last_applied_event_seq: update.last_applied_event_seq,
+                status: ProjectionStatus::Failed,
+                rebuilt_at_ms: update.rebuilt_at_ms,
+                last_error: Some(
+                    update
+                        .last_error
+                        .as_deref()
+                        .expect("failed projection state has an error"),
+                ),
+                last_error_at_ms: Some(
+                    update
+                        .last_error_at_ms
+                        .expect("failed projection state has an error timestamp"),
+                ),
+                last_failed_event_seq: Some(
+                    update
+                        .last_failed_event_seq
+                        .expect("failed projection state has a failed event cursor"),
+                ),
+            },
         )?
     } else {
         write_projection_state(
@@ -268,31 +277,6 @@ fn write_projection_success_state(
     )
 }
 
-fn write_projection_failure_state(
-    tx: &Transaction<'_>,
-    projection_name: &str,
-    projection_version: i64,
-    last_applied_event_seq: i64,
-    rebuilt_at_ms: Option,
-    last_error: &str,
-    last_error_at_ms: i64,
-    last_failed_event_seq: i64,
-) -> Result {
-    write_projection_state(
-        tx,
-        ProjectionStateWrite {
-            projection_name,
-            projection_version,
-            last_applied_event_seq,
-            status: ProjectionStatus::Failed,
-            rebuilt_at_ms,
-            last_error: Some(last_error),
-            last_error_at_ms: Some(last_error_at_ms),
-            last_failed_event_seq: Some(last_failed_event_seq),
-        },
-    )
-}
-
 fn write_projection_state(
     tx: &Transaction<'_>,
     update: ProjectionStateWrite<'_>,
@@ -1948,7 +1932,8 @@ fn inference_option_support_timeline_detail(
         ("requires backend support", counts.requires_backend_support),
     ]
     .into_iter()
-    .filter_map(|(label, count)| (count > 0).then(|| format!("{label} {count}")))
+    .filter(|(_, count)| *count > 0)
+    .map(|(label, count)| format!("{label} {count}"))
     .collect::>();
 
     (!parts.is_empty()).then(|| format!("option support {}", parts.join(", ")))
@@ -4329,3 +4314,46 @@ where
 {
     rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(error))
 }
+
+#[cfg(test)]
+mod projection_style_tests {
+    use super::inference_option_support_timeline_detail;
+    use crate::event::InferenceOptionSupportCounts;
+
+    #[test]
+    fn option_support_detail_preserves_order_and_omits_zero_counts() {
+        let counts = InferenceOptionSupportCounts {
+            honored: 1,
+            mapped: 2,
+            defaulted: 3,
+            ignored: 4,
+            unsupported: 5,
+            rejected: 6,
+            conflict: 7,
+            model_unavailable: 8,
+            backend_unavailable: 9,
+            requires_model_support: 10,
+            requires_backend_support: 11,
+        };
+        assert_eq!(inference_option_support_timeline_detail(&counts).as_deref(), Some(
+            "option support honored 1, mapped 2, defaulted 3, ignored 4, unsupported 5, rejected 6, conflict 7, model unavailable 8, backend unavailable 9, requires model support 10, requires backend support 11"
+        ));
+        let sparse = InferenceOptionSupportCounts {
+            mapped: 2,
+            rejected: 6,
+            ..InferenceOptionSupportCounts::default()
+        };
+        assert_eq!(
+            inference_option_support_timeline_detail(&sparse).as_deref(),
+            Some("option support mapped 2, rejected 6")
+        );
+    }
+
+    #[test]
+    fn option_support_detail_is_absent_for_zero_counts() {
+        assert_eq!(
+            inference_option_support_timeline_detail(&InferenceOptionSupportCounts::default()),
+            None
+        );
+    }
+}
diff --git a/crates/pantograph-diagnostics-ledger/src/tests.rs b/crates/pantograph-diagnostics-ledger/src/tests.rs
index 4ea640319..72993acea 100644
--- a/crates/pantograph-diagnostics-ledger/src/tests.rs
+++ b/crates/pantograph-diagnostics-ledger/src/tests.rs
@@ -2222,6 +2222,10 @@ fn projection_state_persists_failure_health_and_success_clears_it() {
         })
         .expect("failed projection state stores");
 
+    assert_eq!(failed.projection_name, "scheduler_timeline");
+    assert_eq!(failed.projection_version, 1);
+    assert_eq!(failed.last_applied_event_seq, 10);
+    assert_eq!(failed.rebuilt_at_ms, None);
     assert_eq!(failed.status, ProjectionStatus::Failed);
     assert_eq!(
         failed.last_error.as_deref(),
@@ -2230,6 +2234,11 @@ fn projection_state_persists_failure_health_and_success_clears_it() {
     assert_eq!(failed.last_error_at_ms, Some(20));
     assert_eq!(failed.last_failed_event_seq, Some(11));
 
+    assert_eq!(
+        ledger.projection_state("scheduler_timeline").unwrap(),
+        Some(failed.clone())
+    );
+
     let recovered = ledger
         .upsert_projection_state(ProjectionStateUpdate {
             projection_name: "scheduler_timeline".to_string(),
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-private-projection-input.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-private-projection-input.md
new file mode 100644
index 000000000..1c436dc8a
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-private-projection-input.md
@@ -0,0 +1,7 @@
+# Private ledger projection input and ordered timeline formatting
+
+Fresh PR #32 Clippy reports one private eight-argument projection writer and one bool::then filter_map in diagnostics-ledger. Reuse the existing ProjectionStateWrite at the sole failure-writer caller, preserving validation, transaction order, Failed status, all eight stored values, exact expect messages and the lower SQL writer. Remove only the redundant private wrapper.
+
+Replace the timeline filter_map with ordered filter/map. Exact all-category and sparse text tests preserve label order and omission of zero counts; an empty-count test preserves None. The existing failure-health/recovery test now checks every stable stored field and reads the failed state back before recovery. Hosted CI runs the complete ledger crate suite. The public payload enum layout remains a separate change.
+
+Whitespace and Rust syntax formatting are checked locally. Root source review accepted the exact four-file frozen tree ab465f369c064fc8c4d966e9fa99ae7f1ec1255f. Actual hosted Rust execution remains pending.

From 443348201cf73424fc1705a6a9deb90f5021d5ef Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 06:26:46 -0700
Subject: [PATCH 38/65] refactor(diagnostics): box inference event payload

---
 CHANGELOG.md                                  |  4 ++
 .../src/event.rs                              |  2 +-
 .../src/tests.rs                              | 46 ++++++++++++++++++-
 .../src/node_execution_ledger.rs              | 12 ++---
 .../src/workflow/session_execution_api.rs     |  4 +-
 ...6-10-03-ledger-inference-payload-layout.md |  7 +++
 6 files changed, 64 insertions(+), 11 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-inference-payload-layout.md

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67e487d99..94cbba6ec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,3 +51,7 @@ The format is based on Keep a Changelog.
 ### Runtime-host borrowed accessor compatibility
 
 The seven validated runtime-host contract wrappers now implement standard `AsRef` in place of inherent `as_ref` methods. Borrowed values and lifetimes, wrapper types, validation and `into_inner` are unchanged. Ordinary method syntax, qualified calls and function pointers continue to resolve through the standard prelude. Rust callers using `no_implicit_prelude` must explicitly import `std::convert::AsRef`. The workspace crate remains unpublished (`publish = false`).
+
+### Diagnostic event inference payload construction
+
+`DiagnosticEventPayload::InferenceExecutionDiagnosticObserved` now stores `Box`. Rust callers constructing this public variant must wrap the existing raw payload in `Box::new`; owned pattern bindings now contain a box. Tagged serialized JSON, raw payload fields and validation remain unchanged. This adds one allocation for this event variant; no measured performance improvement is claimed. The crate remains `publish = false`.
diff --git a/crates/pantograph-diagnostics-ledger/src/event.rs b/crates/pantograph-diagnostics-ledger/src/event.rs
index a608c9c9d..90b92164c 100644
--- a/crates/pantograph-diagnostics-ledger/src/event.rs
+++ b/crates/pantograph-diagnostics-ledger/src/event.rs
@@ -272,7 +272,7 @@ pub enum DiagnosticEventPayload {
     RetentionPolicyChanged(RetentionPolicyChangedPayload),
     RuntimeCapabilityObserved(RuntimeCapabilityObservedPayload),
     NodeExecutionStatus(NodeExecutionStatusPayload),
-    InferenceExecutionDiagnosticObserved(InferenceExecutionDiagnosticObservedPayload),
+    InferenceExecutionDiagnosticObserved(Box),
     DiagnosticErrorOccurred(DiagnosticErrorOccurredPayload),
 }
 
diff --git a/crates/pantograph-diagnostics-ledger/src/tests.rs b/crates/pantograph-diagnostics-ledger/src/tests.rs
index 72993acea..2563b315c 100644
--- a/crates/pantograph-diagnostics-ledger/src/tests.rs
+++ b/crates/pantograph-diagnostics-ledger/src/tests.rs
@@ -1432,6 +1432,48 @@ fn diagnostic_event_ledger_projects_backend_and_task_on_node_status() {
     assert_eq!(nodes[0].selected_backend_key.as_deref(), Some("pytorch"));
 }
 
+#[test]
+fn inference_diagnostic_payload_preserves_exact_tagged_wire_and_ledger_round_trip() {
+    let expected = serde_json::json!({
+        "payload_type": "inference_execution_diagnostic_observed",
+        "request_id": "req-wire",
+        "task_id": "text_generation",
+        "compatibility_issue_count": 0,
+        "option_support_counts": {
+            "honored": 0, "mapped": 0, "defaulted": 0, "ignored": 0,
+            "unsupported": 0, "rejected": 0, "conflict": 0,
+            "model_unavailable": 0, "backend_unavailable": 0,
+            "requires_model_support": 0, "requires_backend_support": 0
+        }
+    });
+    let payload: DiagnosticEventPayload = serde_json::from_value(expected.clone()).unwrap();
+    assert_eq!(serde_json::to_value(&payload).unwrap(), expected);
+    let mut event = sample_inference_execution_diagnostic_event();
+    event.payload = payload.clone();
+    let mut ledger = SqliteDiagnosticsLedger::open_in_memory().unwrap();
+    ledger
+        .append_diagnostic_event(event)
+        .expect("valid inference payload appends");
+    let records = ledger.diagnostic_events_after(0, 10).unwrap();
+    assert_eq!(records.len(), 1);
+    assert_eq!(
+        serde_json::from_str::(&records[0].payload_json).unwrap(),
+        expected
+    );
+    assert_eq!(
+        serde_json::from_str::(&records[0].payload_json).unwrap(),
+        payload
+    );
+}
+
+#[test]
+fn diagnostic_event_payload_keeps_large_inference_record_indirect() {
+    assert!(
+        std::mem::size_of::()
+            < std::mem::size_of::()
+    );
+}
+
 #[test]
 fn diagnostic_event_ledger_appends_inference_execution_diagnostic_summary() {
     let mut ledger = SqliteDiagnosticsLedger::open_in_memory().expect("ledger opens");
@@ -6404,7 +6446,7 @@ fn sample_inference_execution_diagnostic_event() -> DiagnosticEventAppendRequest
         retention_class: DiagnosticEventRetentionClass::AuditMetadata,
         payload_ref: None,
         payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(
-            InferenceExecutionDiagnosticObservedPayload {
+            Box::new(InferenceExecutionDiagnosticObservedPayload {
                 request_id: "req-a".to_string(),
                 task_id: "text_generation".to_string(),
                 lifecycle_phase: Some("task_validation".to_string()),
@@ -6470,7 +6512,7 @@ fn sample_inference_execution_diagnostic_event() -> DiagnosticEventAppendRequest
                         message: Some("not mapped by this backend boundary".to_string()),
                     },
                 ],
-            },
+            }),
         ),
     }
 }
diff --git a/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs b/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs
index 8a1979cde..499d35347 100644
--- a/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs
+++ b/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs
@@ -952,7 +952,7 @@ fn build_kv_cache_diagnostic_event_ledger_append_request(
         retention_class: DiagnosticEventRetentionClass::AuditMetadata,
         payload_ref: None,
         payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(
-            InferenceExecutionDiagnosticObservedPayload {
+            Box::new(InferenceExecutionDiagnosticObservedPayload {
                 request_id: format!("{task_id}:kv_cache"),
                 task_id: "kv_cache".to_string(),
                 lifecycle_phase: Some("kv_cache".to_string()),
@@ -981,7 +981,7 @@ fn build_kv_cache_diagnostic_event_ledger_append_request(
                     .take(MAX_INFERENCE_OPTION_DIAGNOSTICS)
                     .map(kv_cache_option_diagnostic_summary)
                     .collect(),
-            },
+            }),
         ),
     })
 }
@@ -1048,7 +1048,7 @@ fn build_runtime_settings_diagnostic_event_ledger_append_request(
         retention_class: DiagnosticEventRetentionClass::AuditMetadata,
         payload_ref: None,
         payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(
-            InferenceExecutionDiagnosticObservedPayload {
+            Box::new(InferenceExecutionDiagnosticObservedPayload {
                 request_id: format!("{task_id}:runtime_settings"),
                 task_id: "runtime_settings".to_string(),
                 lifecycle_phase: Some("runtime_settings".to_string()),
@@ -1075,7 +1075,7 @@ fn build_runtime_settings_diagnostic_event_ledger_append_request(
                 compatibility_issues: Vec::new(),
                 option_support_counts: InferenceOptionSupportCounts::default(),
                 option_diagnostics: Vec::new(),
-            },
+            }),
         ),
     })
 }
@@ -1147,7 +1147,7 @@ fn build_inference_diagnostic_event_ledger_append_request(
         retention_class: DiagnosticEventRetentionClass::AuditMetadata,
         payload_ref: None,
         payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(
-            InferenceExecutionDiagnosticObservedPayload {
+            Box::new(InferenceExecutionDiagnosticObservedPayload {
                 request_id: event
                     .request_id
                     .clone()
@@ -1196,7 +1196,7 @@ fn build_inference_diagnostic_event_ledger_append_request(
                     .take(MAX_INFERENCE_OPTION_DIAGNOSTICS)
                     .map(option_diagnostic_summary)
                     .collect(),
-            },
+            }),
         ),
     })
 }
diff --git a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
index e8bd263f2..b636ed332 100644
--- a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
+++ b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
@@ -3103,7 +3103,7 @@ mod tests {
             retention_class: DiagnosticEventRetentionClass::AuditMetadata,
             payload_ref: None,
             payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(
-                pantograph_diagnostics_ledger::InferenceExecutionDiagnosticObservedPayload {
+                Box::new(pantograph_diagnostics_ledger::InferenceExecutionDiagnosticObservedPayload {
                     request_id: "req-a".to_string(),
                     task_id: "image_generation".to_string(),
                     lifecycle_phase: Some("backend_execution".to_string()),
@@ -3138,7 +3138,7 @@ mod tests {
                     option_support_counts:
                         pantograph_diagnostics_ledger::InferenceOptionSupportCounts::default(),
                     option_diagnostics: Vec::new(),
-                },
+                }),
             ),
         }
     }
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-inference-payload-layout.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-inference-payload-layout.md
new file mode 100644
index 000000000..4d26752bc
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-inference-payload-layout.md
@@ -0,0 +1,7 @@
+# Diagnostic event inference payload indirection
+
+Fresh PR #33 aggregate Clippy identifies InferenceExecutionDiagnosticObserved as the 872-byte largest DiagnosticEventPayload variant, with IoArtifactObserved next at 640 bytes. Box only the inference variant; preserve the raw payload type, tagged Serde representation, validation, event-kind mapping and projection readers. Migrate the five existing constructors: three embedded-runtime recorders, one ledger fixture and one workflow-service fixture. Other variants and Result types remain unchanged.
+
+An explicit minimal tagged JSON test deserializes the public enum, checks exact serialization and appends/reads it through the SQLite ledger. A layout test ensures the enum remains smaller than its raw inference payload without claiming throughput or allocation improvements. The inherited full ledger suite retains detailed inference append, projection, rejection and resource-rollup behavior coverage, and Headless qualifies the embedded-runtime callers.
+
+This is a public Rust construction and owned-pattern-binding change despite publish=false: callers wrap raw values in Box::new. It adds one allocation for this variant, with unchanged wire format and raw field semantics. Root source review accepted all six files at frozen tree e7a57e8d1bc9db3100c75dcb45ae9ef4dad8d67a. Hosted execution remains pending. No local Rust execution is claimed.

From 3d8de843930154892fa21903bf57e4933d796b14 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 06:41:49 -0700
Subject: [PATCH 39/65] refactor(diagnostics): box artifact event payload

---
 CHANGELOG.md                                  |  4 ++
 .../src/event.rs                              |  2 +-
 .../src/tests.rs                              | 44 ++++++++++++++++++-
 .../src/node_execution_ledger.rs              |  4 +-
 .../src/workflow/session_execution_api.rs     |  4 +-
 .../src/workflow/tests/diagnostics.rs         |  4 +-
 ...26-10-03-ledger-artifact-payload-layout.md |  7 +++
 7 files changed, 60 insertions(+), 9 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-artifact-payload-layout.md

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94cbba6ec..2c8136067 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -55,3 +55,7 @@ The seven validated runtime-host contract wrappers now implement standard `AsRef
 ### Diagnostic event inference payload construction
 
 `DiagnosticEventPayload::InferenceExecutionDiagnosticObserved` now stores `Box`. Rust callers constructing this public variant must wrap the existing raw payload in `Box::new`; owned pattern bindings now contain a box. Tagged serialized JSON, raw payload fields and validation remain unchanged. This adds one allocation for this event variant; no measured performance improvement is claimed. The crate remains `publish = false`.
+
+### Diagnostic event artifact payload construction
+
+`DiagnosticEventPayload::IoArtifactObserved` now stores `Box`. Rust callers constructing the public variant use `Box::new`, and owned pattern bindings contain a box. Raw DTO fields, validation and tagged JSON stay unchanged; this adds one allocation for this variant without a measured performance claim. The crate remains `publish = false`.
diff --git a/crates/pantograph-diagnostics-ledger/src/event.rs b/crates/pantograph-diagnostics-ledger/src/event.rs
index 90b92164c..68c050673 100644
--- a/crates/pantograph-diagnostics-ledger/src/event.rs
+++ b/crates/pantograph-diagnostics-ledger/src/event.rs
@@ -266,7 +266,7 @@ pub enum DiagnosticEventPayload {
     RunStarted(RunStartedPayload),
     RunTerminal(RunTerminalPayload),
     RunSnapshotAccepted(RunSnapshotAcceptedPayload),
-    IoArtifactObserved(IoArtifactObservedPayload),
+    IoArtifactObserved(Box),
     RetentionArtifactStateChanged(RetentionArtifactStateChangedPayload),
     LibraryAssetAccessed(LibraryAssetAccessedPayload),
     RetentionPolicyChanged(RetentionPolicyChangedPayload),
diff --git a/crates/pantograph-diagnostics-ledger/src/tests.rs b/crates/pantograph-diagnostics-ledger/src/tests.rs
index 2563b315c..2948baa34 100644
--- a/crates/pantograph-diagnostics-ledger/src/tests.rs
+++ b/crates/pantograph-diagnostics-ledger/src/tests.rs
@@ -3051,6 +3051,46 @@ fn run_detail_projection_backfills_selected_runtime_from_node_status() {
     assert_eq!(record.selected_task_id.as_deref(), Some("text_generation"));
 }
 
+#[test]
+fn io_artifact_payload_preserves_exact_tagged_wire_and_ledger_round_trip() {
+    let expected = serde_json::json!({
+        "payload_type": "io_artifact_observed",
+        "artifact_id": "artifact-wire",
+        "artifact_role": "node_output",
+        "producer_node_id": null, "producer_port_id": null,
+        "consumer_node_id": null, "consumer_port_id": null,
+        "media_type": null, "size_bytes": null, "content_hash": null,
+        "retention_state": null, "retention_reason": null
+    });
+    let payload: DiagnosticEventPayload = serde_json::from_value(expected.clone()).unwrap();
+    assert_eq!(serde_json::to_value(&payload).unwrap(), expected);
+    let mut event =
+        sample_io_artifact_event("run-wire", "node-wire", "node_output", "artifact-wire");
+    event.payload = payload.clone();
+    let mut ledger = SqliteDiagnosticsLedger::open_in_memory().unwrap();
+    ledger
+        .append_diagnostic_event(event)
+        .expect("valid artifact payload appends");
+    let records = ledger.diagnostic_events_after(0, 10).unwrap();
+    assert_eq!(records.len(), 1);
+    assert_eq!(
+        serde_json::from_str::(&records[0].payload_json).unwrap(),
+        expected
+    );
+    assert_eq!(
+        serde_json::from_str::(&records[0].payload_json).unwrap(),
+        payload
+    );
+}
+
+#[test]
+fn diagnostic_event_payload_keeps_large_artifact_record_indirect() {
+    assert!(
+        std::mem::size_of::()
+            < std::mem::size_of::()
+    );
+}
+
 #[test]
 fn io_artifact_projection_drains_artifact_events_incrementally() {
     let mut ledger = SqliteDiagnosticsLedger::open_in_memory().expect("ledger opens");
@@ -6262,7 +6302,7 @@ fn sample_io_artifact_event(
         privacy_class: DiagnosticEventPrivacyClass::SensitiveReference,
         retention_class: DiagnosticEventRetentionClass::PayloadReference,
         payload_ref: Some(format!("artifact://{artifact_id}")),
-        payload: DiagnosticEventPayload::IoArtifactObserved(IoArtifactObservedPayload {
+        payload: DiagnosticEventPayload::IoArtifactObserved(Box::new(IoArtifactObservedPayload {
             artifact_fact_id: None,
             payload_artifact_id: None,
             artifact_id: artifact_id.to_string(),
@@ -6303,7 +6343,7 @@ fn sample_io_artifact_event(
                 conversion_command_id: None,
                 conversion_dependencies: Vec::new(),
             }),
-        }),
+        })),
     }
 }
 
diff --git a/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs b/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs
index 499d35347..183fa5f49 100644
--- a/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs
+++ b/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs
@@ -532,7 +532,7 @@ impl NodeExecutionWorkflowLedgerSink {
             privacy_class: artifact.privacy_class,
             retention_class: artifact.retention_class,
             payload_ref: artifact.payload_ref,
-            payload: DiagnosticEventPayload::IoArtifactObserved(IoArtifactObservedPayload {
+            payload: DiagnosticEventPayload::IoArtifactObserved(Box::new(IoArtifactObservedPayload {
                 artifact_fact_id: Some(artifact.artifact_fact_id),
                 payload_artifact_id: Some(artifact.payload_artifact_id),
                 artifact_id: artifact.artifact_id,
@@ -553,7 +553,7 @@ impl NodeExecutionWorkflowLedgerSink {
                 read_handle: artifact.read_handle,
                 stream_handle: None,
                 format: artifact.format,
-            }),
+            })),
         };
 
         self.workflow_service
diff --git a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
index b636ed332..e47fcaa84 100644
--- a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
+++ b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
@@ -1351,7 +1351,7 @@ impl WorkflowService {
                     retention_class: metadata.retention_class,
                     payload_ref: metadata.payload_ref.clone(),
                     payload: DiagnosticEventPayload::IoArtifactObserved(
-                        IoArtifactObservedPayload {
+                        Box::new(IoArtifactObservedPayload {
                             artifact_fact_id: Some(metadata.artifact_fact_id),
                             payload_artifact_id: Some(metadata.payload_artifact_id),
                             artifact_id: metadata.artifact_id,
@@ -1388,7 +1388,7 @@ impl WorkflowService {
                             read_handle: metadata.read_handle,
                             stream_handle: metadata.stream_handle,
                             format: metadata.format,
-                        },
+                        }),
                     ),
                 },
             )
diff --git a/crates/pantograph-workflow-service/src/workflow/tests/diagnostics.rs b/crates/pantograph-workflow-service/src/workflow/tests/diagnostics.rs
index 19b542165..8c44f3122 100644
--- a/crates/pantograph-workflow-service/src/workflow/tests/diagnostics.rs
+++ b/crates/pantograph-workflow-service/src/workflow/tests/diagnostics.rs
@@ -2811,7 +2811,7 @@ fn sample_io_artifact_event(
         privacy_class: DiagnosticEventPrivacyClass::SensitiveReference,
         retention_class: DiagnosticEventRetentionClass::PayloadReference,
         payload_ref: Some(format!("artifact://{artifact_id}")),
-        payload: DiagnosticEventPayload::IoArtifactObserved(IoArtifactObservedPayload {
+        payload: DiagnosticEventPayload::IoArtifactObserved(Box::new(IoArtifactObservedPayload {
             artifact_fact_id: None,
             payload_artifact_id: None,
             artifact_id: artifact_id.to_string(),
@@ -2836,7 +2836,7 @@ fn sample_io_artifact_event(
             read_handle: None,
             stream_handle: None,
             format: None,
-        }),
+        })),
     }
 }
 
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-artifact-payload-layout.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-artifact-payload-layout.md
new file mode 100644
index 000000000..c6d254ec6
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ledger-artifact-payload-layout.md
@@ -0,0 +1,7 @@
+# Diagnostic event artifact payload indirection
+
+Fresh PR #35 Clippy job 111211956000 identifies IoArtifactObserved as the remaining 640-byte largest DiagnosticEventPayload variant, compared with SchedulerRunAdmitted at 360 bytes. Box only this artifact variant. Preserve the raw DTO, validation, event-kind mapping, Serde tag and projection readers. Migrate the four existing constructors in embedded-runtime, workflow-service and their ledger/diagnostics fixtures. No other enum variant or Result type changes.
+
+Add explicit minimal tagged JSON serialization/deserialization and SQLite append/read round-trip coverage, plus an enum-size bound. Inherited full ledger tests retain artifact projection and retention behavior, while Headless qualifies production callers. Public Rust variant construction and owned pattern binding now involve Box; the documentation records the added allocation despite publish=false and makes no measured performance claim.
+
+Root approved this bounded follow-up from fresh evidence. Staged whitespace and new-test formatting are checked locally; root source review accepted all seven files at frozen tree 9271a602ebe593d0f45d29674ebc99ca7cb3a1cc; actual hosted execution remains pending. No local Rust execution is claimed.

From 89ffaef6ee45832604d1d24eecd9e09f67214e61 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 06:54:19 -0700
Subject: [PATCH 40/65] refactor(workflow): box private no-selection error
 payload

---
 .github/workflows/quality-gates.yml           |  5 ++
 .../src/scheduler/task_orchestrator.rs        |  8 ++-
 .../src/scheduler/task_orchestrator_tests.rs  | 61 +++++++++++++++++--
 ...-03-orchestrator-selection-error-layout.md |  7 +++
 4 files changed, 73 insertions(+), 8 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-orchestrator-selection-error-layout.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 9963b1131..4a0aeda46 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -251,6 +251,11 @@ jobs:
       - name: Run workflow-nodes tests
         run: cargo test -p workflow-nodes --lib
 
+      - name: Run workflow scheduler orchestrator regression tests
+        run: |
+          cargo test -p pantograph-workflow-service --lib scheduler::task_orchestrator::tests:: -- --list > "$RUNNER_TEMP/orchestrator-tests.list"
+          python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith("scheduler::task_orchestrator::tests::") and line.endswith(": test")]; print("Orchestrator tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/orchestrator-tests.list"
+          cargo test -p pantograph-workflow-service --lib scheduler::task_orchestrator::tests::
       - name: Run workflow-service contract tests
         run: cargo test -p pantograph-workflow-service --test contract
 
diff --git a/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs b/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
index 2f6217dc2..2f7835d9b 100644
--- a/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
+++ b/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
@@ -631,7 +631,7 @@ impl WorkflowSchedulerTaskOrchestrator {
                 .await?;
             return Err(
                 WorkflowSchedulerTaskOrchestratorError::RuntimeDispatchSelectionNoSelection(
-                    selection,
+                    Box::new(selection),
                 ),
             );
         }
@@ -2115,7 +2115,9 @@ fn dispatch_selected_handoff_from_selection(
 ) -> Result {
     if selection.state != SchedulerDispatchSelectionState::Selected {
         return Err(
-            WorkflowSchedulerTaskOrchestratorError::RuntimeDispatchSelectionNoSelection(selection),
+            WorkflowSchedulerTaskOrchestratorError::RuntimeDispatchSelectionNoSelection(
+                Box::new(selection),
+            ),
         );
     }
     let Some(dispatch_decision) = selection.dispatch_decision else {
@@ -2812,7 +2814,7 @@ pub(crate) enum WorkflowSchedulerTaskOrchestratorError {
     #[error("runtime-host task input mapping failed")]
     RuntimeHostTaskInputMapping(WorkflowRuntimeHostTaskInputMappingError),
     #[error("scheduler dispatch selection did not select a runtime task")]
-    RuntimeDispatchSelectionNoSelection(SchedulerDispatchSelectionDecision),
+    RuntimeDispatchSelectionNoSelection(Box),
     #[error("reservation lifecycle contract validation failed: {0}")]
     ReservationLifecycleContract(ReservationLifecycleContractError),
     #[error("reservation lifecycle port failed: {0}")]
diff --git a/crates/pantograph-workflow-service/src/scheduler/task_orchestrator_tests.rs b/crates/pantograph-workflow-service/src/scheduler/task_orchestrator_tests.rs
index b71aae3ba..ec4be194c 100644
--- a/crates/pantograph-workflow-service/src/scheduler/task_orchestrator_tests.rs
+++ b/crates/pantograph-workflow-service/src/scheduler/task_orchestrator_tests.rs
@@ -689,6 +689,12 @@ async fn orchestrator_selects_scheduler_dispatch_before_runtime_host_port() {
 async fn orchestrator_does_not_dispatch_runtime_host_when_scheduler_selects_no_candidate() {
     let mut selection_request = dispatch_selection_request_fixture();
     selection_request.candidates.clear();
+    let expected_selection = select_scheduler_dispatch(
+        ValidatedSchedulerDispatchSelectionRequest::try_from(selection_request.clone()).unwrap(),
+    )
+    .unwrap()
+    .into_inner();
+    let expected_json = serde_json::to_value(&expected_selection).unwrap();
     let port = Arc::new(RecordingRuntimeHostPort::with_response(
         runtime_host_response_fixture(),
     ));
@@ -708,14 +714,59 @@ async fn orchestrator_does_not_dispatch_runtime_host_when_scheduler_selects_no_c
         .await
         .expect_err("no-selection diagnostics must stop before runtime host");
 
-    assert!(matches!(
-        error,
-        WorkflowSchedulerTaskOrchestratorError::RuntimeDispatchSelectionNoSelection(selection)
-            if selection.state == SchedulerDispatchSelectionState::NoSelection
-    ));
+    assert_eq!(
+        error.to_string(),
+        "scheduler dispatch selection did not select a runtime task"
+    );
+    let WorkflowSchedulerTaskOrchestratorError::RuntimeDispatchSelectionNoSelection(selection) =
+        error
+    else {
+        panic!("expected retained no-selection decision");
+    };
+    assert_eq!(
+        selection.state,
+        SchedulerDispatchSelectionState::NoSelection
+    );
+    assert_eq!(*selection, expected_selection);
+    assert_eq!(
+        serde_json::to_value(selection.as_ref()).unwrap(),
+        expected_json
+    );
     assert!(port.requests().is_empty());
 }
 
+#[test]
+fn no_selection_handoff_error_preserves_the_complete_decision() {
+    let mut request = dispatch_selection_request_fixture();
+    request.candidates.clear();
+    let decision = select_scheduler_dispatch(
+        ValidatedSchedulerDispatchSelectionRequest::try_from(request).unwrap(),
+    )
+    .unwrap()
+    .into_inner();
+    let expected_json = serde_json::to_value(&decision).unwrap();
+    let error = super::dispatch_selected_handoff_from_selection(decision.clone()).unwrap_err();
+    assert_eq!(
+        error.to_string(),
+        "scheduler dispatch selection did not select a runtime task"
+    );
+    let WorkflowSchedulerTaskOrchestratorError::RuntimeDispatchSelectionNoSelection(retained) =
+        error
+    else {
+        panic!("expected retained no-selection decision");
+    };
+    assert_eq!(*retained, decision);
+    assert_eq!(
+        serde_json::to_value(retained.as_ref()).unwrap(),
+        expected_json
+    );
+}
+
+#[test]
+fn orchestrator_error_does_not_embed_the_large_selection_decision() {
+    assert!(std::mem::size_of::() <= 128);
+}
+
 #[tokio::test]
 async fn orchestrator_rejects_missing_runtime_input_before_runtime_host_port() {
     let selection_request = dispatch_selection_request_fixture();
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-orchestrator-selection-error-layout.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-orchestrator-selection-error-layout.md
new file mode 100644
index 000000000..f39d82bf6
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-orchestrator-selection-error-layout.md
@@ -0,0 +1,7 @@
+# Private orchestrator no-selection error payload
+
+Fresh PR #36 Clippy job 111214475174 clears diagnostics-ledger and reaches workflow-service: 134 library findings, including 89 large-error reports. The private WorkflowSchedulerTaskOrchestratorError embeds a 1,984-byte no-selection decision, versus its next 112-byte variant. Box only RuntimeDispatchSelectionNoSelection at its two constructors. Preserve the error Display text, raw scheduler decision type, complete diagnostics and JSON, borrowed readers, success paths and all Result signatures.
+
+The existing no-candidate integration test now checks complete decision/JSON equality and exact Display while retaining the assertion that no runtime-host request occurs. A direct handoff rejection test covers the other constructor, and a layout bound checks the private error remains at most 128 bytes. Existing successful selection and persisted terminal-diagnostic tests remain, with explicit nonzero discovery and the full orchestrator test module in CI.
+
+This adds one allocation on the no-selection error path without changing public contracts or claiming measured performance. Other style, public layout and arity findings remain separate and require fresh evidence. Root approved the bounded design. Staged whitespace and focused test formatting pass locally; root source review accepted all four files at frozen tree da720da95b3f4f6ac6bafb4cfb552937412d483d; hosted execution remains pending.

From 25de27ead981379c1d2f122f9a4a7841dbec0b28 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 07:04:47 -0700
Subject: [PATCH 41/65] refactor(workflow): preserve contracts while clearing
 style lints

---
 .github/workflows/quality-gates.yml           |  5 +++
 .../src/graph/connection_intent.rs            | 20 ++++++++----
 .../src/graph/inference_interface_request.rs  | 23 +++++++++++--
 .../graph/inference_validation_task_owner.rs  | 10 ++----
 .../graph/session_inference_validation_api.rs | 20 ++++++------
 .../src/scheduler/task_orchestrator.rs        |  6 ++--
 .../src/workflow/attribution_api.rs           |  4 +--
 .../src/workflow/contracts.rs                 | 30 ++++++++++-------
 .../src/workflow/diagnostics_api.rs           |  3 +-
 .../executable_validation_snapshot.rs         |  1 -
 .../src/workflow/session_execution_api.rs     | 11 ++-----
 .../src/workflow/session_queue_api.rs         |  1 -
 .../src/workflow/session_runtime.rs           |  3 +-
 .../src/workflow/task_graph.rs                | 32 ++++++++++++++++---
 .../2026-10-03-workflow-lint-basics.md        |  7 ++++
 15 files changed, 113 insertions(+), 63 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-lint-basics.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 4a0aeda46..badccc11a 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -251,6 +251,11 @@ jobs:
       - name: Run workflow-nodes tests
         run: cargo test -p workflow-nodes --lib
 
+      - name: Run workflow style contract regressions
+        run: |
+          cargo test -p pantograph-workflow-service --lib lint_style_ -- --list > "$RUNNER_TEMP/workflow-style-tests.list"
+          python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if "lint_style_regressions::lint_style_" in line and line.endswith(": test")]; print("Workflow style tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/workflow-style-tests.list"
+          cargo test -p pantograph-workflow-service --lib lint_style_
       - name: Run workflow scheduler orchestrator regression tests
         run: |
           cargo test -p pantograph-workflow-service --lib scheduler::task_orchestrator::tests:: -- --list > "$RUNNER_TEMP/orchestrator-tests.list"
diff --git a/crates/pantograph-workflow-service/src/graph/connection_intent.rs b/crates/pantograph-workflow-service/src/graph/connection_intent.rs
index 1b87a6c2a..379f8e3fe 100644
--- a/crates/pantograph-workflow-service/src/graph/connection_intent.rs
+++ b/crates/pantograph-workflow-service/src/graph/connection_intent.rs
@@ -35,7 +35,7 @@ struct ResolvedInputAnchor<'a> {
     port: PortDefinition,
 }
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Default)]
 pub struct InferenceConnectionSurfaceView<'a> {
     surfaces: &'a [InferenceConnectionSurface],
 }
@@ -53,12 +53,6 @@ impl<'a> InferenceConnectionSurfaceView<'a> {
     }
 }
 
-impl Default for InferenceConnectionSurfaceView<'_> {
-    fn default() -> Self {
-        Self { surfaces: &[] }
-    }
-}
-
 fn is_static_llm_connection_input(port_id: &str) -> bool {
     matches!(
         port_id,
@@ -984,3 +978,15 @@ mod tests {
         }
     }
 }
+
+#[cfg(test)]
+mod lint_style_regressions {
+    use super::InferenceConnectionSurfaceView;
+
+    #[test]
+    fn lint_style_default_connection_surface_has_no_current_node() {
+        assert!(InferenceConnectionSurfaceView::default()
+            .current_surface_for("node-a")
+            .is_none());
+    }
+}
diff --git a/crates/pantograph-workflow-service/src/graph/inference_interface_request.rs b/crates/pantograph-workflow-service/src/graph/inference_interface_request.rs
index 527db7027..40fe96ce1 100644
--- a/crates/pantograph-workflow-service/src/graph/inference_interface_request.rs
+++ b/crates/pantograph-workflow-service/src/graph/inference_interface_request.rs
@@ -282,9 +282,7 @@ fn parse_model_ref(
     value: Option<&Value>,
     diagnostics: &mut Vec,
 ) -> Option {
-    let Some(value) = value else {
-        return None;
-    };
+    let value = value?;
 
     match serde_json::from_value::(value.clone()) {
         Ok(model_ref) => match model_ref.validate() {
@@ -895,3 +893,22 @@ mod tests {
         })
     }
 }
+
+#[cfg(test)]
+mod lint_style_regressions {
+    use super::{parse_model_ref, InferenceInterfaceGraphResolutionDiagnosticCode};
+
+    #[test]
+    fn lint_style_absent_model_ref_does_not_emit_invalid_value_diagnostic() {
+        let mut diagnostics = Vec::new();
+        assert!(parse_model_ref("node-a", None, &mut diagnostics).is_none());
+        assert!(diagnostics.is_empty());
+        let invalid = serde_json::json!(7);
+        assert!(parse_model_ref("node-a", Some(&invalid), &mut diagnostics).is_none());
+        assert_eq!(diagnostics.len(), 1);
+        assert_eq!(
+            diagnostics[0].code,
+            InferenceInterfaceGraphResolutionDiagnosticCode::InvalidPumasModelRef
+        );
+    }
+}
diff --git a/crates/pantograph-workflow-service/src/graph/inference_validation_task_owner.rs b/crates/pantograph-workflow-service/src/graph/inference_validation_task_owner.rs
index 0fd4534c9..4917824dd 100644
--- a/crates/pantograph-workflow-service/src/graph/inference_validation_task_owner.rs
+++ b/crates/pantograph-workflow-service/src/graph/inference_validation_task_owner.rs
@@ -89,7 +89,7 @@ impl WorkflowGraphValidationTaskOwner {
                     let mut state = session_handle.lock().await;
                     state.touch();
                     state.canonicalize_graph();
-                    WorkflowGraphRevision::parse(&state.graph.compute_fingerprint())
+                    WorkflowGraphRevision::parse(state.graph.compute_fingerprint())
                         .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))
                 },
             )
@@ -183,12 +183,8 @@ impl WorkflowGraphValidationTaskOwner {
             let finished_keys = state
                 .active
                 .iter()
-                .filter_map(|(graph_session_id, record)| {
-                    record
-                        .handle
-                        .is_finished()
-                        .then(|| graph_session_id.clone())
-                })
+                .filter(|(_, record)| record.handle.is_finished())
+                .map(|(graph_session_id, _)| graph_session_id.clone())
                 .collect::>();
             finished_keys
                 .into_iter()
diff --git a/crates/pantograph-workflow-service/src/graph/session_inference_validation_api.rs b/crates/pantograph-workflow-service/src/graph/session_inference_validation_api.rs
index aa31ecfd7..2007e700a 100644
--- a/crates/pantograph-workflow-service/src/graph/session_inference_validation_api.rs
+++ b/crates/pantograph-workflow-service/src/graph/session_inference_validation_api.rs
@@ -48,7 +48,7 @@ impl GraphSessionStore {
         state.touch();
         state.canonicalize_graph();
         let current_graph_revision =
-            WorkflowGraphRevision::parse(&state.graph.compute_fingerprint())
+            WorkflowGraphRevision::parse(state.graph.compute_fingerprint())
                 .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?;
         drop(state);
 
@@ -73,7 +73,7 @@ impl GraphSessionStore {
         state.touch();
         state.canonicalize_graph();
         let current_graph_revision =
-            WorkflowGraphRevision::parse(&state.graph.compute_fingerprint())
+            WorkflowGraphRevision::parse(state.graph.compute_fingerprint())
                 .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?;
         drop(state);
 
@@ -98,7 +98,7 @@ impl GraphSessionStore {
         state.touch();
         state.canonicalize_graph();
         let graph = state.graph.clone();
-        let current_graph_revision = WorkflowGraphRevision::parse(&graph.compute_fingerprint())
+        let current_graph_revision = WorkflowGraphRevision::parse(graph.compute_fingerprint())
             .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?;
         drop(state);
 
@@ -178,7 +178,7 @@ impl GraphSessionStore {
         let mut state = handle.lock().await;
         state.touch();
         state.canonicalize_graph();
-        let graph_revision = WorkflowGraphRevision::parse(&state.graph.compute_fingerprint())
+        let graph_revision = WorkflowGraphRevision::parse(state.graph.compute_fingerprint())
             .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?;
         drop(state);
 
@@ -223,7 +223,7 @@ impl GraphSessionStore {
             state.touch();
             state.canonicalize_graph();
             let current_graph_revision =
-                WorkflowGraphRevision::parse(&state.graph.compute_fingerprint())
+                WorkflowGraphRevision::parse(state.graph.compute_fingerprint())
                     .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?;
             if current_graph_revision != request.graph_revision {
                 return Err(WorkflowServiceError::InvalidRequest(
@@ -307,7 +307,7 @@ impl GraphSessionStore {
         state.touch();
         state.canonicalize_graph();
         let graph = state.graph.clone();
-        let current_graph_revision = WorkflowGraphRevision::parse(&graph.compute_fingerprint())
+        let current_graph_revision = WorkflowGraphRevision::parse(graph.compute_fingerprint())
             .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?;
         drop(state);
 
@@ -373,7 +373,7 @@ impl GraphSessionStore {
         state.touch();
         state.canonicalize_graph();
         let current_graph_revision =
-            WorkflowGraphRevision::parse(&state.graph.compute_fingerprint())
+            WorkflowGraphRevision::parse(state.graph.compute_fingerprint())
                 .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?;
         if current_graph_revision != validation_session.graph_revision {
             return Err(WorkflowServiceError::InvalidRequest(
@@ -401,7 +401,7 @@ impl GraphSessionStore {
         state.touch();
         state.canonicalize_graph();
         let graph = state.graph.clone();
-        let graph_revision = WorkflowGraphRevision::parse(&graph.compute_fingerprint())
+        let graph_revision = WorkflowGraphRevision::parse(graph.compute_fingerprint())
             .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?;
         drop(state);
 
@@ -444,7 +444,7 @@ impl GraphSessionStore {
         let mut state = handle.lock().await;
         state.touch();
         state.canonicalize_graph();
-        WorkflowGraphRevision::parse(&state.graph.compute_fingerprint())
+        WorkflowGraphRevision::parse(state.graph.compute_fingerprint())
             .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))
     }
 
@@ -461,7 +461,7 @@ impl GraphSessionStore {
         state.touch();
         state.canonicalize_graph();
         let graph = state.graph.clone();
-        let graph_revision = WorkflowGraphRevision::parse(&graph.compute_fingerprint())
+        let graph_revision = WorkflowGraphRevision::parse(graph.compute_fingerprint())
             .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?;
         drop(state);
 
diff --git a/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs b/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
index 2f7835d9b..47365ce4b 100644
--- a/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
+++ b/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
@@ -636,13 +636,13 @@ impl WorkflowSchedulerTaskOrchestrator {
             );
         }
         let handoff = dispatch_selected_handoff_from_selection(selection)?;
-        let dispatch_decision = handoff.dispatch_decision.as_ref().ok_or_else(|| {
+        let dispatch_decision = handoff.dispatch_decision.as_ref().ok_or(
             WorkflowSchedulerTaskOrchestratorError::SchedulerContract(
                 SchedulerContractError::MissingField {
                     field: "dispatch_decision",
                 },
-            )
-        })?;
+            ),
+        )?;
         let reservation_lease_id = dispatch_decision.reservation_lease_id.clone();
         let candidate_id = selected_candidate_id(&selection_request, dispatch_decision);
         Ok(SelectedRuntimeTaskDispatch {
diff --git a/crates/pantograph-workflow-service/src/workflow/attribution_api.rs b/crates/pantograph-workflow-service/src/workflow/attribution_api.rs
index 26c9f626e..175aeaf57 100644
--- a/crates/pantograph-workflow-service/src/workflow/attribution_api.rs
+++ b/crates/pantograph-workflow-service/src/workflow/attribution_api.rs
@@ -216,7 +216,7 @@ impl WorkflowService {
             ));
         }
         validate_workflow_id(&request.workflow_id)?;
-        let graph_revision = WorkflowGraphRevision::parse(&request.graph.compute_fingerprint())
+        let graph_revision = WorkflowGraphRevision::parse(request.graph.compute_fingerprint())
             .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?;
         if graph_revision
             != request
@@ -258,7 +258,7 @@ impl WorkflowService {
                 request.validation_session_id.clone(),
             )
             .await?;
-        let graph_revision = WorkflowGraphRevision::parse(&graph.compute_fingerprint())
+        let graph_revision = WorkflowGraphRevision::parse(graph.compute_fingerprint())
             .map_err(|error| WorkflowServiceError::InvalidRequest(error.to_string()))?;
         if graph_revision != source.graph_revision {
             return Err(WorkflowServiceError::InvalidRequest(
diff --git a/crates/pantograph-workflow-service/src/workflow/contracts.rs b/crates/pantograph-workflow-service/src/workflow/contracts.rs
index 68872fb5b..eb4377d45 100644
--- a/crates/pantograph-workflow-service/src/workflow/contracts.rs
+++ b/crates/pantograph-workflow-service/src/workflow/contracts.rs
@@ -95,7 +95,7 @@ pub struct WorkflowCapabilityModel {
 }
 
 /// Host capability payload consumed by the service.
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
 #[serde(rename_all = "snake_case")]
 pub struct WorkflowRuntimeRequirements {
     #[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -105,17 +105,6 @@ pub struct WorkflowRuntimeRequirements {
     pub required_extensions: Vec,
 }
 
-impl Default for WorkflowRuntimeRequirements {
-    fn default() -> Self {
-        Self {
-            resource_estimates: Vec::new(),
-            required_models: Vec::new(),
-            required_backends: Vec::new(),
-            required_extensions: Vec::new(),
-        }
-    }
-}
-
 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
 pub enum WorkflowRuntimeInstallState {
@@ -1432,3 +1421,20 @@ impl Default for WorkflowRunHandle {
         Self::new()
     }
 }
+
+#[cfg(test)]
+mod lint_style_regressions {
+    use super::WorkflowRuntimeRequirements;
+
+    #[test]
+    fn lint_style_runtime_requirements_default_preserves_empty_wire_fields() {
+        let value = WorkflowRuntimeRequirements::default();
+        assert!(value.resource_estimates.is_empty());
+        assert_eq!(
+            serde_json::to_value(value).unwrap(),
+            serde_json::json!({
+                "required_models": [], "required_backends": [], "required_extensions": []
+            })
+        );
+    }
+}
diff --git a/crates/pantograph-workflow-service/src/workflow/diagnostics_api.rs b/crates/pantograph-workflow-service/src/workflow/diagnostics_api.rs
index 09230eef4..da37b6b57 100644
--- a/crates/pantograph-workflow-service/src/workflow/diagnostics_api.rs
+++ b/crates/pantograph-workflow-service/src/workflow/diagnostics_api.rs
@@ -654,8 +654,7 @@ impl WorkflowService {
                             resource_observation: None,
                         }),
                     },
-                )
-                .map_err(WorkflowServiceError::from)?;
+                )?;
                 repaired = increment_startup_repair_count(repaired)?;
             }
         }
diff --git a/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs b/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs
index 1dc4b8b5b..d873efe0a 100644
--- a/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs
+++ b/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs
@@ -51,7 +51,6 @@ const SNAPSHOT_ID_PREFIX: &str = "wfvalsnap_";
 pub struct WorkflowExecutableValidationSnapshotId(String);
 
 impl WorkflowExecutableValidationSnapshotId {
-    #[must_use]
     pub fn generate() -> Self {
         Self(format!("{SNAPSHOT_ID_PREFIX}{}", Uuid::new_v4()))
     }
diff --git a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
index e47fcaa84..5e0216ef5 100644
--- a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
+++ b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
@@ -1032,7 +1032,6 @@ impl WorkflowService {
             },
         )
         .map(|_| ())
-        .map_err(WorkflowServiceError::from)
     }
 
     fn record_library_model_access_events_if_configured(
@@ -1085,8 +1084,7 @@ impl WorkflowService {
                         },
                     ),
                 },
-            )
-            .map_err(WorkflowServiceError::from)?;
+            )?;
         }
         Ok(())
     }
@@ -1157,7 +1155,6 @@ impl WorkflowService {
             },
         )
         .map(|_| ())
-        .map_err(WorkflowServiceError::from)
     }
 
     fn record_scheduler_queue_placement_event_if_configured(
@@ -1217,7 +1214,6 @@ impl WorkflowService {
             },
         )
         .map(|_| ())
-        .map_err(WorkflowServiceError::from)
     }
 
     pub(super) fn record_run_started_event_if_configured(
@@ -1277,7 +1273,6 @@ impl WorkflowService {
             },
         )
         .map(|_| ())
-        .map_err(WorkflowServiceError::from)
     }
 
     pub(super) fn record_workflow_io_artifact_events_if_configured(
@@ -1391,8 +1386,7 @@ impl WorkflowService {
                         }),
                     ),
                 },
-            )
-            .map_err(WorkflowServiceError::from)?;
+            )?;
         }
         Ok(())
     }
@@ -1481,7 +1475,6 @@ impl WorkflowService {
             },
         )
         .map(|_| ())
-        .map_err(WorkflowServiceError::from)
     }
 }
 
diff --git a/crates/pantograph-workflow-service/src/workflow/session_queue_api.rs b/crates/pantograph-workflow-service/src/workflow/session_queue_api.rs
index 6e9892461..596a4501c 100644
--- a/crates/pantograph-workflow-service/src/workflow/session_queue_api.rs
+++ b/crates/pantograph-workflow-service/src/workflow/session_queue_api.rs
@@ -726,7 +726,6 @@ impl WorkflowService {
             },
         )
         .map(|_| ())
-        .map_err(WorkflowServiceError::from)
     }
 
     fn record_scheduler_estimate_update_for_queue_item_if_configured(
diff --git a/crates/pantograph-workflow-service/src/workflow/session_runtime.rs b/crates/pantograph-workflow-service/src/workflow/session_runtime.rs
index e99db3310..0e5aba03c 100644
--- a/crates/pantograph-workflow-service/src/workflow/session_runtime.rs
+++ b/crates/pantograph-workflow-service/src/workflow/session_runtime.rs
@@ -348,8 +348,7 @@ impl WorkflowService {
                         },
                     ),
                 },
-            )
-            .map_err(WorkflowServiceError::from)?;
+            )?;
         }
         Ok(())
     }
diff --git a/crates/pantograph-workflow-service/src/workflow/task_graph.rs b/crates/pantograph-workflow-service/src/workflow/task_graph.rs
index 46411da8e..23c586a8c 100644
--- a/crates/pantograph-workflow-service/src/workflow/task_graph.rs
+++ b/crates/pantograph-workflow-service/src/workflow/task_graph.rs
@@ -254,10 +254,8 @@ fn dependency_task_ids(bindings: &[WorkflowSchedulerTaskInputBinding]) -> Vec>();
+        let ids = dependency_task_ids(&bindings);
+        assert_eq!(
+            ids.iter().map(|id| id.as_str()).collect::>(),
+            vec!["z", "a", "b"]
+        );
+        assert!(dependency_task_ids(&[]).is_empty());
+    }
+}
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-lint-basics.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-lint-basics.md
new file mode 100644
index 000000000..d0cabf73b
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-lint-basics.md
@@ -0,0 +1,7 @@
+# Workflow behavior-preserving lint basics
+
+Prepare only the 30 mechanical findings evidenced by PR #36: 13 unnecessary fingerprint borrows, 10 identity WorkflowServiceError conversions, two equivalent empty defaults, two ordered filter/map rewrites, one absent-Option early return, one eager literal error construction, and one redundant method must_use annotation where the type retains it. Preserve every signature, error value, branch order, iterator order and default wire field. Other conversions, payload layouts, enum naming, type aliases and seven high-arity methods stay outside this slice.
+
+Four focused regressions cover empty connection surfaces, exact default runtime requirements JSON, absent versus invalid model references and first-seen dependency deduplication order. Explicit nonzero discovery runs those tests; inherited full service/Headless tests retain cancellation, session recovery and dispatch behavior. Finished-task filtering keeps the same state lock, one is_finished check per entry and identical removal sequence.
+
+Root approved the bounded preparation. Root source review accepted the entire fifteen-file frozen tree 61cb018d85a9c49b0da80bd9175fc1bd919fd764. Fresh PR #37 Clippy job 111216602528 confirms all 30 findings remain at the same locations/categories; the aggregate fell from 134 to 47 findings after the separate private error change. Hosted execution of this mechanical slice remains pending. No local Rust execution is claimed.

From b4637a4800ad80bc983137cec0a6e98d7d0e24f3 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 07:13:16 -0700
Subject: [PATCH 42/65] refactor(workflow): box private request and completion
 payloads

---
 .github/workflows/quality-gates.yml           |  7 +++
 .../src/graph/inference_validation_state.rs   | 43 ++++++++++++++++++-
 .../src/workflow/task_execution_worker.rs     | 27 +++++++++---
 ...6-10-03-workflow-private-payload-layout.md |  7 +++
 4 files changed, 75 insertions(+), 9 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-private-payload-layout.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index badccc11a..6cdec4212 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -251,6 +251,13 @@ jobs:
       - name: Run workflow-nodes tests
         run: cargo test -p workflow-nodes --lib
 
+      - name: Run private workflow owner regression suites
+        run: |
+          for module in graph::inference_validation_state::tests:: workflow::task_execution_worker::tests::; do
+            cargo test -p pantograph-workflow-service --lib "$module" -- --list > "$RUNNER_TEMP/private-owner-tests.list"
+            python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith(sys.argv[2]) and line.endswith(": test")]; print(sys.argv[2], "tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/private-owner-tests.list" "$module"
+            cargo test -p pantograph-workflow-service --lib "$module"
+          done
       - name: Run workflow style contract regressions
         run: |
           cargo test -p pantograph-workflow-service --lib lint_style_ -- --list > "$RUNNER_TEMP/workflow-style-tests.list"
diff --git a/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs b/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs
index c0759411e..061f55ae0 100644
--- a/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs
+++ b/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs
@@ -1158,7 +1158,7 @@ pub(crate) enum DependencyEnvironmentActionIntentStateResolution {
     Blocked(DependencyEnvironmentActionIntentResult),
     RequestReady {
         intent: DependencyEnvironmentActionIntent,
-        environment_request: ValidatedDependencyEnvironmentRequest,
+        environment_request: Box,
     },
 }
 
@@ -1846,7 +1846,7 @@ fn request_ready_dependency_environment_action_resolution(
 ) -> DependencyEnvironmentActionIntentStateResolution {
     DependencyEnvironmentActionIntentStateResolution::RequestReady {
         intent,
-        environment_request,
+        environment_request: Box::new(environment_request),
     }
 }
 
@@ -1877,6 +1877,15 @@ mod tests {
 
     use super::*;
 
+    #[test]
+    fn action_resolution_keeps_validated_environment_request_indirect() {
+        assert!(std::mem::size_of::() <= 384);
+        assert!(
+            std::mem::size_of::()
+                < std::mem::size_of::()
+        );
+    }
+
     #[tokio::test]
     async fn action_intent_state_rejects_stale_graph_revision() {
         let store = CurrentInferenceValidationStateStore::new();
@@ -2284,6 +2293,36 @@ mod tests {
             DependencyEnvironmentActionIntentStatus::RequestReady
         );
         assert!(result.diagnostics.is_empty());
+
+        let resolution = store
+            .resolve_dependency_environment_action_request(state_request_with_validation_session(
+                "graph-session-1",
+                "aaaaaaaaaaaaaaaa",
+                "aaaaaaaaaaaaaaaa",
+                "validation.session.1",
+                "dependency-node-1",
+                true,
+            ))
+            .await;
+        let DependencyEnvironmentActionIntentStateResolution::RequestReady {
+            intent,
+            environment_request,
+        } = resolution
+        else {
+            panic!("current proof must retain a ready validated request");
+        };
+        let raw = environment_request.as_request();
+        assert_eq!(raw.action, intent.action);
+        assert_eq!(raw.planning_request.task_id.as_str(), "image_generation");
+        assert_eq!(
+            raw.identity_key,
+            DependencyPlanningIdentityKey::from_planning_request(&raw.planning_request).unwrap()
+        );
+        raw.validate()
+            .expect("retained environment request validates");
+        let encoded = serde_json::to_value(raw).unwrap();
+        let decoded = ValidatedDependencyEnvironmentRequest::try_from(encoded.clone()).unwrap();
+        assert_eq!(serde_json::to_value(decoded.as_request()).unwrap(), encoded);
     }
 
     #[tokio::test]
diff --git a/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs b/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs
index 5a15d2c97..e632a73db 100644
--- a/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs
+++ b/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs
@@ -368,7 +368,7 @@ pub(super) enum WorkflowTaskExecutionWorkerShutdownReason {
 pub(super) enum WorkflowTaskExecutionWorkerOutcome {
     TaskTerminal(WorkflowTaskExecutionWorkerTerminalOutcome),
     TaskDeferred(WorkflowTaskExecutionWorkerDeferredOutcome),
-    RuntimeBranchCompleted(WorkflowTaskExecutionWorkerRuntimeBranchCompletedOutcome),
+    RuntimeBranchCompleted(Box),
     RuntimeBranchFailed(WorkflowTaskExecutionWorkerRuntimeBranchFailedOutcome),
     RuntimeBranchCancelled(WorkflowTaskExecutionWorkerRuntimeBranchCancelledOutcome),
     RuntimeBranchDeferred(WorkflowTaskExecutionWorkerRuntimeBranchDeferredOutcome),
@@ -1124,12 +1124,12 @@ impl WorkflowTaskExecutionWorkerOutcome {
         response: WorkflowRunResponse,
         diagnostics: Vec,
     ) -> Self {
-        Self::RuntimeBranchCompleted(WorkflowTaskExecutionWorkerRuntimeBranchCompletedOutcome {
+        Self::RuntimeBranchCompleted(Box::new(WorkflowTaskExecutionWorkerRuntimeBranchCompletedOutcome {
             session_id: command.session_id.clone(),
             workflow_run_id: command.workflow_run_id.clone(),
             response,
             diagnostics,
-        })
+        }))
     }
 
     pub(super) fn runtime_branch_failed(
@@ -1370,12 +1370,12 @@ async fn claim_and_execute_runtime_branch_event(
         .await
         {
             Ok(response) => WorkflowTaskExecutionWorkerOutcome::RuntimeBranchCompleted(
-                WorkflowTaskExecutionWorkerRuntimeBranchCompletedOutcome {
+                Box::new(WorkflowTaskExecutionWorkerRuntimeBranchCompletedOutcome {
                     session_id: command.session_id.clone(),
                     workflow_run_id: command.workflow_run_id.clone(),
                     response,
                     diagnostics: Vec::new(),
-                },
+                }),
             ),
             Err(error) => WorkflowTaskExecutionWorkerOutcome::runtime_branch_failed(
                 command,
@@ -1886,12 +1886,12 @@ fn runtime_branch_batch_member_completion(
                 unix_timestamp_ms(), proof) {
                 Ok(_record) => match member_outcome.completed_response {
                     Some(response) => WorkflowTaskExecutionWorkerOutcome::RuntimeBranchCompleted(
-                        WorkflowTaskExecutionWorkerRuntimeBranchCompletedOutcome {
+                        Box::new(WorkflowTaskExecutionWorkerRuntimeBranchCompletedOutcome {
                             session_id: member_outcome.session_id.clone(),
                             workflow_run_id: member_outcome.workflow_run_id.clone(),
                             response,
                             diagnostics,
-                        },
+                        }),
                     ),
                     None => WorkflowTaskExecutionWorkerOutcome::RuntimeBranchFailed(
                         WorkflowTaskExecutionWorkerRuntimeBranchFailedOutcome {
@@ -4353,6 +4353,11 @@ mod tests {
         assert_eq!(outcome.diagnostics, vec![diagnostic]);
     }
 
+    #[test]
+    fn worker_outcome_keeps_completed_response_indirect() {
+        assert!(std::mem::size_of::() <= 128);
+    }
+
     #[test]
     fn runtime_branch_completed_outcome_preserves_run_scope_and_response() {
         let command = runtime_branch_command();
@@ -4379,6 +4384,14 @@ mod tests {
         assert_eq!(outcome.session_id, command.session_id);
         assert_eq!(outcome.workflow_run_id, command.workflow_run_id);
         assert_eq!(outcome.response, response);
+        assert_eq!(
+            serde_json::to_value(&outcome.response).unwrap(),
+            serde_json::json!({
+                "workflow_run_id": command.workflow_run_id,
+                "outputs": [],
+                "timing_ms": 42
+            })
+        );
         assert_eq!(outcome.diagnostics, vec![diagnostic]);
     }
 
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-private-payload-layout.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-private-payload-layout.md
new file mode 100644
index 000000000..bc0ce099b
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-private-payload-layout.md
@@ -0,0 +1,7 @@
+# Private validation request and worker completion payloads
+
+Fresh PR #37 Clippy retains an 896-byte private RequestReady branch and six large-error reports caused by a 144-byte private RuntimeBranchCompleted outcome. Box only RequestReady.environment_request and the completed worker outcome. The constructor inventory is one validated-request helper and three completed-outcome constructors. The graph session still borrows the validated request for the dependency service; worker response extraction and all Result signatures remain unchanged. Public contracts and the public Ready inference projection are excluded.
+
+The real ready-proof regression now checks the retained request task, canonical identity, validation and full JSON round trip. Existing blocked/missing/stale proof tests remain. The worker success test retains complete response/scope/diagnostic equality and adds exact response JSON; existing cancellation, unavailable and shutdown behaviors remain. Two private layout bounds provide source-layout evidence, not measured performance claims. CI explicitly discovers and runs both complete owner test modules with nonzero guards.
+
+Root approved this bounded design. Whitespace and focused test syntax formatting are checked locally; root source review accepted all four files at frozen tree c2a6e2439f8df3a7e4d00461cf36175fbbe19ff4; hosted execution remains pending. No local Rust execution is claimed.

From 09ae3182b3efa0e7921763122b25d0f0fc2ada11 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 07:22:35 -0700
Subject: [PATCH 43/65] refactor(workflow): box ready inference projection
 payload

---
 .github/workflows/quality-gates.yml           |  7 +++
 CHANGELOG.md                                  |  4 ++
 .../src/graph/inference_validation_state.rs   |  4 +-
 .../executable_validation_snapshot.rs         |  4 +-
 .../src/workflow/task_graph.rs                |  2 +-
 .../workflow/tests/task_binding_resolution.rs |  4 +-
 .../src/workflow/tests/task_graph.rs          | 50 ++++++++++++++++++-
 ...10-03-ready-inference-projection-layout.md |  7 +++
 8 files changed, 73 insertions(+), 9 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ready-inference-projection-layout.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 6cdec4212..a28eea8d2 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -251,6 +251,13 @@ jobs:
       - name: Run workflow-nodes tests
         run: cargo test -p workflow-nodes --lib
 
+      - name: Run workflow inference projection regression suites
+        run: |
+          for module in workflow::tests::task_graph:: workflow::tests::task_binding_resolution:: workflow::executable_validation_snapshot::tests::; do
+            cargo test -p pantograph-workflow-service --lib "$module" -- --list > "$RUNNER_TEMP/projection-tests.list"
+            python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith(sys.argv[2]) and line.endswith(": test")]; print(sys.argv[2], "tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/projection-tests.list" "$module"
+            cargo test -p pantograph-workflow-service --lib "$module"
+          done
       - name: Run private workflow owner regression suites
         run: |
           for module in graph::inference_validation_state::tests:: workflow::task_execution_worker::tests::; do
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c8136067..ebc94a76a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -59,3 +59,7 @@ The seven validated runtime-host contract wrappers now implement standard `AsRef
 ### Diagnostic event artifact payload construction
 
 `DiagnosticEventPayload::IoArtifactObserved` now stores `Box`. Rust callers constructing the public variant use `Box::new`, and owned pattern bindings contain a box. Raw DTO fields, validation and tagged JSON stay unchanged; this adds one allocation for this variant without a measured performance claim. The crate remains `publish = false`.
+
+### Workflow ready inference projection construction
+
+`WorkflowSchedulerInferenceTaskProjection::Ready` now holds `Box`. Rust constructors use `Box::new`, and owned pattern bindings contain a box; the raw record, equality, borrowed readers and downstream scheduler intent serialization remain unchanged. The projection enum itself has no Serde implementation. This adds one allocation for ready projections without a measured performance claim; the crate remains `publish = false`.
diff --git a/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs b/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs
index 061f55ae0..000e8c783 100644
--- a/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs
+++ b/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs
@@ -1468,7 +1468,7 @@ impl CurrentInferenceValidationNodeRecord {
                     },
                 )?;
             return Ok(WorkflowSchedulerInferenceTaskProjection::Ready(
-                WorkflowSchedulerReadyInferenceTaskProjection {
+                Box::new(WorkflowSchedulerReadyInferenceTaskProjection {
                     node_id: pantograph_scheduler::SchedulerNodeId::parse(self.node_id.as_str())
                         .map_err(|error| {
                             CurrentInferenceSchedulerProjectionError::IncompleteNodeState {
@@ -1532,7 +1532,7 @@ impl CurrentInferenceValidationNodeRecord {
                             .dependency_override_fingerprint
                             .clone(),
                     },
-                },
+                }),
             ));
         }
 
diff --git a/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs b/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs
index d873efe0a..7c35fa013 100644
--- a/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs
+++ b/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs
@@ -492,7 +492,7 @@ impl WorkflowExecutableValidationSnapshotNode {
         })?;
 
         Ok(WorkflowSchedulerInferenceTaskProjection::Ready(
-            WorkflowSchedulerReadyInferenceTaskProjection {
+            Box::new(WorkflowSchedulerReadyInferenceTaskProjection {
                 node_id: scheduler_node_id,
                 descriptor_fingerprint: self.descriptor_fingerprint.clone(),
                 task_type,
@@ -504,7 +504,7 @@ impl WorkflowExecutableValidationSnapshotNode {
                 dependency_readiness_source: workflow_scheduler_dependency_readiness_source(
                     snapshot, self,
                 )?,
-            },
+            }),
         ))
     }
 }
diff --git a/crates/pantograph-workflow-service/src/workflow/task_graph.rs b/crates/pantograph-workflow-service/src/workflow/task_graph.rs
index 23c586a8c..89b9e71f5 100644
--- a/crates/pantograph-workflow-service/src/workflow/task_graph.rs
+++ b/crates/pantograph-workflow-service/src/workflow/task_graph.rs
@@ -64,7 +64,7 @@ impl WorkflowSchedulerInferenceTaskProjections {
 
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum WorkflowSchedulerInferenceTaskProjection {
-    Ready(WorkflowSchedulerReadyInferenceTaskProjection),
+    Ready(Box),
     Blocked(WorkflowSchedulerBlockedInferenceTaskProjection),
 }
 
diff --git a/crates/pantograph-workflow-service/src/workflow/tests/task_binding_resolution.rs b/crates/pantograph-workflow-service/src/workflow/tests/task_binding_resolution.rs
index ea7fa28d8..186d08109 100644
--- a/crates/pantograph-workflow-service/src/workflow/tests/task_binding_resolution.rs
+++ b/crates/pantograph-workflow-service/src/workflow/tests/task_binding_resolution.rs
@@ -34,7 +34,7 @@ fn workflow_run_id() -> WorkflowRunId {
 fn inference_projection() -> WorkflowSchedulerInferenceTaskProjections {
     WorkflowSchedulerInferenceTaskProjections::from_records(vec![
         WorkflowSchedulerInferenceTaskProjection::Ready(
-            WorkflowSchedulerReadyInferenceTaskProjection {
+            Box::new(WorkflowSchedulerReadyInferenceTaskProjection {
                 node_id: SchedulerNodeId::parse("infer").expect("node id"),
                 descriptor_fingerprint: InferenceInterfaceFingerprint::parse("iface.binding.v1")
                     .expect("fingerprint"),
@@ -50,7 +50,7 @@ fn inference_projection() -> WorkflowSchedulerInferenceTaskProjections {
                 trait_settings: Vec::new(),
                 estimate_hints: resource_estimate_hints(),
                 dependency_readiness_source: dependency_readiness_source("iface.binding.v1"),
-            },
+            }),
         ),
     ])
     .expect("projection")
diff --git a/crates/pantograph-workflow-service/src/workflow/tests/task_graph.rs b/crates/pantograph-workflow-service/src/workflow/tests/task_graph.rs
index 9660df775..ec9a18eaa 100644
--- a/crates/pantograph-workflow-service/src/workflow/tests/task_graph.rs
+++ b/crates/pantograph-workflow-service/src/workflow/tests/task_graph.rs
@@ -45,7 +45,7 @@ fn ready_inference_projection(
 ) -> WorkflowSchedulerInferenceTaskProjections {
     WorkflowSchedulerInferenceTaskProjections::from_records(vec![
         WorkflowSchedulerInferenceTaskProjection::Ready(
-            WorkflowSchedulerReadyInferenceTaskProjection {
+            Box::new(WorkflowSchedulerReadyInferenceTaskProjection {
                 node_id: SchedulerNodeId::parse("infer").expect("node id"),
                 descriptor_fingerprint: InferenceInterfaceFingerprint::parse("iface.test.v1")
                     .expect("fingerprint"),
@@ -70,7 +70,7 @@ fn ready_inference_projection(
                 }],
                 estimate_hints,
                 dependency_readiness_source: dependency_readiness_source("iface.test.v1"),
-            },
+            }),
         ),
     ])
     .expect("projection")
@@ -200,6 +200,26 @@ fn graph_with_inline_inference_ref() -> WorkflowGraph {
     }
 }
 
+#[test]
+fn ready_projection_preserves_owned_raw_record_and_compact_layout() {
+    let projections = inference_projection();
+    let projection = projections
+        .get(&SchedulerNodeId::parse("infer").unwrap())
+        .unwrap();
+    let WorkflowSchedulerInferenceTaskProjection::Ready(borrowed) = projection else {
+        panic!("expected ready projection");
+    };
+    let WorkflowSchedulerInferenceTaskProjection::Ready(owned) = projection.clone() else {
+        panic!("expected cloned ready projection");
+    };
+    let raw: WorkflowSchedulerReadyInferenceTaskProjection = *owned;
+    assert_eq!(&raw, borrowed.as_ref());
+    assert!(
+        std::mem::size_of::()
+            < std::mem::size_of::()
+    );
+}
+
 #[test]
 fn scheduler_task_graph_projects_path_free_inference_intent() {
     let graph = workflow_scheduler_task_graph_with_inference_projections(
@@ -295,6 +315,32 @@ fn scheduler_task_graph_projects_path_free_inference_intent() {
         SchedulerTraitValue::String("euler_discrete".to_string())
     );
 
+    let expected_intent = json!({
+        "contract_version": 1,
+        "workflow_id": "workflow-task-graph",
+        "workflow_run_id": "run-task-graph",
+        "node_id": "infer",
+        "task_id": "infer",
+        "task_type": "image_generation",
+        "model_ref": {
+            "model_id": "image/example/tiny-diffusion",
+            "revision": "main",
+            "selected_artifact_id": "diffusers-bundle"
+        },
+        "constraints": { "requested_runtime_id": "pytorch", "requested_device_id": "cuda:0" },
+        "trait_settings": [{ "trait_id": "denoising_scheduler", "value": { "kind": "string", "value": "euler_discrete" } }],
+        "estimate_hints": [
+            { "kind": "peak_ram_bytes", "value": 2_147_483_648_u64 },
+            { "kind": "peak_vram_bytes", "value": 4_294_967_296_u64 }
+        ]
+    });
+    assert_eq!(serde_json::to_value(intent).unwrap(), expected_intent);
+    assert_eq!(
+        serde_json::from_value::(expected_intent)
+            .unwrap(),
+        *intent
+    );
+
     let encoded = serde_json::to_string(&graph).expect("encode task graph");
     assert!(!encoded.contains("model_path"));
     assert!(!encoded.contains("/tmp/legacy-model"));
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ready-inference-projection-layout.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ready-inference-projection-layout.md
new file mode 100644
index 000000000..7e0d24373
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-ready-inference-projection-layout.md
@@ -0,0 +1,7 @@
+# Public ready inference projection payload
+
+Fresh PR #38 Clippy retains the public Ready projection at 528 bytes versus the Blocked record at 80. Box only Ready and migrate four existing constructors: current validation state, executable snapshot projection and two workflow fixture helpers. Raw fields, equality, borrowed readers, freshness/admission checks and blocked behavior remain unchanged. The enum has no Serde implementation; the downstream scheduler intent wire contract is the serialization boundary to preserve.
+
+Tests cover owned raw-record equality and layout, plus exact downstream intent JSON and deserialization while retaining existing path-free projection assertions. CI explicitly discovers and runs task-graph, task-binding and executable-snapshot modules; inherited validation-owner and Headless tests retain ready/stale/missing-estimate/blocked behavior and the embedded-runtime consumer.
+
+This is a public Rust construction/owned-binding change despite publish=false, documented with the extra ready-record allocation and no measured performance claim. Root approved the bounded design. Root source review accepted all eight files at frozen tree 42349ac0a39dd876e5fca06ca4b285f3cc6eaca8. Hosted execution remains pending; no local Rust execution is claimed.

From 0c085394d1f6864cc712b1e934c1c22d41d57f1a Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 07:37:35 -0700
Subject: [PATCH 44/65] refactor(workflow): group terminal diagnostic inputs

---
 .github/workflows/quality-gates.yml           |   5 +
 .../runtime_branch_batch_execution.rs         |  27 ++--
 .../runtime_branch_run_finalization.rs        | 148 ++++++++++++++----
 .../src/workflow/session_scheduler_runner.rs  | 107 +++++++------
 .../2026-10-03-terminal-diagnostic-input.md   |   9 ++
 5 files changed, 198 insertions(+), 98 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-terminal-diagnostic-input.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index a28eea8d2..93fcf95c6 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -251,6 +251,11 @@ jobs:
       - name: Run workflow-nodes tests
         run: cargo test -p workflow-nodes --lib
 
+      - name: Run terminal diagnostic regression suite
+        run: |
+          cargo test -p pantograph-workflow-service --lib workflow::runtime_branch_run_finalization::tests:: -- --list > "$RUNNER_TEMP/terminal-diagnostic-tests.list"
+          python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith("workflow::runtime_branch_run_finalization::tests::") and line.endswith(": test")]; print("Terminal diagnostic tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/terminal-diagnostic-tests.list"
+          cargo test -p pantograph-workflow-service --lib workflow::runtime_branch_run_finalization::tests::
       - name: Run workflow inference projection regression suites
         run: |
           for module in workflow::tests::task_graph:: workflow::tests::task_binding_resolution:: workflow::executable_validation_snapshot::tests::; do
diff --git a/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs b/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs
index bb2cf341f..be0d264c8 100644
--- a/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs
+++ b/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs
@@ -24,6 +24,7 @@ use pantograph_scheduler::{
 
 use super::runtime_branch_run_finalization::{
     completed_scheduler_run_response, record_scheduler_task_attempt_terminal,
+    WorkflowSchedulerTaskAttemptTerminalInput,
 };
 use super::runtime_dispatch_assignment::{
     WorkflowRuntimeDispatchAssignmentBatchClaim,
@@ -439,18 +440,20 @@ where
                         })?;
                 record_scheduler_task_attempt_terminal(
                     service,
-                    application.started_batch_member.started().task(),
-                    application
-                        .started_batch_member
-                        .started()
-                        .attempt_id()
-                        .as_str(),
-                    application.started_batch_member.started().started_at_ms(),
-                    transition,
-                    reason,
-                    error_summary,
-                    Some(application.started_batch_member.selected_dispatch()),
-                    Some(terminal_mutation),
+                    WorkflowSchedulerTaskAttemptTerminalInput {
+                        task: application.started_batch_member.started().task(),
+                        attempt_id: application
+                            .started_batch_member
+                            .started()
+                            .attempt_id()
+                            .as_str(),
+                        started_at_ms: application.started_batch_member.started().started_at_ms(),
+                        transition,
+                        reason,
+                        error_summary,
+                        selected_dispatch: Some(application.started_batch_member.selected_dispatch()),
+                        terminal_mutation: Some(terminal_mutation),
+                    },
                 )
                 .map_err(|error| {
                     WorkflowRuntimeBranchBatchExecutionFailure::active_run_member(
diff --git a/crates/pantograph-workflow-service/src/workflow/runtime_branch_run_finalization.rs b/crates/pantograph-workflow-service/src/workflow/runtime_branch_run_finalization.rs
index dfcc18b15..0289ed26b 100644
--- a/crates/pantograph-workflow-service/src/workflow/runtime_branch_run_finalization.rs
+++ b/crates/pantograph-workflow-service/src/workflow/runtime_branch_run_finalization.rs
@@ -37,6 +37,18 @@ pub(super) struct WorkflowSchedulerTaskAttemptDiagnosticAttribution {
     pub(super) bucket_id: Option,
 }
 
+#[derive(Debug)]
+pub(super) struct WorkflowSchedulerTaskAttemptTerminalInput<'a> {
+    pub(super) task: &'a WorkflowSchedulerTask,
+    pub(super) attempt_id: &'a str,
+    pub(super) started_at_ms: u64,
+    pub(super) transition: SchedulerTaskAttemptLifecycleTransition,
+    pub(super) reason: &'a str,
+    pub(super) error_summary: Option,
+    pub(super) selected_dispatch: Option<&'a SelectedRuntimeTaskDispatch>,
+    pub(super) terminal_mutation: Option<&'a WorkflowSchedulerTaskTerminalMutation>,
+}
+
 #[derive(Debug)]
 pub(super) struct WorkflowSchedulerTaskAttemptTerminalDiagnosticRequest<'a> {
     pub(super) task: &'a WorkflowSchedulerTask,
@@ -142,14 +154,16 @@ pub(super) async fn finalize_started_runtime_task_dispatch(
                 scheduler_task_attempt_terminal_transition_from_result(&result);
             record_scheduler_task_attempt_terminal(
                 service,
-                started_runtime_task.task(),
-                started_runtime_task.attempt_id().as_str(),
-                started_runtime_task.started_at_ms(),
-                transition,
-                reason,
-                error_summary,
-                Some(selected_dispatch),
-                Some(&terminal_mutation),
+                WorkflowSchedulerTaskAttemptTerminalInput {
+                    task: started_runtime_task.task(),
+                    attempt_id: started_runtime_task.attempt_id().as_str(),
+                    started_at_ms: started_runtime_task.started_at_ms(),
+                    transition,
+                    reason,
+                    error_summary,
+                    selected_dispatch: Some(selected_dispatch),
+                    terminal_mutation: Some(&terminal_mutation),
+                },
             )?;
             service
                 .scheduler_task_orchestrator
@@ -191,14 +205,16 @@ pub(super) async fn finalize_started_runtime_task_dispatch(
                 };
                 record_scheduler_task_attempt_terminal(
                     service,
-                    started_runtime_task.task(),
-                    started_runtime_task.attempt_id().as_str(),
-                    started_runtime_task.started_at_ms(),
-                    SchedulerTaskAttemptLifecycleTransition::Cancelled,
-                    "scheduler runtime task cancellation observed",
-                    Some(message.clone()),
-                    Some(selected_dispatch),
-                    Some(&terminal_mutation),
+                    WorkflowSchedulerTaskAttemptTerminalInput {
+                        task: started_runtime_task.task(),
+                        attempt_id: started_runtime_task.attempt_id().as_str(),
+                        started_at_ms: started_runtime_task.started_at_ms(),
+                        transition: SchedulerTaskAttemptLifecycleTransition::Cancelled,
+                        reason: "scheduler runtime task cancellation observed",
+                        error_summary: Some(message.clone()),
+                        selected_dispatch: Some(selected_dispatch),
+                        terminal_mutation: Some(&terminal_mutation),
+                    },
                 )?;
                 service
                     .scheduler_task_orchestrator
@@ -234,14 +250,16 @@ pub(super) async fn finalize_started_runtime_task_dispatch(
             };
             record_scheduler_task_attempt_terminal(
                 service,
-                started_runtime_task.task(),
-                started_runtime_task.attempt_id().as_str(),
-                started_runtime_task.started_at_ms(),
-                SchedulerTaskAttemptLifecycleTransition::Failed,
-                "scheduler runtime task dispatch failed",
-                Some(error.to_string()),
-                Some(selected_dispatch),
-                Some(&terminal_mutation),
+                WorkflowSchedulerTaskAttemptTerminalInput {
+                    task: started_runtime_task.task(),
+                    attempt_id: started_runtime_task.attempt_id().as_str(),
+                    started_at_ms: started_runtime_task.started_at_ms(),
+                    transition: SchedulerTaskAttemptLifecycleTransition::Failed,
+                    reason: "scheduler runtime task dispatch failed",
+                    error_summary: Some(error.to_string()),
+                    selected_dispatch: Some(selected_dispatch),
+                    terminal_mutation: Some(&terminal_mutation),
+                },
             )?;
             service
                 .scheduler_task_orchestrator
@@ -358,15 +376,18 @@ fn scheduler_task_attempt_terminal_diagnostic_event_at(
 
 pub(super) fn record_scheduler_task_attempt_terminal(
     service: &WorkflowService,
-    task: &WorkflowSchedulerTask,
-    attempt_id: &str,
-    started_at_ms: u64,
-    transition: SchedulerTaskAttemptLifecycleTransition,
-    reason: &str,
-    error_summary: Option,
-    selected_dispatch: Option<&SelectedRuntimeTaskDispatch>,
-    terminal_mutation: Option<&WorkflowSchedulerTaskTerminalMutation>,
+    input: WorkflowSchedulerTaskAttemptTerminalInput<'_>,
 ) -> Result<(), WorkflowServiceError> {
+    let WorkflowSchedulerTaskAttemptTerminalInput {
+        task,
+        attempt_id,
+        started_at_ms,
+        transition,
+        reason,
+        error_summary,
+        selected_dispatch,
+        terminal_mutation,
+    } = input;
     let attribution =
         scheduler_task_attempt_diagnostic_attribution(service, task.workflow_run_id.as_str())?;
     service.workflow_diagnostic_event_record(scheduler_task_attempt_terminal_diagnostic_event(
@@ -541,6 +562,69 @@ mod tests {
 
     use super::*;
 
+    #[test]
+    fn terminal_input_records_success_failure_and_cancellation_scope() {
+        use pantograph_diagnostics_ledger::{DiagnosticsLedgerRepository, SqliteDiagnosticsLedger};
+
+        for transition in [
+            SchedulerTaskAttemptLifecycleTransition::Completed,
+            SchedulerTaskAttemptLifecycleTransition::Failed,
+            SchedulerTaskAttemptLifecycleTransition::Cancelled,
+        ] {
+            let service = WorkflowService::new()
+                .with_diagnostics_ledger(SqliteDiagnosticsLedger::open_in_memory().unwrap());
+            let task = runtime_task();
+            let started_at_ms = unix_timestamp_ms();
+            let error_summary = (transition != SchedulerTaskAttemptLifecycleTransition::Completed)
+                .then(|| "terminal fixture detail".to_string());
+            record_scheduler_task_attempt_terminal(
+                &service,
+                WorkflowSchedulerTaskAttemptTerminalInput {
+                    task: &task,
+                    attempt_id: "attempt.terminal.input",
+                    started_at_ms,
+                    transition,
+                    reason: "terminal input fixture",
+                    error_summary: error_summary.clone(),
+                    selected_dispatch: None,
+                    terminal_mutation: None,
+                },
+            )
+            .expect("terminal input records event");
+            let records = service
+                .diagnostics_ledger_guard()
+                .unwrap()
+                .diagnostic_events_after(0, 10)
+                .unwrap();
+            assert_eq!(records.len(), 1);
+            let record = &records[0];
+            assert_eq!(
+                record.workflow_run_id.as_ref().unwrap().as_str(),
+                task.workflow_run_id.as_str()
+            );
+            assert_eq!(record.node_id.as_deref(), Some(task.node_id.as_str()));
+            assert!(record.client_id.is_none());
+            assert!(record.client_session_id.is_none());
+            assert!(record.bucket_id.is_none());
+            let DiagnosticEventPayload::SchedulerTaskAttemptLifecycleChanged(payload) =
+                serde_json::from_str(&record.payload_json).unwrap()
+            else {
+                panic!("expected task-attempt terminal event");
+            };
+            assert_eq!(payload.scheduler_task_id, task.task_id.as_str());
+            assert_eq!(payload.scheduler_attempt_id, "attempt.terminal.input");
+            assert_eq!(payload.transition, transition);
+            assert_eq!(payload.started_at_ms, Some(started_at_ms as i64));
+            assert_eq!(
+                payload.duration_ms,
+                Some((payload.ended_at_ms.unwrap() as u64).saturating_sub(started_at_ms))
+            );
+            assert_eq!(payload.reason.as_deref(), Some("terminal input fixture"));
+            assert_eq!(payload.error_summary, error_summary);
+            assert!(payload.reservation_id.is_none());
+        }
+    }
+
     #[test]
     fn terminal_diagnostic_event_preserves_scheduler_task_attempt_scope() {
         let task = runtime_task();
diff --git a/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs b/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs
index 74fd40b9a..31456e718 100644
--- a/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs
+++ b/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs
@@ -24,7 +24,7 @@ use crate::scheduler::task_orchestrator::{
 };
 use crate::scheduler::{
     WorkflowDependencyReadinessLifecycle, WorkflowDependencyReadinessLifecycleError,
-    WorkflowSchedulerRetryLifecycle, WorkflowSchedulerTaskTerminalMutation,
+    WorkflowSchedulerRetryLifecycle,
 };
 
 use super::runtime_branch_run_finalization::{
@@ -33,6 +33,7 @@ use super::runtime_branch_run_finalization::{
     WorkflowRuntimeTaskDispatchFinalizationOutcome,
     WorkflowSchedulerTaskAttemptDiagnosticAttribution,
     WorkflowSchedulerTaskAttemptTerminalDiagnosticRequest,
+    WorkflowSchedulerTaskAttemptTerminalInput,
 };
 use super::{
     WorkflowHost, WorkflowOutputTarget, WorkflowPortBinding, WorkflowRunResponse,
@@ -513,17 +514,16 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
                                 "scheduler non-runtime task completion failed: {error}"
                             ))
                         })?;
-                    self.record_scheduler_task_attempt_terminal(
-                        session_id,
-                        started.task(),
-                        started.attempt_id().as_str(),
-                        started.started_at_ms(),
-                        SchedulerTaskAttemptLifecycleTransition::Completed,
-                        "scheduler task attempt completed",
-                        None,
-                        None,
-                        None,
-                    )?;
+                    self.record_scheduler_task_attempt_terminal(WorkflowSchedulerTaskAttemptTerminalInput {
+                        task: started.task(),
+                        attempt_id: started.attempt_id().as_str(),
+                        started_at_ms: started.started_at_ms(),
+                        transition: SchedulerTaskAttemptLifecycleTransition::Completed,
+                        reason: "scheduler task attempt completed",
+                        error_summary: None,
+                        selected_dispatch: None,
+                        terminal_mutation: None,
+                    })?;
                 }
                 Err(
                     crate::scheduler::WorkflowSchedulerTaskOrchestratorError::NonRuntimeTaskAdapter(
@@ -542,17 +542,16 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
                             &error,
                         );
                     if failed.is_ok() {
-                        self.record_scheduler_task_attempt_terminal(
-                            session_id,
-                            started.task(),
-                            started.attempt_id().as_str(),
-                            started.started_at_ms(),
-                            SchedulerTaskAttemptLifecycleTransition::Failed,
-                            "scheduler non-runtime task execution failed",
-                            Some(error.to_string()),
-                            None,
-                            None,
-                        )?;
+                        self.record_scheduler_task_attempt_terminal(WorkflowSchedulerTaskAttemptTerminalInput {
+                            task: started.task(),
+                            attempt_id: started.attempt_id().as_str(),
+                            started_at_ms: started.started_at_ms(),
+                            transition: SchedulerTaskAttemptLifecycleTransition::Failed,
+                            reason: "scheduler non-runtime task execution failed",
+                            error_summary: Some(error.to_string()),
+                            selected_dispatch: None,
+                            terminal_mutation: None,
+                        })?;
                     }
                     return Err(WorkflowServiceError::InvalidRequest(format!(
                         "scheduler non-runtime task execution failed: {error}"
@@ -886,17 +885,16 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
                                     ))
                                 })?
                         };
-                        self.record_scheduler_task_attempt_terminal(
-                            session_id,
-                            started_runtime_task.task(),
-                            started_runtime_task.attempt_id().as_str(),
-                            started_runtime_task.started_at_ms(),
-                            SchedulerTaskAttemptLifecycleTransition::Failed,
-                            "scheduler runtime dispatch selection failed",
-                            Some(scheduler_error.to_string()),
-                            None,
-                            Some(&terminal_mutation),
-                        )?;
+                        self.record_scheduler_task_attempt_terminal(WorkflowSchedulerTaskAttemptTerminalInput {
+                            task: started_runtime_task.task(),
+                            attempt_id: started_runtime_task.attempt_id().as_str(),
+                            started_at_ms: started_runtime_task.started_at_ms(),
+                            transition: SchedulerTaskAttemptLifecycleTransition::Failed,
+                            reason: "scheduler runtime dispatch selection failed",
+                            error_summary: Some(scheduler_error.to_string()),
+                            selected_dispatch: None,
+                            terminal_mutation: Some(&terminal_mutation),
+                        })?;
                     } else {
                         let terminal_mutation = {
                             let mut store = self.service.session_store_guard()?;
@@ -915,17 +913,16 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
                                 ))
                             })?
                         };
-                        self.record_scheduler_task_attempt_terminal(
-                            session_id,
-                            started_runtime_task.task(),
-                            started_runtime_task.attempt_id().as_str(),
-                            started_runtime_task.started_at_ms(),
-                            SchedulerTaskAttemptLifecycleTransition::Failed,
-                            "scheduler runtime dispatch failed",
-                            Some(scheduler_error.to_string()),
-                            None,
-                            Some(&terminal_mutation),
-                        )?;
+                        self.record_scheduler_task_attempt_terminal(WorkflowSchedulerTaskAttemptTerminalInput {
+                            task: started_runtime_task.task(),
+                            attempt_id: started_runtime_task.attempt_id().as_str(),
+                            started_at_ms: started_runtime_task.started_at_ms(),
+                            transition: SchedulerTaskAttemptLifecycleTransition::Failed,
+                            reason: "scheduler runtime dispatch failed",
+                            error_summary: Some(scheduler_error.to_string()),
+                            selected_dispatch: None,
+                            terminal_mutation: Some(&terminal_mutation),
+                        })?;
                     }
                     return Err(WorkflowServiceError::CapabilityViolation(format!(
                         "runtime scheduler dispatch selection failed closed for {count} runtime inference task(s): {scheduler_error}",
@@ -1101,16 +1098,18 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
 
     fn record_scheduler_task_attempt_terminal(
         &self,
-        _session_id: &str,
-        task: &WorkflowSchedulerTask,
-        attempt_id: &str,
-        started_at_ms: u64,
-        transition: SchedulerTaskAttemptLifecycleTransition,
-        reason: &str,
-        error_summary: Option,
-        selected_dispatch: Option<&SelectedRuntimeTaskDispatch>,
-        terminal_mutation: Option<&WorkflowSchedulerTaskTerminalMutation>,
+        input: WorkflowSchedulerTaskAttemptTerminalInput<'_>,
     ) -> Result<(), WorkflowServiceError> {
+        let WorkflowSchedulerTaskAttemptTerminalInput {
+            task,
+            attempt_id,
+            started_at_ms,
+            transition,
+            reason,
+            error_summary,
+            selected_dispatch,
+            terminal_mutation,
+        } = input;
         let attribution =
             self.scheduler_task_attempt_diagnostic_attribution(task.workflow_run_id.as_str())?;
         self.service.workflow_diagnostic_event_record(
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-terminal-diagnostic-input.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-terminal-diagnostic-input.md
new file mode 100644
index 000000000..88a33f02f
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-terminal-diagnostic-input.md
@@ -0,0 +1,9 @@
+# Shared private terminal diagnostic input
+
+Fresh PR #39 Clippy retains high-arity terminal diagnostic recorders in the session runner and runtime finalization owner. Group exactly eight existing values into private WorkflowSchedulerTaskAttemptTerminalInput: task, attempt_id, started_at_ms, transition, reason, owned error_summary, borrowed selected_dispatch and borrowed terminal_mutation. Migrate eight callers: four runner paths, three finalization paths and one batch path.
+
+Remove the runner's proven-unused _session_id argument. All four removed expressions were a plain session_id local with no side effects, and the parameter performed no validation. Both recorder bodies after destructuring remain byte-identical, including task.workflow_run_id attribution lookup and the unchanged lower event builder. No attribution-policy or public API change is made.
+
+A real in-memory ledger regression sends the new input through the recorder for success, failure and cancellation, verifying run/node/task/attempt scope, absent attribution, timing consistency, reason and original error detail. Existing precise-timing and incomplete-run tests remain, with nonzero module discovery in hosted CI. Source comparison, changed-call syntax formatting and staged whitespace pass locally. Root approved the design; source review and actual hosted execution remain pending.
+
+Root initial review confirmed the field/caller mapping and recorder-body preservation. The narrow follow-up removes six redundant field labels and the now-unused runner terminal-mutation import; the new duration assertion uses saturating subtraction. The production event builder remains unchanged and rejects a terminal timestamp preceding the start. Root narrow re-review accepted frozen tree a79c7c43eb66667ec8c0e97c5fc83e5dc9e93ab4. Hosted execution remains pending.

From 48b728c5cc73d0227a3c596a7a8eab58308064f9 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 07:47:14 -0700
Subject: [PATCH 45/65] refactor(workflow): group private session run context

---
 .github/workflows/quality-gates.yml           |  5 ++
 .../src/workflow/session_execution_api.rs     | 48 ++++++-----
 .../src/workflow/session_scheduler_runner.rs  | 84 ++++++++++---------
 .../src/workflow/task_execution_owner.rs      | 42 +++++++---
 .../reports/2026-10-03-session-run-context.md |  7 ++
 5 files changed, 112 insertions(+), 74 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-session-run-context.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 93fcf95c6..a6074e552 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -251,6 +251,11 @@ jobs:
       - name: Run workflow-nodes tests
         run: cargo test -p workflow-nodes --lib
 
+      - name: Run complete session execution regression suite
+        run: |
+          cargo test -p pantograph-workflow-service --lib workflow::tests::session_execution:: -- --list > "$RUNNER_TEMP/session-execution-tests.list"
+          python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith("workflow::tests::session_execution::") and line.endswith(": test")]; print("Session execution tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/session-execution-tests.list"
+          cargo test -p pantograph-workflow-service --lib workflow::tests::session_execution::
       - name: Run terminal diagnostic regression suite
         run: |
           cargo test -p pantograph-workflow-service --lib workflow::runtime_branch_run_finalization::tests:: -- --list > "$RUNNER_TEMP/terminal-diagnostic-tests.list"
diff --git a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
index 5e0216ef5..8b6727ed0 100644
--- a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
+++ b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
@@ -48,8 +48,10 @@ use super::runtime_dispatch_assignment::{
     WorkflowRuntimeDispatchAssignmentRepository,
 };
 use super::session_io_artifacts::workflow_io_artifact_metadata;
-use super::session_scheduler_runner::WorkflowSchedulerSessionRunner;
-use super::task_execution_owner::WorkflowTaskExecutionOwner;
+use super::session_scheduler_runner::{
+    WorkflowSchedulerRunContext, WorkflowSchedulerSessionRunner,
+};
+use super::task_execution_owner::{WorkflowNonRuntimeExecutionInput, WorkflowTaskExecutionOwner};
 use super::task_execution_runtime::WorkflowTaskExecutionRuntimeOwner;
 use super::task_execution_worker::{
     WorkflowTaskExecutionWorkerOutcome, WorkflowTaskExecutionWorkerRuntimeBranchCommand,
@@ -415,12 +417,14 @@ impl WorkflowService {
             return WorkflowTaskExecutionOwner::run_non_runtime_to_completion(
                 self,
                 host,
-                &session,
-                run_snapshot.as_ref(),
-                &session_id,
-                &workflow_run_id,
-                &queued_run,
-                &scheduler_task_run_summary,
+                WorkflowNonRuntimeExecutionInput {
+                    session: &session,
+                    run_snapshot: run_snapshot.as_ref(),
+                    session_id: &session_id,
+                    workflow_run_id: &workflow_run_id,
+                    queued_run: &queued_run,
+                    summary: &scheduler_task_run_summary,
+                },
             )
             .await;
         }
@@ -565,12 +569,14 @@ impl WorkflowService {
             Ok(Some(timeout_ms)) => {
                 let run_future = runner.resume_runtime_dependency_readiness(
                     host,
-                    &session_id,
-                    &workflow_run_id,
-                    &active_run.workflow_id,
-                    active_run.output_targets.as_deref(),
-                    &scheduler_task_run_summary,
-                    started_at,
+                    WorkflowSchedulerRunContext {
+                        session_id: &session_id,
+                        workflow_run_id: &workflow_run_id,
+                        workflow_id: &active_run.workflow_id,
+                        output_targets: active_run.output_targets.as_deref(),
+                        summary: &scheduler_task_run_summary,
+                        started_at,
+                    },
                     attempt_start_transition,
                 );
                 match tokio::time::timeout(Duration::from_millis(timeout_ms), run_future).await {
@@ -585,12 +591,14 @@ impl WorkflowService {
                 runner
                     .resume_runtime_dependency_readiness(
                         host,
-                        &session_id,
-                        &workflow_run_id,
-                        &active_run.workflow_id,
-                        active_run.output_targets.as_deref(),
-                        &scheduler_task_run_summary,
-                        started_at,
+                        WorkflowSchedulerRunContext {
+                            session_id: &session_id,
+                            workflow_run_id: &workflow_run_id,
+                            workflow_id: &active_run.workflow_id,
+                            output_targets: active_run.output_targets.as_deref(),
+                            summary: &scheduler_task_run_summary,
+                            started_at,
+                        },
                         attempt_start_transition,
                     )
                     .await
diff --git a/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs b/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs
index 31456e718..e89b18850 100644
--- a/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs
+++ b/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs
@@ -231,6 +231,16 @@ impl WorkflowPreDispatchPreparationOutcome {
     }
 }
 
+#[derive(Debug, Clone, Copy)]
+pub(super) struct WorkflowSchedulerRunContext<'a> {
+    pub(super) session_id: &'a str,
+    pub(super) workflow_run_id: &'a str,
+    pub(super) workflow_id: &'a str,
+    pub(super) output_targets: Option<&'a [WorkflowOutputTarget]>,
+    pub(super) summary: &'a WorkflowSchedulerTaskRunSummary,
+    pub(super) started_at: Instant,
+}
+
 struct ReadyRuntimeDispatchContext {
     task: WorkflowSchedulerTask,
     ready_record: SchedulerTaskStateRecord,
@@ -244,14 +254,17 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
     pub(super) async fn run_non_runtime_only(
         &self,
         host: &H,
-        session_id: &str,
-        workflow_run_id: &str,
-        workflow_id: &str,
         inputs: &[WorkflowPortBinding],
-        output_targets: Option<&[WorkflowOutputTarget]>,
-        summary: &WorkflowSchedulerTaskRunSummary,
-        started_at: Instant,
+        context: WorkflowSchedulerRunContext<'_>,
     ) -> Result {
+        let WorkflowSchedulerRunContext {
+            session_id,
+            workflow_run_id,
+            workflow_id,
+            output_targets,
+            summary,
+            started_at,
+        } = context;
         if !summary.is_non_runtime_only() || summary.has_runtime_inference() {
             return Err(WorkflowServiceError::Internal(
                 "scheduler session runner received a runtime-containing run".to_string(),
@@ -278,31 +291,22 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
     pub(super) async fn resume_runtime_dependency_readiness(
         &self,
         host: &(impl WorkflowHost + ?Sized),
-        session_id: &str,
-        workflow_run_id: &str,
-        workflow_id: &str,
-        output_targets: Option<&[WorkflowOutputTarget]>,
-        summary: &WorkflowSchedulerTaskRunSummary,
-        started_at: Instant,
+        context: WorkflowSchedulerRunContext<'_>,
         attempt_start_transition: SchedulerTaskAttemptLifecycleTransition,
     ) -> Result {
+        let WorkflowSchedulerRunContext {
+            workflow_run_id,
+            summary,
+            ..
+        } = context;
         if !summary.has_runtime_inference() {
             return Err(WorkflowServiceError::InvalidRequest(format!(
                 "workflow run '{}' is not a runtime inference run",
                 workflow_run_id
             )));
         }
-        self.continue_runtime_dependency_readiness(
-            host,
-            session_id,
-            workflow_run_id,
-            workflow_id,
-            output_targets,
-            summary,
-            started_at,
-            attempt_start_transition,
-        )
-        .await
+        self.continue_runtime_dependency_readiness(host, context, attempt_start_transition)
+            .await
     }
 
     pub(super) async fn resume_progress_loop(
@@ -318,14 +322,14 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
     async fn continue_runtime_dependency_readiness(
         &self,
         host: &(impl WorkflowHost + ?Sized),
-        session_id: &str,
-        workflow_run_id: &str,
-        workflow_id: &str,
-        output_targets: Option<&[WorkflowOutputTarget]>,
-        summary: &WorkflowSchedulerTaskRunSummary,
-        started_at: Instant,
+        context: WorkflowSchedulerRunContext<'_>,
         attempt_start_transition: SchedulerTaskAttemptLifecycleTransition,
     ) -> Result {
+        let WorkflowSchedulerRunContext {
+            session_id,
+            workflow_run_id,
+            ..
+        } = context;
         let preparation = WorkflowPreDispatchPreparationBoundary::new(self.service)
             .prepare_runtime_dispatch(session_id, workflow_run_id)
             .await?;
@@ -341,12 +345,7 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
         }
         self.run_runtime_dispatch_ready_tasks(
             host,
-            session_id,
-            workflow_run_id,
-            workflow_id,
-            output_targets,
-            summary,
-            started_at,
+            context,
             preparation.admitted_runtime_readiness(),
             attempt_start_transition,
         )
@@ -804,15 +803,18 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
     async fn run_runtime_dispatch_ready_tasks(
         &self,
         host: &(impl WorkflowHost + ?Sized),
-        session_id: &str,
-        workflow_run_id: &str,
-        workflow_id: &str,
-        output_targets: Option<&[WorkflowOutputTarget]>,
-        summary: &WorkflowSchedulerTaskRunSummary,
-        started_at: Instant,
+        context: WorkflowSchedulerRunContext<'_>,
         admitted_runtime_readiness: &[AdmittedRuntimeTaskReadiness],
         attempt_start_transition: SchedulerTaskAttemptLifecycleTransition,
     ) -> Result {
+        let WorkflowSchedulerRunContext {
+            session_id,
+            workflow_run_id,
+            workflow_id,
+            output_targets,
+            summary,
+            started_at,
+        } = context;
         let runtime_task_ids =
             runtime_task_ids_in_state(self.service, session_id, workflow_run_id, |kind| {
                 kind == SchedulerTaskStateKind::Ready
diff --git a/crates/pantograph-workflow-service/src/workflow/task_execution_owner.rs b/crates/pantograph-workflow-service/src/workflow/task_execution_owner.rs
index e449be161..9f417c342 100644
--- a/crates/pantograph-workflow-service/src/workflow/task_execution_owner.rs
+++ b/crates/pantograph-workflow-service/src/workflow/task_execution_owner.rs
@@ -3,7 +3,9 @@ use std::time::Duration;
 use crate::scheduler::WorkflowExecutionSessionDequeuedRun;
 use pantograph_runtime_attribution::WorkflowRunSnapshotRecord;
 
-use super::session_scheduler_runner::WorkflowSchedulerSessionRunner;
+use super::session_scheduler_runner::{
+    WorkflowSchedulerRunContext, WorkflowSchedulerSessionRunner,
+};
 use super::workflow_run_finalization::{
     finalize_admitted_workflow_run, WorkflowRunFinalizationRequest,
 };
@@ -12,6 +14,15 @@ use super::{
     WorkflowSchedulerTaskRunSummary, WorkflowService, WorkflowServiceError,
 };
 
+pub(super) struct WorkflowNonRuntimeExecutionInput<'a> {
+    pub(super) session: &'a WorkflowExecutionSessionSummary,
+    pub(super) run_snapshot: Option<&'a WorkflowRunSnapshotRecord>,
+    pub(super) session_id: &'a str,
+    pub(super) workflow_run_id: &'a str,
+    pub(super) queued_run: &'a WorkflowExecutionSessionDequeuedRun,
+    pub(super) summary: &'a WorkflowSchedulerTaskRunSummary,
+}
+
 pub(super) struct WorkflowTaskExecutionOwner;
 
 impl WorkflowTaskExecutionOwner {
@@ -26,13 +37,16 @@ impl WorkflowTaskExecutionOwner {
     pub(super) async fn run_non_runtime_to_completion(
         service: &WorkflowService,
         host: &H,
-        session: &WorkflowExecutionSessionSummary,
-        run_snapshot: Option<&WorkflowRunSnapshotRecord>,
-        session_id: &str,
-        workflow_run_id: &str,
-        queued_run: &WorkflowExecutionSessionDequeuedRun,
-        summary: &WorkflowSchedulerTaskRunSummary,
+        input: WorkflowNonRuntimeExecutionInput<'_>,
     ) -> Result {
+        let WorkflowNonRuntimeExecutionInput {
+            session,
+            run_snapshot,
+            session_id,
+            workflow_run_id,
+            queued_run,
+            summary,
+        } = input;
         service.record_run_started_event_if_configured(session, run_snapshot, queued_run)?;
         let run_started_at = std::time::Instant::now();
         let queued_workflow_semantic_version = queued_run.queued.workflow_semantic_version.clone();
@@ -40,13 +54,15 @@ impl WorkflowTaskExecutionOwner {
         let runner = WorkflowSchedulerSessionRunner::new(service);
         let run_future = runner.run_non_runtime_only(
             host,
-            session_id,
-            workflow_run_id,
-            &queued_run.workflow_id,
             &queued_run.queued.inputs,
-            queued_run.queued.output_targets.as_deref(),
-            summary,
-            run_started_at,
+            WorkflowSchedulerRunContext {
+                session_id,
+                workflow_run_id,
+                workflow_id: &queued_run.workflow_id,
+                output_targets: queued_run.queued.output_targets.as_deref(),
+                summary,
+                started_at: run_started_at,
+            },
         );
         let run_result = if let Some(timeout_ms) = queued_run.queued.timeout_ms {
             match tokio::time::timeout(Duration::from_millis(timeout_ms), run_future).await {
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-session-run-context.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-session-run-context.md
new file mode 100644
index 000000000..dd5d1cd58
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-session-run-context.md
@@ -0,0 +1,7 @@
+# Private session runner and non-runtime execution inputs
+
+Group the six values shared by four high-arity runner methods into private WorkflowSchedulerRunContext: borrowed session_id, workflow_run_id, workflow_id, output_targets and summary, plus the original Instant. Keep host, inputs, admitted readiness and attempt transition separate. There are five runner calls: one non-runtime entry, two resume entries and two internal forwards. The Copy context contains only existing borrowed values and Instant; no allocation, new clock read or cloned run data is introduced.
+
+The sole WorkflowTaskExecutionOwner caller receives a separate six-field WorkflowNonRuntimeExecutionInput: session, run_snapshot, session_id, workflow_run_id, queued_run and summary. Existing finalization requests carry extra result/policy values and are not reused. All existing service/host parameters, borrowing and result signatures remain. Timeout, cancellation cleanup, finalization, no-load behavior and runtime readiness decisions are unchanged.
+
+Programmatic source comparisons confirm unchanged non-runtime execution/finalization, ready-dispatch body and owner timeout/finalization block. CI explicitly discovers and runs the entire session-execution module, including non-runtime lifecycle, timeout, readiness defer/resume, bootstrap recovery and no-runtime-load cases, with a nonzero guard. Root approved this bounded design. Whitespace/changed-call syntax checks pass locally; root source review accepted all five files at frozen tree 66a25e87fe897ea1001622c0768a51cc51d07882; hosted execution remains pending.

From 533a80b9650cff2c1b4f9a8b9087760139fe89c9 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 07:52:07 -0700
Subject: [PATCH 46/65] refactor(workflow): clarify private projection and
 summary types

---
 .github/workflows/quality-gates.yml           |  5 ++
 .../src/workflow/task_graph.rs                | 16 ++++---
 .../src/workflow/task_run_summary.rs          | 46 +++++++++++++++----
 .../2026-10-03-workflow-private-type-names.md |  7 +++
 4 files changed, 59 insertions(+), 15 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-private-type-names.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index a6074e552..52fb41d4b 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -251,6 +251,11 @@ jobs:
       - name: Run workflow-nodes tests
         run: cargo test -p workflow-nodes --lib
 
+      - name: Run task summary contract tests
+        run: |
+          cargo test -p pantograph-workflow-service --lib workflow::task_run_summary::tests:: -- --list > "$RUNNER_TEMP/task-summary-tests.list"
+          python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith("workflow::task_run_summary::tests::") and line.endswith(": test")]; print("Task summary tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/task-summary-tests.list"
+          cargo test -p pantograph-workflow-service --lib workflow::task_run_summary::tests::
       - name: Run complete session execution regression suite
         run: |
           cargo test -p pantograph-workflow-service --lib workflow::tests::session_execution:: -- --list > "$RUNNER_TEMP/session-execution-tests.list"
diff --git a/crates/pantograph-workflow-service/src/workflow/task_graph.rs b/crates/pantograph-workflow-service/src/workflow/task_graph.rs
index 89b9e71f5..267a1169e 100644
--- a/crates/pantograph-workflow-service/src/workflow/task_graph.rs
+++ b/crates/pantograph-workflow-service/src/workflow/task_graph.rs
@@ -259,6 +259,14 @@ fn dependency_task_ids(bindings: &[WorkflowSchedulerTaskInputBinding]) -> Vec,
+    Option,
+    Option,
+    Option,
+    Vec,
+);
+
 fn schedulable_intent_for_node(
     workflow_id: &SchedulerWorkflowId,
     workflow_run_id: &SchedulerWorkflowRunId,
@@ -266,13 +274,7 @@ fn schedulable_intent_for_node(
     task_id: &SchedulerTaskId,
     execution_class: WorkflowSchedulerTaskExecutionClass,
     inference_task_projection: Option<&WorkflowSchedulerInferenceTaskProjection>,
-) -> (
-    Option,
-    Option,
-    Option,
-    Option,
-    Vec,
-) {
+) -> SchedulableIntentProjection {
     if execution_class != WorkflowSchedulerTaskExecutionClass::RuntimeInference {
         return (None, None, None, None, Vec::new());
     }
diff --git a/crates/pantograph-workflow-service/src/workflow/task_run_summary.rs b/crates/pantograph-workflow-service/src/workflow/task_run_summary.rs
index cbad3add0..e085ba2ae 100644
--- a/crates/pantograph-workflow-service/src/workflow/task_run_summary.rs
+++ b/crates/pantograph-workflow-service/src/workflow/task_run_summary.rs
@@ -54,7 +54,7 @@ pub(crate) fn workflow_scheduler_task_run_summary(
             .iter()
             .find(|record| record.task_id.as_str() == task_id)
         else {
-            return Err(WorkflowSchedulerTaskRunSummaryError::MissingTaskState {
+            return Err(WorkflowSchedulerTaskRunSummaryError::Missing {
                 task_id: task_id.to_string(),
             });
         };
@@ -62,7 +62,7 @@ pub(crate) fn workflow_scheduler_task_run_summary(
             || record.workflow_run_id.as_str() != task.workflow_run_id.as_str()
             || record.node_id.as_str() != task.node_id.as_str()
         {
-            return Err(WorkflowSchedulerTaskRunSummaryError::MismatchedTaskState {
+            return Err(WorkflowSchedulerTaskRunSummaryError::Mismatched {
                 task_id: task_id.to_string(),
             });
         }
@@ -95,7 +95,7 @@ pub(crate) fn workflow_scheduler_task_run_summary(
     }
 
     if let Some(extra_task_id) = record_task_ids.into_iter().next() {
-        return Err(WorkflowSchedulerTaskRunSummaryError::UnexpectedTaskState {
+        return Err(WorkflowSchedulerTaskRunSummaryError::Unexpected {
             task_id: extra_task_id.to_string(),
         });
     }
@@ -107,11 +107,11 @@ pub(crate) fn workflow_scheduler_task_run_summary(
 #[non_exhaustive]
 pub(crate) enum WorkflowSchedulerTaskRunSummaryError {
     #[error("scheduler task '{task_id}' has no active task-state record")]
-    MissingTaskState { task_id: String },
+    Missing { task_id: String },
     #[error("active task-state record for scheduler task '{task_id}' has mismatched correlation")]
-    MismatchedTaskState { task_id: String },
+    Mismatched { task_id: String },
     #[error("active task-state record exists for unknown scheduler task '{task_id}'")]
-    UnexpectedTaskState { task_id: String },
+    Unexpected { task_id: String },
 }
 
 #[cfg(test)]
@@ -269,10 +269,36 @@ mod tests {
         )]);
 
         let error = workflow_scheduler_task_run_summary(&graph, &[]).expect_err("missing record");
+        assert_eq!(
+            error.to_string(),
+            "scheduler task 'prompt' has no active task-state record"
+        );
+
+        assert_eq!(
+            error,
+            WorkflowSchedulerTaskRunSummaryError::Missing {
+                task_id: "prompt".to_string()
+            }
+        );
+    }
 
+    #[test]
+    fn rejects_mismatched_task_state_with_unchanged_display() {
+        let graph = task_graph(&[(
+            "prompt",
+            WorkflowSchedulerTaskExecutionClass::NonRuntimeNodeEngine,
+        )]);
+        let mut mismatched = record("prompt", awaiting_inputs());
+        mismatched.workflow_run_id = SchedulerWorkflowRunId::parse("other-run").unwrap();
+        let error = workflow_scheduler_task_run_summary(&graph, &[mismatched])
+            .expect_err("mismatched correlation");
+        assert_eq!(
+            error.to_string(),
+            "active task-state record for scheduler task 'prompt' has mismatched correlation"
+        );
         assert_eq!(
             error,
-            WorkflowSchedulerTaskRunSummaryError::MissingTaskState {
+            WorkflowSchedulerTaskRunSummaryError::Mismatched {
                 task_id: "prompt".to_string()
             }
         );
@@ -293,10 +319,14 @@ mod tests {
             ],
         )
         .expect_err("unexpected record");
+        assert_eq!(
+            error.to_string(),
+            "active task-state record exists for unknown scheduler task 'extra'"
+        );
 
         assert_eq!(
             error,
-            WorkflowSchedulerTaskRunSummaryError::UnexpectedTaskState {
+            WorkflowSchedulerTaskRunSummaryError::Unexpected {
                 task_id: "extra".to_string()
             }
         );
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-private-type-names.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-private-type-names.md
new file mode 100644
index 000000000..292c07e47
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-workflow-private-type-names.md
@@ -0,0 +1,7 @@
+# Private projection tuple and summary error names
+
+Name the existing five-element schedulable projection tuple with the private SchedulableIntentProjection alias; tuple members, order, function body and caller destructuring are unchanged. Shorten only the crate-private summary error variants from MissingTaskState/MismatchedTaskState/UnexpectedTaskState to Missing/Mismatched/Unexpected, retaining every task_id field and exact thiserror Display string. Neither type has a new public or serialized representation.
+
+The source inventory finds two production summary consumers, both using Display interpolation before admission/resume; all variant matches are local tests. Derived Debug variant labels intentionally change with the private names and may appear in test failures, but no production Debug consumer was found. Real missing/unexpected summary tests now assert exact Display, and a real mismatched-run correlation test covers the third branch. CI runs the complete summary module with nonzero discovery, retaining projection tests for the tuple alias.
+
+Root approved this bounded cleanup. Root source review accepted all four files at frozen tree c3df7026f434651b5b57876d967110d55f3150c6. Hosted execution remains pending. No local Rust execution is claimed.

From c369cbe4b12b563eb8a5d4419e6e0ed729721137 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 08:06:30 -0700
Subject: [PATCH 47/65] refactor(registry): box private selection diagnostic
 errors

---
 .github/workflows/quality-gates.yml           |  2 +
 .../src/runtime_selection_policy.rs           | 51 +++++++++++++++----
 .../src/technical_fit_tests.rs                |  9 ++++
 ...0-03-runtime-registry-diagnostic-errors.md |  7 +++
 4 files changed, 58 insertions(+), 11 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-registry-diagnostic-errors.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 52fb41d4b..2edc2cb9d 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -251,6 +251,8 @@ jobs:
       - name: Run workflow-nodes tests
         run: cargo test -p workflow-nodes --lib
 
+      - name: Run runtime registry contract tests
+        run: cargo test -p pantograph-runtime-registry
       - name: Run task summary contract tests
         run: |
           cargo test -p pantograph-workflow-service --lib workflow::task_run_summary::tests:: -- --list > "$RUNNER_TEMP/task-summary-tests.list"
diff --git a/crates/pantograph-runtime-registry/src/runtime_selection_policy.rs b/crates/pantograph-runtime-registry/src/runtime_selection_policy.rs
index 2eb78250f..a696b5284 100644
--- a/crates/pantograph-runtime-registry/src/runtime_selection_policy.rs
+++ b/crates/pantograph-runtime-registry/src/runtime_selection_policy.rs
@@ -178,7 +178,7 @@ pub(crate) fn select_runtime_technical_fit_automatically(
                 return RuntimeSelectionDecision::new(unselected_decision_with_device_diagnostics(
                     RuntimeTechnicalFitSelectionMode::Automatic,
                     Vec::new(),
-                    vec![diagnostic],
+                    vec![*diagnostic],
                 ));
             }
         };
@@ -292,7 +292,7 @@ fn automatic_selection_policy_trace(
     controlled_exploration_seed_basis: Option<&str>,
     history_threshold_state: RuntimeTechnicalFitHistoryThresholdState,
     history_ranking_enabled: bool,
-) -> Result {
+) -> Result> {
     let candidate_set_summary = automatic_candidate_set_summary(request, eligible_candidates)?;
     Ok(RuntimeTechnicalFitSelectionPolicyTrace {
         policy_version: TECHNICAL_FIT_SELECTION_POLICY_VERSION,
@@ -327,12 +327,12 @@ fn automatic_selection_policy_trace(
 fn automatic_candidate_set_summary(
     request: &RuntimeTechnicalFitRequest,
     eligible_candidates: &[&RuntimeTechnicalFitCandidate],
-) -> Result {
+) -> Result> {
     let total_candidate_count = checked_candidate_count(request.candidates.len())?;
     let eligible_candidate_count = checked_candidate_count(eligible_candidates.len())?;
     let rejected_candidate_count = total_candidate_count
         .checked_sub(eligible_candidate_count)
-        .ok_or_else(candidate_summary_count_diagnostic)?;
+        .ok_or_else(|| Box::new(candidate_summary_count_diagnostic()))?;
 
     Ok(RuntimeTechnicalFitCandidateSetSummary {
         total_candidate_count,
@@ -346,8 +346,8 @@ fn automatic_candidate_set_summary(
     .normalized())
 }
 
-fn checked_candidate_count(count: usize) -> Result {
-    u32::try_from(count).map_err(|_| candidate_summary_count_diagnostic())
+fn checked_candidate_count(count: usize) -> Result> {
+    u32::try_from(count).map_err(|_| Box::new(candidate_summary_count_diagnostic()))
 }
 
 fn candidate_summary_count_diagnostic() -> RuntimeTechnicalFitDeviceDiagnostic {
@@ -822,7 +822,7 @@ fn resource_budget_diagnostic(
     };
     let reserved_bytes = match active_reserved_bytes(runtime_snapshot, resource_kind) {
         Ok(reserved_bytes) => reserved_bytes,
-        Err(diagnostic) => return Some(diagnostic_context_from_candidate(diagnostic, candidate)),
+        Err(diagnostic) => return Some(diagnostic_context_from_candidate(*diagnostic, candidate)),
     };
     let Some(available_bytes) = after_safety_margin_bytes.checked_sub(reserved_bytes) else {
         return Some(resource_budget_underflow_diagnostic(
@@ -871,7 +871,7 @@ fn resource_budget_diagnostic(
 fn active_reserved_bytes(
     runtime_snapshot: &RuntimeRegistryRuntimeSnapshot,
     resource_kind: RuntimeAdmissionResourceKind,
-) -> Result {
+) -> Result> {
     let mut reserved_bytes = 0_u64;
     for active_claim in &runtime_snapshot.active_reservation_claims {
         for claim in active_claim
@@ -891,10 +891,10 @@ fn checked_add_claim_bytes(
     resource_kind: RuntimeAdmissionResourceKind,
     reserved_bytes: u64,
     claim: &RuntimeReservationResourceClaim,
-) -> Result {
+) -> Result> {
     reserved_bytes
         .checked_add(claim.bytes)
-        .ok_or_else(|| RuntimeTechnicalFitDeviceDiagnostic {
+        .ok_or_else(|| Box::new(RuntimeTechnicalFitDeviceDiagnostic {
             code: RuntimeTechnicalFitDeviceDiagnosticCode::ResourceAccountingOverflow,
             severity: RuntimeTechnicalFitDeviceDiagnosticSeverity::Error,
             message: format!(
@@ -911,7 +911,7 @@ fn checked_add_claim_bytes(
             model_id: None,
             evidence_key: Some(resource_kind.resource_label().to_string()),
             requested_runtime_key: None,
-        })
+        }))
 }
 
 fn resource_budget_underflow_diagnostic(
@@ -1132,3 +1132,32 @@ fn candidate_has_available_peak_memory_estimate(candidate: &RuntimeTechnicalFitC
             && estimate.value_bytes().is_some()
     })
 }
+
+#[cfg(test)]
+mod private_diagnostic_tests {
+    use super::checked_candidate_count;
+
+    #[test]
+    fn candidate_counts_preserve_exact_valid_boundaries() {
+        assert_eq!(checked_candidate_count(0).unwrap(), 0);
+        assert_eq!(
+            checked_candidate_count(u32::MAX as usize).unwrap(),
+            u32::MAX
+        );
+    }
+
+    #[test]
+    #[cfg(target_pointer_width = "64")]
+    fn candidate_count_overflow_preserves_exact_diagnostic_json() {
+        let diagnostic = checked_candidate_count(u32::MAX as usize + 1).unwrap_err();
+        let expected = serde_json::json!({
+            "code": "no_valid_candidate",
+            "severity": "error",
+            "message": "technical-fit candidate set is too large to summarize exactly"
+        });
+        assert_eq!(serde_json::to_value(diagnostic.as_ref()).unwrap(), expected);
+        let decoded: crate::technical_fit::RuntimeTechnicalFitDeviceDiagnostic =
+            serde_json::from_value(expected).unwrap();
+        assert_eq!(*diagnostic, decoded);
+    }
+}
diff --git a/crates/pantograph-runtime-registry/src/technical_fit_tests.rs b/crates/pantograph-runtime-registry/src/technical_fit_tests.rs
index 33fb0bc60..ad0971b37 100644
--- a/crates/pantograph-runtime-registry/src/technical_fit_tests.rs
+++ b/crates/pantograph-runtime-registry/src/technical_fit_tests.rs
@@ -2563,6 +2563,15 @@ fn selector_reports_resource_accounting_overflow_before_selection() {
 
     assert_eq!(decision.selected_candidate_id, None);
     assert_eq!(decision.device_diagnostics.len(), 1);
+    assert_eq!(serde_json::to_value(&decision.device_diagnostics[0]).unwrap(), serde_json::json!({
+        "code": "resource_accounting_overflow",
+        "severity": "error",
+        "message": "technical-fit resource accounting overflowed while summing active ram_bytes claims for runtime 'runtime-overflow'",
+        "runtime_id": "runtime-overflow",
+        "backend_key": "pytorch",
+        "evidence_key": "ram_bytes",
+        "requested_runtime_key": "pytorch"
+    }));
     assert_eq!(
         decision.device_diagnostics[0].code,
         RuntimeTechnicalFitDeviceDiagnosticCode::ResourceAccountingOverflow
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-registry-diagnostic-errors.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-registry-diagnostic-errors.md
new file mode 100644
index 000000000..345a7a049
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-runtime-registry-diagnostic-errors.md
@@ -0,0 +1,7 @@
+# Private runtime selection diagnostic errors
+
+Fresh PR #39 aggregate Clippy clears workflow-service and stops at five private runtime-registry Result paths whose error is the same 224-byte RuntimeTechnicalFitDeviceDiagnostic DTO. Box only these errors in automatic_selection_policy_trace, automatic_candidate_set_summary, checked_candidate_count, active_reserved_bytes and checked_add_claim_bytes. Unwrap at the existing automatic decision-vector and resource-budget context adapters. Preserve public signatures, the raw DTO, diagnostic fields/JSON, ranking/accounting decisions and success values.
+
+Boundary tests retain zero/u32-max candidate counts and exact overflow diagnostic JSON (the usize overflow case is explicitly a 64-bit test). The existing public selector resource-overflow test now checks the entire contextualized payload, while full registry tests retain successful selection, budget and underflow behavior. Only failing paths add a box allocation. No broad Result or public layout change is made.
+
+Root approved this bounded registry branch using the existing checkout. Root source review accepted all four files at frozen tree e88e208b5567da1dda64974dd1c65bb4524d5801. Hosted full-crate/aggregate qualification remains pending; no local Rust execution is claimed.

From 2a4d5c8735188e18e2ab2e085b73c48bf7362807 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 08:16:19 -0700
Subject: [PATCH 48/65] style(rust): apply pinned formatter to repair
 milestones

---
 .../src/tests.rs                              |  8 +--
 .../src/node_execution_ledger.rs              | 70 ++++++++++---------
 .../src/technical_fit_tests.rs                | 21 +++---
 .../src/graph/inference_validation_state.rs   |  8 +--
 .../src/scheduler/task_orchestrator.rs        |  6 +-
 .../executable_validation_snapshot.rs         |  8 +--
 .../src/workflow/session_execution_api.rs     |  8 +--
 .../src/workflow/session_scheduler_runner.rs  | 44 ++++++------
 .../src/workflow/task_execution_worker.rs     | 22 +++---
 .../workflow/tests/task_binding_resolution.rs |  8 +--
 .../src/workflow/tests/task_graph.rs          |  8 +--
 .../2026-10-03-pinned-rustfmt-correction.md   | 21 ++++++
 12 files changed, 132 insertions(+), 100 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pinned-rustfmt-correction.md

diff --git a/crates/pantograph-diagnostics-ledger/src/tests.rs b/crates/pantograph-diagnostics-ledger/src/tests.rs
index 2948baa34..5d8ff19ba 100644
--- a/crates/pantograph-diagnostics-ledger/src/tests.rs
+++ b/crates/pantograph-diagnostics-ledger/src/tests.rs
@@ -6485,8 +6485,8 @@ fn sample_inference_execution_diagnostic_event() -> DiagnosticEventAppendRequest
         privacy_class: DiagnosticEventPrivacyClass::SystemMetadata,
         retention_class: DiagnosticEventRetentionClass::AuditMetadata,
         payload_ref: None,
-        payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(
-            Box::new(InferenceExecutionDiagnosticObservedPayload {
+        payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(Box::new(
+            InferenceExecutionDiagnosticObservedPayload {
                 request_id: "req-a".to_string(),
                 task_id: "text_generation".to_string(),
                 lifecycle_phase: Some("task_validation".to_string()),
@@ -6552,8 +6552,8 @@ fn sample_inference_execution_diagnostic_event() -> DiagnosticEventAppendRequest
                         message: Some("not mapped by this backend boundary".to_string()),
                     },
                 ],
-            }),
-        ),
+            },
+        )),
     }
 }
 
diff --git a/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs b/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs
index 183fa5f49..9abde039f 100644
--- a/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs
+++ b/crates/pantograph-embedded-runtime/src/node_execution_ledger.rs
@@ -532,28 +532,30 @@ impl NodeExecutionWorkflowLedgerSink {
             privacy_class: artifact.privacy_class,
             retention_class: artifact.retention_class,
             payload_ref: artifact.payload_ref,
-            payload: DiagnosticEventPayload::IoArtifactObserved(Box::new(IoArtifactObservedPayload {
-                artifact_fact_id: Some(artifact.artifact_fact_id),
-                payload_artifact_id: Some(artifact.payload_artifact_id),
-                artifact_id: artifact.artifact_id,
-                artifact_role,
-                logical_payload_lineage_id: Some(artifact.logical_payload_lineage_id),
-                producer_node_id,
-                producer_port_id,
-                consumer_node_id,
-                consumer_port_id,
-                media_type: artifact.media_type,
-                size_bytes: artifact.size_bytes,
-                content_hash: artifact.content_hash,
-                retention_state: Some(artifact.retention_state),
-                retention_reason: artifact.retention_reason,
-                payload_kind: artifact.payload_kind,
-                lifecycle_state: artifact.lifecycle_state,
-                access_modes: artifact.access_modes,
-                read_handle: artifact.read_handle,
-                stream_handle: None,
-                format: artifact.format,
-            })),
+            payload: DiagnosticEventPayload::IoArtifactObserved(Box::new(
+                IoArtifactObservedPayload {
+                    artifact_fact_id: Some(artifact.artifact_fact_id),
+                    payload_artifact_id: Some(artifact.payload_artifact_id),
+                    artifact_id: artifact.artifact_id,
+                    artifact_role,
+                    logical_payload_lineage_id: Some(artifact.logical_payload_lineage_id),
+                    producer_node_id,
+                    producer_port_id,
+                    consumer_node_id,
+                    consumer_port_id,
+                    media_type: artifact.media_type,
+                    size_bytes: artifact.size_bytes,
+                    content_hash: artifact.content_hash,
+                    retention_state: Some(artifact.retention_state),
+                    retention_reason: artifact.retention_reason,
+                    payload_kind: artifact.payload_kind,
+                    lifecycle_state: artifact.lifecycle_state,
+                    access_modes: artifact.access_modes,
+                    read_handle: artifact.read_handle,
+                    stream_handle: None,
+                    format: artifact.format,
+                },
+            )),
         };
 
         self.workflow_service
@@ -951,8 +953,8 @@ fn build_kv_cache_diagnostic_event_ledger_append_request(
         privacy_class: DiagnosticEventPrivacyClass::SystemMetadata,
         retention_class: DiagnosticEventRetentionClass::AuditMetadata,
         payload_ref: None,
-        payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(
-            Box::new(InferenceExecutionDiagnosticObservedPayload {
+        payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(Box::new(
+            InferenceExecutionDiagnosticObservedPayload {
                 request_id: format!("{task_id}:kv_cache"),
                 task_id: "kv_cache".to_string(),
                 lifecycle_phase: Some("kv_cache".to_string()),
@@ -981,8 +983,8 @@ fn build_kv_cache_diagnostic_event_ledger_append_request(
                     .take(MAX_INFERENCE_OPTION_DIAGNOSTICS)
                     .map(kv_cache_option_diagnostic_summary)
                     .collect(),
-            }),
-        ),
+            },
+        )),
     })
 }
 
@@ -1047,8 +1049,8 @@ fn build_runtime_settings_diagnostic_event_ledger_append_request(
         privacy_class: DiagnosticEventPrivacyClass::SystemMetadata,
         retention_class: DiagnosticEventRetentionClass::AuditMetadata,
         payload_ref: None,
-        payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(
-            Box::new(InferenceExecutionDiagnosticObservedPayload {
+        payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(Box::new(
+            InferenceExecutionDiagnosticObservedPayload {
                 request_id: format!("{task_id}:runtime_settings"),
                 task_id: "runtime_settings".to_string(),
                 lifecycle_phase: Some("runtime_settings".to_string()),
@@ -1075,8 +1077,8 @@ fn build_runtime_settings_diagnostic_event_ledger_append_request(
                 compatibility_issues: Vec::new(),
                 option_support_counts: InferenceOptionSupportCounts::default(),
                 option_diagnostics: Vec::new(),
-            }),
-        ),
+            },
+        )),
     })
 }
 
@@ -1146,8 +1148,8 @@ fn build_inference_diagnostic_event_ledger_append_request(
         privacy_class: DiagnosticEventPrivacyClass::SystemMetadata,
         retention_class: DiagnosticEventRetentionClass::AuditMetadata,
         payload_ref: None,
-        payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(
-            Box::new(InferenceExecutionDiagnosticObservedPayload {
+        payload: DiagnosticEventPayload::InferenceExecutionDiagnosticObserved(Box::new(
+            InferenceExecutionDiagnosticObservedPayload {
                 request_id: event
                     .request_id
                     .clone()
@@ -1196,8 +1198,8 @@ fn build_inference_diagnostic_event_ledger_append_request(
                     .take(MAX_INFERENCE_OPTION_DIAGNOSTICS)
                     .map(option_diagnostic_summary)
                     .collect(),
-            }),
-        ),
+            },
+        )),
     })
 }
 
diff --git a/crates/pantograph-runtime-registry/src/technical_fit_tests.rs b/crates/pantograph-runtime-registry/src/technical_fit_tests.rs
index ad0971b37..570112317 100644
--- a/crates/pantograph-runtime-registry/src/technical_fit_tests.rs
+++ b/crates/pantograph-runtime-registry/src/technical_fit_tests.rs
@@ -2563,15 +2563,18 @@ fn selector_reports_resource_accounting_overflow_before_selection() {
 
     assert_eq!(decision.selected_candidate_id, None);
     assert_eq!(decision.device_diagnostics.len(), 1);
-    assert_eq!(serde_json::to_value(&decision.device_diagnostics[0]).unwrap(), serde_json::json!({
-        "code": "resource_accounting_overflow",
-        "severity": "error",
-        "message": "technical-fit resource accounting overflowed while summing active ram_bytes claims for runtime 'runtime-overflow'",
-        "runtime_id": "runtime-overflow",
-        "backend_key": "pytorch",
-        "evidence_key": "ram_bytes",
-        "requested_runtime_key": "pytorch"
-    }));
+    assert_eq!(
+        serde_json::to_value(&decision.device_diagnostics[0]).unwrap(),
+        serde_json::json!({
+            "code": "resource_accounting_overflow",
+            "severity": "error",
+            "message": "technical-fit resource accounting overflowed while summing active ram_bytes claims for runtime 'runtime-overflow'",
+            "runtime_id": "runtime-overflow",
+            "backend_key": "pytorch",
+            "evidence_key": "ram_bytes",
+            "requested_runtime_key": "pytorch"
+        })
+    );
     assert_eq!(
         decision.device_diagnostics[0].code,
         RuntimeTechnicalFitDeviceDiagnosticCode::ResourceAccountingOverflow
diff --git a/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs b/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs
index 000e8c783..5b5186a60 100644
--- a/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs
+++ b/crates/pantograph-workflow-service/src/graph/inference_validation_state.rs
@@ -1467,8 +1467,8 @@ impl CurrentInferenceValidationNodeRecord {
                         message: format!("dependency requirements proof is not current: {error:?}"),
                     },
                 )?;
-            return Ok(WorkflowSchedulerInferenceTaskProjection::Ready(
-                Box::new(WorkflowSchedulerReadyInferenceTaskProjection {
+            return Ok(WorkflowSchedulerInferenceTaskProjection::Ready(Box::new(
+                WorkflowSchedulerReadyInferenceTaskProjection {
                     node_id: pantograph_scheduler::SchedulerNodeId::parse(self.node_id.as_str())
                         .map_err(|error| {
                             CurrentInferenceSchedulerProjectionError::IncompleteNodeState {
@@ -1532,8 +1532,8 @@ impl CurrentInferenceValidationNodeRecord {
                             .dependency_override_fingerprint
                             .clone(),
                     },
-                }),
-            ));
+                },
+            )));
         }
 
         Ok(WorkflowSchedulerInferenceTaskProjection::Blocked(
diff --git a/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs b/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
index 47365ce4b..34c3167df 100644
--- a/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
+++ b/crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs
@@ -2115,9 +2115,9 @@ fn dispatch_selected_handoff_from_selection(
 ) -> Result {
     if selection.state != SchedulerDispatchSelectionState::Selected {
         return Err(
-            WorkflowSchedulerTaskOrchestratorError::RuntimeDispatchSelectionNoSelection(
-                Box::new(selection),
-            ),
+            WorkflowSchedulerTaskOrchestratorError::RuntimeDispatchSelectionNoSelection(Box::new(
+                selection,
+            )),
         );
     }
     let Some(dispatch_decision) = selection.dispatch_decision else {
diff --git a/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs b/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs
index 7c35fa013..ea29b7d88 100644
--- a/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs
+++ b/crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs
@@ -491,8 +491,8 @@ impl WorkflowExecutableValidationSnapshotNode {
             }
         })?;
 
-        Ok(WorkflowSchedulerInferenceTaskProjection::Ready(
-            Box::new(WorkflowSchedulerReadyInferenceTaskProjection {
+        Ok(WorkflowSchedulerInferenceTaskProjection::Ready(Box::new(
+            WorkflowSchedulerReadyInferenceTaskProjection {
                 node_id: scheduler_node_id,
                 descriptor_fingerprint: self.descriptor_fingerprint.clone(),
                 task_type,
@@ -504,8 +504,8 @@ impl WorkflowExecutableValidationSnapshotNode {
                 dependency_readiness_source: workflow_scheduler_dependency_readiness_source(
                     snapshot, self,
                 )?,
-            }),
-        ))
+            },
+        )))
     }
 }
 
diff --git a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
index 8b6727ed0..921c7ef0c 100644
--- a/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
+++ b/crates/pantograph-workflow-service/src/workflow/session_execution_api.rs
@@ -1353,8 +1353,8 @@ impl WorkflowService {
                     privacy_class: metadata.privacy_class,
                     retention_class: metadata.retention_class,
                     payload_ref: metadata.payload_ref.clone(),
-                    payload: DiagnosticEventPayload::IoArtifactObserved(
-                        Box::new(IoArtifactObservedPayload {
+                    payload: DiagnosticEventPayload::IoArtifactObserved(Box::new(
+                        IoArtifactObservedPayload {
                             artifact_fact_id: Some(metadata.artifact_fact_id),
                             payload_artifact_id: Some(metadata.payload_artifact_id),
                             artifact_id: metadata.artifact_id,
@@ -1391,8 +1391,8 @@ impl WorkflowService {
                             read_handle: metadata.read_handle,
                             stream_handle: metadata.stream_handle,
                             format: metadata.format,
-                        }),
-                    ),
+                        },
+                    )),
                 },
             )?;
         }
diff --git a/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs b/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs
index e89b18850..bf65931b2 100644
--- a/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs
+++ b/crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs
@@ -513,16 +513,18 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
                                 "scheduler non-runtime task completion failed: {error}"
                             ))
                         })?;
-                    self.record_scheduler_task_attempt_terminal(WorkflowSchedulerTaskAttemptTerminalInput {
-                        task: started.task(),
-                        attempt_id: started.attempt_id().as_str(),
-                        started_at_ms: started.started_at_ms(),
-                        transition: SchedulerTaskAttemptLifecycleTransition::Completed,
-                        reason: "scheduler task attempt completed",
-                        error_summary: None,
-                        selected_dispatch: None,
-                        terminal_mutation: None,
-                    })?;
+                    self.record_scheduler_task_attempt_terminal(
+                        WorkflowSchedulerTaskAttemptTerminalInput {
+                            task: started.task(),
+                            attempt_id: started.attempt_id().as_str(),
+                            started_at_ms: started.started_at_ms(),
+                            transition: SchedulerTaskAttemptLifecycleTransition::Completed,
+                            reason: "scheduler task attempt completed",
+                            error_summary: None,
+                            selected_dispatch: None,
+                            terminal_mutation: None,
+                        },
+                    )?;
                 }
                 Err(
                     crate::scheduler::WorkflowSchedulerTaskOrchestratorError::NonRuntimeTaskAdapter(
@@ -541,16 +543,18 @@ impl<'a> WorkflowSchedulerSessionRunner<'a> {
                             &error,
                         );
                     if failed.is_ok() {
-                        self.record_scheduler_task_attempt_terminal(WorkflowSchedulerTaskAttemptTerminalInput {
-                            task: started.task(),
-                            attempt_id: started.attempt_id().as_str(),
-                            started_at_ms: started.started_at_ms(),
-                            transition: SchedulerTaskAttemptLifecycleTransition::Failed,
-                            reason: "scheduler non-runtime task execution failed",
-                            error_summary: Some(error.to_string()),
-                            selected_dispatch: None,
-                            terminal_mutation: None,
-                        })?;
+                        self.record_scheduler_task_attempt_terminal(
+                            WorkflowSchedulerTaskAttemptTerminalInput {
+                                task: started.task(),
+                                attempt_id: started.attempt_id().as_str(),
+                                started_at_ms: started.started_at_ms(),
+                                transition: SchedulerTaskAttemptLifecycleTransition::Failed,
+                                reason: "scheduler non-runtime task execution failed",
+                                error_summary: Some(error.to_string()),
+                                selected_dispatch: None,
+                                terminal_mutation: None,
+                            },
+                        )?;
                     }
                     return Err(WorkflowServiceError::InvalidRequest(format!(
                         "scheduler non-runtime task execution failed: {error}"
diff --git a/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs b/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs
index e632a73db..2f2116309 100644
--- a/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs
+++ b/crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs
@@ -1124,12 +1124,14 @@ impl WorkflowTaskExecutionWorkerOutcome {
         response: WorkflowRunResponse,
         diagnostics: Vec,
     ) -> Self {
-        Self::RuntimeBranchCompleted(Box::new(WorkflowTaskExecutionWorkerRuntimeBranchCompletedOutcome {
-            session_id: command.session_id.clone(),
-            workflow_run_id: command.workflow_run_id.clone(),
-            response,
-            diagnostics,
-        }))
+        Self::RuntimeBranchCompleted(Box::new(
+            WorkflowTaskExecutionWorkerRuntimeBranchCompletedOutcome {
+                session_id: command.session_id.clone(),
+                workflow_run_id: command.workflow_run_id.clone(),
+                response,
+                diagnostics,
+            },
+        ))
     }
 
     pub(super) fn runtime_branch_failed(
@@ -1369,14 +1371,14 @@ async fn claim_and_execute_runtime_branch_event(
         )
         .await
         {
-            Ok(response) => WorkflowTaskExecutionWorkerOutcome::RuntimeBranchCompleted(
-                Box::new(WorkflowTaskExecutionWorkerRuntimeBranchCompletedOutcome {
+            Ok(response) => WorkflowTaskExecutionWorkerOutcome::RuntimeBranchCompleted(Box::new(
+                WorkflowTaskExecutionWorkerRuntimeBranchCompletedOutcome {
                     session_id: command.session_id.clone(),
                     workflow_run_id: command.workflow_run_id.clone(),
                     response,
                     diagnostics: Vec::new(),
-                }),
-            ),
+                },
+            )),
             Err(error) => WorkflowTaskExecutionWorkerOutcome::runtime_branch_failed(
                 command,
                 error.to_string(),
diff --git a/crates/pantograph-workflow-service/src/workflow/tests/task_binding_resolution.rs b/crates/pantograph-workflow-service/src/workflow/tests/task_binding_resolution.rs
index 186d08109..18f14a6a8 100644
--- a/crates/pantograph-workflow-service/src/workflow/tests/task_binding_resolution.rs
+++ b/crates/pantograph-workflow-service/src/workflow/tests/task_binding_resolution.rs
@@ -33,8 +33,8 @@ fn workflow_run_id() -> WorkflowRunId {
 
 fn inference_projection() -> WorkflowSchedulerInferenceTaskProjections {
     WorkflowSchedulerInferenceTaskProjections::from_records(vec![
-        WorkflowSchedulerInferenceTaskProjection::Ready(
-            Box::new(WorkflowSchedulerReadyInferenceTaskProjection {
+        WorkflowSchedulerInferenceTaskProjection::Ready(Box::new(
+            WorkflowSchedulerReadyInferenceTaskProjection {
                 node_id: SchedulerNodeId::parse("infer").expect("node id"),
                 descriptor_fingerprint: InferenceInterfaceFingerprint::parse("iface.binding.v1")
                     .expect("fingerprint"),
@@ -50,8 +50,8 @@ fn inference_projection() -> WorkflowSchedulerInferenceTaskProjections {
                 trait_settings: Vec::new(),
                 estimate_hints: resource_estimate_hints(),
                 dependency_readiness_source: dependency_readiness_source("iface.binding.v1"),
-            }),
-        ),
+            },
+        )),
     ])
     .expect("projection")
 }
diff --git a/crates/pantograph-workflow-service/src/workflow/tests/task_graph.rs b/crates/pantograph-workflow-service/src/workflow/tests/task_graph.rs
index ec9a18eaa..30d8d4611 100644
--- a/crates/pantograph-workflow-service/src/workflow/tests/task_graph.rs
+++ b/crates/pantograph-workflow-service/src/workflow/tests/task_graph.rs
@@ -44,8 +44,8 @@ fn ready_inference_projection(
     estimate_hints: Vec,
 ) -> WorkflowSchedulerInferenceTaskProjections {
     WorkflowSchedulerInferenceTaskProjections::from_records(vec![
-        WorkflowSchedulerInferenceTaskProjection::Ready(
-            Box::new(WorkflowSchedulerReadyInferenceTaskProjection {
+        WorkflowSchedulerInferenceTaskProjection::Ready(Box::new(
+            WorkflowSchedulerReadyInferenceTaskProjection {
                 node_id: SchedulerNodeId::parse("infer").expect("node id"),
                 descriptor_fingerprint: InferenceInterfaceFingerprint::parse("iface.test.v1")
                     .expect("fingerprint"),
@@ -70,8 +70,8 @@ fn ready_inference_projection(
                 }],
                 estimate_hints,
                 dependency_readiness_source: dependency_readiness_source("iface.test.v1"),
-            }),
-        ),
+            },
+        )),
     ])
     .expect("projection")
 }
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pinned-rustfmt-correction.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pinned-rustfmt-correction.md
new file mode 100644
index 000000000..6ae6c11db
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-pinned-rustfmt-correction.md
@@ -0,0 +1,21 @@
+# Pinned rustfmt correction for the reviewed repair stack
+
+The full PR #39 format audit failed on wrapping and indentation introduced by recent boxed constructors and terminal calls. Reproduced locally with the repository-pinned Rust 1.92.0 toolchain: cargo 1.92.0 (344c4567c 2025-10-21), rustfmt 1.8.0-stable (ded5c06cf2 2025-12-08). Ran the actual `cargo fmt --all -- --check` before and after, rather than treating focused syntax/whitespace checks as format qualification.
+
+Formatted only the eleven files named by the pre-check (including the subsequent registry test assertion). No other baseline files were changed. The full post-check exits zero. A Rust lexer comparison preserves every non-whitespace token after ignoring optional trailing commas before closing delimiters; string/comment tokens remain exact. The registry test file has exact non-whitespace token equality. These are formatter changes, with no intended behavior or public API change; hosted aggregate qualification remains required.
+
+Changed source files:
+
+- `crates/pantograph-diagnostics-ledger/src/tests.rs`
+- `crates/pantograph-embedded-runtime/src/node_execution_ledger.rs`
+- `crates/pantograph-runtime-registry/src/technical_fit_tests.rs`
+- `crates/pantograph-workflow-service/src/graph/inference_validation_state.rs`
+- `crates/pantograph-workflow-service/src/scheduler/task_orchestrator.rs`
+- `crates/pantograph-workflow-service/src/workflow/executable_validation_snapshot.rs`
+- `crates/pantograph-workflow-service/src/workflow/session_execution_api.rs`
+- `crates/pantograph-workflow-service/src/workflow/session_scheduler_runner.rs`
+- `crates/pantograph-workflow-service/src/workflow/task_execution_worker.rs`
+- `crates/pantograph-workflow-service/src/workflow/tests/task_binding_resolution.rs`
+- `crates/pantograph-workflow-service/src/workflow/tests/task_graph.rs`
+
+The accepted registry commit c369cbe4b12b563eb8a5d4419e6e0ed729721137 and PR #39 history remain preserved. Root source review accepted frozen tree 62f3f04d4017d29592cc243167ae46841bcaa901 after checking the file list, pinned formatter/token receipts and representative full constructor/call diffs. The earlier red hosted format runs remain recorded; actual full hosted format and all other aggregate gates still require qualification.

From c023cec148b1c4a340bcedd4d7f01f327a2eb2f5 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 08:41:53 -0700
Subject: [PATCH 49/65] refactor(embedded): preserve defaults and routing while
 clearing style lints

---
 .github/workflows/quality-gates.yml           |  2 ++
 .../src/dependency_inventory_dispatch.rs      |  2 +-
 .../runtime_dispatch_candidate_provider.rs    | 30 ++++++++++++-------
 .../dependency_environment/helpers.rs         | 30 ++++++++++++++++---
 .../src/task_executor/puma_lib.rs             |  2 +-
 ...2026-10-03-embedded-runtime-lint-basics.md |  7 +++++
 6 files changed, 57 insertions(+), 16 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-runtime-lint-basics.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 2edc2cb9d..8a0d02108 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -251,6 +251,8 @@ jobs:
       - name: Run workflow-nodes tests
         run: cargo test -p workflow-nodes --lib
 
+      - name: Run embedded runtime unit tests
+        run: cargo test -p pantograph-embedded-runtime --lib
       - name: Run runtime registry contract tests
         run: cargo test -p pantograph-runtime-registry
       - name: Run task summary contract tests
diff --git a/crates/pantograph-embedded-runtime/src/dependency_inventory_dispatch.rs b/crates/pantograph-embedded-runtime/src/dependency_inventory_dispatch.rs
index 5e6894a0d..64908d305 100644
--- a/crates/pantograph-embedded-runtime/src/dependency_inventory_dispatch.rs
+++ b/crates/pantograph-embedded-runtime/src/dependency_inventory_dispatch.rs
@@ -360,7 +360,7 @@ impl DependencyInventoryProvider for NotImplementedDependencyInventoryProvider {
         let failure = PackageReadinessProbeFailure::new(
             PackageReadinessProviderDiagnosticCode::ProbeNotImplemented,
             None,
-            CapabilityAvailabilityReason::parse(¬_implemented_reason(&request.payload))
+            CapabilityAvailabilityReason::parse(not_implemented_reason(&request.payload))
                 .expect("inventory provider not implemented reason is valid"),
         );
         let (rows, diagnostics) =
diff --git a/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs b/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs
index a00456cce..ade14c215 100644
--- a/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs
+++ b/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs
@@ -70,7 +70,7 @@ const INCOMPATIBLE_RUNTIME_BACKEND_HINT: &str =
 const MISSING_RUNTIME_DISPATCH_EVIDENCE_HINT: &str =
     "embedded_runtime_dispatch_candidate_provider.missing_runtime_dispatch_evidence";
 
-#[derive(Clone)]
+#[derive(Clone, Default)]
 pub(crate) struct EmbeddedRuntimeDispatchCandidateProvider {
     source_snapshot: EmbeddedRuntimeDispatchCandidateSource,
     resource_facts_source: Option,
@@ -88,15 +88,6 @@ impl Default for EmbeddedRuntimeDispatchCandidateSource {
     }
 }
 
-impl Default for EmbeddedRuntimeDispatchCandidateProvider {
-    fn default() -> Self {
-        Self {
-            source_snapshot: EmbeddedRuntimeDispatchCandidateSource::default(),
-            resource_facts_source: None,
-        }
-    }
-}
-
 #[derive(Debug, Clone, PartialEq, Eq)]
 struct EmbeddedRuntimeDispatchCandidateDraft {
     candidate_id: SchedulerDispatchCandidateId,
@@ -1011,6 +1002,25 @@ mod tests {
         RuntimeDispatchCapabilityFactsProjection, RuntimeDispatchRuntimeCapabilityFacts,
     };
 
+    #[test]
+    fn default_provider_retains_empty_snapshot_and_fail_closed_diagnostics() {
+        let provider = EmbeddedRuntimeDispatchCandidateProvider::default();
+        assert!(provider.resource_facts_source.is_none());
+        let EmbeddedRuntimeDispatchCandidateSource::Snapshot(snapshot) = provider.source_snapshot
+        else {
+            panic!("default provider must retain snapshot mode");
+        };
+        assert_eq!(
+            snapshot,
+            EmbeddedRuntimeDispatchCandidateSourceSnapshot::default()
+        );
+        let diagnostics = fail_closed_diagnostics(&snapshot, &path_free_model_ref());
+        assert_eq!(diagnostics.len(), 4);
+        assert!(diagnostics.iter().all(|diagnostic| diagnostic.code
+            == SchedulerDispatchSelectionDiagnosticCode::NoCandidates
+            && diagnostic.severity == SchedulerDispatchSelectionDiagnosticSeverity::Error));
+    }
+
     #[test]
     fn fail_closed_provider_reports_missing_source_facts() {
         let diagnostics = fail_closed_diagnostics(
diff --git a/crates/pantograph-embedded-runtime/src/task_executor/dependency_environment/helpers.rs b/crates/pantograph-embedded-runtime/src/task_executor/dependency_environment/helpers.rs
index 52a1db68e..72b252f4c 100644
--- a/crates/pantograph-embedded-runtime/src/task_executor/dependency_environment/helpers.rs
+++ b/crates/pantograph-embedded-runtime/src/task_executor/dependency_environment/helpers.rs
@@ -103,10 +103,7 @@ impl TauriTaskExecutor {
     }
 
     pub(in crate::task_executor) fn python_runtime_handles_node(node_type: &str) -> bool {
-        match node_type {
-            "audio-generation" | "onnx-inference" => true,
-            _ => false,
-        }
+        matches!(node_type, "audio-generation" | "onnx-inference")
     }
 
     pub(in crate::task_executor) fn sanitize_key_component(raw: &str) -> String {
@@ -130,3 +127,28 @@ impl TauriTaskExecutor {
         format!("{:016x}", digest)
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::TauriTaskExecutor;
+
+    #[test]
+    fn python_runtime_node_classification_keeps_exact_accepted_set() {
+        for node_type in ["audio-generation", "onnx-inference"] {
+            assert!(TauriTaskExecutor::python_runtime_handles_node(node_type));
+        }
+        for node_type in [
+            "llm-inference",
+            "embedding",
+            "vision",
+            "",
+            "Audio-generation",
+            "onnx-inference ",
+        ] {
+            assert!(
+                !TauriTaskExecutor::python_runtime_handles_node(node_type),
+                "{node_type}"
+            );
+        }
+    }
+}
diff --git a/crates/pantograph-embedded-runtime/src/task_executor/puma_lib.rs b/crates/pantograph-embedded-runtime/src/task_executor/puma_lib.rs
index a6da852cd..02135e330 100644
--- a/crates/pantograph-embedded-runtime/src/task_executor/puma_lib.rs
+++ b/crates/pantograph-embedded-runtime/src/task_executor/puma_lib.rs
@@ -151,7 +151,7 @@ impl TauriTaskExecutor {
             if let Some(selector_access) =
                 extensions.get::>(PUMAS_SELECTOR_ACCESS)
             {
-                match Self::resolve_puma_lib_selected_detail(&selector_access, requested_model_id)
+                match Self::resolve_puma_lib_selected_detail(selector_access, requested_model_id)
                     .await
                 {
                     Ok(Some(detail)) => {
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-runtime-lint-basics.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-runtime-lint-basics.md
new file mode 100644
index 000000000..fd1b2ba93
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-runtime-lint-basics.md
@@ -0,0 +1,7 @@
+# Embedded runtime mechanical lint basics
+
+Fresh PR #40 Clippy clears runtime-registry and exposes thirteen embedded-runtime library findings. This first area milestone addresses only four mechanical findings: derive the candidate provider's identical default, remove a redundant inventory-reason borrow and selector-access borrow, and express the Python runtime node predicate with matches!. Payload/error layout and arity findings remain separate.
+
+Regressions prove the default still uses an empty snapshot with no resource facts and four fail-closed diagnostics, and the helper accepts exactly audio-generation/onnx-inference while rejecting other, differently cased and padded node identifiers. CI runs the complete embedded-runtime unit suite with its default backend features. Public APIs, runtime policy and errors are unchanged.
+
+The same checkout is reused on an embedded-runtime branch, preserving prior refs. The actual full pinned cargo fmt --all -- --check passes locally; root source review accepted frozen tree 405aea674557289e585587c1b23b48e381acbb0d; hosted tests remain required. No local Rust execution or heavy build is claimed.

From b73c5d1f1912ec1726b7f9c7b68e98a3cfc6a51a Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 08:48:13 -0700
Subject: [PATCH 50/65] refactor(embedded): preserve artifact error source
 through boxed payload

---
 .../src/runtime_host_media_artifact_sink.rs   | 87 +++++++++++++++----
 ...2026-10-03-artifact-write-error-payload.md |  7 ++
 2 files changed, 76 insertions(+), 18 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-artifact-write-error-payload.md

diff --git a/crates/pantograph-embedded-runtime/src/runtime_host_media_artifact_sink.rs b/crates/pantograph-embedded-runtime/src/runtime_host_media_artifact_sink.rs
index c80763974..bc4bb4f13 100644
--- a/crates/pantograph-embedded-runtime/src/runtime_host_media_artifact_sink.rs
+++ b/crates/pantograph-embedded-runtime/src/runtime_host_media_artifact_sink.rs
@@ -37,14 +37,18 @@ pub(crate) enum RuntimeHostMediaArtifactSinkError {
         image_index: usize,
         message: String,
     },
-    #[error("runtime-host image output artifact write failed for {task_id}.{port_id}[{image_index}]: {source}")]
-    ArtifactWriteFailed {
-        task_id: String,
-        port_id: String,
-        image_index: usize,
-        #[source]
-        source: WorkflowServiceError,
-    },
+    #[error(transparent)]
+    ArtifactWriteFailed(Box),
+}
+
+#[derive(Debug, Error)]
+#[error("runtime-host image output artifact write failed for {task_id}.{port_id}[{image_index}]: {source}")]
+pub(crate) struct RuntimeHostArtifactWriteFailure {
+    task_id: String,
+    port_id: String,
+    image_index: usize,
+    #[source]
+    source: WorkflowServiceError,
 }
 
 #[derive(Clone)]
@@ -101,14 +105,16 @@ impl RuntimeHostMediaArtifactSink for WorkflowServiceRuntimeHostMediaArtifactSin
                 revision_index: Some(request.image_index as u64),
                 body,
             })
-            .map_err(
-                |source| RuntimeHostMediaArtifactSinkError::ArtifactWriteFailed {
-                    task_id: request.task_id.to_string(),
-                    port_id: request.port_id.to_string(),
-                    image_index: request.image_index,
-                    source,
-                },
-            )?;
+            .map_err(|source| {
+                RuntimeHostMediaArtifactSinkError::ArtifactWriteFailed(Box::new(
+                    RuntimeHostArtifactWriteFailure {
+                        task_id: request.task_id.to_string(),
+                        port_id: request.port_id.to_string(),
+                        image_index: request.image_index,
+                        source,
+                    },
+                ))
+            })?;
 
         Ok(RuntimeHostExecutionMediaArtifactRef {
             artifact_id: descriptor.artifact_id,
@@ -316,11 +322,56 @@ mod tests {
             })
             .expect_err("missing artifact store must fail closed");
 
-        let RuntimeHostMediaArtifactSinkError::ArtifactWriteFailed { source, .. } = error else {
+        let concrete_source = std::error::Error::source(&error)
+            .expect("artifact write failure retains its source")
+            .downcast_ref::()
+            .expect("source remains the concrete workflow error");
+        assert_eq!(concrete_source.code(), WorkflowErrorCode::InternalError);
+        let RuntimeHostMediaArtifactSinkError::ArtifactWriteFailed(failure) = error else {
             panic!("expected artifact write failure");
         };
+        assert_eq!(failure.source.code(), WorkflowErrorCode::InternalError);
+        assert!(failure
+            .source
+            .to_string()
+            .contains("artifact store io error"));
+    }
+
+    #[test]
+    fn boxed_write_failure_preserves_exact_display_and_concrete_source_chain() {
+        let error = RuntimeHostMediaArtifactSinkError::ArtifactWriteFailed(Box::new(
+            RuntimeHostArtifactWriteFailure {
+                task_id: "task.image".to_string(),
+                port_id: "image".to_string(),
+                image_index: 2,
+                source: WorkflowServiceError::Internal("fixture failure".to_string()),
+            },
+        ));
+        assert_eq!(error.to_string(), "runtime-host image output artifact write failed for task.image.image[2]: internal_error: fixture failure");
+        let source = std::error::Error::source(&error).unwrap();
+        assert!(source
+            .downcast_ref::()
+            .is_none());
+        assert!(source.downcast_ref::>().is_none());
+        let source = source
+            .downcast_ref::()
+            .expect("unboxed workflow source identity");
         assert_eq!(source.code(), WorkflowErrorCode::InternalError);
-        assert!(source.to_string().contains("artifact store io error"));
+        assert_eq!(source.to_string(), "internal_error: fixture failure");
+        assert!(std::error::Error::source(source).is_none());
+        assert!(std::mem::size_of::() <= 128);
+    }
+
+    #[test]
+    fn invalid_image_failure_retains_message_and_absent_source() {
+        let error = RuntimeHostMediaArtifactSinkError::InvalidImagePayload {
+            task_id: "task.image".to_string(),
+            port_id: "image".to_string(),
+            image_index: 2,
+            message: "invalid fixture bytes".to_string(),
+        };
+        assert_eq!(error.to_string(), "runtime-host image output base64 decode failed for task.image.image[2]: invalid fixture bytes");
+        assert!(std::error::Error::source(&error).is_none());
     }
 
     fn artifact_writer(temp: &TempDir) -> WorkflowArtifactWriter {
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-artifact-write-error-payload.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-artifact-write-error-payload.md
new file mode 100644
index 000000000..1bc1eb74c
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-artifact-write-error-payload.md
@@ -0,0 +1,7 @@
+# Private artifact-write failure payload and source identity
+
+Fresh embedded-runtime Clippy identifies three large-error returns caused by ArtifactWriteFailed carrying a raw WorkflowServiceError plus attribution. Keep all media sink trait and Result signatures unchanged, but box a private RuntimeHostArtifactWriteFailure payload containing the unchanged raw source. The enum variant is error-transparent. One production constructor and one existing test pattern are migrated.
+
+Directly boxing the #[source] field was rejected during design: pinned thiserror 1.0.69 calls source.as_dyn_error(), which can expose Box to downcast consumers. The transparent payload delegates Error::source through its own raw #[source] field instead. Tests explicitly require the concrete WorkflowServiceError downcast and classification, reject wrapper/boxed-source downcasts, preserve exact outer/source messages, and retain the invalid-image variant's message and absent source. The real missing-artifact-store failure now also checks concrete source identity. A private enum-size bound targets the reported large errors.
+
+Root approved this source-chain-preserving design. Full pinned cargo fmt and staged checks passed. Root source review accepted both files at frozen tree 002ee22ba66b1a957906fc530d1ac080f54c8dbe; actual hosted execution remains required. No public DTO/API or failure classification change is intended.

From 788ce31ec3f522cc2a243455f298fbe31c566ff8 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 09:05:54 -0700
Subject: [PATCH 51/65] Reduce private embedded source payload sizes

---
 .../src/inference_interface_facts_provider.rs |  4 +-
 .../src/pumas_dispatch_package_facts.rs       |  4 +-
 .../runtime_dispatch_candidate_provider.rs    | 53 ++++++++++++++++---
 ...26-10-03-embedded-private-source-layout.md |  9 ++++
 4 files changed, 58 insertions(+), 12 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-private-source-layout.md

diff --git a/crates/pantograph-embedded-runtime/src/inference_interface_facts_provider.rs b/crates/pantograph-embedded-runtime/src/inference_interface_facts_provider.rs
index d6854450e..69efd748c 100644
--- a/crates/pantograph-embedded-runtime/src/inference_interface_facts_provider.rs
+++ b/crates/pantograph-embedded-runtime/src/inference_interface_facts_provider.rs
@@ -357,7 +357,7 @@ mod tests {
 
         let facts = resolver_facts_from_sources(
             PumasDispatchPackageFactsBridgeOutcome::Projected {
-                facts: package,
+                facts: Box::new(package),
                 diagnostics: Vec::new(),
             },
             &runtime,
@@ -388,7 +388,7 @@ mod tests {
     fn missing_runtime_facts_keep_capability_but_publish_no_runtime_availability() {
         let facts = resolver_facts_from_sources(
             PumasDispatchPackageFactsBridgeOutcome::Projected {
-                facts: projected_package_facts(),
+                facts: Box::new(projected_package_facts()),
                 diagnostics: Vec::new(),
             },
             &RuntimeDispatchCapabilityFactsOutcome::Unavailable {
diff --git a/crates/pantograph-embedded-runtime/src/pumas_dispatch_package_facts.rs b/crates/pantograph-embedded-runtime/src/pumas_dispatch_package_facts.rs
index 7e561d403..16366135d 100644
--- a/crates/pantograph-embedded-runtime/src/pumas_dispatch_package_facts.rs
+++ b/crates/pantograph-embedded-runtime/src/pumas_dispatch_package_facts.rs
@@ -84,7 +84,7 @@ pub(crate) enum PumasDispatchPackageFactsDiagnosticCode {
 #[derive(Debug, Clone, PartialEq)]
 pub(crate) enum PumasDispatchPackageFactsBridgeOutcome {
     Projected {
-        facts: PumasDispatchPackageFactsProjection,
+        facts: Box,
         diagnostics: Vec,
     },
     Unavailable {
@@ -210,7 +210,7 @@ fn validate_and_project_dispatch_package_facts(
     }
 
     PumasDispatchPackageFactsBridgeOutcome::Projected {
-        facts: project_dispatch_package_facts(facts),
+        facts: Box::new(project_dispatch_package_facts(facts)),
         diagnostics,
     }
 }
diff --git a/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs b/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs
index ade14c215..f167e753c 100644
--- a/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs
+++ b/crates/pantograph-embedded-runtime/src/runtime_dispatch_candidate_provider.rs
@@ -78,13 +78,13 @@ pub(crate) struct EmbeddedRuntimeDispatchCandidateProvider {
 
 #[derive(Clone)]
 enum EmbeddedRuntimeDispatchCandidateSource {
-    Snapshot(EmbeddedRuntimeDispatchCandidateSourceSnapshot),
+    Snapshot(Box),
     Store(EmbeddedRuntimeDispatchSourceFactSnapshotStore),
 }
 
 impl Default for EmbeddedRuntimeDispatchCandidateSource {
     fn default() -> Self {
-        Self::Snapshot(EmbeddedRuntimeDispatchCandidateSourceSnapshot::default())
+        Self::Snapshot(Box::default())
     }
 }
 
@@ -113,7 +113,9 @@ impl EmbeddedRuntimeDispatchCandidateProvider {
         source_snapshot: EmbeddedRuntimeDispatchCandidateSourceSnapshot,
     ) -> Self {
         Self {
-            source_snapshot: EmbeddedRuntimeDispatchCandidateSource::Snapshot(source_snapshot),
+            source_snapshot: EmbeddedRuntimeDispatchCandidateSource::Snapshot(Box::new(
+                source_snapshot,
+            )),
             resource_facts_source: None,
         }
     }
@@ -172,7 +174,7 @@ impl EmbeddedRuntimeDispatchCandidateSource {
         model_ref: &PumasModelRef,
     ) -> EmbeddedRuntimeDispatchCandidateSourceSnapshot {
         match self {
-            Self::Snapshot(snapshot) => snapshot.clone(),
+            Self::Snapshot(snapshot) => snapshot.as_ref().clone(),
             Self::Store(store) => store.snapshot_for_dispatch(model_ref, current_time_ms()),
         }
     }
@@ -1002,6 +1004,35 @@ mod tests {
         RuntimeDispatchCapabilityFactsProjection, RuntimeDispatchRuntimeCapabilityFacts,
     };
 
+    #[test]
+    fn private_source_indirections_preserve_raw_facts_and_snapshot_clone_isolation() {
+        let expected = source_snapshot(Vec::new(), Vec::new());
+        let expected_facts = pumas_package_facts(vec![inference::BackendHintLabel::Diffusers]);
+        let provider =
+            EmbeddedRuntimeDispatchCandidateProvider::with_source_snapshot(expected.clone());
+        let mut returned = provider
+            .source_snapshot
+            .snapshot_for_dispatch(&path_free_model_ref());
+        assert_eq!(returned, expected);
+        let Some(PumasDispatchPackageFactsBridgeOutcome::Projected { facts, diagnostics }) =
+            returned.pumas_package_facts.as_mut()
+        else {
+            panic!("snapshot must preserve projected package facts");
+        };
+        assert_eq!(facts.as_ref(), &expected_facts);
+        assert!(diagnostics.is_empty());
+        facts.model_ref.model_id = "changed-returned-copy".to_string();
+        returned.snapshot_version = 99;
+        assert_eq!(
+            provider
+                .source_snapshot
+                .snapshot_for_dispatch(&path_free_model_ref()),
+            expected
+        );
+        assert!(std::mem::size_of::() <= 64);
+        assert!(std::mem::size_of::() <= 64);
+    }
+
     #[test]
     fn default_provider_retains_empty_snapshot_and_fail_closed_diagnostics() {
         let provider = EmbeddedRuntimeDispatchCandidateProvider::default();
@@ -1011,7 +1042,7 @@ mod tests {
             panic!("default provider must retain snapshot mode");
         };
         assert_eq!(
-            snapshot,
+            *snapshot,
             EmbeddedRuntimeDispatchCandidateSourceSnapshot::default()
         );
         let diagnostics = fail_closed_diagnostics(&snapshot, &path_free_model_ref());
@@ -1245,7 +1276,9 @@ mod tests {
         let provider = EmbeddedRuntimeDispatchCandidateProvider::with_source_snapshot(
             EmbeddedRuntimeDispatchCandidateSourceSnapshot {
                 pumas_package_facts: Some(PumasDispatchPackageFactsBridgeOutcome::Projected {
-                    facts: pumas_package_facts(vec![inference::BackendHintLabel::Diffusers]),
+                    facts: Box::new(pumas_package_facts(vec![
+                        inference::BackendHintLabel::Diffusers,
+                    ])),
                     diagnostics: Vec::new(),
                 }),
                 runtime_capability_facts: Some(RuntimeDispatchCapabilityFactsOutcome::Projected {
@@ -1405,7 +1438,9 @@ mod tests {
         let provider = EmbeddedRuntimeDispatchCandidateProvider::with_source_snapshot(
             EmbeddedRuntimeDispatchCandidateSourceSnapshot {
                 pumas_package_facts: Some(PumasDispatchPackageFactsBridgeOutcome::Projected {
-                    facts: pumas_package_facts(vec![inference::BackendHintLabel::Diffusers]),
+                    facts: Box::new(pumas_package_facts(vec![
+                        inference::BackendHintLabel::Diffusers,
+                    ])),
                     diagnostics: Vec::new(),
                 }),
                 runtime_capability_facts: Some(RuntimeDispatchCapabilityFactsOutcome::Projected {
@@ -1546,7 +1581,9 @@ mod tests {
     ) -> EmbeddedRuntimeDispatchCandidateSourceSnapshot {
         EmbeddedRuntimeDispatchCandidateSourceSnapshot {
             pumas_package_facts: Some(PumasDispatchPackageFactsBridgeOutcome::Projected {
-                facts: pumas_package_facts(vec![inference::BackendHintLabel::Diffusers]),
+                facts: Box::new(pumas_package_facts(vec![
+                    inference::BackendHintLabel::Diffusers,
+                ])),
                 diagnostics: Vec::new(),
             }),
             runtime_capability_facts: Some(RuntimeDispatchCapabilityFactsOutcome::Projected {
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-private-source-layout.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-private-source-layout.md
new file mode 100644
index 000000000..a9b50dbc4
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-private-source-layout.md
@@ -0,0 +1,9 @@
+# Private embedded source payload layout
+
+Fresh PR42 Clippy reports large variants in the package-facts bridge outcome and dispatch candidate source. Box only Projected.facts and Snapshot, keeping raw projection/snapshot DTOs and public APIs unchanged. The package bridge has one production constructor and five fixture constructors across three files. Candidate source has two constructors (default and explicit snapshot) and one raw-cloning reader. Borrowed readers retain deref coercion; the owned reader explicitly clones the raw snapshot rather than returning a box.
+
+The regression compares complete raw package facts and snapshots, mutates a returned nested fact and snapshot version, then proves provider state remains isolated. Existing source-owner, stale/missing/blocked, resolver, and candidate-selection tests remain in the full embedded unit gate. Size bounds apply only to these private enums; no measured throughput claim is made.
+
+Prior artifact-error head b73c5d1 has actual hosted concrete source/downcast and artifact-write tests passing. Its full embedded unit run has 449 passed and three failures: image dispatch gets RetryDeferred instead of RuntimeHostCompleted; resource-backed validation gets Blocked instead of Executable; publication dependency readiness gets Blocked instead of RequestReady with DriftDetected. Those failures remain separate and are not waived or classified as baseline without exact comparison. Format and workspace default/all-feature compilation pass at that head.
+
+Root source review accepted all four files at frozen tree 614eb5f7f61ee8ffa96d4d07cdaa238f7275e1a2. Fresh hosted execution remains required. Local Rust execution is not attempted under the shared disk constraint; full pinned formatting is checked separately.

From bbce93c4890fad38ec7bb0d901bf3463e4e0a763 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 09:12:10 -0700
Subject: [PATCH 52/65] Align embedded fixtures with validation and reservation
 contracts

---
 .../lib_tests/workflow_run_execution_tests.rs | 37 ++++++++++++++++---
 .../src/workflow_service_composition.rs       | 28 +++++++++++++-
 .../2026-10-03-embedded-contract-fixtures.md  |  9 +++++
 3 files changed, 67 insertions(+), 7 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-contract-fixtures.md

diff --git a/crates/pantograph-embedded-runtime/src/lib_tests/workflow_run_execution_tests.rs b/crates/pantograph-embedded-runtime/src/lib_tests/workflow_run_execution_tests.rs
index 31a01ef9c..40fe05b9c 100644
--- a/crates/pantograph-embedded-runtime/src/lib_tests/workflow_run_execution_tests.rs
+++ b/crates/pantograph-embedded-runtime/src/lib_tests/workflow_run_execution_tests.rs
@@ -34,9 +34,10 @@ use pantograph_inference_interface_contracts::{
 use pantograph_runtime_attribution::WorkflowVersionRecord;
 use pantograph_runtime_host_contracts::{
     ReservationLifecycleApplication, ReservationLifecycleApplicationState,
-    ReservationLifecycleEvent, ReservationLifecycleOutcome, ReservationLifecyclePort,
-    ReservationLifecyclePortError, RESERVATION_LIFECYCLE_CONTRACT_VERSION,
-    RUNTIME_SESSION_LOAD_PROOF_CONTRACT_VERSION,
+    ReservationLifecycleDiagnostic, ReservationLifecycleDiagnosticCode,
+    ReservationLifecycleDiagnosticSeverity, ReservationLifecycleEvent, ReservationLifecycleOutcome,
+    ReservationLifecyclePort, ReservationLifecyclePortError,
+    RESERVATION_LIFECYCLE_CONTRACT_VERSION, RUNTIME_SESSION_LOAD_PROOF_CONTRACT_VERSION,
 };
 use pantograph_scheduler::{
     SchedulerDispatchCandidateId, SchedulerEstimateHint, SchedulerEstimateHintKind,
@@ -239,17 +240,41 @@ async fn workflow_execution_session_dispatches_through_production_embedded_image
     assert_eq!(body.response.media_type, "image/png");
     assert_eq!(dependency_readiness_work_queue.len(), 1);
     assert_eq!(source_refresher.model_refs(), vec![MODEL_ID.to_string()]);
+    let lifecycle_events = reservation_lifecycle_port.events();
     assert_eq!(
-        reservation_lifecycle_port
-            .events()
+        lifecycle_events
             .iter()
             .map(|event| &event.outcome)
             .collect::>(),
         vec![
             &ReservationLifecycleOutcome::DispatchStarted,
-            &ReservationLifecycleOutcome::RuntimeHostCompleted,
+            &ReservationLifecycleOutcome::RetryDeferred,
         ]
     );
+    let deferred = &lifecycle_events[1];
+    assert_eq!(
+        deferred.reservation_lease_id,
+        lifecycle_events[0].reservation_lease_id
+    );
+    assert_eq!(
+        deferred.reservation_lease_id.as_str(),
+        "reservation.embedded_runtime_session_test.infer"
+    );
+    assert_eq!(
+        deferred.workflow_run_id,
+        lifecycle_events[0].workflow_run_id
+    );
+    assert_eq!(deferred.task_id, lifecycle_events[0].task_id);
+    assert_eq!(deferred.candidate_id, lifecycle_events[0].candidate_id);
+    assert_eq!(
+        deferred.diagnostics,
+        vec![ReservationLifecycleDiagnostic {
+            severity: ReservationLifecycleDiagnosticSeverity::Info,
+            code: ReservationLifecycleDiagnosticCode::RetryDeferred,
+            message: "embedded runtime-host image batch member completed".to_string(),
+            hint: None,
+        }]
+    );
     assert_eq!(host.runtime_load_attempts.load(Ordering::SeqCst), 0);
     assert_eq!(host.run_attempts.load(Ordering::SeqCst), 0);
 }
diff --git a/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs b/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs
index 87b076248..023092405 100644
--- a/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs
+++ b/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs
@@ -850,6 +850,12 @@ mod tests {
             .await
             .expect("refresh validation summary");
 
+        assert!(
+            validation.summary.diagnostics.is_empty(),
+            "validation diagnostics: {:?}",
+            validation.summary.diagnostics
+        );
+        assert_eq!(validation.node_projections.len(), 1);
         let summary = validation
             .summary
             .summary
@@ -946,6 +952,21 @@ mod tests {
             )
             .await
             .expect("refresh validation summary");
+        assert!(
+            validation.summary.diagnostics.is_empty(),
+            "validation diagnostics: {:?}",
+            validation.summary.diagnostics
+        );
+        assert_eq!(validation.node_projections.len(), 1);
+        assert_eq!(
+            validation
+                .summary
+                .summary
+                .as_ref()
+                .expect("current validation summary")
+                .status,
+            DraftGraphValidationStatus::Executable
+        );
         let validation_session_id = validation
             .summary
             .validation_session_id
@@ -1435,7 +1456,12 @@ mod tests {
                     position: Position { x: 200.0, y: 0.0 },
                     data: serde_json::json!({
                         "task_kind": "image_generation",
-                        "runtime": "pytorch"
+                        "runtime": "pytorch",
+                        "runtime_source_context": {
+                            "operation_type": "image-generation.txt2img",
+                            "context_shape_key": "txt2img.1024x1024.steps30",
+                            "cancellation_mode": "per-run-fanout"
+                        }
                     }),
                 },
             ],
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-contract-fixtures.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-contract-fixtures.md
new file mode 100644
index 000000000..b7e5ba4ae
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-contract-fixtures.md
@@ -0,0 +1,9 @@
+# Embedded runtime integration fixture contracts
+
+The full default-feature embedded suite was newly enabled on PR42 and exposed three failures. At b73c5d1 it executed 452 tests: 449 passed, three failed. Preserve the full gate and original execution/publication assertions; this milestone changes only fixtures and assertions.
+
+The two resource-backed composition fixtures omit mandatory runtime_source_context. inference_interface_request.rs rejects that absence before facts resolution; aggregate publication makes request diagnostics Blocked. The dependency-action owner maps that blocked summary to DriftDetected, so this downstream code alone does not establish descriptor fingerprint drift. Add the explicit image-generation operation/context/cancellation values already used by the image execution fixture. Assert no validation diagnostics and exactly one projection before the original Executable/RequestReady/publication assertions. No production admission bypass or inferred default is added.
+
+The image test already verifies real production runtime-host output, stored image bytes/media type and no legacy host load/run. Its reservation assertion was outdated: the scheduler explicitly requests DeferToScheduler; the embedded batch response reports DeferredToScheduler even when Completed; the scheduler maps that reservation disposition to RetryDeferred. Preserve every output assertion and require the exact deferred diagnostic (Info, RetryDeferred, successful image-member completion message, no hint) and correlated lease/run/task/candidate across start and terminal events. This asserts deferred reservation ownership without confusing it with failed image execution.
+
+Root source review accepted the complete three-file fixture-only diff at frozen tree d204dd518d905a4d6d1f1fe471678cb511822f5c. Fresh hosted full embedded execution remains pending. Local execution is not claimed; full pinned formatting is checked. The prior three failing assertions remain recorded in hosted logs rather than reclassified without baseline execution.

From badd8026924dab251e2122194ba2b6e9d09f9496 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 09:22:57 -0700
Subject: [PATCH 53/65] Group private inventory diagnostics and reuse stream
 attribution

---
 .../src/dependency_inventory.rs               |  8 ++
 .../dependency_inventory_device_toolchain.rs  | 63 +++++++++-----
 .../dependency_inventory_system_package.rs    | 63 +++++++++-----
 .../src/dependency_inventory_tests.rs         | 25 ++++++
 .../src/task_executor/stream_artifacts.rs     | 85 +++++++++++++++----
 .../2026-10-03-embedded-private-inputs.md     |  9 ++
 6 files changed, 192 insertions(+), 61 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-private-inputs.md

diff --git a/crates/pantograph-embedded-runtime/src/dependency_inventory.rs b/crates/pantograph-embedded-runtime/src/dependency_inventory.rs
index ebb0b0652..d4cfb8a31 100644
--- a/crates/pantograph-embedded-runtime/src/dependency_inventory.rs
+++ b/crates/pantograph-embedded-runtime/src/dependency_inventory.rs
@@ -48,6 +48,14 @@ use crate::dependency_inventory_system_package_source::SystemPackageProviderSour
 use crate::package_readiness_provider::PackageReadinessProbeRunner;
 use crate::python_package_readiness_probe::ProcessPythonPackageReadinessProbeRunner;
 
+/// Provider-owned diagnostic details; request attribution remains with the observer.
+#[cfg(any(test, feature = "standalone"))]
+pub(crate) struct DependencyInventoryDiagnosticInput {
+    pub code: pantograph_dependency_planning::DependencyPlanningDiagnosticCode,
+    pub message: String,
+    pub field_path: &'static str,
+}
+
 /// Request context passed from the snapshot producer to dependency inventory.
 #[derive(Debug, Clone)]
 pub(crate) struct DependencyInventoryRequest {
diff --git a/crates/pantograph-embedded-runtime/src/dependency_inventory_device_toolchain.rs b/crates/pantograph-embedded-runtime/src/dependency_inventory_device_toolchain.rs
index 6455dee2d..e228458eb 100644
--- a/crates/pantograph-embedded-runtime/src/dependency_inventory_device_toolchain.rs
+++ b/crates/pantograph-embedded-runtime/src/dependency_inventory_device_toolchain.rs
@@ -16,7 +16,8 @@ use pantograph_dependency_planning::{
 };
 
 use crate::dependency_inventory::{
-    DependencyInventoryObservation, DependencyInventoryProvider, DependencyInventoryRequest,
+    DependencyInventoryDiagnosticInput, DependencyInventoryObservation,
+    DependencyInventoryProvider, DependencyInventoryRequest,
 };
 use crate::dependency_inventory_device_toolchain_source::DeviceToolchainProviderSource;
 
@@ -128,9 +129,12 @@ fn observe_device_toolchain_binding(
             binding.binding_id.clone(),
             DependencyInventoryObservationState::Missing,
             DependencyEnvironmentValidationState::Valid,
-            DependencyPlanningDiagnosticCode::ArtifactMissing,
-            "Device-toolchain source facts are missing for the requested toolchain.",
-            "dependency_environment.device_toolchain.source",
+            DependencyInventoryDiagnosticInput {
+                code: DependencyPlanningDiagnosticCode::ArtifactMissing,
+                message: "Device-toolchain source facts are missing for the requested toolchain."
+                    .into(),
+                field_path: "dependency_environment.device_toolchain.source",
+            },
             ready_alternatives(&snapshot.rows),
         );
     }
@@ -225,9 +229,11 @@ fn observation_from_source_row(
             binding_id,
             DependencyInventoryObservationState::Unavailable,
             DependencyEnvironmentValidationState::Stale,
-            DependencyPlanningDiagnosticCode::ArtifactStale,
-            "Device-toolchain source facts are stale.",
-            "dependency_environment.device_toolchain.source",
+            DependencyInventoryDiagnosticInput {
+                code: DependencyPlanningDiagnosticCode::ArtifactStale,
+                message: "Device-toolchain source facts are stale.".into(),
+                field_path: "dependency_environment.device_toolchain.source",
+            },
             source_row.alternatives.clone(),
         ),
         (DependencyProviderSourceState::Ready, _) => row(
@@ -242,9 +248,11 @@ fn observation_from_source_row(
             binding_id,
             DependencyInventoryObservationState::Missing,
             DependencyEnvironmentValidationState::Valid,
-            DependencyPlanningDiagnosticCode::ArtifactMissing,
-            "Device-toolchain source facts are missing.",
-            "dependency_environment.device_toolchain.source",
+            DependencyInventoryDiagnosticInput {
+                code: DependencyPlanningDiagnosticCode::ArtifactMissing,
+                message: "Device-toolchain source facts are missing.".into(),
+                field_path: "dependency_environment.device_toolchain.source",
+            },
             source_row.alternatives.clone(),
         ),
         (DependencyProviderSourceState::Failed, _) => observation_with_diagnostic(
@@ -252,9 +260,11 @@ fn observation_from_source_row(
             binding_id,
             DependencyInventoryObservationState::Failed,
             DependencyEnvironmentValidationState::Valid,
-            DependencyPlanningDiagnosticCode::RuntimeUnavailable,
-            "Device-toolchain source reported a failure.",
-            "dependency_environment.device_toolchain.source",
+            DependencyInventoryDiagnosticInput {
+                code: DependencyPlanningDiagnosticCode::RuntimeUnavailable,
+                message: "Device-toolchain source reported a failure.".into(),
+                field_path: "dependency_environment.device_toolchain.source",
+            },
             source_row.alternatives.clone(),
         ),
         (DependencyProviderSourceState::Unsupported, _)
@@ -267,9 +277,11 @@ fn observation_from_source_row(
             binding_id,
             DependencyInventoryObservationState::Unavailable,
             DependencyEnvironmentValidationState::Valid,
-            DependencyPlanningDiagnosticCode::RuntimeUnavailable,
-            "Device-toolchain source is not ready for the requested toolchain.",
-            "dependency_environment.device_toolchain.source",
+            DependencyInventoryDiagnosticInput {
+                code: DependencyPlanningDiagnosticCode::RuntimeUnavailable,
+                message: "Device-toolchain source is not ready for the requested toolchain.".into(),
+                field_path: "dependency_environment.device_toolchain.source",
+            },
             source_row.alternatives.clone(),
         ),
     }
@@ -336,9 +348,11 @@ fn invalid_row(
         binding_id,
         DependencyInventoryObservationState::Invalid,
         DependencyEnvironmentValidationState::Invalid,
-        DependencyPlanningDiagnosticCode::InvalidRequest,
-        message,
-        field_path,
+        DependencyInventoryDiagnosticInput {
+            code: DependencyPlanningDiagnosticCode::InvalidRequest,
+            message: message.into(),
+            field_path,
+        },
         Vec::new(),
     )
 }
@@ -348,12 +362,15 @@ fn observation_with_diagnostic(
     binding_id: DependencyBindingId,
     state: DependencyInventoryObservationState,
     validation_state: DependencyEnvironmentValidationState,
-    code: DependencyPlanningDiagnosticCode,
-    message: impl Into,
-    field_path: &'static str,
+    diagnostic_input: DependencyInventoryDiagnosticInput,
     alternatives: Vec,
 ) -> DependencyInventoryObservationRow {
-    let diagnostic = diagnostic(item, code, message.into(), field_path);
+    let DependencyInventoryDiagnosticInput {
+        code,
+        message,
+        field_path,
+    } = diagnostic_input;
+    let diagnostic = diagnostic(item, code, message, field_path);
     row(
         binding_id,
         state,
diff --git a/crates/pantograph-embedded-runtime/src/dependency_inventory_system_package.rs b/crates/pantograph-embedded-runtime/src/dependency_inventory_system_package.rs
index 61686a8d6..526b698ea 100644
--- a/crates/pantograph-embedded-runtime/src/dependency_inventory_system_package.rs
+++ b/crates/pantograph-embedded-runtime/src/dependency_inventory_system_package.rs
@@ -16,7 +16,8 @@ use pantograph_dependency_planning::{
 };
 
 use crate::dependency_inventory::{
-    DependencyInventoryObservation, DependencyInventoryProvider, DependencyInventoryRequest,
+    DependencyInventoryDiagnosticInput, DependencyInventoryObservation,
+    DependencyInventoryProvider, DependencyInventoryRequest,
 };
 use crate::dependency_inventory_system_package_source::{
     SystemPackageProviderSource, SystemPackageProviderSourceError,
@@ -135,9 +136,12 @@ fn observe_system_package_binding(
             binding.binding_id.clone(),
             DependencyInventoryObservationState::Missing,
             DependencyEnvironmentValidationState::Valid,
-            DependencyPlanningDiagnosticCode::ArtifactMissing,
-            "System-package source facts are missing for the requested package.",
-            "dependency_environment.system_package.source",
+            DependencyInventoryDiagnosticInput {
+                code: DependencyPlanningDiagnosticCode::ArtifactMissing,
+                message: "System-package source facts are missing for the requested package."
+                    .into(),
+                field_path: "dependency_environment.system_package.source",
+            },
             ready_alternatives(&snapshot.rows),
         );
     }
@@ -240,9 +244,11 @@ fn observation_from_source_row(
             binding_id,
             DependencyInventoryObservationState::Unavailable,
             DependencyEnvironmentValidationState::Stale,
-            DependencyPlanningDiagnosticCode::ArtifactStale,
-            "System-package source facts are stale.",
-            "dependency_environment.system_package.source",
+            DependencyInventoryDiagnosticInput {
+                code: DependencyPlanningDiagnosticCode::ArtifactStale,
+                message: "System-package source facts are stale.".into(),
+                field_path: "dependency_environment.system_package.source",
+            },
             source_row.alternatives.clone(),
         ),
         (DependencyProviderSourceState::Ready, _) => row(
@@ -257,9 +263,11 @@ fn observation_from_source_row(
             binding_id,
             DependencyInventoryObservationState::Missing,
             DependencyEnvironmentValidationState::Valid,
-            DependencyPlanningDiagnosticCode::ArtifactMissing,
-            "System-package source facts are missing.",
-            "dependency_environment.system_package.source",
+            DependencyInventoryDiagnosticInput {
+                code: DependencyPlanningDiagnosticCode::ArtifactMissing,
+                message: "System-package source facts are missing.".into(),
+                field_path: "dependency_environment.system_package.source",
+            },
             source_row.alternatives.clone(),
         ),
         (DependencyProviderSourceState::Failed, _) => observation_with_diagnostic(
@@ -267,9 +275,11 @@ fn observation_from_source_row(
             binding_id,
             DependencyInventoryObservationState::Failed,
             DependencyEnvironmentValidationState::Valid,
-            DependencyPlanningDiagnosticCode::RuntimeUnavailable,
-            "System-package source reported a failure.",
-            "dependency_environment.system_package.source",
+            DependencyInventoryDiagnosticInput {
+                code: DependencyPlanningDiagnosticCode::RuntimeUnavailable,
+                message: "System-package source reported a failure.".into(),
+                field_path: "dependency_environment.system_package.source",
+            },
             source_row.alternatives.clone(),
         ),
         (DependencyProviderSourceState::Unsupported, _)
@@ -282,9 +292,11 @@ fn observation_from_source_row(
             binding_id,
             DependencyInventoryObservationState::Unavailable,
             DependencyEnvironmentValidationState::Valid,
-            DependencyPlanningDiagnosticCode::RuntimeUnavailable,
-            "System-package source is not ready for the requested package.",
-            "dependency_environment.system_package.source",
+            DependencyInventoryDiagnosticInput {
+                code: DependencyPlanningDiagnosticCode::RuntimeUnavailable,
+                message: "System-package source is not ready for the requested package.".into(),
+                field_path: "dependency_environment.system_package.source",
+            },
             source_row.alternatives.clone(),
         ),
     }
@@ -380,9 +392,11 @@ fn invalid_row(
         binding_id,
         DependencyInventoryObservationState::Invalid,
         DependencyEnvironmentValidationState::Invalid,
-        DependencyPlanningDiagnosticCode::InvalidRequest,
-        message,
-        field_path,
+        DependencyInventoryDiagnosticInput {
+            code: DependencyPlanningDiagnosticCode::InvalidRequest,
+            message: message.into(),
+            field_path,
+        },
         Vec::new(),
     )
 }
@@ -392,12 +406,15 @@ fn observation_with_diagnostic(
     binding_id: DependencyBindingId,
     state: DependencyInventoryObservationState,
     validation_state: DependencyEnvironmentValidationState,
-    code: DependencyPlanningDiagnosticCode,
-    message: impl Into,
-    field_path: &'static str,
+    diagnostic_input: DependencyInventoryDiagnosticInput,
     alternatives: Vec,
 ) -> DependencyInventoryObservationRow {
-    let diagnostic = diagnostic(item, code, message.into(), field_path);
+    let DependencyInventoryDiagnosticInput {
+        code,
+        message,
+        field_path,
+    } = diagnostic_input;
+    let diagnostic = diagnostic(item, code, message, field_path);
     row(
         binding_id,
         state,
diff --git a/crates/pantograph-embedded-runtime/src/dependency_inventory_tests.rs b/crates/pantograph-embedded-runtime/src/dependency_inventory_tests.rs
index a452e640f..bb4126025 100644
--- a/crates/pantograph-embedded-runtime/src/dependency_inventory_tests.rs
+++ b/crates/pantograph-embedded-runtime/src/dependency_inventory_tests.rs
@@ -326,6 +326,31 @@ async fn inventory_service_routes_device_toolchain_payloads_with_alternatives()
         device_toolchain_status.state,
         DependencyBindingStatusState::Unavailable
     );
+    assert_eq!(
+        device_toolchain_status.diagnostics,
+        vec![
+            pantograph_dependency_planning::DependencyPlanningDiagnostic {
+                code: DependencyPlanningDiagnosticCode::RuntimeUnavailable,
+                severity: pantograph_dependency_planning::DependencyPlanningSeverity::Error,
+                message: "Device-toolchain source is not ready for the requested toolchain."
+                    .to_string(),
+                model_id: Some(request.as_request().identity_key.model_ref.model_id.clone()),
+                runtime_id: request
+                    .as_request()
+                    .identity_key
+                    .scheduler_intent
+                    .requested_runtime_id
+                    .clone(),
+                device_id: request
+                    .as_request()
+                    .identity_key
+                    .scheduler_intent
+                    .requested_device_id
+                    .clone(),
+                field_path: Some("dependency_environment.device_toolchain.source".to_string()),
+            }
+        ]
+    );
     assert_eq!(device_toolchain_status.alternatives.len(), 1);
     assert_eq!(
         device_toolchain_status.alternatives[0]
diff --git a/crates/pantograph-embedded-runtime/src/task_executor/stream_artifacts.rs b/crates/pantograph-embedded-runtime/src/task_executor/stream_artifacts.rs
index 64f9f9f7c..5c0002629 100644
--- a/crates/pantograph-embedded-runtime/src/task_executor/stream_artifacts.rs
+++ b/crates/pantograph-embedded-runtime/src/task_executor/stream_artifacts.rs
@@ -57,15 +57,8 @@ impl StreamArtifactizer {
         let relationship = artifact_relationship_from_chunk(chunk);
 
         let key = MediaStreamKey::new(execution_id, task_id, port);
-        let (artifact_id, stream_handle, byte_range_start) = self.open_or_get_stream(
-            &key,
-            task_id,
-            execution_id,
-            port,
-            media_body.kind,
-            &media_type,
-            &relationship,
-        )?;
+        let (artifact_id, stream_handle, byte_range_start) =
+            self.open_or_get_stream(&key, media_body.kind, &media_type, &relationship)?;
         let (byte_range_end_exclusive, next_sequence) =
             checked_stream_artifact_progress(byte_range_start, body.len(), sequence)?;
 
@@ -167,9 +160,6 @@ impl StreamArtifactizer {
     fn open_or_get_stream(
         &self,
         key: &MediaStreamKey,
-        task_id: &str,
-        execution_id: &str,
-        port: &str,
         payload_kind: ArtifactPayloadKind,
         media_type: &str,
         relationship: &ArtifactRelationship,
@@ -184,11 +174,11 @@ impl StreamArtifactizer {
                     media_type: media_type.to_string(),
                     format: Some(format_metadata(payload_kind, media_type)),
                     attribution: ArtifactAttribution {
-                        workflow_run_id: execution_id.to_string(),
+                        workflow_run_id: key.workflow_run_id.clone(),
                         workflow_id: None,
                         workflow_version_id: None,
-                        node_id: Some(task_id.to_string()),
-                        port_id: Some(port.to_string()),
+                        node_id: Some(key.node_id.clone()),
+                        port_id: Some(key.port.clone()),
                         model_id: None,
                         runtime_id: None,
                     },
@@ -414,6 +404,71 @@ mod tests {
     use pantograph_workflow_service::WorkflowService;
     use std::sync::Arc;
 
+    #[test]
+    fn stream_key_preserves_attribution_reuse_and_scope_isolation() {
+        use pantograph_workflow_service::{
+            ArtifactDescriptorQueryRequest, ArtifactPolicy, ArtifactReadRequest, ArtifactStore,
+            WorkflowArtifactWriter,
+        };
+        let temp = tempfile::tempdir().expect("artifact directory");
+        let store = ArtifactStore::open(
+            temp.path().join("artifacts"),
+            ArtifactPolicy {
+                policy_id: "stream-key-test".to_string(),
+                policy_version: 1,
+                ttl_seconds: None,
+                max_disk_bytes: None,
+                max_memory_bytes: None,
+                max_single_artifact_bytes: None,
+                spill_threshold_bytes: None,
+                delete_on_consume: false,
+            },
+        )
+        .expect("artifact store");
+        let service = Arc::new(
+            WorkflowService::new().with_artifact_writer(WorkflowArtifactWriter::new(store)),
+        );
+        let artifactizer = StreamArtifactizer::new(service.clone());
+        let chunk = serde_json::json!({"audio_base64":"AA==", "media_type":"audio/wav"});
+        let first = artifactizer.artifactize_chunk("node-a", "run-a", "audio", chunk.clone());
+        let mut final_chunk = chunk.clone();
+        final_chunk["is_final"] = serde_json::json!(true);
+        let second = artifactizer.artifactize_chunk("node-a", "run-a", "audio", final_chunk);
+        assert_eq!(first["artifact_id"], second["artifact_id"]);
+        assert_eq!(first["byte_range_start"], 0);
+        assert_eq!(second["byte_range_start"], 1);
+        assert_eq!(second["available_byte_length"], 2);
+        let id = first["artifact_id"].as_str().expect("artifact id");
+        let descriptor = service
+            .artifact_descriptor(ArtifactDescriptorQueryRequest {
+                artifact_id: id.to_string(),
+            })
+            .expect("descriptor")
+            .artifact
+            .expect("stored artifact");
+        assert_eq!(descriptor.attribution.workflow_run_id, "run-a");
+        assert_eq!(descriptor.attribution.node_id.as_deref(), Some("node-a"));
+        assert_eq!(descriptor.attribution.port_id.as_deref(), Some("audio"));
+        let body = service
+            .read_artifact_body(ArtifactReadRequest {
+                artifact_id: id.to_string(),
+                byte_range_start: None,
+                byte_range_end_exclusive: None,
+            })
+            .expect("body");
+        assert_eq!(body.body, vec![0, 0]);
+        for (node, run, port) in [
+            ("node-b", "run-a", "audio"),
+            ("node-a", "run-b", "audio"),
+            ("node-a", "run-a", "other"),
+        ] {
+            let isolated = artifactizer.artifactize_chunk(node, run, port, chunk.clone());
+            assert_ne!(isolated["artifact_id"], first["artifact_id"]);
+            assert_eq!(isolated["byte_range_start"], 0);
+        }
+        assert_eq!(artifactizer.streams.lock().expect("streams").len(), 3);
+    }
+
     #[test]
     fn stream_artifact_progress_rejects_byte_range_overflow() {
         assert_eq!(checked_stream_artifact_progress(u64::MAX, 1, 0), None);
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-private-inputs.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-private-inputs.md
new file mode 100644
index 000000000..efa88b303
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-embedded-private-inputs.md
@@ -0,0 +1,9 @@
+# Private inventory diagnostic input and stream attribution
+
+Fresh Clippy at 788ce31 reports only four embedded-runtime arities. This milestone repairs three private methods; the public hosted startup constructor remains separate.
+
+Both device-toolchain and system-package observation helpers had eight arguments and six callers each. Group only code, owned message and static field_path in a shared private DependencyInventoryDiagnosticInput, gated with the existing test/standalone owners. Keep work item, binding identity, observation state, validation state and alternatives distinct. The message conversion moves from helper entry to the named field at each call; diagnostic attribution lookup and row builder remain unchanged. Exact diagnostic equality is added to the existing real inventory-service device-toolchain test, including model/runtime/device and field path; existing ready/unavailable/system-package coverage remains.
+
+The stream helper has one caller. MediaStreamKey already copies the exact execution/task/port strings, so remove those three duplicated arguments and clone attribution directly from that key. Lock/map/reuse/relationship/progress/failure behavior remains unchanged. A real artifact-store regression verifies two chunks reuse one artifact, finalization retains exact bytes, attribution matches all three key fields, and changing each field isolates a fresh stream. Existing overflow and redaction regressions remain.
+
+All changes are private; public DTOs and Result signatures are unchanged. Root source review accepted all six files at frozen tree 6d72a526a367dfd9bfd60e7e1d78ad0eb46d265c. Hosted full embedded execution remains pending. Full pinned cargo fmt passes locally; no local Rust execution is claimed. Prior layout head has 450 embedded passes and three fixture failures, with actual clone-isolation test, Headless and Runtime Separation passing. The fixture repair is a separate preceding commit bbce93c whose hosted result remains pending.

From e7e52775f3479dc89f1d703ae9ab7715124756f8 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 09:28:24 -0700
Subject: [PATCH 54/65] Name owned hosted startup configuration inputs

---
 crates/pantograph-embedded-runtime/src/lib.rs |   3 +-
 .../src/workflow_service_composition.rs       | 165 ++++++++++++------
 .../2026-10-03-hosted-startup-named-config.md |   9 +
 src-tauri/src/app_setup.rs                    |  29 +--
 4 files changed, 141 insertions(+), 65 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-hosted-startup-named-config.md

diff --git a/crates/pantograph-embedded-runtime/src/lib.rs b/crates/pantograph-embedded-runtime/src/lib.rs
index 9eb2ad885..4873a50b9 100644
--- a/crates/pantograph-embedded-runtime/src/lib.rs
+++ b/crates/pantograph-embedded-runtime/src/lib.rs
@@ -163,7 +163,8 @@ pub use task_executor::{runtime_extension_keys, TauriTaskExecutor as PantographT
 pub(crate) use workflow_scheduler_diagnostics::EmbeddedWorkflowSchedulerDiagnosticsProvider;
 pub use workflow_service_composition::{
     EmbeddedHostedStartupCompositionInput, EmbeddedHostedStartupCompositionOutput,
-    EmbeddedHostedStartupPumasSelectorSource, EmbeddedWorkflowServiceComposition,
+    EmbeddedHostedStartupConfig, EmbeddedHostedStartupPumasSelectorSource,
+    EmbeddedWorkflowServiceComposition,
 };
 
 pub type SharedWorkflowService = Arc;
diff --git a/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs b/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs
index 023092405..189dc9c09 100644
--- a/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs
+++ b/crates/pantograph-embedded-runtime/src/workflow_service_composition.rs
@@ -140,6 +140,22 @@ pub enum EmbeddedHostedStartupPumasSelectorSource {
     SetupPath(Option),
 }
 
+/// Owned inputs for hosted startup composition, without changing startup defaults.
+///
+/// Rust callers construct these named fields and pass the config to
+/// `EmbeddedHostedStartupCompositionInput::new`.
+pub struct EmbeddedHostedStartupConfig {
+    pub runtime_registry: SharedRuntimeRegistry,
+    pub runtime_registry_controller: Arc,
+    pub gateway: Arc,
+    pub pumas_selector_source: Option,
+    pub project_root: PathBuf,
+    pub kv_cache_dir: PathBuf,
+    pub dependency_readiness_runtime_handle: tokio::runtime::Handle,
+    pub max_loaded_sessions: Option,
+    pub max_dispatch_source_snapshot_age_ms: u64,
+}
+
 pub struct EmbeddedHostedStartupCompositionInput {
     workflow_service: WorkflowService,
     max_loaded_sessions: Option,
@@ -156,17 +172,18 @@ pub struct EmbeddedHostedStartupCompositionInput {
 
 impl EmbeddedHostedStartupCompositionInput {
     #[must_use]
-    pub fn new(
-        runtime_registry: SharedRuntimeRegistry,
-        runtime_registry_controller: Arc,
-        gateway: Arc,
-        pumas_selector_source: Option,
-        project_root: PathBuf,
-        kv_cache_dir: PathBuf,
-        dependency_readiness_runtime_handle: tokio::runtime::Handle,
-        max_loaded_sessions: Option,
-        max_dispatch_source_snapshot_age_ms: u64,
-    ) -> Self {
+    pub fn new(config: EmbeddedHostedStartupConfig) -> Self {
+        let EmbeddedHostedStartupConfig {
+            runtime_registry,
+            runtime_registry_controller,
+            gateway,
+            pumas_selector_source,
+            project_root,
+            kv_cache_dir,
+            dependency_readiness_runtime_handle,
+            max_loaded_sessions,
+            max_dispatch_source_snapshot_age_ms,
+        } = config;
         Self {
             workflow_service: WorkflowService::new(),
             max_loaded_sessions,
@@ -1216,6 +1233,52 @@ mod tests {
             .contains("dependency-readiness snapshot producer poll interval"));
     }
 
+    #[tokio::test]
+    async fn hosted_startup_named_config_preserves_owned_values_and_defaults() {
+        let registry: SharedRuntimeRegistry = Arc::new(RuntimeRegistry::new());
+        let gateway = Arc::new(inference::InferenceGateway::new());
+        let input = EmbeddedHostedStartupCompositionInput::new(EmbeddedHostedStartupConfig {
+            runtime_registry: registry.clone(),
+            runtime_registry_controller: gateway.clone(),
+            gateway: gateway.clone(),
+            pumas_selector_source: Some(EmbeddedHostedStartupPumasSelectorSource::SetupPath(Some(
+                PathBuf::from("pumas-root"),
+            ))),
+            project_root: PathBuf::from("project-root"),
+            kv_cache_dir: PathBuf::from("kv-root"),
+            dependency_readiness_runtime_handle: tokio::runtime::Handle::current(),
+            max_loaded_sessions: Some(7),
+            max_dispatch_source_snapshot_age_ms: 2_345,
+        });
+        assert!(Arc::ptr_eq(&input.runtime_registry, ®istry));
+        assert!(Arc::ptr_eq(&input.runtime_registry_controller, &gateway));
+        assert!(Arc::ptr_eq(&input.gateway, &gateway));
+        assert!(
+            matches!(&input.pumas_selector_source, Some(EmbeddedHostedStartupPumasSelectorSource::SetupPath(Some(path))) if path == std::path::Path::new("pumas-root"))
+        );
+        assert_eq!(input.project_root, PathBuf::from("project-root"));
+        assert_eq!(input.kv_cache_dir, PathBuf::from("kv-root"));
+        assert_eq!(input.max_loaded_sessions, Some(7));
+        assert_eq!(input.max_dispatch_source_snapshot_age_ms, 2_345);
+        assert_eq!(
+            input.dependency_readiness_producer_config,
+            EmbeddedDependencyReadinessSnapshotProducerConfig::default()
+        );
+        assert_eq!(
+            input
+                .dependency_readiness_runtime_handle
+                .spawn(async { 42 })
+                .await
+                .expect("original runtime handle"),
+            42
+        );
+        let config = EmbeddedDependencyReadinessSnapshotProducerConfig {
+            poll_interval: std::time::Duration::from_secs(17),
+        };
+        let input = input.with_dependency_readiness_producer_config(config.clone());
+        assert_eq!(input.dependency_readiness_producer_config, config);
+    }
+
     #[tokio::test]
     async fn hosted_startup_composition_returns_service_extensions_and_lifecycle_handle() {
         let temp_dir = tempfile::tempdir().expect("tempdir");
@@ -1229,19 +1292,19 @@ mod tests {
         );
         let registry: SharedRuntimeRegistry = Arc::new(RuntimeRegistry::new());
         let gateway = Arc::new(inference::InferenceGateway::new());
-        let input = EmbeddedHostedStartupCompositionInput::new(
-            registry,
-            gateway.clone(),
+        let input = EmbeddedHostedStartupCompositionInput::new(EmbeddedHostedStartupConfig {
+            runtime_registry: registry,
+            runtime_registry_controller: gateway.clone(),
             gateway,
-            Some(EmbeddedHostedStartupPumasSelectorSource::Provided(
+            pumas_selector_source: Some(EmbeddedHostedStartupPumasSelectorSource::Provided(
                 Arc::new(PumasSelectorAccess::Owner(pumas_api.clone())),
             )),
-            temp_dir.path().to_path_buf(),
-            temp_dir.path().join("kv-cache"),
-            tokio::runtime::Handle::current(),
-            Some(1),
-            1_000,
-        )
+            project_root: temp_dir.path().to_path_buf(),
+            kv_cache_dir: temp_dir.path().join("kv-cache"),
+            dependency_readiness_runtime_handle: tokio::runtime::Handle::current(),
+            max_loaded_sessions: Some(1),
+            max_dispatch_source_snapshot_age_ms: 1_000,
+        })
         .with_workflow_service(workflow_service_with_artifact_store(&temp_dir));
 
         let output = EmbeddedWorkflowServiceComposition::resource_backed_hosted_startup(input)
@@ -1304,17 +1367,17 @@ mod tests {
         let temp_dir = tempfile::tempdir().expect("tempdir");
         let registry: SharedRuntimeRegistry = Arc::new(RuntimeRegistry::new());
         let gateway = Arc::new(inference::InferenceGateway::new());
-        let input = EmbeddedHostedStartupCompositionInput::new(
-            registry,
-            gateway.clone(),
+        let input = EmbeddedHostedStartupCompositionInput::new(EmbeddedHostedStartupConfig {
+            runtime_registry: registry,
+            runtime_registry_controller: gateway.clone(),
             gateway,
-            None,
-            temp_dir.path().to_path_buf(),
-            temp_dir.path().join("kv-cache"),
-            tokio::runtime::Handle::current(),
-            Some(1),
-            1_000,
-        );
+            pumas_selector_source: None,
+            project_root: temp_dir.path().to_path_buf(),
+            kv_cache_dir: temp_dir.path().join("kv-cache"),
+            dependency_readiness_runtime_handle: tokio::runtime::Handle::current(),
+            max_loaded_sessions: Some(1),
+            max_dispatch_source_snapshot_age_ms: 1_000,
+        });
 
         let error =
             match EmbeddedWorkflowServiceComposition::resource_backed_hosted_startup(input).await {
@@ -1347,19 +1410,19 @@ mod tests {
         .expect("read-only pumas");
         let registry: SharedRuntimeRegistry = Arc::new(RuntimeRegistry::new());
         let gateway = Arc::new(inference::InferenceGateway::new());
-        let input = EmbeddedHostedStartupCompositionInput::new(
-            registry,
-            gateway.clone(),
+        let input = EmbeddedHostedStartupCompositionInput::new(EmbeddedHostedStartupConfig {
+            runtime_registry: registry,
+            runtime_registry_controller: gateway.clone(),
             gateway,
-            Some(EmbeddedHostedStartupPumasSelectorSource::Provided(
+            pumas_selector_source: Some(EmbeddedHostedStartupPumasSelectorSource::Provided(
                 Arc::new(PumasSelectorAccess::ReadOnly(Arc::new(read_only))),
             )),
-            temp_dir.path().to_path_buf(),
-            temp_dir.path().join("kv-cache"),
-            tokio::runtime::Handle::current(),
-            Some(1),
-            1_000,
-        );
+            project_root: temp_dir.path().to_path_buf(),
+            kv_cache_dir: temp_dir.path().join("kv-cache"),
+            dependency_readiness_runtime_handle: tokio::runtime::Handle::current(),
+            max_loaded_sessions: Some(1),
+            max_dispatch_source_snapshot_age_ms: 1_000,
+        });
 
         let error =
             match EmbeddedWorkflowServiceComposition::resource_backed_hosted_startup(input).await {
@@ -1386,19 +1449,19 @@ mod tests {
         );
         let registry: SharedRuntimeRegistry = Arc::new(RuntimeRegistry::new());
         let gateway = Arc::new(inference::InferenceGateway::new());
-        let input = EmbeddedHostedStartupCompositionInput::new(
-            registry,
-            gateway.clone(),
+        let input = EmbeddedHostedStartupCompositionInput::new(EmbeddedHostedStartupConfig {
+            runtime_registry: registry,
+            runtime_registry_controller: gateway.clone(),
             gateway,
-            Some(EmbeddedHostedStartupPumasSelectorSource::Provided(
+            pumas_selector_source: Some(EmbeddedHostedStartupPumasSelectorSource::Provided(
                 Arc::new(PumasSelectorAccess::Owner(pumas_api)),
             )),
-            temp_dir.path().to_path_buf(),
-            temp_dir.path().join("kv-cache"),
-            tokio::runtime::Handle::current(),
-            Some(0),
-            1_000,
-        )
+            project_root: temp_dir.path().to_path_buf(),
+            kv_cache_dir: temp_dir.path().join("kv-cache"),
+            dependency_readiness_runtime_handle: tokio::runtime::Handle::current(),
+            max_loaded_sessions: Some(0),
+            max_dispatch_source_snapshot_age_ms: 1_000,
+        })
         .with_workflow_service(workflow_service_with_store(
             &temp_dir,
             WorkflowService::with_capacity_limits(2, 2),
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-hosted-startup-named-config.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-hosted-startup-named-config.md
new file mode 100644
index 000000000..2e6c22ada
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-hosted-startup-named-config.md
@@ -0,0 +1,9 @@
+# Named owned hosted startup configuration
+
+The last public embedded-runtime arity finding is the nine-argument EmbeddedHostedStartupCompositionInput::new. Introduce exported EmbeddedHostedStartupConfig with exactly the nine existing owned inputs: runtime registry, registry controller, inference gateway, optional selector source, project root, KV cache path, readiness runtime handle, optional loaded-session limit, and source-snapshot age limit. The constructor destructures these fields into its unchanged initializer. WorkflowService::new and default producer configuration remain unchanged, as do both builder methods.
+
+All five existing calls are migrated: desktop app_setup and four hosted composition tests. Each original expression, clone and ownership transfer stays attached to its corresponding named field; no policy is inferred or optional field silently defaulted. A constructor regression verifies Arc identity, paths, selector setup path, limits, functional runtime handle, default producer configuration and producer builder override. Existing real startup tests retain success/lifecycle, missing selector, wrong owner and invalid service-capacity coverage. Hosted workspace all-feature compilation covers the desktop caller, and the full embedded unit gate exercises startup tests.
+
+This is a public Rust source signature change: callers must pass the new config instead of nine positional arguments. The crate is publish=false and uses the workspace version, so no independently published crate version is changed. Unseen Rust consumers are not claimed source-compatible. Serialized DTOs, runtime policy and startup order are unchanged.
+
+Root source review accepted all four files at frozen tree afc4cc26997ba3f61dde38ab77e1927563e1f18e. Hosted execution remains pending. The constructor initializer and both existing builder bodies were also compared byte-for-byte with the published parent and remain identical. Full pinned formatting passes locally; no local Rust execution is claimed under the shared resource constraint.
diff --git a/src-tauri/src/app_setup.rs b/src-tauri/src/app_setup.rs
index d68f0ed1b..cb5d69ee5 100644
--- a/src-tauri/src/app_setup.rs
+++ b/src-tauri/src/app_setup.rs
@@ -9,8 +9,8 @@ use crate::llm::{
 use crate::project_root::resolve_project_root;
 use crate::workflow;
 use pantograph_embedded_runtime::{
-    EmbeddedHostedStartupCompositionInput, EmbeddedHostedStartupPumasSelectorSource,
-    EmbeddedWorkflowServiceComposition,
+    EmbeddedHostedStartupCompositionInput, EmbeddedHostedStartupConfig,
+    EmbeddedHostedStartupPumasSelectorSource, EmbeddedWorkflowServiceComposition,
 };
 use std::sync::Arc;
 use tauri::{Emitter, Manager};
@@ -245,17 +245,20 @@ pub fn run_app() -> AppStartupResult<()> {
                 let pumas_library_path = pumas_release_dir.or(pumas_launcher_root);
 
                 let startup_input = EmbeddedHostedStartupCompositionInput::new(
-                    runtime_registry.clone(),
-                    gateway.clone(),
-                    gateway.inner_arc(),
-                    Some(EmbeddedHostedStartupPumasSelectorSource::SetupPath(
-                        pumas_library_path,
-                    )),
-                    project_root.clone(),
-                    kv_cache_dir,
-                    runtime_handle.clone(),
-                    max_loaded_sessions,
-                    HOSTED_DISPATCH_SOURCE_SNAPSHOT_MAX_AGE_MS,
+                    EmbeddedHostedStartupConfig {
+                        runtime_registry: runtime_registry.clone(),
+                        runtime_registry_controller: gateway.clone(),
+                        gateway: gateway.inner_arc(),
+                        pumas_selector_source: Some(
+                            EmbeddedHostedStartupPumasSelectorSource::SetupPath(pumas_library_path),
+                        ),
+                        project_root: project_root.clone(),
+                        kv_cache_dir,
+                        dependency_readiness_runtime_handle: runtime_handle.clone(),
+                        max_loaded_sessions,
+                        max_dispatch_source_snapshot_age_ms:
+                            HOSTED_DISPATCH_SOURCE_SNAPSHOT_MAX_AGE_MS,
+                    },
                 )
                 .with_workflow_service(workflow_service);
                 let startup_output = tauri::async_runtime::block_on(

From e2dbe87c14a0bd34fc8519991f42bb522f4ce186 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 09:50:11 -0700
Subject: [PATCH 55/65] Propagate owned runtime shutdown failures through
 UniFFI

---
 Cargo.lock                                    |   1 +
 bindings/csharp/README.md                     |  10 ++
 crates/pantograph-uniffi/Cargo.toml           |   1 +
 crates/pantograph-uniffi/src/runtime.rs       |  13 +-
 .../src/runtime_shutdown_tests.rs             | 159 ++++++++++++++++++
 crates/pantograph-uniffi/src/runtime_tests.rs |  18 +-
 .../src/runtime_validation_tests.rs           |   6 +-
 .../2026-10-03-uniffi-shutdown-result.md      |  13 ++
 8 files changed, 207 insertions(+), 14 deletions(-)
 create mode 100644 crates/pantograph-uniffi/src/runtime_shutdown_tests.rs
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-shutdown-result.md

diff --git a/Cargo.lock b/Cargo.lock
index 4be64790e..1127ac859 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7552,6 +7552,7 @@ name = "pantograph-uniffi"
 version = "0.1.0"
 dependencies = [
  "async-trait",
+ "futures-util",
  "graph-flow",
  "inference",
  "node-engine",
diff --git a/bindings/csharp/README.md b/bindings/csharp/README.md
index ff8566ece..febd8eed0 100644
--- a/bindings/csharp/README.md
+++ b/bindings/csharp/README.md
@@ -30,6 +30,16 @@ with any redistributed generator source/binary; this repository does not vendor
 or package the generator itself. Existing generated-binding/artifact licensing
 obligations remain unchanged.
 
+## Shutdown result
+
+Await `runtime.Shutdown()` and handle the generated `FfiException` if the
+owned backend cannot stop. Its message contains the standard JSON error envelope
+with `internal_error` and the original shutdown cause. A failed stop does not
+mean the runtime released its residency; callers may retry after resolving the
+cause. Existing C# await expressions remain valid, but regenerate bindings and
+ship them with the matching native library when adopting this fallible API.
+Rust callers now handle `Result<(), FfiError>` explicitly.
+
 ## Usage
 Run the repository-level smoke script:
 
diff --git a/crates/pantograph-uniffi/Cargo.toml b/crates/pantograph-uniffi/Cargo.toml
index ddd64f636..68b0e573a 100644
--- a/crates/pantograph-uniffi/Cargo.toml
+++ b/crates/pantograph-uniffi/Cargo.toml
@@ -54,6 +54,7 @@ required-features = ["cli"]
 uniffi = { version = "0.28", features = ["build"] }
 
 [dev-dependencies]
+futures-util.workspace = true
 uniffi = { version = "0.28", features = ["bindgen-tests"] }
 
 [features]
diff --git a/crates/pantograph-uniffi/src/runtime.rs b/crates/pantograph-uniffi/src/runtime.rs
index 1e4ab0b11..22a0b07d5 100644
--- a/crates/pantograph-uniffi/src/runtime.rs
+++ b/crates/pantograph-uniffi/src/runtime.rs
@@ -208,8 +208,13 @@ impl FfiPantographRuntime {
     }
 
     /// Stop inference backends owned by this runtime.
-    pub async fn shutdown(&self) {
-        self.runtime.shutdown().await;
+    ///
+    /// A failed shutdown is returned through the standard error envelope; the
+    /// embedded owner retains residency and permits a later retry.
+    pub async fn shutdown(&self) -> Result<(), FfiError> {
+        self.runtime.shutdown().await.map_err(|error| {
+            workflow_adapter_error(WorkflowErrorCode::InternalError, error.to_string())
+        })
     }
 
     /// Register an attribution client and return ClientRegistrationResponse JSON.
@@ -1116,3 +1121,7 @@ mod runtime_tests;
 #[cfg(test)]
 #[path = "runtime_validation_tests.rs"]
 mod runtime_validation_tests;
+
+#[cfg(test)]
+#[path = "runtime_shutdown_tests.rs"]
+mod runtime_shutdown_tests;
diff --git a/crates/pantograph-uniffi/src/runtime_shutdown_tests.rs b/crates/pantograph-uniffi/src/runtime_shutdown_tests.rs
new file mode 100644
index 000000000..940b3f0c8
--- /dev/null
+++ b/crates/pantograph-uniffi/src/runtime_shutdown_tests.rs
@@ -0,0 +1,159 @@
+use std::pin::Pin;
+use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use futures_util::Stream;
+use inference::backend::BackendStartOutcome;
+use inference::{
+    BackendCapabilities, BackendConfig, BackendError, ChatChunk, EmbeddingResult, InferenceBackend,
+    InferenceGateway, ProcessSpawner, RerankRequest, RerankResponse,
+};
+use node_engine::ExecutorExtensions;
+use pantograph_embedded_runtime::{EmbeddedRuntime, EmbeddedRuntimeConfig};
+use pantograph_workflow_service::WorkflowService;
+use tokio::sync::RwLock;
+
+use super::FfiPantographRuntime;
+use crate::FfiError;
+
+struct ShutdownBackend {
+    fail_next_stop: AtomicBool,
+    ready: AtomicBool,
+    stop_calls: Arc,
+}
+
+#[async_trait]
+impl InferenceBackend for ShutdownBackend {
+    fn name(&self) -> &'static str {
+        "shutdown-fixture"
+    }
+    fn description(&self) -> &'static str {
+        "bounded shutdown fixture"
+    }
+    fn capabilities(&self) -> BackendCapabilities {
+        BackendCapabilities::default()
+    }
+    async fn start(
+        &mut self,
+        _: &BackendConfig,
+        _: Arc,
+    ) -> Result {
+        panic!("shutdown fixture must not start a backend")
+    }
+    async fn stop(&mut self) -> Result<(), BackendError> {
+        self.stop_calls.fetch_add(1, Ordering::SeqCst);
+        if self.fail_next_stop.swap(false, Ordering::SeqCst) {
+            return Err(BackendError::Inference("fixture stop failure".to_string()));
+        }
+        self.ready.store(false, Ordering::SeqCst);
+        Ok(())
+    }
+    fn is_ready(&self) -> bool {
+        self.ready.load(Ordering::SeqCst)
+    }
+    async fn health_check(&self) -> bool {
+        self.is_ready()
+    }
+    fn base_url(&self) -> Option {
+        None
+    }
+    async fn chat_completion_stream(
+        &self,
+        _: String,
+    ) -> Result> + Send>>, BackendError>
+    {
+        panic!("shutdown fixture must not execute inference")
+    }
+    async fn embeddings(
+        &self,
+        _: Vec,
+        _: &str,
+    ) -> Result, BackendError> {
+        panic!("shutdown fixture must not execute embeddings")
+    }
+    async fn rerank(&self, _: RerankRequest) -> Result {
+        panic!("shutdown fixture must not execute reranking")
+    }
+}
+
+fn runtime(
+    fail_next_stop: bool,
+) -> (
+    FfiPantographRuntime,
+    Arc,
+    Arc,
+    std::path::PathBuf,
+) {
+    let root = super::runtime_tests::create_temp_root("shutdown-fixture");
+    let stop_calls = Arc::new(AtomicUsize::new(0));
+    let gateway = Arc::new(InferenceGateway::with_backend(
+        Box::new(ShutdownBackend {
+            fail_next_stop: AtomicBool::new(fail_next_stop),
+            ready: AtomicBool::new(true),
+            stop_calls: stop_calls.clone(),
+        }),
+        "shutdown-fixture",
+    ));
+    let extensions = Arc::new(RwLock::new(ExecutorExtensions::new()));
+    let app_data_dir = root.join("app-data");
+    let embedded = EmbeddedRuntime::with_default_python_runtime(
+        EmbeddedRuntimeConfig::new(app_data_dir.clone(), root.clone()),
+        gateway.clone(),
+        extensions.clone(),
+        Arc::new(WorkflowService::new()),
+        None,
+    );
+    (
+        FfiPantographRuntime {
+            runtime: Arc::new(embedded),
+            app_data_dir,
+            node_registry: Arc::new(node_engine::NodeRegistry::with_builtins()),
+            extensions,
+        },
+        gateway,
+        stop_calls,
+        root,
+    )
+}
+
+#[tokio::test]
+async fn ffi_shutdown_reports_owner_failure_and_allows_retry() {
+    let (runtime, gateway, stop_calls, root) = runtime(true);
+    assert!(gateway.is_ready().await);
+    let error = runtime
+        .shutdown()
+        .await
+        .expect_err("failed owner stop must cross FFI");
+    let FfiError::Other { message } = error else {
+        panic!("expected standard FFI error envelope");
+    };
+    assert_eq!(
+        serde_json::from_str::(&message).unwrap(),
+        serde_json::json!({
+            "code": "internal_error", "message": "Backend error: Inference error: fixture stop failure"
+        })
+    );
+    assert_eq!(stop_calls.load(Ordering::SeqCst), 1);
+    assert!(
+        gateway.is_ready().await,
+        "failed stop must preserve backend readiness"
+    );
+    runtime
+        .shutdown()
+        .await
+        .expect("retry observes successful owner stop");
+    assert_eq!(stop_calls.load(Ordering::SeqCst), 2);
+    assert!(!gateway.is_ready().await);
+    std::fs::remove_dir_all(root).expect("remove fixture files");
+}
+
+#[tokio::test]
+async fn ffi_shutdown_success_and_repeated_success_are_explicit() {
+    let (runtime, gateway, stop_calls, root) = runtime(false);
+    runtime.shutdown().await.expect("successful shutdown");
+    assert!(!gateway.is_ready().await);
+    runtime.shutdown().await.expect("repeated shutdown");
+    assert_eq!(stop_calls.load(Ordering::SeqCst), 2);
+    std::fs::remove_dir_all(root).expect("remove fixture files");
+}
diff --git a/crates/pantograph-uniffi/src/runtime_tests.rs b/crates/pantograph-uniffi/src/runtime_tests.rs
index 9b3492f5e..481f2eade 100644
--- a/crates/pantograph-uniffi/src/runtime_tests.rs
+++ b/crates/pantograph-uniffi/src/runtime_tests.rs
@@ -371,7 +371,7 @@ async fn direct_runtime_runs_workflow_session_from_json() {
     let close: serde_json::Value = serde_json::from_str(&close_json).expect("parse close");
     assert_eq!(close["ok"], true);
 
-    runtime.shutdown().await;
+    runtime.shutdown().await.expect("runtime shutdown");
 
     let _ = std::fs::remove_dir_all(root);
 }
@@ -434,7 +434,7 @@ async fn direct_runtime_rejects_interactive_graph_at_scheduler_boundary() {
         .contains("scheduler task session runner has no execution path"));
     assert!(envelope.message.contains("unsupported=1"));
 
-    runtime.shutdown().await;
+    runtime.shutdown().await.expect("runtime shutdown");
     let _ = std::fs::remove_dir_all(root);
 }
 
@@ -490,7 +490,7 @@ async fn direct_runtime_exposes_attribution_client_session_json() {
         serde_json::from_str(&open_session_json).expect("parse open response");
     assert!(opened["session"]["client_session_id"].as_str().is_some());
 
-    runtime.shutdown().await;
+    runtime.shutdown().await.expect("runtime shutdown");
     let _ = std::fs::remove_dir_all(root);
 }
 
@@ -610,7 +610,7 @@ async fn direct_runtime_exposes_workflow_graph_persistence_and_edit_session() {
         )
         .await
         .expect("close graph edit session");
-    runtime.shutdown().await;
+    runtime.shutdown().await.expect("runtime shutdown");
 
     let _ = std::fs::remove_dir_all(root);
 }
@@ -701,7 +701,7 @@ async fn direct_runtime_exposes_backend_owned_graph_authoring_discovery() {
         .message
         .contains("No options provider for text-input:text"));
 
-    runtime.shutdown().await;
+    runtime.shutdown().await.expect("runtime shutdown");
     let _ = std::fs::remove_dir_all(root);
 }
 
@@ -781,7 +781,7 @@ async fn direct_runtime_puma_lib_options_use_selector_access_from_pumas_api() {
         .as_str()
         .is_some_and(|cursor| cursor.starts_with("model-library-updates:")));
 
-    runtime.shutdown().await;
+    runtime.shutdown().await.expect("runtime shutdown");
     let _ = std::fs::remove_dir_all(root);
 }
 
@@ -841,7 +841,7 @@ async fn direct_runtime_exposes_artifact_store_contract_surface() {
     assert_eq!(envelope.code, WorkflowErrorCode::InvalidRequest);
     assert!(envelope.message.contains("artifact not found"));
 
-    runtime.shutdown().await;
+    runtime.shutdown().await.expect("runtime shutdown");
     let _ = std::fs::remove_dir_all(root);
 }
 
@@ -987,7 +987,7 @@ async fn direct_runtime_exposes_artifact_format_settings_and_capabilities_json()
         "image quality_percent 0 is outside allowed range"
     );
 
-    runtime.shutdown().await;
+    runtime.shutdown().await.expect("runtime shutdown");
     let _ = std::fs::remove_dir_all(root);
 }
 
@@ -1184,7 +1184,7 @@ async fn direct_runtime_exposes_managed_media_dependency_statuses_and_actions_js
         assert!(envelope.message.contains("missing expected file"));
     }
 
-    runtime.shutdown().await;
+    runtime.shutdown().await.expect("runtime shutdown");
     let _ = std::fs::remove_dir_all(root);
 }
 
diff --git a/crates/pantograph-uniffi/src/runtime_validation_tests.rs b/crates/pantograph-uniffi/src/runtime_validation_tests.rs
index b1c09bfa7..96fc1b288 100644
--- a/crates/pantograph-uniffi/src/runtime_validation_tests.rs
+++ b/crates/pantograph-uniffi/src/runtime_validation_tests.rs
@@ -170,7 +170,7 @@ async fn public_validation_publication_is_required_again_after_runtime_reopen()
             .expect("published text runs"),
     );
     assert_eq!(response["outputs"][0]["value"], "published text");
-    runtime.shutdown().await;
+    runtime.shutdown().await.expect("runtime shutdown");
     drop(runtime);
 
     let reopened = open_runtime(&root).await;
@@ -186,7 +186,7 @@ async fn public_validation_publication_is_required_again_after_runtime_reopen()
             .expect("republished text runs"),
     );
     assert_eq!(response["outputs"][0]["value"], "published text");
-    reopened.shutdown().await;
+    reopened.shutdown().await.expect("runtime shutdown");
     drop(reopened);
     std::fs::remove_dir_all(root).expect("remove fixture");
 }
@@ -234,7 +234,7 @@ async fn public_publication_rejects_stale_validation_and_caller_supplied_proof()
             .await
             .expect_err("rejected publication stores no executable snapshot"),
     );
-    runtime.shutdown().await;
+    runtime.shutdown().await.expect("runtime shutdown");
     drop(runtime);
     std::fs::remove_dir_all(root).expect("remove fixture");
 }
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-shutdown-result.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-shutdown-result.md
new file mode 100644
index 000000000..80a09b69e
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-uniffi-shutdown-result.md
@@ -0,0 +1,13 @@
+# Preserve fallible shutdown through UniFFI
+
+Exact PR42 head e7e5277 clears embedded-runtime Clippy and exposes one UniFFI unused Result at runtime.rs shutdown. EmbeddedRuntime::shutdown already awaits owned shutdown, preserves gateway residency on stop failure and only invalidates loaded session runtimes after successful stop. The adapter must propagate that result rather than report success or discard it.
+
+Return Result<(), FfiError> through the existing workflow_adapter_error internal_error JSON envelope, retaining the original owner Display cause. Twelve existing Rust shutdown calls now require success explicitly. No embedded lifecycle, invalidation, retry or backend policy is changed. A dedicated real adapter-chain regression uses the production FFI, EmbeddedRuntime and InferenceGateway with a failure-once backend: it checks the exact envelope, one stop invocation, retained actual gateway readiness, then successful retry and stopped readiness. A second test covers initial/repeated success. Non-shutdown trait methods panic if unexpectedly invoked.
+
+The test-only futures-util manifest/lock edge reuses the existing locked workspace version for the backend trait stream signature; no package version changes. The mock remains a backend fixture, not an alternate adapter implementation.
+
+Generated C# calls remain await runtime.Shutdown() (NativeSmoke has one executed call plus its compile-surface call; Quickstart has one executed call). Fallible metadata now enables the existing generated FfiException path for Rust FfiError; bindings and native library must be regenerated/shipped together, with no old-generated-binary compatibility claim. Headless CI regenerates and executes NativeSmoke and compiles/runs the packaged quickstart. A C# shutdown-failure injection is not available through the public runtime constructor without adding a production testing API or forcing an actual backend failure; the bounded failure test therefore exercises the real Rust adapter chain, while C# success/generated plumbing remains hosted qualification.
+
+Root source review accepted the complete eight-file checkpoint at frozen tree 18436e4cf375a4bb204f086453991fa07966eca0. Full pinned format passes locally, with no local Rust/native build claimed. PR42 has 455/455 embedded tests, full focused tests, workspace feature checks and format green; this distinct adapter milestone requires new exact-head aggregate and generated binding qualification.
+
+The pinned generator source confirms the C# error naming: CsCodeOracle::convert_error_suffix maps Error to Exception, so Rust FfiError is generated as FfiException ([official source](https://github.com/NordSecurity/uniffi-bindgen-cs/blob/2f4880f03ed08d960ad0ee14d11cf95444eee540/bindgen/src/gen_cs/mod.rs)).

From a387e75bdf0d417257d63bf9c05db19b6a2c9574 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 10:23:14 -0700
Subject: [PATCH 56/65] Preserve node test contracts while clearing strict lint

---
 .github/workflows/quality-gates.yml               | 15 +++++++++++++++
 .../src/core_executor/inference_tests.rs          |  5 ++++-
 .../node-engine/src/engine/dependency_inputs.rs   |  2 +-
 .../reports/2026-10-03-node-engine-test-lints.md  |  7 +++++++
 4 files changed, 27 insertions(+), 2 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-engine-test-lints.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 8a0d02108..d2b2244e0 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -245,6 +245,21 @@ jobs:
           python3 -c 'import pathlib, sys; names = [line for line in pathlib.Path(sys.argv[1]).read_text().splitlines() if line.startswith("core_executor::tests::inference_tests::") and "rejects_contract_only_with_lifecycle" in line and line.endswith(": test")]; print("Contract-only lifecycle tests discovered:", len(names)); sys.exit(0 if names else 1)' "$RUNNER_TEMP/node-validation-lifecycle-tests.list"
           cargo test -p node-engine --features inference-nodes --lib rejects_contract_only_with_lifecycle
 
+      - name: Run typed embedding capture regressions
+        run: |
+          set -euo pipefail
+          tests=(
+            core_executor::tests::inference_tests::test_canonical_llm_embedding_uses_typed_gateway_boundary
+            core_executor::tests::inference_tests::test_canonical_llm_embedding_with_package_facts_emits_compatibility_lifecycle
+          )
+          cargo test -p node-engine --features inference-nodes --lib test_canonical_llm_embedding -- --list > "$RUNNER_TEMP/node-embedding-tests.list"
+          for test_name in "${tests[@]}"; do
+            python3 -c 'import pathlib, sys; lines = pathlib.Path(sys.argv[1]).read_text().splitlines(); sys.exit(0 if sys.argv[2] + ": test" in lines else 1)' "$RUNNER_TEMP/node-embedding-tests.list" "$test_name"
+            result_path="$RUNNER_TEMP/node-embedding-${test_name##*::}.log"
+            cargo test -p node-engine --features inference-nodes --lib "$test_name" -- --exact 2>&1 | tee "$result_path"
+            python3 -c 'import pathlib, re, sys; text = pathlib.Path(sys.argv[1]).read_text(); sys.exit(0 if re.search(r"^test result: ok\. 1 passed; 0 failed; 0 ignored;", text, re.M) else 1)' "$result_path"
+          done
+
       - name: Run node-engine tests
         run: cargo test -p node-engine --lib
 
diff --git a/crates/node-engine/src/core_executor/inference_tests.rs b/crates/node-engine/src/core_executor/inference_tests.rs
index 6e4660cbe..5261b7ab2 100644
--- a/crates/node-engine/src/core_executor/inference_tests.rs
+++ b/crates/node-engine/src/core_executor/inference_tests.rs
@@ -3177,9 +3177,12 @@ impl InferenceBackend for MockTypedTextBackend {
     }
 }
 
+#[cfg(feature = "inference-nodes")]
+type CapturedEmbeddingRequest = (Vec, String);
+
 #[cfg(feature = "inference-nodes")]
 struct MockTypedEmbeddingBackend {
-    embedding_requests: Arc, String)>>>,
+    embedding_requests: Arc>>,
 }
 
 #[cfg(feature = "inference-nodes")]
diff --git a/crates/node-engine/src/engine/dependency_inputs.rs b/crates/node-engine/src/engine/dependency_inputs.rs
index 89869b4fb..df812fe07 100644
--- a/crates/node-engine/src/engine/dependency_inputs.rs
+++ b/crates/node-engine/src/engine/dependency_inputs.rs
@@ -421,7 +421,7 @@ mod tests {
             inputs.get("text"),
             Some(&serde_json::json!("generated text"))
         );
-        assert!(inputs.get("stream").is_none());
+        assert!(!inputs.contains_key("stream"));
     }
 
     #[test]
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-engine-test-lints.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-engine-test-lints.md
new file mode 100644
index 000000000..1c6ffef43
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-engine-test-lints.md
@@ -0,0 +1,7 @@
+# Node-engine test-only lint repair
+
+Fresh strict Clippy at PR43 head e2dbe87 clears the UniFFI library and reaches two node-engine lib-test findings. Both exact source expressions also exist at main 4938e405. This milestone only names the captured embedding request tuple (Vec, String) with a private feature-gated alias and expresses absent stream membership with !contains_key instead of get().is_none(). All tuple ordering, values, test actions and assertions are unchanged; there is no production API or behavior change.
+
+The embedding mock has two constructors and two existing test readers, plus one push site. Existing tests assert captured input/model and compatibility lifecycle/output behavior. Because the default node-engine suite does not enable inference-nodes, add a feature-enabled gate requiring both exact existing canonical embedding test names and an individual execution receipt of 1 passed / 0 failed / 0 ignored for each, retaining the full default suite and strict aggregate lint.
+
+Root source review accepted the mechanical repairs and the corrected exact-name/execution-count gate at frozen tree 2b5bc9252887e86bed791b4c759923592ff9435d. Fresh hosted execution remains pending. Full pinned formatting passes locally; no local Rust build is claimed. PR43 shutdown success/failure and regenerated C# qualification continue independently on its exact published head.

From 9a4679604ca65647fdb49d4b940b00d4d9b2bb69 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 10:27:15 -0700
Subject: [PATCH 57/65] Collect independent strict Clippy failures without
 early abort

---
 .github/workflows/quality-gates.yml                           | 2 +-
 .../reports/2026-10-03-node-engine-test-lints.md              | 4 ++++
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index d2b2244e0..50ee54cf5 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -456,7 +456,7 @@ jobs:
       - name: Run warning-deny clippy audit
         id: run-clippy-audit
         continue-on-error: true
-        run: cargo clippy --workspace --all-targets --all-features -- -D warnings
+        run: cargo clippy --workspace --all-targets --all-features --keep-going -- -D warnings
 
       - name: Record clippy audit result
         id: report-clippy-audit
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-engine-test-lints.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-engine-test-lints.md
index 1c6ffef43..995b8dbd3 100644
--- a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-engine-test-lints.md
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-node-engine-test-lints.md
@@ -5,3 +5,7 @@ Fresh strict Clippy at PR43 head e2dbe87 clears the UniFFI library and reaches t
 The embedding mock has two constructors and two existing test readers, plus one push site. Existing tests assert captured input/model and compatibility lifecycle/output behavior. Because the default node-engine suite does not enable inference-nodes, add a feature-enabled gate requiring both exact existing canonical embedding test names and an individual execution receipt of 1 passed / 0 failed / 0 ignored for each, retaining the full default suite and strict aggregate lint.
 
 Root source review accepted the mechanical repairs and the corrected exact-name/execution-count gate at frozen tree 2b5bc9252887e86bed791b4c759923592ff9435d. Fresh hosted execution remains pending. Full pinned formatting passes locally; no local Rust build is claimed. PR43 shutdown success/failure and regenerated C# qualification continue independently on its exact published head.
+
+## Complete independent diagnostics without weakening failure
+
+Root separately reviewed and accepted adding only --keep-going to the existing strict workspace/all-targets/all-features Clippy invocation. Pinned Cargo 1.92 lists this option, and Clippy delegates these options to cargo check. A dependency-free two-crate local probe emitted both independent denied warnings and still exited 101 (56 KiB output directory, 0.13 seconds). The workflow retains -D warnings and its explicit failing audit/aggregate result. This permits diagnostics from independent compilable crates; dependencies that fail compilation can still prevent downstream checks. No lint is suppressed and no warning becomes acceptable.

From 854a245b7ed711b585c45ba8ff66ae75dd3920b2 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 12:50:18 -0700
Subject: [PATCH 58/65] fix(ci): clear aggregate mechanical and test lint
 findings

---
 .github/workflows/quality-gates.yml           |   2 +
 crates/inference/src/resource_monitor/mod.rs  |   2 +-
 .../src/tests.rs                              |  14 +-
 .../lib_tests/data_graph_execution_tests.rs   |   2 +-
 .../lib_tests/workflow_run_execution_tests.rs |   2 +-
 .../src/node_io_artifacts.rs                  | 134 +++++++++---------
 .../src/task_executor_tests/puma_lib.rs       |  42 +++---
 .../src/technical_fit.rs                      |   2 +-
 .../src/redistributables/paths.rs             |  44 +++---
 .../pantograph-scheduler/tests/queue_state.rs |   4 +-
 .../src/graph/connection_insert.rs            |  56 ++++----
 .../src/graph/registry.rs                     |   2 +-
 .../runtime_branch_batch_execution.rs         |   2 +-
 .../src/workflow/service_config.rs            |  40 +++---
 .../src/workflow/tests/session_execution.rs   |   2 +-
 .../workflow/tests/task_result_contracts.rs   |   6 +-
 .../src/workflow/tests/workflow_version.rs    |   2 +-
 .../src/workflow/validation.rs                |  34 ++---
 .../tests/artifact_store.rs                   |   2 +-
 crates/workflow-nodes/src/input/puma_lib.rs   |   2 +-
 .../2026-10-03-aggregate-test-style.md        |  11 ++
 src-tauri/src/llm/commands/agent.rs           |  26 ++--
 src-tauri/src/workflow/diagnostics/overlay.rs |   2 +-
 .../tests/translation_projection.rs           |   2 +-
 src-tauri/src/workflow/puma_lib_commands.rs   |   2 +-
 25 files changed, 225 insertions(+), 214 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-aggregate-test-style.md

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 50ee54cf5..5a4a6a76e 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -311,6 +311,8 @@ jobs:
           cargo test -p pantograph-workflow-service --lib scheduler::task_orchestrator::tests::
       - name: Run workflow-service contract tests
         run: cargo test -p pantograph-workflow-service --test contract
+      - name: Run artifact-store integration contracts
+        run: cargo test -p pantograph-workflow-service --test artifact_store
 
   rust-doc-tests:
     name: Rust doc tests
diff --git a/crates/inference/src/resource_monitor/mod.rs b/crates/inference/src/resource_monitor/mod.rs
index 2a9d64f49..8ad12f028 100644
--- a/crates/inference/src/resource_monitor/mod.rs
+++ b/crates/inference/src/resource_monitor/mod.rs
@@ -135,7 +135,7 @@ mod tests {
 
     #[test]
     fn unsupported_resource_monitor_returns_typed_unavailable_observation() {
-        let monitor = unsupported::UnsupportedRuntimeResourceMonitor::default();
+        let monitor = unsupported::UnsupportedRuntimeResourceMonitor;
         let guard = monitor
             .start_process_monitor(std::process::id())
             .expect("unsupported monitor starts");
diff --git a/crates/pantograph-diagnostics-ledger/src/tests.rs b/crates/pantograph-diagnostics-ledger/src/tests.rs
index 5d8ff19ba..8d28d95ba 100644
--- a/crates/pantograph-diagnostics-ledger/src/tests.rs
+++ b/crates/pantograph-diagnostics-ledger/src/tests.rs
@@ -1571,12 +1571,9 @@ fn scheduler_timeline_projection_includes_inference_execution_diagnostics() {
     assert!(detail.contains("cache handle observed"));
     assert!(detail.contains("artifact refs 1"));
     assert!(detail.contains("kv cache restore_input hit"));
-    assert_eq!(
-        record
-            .payload_json
-            .contains("generated text should not appear"),
-        false
-    );
+    assert!(!record
+        .payload_json
+        .contains("generated text should not appear"));
 }
 
 #[test]
@@ -5083,10 +5080,9 @@ fn sqlite_column_exists(conn: &Connection, table_name: &str, column_name: &str)
     let columns = stmt
         .query_map([], |row| row.get::<_, String>(1))
         .expect("table info query succeeds");
-    let exists = columns
+    columns
         .map(|column| column.expect("column row loads"))
-        .any(|column| column == column_name);
-    exists
+        .any(|column| column == column_name)
 }
 
 fn assert_columns_exist(conn: &Connection, table_name: &str, column_names: &[&str]) {
diff --git a/crates/pantograph-embedded-runtime/src/lib_tests/data_graph_execution_tests.rs b/crates/pantograph-embedded-runtime/src/lib_tests/data_graph_execution_tests.rs
index f90e8a9bf..3c3c1b01e 100644
--- a/crates/pantograph-embedded-runtime/src/lib_tests/data_graph_execution_tests.rs
+++ b/crates/pantograph-embedded-runtime/src/lib_tests/data_graph_execution_tests.rs
@@ -41,7 +41,7 @@ async fn execute_data_graph_retired_onnx_audio_path_does_not_call_python_sidecar
         .await
         .expect("data graph execution");
 
-    assert!(outputs.get("audio").is_none());
+    assert!(!outputs.contains_key("audio"));
     assert_eq!(
         outputs.get("_graph_id"),
         Some(&serde_json::json!("runtime-onnx-audio-data-graph"))
diff --git a/crates/pantograph-embedded-runtime/src/lib_tests/workflow_run_execution_tests.rs b/crates/pantograph-embedded-runtime/src/lib_tests/workflow_run_execution_tests.rs
index 40fe05b9c..89336a945 100644
--- a/crates/pantograph-embedded-runtime/src/lib_tests/workflow_run_execution_tests.rs
+++ b/crates/pantograph-embedded-runtime/src/lib_tests/workflow_run_execution_tests.rs
@@ -450,7 +450,7 @@ fn image_runtime_validation_snapshot(
         workflow_semantic_version: version.semantic_version.clone(),
         workflow_execution_fingerprint: version.execution_fingerprint.clone(),
         descriptor_contract_version: INFERENCE_INTERFACE_CONTRACT_VERSION,
-        graph_revision: WorkflowGraphRevision::parse(&graph.compute_fingerprint())
+        graph_revision: WorkflowGraphRevision::parse(graph.compute_fingerprint())
             .expect("valid graph revision"),
         validation_session_id: DraftGraphValidationSessionId::parse(
             "embedded_runtime_validation_session_1",
diff --git a/crates/pantograph-embedded-runtime/src/node_io_artifacts.rs b/crates/pantograph-embedded-runtime/src/node_io_artifacts.rs
index d7ddc31e1..d5034f075 100644
--- a/crates/pantograph-embedded-runtime/src/node_io_artifacts.rs
+++ b/crates/pantograph-embedded-runtime/src/node_io_artifacts.rs
@@ -304,6 +304,73 @@ fn node_io_artifact_format_metadata(media_type: &str) -> ArtifactFormatMetadata
     }
 }
 
+fn io_artifact_payload_kind(kind: ArtifactPayloadKind) -> IoArtifactPayloadKind {
+    match kind {
+        ArtifactPayloadKind::Text => IoArtifactPayloadKind::Text,
+        ArtifactPayloadKind::Image => IoArtifactPayloadKind::Image,
+        ArtifactPayloadKind::Audio => IoArtifactPayloadKind::Audio,
+        ArtifactPayloadKind::Video => IoArtifactPayloadKind::Video,
+        ArtifactPayloadKind::ThreeD => IoArtifactPayloadKind::ThreeD,
+        ArtifactPayloadKind::LargeTable => IoArtifactPayloadKind::LargeTable,
+        ArtifactPayloadKind::GenericBinary => IoArtifactPayloadKind::GenericBinary,
+        ArtifactPayloadKind::Structured => IoArtifactPayloadKind::Structured,
+    }
+}
+
+fn io_artifact_lifecycle_state(state: ArtifactLifecycleState) -> IoArtifactLifecycleState {
+    match state {
+        ArtifactLifecycleState::Declared => IoArtifactLifecycleState::Declared,
+        ArtifactLifecycleState::Writing => IoArtifactLifecycleState::Writing,
+        ArtifactLifecycleState::Streaming => IoArtifactLifecycleState::Streaming,
+        ArtifactLifecycleState::Finalizing => IoArtifactLifecycleState::Finalizing,
+        ArtifactLifecycleState::Retained => IoArtifactLifecycleState::Retained,
+        ArtifactLifecycleState::Failed => IoArtifactLifecycleState::Failed,
+        ArtifactLifecycleState::Expired => IoArtifactLifecycleState::Expired,
+        ArtifactLifecycleState::Deleted => IoArtifactLifecycleState::Deleted,
+    }
+}
+
+fn io_artifact_access_mode(mode: ArtifactAccessMode) -> IoArtifactAccessMode {
+    match mode {
+        ArtifactAccessMode::Read => IoArtifactAccessMode::Read,
+        ArtifactAccessMode::Download => IoArtifactAccessMode::Download,
+        ArtifactAccessMode::Stream => IoArtifactAccessMode::Stream,
+    }
+}
+
+fn io_artifact_format_metadata(format: ArtifactFormatMetadata) -> IoArtifactFormatMetadata {
+    IoArtifactFormatMetadata {
+        format_id: format.format_id,
+        media_type: format.media_type,
+        codec_id: format.codec_id,
+        quality_percent: format.quality_percent,
+        bitrate_kbps: format.bitrate_kbps,
+        crf: format.crf,
+        bit_depth: format.bit_depth,
+        color_profile_id: format.color_profile_id,
+        converter_id: format.converter_id,
+        converter_version: format.converter_version,
+        library_version: format.library_version,
+        conversion_id: format.conversion_id,
+        conversion_status: format.conversion_status.map(|status| match status {
+            ArtifactConversionStatus::Converted => IoArtifactConversionStatus::Converted,
+            ArtifactConversionStatus::PassedThrough => IoArtifactConversionStatus::PassedThrough,
+            ArtifactConversionStatus::Failed => IoArtifactConversionStatus::Failed,
+        }),
+        conversion_command_id: format.conversion_command_id,
+        conversion_dependencies: format
+            .conversion_dependencies
+            .into_iter()
+            .map(|dependency| IoArtifactConversionDependency {
+                dependency_id: dependency.dependency_id,
+                active_version: dependency.active_version,
+                lease_id: dependency.lease_id,
+                lease_holder: dependency.lease_holder,
+            })
+            .collect(),
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::{
@@ -394,70 +461,3 @@ mod tests {
         );
     }
 }
-
-fn io_artifact_payload_kind(kind: ArtifactPayloadKind) -> IoArtifactPayloadKind {
-    match kind {
-        ArtifactPayloadKind::Text => IoArtifactPayloadKind::Text,
-        ArtifactPayloadKind::Image => IoArtifactPayloadKind::Image,
-        ArtifactPayloadKind::Audio => IoArtifactPayloadKind::Audio,
-        ArtifactPayloadKind::Video => IoArtifactPayloadKind::Video,
-        ArtifactPayloadKind::ThreeD => IoArtifactPayloadKind::ThreeD,
-        ArtifactPayloadKind::LargeTable => IoArtifactPayloadKind::LargeTable,
-        ArtifactPayloadKind::GenericBinary => IoArtifactPayloadKind::GenericBinary,
-        ArtifactPayloadKind::Structured => IoArtifactPayloadKind::Structured,
-    }
-}
-
-fn io_artifact_lifecycle_state(state: ArtifactLifecycleState) -> IoArtifactLifecycleState {
-    match state {
-        ArtifactLifecycleState::Declared => IoArtifactLifecycleState::Declared,
-        ArtifactLifecycleState::Writing => IoArtifactLifecycleState::Writing,
-        ArtifactLifecycleState::Streaming => IoArtifactLifecycleState::Streaming,
-        ArtifactLifecycleState::Finalizing => IoArtifactLifecycleState::Finalizing,
-        ArtifactLifecycleState::Retained => IoArtifactLifecycleState::Retained,
-        ArtifactLifecycleState::Failed => IoArtifactLifecycleState::Failed,
-        ArtifactLifecycleState::Expired => IoArtifactLifecycleState::Expired,
-        ArtifactLifecycleState::Deleted => IoArtifactLifecycleState::Deleted,
-    }
-}
-
-fn io_artifact_access_mode(mode: ArtifactAccessMode) -> IoArtifactAccessMode {
-    match mode {
-        ArtifactAccessMode::Read => IoArtifactAccessMode::Read,
-        ArtifactAccessMode::Download => IoArtifactAccessMode::Download,
-        ArtifactAccessMode::Stream => IoArtifactAccessMode::Stream,
-    }
-}
-
-fn io_artifact_format_metadata(format: ArtifactFormatMetadata) -> IoArtifactFormatMetadata {
-    IoArtifactFormatMetadata {
-        format_id: format.format_id,
-        media_type: format.media_type,
-        codec_id: format.codec_id,
-        quality_percent: format.quality_percent,
-        bitrate_kbps: format.bitrate_kbps,
-        crf: format.crf,
-        bit_depth: format.bit_depth,
-        color_profile_id: format.color_profile_id,
-        converter_id: format.converter_id,
-        converter_version: format.converter_version,
-        library_version: format.library_version,
-        conversion_id: format.conversion_id,
-        conversion_status: format.conversion_status.map(|status| match status {
-            ArtifactConversionStatus::Converted => IoArtifactConversionStatus::Converted,
-            ArtifactConversionStatus::PassedThrough => IoArtifactConversionStatus::PassedThrough,
-            ArtifactConversionStatus::Failed => IoArtifactConversionStatus::Failed,
-        }),
-        conversion_command_id: format.conversion_command_id,
-        conversion_dependencies: format
-            .conversion_dependencies
-            .into_iter()
-            .map(|dependency| IoArtifactConversionDependency {
-                dependency_id: dependency.dependency_id,
-                active_version: dependency.active_version,
-                lease_id: dependency.lease_id,
-                lease_holder: dependency.lease_holder,
-            })
-            .collect(),
-    }
-}
diff --git a/crates/pantograph-embedded-runtime/src/task_executor_tests/puma_lib.rs b/crates/pantograph-embedded-runtime/src/task_executor_tests/puma_lib.rs
index bce51235e..479ee3fa7 100644
--- a/crates/pantograph-embedded-runtime/src/task_executor_tests/puma_lib.rs
+++ b/crates/pantograph-embedded-runtime/src/task_executor_tests/puma_lib.rs
@@ -49,7 +49,7 @@ async fn puma_lib_execution_hydrates_model_ref_from_model_id_without_path_output
         .expect("puma-lib should resolve selector metadata");
 
     assert!(
-        outputs.get("model_path").is_none(),
+        !outputs.contains_key("model_path"),
         "puma-lib must not emit executable path outputs"
     );
     assert_eq!(
@@ -74,15 +74,15 @@ async fn puma_lib_execution_hydrates_model_ref_from_model_id_without_path_output
         "puma-lib must not hide executable paths inside pumas_model_ref"
     );
     assert!(
-        outputs.get("backend_key").is_none(),
+        !outputs.contains_key("backend_key"),
         "puma-lib must not emit graph-visible backend-key aliases"
     );
     assert!(
-        outputs.get("resolved_model_package_facts").is_none(),
+        !outputs.contains_key("resolved_model_package_facts"),
         "puma-lib must not emit hidden package facts"
     );
     assert!(
-        outputs.get("resolved_model_artifact_load_target").is_none(),
+        !outputs.contains_key("resolved_model_artifact_load_target"),
         "puma-lib must not emit hidden artifact load targets"
     );
 }
@@ -117,7 +117,7 @@ async fn puma_lib_execution_preserves_explicit_model_ref_without_model_path_outp
         .expect("puma-lib should preserve explicit model ref");
 
     assert!(
-        outputs.get("model_path").is_none(),
+        !outputs.contains_key("model_path"),
         "puma-lib must not promote selected artifact paths to executable outputs"
     );
     assert_eq!(
@@ -133,7 +133,7 @@ async fn puma_lib_execution_preserves_explicit_model_ref_without_model_path_outp
         ))
     );
     assert!(
-        outputs.get("backend_key").is_none(),
+        !outputs.contains_key("backend_key"),
         "puma-lib must not emit graph-visible backend-key aliases"
     );
 }
@@ -182,7 +182,7 @@ async fn puma_lib_execution_does_not_rebind_model_id_from_raw_pumas_api() {
         .expect("puma-lib should preserve saved data without selector access");
 
     assert!(
-        outputs.get("model_path").is_none(),
+        !outputs.contains_key("model_path"),
         "raw saved paths must not be emitted as executable outputs"
     );
     assert_eq!(
@@ -196,15 +196,15 @@ async fn puma_lib_execution_does_not_rebind_model_id_from_raw_pumas_api() {
         Some(&serde_json::json!(model_id))
     );
     assert!(
-        outputs.get("backend_key").is_none(),
+        !outputs.contains_key("backend_key"),
         "puma-lib must not emit graph-visible backend-key aliases"
     );
     assert!(
-        outputs.get("resolved_model_package_facts").is_none(),
+        !outputs.contains_key("resolved_model_package_facts"),
         "raw PUMAS_API alone must not rehydrate selected model facts"
     );
     assert!(
-        outputs.get("resolved_model_artifact_load_target").is_none(),
+        !outputs.contains_key("resolved_model_artifact_load_target"),
         "raw PUMAS_API alone must not rehydrate selected artifact load targets"
     );
 }
@@ -260,7 +260,7 @@ async fn puma_lib_execution_hydrates_model_ref_from_selector_access_without_puma
         .expect("puma-lib should resolve selector metadata from selector access");
 
     assert!(
-        outputs.get("model_path").is_none(),
+        !outputs.contains_key("model_path"),
         "puma-lib must not emit executable path outputs"
     );
     assert_eq!(outputs.get("model_id"), Some(&serde_json::json!(model_id)));
@@ -282,11 +282,11 @@ async fn puma_lib_execution_hydrates_model_ref_from_selector_access_without_puma
         "puma-lib must not hide executable paths inside pumas_model_ref"
     );
     assert!(
-        outputs.get("resolved_model_package_facts").is_none(),
+        !outputs.contains_key("resolved_model_package_facts"),
         "read-only selector rows must not be promoted to full package facts"
     );
     assert!(
-        outputs.get("resolved_model_artifact_load_target").is_none(),
+        !outputs.contains_key("resolved_model_artifact_load_target"),
         "read-only selector rows must not be promoted to artifact load targets"
     );
 }
@@ -349,7 +349,7 @@ async fn puma_lib_execution_does_not_emit_inference_settings_from_saved_or_selec
         .expect("puma-lib should resolve selected detail without inference settings");
 
     assert!(
-        outputs.get("inference_settings").is_none(),
+        !outputs.contains_key("inference_settings"),
         "puma-lib must not emit inference settings from saved node data or selected detail"
     );
 }
@@ -396,13 +396,13 @@ async fn puma_lib_execution_does_not_resolve_saved_model_name_without_model_id()
         .expect("puma-lib should execute with saved data only");
 
     assert!(
-        outputs.get("model_path").is_none(),
+        !outputs.contains_key("model_path"),
         "puma-lib must not emit empty path outputs"
     );
-    assert!(outputs.get("model_id").is_none());
-    assert!(outputs.get("model_type").is_none());
-    assert!(outputs.get("task_type_primary").is_none());
-    assert!(outputs.get("resolved_model_package_facts").is_none());
-    assert!(outputs.get("resolved_model_artifact_load_target").is_none());
-    assert!(outputs.get("pumas_model_ref").is_none());
+    assert!(!outputs.contains_key("model_id"));
+    assert!(!outputs.contains_key("model_type"));
+    assert!(!outputs.contains_key("task_type_primary"));
+    assert!(!outputs.contains_key("resolved_model_package_facts"));
+    assert!(!outputs.contains_key("resolved_model_artifact_load_target"));
+    assert!(!outputs.contains_key("pumas_model_ref"));
 }
diff --git a/crates/pantograph-embedded-runtime/src/technical_fit.rs b/crates/pantograph-embedded-runtime/src/technical_fit.rs
index 74d1534e3..5cf66eef6 100644
--- a/crates/pantograph-embedded-runtime/src/technical_fit.rs
+++ b/crates/pantograph-embedded-runtime/src/technical_fit.rs
@@ -1837,7 +1837,7 @@ mod tests {
                         kv_cache: inference::BackendFeatureSupport::Supported,
                     },
                     runtime_variants: vec![inference::RuntimeVariantCapability {
-                        runtime_variant_id: inference::RuntimeVariantId::parse(&format!(
+                        runtime_variant_id: inference::RuntimeVariantId::parse(format!(
                             "{}.cuda",
                             backend_key
                         ))
diff --git a/crates/pantograph-managed-dependencies/src/redistributables/paths.rs b/crates/pantograph-managed-dependencies/src/redistributables/paths.rs
index 8e89e663a..7f623f046 100644
--- a/crates/pantograph-managed-dependencies/src/redistributables/paths.rs
+++ b/crates/pantograph-managed-dependencies/src/redistributables/paths.rs
@@ -121,6 +121,28 @@ pub(crate) fn library_path(name: &str) -> String {
     }
 }
 
+pub(crate) fn sanitize_path_segment(segment: &str) -> String {
+    segment
+        .chars()
+        .map(|character| {
+            if character.is_ascii_alphanumeric() || matches!(character, '.' | '-' | '_') {
+                character
+            } else {
+                '_'
+            }
+        })
+        .collect()
+}
+
+pub(crate) fn current_unix_timestamp_ms() -> u64 {
+    use std::time::{SystemTime, UNIX_EPOCH};
+
+    SystemTime::now()
+        .duration_since(UNIX_EPOCH)
+        .unwrap_or_default()
+        .as_millis() as u64
+}
+
 #[cfg(test)]
 mod tests {
     use super::{managed_redistributable_version_dir, ManagedRedistributableId};
@@ -146,25 +168,3 @@ mod tests {
         );
     }
 }
-
-pub(crate) fn sanitize_path_segment(segment: &str) -> String {
-    segment
-        .chars()
-        .map(|character| {
-            if character.is_ascii_alphanumeric() || matches!(character, '.' | '-' | '_') {
-                character
-            } else {
-                '_'
-            }
-        })
-        .collect()
-}
-
-pub(crate) fn current_unix_timestamp_ms() -> u64 {
-    use std::time::{SystemTime, UNIX_EPOCH};
-
-    SystemTime::now()
-        .duration_since(UNIX_EPOCH)
-        .unwrap_or_default()
-        .as_millis() as u64
-}
diff --git a/crates/pantograph-scheduler/tests/queue_state.rs b/crates/pantograph-scheduler/tests/queue_state.rs
index 766d2814e..ec04122b3 100644
--- a/crates/pantograph-scheduler/tests/queue_state.rs
+++ b/crates/pantograph-scheduler/tests/queue_state.rs
@@ -34,8 +34,10 @@ fn boxed_runtime_intent_preserves_exact_json_and_borrowed_payload() {
 #[test]
 fn boxed_runtime_intent_retains_validation_and_task_correlation() {
     let valid = task_intent("run.001", "task.001");
-    ValidatedSchedulerTaskStateRecord::try_from(task_record_with_state(ready_state(valid.clone())))
+    let expected_record = task_record_with_state(ready_state(valid.clone()));
+    let validated = ValidatedSchedulerTaskStateRecord::try_from(expected_record.clone())
         .expect("valid runtime intent remains accepted");
+    assert_eq!(validated.as_ref(), &expected_record);
     let mut invalid = valid;
     invalid.contract_version = 0;
     let expected = invalid.validate().expect_err("invalid raw version");
diff --git a/crates/pantograph-workflow-service/src/graph/connection_insert.rs b/crates/pantograph-workflow-service/src/graph/connection_insert.rs
index e436aa8f5..bd49a8b39 100644
--- a/crates/pantograph-workflow-service/src/graph/connection_insert.rs
+++ b/crates/pantograph-workflow-service/src/graph/connection_insert.rs
@@ -383,6 +383,34 @@ pub fn rejected_insert_response(
     }
 }
 
+pub fn rejected_edge_insert_preview_response(
+    graph: &WorkflowGraph,
+    rejection: ConnectionRejection,
+) -> EdgeInsertionPreviewResponse {
+    EdgeInsertionPreviewResponse {
+        accepted: false,
+        graph_revision: graph.compute_fingerprint(),
+        bridge: None,
+        rejection: Some(rejection),
+    }
+}
+
+pub fn rejected_insert_on_edge_response(
+    graph: &WorkflowGraph,
+    rejection: ConnectionRejection,
+) -> InsertNodeOnEdgeResponse {
+    InsertNodeOnEdgeResponse {
+        accepted: false,
+        graph_revision: graph.compute_fingerprint(),
+        inserted_node_id: None,
+        bridge: None,
+        graph: Some(graph.clone()),
+        workflow_event: None,
+        workflow_execution_session_state: None,
+        rejection: Some(rejection),
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -419,31 +447,3 @@ mod tests {
         );
     }
 }
-
-pub fn rejected_edge_insert_preview_response(
-    graph: &WorkflowGraph,
-    rejection: ConnectionRejection,
-) -> EdgeInsertionPreviewResponse {
-    EdgeInsertionPreviewResponse {
-        accepted: false,
-        graph_revision: graph.compute_fingerprint(),
-        bridge: None,
-        rejection: Some(rejection),
-    }
-}
-
-pub fn rejected_insert_on_edge_response(
-    graph: &WorkflowGraph,
-    rejection: ConnectionRejection,
-) -> InsertNodeOnEdgeResponse {
-    InsertNodeOnEdgeResponse {
-        accepted: false,
-        graph_revision: graph.compute_fingerprint(),
-        inserted_node_id: None,
-        bridge: None,
-        graph: Some(graph.clone()),
-        workflow_event: None,
-        workflow_execution_session_state: None,
-        rejection: Some(rejection),
-    }
-}
diff --git a/crates/pantograph-workflow-service/src/graph/registry.rs b/crates/pantograph-workflow-service/src/graph/registry.rs
index 0850822d0..1828c6433 100644
--- a/crates/pantograph-workflow-service/src/graph/registry.rs
+++ b/crates/pantograph-workflow-service/src/graph/registry.rs
@@ -166,7 +166,7 @@ mod tests {
         let payload = &encoded["inference_payloads"][0];
         assert_eq!(payload["role"], serde_json::json!("diagnostics"));
         assert_eq!(payload["task_id"], serde_json::json!("text_generation"));
-        assert_llm_inference_payloads_do_not_expose_runtime_policy(&definition);
+        assert_llm_inference_payloads_do_not_expose_runtime_policy(definition);
     }
 
     #[test]
diff --git a/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs b/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs
index be0d264c8..b91ca792c 100644
--- a/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs
+++ b/crates/pantograph-workflow-service/src/workflow/runtime_branch_batch_execution.rs
@@ -1981,7 +1981,7 @@ mod tests {
             plan.runtime_host_request
                 .members
                 .iter()
-                .map(|member| prompt_text_from_runtime_host_member_request(member))
+                .map(prompt_text_from_runtime_host_member_request)
                 .collect::>(),
             vec![
                 "prompt owned by run.2026-05-22.001".to_string(),
diff --git a/crates/pantograph-workflow-service/src/workflow/service_config.rs b/crates/pantograph-workflow-service/src/workflow/service_config.rs
index 1fe7892f8..7baeca144 100644
--- a/crates/pantograph-workflow-service/src/workflow/service_config.rs
+++ b/crates/pantograph-workflow-service/src/workflow/service_config.rs
@@ -439,6 +439,26 @@ fn dependency_requirements_registry_error(
     ))
 }
 
+fn load_artifact_format_settings(
+    path: &Path,
+) -> Result {
+    if !path.exists() {
+        return Ok(ArtifactFormatSettings::default());
+    }
+    let content = std::fs::read_to_string(path).map_err(|error| {
+        WorkflowServiceError::Internal(format!(
+            "failed to read artifact format settings {:?}: {error}",
+            path
+        ))
+    })?;
+    serde_json::from_str(&content).map_err(|error| {
+        WorkflowServiceError::InvalidRequest(format!(
+            "artifact format settings file {:?} is invalid: {error}",
+            path
+        ))
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use std::sync::{Arc, Mutex};
@@ -594,23 +614,3 @@ mod tests {
         }
     }
 }
-
-fn load_artifact_format_settings(
-    path: &Path,
-) -> Result {
-    if !path.exists() {
-        return Ok(ArtifactFormatSettings::default());
-    }
-    let content = std::fs::read_to_string(path).map_err(|error| {
-        WorkflowServiceError::Internal(format!(
-            "failed to read artifact format settings {:?}: {error}",
-            path
-        ))
-    })?;
-    serde_json::from_str(&content).map_err(|error| {
-        WorkflowServiceError::InvalidRequest(format!(
-            "artifact format settings file {:?} is invalid: {error}",
-            path
-        ))
-    })
-}
diff --git a/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs b/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs
index fa52cdfee..311f1a1cd 100644
--- a/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs
+++ b/crates/pantograph-workflow-service/src/workflow/tests/session_execution.rs
@@ -4801,7 +4801,7 @@ fn runtime_executable_validation_snapshot(
         workflow_semantic_version: version.semantic_version.clone(),
         workflow_execution_fingerprint: version.execution_fingerprint.clone(),
         descriptor_contract_version: INFERENCE_INTERFACE_CONTRACT_VERSION,
-        graph_revision: WorkflowGraphRevision::parse(&graph.compute_fingerprint())
+        graph_revision: WorkflowGraphRevision::parse(graph.compute_fingerprint())
             .expect("valid graph revision"),
         validation_session_id: DraftGraphValidationSessionId::parse("runtime_validation_session_1")
             .expect("valid validation session id"),
diff --git a/crates/pantograph-workflow-service/src/workflow/tests/task_result_contracts.rs b/crates/pantograph-workflow-service/src/workflow/tests/task_result_contracts.rs
index fd3b1e8be..bea71a2ee 100644
--- a/crates/pantograph-workflow-service/src/workflow/tests/task_result_contracts.rs
+++ b/crates/pantograph-workflow-service/src/workflow/tests/task_result_contracts.rs
@@ -61,9 +61,9 @@ fn scheduler_task_result_validates_path_free_typed_outputs() {
         encoded["outputs"][1]["value"]["value"]["artifact_id"],
         "artifact-image-1"
     );
-    assert_eq!(encoded.to_string().contains("model_path"), false);
-    assert_eq!(encoded.to_string().contains("local_load_path"), false);
-    assert_eq!(encoded.to_string().contains("runtime_handoff"), false);
+    assert!(!encoded.to_string().contains("model_path"));
+    assert!(!encoded.to_string().contains("local_load_path"));
+    assert!(!encoded.to_string().contains("runtime_handoff"));
 }
 
 #[test]
diff --git a/crates/pantograph-workflow-service/src/workflow/tests/workflow_version.rs b/crates/pantograph-workflow-service/src/workflow/tests/workflow_version.rs
index 762ff0cb0..79bbcb226 100644
--- a/crates/pantograph-workflow-service/src/workflow/tests/workflow_version.rs
+++ b/crates/pantograph-workflow-service/src/workflow/tests/workflow_version.rs
@@ -423,7 +423,7 @@ fn executable_validation_publication(
     graph: &WorkflowGraph,
 ) -> WorkflowGraphInferenceValidationPublication {
     let graph_revision =
-        WorkflowGraphRevision::parse(&graph.compute_fingerprint()).expect("valid graph revision");
+        WorkflowGraphRevision::parse(graph.compute_fingerprint()).expect("valid graph revision");
     let validation_session_id = DraftGraphValidationSessionId::parse("validation_session_publish")
         .expect("valid validation session id");
     let summary = DraftGraphValidationSummary {
diff --git a/crates/pantograph-workflow-service/src/workflow/validation.rs b/crates/pantograph-workflow-service/src/workflow/validation.rs
index 7ba8508a9..d0588c9e8 100644
--- a/crates/pantograph-workflow-service/src/workflow/validation.rs
+++ b/crates/pantograph-workflow-service/src/workflow/validation.rs
@@ -154,23 +154,6 @@ pub(super) fn validate_bindings(
     Ok(())
 }
 
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn stale_graph_remaining_count_rejects_formatter_underflow() {
-        let error = remaining_stale_graph_diagnostic_count(1, 2)
-            .expect_err("shown diagnostics cannot exceed total diagnostics");
-
-        assert!(matches!(
-            error,
-            WorkflowServiceError::Internal(message)
-                if message.contains("formatter showed 2 reasons for 1 diagnostics")
-        ));
-    }
-}
-
 pub(super) fn validate_host_output_bindings(
     bindings: &[WorkflowPortBinding],
     field_name: &str,
@@ -340,3 +323,20 @@ pub(super) fn validate_payload_size(
 
     Ok(())
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn stale_graph_remaining_count_rejects_formatter_underflow() {
+        let error = remaining_stale_graph_diagnostic_count(1, 2)
+            .expect_err("shown diagnostics cannot exceed total diagnostics");
+
+        assert!(matches!(
+            error,
+            WorkflowServiceError::Internal(message)
+                if message.contains("formatter showed 2 reasons for 1 diagnostics")
+        ));
+    }
+}
diff --git a/crates/pantograph-workflow-service/tests/artifact_store.rs b/crates/pantograph-workflow-service/tests/artifact_store.rs
index 2178eea24..236531ae3 100644
--- a/crates/pantograph-workflow-service/tests/artifact_store.rs
+++ b/crates/pantograph-workflow-service/tests/artifact_store.rs
@@ -83,7 +83,7 @@ fn artifact_store_writes_descriptor_and_reads_body_without_path_leak() {
     );
     assert!(!serde_json::to_string(&descriptor)
         .expect("serialize descriptor")
-        .contains(temp.path().to_string_lossy().as_ref()));
+        .contains::<&str>(temp.path().to_string_lossy().as_ref()));
 
     let read = store
         .read_body(ArtifactReadRequest {
diff --git a/crates/workflow-nodes/src/input/puma_lib.rs b/crates/workflow-nodes/src/input/puma_lib.rs
index 6c6680cdb..2d7c09e86 100644
--- a/crates/workflow-nodes/src/input/puma_lib.rs
+++ b/crates/workflow-nodes/src/input/puma_lib.rs
@@ -271,7 +271,7 @@ mod options_provider {
         }
 
         pub(crate) fn needs_resolution(&self, model_id: &str) -> bool {
-            self.summaries.get(model_id).map_or(true, |result| {
+            self.summaries.get(model_id).is_none_or(|result| {
                 result.summary.is_none()
                     || matches!(
                         result.status,
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-aggregate-test-style.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-aggregate-test-style.md
new file mode 100644
index 000000000..741986314
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-aggregate-test-style.md
@@ -0,0 +1,11 @@
+# Aggregate mechanical and test qualification repair
+
+The first keep-going strict audit at 9a46796 emitted 51 diagnostics at 50 unique source sites across eight crate/desktop groups. Four desktop command/delegate arity sites are excluded for a wire-compatible design. This milestone addresses the remaining 46 sites without changing public signatures, data contracts, errors or validation policy.
+
+Exact semantic groups: 23 absent-key assertions become !contains_key (a present null value remains present); six redundant borrows are removed; four literal-false comparisons become negated assertions; one unit Default becomes its unit value; one Option::map_or(true, predicate) becomes is_none_or with the same predicate; one redundant closure becomes its function; one iterator result returns directly; one successful must-use scheduler validation now compares the complete validated raw record; and one desktop base64-length divisibility check uses is_multiple_of(4). The artifact-store path-redaction assertion explicitly selects the &str Pattern type, resolving the all-feature Cow AsRef ambiguity caused by typed_path without allocation or weakening the assertion.
+
+Six existing cfg(test) modules move after production items in node_io_artifacts, connection_insert, service_config, validation, redistributables/paths and desktop llm/commands/agent. Each moved test-module body is byte-identical after formatting; a Rust lexer comparison verifies all production tokens (including literals and comments) are identical before/after these moves. Production item ordering is retained.
+
+The required focused workflow retains all existing suites and adds the complete artifact_store integration target (13 source tests), so the compile-blocked redaction/storage contract now executes. Existing embedded, workflow-service, scheduler, ledger, inference and workflow-node suites remain. Desktop/test-only source changes still require actual hosted strict all-target compilation; no blanket lint allowance or assertion removal is introduced.
+
+Root requested this grouped mechanical milestone from the complete diagnostic inventory. Root source review accepted frozen tree 29cb5ce693a0b6a091a807d3cfa3b203a60468d3 after reviewing the non-move changes and independently checking identical nonblank line multisets for all six module moves. Exact-head hosted execution remains pending. Full pinned cargo fmt is checked locally; no heavy local Rust build is performed. The separate four Tauri arity findings remain open and must preserve existing IPC wire keys.
diff --git a/src-tauri/src/llm/commands/agent.rs b/src-tauri/src/llm/commands/agent.rs
index 889879154..65c9605da 100644
--- a/src-tauri/src/llm/commands/agent.rs
+++ b/src-tauri/src/llm/commands/agent.rs
@@ -508,19 +508,6 @@ fn format_agent_prompt_with_analysis(
     prompt
 }
 
-#[cfg(test)]
-mod tests {
-    use super::drawing_vision_contract_only_error;
-
-    #[test]
-    fn drawing_vision_error_points_to_typed_inference_contracts() {
-        let error = drawing_vision_contract_only_error();
-
-        assert!(error.contains("image_understanding"));
-        assert!(error.contains("canonical typed inference contracts"));
-    }
-}
-
 /// Format the agent prompt for fix/repair mode.
 /// Skips vision analysis, includes error context and file content directly.
 fn format_fix_mode_prompt(request: &AgentRequest) -> String {
@@ -588,3 +575,16 @@ fn create_component_updates(
         })
         .collect()
 }
+
+#[cfg(test)]
+mod tests {
+    use super::drawing_vision_contract_only_error;
+
+    #[test]
+    fn drawing_vision_error_points_to_typed_inference_contracts() {
+        let error = drawing_vision_contract_only_error();
+
+        assert!(error.contains("image_understanding"));
+        assert!(error.contains("canonical typed inference contracts"));
+    }
+}
diff --git a/src-tauri/src/workflow/diagnostics/overlay.rs b/src-tauri/src/workflow/diagnostics/overlay.rs
index 369eeee1c..f576c236d 100644
--- a/src-tauri/src/workflow/diagnostics/overlay.rs
+++ b/src-tauri/src/workflow/diagnostics/overlay.rs
@@ -434,7 +434,7 @@ fn is_inline_media_string_key(key: &str, value: &str) -> bool {
 fn is_probably_base64_body(value: &str) -> bool {
     let value = value.trim();
     value.len() >= 128
-        && value.len() % 4 == 0
+        && value.len().is_multiple_of(4)
         && value
             .bytes()
             .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'='))
diff --git a/src-tauri/src/workflow/event_adapter/tests/translation_projection.rs b/src-tauri/src/workflow/event_adapter/tests/translation_projection.rs
index 719fe35ea..5c7403afe 100644
--- a/src-tauri/src/workflow/event_adapter/tests/translation_projection.rs
+++ b/src-tauri/src/workflow/event_adapter/tests/translation_projection.rs
@@ -143,7 +143,7 @@ fn translated_task_inputs_resolved_event_preserves_inputs_without_trace_noise()
         TauriWorkflowEvent::DiagnosticsSnapshot { snapshot, .. } => {
             let trace = snapshot.runs_by_id.get("exec-1").expect("trace");
             assert_eq!(trace.event_count, 1);
-            assert!(trace.nodes.get("node-a").is_none());
+            assert!(!trace.nodes.contains_key("node-a"));
         }
         other => panic!("unexpected diagnostics event: {other:?}"),
     }
diff --git a/src-tauri/src/workflow/puma_lib_commands.rs b/src-tauri/src/workflow/puma_lib_commands.rs
index 3b670f181..ca8498756 100644
--- a/src-tauri/src/workflow/puma_lib_commands.rs
+++ b/src-tauri/src/workflow/puma_lib_commands.rs
@@ -160,7 +160,7 @@ pub async fn resolve_model_package_facts_summary(
         let ext = extensions.read().await;
         pumas_update_feed_access_from_extensions(&ext)
     };
-    resolve_model_package_facts_summary_from_access(selector_access, &model_id).await
+    resolve_model_package_facts_summary_from_access(selector_access, model_id).await
 }
 
 pub async fn list_model_library_updates_since(

From e47ddc85e42e0d3e65888e3fbee03e7dea3b2c4c Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 12:50:21 -0700
Subject: [PATCH 59/65] refactor(desktop): bundle injected Tauri command state

---
 .github/workflows/quality-gates.yml           |   3 +
 .../reports/2026-10-03-tauri-command-state.md |  21 ++
 scripts/check-tauri-command-state-tests.sh    |  21 ++
 src-tauri/Cargo.toml                          |   1 +
 src-tauri/src/workflow/command_state.rs       |  66 +++++
 src-tauri/src/workflow/command_state_tests.rs | 230 ++++++++++++++++++
 src-tauri/src/workflow/commands.rs            |  24 +-
 .../workflow/headless_workflow_commands.rs    |  18 +-
 src-tauri/src/workflow/mod.rs                 |   1 +
 .../workflow/workflow_port_query_commands.rs  |  12 +-
 10 files changed, 368 insertions(+), 29 deletions(-)
 create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-tauri-command-state.md
 create mode 100644 scripts/check-tauri-command-state-tests.sh
 create mode 100644 src-tauri/src/workflow/command_state.rs
 create mode 100644 src-tauri/src/workflow/command_state_tests.rs

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index 5a4a6a76e..e81a5ab8e 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -156,6 +156,9 @@ jobs:
       - name: Check workspace with all features
         run: cargo check --workspace --all-features
 
+      - name: Verify Tauri injected-state IPC contracts
+        run: bash scripts/check-tauri-command-state-tests.sh
+
   rust-tests:
     name: Rust focused tests
     runs-on: ubuntu-latest
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-tauri-command-state.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-tauri-command-state.md
new file mode 100644
index 000000000..b4b75606d
--- /dev/null
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-tauri-command-state.md
@@ -0,0 +1,21 @@
+# Tauri injected-state bundles with unchanged IPC payloads
+
+The keep-going strict audit reports four nine-argument sites: workflow_run_execution_session and query_port_options command wrappers plus their two sole delegates. Bundle only framework-injected State arguments in two operation-specific CommandArg implementations. No new managed owner, serialized request, client field, default or service lookup policy is introduced.
+
+WorkflowRunCommandState preserves this exact extraction sequence and macro-generated keys: gateway, runtimeRegistry, extensions, ragManager, workflowService, diagnosticsStore. PortOptionsCommandState preserves registry, extensions, workflowService. Each extraction calls Tauri State::from_command with the original plugin, command name, InvokeMessage and ACL reference, substituting only the original field key. The bundles hold the same borrowed State values and do not implement Deserialize. Client-supplied state fields cannot replace application-managed values. Missing-state errors retain the framework's original key/name text and first-failure order.
+
+The two wrappers have one delegate caller each; both delegates destructure the corresponding bundle then execute byte-identical previous bodies. The execution command still receives request, AppHandle and channel in their original relative order. The query command still receives flat nodeType, portId, search, limit, offset and context. The three frontend invokers (WorkflowCommandService, portOptionsCache and pumaModelOptionsCache) are unchanged. This is an internal Rust parameter grouping, not a Tauri wire-key rename.
+
+Five tests use the official Tauri MockRuntime/IPC path. They verify all six/three managed Arc identities, exact missing-state order/keys, and the real query_port_options handler with full and minimal flat payloads, original optional defaults, provider callbacks and managed extension data. Deliberately supplied client state values are ignored. A dormant gateway's test ProcessSpawner panics if unexpectedly used; no model/process/database setup is part of state extraction. Session state extraction is exercised through a test command using the same production bundle; the real session delegate body is separately byte-compared unchanged, rather than claimed executed by that test.
+
+The existing Tauri dependency gains only its test-only test feature (empty upstream feature, same locked 2.9.5 package). Required hosted workspace verification adds a script requiring all five exact test names and an individual 1 passed / 0 failed / 0 ignored receipt for each, with backend-llamacpp and no default app features. Existing all-feature workspace compilation and strict all-target Clippy remain. Frontend invocation tests remain part of the existing full frontend suite.
+
+Source review is pending. The source checkout was restored after a filesystem reset, and pinned Rust/rustfmt are currently unavailable. Formatting, Rust compilation and native mock-IPC execution are unrun locally; no installation or heavy build is attempted under the recovery hold. Bash syntax and the unchanged-body comparison pass. Publication remains paused with the separate mechanical batch until explicit retry authorization and the serialized write window are available.
+
+## Exact pinned-source compatibility
+
+Cargo.lock pins Tauri 2.9.5 and tauri-macros 2.5.2. The official tauri-v2.9.5 tag resolves to f2e0405dc2d8bae949de6bb5e479bbf3cde401b6, whose two manifests match those versions. [CommandItem and CommandArg](https://github.com/tauri-apps/tauri/blob/f2e0405dc2d8bae949de6bb5e479bbf3cde401b6/crates/tauri/src/ipc/command.rs) expose exactly the fields and method used here. [State extraction](https://github.com/tauri-apps/tauri/blob/f2e0405dc2d8bae949de6bb5e479bbf3cde401b6/crates/tauri/src/state.rs) has the same lifetime/outlives bounds, state-map lookup and missing-state error as the delegated calls.
+
+The pinned [command wrapper](https://github.com/tauri-apps/tauri/blob/f2e0405dc2d8bae949de6bb5e479bbf3cde401b6/crates/tauri-macros/src/command/wrapper.rs) builds arguments in signature order, defaults keys to camelCase, and calls CommandArg with the original plugin/name/message/ACL. Its async wrapper propagates the first argument error with ?, while its blocking wrapper reports the first match error. Grouping adjacent state arguments preserves this sequence inside the bundle.
+
+The pinned [mock IPC helpers](https://github.com/tauri-apps/tauri/blob/f2e0405dc2d8bae949de6bb5e479bbf3cde401b6/crates/tauri/src/test/mod.rs), InvokeRequest fields and InvokeBody::Json match the test fixture. The test module is enabled by the empty test feature; no package upgrade or additional dependency package is required. These are source compatibility checks, not compiled macro expansion or executed test evidence.
diff --git a/scripts/check-tauri-command-state-tests.sh b/scripts/check-tauri-command-state-tests.sh
new file mode 100644
index 000000000..458965b57
--- /dev/null
+++ b/scripts/check-tauri-command-state-tests.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+log_dir="${RUNNER_TEMP:-target}/tauri-command-state-tests"
+mkdir -p "$log_dir"
+cargo_args=(test -p pantograph --bin pantograph --no-default-features --features backend-llamacpp)
+tests=(
+  workflow::command_state::tests::run_bundle_preserves_managed_identity_without_payload_state
+  workflow::command_state::tests::run_bundle_preserves_missing_state_order_and_original_camel_case_keys
+  workflow::command_state::tests::query_bundle_preserves_managed_identity_without_payload_state
+  workflow::command_state::tests::query_command_preserves_missing_state_order_and_original_keys
+  workflow::command_state::tests::query_command_preserves_flat_required_optional_and_context_payloads
+)
+
+cargo "${cargo_args[@]}" workflow::command_state::tests:: -- --list > "$log_dir/tests.list"
+for test_name in "${tests[@]}"; do
+  python3 -c 'import pathlib, sys; lines = pathlib.Path(sys.argv[1]).read_text().splitlines(); sys.exit(0 if sys.argv[2] + ": test" in lines else 1)' "$log_dir/tests.list" "$test_name"
+  result_path="$log_dir/${test_name##*::}.log"
+  cargo "${cargo_args[@]}" "$test_name" -- --exact 2>&1 | tee "$result_path"
+  python3 -c 'import pathlib, re, sys; text = pathlib.Path(sys.argv[1]).read_text(); sys.exit(0 if re.search(r"^test result: ok\. 1 passed; 0 failed; 0 ignored;", text, re.M) else 1)' "$result_path"
+done
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 929b6e50a..1107c59b9 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -96,4 +96,5 @@ axum = { workspace = true }
 tower-http = { workspace = true, optional = true }
 
 [dev-dependencies]
+tauri = { version = "2.9.0", features = ["test"] }
 tempfile.workspace = true
diff --git a/src-tauri/src/workflow/command_state.rs b/src-tauri/src/workflow/command_state.rs
new file mode 100644
index 000000000..572532809
--- /dev/null
+++ b/src-tauri/src/workflow/command_state.rs
@@ -0,0 +1,66 @@
+//! Operation-specific bundles of Tauri-injected state; no client payload fields.
+
+use tauri::ipc::{CommandArg, CommandItem, InvokeError};
+use tauri::{Runtime, State};
+
+use crate::agent::rag::SharedRagManager;
+use crate::llm::{SharedGateway, SharedRuntimeRegistry};
+use super::commands::{
+    SharedExtensions, SharedNodeRegistry, SharedWorkflowDiagnosticsStore, SharedWorkflowService,
+};
+
+pub struct WorkflowRunCommandState<'r> {
+    pub gateway: State<'r, SharedGateway>,
+    pub runtime_registry: State<'r, SharedRuntimeRegistry>,
+    pub extensions: State<'r, SharedExtensions>,
+    pub rag_manager: State<'r, SharedRagManager>,
+    pub workflow_service: State<'r, SharedWorkflowService>,
+    pub diagnostics_store: State<'r, SharedWorkflowDiagnosticsStore>,
+}
+
+impl<'r, 'de: 'r, R: Runtime> CommandArg<'de, R> for WorkflowRunCommandState<'r> {
+    fn from_command(command: CommandItem<'de, R>) -> Result {
+        // Match the original command's extraction order and macro-generated keys.
+        Ok(Self {
+            gateway: managed_state(&command, "gateway")?,
+            runtime_registry: managed_state(&command, "runtimeRegistry")?,
+            extensions: managed_state(&command, "extensions")?,
+            rag_manager: managed_state(&command, "ragManager")?,
+            workflow_service: managed_state(&command, "workflowService")?,
+            diagnostics_store: managed_state(&command, "diagnosticsStore")?,
+        })
+    }
+}
+
+pub struct PortOptionsCommandState<'r> {
+    pub registry: State<'r, SharedNodeRegistry>,
+    pub extensions: State<'r, SharedExtensions>,
+    pub workflow_service: State<'r, SharedWorkflowService>,
+}
+
+impl<'r, 'de: 'r, R: Runtime> CommandArg<'de, R> for PortOptionsCommandState<'r> {
+    fn from_command(command: CommandItem<'de, R>) -> Result {
+        Ok(Self {
+            registry: managed_state(&command, "registry")?,
+            extensions: managed_state(&command, "extensions")?,
+            workflow_service: managed_state(&command, "workflowService")?,
+        })
+    }
+}
+
+fn managed_state<'r, 'de: 'r, T: Send + Sync + 'static, R: Runtime>(
+    command: &CommandItem<'de, R>,
+    key: &'static str,
+) -> Result, InvokeError> {
+    State::from_command(CommandItem {
+        plugin: command.plugin,
+        name: command.name,
+        key,
+        message: command.message,
+        acl: command.acl,
+    })
+}
+
+#[cfg(test)]
+#[path = "command_state_tests.rs"]
+mod tests;
diff --git a/src-tauri/src/workflow/command_state_tests.rs b/src-tauri/src/workflow/command_state_tests.rs
new file mode 100644
index 000000000..3774d62c5
--- /dev/null
+++ b/src-tauri/src/workflow/command_state_tests.rs
@@ -0,0 +1,230 @@
+use std::path::PathBuf;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use serde_json::{json, Value};
+use tauri::test::{mock_builder, mock_context, noop_assets, MockRuntime};
+use tauri::{App, Builder, WebviewWindow, WebviewWindowBuilder};
+use tokio::sync::{mpsc, RwLock};
+
+use super::*;
+use crate::workflow::commands;
+
+struct NoSpawn;
+
+#[async_trait]
+impl inference::ProcessSpawner for NoSpawn {
+    async fn spawn_sidecar(
+        &self,
+        _: &str,
+        _: &[&str],
+    ) -> Result<(mpsc::Receiver, Box), String> {
+        panic!("state extraction must not spawn a backend")
+    }
+
+    fn app_data_dir(&self) -> Result {
+        panic!("state extraction must not resolve runtime paths")
+    }
+
+    fn binaries_dir(&self) -> Result {
+        panic!("state extraction must not resolve runtime paths")
+    }
+}
+
+struct ManagedStates {
+    gateway: SharedGateway,
+    runtime_registry: SharedRuntimeRegistry,
+    extensions: SharedExtensions,
+    rag_manager: SharedRagManager,
+    workflow_service: SharedWorkflowService,
+    diagnostics_store: SharedWorkflowDiagnosticsStore,
+    registry: SharedNodeRegistry,
+}
+
+impl ManagedStates {
+    fn new(registry: SharedNodeRegistry) -> Self {
+        let mut extensions = node_engine::ExecutorExtensions::new();
+        extensions.set("state-fixture-marker", "managed extension".to_string());
+        Self {
+            gateway: Arc::new(crate::llm::InferenceGateway::new(Arc::new(NoSpawn))),
+            runtime_registry: Arc::new(pantograph_runtime_registry::RuntimeRegistry::new()),
+            extensions: Arc::new(RwLock::new(extensions)),
+            rag_manager: crate::agent::rag::create_rag_manager(PathBuf::from("unused-state-fixture")),
+            workflow_service: Arc::new(pantograph_workflow_service::WorkflowService::new()),
+            diagnostics_store: Arc::new(crate::workflow::WorkflowDiagnosticsStore::default()),
+            registry,
+        }
+    }
+
+    fn run_builder(&self, prefix: usize) -> Builder {
+        let mut builder = mock_builder();
+        if prefix > 0 {
+            builder = builder.manage(self.gateway.clone());
+        }
+        if prefix > 1 {
+            builder = builder.manage(self.runtime_registry.clone());
+        }
+        if prefix > 2 {
+            builder = builder.manage(self.extensions.clone());
+        }
+        if prefix > 3 {
+            builder = builder.manage(self.rag_manager.clone());
+        }
+        if prefix > 4 {
+            builder = builder.manage(self.workflow_service.clone());
+        }
+        if prefix > 5 {
+            builder = builder.manage(self.diagnostics_store.clone());
+        }
+        builder
+    }
+
+    fn query_builder(&self, prefix: usize) -> Builder {
+        let mut builder = mock_builder();
+        if prefix > 0 {
+            builder = builder.manage(self.registry.clone());
+        }
+        if prefix > 1 {
+            builder = builder.manage(self.extensions.clone());
+        }
+        if prefix > 2 {
+            builder = builder.manage(self.workflow_service.clone());
+        }
+        builder
+    }
+}
+
+fn identity(value: &Arc) -> String {
+    format!("{:p}", Arc::as_ptr(value))
+}
+
+#[tauri::command]
+fn inspect_run_state(state: WorkflowRunCommandState<'_>) -> Vec {
+    vec![
+        identity(state.gateway.inner()),
+        identity(state.runtime_registry.inner()),
+        identity(state.extensions.inner()),
+        identity(state.rag_manager.inner()),
+        identity(state.workflow_service.inner()),
+        identity(state.diagnostics_store.inner()),
+    ]
+}
+
+#[tauri::command]
+fn inspect_query_state(state: PortOptionsCommandState<'_>) -> Vec {
+    vec![
+        identity(state.registry.inner()),
+        identity(state.extensions.inner()),
+        identity(state.workflow_service.inner()),
+    ]
+}
+
+fn build(builder: Builder) -> (App, WebviewWindow) {
+    let app = builder
+        .invoke_handler(tauri::generate_handler![
+            inspect_run_state,
+            inspect_query_state,
+            commands::query_port_options,
+        ])
+        .build(mock_context(noop_assets()))
+        .expect("mock app");
+    let webview = WebviewWindowBuilder::new(&app, "main", Default::default())
+        .build()
+        .expect("mock webview");
+    (app, webview)
+}
+
+fn invoke(webview: &WebviewWindow, command: &str, body: Value) -> Result {
+    tauri::test::get_ipc_response(webview, tauri::webview::InvokeRequest {
+        cmd: command.to_string(),
+        callback: tauri::ipc::CallbackFn(0),
+        error: tauri::ipc::CallbackFn(1),
+        url: if cfg!(any(windows, target_os = "android")) {
+            "http://tauri.localhost"
+        } else {
+            "tauri://localhost"
+        }.parse().expect("mock IPC URL"),
+        body: tauri::ipc::InvokeBody::Json(body),
+        headers: Default::default(),
+        invoke_key: tauri::test::INVOKE_KEY.to_string(),
+    }).map(|body| body.deserialize().expect("JSON response"))
+}
+
+fn missing_state(command: &str, key: &str) -> Value {
+    json!(format!("state not managed for field `{key}` on command `{command}`. You must call `.manage()` before using this command"))
+}
+
+#[test]
+fn run_bundle_preserves_managed_identity_without_payload_state() {
+    let states = ManagedStates::new(Arc::new(node_engine::NodeRegistry::new()));
+    let (_app, webview) = build(states.run_builder(6));
+    let actual = invoke(&webview, "inspect_run_state", json!({"state": "ignored client input"})).expect("managed state");
+    assert_eq!(actual, json!([
+        identity(&states.gateway), identity(&states.runtime_registry), identity(&states.extensions),
+        identity(&states.rag_manager), identity(&states.workflow_service), identity(&states.diagnostics_store),
+    ]));
+}
+
+#[test]
+fn run_bundle_preserves_missing_state_order_and_original_camel_case_keys() {
+    let states = ManagedStates::new(Arc::new(node_engine::NodeRegistry::new()));
+    for (prefix, key) in ["gateway", "runtimeRegistry", "extensions", "ragManager", "workflowService", "diagnosticsStore"].iter().enumerate() {
+        let (_app, webview) = build(states.run_builder(prefix));
+        assert_eq!(invoke(&webview, "inspect_run_state", json!({})), Err(missing_state("inspect_run_state", key)));
+    }
+}
+
+#[test]
+fn query_bundle_preserves_managed_identity_without_payload_state() {
+    let states = ManagedStates::new(Arc::new(node_engine::NodeRegistry::new()));
+    let (_app, webview) = build(states.query_builder(3));
+    let actual = invoke(&webview, "inspect_query_state", json!({})).expect("managed state");
+    assert_eq!(actual, json!([identity(&states.registry), identity(&states.extensions), identity(&states.workflow_service)]));
+}
+
+#[test]
+fn query_command_preserves_missing_state_order_and_original_keys() {
+    let states = ManagedStates::new(Arc::new(node_engine::NodeRegistry::new()));
+    for (prefix, key) in ["registry", "extensions", "workflowService"].iter().enumerate() {
+        let (_app, webview) = build(states.query_builder(prefix));
+        assert_eq!(invoke(&webview, "query_port_options", json!({"nodeType":"fixture", "portId":"choice"})), Err(missing_state("query_port_options", key)));
+    }
+}
+
+struct EchoOptionsProvider(Arc);
+
+#[async_trait]
+impl node_engine::PortOptionsProvider for EchoOptionsProvider {
+    async fn query_options(
+        &self,
+        query: &node_engine::PortOptionsQuery,
+        extensions: &node_engine::ExecutorExtensions,
+    ) -> node_engine::Result {
+        self.0.fetch_add(1, Ordering::SeqCst);
+        Ok(node_engine::PortOptionsResult {
+            options: Vec::new(), total_count: 0, searchable: true,
+            metadata: Some(json!({"query":query,"marker":extensions.get::("state-fixture-marker")})),
+        })
+    }
+}
+
+#[test]
+fn query_command_preserves_flat_required_optional_and_context_payloads() {
+    let calls = Arc::new(AtomicUsize::new(0));
+    let mut registry = node_engine::NodeRegistry::new();
+    registry.register_port_provider("fixture", "choice", Box::new(EchoOptionsProvider(calls.clone())));
+    let states = ManagedStates::new(Arc::new(registry));
+    let (_app, webview) = build(states.query_builder(3));
+    let context = json!({"targetNodeId":"target-1", "taskKind":"embedding"});
+    let actual = invoke(&webview, "query_port_options", json!({
+        "nodeType":"fixture", "portId":"choice", "search":"needle", "limit":7, "offset":3,
+        "context":context, "state":"must not deserialize", "registry":"must not replace managed state",
+    })).expect("original flat command payload");
+    assert_eq!(actual, json!({"options":[],"totalCount":0,"searchable":true,"metadata":{
+        "query":{"search":"needle","limit":7,"offset":3,"context":context},"marker":"managed extension",
+    }}));
+    let minimal = invoke(&webview, "query_port_options", json!({"nodeType":"fixture", "portId":"choice"})).expect("original optional defaults");
+    assert_eq!(minimal["metadata"]["query"], json!({"search":null,"limit":null,"offset":null}));
+    assert_eq!(calls.load(Ordering::SeqCst), 2);
+}
diff --git a/src-tauri/src/workflow/commands.rs b/src-tauri/src/workflow/commands.rs
index 1c23b739f..495b23e69 100644
--- a/src-tauri/src/workflow/commands.rs
+++ b/src-tauri/src/workflow/commands.rs
@@ -6,6 +6,8 @@
 use std::{path::PathBuf, sync::Arc};
 
 use tauri::{command, AppHandle, Manager, State};
+
+pub use super::command_state::{PortOptionsCommandState, WorkflowRunCommandState};
 use tokio::sync::RwLock;
 
 use crate::agent::rag::SharedRagManager;
@@ -199,23 +201,13 @@ pub async fn workflow_create_execution_session(
 pub async fn workflow_run_execution_session(
     request: pantograph_workflow_service::WorkflowExecutionSessionRunRequest,
     app: AppHandle,
-    gateway: State<'_, SharedGateway>,
-    runtime_registry: State<'_, SharedRuntimeRegistry>,
-    extensions: State<'_, SharedExtensions>,
-    rag_manager: State<'_, SharedRagManager>,
-    workflow_service: State<'_, SharedWorkflowService>,
-    diagnostics_store: State<'_, SharedWorkflowDiagnosticsStore>,
+    state: WorkflowRunCommandState<'_>,
     channel: tauri::ipc::Channel,
 ) -> Result {
     super::headless_workflow_commands::workflow_run_execution_session(
         request,
         app,
-        gateway,
-        runtime_registry,
-        extensions,
-        rag_manager,
-        workflow_service,
-        diagnostics_store,
+        state,
         channel,
     )
     .await
@@ -769,9 +761,7 @@ pub async fn workflow_set_execution_session_keep_alive(
 
 #[command]
 pub async fn query_port_options(
-    registry: State<'_, SharedNodeRegistry>,
-    extensions: State<'_, SharedExtensions>,
-    workflow_service: State<'_, SharedWorkflowService>,
+    state: PortOptionsCommandState<'_>,
     node_type: String,
     port_id: String,
     search: Option,
@@ -780,9 +770,7 @@ pub async fn query_port_options(
     context: Option,
 ) -> Result {
     super::workflow_port_query_commands::query_port_options(
-        registry,
-        extensions,
-        workflow_service,
+        state,
         node_type,
         port_id,
         search,
diff --git a/src-tauri/src/workflow/headless_workflow_commands.rs b/src-tauri/src/workflow/headless_workflow_commands.rs
index 88b439c11..ef8e2ab8b 100644
--- a/src-tauri/src/workflow/headless_workflow_commands.rs
+++ b/src-tauri/src/workflow/headless_workflow_commands.rs
@@ -59,7 +59,8 @@ use tauri::{ipc::Channel, AppHandle, State};
 use crate::agent::rag::SharedRagManager;
 use crate::llm::{SharedGateway, SharedRuntimeRegistry};
 
-use super::commands::{SharedExtensions, SharedWorkflowDiagnosticsStore, SharedWorkflowService};
+use super::command_state::WorkflowRunCommandState;
+use super::commands::{SharedExtensions, SharedWorkflowService};
 use super::events::WorkflowEvent;
 use super::headless_diagnostics::workflow_scheduler_snapshot_response;
 pub(crate) use super::headless_runtime::build_runtime;
@@ -188,14 +189,17 @@ pub async fn workflow_create_execution_session(
 pub async fn workflow_run_execution_session(
     request: WorkflowExecutionSessionRunRequest,
     app: AppHandle,
-    gateway: State<'_, SharedGateway>,
-    runtime_registry: State<'_, SharedRuntimeRegistry>,
-    extensions: State<'_, SharedExtensions>,
-    rag_manager: State<'_, SharedRagManager>,
-    workflow_service: State<'_, SharedWorkflowService>,
-    diagnostics_store: State<'_, SharedWorkflowDiagnosticsStore>,
+    state: WorkflowRunCommandState<'_>,
     channel: Channel,
 ) -> Result {
+    let WorkflowRunCommandState {
+        gateway,
+        runtime_registry,
+        extensions,
+        rag_manager,
+        workflow_service,
+        diagnostics_store,
+    } = state;
     let runtime = build_runtime(
         &app,
         gateway.inner(),
diff --git a/src-tauri/src/workflow/mod.rs b/src-tauri/src/workflow/mod.rs
index a338767b3..e6486440e 100644
--- a/src-tauri/src/workflow/mod.rs
+++ b/src-tauri/src/workflow/mod.rs
@@ -21,6 +21,7 @@
 //!                            └─────────────────────────────────┘
 //! ```
 
+mod command_state;
 pub mod commands;
 pub mod diagnostics;
 pub mod event_adapter;
diff --git a/src-tauri/src/workflow/workflow_port_query_commands.rs b/src-tauri/src/workflow/workflow_port_query_commands.rs
index 269704fed..af16377ba 100644
--- a/src-tauri/src/workflow/workflow_port_query_commands.rs
+++ b/src-tauri/src/workflow/workflow_port_query_commands.rs
@@ -1,14 +1,13 @@
+use super::command_state::PortOptionsCommandState;
 use tauri::State;
 
-use super::commands::{SharedExtensions, SharedNodeRegistry, SharedWorkflowService};
+use super::commands::{SharedNodeRegistry, SharedWorkflowService};
 
 const PUMA_LIB_NODE_TYPE: &str = "puma-lib";
 const PUMAS_MODEL_REF_PORT_ID: &str = "pumas_model_ref";
 
 pub async fn query_port_options(
-    registry: State<'_, SharedNodeRegistry>,
-    extensions: State<'_, SharedExtensions>,
-    workflow_service: State<'_, SharedWorkflowService>,
+    state: PortOptionsCommandState<'_>,
     node_type: String,
     port_id: String,
     search: Option,
@@ -16,6 +15,11 @@ pub async fn query_port_options(
     offset: Option,
     context: Option,
 ) -> Result {
+    let PortOptionsCommandState {
+        registry,
+        extensions,
+        workflow_service,
+    } = state;
     let ext = extensions.read().await;
     let query = node_engine::PortOptionsQuery {
         search: search.clone(),

From b2490aeae74f891a7e24dd8360f62d28018c90b3 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 12:50:26 -0700
Subject: [PATCH 60/65] style(desktop): apply pinned Rust formatting to IPC
 fixtures

---
 .../reports/2026-10-03-tauri-command-state.md |   2 +-
 src-tauri/src/workflow/command_state.rs       |   4 +-
 src-tauri/src/workflow/command_state_tests.rs | 147 ++++++++++++++----
 src-tauri/src/workflow/commands.rs            |  17 +-
 4 files changed, 119 insertions(+), 51 deletions(-)

diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-tauri-command-state.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-tauri-command-state.md
index b4b75606d..91792cf7a 100644
--- a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-tauri-command-state.md
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-tauri-command-state.md
@@ -10,7 +10,7 @@ Five tests use the official Tauri MockRuntime/IPC path. They verify all six/thre
 
 The existing Tauri dependency gains only its test-only test feature (empty upstream feature, same locked 2.9.5 package). Required hosted workspace verification adds a script requiring all five exact test names and an individual 1 passed / 0 failed / 0 ignored receipt for each, with backend-llamacpp and no default app features. Existing all-feature workspace compilation and strict all-target Clippy remain. Frontend invocation tests remain part of the existing full frontend suite.
 
-Source review is pending. The source checkout was restored after a filesystem reset, and pinned Rust/rustfmt are currently unavailable. Formatting, Rust compilation and native mock-IPC execution are unrun locally; no installation or heavy build is attempted under the recovery hold. Bash syntax and the unchanged-body comparison pass. Publication remains paused with the separate mechanical batch until explicit retry authorization and the serialized write window are available.
+Root source review accepted corrected tree 58ef1d4de278b899c3d2c72553275d7d3697f1bd after inspecting the import placement and exact pinned-version evidence. This is source acceptance, not Rust execution. After the user approved recovery, the restored official Rust 1.92.0 formatter found formatting changes in three IPC files. Applying pinned cargo fmt --all and rerunning the full cargo fmt --all -- --check passed. The formatting delta contains whitespace, optional trailing commas and two unchanged reordered imports; token comparison preserves the remaining source, literals and comments. Bash syntax and the unchanged delegate-body comparison also pass. Rust compilation and all five native mock-IPC executions remain pending hosted qualification. The optional formatting-artifact workflow is omitted because local pinned formatting is now available.
 
 ## Exact pinned-source compatibility
 
diff --git a/src-tauri/src/workflow/command_state.rs b/src-tauri/src/workflow/command_state.rs
index 572532809..689d99010 100644
--- a/src-tauri/src/workflow/command_state.rs
+++ b/src-tauri/src/workflow/command_state.rs
@@ -3,11 +3,11 @@
 use tauri::ipc::{CommandArg, CommandItem, InvokeError};
 use tauri::{Runtime, State};
 
-use crate::agent::rag::SharedRagManager;
-use crate::llm::{SharedGateway, SharedRuntimeRegistry};
 use super::commands::{
     SharedExtensions, SharedNodeRegistry, SharedWorkflowDiagnosticsStore, SharedWorkflowService,
 };
+use crate::agent::rag::SharedRagManager;
+use crate::llm::{SharedGateway, SharedRuntimeRegistry};
 
 pub struct WorkflowRunCommandState<'r> {
     pub gateway: State<'r, SharedGateway>,
diff --git a/src-tauri/src/workflow/command_state_tests.rs b/src-tauri/src/workflow/command_state_tests.rs
index 3774d62c5..905774798 100644
--- a/src-tauri/src/workflow/command_state_tests.rs
+++ b/src-tauri/src/workflow/command_state_tests.rs
@@ -19,7 +19,13 @@ impl inference::ProcessSpawner for NoSpawn {
         &self,
         _: &str,
         _: &[&str],
-    ) -> Result<(mpsc::Receiver, Box), String> {
+    ) -> Result<
+        (
+            mpsc::Receiver,
+            Box,
+        ),
+        String,
+    > {
         panic!("state extraction must not spawn a backend")
     }
 
@@ -50,7 +56,9 @@ impl ManagedStates {
             gateway: Arc::new(crate::llm::InferenceGateway::new(Arc::new(NoSpawn))),
             runtime_registry: Arc::new(pantograph_runtime_registry::RuntimeRegistry::new()),
             extensions: Arc::new(RwLock::new(extensions)),
-            rag_manager: crate::agent::rag::create_rag_manager(PathBuf::from("unused-state-fixture")),
+            rag_manager: crate::agent::rag::create_rag_manager(PathBuf::from(
+                "unused-state-fixture",
+            )),
             workflow_service: Arc::new(pantograph_workflow_service::WorkflowService::new()),
             diagnostics_store: Arc::new(crate::workflow::WorkflowDiagnosticsStore::default()),
             registry,
@@ -135,20 +143,30 @@ fn build(builder: Builder) -> (App, WebviewWindow, command: &str, body: Value) -> Result {
-    tauri::test::get_ipc_response(webview, tauri::webview::InvokeRequest {
-        cmd: command.to_string(),
-        callback: tauri::ipc::CallbackFn(0),
-        error: tauri::ipc::CallbackFn(1),
-        url: if cfg!(any(windows, target_os = "android")) {
-            "http://tauri.localhost"
-        } else {
-            "tauri://localhost"
-        }.parse().expect("mock IPC URL"),
-        body: tauri::ipc::InvokeBody::Json(body),
-        headers: Default::default(),
-        invoke_key: tauri::test::INVOKE_KEY.to_string(),
-    }).map(|body| body.deserialize().expect("JSON response"))
+fn invoke(
+    webview: &WebviewWindow,
+    command: &str,
+    body: Value,
+) -> Result {
+    tauri::test::get_ipc_response(
+        webview,
+        tauri::webview::InvokeRequest {
+            cmd: command.to_string(),
+            callback: tauri::ipc::CallbackFn(0),
+            error: tauri::ipc::CallbackFn(1),
+            url: if cfg!(any(windows, target_os = "android")) {
+                "http://tauri.localhost"
+            } else {
+                "tauri://localhost"
+            }
+            .parse()
+            .expect("mock IPC URL"),
+            body: tauri::ipc::InvokeBody::Json(body),
+            headers: Default::default(),
+            invoke_key: tauri::test::INVOKE_KEY.to_string(),
+        },
+    )
+    .map(|body| body.deserialize().expect("JSON response"))
 }
 
 fn missing_state(command: &str, key: &str) -> Value {
@@ -159,19 +177,44 @@ fn missing_state(command: &str, key: &str) -> Value {
 fn run_bundle_preserves_managed_identity_without_payload_state() {
     let states = ManagedStates::new(Arc::new(node_engine::NodeRegistry::new()));
     let (_app, webview) = build(states.run_builder(6));
-    let actual = invoke(&webview, "inspect_run_state", json!({"state": "ignored client input"})).expect("managed state");
-    assert_eq!(actual, json!([
-        identity(&states.gateway), identity(&states.runtime_registry), identity(&states.extensions),
-        identity(&states.rag_manager), identity(&states.workflow_service), identity(&states.diagnostics_store),
-    ]));
+    let actual = invoke(
+        &webview,
+        "inspect_run_state",
+        json!({"state": "ignored client input"}),
+    )
+    .expect("managed state");
+    assert_eq!(
+        actual,
+        json!([
+            identity(&states.gateway),
+            identity(&states.runtime_registry),
+            identity(&states.extensions),
+            identity(&states.rag_manager),
+            identity(&states.workflow_service),
+            identity(&states.diagnostics_store),
+        ])
+    );
 }
 
 #[test]
 fn run_bundle_preserves_missing_state_order_and_original_camel_case_keys() {
     let states = ManagedStates::new(Arc::new(node_engine::NodeRegistry::new()));
-    for (prefix, key) in ["gateway", "runtimeRegistry", "extensions", "ragManager", "workflowService", "diagnosticsStore"].iter().enumerate() {
+    for (prefix, key) in [
+        "gateway",
+        "runtimeRegistry",
+        "extensions",
+        "ragManager",
+        "workflowService",
+        "diagnosticsStore",
+    ]
+    .iter()
+    .enumerate()
+    {
         let (_app, webview) = build(states.run_builder(prefix));
-        assert_eq!(invoke(&webview, "inspect_run_state", json!({})), Err(missing_state("inspect_run_state", key)));
+        assert_eq!(
+            invoke(&webview, "inspect_run_state", json!({})),
+            Err(missing_state("inspect_run_state", key))
+        );
     }
 }
 
@@ -180,15 +223,32 @@ fn query_bundle_preserves_managed_identity_without_payload_state() {
     let states = ManagedStates::new(Arc::new(node_engine::NodeRegistry::new()));
     let (_app, webview) = build(states.query_builder(3));
     let actual = invoke(&webview, "inspect_query_state", json!({})).expect("managed state");
-    assert_eq!(actual, json!([identity(&states.registry), identity(&states.extensions), identity(&states.workflow_service)]));
+    assert_eq!(
+        actual,
+        json!([
+            identity(&states.registry),
+            identity(&states.extensions),
+            identity(&states.workflow_service)
+        ])
+    );
 }
 
 #[test]
 fn query_command_preserves_missing_state_order_and_original_keys() {
     let states = ManagedStates::new(Arc::new(node_engine::NodeRegistry::new()));
-    for (prefix, key) in ["registry", "extensions", "workflowService"].iter().enumerate() {
+    for (prefix, key) in ["registry", "extensions", "workflowService"]
+        .iter()
+        .enumerate()
+    {
         let (_app, webview) = build(states.query_builder(prefix));
-        assert_eq!(invoke(&webview, "query_port_options", json!({"nodeType":"fixture", "portId":"choice"})), Err(missing_state("query_port_options", key)));
+        assert_eq!(
+            invoke(
+                &webview,
+                "query_port_options",
+                json!({"nodeType":"fixture", "portId":"choice"})
+            ),
+            Err(missing_state("query_port_options", key))
+        );
     }
 }
 
@@ -203,8 +263,12 @@ impl node_engine::PortOptionsProvider for EchoOptionsProvider {
     ) -> node_engine::Result {
         self.0.fetch_add(1, Ordering::SeqCst);
         Ok(node_engine::PortOptionsResult {
-            options: Vec::new(), total_count: 0, searchable: true,
-            metadata: Some(json!({"query":query,"marker":extensions.get::("state-fixture-marker")})),
+            options: Vec::new(),
+            total_count: 0,
+            searchable: true,
+            metadata: Some(
+                json!({"query":query,"marker":extensions.get::("state-fixture-marker")}),
+            ),
         })
     }
 }
@@ -213,7 +277,11 @@ impl node_engine::PortOptionsProvider for EchoOptionsProvider {
 fn query_command_preserves_flat_required_optional_and_context_payloads() {
     let calls = Arc::new(AtomicUsize::new(0));
     let mut registry = node_engine::NodeRegistry::new();
-    registry.register_port_provider("fixture", "choice", Box::new(EchoOptionsProvider(calls.clone())));
+    registry.register_port_provider(
+        "fixture",
+        "choice",
+        Box::new(EchoOptionsProvider(calls.clone())),
+    );
     let states = ManagedStates::new(Arc::new(registry));
     let (_app, webview) = build(states.query_builder(3));
     let context = json!({"targetNodeId":"target-1", "taskKind":"embedding"});
@@ -221,10 +289,21 @@ fn query_command_preserves_flat_required_optional_and_context_payloads() {
         "nodeType":"fixture", "portId":"choice", "search":"needle", "limit":7, "offset":3,
         "context":context, "state":"must not deserialize", "registry":"must not replace managed state",
     })).expect("original flat command payload");
-    assert_eq!(actual, json!({"options":[],"totalCount":0,"searchable":true,"metadata":{
-        "query":{"search":"needle","limit":7,"offset":3,"context":context},"marker":"managed extension",
-    }}));
-    let minimal = invoke(&webview, "query_port_options", json!({"nodeType":"fixture", "portId":"choice"})).expect("original optional defaults");
-    assert_eq!(minimal["metadata"]["query"], json!({"search":null,"limit":null,"offset":null}));
+    assert_eq!(
+        actual,
+        json!({"options":[],"totalCount":0,"searchable":true,"metadata":{
+            "query":{"search":"needle","limit":7,"offset":3,"context":context},"marker":"managed extension",
+        }})
+    );
+    let minimal = invoke(
+        &webview,
+        "query_port_options",
+        json!({"nodeType":"fixture", "portId":"choice"}),
+    )
+    .expect("original optional defaults");
+    assert_eq!(
+        minimal["metadata"]["query"],
+        json!({"search":null,"limit":null,"offset":null})
+    );
     assert_eq!(calls.load(Ordering::SeqCst), 2);
 }
diff --git a/src-tauri/src/workflow/commands.rs b/src-tauri/src/workflow/commands.rs
index 495b23e69..6b2b64548 100644
--- a/src-tauri/src/workflow/commands.rs
+++ b/src-tauri/src/workflow/commands.rs
@@ -204,13 +204,8 @@ pub async fn workflow_run_execution_session(
     state: WorkflowRunCommandState<'_>,
     channel: tauri::ipc::Channel,
 ) -> Result {
-    super::headless_workflow_commands::workflow_run_execution_session(
-        request,
-        app,
-        state,
-        channel,
-    )
-    .await
+    super::headless_workflow_commands::workflow_run_execution_session(request, app, state, channel)
+        .await
 }
 
 #[command]
@@ -770,13 +765,7 @@ pub async fn query_port_options(
     context: Option,
 ) -> Result {
     super::workflow_port_query_commands::query_port_options(
-        state,
-        node_type,
-        port_id,
-        search,
-        limit,
-        offset,
-        context,
+        state, node_type, port_id, search, limit, offset, context,
     )
     .await
 }

From 60302e6d1e97b47974b72d2b5e7cb955298e5f9a Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 13:16:01 -0700
Subject: [PATCH 61/65] fix(test): keep SQLite column iterator within statement
 lifetime

---
 crates/pantograph-diagnostics-ledger/src/tests.rs           | 6 ++----
 .../reports/2026-10-03-aggregate-test-style.md              | 4 ++++
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/crates/pantograph-diagnostics-ledger/src/tests.rs b/crates/pantograph-diagnostics-ledger/src/tests.rs
index 8d28d95ba..4ddeeafce 100644
--- a/crates/pantograph-diagnostics-ledger/src/tests.rs
+++ b/crates/pantograph-diagnostics-ledger/src/tests.rs
@@ -5077,12 +5077,10 @@ fn sqlite_column_exists(conn: &Connection, table_name: &str, column_name: &str)
     let mut stmt = conn
         .prepare(&format!("PRAGMA table_info({table_name})"))
         .expect("table info statement prepares");
-    let columns = stmt
+    let mut columns = stmt
         .query_map([], |row| row.get::<_, String>(1))
         .expect("table info query succeeds");
-    columns
-        .map(|column| column.expect("column row loads"))
-        .any(|column| column == column_name)
+    columns.any(|column| column.expect("column row loads") == column_name)
 }
 
 fn assert_columns_exist(conn: &Connection, table_name: &str, column_names: &[&str]) {
diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-aggregate-test-style.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-aggregate-test-style.md
index 741986314..4f792788e 100644
--- a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-aggregate-test-style.md
+++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-aggregate-test-style.md
@@ -9,3 +9,7 @@ Six existing cfg(test) modules move after production items in node_io_artifacts,
 The required focused workflow retains all existing suites and adds the complete artifact_store integration target (13 source tests), so the compile-blocked redaction/storage contract now executes. Existing embedded, workflow-service, scheduler, ledger, inference and workflow-node suites remain. Desktop/test-only source changes still require actual hosted strict all-target compilation; no blanket lint allowance or assertion removal is introduced.
 
 Root requested this grouped mechanical milestone from the complete diagnostic inventory. Root source review accepted frozen tree 29cb5ce693a0b6a091a807d3cfa3b203a60468d3 after reviewing the non-move changes and independently checking identical nonblank line multisets for all six module moves. Exact-head hosted execution remains pending. Full pinned cargo fmt is checked locally; no heavy local Rust build is performed. The separate four Tauri arity findings remain open and must preserve existing IPC wire keys.
+
+## Hosted qualification correction
+
+PR43 head b2490ae reached the full ledger target in Quality run 37149377334, job 111279795852, and exposed E0597 in the test-only sqlite_column_exists helper. Returning the Map iterator expression extended its temporary borrow beyond the statement's lifetime under the crate's Rust 2021 edition. The original direct-return lint repair was therefore not source-compatible despite its equivalent row predicate. Bind the mapped rows mutably and call any with the same expect and equality directly; the named iterator then drops before the statement, with the same short-circuit order and row-error panic. No production code, SQL, assertions or error text changes. Full ledger and remaining focused suites require a fresh hosted run; the failed run is retained as evidence.

From ca9edde6850cd58ade0b7e534bb4f58e704c2c64 Mon Sep 17 00:00:00 2001
From: Puma 
Date: Sat, 3 Oct 2026 19:24:49 -0700
Subject: [PATCH 62/65] fix(quality): address external range and snippet
 findings

Fetch a missing exact event base and fail closed if unavailable, without substituting an empty range. Exclude inert snippet declarations from rendered-name evidence. Exercise actual CI shell ranges with real Git fixtures and add the snippet regression.

Locally verified tree: c5a72e68a784ff09756ad7ce5000660be58d2868. Parent independent review accepted; external repair review and hosted exact-head qualification remain pending.
---
 .github/workflows/quality-gates.yml          | 11 ++++
 scripts/check-decision-traceability.test.mjs | 54 +++++++++++++++++++-
 scripts/svelte-role-button-check.mjs         |  5 +-
 scripts/svelte-role-button-check.test.mjs    |  5 ++
 4 files changed, 72 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
index e1d63cdc9..186aa8b69 100644
--- a/.github/workflows/quality-gates.yml
+++ b/.github/workflows/quality-gates.yml
@@ -44,6 +44,17 @@ jobs:
         shell: bash
         run: |
           set -euo pipefail
+          if [[ ! "$EVENT_BASE" =~ ^[0-9a-fA-F]{40}$ ]]; then
+            echo "Traceability requires the event's full base commit SHA" >&2
+            exit 1
+          fi
+          if ! git cat-file -e "${EVENT_BASE}^{commit}" 2>/dev/null; then
+            if ! git fetch --no-tags origin "$EVENT_BASE" ||
+              ! git cat-file -e "${EVENT_BASE}^{commit}" 2>/dev/null; then
+              echo "Traceability event base is unavailable; cannot compute the no-new lint range" >&2
+              exit 1
+            fi
+          fi
           base="$(git rev-parse --verify --end-of-options "${EVENT_BASE}^{commit}")"
           head="$(git rev-parse --verify --end-of-options "${EVENT_HEAD}^{commit}")"
           if [[ "$EVENT_NAME" == pull_request ]]; then
diff --git a/scripts/check-decision-traceability.test.mjs b/scripts/check-decision-traceability.test.mjs
index 104f91296..1b02407ba 100644
--- a/scripts/check-decision-traceability.test.mjs
+++ b/scripts/check-decision-traceability.test.mjs
@@ -1,6 +1,6 @@
 import assert from 'node:assert/strict';
 import { spawnSync } from 'node:child_process';
-import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
+import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
 import { tmpdir } from 'node:os';
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
@@ -327,7 +327,59 @@ test('PR callers select the fork point explicitly without changing exact range s
   failed(f.run(env), /unavailable: docs\/target-guide.md is missing/);
   const base = f.git('merge-base', '--all', target, head);
   assert.equal(base, f.base);
+  const workflowRange = runWorkflowRange(f.root, target, head, 'pull_request');
+  assert.equal(workflowRange.status, 0, workflowRange.stderr);
+  assert.equal(workflowRange.range, `base=${base}\nhead=${head}\n`);
   const result = f.run({ ...env, TRACEABILITY_BASE_REF: base });
   passed(result);
   assert.match(result.output, /1 changed path\(s\), 0 mapped impact\(s\)/);
 });
+
+// Exercise the actual CI shell block, not a second implementation of its range policy.
+function runWorkflowRange(root, base, head, event = 'push') {
+  const workflow = readFileSync(new URL('../.github/workflows/quality-gates.yml', import.meta.url), 'utf8');
+  const step = workflow.split('        id: traceability-range\n')[1];
+  assert.ok(step, 'CI traceability step must exist');
+  const block = step.split('        run: |\n')[1]?.split('\n      - name:')[0];
+  assert.ok(block, 'CI traceability shell block must exist');
+  const script = block.split('\n').map((line) => line.replace(/^ {10}/, '')).join('\n');
+  const outputPath = path.join(root, 'event-range-output');
+  const result = spawnSync('bash', ['-c', script], {
+    cwd: root, encoding: 'utf8',
+    env: { ...process.env, EVENT_NAME: event, EVENT_BASE: base, EVENT_HEAD: head, GITHUB_OUTPUT: outputPath },
+  });
+  return { ...result, range: existsSync(outputPath) ? readFileSync(outputPath, 'utf8') : '' };
+}
+
+test('CI uses a present event base without fetching or replacing its push range', t => {
+  const f = fixture(t);
+  f.write('src/internal.rs', 'fn next() {}\n');
+  const head = f.commit();
+  // No origin is configured, so an unnecessary fetch would fail this check.
+  const result = runWorkflowRange(f.root, f.base, head);
+  assert.equal(result.status, 0, result.stderr);
+  assert.equal(result.range, `base=${f.base}\nhead=${head}\n`);
+});
+
+test('CI fetches the exact missing event base and retains the explicit push range', t => {
+  const f = fixture(t);
+  f.write('src/internal.rs', 'fn next() {}\n');
+  const head = f.commit();
+  const client = path.join(f.root, 'client');
+  const clone = spawnSync(realGit, ['clone', '--depth', '1', `file://${f.root}`, client], { encoding: 'utf8' });
+  assert.equal(clone.status, 0, clone.stderr);
+  assert.notEqual(spawnSync(realGit, ['cat-file', '-e', `${f.base}^{commit}`], { cwd: client }).status, 0);
+  const result = runWorkflowRange(client, f.base, head);
+  assert.equal(result.status, 0, result.stderr);
+  assert.equal(result.range, `base=${f.base}\nhead=${head}\n`);
+});
+
+test('CI fails closed when the event base cannot be fetched or is not a full SHA', t => {
+  for (const base of ['f'.repeat(40), '', '--all']) {
+    const f = fixture(t);
+    const result = runWorkflowRange(f.root, base, f.base);
+    assert.equal(result.status, 1, result.stderr);
+    assert.equal(result.range, '');
+    assert.match(result.stderr, /Traceability (event base is unavailable|requires the event's full base commit SHA)/);
+  }
+});
diff --git a/scripts/svelte-role-button-check.mjs b/scripts/svelte-role-button-check.mjs
index 7b0e9fc17..de472f84f 100644
--- a/scripts/svelte-role-button-check.mjs
+++ b/scripts/svelte-role-button-check.mjs
@@ -7,8 +7,9 @@ const INTERACTIVE_SEMANTIC_TAGS = new Set(['button', 'a', 'input', 'select', 'te
 function hasRenderedName(node) {
   if (!node || typeof node !== 'object') return false;
   if (node.type === 'Text') return /[A-Za-z0-9]/.test(node.data);
-  if (node.type === 'ExpressionTag') return true;
-  if (node.type === 'Comment') return false;
+  if (node.type === 'ExpressionTag' || node.type === 'RenderTag') return true;
+  // A snippet declaration is inert until a RenderTag invokes it.
+  if (node.type === 'Comment' || node.type === 'SnippetBlock') return false;
   if (node.type === 'RegularElement' && ['script', 'style'].includes(node.name)) return false;
   return ['nodes', 'fragment', 'consequent', 'alternate', 'body', 'fallback'].some((key) => {
     const value = node[key];
diff --git a/scripts/svelte-role-button-check.test.mjs b/scripts/svelte-role-button-check.test.mjs
index 6b65e31cc..dec79b9ff 100644
--- a/scripts/svelte-role-button-check.test.mjs
+++ b/scripts/svelte-role-button-check.test.mjs
@@ -40,3 +40,8 @@ test('visible descendant text supplies a name, but handler expressions do not',
   assert.deepEqual(check('
{label}
'), []); assert.deepEqual(rules('
activate()}>
'), ['role-button-accessible-name']); }); + +test('an unused snippet declaration is not rendered name evidence', () => { + assert.deepEqual(rules('
{#snippet label()}Select{/snippet}
'), ['role-button-accessible-name']); + assert.deepEqual(check('
{#snippet label()}Select{/snippet}{@render label()}
'), []); +}); From 572ccbc45c0030a7eef9938702c4c3b2f9b97d27 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 4 Oct 2026 02:49:58 +0000 Subject: [PATCH 63/65] fix(test): isolate shared Python fixtures in aggregate inference runs Guard Python fixture lifetimes across the worker contract, image and text suites, including asynchronous production-worker lifecycle tests. The GIL can be released during imports and worker execution and does not isolate process-global modules. Keep runtime behavior and existing assertions intact. Add the full PyTorch-enabled inference library suite to Quality Gates so cross-suite interference is exercised alongside the focused contract checks. Qualification on PR44 ca9edde6850cd58ade0b7e534bb4f58e704c2c64: - Before: parallel aggregate 675 passed, 7 failed; serial 682 passed. - After: default parallel 682 passed; three 16-thread runs each 682 passed. - Strict inference library/tests Clippy, Rust formatting and staged critical/a11y/traceability gates passed. - Independent domain aggregate: 566 passed; node/workflow: 426 passed, one unit test and 14 doctests ignored; frontend: 547 passed; tooling: 45. - Full frontend lint, typecheck, dependency tree and production audit passed. Native qualification remains incomplete: workflow-service compilation hit cdn.pyke.io CONNECT proxy 403; Tauri IPC compilation hit missing glib-2.0.pc. No Tauri IPC tests, desktop GUI or model inference were executed. Python fixtures use stubs and do not prove real inference. No dependency pins, audit gates, network settings or permissions were changed. --- .github/workflows/quality-gates.yml | 3 ++ crates/inference/src/backend/pytorch.rs | 6 +++ crates/inference/src/backend/pytorch_tests.rs | 40 +++++++++++++++++++ .../pytorch_worker_image_contract_tests.rs | 4 ++ .../pytorch_worker_image_python_tests.rs | 8 ++++ 5 files changed, 61 insertions(+) diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml index 186aa8b69..5fc56a060 100644 --- a/.github/workflows/quality-gates.yml +++ b/.github/workflows/quality-gates.yml @@ -251,6 +251,9 @@ jobs: cargo test -p inference --features backend-pytorch --lib pytorch_named_text_request cargo test -p inference --features backend-pytorch --lib test_pytorch_generate_text_ + - name: Run aggregate inference tests with Python worker fixtures + run: cargo test -p inference --features backend-pytorch --lib + - name: Run llama server startup and reuse contract tests run: | for test_filter in server::tests backend::llamacpp::tests; do diff --git a/crates/inference/src/backend/pytorch.rs b/crates/inference/src/backend/pytorch.rs index 48271cdb3..3ba25a139 100644 --- a/crates/inference/src/backend/pytorch.rs +++ b/crates/inference/src/backend/pytorch.rs @@ -3103,6 +3103,12 @@ fn extract_system_prompt(request: &serde_json::Value) -> Option { }) } +// Python fixtures replace process-global modules and production worker functions. +// The GIL may be released during imports and worker execution, so hold this lock +// for each complete fixture lifetime, including asynchronous lifecycle tests. +#[cfg(test)] +static PYTHON_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + #[cfg(test)] #[path = "pytorch_worker_image_contract_tests.rs"] mod pytorch_worker_image_contract_tests; diff --git a/crates/inference/src/backend/pytorch_tests.rs b/crates/inference/src/backend/pytorch_tests.rs index ca751e16a..910748fea 100644 --- a/crates/inference/src/backend/pytorch_tests.rs +++ b/crates/inference/src/backend/pytorch_tests.rs @@ -645,6 +645,7 @@ fn test_pytorch_worker_load_envelope_tolerates_additive_fields() { #[test] fn test_python_worker_contract_projects_task_profile_loader() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let envelope = serde_json::json!({ @@ -689,6 +690,7 @@ fn test_python_worker_contract_projects_task_profile_loader() { #[test] fn test_python_worker_contract_tolerates_additive_load_fields() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let envelope = serde_json::json!({ @@ -748,6 +750,7 @@ fn test_python_worker_contract_tolerates_additive_load_fields() { #[test] fn test_python_worker_contract_rejects_unsupported_task_profile_loader() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let envelope = serde_json::json!({ @@ -789,6 +792,7 @@ fn test_python_worker_contract_rejects_unsupported_task_profile_loader() { #[test] fn test_python_worker_contract_rejects_missing_load_entry_path() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let envelope = serde_json::json!({ @@ -826,6 +830,7 @@ fn test_python_worker_contract_rejects_missing_load_entry_path() { #[test] fn test_python_worker_load_value_error_after_projection_maps_to_model_load_failed() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_stubbed_dependencies(py); let patch = CString::new( @@ -882,6 +887,7 @@ module.load_model = fail_load_model #[test] fn test_python_worker_load_unexpected_loader_exception_maps_to_model_load_failed() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_stubbed_dependencies(py); let patch = CString::new( @@ -938,6 +944,7 @@ module.load_model = fail_load_model #[test] fn test_python_worker_load_invalid_loader_stays_invalid_request() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_stubbed_dependencies(py); let envelope = serde_json::json!({ @@ -1069,6 +1076,7 @@ fn test_pytorch_worker_generate_text_dllm_envelope_decodes_backend_local_control #[test] fn test_python_worker_contract_projects_dllm_generation_controls() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let fixture = include_str!( @@ -1103,6 +1111,7 @@ fn test_python_worker_contract_projects_dllm_generation_controls() { #[test] fn test_python_worker_contract_tolerates_additive_generate_fields() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let envelope = serde_json::json!({ @@ -1572,6 +1581,7 @@ fn test_pytorch_worker_truncate_kv_cache_envelope_rejects_empty_path() { #[test] fn test_python_worker_contract_projects_unload_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let fixture = @@ -1588,6 +1598,7 @@ fn test_python_worker_contract_projects_unload_envelope() { #[test] fn test_python_worker_contract_projects_init_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let fixture = @@ -1604,6 +1615,7 @@ fn test_python_worker_contract_projects_init_envelope() { #[test] fn test_python_worker_contract_projects_shutdown_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let fixture = include_str!( @@ -1621,6 +1633,7 @@ fn test_python_worker_contract_projects_shutdown_envelope() { #[test] fn test_python_worker_shutdown_from_envelope_returns_structured_success() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_stubbed_dependencies(py); let fixture = include_str!( @@ -1645,6 +1658,7 @@ fn test_python_worker_shutdown_from_envelope_returns_structured_success() { #[test] fn test_python_worker_response_helpers_emit_success_contract() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let locals = PyDict::new(py); @@ -1690,6 +1704,7 @@ response_json = worker_contract.worker_success_response_json( #[test] fn test_python_worker_response_helpers_emit_error_contract() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); @@ -1727,6 +1742,7 @@ fn test_python_worker_response_helpers_emit_error_contract() { #[test] fn test_python_worker_contract_rejects_invalid_init_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let wrong_operation = serde_json::json!({ @@ -1769,6 +1785,7 @@ fn test_python_worker_contract_rejects_invalid_init_envelope() { #[test] fn test_python_worker_contract_rejects_invalid_shutdown_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let wrong_operation = serde_json::json!({ @@ -1811,6 +1828,7 @@ fn test_python_worker_contract_rejects_invalid_shutdown_envelope() { #[test] fn test_python_worker_contract_projects_get_loaded_info_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let fixture = include_str!( @@ -1828,6 +1846,7 @@ fn test_python_worker_contract_projects_get_loaded_info_envelope() { #[test] fn test_python_worker_contract_rejects_invalid_get_loaded_info_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let wrong_operation = serde_json::json!({ @@ -1870,6 +1889,7 @@ fn test_python_worker_contract_rejects_invalid_get_loaded_info_envelope() { #[test] fn test_python_worker_contract_projects_clear_kv_cache_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let fixture = include_str!( @@ -1887,6 +1907,7 @@ fn test_python_worker_contract_projects_clear_kv_cache_envelope() { #[test] fn test_python_worker_contract_rejects_invalid_clear_kv_cache_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let wrong_operation = serde_json::json!({ @@ -1929,6 +1950,7 @@ fn test_python_worker_contract_rejects_invalid_clear_kv_cache_envelope() { #[test] fn test_python_worker_contract_projects_save_kv_cache_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let fixture = @@ -1951,6 +1973,7 @@ fn test_python_worker_contract_projects_save_kv_cache_envelope() { #[test] fn test_python_worker_contract_rejects_invalid_save_kv_cache_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let wrong_operation = serde_json::json!({ @@ -1993,6 +2016,7 @@ fn test_python_worker_contract_rejects_invalid_save_kv_cache_envelope() { #[test] fn test_python_worker_contract_projects_restore_kv_cache_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let fixture = include_str!( @@ -2016,6 +2040,7 @@ fn test_python_worker_contract_projects_restore_kv_cache_envelope() { #[test] fn test_python_worker_contract_rejects_invalid_restore_kv_cache_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let wrong_operation = serde_json::json!({ @@ -2058,6 +2083,7 @@ fn test_python_worker_contract_rejects_invalid_restore_kv_cache_envelope() { #[test] fn test_python_worker_contract_projects_truncate_kv_cache_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let fixture = include_str!( @@ -2089,6 +2115,7 @@ fn test_python_worker_contract_projects_truncate_kv_cache_envelope() { #[test] fn test_python_worker_contract_rejects_invalid_truncate_kv_cache_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let wrong_operation = serde_json::json!({ @@ -2131,6 +2158,7 @@ fn test_python_worker_contract_rejects_invalid_truncate_kv_cache_envelope() { #[test] fn test_python_worker_contract_rejects_invalid_unload_envelope() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let wrong_operation = serde_json::json!({ @@ -2254,6 +2282,7 @@ fn test_pytorch_worker_audio_transcription_envelope_tolerates_additive_fields() #[test] fn test_python_worker_contract_projects_audio_transcription_fields() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let fixture = include_str!( @@ -2309,6 +2338,7 @@ fn test_python_worker_contract_projects_audio_transcription_fields() { #[test] fn test_python_worker_contract_tolerates_additive_audio_transcription_fields() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let envelope = serde_json::json!({ @@ -2344,6 +2374,7 @@ fn test_python_worker_contract_tolerates_additive_audio_transcription_fields() { #[test] fn test_python_worker_contract_rejects_audio_transcription_legacy_device() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let mut value: serde_json::Value = serde_json::from_str(include_str!( @@ -2366,6 +2397,7 @@ fn test_python_worker_contract_rejects_audio_transcription_legacy_device() { #[test] fn test_python_worker_contract_rejects_audio_transcription_auto_device_field() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let mut value: serde_json::Value = serde_json::from_str(include_str!( @@ -2748,6 +2780,7 @@ fn test_pytorch_generate_text_stream_envelope_rejects_policy_transformers_kwargs #[test] fn test_python_worker_contract_rejects_additive_backend_kwargs() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_contract_module(py); let envelope = serde_json::json!({ @@ -3625,6 +3658,7 @@ fn test_pytorch_worker_stream_token_extraction_error_normalizes_to_backend_error #[test] fn test_pytorch_worker_stream_token_accepts_string_chunk() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let token = pyo3::types::PyString::new(py, "hello"); let chunk = @@ -3638,6 +3672,7 @@ fn test_pytorch_worker_stream_token_accepts_string_chunk() { #[test] fn test_pytorch_worker_stream_token_accepts_replace_dict_chunk() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let token = pyo3::types::PyDict::new(py); token.set_item("mode", "replace").expect("set mode"); @@ -3665,6 +3700,7 @@ fn test_pytorch_worker_stream_token_accepts_replace_dict_chunk() { #[test] fn test_pytorch_worker_stream_token_accepts_usage_only_dict_chunk() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let token = pyo3::types::PyDict::new(py); let usage = pyo3::types::PyDict::new(py); @@ -3690,6 +3726,7 @@ fn test_pytorch_worker_stream_token_accepts_usage_only_dict_chunk() { #[test] fn test_pytorch_worker_stream_token_bounds_usage_counts() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let token = pyo3::types::PyDict::new(py); let usage = pyo3::types::PyDict::new(py); @@ -3715,6 +3752,7 @@ fn test_pytorch_worker_stream_token_bounds_usage_counts() { #[test] fn test_pytorch_worker_stream_token_rejects_dict_without_text_or_usage() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let token = pyo3::types::PyDict::new(py); token.set_item("mode", "replace").expect("set mode"); @@ -5905,6 +5943,7 @@ impl TextIteratorBarrier { #[tokio::test] async fn production_text_iterator_drains_before_successful_load_and_unload() { + let _python_fixture = super::PYTHON_TEST_LOCK.lock().await; use futures_util::{FutureExt, StreamExt}; for action in ["load", "unload", "stop", "failed_restart", "failed_load"] { let (entered_tx, entered_rx) = tokio::sync::oneshot::channel(); @@ -6213,6 +6252,7 @@ impl crate::InferenceExecutionCancellationSignal for SelectedTextCancellation { #[tokio::test] async fn selected_text_production_loader_and_worker_retain_selection_until_termination() { + let _python_fixture = super::PYTHON_TEST_LOCK.lock().await; use futures_util::FutureExt; for mode in ["success", "error", "cancel"] { let (_directory, request, target, mut decision) = crate::selected_text_execution::fixture(); diff --git a/crates/inference/src/backend/pytorch_worker_image_contract_tests.rs b/crates/inference/src/backend/pytorch_worker_image_contract_tests.rs index 410d80dc2..7fa4c6ac7 100644 --- a/crates/inference/src/backend/pytorch_worker_image_contract_tests.rs +++ b/crates/inference/src/backend/pytorch_worker_image_contract_tests.rs @@ -292,6 +292,7 @@ fn test_pytorch_worker_generate_image_request_maps_from_validated_plan() { #[test] fn test_python_worker_generate_image_contract_projects_planned_kwargs() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_image_contract_module(py); let envelope = include_str!( @@ -349,6 +350,7 @@ fn test_python_worker_generate_image_contract_projects_planned_kwargs() { #[test] fn test_python_worker_generate_image_batch_contract_projects_member_kwargs() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_image_contract_module(py); let envelope = include_str!( @@ -413,6 +415,7 @@ fn test_python_worker_generate_image_batch_contract_projects_member_kwargs() { #[test] fn test_python_worker_generate_image_contract_rejects_unknown_payload_fields() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_image_contract_module(py); let mut envelope: serde_json::Value = serde_json::from_str(include_str!( @@ -436,6 +439,7 @@ fn test_python_worker_generate_image_contract_rejects_unknown_payload_fields() { #[test] fn test_python_worker_generate_image_contract_requires_rust_selected_device() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_image_contract_module(py); let mut envelope: serde_json::Value = serde_json::from_str(include_str!( diff --git a/crates/inference/src/backend/pytorch_worker_image_python_tests.rs b/crates/inference/src/backend/pytorch_worker_image_python_tests.rs index 11c6cb0fb..580c96c57 100644 --- a/crates/inference/src/backend/pytorch_worker_image_python_tests.rs +++ b/crates/inference/src/backend/pytorch_worker_image_python_tests.rs @@ -174,6 +174,7 @@ def load_diffusion_model(path, device=None, torch_dtype=None): #[test] fn test_python_worker_generate_image_batch_from_envelope_returns_worker_response() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_image_stubs(py); attach_stub_diffusion_pipeline(&module); @@ -240,6 +241,7 @@ fn test_python_worker_generate_image_batch_from_envelope_returns_worker_response #[test] fn test_python_worker_generate_image_from_envelope_returns_worker_response() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_image_stubs(py); attach_stub_diffusion_pipeline(&module); @@ -275,6 +277,7 @@ fn test_python_worker_generate_image_from_envelope_returns_worker_response() { #[test] fn test_python_worker_generate_image_from_envelope_reports_cuda_peak_vram() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_image_stubs(py); attach_stub_diffusion_pipeline(&module); @@ -334,6 +337,7 @@ worker.torch.cuda = _Cuda() #[test] fn test_python_worker_generate_image_from_envelope_reports_mps_metric_unimplemented() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_image_stubs(py); attach_stub_diffusion_pipeline(&module); @@ -386,6 +390,7 @@ worker.torch.backends = types.SimpleNamespace( #[test] fn test_python_worker_generate_image_from_envelope_reports_oom_failure() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_image_stubs(py); attach_stub_diffusion_pipeline(&module); @@ -432,6 +437,7 @@ worker._diffusion_pipeline = _FailingPipeline() #[test] fn test_python_worker_generate_image_from_envelope_rejects_unsupported_denoising_scheduler() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_image_stubs(py); attach_stub_diffusion_pipeline(&module); @@ -462,6 +468,7 @@ fn test_python_worker_generate_image_from_envelope_rejects_unsupported_denoising #[test] fn test_python_worker_generate_image_from_envelope_rejects_unplanned_fields() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_image_stubs(py); attach_stub_diffusion_pipeline(&module); @@ -494,6 +501,7 @@ fn test_python_worker_generate_image_from_envelope_rejects_unplanned_fields() { #[test] fn test_python_worker_real_diffusion_loader_enforces_closed_bundle_admission() { + let _python_fixture = super::PYTHON_TEST_LOCK.blocking_lock(); Python::with_gil(|py| { let module = load_worker_module_with_image_stubs(py); let locals = PyDict::new(py); From 827b1b264a5918e2339c503f447aeb01008ac9b4 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 4 Oct 2026 02:58:48 +0000 Subject: [PATCH 64/65] docs: record PR44 native qualification evidence and blockers --- ...026-10-04-native-workflow-qualification.md | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/plans/domain-architecture-and-multimodal/reports/2026-10-04-native-workflow-qualification.md diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-04-native-workflow-qualification.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-04-native-workflow-qualification.md new file mode 100644 index 000000000..e04bafa5b --- /dev/null +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-04-native-workflow-qualification.md @@ -0,0 +1,156 @@ +# PR44 native/workflow qualification + +Status on 2026-10-04: aggregate inference repair ready for independent review; +native/workflow qualification remains blocked. This report records local +execution, not a new hosted Actions run or model acceptance. + +## Exact source and repair + +- Requested PR44 base: `ca9edde6850cd58ade0b7e534bb4f58e704c2c64`. +- Repair: `572ccbc45c0030a7eef9938702c4c3b2f9b97d27`. +- Repair tree: `265d802354146b20bd1bf3bd6d4d3e350f3589c3`. +- Branch: `qualification/pr44-native-workflow`. +- Repair parent is exactly the requested base; full stacked ancestry is retained. +- Pumas dependency remains pinned to `f87c3da8276a914a54c6f4f36d617bef9d9f424e`. + +The repair changes five files with 61 insertions. A shared test-only Tokio mutex +protects 52 Python fixture lifetimes across text, image and worker-contract +suites. Synchronous tests acquire it before the GIL; two asynchronous lifecycle +tests retain it across worker execution. The GIL can be released during imports +and worker execution and does not isolate process-global `sys.modules` or +patched worker functions. Runtime behavior and existing assertions are unchanged. +Quality Gates adds the full PyTorch-enabled inference library run while retaining +all focused checks, audit and traceability gates. + +The requested base failed seven of 682 tests in the ordinary parallel aggregate; +all 682 passed serially. Failures included replaced/missing module symbols, +altered envelope validation and worker-state interference. This demonstrates a +failure present at the requested head, not its historical introducing commit. + +## Executed checks + +| Check | Result | +| --- | --- | +| Baseline parallel inference aggregate | 675 passed, 7 failed | +| Baseline diagnostic serial inference aggregate | 682 passed | +| Repaired default parallel inference aggregate | 682 passed | +| Rebuilt inference binary, three separate 16-thread runs | 682 passed in each run | +| Inference library and tests, Clippy with warnings denied | Passed | +| Fourteen domain packages, unit/integration/doc targets | 566 passed, zero ignored | +| Node-engine and workflow-nodes, including doctests | 426 passed; one unit test and 14 doctests ignored | +| Frontend discovery, including command compatibility | 547 passed | +| All tooling test files | 45 passed | +| Full frontend lint, TypeScript, Rust formatting and diff whitespace | Passed | +| Production dependency audit | Zero vulnerabilities | +| Installed dependency tree | Passed; optional native addons absent | +| Critical lint, accessibility and explicit base-range traceability | Passed | +| Staged repair critical lint/accessibility/traceability | Passed, five paths and zero mapped impacts | +| Canonical image-workflow and scheduler-only guard scripts | Passed | + +The frontend invocation included command-related arguments but ran the complete +discovered suite; it was not a filtered run. Aggregate domain packages were +`pantograph-scheduler`, `pantograph-runtime-host-contracts`, +`pantograph-diagnostics-ledger`, `pantograph-media-conversion`, +`pantograph-dependency-environment-service`, `pantograph-dependency-planning`, +`pantograph-managed-dependencies`, `pantograph-runtime-registry`, +`pantograph-runtime-attribution`, `pantograph-runtime-identity`, +`pantograph-node-contracts`, `pantograph-inference-interface-contracts`, +`pantograph-path-security` and `pantograph-timing-contracts`. + +The source qualification commands include: + +```bash +cargo test --locked -p inference --features backend-pytorch --lib +cargo clippy --locked -p inference --features backend-pytorch --lib --tests -- -D warnings +cargo test --locked -p node-engine -p workflow-nodes +node --test scripts/*.test.mjs +npm run lint:full +npm run typecheck +npm audit --omit=dev --audit-level=high +npm ls --all +cargo fmt --all -- --check +TRACEABILITY_STAGED_ONLY=1 npm run lint:no-new +``` + +The earlier complete-range gate explicitly compared main +`4938e405c7f656365eefdca492774ccae110c90d` with the requested PR44 base. + +## Environment and native blockers + +The prepared environment is Debian GNU/Linux 13.6, with pinned Node 24.12.0, +npm 11.6.2, Rust 1.92.0 and Python 3.12.3. Builds used the existing two-job CPU +configuration and a 600-second timeout. In this environment, activate +`/workspace/pantograph-tools/activate.sh` before running checks. PyO3 test +execution also required the process-local `LD_LIBRARY_PATH` to include the +existing `/workspace/pantograph-tools/python/cpython-3.12.3-linux-x86_64-gnu/lib`. +The first inference executable exited 127 without that loader path; its build +had succeeded. No Python installation or system configuration was changed. + +`cargo test --locked -p pantograph-workflow-service` stopped in `ort-sys` while +fetching ONNX Runtime 1.24.2. No workflow-service tests executed. +After the parent reported the approved network allowance saved and republished, +exactly one follow-up HEAD request at 2026-10-04T02:53:35Z used a 15-second +connect timeout, 45-second total timeout, zero retries and no redirect following: + +```text +https://cdn.pyke.io/0/pyke:ort-rs/ms@1.24.2/x86_64-unknown-linux-gnu.tar.lzma2 +curl exit: 56 +CONNECT proxy status: 403 Forbidden +Origin response: none (http_code 000) +``` + +The existing task remained denied. No further request or native build followed. +A fresh environment with the updated policy is required before retrying. + +`scripts/check-tauri-command-state-tests.sh` stopped building `glib-sys` because +`glib-2.0 >= 2.70` pkg-config metadata was unavailable. Zero of its five IPC tests +executed. Read-only package and pkg-config probes confirmed these missing native +prerequisites from the repository's shared Ubuntu installer: + +```text +libayatana-appindicator3-dev +libglib2.0-dev +libgtk-3-dev +libjavascriptcoregtk-4.1-dev +librsvg2-dev +libsoup-3.0-dev +libwebkit2gtk-4.1-dev +libxdo-dev +protobuf-compiler +``` + +`build-essential`, compilers, make and `libssl-dev` are installed. The functional +`pkg-config` command is supplied by installed `pkgconf`/`pkgconf-bin`; the absent +`pkg-config` package name is not a missing tool. `protoc`, `tauri-driver`, +`WebKitWebDriver`, `Xvfb`, `dotnet`, `elixir` and `erl` were not found. +Neither DISPLAY nor WAYLAND_DISPLAY was set. No system packages were installed; +the repository installer targets Ubuntu, while this host is Debian. + +## Acceptance limits and handoff + +No actual desktop IPC round trip, GUI, model loading or model inference was +executed. Existing Tauri fixtures use MockRuntime dispatch; Python contract and +lifecycle fixtures use stubs, including tests whose names mention production or +real loaders. Passing these tests does not prove inference acceptance. +Model-dependent smokes still require canonical Pumas model/artifact IDs, a saved +image workflow and native/runtime prerequisites. No model downloads were made. +C#/BEAM/package acceptance, the full workspace feature matrix and native linkage +were not locally executed. Previously green CI was reported by the parent, +not independently re-queried here. + +Local detailed logs and the full text handoff remain in +`/workspace/pantograph-cache/qualification-*`. In particular, the before/after +logs are `qualification-inference-retry.log` and +`qualification-inference-fixed.log`; the blocker evidence is +`qualification-workflow-service.log`, `qualification-tauri-ipc.log`, +`qualification-onnx-policy-check.log` and its `.headers` companion; +`qualification-native-prerequisites.txt` records the read-only inventory. +These local files are not assumed to transfer to a fresh environment; this +tracked report preserves their results and exact source identities. + +The original verified `pr44-native-qualification.bundle` remains in the local +cache and contains repair 572ccbc on this branch, requiring the exact PR44 base. +This report is a documentation-only follow-up to that unchanged repair. The +parent owns independent review and coordinated integration. No PR44/47 metadata +or review requests are part of this publication. No credentials, permissions, +network/security, speed or paid settings were changed. From 9996ddfd8b4c15c11de4d7141e20e0165dd0f12c Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sun, 4 Oct 2026 03:13:39 +0000 Subject: [PATCH 65/65] fix(a11y): require independent name evidence for snippet renders Address PR47 review comments 4175927978 and 4175927981 on exact base ca9edde6850cd58ade0b7e534bb4f58e704c2c64. RenderTag alone no longer counts as a role-button name. Keep ExpressionTag and direct-text evidence, explicit labels, native-button rules and reviewed-ignore policy unchanged. Use the parent-approved conservative static policy instead of implementing Svelte binding evaluation. Document that even nonempty local snippet-only labels need independent evidence. Test empty, nonempty, nested, shadowed, parameter, self/mutual recursive, unknown, optional and member calls, plus explicit labels and adjacent visible text/expressions. Scanning all 126 Svelte files produces no changed violations. Correct the dated dependency report with verified Quality Gates run 37170993215 and its successful dependency checks for ca9edde only. Retain historical evidence and pending GUI/package acceptance; do not attribute that hosted run to this repair. Validation on pinned Node 24.12.0/npm 11.6.2: - New tests against original scanner: 15 pass, 12 fail. - Repaired focused scanner suite: 27 pass; all tooling: 63 pass. - Frontend aggregate: 547 pass. - Full lint, typecheck, production build, npm ls --all, staged critical lint, accessibility, decision traceability and diff whitespace: pass. - Production audit: zero vulnerabilities. - Build emits an existing stale Browserslist data notice; no dependency change. No GUI/native/model checks, blocked download retries, external review requests or merge. This branch is directly atop ca9 and excludes fixture-lock 572ccbc. --- ...2026-10-03-frontend-dependency-security.md | 8 ++-- scripts/README.md | 15 +++++++ scripts/svelte-role-button-check.mjs | 8 ++-- scripts/svelte-role-button-check.test.mjs | 42 ++++++++++++++++++- 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md index 81477aa9e..9ddaa48d9 100644 --- a/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md +++ b/docs/plans/domain-architecture-and-multimodal/reports/2026-10-03-frontend-dependency-security.md @@ -1,6 +1,6 @@ # Frontend dependency security repair (2026-10-03) -Status: implemented and source-reviewed; exact-head hosted and GUI qualification remain pending. Parent: PR #20, `8a08b6a010ee62fbd3b29e193c65922c0769b928`. Standards reviewed at Coding-Standards `dcc56f26e884ade260770beceba2501d3746200d`; this is newer than the active plan's planning baseline, without changing plan or release authority. +Status: implemented and source-reviewed; dependency-audit checks passed on reviewed head `ca9edde6850cd58ade0b7e534bb4f58e704c2c64`. GUI and packaged-release qualification remain pending. Parent: PR #20, `8a08b6a010ee62fbd3b29e193c65922c0769b928`. Standards reviewed at Coding-Standards `dcc56f26e884ade260770beceba2501d3746200d`; this is newer than the active plan's planning baseline, without changing plan or release authority. ## Resolution and rationale @@ -22,7 +22,7 @@ A fresh production audit of the parent lock reported five affected package nodes - markdown-it: [GHSA-6v5v-wf23-fmfq](https://github.com/advisories/GHSA-6v5v-wf23-fmfq), [GHSA-253c-mchw-3w2r](https://github.com/advisories/GHSA-253c-mchw-3w2r). - svelte: [GHSA-f3cj-j4f6-wq85](https://github.com/advisories/GHSA-f3cj-j4f6-wq85), [GHSA-rcqx-6q8c-2c42](https://github.com/advisories/GHSA-rcqx-6q8c-2c42), [GHSA-9rmh-mm8f-r9h6](https://github.com/advisories/GHSA-9rmh-mm8f-r9h6), [GHSA-pr6f-5x2q-rwfp](https://github.com/advisories/GHSA-pr6f-5x2q-rwfp). -## Verification and limits +## Original slice verification and limits (historical) Local generation and clean install used Node 24.19.0/npm 11.9.0 because the pinned Node 24.12.0/npm 11.6.2 were unavailable. No global toolchain was changed. A clean hosted install and applicable checks using both declared versions are required before qualification; the local results do not waive this requirement. @@ -38,6 +38,6 @@ The source manifest/lock pair was independently reviewed before publication at s [Quality Gates run 37115665903](https://github.com/MrScripty/Pantograph/actions/runs/37115665903) confirms Node 24.12.0/npm 11.6.2, clean npm ci, zero production-audit findings, 540/540 frontend tests, typecheck and full lint. Its synthetic merge `e66f54bc7b3f2963956918a6a781fc3a2ecf47ca` has the same tree `1c51bc4dcce332846e550ecf6374864521a11099` as published source commit `0b5c0df663d76dc97f2ba3836d33ea2404ca79df`. The unchanged critical-lint failure remains visible. -That workflow lacked installed-tree and frontend-build checks. Add `npm ls --all` and `npm run build` to the existing dependency-audit job after clean installation and the production audit, preserving every existing command and aggregate gate. These checks prove resolver consistency and frontend asset compilation on the declared toolchain; they do not establish GUI or packaged-release acceptance. Exact-head hosted results for this workflow change remain pending. +That earlier workflow lacked installed-tree and frontend-build checks. The integration added `npm ls --all` and `npm run build` to the existing dependency-audit job after clean installation and the production audit, preserving every existing command and aggregate gate. [Quality Gates run 37170993215](https://github.com/MrScripty/Pantograph/actions/runs/37170993215), associated with reviewed head `ca9edde6850cd58ade0b7e534bb4f58e704c2c64`, completed successfully. Its Dependency audit job passed clean installation, the production audit, installed-tree verification and frontend asset compilation. These results qualify those checks for that head only; they do not establish GUI or packaged-release acceptance or qualify subsequent repair commits. -The hosted development-inclusive install audit reports 41 affected package nodes (1 low, 5 moderate, 35 high). Dependency maintainers must triage that separate follow-up before any broader security claim; this slice does not upgrade unrelated development tools or claim zero vulnerabilities across the complete graph. Headless/Runtime Separation results remain under observation, and the real-model GUI prerequisites above remain unavailable. +At the earlier hosted milestone, the development-inclusive install audit reported 41 affected package nodes (1 low, 5 moderate, 35 high). Dependency maintainers must triage that separate follow-up before any broader security claim; this slice does not upgrade unrelated development tools or claim zero vulnerabilities across the complete graph. Headless/Runtime Separation results were still under observation at that earlier milestone; this report does not use those suites to claim GUI or packaged-release acceptance. The real-model GUI prerequisites above remain unqualified. diff --git a/scripts/README.md b/scripts/README.md index c4d431ff6..af5b4ff74 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -219,3 +219,18 @@ hosted runners. It retains the existing GTK/WebKit/libsoup packages and installs uses `sudo apt-get` and changes the runner's system packages; do not treat it as a read-only check or run it on another computer without authorization. A failed install fails the step. It changes no Rust features, pins or verification gates. + +## Role-Button Name Evidence + +The parsed literal-role scanner accepts explicit `aria-label`/`aria-labelledby` +attributes or directly rendered descendant text and `ExpressionTag` evidence. +Snippet declarations are inert, and a `RenderTag` alone does not prove an +accessible name: local, nested, shadowed, recursive and external calls may +produce no text. The guard intentionally does not resolve snippet bindings or +execute calls. Even a nonempty local snippet used as the sole label needs an +explicit label attribute or independent rendered evidence to pass this gate. +This conservative policy avoids treating render-call source as visible content; +it preserves the existing expression-evidence policy rather than proving its +runtime value. Native-button rules and reviewed-ignore rules are unchanged. +Runtime accessibility and dynamically empty/hidden content still require UI +validation. diff --git a/scripts/svelte-role-button-check.mjs b/scripts/svelte-role-button-check.mjs index de472f84f..5590b4aea 100644 --- a/scripts/svelte-role-button-check.mjs +++ b/scripts/svelte-role-button-check.mjs @@ -7,9 +7,11 @@ const INTERACTIVE_SEMANTIC_TAGS = new Set(['button', 'a', 'input', 'select', 'te function hasRenderedName(node) { if (!node || typeof node !== 'object') return false; if (node.type === 'Text') return /[A-Za-z0-9]/.test(node.data); - if (node.type === 'ExpressionTag' || node.type === 'RenderTag') return true; - // A snippet declaration is inert until a RenderTag invokes it. - if (node.type === 'Comment' || node.type === 'SnippetBlock') return false; + if (node.type === 'ExpressionTag') return true; + // Snippet declarations are inert. Render calls may produce no text, including + // local, nested or recursive calls. Resolving Svelte bindings is outside this + // static guard: require a label or direct rendered text/expression evidence. + if (['Comment', 'SnippetBlock', 'RenderTag'].includes(node.type)) return false; if (node.type === 'RegularElement' && ['script', 'style'].includes(node.name)) return false; return ['nodes', 'fragment', 'consequent', 'alternate', 'body', 'fallback'].some((key) => { const value = node[key]; diff --git a/scripts/svelte-role-button-check.test.mjs b/scripts/svelte-role-button-check.test.mjs index dec79b9ff..b04810e7d 100644 --- a/scripts/svelte-role-button-check.test.mjs +++ b/scripts/svelte-role-button-check.test.mjs @@ -43,5 +43,45 @@ test('visible descendant text supplies a name, but handler expressions do not', test('an unused snippet declaration is not rendered name evidence', () => { assert.deepEqual(rules('
{#snippet label()}Select{/snippet}
'), ['role-button-accessible-name']); - assert.deepEqual(check('
{#snippet label()}Select{/snippet}{@render label()}
'), []); }); + +// This static guard deliberately does not resolve snippet calls or their bindings. +const renderOnlyCases = [ + ['empty local snippet', '{#snippet label()}{/snippet}{@render label()}'], + ['whitespace and comments', '{#snippet label()} \n{/snippet}{@render label()}'], + ['nonempty local snippet', '{#snippet label()}Select{/snippet}{@render label()}'], + ['nested unused snippet', '{#snippet label()}{#snippet unused()}Select{/snippet}{/snippet}{@render label()}'], + ['nested render call', '{#snippet inner()}Select{/snippet}{#snippet label()}{@render inner()}{/snippet}{@render label()}'], + ['shadowed local snippet', '{#snippet label()}Select{/snippet}{#if visible}{#snippet label()}{/snippet}{@render label()}{/if}'], + ['snippet parameter', '{#snippet wrapper(label)}{@render label()}{/snippet}{@render wrapper(label)}'], + ['self recursion', '{#snippet label()}{@render label()}{/snippet}{@render label()}'], + ['mutual recursion', '{#snippet first()}{@render second()}{/snippet}{#snippet second()}{@render first()}{/snippet}{@render first()}'], + ['unknown snippet', '{@render unknown()}'], + ['optional snippet', '{@render unknown?.()}'], + ['member call', '{@render snippets.label()}'], +]; + +for (const [name, content] of renderOnlyCases) { + test(`render-only content requires independent name evidence: ${name}`, () => { + const violations = check(`\n
${content}
`); + assert.deepEqual(violations.map(({ rule }) => rule), ['role-button-accessible-name']); + assert.equal(violations[0].file, 'Fixture.svelte'); + assert.equal(violations[0].line, 2); + }); +} + +for (const attribute of ['aria-label="Select"', 'aria-labelledby="label"']) { + test(`explicit ${attribute} supports render-only content`, () => { + for (const [, content] of renderOnlyCases) { + assert.deepEqual(check(`
${content}
`), []); + } + }); +} + +for (const name of ['Select', 'Select', '{label}', '{label}']) { + test(`adjacent rendered evidence remains accepted: ${name}`, () => { + for (const [, content] of renderOnlyCases) { + assert.deepEqual(check(`
${content}${name}
`), []); + } + }); +}