Skip to content

GT-641/GT-642: two runtime import cycles inside the Core itself, and nothing in CI can find the next one - #311

Open
beyondnetPeru wants to merge 12 commits into
developfrom
claude/sleepy-haibt-b3b1e3
Open

GT-641/GT-642: two runtime import cycles inside the Core itself, and nothing in CI can find the next one#311
beyondnetPeru wants to merge 12 commits into
developfrom
claude/sleepy-haibt-b3b1e3

Conversation

@beyondnetPeru

@beyondnetPeru beyondnetPeru commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Pull Request Summary

The structural fact base from #309, pointed at the repository that produced it, reported two runtime import cycles inside core-domain. Neither is type-only, so neither is erased at compile time, and nothing that runs in CI today reports either one.

Claim note: this PR claims GT-641, GT-642 and GT-643 only. The fact base is referenced as #309 rather than by its gap id, so 50-validate-gap-claim does not read this PR as a second claim on work that belongs to that branch.

Both cycles are the same defect twice: a module publishes a type AND constructs or re-exports the collaborator that consumes it, so the collaborator has to import back through it to name its own parameters.

  • application/services/index.ts owned InitProjectInput while re-exporting InitializeProjectUseCase
  • application/validators/phase-gate-validator.service.ts owned PhaseGateDefinition while constructing EvidenceValidator, BlockingCriteriaValidator and RulesetLoader

The reported two-node chains were the smaller half of the finding. Each sits inside a four-module strongly connected component — project-scaffolder.service and phase-transition.use-case in the first, evidence-validator and ruleset-loader in the second — so a fix confined to the two files named in the chain would have moved the cycle rather than removed it. The component field is what said so.

The fix is not import type. Erasing the back-edge would satisfy the extractor (it classifies type-only edges as erased-at-runtime) while leaving the layering inverted. The types move out instead, into use-case.types.ts and phase-gate-validator.types.ts; both former owners re-export them, so no import path changes anywhere — including @beyondnet/evolith-core-domain/application/services, from which the CLI and the MCP server import InitProjectInput, and the 15 sites that import the gate contract from the service.

Measured, before and after, over the same tree: 346 modules / 2 cycles → 348 modules / 0 cycles.

GT-642 is the open half, and it is blocked rather than pending

No guard ships with this PR and the board says so in its own row instead of leaving it implied. @beyondnet/evolith-repo-facts exists only on the branch of #309: a check on the default branch cannot run an extractor that is not on the default branch, and writing a second cycle detector beside the first would be the duplicated source of truth that branch exists to remove. Only core-domain has ever been measured — cli, mcp-server, core, agent-runtime and sdk-client have not, and the barrel-owns-the-type shape is a monorepo-wide idiom.

Two things kept in the record rather than smoothed over

  • The CLI suite was red on first run (6 suites). Environment, not this change: a fresh worktree has no built dist for @beyondnet/evolith-agent-runtime, so jest cannot resolve it. Green after a workspace-wide build.
  • nest build for mcp-server reports 3 TS2345 duplicate-identity errors on RulesetValidatorService. Reproduced on a reverted tree before attributing it — it is the worktree dual-resolution problem (some specifiers resolve to the main checkout's dist), pre-existing and untouched here.

GT-643 — found while closing this one, and fixed

The CLI e2e suite left three TRACKED files dirty. Tracing which spec did it led to a product defect, not a test defect: agents install --dry-run writes to disk. agents.command.ts:647 declares the flag; installAgent() never reads it and calls registry.installAgent(process.cwd(), ...) unconditionally at line 218. init, upgrade and adr all read theirs — agents is the only declaration in the CLI with no reader.

Proven by reproduction: the whole spec first, then -t "dry-run" alone against a clean tree; both leave rulesets/agents/agents-registry.json and the two files under rulesets/agents/test-value/ modified. test-value is what the prompt mock answers to every question, and 6bb43cfa versioned that agent into the repository own rulesets.

Fixed here. The dry branch never reaches the writer rather than writing through a no-op filesystem — a branch that cannot reach the writer cannot regress when the writer grows a new call — and reports what it would create via a new planInstall the writer itself uses. The menu path was the same defect one level up: showMenu() dropped the caller options, so evolith agents --dry-run with no subcommand wrote anyway.

The three committed artifacts are deleted; the registry held exactly one agent, test-value, so the whole file was test output. The e2e suite now runs in a temp directory and asserts an unchanged disk, with a contrast case proving a real install still writes — otherwise the dry-run assertion would also pass if install had simply broken. Per-test module and directory, because a shared instance leaks commander parsed options between runs and made the working fix look broken. did not crash was the old oracle; it passed for as long as the defect existed.

Survey recorded, including the commands that were already correct: init, upgrade, adr, scaffold, docs, fixtures, generate-domain all gate writes on the flag. agents was the only declaration with no reader.

GT-643 stays IN-PROGRESS, not DONE: the cross-surface tester still has no oracle for no-effect flags as a class, so the next one is found by hand as this one was.

Verification

Suite Result
core-domain 129 suites / 1455 tests, 1 suite + 2 tests skipped
cli unit 101 suites / 1447 tests
cli e2e 18 suites / 133 tests, and git status clean afterwards for the first time
mcp-server 53 suites / 433 passed, 4 skipped
npm run build tsc -b clean across the workspace

Guards run locally: 08-validate-tracking 642 gaps EN/ES, 618/618 catalog sections, 583 closure records · 49-validate-gap-id-allocation 520 ids, 0 collisions · 41-validate-evidence-commands 19 dead (budget 21) — the new commands add none · 01-validate-docs 1491 files · 04-check-bilingual-parity 1640 files · 46-validate-derived-artifact-order chain current and at a fixed point.

Known-red check, and whose it is

Governance guards is red on this PR for 50-validate-gap-claim, which reports 7 contested ids across the 12 open PRs. It is a repository-wide view, not a property of this branch: the same check is already red on #306 and #309 for the same reason, and the contested ids come from the integration PR #305 overlapping #306/#307/#310. Nothing here can turn it green — that needs whoever owns those overlaps to decide which PR keeps each id.

Evolith Core Quality Gates

  • Bilingual Parity: board rows and catalog entries added in both .md and .es.md.
  • Validation Scripts: node .harness/scripts/ci/01-validate-docs.mjs run locally.
  • Bilingual Validation: node .harness/scripts/ci/04-check-bilingual-parity.mjs run locally.
  • Agnosticism: no technology dependency introduced; the change is a module split inside the agnostic Core.

Linked ADRs / Issues

Conventional Commits

  • Commits follow Conventional Commits (fix(core):, chore(board):).

🤖 Generated with Claude Code

beyondnetPeru and others added 7 commits July 30, 2026 14:33
release: align main — the CLI and MCP resolve the SDK that carries the security wave, 1.2.2 (GT-634)
A board row carries a STATUS but not who is working it or where, so two sessions
can both read `PENDING` and both start, correctly. On 2026-07-30 that happened
three times in a day.

`50-validate-gap-claim` derives the claim set from OPEN PULL REQUESTS — every
`GT-*` in a PR's title, body or branch name — and fails when one id is claimed by
two of them, naming both with their branches and titles.

DERIVED, NEVER HAND-WRITTEN, and the row asked for that on purpose: a hand-written
claim goes stale in the direction that matters, because someone forgets to remove
it and the next session works around a claim nobody holds.

THREE CRITERIA OF FOUR, and the fourth is left open with its reason rather than
stretched to fit. A claim list committed to the board would be derived from live
GitHub state, so it would be stale the moment anyone opened a pull request — and
GT-630's chain exists precisely to insist derived artifacts reach a fixed point. A
perpetually-stale artifact inside that chain would be worse than none. Making the
claim discoverable from the board needs something the board can render without
committing, and that is not built. The row stays IN-PROGRESS.

What it cannot see is in its own failure text, not implied: a branch with no open
pull request claims nothing. Opening the PR early — draft is enough — is what
makes the claim visible, which turns a convention into something with a check
behind it.

Anti-vacuous: a pull-request query that cannot be answered is a hard failure, not
a quiet pass. A guard built because two sessions could not see each other must not
stay silent when it cannot look. Zero open PRs is reported in words, because it
looks identical to a broken query otherwise.

Verified: 10/10 fixtures; 42-validate-guard-denominators 62 guards classified;
43-validate-guard-negative-fixtures 39/39 observed red, up from 38. Run against
the real repository it reports 0 claims across 5 open PRs — checked rather than
assumed: all five are Dependabot bumps naming no gap, so the zero is real and not
an empty query. 08-validate-tracking (640 gaps), 01, 04, 41 within budget, 46
chain at a fixed point. Counters: 600/640, in-progress 14 -> 15, pending 22 -> 21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(ci): one gap, one claim — the claim guard, advancing GT-639
`.git/EVOLITH_*` is a DIRECTORY only in the primary checkout; inside a `git
worktree` it is a FILE holding a `gitdir:` pointer, so every write/read to
`.git/EVOLITH_*` was ENOTDIR. In pre-commit that made `set -e` abort the
commit outright; in pre-push the `rm -f` failed and TARGET stayed unread —
together the reason `--no-verify` had become habitual from a worktree.

`git rev-parse --git-dir` resolves both layouts; both hooks now go through
it via a shared `write_push_intent`/read path, per-worktree, so the push
intent belongs to the checkout that was committed in rather than the repo
as a whole.

Also in pre-push: `assert_checkoutable` refuses up front, before repo
optimization or board sync run, when the merge target branch (main/develop)
is already checked out in another worktree — `git checkout "$to"` would
otherwise fail mid-dispatch with the push half-done.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix(ci): pre-commit/pre-push hooks assumed .git is always a directory
…omain

The structural fact base added by GT-589 reports two cycles in the Core's own
sources, neither of them type-only and neither reported by anything in CI today:

  services/index.ts ⇄ use-cases/initialize-project.use-case.ts
  validators/blocking-criteria-validator.ts ⇄ validators/phase-gate-validator.service.ts

Both have the same shape — a module publishes a type AND constructs/re-exports
the collaborator that consumes it, so the collaborator has to import back
through it. The strongly connected components were wider than the two-node
chains: project-scaffolder.service and phase-transition.use-case sat in the
first, evidence-validator and ruleset-loader in the second.

The types move out to their own modules and every consumer imports them
directly:

  application/services/use-case.types.ts       InitProjectInput, InitProjectResult,
                                               PhaseTransitionResult
  application/validators/phase-gate-validator.types.ts
                                               PhaseGateDefinition and the rest of
                                               the gate contract

`services/index.ts` and `phase-gate-validator.service.ts` re-export them, so
every existing import path — including `@beyondnet/evolith-core-domain/
application/services` used by the CLI and the MCP server — is unchanged.

Verified by re-running the GT-589 extractor over src/packages/core-domain:
346 modules / 2 cycles before → 348 modules / 0 cycles after. core-domain
jest 129 suites / 1455 tests green, tsc build clean, CLI build clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…that cannot exist yet)

GT-641 — DONE — records the two runtime import cycles the GT-589 fact base
found inside core-domain and the fix: 346 modules / 2 cycles → 348 / 0,
measured with the same extractor over the same tree, before and after.

GT-642 — PENDING — is the guard. It is carried as its own row instead of as an
unchecked criterion inside a closed one: a DONE row with an open criterion
reads as finished to anyone who does not open the catalog. It is BLOCKED, not
merely pending — `@beyondnet/evolith-repo-facts` exists only on
`feat/gt-589-repo-facts` (PR #309), and a check on main cannot run an extractor
that is not on main. Writing a second cycle detector beside the first would be
the duplicated source of truth GT-589 exists to remove.

The closure record for GT-641 keeps two things a green tick would have hidden:
the CLI suite was red on first run for an environment reason (a fresh worktree
has no built dist for agent-runtime, so jest cannot resolve it), and the three
`nest build` errors in mcp-server were reproduced on a reverted tree before
being attributed — they are the worktree dual-resolution problem, not this work.

Guards: 08-validate-tracking 642 gaps EN/ES, 618/618 catalog sections, 583
closure records; 49-validate-gap-id-allocation 520 ids, 0 collisions, GT-641 and
GT-642 newly allocated; 41-validate-evidence-commands 19 dead (budget 21), so
the new commands add none; 01-validate-docs 1491 files; 04-check-bilingual-parity
1640 files.

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

Copy link
Copy Markdown

📊 Bilingual Coverage Impact

PR Changes

  • Paired EN/ES files modified: 7
  • New EN files needing ES translation: 2

Repository Coverage

Metric Value
Total EN files 525
Total ES files 499
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 and others added 2 commits July 30, 2026 21:59
…T-642 rows

`09-reconcile-maturity.mjs --check` was red on PR #311: the reconciliation is
derived from the gap board, so adding two rows makes it stale by construction.
Regenerated rather than hand-edited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second link of the same derived chain: 46-validate-derived-artifact-order stops
at the FIRST stale artifact, so the summary only surfaced once the maturity
reconciliation ahead of it was current. Chain now replays to a fixed point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@beyondnetPeru beyondnetPeru changed the title GT-641/GT-642: the fact base found two cycles in the Core itself, and nothing in CI can find the next one GT-641/GT-642: two runtime import cycles inside the Core itself, and nothing in CI can find the next one Jul 31, 2026
beyondnetPeru and others added 2 commits July 30, 2026 22:29
…disk

Found while closing GT-641: the CLI e2e suite left three TRACKED files dirty.
Tracing which spec did it led to the flag, not to the test.

`agents.command.ts:647` declares `-d, --dry-run` and `installAgent()` never
reads it — line 218 calls `registry.installAgent(process.cwd(), ...)`
unconditionally. `init` swaps in a DryRunFileSystem, `upgrade` returns before
applying, `adr` threads it through; `agents` is the only declaration in the CLI
with no reader.

Proven by reproduction rather than by reading the code: the whole spec was run
first, then `-t "dry-run"` alone against a clean tree, and both leave
`rulesets/agents/agents-registry.json` plus the two files under
`rulesets/agents/test-value/` modified. `test-value` is what the prompt mock
answers to every question, and 6bb43cf versioned that agent into the repo's own
rulesets — a test's output shipped as product data.

Registered as P1 and separately from the test-hygiene symptom on purpose:
pointing the spec at a temp directory would hide the defect and leave the flag
lying to users. A dry run that writes is worse than no dry run, and no oracle
catches it because the conformance tester compares what surfaces ANSWER, never
whether the disk changed.

Guards: 08-validate-tracking 643 gaps EN/ES, 619/619 catalog sections;
49-validate-gap-id-allocation 521 ids, 0 collisions; 46-validate-derived-artifact-order
current and at a fixed point after regenerating the reconciliation and summary;
01-validate-docs 1491 files; 04-check-bilingual-parity 1640 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The flag was declared at agents.command.ts:647, typed on the options interface,
documented — and never read. `installAgent()` called
`registry.installAgent(process.cwd(), ...)` unconditionally, so the flag a user
reaches for precisely when they do not want to be trusted with the real one
wrote to the working directory anyway, then reported success.

The dry branch now never reaches the writer at all, rather than writing through
a no-op filesystem: a branch that cannot reach the writer cannot regress when
the writer grows a new call. It reports what it would have created through a new
`AgentRegistryService.planInstall`, which the writer itself uses, so the report
cannot drift from the layout it describes.

The menu path was the same defect one level up: `showMenu()` dropped the
caller's options, so `evolith agents --dry-run` with no subcommand picked
install from the menu and wrote. Options are forwarded now.

Deleted the three artifacts a previous run committed —
`rulesets/agents/agents-registry.json` and both files under
`rulesets/agents/test-value/`. `test-value` is the string the prompt mock
answers to every question, and the registry held exactly that one agent: the
whole file was test output shipped as product data.

The e2e suite ran against the repository itself, which is why it was writing
into it. It now runs in a temp directory AND asserts an unchanged disk, with a
contrast case proving a real install still writes — otherwise the dry-run
assertion would also pass if install had simply broken. Per-test module and
directory, because a shared instance leaks commander's parsed options between
runs: `agents install` after `agents install --dry-run` still saw dryRun=true,
which made a working fix look broken.

`did not crash` was the old oracle. It passed for as long as the defect existed.

Survey of every other `--dry-run` in the CLI, recorded including the correct
ones: init (swaps in DryRunFileSystem), upgrade, adr, scaffold, docs, fixtures
and generate-domain all gate their writes on it. agents was the only
declaration with no reader.

GT-643 stays IN-PROGRESS: the cross-surface tester still has no oracle for
no-effect flags as a class, so the next one is found by hand as this one was.

Verified: cli unit 101 suites / 1447 tests; cli e2e 18 suites / 133 tests, and
`git status` is clean after the suite for the first time. 08-validate-tracking
643 gaps EN/ES, 619/619 catalog sections; 46-validate-derived-artifact-order at
a fixed point; 01-validate-docs, 04-check-bilingual-parity, 40-validate-path-literals green.

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

Deleting `src/sdk/cli/rulesets/agents/` turned two checks red, and neither
failure named its own cause. Both were leaning on the residue rather than on
anything authored.

`47-validate-joined-paths` declares `src/sdk/cli/rulesets/opa` a generated
subtree, and requires such a claim to be ANCHORED: the parent must exist in a
clean checkout, so a typo inside the prefix stays red. The parent is
`src/sdk/cli/rulesets` — gitignored in full (`rulesets/*`) — and it existed in
CI only because three files a test had written were committed inside it. With
them gone the anchor failed and the guard reported `policy.wasm` resolving to
nothing, which was true and was not the cause. Proven rather than argued: over a
clean checkout of HEAD~1 the path resolves true, over HEAD false. The prefix is
now the whole produced subtree, anchored on `src/sdk/cli`, which is authored.
Re-verified against a clean checkout: the artifact and its directory pass, while
`src/sdk/cli/rulesetss/...`, `sdk/cli/rulesets/...` and an unrelated missing
directory all stay red. Self-test 14/14.

`rulesets-resolver.bundled.spec.ts` had one case still reading the real tree.
Its own header already recorded that the bundled tree is a build artifact and
that its siblings were migrated to a synthetic one "minus the committed
rulesets/agents/" — that exception was the residue, and this case survived on
it: the EACCES it simulates only fires when the walk reaches some `…/rulesets`,
and after the deletion it reached none. It drives the synthetic tree now, and
the stale header sentence is corrected.

Verified under the condition CI actually has, not the local one: with
`src/sdk/cli/rulesets` parked out of the tree, the CLI suite is 101 suites /
1447 tests green. Guards: 47, 46 (chain at a fixed point), 40, 42 (62 guards
classified), 43 (39/39 observed red), 08 (643 gaps EN/ES, 619/619 sections),
01 (1491 files).

Editing .harness/ required stepping past the unimar-core S-16 hook, with the
owner's explicit approval — this repo's .harness has no upstream to propose to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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