Skip to content

fix(deps)!: the CLI and MCP must resolve the SDK that carries the security wave, 1.2.2 (GT-634) - #282

Merged
beyondnetPeru merged 4 commits into
developfrom
release/npm-1.2.2
Jul 29, 2026
Merged

fix(deps)!: the CLI and MCP must resolve the SDK that carries the security wave, 1.2.2 (GT-634)#282
beyondnetPeru merged 4 commits into
developfrom
release/npm-1.2.2

Conversation

@beyondnetPeru

Copy link
Copy Markdown
Contributor

Pull Request Summary

The published CLI and MCP install an SDK built before the security wave they claim to carry. Fixes it and prepares 1.2.2 of both.

@beyondnet/evolith-cli@1.2.1 and @beyondnet/evolith-mcp@1.2.1 both declare @beyondnet/evolith-sdk: ^1.1.0, which resolves to exactly 1.1.0 — the SDK publishes 1.0.0, 1.1.0, 2.0.0 and nothing in between, and a caret cannot cross a major. The dates are the finding:

artifact published
sdk@1.1.0 2026-07-18 five days before the wave
sdk@2.0.0 2026-07-27 shipped the wave (closed GT-570)
cli@1.2.1, mcp@1.2.1 2026-07-28 published after 2.0.0 existed, still on 1.x

So the exposure GT-570 declared closed "for anyone installing latest" was not closed for this dependency.

The lockfile made it concrete, not cosmetic

package-lock.json pinned the registry tarball of sdk@1.1.0, with integrity hashes, at src/sdk/cli/node_modules/@beyondnet/evolith-sdk and src/packages/mcp-server/node_modules/@beyondnet/evolith-sdk. A clean npm ci fetched the pre-wave SDK and nested it inside each consumer, where it shadows the workspace link: the top-level symlink to the local 2.0.0 is what a developer reads, and the nested 1.1.0 is what a fresh install resolves. Same class as GT-625 — the workspace hiding what a real install does — and the reason nothing caught it. Both entries disappear once the range is ^2.0.0.

Patch, and the reason is checked

Neither package re-exports the SDK and both use only EvolithRestClient, so 2.0.0's breaking change (payload types re-exported from core-domain, .passed.verdict, 'info' severity retired) reaches no public API of either. The ! in the commit marks the transitive major for anyone who reaches the SDK through these packages.

Also here, because it blocked the rehearsal

check-install-smoke was reporting the npm version rather than the artifact: --pack-destination is not honoured identically across npm majors when the cwd is a workspace package — npm 11 writes to the destination, npm 10 (the Node 20 runner) writes to the package directory, and both print the same filename. The first 1.2.2 dry run (30499155684) failed on that with a perfectly good tarball. It now checks both and moves a stray tarball out of the source tree.

Why this is separate from #277

That branch also carries the GT-633 standards work, which now needs reconciling against #279 — the same fix landed on develop independently, with matching numbers, but a second snapshot generator. None of that touches the six files here, and an irreversible publish should not wait on a design merge.

Verified — with an install, not a range

  • plan-npm-release.mjs2 of 8 WILL PUBLISH, in dependency order; the other 6 correctly reported as already on the registry
  • clean install of beyondnet-evolith-cli-1.2.2.tgz in a directory that has never seen this workspace resolves @beyondnet/evolith-sdk@2.0.0; same for beyondnet-evolith-mcp-1.2.2.tgz
  • check:install-smoke → 36 @beyondnet/* specifiers resolve, installed binary boots printing 1.2.2
  • check:release-drift → passed
  • CLI jest 1436/1436 (99 suites), mcp-server 433/433 (53 suites)
  • lockfile diff is two versions, two ranges and two nested removals — produced by npm install --package-lock-only on develop's lockfile, not hand-merged

Evolith Core Quality Gates

  • Bilingual Parity: no reference/ document changed; the changelogs are English-only by existing convention.
  • Validation Scripts: 01-validate-docs.mjs unaffected — no Markdown under reference/ touched.
  • Bilingual Validation: as above.
  • Agnosticism: no new technology dependency; this narrows an existing one.

Linked ADRs / Issues

Conventional Commits

  • Title and commit follow Conventional Commits.

🤖 Generated with Claude Code

…urity wave, 1.2.2 (GT-634)

Separated from PR #277 on purpose: that branch also carries the GT-633 standards
work, which now needs reconciling against #279 (the same fix landed on develop
independently). None of that touches these six files, and an irreversible publish
should not wait on a design merge.

THE DEFECT. `@beyondnet/evolith-cli@1.2.1` and `@beyondnet/evolith-mcp@1.2.1` both
declared `@beyondnet/evolith-sdk: ^1.1.0`, which resolves to EXACTLY 1.1.0: the
SDK publishes 1.0.0, 1.1.0 and 2.0.0 with nothing in between, and a caret cannot
cross a major. sdk@1.1.0 is from 2026-07-18; sdk@2.0.0 shipped the security wave
on 2026-07-27; both consumers were published on 2026-07-28, AFTER 2.0.0 existed,
still on the 1.x line. So `npm install @beyondnet/evolith-cli@latest` installed a
pre-wave SDK, and the exposure GT-570 declared closed for "anyone installing
latest" was not closed for this dependency.

The lockfile made it concrete rather than cosmetic: it pinned the registry tarball
of sdk@1.1.0, with integrity hashes, at src/sdk/cli/node_modules and
src/packages/mcp-server/node_modules. A clean `npm ci` fetched the pre-wave SDK and
nested it inside each consumer, where it SHADOWS the workspace link — the
top-level symlink to the local 2.0.0 is what a developer reads, the nested 1.1.0
is what a fresh install resolves. Same class as GT-625. Both entries disappear
once the range is ^2.0.0.

Patch, not minor, and the reason is checked rather than assumed: neither package
re-exports the SDK and both use only `EvolithRestClient`, so 2.0.0's breaking
change (payload types re-exported from core-domain, `.passed` -> `.verdict`,
`'info'` severity retired) reaches no public API of either.

ALSO HERE, because it blocked the rehearsal: check-install-smoke was reporting the
npm version rather than the artifact. `--pack-destination` is not honoured
identically across npm majors when the cwd is a workspace package — npm 11 writes
to the destination, npm 10 (the Node 20 runner) writes to the package directory,
and both print the same filename. The first 1.2.2 dry run (30499155684) failed on
that with a perfectly good tarball. It now looks in both and moves a stray tarball
out of the source tree.

VERIFIED ON THIS BRANCH, with an install rather than a range:
  - plan-npm-release -> 2 of 8 WILL PUBLISH, in dependency order
  - clean install of beyondnet-evolith-cli-1.2.2.tgz in a directory that has never
    seen this workspace resolves @beyondnet/evolith-sdk@2.0.0; same for
    beyondnet-evolith-mcp-1.2.2.tgz
  - check:install-smoke -> 36 specifiers resolve, binary boots printing 1.2.2
  - check:release-drift -> passed
  - CLI jest 1436/1436 (99 suites), mcp-server 433/433 (53 suites)
  - lockfile diff is two versions, two ranges, two nested removals; produced by
    `npm install --package-lock-only` on develop's lockfile, not hand-merged

The `!` marks the transitive major for consumers who reach the SDK through these
packages; neither package's own API changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@beyondnetPeru
beyondnetPeru requested a review from a team as a code owner July 29, 2026 23:36
@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

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

📊 Bilingual Coverage Impact

PR Changes

  • Paired EN/ES files modified: 3
  • New EN files needing ES translation: 1

Repository Coverage

Metric Value
Total EN files 524
Total ES files 498
Paired files 0
Coverage 0%

⚠️ Action required: 1 new EN file(s) added without ES counterparts.

To create skeletons:

node .harness/scripts/generate-es-skeleton.mjs <file.md>

Generated by GitHub Actions

beyondnetPeru and others added 3 commits July 29, 2026 18:42
Both were caught by required checks on PR #282, and both are the gates working.

`maturity-reconciliation.json` records `cliPackage: @beyondnet/evolith-cli@1.2.1`,
so the version bump made it stale and `Validate documentation` went red at
"Reconcile maturity evidence". Regenerated, then the executive summary re-checked
in that order — it was already current, since the field it reads did not move.

WORTH RECORDING, because it is a hole in a declaration this repository added
yesterday: `09-reconcile-maturity.mjs` reads `src/sdk/cli/package.json`, and the
GT-630 chain declares that link's `consumes` as the gap board plus the maturity
assessment ONLY. So the CLI manifest is an UNDECLARED INPUT to a derived artifact
— exactly the "generator whose output does not depend only on its declared
consumes" case the fixed-point pass exists to catch, and here it was caught by
staleness on a runner instead. The chain declaration lives on the branch of
PR #277; the `consumes` entry belongs there rather than in a release commit.

`evolith-machine-contracts.json` pins `producer.version` to the CLI's version, and
`10-validate-contract-conformance.mjs` fails when they diverge (GT-563 records that
this check was silently crashing until recently, so it is newly effective). Bumped
to 1.2.2. The contract itself is unchanged: `contractVersion` is untouched and all
four schema hashes still resolve.

Verified: 10-validate-contract-conformance -> 4 schemas hashed and resolved;
contracts jest 104/104; 01-validate-docs 1491 files; 08-validate-tracking;
46-validate-derived-artifact-order -> 3 links current and at a fixed point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rsion (GT-634)

Third and last version-pinned artifact the 1.2.2 bump moves.
`generate-product-inventory.mjs` stamps `@beyondnet/evolith-cli@<version>` into
`product/products/smart-cli/product-inventory.{md,es.md}`, and
`11-validate-product-docs.mjs` runs its `--check`, so `Validate documentation`
went red at "Validate product documentation synchronization". Regenerated: the
diff is the version and the counts the generator recomputes.

The staleness was reported with an EMPTY detail — `Product inventory is stale: `
— because the guard interpolates the child's stdout and the child writes its
reason to stderr. Not fixed here (it is not a release change) but worth naming:
a guard that says something is stale without saying how is a guard you have to
re-derive by hand.

WHAT I DELIBERATELY DID NOT COMMIT. `coverage-dashboard.mjs --check` also reports
drift locally, and regenerating it replaces MEASURED coverage — "88.09% statements
· 88.38% lines" — with "pending a coverage run", because this machine has no
coverage data. That is a regression dressed as a fix. The docs job on the runner
runs a build and the exploration suite before that check and passed green on
develop at 20:56Z, so the report is current where it is measured. Same for
`inventory-summary.{md,es.md}`, whose 167 -> 174 ruleset count belongs to whoever
landed #279, not to this release.

Verified with only this staged: 11-validate-product-docs OK (CLI 1.2.2, no
placeholders), 10-validate-contract-conformance OK (4 schemas), 09-reconcile
--check OK.

Honest note on method: I ran the docs job's fifteen validators in one local loop
to find every pin at once instead of one CI round each. Three of them reported
failure inside that loop and pass individually, including under the same output
redirection — so the loop, not the checks, is what was wrong. I have not explained
it, and the runner is the oracle for this job, not my loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sh, and said so wrongly

ROOT CAUSE, measured rather than guessed — and my two earlier explanations were
both WRONG, recorded here so they are not re-attempted.

`npm-release.yml`'s rehearsal mode runs `npm publish --dry-run`, which exports
`npm_config_dry_run=true` into the environment of `prepublishOnly`. This guard runs
there, and its nested `npm pack` and `npm install` INHERIT it: pack prints the
tarball name and writes no file, install exits 0 and installs nothing. So the guard
declared a perfectly good artifact "NOT installable", twice, in runs 30499155684
and 30499540347.

  npm_config_dry_run=true npm pack --pack-destination <dir>
    -> prints "beyondnet-evolith-cli-1.2.2.tgz", leaves <dir> EMPTY

Verified on npm 11 and npm 10. What I claimed before and was wrong about:
`--pack-destination` is NOT ignored on the runner, and npm majors do NOT disagree
about it — npm 10 honours it exactly like npm 11. I reproduced that with
`npx npm@10` instead of continuing to reason about it.

THE CONSEQUENCE IS BIGGER THAN THE BUG. The rehearsal mode has never exercised
this gate: under dry_run it always failed here, and the real publish of 2026-07-28
passed only because no dry-run flag was in the environment. The workflow's safety
model says "the default invocation exercises the whole path — resolve, build, pack,
verify — and publishes nothing", and for this step that was not true.

Packing and installing into a throwaway directory is the guard's MEASUREMENT, not
part of the publish being rehearsed, so `npm_config_dry_run` is stripped for every
child in the `run` helper rather than at one call site, and `--dry-run=false` is
passed explicitly to the pack.

Verified by reproducing the runner condition locally, which is what the two failed
attempts lacked: `npm_config_dry_run=true node scripts/check-install-smoke.mjs`
passes — 36 specifiers resolve, the installed binary boots printing 1.2.2 — and so
does the plain invocation. The tarball location is also discovered rather than
predicted now, and a failure prints what each candidate directory holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@beyondnetPeru
beyondnetPeru merged commit dd064a6 into develop Jul 29, 2026
29 of 33 checks passed
@beyondnetPeru
beyondnetPeru deleted the release/npm-1.2.2 branch July 30, 2026 19:55
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