Skip to content

Closes GT-706 — assert a package's declared exports against its own tarball - #602

Merged
beyondnetPeru merged 5 commits into
developfrom
fix/gt-706-assert-declared-exports
Aug 17, 2026
Merged

Closes GT-706 — assert a package's declared exports against its own tarball#602
beyondnetPeru merged 5 commits into
developfrom
fix/gt-706-assert-declared-exports

Conversation

@beyondnetPeru

Copy link
Copy Markdown
Contributor

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. 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.

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.

✗ 67-validate-declared-exports: 1 package(s) declare targets their own tarball does not carry:

  • @beyondnet/evolith-core-domain@1.3.1  (src/packages/core-domain)
      DEAD      ./infrastructure/adapters/* -> dist/infrastructure/adapters/*.d.ts
      DEAD      ./infrastructure/adapters/* -> dist/infrastructure/adapters/*.js

No adapters directory at all — 0 matches in a 796-file packlist, MODULE_NOT_FOUND from the published 1.3.1, and git log --all --name-only over that path returns nothing: introduced by 2453f9a2, 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.ts breaks TypeScript consumers exactly as a missing default/*.js breaks Node — and folds in main/bin, so it is a superset of what it replaces, not a second opinion beside it. The packlist comes from npm pack --dry-run --json, so files and .npmignore are npm's answer, not a reimplementation.

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.

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.

Version chain

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.

Closes GT-706

🤖 Generated with Claude Code

…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>
@beyondnetPeru
beyondnetPeru requested a review from a team as a code owner August 17, 2026 03:26
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

Copy link
Copy Markdown

📊 Bilingual Coverage Impact

PR Changes

  • Paired EN/ES files modified: 4
  • New EN files needing ES translation: 0

Repository Coverage

Metric Value
Total EN files 532
Total ES files 502
Paired files 0
Coverage 0%

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>
@beyondnetPeru

Copy link
Copy Markdown
Contributor Author

Aviso desde otra sesión — solo reporto, no toco nada de este PR.

Governance guards (GT-578) está rojo, y la causa es el guard que este mismo PR introduce:

✗ 67-validate-declared-exports: 4 package(s) declare targets their own tarball does not carry:

  • @beyondnet/evolith-cli@1.3.2   (src/sdk/cli)          MISSING (main/bin) -> dist/main.js
  • @beyondnet/evolith-core@1.2.0  (src/packages/core)    MISSING . -> dist/index.d.ts
                                                          MISSING . -> dist/index.js
                                                          MISSING (main/bin) -> dist/index.js
  • @beyondnet/evolith-mcp@1.3.3   (src/packages/mcp-server)  MISSING (main/bin) -> dist/main.js
  • @beyondnet/evolith-sdk@2.0.0   (src/packages/sdk-client)  MISSING (main/bin) -> dist/index.js

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. @beyondnet/evolith-repo-facts pasa con 4 targets contra 25 ficheros empaquetados, así que el guard sí distingue.

Dos precisiones por si ayudan al triaje:

  • El job terminó; lo que sigue en curso es el workflow run completo (otros jobs). Esa distinción me confundió un momento al mirarlo, así que la dejo dicha.
  • Governance guards no es un check requerido en develop, así que esto sale como UNSTABLE/BLOCKED según lo demás, no bloquea por sí solo — pero es un hallazgo real, no ruido de CI.

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.

beyondnetPeru and others added 3 commits August 16, 2026 22:50
…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>
@beyondnetPeru
beyondnetPeru merged commit 631ee81 into develop Aug 17, 2026
36 checks passed
@beyondnetPeru
beyondnetPeru deleted the fix/gt-706-assert-declared-exports branch August 17, 2026 05:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant