Skip to content

GT-589: a structural fact base, so the depth of architecture stops ending where a grep ends - #309

Merged
beyondnetPeru merged 7 commits into
developfrom
feat/gt-589-repo-facts
Jul 31, 2026
Merged

GT-589: a structural fact base, so the depth of architecture stops ending where a grep ends#309
beyondnetPeru merged 7 commits into
developfrom
feat/gt-589-repo-facts

Conversation

@beyondnetPeru

Copy link
Copy Markdown
Contributor

Closes GT-589 (P1, complexity L). All three acceptance criteria met and observed.

The shape, and why it does not erode ADR-0101

New package src/packages/repo-facts owns everything side-effectful — filesystem walk, compiler program, clock, sha256. The canonical contract lives in the Core and is pure: core-domain/src/evaluation/contracts/repo-facts.ts, no fs, no crypto, no clock. EvaluationContext.repoFacts, ArchitectureContext.symbolBoundaries (policy on the context, facts beside it), ArchitectureEvaluationResult.structuralFacts; both JSON schemas updated.

The Core never calls the extractor. It receives a finished value inline — the identical seam qualitySignals/Evidence use (ADR-0111) and OverlayFileSystem uses for source files (ADR-0080). contentHash is opaque provenance the Core echoes and never recomputes, exactly as Provenance.artifactHash is treated. The end-to-end spec injects a drift service that throws if touched and passes satellitePath: '' — so the evaluator provably judges a repository it has never seen, from facts alone. All queries are pure; nothing is retained.

The indexer is the TypeScript compiler API — the same type-resolution engine scip-typescript drives — emitting SCIP-shaped symbol ids <module>#<name>.

Criterion 2: the question no current ruleset can express

Which forbidden symbol does a boundary reach, and through which exact chain?

"symbolChain": ["src/cli/checkout-command.ts#runCheckout",
                "src/application/place-order.ts#placeOrder",
                "src/infrastructure/db-pool.ts#connectionPool"],
"viaLegalImportsOnly": true

Proof it could not before: cli → application and application → infrastructure are each legal, and there is no cli → infrastructure import edge anywhere in the tree (asserted in the spec). Import checks — ESLint boundaries, dependency-cruiser through the enforcer seam — are file-to-file and pairwise, so no single edge is a violation and none of them can see the composition. A symbol is also not a file.

Cycle detection is deliberately not claimed as the criterion-2 question: policy-compiler.ts already compiles dependency-cruiser to.circular, so cycles are expressible today. findImportCycles shipped anyway (same graph, nearly free) because it names the concrete chain instead of asserting existence — and it found two genuine runtime cycles inside core-domain itself that nothing in CI reports. Both independently confirmed by the integrating session by reading the imports:

  • application/services/index.ts re-exports ../use-cases/initialize-project.use-case, which imports back from ../services/index.
  • validators/blocking-criteria-validator.tsvalidators/phase-gate-validator.service.ts import each other.

Those are a separate task, not fixed here.

Criterion 3: reproducibility, by diff rather than by argument

sha256:73c924ec1569a1027db517c0cee08028ceb87e82c7155b3b10b3870247220d4c
sha256:73c924ec1569a1027db517c0cee08028ceb87e82c7155b3b10b3870247220d4c
FACTS: byte-identical      VERDICT: byte-identical
a34994648cc2496f9dd78c07b0934c7fa9c3de305748e222ca1bb419f7b29d07  run1.result.json
a34994648cc2496f9dd78c07b0934c7fa9c3de305748e222ca1bb419f7b29d07  run2.result.json

canonicalizeRepoFacts excludes both the hash and provenance.extractedAt, so a timestamp cannot leak into a verdict.

Verification

suite result
npm run build (tsc -b, full solution) green
core-domain 131 suites, 1479 passed (baseline 1455)
repo-facts (new) 12 passed — re-run independently by the integrating session
agent-runtime 406 passed · mcp-server
CLI test:unit 1446 passed · core-api

Real-scale check: extracting this repo's core-domain/src yields 349 modules / 933 imports / 1572 symbols / 2693 references in ~1.6s.

Environment caveat, stated because it invalidates results silently: this worktree's node_modules originally resolved @beyondnet/* into the MAIN checkout. Every node_modules (root + 6 per-package, including the nested ones holding @nestjs/cache-manager) was rebuilt with worktree-local links, verified with readlink -f, and every number above is post-fix. dist/ is load-bearing — mcp-server/core-api jest resolve core-domain through its exports map — so npm run build ran before every measurement.

Deliberately left for later

  • SCIP payload ingestion and tree-sitter. The indexer is the TS compiler API, not the scip-typescript binary and not an index.scip reader. Both are second adapters behind the same output shape; documented as not-implemented in the package README.
  • TypeScript only. One language.
  • Two queries only. Duplication ratios, refactor:copy and dead abstraction (GT-594's signals) sit on this graph, untouched.
  • No surface wiring. No CLI/MCP/REST command extracts facts yet — the seam is the package plus the context field.
  • Boundary rules are per-request; there is no ruleset file format for symbolBoundaries yet.

🤖 Generated with Claude Code

beyondnetPeru and others added 6 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
The engine had no queryable structural facts, so the depth of `architecture`
ended where a grep ends: the OSS-enforcer seam returns a flat `Violation[]` per
tool run, which answers exactly the questions the tool was configured to ask and
no others. There was no module graph, no symbol graph, nothing an evaluator could
interrogate.

This adds `RepoFacts` — a content-hashed module graph + symbol graph — produced
OUTSIDE the Core and delivered INLINE as a deterministic member of the
`EvaluationContext`, the same shape ADR-0101/ADR-0080 already mandate for source
files and ADR-0111 for quality signals. It reinforces statelessness rather than
eroding it: the Core runs no indexer, opens no repository and retains nothing.

- `core-domain/evaluation/contracts/repo-facts.ts` — the pure contract plus its
  queries: `canonicalizeRepoFacts` (the deterministic form a hash names),
  `findImportCycles` (Tarjan SCC + a concrete chain per component) and
  `findSymbolBoundaryCrossings` (multi-source BFS over the symbol reference
  graph). No filesystem, no crypto, no clock.
- `EvaluationContext.repoFacts` + `ArchitectureContext.symbolBoundaries` (policy
  travels on the context, facts travel beside it), and
  `ArchitectureEvaluationResult.structuralFacts`, which echoes the extractor's
  `contentHash` so a verdict names the input it judged.
- `createArchitectureKindEvaluator` merges the drift report with the structural
  queries, and now evaluates with NO workspace at all when facts are supplied —
  the "judge a repository the Core has never seen" path.
- New package `@beyondnet/evolith-repo-facts`: the out-of-core extractor over the
  TypeScript compiler API (the engine `scip-typescript` itself drives), emitting
  SCIP-shaped symbol ids, plus a CLI and a `sha256` content hash.
- Contract Schema Registry updated for both new shapes.

The question the evaluator can now answer and could not before: *which forbidden
SYMBOL does a boundary reach, and through which chain* — when `cli → application`
and `application → infrastructure` are each legal, no single edge is a violation,
so no pairwise import check can see that the CLI reaches the connection pool.

Reproducibility is asserted, not argued: two extractions of the same tree produce
byte-identical facts and two evaluations produce byte-identical verdicts.

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:34
@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: 1
  • New EN files needing ES translation: 0

Repository Coverage

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

Good: All EN changes have ES counterparts.


Generated by GitHub Actions

…to 1.1.0

`Test contracts` was red on this PR: `evaluation-context` and `evaluation-result`
grew `symbolBoundaries` / `repoFacts` / `structuralFacts`, and both the published
`MACHINE_CONTRACT_SET` and the producer descriptor still pinned the pre-change
sha256. The pin doing its job — a schema cannot widen without the contract set
saying so.

Both changes are strictly ADDITIVE (the only removed line in the diff is a
trailing comma), so this is a minor bump, not a major: a 1.0.0 consumer keeps
validating against a 1.1.0 payload. `supportedConsumers[].contractVersion` is
deliberately left at 1.0.0 — that pins what the Tracker was built against, and
moving it would assert a consumer change nobody made.

contracts 104/104, guard 10 "4 schema(s) hashed and resolved".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@beyondnetPeru
beyondnetPeru merged commit ee51464 into develop Jul 31, 2026
33 of 34 checks passed
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