Skip to content

Merge main into develop — resolve the 17-file divergence, unblocking #284 - #302

Merged
beyondnetPeru merged 46 commits into
developfrom
integration/develop-to-main-2026-07-30
Jul 30, 2026
Merged

Merge main into develop — resolve the 17-file divergence, unblocking #284#302
beyondnetPeru merged 46 commits into
developfrom
integration/develop-to-main-2026-07-30

Conversation

@beyondnetPeru

Copy link
Copy Markdown
Contributor

main was 45 commits ahead, develop 30, and 17 files conflicted. This merges main into develop so #284 (developmain) becomes clean. Every resolution was measured or derived, not chosen by preference.

Closure registry — chosen by measurement

91 records differed, because both branches independently migrated the recorded commands onto real paths — the duplication GT-639 records, again. Rather than argue, I built both candidate unions and ran the guard against each with the same parser:

kept dead references on a clean checkout
develop's migration 19
main's migration 22

develop's is kept. Union first, so all 582 records survive — the 3 only develop had (GT-633, GT-636, GT-637) and the 5 only main had (GT-624, GT-634, GT-635, GT-638, GT-640), verified present by id rather than assumed.

Guard 41 — the more careful parser

Both sides fixed the same two parser defects independently, with different helpers. Took main's firstOperand: it handles -- and the value of every value-taking flag, while develop's npmScriptOperand knew only --workspace, so npm run --prefix ./x build would have read ./x as the script name. develop's now-unused helper is deleted rather than left as a second parser for one job.

Ratchet budget — 19

The number this tree actually counts, not either side's (21 on develop, 38 on main). Both were measured against their own corpus and their own parser; neither describes the merged one.

The rest

  • Standards (GT-640, was GT-633): main's files wholesale — they carry the one-renderer reconciliation, the renumber and the sticky-date fix develop's copy lacked. The two derived JSONs regenerated on the merged corpus, not merged: 389 rules, native-handler 154.
  • Board + catalog: union of rows and entries (eight new ids across both sides), counters recomputed from the merged table — 600 / 640 done, 14 in progress, 22 pending, 4 deferred.
  • Workflows: union of steps. docs.yml took main's telling of the GT-598 comment; ci-cd.yml took develop's ratchet history, which owns the GT-637 fixes.
  • Maturity reports: regenerated in GT-630 order.
  • CLI e2e: kept develop's GT-636 comment over identical code.

Verified on the merged tree

core-domain jest 1455 passed / 129 suites · tsc --noEmit clean · mcp-server green · guard 41 self-tests 45/45 · guard 49 13/13 · iso-5055-mapping 9/9 · 43-validate-guard-negative-fixtures 38/38 observed red · 46 chain current and at a fixed point · 01/04/08/49 green · 41 within its new budget.

Evolith Core Quality Gates

  • Bilingual Parity: board and catalog unioned in EN and ES together.
  • Validation Scripts: 01-validate-docs.mjs green.
  • Bilingual Validation: 04-check-bilingual-parity.mjs green.
  • Agnosticism: no new dependency.

Linked ADRs / Issues

Conventional Commits

  • Merge commit with a stated resolution per file.

🤖 Generated with Claude Code

beyondnetPeru and others added 30 commits July 29, 2026 14:00
fix: three P0s, and twelve rules that promised enforcement and delivered nothing (GT-572, GT-595, GT-604)
…the exit taxonomy (GT-580)

Closes the two open criteria of GT-580.

CRITERION 2 — `--format json` writes DATA ONLY to stdout.
Output formatting and process exits were ALREADY centralized: `installMachineChannelGuard`
(main.ts, armed before the command graph boots) reroutes every non-JSON stdout write to
stderr while a machine format is active, and `exit-codes.ts` + `BaseEvolithCommand.handleError`
map a thrown error onto the taxonomy in one place. What was missing was proof that the
centralization actually holds across the CLI: the existing subprocess spec pipes two
hand-picked commands. `machine-channel.registry-sweep.spec.ts` asks commander — the CLI's
own registry, built from the real AppModule — which commands declare `--format`, and sweeps
all 32 in real subprocesses with stdout and stderr on separate pipes. Each must hand back
exactly one parseable JSON document and exit from the taxonomy. The enumeration has a floor,
so a sweep over nothing fails instead of passing vacuously.

Observed red: commenting out `installMachineChannelGuard()` turns `agents` (a clack banner)
and `init-wizard` (a progress line) DIRTY while the other thirty stay clean.

CRITERION 3 — a ruleset with Rego parity fails a command that exits outside the taxonomy.
  - `src/rulesets/cli/exit-code-taxonomy.rules.json` — CLI-EXIT-01 (no exit outside
    {0,1,2,3}), CLI-EXIT-02 (the scan must be non-vacuous), CLI-EXIT-03 (the taxonomy may
    not be widened to absorb an offender — otherwise CLI-EXIT-01 has a one-line escape).
  - `src/rulesets/opa/cli-exit-code-taxonomy.rego` + tests — the same three ids decided over
    the fact document the CLI's own producer emits. Aggregated into `main.rego`, and scoped
    to `input.core.cli` so a satellite evaluation stays silent.
  - `CliExitTaxonomyRuleHandler` — the native half, so `evolith validate` evaluates the rules
    instead of reporting three blocking rules that never ran.

Observed red, end to end: planting `process.exit(7)` in a real CLI command file makes
`evolith validate --core .` emit a BLOCKING CLI-EXIT-01 naming the file, and the CLI exits 2
(BLOCKED). The same file drives the committed producer and the committed .rego to the same
verdict in `exit-code-taxonomy-ruleset.spec.ts`.

Derived artifacts moved with the corpus: native-handler 151 -> 154, ISO/IEC 5055 mapping and
the inventory summary regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GT-580's exit-code taxonomy guards catch a status outside {0,1,2,3}, but
three commands stayed inside the taxonomy while contradicting their own
--format json envelope: gate-status exited 0 on an INTERNAL_ERROR envelope,
alias exited 1 (tool failure) instead of 3 (invalid input) on a usage
error, and init-wizard's catch-all collapsed a NonInteractiveError to
INTERNAL_ERROR/1 instead of preserving its VALIDATION_FAILED/3
classification. The registry sweep spec now asserts envelope.success
against status === 0 for every --format json command, which also caught
sdlc generate exiting 0 with success:false — its JSON branch returned
before reaching the process.exitCode assignment below it.
The registry sweep and Rego-parity ruleset landed in PR #278 close the
stdout/stderr-purity half of criterion 2 (machine-channel.registry-sweep.spec.ts,
32/32 --format json commands clean) and criterion 3 (exit-code-taxonomy.rules.json
+ cli-exit-code-taxonomy.rego, opa test 7/7) — the taxonomy is now governed
rather than merely scanned. Records the four envelope/exit-code mismatches the
strengthened sweep found and fixed along the way. --format ndjson streaming and
the Tracker cross-repo follow-up remain open, so status stays IN-PROGRESS.
09-reconcile-maturity.mjs --check failed CI's Validate documentation job:
rulesetCount was stale at 174, one behind the exit-code-taxonomy.rules.json
that PR #278 added. Regenerated via 09-reconcile-maturity.mjs.
…match it then found

CI's required Test job turned up two things the strengthened sweep assertion
missed or got wrong:

1. The assertion itself was too strict. `validate`/`evaluate` legitimately pair
   `createSuccessEnvelope` with exit 2 (BLOCKED) — the tool succeeded and
   produced a verdict, and the verdict happens to block. success:true does not
   imply exit 0; it only rules out TOOL_FAILURE/INVALID_INPUT, which success
   itself denies happened. Rewrote the assertion: success:true allows OK or
   BLOCKED, success:false must never be OK and, where the envelope names an
   ADR-0073 error.code, must match exitCodeForErrorCode's mapping for that
   code exactly — the same function BaseEvolithCommand.handleError already
   uses, so the sweep checks commands against the taxonomy's own source of
   truth instead of a second copy of it.

2. With that corrected, the sweep found a fifth real mismatch:
   `topology phase-artifacts` with no --phase emits an INVALID_PHASE envelope
   but hardcoded exit 1 (tool failure) instead of the 3 the taxonomy assigns
   that code. Fixed the same way as the other four: exitCodeForErrorCode
   instead of a literal. Updated the command's own unit spec, which had
   asserted the wrong exit code.

Verified: full CLI suite 101 suites/1446 tests, tsc --noEmit clean.
…berg-90c1b4

GT-580: routes 5 exit-code/envelope taxonomy mismatches through the existing exitCodeForErrorCode/resolveExitCode/carriesCliExitCode helpers (sdlc gate-status, alias, init-wizard, sdlc generate, topology phase-artifacts), strengthens the registry sweep to assert envelope/exit-code agreement for all 32 --format json commands, and closes GT-580 criteria 2+3 (JSON-channel purity + Rego-governed taxonomy). All 7 required checks green.
The test passed --architecture and --arch-level, neither of which exist
on the validate command (real flags are -a/--arch and -t/--topology).
Commander rejected the unknown option before any command code ran,
exiting 1 instead of the [0,2] pass/blocked verdict the test expected.
…e snapshot it guards

`native-evaluability-snapshot.json` declared in its own header that it was a
capture. Nothing captured it — there was no capture script in the repository. It
was hand-maintained, and it drifted: it said `documentation-only: 129` while Core
pinned 136, and it kept calling rules `unimplemented-native` after they got
handlers.

The guard was blind to that by construction. `iso-5055-mapping.test.mjs` compared
the snapshot's counts against six numbers typed into the test — the same six that
are literals inside the snapshot. Expected value and actual value were copies of
each other, so it passed for as long as the file went untouched, whatever Core
actually pinned. Its comment claimed it read `rule-corpus-triage.spec.ts`; it
never opened that file.

Drift does not stay contained. `build-iso-5055-mapping.mjs` stamps
`nativeEvaluability` onto every row of the mapping FROM the snapshot, so a stale
class is laundered into a 391-row artifact and overstates the handler backlog.

Three checks replace the six literals, each in the job that can afford it:

  - `rule-corpus-triage.spec.ts` (core-domain jest, required on main) asserts the
    committed snapshot against a FRESHLY COMPUTED triage — counts and the class of
    every rule, in both directions. The real guard: the only place with the
    dependencies to recompute the truth.
  - `iso-5055-mapping.test.mjs` runs in the documentation job, which has no
    node_modules, so it reads Core's counts OUT OF the spec that pins them.
    Parsing a source file is a real coupling, stated rather than hidden: a missing
    or reshaped literal THROWS, because failing to find the source of truth must
    never read as agreement with it. Plus: the snapshot's header must agree with
    its own body, and every class stamped into the mapping must match the snapshot.
  - `capture-native-evaluability-snapshot.mjs --check` is the end-to-end
    re-derivation.

The capture script is the missing half. It runs Core's REAL triage through
ts-node, resolved via `createRequire` from a package that DECLARES ts-node rather
than relying on workspace hoisting — otherwise a required check would be green
locally and red on a clean runner. Nothing is reimplemented: a second classifier
would be a second source of truth, which is the defect being removed. That
required moving the measurement out of the spec, where only jest could reach it,
into `test/rule-corpus-triage.ts` — that unreachability is why the snapshot was
maintained by hand.

It reads the committed file ONCE and keeps both views of that read. `--check`
decides whether what is on disk equals a fresh capture, so it must compare the
bytes it parsed; the first draft did `existsSync` then two separate reads, which
CodeQL correctly flagged, and which could report a diff it had not measured.

Order is written into both READMEs and into CI: recapture, THEN rebuild.

Rebased onto main, which moved three times while this was open — each time moving
the very numbers this branch pins (GT-595 and GT-632 closed twelve rules, GT-580
added three). Re-extracted against main's current spec, which now carries
`enforce` in the loader, then recaptured and rebuilt in order: 389 rules triaged,
`native-handler` 154, backlog 48; mapping 391 rows.

Also: the mapping guard ran in NO workflow. It does now. And `.gitignore` matched
`node_modules/` as a directory only, so the symlink a fresh worktree needs was
left untracked-but-visible and `git add -A` staged a link to an absolute path.

Verified: rule-corpus-triage 23/23; mapping guard 9/9; both `--check` modes
current; docs 01 and bilingual 04 green. Negative-tested both failure modes —
reverting a count goes red, and building the mapping from a stale snapshot goes
red on the stamp test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(standards): make the evaluability guard read Core, and capture the snapshot it guards
…a35ab

fix(cli): use real --arch flag in E2E architecture validation test
… actually live at

The dead-reference ratchet was at 309 against a budget of 305, so
`Governance guards (GT-578)` has been red on main. Five of those dead references
are one transcription slip repeated: the closure records for GT-424, GT-621,
GT-629, GT-630 and GT-632 recorded their self-test as

    node --test 34-check-skill-registry-parity.test.mjs

with no directory. Every one of those files exists at
`.harness/scripts/ci/<name>.test.mjs` — the `files` list in the same record names
the full path — so the evidence pointed at something real and quoted a command
that cannot run. That is exactly what the guard is for, and it is a repair rather
than a re-write: the command now names what was actually executed.

309 -> 304, which is under the existing budget, so the job goes green on the
repair alone.

The budget stays at 305 in this commit ON PURPOSE. The comment above that flag
records the rule and the burn behind it: it was once lowered to 303 from the
guard's computed basis, the runner then counted 305, and the job went red. Only a
count this job PRINTS may lower it. This branch's own run will print one; lowering
follows from that number, not from my machine's.

Verified: 41-validate-evidence-commands 304 dead / 0 failed; its self-test 34/34;
08-validate-tracking green over 574 closure records; the JSON still parses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nt this job printed

The five repaired closure records took the count to 304, and the budget stayed at
305 in that commit on purpose: the comment above the flag records that it was once
lowered from the guard's computed basis, the runner then disagreed, and the job
went red. The rule it draws is that only a count THIS JOB prints may lower it.

Run 30498743635 printed `304 dead (budget 305)`. That is the authorisation, so the
budget moves now and not before. My local count also said 304 — worth noting and
explicitly NOT the reason: the agreement is a coincidence of this checkout, and
the printed line is the evidence.

Leaving the slack would have meant one more dead reference could land silently,
which is the whole failure mode a ratchet exists to prevent.

Verified locally at the new setting: `--strict --max-dead 304` exits 0 with
`304 dead (budget 304)`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(evidence): qualify five recorded test commands with the path they actually live at
…his repo has

264 of the 304 dead references were one event: the `src/` refactor moved every
workspace, and the closure evidence kept quoting the pre-refactor layout. It is
the largest single block of dead evidence in the board and it had never been
migrated, so `41-validate-evidence-commands` has been carrying it as permanent
budget.

Rewritten only where the SAME artifact moved or was renamed, and only where the
new target was verified to exist in this checkout before a byte was written:

  • workspaces gained the `src/` prefix — sdk/cli, apps/core-api and the six
    packages (174 references);
  • the retired `@evolith/*` scope became `@beyondnet/evolith-*`, with
    `@evolith/core-api` becoming the bare `core-api` its package.json declares
    and `@evolith/smart-cli` becoming `@beyondnet/evolith-cli` (29);
  • `.harness/scripts/{validate-docs,check-bilingual-parity}.mjs` moved into
    `ci/` with numeric prefixes; `review-input.test.mjs` into `ci/agentic/` (41);
  • `reference/operations/` -> `product/operations/`,
    `reference/infrastructure/docker-compose.yml` -> `product/infra/`,
    `.bmad-core/agents/` -> `reference/core/foundations/agent-skills/`, and the
    gap catalog into `gaps/` (18);
  • source files quoted as targets gained the same `src/` prefix (19).

A further 27 read `npm run --workspace <path> <script>`, which npm accepts but
the guard parses by taking the token after `run` as the script name. Reordered to
the canonical `npm run <script> --workspace <path>`: same invocation, now
verifiable. The parser limitation is real and belongs in the guard — noted for
the .harness follow-up, since S-16 blocks editing it from here.

WHAT WAS DELIBERATELY LEFT DEAD (40)
A recorded command may be repaired when its artifact MOVED. It must not be
rewritten when the artifact was deleted, replaced, or absorbed — that would be
inventing evidence, which is worse than a red guard. So these stay:
  • 15 `.harness/bin/opa` — a vendored, gitignored binary; environment-sensitive,
    not a wrong path;
  • 4 `.harness/scripts/*check-orphan-bilingual*` — absorbed into
    `04-check-bilingual-parity`, not moved, so pointing them at 04 would claim a
    command that was never run;
  • 6 deleted artifacts (deploy/kubernetes/*, reference/infrastructure/helm/*);
  • 5 root-level `npm test` / `test:cov` recorded without the workspace they were
    run in, which cannot be inferred;
  • 2 `pnpm info` / `yarn info` the guard mis-parses as npm scripts — valid
    commands, second guard limitation;
  • 2 `packages/mcp-tools`, 1 `apps/agent-sandbox`, 1 `dist/main.js`, and 4 prose
    fragments the extractor reads as commands.

MEASURED, on this checkout, against origin/main:
  dead      304 -> 40
  executed   58 -> 83   (more commands resolve, so more actually run)
  failed     11 ->  6   (five were failing BECAUSE of the stale path)
The remaining 6 failures are untouched by this change and were failing before it:
the bilingual terminology lint (red on main independently), a cross-repo
`--consumer` path, root cleanliness in a worktree, a validator that crashes, and
one command with prose appended to it.

The budget stays at 304. Only a count THIS JOB prints may lower it — the rule the
comment in ci-cd.yml records from a burn. This branch's run will print one.

Verified: 08-validate-tracking green over 632 gaps and 574 closure records;
41-validate-evidence-commands self-test 34/34; the JSON parses and its top-level
shape is unchanged (asserted by the migration itself).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t this job printed

Run 30500947483 printed `40 dead (budget 304)`. That is the authorisation, so the
budget moves now and not in the commit that did the repair — the same two-step as
305 -> 304, and for the same reason: the comment above this flag records that the
budget was once set from a derived figure, the runner disagreed, and the job went
red. My local count also said 40. That agreement is a coincidence of this
checkout and is explicitly not the evidence; the printed line is.

Leaving 304 would have parked 264 references' worth of slack in a ratchet whose
entire job is to refuse slack.

Also recorded above the flag: what the remaining 40 ARE, so the next person does
not read a non-zero budget as unfinished migration. They are not path rot. 15
reference a vendored gitignored binary, 4 name a script absorbed into another
rather than moved, 6 name deleted artifacts, 5 are root-level `npm test` recorded
without their workspace, 2 are valid pnpm/yarn commands this guard mis-parses as
npm, and the rest are build output and prose the extractor reads as commands.
Two of those are guard limitations, and they are named as the next real move.

Verified locally at the new setting: `--strict --max-dead 40` exits 0 with
`40 dead (budget 40)`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(evidence): migrate the recorded closure commands onto the paths this repo has
…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>
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>
…er defects

Both defects made `41-validate-evidence-commands` report `dead` for a command
that runs perfectly well. That is the worst way for this guard to be wrong: a
false dead reads as stale evidence and invites someone to "repair" a record that
was already correct — and 27 records were reordered in the last change partly to
work around the first of these.

1. THE SCRIPT NAME WAS THE FLAG'S VALUE.
   `npm run --workspace src/sdk/cli test` is valid npm. The parser took the first
   non-dash token after `run`, which is the value of `--workspace`, and then
   reported that the script "src/sdk/cli" is not declared. Flags that consume the
   next token are now declared as data (`VALUE_TAKING_FLAGS`) and skipped, and the
   SUBCOMMAND is found the same way, so `npm --workspace w run test` — flag before
   the subcommand — parses too.

2. `pnpm`/`yarn` ARE NOT `npm`.
   `pnpm info <pkg>` queries the registry. The parser read `info` as a script name
   and failed because no `"info"` script is declared. Built-in subcommands of the
   three package managers are now declared as data.

The fix must not turn the check OFF, so a genuinely undeclared script in the
same flag-first shape is asserted to STILL be dead, and a bad workspace behind a
built-in subcommand is asserted to still be reported.

A FIXTURE THAT MADE TWO TESTS VACUOUS, FOUND BY CHECKING THE TESTS FAIL FIRST
The undeclared-script branch is guarded by `pkg?.scripts`, and the fixture root
declared none — so every assertion about an undeclared ROOT script was skipped
entirely and passed without exercising anything. Written against the old parser,
the six new tests failed 3 of 6. With `scripts` present in the fixture root they
fail 5 of 6, which is what they are worth. The sixth passes either way and is
kept as a regression guard, not claimed as a defect proof.

  old parser: 35 pass / 5 fail      new parser: 40 pass / 0 fail

Dead references 40 -> 38: only the two pnpm/yarn commands move today, because the
`--workspace` records were already reordered. Defect 1's value is preventive —
the canonical form is no longer the ONLY form this guard can read, so the next
record written the other way is not misreported as stale evidence.

Verified: self-test 40/40 and 5/6 red against the old parser;
42-validate-guard-denominators green (59 guards classified, 39/39 scanners refuse
a zero-element scan).

The budget stays at 40. Only a count this job prints may lower it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(deps)!: the CLI and MCP must resolve the SDK that carries the security wave, 1.2.2 (GT-634)
… this job printed

Run 30502009205 printed `38 dead (budget 40)`. Same two-step as 305 -> 304 and
304 -> 40, for the same reason recorded above the flag: only a count this job
prints may lower it.

The two references that stopped being dead are the `pnpm info` / `yarn info`
commands the guard used to mis-parse as npm scripts. They were always valid; the
guard was wrong about them, and the fix means the budget can drop without anyone
touching the evidence.

Worth recording what did NOT move: the flag-value defect recovered nothing today,
because those records had already been reordered to suit the broken parser. Its
value is preventive, and a budget that only ever moves on recovered references
would price that at zero.

Verified locally at the new setting: `--strict --max-dead 38` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(guard): stop the evidence guard blaming the evidence for two parser defects
…ing GT-624

GT-624's first criterion (deprecate 1.1.0) was the small half. The wave of
2026-07-23 sat unpublished until 2026-07-27 while SECURITY.md declared the 1.1.x
line "actively patched" — four days, on a public registry, with the CHANGELOG
naming the vulnerable files. Nothing detected it; an audit did. This is the gate.

`48-validate-security-publish-lag` asks the REGISTRY what is published, finds the
commit where each package's published version was SET, and fails on any later
commit whose type or scope marks it as security.

THE REGISTRY, NOT A TAG, and that is measured rather than assumed: the newest `v*`
tag here is `v1.1.0` while npm serves `@beyondnet/evolith-cli@1.2.2`. Three
releases shipped untagged, so a git-tag oracle would have reported a four-release
lag that does not exist and flagged everything since 1.1.0.

TWO FALSE NEGATIVES, found while building it, each pinned by a fixture:

  1. `git log -S'"version": "<published>"'` matches the commit that DELETED the
     string as well as the one that added it. On a just-bumped package the
     boundary landed on the bump itself, skipping the window.
  2. Taking the NEWEST commit at the published version let a later manifest edit
     that keeps the version — a dependency bump — push the boundary forward and
     hide everything before it.

The boundary is now the commit where the version was SET. Denominator: 47 commits
examined across 8 packages, against 31 for the first implementation — 16 commits
the first version would have skipped in silence.

The marker is STRUCTURAL — type `security(...)`, or scope `security`/`sec` — and
refuses prose deliberately. `fix(deps)!: … the SDK that carries the security wave`
is a dependency fix; a gate that fires on any subject mentioning security gets
switched off. A unit test pins that exact subject as NOT a marker. The guard also
reports the `security(...)` type separately, because per GT-623 it is not a
Conventional Commits type and release-please derives no bump from it — a commit
that announces itself as security and cannot move a version is GT-570's failure
mode by another route.

Criterion (c) in this repository's strong form: `43-validate-guard-negative-
fixtures` OBSERVED it refuse the empty fixture, 37/37 (was 36). Registered in
`guard-classification.mjs`, so `42` counts it — 60 guards classified, 40/40
scanners refuse a zero-element scan.

Also recorded on the board rather than tidied away: two packages were deliberately
NOT deprecated. `evolith-sdk@1.1.0` is what the published line actually RESOLVED
(GT-634, fixed in 1.2.2) and `evolith-contracts@1.1.0` is that package's only
version, so deprecating it offers no upgrade path.

Verified: 12/12 fixtures over real git repositories; guard green on this repo;
08-validate-tracking (632 gaps, 575 closure records), 41 at 40 dead against a
budget of 40, 04 bilingual parity, 01-validate-docs 1491 files, 46 chain at a
fixed point. Counters 592/632 -> 593/632, pending 22 -> 21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…fact chain

GT-598 left one thing unenforced. `build-iso-5055-mapping.mjs` stamps
`nativeEvaluability` onto every row of a 391-row artifact FROM
`native-evaluability-snapshot.json`, so rebuilding before recapturing launders a
stale classification into the mapping and overstates the handler backlog by
exactly the rules Core has closed since the last capture. Both artifacts pass
their own `--check` at that moment, which is precisely the failure that
one-artifact-at-a-time checking cannot see — the reason this guard exists.

Two links added, in order. The edge is real and enforced: swapping them is
rejected with "ISO/IEC 5055 corpus mapping consumes
native-evaluability-snapshot.json, which a LATER link writes."

The snapshot link is the same shape as the ABAC one — derived from the runtime
through ts-node, so its declared inputs are Core's triage sources rather than a
data file. The corpus half matters too: `documentation-only` moved 129 -> 136
purely because seven generated ADR rulesets appeared, and nothing noticed for as
long as the file was hand-maintained.

THE SELF-TEST HAD TO CHANGE, AND ONE CHANGE IS WORTH MORE THAN THIS COMMIT
Adding links broke six of its eight tests, and only one of those breakages was
about the new links: the fixture stubs every REAL link that is not under test, so
two links without stubs failed the shape check first. The rest were positions
typed as literals — `link 2 of 3`, `2. node 09-reconcile-maturity.mjs` — which
made an unrelated addition look like a regression in the reconciler.

Those positions are now DERIVED from CHAIN by producer name, so the next link
added ahead of the reconciler does not break assertions that are not about it.
The fixture stubs are generated from one helper and carry a note to keep them in
step with CHAIN.

Also strengthened while here: the byte-identical test now covers the snapshot and
the mapping. The snapshot is the sharper case — its producer stamps `capturedOn`
and that date is deliberately STICKY when the classification has not changed.
Were it not, every replay would rewrite the date and the fixed-point pass would
report drift daily. That property now has a test instead of a comment.

Verified: chain guard green at 5 links, 7 artifacts, fixed point holds; self-test
8/8; swapping the pair fails as designed; 42-validate-guard-denominators green
over 59 guards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(guard): declare the capture -> mapping order in the derived-artifact chain
…sh-gate

feat(ci): fail when a security fix is committed and unpublished, closing GT-624
…d nothing else

Everything else that was on the `claude/happy-rubin-634587` branch has landed
from other sessions, so this carries only what the board is still missing. The
branch is abandoned rather than merged, and what superseded each piece is
recorded here so the history is not a mystery:

  - the capture script and the extracted triage -> PR #276
  - the two derived-artifact chain links          -> PR #287 (identical to the
    specification: same producer, checkArgs, the three `consumes` sources and
    both written artifacts, in the position after the ABAC link)
  - the SDK range and the 1.2.2 release           -> PR #285
  - GT-624's deprecations and its gate            -> PR #288

GT-633 (DONE) — the failure mode is the content, not the fix: a guard whose
expected value was a copy of its actual value, and a derived artifact that
launders a stale input into one five times its size. Recorded with how it
actually landed, because three sessions converged on it: #276 and #287 on main,
and a SECOND independent implementation on develop as #279 — an in-spec renderer
instead of a standalone script. Both recaptured the same numbers, which
corroborates the measurement; two generators for one artifact is this row's own
defect one level up, and reconciling them is the open blocker on develop -> main.
That reconciliation is deliberately NOT done here: choosing which generator
survives belongs to the sessions that wrote the two halves.

GT-634 (PENDING, and it stays that way) — 1.2.2 is on main and NOT published. A
range in the repository is not a range on the registry; until npm serves it, the
published `latest` of the CLI and the MCP still installs the 2026-07-18 SDK.

GT-635 (DONE) — closed the way the row asked, by someone else, within the hour.
`f34a10f1` migrated the recorded commands onto the paths this repository has and
`600dd7a9` moved the budget 304 -> 40 from the count that produced: 269 of the
309 dead references were REPAIRED, not exempted. The row's value was making a
permanently red job visible, not owning the repair, and the record says so.

Verified: 08-validate-tracking (635 gaps, 611/611 EN/ES sections, 577 closure
records), 41 at 38 dead on a clean checkout against a budget of 40 — these two
new closure records add none — 01-validate-docs (1491 files), 04 bilingual
parity, 46 chain current and at a fixed point. Counters 593/632 -> 595/635.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beyondnetPeru and others added 16 commits July 30, 2026 08:40
docs(gaps): register GT-633, GT-634 and GT-635 on main
…ctrine

`coverage.mjs` states the rule for guards: zero scanned is a failure, because a
check that scanned nothing did not run. Nothing states it for the guard's TEST,
and the failure has the same shape one level up — a test whose fixture never
reaches the branch under test passes, and reads as proof.

Observed on 2026-07-30 (GT-633) while fixing two parser defects in
`41-validate-evidence-commands`. Six tests were written for the fix; three of
them asserted that a command is NOT reported dead, and passed against the
UNFIXED parser too, because the branch that reports it dead is guarded by
`pkg?.scripts` and the fixture's root package.json declared no `scripts`. The
assertions were true, the fix was real, and those tests proved nothing about it.

The rule now written down next to its sibling: when a test is written to pin a
fix, RUN IT AGAINST THE UNFIXED CODE AND WATCH IT FAIL. A test that passes before
the fix is the vacuous pass this file already describes, wearing a green tick
from the test runner instead of from a guard.

Its other half is recorded too, because it is the part that gets skipped: making
the fixture live took those six from 3-of-6 red to 5-of-6 red, and the sixth
passes either way. That one is kept and LABELLED a regression guard rather than
counted as proof — when a test cannot be made to fail, say so in the test instead
of letting the count imply coverage it does not have.

This is doctrine for whoever writes the next guard, which is why it lives here
rather than in the GT-633 board entry: a gap row is read once, this file is read
by everyone who reaches for `assertScanned`.

Verified: coverage.test.mjs 12/12; the module still loads and exports
ZeroCoverageError, assertScanned, assertScannedPerSource, scanned;
42-validate-guard-denominators green over 60 guards; 01-validate-docs green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… framing was wrong

1.2.2 shipped from main with provenance (run 30548042781). Verified against the
REGISTRY rather than the workflow's tick: `npm view` returns 1.2.2 for both
packages, both declare `@beyondnet/evolith-sdk: ^2.0.0`, both carry
dist.attestations. The check that matters: installing @beyondnet/evolith-cli@latest
into a directory that has never seen this workspace resolves sdk@2.0.0 and the
binary boots. Before this release the same install pulled the 2026-07-18 build.

CRITERION 2 REFUTED THE ROW'S OWN PREMISE, which is the finding worth keeping.
"Verify the wave's fixes are present rather than assumed" was written to stop a
version number standing in for evidence. Doing it: NONE of the seven security-wave
commits (1b077ab, 3a39dd0, 60bfc63, 3f31fa6, 90eacdb, 4f29e11, 8ad4e2d)
touches src/packages/sdk-client — zero files each, checked one by one — and the
wave's release commit 21205d2 names the three packages it affected: evolith-mcp
(15 files), evolith-cli (7), evolith-agent-runtime (1). The SDK was republished
alongside them, not fixed.

So sdk@1.1.0 predates the wave chronologically and carries none of it. There was
no security exposure through this dependency. What WAS real, and what the row
should have said: a caret range under 1.x cannot cross a major, so both consumers
were pinned a MAJOR behind on a contract dependency, and the lockfile nested the
registry tarball inside each consumer where it shadowed the workspace link — the
GT-625 class. The hedge written in when the row was opened (it measured dates and
resolution, not contents) is what made the correction cheap instead of embarrassing.

AND A CORRECTION TO MY OWN EARLIER WORK, made rather than left standing: the
GT-624 deprecations of evolith-core-domain, evolith-core and evolith-infra-providers
at 1.1.0 claimed "Security fixes landed in 1.2.0". The same evidence refutes it.
All three registry messages were rewritten to say those versions were republished
alongside the wave, not fixed by it. A false security claim on a public registry is
worse than no claim.

Criterion 3: sdk@1.1.0 deprecated, with the real reason (`.passed` -> `verdict`)
and an explicit "not a security advisory". Verified after `npm cache clean --force`
— the first read-back served the previous message from cache.

Left open on purpose and recorded: cli@1.2.1 and mcp@1.2.1 are not deprecated
(stale contract, not exposure, and one patch old is an owner's call), and
contracts@1.1.0 still has no successor to point a deprecation at.

Verified: 08-validate-tracking (635 gaps, 611/611 EN/ES, 578 closure records), 41
at 38 dead on a clean checkout against a budget of 40, 01-validate-docs (1491
files), 04 bilingual parity, 46 chain at a fixed point, 48-validate-security-
publish-lag green. Counters 595/635 -> 596/635.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(gaps): close GT-634 — 1.2.2 is published, and the row's security framing was wrong
…doctrine

docs(harness): record the fixture-vacuity case in the anti-vacuous doctrine
…ky-date bug it exposed

Converges main onto the GT-633 reconciliation already on develop (#294), so the
standards half of develop -> main stops being a design conflict: one renderer in
`test/rule-corpus-triage.ts`, two callers — the spec pins it byte-for-byte, the
capture script writes it and `--check`s it for the GT-630 chain and docs.yml.

AND IT EXPOSED A REAL BUG IN THAT RECONCILIATION, which is why this is not a
copy-paste. The driver decided sticky-vs-fresh `capturedOn` by comparing
`JSON.stringify(previous.counts)` against a counts object rebuilt in
KNOWN_CLASSES order, while the renderer emits them in CLASS_ORDER — a different
order for the same six numbers. So "changed" was ALWAYS true and the date was
rewritten on every run.

That would have failed the GT-630 fixed-point replay on any day after a capture.
It passed on develop only because both runs happened on the same date and stamped
the same value — the precise shape of green this chain exists to distrust. It
surfaced here because main's committed snapshot carries 2026-07-29: the first run
rewrote it to 2026-07-30, the second run rewrote it back.

The comparison is now order-insensitive, and observed: with the classification
unchanged, `capturedOn` stays 2026-07-29 across runs and the replay is
byte-identical.

NOTE: develop still carries the buggy comparison. It is invisible there today and
will bite tomorrow; it needs the same one-function fix, which is a follow-up
against develop rather than something to smuggle into a main-targeted change.

Numbers on this base are main's own, measured rather than copied: native-handler
154 (develop computes 151, having fewer handler closures landed), corpus 389,
mapping 391 rows.

Verified: capture --check faithful, replay byte-identical, build --check up to date
(391 rules, 37 mapped), iso-5055-mapping guard 9/9, chain 5 links current and at a
fixed point, core-domain jest 1446 passed / 128 suites, tsc --noEmit clean,
01/04/08 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-on-main

fix(standards): bring the one-renderer design to main, and fix a sticky-date bug it exposed
A gap id is chosen by reading the highest `GT-*` on whichever branch you are on,
and nothing contrasts that with any other branch. Two parallel sessions therefore
allocate the same number and one finds out at merge time.

IT ALREADY HAPPENED, which is why this is a row and not a note. `8449af3d` on
develop reads "renumber the ratchet fix GT-634 -> GT-637, ID collision with
develop": that row and GT-634 on main took the same id within hours, from sessions
that could not see each other.

The renumber is manual and LOSSY, and that costs more than the clash. An id lives
in the board row, the catalog anchor, the closure-evidence record, cross-references
from other rows in both languages, commit messages and PR bodies — and only the
first three are mechanically checkable. `08-validate-tracking` cannot help by
construction: it validates within one working tree, and branches are outside its
world.

Nor is it an isolated slip. In the 2026-07-30 develop -> main merge the same
convergent-duplication pattern appears three times: GT-633 was fixed twice by two
sessions, the evidence guard's parser was fixed twice, and this id was allocated
twice. A merge does not fix that.

THE ID FOR THIS ROW was allocated by taking the UNION of ids across main and
develop rather than the maximum on one — main was at 635, develop already had 636
and 637 — which is exactly the workaround the fix should make unnecessary. Doing
it any other way would have reproduced the defect while registering it.

Registered PENDING, P2/S, with criteria that ask for a check at PR time naming
both users of a colliding id, and a negative fixture OBSERVED red rather than
declared able to fail.

Verified: 08-validate-tracking (636 gaps, 612/612 EN/ES sections, 578 closure
records), 01-validate-docs (1491 files), 04 bilingual parity, 41 at 38 dead on a
clean checkout against a budget of 40, 46 chain current and at a fixed point.
Counters 596/635 -> 596/636, pending 21 -> 22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(gaps): register GT-638 — the board has no id allocator
…nothing shows work in flight

On 2026-07-30 the same work was done twice, THREE separate times, by sessions that
could not see each other:

  1. GT-633 was fixed twice — a standalone capture script on main (#276) and an
     independent in-spec renderer on develop (#279). Both correct, both recapturing
     the same numbers. Two generators for one artifact is GT-633's own defect one
     level up, so a THIRD session spent a full reconciliation (#294..#297)
     collapsing them into one renderer.
  2. The evidence guard's parser was fixed twice — `d1ea72a3` on main ("stop the
     evidence guard blaming the evidence for two parser defects") and `5acb29ed` on
     develop ("the ratchet was stuck by two bugs in itself").
  3. A GT id was allocated twice, forcing the renumber behind GT-638.

THE COST IS NOT THE MERGE CONFLICT. It is the duplicated work that happened before
anyone reached the conflict, plus the reconciliation it then required — and that
reconciliation introduced a defect of its own: an order-sensitive comparison that
rewrote the capture date on every run, invisible for a day because both runs
stamped the same date. Three duplications, one repair session, one new bug.

Why the board cannot see it: a row carries a status but not WHO is working it or
WHERE. Two sessions can both read `GT-633 · PENDING` and both start, correctly.
`08-validate-tracking` validates within one working tree by construction and
cannot know another branch exists.

AND THIS IS NOT A MISSING CONVENTION — the single-driver rule for gap waves is
already established practice. What is missing is any mechanism that makes it
observable, so on a busy day it degrades silently and nobody learns until the
merge. That is why the criteria ask for a claim DERIVED from open pull requests
rather than a rule written down again.

Distinct from GT-638, and the row says so: that one is allocating a NAME twice,
this one is doing the WORK twice. Same coordination gap; the id collision is its
cheapest symptom.

Id allocated by union across main and develop, per GT-638.

Verified: 08-validate-tracking, 01-validate-docs (1491 files), 04 bilingual
parity, 41 at 38 dead on a clean checkout against a budget of 40, 46 chain current
and at a fixed point. Counters 596/636 -> 596/637, pending 22 -> 23.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(gaps): register GT-639 — the same gap gets fixed twice, because nothing shows work in flight
A gap id is chosen by reading the highest `GT-*` on whichever branch you are on,
and nothing contrasts that with any other branch. `8449af3d` had to renumber
GT-634 -> GT-637 for exactly this. `08-validate-tracking` cannot help: it
validates within ONE working tree by construction, and branches are outside its
world. This guard is the part that needs two.

`49-validate-gap-id-allocation` compares every id's **Title:** on HEAD against the
title the same id carries on the base branch:

  absent from base        -> newly allocated, fine
  present, same title     -> the same gap, edited; fine
  present, DIFFERENT title -> one number, two gaps

THE TITLE IS THE DISCRIMINATOR ON PURPOSE. "The id exists on base" is the normal
case for 515 rows and says nothing; what makes it a defect is the id naming a
different gap on each side.

IT FOUND A SECOND COLLISION ON ITS FIRST REAL RUN, which nobody knew about.
Against origin/develop it reports GT-633: there it is "`evolith init` scaffolds a
repository that cannot pass its own governance on the first run" (c8270e4,
2026-07-29); on main it is the tautological-guard row (c322127, 2026-07-30). Two
gaps, one number, and the merge had not surfaced it. By the guard's own advice the
NEWER row is the one to renumber — which is mine.

It refuses to guess which side is wrong. A retitle and a collision are
indistinguishable from outside, so it names both titles and both refs and says in
the failure that it should not guess.

Wired into `Governance guards (GT-578)` with an explicit `git fetch` of the base:
a shallow CI checkout does not have it, and a guard that failed for THAT reason
would teach people to ignore it.

Anti-vacuous: a missing catalog, an unreadable base ref, and a parse yielding zero
ids on either side are all hard failures. "Unable to answer" is not "nothing to
report" — a quiet skip is how the original collision reached main.

WHAT IT STILL CANNOT SEE, said out loud rather than implied: an id allocated on a
branch with no open PR. It compares HEAD against ONE base, so two feature branches
that both take GT-640 stay invisible to each other until the first merges. The
`--verbose` output warns on every newly allocated id, and closing that remainder
needs the open-PR claim set tracked in GT-639.

Verified: 13/13 fixtures over real git repositories (not a mock of `git show`);
43-validate-guard-negative-fixtures 38/38 observed red, up from 37;
42-validate-guard-denominators 61 guards classified; 08-validate-tracking (637
gaps, 579 closure records), 41 at 38 dead against a budget of 40, 01-validate-docs
(1491 files), 04 bilingual parity, 46 chain at a fixed point. Counters 596/637 ->
597/637.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…uard

feat(ci): one gap id, one gap — the allocator guard, closing GT-638
`49-validate-gap-id-allocation` reported on its first real run that `GT-633` names
two different gaps: on develop it is "`evolith init` scaffolds a repository that
cannot pass its own governance on the first run" (`c8270e48`, 2026-07-29), on main
it is the tautological-guard row (`c3221276`, 2026-07-30). Per the guard's own
advice the NEWER row moves, and that is mine.

THIS COMMIT IS THE COST GT-638 DESCRIBES, paid once so it is on the record. The id
had to be changed in:

  - the board row and its anchor, EN and ES
  - the catalog heading, EN and ES
  - the closure-evidence record
  - cross-references from GT-638 and GT-639, EN and ES, including the narrative
    that describes this very collision — updated to say how it was resolved rather
    than left describing a state that no longer exists
  - three source comments (`capture-native-evaluability-snapshot.mjs`,
    `test/rule-corpus-triage.ts`, `rule-corpus-triage.spec.ts`)

and it CANNOT be changed in the commit messages and PR bodies that already name
GT-633 — #294 through #300 among them. That is the lossy half, and it is why the
row asks for allocation to be right the first time rather than repairable.

Historical mentions are kept and marked ("registered as GT-633 at the time")
rather than rewritten, so the record still reads correctly against those commits.

VERIFIED THE COLLISION IS GONE, not assumed: `49-validate-gap-id-allocation
--base origin/develop` now reports 0 collisions and lists GT-640 among the newly
allocated ids, where before it named GT-633 on both sides. Also 08-validate-
tracking, 01-validate-docs (1491 files), 04 bilingual parity, core-domain
rule-corpus-triage 22/22, iso-5055-mapping guard 9/9, 46 chain at a fixed point.

NOT DONE HERE: develop carries the same three source comments, and after this
lands they will name GT-640 on main and GT-633 on develop — where GT-633 is the
scaffolding gap. Whoever resolves the develop -> main merge should take main's
version of those files; noted on #284.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs(gaps): renumber GT-633 → GT-640, the collision the new guard found
Unblocks #284. main was 45 commits ahead, develop 30, and 17 files conflicted.
Every resolution below was MEASURED or derived, not chosen by preference.

STANDARDS (GT-640, was GT-633) — took main's files wholesale: they carry the
one-renderer reconciliation, the GT-640 renumber and the sticky-date fix that
develop's copy still lacked. The two derived JSONs were REGENERATED on the merged
corpus rather than merged; the capture reports 389 rules / native-handler 154,
which is main's number because main is where those handler closures landed.

CLOSURE REGISTRY — 91 records differed, because BOTH branches independently
migrated the recorded commands onto real paths (the duplication GT-639 records,
again). Chosen by measurement rather than by argument: built both candidate
unions and ran the guard against each with the same parser.

    prefer develop's migration -> 19 dead on a clean checkout
    prefer main's migration    -> 22

develop's is kept. Union first, so all 582 records survive — the 3 only develop
had (GT-633, GT-636, GT-637) and the 5 only main had (GT-624, GT-634, GT-635,
GT-638, GT-640), verified present by id after the merge rather than assumed.

GUARD 41 — both sides fixed the same two parser defects independently, with
different helpers. Took main's `firstOperand`: it handles `--` and the VALUE of
every value-taking flag, while develop's `npmScriptOperand` knew only
`--workspace`, so `npm run --prefix ./x build` would have read `./x` as the script
name. develop's now-unused helper is deleted rather than left as a second parser
for one job.

RATCHET BUDGET — 19, the number this tree actually counts, not either side's (21
on develop, 38 on main). Both were measured against their own corpus and their
own parser; neither describes the merged one.

BOARD + CATALOG — union of rows and entries, eight new ids across both sides, and
the counters RECOMPUTED from the merged table: 600 / 640 done, 14 in progress, 22
pending, 4 deferred. Never merged as text.

WORKFLOWS — union of steps. docs.yml took main's telling of the GT-598 comment
(both sides added the same two steps); ci-cd.yml took develop's ratchet history,
which owns the GT-637 fixes.

MATURITY REPORTS — regenerated in GT-630 order, not merged.

CLI E2E — kept develop's GT-636 comment over identical code.

Verified on the merged tree: core-domain jest 1455 passed / 129 suites, tsc
--noEmit clean, mcp-server suite green, guard 41 self-tests 45/45, guard 49 13/13,
iso-5055-mapping 9/9, 43-validate-guard-negative-fixtures 38/38 observed red,
46 chain current and at a fixed point, 01/04/08/49 green, 41 within its new budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@beyondnetPeru
beyondnetPeru requested a review from a team as a code owner July 30, 2026 18:29
@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: 3
  • New EN files needing ES translation: 2

Repository Coverage

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

⚠️ Action required: 2 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
beyondnetPeru merged commit 983db90 into develop Jul 30, 2026
33 checks passed
@beyondnetPeru
beyondnetPeru deleted the integration/develop-to-main-2026-07-30 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