enhance(#2671): brand raw vs calibrated token types so double-application is a compile error (#2676)

* test(#2671): add failing-first brand-typing compile fixtures

* feat(#2671): brand raw vs calibrated token types

* refactor(#2671): hoist type-compile into a before() hook

Two review responses:

- The fixture compile ran in the describe() body, so it executed at
  collection time even when the block was filtered out, and a failed
  precondition collapsed eight independent assertions into one opaque
  describe-level failure. A before() hook is this repo's documented
  idiom and preserves per-test granularity.

- parseTokensFlag now records WHY it returns an unbranded number: it
  validates the magnitude of --tokens, but the basis is decided by
  --calibrated, so branding here would be wrong for half its callers.
  The assertion belongs to cmdEstimateCheck, its only caller.

* test(#2671): pin each brand diagnostic to its OFFENDING marker

Adversarial review demonstrated that asserting only exactly-one-diagnostic-
at-code-N is not airtight. Repairing a fixture's brand violation while
injecting an unrelated error of the same code (a string passed as the
budget argument) still yielded exactly one TS2345, so the fixture would
have reported green while no longer testing its regression at all.

Each bad-* fixture now routes its violating value through a const named
OFFENDING, and the test asserts the diagnostic's start offset falls inside
that node — located through the AST, so it survives reformatting and never
pattern-matches source text. Replaying the proof-of-concept against the new
assertion rejects it: the diagnostic lands on the budget literal, not the
marker.

Also corrects a doc comment that claimed the program type-checks all of
src/; it covers phase-estimation.cts and its transitive dependencies.

* chore(#2671): backfill changeset PR number (#2676)
This commit is contained in:
Tom Boucher
2026-07-26 21:42:50 -04:00
committed by GitHub
parent 0d08c32048
commit a5633bb32f
13 changed files with 460 additions and 16 deletions

View File

@@ -114,6 +114,15 @@ export function readCalibrationSamples(cwd: string): ReturnType<typeof estimatio
* digit match rather than Number()/parseInt so that "1; touch x", "1e5",
* "0x10", and " 1 " are all refused — the value reaches us as argv, is never
* shell-interpolated, and must not be coerced into looking valid.
*
* Returns a PLAIN number, deliberately (#2671). This function validates the
* magnitude of `--tokens`; it cannot know the figure's basis, because that is
* decided by a DIFFERENT flag (`--calibrated`). Branding here would force it to
* pick RawTokens or CalibratedTokens for every caller, and it would be wrong
* half the time — a type that lies is worse than no type. The basis assertion
* therefore belongs to the caller that reads both flags; today that is
* `cmdEstimateCheck`, which is this function's only caller. A future second
* caller must make the same assertion explicitly rather than inherit a guess.
*/
export function parseTokensFlag(args: string[]): number {
const idx = args.indexOf('--tokens');
@@ -159,9 +168,15 @@ export function cmdEstimateCheck(cwd: string, args: string[], raw: boolean): voi
// factor === 1, and 1^2 === 1). A plan's recorded `estimate.tokens` is
// calibrated at emission time per ADR-2629 Decision 1, so the plan-checker
// MUST pass this flag.
//
// `--calibrated` is the whole basis question, and argv cannot answer it for
// the type system — so this is where the caller's claim is turned into a type
// (#2671). Past this expression the basis is carried by RawTokens /
// CalibratedTokens and the wrong composition stops compiling; the ambiguity
// is confined to these two lines instead of running the length of the seam.
const calibratedTokens = preCalibrated
? inputTokens
: estimation.applyCalibration(inputTokens, calibration.factor);
? estimation.asCalibratedTokens(inputTokens)
: estimation.applyCalibration(estimation.asRawTokens(inputTokens), calibration.factor);
const classification = estimation.classifyAgainstBudget(calibratedTokens, budget);
output({