Release validation¶
Installs the published DCP packages from their official registries into temporary consumers, checks
their identity and origin, and runs the existing external conformance kit
(tools/conformance) against what was installed. It adds no catalog, no
oracle and no conformance model. The guide for readers is
docs/conformance/RELEASE_VALIDATION.md; this file is the
reference for running and maintaining the tool.
A PASS states only this: these artifacts, with these hashes, obtained from these registries, passed these catalogs with the kit at this revision in this environment. It is not a certification and covers nothing that was not executed.
node tools/release-validation/run.mjs [--list] [--lane <id>]... [--catalog <id>]...
[--resolution locked|fresh] [--work <dir>] [--clean]
[--python <exe>] [--go <exe>] [--cargo <exe>] [--chrome <exe>]
node --test tools/release-validation/release-validation.test.mjs # no external network
| Exit | Meaning |
|---|---|
| 0 | PASS: every lane, every catalog cell (60), every block (3), every control |
| 1 | a discrepancy: an artifact answered and the committed expectation does not match |
| 2 | usage |
| 3 | error: infrastructure, identity, unsupported, incomplete or implementation |
| 4 | PARTIAL: a selection without failures. A diagnostic, never a validation |
An error is never counted as a rejection, and it outranks a discrepancy. Installing a package and proving its origin is not conformance: only the cells and blocks are.
The tests use no external network; one of them serves a package index from a loopback HTTP server.
Requirements¶
Node 20 or later on a POSIX host, with the npm next to node, /bin/ps, /usr/bin/tar and, for the
Go lane, /usr/bin/unzip. Built-in Node modules only.
Python, Go, Rust and Chrome are tools of the host: the path given with --python, --go, --cargo or
--chrome, or the first python3, go or cargo on the tool's own PATH (the directory of node,
/usr/bin, /bin) and the usual Chrome of the platform. Nothing is installed, updated or switched:
GOTOOLCHAIN=local, and a rustup proxy is refused, so pass the toolchain's own cargo
(rustup which cargo), which must have rustc next to it. Python must be 3.11 or later (the fresh
sdist lane reads pyproject.toml with tomllib); the committed locks are for CPython 3.12.
Lanes¶
| Lane | Published artifact | Evidence |
|---|---|---|
npm-sdk |
npm @dcp-ai/sdk 2.4.0 |
8 catalogs |
npm-wasm-node |
npm @dcp-ai/wasm 2.2.0, in Node |
6 catalogs |
npm-wasm-browser |
the same package, public API in a real Chrome | 6 catalogs and the block direct-sections (103 cases) |
npm-cli |
npm @dcp-ai/cli 2.2.0 |
no catalog: the blocks cli-suites (218 tests) and cli-negative-control |
pypi-wheel |
PyPI dcp-ai 2.11.0, wheel |
8 catalogs |
pypi-sdist |
PyPI dcp-ai 2.11.0, sdist, built here |
8 catalogs |
go |
Go module sdks/go/v2 v2.14.0 |
8 catalogs |
crates-default |
crates.io dcp-ai 2.14.0, default features |
8 catalogs |
crates-otlp |
the same crate with the otlp feature |
8 catalogs |
Six published versions and nine lanes. Six lanes run 8 catalogs and the two WebAssembly lanes run 6:
60 catalog cells, plus 3 blocks. manifest.json holds the expected identities (those of the audit
dossier), the catalogs and the lanes; expected-blocks.json holds the ids each block must report.
What a run does¶
- Work directory. A new
mkdtempdirectory, or--work <dir>, which must not exist and must be outside the checkout. Nothing is reused. - Environment. Every subprocess gets an environment built by the tool.
HOME,TMPDIR, the caches and each package manager's settings are inside the work directory.PATHis not: it is the tool's own short list of host directories (the directory ofnode,/usr/bin,/bin, and the directory of a toolchain given by option). Nothing is inherited: no credentials, no proxy, no user or project configuration. - Installation, identity and origin, per ecosystem (below). Three values must agree for every package: the manifest's, the hash the tool computes from the downloaded bytes, and the one the package manager or registry records. The downloaded archive must equal, file by file and both ways, what is installed or compiled, apart from the few files an installer writes itself, which are named below.
- Cells. One run of the kit's runner per lane and catalog, with the kit's own wrapper pointed at
the installed package. A report counts only if it is this execution's: a path that did not exist, a
run inside the observed interval with an id not seen before, the argv,
kit.catalog_sha256, fixture hashes, suites and case ids that were asked, and a status that agrees with the runner's exit code. Every case must have a status its class allows, and the counts of every suite and of the report (executedincluded) must be those of the cases. - Blocks. Evidence that is not a catalog cell (the CLI suites, their negative control, the direct browser sections) is a block of its lane. The campaign needs each block exactly once, like a cell: a lane's installation or status never stands in for it.
- Controls, in the same execution. Each lane that brings controls (
npm-sdk,npm-cli,npm-wasm-browser) owes its own fixed set whenever it is selected: exactly one result withok === truefor each id and no other, or the campaign is an error. Some are positives that must pass. The others are altered copies of the real outputs (missing, duplicated and unknown results, stale reports, inconsistent counts, altered identities and local origins) and real runs (an inverted expectation that must fail, wrapper and harness faults, timeouts, a process left behind, a browser run stopped from outside), each of which must be refused in the way that is required of it. - Report.
report.jsonin the work directory, with the runner's reports, block outputs, logs, archives and locks. Everything is kept;--cleanremoves only the installed consumers and caches of this execution.
A process counts as having run only if every observation of it is clean (no signal, no deadline, no output limit, nothing left behind); exit 0 alone is not enough. Every phase has a deadline, and the supervisor stops the whole process tree, whatever its process groups.
npm (npm-sdk, npm-wasm-node, npm-wasm-browser, npm-cli)¶
One consumer (consumers/npm) with the three packages at exact versions and a lock. Static check of
the consumer and lock (exact versions, every entry from registry.npmjs.org with a sha512 integrity, no
links, each DCP package once); npm pack into an empty cache, hashed by the tool; then
npm ci --ignore-scripts (locked) or npm install --ignore-scripts (fresh); then the archive
against the installed tree.
@dcp-ai/wasm 2.2.0 contains pkg/package.json with the name and version of the Rust crate it was
compiled from (dcp-ai 2.14.0). The report records both identities and never expects them to be equal.
npm-cli. The four suites ofcli/__tests__and the kit'snegative-control.mjsrun against the installed CLI (DCP_CLI_ROOT): its binary, itslib/and its dependencies as npm installed them. The tool first checks that the installed CLI would load the one verified@dcp-ai/sdk. The negative control's own verdict is not taken on trust: its output must show exactly the four inverted cases failing, each on an assertion, and the other 214 passing.npm-wasm-browser. The kit'swasm-browser.mjsdrives the publicDcpWasmAPI in a real Chrome with a temporary profile. The files it serves are pinned to aSHA256SUMSthe tool writes from the installed tree. Every cell and the direct sections must come with the wrapper's own browser record of that run: new, with the run's id, inside the observed interval, serving the installed files, with a browser that answered, exited and whose profile was removed.
Python (pypi-wheel, pypi-sdist)¶
Each lane has its own virtual environments. Python runs with -I -B, pip with --isolated, no cache,
no configuration file, and https://pypi.org/simple/ as the only index.
- Wheel.
pip download; the SHA-256 is checked before anything is installed; thenpip install --only-binary :all:; pip's own report gives the hash it installed and shows every file came fromfiles.pythonhosted.org. The wheel and the installed distribution are compared both ways: every module file must be equal and nothing else may be installed. Left out are only the files pip writes itself in*.dist-info:RECORD(rewritten),INSTALLERandREQUESTED; adirect_url.jsonis a problem here. No bytecode exists when the trees are compared (--no-compile,-B).import dcp_aimust load from that site-packages, with no user site and no path outside the work directory and the interpreter. - Sdist. The file is first downloaded as bytes by
fetch-pypi.mjs, which unpacks and runs nothing, and its SHA-256 is compared with the manifest; what the index declares for it is recorded apart and is not trusted. Only then is it unpacked, the build dependencies installed, and the wheel built from the local file with--no-index --no-build-isolation. The built wheel is a separate artifact in the report; its module files must be exactly the sdist's, both ways. It is installed in a clean environment and compared like the wheel lane, except thatdirect_url.jsonis expected there (a local wheel was installed) and must point inside the work directory. - Locks.
consumers/python/<platform>/holdswheel.txt(the wheel and its runtime dependencies),sdist.txtandbuild.txt(the build dependencies), all pinned with hashes, andplatform.json. There is one directory per platform, chosen by the interpreter that runs:cpython-3.12-darwin-arm64andcpython-3.12-linux-x86_64. On any other interpreter or platformlockedis refused as unsupported rather than attempted.
Go (go)¶
The kit's main.go, byte for byte, with the committed go.mod and go.sum (consumers/go, no
replace), built with -mod=readonly, GOPROXY=https://proxy.golang.org, GOSUMDB=sum.golang.org,
GOENV=off, GOWORK=off. The module is first downloaded outside any module, so go checks it against
the checksum database. The tool computes h1 itself over the files of the downloaded zip, compares the
zip with the directory go compiles from, and reads the build information of the binary: the module,
its version and h1, nothing replaced. The report records the architecture the go tool builds for,
which need not be the host's.
Rust (crates-default, crates-otlp)¶
The kit's src/main.rs, byte for byte, in two consumers with their own Cargo.toml and Cargo.lock
(consumers/rust-default, consumers/rust-otlp), CARGO_HOME and target directory. The consumer's
Cargo.toml must be, line for line, the one manifest the tool expects. After cargo fetch --locked
the .crate is hashed and compared with the manifest and the lock before anything is compiled;
cargo metadata must say dcp-ai comes from crates.io, from a directory inside the work directory,
with the features of the lane; the .crate is compared both ways with that directory, apart from
cargo's own .cargo-ok marker; then cargo build --locked.
locked and fresh¶
locked(the default): third-party dependencies come from the committed locks. The selection is reproducible; the host's tools (npm, pip, Go, cargo, rustc, the C compiler, Chrome) are not pinned, only recorded.fresh: resolved now, as a new consumer would get them. The resulting locks are kept in the work directory:package-lock.fresh.jsonat its root, the others underlocks/. In the sdist lane the build dependencies are then recorded but not pinned.
The six DCP identities are required in both modes.
CI¶
.github/workflows/release-validation.yml runs the tool on ubuntu-24.04, manually
(workflow_dispatch) or on pull requests that change this tool or its conformance inputs. Pull requests
run the complete campaign (all, locked). It has read access to the repository, no secret and no
publishing step. Its manual inputs
are a lane (all or one lane) and the resolution. ci.mjs decides the result: PASS only for all
with exit 0; PARTIAL, reported as a diagnostic, only for one lane with exit 4; any other combination
fails the job. The work directory is uploaded as an artifact in every case.
Updating for another release¶
This tool publishes nothing. After a release has been published and its identifiers recorded in the audit dossier:
- Put the new versions and identifiers in
manifest.jsonand in the test that freezes them. A package's file names (file) and the inner crate version of@dcp-ai/wasmchange with it. - Update the consumers: the versions in
consumers/npm/package.json,consumers/go/go.modand the twoCargo.toml, and the DCP lines of the Python locks. - Regenerate the locks with
--resolution freshon each platform that has one, and copy them from the work directory:package-lock.fresh.json, which is at its root, andlocks/python/<platform>/,locks/go/,locks/rust-default/andlocks/rust-otlp/. A Python lock is only produced by running on that platform. - If the kit's CLI suites or direct sections changed, update
expected-blocks.json. - Run the tests, then the complete campaign in
lockedmode.
Limits¶
- One campaign is one environment. What has been executed is in the guide; nothing else is claimed.
- The wrappers and the runner are the checkout's; the package under test is the registry's. Module resolution is not instrumented: the origin check is the lock, the installed tree and the path the wrapper reports.
- The Python locks cover two platforms. The hashes a lock holds say nothing about any other.
- The browser's version and user agent are recorded as the browser reports them. The user agent is a reduced one and does not say which architecture ran.
- Process cleanup reads the process table. A process detached by a parent that dies within one polling
interval (250 ms) is not seen, nothing is cleaned if the tool itself is killed with SIGKILL, and if
psfails nothing is signalled and the report says the cleanup was not confirmed. A browser run stopped from outside leaves its temporary profile in the work directory. - No proxy: the environment built for subprocesses has no proxy variable.
- The report contains absolute paths of the host, and no environment value other than the ones the tool built.