Skip to content

Commit 860f4a1

Browse files
committed
docs(lint): name os lint as the one door that leaves a retired visibility key's predicate unjudged (F4 of #23543, review patch)
Claude-Session: https://claude.ai/code/session_014ruwnS8CSGGVA5UWfJA8xZ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 24622be commit 860f4a1

6 files changed

Lines changed: 47 additions & 22 deletions

‎.changeset/23543-f4-visibility-alias-keys-retired.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Clause-②: yes (narrowing)
3535
- **The D3 entry `ui-visibility-alias-keys-retired`**, one for both keys.
3636
- **No deprecation window**, per the project's startup-stage posture.
3737

38-
`@objectstack/lint`: the visibility-predicate rules (`visibility-root-mislayered`, `visibility-bare-identifier`, `visibility-predicate-syntax`) and the predicate-path rules read `visibleWhen` only. Every door runs them on a body the spec parse accepted, which cannot carry either retired key.
38+
`@objectstack/lint`: the visibility-predicate rules (`visibility-root-mislayered`, `visibility-bare-identifier`, `visibility-predicate-syntax`) and the predicate-path rules read `visibleWhen` only. `os validate`, `os build`, `os verify`, `defineStack` and the metadata save door parse first and refuse either retired key by name, so a body they judge cannot carry one. `os lint` is the one door that hands these rules an unparsed stack: there a predicate written under a retired key is not judged, by design — the parsing doors refuse the key itself, and `os lint` reports no refused shape.
3939

4040
`@objectstack/metadata-core`: `detectUnboundFormViewPredicateRoots` reads `visibleWhen` only. Its caller scans an artifact after `applyArtifactForwardConversions`, which renames a legacy `visibleOn` first.
4141

‎packages/lint/src/authoring-rule-input-tier.test.ts‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,14 @@ describe('premise 3 (FALSE): "the `visibleOn` alias survives until the parse"',
234234
// * the authoring funnel no longer renames the key — `normalizeStackInput`
235235
// passes it through, so a rule fed this tier would see it;
236236
// * no rule reads it either way — the family reads `visibleWhen` only;
237-
// * the author is not silent — `defineStack` (and the parse every door
238-
// runs before its rule pass) refuses the key by name with the rename.
239-
// That refusal IS the recorded guard for this surface now.
237+
// * the author is not silent — the doors that parse first (`defineStack`,
238+
// `os validate`, `os build`, `os verify`, the metadata save door) refuse
239+
// the key by name with the rename. That refusal IS the recorded guard for
240+
// this surface now;
241+
// * `os lint` is the one door that hands the rules an UNPARSED stack
242+
// (`runAuthoringRules('lint', …)` over the normalized tier). There a
243+
// predicate under a retired key is nobody's verdict, by design — pinned
244+
// below as that door's reading, beside a canonical control.
240245
/** The three alias sites, each carrying `predicate` under its RETIRED key. */
241246
const aliasSitesWith = (predicate: string): Array<[string, AnyRec]> => [
242247
['views[].form.sections[]', {
@@ -275,7 +280,7 @@ describe('premise 3 (FALSE): "the `visibleOn` alias survives until the parse"',
275280
// The key passes through: the D2 conversion is retired from the load path.
276281
expect(JSON.stringify(normalized)).toMatch(/"visibleOn"|"visibility"/);
277282
// A VALUE defect under it is nobody's verdict here, raw or normalized —
278-
// the parse refuses the key before any rule runs (next case).
283+
// the parsing doors refuse the key itself (next case).
279284
expect(validateVisibilityPredicates(structuredClone(stack))).toEqual([]);
280285
expect(validateVisibilityPredicates(normalized)).toEqual([]);
281286
},
@@ -295,6 +300,23 @@ describe('premise 3 (FALSE): "the `visibleOn` alias survives until the parse"',
295300
},
296301
);
297302

303+
it.each(aliasSitesBadValue)(
304+
'%s: `os lint`\'s reading — the unparsed stack it hands the registry leaves a retired key\'s predicate unjudged, by design',
305+
(_site, stack) => {
306+
// What `os lint` runs (`commands/lint.ts`): the `'lint'` registry door over
307+
// the NORMALIZED stack, never parsed. The retired key reaches it unrenamed
308+
// and no visibility rule reads it.
309+
const visibilityRules = (s: AnyRec) => runAuthoringRules('lint', { normalized: normalizeStackInput(structuredClone(s)) as AnyRec })
310+
.filter((f) => f.rule.startsWith('visibility-'))
311+
.map((f) => f.rule);
312+
expect(visibilityRules(stack)).toEqual([]);
313+
// CONTROL — the same predicate under the canonical key is the finding at
314+
// the same door, so the empty reading above is the posture, not a dead rule.
315+
const canonical = JSON.parse(JSON.stringify(stack).replace(/"(visibleOn|visibility)":/g, '"visibleWhen":')) as AnyRec;
316+
expect(visibilityRules(canonical)).toEqual(['visibility-bare-identifier']);
317+
},
318+
);
319+
298320
it('the predicate-VALUE rules in the same file are unaffected — do not connect them', () => {
299321
// The value moves into `visibleWhen` intact, so these still report on the
300322
// tier. #6318 was about the alias-KEY rule only, and this is the pin that

‎packages/lint/src/validate-predicate-path-refs.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,9 @@ type AnyRec = Record<string, unknown>;
301301
/**
302302
* The predicate keys a form field / section can carry (ADR-0089): `visibleWhen`
303303
* alone. The view-side `visibleOn` retired at 18 as a spec tombstone (#23543
304-
* F4), and every door that runs this rule hands it a body the spec parse
305-
* accepted — the reason `validate-visibility-predicates.ts` states at its own
306-
* `CANONICAL` — so no input carries it.
304+
* F4). The doors that parse first refuse it by name; `os lint`, which hands
305+
* this rule an unparsed stack, leaves a predicate under it unjudged by design —
306+
* the posture `validate-visibility-predicates.ts` states at its own `CANONICAL`.
307307
*/
308308
const PREDICATE_KEYS = ['visibleWhen'] as const;
309309

‎packages/lint/src/validate-visibility-predicates.test.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,12 @@ describe('validateVisibilityPredicates (ADR-0089 D3b)', () => {
6969
//
7070
// #6318 retired the alias-KEY rule and kept an alias-VALUE read
7171
// (`visibleWhen ?? visibleOn ?? visibility`) for raw callers. Both aliases
72-
// retired at 18 as spec tombstones, and every door that runs this rule hands
73-
// it a body the spec parse accepted, which cannot carry either key (the
74-
// measurement is in `CANONICAL`'s docblock). So the limbs went too: a defect
75-
// written under a retired key produces no finding HERE — the parse refuses
76-
// the key by name one layer earlier. Each row pairs the alias spelling with
72+
// retired at 18 as spec tombstones, and the doors that parse first (`os
73+
// validate`, `os build`, `os verify`, `defineStack`, the metadata save door)
74+
// refuse either key by name. So the limbs went too: a defect written under a
75+
// retired key produces no finding HERE. At `os lint`, the one door that hands
76+
// this rule an unparsed stack, that is the reading by design (`CANONICAL`'s
77+
// docblock; pinned as that door's reading in `authoring-rule-input-tier.test.ts`). Each row pairs the alias spelling with
7778
// the same value under `visibleWhen`, which must still report, so an empty
7879
// alias reading cannot pass by the rule having stopped working.
7980
describe('the retired alias keys are not read (#23543 F4)', () => {

‎packages/lint/src/validate-visibility-predicates.ts‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -548,14 +548,16 @@ type AnyRec = Record<string, unknown>;
548548
*
549549
* `checkElement` used to read `visibleWhen ?? visibleOn ?? visibility`, for a
550550
* caller handing this export a raw authored object. Both aliases retired at 18
551-
* as spec tombstones, and every door that runs this rule hands it a body the
552-
* spec parse ACCEPTED: the three CLI commands run the rule stage only after
553-
* `ObjectStackDefinitionSchema` passes (`judgeAuthorTimeRules`), per-package
554-
* bodies come from that parsed stack, and the metadata save door parses the
555-
* `view` body (a 422 on refusal) before its runtime rule pass. An accepted body
556-
* cannot carry either key, so the limbs could only ever judge the value of a key
557-
* the spec already refuses — the consumer-side alias tolerance the retirement
558-
* ends.
551+
* as spec tombstones. The doors that PARSE first — `os validate`, `os build`,
552+
* `os verify` (`judgeAuthorTimeRules`), `defineStack`, and the metadata save
553+
* door, which parses the `view` body (a 422 on refusal) before its runtime rule
554+
* pass — refuse either key by name, so a body they hand this rule cannot carry
555+
* one. `os lint` is the one door that hands this rule an UNPARSED stack (its
556+
* `'lint'` run reads the normalized stack on both tiers): there a predicate
557+
* written under a retired key is nobody's verdict, by design — the parsing
558+
* doors refuse the key itself, and `os lint` reports no refused shape. Reading
559+
* the limbs would only judge the value of a key the spec refuses — the
560+
* consumer-side alias tolerance the retirement ends.
559561
*/
560562
const CANONICAL = 'visibleWhen';
561563

‎packages/spec/src/shared/visible-when-alias-guidance.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ describe('the two `visibleWhen` shapes the first sweep never enumerated', () =>
294294
// The judgment call: `visibility` / `visibleOn` are the ADR-0089 spellings
295295
// this surface's own docblock says are deliberately NOT folded in (the
296296
// key is new; there is no legacy convention to carry forward here, unlike
297-
// the view/page shapes that fold them via `normalizeVisibleWhen`). That
297+
// the view/page shapes that folded them until both retired at 18). That
298298
// sentence is about ACCEPTANCE and an alias row does not disturb it — both
299299
// stay rejected below. But an author who used the ADR-0089 spelling
300300
// correctly on a page component or view form and reaches for the same

0 commit comments

Comments
 (0)