77 * raised used to open with a `[lower_snake]` tag that was the restatement of the
88 * `code` the very same throw declared — `[no_draft]` in front of `NO_DRAFT`,
99 * `[item_locked]` in front of `ITEM_LOCKED`, and so on for the whole family.
10+ * The runtime authoring gate (`runtime-authoring-gate.ts`) is the third
11+ * producer: its one refusal opened with `[invalid_metadata]` in front of its own
12+ * `INVALID_METADATA` / 422, the same shape, and reached `dist` the same way.
1013 *
1114 * They were not invisible. `withoutDeclaredCodePrefix`
1215 * (`packages/rest/src/error-response.ts`) strips a leading restatement only when
2831 * this package assert the prose a given door answers, so one re-introduced tag
2932 * reds exactly one of them and a newly-written refusal reds none — and a new
3033 * refusal copied from a neighbouring producer is precisely how the idiom spread
31- * in the first place. Reading the source covers every throw site in both files,
32- * including ones no test can provoke.
34+ * in the first place. Reading the source covers every throw site in every
35+ * producer file, including ones no test can provoke.
3336 *
3437 * ⚠️ A scan that matches nothing passes for free, so the family floor below is
3538 * part of the pin: the scanner must still be finding refusals to have an opinion
@@ -48,11 +51,29 @@ import { readFileSync } from 'node:fs';
4851import { dirname , join } from 'node:path' ;
4952import { fileURLToPath } from 'node:url' ;
5053import { ObjectStackProtocolImplementation } from './protocol.js' ;
54+ import {
55+ PLATFORM_SCHEDULE_CREATE_RECORD_ORG_MISSING ,
56+ evaluateRuntimeAuthoringGate ,
57+ } from './runtime-authoring-gate.js' ;
5158
5259const HERE = dirname ( fileURLToPath ( import . meta. url ) ) ;
5360
54- /** The two producers the ruling was applied to. */
55- const PRODUCERS = [ 'protocol.ts' , 'sys-metadata-repository.ts' ] as const ;
61+ /**
62+ * The producers the ruling was applied to, each with its REFUSAL FLOOR: the
63+ * minimum `new Error(` count the scan must still see in that file for its
64+ * silence to mean anything.
65+ *
66+ * ⚠️ Per file, not one shared number. `runtime-authoring-gate.ts` constructs
67+ * exactly one error — its 422 — so the floor of 5 the first two producers
68+ * carry would red it for being small rather than for being wrong, and a
69+ * shared floor of 1 would let either large producer lose nearly every throw
70+ * site before this pin noticed.
71+ */
72+ const PRODUCERS : Readonly < Record < string , number > > = {
73+ 'protocol.ts' : 5 ,
74+ 'sys-metadata-repository.ts' : 5 ,
75+ 'runtime-authoring-gate.ts' : 1 ,
76+ } ;
5677
5778/**
5879 * A string literal whose FIRST characters are a bracketed tag — the shape
@@ -65,7 +86,7 @@ const PRODUCERS = ['protocol.ts', 'sys-metadata-repository.ts'] as const;
6586 * every grep for a literal tag walked straight past. A detector that reads only
6687 * literals would let exactly that shape back in.
6788 */
68- const TAGGED_OPENER = / ( ` | ' ) \[ (?: ( [ A - Z a - z ] [ A - Z a - z 0 - 9 _ ] * ) \] | \$ \{ ) / ;
89+ const TAGGED_OPENER = / ( ` | ' ) \[ (?: ( [ A - Z a - z ] [ A - Z a - z 0 - 9 _ ] * ) \] | \$ \{ ( [ ^ } ] * ) ) / ;
6990
7091/**
7192 * The bracketed openers that are NOT this family, by name.
@@ -82,6 +103,25 @@ const TAGGED_OPENER = /(`|')\[(?:([A-Za-z][A-Za-z0-9_]*)\]|\$\{)/;
82103 */
83104const NON_REFUSAL_PREFIXES = new Set ( [ 'Protocol' , 'SysMetadataRepository' ] ) ;
84105
106+ /**
107+ * The INTERPOLATED bracket that is NOT this family, by the expression it
108+ * interpolates.
109+ *
110+ * `advisory.rule` is the `[rule]` locator the author-time gate composes on its
111+ * advisory log line: it opens a continuation literal in the middle of that
112+ * line, after the `[Protocol]` prefix, and names WHICH rule produced the
113+ * finding. That is the `[rule]`-locator vocabulary the header above already
114+ * declares out of scope — it restates no declared `code`, and no throw sits
115+ * beside it. The line-based scan cannot tell a mid-message continuation from
116+ * an opener, so the exemption is spelled here rather than inferred.
117+ *
118+ * ⚠️ Keyed on the exact expression, never on "anything interpolated": the
119+ * interpolated arm exists because an opener written as `[${code}]` from the
120+ * throw's own `code` variable is the most redundant member of the family.
121+ * ⛔ Never add an expression here that holds a declared error code.
122+ */
123+ const NON_REFUSAL_LOCATORS = new Set ( [ 'advisory.rule' ] ) ;
124+
85125function scan ( file : string ) : { openers : string [ ] ; refusals : number } {
86126 const lines = readFileSync ( join ( HERE , file ) , 'utf8' ) . split ( '\n' ) ;
87127 const openers : string [ ] = [ ] ;
@@ -96,13 +136,14 @@ function scan(file: string): { openers: string[]; refusals: number } {
96136 // The bracket must open the literal, not merely appear inside it.
97137 if ( ! m || line [ m . index + 1 ] !== '[' ) continue ;
98138 if ( m [ 2 ] !== undefined && NON_REFUSAL_PREFIXES . has ( m [ 2 ] ) ) continue ;
139+ if ( m [ 3 ] !== undefined && NON_REFUSAL_LOCATORS . has ( m [ 3 ] . trim ( ) ) ) continue ;
99140 openers . push ( `${ file } :${ i + 1 } ${ trimmed . slice ( 0 , 100 ) } ` ) ;
100141 }
101142 return { openers, refusals } ;
102143}
103144
104145describe ( 'refusal messages open with prose, never with a bracketed restatement of their own code' , ( ) => {
105- it . each ( PRODUCERS ) ( '%s raises no message opening with a bracketed lowercase tag' , ( file ) => {
146+ it . each ( Object . entries ( PRODUCERS ) ) ( '%s raises no message opening with a bracketed lowercase tag' , ( file , floor ) => {
106147 const { openers, refusals } = scan ( file ) ;
107148
108149 // THE FLOOR — the scan has to still be looking at refusals for its silence
@@ -111,16 +152,16 @@ describe('refusal messages open with prose, never with a bracketed restatement o
111152 expect (
112153 refusals ,
113154 `${ file } no longer constructs errors here — this pin is scanning the wrong file` ,
114- ) . toBeGreaterThanOrEqual ( 5 ) ;
155+ ) . toBeGreaterThanOrEqual ( floor ) ;
115156
116157 expect (
117158 openers ,
118159 `these refusals open with a tag restating their own declared code:\n${ openers . join ( '\n' ) } ` ,
119160 ) . toEqual ( [ ] ) ;
120161 } ) ;
121162
122- it ( 'the whole family is covered — both producers together still raise the refusals this pin is about' , ( ) => {
123- const total = PRODUCERS . reduce ( ( n , f ) => n + scan ( f ) . refusals , 0 ) ;
163+ it ( 'the whole family is covered — the producers together still raise the refusals this pin is about' , ( ) => {
164+ const total = Object . keys ( PRODUCERS ) . reduce ( ( n , f ) => n + scan ( f ) . refusals , 0 ) ;
124165 expect ( total ) . toBeGreaterThanOrEqual ( 30 ) ;
125166 } ) ;
126167} ) ;
@@ -168,4 +209,43 @@ describe('the refusal a caller actually receives', () => {
168209 expect ( err . message . startsWith ( '[' ) , `message opens with a tag: ${ err . message . slice ( 0 , 48 ) } ` ) . toBe ( false ) ;
169210 expect ( err . message ) . toContain ( "rollbackMetaItem requires a positive integer 'toVersion'" ) ;
170211 } ) ;
212+
213+ it ( 'carries the token on `code` and opens with the sentence — the author-time gate `INVALID_METADATA`' , ( ) => {
214+ // A schedule-triggered, platform-level flow that creates rows naming no
215+ // organization, on a deployment that walls organizations: the gate-local
216+ // refusal, which needs no engine and no registry to fire.
217+ const { error : err } = evaluateRuntimeAuthoringGate ( {
218+ type : 'flow' ,
219+ name : 'nightly_sweep' ,
220+ state : 'active' ,
221+ organizationId : null ,
222+ orgWallEnforced : true ,
223+ body : {
224+ name : 'nightly_sweep' ,
225+ label : 'Nightly Sweep' ,
226+ type : 'schedule' ,
227+ status : 'active' ,
228+ runAs : 'system' ,
229+ nodes : [
230+ { id : 'start' , type : 'start' , label : 'Start' , config : { schedule : '0 1 * * *' } } ,
231+ {
232+ id : 'log' ,
233+ type : 'create_record' ,
234+ label : 'Write sweep log' ,
235+ config : { objectName : 'sweep_log' , fields : { note : 'swept' } } ,
236+ } ,
237+ ] ,
238+ edges : [ { id : 'e1' , source : 'start' , target : 'log' } ] ,
239+ } ,
240+ } ) as { error : any } ;
241+
242+ expect ( err , 'the gate let a refused publish through — nothing to assert on' ) . not . toBeNull ( ) ;
243+ expect ( err . code ) . toBe ( 'INVALID_METADATA' ) ;
244+ expect ( err . status ) . toBe ( 422 ) ;
245+ expect ( err . message . startsWith ( '[' ) , `message opens with a tag: ${ err . message . slice ( 0 , 48 ) } ` ) . toBe ( false ) ;
246+ expect ( err . message ) . not . toContain ( '[invalid_metadata]' ) ;
247+ expect ( err . message ) . toMatch ( / ^ f l o w \/ n i g h t l y _ s w e e p f a i l e d a u t h o r - t i m e v a l i d a t i o n : \d + i s s u e s ? — / ) ;
248+ // The `[rule]` locator is NOT this family and stays in the sentence.
249+ expect ( err . message ) . toContain ( `[${ PLATFORM_SCHEDULE_CREATE_RECORD_ORG_MISSING } ]` ) ;
250+ } ) ;
171251} ) ;
0 commit comments