@@ -404,6 +404,161 @@ const queryOptionsPlugin = {
404404 } ,
405405} ;
406406
407+ // ---------------------------------------------------------------------------
408+ // [#6399] `@objectstack/verify` structural stand-in erasure guard.
409+ //
410+ // The third member of the family above, and the narrowest. `checkReadCoercion`
411+ // and `checkDateBucketParity` take their driver STRUCTURALLY — `CoercibleDriver`
412+ // / `BucketableDriver` — so an out-of-tree driver (cloud's `driver-turso` in
413+ // remote mode) can run the identical contract without importing a concrete
414+ // driver type. That parameter type is not decoration around the check: for the
415+ // compile-time half of the conformance it IS the check. Assert the argument and
416+ // the stand-in stops standing for anything, at that call site, while the code
417+ // reads exactly like the checked kind.
418+ //
419+ // #6354/PR #6396 is the bill: TEN `as never` casts — every call site of both
420+ // helpers — had switched that half off, long enough that nobody remembered
421+ // writing them. They were provably dead (removing all ten left three packages'
422+ // typecheck at exit=0) and the compile-time check they had been hiding is
423+ // provably live (adding a member no real driver can have turned all ten sites
424+ // red, 8+2 matching the cast count exactly). Nothing rang for either fact.
425+ //
426+ // ⚠️ The cost is highest on the FAKE-driver side. Six of the ten sites pass a
427+ // hand-written literal; four pass a real driver. A real driver comes from
428+ // production code and mostly satisfies the stand-in whether or not anyone
429+ // checks — a hand-written fake is precisely the thing that drifts, and it is
430+ // the arm an assertion silences most cheaply.
431+ //
432+ // Scope is argument 0 — the driver — and nothing else. The options bag is a
433+ // different type with its own `unknown` slots, and an assertion there is
434+ // #6394's subject, not this rule's.
435+ //
436+ // WHY A DEDICATED RULE, not a widened `check:query-options-erasure`: measured,
437+ // that ratchet cannot reach these sites at all. `query-options/no-any-erasure`
438+ // keys on a MEMBER-expression callee named `find|findOne|count|aggregate` and
439+ // only inspects arguments at index >= 1; every site here is a bare-identifier
440+ // callee with the driver at index 0. Teaching it the word `never` would have
441+ // matched zero of the ten while pulling several hundred unrelated `as never`
442+ // sites into its baseline and blurring what "query/options type erasure" means.
443+ //
444+ // WHY NOT a blanket ban on `as never` at call arguments in tests: 550 of the
445+ // repo's 703 `as never` assertions sit at a call-argument position, 536 of them
446+ // in test files across 33 packages, and the large majority are legitimate —
447+ // a negative test constructing input `tsc` is supposed to refuse. That is the
448+ // same trade-off QUERY_OPTIONS_TEST_GLOBS already resolved the same way: a
449+ // blocking rule there fights the tests that prove the contract is enforced.
450+ //
451+ // The guarded set is reconciled against `packages/verify/src` in BOTH
452+ // directions by `pnpm check:verify-stand-in`, so a third stand-in check cannot
453+ // arrive unguarded and a renamed helper cannot leave this rule silently
454+ // matching nothing. A guard whose covered set is a hand-list nobody re-checks
455+ // is the dead-pin shape (#4984 / #5018), and this one is not allowed to become
456+ // it.
457+ export const VERIFY_STAND_IN_CHECKS = {
458+ checkReadCoercion : 'CoercibleDriver' ,
459+ checkDateBucketParity : 'BucketableDriver' ,
460+ } ;
461+
462+ // The rule's own id, exported so `check:verify-stand-in` identifies this rule's
463+ // reports exactly rather than by message text — same reasoning as
464+ // QUERY_OPTIONS_RULE_ID.
465+ export const VERIFY_STAND_IN_RULE_ID = 'verify-stand-in/no-asserted-driver-argument' ;
466+
467+ export const VERIFY_STAND_IN_MESSAGE =
468+ 'Do not type-assert the driver argument of a @objectstack/verify conformance check. ' +
469+ '`checkReadCoercion(driver)` / `checkDateBucketParity(driver)` declare that parameter as a ' +
470+ 'structural stand-in (`CoercibleDriver` / `BucketableDriver`) so any driver — including an ' +
471+ 'out-of-tree one — can run the identical contract; that declaration is the compile-time half ' +
472+ 'of the conformance, and an assertion on the argument deletes it for this call site while ' +
473+ 'looking identical to a call that has it. Ten such casts (`as never`, every call site of both ' +
474+ 'helpers) lived in this repo long enough that nobody remembered writing them — all ten dead, ' +
475+ 'and the check underneath them alive (#6354 / PR #6396). Six of the ten passed a HAND-WRITTEN ' +
476+ 'fake driver, which is the arm that actually drifts. Pass the driver unasserted. If it does ' +
477+ 'not satisfy the stand-in, that is the finding — fix the driver or widen the stand-in ' +
478+ 'deliberately, in `packages/verify/src`, where the change is reviewed once instead of ' +
479+ 'silenced per call site. See issues #6354, #6394 and #6399.' ;
480+
481+ const verifyStandInPlugin = {
482+ rules : {
483+ 'no-asserted-driver-argument' : {
484+ meta : {
485+ type : 'problem' ,
486+ docs : {
487+ description :
488+ 'Ban type-asserting the driver argument of a @objectstack/verify structural conformance check.' ,
489+ } ,
490+ schema : [ ] ,
491+ messages : { erased : VERIFY_STAND_IN_MESSAGE } ,
492+ } ,
493+ create ( context ) {
494+ const guarded = new Set ( Object . keys ( VERIFY_STAND_IN_CHECKS ) ) ;
495+
496+ /**
497+ * True when `node` is, or wraps, ANY type assertion.
498+ *
499+ * Deliberately wider than `erasesToAny` above: on this argument there is
500+ * no assertion worth allowing. `as unknown as BucketableDriver` is the
501+ * sanctioned escape for engine query OPTIONS because a test may need
502+ * off-contract input on purpose; here the parameter type is the contract
503+ * UNDER TEST, so re-labelling the argument with it asserts exactly the
504+ * thing the call was supposed to prove. Every one of the ten historical
505+ * casts would be re-admitted by an `any`-only test.
506+ */
507+ const isAsserted = ( node ) => {
508+ for ( let cur = node ; cur ; cur = cur . expression ) {
509+ if ( cur . type === 'TSAsExpression' || cur . type === 'TSTypeAssertion' ) return true ;
510+ if ( cur . type === 'TSNonNullExpression' ) continue ;
511+ return false ;
512+ }
513+ return false ;
514+ } ;
515+
516+ /**
517+ * True when `name` resolves, in scope, to a variable declared `: any` or
518+ * `: never` — the split form (`const d: any = brokenDriver(); check(d)`),
519+ * which erases the stand-in exactly as the inline assertion does and is
520+ * the first shape someone reaches for once the inline one is blocked.
521+ * Scope analysis, not a name heuristic — same mechanism, and the same
522+ * reason, as `slot-lookup/no-any-assignment`.
523+ */
524+ const declaredErasedVariable = ( name , node ) => {
525+ for ( let scope = context . sourceCode . getScope ( node ) ; scope ; scope = scope . upper ) {
526+ const variable = scope . variables . find ( ( v ) => v . name === name ) ;
527+ if ( ! variable ) continue ;
528+ return variable . defs . some ( ( d ) => {
529+ const kind = d . node ?. id ?. typeAnnotation ?. typeAnnotation ?. type ;
530+ return (
531+ d . node ?. type === 'VariableDeclarator' &&
532+ ( kind === 'TSAnyKeyword' || kind === 'TSNeverKeyword' )
533+ ) ;
534+ } ) ;
535+ }
536+ return false ;
537+ } ;
538+
539+ return {
540+ CallExpression ( node ) {
541+ // Bare-identifier callee only. These names are distinctive enough to
542+ // treat as reserved, and matching the name rather than the resolved
543+ // import is what keeps a re-export or a test-local alias from
544+ // quietly leaving the rule behind.
545+ if ( node . callee ?. type !== 'Identifier' || ! guarded . has ( node . callee . name ) ) return ;
546+ const driver = node . arguments ?. [ 0 ] ;
547+ if ( ! driver ) return ;
548+ if ( isAsserted ( driver ) ) {
549+ context . report ( { node : driver , messageId : 'erased' } ) ;
550+ return ;
551+ }
552+ if ( driver . type === 'Identifier' && declaredErasedVariable ( driver . name , node ) ) {
553+ context . report ( { node : driver , messageId : 'erased' } ) ;
554+ }
555+ } ,
556+ } ;
557+ } ,
558+ } ,
559+ } ,
560+ } ;
561+
407562export default [
408563 {
409564 files : [ '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}' ] ,
@@ -629,4 +784,36 @@ export default [
629784 plugins : { 'query-options' : queryOptionsPlugin } ,
630785 rules : { 'query-options/no-any-erasure' : 'error' } ,
631786 } ,
787+ // issue #6399 — @objectstack/verify stand-in erasure guard. Rationale and the
788+ // #6354 / PR #6396 measurement are on `VERIFY_STAND_IN_MESSAGE` above.
789+ //
790+ // No `ignores` beyond the build dirs and NO baseline, which is the whole
791+ // reason this is its own rule rather than a widening of one of the two above:
792+ // the tree is clean TODAY (all ten casts removed by PR #6396), so the guard
793+ // starts at zero and every future violation is a new one. Both siblings had
794+ // to grandfather hundreds of pre-existing sites; there is nothing here to
795+ // grandfather, and adding one later would mean the state stopped being locked.
796+ //
797+ // Scope is unrestricted on purpose. The four files holding call sites today
798+ // are `packages/qa/dogfood/test/` and `packages/drivers/driver-turso/src/`,
799+ // but `@objectstack/verify` is a PUBLISHED helper whose whole point is being
800+ // callable from anywhere — a package-scoped rule would go quiet exactly when
801+ // the eleventh call site lands somewhere new, which is the case this issue
802+ // exists to cover.
803+ //
804+ // ⚠️ Test files are IN scope here, unlike the query-options rule. That rule
805+ // lifts them because a test may legitimately need off-contract engine input;
806+ // this argument has no legitimate off-contract form (see the message), and
807+ // six of the ten historical casts were in test files passing hand-written
808+ // fakes — the arm the guard is worth the most on.
809+ {
810+ files : [ '**/*.{ts,tsx,mts,cts}' ] ,
811+ ignores : [ '**/node_modules/**' , '**/dist/**' , '**/build/**' , '**/.next/**' , '**/.turbo/**' ] ,
812+ languageOptions : {
813+ parser : tsParser ,
814+ parserOptions : { ecmaVersion : 'latest' , sourceType : 'module' } ,
815+ } ,
816+ plugins : { 'verify-stand-in' : verifyStandInPlugin } ,
817+ rules : { 'verify-stand-in/no-asserted-driver-argument' : 'error' } ,
818+ } ,
632819] ;
0 commit comments