4949 * that fires on any `packages[]` at all — would satisfy the failing case alone.
5050 * The pair asserts what the card actually claims: that the option-B stack is
5151 * READ, not that it is rejected.
52+ *
53+ * ## ⚠️ [#18897] The refusal cases were satisfied by an IMPOSTOR — and the
54+ * ## impostor is a SIBLING PASS that arrived after this file was written
55+ *
56+ * These three refusal cases claimed to pin `authoringRuleUnionStack` — the fold
57+ * that makes the rule table's INPUT non-empty on an option-B stack. They could
58+ * not. Since #18677 (`os validate`) and #18778 (`os lint`) all three doors ALSO
59+ * run `runPerPackageAuthoringRules`, which judges each `packages[]` entry as its
60+ * own stack. That pass sees this fixture's one package body, raises the SAME
61+ * rule at the SAME path, and refuses with the same exit code. ⇒ `exit 1` +
62+ * `contains(RULE)` + `contains(RULE_PATH)` is satisfied by EITHER mechanism, so
63+ * the control could not tell the fold from its sibling.
64+ *
65+ * ⛔ Not reasoned about — measured by ablation, at `origin/main` 13d52947d8:
66+ *
67+ * ablation this file
68+ * `authoringRuleUnionStack` never folds 6/6 GREEN
69+ * `runPerPackageAuthoringRules` yields no findings 6/6 GREEN
70+ * BOTH of the above 3 RED
71+ *
72+ * ⭐ Either mechanism alone kept every case green; only deleting both turned the
73+ * three refusal cases red. That is what a control satisfied by something other
74+ * than the mechanism it names looks like from the inside — green, and green for
75+ * the wrong reason.
76+ *
77+ * ## What discriminates them: a PEDIGREE, ⛔ not a count and ⛔ not an exit code
78+ *
79+ * `runPerPackageAuthoringRules` prefixes the `where` of every finding IT raises
80+ * with `package '<id>' — `; it owns that prefix, and three sibling files pin the
81+ * same regex. The union run renders `where` bare. Measured on THIS fixture, same
82+ * command, with only the fold's presence moving:
83+ *
84+ * fold intact • object "ob_order" · field "ghost": …
85+ * fold ablated • package 'com.example.ob' — object "ob_order" · field "ghost": …
86+ *
87+ * So each refusal case now asserts the refusal carries NO per-package prefix —
88+ * the finding is the UNION run's, which is the mechanism this file is about.
89+ *
90+ * ⚠️ A real falsifier — a fixture in which the impostor structurally CANNOT
91+ * raise this finding — does not exist for this rule class, and that was measured
92+ * rather than assumed: the per-package pass is a SUPERSET of the union run for
93+ * reference rules (`utils/artifact-packages.ts`: "the per-package run is
94+ * STRICTER"), because each body is judged with the artifact's own `packages[]`
95+ * handed in as resolution context. A name no package provides therefore dangles
96+ * in BOTH views, under every arrangement of packages. ⇒ here the pedigree is the
97+ * whole discriminant, and the negative assertion is made load-bearing by the
98+ * POSITIVE control at the foot of this file — a fixture whose finding really is
99+ * per-package-only. Without that pair, "the prefix is absent" would be satisfied
100+ * by a prefix this suite never produces at all.
52101 */
53102
54103import { describe , expect , it } from 'vitest' ;
@@ -69,6 +118,15 @@ const AUTHORING_COMMANDS = ['validate', 'lint', 'build'] as const;
69118const RULE = 'object-reference-unknown' ;
70119const RULE_PATH = 'objects[0].fields.ghost.reference' ;
71120
121+ /**
122+ * [#18897] The `where` prefix `runPerPackageAuthoringRules` puts on every
123+ * finding IT raises — the one thing that tells the union run's finding apart
124+ * from the per-package pass's copy of it. ⛔ Not this file's guess at a format:
125+ * the pass owns the prefix, and `validate-/lint-per-package-authoring-parity`
126+ * and `build-text-face-advisory-count` pin the identical regex.
127+ */
128+ const PER_PACKAGE_WHERE = / p a c k a g e ' [ ^ ' ] + ' — / ;
129+
72130/**
73131 * The card's repro verbatim: no top-level `objects`, one `packages[]` entry
74132 * carrying an object whose `ghost` lookup points at an object that does not
@@ -100,6 +158,73 @@ const optionBStack = (reference: string): Record<string, unknown> => ({
100158 ] ,
101159} ) ;
102160
161+ /**
162+ * [#18897] The POSITIVE half of the pedigree pair: a project whose ONLY
163+ * author-time finding is one the union run genuinely cannot see.
164+ *
165+ * `core` owns `ob_account`; `orders` owns the view that displays
166+ * `ob_account.industry`. Folded into one union the field HAS a consumer and
167+ * nothing is raised; judged per package, `core` declares a field nothing in
168+ * `core` reads — so the survivor is the per-package pass's alone and carries its
169+ * `where` prefix. It is the falsifier shape PR #18878 landed in
170+ * `lint-per-package-authoring-parity.test.ts`, and it is here for one job: to
171+ * prove that {@link PER_PACKAGE_WHERE} is a prefix this suite CAN observe, so
172+ * the refusal cases' `toBe(false)` is a measurement and not the silence of a
173+ * regex that never matches anything. ⛔ Do not remove the view to "simplify" it.
174+ */
175+ const perPackageOnlyStack = ( ) : Record < string , unknown > => {
176+ const coreManifest = {
177+ id : 'com.example.obflip.core' , name : 'obflip core' , namespace : 'ob' ,
178+ version : '1.0.0' , type : 'app' , engines : { protocol : '^17' } ,
179+ } ;
180+ const coreObjects = [ {
181+ name : 'ob_account' , label : 'Account' , pluralLabel : 'Accounts' , sharingModel : 'private' ,
182+ fields : {
183+ name : { name : 'name' , type : 'text' , label : 'Account Name' , required : true } ,
184+ industry : { name : 'industry' , type : 'text' , label : 'Industry' } ,
185+ } ,
186+ } ] ;
187+ const coreApps = [ {
188+ name : 'ob_crm' , label : 'OB CRM' ,
189+ navigation : [ {
190+ id : 'sales_group' , type : 'group' , label : 'Sales' ,
191+ children : [ { id : 'nav_accounts' , type : 'object' , objectName : 'ob_account' , label : 'Accounts' } ] ,
192+ } ] ,
193+ } ] ;
194+ const ordersManifest = {
195+ id : 'com.example.obflip.orders' , name : 'obflip orders' , namespace : 'ob' ,
196+ version : '1.0.0' , type : 'module' , engines : { protocol : '^17' } ,
197+ dependencies : { 'com.example.obflip.core' : '^1.0.0' } ,
198+ } ;
199+ const ordersObjects = [ {
200+ name : 'ob_order' , label : 'Order' , pluralLabel : 'Orders' , sharingModel : 'private' ,
201+ fields : {
202+ name : { name : 'name' , type : 'text' , label : 'Order Number' , required : true } ,
203+ account : { name : 'account' , type : 'lookup' , label : 'Account' , reference : 'ob_account' } ,
204+ } ,
205+ } ] ;
206+ const ordersViews = [
207+ {
208+ name : 'ob_account_list' , label : 'Account List' , object : 'ob_account' ,
209+ list : { label : 'Account List' , columns : [ 'name' , 'industry' ] } ,
210+ } ,
211+ {
212+ name : 'ob_order_list' , label : 'Order List' , object : 'ob_order' ,
213+ list : { label : 'Order List' , columns : [ 'name' , 'account' ] } ,
214+ } ,
215+ ] ;
216+ return {
217+ manifest : coreManifest ,
218+ objects : [ ...ordersObjects , ...coreObjects ] ,
219+ apps : [ ...coreApps ] ,
220+ views : [ ...ordersViews ] ,
221+ packages : [
222+ { manifest : { ...ordersManifest , objects : ordersObjects , views : ordersViews } } ,
223+ { manifest : { ...coreManifest , objects : coreObjects , apps : coreApps } } ,
224+ ] ,
225+ } ;
226+ } ;
227+
103228interface Run {
104229 code : number ;
105230 output : string ;
@@ -152,6 +277,23 @@ describe('#17069 — os validate and os lint judge the option-B stack, not an em
152277 `os ${ command } named ${ RULE } at a different path than the other doors — the three commands ` +
153278 `must report one finding one way` ,
154279 ) . toContain ( RULE_PATH ) ;
280+ // ⭐ [#18897] PEDIGREE. Everything above this line is satisfied by the
281+ // per-package authoring pass, which raises the same rule at the same path
282+ // on this one-package body and refuses with the same code — measured: with
283+ // `authoringRuleUnionStack` ablated all six cases here stayed GREEN, and
284+ // only ablating the per-package pass TOO turned these three red. The union
285+ // run renders `where` bare; the per-package pass prefixes it. So the
286+ // absence of that prefix is what makes this case a reading of the FOLD.
287+ expect (
288+ PER_PACKAGE_WHERE . test ( run . output ) ,
289+ `os ${ command } refused this stack through the PER-PACKAGE authoring pass, not through the ` +
290+ `union fold this file pins: the finding carries ${ String ( PER_PACKAGE_WHERE ) } , the prefix ` +
291+ `runPerPackageAuthoringRules puts on its own findings. The fold is what makes the rule ` +
292+ `table's INPUT non-empty on an option-B stack, and with it gone this command is refusing ` +
293+ `for a reason #17069 did not buy. ⛔ Do not answer this by deleting the assertion — the ` +
294+ `pedigree is the only thing here that can tell the two mechanisms apart.` +
295+ `\n--- output ---\n${ run . output } ` ,
296+ ) . toBe ( false ) ;
155297 } ,
156298 180_000 ,
157299 ) ;
@@ -171,4 +313,34 @@ describe('#17069 — os validate and os lint judge the option-B stack, not an em
171313 } ,
172314 180_000 ,
173315 ) ;
316+
317+ it . each ( AUTHORING_COMMANDS ) (
318+ '⭐ PEDIGREE CONTROL — os %s DOES render the per-package prefix when the finding is the pass\'s own' ,
319+ ( command ) => {
320+ // The other half of the pair above. A `toBe(false)` on a regex is only a
321+ // measurement if the same suite can make it true; without this case an
322+ // absent prefix and a prefix nothing ever emits read identically — which
323+ // is the whole class of defect #18897 is about, one level up.
324+ const run = runCommand ( command , perPackageOnlyStack ( ) ) ;
325+ expect (
326+ run . code ,
327+ `os ${ command } exited ${ run . code } on the per-package-only fixture. Its single finding is an ` +
328+ `ADVISORY (field-no-consumers), so every door reports it and none of them fails on it.` +
329+ `\n--- output ---\n${ run . output } ` ,
330+ ) . toBe ( 0 ) ;
331+ expect (
332+ PER_PACKAGE_WHERE . test ( run . output ) ,
333+ `os ${ command } raised no per-package-prefixed finding on a fixture whose only finding the ` +
334+ `union run cannot see (core declares ob_account.industry; orders owns the view that ` +
335+ `displays it). Either the pass stopped running on this door, or the prefix was re-spelled ` +
336+ `— and in both cases the refusal cases above are asserting the absence of something this ` +
337+ `suite no longer produces.\n--- output ---\n${ run . output } ` ,
338+ ) . toBe ( true ) ;
339+ // …and it is that field, not some other advisory that happens to be
340+ // prefixed: the pedigree names the member, never just the shape.
341+ expect ( run . output , `os ${ command } prefixed a finding, but not the one this fixture is built on` )
342+ . toContain ( 'industry' ) ;
343+ } ,
344+ 180_000 ,
345+ ) ;
174346} ) ;
0 commit comments