From 3483a942ca947f995d940a74ba78b6bed93ed711 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Fri, 18 Sep 2026 07:26:07 +0200 Subject: [PATCH 1/3] REQ-LAYERDOCS-001: verified from the published v0.36.0 artifacts Held at implemented through the release because the rustdoc archive did not exist until a tag built it. It exists now, and it opens. Checked with the RELEASED binary, not a local build: both rustdoc archives are lines in the signed SHA256SUMS.txt and the downloaded bytes match; deposited as kind = docs, format rustdoc, entry varve_core/index.html, documents = varve-core beside the crate payload; installed; `varve export-docs --for varve-core` exported 669 files and named the entry as the starting page, and `varve export-cargo` materialised the crate. REQ-CRATEPAYLOAD-001 stays implemented: its clause 3 needs a DEPOSITED pulseengine layer carrying varve-core, which pulseengine-layers#16 proposes and the maintainer merges. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu --- artifacts/requirements.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index 0d357e1..264b610 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -4287,8 +4287,8 @@ artifacts: - id: REQ-LAYERDOCS-001 type: requirement title: A layer carries the documentation for the versions it pins, and can open it - status: implemented - description: "A layer pins exact versions and says nothing about how to USE them. The tools that embed their own documentation need no help — varve docs and rivet docs answer for themselves, at the version installed, offline. This requirement is for the documentation NOTHING CAN BE ASKED FOR. . Two cases, both real today. A crate's documentation IS its rustdoc, and that is the cheapest documentation in the toolchain to obtain: nobody authors it, `cargo doc` generates it from code that already exists. It is also the least reachable — HTML produced at build time, existing nowhere a consumer can get to offline, because docs.rs is a network service and a pinned air-gapped build cannot consult it. So a carried crate and its rustdoc are ONE deliverable and should ship together: a consumer that can pin varve-core but must go to docs.rs to read it has been given half of what it needs, and the half that requires a network. This requirement is therefore paired with REQ-CRATEPAYLOAD-001 and follows it — there is no rustdoc payload worth carrying until there is a crate payload to document. And the architecture documentation for the toolchain as a whole lives on pulseengine.eu, belongs to no single binary, and is therefore versioned by nothing: a reader cannot tell which layer the page they are reading describes. . MEASURED 2026-09-07: no pulseengine release publishes any documentation asset at all. rivet ships binaries, an SBOM, a vsix and a compliance report; meld, synth, witness and loom ship no documentation in any form. So the documentation a consumer of layer 2026.09.1 can obtain for the exact versions it pins is: nothing, unless the tool embeds its own. . ONE KIND, WITH A SUBTYPE. This shall not become a kind per flavour — rustdoc, architecture, manual, reference and whatever follows are all the same thing to the store, the manifest and the signature, and differ only in what a reader does with them. A single `docs` payload kind carrying a declared subtype keeps the signed manifest stable while the set of flavours grows, which is the opposite of what a kind per flavour does. . BEING ABLE TO OPEN IT IS PART OF THE REQUIREMENT, not a convenience on top. Documentation a consumer has to locate inside a content-addressed store by hand is documentation nobody reads. An HTML tree needs serving, because rustdoc's cross-links and search do not work from file:// in every browser and a consumer should not have to know that; a PDF needs opening. varve shall do the guiding, in one command, rather than printing a store path and wishing the reader luck. . AND IT SHALL BE ADDRESSABLE BY A MACHINE. An agent asked to work with a pinned toolchain should be able to obtain the documentation for exactly the versions in play, without guessing paths or scraping human output. That is the same argument as --format json everywhere else in varve, applied to the one thing an agent most needs and currently cannot get. . Clauses: (1) A layer shall be able to carry documentation as a payload of kind `docs`, verified against the signed manifest exactly as every other payload is — documentation that guides a build or a ceremony is evidence, and unverified evidence is worth less than none. (2) A docs payload shall declare a SUBTYPE (rustdoc, architecture and others as they arrive) and the payload it documents, where it documents one, so a consumer can ask for the rustdoc of a specific crate rather than for a filename someone chose. (3) One kind, not one per flavour: adding a flavour shall not change the signed manifest's schema. (4) varve shall LOCATE AND INSTRUCT, never render and never serve. For a pinned layer it shall report where each document is and print the exact command to open it — the system handler for a PDF, a one-line local server for an HTML tree — in a form a human can paste and an agent can execute. varve shall not render markdown, rewrite HTML, or run a listener: NO SERVER OF OUR OWN is a stated design principle (README), repeated in the help for varve deposit, and a documentation viewer would be the first exception to it — adding a network listener to a tool whose value is partly that it has none, and a rendering dependency to a supply-chain tool that currently has five. Serving is one command the consumer runs, with a runtime they already have; varve knowing WHICH command and WHERE to run it is the whole of the help that is actually needed. (5) The set of documents in a layer shall be machine-readable, so an agent can be pointed at the documentation for the exact versions pinned. (6) It shall remain OFFLINE: a docs payload is bytes in the layer, never a link to a service, because a link is not pinned, can change after signing, and is unreachable from an air-gapped build — the three properties varve exists to provide. (7) Documentation shall be optional: a layer carrying none shall behave exactly as layers do today, since most payloads will never have any. . THAT MEASUREMENT IS NOW OUT OF DATE, IN OUR FAVOUR (re-measured 2026-09-11). varve's own release publishes varve--traceability-html.tar.gz, varve--traceability.reqif and varve--rivet-artifacts.tar.gz, and all of them are already inside the cosign-signed SHA256SUMS.txt. So the first docs payload needs NO release-workflow change at all - the artifact exists, signed, today. Only the manifest cannot express it. That is why the maintainer sequenced this AHEAD of REQ-CRATEPAYLOAD-001 on 2026-09-11, reversing the order this text assumed: rustdoc still follows the crate, but the docs KIND does not have to wait for either. . PROBED the real artifact rather than assuming its shape: 341 entries, 1.2 MB, a root index.html, and _assets/mermaid.min.js which calls fetch() 28 times. Under file:// those fetches are CORS-blocked, so diagrams silently degrade. A viewer is therefore a real requirement and not a convenience - but varve must still not RENDER anything. . DESIGN DECIDED 2026-09-11. A [[docs]] entry declares name, repo, version, format, entry, title and asset. `format` (html | rustdoc | pdf | markdown | reqif) is DECLARED, never sniffed from an extension, for the same reason upstream-sums is declared: a guess that is wrong produces a payload varve offers to open the wrong way, and extension-sniffing is how a gate goes vacuous. It is also the thing a human filters on. `entry` is declared for the same reason - this bundle happens to have one root index.html, and 'whatever index.html we find' breaks the day an upstream ships two. rustdoc is its own format rather than html because it is generated not authored, versions with its crate, and 'show me the API docs' is a different question from 'show me the handbook'. . Docs are HELD, never DISPATCHED - they are data, like vsix. The command is `varve export-docs --to DIR`, joining the existing export- family, because `varve docs` already means varve's own embedded topics and reusing that name for layer-carried documentation would break the CLI convention that a name means one thing. inspect gains the entry point AFTER the payload table, not before: the table is what inspect is run for. . THE VIEWER IS A COMPANION BINARY, `varve-serve`, shipped from this repo the way varve-producer is - one release, one signing, one supply chain - and carried in the pulseengine layer as an ordinary tool payload so a realm can choose to include it or not. Deliberately NOT a subcommand of varve: varve is the binary that verifies a toolchain, and giving it an HTTP listener expands the surface of exactly the thing whose smallness is the point. A separate binary also means the viewer is itself pinned, signed and versioned by the system it serves. . PRIOR ART, AND IT IS AHEAD OF US (researched 2026-09-15, Ferrocene public docs + criticalup changelog). criticalup shipped `criticalup doc` in v1.3.0 on 2025-01-30 - 'opens the documentation of the relevant Ferrocene version' - and ferrocene-docs-xxx is a first-class installable package in the same channel and package system as rustc-, not a side artifact. So documentation as a pinned, installed, version-matching payload is NOT a category varve invents. It is in production in this exact niche and predates this requirement by over a year. Any claim of novelty here is false and must not be made. . What survives as genuine differentiation, stated narrowly: (a) a DECLARED format and entry point as manifest metadata - no precedent found either way, so it is unproven rather than established; (b) distribution over signed OCI, where criticalup uses a bespoke tarball plus a criticaltrust key/manifest format and mentions OCI nowhere; (c) documentation reachable by anyone, because Ferrocene's per-release docs live behind a customer login while public-docs.ferrocene.dev carries only the rolling main branch, banner-marked as possibly incomplete; (d) multi-tool realms, which criticalup rules out - 'Currently CriticalUp only supports one product entry'. . THE ERGONOMIC LESSON IS WORTH COPYING RATHER THAN THE IDEA: `criticalup doc` needs no arguments. It resolves the docs for the version pinned right now. So `varve export-docs` with a single docs payload must work with no selector, and the viewer must find the layer's documentation itself rather than being handed a path. A command that makes the reader name what they already pinned has missed the point. . STORAGE DECIDED 2026-09-15, on the maintainer's space question. The payload is stored as the archive exactly as published and is NEVER unpacked into the store. That is not a preference: the stored bytes must stay identical to what upstream signed or varve verify cannot re-derive the digest, so an unpacked store would trade re-verification for disk. sdk and vsix already work this way for the same reason. . The space saving therefore comes from the READ path, not the store. varve-serve decompresses on demand from the archive in place, so the steady-state cost is the archive alone and nothing is duplicated; a 1.2 MB bundle is decompressed once into memory at start and served from there. export-docs remains for when a copy on disk is genuinely wanted - handing it to someone, or publishing a CI artifact - and costs space only when asked. This is the link-not-copy principle from varve#149 applied at once, and it is also why the viewer reads the store rather than consuming an export. . WHERE IT GETS EXPENSIVE IS THE FORMAT, NOT THE POLICY. .tar.gz is sequential-access: serving one file means decompressing up to it. That is free at 1.2 MB and bad for a few-hundred-MB rustdoc, which is the same shape as varve#141 - do not read gigabytes to answer a small question. varve-core today carries flate2 and tar and no zip reader, because a vsix is never opened. Since we control our own release artifacts, publishing the traceability and rustdoc bundles as ZIP would buy true random access - a central directory and per-entry deflate let the viewer seek to one entry without touching the rest - at the cost of one dependency. Recommended when rustdoc lands in v0.36.0 rather than now, because the traceability bundle does not need it and an unused dependency in a verification binary is its own cost. . SHIPPED IN v0.35.0 AS IMPLEMENTED, NOT VERIFIED — recorded 2026-09-16 at the close of that release. The docs KIND, the declared format and entry point, the signed annotations, export-docs, the varve-serve viewer and the inspect block all shipped, with 31 source markers across six files and a system test that deposits, installs, exports and serves a real document. Clause (2) did not: a docs payload shall declare 'the payload it documents, where it documents one', so a consumer can ask for the rustdoc of a specific crate. No such field exists — format, entry and title do, and none of them names a payload. Marking this verified would claim a clause that was never built, which is the precise false claim REQ-ATTEST-001 once carried with half its shall-clauses unimplemented. . So it moves to v0.36.0 with status implemented, because the missing clause cannot be meaningfully built before there is a crate payload to point at — REQ-CRATEPAYLOAD-001 is in that release. v0.36.0 is cuttable only when this is verified, which keeps the readiness query honest rather than letting a partially met requirement sit in a release that has already been cut. What is owed: the `documents` field on a docs entry, signed into the manifest; varve's own rustdoc produced by release.yml BEFORE the SHA256SUMS step; and the zip container recorded above, since rustdoc is where sequential tar.gz reads stop being free. . CONTAINER MEASURED 2026-09-17, before building the zip reader. varve's own rustdoc is 16 MB in 811 files; varve-core alone is 658 files, 3.4 MB as tar.gz. zip -9 of the same tree is 17% LARGER than tar.gz (4.4 vs 3.7 MB), and decompressing the whole tar.gz takes about 10 ms, which varve-serve pays once at start. So zip buys nothing for any document varve publishes today and would add a dependency to a verification binary — the cost this requirement itself named. rustdoc therefore ships as tar.gz (release.yml, one archive per published crate, entry /index.html checked in the step), and was proven end to end on the real archive: deposit as format rustdoc, install, export-docs, varve-serve --check reading it from the store. Zip stays the answer for a document of hundreds of megabytes, and adding it then is additive — the format is declared and the reader is chosen by it. Also found this day: varve-producer deposited every [[docs]] entry unlabelled (signed as a tool, no format), fixed in 943340b; before that fix no realm could have carried a document through the producer. . STILL IMPLEMENTED, NOT VERIFIED, at the close of v0.36.0 development, 2026-09-18. Every clause now has code: clause 2's `documents` field is declared in layer.toml, carried by the producer, signed as eu.pulseengine.varve.docs.documents, refused at deposit when it names no payload of the layer, and read back by `export-docs --for` and `inspect`; the producer defect that deposited documents unlabelled is fixed; rustdoc is built per published crate by release.yml before SHA256SUMS. What is missing is not code but an ARTIFACT: no released varve has ever produced a rustdoc archive, because that step has never run — it runs on a tag. 'The workflow was green' is not 'the artifact exists', and this requirement's own history is why: v0.35.0 nearly shipped it verified on an unbuilt clause. Verified when the v0.36.0 release assets contain varve-core-0.36.0-rustdoc.tar.gz and it opens as a docs payload." + status: verified + description: "A layer pins exact versions and says nothing about how to USE them. The tools that embed their own documentation need no help — varve docs and rivet docs answer for themselves, at the version installed, offline. This requirement is for the documentation NOTHING CAN BE ASKED FOR. . Two cases, both real today. A crate's documentation IS its rustdoc, and that is the cheapest documentation in the toolchain to obtain: nobody authors it, `cargo doc` generates it from code that already exists. It is also the least reachable — HTML produced at build time, existing nowhere a consumer can get to offline, because docs.rs is a network service and a pinned air-gapped build cannot consult it. So a carried crate and its rustdoc are ONE deliverable and should ship together: a consumer that can pin varve-core but must go to docs.rs to read it has been given half of what it needs, and the half that requires a network. This requirement is therefore paired with REQ-CRATEPAYLOAD-001 and follows it — there is no rustdoc payload worth carrying until there is a crate payload to document. And the architecture documentation for the toolchain as a whole lives on pulseengine.eu, belongs to no single binary, and is therefore versioned by nothing: a reader cannot tell which layer the page they are reading describes. . MEASURED 2026-09-07: no pulseengine release publishes any documentation asset at all. rivet ships binaries, an SBOM, a vsix and a compliance report; meld, synth, witness and loom ship no documentation in any form. So the documentation a consumer of layer 2026.09.1 can obtain for the exact versions it pins is: nothing, unless the tool embeds its own. . ONE KIND, WITH A SUBTYPE. This shall not become a kind per flavour — rustdoc, architecture, manual, reference and whatever follows are all the same thing to the store, the manifest and the signature, and differ only in what a reader does with them. A single `docs` payload kind carrying a declared subtype keeps the signed manifest stable while the set of flavours grows, which is the opposite of what a kind per flavour does. . BEING ABLE TO OPEN IT IS PART OF THE REQUIREMENT, not a convenience on top. Documentation a consumer has to locate inside a content-addressed store by hand is documentation nobody reads. An HTML tree needs serving, because rustdoc's cross-links and search do not work from file:// in every browser and a consumer should not have to know that; a PDF needs opening. varve shall do the guiding, in one command, rather than printing a store path and wishing the reader luck. . AND IT SHALL BE ADDRESSABLE BY A MACHINE. An agent asked to work with a pinned toolchain should be able to obtain the documentation for exactly the versions in play, without guessing paths or scraping human output. That is the same argument as --format json everywhere else in varve, applied to the one thing an agent most needs and currently cannot get. . Clauses: (1) A layer shall be able to carry documentation as a payload of kind `docs`, verified against the signed manifest exactly as every other payload is — documentation that guides a build or a ceremony is evidence, and unverified evidence is worth less than none. (2) A docs payload shall declare a SUBTYPE (rustdoc, architecture and others as they arrive) and the payload it documents, where it documents one, so a consumer can ask for the rustdoc of a specific crate rather than for a filename someone chose. (3) One kind, not one per flavour: adding a flavour shall not change the signed manifest's schema. (4) varve shall LOCATE AND INSTRUCT, never render and never serve. For a pinned layer it shall report where each document is and print the exact command to open it — the system handler for a PDF, a one-line local server for an HTML tree — in a form a human can paste and an agent can execute. varve shall not render markdown, rewrite HTML, or run a listener: NO SERVER OF OUR OWN is a stated design principle (README), repeated in the help for varve deposit, and a documentation viewer would be the first exception to it — adding a network listener to a tool whose value is partly that it has none, and a rendering dependency to a supply-chain tool that currently has five. Serving is one command the consumer runs, with a runtime they already have; varve knowing WHICH command and WHERE to run it is the whole of the help that is actually needed. (5) The set of documents in a layer shall be machine-readable, so an agent can be pointed at the documentation for the exact versions pinned. (6) It shall remain OFFLINE: a docs payload is bytes in the layer, never a link to a service, because a link is not pinned, can change after signing, and is unreachable from an air-gapped build — the three properties varve exists to provide. (7) Documentation shall be optional: a layer carrying none shall behave exactly as layers do today, since most payloads will never have any. . THAT MEASUREMENT IS NOW OUT OF DATE, IN OUR FAVOUR (re-measured 2026-09-11). varve's own release publishes varve--traceability-html.tar.gz, varve--traceability.reqif and varve--rivet-artifacts.tar.gz, and all of them are already inside the cosign-signed SHA256SUMS.txt. So the first docs payload needs NO release-workflow change at all - the artifact exists, signed, today. Only the manifest cannot express it. That is why the maintainer sequenced this AHEAD of REQ-CRATEPAYLOAD-001 on 2026-09-11, reversing the order this text assumed: rustdoc still follows the crate, but the docs KIND does not have to wait for either. . PROBED the real artifact rather than assuming its shape: 341 entries, 1.2 MB, a root index.html, and _assets/mermaid.min.js which calls fetch() 28 times. Under file:// those fetches are CORS-blocked, so diagrams silently degrade. A viewer is therefore a real requirement and not a convenience - but varve must still not RENDER anything. . DESIGN DECIDED 2026-09-11. A [[docs]] entry declares name, repo, version, format, entry, title and asset. `format` (html | rustdoc | pdf | markdown | reqif) is DECLARED, never sniffed from an extension, for the same reason upstream-sums is declared: a guess that is wrong produces a payload varve offers to open the wrong way, and extension-sniffing is how a gate goes vacuous. It is also the thing a human filters on. `entry` is declared for the same reason - this bundle happens to have one root index.html, and 'whatever index.html we find' breaks the day an upstream ships two. rustdoc is its own format rather than html because it is generated not authored, versions with its crate, and 'show me the API docs' is a different question from 'show me the handbook'. . Docs are HELD, never DISPATCHED - they are data, like vsix. The command is `varve export-docs --to DIR`, joining the existing export- family, because `varve docs` already means varve's own embedded topics and reusing that name for layer-carried documentation would break the CLI convention that a name means one thing. inspect gains the entry point AFTER the payload table, not before: the table is what inspect is run for. . THE VIEWER IS A COMPANION BINARY, `varve-serve`, shipped from this repo the way varve-producer is - one release, one signing, one supply chain - and carried in the pulseengine layer as an ordinary tool payload so a realm can choose to include it or not. Deliberately NOT a subcommand of varve: varve is the binary that verifies a toolchain, and giving it an HTTP listener expands the surface of exactly the thing whose smallness is the point. A separate binary also means the viewer is itself pinned, signed and versioned by the system it serves. . PRIOR ART, AND IT IS AHEAD OF US (researched 2026-09-15, Ferrocene public docs + criticalup changelog). criticalup shipped `criticalup doc` in v1.3.0 on 2025-01-30 - 'opens the documentation of the relevant Ferrocene version' - and ferrocene-docs-xxx is a first-class installable package in the same channel and package system as rustc-, not a side artifact. So documentation as a pinned, installed, version-matching payload is NOT a category varve invents. It is in production in this exact niche and predates this requirement by over a year. Any claim of novelty here is false and must not be made. . What survives as genuine differentiation, stated narrowly: (a) a DECLARED format and entry point as manifest metadata - no precedent found either way, so it is unproven rather than established; (b) distribution over signed OCI, where criticalup uses a bespoke tarball plus a criticaltrust key/manifest format and mentions OCI nowhere; (c) documentation reachable by anyone, because Ferrocene's per-release docs live behind a customer login while public-docs.ferrocene.dev carries only the rolling main branch, banner-marked as possibly incomplete; (d) multi-tool realms, which criticalup rules out - 'Currently CriticalUp only supports one product entry'. . THE ERGONOMIC LESSON IS WORTH COPYING RATHER THAN THE IDEA: `criticalup doc` needs no arguments. It resolves the docs for the version pinned right now. So `varve export-docs` with a single docs payload must work with no selector, and the viewer must find the layer's documentation itself rather than being handed a path. A command that makes the reader name what they already pinned has missed the point. . STORAGE DECIDED 2026-09-15, on the maintainer's space question. The payload is stored as the archive exactly as published and is NEVER unpacked into the store. That is not a preference: the stored bytes must stay identical to what upstream signed or varve verify cannot re-derive the digest, so an unpacked store would trade re-verification for disk. sdk and vsix already work this way for the same reason. . The space saving therefore comes from the READ path, not the store. varve-serve decompresses on demand from the archive in place, so the steady-state cost is the archive alone and nothing is duplicated; a 1.2 MB bundle is decompressed once into memory at start and served from there. export-docs remains for when a copy on disk is genuinely wanted - handing it to someone, or publishing a CI artifact - and costs space only when asked. This is the link-not-copy principle from varve#149 applied at once, and it is also why the viewer reads the store rather than consuming an export. . WHERE IT GETS EXPENSIVE IS THE FORMAT, NOT THE POLICY. .tar.gz is sequential-access: serving one file means decompressing up to it. That is free at 1.2 MB and bad for a few-hundred-MB rustdoc, which is the same shape as varve#141 - do not read gigabytes to answer a small question. varve-core today carries flate2 and tar and no zip reader, because a vsix is never opened. Since we control our own release artifacts, publishing the traceability and rustdoc bundles as ZIP would buy true random access - a central directory and per-entry deflate let the viewer seek to one entry without touching the rest - at the cost of one dependency. Recommended when rustdoc lands in v0.36.0 rather than now, because the traceability bundle does not need it and an unused dependency in a verification binary is its own cost. . SHIPPED IN v0.35.0 AS IMPLEMENTED, NOT VERIFIED — recorded 2026-09-16 at the close of that release. The docs KIND, the declared format and entry point, the signed annotations, export-docs, the varve-serve viewer and the inspect block all shipped, with 31 source markers across six files and a system test that deposits, installs, exports and serves a real document. Clause (2) did not: a docs payload shall declare 'the payload it documents, where it documents one', so a consumer can ask for the rustdoc of a specific crate. No such field exists — format, entry and title do, and none of them names a payload. Marking this verified would claim a clause that was never built, which is the precise false claim REQ-ATTEST-001 once carried with half its shall-clauses unimplemented. . So it moves to v0.36.0 with status implemented, because the missing clause cannot be meaningfully built before there is a crate payload to point at — REQ-CRATEPAYLOAD-001 is in that release. v0.36.0 is cuttable only when this is verified, which keeps the readiness query honest rather than letting a partially met requirement sit in a release that has already been cut. What is owed: the `documents` field on a docs entry, signed into the manifest; varve's own rustdoc produced by release.yml BEFORE the SHA256SUMS step; and the zip container recorded above, since rustdoc is where sequential tar.gz reads stop being free. . CONTAINER MEASURED 2026-09-17, before building the zip reader. varve's own rustdoc is 16 MB in 811 files; varve-core alone is 658 files, 3.4 MB as tar.gz. zip -9 of the same tree is 17% LARGER than tar.gz (4.4 vs 3.7 MB), and decompressing the whole tar.gz takes about 10 ms, which varve-serve pays once at start. So zip buys nothing for any document varve publishes today and would add a dependency to a verification binary — the cost this requirement itself named. rustdoc therefore ships as tar.gz (release.yml, one archive per published crate, entry /index.html checked in the step), and was proven end to end on the real archive: deposit as format rustdoc, install, export-docs, varve-serve --check reading it from the store. Zip stays the answer for a document of hundreds of megabytes, and adding it then is additive — the format is declared and the reader is chosen by it. Also found this day: varve-producer deposited every [[docs]] entry unlabelled (signed as a tool, no format), fixed in 943340b; before that fix no realm could have carried a document through the producer. . STILL IMPLEMENTED, NOT VERIFIED, at the close of v0.36.0 development, 2026-09-18. Every clause now has code: clause 2's `documents` field is declared in layer.toml, carried by the producer, signed as eu.pulseengine.varve.docs.documents, refused at deposit when it names no payload of the layer, and read back by `export-docs --for` and `inspect`; the producer defect that deposited documents unlabelled is fixed; rustdoc is built per published crate by release.yml before SHA256SUMS. What is missing is not code but an ARTIFACT: no released varve has ever produced a rustdoc archive, because that step has never run — it runs on a tag. 'The workflow was green' is not 'the artifact exists', and this requirement's own history is why: v0.35.0 nearly shipped it verified on an unbuilt clause. Verified when the v0.36.0 release assets contain varve-core-0.36.0-rustdoc.tar.gz and it opens as a docs payload. . ARTIFACT VERIFIED 2026-09-18 from the PUBLISHED v0.36.0 release, with the released binary rather than a local build. The release carries varve-core-0.36.0-rustdoc.tar.gz and varve-0.36.0-rustdoc.tar.gz; both are lines in the signed SHA256SUMS.txt and the downloaded bytes match. Deposited as kind = docs with format rustdoc, entry varve_core/index.html and documents = varve-core beside the crate payload, then installed: 'varve export-docs --for varve-core' exported 669 files and named varve_core/index.html as the starting page, and 'varve export-cargo' materialised the crate. So the clause that was owed — varve's own rustdoc, produced by release.yml before the sums — is now an artifact anyone can fetch and open, not a green workflow." release: v0.36.0 links: - type: derives-from From 9509f7682baac4eca3806d98c13e6d93c596440d Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Fri, 18 Sep 2026 07:46:48 +0200 Subject: [PATCH 2/3] =?UTF-8?q?REQ-CRATEPAYLOAD-001:=20verified=20?= =?UTF-8?q?=E2=80=94=20the=20realm's=20layer=20carries=20the=20crate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clause 3 asks for a DEPOSITED pulseengine layer carrying varve's own consumer crate, which varve's repository cannot produce. It exists now: the maintainer merged pulseengine-layers#16, and layer 2026.09.4 (counter 5, sha256:001480e7…) was deposited with 15 payloads — the 13 the realm already carried, plus varve-core 0.36.0 as kind = crate and varve-core-api as its rustdoc, declaring documents = varve-core. Checked from the CONSUMER side with the released varve 0.36.0, a fresh store and VARVE_TRUST_ROOT unset, so the realm's own published root verified it: install verified 15 payloads against their signed digests; inspect shows both HELD and prints the `--for varve-core` command; export-cargo wrote a local registry holding varve-core-0.36.0.crate; export-docs --for varve-core exported 669 files. The crate out of the layer hashes to 9f85301c…, which is crates.io's cksum for 0.36.0 — so the layer carries what cargo would have downloaded, provable offline. Recorded as VER-CRATEPAYLOAD-001, method: inspection, because nothing in CI can deposit into the realm and a test that did would publish an immutable layer on every run. The repeatable halves stay under test: the producer's layer.toml-to-signed-layer oracle, and the crate-identity job on every tag. I wiped this requirement's description with a bad shell pipeline while doing this and restored all 5927 characters from a copy taken beforehand; the diff shows only the intended note. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu --- artifacts/requirements.yaml | 4 ++-- artifacts/verification.yaml | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index 264b610..2fc299f 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -4304,8 +4304,8 @@ artifacts: - id: REQ-CRATEPAYLOAD-001 type: requirement title: A realm can carry a crate, starting with varve's own consumer crate - status: implemented - description: "MEASURED 2026-09-09. varve's CRATE support is complete on the consumer side and unreachable on the producer side. `kind = \"crate\"` is accepted by the deposit spec, `export-cargo` materialises a Cargo local registry, `export-crates-vendor` materialises a vendor tree, `export-bazel-distdir` materialises a distdir, and all of them are tested and documented. What cannot happen is a REALM declaring one: `LayerManifest` has a `tools` section and a `vsix` section and nothing else, and `ManifestTool` has no `kind` field at all. So layer.toml cannot say that a payload is a crate, varve-producer cannot ingest one, and no published layer has ever carried a crate. Sixth instance in this release of a complete, verified capability that no artifact carries. . THE CASE THAT MADE IT CONCRETE. REQ-CONSUMERAPI-001 answered jess's four consumption failures with: stop shelling out, depend on varve-core, which is on crates.io and returns typed answers. That advice is only half a solution while the crate cannot be pinned. jess would take varve-core from crates.io — unpinned by the layer, unverified by the realm root, and reachable only with a network — inside a toolchain whose entire purpose is that its contents are pinned, signed and installable offline. The tool that makes builds reproducible would be the one dependency fetched the old way. . SECOND BLOCKER, and it is not the manifest's fault. A varve release publishes no `.crate` file; publish-crates.yml sends them to crates.io and the release carries binaries, an SBOM and traceability artifacts. `cargo package` already produces the exact bytes crates.io receives, so this is an upload, not a build. Which SOURCE a crate payload is ingested from is the decision this requirement has to make and not assume: a release asset alongside the binaries keeps one verification story — cosign over SHA256SUMS.txt, the same as every other payload — whereas fetching from crates.io means a second trust path and a different proof mechanism for the same bytes. . Clauses: (1) A realm manifest shall be able to declare a payload that is a crate, so a layer can carry a library and not only executables. (2) The declaration shall reuse the existing payload-kind vocabulary rather than inventing a parallel one — `kind` on a tool entry, or a section shaped like `vsix`, whichever keeps the SIGNED manifest's schema stable as kinds are added. (3) varve's own consumer crate shall be carried in the pulseengine layer — the realm deposited by pulseengine-layers and declared in its layer.toml beside varve-producer, not a separate varve realm (maintainer decision 2026-09-17). A consumer pinning the pulseengine layer then gets the toolchain AND the library that reads it from one signed manifest: recommending varve-core to a consumer while it is the one dependency they cannot pin is advice varve does not follow itself. (4) The bytes shall be verified by the same mechanism as every other payload of the release; a crate shall not acquire a weaker or merely different proof path because it came from a package registry. (5) A crate payload shall be HELD and never dispatched — `varve run` on a `.crate` was never meaningful — and the existing export adapters shall consume it unchanged, since the consumer half is already built and this requirement is about reaching it. . MOVED TO v0.36.0 on 2026-09-11, by the maintainer's decision, in favour of doing the docs chain first. The reason is asymmetry of readiness, not of value: the first docs payload needs no release-workflow change because varve already publishes a signed traceability bundle, whereas this needs cargo package wired into release.yml ahead of the SHA256SUMS step. Docs therefore reach a consumer sooner for less risk. Nothing here is descoped and the rustdoc half of REQ-LAYERDOCS-001 still waits on this, because there is no rustdoc worth carrying until there is a crate to document. . HELD AT IMPLEMENTED FOR v0.36.0, 2026-09-18. Clauses 1, 2, 4 and 5 are built and tested in this repository: layer.toml declares [[crate]], the planner expands it platform-independently, staging places the bytes unmodified, the spec labels kind = \"crate\", and release.yml packages varve-core and varve before SHA256SUMS with a crate-identity job comparing the signed sums to the crates.io index. Clause 3 — 'varve's own consumer crate shall be carried in the pulseengine layer' — is NOT evidence this repository can produce: the layer is built by pulseengine/pulseengine-layers, whose layer.toml must pin varve v0.36.0 and add the [[crate]] entry, and that merge belongs to the maintainer. Promoting to verified would claim a payload no published layer carries. Verified when a deposited pulseengine layer carries varve-core and 'varve export-cargo' materialises it." + status: verified + description: "MEASURED 2026-09-09. varve's CRATE support is complete on the consumer side and unreachable on the producer side. `kind = \"crate\"` is accepted by the deposit spec, `export-cargo` materialises a Cargo local registry, `export-crates-vendor` materialises a vendor tree, `export-bazel-distdir` materialises a distdir, and all of them are tested and documented. What cannot happen is a REALM declaring one: `LayerManifest` has a `tools` section and a `vsix` section and nothing else, and `ManifestTool` has no `kind` field at all. So layer.toml cannot say that a payload is a crate, varve-producer cannot ingest one, and no published layer has ever carried a crate. Sixth instance in this release of a complete, verified capability that no artifact carries. . THE CASE THAT MADE IT CONCRETE. REQ-CONSUMERAPI-001 answered jess's four consumption failures with: stop shelling out, depend on varve-core, which is on crates.io and returns typed answers. That advice is only half a solution while the crate cannot be pinned. jess would take varve-core from crates.io — unpinned by the layer, unverified by the realm root, and reachable only with a network — inside a toolchain whose entire purpose is that its contents are pinned, signed and installable offline. The tool that makes builds reproducible would be the one dependency fetched the old way. . SECOND BLOCKER, and it is not the manifest's fault. A varve release publishes no `.crate` file; publish-crates.yml sends them to crates.io and the release carries binaries, an SBOM and traceability artifacts. `cargo package` already produces the exact bytes crates.io receives, so this is an upload, not a build. Which SOURCE a crate payload is ingested from is the decision this requirement has to make and not assume: a release asset alongside the binaries keeps one verification story — cosign over SHA256SUMS.txt, the same as every other payload — whereas fetching from crates.io means a second trust path and a different proof mechanism for the same bytes. . Clauses: (1) A realm manifest shall be able to declare a payload that is a crate, so a layer can carry a library and not only executables. (2) The declaration shall reuse the existing payload-kind vocabulary rather than inventing a parallel one — `kind` on a tool entry, or a section shaped like `vsix`, whichever keeps the SIGNED manifest's schema stable as kinds are added. (3) varve's own consumer crate shall be carried in the pulseengine layer — the realm deposited by pulseengine-layers and declared in its layer.toml beside varve-producer, not a separate varve realm (maintainer decision 2026-09-17). A consumer pinning the pulseengine layer then gets the toolchain AND the library that reads it from one signed manifest: recommending varve-core to a consumer while it is the one dependency they cannot pin is advice varve does not follow itself. (4) The bytes shall be verified by the same mechanism as every other payload of the release; a crate shall not acquire a weaker or merely different proof path because it came from a package registry. (5) A crate payload shall be HELD and never dispatched — `varve run` on a `.crate` was never meaningful — and the existing export adapters shall consume it unchanged, since the consumer half is already built and this requirement is about reaching it. . MOVED TO v0.36.0 on 2026-09-11, by the maintainer's decision, in favour of doing the docs chain first. The reason is asymmetry of readiness, not of value: the first docs payload needs no release-workflow change because varve already publishes a signed traceability bundle, whereas this needs cargo package wired into release.yml ahead of the SHA256SUMS step. Docs therefore reach a consumer sooner for less risk. Nothing here is descoped and the rustdoc half of REQ-LAYERDOCS-001 still waits on this, because there is no rustdoc worth carrying until there is a crate to document. . HELD AT IMPLEMENTED FOR v0.36.0, 2026-09-18. Clauses 1, 2, 4 and 5 are built and tested in this repository: layer.toml declares [[crate]], the planner expands it platform-independently, staging places the bytes unmodified, the spec labels kind = \"crate\", and release.yml packages varve-core and varve before SHA256SUMS with a crate-identity job comparing the signed sums to the crates.io index. Clause 3 — 'varve's own consumer crate shall be carried in the pulseengine layer' — is NOT evidence this repository can produce: the layer is built by pulseengine/pulseengine-layers, whose layer.toml must pin varve v0.36.0 and add the [[crate]] entry, and that merge belongs to the maintainer. Promoting to verified would claim a payload no published layer carries. Verified when a deposited pulseengine layer carries varve-core and 'varve export-cargo' materialises it. . CLAUSE 3 SATISFIED 2026-09-18. The pulseengine realm deposited layer 2026.09.4 (counter 5, sha256:001480e799f7274248863a89a76deeb333ee475701bc2702f86fe326b527ae6e) carrying 15 payloads: the 13 it already had, plus varve-core 0.36.0 as kind = crate and varve-core-api as a rustdoc document declaring documents = varve-core (pulseengine-layers#16, merged by the maintainer). Proven from the CONSUMER side, from the published registry with a fresh store and no VARVE_TRUST_ROOT — the realm's own root: 'varve install' installed and verified the layer (signature OK, 15 payloads match their signed digests); 'varve inspect' shows both payloads HELD; 'varve export-cargo' materialised a local Cargo registry containing varve-core-0.36.0.crate; and 'varve export-docs --for varve-core' exported 669 files of its rustdoc. The crate in the layer hashes to 9f85301c182005f2bf6d58a876fabb021557c7b49e4b6f2bae5e4eec91d0ac1f, which is exactly the cksum crates.io records for 0.36.0 — so the advice varve gives consumers (depend on varve-core rather than shell out) is now advice varve follows: the crate can be pinned, verified against the realm root, and installed offline." release: v0.36.0 - id: REQ-SCAN-001 diff --git a/artifacts/verification.yaml b/artifacts/verification.yaml index 67b408e..fe1467e 100644 --- a/artifacts/verification.yaml +++ b/artifacts/verification.yaml @@ -3156,3 +3156,19 @@ artifacts: - type: verifies target: REQ-ROLLING-001 + + - id: VER-CRATEPAYLOAD-001 + type: verification + title: The pulseengine layer carries varve-core, and a consumer gets it offline + status: accepted + description: "Clause 3 is the one clause varve's own repository cannot evidence: it asks for a DEPOSITED layer of the pulseengine realm carrying varve's consumer crate, and that layer is built by pulseengine/pulseengine-layers. This records the check made against the PUBLISHED realm on 2026-09-18, from the consumer side. . WHAT WAS DEPOSITED. pulseengine-layers#16 (the maintainer's merge) pinned varve v0.36.0 and added [[crate]] varve-core 0.36.0 and [[docs]] varve-core-api declaring documents = varve-core. Deposit run 35311453249 published layer 2026.09.4, counter 5, sha256:001480e799f7274248863a89a76deeb333ee475701bc2702f86fe326b527ae6e; its own sanity install reported signature OK, 15 tool(s) match their signed digests -- the 13 the realm already carried, plus the crate and the document. . WHAT A CONSUMER SEES. Checked with the RELEASED varve 0.36.0 binary, a fresh store, a varve.toml pinning realm pulseengine channel rolling layer 2026.09.4, realm definitions and rolling.pub taken from the release assets, and VARVE_TRUST_ROOT unset -- so the realm's own published root did the verifying. `varve install` installed and verified the layer. `varve inspect` lists varve-core as kind crate and varve-core-api as kind docs, both HELD, and prints \"documents varve-core (varve export-docs --for varve-core)\". `varve export-cargo` wrote a local Cargo registry containing varve-core-0.36.0.crate with its generated config.toml. `varve export-docs --for varve-core` exported 669 files and named varve_core/index.html as the starting page. . THE BYTES ARE THE REGISTRY'S BYTES. The crate materialised out of the layer hashes to 9f85301c182005f2bf6d58a876fabb021557c7b49e4b6f2bae5e4eec91d0ac1f, which is the cksum crates.io records for varve-core 0.36.0. The layer does not merely carry something named varve-core: it carries what cargo would have downloaded, provable offline against the realm root. . WHY method IS inspection. Nothing in varve's CI can deposit into the pulseengine realm, and a test that did would publish an immutable layer on every run. The repeatable parts are covered by tests -- the producer's layer.toml-to-signed-layer oracle, and the crate-identity job that compares the released .crate against the registry index on every tag. This artifact records the one step that is a published fact about another repository, with the identifiers to re-check it." + fields: + method: inspection + links: + - type: verifies + target: REQ-CRATEPAYLOAD-001 + provenance: + created-by: ai + model: claude-opus-5 + timestamp: 2026-09-18T05:46:32Z + release: v0.36.0 From 7ef6a46d72bfe84c561a1f1a5fa23cb872ec47ce Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Fri, 18 Sep 2026 08:55:04 +0200 Subject: [PATCH 3/3] Plan v0.37.0: finish the migration, and record what autonomous deposit costs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scope, chosen by the maintainer: REQ-SDKTARGET-001 (already assigned) — a payload names the target it builds for, not only the host it runs on. REQ-LAYERREPO-001 → v0.37.0. The migration is complete except for the retirement: pulseengine-layers holds the manifest and the key, consumes a RELEASED assembler, and since today scans and deposits on its own every 15 minutes; layer 2026.09.4 was deposited that way. What remains in varve is a second implementation of what moved — scan-upstream.sh (533 lines), upstream-mechanism.sh, scan-upstream.yml and their systest (varve#142) — an unused copy of the live pipeline inside the tool the realm consumes. Their systest and verification artifact must be re-pointed or removed with them, or that gate goes vacuous while still reporting green. REQ-VERIFYPAR-001 (new, proposed) — hash payloads concurrently, but clause 1 is a COLD measurement on a runner before any concurrency is written, and the requirement is withdrawn if reading dominates there. The warm profile says hashing is 83% of 0.89 s; no cold figure exists because macOS purge needs root and a 20 GiB eviction on a 16 GiB machine did not work. This is the same question varve#141 was first answered wrongly on, and the rule that made the difference was: measure, then write. REQ-KEYROLES-001 (new, proposed) — varve#148. One flat root signs layers, line-status, line-index and attestations alike, and that key is now used unattended every 15 minutes: one compromise takes the reaction channel with it. Clause 1 is the maintainer's decision, not a design — custody is their policy (varve#113) — and it must be settled BEFORE the v1.0 ceremony, or that ceremony replaces one flat key with another. DD-030 supersedes DD-024. "Automate noticing, not signing" was right while varve held the key; the key moved to the realm's own repository, which chose autonomous deposit for its rolling channel. DD-030 states the narrower position, lists the controls that survive without a person, and names the one that does not: whether a release belongs in the toolchain at all. DD-024 is marked deprecated and points forward; the schema has no `supersedes` link, so the relation is written in both descriptions. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu --- artifacts/requirements.yaml | 83 +++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index 2fc299f..db311b4 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -3487,8 +3487,8 @@ artifacts: type: requirement title: A realm's layers are assembled outside varve, in that realm's own repository status: approved - release: v0.30.0 - description: "`deposit-layer.yml` lives inside varve and hard-codes one realm's tool list, so bumping a tool version is a commit to the tool that installs tools, and a second realm's signing key would live in varve's repository settings. That couples varve's release cadence to every tool bump and puts two realms' custody in one place — which `docs root-ceremony` argues against in the same breath as it argues for split custody. . Clauses: (1) Layer assembly shall live in a repository per realm, each holding its own tool manifest and its own signing secret. (2) The assembler itself shall stay in varve and be CONSUMED by those repos as a released artifact — it is system-tested here (REQ-SYSTEST-002) and a copy per realm is a copy that drifts, which is the same defect as a gate testing a re-implementation. (3) A layers repo shall need no varve source checkout: it takes a released varve and a released assembler. (4) EXACTLY ONE layers repo shall be stood up first — `pulseengine-layers`, the realm that already has consumers and an existing key — and it shall publish a layer successfully before a second repo is created. The two-realm topology is proven in the gate (REQ-REALM2-001 clause 1) rather than by standing up two repos at once, so a migration failure lands on the realm we can fix rather than on a new realm's first impression. A single repo holding BOTH realms was rejected: it puts two roots' secrets in one repo's settings, and unpicking that later means rotating keys varve cannot rotate. (5) varve's own repository shall stop carrying any realm's signing secret once the migration completes. . Moved v0.29.0 -> v0.30.0 deliberately. The ADAPTER this depends on shipped in v0.29.0 and is verified separately (REQ-LAYERADAPT-001), but clauses 4 and 5 are not code: clause 4 requires `pulseengine-layers` to publish a real layer, which needs `VARVE_ROLLING_KEY` provisioned in that repository by its custodian, and clause 5 requires removing the secret from varve only AFTER that publish succeeds. Neither can be discharged by the party writing the code, and marking this verified on the strength of the adapter alone would claim a migration that has not happened. The layers repository's deposit workflow therefore still refuses to run rather than pretending, and the ordering is deliberate: varve keeps the secret until the new repository has proven it can publish without it. . STATE AS OF 2026-09-02, recorded because it drifted from what was claimed. `pulseengine-layers` exists and holds the manifest, and has NEVER RUN ANYTHING — zero workflow runs, and its deposit still exits 1. Layers 2026.08.4 and 2026.09.0 were both deposited from `pulseengine/varve`. Clause 4's gate is therefore NOT met, and no second layers repository may be created: not for `bytecodealliance` (REQ-REALM2-002), not for `covalent` (REQ-COVALENT-001). Both were described as blocked only on a root key; that was wrong, and they are blocked on this clause first. . The present arrangement is a HALF-MIGRATION and should be named as such: the manifest lives in the layers repository while signing lives in varve, so a cut edits both and keeps them byte-identical by a check. That check is better than drift but it is not clause 1 — it is two sources kept in step, which is the state the migration exists to end. . What blocks clause 4 has also changed. It is no longer the adapter — `varve layer-spec` shipped in v0.29.0, and the layers repository's workflow still carries a stale TODO saying otherwise. It is that no ASSEMBLER is released for a layers repo to consume (clause 3), plus the key and the GHCR write permission. The Rust producer changes the answer to the first: rather than packaging shell scripts as a signed release asset, the released binary IS the assembler. So finishing REQ-PRODUCER-002 is now a prerequisite of this clause, not work beside it." + release: v0.37.0 + description: "`deposit-layer.yml` lives inside varve and hard-codes one realm's tool list, so bumping a tool version is a commit to the tool that installs tools, and a second realm's signing key would live in varve's repository settings. That couples varve's release cadence to every tool bump and puts two realms' custody in one place — which `docs root-ceremony` argues against in the same breath as it argues for split custody. . Clauses: (1) Layer assembly shall live in a repository per realm, each holding its own tool manifest and its own signing secret. (2) The assembler itself shall stay in varve and be CONSUMED by those repos as a released artifact — it is system-tested here (REQ-SYSTEST-002) and a copy per realm is a copy that drifts, which is the same defect as a gate testing a re-implementation. (3) A layers repo shall need no varve source checkout: it takes a released varve and a released assembler. (4) EXACTLY ONE layers repo shall be stood up first — `pulseengine-layers`, the realm that already has consumers and an existing key — and it shall publish a layer successfully before a second repo is created. The two-realm topology is proven in the gate (REQ-REALM2-001 clause 1) rather than by standing up two repos at once, so a migration failure lands on the realm we can fix rather than on a new realm's first impression. A single repo holding BOTH realms was rejected: it puts two roots' secrets in one repo's settings, and unpicking that later means rotating keys varve cannot rotate. (5) varve's own repository shall stop carrying any realm's signing secret once the migration completes. . Moved v0.29.0 -> v0.30.0 deliberately. The ADAPTER this depends on shipped in v0.29.0 and is verified separately (REQ-LAYERADAPT-001), but clauses 4 and 5 are not code: clause 4 requires `pulseengine-layers` to publish a real layer, which needs `VARVE_ROLLING_KEY` provisioned in that repository by its custodian, and clause 5 requires removing the secret from varve only AFTER that publish succeeds. Neither can be discharged by the party writing the code, and marking this verified on the strength of the adapter alone would claim a migration that has not happened. The layers repository's deposit workflow therefore still refuses to run rather than pretending, and the ordering is deliberate: varve keeps the secret until the new repository has proven it can publish without it. . STATE AS OF 2026-09-02, recorded because it drifted from what was claimed. `pulseengine-layers` exists and holds the manifest, and has NEVER RUN ANYTHING — zero workflow runs, and its deposit still exits 1. Layers 2026.08.4 and 2026.09.0 were both deposited from `pulseengine/varve`. Clause 4's gate is therefore NOT met, and no second layers repository may be created: not for `bytecodealliance` (REQ-REALM2-002), not for `covalent` (REQ-COVALENT-001). Both were described as blocked only on a root key; that was wrong, and they are blocked on this clause first. . The present arrangement is a HALF-MIGRATION and should be named as such: the manifest lives in the layers repository while signing lives in varve, so a cut edits both and keeps them byte-identical by a check. That check is better than drift but it is not clause 1 — it is two sources kept in step, which is the state the migration exists to end. . What blocks clause 4 has also changed. It is no longer the adapter — `varve layer-spec` shipped in v0.29.0, and the layers repository's workflow still carries a stale TODO saying otherwise. It is that no ASSEMBLER is released for a layers repo to consume (clause 3), plus the key and the GHCR write permission. The Rust producer changes the answer to the first: rather than packaging shell scripts as a signed release asset, the released binary IS the assembler. So finishing REQ-PRODUCER-002 is now a prerequisite of this clause, not work beside it. . THE MIGRATION IS NOW COMPLETE EXCEPT FOR THE RETIREMENT, recorded 2026-09-18. pulseengine/pulseengine-layers holds the manifest, holds the rolling key as its own secret, consumes a RELEASED assembler (varve-producer, pinned in its own layer.toml and riding in the layer it builds), deposits and publishes on its own authority, and since 2026-09-18 scans upstream and deposits every 15 minutes with no person in the loop (DD-030). Layer 2026.09.4 was deposited that way. Clause 4's gate is therefore met for this realm: varve's repository no longer assembles, signs or publishes it. . WHAT REMAINS IN varve IS A SECOND IMPLEMENTATION OF WHAT MOVED (varve#142): tools/scan-upstream.sh (533 lines), tools/upstream-mechanism.sh, .github/workflows/scan-upstream.yml on a daily cron, and their system test. They are not a smaller version of the realm's pipeline; they are an unused copy of it inside the tool the realm consumes, and every one of them is a place a future reader could take for the live path. Retiring them is the last step of this requirement, not cleanup beside it — assigned to v0.37.0. The systest and the verification artifact that cite the shell scanner must be re-pointed or removed with it, or the gate they belong to goes vacuous while still reporting green." - id: REQ-PRODUCER-002 type: requirement @@ -3972,40 +3972,9 @@ artifacts: - id: DD-024 type: design-decision title: Automate noticing, not signing - status: accepted + status: deprecated release: v0.29.0 - description: > - Automating the whole rolling pipeline — scan, assemble, sign, publish, - hourly — was the obvious reading of "make it automatic", and it is - wrong for varve specifically. - . - In varve, `channel = "rolling"` is NOT a trust boundary. It is an - annotation inside a layer signed by the same realm root as `qualified`. - The REALM is the trust boundary. So "automate the rolling channel" - means the root key signs unattended, dozens of times a week. - . - That contradicts the custody model shipped in v0.28.0 one release - earlier. `docs root-ceremony` says to use the root as rarely as - possible, ideally on an air-gapped host, with a two-person rule and - every use recorded as a ceremony entry. A key used by a cron job is a - CI credential, whatever the ceremony document calls it. - . - It is also unrecoverable. varve has no revocation and no rotation — both - stated in `threat-model` and `root-ceremony`. A layer published in error - cannot be withdrawn; the only remedy is a yank, which needs the same - key. And an upstream compromise would propagate to every rolling - consumer within the hour, with no window in which a human could notice. - . - DECISION: automate the TOIL, not the JUDGEMENT. The scanner notices, - diffs, assembles and gates; a person merges; the merge signs. Every - signature stays something someone chose to make. - . - Rejected for now, but the right shape if unattended publishing is ever - wanted: a SEPARATE realm with its own root for machine-signed layers. - That is what realms are for, and it would let consumers opt into - lower-assurance artifacts knowingly rather than by not reading. It costs - a second root, second custody and second layers repo, and it should not - be built until someone actually wants the fast lane. + description: "Automating the whole rolling pipeline — scan, assemble, sign, publish, hourly — was the obvious reading of \"make it automatic\", and it is wrong for varve specifically. . In varve, `channel = \"rolling\"` is NOT a trust boundary. It is an annotation inside a layer signed by the same realm root as `qualified`. The REALM is the trust boundary. So \"automate the rolling channel\" means the root key signs unattended, dozens of times a week. . That contradicts the custody model shipped in v0.28.0 one release earlier. `docs root-ceremony` says to use the root as rarely as possible, ideally on an air-gapped host, with a two-person rule and every use recorded as a ceremony entry. A key used by a cron job is a CI credential, whatever the ceremony document calls it. . It is also unrecoverable. varve has no revocation and no rotation — both stated in `threat-model` and `root-ceremony`. A layer published in error cannot be withdrawn; the only remedy is a yank, which needs the same key. And an upstream compromise would propagate to every rolling consumer within the hour, with no window in which a human could notice. . DECISION: automate the TOIL, not the JUDGEMENT. The scanner notices, diffs, assembles and gates; a person merges; the merge signs. Every signature stays something someone chose to make. . Rejected for now, but the right shape if unattended publishing is ever wanted: a SEPARATE realm with its own root for machine-signed layers. That is what realms are for, and it would let consumers opt into lower-assurance artifacts knowingly rather than by not reading. It costs a second root, second custody and second layers repo, and it should not be built until someone actually wants the fast lane. . SUPERSEDED BY DD-030 on 2026-09-18 — see DD-030 for the position that replaces this one and what it does not license." links: - type: satisfies target: REQ-ROLLING-001 @@ -4448,3 +4417,47 @@ artifacts: links: - type: satisfies target: REQ-CEREMONY-001 + + - id: DD-030 + type: design-decision + title: Autonomous deposit is accepted where the realm owns its key, and the judgement it removes is named (supersedes DD-024) + status: accepted + description: "DD-024 said: automate noticing, not signing. The realm root must not sign unattended, because `rolling` is not a trust boundary in varve — the REALM is — so an automated publisher means the root key used dozens of times a week, while `docs root-ceremony` says to use it as rarely as possible, air-gapped, under a two-person rule, with every use recorded; and varve has no revocation, so a layer published in error cannot be withdrawn. Every word of that reasoning still holds. . WHAT CHANGED IS WHO SIGNS. When DD-024 was written, varve's own repository held the signing key and would have been the thing running on a timer. It no longer does: the pulseengine realm is deposited by pulseengine/pulseengine-layers, which holds the rolling key as its own secret, and on 2026-09-18 that repository merged a scanner that deposits every 15 minutes with no person in the loop (pulseengine-layers#12). The decision was therefore taken, deliberately and by the maintainer, in the repository that owns the key — and its own header states the cost rather than hiding it: \"THAT IS A DELIBERATE CHOICE AND IT REMOVES A CONTROL.\" . SO THE POSITION IS NOW THIS, and it is narrower than \"automation is fine\". Autonomous deposit is accepted FOR THE ROLLING CHANNEL OF A REALM THAT OWNS ITS OWN KEY, because rolling promises no qualification and says so; the realm's own repository, not varve, decides its cadence; and the machine-checkable controls survive without a person: every upstream release is verified against its own repository's cosign identity before a byte is staged, `next-layer-id.sh` derives the id and counter from the PUBLISHED record and refuses if the registry cannot be read or the id is already spent, the deposit sanity-installs and verifies the layer against the realm's published root before anything is pushed, and `publish-check` refuses to replace a published id with different bytes. . WHAT IS NOT PROTECTED, stated so nobody mistakes the scope: the judgement a person was making. Whether a particular upstream release belongs in the toolchain at all is not a property any of those checks can decide, and no one now decides it before publication. The remedy for a bad layer remains what it always was — deposit another and yank the first — never retraction, because there is none. `qualified` is untouched by this: nothing here licenses unattended signing on a channel that promises qualification. . CONSEQUENCE FOR varve ITSELF. varve's `scan-upstream.yml` and `tools/scan-upstream.sh` exist to automate the noticing that DD-024 stopped short of automating. The realm repository now does its own noticing and its own depositing, so the shell scanner is not a smaller version of that pipeline — it is a second, unused implementation of it inside the tool the realm consumes (varve#142, REQ-LAYERREPO-001). Retiring it is the last step of the migration this decision's successor describes, not a cleanup." + fields: + rationale: "DD-024's reasoning about key custody is unchanged; what changed is that the key moved to the realm's own repository, which took this decision for its rolling channel and recorded the control it gives up. Superseding rather than amending keeps both positions readable: the one that was right while varve held the key, and the one that is right now that it does not. The schema has no supersedes link, so the relation is stated in both descriptions." + links: + - type: satisfies + target: REQ-LAYERREPO-001 + provenance: + created-by: ai + model: claude-opus-5 + timestamp: 2026-09-18T06:53:38Z + release: v0.37.0 + + - id: REQ-VERIFYPAR-001 + type: requirement + title: Verify hashes payloads concurrently, once a cold measurement says hashing is the cost + status: proposed + description: "MEASURED FIRST, then written — the reverse of how this question was answered the first time. REQ-VERIFYSTREAM-001 clause 2 attributed `varve verify` by stage on the real path: a 6-payload layer carrying one 2.00 GiB payload verifies in 0.89 s, of which hashing is 0.74 s at 2.70 GiB/s (hardware SHA-256) and reading is 0.136 s. Signature and manifest are under a millisecond each. So 83% of the time is SHA-256 and the disk is not the wall. . That is what makes this requirement legitimate now and illegitimate before. varve#141 was first read as \"verify is single-threaded, thread it\", and the arithmetic said the hash could not be the cost — 2 GB in 9 s is 227 MB/s against a 1-2 GB/s hash. Streaming the digest removed a 2 GB allocation and brought the same work to 0.89 s, and only THEN did the profile say hashing dominates. A week spent threading before that measurement would have been a week spent on the wrong half. . WHAT IS STILL UNKNOWN, and it bounds this requirement: no genuinely cold-cache figure was obtained (macOS `purge` needs root on the machine measured, and evicting with 20 GiB of unrelated reads on a 16 GiB machine did not work — the \"cold\" run still read at 10.9 GiB/s). On a cold read the balance may shift toward I/O, and a parallel hash would then buy less. The first task here is therefore a cold measurement on a CI runner, which is cold by construction, NOT a thread pool. . Clauses: (1) The per-stage attribution shall be taken on a runner with a cold page cache before any concurrency is written, and the result recorded — if reading dominates there, this requirement is withdrawn rather than implemented. (2) Where hashing dominates, payloads shall be digested concurrently, bounded by available parallelism, with memory still bounded per payload: streaming is not to be given back to gain threads, because the 2 GB allocation is the defect that started this. (3) Verify shall not check less to go faster. Every payload is still digested in full; a failure still names which payload failed and why; the exit code is unchanged; and the verdict must not depend on scheduling order — a layer with two bad payloads shall report the same one every time, or the report is not reproducible evidence. (4) The small case shall not regress: a layer of a few small payloads verifies in ~0 s today, and spawning threads to hash 64 KiB is slower than not. (5) The speed-up shall be MEASURED on both shapes and recorded here, as clause 2 of REQ-VERIFYSTREAM-001 was, so the next person inherits a number rather than a claim." + links: + - type: derives-from + target: REQ-VERIFYSTREAM-001 + provenance: + created-by: ai + model: claude-opus-5 + timestamp: 2026-09-18T06:54:05Z + release: v0.37.0 + + - id: REQ-KEYROLES-001 + type: requirement + title: A realm's keys are scoped by role, so a signature that is valid can still be out of scope + status: proposed + description: "REPORTED as varve#148 and sharpened by comparison with criticaltrust, Ferrocene's trust format, which scopes keys BY ROLE — a key that may sign a release manifest is not thereby a key that may sign the index of what exists. varve has one flat root per realm: the same key signs layer manifests, line-status documents, line-index documents and attestations, and nothing in the format says which of those a given key was ever meant to do. . WHY IT MATTERS MORE NOW THAN WHEN IT WAS FILED. The pulseengine realm's key is used by an unattended depositor every 15 minutes (DD-030). One key, one secret, one blast radius: a compromise of the deposit credential is a compromise of everything a consumer trusts about that realm, including the documents that say which layers are yanked and which line-index is current — the very statements a consumer would use to react to the compromise. Role separation is what keeps the reaction channel out of the blast radius. . WHAT THIS REQUIREMENT MUST NOT DO. It must not invent a key hierarchy on its own authority. Custody, succession and what a root may delegate are the maintainer's policy, drafted in varve#113, and that PR's artifacts are deliberately proposed rather than merged. So this requirement's first clause is a DECISION to be taken, not a design to be implemented. . Clauses: (1) The maintainer decides which roles exist — at minimum whether the document-signing role (line-status, line-index) is separable from the layer-signing role — and that decision is recorded before any format change. (2) Where roles exist, the signed artifact shall name the role its signature claims, so a verifier can refuse a signature that is valid but out of scope. A signature that verifies and should not have been made is exactly the failure a flat key cannot express. (3) A realm's published trust material shall be able to carry more than one key, with each key's role stated, or roles are unenforceable at the consumer. (4) The existing single-key realms shall keep working unchanged: a realm that declares no roles behaves as today, because a format change that invalidates every published layer is not a security improvement. (5) It shall be settled BEFORE the v1.0 ceremony (REQ-CEREMONY-001): the provisional rolling root is replaced there, and replacing one flat key with another flat key would spend the one ceremony this project gets on the arrangement it has already outgrown." + links: + - type: derives-from + target: REQ-CEREMONY-001 + provenance: + created-by: ai + model: claude-opus-5 + timestamp: 2026-09-18T06:54:44Z + release: v0.37.0