Gradual mode: publish where type information reaches - #211
Merged
Merged
Conversation
Suppressing by diagnostic code cannot be gradual: a missing annotation is born in the implicit-any family (suppressed) and dies fifty lines later as an ordinary type error (published) on a line the author never annotated. The honest axis is the declaration, so the gate answers one question per line: does type information reach it? Inward from an annotated header, outward from an annotation in a declaration's header, sideways along assignment to a fixpoint, and across an import of a typed export — an annotated .rip export, a relative .ts module, or a bare workspace .rip package, resolved the way the runtime resolves it (node_modules walk-up, manifest exports, realpath; the generated tsconfig maps each package name onto its mirror face, and rip check anchors the mirror at the nearest workspaces-declaring root). Schemas and components are typed by construction — the face materializes their member types whether or not the author spelled one — and two defect families publish in every mode because no annotation answers them: names and modules that do not resolve, and definition cycles. Inference alone never publishes: the misused unannotated export stays silent until someone writes a type, which keeps the rule statable — you get diagnostics where type information reaches — and rip.strict reports everything. The gate fails open, and the posture is pinned in both directions. Gradual also supplies strictNullChecks: false, yielding to any strictness the project's own tsconfig chain sets. Two compiler fixes ride along on their own merits: the face's class field declarations now come from every instance method body, not the constructor's alone (a constructor that delegates field setup to a helper establishes those fields just as surely), and the trailing run of bare unannotated parameters emits a TS-only optional marker, since calling with fewer arguments is legal in rip as in JavaScript — with carve-outs wherever something else already types the parameter. Editor: the three gate construction sites share one resolution and one plain-compile memo (the cold-open double compile measured, then removed); a dependency touched on disk refreshes its open importers, whose gates can change; a drifted mirror heals by re-materialization instead of stranding cross-file asks (a pinned face outliving its session was unreproducible by design); and go-to-definition from inside an import specifier names the whole string literal as its origin, both quote spellings, where the word pattern underlined one path segment. rip check packages: 1899 diagnostics before, 274 after, measured at this branch's base — the survivors readable one by one, cannot-finds down to genuinely-uninstalled modules. All four suites green: root 6091, audit 34, extended 54, editor 166.
Main's CI runs the extended tier over every test file; the branch had only ever run it over check.test.js, and six never-executed tests met the gate. Two exposed real gaps, now fixed in scopes.js: assigning to an imported binding (TS2632) is a runtime TypeError no annotation answers, so it joins the always-reported family beside the module boundary's other cannot-finds (2305, 2613, 2614, 2724 — importing a member that does not exist is a typo'd name spelled at the import); and exportedNamesOf never listed the reactive kinds, so an annotated `export count: number := 0` silently failed to carry its type to any importer. The other four asserted diagnostics on unannotated fixtures — each now rides an annotation so it still pins its real subject: the reactive-cell limit publishes its TS2365 through a typed store, the await-hint positions light their sugar lines, and the host-floor tests read their probes through annotated bindings, since an ambient global never opens the lines that merely mention it. The hidden-diagnostics summary now says one thing one way: both lines that offer strict spell the remedy identically, the missing-types advisory names the declarations it is about (`fs`, `describe` — not "install the @types package" with no noun), and when the hiding happens in a dependency the target does not govern, the line names that project — config is per file, so a strict consumer's check still hides its gradual dependencies' counts, and pointing at the wrong package.json read as the flag being broken. `rip check --build` prints the build identity — the same content hash over the compiler and server trees the editor computes for its cache key and now logs in its ready block — so when the CLI and the editor disagree, one glance says whether the installed extension is stale. Both outputs are one aligned fact per line, home shortened to `~`, the mirror shown workspace-relative: the single-line forms wrapped illegibly the moment real paths landed in them.
.rip/check now follows .rip/editor's doctrine: left in place between runs (self-gitignored, inspectable), with correctness carried by the start-of-run wipe rather than exit-time deletion. Only the tmpdir fallback is still removed on exit. This also stops the .rip dir from flickering in and out of editor file trees on every run. --keep-mirror described the new default, so it is gone; passing it now fails as an unknown option.
Stamp's directives read `sh`, `ok`, and `run` bare — the DSL's design says handlers import nothing, and the runtime installs the vocabulary with `globalThis.sh ??= sh` at module load. The checker could not see a runtime assignment, so every use drew the cannot-find defect: 144 diagnostics, half the corpus, all one pattern. The pattern is repo-wide (ui's focus tracker, app's launch globals, fetch mocks in tests), and it splits into two species exactly at the operator: `??=` says "install unless someone already did" — a declaration wearing runtime clothes — while plain `=` is an overwrite (a mocked fetch must not redeclare the host's) and a non-top-level install is lifecycle state (app's guarded __ripApp, deleted on destroy, is not ever-present vocabulary). So the face now reads the declaration where Steve already wrote it: a top-level `globalThis.NAME ??= expr` emits a TS-only declare-global block, typed through a module-level alias — inside `declare global` the bare name resolves to the global being declared (TS2502, driven against real tsc), so `typeof` is taken where the name still means the module binding. Identifier initializers carry their type; keyword literals (`??= null`) declare `any`. Strip identity holds. The declaring package becomes its own program — an automatic project boundary anchored at its package.json, built by the same wrapper machinery nested tsconfigs use, now able to anchor on the workspace root's config when the package owns none. Scoping is runtime-honest: the vocabulary reaches the package and anyone who imports it (an import runs the installer), while a non-importing neighbor keeps its cannot-find — the typo protection ambient-everywhere would have spent. The editor builds the boundary three ways (the cold stub scan, a dependency's face materializing, an open buffer's compile) and its declaration-only stubs carry an any-typed twin of the block, so a cold-open handler resolves the vocabulary before anything compiles. packages/stamp: 144 diagnostics to zero. The corpus halves, 292 to 148.
db's exec/listTables/describeTable catch blocks returned
`{ success: false, error, … }` — shorthand for a binding named `error`
that does not exist; the catch binds `err`. Every error path those
envelopes exist for threw ReferenceError from inside its own handler
(driven: the compiled catch emits `error` bare and Bun throws on first
use). The checker had said so all along — TS2552, "Did you mean 'err'?",
the always-reported family gradual never hides — but the report sat in a
1,899-diagnostic corpus nobody could read. At 148, someone read it.
`error:` now carries `err.message or String(err)`, the string shape
every sibling envelope in the file already uses. db's own suite: 164
passing before and after.
Two halves of one ruling about who owes what to make a squiggle move.
Gradual owes forgiveness: `import { Database } from 'bun:sqlite'` is
ordinary Bun code, and without installed host types it drew the
cannot-find defect on a module the runtime demonstrably serves — nobody
should install @types/bun just to quiet an import that runs. One
bodyless wildcard (`declare module "bun:*"`) floors the builtin modules
as `any`, with the floor's existing gates: an exact declaration from
installed types outranks a wildcard by TS's own pattern rules, and the
whole floor vanishes the moment the package is present.
Strict owes complaints — and was not getting them: floors and null
posture are per-PROGRAM, so a nested package flipping rip.strict kept
receiving the root program's gradual floor (driven: a strict
packages/ai showed no squiggle on bun:sqlite). A nested rip.strict
package now becomes its own program, the same automatic boundary a
globals-declaring package gets, so its own strictness governs its
floors and its nulls. The gradual sibling beside it stays floored.
Also: every comment in mirror.js swept to its constraint — the driven
findings stay, one clause each; the mechanism narrations repeated per
entry are gone (155 lines).
The check BFS no longer marks the run incomplete when a queued import's module does not exist as specified — ENOENT, ENOTDIR, ELOOP — because tsgo already owns that report: TS2307 on the importing line, or silence under @ts-nocheck, whose writ covers the file's imports. The predicate (missingModuleRead) lives in mirror.js beside ripImportsOf, the producer of the unchecked edges. Explicit targets and imports that exist but cannot be read stay loud: skipping those would buy the importer a "cannot find module" that misstates the problem. Review fixes riding along: the mirror wipe is unconditional (a run whose targets all fail to parse still clears stale faces), the tree is stamped with the compiler build identity in .build, the tmpdir fallback announces itself (tsconfig/@types fidelity degrades), the fallback cleanup and unwritable-workspace path gained coverage, and the unreadable-file tests share one withUnreadable helper.
…not a typo
csv's bench quarantines its competitor parsers behind its own manifest —
by design, never in @rip-lang/csv — and installing them changed nothing:
the face's module walk lives in the MIRROR tree, which never passes
through nested source directories, so bun ran imports tsgo called
cannot-finds. The mirror now plants a node_modules symlink at every dir
whose source twin carries one (both surfaces, best-effort), and the
generated excludes harden to **/node_modules so the linked trees never
join the program as files.
That alone made rip check depend on optional install state — a fresh
clone would go red until someone installed a benchmark's competitors.
So the absent half gets its own posture: a bare import DECLARED in the
governing package.json but not installed is the manifest's stated
intent, and gradual holds its 2307 while the summary names the remedy
("run bun install in packages/csv/bench"). Strict publishes it —
complaints mode, like the floors — and undeclared-and-uninstalled stays
a published defect everywhere: that one really is a typo's shape. Both
halves driven live: installed, the typed benchmark checks clean;
removed, the corpus stays green with the advisory.
# Conflicts: # packages/vscode/src/mirror.js # src/check.js
The gate read the face's TS-only regions as type information, and every region is emitter output: swarm.rip carries not one annotation, yet three bang-defs' lowered `: void` return types made their names typed bindings and the sideways cascade opened most of the file — ten diagnostics on code nobody asked to check, the exact posture violation the mode exists to prevent. Pin annotations are regions too, so the probe pass would have re-admitted held inference through the same door. The region pass is gone. Author annotations are counted from their TYPE tokens (bare forwards included — the old claim that they lex as object literals is stale), wholesale-typed constructs by KIND, and nothing the emitter writes counts as intent. swarm: ten diagnostics to zero with no annotation added; the corpus drops 140 to 130. Comment narration stripped from the files this touched: comments state constraints, and the discovery story lives here instead.
tsgo flips the strict default — an empty config now flags `let x: string = null` and hands catch bindings `unknown`, where TypeScript 5.9 accepts both. Gradual's posture had been patching the leakage one sub-flag at a time (strictNullChecks); the whole strict family was riding in behind it: useUnknownInCatchVariables demanded narrowing ceremony on unannotated `catch err`, and noImplicitThis published member reads inside object-literal methods. The posture now says what it means: `strict: false`, whole. The generated config's explicit `noImplicitAny: true` outranks the umbrella, so evolving-array and evolving-null inference keep working (probed on tsgo directly), and a chain that sets its own strictness is still yielded to whole — pinned both ways: gradual catch bindings publish nothing, an author's strictNullChecks keeps the unknown (TS18046). 19 corpus errors were this leakage, not code: packages 130 -> 111.
9275b58 ("Keep the reconstruction dynamically typed") removed the provisional `: any` annotations from the server reconstruction on purpose — dynamically typed until the shapes settle. The next day's clean-room burn-down (9d6e9ca) rewrote the `args = {...}` line with the richer argument set and carried the annotation back in from the older copy. That one `: any` was manager.rip's only annotation, and an annotation — any annotation — is how a declaration asks for checking: it seeded `args`, and the assignment-flow fixpoint opened 924 of 1,571 lines, all 27 of the file's published diagnostics among them. Removing it restores 9275b58's intent and the file goes silent: packages 111 -> 84.
The scanner classifies a line-ending `>` as either a generic close (the line is finished) or a comparison operator (the next line continues it), by the head the angle run hangs off: return-type and declaration colons, alias `=`, interface heads. Postfix casts were not on the list, so `return g(x) as D<T>` ending an indented block kept the comparison reading — the dedent was swallowed and the next statement nested into the block as dead code after the return, silently. A sibling `if` at least failed loudly as a stray postfix-if. Cast heads now record the same answer the other heads do (syncTypeGenericMemo), using rewriteTypes' own cast trigger. Battery pins both shapes, the silent one by its exact emitted structure. Found writing the first block-final generic cast in the repo — the packages/app brand-cast cleanup in the next commit.
45 of app's 71 gradual diagnostics were two declarations narrower than
their own runtime. Passport spelled `compiled` (and `source`) required
when populate accepts their absence — `| undefined` still demands the
key; only `?` makes it omittable, and asset passports never carry
source. ApplyOpts demanded Promises from callbacks absorb bang-awaits
anyway; remountDirty now admits sync verdicts, and escape is exactly
`=> void` — the union spelling would forfeit TypeScript's void-return
exception, which IS the contract: result ignored, possibly awaited.
The implementation seams state their nature where they sit: the
__ripSource phantom brand is cast on at source()'s returns, the family
function-with-members is `any`, unwrapStash casts past its RAW escape
hatch, createStash defaults `{} as D`, and the watcher Sets carry their
callback signature. The resetSources SIGNALS guard is gone — for own
lowers to for..in, where a symbol key can never appear.
Deliberate-garbage tests (`ws.read(42)`, `createApply({})`) carry
`# @ts-expect-error`: every directive absorbs a live error today, and
TS2578 flags any that go stale. packages corpus: 84 -> 13, app 0.
test/corpus is a compilation fixture set, not a type-correct program: free names are its idiom, and its own gates enforce a TS1xxx syntax floor plus designated clean rows. A repo-root `rip check` (and an editor opening a fixture) should therefore see silence, not 177 deliberate unresolveds. The marker package.json carries rip.noCheck for the whole directory — config living with the thing it describes, same as test/audit's own package.json — and the files stay in the program, so hover and completions keep answering. Root `rip check .`: 206 -> 29, every remainder a real program (examples/cart, parked packages/server, the disposable tmp-consumer).
corpus/gradual is the Diagnostics Audit's second mode: held.rip carries every family gradual holds — code-suppressed, gate-held, posture-held, floored, and the declared-but-uninstalled hold — one section each, with no directives, so the file doubles as an in-tree canary the moment any hold regresses. The same text re-measured under the corpus's strict config must publish every family gradual-pins.json pins: a family quiet in both modes is `vacuous`, never a pass, which is what makes the pair proof against a toolchain default flip rather than a photo of today's suppressions. published.rip pins the other side — reach by annotation, by flow, by compiler-typed construction, and the always-reported defects — and `rip check` runs over a stripped copy so the CLI and the editor prove they share the gate. Two contract invariants gate it all. Building the pair surfaced two defects the same day. The held fixture leaked on its first run: a gradual package nested in a strict workspace rode the strict program's posture and refused floors — the mirror image of the nested-rip.strict case. The auto boundary now follows the MODE FLIP in either direction (check.js, server.js), and a tsconfig above the flipped package no longer swallows the boundary, since a wrapper's posture is the wrapper's, not the package's. The vscode suite then caught `strict: false` collapsing every type the face routes through a `.call` — component computed members hovered `any` — because strictBindCallApply had silently left with the family. The posture is now spelled in the OTHER direction: gradual rides TypeScript 7's default strict and subtracts exactly three named loosenings (strictNullChecks, useUnknownInCatchVariables, noImplicitThis). A future strict-family member therefore arrives ON, where any noise it brings leaks into the canaries loudly, instead of OFF, where it degrades inference silently. Teeth verified by sabotage: reverting the posture reds gradual.held with the exact leaked families, on both instruments. Full audit 36/36; packages corpus stays 13, repo root 29.
The README described the example rather than stating anything the
example could not: a hand-kept Layout table that had already drifted
(it credited index.rip with the migrate/seed that setup.rip owns) and a
roadmap sentence promising a swap to @rip-lang/db. index.rip's header
comment restated the README's opening line and pointed at it. Neither
survives the rule that a doc says what must stay true; the directory,
the imports, and three statements say the rest.
The two mutation handlers go back to v3's plain calls. A bang inside an
arrow makes the arrow async — `(-> placeOrder!)` is `async function(){
return await placeOrder(); }` — so the click and submit handlers had
quietly become async while the sweep around them read as formatting.
Nothing consumes the result and createMutation routes its own failures
through onError, so the await bought nothing it did not also hide.
Bang-calls keep the paren-less spelling AGENTS.md documents.
The closure must be compiled and checked whole — a target's types cannot resolve otherwise — but reporting it whole made every file in the closure the caller's problem. Checking examples/cart/api returned one diagnostic of cart's own and five from packages/server, and exited non-zero for all six, so a package could not go green while any dependency it imports was red. One of those five was worse than noise: the phantom TS2307 on a dynamic `import!` target appears only from a consumer, so cart's author saw a defect server's own check cannot reproduce. Diagnostics now report only for the requested paths. A dependency's count rides one summary line naming where to look, in the report's own currency (error and warning, no unused/deprecated fade classes) and covering the dependency files this closure reached. Its HIDDEN families still count as before: those name which package.json a `rip.strict` remedy belongs to, which is as true of a dependency as of a target. The editor already drew this line from the other side — it publishes per open document, so a dependency is silent until opened. The two surfaces now agree about scope as well as verdict. `rip check examples/cart`: 17 -> 12 with five accounted for; packages and the repo root are unmoved.
philiplindberg
marked this pull request as draft
August 5, 2026 12:07
Steve's App publication rewrite (#209) lands under the gate. Four conflicts resolved: packages/app — apply.rip, workspace.rip, and their tests take main whole. The type widenings this branch made there were fixes to `Passport`, a type the rewrite deletes; `ApplyOpts` grew a candidate parameter and a `reload` verdict that supersede the sync-return widening. The one fix still owed is unrelated to the conflict: the watcher Set is plain again, so `watcher event, path` is uncallable. test/toolchain/audit-contract.test.js — both sides added a summary field to the clean state (this branch's `gl`, main's `hp.untyped`); the mock carries both, and each side's contract rows keep their own fire entries. examples/cart/README.md — this branch deleted it as drifting prose; main updated it the same day for the publication lifecycle. Restored: a file its author is actively tending is not drift. `rip check packages` reads 38, up from 13 — every new one in the rewritten App, and none of them merge damage. Twenty are one class, an implicit return reaching a slot typed `void`, which this makes the third sighting of.
The section justified holding inference by the rule staying statable,
which is an argument about prose rather than about types. Measured
instead: what inference produces over unannotated Rip is dominated by
confident errors about correct code — a parameter typed from its `= {}`
default so every legitimate member access is missing, an object built
by spread read as closed to its initial keys, a Bun API unknown for
want of @types/bun. All of it lands where the author declined to
annotate, and only an annotation answers any of it. The case the other
side catches is real and is now named as such, along with the fact that
it appears nowhere in this repository.
A component member's type is rendered twice — the class declare and the same-name companion interface — so a fault in it reaches the editor twice. The companion built each line by flattening its segments (segmentsText drops every node and role), and the companion has no source line of its own, so the unmapped bytes fell to the component's $self cover. One unresolved type name therefore painted every line of its component red while the Problems count still read one, and that was true of every component in the repo, not one spelling. instanceTypeLines now returns segment lists. Each member line carries the member as its own cover with the annotation's segments nested inside, so a byte with a finer span uses it and a byte without one — the container's `value`, where TypeScript reports a computed cycle — lands on the member rather than the component. The container spells its type twice (`value` and `read()`'s return); both are the same annotation and both now carry its span. Rendering the type twice also means one claim can arrive on spans that NEST rather than match, which the existing exact-range collapse could not see: `: T` against `T`. Same code, severity and message over a containing range is the same claim, so the narrower span keeps it. Diagnosed by driving, and the first diagnosis was wrong: the missing node looked like the `isBehaviorProjected` condition, but attaching it only added a third diagnostic while the whole-component span survived. tsc over the face named the four references that actually exist. The suite missed it because the component diagnostics test asserts starts and a whole-component span has a correct start; its fixture also plants the fault in a render body, where no companion row is involved. The new test plants it on a member and asserts both ends. `rip check examples/cart`: 12 -> 9.
A directive is charged by a diagnostic STARTING on its governed line, so retiring rows before that loop can retire the only row that starts there. Two renderings of one claim need not share a start — the wider span can begin a line or more above the narrower — and dropping the wider one first left the directive uncharged, resurrecting the TS2578 the charge exists to drop: an `@ts-expect-error` reported unused while it is in fact suppressing an error. The collapse now runs last, on what survives, and its tie-break states the whole rule rather than a case: the narrowest span keeps the claim, exact ties go to the first. The previous spelling deferred to index order only for mutually-contained spans, which the identity collapse above has already removed — a branch that could not run, reading as though it governed something. Both pinned, and the ordering pin was watched to fail with the collapse moved back ahead of the charging loop.
Rip erases annotations, but the import naming the annotated type was
emitted whole, so JS asked for a binding the module exports only as a
type and the module died at load — `SyntaxError: Export named 'X' not
found` — while `rip check` reported it clean. A green checker over a
program that cannot load is the worst shape a type story has, and it
had already cost the cart example its imports: the port dropped the
names to keep the app running, which is why they read as unresolved.
The test is LOCAL and needs no cross-module resolution. Type syntax is
absent from the s-expression tree, so a name used only in an annotation
appears nowhere in the value tree; a name that also appears among the
recorded type-role spans is one a type genuinely uses. Both halves fail
the safe way — an over-counted use or an unlisted type role only
declines an elision, where the opposite emits an import that cannot
resolve.
Being unreferenced is not enough on its own. An import nobody mentions
at all is dead code, and rewriting it away is not the emitter's
business; `import { useState } from 'react'` in a file that never says
`useState` still emits.
The face keeps every name — the annotations have to resolve there — so
the erased specifier and its separator ride a TS-only region and strip
identity holds byte for byte. When the whole clause erases, the clause
and its `from` ride the region and a bare `import 'mod'` remains: the
module still RUNS, where dropping the statement would have discarded
its side effects in silence.
Pinned by loading the emission, not by reading it: both runtime pins
were watched to fail with the elision disabled, each on the SyntaxError
this commit exists to prevent. Cart's routes go back to the spelling v3
wrote, and `rip check examples/cart` reads 12 -> 8.
A bind into a chain notifies the root container through `touch` — a
nested write changes no container identity — but the face spelled no
`touch`, so every such bind drew TS2339.
The seam has two honest spellings, and a container position takes the
one matching its provenance. A slot holding a container `__state`
minted has `touch` outright, so a consumer calls it unguarded; a slot
that ACCEPTS a container takes whatever arrives on its bind channel,
and the sharing contract admits a caller-supplied `{ value, read }`
the runtime treats as a container but which has no `touch`. Public is
the line, not the member kind: a defaulted prop carries kind 'state'
while `_init` still reads its bind slot first.
A computed has no notify seam at runtime and spells neither.
The editor's cell-hover presenter matches the container shape
verbatim, so it accepts both spellings or leaks the raw container into
every `count := 0` hover.
A derived binding (`UserPublic = User.pick("id", "email")`) builds its
schema by calling the algebra, so the checker types the VALUE from the
intrinsic signatures — but the name meant nothing in type space, and
`form: UserPublic` reads it as one. TS2749 on every such annotation.
The companion is the RESOLVED shape, projected by the folder that
already backs the browser bundle, so a derivation types as what the
runtime builds rather than as a second spelling of the algebra. The
two are checked against each other: rows assign the companion and the
call's result each to the other, so a folder drifting from
`projectableFields` fails at the checker instead of shipping a shape
nothing computed.
The face states no const — the algebra call types the value, and a
fold that disagreed must not become a diagnostic on the user's line.
The declaration road states one: it has no call to infer from, and a
type name whose value never declared cannot be called.
Silence stays the answer wherever the shape is not knowable. Folding
already refuses an unknown base, dynamic keys, and a mixin. A name the
module binds MORE than once joins them: it holds two shapes, one alias
cannot describe both, and two claims on one type name would reject a
program that compiles fine with no companion at all.
philiplindberg
marked this pull request as ready for review
August 5, 2026 22:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gradual mode: publish where type information reaches
rip check packagesonmainreports thousands of errors across most of the repo. On this branch it reports dozens — a list short enough to read, where each entry is a decision worth making rather than noise to wade through. Run it both ways; the change in kind is the claim, not any particular number.Nothing was suppressed to get there. The old model filtered by diagnostic code — a fixed list of families to hide — and that cannot hold a line: the same code means "the author asked for this" in one file and "nobody wrote a type here" in the next, so the list either drowns real defects or hides them.
main's count is what that looks like once a repo grows.The rule
Gradual mode publishes a diagnostic where type information actually reaches the line: an annotation in the declaration's header, the compiler's own types (schemas, components), flow along assignment, or an import of a typed export.
Inference alone never publishes, and that is the one part worth arguing about. Measured across the corpus, what inference produces over unannotated Rip is overwhelmingly confident errors about correct code: a parameter typed from its
= {}default, so every legitimateopts.foois "does not exist on type{}"; an object built by spread and later read for another key; Bun APIs flagged because@types/bunis not installed. The case it would genuinely catch —answer = 42later misused as a string — does not occur anywhere in this repo. So the trade is not free inference against a tidy rule: it is a large volume of wrongness about code whose author declined to annotate, against a class we could not find a single instance of. An annotation remains how you ask for more.Names and modules that don't resolve, and definition cycles, publish in every mode. No annotation answers a typo.
rip.strictpublishes everything, unchanged.The gate lives in
packages/vscode/src/scopes.js, shared verbatim by the editor andrip check, and it fails open: a source the lexer refuses publishes everything.What it found
The point of a readable corpus is that real defects stop hiding in it. Getting there turned up:
packages/db(08c1ccc).exec,listTables, anddescribeTableeach returned{ success: false, error, … }— shorthand for a binding namederrorthat doesn't exist; the catch bindserr. Every error path threw a ReferenceError from inside its own error handler. Runtime-confirmed before fixing.packages/stamp(9c5e962).globalThis.sh ??= shwas already the idiom; the compiler just didn't read it as a declaration. It does now — typed, and scoped to the declaring package via an automatic project boundary, so a non-importing neighbor still gets its cannot-find. The package went quiet with no edits to it.stricton by default (cc890e7). Probed head-to-head against 5.9: an empty config flagslet x: string = nullon 7 and passes on 5.9. Gradual now rides that default and subtracts three named loosenings rather than guessing at sub-flags, so a future strict-family member arrives visible instead of silently degrading inference.98220cd). A generic cast ending an indented block (return g(x) as D<T>) had its trailing>read as a comparison awaiting an operand, which swallowed the dedent and nested the next statement inside the block as dead code after the return. Cast heads now end their line like every other type head.d436007).9275b58deliberately removed the provisional: anys from the server reconstruction; the next day's burn-down rewrote the line and carried one back in. That single annotation wasmanager.rip's only one, and it opened most of the file.How it's gated
test/audit/corpus/gradual/is a suppression matrix, not a snapshot:held.ripcarries every family gradual holds — code-suppressed, gate-held, posture-held, floored, install-advised — with no directives, so it doubles as an in-tree canary: any hold regressing turnsrip checkred on its own.vacuous, never a pass — that is what stops the matrix from certifying whatever the checker happens to do today.published.rippins the other side: reach by annotation, by flow, by construction, and the always-reported defects.rip checkruns over the same pair and must agree with the editor, which is the seam where the two surfaces previously drifted.Two contract invariants gate it. Teeth verified by sabotage: reverting the posture reds
gradual.heldwith the exact families that leaked.Also here
9d6ffff). The closure is still compiled and checked whole, but a dependency's diagnostics report through its own check — previously a package couldn't go green while anything it imported was red.test/corpusdeclares itself a no-check zone (7d426e2). It's a compilation fixture set whose free names are deliberate; its own gates enforce a syntax floor and designated clean rows. Files stay in the program, so hover and completions keep working.e6b1ea5). A component member's type is rendered twice — the class declare and the same-name companion interface — and the companion flattened its segments, so the unmapped bytes fell to the component's cover. One unresolved type name painted every line of its component red while the Problems count still read one, in every component in the repo. Member lines now carry their own cover with the annotation nested inside, so a computed cycle still anchors per computed while an annotation fault anchors on the annotation.Found, and fixed
Porting
examples/cartfrom v3 surfaced three v3→v4 regressions, each verified head-to-head against v3 on identical — in one case byte-identical — source. All three are fixed here, and cart now checks clean.Type-only imports were no longer elided. v4 emitted a real JS import for a name used only in type positions, so the module died at load with
SyntaxError: Export named 'X' not found— whilerip checkreported "No type errors". A green checker on a program that cannot load. Elision is local analysis, as it was in v3: a name that appears in a type-role span and nowhere in the value tree. Pinned by writing both modules and importing one, because nothing short of loading the emitted module can see this — the face type-checks and the JS looks reasonable either way.Schema derivations got no type companion. A declared schema emits a value and a same-name type;
UserPublic = User.pick(…)emitted only the value, so annotating with it drew TS2749. The companion is now the resolved shape, projected by the folder that already backs the browser bundle — so a derivation types as what the runtime builds rather than as a second spelling of the algebra, and reads as a shape rather than aPick<…>to apply. The two computations are checked against each other: a folder drifting fromprojectableFieldsfails at the checker instead of shipping a shape nothing computed.The reactive container's face type omitted
touch. The<=>lowering calls.touch?.()on a chain's root container, which the type declarations didn't admit. The seam now has two spellings, chosen by how the slot got its container: a slot holding one__stateminted saystouch(): void, so a consumer calls it unguarded; a slot that accepts a container saystouch?(): void, because the sharing contract admits a caller-supplied{ value, read }that the runtime treats as a container but which has notouch. Spelling the first optional was measured, not assumed — underrip.strictthe guardless call draws TS2722 on a notify that cannot be absent.Still open from the same port: dynamic
import!targets don't join the closure, so a consumer sees a phantom TS2307 inside a dependency that the dependency's own check can't reproduce.Deliberately not fixed here
The corpus still reports diagnostics in
packages/appandpackages/server. They are not oversights — each is a decision rather than an edit, and the decisions travel in their own PR. The largest is one shape, not a list: an implicit return reaching a slot typedvoid | T, where TypeScript's void-return exception does not apply through the union. That wants a compiler-level answer, which is why no site is patched here.Both packages are under active work, so read the current output rather than anything quoted here.
Review notes
docs/TYPES.md(§ Diagnostic publishing, § Declared globals, § Project configuration). There is no standalone design doc on purpose: the design lives in the code it governs, the doc, and the tests.mainwas briefly force-rewound during this work to undo commits that leaked onto it. If you fetched on 2026-08-05, re-fetch.