Closes GT-706 — assert a package's declared exports against its own tarball - #602
Conversation
…ball, and delete a promise that was never real GT-706. The release DID assert that a tarball carries the entry points its manifest promises -- and computed "promised" as `[pkg.main, ...Object.values(pkg.bin)]`. `exports` was not in that list, so it answered a narrower question than the manifest asks, and contracts@1.1.0 published an export subpath it did not ship. That failed one package later, at infra-providers@1.2.1's install smoke, with core-domain@1.3.1 already irreversibly on the registry and no unpublish available past 72 hours. Measured, not argued: a two-file package declaring "./ingest" with only dist/index.js on disk passes the old assertion with exit 0, while `require pkg/ingest` answers MODULE_NOT_FOUND. 67-validate-declared-exports.mjs collects EVERY string leaf of the condition tree -- a missing types/*.d.ts breaks TypeScript consumers exactly as a missing default/*.js breaks Node -- and folds in main/bin, so it is a superset of the check it replaces rather than a second opinion beside it. The packlist comes from `npm pack --dry-run --json`, so `files` and `.npmignore` are npm's answer. Two places on purpose: ci-cd.yml over every publishable workspace at PR time, npm-release.yml per package inside the publish loop immediately before `npm publish`. A red PR costs a commit; a red release costs a version nobody can withdraw. THE GUARD REFUTED THE ROW THAT ASKED FOR IT, ON ITS FIRST RUN. GT-706 claimed the registry was clean -- 22 of 22 subpaths resolve, 0 phantom. That measurement excluded wildcard keys by its own filter, and one is DEAD: core-domain declares `./infrastructure/adapters/*` and there is no `adapters` directory at all. 0 matches in a 796-file packlist, MODULE_NOT_FOUND from the published 1.3.1 for every name under it, and `git log --all --name-only` over that path returns NOTHING -- introduced by 2453f9a, never carried by any commit. It has been a promise nobody could keep since the day it was written. Deleted, not widened, because there was never anything behind it. The row now records the correction rather than the claim. Falsifiability observed on both sides, four ways. RED: the ./ingest fixture; core-domain on the real tree; and a file present ON DISK but excluded by `files`, which a filesystem check would call clean. GREEN: the same fixture once the file ships, and the whole repository -- 68 declared targets across 9 packages. core-domain 1.3.1 -> 1.3.2 so the fix can reach the registry: the planner is idempotent by version, so a fix that does not bump never ships. That drags the CLI to 1.3.2 with it, which is exactly the cost the exact-pin commit documented -- "a sibling patch now REQUIRES a CLI release" -- arriving on its first use. The four artifacts that DECLARE the CLI version were regenerated, not hand-edited. Board: 673/704 done, 2 pending. Verified: 67 green (68 targets, 9 packages), its 25 fixtures pass, tsc -b clean, core-domain 1992/1992, and 08 / 49 / 01 / 04 / 41 / 57 green with 46 at a fixed point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: aarroyo <beyondnet.peru@gmail.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📊 Bilingual Coverage ImpactPR Changes
Repository Coverage
✅ Good: All EN changes have ES counterparts. Generated by GitHub Actions |
…h the corpus caught it lacking `42-validate-guard-denominators` turned red on `67-validate-declared-exports.mjs` for precisely the defect that guard exists to prevent -- and it was right. The new guard DOES refuse a zero-element scan, but it did so in hand-rolled code that no classification knew about, and an unclassified guard is one that can report a pass over an empty scan. Registered SELF_GUARDED with a proof regex on its own refusal message. Its denominator is the set of publishable workspaces, read from the root manifest rather than hardcoded, and all three ways it could pass over nothing are hard failures: zero workspaces resolved, a package directory with no manifest, and a packlist `npm pack` cannot produce -- that last one being the state in which "nothing was found to be missing" is most misleading. The registration is not the evidence; the exercise is. `43-validate-guard-negative- fixtures` now points 59 guards at a repo-shaped tree with no corpus, up from 58, and 59/59 refused it. Guard 67 has been OBSERVED failing on an empty tree, not merely declared able to. Verified: 42 (62/62 scanning guards, 83 classified, 0 PENDING) and 43 (59/59) green, every self-test in that CI step green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: aarroyo <beyondnet.peru@gmail.com>
|
Aviso desde otra sesión — solo reporto, no toco nada de este PR.
No parece un falso positivo: el detector encuentra en su estreno exactamente el defecto para el que GT-706 se registró — paquetes que declaran targets que su propio tarball no lleva. Dos precisiones por si ayudan al triaje:
Si el hallazgo es correcto, la decisión es si el guard entra ya (y se arreglan los 4 paquetes) o entra detrás del arreglo. |
…r the runner's condition The declared-exports guard was green locally and red on the runner. The asymmetry was not subtle and it was mine: the guard reads each package's PACKLIST, and a workspace whose dist has not been emitted packs no dist, so every declared target reads as missing. The governance job builds FIVE workspaces; the guard covers NINE. Not diagnosed from the log alone -- reproduced. Every dist in the tree was parked and only the five builds that job runs were replayed, which reproduced the runner's four failures exactly: core -> dist/index.js sdk -> dist/index.js mcp -> dist/main.js cli -> dist/index.js Adding those four builds turns the same parked-dist tree green: 68 declared targets across 9 publishable packages. The alternative was to scan only the packages that happen to be built. Rejected on purpose: that is the same "narrower question than the manifest asks" that let contracts@1.1.0 publish an export it never shipped, which is the whole reason this guard exists. The ratchet the neighbouring comment warns about is unaffected -- 41's basis is 35 on a clean checkout by construction, independent of what this machine has built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: aarroyo <beyondnet.peru@gmail.com>
…the one that does not The guard reads each package's PACKLIST, so a workspace whose dist has not been emitted reports every declared target as missing. It was placed in `governance-guards`, which builds five workspaces; the guard scans nine. Adding the missing four there worked, and cost more than it was worth. It now lives in `test`, which already builds eight of the nine — `repo-facts` is added, and that is the whole delta. `Test` is a REQUIRED check, so a phantom export blocks the MERGE rather than the release, which is strictly earlier than where this row asked the assertion to sit. WHAT BUILDING THE CLI IN THE GOVERNANCE JOB UNCOVERED, recorded rather than re-buried. `41-validate-evidence-commands --strict` counts a recorded validationCommand whose binary is absent as DEAD and tolerates it under `--max-dead 17`. With the CLI unbuilt there, four commands had NEVER executed: [GT-665] evolith rulesets --core "$PWD" [GT-665] evolith validate --core "$PWD" --select …/slsa-v1.rules.json --format json [GT-677] evolith evaluate --workspace $W --format drift --evidence $W/ev.json [GT-688] evolith evaluate -t modular-monolith --format json Built, all four exited non-zero in CI. Locally on a full tree, 1 and 2 exit 0 and 4 exits 2 -- `evaluate` exits 2 by design on blocking findings, so THAT one looks like a defect in the record. The divergence on 1 and 2 is unexplained and is not being guessed at here. Evidence recorded against a command that never once ran is indistinguishable from evidence that passed, and `--max-dead` exists for generated state, which is exactly what hides it. Its own row, not a rider on this one. Verified under the runner's condition rather than on a warm tree: every dist parked, only the `test` job's builds replayed, guard green -- 68 declared targets across 9 publishable packages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: aarroyo <beyondnet.peru@gmail.com>
…stead of failing where nothing is built GT-706's own closure record named its guard as a validationCommand, and `41-validate-evidence-commands --execute --strict` ran it in `governance-guards`, which builds five of the nine workspaces it scans. It exited non-zero on the absence of a build, not on anything it was written to check. That is precisely the shape GT-675 already fixed for a remote-tracking ref: the command was fine and the ENVIRONMENT was the wrong one, so the answer was to classify rather than to skip. Same here, with the same consequence -- the command stays in the census and is reported, it is simply not executed in a job that cannot answer it. THE DECLARATION LIVES IN THE GUARD. `export const REQUIRES_BUILT_WORKSPACE = true` in 67, read from SOURCE by 41 -- not imported, because importing every referenced script to ask one question would execute their module bodies, and a classifier that runs the thing it is classifying is not a classifier. A hardcoded roster of build-dependent scripts in 41 was the alternative: one more list that silently stops covering the next one. Two fixtures, because a classification that cannot be observed NOT to apply is a silent skip: a script declaring the marker is classified `heavy-toolchain`, and a script that merely MENTIONS it in a comment is still executed. The marker is anchored to a top-level export for that reason. Verified: 41's 48 self-tests pass, the classifier answers `heavy-toolchain` for 67 and `executable` for 63 as a control, 42 still classifies all 83 guards, and 67 itself is green over 68 targets in 9 packages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: aarroyo <beyondnet.peru@gmail.com>
The release did assert that a tarball carries the entry points its manifest promises — and computed "promised" as
[pkg.main, ...Object.values(pkg.bin)].exportswas not in that list, so it answered a narrower question than the manifest asks.contracts@1.1.0published an export subpath it did not ship; that failed one package later, atinfra-providers@1.2.1's install smoke, withcore-domain@1.3.1already irreversibly on the registry and no unpublish available past 72 hours.Measured, not argued — a two-file package declaring
"./ingest"with onlydist/index.json disk passes the old assertion withexit 0whilerequire pkg/ingestanswersMODULE_NOT_FOUND.The guard refuted the row that asked for it, on its first run
GT-706 claimed the registry was clean: 22 of 22 subpaths resolve, 0 phantom. That measurement excluded wildcard keys by its own filter, and one is dead.
No
adaptersdirectory at all — 0 matches in a 796-file packlist,MODULE_NOT_FOUNDfrom the published1.3.1, andgit log --all --name-onlyover that path returns nothing: introduced by2453f9a2, never carried by any commit. A promise nobody could keep since the day it was written. Deleted, not widened — there was never anything behind it. The row now records the correction rather than the claim.The check
Collects every string leaf of the condition tree — a missing
types/*.d.tsbreaks TypeScript consumers exactly as a missingdefault/*.jsbreaks Node — and folds inmain/bin, so it is a superset of what it replaces, not a second opinion beside it. The packlist comes fromnpm pack --dry-run --json, sofilesand.npmignoreare npm's answer, not a reimplementation.Two places on purpose:
ci-cd.ymlover every publishable workspace at PR time;npm-release.ymlper package inside the publish loop, immediately beforenpm publish. A red PR costs a commit; a red release costs a version nobody can withdraw.Falsifiability observed on both sides, four ways. Red: the
./ingestfixture;core-domainon the real tree; and a file present on disk but excluded byfiles, which a filesystem check would call clean. Green: the same fixture once the file ships, and the whole repository — 68 declared targets across 9 packages.Version chain
core-domain1.3.1 → 1.3.2 so the fix can reach the registry — the planner is idempotent by version, so a fix that does not bump never ships. That drags the CLI to 1.3.2 with it, which is exactly the cost the exact-pin commit documented ("a sibling patch now requires a CLI release") arriving on its first use. The four artifacts that declare the CLI version were regenerated, not hand-edited.Board: 673/704 done, 2 pending.
Verified:
67green (68 targets, 9 packages), its 25 fixtures pass,tsc -bclean, core-domain 1992/1992, and08/49/01/04/41/57green with46at a fixed point.Closes GT-706
🤖 Generated with Claude Code