Skip to content

Phase C, the Node 18 CI failure, and an audit-gated roadmap - #1

Merged
ojassug merged 5 commits into
mainfrom
phase-c-code-is-not-typescript
Aug 9, 2026
Merged

Phase C, the Node 18 CI failure, and an audit-gated roadmap#1
ojassug merged 5 commits into
mainfrom
phase-c-code-is-not-typescript

Conversation

@ojassug

@ojassug ojassug commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Four independent changes on this branch. They are separable commits on purpose — the CI fix can be reverted without losing Phase C.

2cbdb45 — fix(phase-C): code is a family, not a language

CONTENT_TYPE_VALIDATORS.code mapped to the TypeScript validator as a historical default for the ~19 extensions isCodeExtension covers, of which the AST-lite suite implements three. Every language outside the TS family was lexed by a scanner written for a different grammar — which is not a weaker check, it is a check that invents findings:

language false AST_UNTERMINATED_STRING / AST_UNBALANCED_BRACKET
perl 39 / 40
tcl 30 / 40
shell 22 / 40
powershell 7 / 40
c, css 0 / 50 (C-family; the scanner happens to fit)

Shell and PowerShell are in isCodeExtension today, so those were live false verdicts on the file route. This is DECISIONS §17's finding — a verdict decided by apostrophe parity is not validating anything — reached from the extension side instead of the fence side.

Nothing genuinely covered loses coverage: TS, JS, Python and JSON all reach their validator through the language and path branches of selectValidator, which run first and are unchanged. The remainder now report validated: false and appear on trace.astCoverage — §23's distinction that an unexamined item is not a passing one.

Two hazard-pinning tests were re-aimed, not deleted. They asserted the old mapping deliberately, as recorded traps. The trap did not disappear; it changed shape from wrong validation to absent validation, so both now assert null where they asserted 'typescript'.

Five stale claims moved with it — not the three previously identified. Two doc comments in constructors.ts, phase-4b-pathless-code-scope §4b.2 and §6.3, CHANGELOG's Phase 4b.1 entry, and DECISIONS §29. Historical records are annotated as superseded rather than rewritten.

46b5ff9 — fix(ci): the Node 18 leg tests a version the toolchain stopped supporting

This is the CI failure, and it is not a regression in any commit that ran it. package.json claimed engines >=18.0.0 and the matrix tested 18.x, but 13 packages in the tree require ^20.19.0 || ^22.13.0 || >=24. The leg failed at two independent steps:

  • Lint — the whole eslint 10 stack (eslint@10.8.0, @eslint/core, espree@11, eslint-scope@9, eslint-visitor-keys@5, @eslint/config-array, @eslint/plugin-kit, brace-expansion@5)
  • Testvitest@3.2.7vite@7.3.6. vitest's own engines advertises ^18.0.0 and is misleading; its dependency tree does not honour it, so the leg failed even with lint removed.

Scanned per leg: 18.20.8 → 13 incompatible; 20.19.5 → 0; 22.21.1 → 0; 24.14.0 → 0; 26.4.0 → 0. Only the 18 leg was ever red. Node 18 reached EOL 2025-04-30, so the fix is to stop testing a version nothing supports rather than pin the toolchain backwards.

engines is set to the actual intersection (^20.19.0 || ^22.13.0 || >=24, not >=20.19.0) because eslint excludes 21 and 23 and brace-expansion excludes 21. The lockfile refresh touched one line, zero version/resolved/integrity entries.

.npmrc adds engine-strict so this fails at npm ci with EBADENGINE instead of three steps later at Lint — that delay is what made the diagnosis expensive. npm never publishes .npmrc, so it constrains contributors, not consumers.

Incidentally closes a latent bug the old floor was already wrong about: gateway/proxy.ts calls AbortSignal.any, which needs >=18.17.0, so >=18.0.0 was never true.

929b3d2 — docs(claude): trim CLAUDE.md, scope the rest to its directory

33 lines out, 1 in. Removed restatements that drift silently against package.json, SUPPORTED_FLAGS and TOOL_DEFINITIONS. The two benchmark gotchas were moved, not deleted, into a new directory-scoped tokendamper-benchmark/CLAUDE.md. These edits predate the session and are committed as their own unit so the code changes stay separable.

0f7dbb9 — docs(roadmap): gate feature releases on the audit

The roadmap scheduled BM25, MMR, AST folding and Prometheus metrics on top of a pipeline measured at 14.04% on its own corpus, 0.0% on its own benchmark, and 0 bytes on the Gateway. The gate is a precondition, not a priority note: v1.2.0's scorer/MMR, v1.3.0's cache_control and Milestone 8 all build on solve01Knapsack, which is unreachable on every shipping path (one-item bundles, prefix locking pins item 0, solver always selects it).

Adds a blocking v1.1.x track plus a Scope Decision Gate. Release numbering v1.2.0–v2.0.0 is unchanged so cross-references stay valid.

One correction came from reading source, not the audit: v1.3.0's "Declaration Boundary Detector" is scheduled as new work, but FUNCTION_HEADER + CONTROL_FLOW_HEADER in elision/regions.ts already implement it. Re-scoped rather than gated.


Verification

npm citypechecklintbuildtest, run on a clean f93c385 worktree with the fix applied, and again against this branch's HEAD:

typecheck PASS   lint PASS   build PASS
Test Files  54 passed (54)
Tests      481 passed (481)

⚠️ This is the first GitHub Actions run on the new [20.x, 22.x, 24.x] matrix. The branch push did not trigger CI (ci.yml fires only on push to main / PR to main), so the matrix change proves itself here for the first time. The per-leg dependency scan above is deterministic, but it is not the same as a green run.

Reviewer notes

  • max_audit.md is now tracked (812 lines). Included because ROADMAP.md cites it as its evidence base throughout, and a roadmap referencing a file absent from the repo is the exact failure mode this project guards against. git rm --cached if it was meant to stay scratch.
  • Phase C has no DECISIONS.md entry of its own — recorded as a supersession note on §29 and a CHANGELOG bullet. A §36 is the remaining piece.

🤖 Generated with Claude Code

ojassug and others added 5 commits August 8, 2026 19:57
CONTENT_TYPE_VALIDATORS.code mapped to the TypeScript validator as a historical default for
the ~19 extensions isCodeExtension covers, of which the AST-lite suite implements three. That
meant every language outside the TS family was lexed by a scanner written for a different
grammar. It is not a weaker check; it is a check that invents findings.

Measured false AST_UNTERMINATED_STRING / AST_UNBALANCED_BRACKET verdicts: perl 39 of 40, tcl
30 of 40, shell 22 of 40, powershell 7 of 40. c and css score 0 of 50, because the C-family
scanner happens to fit. Shell and PowerShell are in isCodeExtension today, so those were live
false verdicts on the file route - $'...' quoting, ' inside comments, ${...} expansion and
[[ ... ]] tests are ordinary syntax a TS lexer reads as unterminated or unbalanced. This is
DECISIONS §17's finding, that a verdict decided by apostrophe parity is not validating
anything, reached from the extension side instead of the fence side. Removed for the same
reason rather than tuned.

Nothing genuinely covered loses coverage. TypeScript, JavaScript, Python and JSON all reach
their validator through the language and path branches of selectValidator, which run first
and are unchanged. The remainder now report validated:false and appear on trace.astCoverage,
which is §23's distinction - an unexamined item is not a passing one.

Two tests asserted the old mapping deliberately, as recorded traps. The trap did not
disappear; it changed shape, from *wrong* validation to *absent* validation, so both pins were
re-aimed rather than deleted. declared-language and bench/evaluator now assert null where they
asserted 'typescript', and evaluator additionally pins item.path as undefined - those fixtures
pass the path as origin, which is why the extension arm never rescues them and why `language`
remains the only thing standing between a benchmark fixture and an unchecked item.

Five stale claims moved with it, not the three the audit listed: two doc comments in
constructors.ts, phase-4b-pathless-code-scope §4b.2 and §6.3, CHANGELOG's Phase 4b.1 entry,
and DECISIONS §29. The three historical records are annotated as superseded rather than
rewritten. max_audit.md quotes the old comment too and is left alone, because it is correct as
history.

typecheck, lint, build clean; 481/481 tests.

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

CI runs the matrix [18.x, 20.x, 22.x] and package.json claims engines >=18.0.0, but 13
packages in the installed tree require ^20.19.0 || ^22.13.0 || >=24. The 18.x leg has been
failing for two independent reasons, neither of them a regression in any commit that ran it.

Lint dies on the eslint 10 stack: eslint@10.8.0, @eslint/core, @eslint/config-array,
@eslint/config-helpers, @eslint/object-schema, @eslint/plugin-kit, espree@11, eslint-scope@9,
eslint-visitor-keys@5, brace-expansion@5. Test dies separately on vite@7.3.6, reached through
vitest. vitest's own engines field advertises ^18.0.0 and is misleading - its dependency tree
does not honour it, so the leg would fail even with lint removed.

Scanned per leg: 18.20.8 has 13 incompatible packages, 20.19.5 has 0, 22.21.1 has 0,
24.14.0 has 0, 26.4.0 has 0. So only the 18 leg was ever red, and the fix is to stop testing
a version nothing supports rather than to pin the toolchain backwards. Node 18 reached end of
life on 2025-04-30.

engines is set to ^20.19.0 || ^22.13.0 || >=24 rather than >=20.19.0, because that is the
actual intersection: eslint excludes 21 and 23, and brace-expansion excludes 21. The lockfile
carries the same string; the refresh touched one line and zero version/resolved/integrity
entries.

.npmrc sets engine-strict so this fails at npm ci with EBADENGINE instead of three steps later
at Lint. That is the part that made the diagnosis expensive: npm only warns by default, the
install succeeds, and the error surfaces somewhere that does not name the cause. npm never
publishes .npmrc, so it constrains contributors and not consumers.

Verified end to end on a clean f93c385 worktree with the fix applied: npm ci, typecheck, lint,
build all clean, 481/481 tests.

Incidentally closes a latent runtime bug the old floor was already wrong about:
src/gateway/proxy.ts calls AbortSignal.any, which needs >=18.17.0, so >=18.0.0 was never true.

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

CLAUDE.md was restating things a reader can get faster from source: the npm script list, the
full optimize flag list, six architecture table rows naming directories by their contents, the
MCP tool and resource names, and the TypeScript/Prettier/ESLint conventions line. All of that
drifts silently against package.json, SUPPORTED_FLAGS and TOOL_DEFINITIONS, and none of it is
a decision - it is a directory listing with extra steps. 33 lines out, 1 in.

The two benchmark gotchas are not derivable and were not deleted. They move to a new
directory-scoped tokendamper-benchmark/CLAUDE.md, which loads when working in that directory:
that TokenDamper is timed through a Node process spawn while Headroom is an in-process Python
call, so the latency numbers are not apples-to-apples, and that Headroom's target_ratio is a
soft hint rather than an enforced budget. Both are traps for someone reading
BENCHMARK_RESULTS.md, and both belong next to the harness rather than in a root file that a
TypeScript-only session also pays for.

This is the M11 direction from the audit applied to one file - the invariants and measured
baselines earn their place, the restatements do not - not a full docs retirement.

These edits predate this session and were sitting uncommitted in the working tree; they are
committed here as their own unit so the Phase C and CI changes stay separable from them.

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

ROADMAP.md scheduled BM25 scoring, MMR, AST folding and Prometheus metrics on top of a
pipeline the 2026-08-07 audit measured at 14.04% on its own corpus, 0.0% on its own benchmark,
and 0 bytes on the Gateway. CLAUDE.md has said "do this before roadmap feature work" for some
time without the roadmap reflecting it. It does now.

The gate is not a priority note, it is a precondition. v1.2.0's BM25 scorer and MMR pass,
v1.3.0's cache_control placement and Milestone 8 all build on solve01Knapsack and
scoreBundleTopology, and those are unreachable on every shipping path: createContextBundle
emits a one-item bundle for CLI, MCP and bench, prefix locking pins item 0, and the solver
always selects it. Building on them produces correct code with no observable effect, which is
the condition the audit already found in ~1,000 existing lines.

Inserted a blocking v1.1.x track - v1.1.1 green tree, v1.1.2 data loss, v1.1.3 honest
instruments - and a Scope Decision Gate carrying the four questions whose answers determine
whether the later releases are buildable at all. Numbering of v1.2.0 through v2.0.0 is
deliberately unchanged so existing cross-references stay valid; the remediation work is
versioned as patch releases instead of renumbering the chain.

Three items absent from the audit's own recommended order are promoted into the track: M8+M9,
a credential echo one env var from being live; M5a, roughly ten lines of schema that convert
the whole MCP mode from guaranteed no-op to functional; and M10, bench throwing for every
installed user. M2 moves from the audit's fourth position to first, because a red tree makes
every later fix unverifiable.

v1.3.0 is re-scoped rather than gated, and that correction came from reading source rather
than from the audit. Its "Declaration Boundary Detector" is scheduled as new work, but
FUNCTION_HEADER and CONTROL_FLOW_HEADER in elision/regions.ts already implement that
discriminator and selectElisionRegions already folds bodies on TS, JS and Python. The roadmap
was scheduling something that shipped - the same class of error its own appendix records once
already.

Milestone 9 is flagged for re-derivation: its proposed w_atom term would add a third weight to
a formula whose R_struct term is a pinned constant for code, and its TD_PRESERVE tracking
formalizes CONSTRAINT_DIRECTIVE_LOST, currently the largest single cause of 0% reduction.

Added a measured starting position table, because every benchmark target in the document was
stated without the number it improves on - per the audit's own §3.3, a target with no baseline
has the same shape as a green check that never ran.

max_audit.md is committed with it. ROADMAP.md now cites it as the evidence base throughout,
and a roadmap citing a file that is not in the repository is the failure mode this project
cares most about. Drop it with git rm if it was meant to stay a scratch artifact.

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

Closes H3, M3 and M4a from `max_audit.md`. No source code changes: tests,
fixtures, docs and `package.json` only, so `dist/` is unaffected.

H3 — the regression suite guarded a set the product does not ship.
`tokendamper bench` printed 0.0% reduction at a 40% fallback rate while the
suite was green. The two facts never met because no test ran the combined
fixtures: Tests 1-5 loaded `loadBenchmarkFixtures('humaneval')` — the half
that structurally cannot fall back — and Test 2 built a private two-fixture
set inline under an artificial `maxInputTokens: 50` and asserted 40% against
that. codexglue, at 0.80 fallback, was never run at all.

All five now load the shipped combined set, and `baseline.json` records
measured truth rather than a target: `minTokenReductionRatio` 0.40 -> 0.0,
`maxFallbackRate` 0.0 -> 0.40. The old values move to an `aspirational` block
so the gap stays visible instead of being deleted.

The assertions use equality, not `>=`. A `>=` check against a measured floor
of 0.0 is vacuously true and can never fail, which is the same defect one
level up — invariant 10 applied to the guardrail rather than to the engine.
Equality means an improvement breaks the suite too and has to be recorded
deliberately. Verified able to fail: mutating the recorded numbers turns 3 of
6 tests red.

Two things the measurement surfaced that were not written down anywhere.
Every non-fallback fixture also reduces exactly 0%, because `BenchmarkRunner`
supplies a `TokenHasher` and the engine rehydrates what it elided — so the
shipped set produces no reduction by two independent mechanisms, not one. And
`syntaxPassRate: 1.0` is now asserted alongside the 40% fallback rate, since
syntax is evaluated on emitted output and emitted output on fallback is the
input; a metric that reads 1.0 whenever the engine does nothing cannot
distinguish success from inaction, and pairing them keeps that legible.

Test 3 keeps the `aba84df` finding intact, rescoped to humaneval. Its claim is
still true; what was wrong was its scope, so it is narrowed rather than
removed.

M3 — `package.json` declared MIT while `LICENSE` grants MPL-2.0 and the README
said so. `package.json` is publishable (`"private": false`, `files`,
`prepublishOnly`) and npm treats its `license` field as authoritative, so
scanners read MIT — permissive — and receive MPL-2.0 copyleft. That error runs
in the harmful direction: it understates the obligations a consumer takes on.
`package.json` and `CLAUDE.md` now match `LICENSE`. "All rights reserved" is
dropped from the README copyright notice, where it sat directly above an
open-source grant and asserted its opposite. Recorded as DECISIONS §36, which
also notes the MIT -> MPL migration itself was never recorded.

M4a — the README's Gateway warning outlived the fix it described. It claimed
the Gateway "bypasses TokenDamper's validation pipeline" and that
`fallbackUsed` is "hardcoded `false`"; both have been untrue since Phase 1.0b
(`proxy.ts` imports and calls `core/engine.optimize`, and `fallbackUsed` is
`result.fallbackUsed`). It is replaced rather than deleted: removing a false
warning while leaving "0/1 Knapsack Planning", "Cross-turn Session
Deduplication" and "Reversible Token Hashing" uncaveated would have made the
page less accurate, not more. The notice now carries the measured status —
cross-turn dedup saves 0 bytes on ordinary traffic (H1), `tokendamper exec`
returns 401 to its own child (C3), non-ASCII bodies can be corrupted at the
socket (C2). Qualifying those feature claims properly is Wave 3, after the
Gateway question is decided.

Typecheck clean, lint clean, 481/481 tests passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ojassug
ojassug merged commit 078a21c into main Aug 9, 2026
3 checks passed
@ojassug
ojassug deleted the phase-c-code-is-not-typescript branch August 9, 2026 15:25
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