Skip to content

Deterministic intelligence: no LLM, packaged pipeline, and surfaces that tell the truth - #42

Merged
archdex-art merged 9 commits into
mainfrom
feat/deterministic-intelligence
Aug 10, 2026
Merged

Deterministic intelligence: no LLM, packaged pipeline, and surfaces that tell the truth#42
archdex-art merged 9 commits into
mainfrom
feat/deterministic-intelligence

Conversation

@archdex-art

@archdex-art archdex-art commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Seven themed commits over one working state, rebased onto current main. Every gate is green on the tip; the intermediate commits are reviewable groupings, not independently-green steps.

Gates: 2,206 tests / 120 files · typecheck clean · depcruise 0 violations · build compiled.


What is in here

Commit Why
chore(web) remove the LLM assistant layer The product claims it needs no API key; an assistant that called one contradicted that in the same binary. 3,670 lines deleted, no replacement.
refactor extract the pipeline into packages The web app owned the indexer, detectors, scorer and persistence, so the CLI could not run them without importing the app. Sixteen packages behind published entry points, enforced by .dependency-cruiser.cjs in CI.
feat(vcs) read repositories out of git microsoft/TypeScript is 41 MB of git objects and a 655 MB checkout, almost all of which analysis never opens.
feat(ask) degrade to ranked search A closed grammar refused 30 of 35 real questions.
fix(web) surfaces tell the truth up front The headline CTA promised a fix it could not deliver, and the dashboard led with failures.
docs re-derive the published figures The landing page quoted a profile taken on a tree 24 files smaller, missing its largest stage.
fix reconcile with main's security work Three merged PRs touched the same code. Kept separate on purpose — see below.

The measurements

Reading out of git. Verified end to end on expressjs/express: 380 KB indexed against 1.6 MB checked out — 76% of bytes avoided. The checkout still happens, lazily, on first real file access, so the editor, search and git panels are unchanged. git-source.test.ts pins the equivalence: the same commit scores identically from either source, byte-for-byte on every coverage counter. Two real bugs surfaced only because that test compares the two sources against each other rather than against hardcoded numbers.

Ask. Measured against 35 questions written without reading the cue table: 5 answered → 33. A refusal is now the worst available answer — an unparseable question degrades to a ranked lookup and says SEARCH on the receipt, so a reader knows how much to trust it. Four intents added over data the product already had and could not reach (ownership, stale, packages, unused_packages): 17 of 18 answer precisely on the capability set they serve.

Also fixes a confidently wrong answer — "which libraries are unused" folded to dead before package and returned a hundred dead symbols under a confident headline. And normalise no longer strips the dot between two characters, so store.ts reaches the resolver as a path: no file entity had ever bound.

The fix button. It ran 4.3 s to say "Not verified — no verification gate completed / nothing to patch" — the product's third verb apparently failing, in its most prominent place. Knowable before the click, so it now says so instantly, or Generate verified fix for N findings when there is work.

The dashboard. Under "ranked by where attention is needed", the top three rows were failed repositories — one a typo from the previous day. 4 of 19 rows outranking the other 15, because a failure has no score and the risk order sorted its absence to the top. In-flight rows still float; permanently failed ones get their own group with Retry.


The rebase, and why the reconciliation is its own commit

main merged three PRs while this work sat in the tree, two of them touching the same code. A conflict resolution is the part of a rebase a reviewer most needs to see, so it is not folded into the 152-file refactor above it. Read fix: reconcile with main's security work closely — it is where a dropped guard would hide.

  • The SSRF guard survives, and is now actually tested. -c http.followRedirects=false is intact. Its test was asserting the argv of an execFile call that no longer happens (runBoundedClone replaced it), so the mock never matched, a real git clone ran and exited 128 — a security test green-lighting by accident. It now mocks spawn. Mutation-checked: flipping the flag to git's default, or dropping it, both fail.
  • Anonymous indexing was fixed twice, differently. Main refuses it unless an operator allows it and the caller acknowledges the repo will be public; this branch gives anonymous callers a private, cookie-scoped bucket. Both controls are kept, with consent demanded only when the result really will be world-readable — asking for it on a run that lands somewhere private is a warning about a thing that is not happening.
  • Three functions existed twice after the merge (assertNotGitDir, isCommitHash, err). Main's versions kept: they distinguish missing from malformed and count the skip in coverage accounting.

Two things I got wrong, caught by running it

Retry duplicated rows. The first version re-submitted the URL assuming the row would be reused. It is not — a control added to clear clutter created it. Now retires the superseded row, guarded on the id actually changing. Re-measured: 24 → 24 rows where the buggy version added one per click.

I nearly weakened a boundary. The registry import tripped no-deep-import-from-app. Before editing a guard rule to suit myself I tested the alternative — importing the package root fails the client build, because the index re-exports node:fs. So the rule's hardcoded index.ts was the imprecise part: it says "published entry point" but could not read the exports map that defines one. It now does. Proved it still bites: genuine deep imports into package internals are still errors.


Review notes

  • Start with commit 7 (the reconciliation) and commit 5 (fix(web)) — the highest-risk and the most user-visible, both small.
  • Commit 2 is 152 files and is the bulk of the diff. It is mechanical: extraction plus consumer updates, atomic because the layering gate fails on a half-move.
  • New behaviour is mutation-tested, not just covered: 28 mutants on the Ask work, 15 on the UX work, 5 on the reconciliation — all caught, several only after I strengthened fixtures that were too kind. One test was deleted for being unable to fail.
  • Known flake, pre-existing and not touched here: refreshWorkspace > ignores credentials, a trailing slash… times out at exactly 5,000 ms spawning git under full-suite load. Passes 3/3 in isolation. Widening that timeout is a separate call.

Still open, deliberately

  • FIXERS has one provider. empty-catch is also line-scoped, so it only fires on a single-line catch (e) {} — the multi-line form every formatter produces is invisible. The button is now honest about this; the coverage gap is real and unaddressed.
  • SymbolEdgeKind declares six kinds and emits two (calls, contains). references, imports, extends and implements are in the union and never produced. Either build them or delete them from the type — I did not want to choose unilaterally.
  • Dead tree-sitter dependencies (49 MB) remain in apps/web/package.json and next.config.ts, imported nowhere since the OOM postmortem.

What CI caught that I had not

Three more commits, all from the pipeline rather than from me. Worth reading as part of the review:

  • lint was a gate I never ran. Five of six locally, not the sixth. Three react-hooks/set-state-in-effect errors, all in code this branch adds, all fixed by deriving the state rather than suppressing the rule — Ask no longer flashes the previous question's answer before correcting itself, which was the same bug the lint rule describes.
  • The packaged GitHub Action is withdrawn. Its gate runs correctly here (75 unaccepted findings at or above high — the gate doing its job) but on a hosted runner it wrote neither the SARIF nor the summary. Baselining would have turned the check green without explaining why. The README now documents the two-line codegraph ci workflow that is verified.
  • A test timed out on CI at 30s. Two self-index tests each walked the whole repository, asserting different properties of the same analysis. Indexing once in beforeAll took the file from 31.4s to 16.8s. Raising the timeout would have hidden the duplication.

One of those 75 findings is mine and belongs above the line, not below it: ask.ts is 1,257 lines against this repository's own 600-line bar. It is the file that grew most in this PR, and splitting it is the obvious follow-up.

Final CI: both required jobs green — Test & Build, and Docker build + adversarial smoke test.

The product's claim is that it needs no API key, and an assistant that
called one contradicted it in the same binary. Every path here was either
dormant behind a key nobody sets or a second, weaker answer to a question
the graph already answers deterministically.

Removed: the assistant routes and panel, the model-settings surface and its
persistence, the local/narrative agent fallbacks, and the Anthropic key
check. The findings, the score and the swarm are unaffected - none of them
ever consulted a model.

3,670 lines deleted, no replacement.
The web app owned the indexer, the detectors, the scorer, the graph
builder and the persistence layer, so the CLI could not run any of them
without importing the app - which the layering gate forbids, for the
reason that a codemod running in two hosts must be one codemod.

Splits the pipeline into packages behind published entry points:
core-domain, config, observability, analysis-model, analysis, core-graph,
detect-engine, score-engine, viz, imports, persistence, jobs, verify,
sandbox, fsx and remediate-engine. Consumers move with it, which is why
this touches every route and most components.

The boundary is mechanical, not aspirational: `.dependency-cruiser.cjs`
declares who may import whom and CI fails on a violation, so a new edge
is a deliberate act recorded in that file.
Analysis materialised every file on disk and then read a fraction of it.
Measured on microsoft/TypeScript: the git objects are 41 MB and the
checkout is 655 MB, almost all of which the analysers never open.

Adds a git-object reader - `gitTreeFiles`, `readBlobs`, `hasWorkingTree`,
`materialisePaths` - and clones with `--no-checkout`. Manifests, and only
manifests, are materialised, because `advisories.ts` reads `package.json`
off disk and a tree without one looks like a project declaring no
dependencies: a wrong answer, and a silent one.

The checkout still happens, lazily, on the one route guaranteed to need
real paths: `requireWorkspace` materialises on first file access, so the
editor, search and git panels are unchanged. `reindex` opts out - it reads
out of git and would otherwise reintroduce the 614 MB it exists to avoid.

Measured end to end on expressjs/express: 380 KB indexed against 1.6 MB
checked out, 76% of bytes avoided, and `git-source.test.ts` pins the
equivalence - the same commit scores identically from either source,
byte-for-byte on every coverage counter.
Measured against thirty-five questions written without reading the cue
table: five answered, thirty refused. The classifier was not wrong on
those thirty - a closed grammar has bounded recall by construction - but
"I cannot answer that" six times in seven trains people to stop typing.

A refusal is now the worst available answer. An unparseable question
degrades to a ranked lookup across symbols, topic tags, paths, packages
and endpoints, and says SEARCH on the receipt so a reader knows how much
to trust it. Only `ambiguous` (a real question back) and `not_analysed`
(a missing analysis) still refuse.

Four intents added over data the product already had and could not reach:
`ownership`, `stale`, `packages` and `unused_packages`. Same corpus after:
33 of 35 answered, and on the capability set those intents serve, 17 of 18
answer precisely.

Also fixes a confidently wrong answer: "which libraries are unused" folded
to `dead` before `package` and returned a hundred dead SYMBOLS under a
confident headline.

`normalise` no longer strips the dot between two characters, so `store.ts`
reaches the resolver as a path - no file entity had ever bound. Direction
is pinned on passive forms ("usages of X" was answering what X uses), and
the Ask button now takes the same ink as every other button, having asked
for a token that does not exist.
Three findings from driving the product rather than reading it.

The headline CTA promised nothing it could deliver. "Generate verified fix
PR" was offered at full prominence on every repository; on one with nothing
to fix it ran 4.3s and returned "Not verified - no verification gate
completed / nothing to patch". The product's third verb, apparently
failing, in its most prominent place. It was knowable before the click:
findings carry a rule and `FIXERS` declares which rules it handles, so
`autoFixable` now answers first. The button reads "No auto-fixable
findings" and is disabled, or "Generate verified fix for N findings" when
there is work - checking both rule-id spellings, because migrated rows and
freshly detected ones key differently.

A no-op is no longer painted as a failure. `verdictFor` gains a fifth
verdict: a run that produced no edits has nothing to verify and says so in
neutral grey, distinct from a patch whose gates could not run.

The dashboard led with garbage. Under "ranked by where attention is needed"
the top three rows were FAILED repositories, one a typo from the previous
day - 4 of 19 rows outranking the other 15, because a failure has no score
and the risk order sorted its absence to the top. In-flight rows still
float (a run in progress genuinely is urgent); permanently failed ones get
their own group with Retry and Remove. `triage` is pure and exported, so
the partition is testable outside a component.

Retry re-submits rather than re-indexing - verified, not assumed: re-index
answers 404 "Workspace not ready" for a repo that never cloned. It also
retires the row it supersedes, guarded on the id changing; the first
version duplicated a row on every attempt.

Also: the ownership share bar asked for `--accent`, which does not exist,
and rendered no bar. `design-system.test.ts` now fails on any custom
property nothing defines - an undefined one is silently inherited, never an
error, so it is exactly the class of bug review cannot catch.

`no-deep-import-from-app` said "published entry point" but hardcoded
index.ts. It now reads each package's `exports` map, because the registry
must reach a client bundle and the package root drags `node:fs` in - the
root import fails the build, verified. The guard still bites: a genuine
deep import into package internals is still an error.
The landing page advertised "2.1s to index 327 files" with a five-bar
breakdown. That profile was taken on a 303-file tree, six of the eleven
stages the pipeline now has did not exist when it was written, and the
largest one today was missing from the chart. The page's own footer
promises the commands measuring its numbers are in the repo.

`npm run selfindex` prints the per-stage timings, the cold total and the
TypeScript file count, so the numbers map to something anyone can re-run -
the same reason `scripts/bench.mts` exists for the README's benchmark rows.

Counts synced to the tree: 116 test files, 2,185 cases, 375 TypeScript
files. `readme-claims` and `landing-claims` assert the file counts against
the working tree, so these cannot drift again silently; the case count and
the timings are published with a date, because a wall clock measures the
machine as much as the code.
Deliberately its own commit rather than folded into the ones above: a
conflict resolution is the part of a rebase a reviewer most needs to see,
and hiding three of them inside a 152-file refactor is how a dropped guard
ships.

`main` merged three PRs while this branch was in the working tree, and two
of them touched the same code.

- The clone gained `--no-checkout`, `--progress` and `--single-branch` here
  while main added `-c http.followRedirects=false` there. Both survive: the
  SSRF guard is unchanged and the redirect test now mocks `spawn` rather
  than `execFile`, because `runBoundedClone` replaced the `execFile` call
  the test was asserting against - so it had stopped matching, ran a real
  `git clone` and exited 128. A security test that green-lights by accident
  is worse than none. Mutation-checked: flipping the flag to git's default,
  or dropping it, both fail the suite.

- `assertNotGitDir`, `isCommitHash` and `err()` each existed twice after the
  merge - once from main's hardening, once from this branch's extraction.
  Main's versions are kept; they distinguish missing from malformed and
  count the skip in coverage accounting.

- Anonymous indexing was fixed twice, differently. Main refuses it unless an
  operator allows it AND the caller acknowledges the repo will be public;
  this branch gives anonymous callers a private, cookie-scoped bucket. Both
  controls are kept, with consent now demanded only when the result really
  will be world-readable. Asking for consent on a run that lands somewhere
  private is a warning about a thing that is not happening, which is how
  consent prompts get clicked through.

- `NodeGraph`'s character-count truncation is gone in favour of the measured
  `fitText`; the estimator divided by an average advance, so it cut `IIII`
  early and let `WWWW` overflow.

Counts re-derived after the merge: 120 test files, 2,206 cases, 379
TypeScript files.
@archdex-art
archdex-art force-pushed the feat/deterministic-intelligence branch from d147c8f to da38da2 Compare August 10, 2026 16:39
@github-actions

Copy link
Copy Markdown

CodeGraph

⚠️ The gate did not produce a summary — it crashed before writing codegraph-summary.json. See the job log.

Two things CI caught that I had not run locally. `npm run lint` is a gate
and I checked the other five; that is my miss, and the fix is behavioural
rather than a suppression.

Three `react-hooks/set-state-in-effect` errors, all in code this branch
adds. Each was state that should have been DERIVED:

  · Ask copied `?q=` into the input and cleared the answer from two
    effects, so the component painted the previous question's answer and
    then re-rendered to correct itself. Comparing against the question the
    tree last synced to removes the flash, not just the warning.
  · `loading` was set inside both fetch effects. "Loading" is exactly "the
    data I hold is not for the thing I am rendering", so both now compare
    the two directly and cannot fall out of step.

`action.yml` and its dogfooding workflow are withdrawn. The gate they wrap
runs correctly here — 75 unaccepted findings at or above high, which is the
gate doing its job — but on a hosted runner it wrote neither the SARIF nor
the summary, so the composite action failed for a reason I cannot reproduce
locally. Baselining would have turned the check green without explaining
that, and a gate whose own CI cannot be reproduced is not one to hand
anybody else. The README now documents the two-line `codegraph ci` workflow
that IS verified, and says where the packaged action went.

One of those 75 findings is mine and worth stating: `ask.ts` is 1,257 lines
against this repository's own 600-line bar. It is on the list below the
line, not hidden.
`dependencies.test.ts` timed out on CI at the 30s default. Both self-index
tests called `indexRepo(".")` — a full walk of the whole repository — and
they assert different properties of the SAME analysis, so the second walk
bought nothing. One already carried a 120s override; the other inherited
the default and fell over as soon as this branch made the tree bigger.

Raising the second timeout would have hidden the duplication. Indexing once
in `beforeAll` removes it: 31.4s to 16.8s for the file, and no test now
sits anywhere near a default timeout.
@archdex-art
archdex-art merged commit 848070b into main Aug 10, 2026
2 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