2020 * set and `\S` is its complement, so the pin is the whole argument; JSON
2121 * Schema specifies `pattern` as an ECMA-262 regex, which is the same engine
2222 * this assertion runs on.
23+ * - `banned-keys` — over the whole key-presence lattice, with a
24+ * present-but-`null` value, and on a name `Object.prototype` carries, which
25+ * is the one JSON shape where "own property" and `in` come apart.
2326 *
2427 * ## Why an equality pin and not a comment
2528 *
@@ -44,11 +47,13 @@ import {
4447 NON_BLANK_PATTERN ,
4548 NON_BLANK_STRING ,
4649 PROJECTABLE_REFINEMENT_PATTERNS ,
50+ bannedKeys ,
4751 dependentRequired ,
4852 projectableRefinementOf ,
4953 requiredOneOf ,
5054} from '../src/shared/refinement-projection' ;
5155import { SSLConfigSchema } from '../src/data/driver-sql.zod' ;
56+ import { TraceSamplingConfigSchema } from '../src/system/tracing.zod' ;
5257import {
5358 emitProjectableRefinement ,
5459 projectPublishedJsonSchema ,
@@ -85,6 +90,30 @@ const requiredOneOfSatisfied = (node: Record<string, unknown>, doc: Record<strin
8590 return branches . some ( ( branch ) => branch . required . every ( ( key ) => Object . prototype . hasOwnProperty . call ( doc , key ) ) ) ;
8691} ;
8792
93+ /**
94+ * The node's banned-key rule — `propertyNames.not.enum`, wherever the emitter put
95+ * it — evaluated the way a validator would, and refusing to report anything when
96+ * the node carries no such rule, so a projection that stopped emitting fails
97+ * rather than passing vacuously.
98+ *
99+ * Both placements are read because the emitter chooses between them by what the
100+ * node already carries: a record already states `propertyNames: { type:
101+ * 'string' }`, so its ban is conjoined through `allOf`; a bare object has no
102+ * `propertyNames` and takes the rule directly.
103+ */
104+ const bannedKeysSatisfied = ( node : Record < string , unknown > , doc : Record < string , unknown > ) : boolean => {
105+ const clauses = [ node , ...( ( node . allOf as Record < string , unknown > [ ] | undefined ) ?? [ ] ) ] ;
106+ const banned = clauses
107+ . map ( ( clause ) => ( clause . propertyNames as { not ?: { enum ?: string [ ] } } | undefined ) ?. not ?. enum )
108+ . filter ( ( list ) : list is string [ ] => Array . isArray ( list ) ) ;
109+ if ( banned . length === 0 ) {
110+ throw new Error ( 'the node carries no propertyNames.not.enum — nothing to evaluate' ) ;
111+ }
112+ return banned . every ( ( list ) =>
113+ Object . keys ( doc ) . every ( ( name ) => ! list . includes ( name ) ) ,
114+ ) ;
115+ } ;
116+
88117/**
89118 * ECMA-262 WhiteSpace ∪ LineTerminator, by code point so no control byte is
90119 * ever written into this file (`scripts/check-nul-bytes.mjs` is the authority
@@ -98,7 +127,7 @@ const BLANK_CODE_POINTS = [
98127] ;
99128
100129describe ( 'the list of projectable patterns is CLOSED' , ( ) => {
101- it ( 'names exactly the two arms this change landed' , ( ) => {
130+ it ( 'names exactly the arms this list has landed, and nothing else ' , ( ) => {
102131 // ⛔ Growing this is a public-contract decision: every arm narrows a
103132 // published artifact. A new arm updates this line in the same PR, which is
104133 // what makes it a reviewed diff rather than a quiet widening of the
@@ -107,6 +136,7 @@ describe('the list of projectable patterns is CLOSED', () => {
107136 'required-one-of' ,
108137 'non-blank-string' ,
109138 'dependent-required' ,
139+ 'banned-keys' ,
110140 ] ) ;
111141 } ) ;
112142
@@ -462,6 +492,175 @@ describe('dependent-required: one dependency map, read twice', () => {
462492 } ) ;
463493} ) ;
464494
495+ describe ( 'banned-keys: one key list, read twice' , ( ) => {
496+ it ( 'declares the keys it was given' , ( ) => {
497+ const rule = bannedKeys ( [ 'dialect' ] ) ;
498+ expect ( projectableRefinementOf ( rule ) ) . toEqual ( { pattern : 'banned-keys' , keys : [ 'dialect' ] } ) ;
499+ } ) ;
500+
501+ it ( 'emits `propertyNames` with a `not` over the names, when the node states none' , ( ) => {
502+ const node : Record < string , unknown > = { type : 'object' } ;
503+ emitProjectableRefinement ( node , { pattern : 'banned-keys' , keys : [ 'a' , 'b' ] } ) ;
504+ expect ( node ) . toEqual ( { type : 'object' , propertyNames : { not : { enum : [ 'a' , 'b' ] } } } ) ;
505+ } ) ;
506+
507+ it ( 'conjoins through `allOf` rather than replacing the `propertyNames` a record already states' , ( ) => {
508+ // A record emits `propertyNames: { type: 'string' }` of its own. Replacing
509+ // it would trade the key-TYPE rule the node already stated for the key-NAME
510+ // rule this arm adds, which is a narrowing paid for with a widening.
511+ const node : Record < string , unknown > = { type : 'object' , propertyNames : { type : 'string' } } ;
512+ emitProjectableRefinement ( node , { pattern : 'banned-keys' , keys : [ 'dialect' ] } ) ;
513+ expect ( node . propertyNames ) . toEqual ( { type : 'string' } ) ;
514+ expect ( node . allOf ) . toEqual ( [ { propertyNames : { not : { enum : [ 'dialect' ] } } } ] ) ;
515+ } ) ;
516+
517+ it ( '⛔ never writes a TOP-LEVEL `anyOf` or disturbs the node’s own shape' , ( ) => {
518+ const node : Record < string , unknown > = { type : 'object' , properties : { a : { type : 'string' } } } ;
519+ emitProjectableRefinement ( node , { pattern : 'banned-keys' , keys : [ 'b' ] } ) ;
520+ expect ( node . anyOf ) . toBeUndefined ( ) ;
521+ expect ( node . properties ) . toEqual ( { a : { type : 'string' } } ) ;
522+ } ) ;
523+
524+ it ( 'is idempotent — the same arm twice states one rule, not two' , ( ) => {
525+ const node : Record < string , unknown > = { type : 'object' , propertyNames : { type : 'string' } } ;
526+ emitProjectableRefinement ( node , { pattern : 'banned-keys' , keys : [ 'dialect' ] } ) ;
527+ emitProjectableRefinement ( node , { pattern : 'banned-keys' , keys : [ 'dialect' ] } ) ;
528+ expect ( node . allOf ) . toEqual ( [ { propertyNames : { not : { enum : [ 'dialect' ] } } } ] ) ;
529+ } ) ;
530+
531+ it ( 'drops an empty key list rather than publishing a rule that bans nothing' , ( ) => {
532+ const node : Record < string , unknown > = { type : 'object' } ;
533+ emitProjectableRefinement ( node , { pattern : 'banned-keys' , keys : [ ] } ) ;
534+ expect ( node ) . toEqual ( { type : 'object' } ) ;
535+ } ) ;
536+
537+ it ( 'the predicate and the keywords agree over the whole presence lattice' , ( ) => {
538+ const rule = bannedKeys ( [ 'x' , 'y' ] ) ;
539+ const node = publish ( z . record ( z . string ( ) , z . unknown ( ) ) . refine ( rule ) ) ;
540+ const keys = [ 'x' , 'y' , 'z' ] as const ;
541+ for ( let mask = 0 ; mask < 8 ; mask += 1 ) {
542+ const doc : Record < string , unknown > = { } ;
543+ keys . forEach ( ( key , i ) => {
544+ if ( mask & ( 1 << i ) ) doc [ key ] = 'v' ;
545+ } ) ;
546+ const asJson = JSON . parse ( JSON . stringify ( doc ) ) as Record < string , unknown > ;
547+ expect (
548+ rule ( asJson ) ,
549+ `runtime vs keywords disagree for ${ JSON . stringify ( asJson ) } ` ,
550+ ) . toBe ( bannedKeysSatisfied ( node , asJson ) ) ;
551+ }
552+ } ) ;
553+
554+ it ( 'a banned key present with a `null` value is PRESENT on both sides' , ( ) => {
555+ const rule = bannedKeys ( [ 'dialect' ] ) ;
556+ const node = publish ( z . record ( z . string ( ) , z . unknown ( ) ) . refine ( rule ) ) ;
557+ const doc = JSON . parse ( '{"dialect":null}' ) as Record < string , unknown > ;
558+ expect ( rule ( doc ) ) . toBe ( false ) ;
559+ expect ( bannedKeysSatisfied ( node , doc ) ) . toBe ( false ) ;
560+ } ) ;
561+
562+ it ( '⛔ judges OWN properties — a name `Object.prototype` carries is not "present" in an empty document' , ( ) => {
563+ // The measurement behind the predicate reading `hasOwnProperty` and never
564+ // `key in value`: `in` walks the prototype chain, so a ban spelled with it
565+ // would refuse `{}` itself while `propertyNames` accepts it. That is a
566+ // disagreement about a JSON DOCUMENT, not an edge outside the domain.
567+ const rule = bannedKeys ( [ 'toString' ] ) ;
568+ const node = publish ( z . record ( z . string ( ) , z . unknown ( ) ) . refine ( rule ) ) ;
569+ const empty = JSON . parse ( '{}' ) as Record < string , unknown > ;
570+ expect ( 'toString' in empty ) . toBe ( true ) ;
571+ expect ( rule ( empty ) ) . toBe ( true ) ;
572+ expect ( bannedKeysSatisfied ( node , empty ) ) . toBe ( true ) ;
573+ } ) ;
574+
575+ it ( 'LIT CONTROL — the same name written INTO the document is refused by both' , ( ) => {
576+ const rule = bannedKeys ( [ 'toString' ] ) ;
577+ const node = publish ( z . record ( z . string ( ) , z . unknown ( ) ) . refine ( rule ) ) ;
578+ const doc = JSON . parse ( '{"toString":"x"}' ) as Record < string , unknown > ;
579+ expect ( rule ( doc ) ) . toBe ( false ) ;
580+ expect ( bannedKeysSatisfied ( node , doc ) ) . toBe ( false ) ;
581+ } ) ;
582+ } ) ;
583+
584+ describe ( "the LIVE seam: the card's own worked instance stops saying yes" , ( ) => {
585+ /** The published structured-filter arm of `TraceSamplingConfig.composite[].condition`. */
586+ const structuredFilterArm = ( ) : Record < string , unknown > => {
587+ const node = publish ( TraceSamplingConfigSchema , 'input' ) ;
588+ const composite = ( node . properties as Record < string , Record < string , unknown > > ) . composite ;
589+ const item = composite . items as Record < string , Record < string , Record < string , unknown > > > ;
590+ const condition = item . properties . condition as unknown as Record < string , unknown > ;
591+ return ( condition . anyOf as Record < string , unknown > [ ] ) [ 0 ] ;
592+ } ;
593+
594+ /** A `TraceSamplingConfig` that parses, with only `condition` varying. */
595+ const parses = ( condition : unknown ) : boolean =>
596+ TraceSamplingConfigSchema . safeParse ( {
597+ type : 'composite' ,
598+ composite : [ { strategy : 'always_on' , condition } ] ,
599+ } ) . success ;
600+
601+ it ( 'states the ban, and keeps the record shape it always stated' , ( ) => {
602+ const arm = structuredFilterArm ( ) ;
603+ expect ( arm . type ) . toBe ( 'object' ) ;
604+ expect ( arm . propertyNames ) . toEqual ( { type : 'string' } ) ;
605+ expect ( arm . allOf ) . toEqual ( [ { propertyNames : { not : { enum : [ 'dialect' ] } } } ] ) ;
606+ } ) ;
607+
608+ it ( "the card's own specimen — `{ dialect: 'cel' }` — is refused by BOTH sides now" , ( ) => {
609+ const doc = JSON . parse ( '{"dialect":"cel"}' ) as Record < string , unknown > ;
610+ expect ( parses ( doc ) ) . toBe ( false ) ;
611+ expect ( bannedKeysSatisfied ( structuredFilterArm ( ) , doc ) ) . toBe ( false ) ;
612+ } ) ;
613+
614+ it ( '⛔ no document this arm ACCEPTS is refused by the emitted keywords' , ( ) => {
615+ const arm = structuredFilterArm ( ) ;
616+ const rule = bannedKeys ( [ 'dialect' ] ) ;
617+ const corpus : Array < Record < string , unknown > > = [
618+ { } ,
619+ { amount : { $gt : 1 } } ,
620+ { 'account.name' : { $eq : 'acme' } } ,
621+ { dialect : 'cel' } ,
622+ { dialect : null } ,
623+ { dialect : 'cel' , source : 'record.amount > 10' } ,
624+ ] ;
625+ for ( const doc of corpus ) {
626+ const asJson = JSON . parse ( JSON . stringify ( doc ) ) as Record < string , unknown > ;
627+ // Equality, not implication: this arm is exact, so a one-sided pin would
628+ // pass a projection that had stopped narrowing at all.
629+ expect (
630+ bannedKeysSatisfied ( arm , asJson ) ,
631+ `disagreement on ${ JSON . stringify ( asJson ) } ` ,
632+ ) . toBe ( rule ( asJson ) ) ;
633+ }
634+ } ) ;
635+
636+ it ( '⛔ and the EXPRESSION the runtime still accepts is still accepted by the file' , ( ) => {
637+ // The union's other arm is what carries a dialect-bearing document, so
638+ // narrowing the structured-filter arm refuses nothing the runtime accepts.
639+ const doc = { dialect : 'cel' , source : 'record.amount > 10' } ;
640+ expect ( parses ( doc ) ) . toBe ( true ) ;
641+ const node = publish ( TraceSamplingConfigSchema , 'input' ) ;
642+ const composite = ( node . properties as Record < string , Record < string , unknown > > ) . composite ;
643+ const item = composite . items as Record < string , Record < string , Record < string , unknown > > > ;
644+ const condition = item . properties . condition as unknown as Record < string , unknown > ;
645+ const envelope = ( condition . anyOf as Record < string , unknown > [ ] ) [ 1 ] ;
646+ const objectArm = ( envelope . anyOf as Record < string , unknown > [ ] ) [ 1 ] ;
647+ expect ( objectArm . required ) . toEqual ( [ 'dialect' , 'source' ] ) ;
648+ } ) ;
649+
650+ it ( 'LIT CONTROL — a structured filter with no `dialect` is accepted by both' , ( ) => {
651+ const doc = JSON . parse ( '{"amount":{"$gt":10}}' ) as Record < string , unknown > ;
652+ expect ( parses ( doc ) ) . toBe ( true ) ;
653+ expect ( bannedKeysSatisfied ( structuredFilterArm ( ) , doc ) ) . toBe ( true ) ;
654+ } ) ;
655+
656+ it ( 'its ledger row is gone because the site now reads `projected`, naming the arm' , ( ) => {
657+ const census = collectDroppedRefinements ( 'system/TraceSamplingConfig' , TraceSamplingConfigSchema ) ;
658+ const site = census . projected . find ( ( s ) => s . path === 'composite.element.condition.options[0]' ) ;
659+ expect ( site ?. declaredPatterns ) . toEqual ( [ 'banned-keys' ] ) ;
660+ expect ( census . dropped . map ( ( s ) => s . path ) ) . not . toContain ( 'composite.element.condition.options[0]' ) ;
661+ } ) ;
662+ } ) ;
663+
465664describe ( 'the verdict is adjudicated per NODE over every check on it' , ( ) => {
466665 const nonBlank = ( ) : z . ZodString => z . string ( ) . refine ( NON_BLANK_STRING , 'non-blank' ) ;
467666
0 commit comments