refactor(rest)!: ImportProtocolLike declares the request each of its three required members receives - #17420
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6cbf6588aa3d37fdf02f62ddf7d066c6d78d5675 && git checkout 6cbf6588aa3d37fdf02f62ddf7d066c6d78d5675
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cefe06870260ba7a5a85251189d6e663e991d9b9 3ce4fc73220d8d12d00a2cfd0b30ea3f20b6161c && git checkout -B drift-repro cefe06870260ba7a5a85251189d6e663e991d9b9 && git merge --no-ff 3ce4fc73220d8d12d00a2cfd0b30ea3f20b6161c
node scripts/docs-audit/affected-docs.mjs --json cefe06870260ba7a5a85251189d6e663e991d9b9
|
Fixes #16952
Clause-②: yes
ImportProtocolLikeis an exported extension point, and all three of its required members declaredargs: any.runImportaccepts an injected protocol through it, so an implementor had no contract to compile against and could only freeze on the spelling it happened to observe. This gives the declaration a real type.Option 1 as ruled. All three methods, not only
findData— #16950 deliberately left all three alone because narrowing a published extension point is a contract decision rather than a spelling fix.What changed
The envelope the runner adds is declared once and exported, and each required member names the spec request it receives:
Nothing about the values the runner sends moves — the three literals are byte-for-byte the canonical ones #16950 landed.
findArgsBaseis untouched, deliberately: the census pin reads its exact signature, and with the interface typed the return annotation would be redundant.Converging the three non-authoritative sites, in the same stroke
Triage's binding instruction:
Two of the three moved when #16950 landed, before this branch existed:
admin-import-users.ts:352and:354now read the canonical keys and say why. What was left was the third — a local parameter annotation in a test double, plus its siblings. Every double in this package now derives its annotation from the exported declaration itself rather than restating a shape:A derived alias cannot become a fourth truth: revert the declaration and the alias reverts with it, which is what makes the ablation below possible. Ten inline restatements and five
args: anyannotations are gone; the file-localFindProbetype in the idempotency suite is now that alias.The pins, and the ablation that shows they can fail
Two layers, because a type-level narrowing is invisible to a runtime test and a source-level revert is invisible to the type checker.
rest-server-canonical-query-ast.test.ts) — the existing erasure detector matched(query: anyand(request: any, which is why it swept the whole file and reported nothing while the exported extension point declared no dialect at all. The name an implementor actually writes isargs; it is now in the alternation, with a control asserting the detector fires on the exact spelling this PR removed and stays quiet on the declared form.@ts-expect-errordirectives over the wire dialect and the required members, all written against aliases derived from the declaration, plus a positive case showing an implementor that leaves its parameter unannotated gets typed by the contract.Ablation — the declaration reverted to its pre-card spelling, both legs proven on disk and in
dist/:Every directive is live rather than decorative:
tsc --listFiles -p tsconfig.test.jsonlists this file, so an unused directive is TS2578 here, and the ablation is exactly the run that turns them unused.⭐ The card asked for pins that would go red on the degraded behaviour rather than pass vacuously. The vacuity being closed is measurable: reverting the declaration does not merely leave the pins passing, it makes five of them structurally incapable of failing — and that is what reddens.
The cross-lane consequence: measured, and FALSIFIED
Triage expected
plugin-auth's hand-written implementor to stop compiling, and flagged it unmeasured. It does not break.async findData(args: any)stays assignable to the narrower signature — ananyparameter is bivariant with everything.packages/plugins/plugin-auth/**is untouched by this diff.That green is reverse-verified rather than assumed, because a clean run against a stale
.d.tswould look identical. A probe carrying a key the new type refuses was appended to that file, and it reddens:restored by blob hash against HEAD (
a557f3ca…both sides) withgit diff HEADempty for that path. The probe was a measurement, never a delivered edit.Acceptance notes
rest-server.tswas not needed and is not touched. It declares a structurally identical local envelope alias for its own dispatch sites. Converging the two would edit a file a sibling PR holds, so this PR declares its own and the divergence (context?: anyhere, matching what this interface's members already spelled, versuscontext?: unknownthere) is stated in the type's docblock. noted, not filed — the successor is whoever next unifies the two envelopes, which is one edit oncerest-server.tsis free.anyopts back out — the annotation wins over the contextual type, so the contract reaches nothing. That is exactly the stateplugin-auth's implementor is in today: it compiles, it reads the right keys, and it is not held to them. Stated in the interface docblock and in the changeset's migration line; not fixed here, because that file is another lane's. noted, not filed.check-adr-0087-registration'stype-surface-onlycategory cannot express a PARAMETER narrowing. Its predicate 4 reads a return annotation or an exported type declaration, and an interface whose members takeanyreads as already CONCRETE, so the category refuses. A truthful disposition was still available (no-migration-prescription, verified green) so nothing was blocked and nothing is filed — but the next author narrowing a published parameter will reach fortype-surface-onlyfirst and be refused by a predicate that is measuring the wrong position. noted, not filed.$top ?? 2, the sibling default triage named, is already gone — fix(rest): import-runner builds the canonical QueryAST through a typed findData envelope #16950 removed it along with?? {}. No??fallback was added anywhere in this diff; the doubles read the canonical key straight and throw on absence, which is the loudness the card asked for.minor, notmajor—check-changeset-no-majoris green, and this repo records breaking-ish narrowings asminor. The breaking nature, FROM/TO and the implementor migration line are spelled in prose there.Verification
pnpm --filter @objectstack/rest testpnpm --filter @objectstack/rest typechecktsc --noEmit+check:test-typecheck: 0 files / 0 errors)pnpm --filter '@objectstack/rest^...' buildpnpm --filter @objectstack/plugin-auth exec tsc --noEmitpnpm lintdispatch-gates --ranNOT MEASURED:
check:dual-build-cjs-loadsandcheck:type-check-debt, bothPREREQUISITE NOT MET(exit 3) — they read built output for the whole workspace and ask for a fullpnpm buildfirst, which is CI's farm-wide run, not this container's. ⛔ Neither is a finding and neither is a pass.Gate verdicts were captured to disk before any of them was read, and reconciled with
node scripts/pm/dispatch-gates.mjs --ran; the sweep and the table above were run at3ce4fc73, the final commit, afterorigin/mainwas merged in.Generated by Claude Code