@@ -45,7 +45,7 @@ test("the reason factory's own return object declares no category", () => {
4545// --- Nothing but a literal is derived ---------------------------------------
4646
4747/**
48- * Every shape that six rounds of review argued about, plus the two that ended the argument .
48+ * Every shape that seven rounds of review argued about.
4949 *
5050 * Earlier revisions tried to decide these case by case — is this `entry` really the ownership
5151 * table's? — and each answer left the next construct open. They are one test now because the
@@ -141,16 +141,12 @@ const NOT_A_LITERAL: readonly { name: string; source: string }[] = [
141141 });` ,
142142 } ,
143143 {
144- // Round seven, first hole: the mutation is in an UPSTREAM callback, so a proof that only
145- // looked at the forwarding callback saw nothing wrong with the map below it.
146144 name : 'a mutating upstream filter callback' ,
147145 source : `${ TABLE }
148146 const bad = BUILD_OWNERSHIP.filter((entry) => { entry.rule = computed; return true; })
149147 .map((entry) => reason('lint', file, entry.rule, 'd'));` ,
150148 } ,
151149 {
152- // Round seven, second hole: `entry.mutate` is a member access whose object is `entry`, so a
153- // read/write classifier counted invoking it as a read.
154150 name : 'a member call on the entry before reading it' ,
155151 source : `${ TABLE }
156152 const bad = BUILD_OWNERSHIP.map((entry) => {
@@ -167,7 +163,7 @@ const NOT_A_LITERAL: readonly { name: string; source: string }[] = [
167163
168164for ( const { name, source } of NOT_A_LITERAL ) {
169165 test ( `fails closed: ${ name } ` , ( ) => {
170- assert . throws ( ( ) => readSelectorRules ( 'm.ts' , source ) , / r u l e a r g u m e n t i s n o t a s t r i n g l i t e r a l / ) ;
166+ assert . throws ( ( ) => readSelectorRules ( 'm.ts' , source ) , / n o t a s t r i n g l i t e r a l / ) ;
171167 } ) ;
172168}
173169
@@ -182,99 +178,199 @@ test('a computed ownership-table rule fails closed too', () => {
182178 ) ;
183179} ) ;
184180
185- test ( 'the failure names the line and quotes the call, so it can be waived or fixed' , ( ) => {
186- try {
187- readSelectorRules (
188- 'm.ts' ,
189- "const ok = reason('lint', f, 'gate:lint', 'd');\nconst bad = reason('lint', f, X, 'd');" ,
190- ) ;
191- assert . fail ( 'expected a throw' ) ;
192- } catch ( error ) {
193- assert . match ( String ( error ) , / l i n e 2 / ) ;
194- assert . match ( String ( error ) , / r e a s o n \( ' l i n t ' , f , X , ' d ' \) / ) ;
195- assert . match ( String ( error ) , / F O R W A R D E D _ S E L E C T O R _ R U L E S / ) ;
196- }
197- } ) ;
198-
199181// --- Declared forwards ------------------------------------------------------
200182
201- const LIVE_CHAIN = `${ TABLE }
202- const s = BUILD_OWNERSHIP.filter((entry) => entry.owns(file)).map((entry) =>
203- reason(entry.check, file, entry.rule, entry.detail),
204- );
205- const a = reason('lint', file, 'gate:lint', 'd');` ;
183+ /**
184+ * The live forwarding statement, on one line so its normalized fingerprint is obvious.
185+ *
186+ * The variants below keep this statement's inner `reason(...)` call byte for byte and change
187+ * only the chain around it. That is the point of fingerprinting the statement: an entry keyed on
188+ * the call alone accepts every one of them.
189+ */
190+ const LIVE_STATEMENT =
191+ 'const s = BUILD_OWNERSHIP.filter((entry) => entry.owns(file)).map((entry) => ' +
192+ 'reason(entry.check, file, entry.rule, entry.detail));' ;
193+
194+ const MUTATING_FILTER =
195+ 'const s = BUILD_OWNERSHIP.filter((entry) => { entry.rule = computed; return true; })' +
196+ '.map((entry) => reason(entry.check, file, entry.rule, entry.detail));' ;
206197
207- const LIVE_CALL = 'reason(entry.check, file, entry.rule, entry.detail)' ;
198+ const MEMBER_CALL =
199+ 'const s = BUILD_OWNERSHIP.map((entry) => { entry.mutate(); ' +
200+ 'return reason(entry.check, file, entry.rule, entry.detail); });' ;
208201
209- test ( 'a waiver admits exactly the call it names, and the universe stays complete' , ( ) => {
210- const derived = readSelectorRules ( 'm.ts' , LIVE_CHAIN , [ LIVE_CALL ] ) ;
202+ function inFunction ( statement : string , enclosing = 'buildOwnership' ) : string {
203+ return `${ TABLE }
204+ const ${ enclosing } = () => {
205+ ${ statement }
206+ return s;
207+ };
208+ const a = reason('lint', file, 'gate:lint', 'd');` ;
209+ }
210+
211+ const LIVE_KEY = { file : 'm.ts' , enclosing : 'buildOwnership' , statement : LIVE_STATEMENT } ;
212+
213+ test ( 'a waiver admits exactly the statement it fingerprints, and loses no category' , ( ) => {
214+ const derived = readSelectorRules ( 'm.ts' , inFunction ( LIVE_STATEMENT ) , [ LIVE_KEY ] ) ;
211215 // `own:swift` is still there — read from the table entry, which is the whole reason the
212- // forward is safe to declare: the waiver excuses the call , it does not drop a category.
216+ // forward is safe to declare: the waiver excuses the statement , it does not drop a category.
213217 assert . deepEqual ( derived . rules , [ 'gate:lint' , 'own:swift' ] ) ;
214- assert . equal ( derived . waiverMatches . get ( LIVE_CALL ) , 1 ) ;
218+ assert . deepEqual ( derived . waiverMatches , [ 1 ] ) ;
215219} ) ;
216220
217- test ( 'a waiver matches across reformatting, because the text is normalized' , ( ) => {
218- const rewrapped = `${ TABLE }
219- const s = BUILD_OWNERSHIP.map((entry) => reason(
220- entry.check,
221- file,
222- entry.rule,
223- entry.detail
224- ));
225- const a = reason('lint', file, 'gate:lint', 'd');` ;
221+ test ( 'a waiver survives reformatting, because the statement text is normalized' , ( ) => {
222+ const rewrapped = inFunction (
223+ `const s = BUILD_OWNERSHIP.filter((entry) => entry.owns(file)).map(
224+ (entry) => reason(entry.check, file, entry.rule, entry.detail),
225+ );` ,
226+ ) ;
227+ // Rewrapped across lines with extra indentation, but the same tokens: still one match. (The
228+ // trailing comma IS a token difference, so this variant keeps the argument list as written.)
229+ const normalized = LIVE_STATEMENT . replace (
230+ '.map((entry) => reason(entry.check, file, entry.rule, entry.detail));' ,
231+ '.map( (entry) => reason(entry.check, file, entry.rule, entry.detail), );' ,
232+ ) ;
233+ const derived = readSelectorRules ( 'm.ts' , rewrapped , [ { ...LIVE_KEY , statement : normalized } ] ) ;
234+ assert . deepEqual ( derived . rules , [ 'gate:lint' , 'own:swift' ] ) ;
235+ assert . deepEqual ( derived . waiverMatches , [ 1 ] ) ;
236+ } ) ;
237+
238+ /**
239+ * The round-eight regressions: the two unsafe chains, each keeping the waived call verbatim.
240+ *
241+ * A waiver keyed on `reason(entry.check, file, entry.rule, entry.detail)` accepted all of these.
242+ * Keyed on the statement, every one is a different statement and fails closed.
243+ */
244+ const BYPASS_ATTEMPTS : readonly { name : string ; source : string } [ ] = [
245+ { name : 'a mutating upstream filter callback' , source : inFunction ( MUTATING_FILTER ) } ,
246+ { name : 'a member call on the entry' , source : inFunction ( MEMBER_CALL ) } ,
247+ {
248+ name : 'the waived statement relocated to another function' ,
249+ source : inFunction ( LIVE_STATEMENT , 'somewhereElse' ) ,
250+ } ,
251+ {
252+ name : 'the waived statement wrapped in a value-changing chain' ,
253+ source : inFunction (
254+ 'const s = BUILD_OWNERSHIP.map(transform).map((entry) => ' +
255+ 'reason(entry.check, file, entry.rule, entry.detail));' ,
256+ ) ,
257+ } ,
258+ ] ;
259+
260+ test ( 'the bypass fixtures really do keep the waived call, or they prove nothing' , ( ) => {
261+ // Non-vacuity guard. These negatives only exercise the bypass while their inner call is byte
262+ // for byte the one a call-keyed waiver would have named; edit a fixture so the call differs
263+ // and they quietly degrade into ordinary "not a literal" cases that never touched the waiver.
264+ const call = 'reason(entry.check, file, entry.rule, entry.detail)' ;
265+ for ( const statement of [ LIVE_STATEMENT , MUTATING_FILTER , MEMBER_CALL ] ) {
266+ assert . ok ( statement . includes ( call ) , `fixture must contain the waived call: ${ statement } ` ) ;
267+ }
268+ } ) ;
269+
270+ for ( const { name, source } of BYPASS_ATTEMPTS ) {
271+ test ( `the waiver does not admit: ${ name } ` , ( ) => {
272+ assert . throws ( ( ) => readSelectorRules ( 'm.ts' , source , [ LIVE_KEY ] ) , / n o t a s t r i n g l i t e r a l / ) ;
273+ } ) ;
274+ }
275+
276+ test ( 'a forward inside a block callback fingerprints the chain, not the bare return' , ( ) => {
277+ // The tightest statement around this forward is `return reason(…);`, which says nothing about
278+ // the chain that produced `entry`. Fingerprinting that would leave the chain swappable — the
279+ // same hole one shape over — so the key climbs to the outermost statement in the function.
280+ const blockForm =
281+ 'const s = BUILD_OWNERSHIP.map((entry) => { return reason(entry.check, file, entry.rule, entry.detail); });' ;
282+ const bareReturn = 'return reason(entry.check, file, entry.rule, entry.detail);' ;
283+
226284 assert . throws (
227- ( ) => readSelectorRules ( 'm.ts' , rewrapped , [ LIVE_CALL ] ) ,
228- / r u l e a r g u m e n t i s n o t a s t r i n g l i t e r a l / ,
229- 'a trailing-comma difference is a real text difference' ,
285+ ( ) =>
286+ readSelectorRules ( 'm.ts' , inFunction ( blockForm ) , [
287+ { file : 'm.ts' , enclosing : 'buildOwnership' , statement : bareReturn } ,
288+ ] ) ,
289+ / n o t a s t r i n g l i t e r a l / ,
290+ 'a waiver naming only the return must not admit the chain around it' ,
230291 ) ;
231- const normalized = 'reason( entry.check, file, entry.rule, entry.detail )' ;
232- assert . deepEqual ( readSelectorRules ( 'm.ts' , rewrapped , [ normalized ] ) . rules , [
233- 'gate:lint' ,
234- 'own:swift' ,
292+ const derived = readSelectorRules ( 'm.ts' , inFunction ( blockForm ) , [
293+ { file : 'm.ts' , enclosing : 'buildOwnership' , statement : blockForm } ,
235294 ] ) ;
295+ assert . deepEqual ( derived . waiverMatches , [ 1 ] ) ;
236296} ) ;
237297
238- test ( 'a waiver does not admit a different forward in the same file' , ( ) => {
239- const source = `${ LIVE_CHAIN }
240- const other = BUILD_OWNERSHIP.map((entry) => reason('lint', file, entry.rule, 'd'));` ;
298+ test ( 'a waiver is bound to its file, so the same statement elsewhere is unreviewed' , ( ) => {
241299 assert . throws (
242- ( ) => readSelectorRules ( 'm .ts' , source , [ LIVE_CALL ] ) ,
243- / r e a s o n \( ' l i n t ' , f i l e , e n t r y \. r u l e , ' d ' \) / ,
300+ ( ) => readSelectorRules ( 'other .ts' , inFunction ( LIVE_STATEMENT ) , [ LIVE_KEY ] ) ,
301+ / n o t a s t r i n g l i t e r a l / ,
244302 ) ;
245303} ) ;
246304
247- test ( 'an unmatched waiver is reported as matching nothing , so check.ts can call it inert' , ( ) => {
305+ test ( 'an unmatched waiver reports zero , so check.ts can call it inert' , ( ) => {
248306 const derived = readSelectorRules ( 'm.ts' , "const a = reason('lint', file, 'gate:lint', 'd');" , [
249- LIVE_CALL ,
307+ LIVE_KEY ,
250308 ] ) ;
251- assert . equal ( derived . waiverMatches . get ( LIVE_CALL ) , undefined ) ;
309+ assert . deepEqual ( derived . waiverMatches , [ 0 ] ) ;
252310} ) ;
253311
254- test ( 'a waiver covering two identical calls reports both, so check.ts can refuse it' , ( ) => {
255- // One reviewed claim must not silently spread to a second call nobody looked at.
312+ test ( 'a waiver covering two identical statements reports both, so check.ts can refuse it' , ( ) => {
313+ // One reviewed claim must not silently spread to a second forward nobody looked at.
314+ const forward = 'use(BUILD_OWNERSHIP.map((entry) => reason(entry.check, file, entry.rule, 1)));' ;
256315 const source = `${ TABLE }
257- const one = BUILD_OWNERSHIP.map((entry) => reason(entry.check, file, entry.rule, entry.detail));
258- const two = OTHER.map((entry) => reason(entry.check, file, entry.rule, entry.detail));` ;
259- assert . equal ( readSelectorRules ( 'm.ts' , source , [ LIVE_CALL ] ) . waiverMatches . get ( LIVE_CALL ) , 2 ) ;
316+ const buildOwnership = () => {
317+ ${ forward }
318+ ${ forward }
319+ };` ;
320+ const derived = readSelectorRules ( 'm.ts' , source , [
321+ { file : 'm.ts' , enclosing : 'buildOwnership' , statement : forward } ,
322+ ] ) ;
323+ assert . deepEqual ( derived . waiverMatches , [ 2 ] ) ;
324+ } ) ;
325+
326+ test ( 'the failure hands back the exact three fields a waiver needs' , ( ) => {
327+ try {
328+ readSelectorRules ( 'm.ts' , inFunction ( LIVE_STATEMENT ) ) ;
329+ assert . fail ( 'expected a throw' ) ;
330+ } catch ( error ) {
331+ const message = String ( error ) ;
332+ assert . match ( message , / f i l e : { 6 } m \. t s / ) ;
333+ assert . match ( message , / e n c l o s i n g : b u i l d O w n e r s h i p / ) ;
334+ assert . match (
335+ message ,
336+ new RegExp ( `statement: ${ LIVE_STATEMENT . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) } ` ) ,
337+ ) ;
338+ }
260339} ) ;
261340
341+ test ( 'the failure names the line, and top-level forwards report as such' , ( ) => {
342+ try {
343+ readSelectorRules (
344+ 'm.ts' ,
345+ "const ok = reason('lint', f, 'gate:lint', 'd');\nconst bad = reason('lint', f, X, 'd');" ,
346+ ) ;
347+ assert . fail ( 'expected a throw' ) ;
348+ } catch ( error ) {
349+ assert . match ( String ( error ) , / l i n e 2 / ) ;
350+ assert . match ( String ( error ) , / e n c l o s i n g : \( t o p l e v e l \) / ) ;
351+ assert . match ( String ( error ) , / F O R W A R D E D _ S E L E C T O R _ R U L E S / ) ;
352+ }
353+ } ) ;
354+
355+ // --- The real tree ----------------------------------------------------------
356+
262357test ( "the repo's real selector derives, and its one declared forward is live" , ( ) => {
263- const waived = FORWARDED_SELECTOR_RULES . map ( ( entry ) => entry . call ) ;
264358 const derived = readSelectorRules (
265359 SELECTOR_SOURCE ,
266360 fs . readFileSync ( path . join ( repoRoot , SELECTOR_SOURCE ) , 'utf8' ) ,
267- waived ,
361+ FORWARDED_SELECTOR_RULES ,
268362 ) ;
269363 assert . ok (
270364 derived . rules . length >= 20 ,
271365 `expected the live selector to yield its categories, got ${ derived . rules . length } ` ,
272366 ) ;
273- // Each declared forward matches exactly one real call. check.ts fails the gate on 0 or 2+;
274- // asserting it here too means the unit lane catches a rotted waiver without the full manifest.
275- for ( const call of waived ) {
276- assert . equal ( derived . waiverMatches . get ( call ) , 1 , `waiver "${ call } " should match one call` ) ;
277- }
367+ // Each declared forward matches exactly one real statement. check.ts fails the gate on 0 or
368+ // 2+; asserting it here means the unit lane catches a rotted waiver without the full manifest.
369+ assert . deepEqual (
370+ derived . waiverMatches ,
371+ FORWARDED_SELECTOR_RULES . map ( ( ) => 1 ) ,
372+ 'every FORWARDED_SELECTOR_RULES entry must fingerprint exactly one live statement' ,
373+ ) ;
278374} ) ;
279375
280376test ( "the repo's real selector fails closed with the waiver removed" , ( ) => {
@@ -286,6 +382,6 @@ test("the repo's real selector fails closed with the waiver removed", () => {
286382 SELECTOR_SOURCE ,
287383 fs . readFileSync ( path . join ( repoRoot , SELECTOR_SOURCE ) , 'utf8' ) ,
288384 ) ,
289- / r u l e a r g u m e n t i s n o t a s t r i n g l i t e r a l / ,
385+ / n o t a s t r i n g l i t e r a l / ,
290386 ) ;
291387} ) ;
0 commit comments