Skip to content

Commit e058ce5

Browse files
committed
feat(spec/ui): navItemVisibleDataNeedsDataSource reads an authored bare-string visible too
The export now normalises a bare-string visible through EvaluatedExpressionInputSchema, the slot's own schema, so a mirror whose visible stays the authored string chains it as it is. NavigationItemSchema accepts and refuses exactly what it did: its own check only ever sees the parsed envelope. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
1 parent 50c3ba8 commit e058ce5

3 files changed

Lines changed: 79 additions & 23 deletions

File tree

‎.changeset/24534-export-nav-visible-data-check.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ Clause-②: yes (widening)
88

99
- **What is added.** One function export, `navItemVisibleDataNeedsDataSource(item, ctx)`: the refinement `NavigationItemSchema` already runs, which refuses a navigation entry with no `dataSource` whose `visible` reads `data`. It is exported the way `objectNavTargetExclusivity`, `checkPageRegionPlacement` and `checkPageMainRegionWidth` are, so a hand-written mirror of the navigation item can chain the same function in its own `superRefine` instead of restating the rule.
1010
- **What does not change.** No schema accepts or refuses anything new. The issue is the same: one `custom` issue at `visible`, with the same message. `NavigationItemSchema` still attaches the same function, once, by name.
11-
- **What the function reads.** It judges the entry after the spec's parse, where `visible` is the `{ dialect, source }` envelope. A bare-string `visible` (`'data.total == 1'`, the shorthand an author writes before the spec normalises it) is not read and raises nothing. So a mirror whose own `visible` stays the authored string must hand the function the parsed `{ dialect: 'cel', source }` form, or it refuses nothing.
11+
- **What the function reads.** Both spellings of `visible`, so it answers the same on an entry as authored and as parsed:
12+
- the authored bare string (`'data.total == 1'`), normalised by `EvaluatedExpressionInputSchema`, the slot's own schema, exactly as the spec's parse normalises it;
13+
- the parsed `{ dialect: 'cel', source }` envelope.
14+
15+
A boolean or absent `visible` judges nothing. So a mirror whose own `visible` stays the authored string chains the function as it is and refuses what the spec refuses.

‎packages/spec/src/ui/app-nav-visible-data-needs-data-source-export.test.ts‎

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323
* cannot be running an inline copy that merely agrees on this matrix.
2424
* 4. BARREL identity: `./index` (what `@objectstack/spec/ui` ships) exports
2525
* the very same function object, with the `(value, ctx)` arity.
26-
* 5. INPUT CONTRACT: the card's own entry, written with the bare-string
27-
* `visible`, is refused by the door, and the export raises the identical
28-
* issue on that entry's PARSED `visible`, but nothing on the bare string
29-
* itself. The check judges the spec's parsed entry; a mirror whose
30-
* `visible` stays the authored string gets no refusal by attaching it as
31-
* it is. That is the export's docblock, measured, so a later change to
32-
* what the function reads flips this leg on purpose.
26+
* 5. BOTH SPELLINGS: the export accepts `visible` as authored (the bare
27+
* string a mirror whose own `visible` stays the authored string hands it)
28+
* and as parsed (the envelope the spec's door hands it), and answers the
29+
* same on both. The card's own entry as written draws the door's identical
30+
* issue from a direct call, and so does its parsed `visible`. Over the leg-1
31+
* matrix rewritten with bare strings, the direct call and the door agree
32+
* issue for issue. The same bare string with a `dataSource` draws nothing,
33+
* and neither does a boolean or absent `visible`. So a mirror attaches the
34+
* export as it is, by identifier.
3335
*
3436
* What it does NOT prove: reference identity between the export and the
3537
* check object the schema holds. zod 4 wraps the function handed to
@@ -245,10 +247,19 @@ describe('`./index` (the `@objectstack/spec/ui` surface) exports the same functi
245247
});
246248

247249
// ---------------------------------------------------------------------------
248-
// Leg 5 — the input contract: the export judges the PARSED entry
250+
// Leg 5 — both spellings: the export reads `visible` as authored and as parsed
249251
// ---------------------------------------------------------------------------
250252

251-
describe('the input contract — the export judges the entry after the spec\'s parse', () => {
253+
/** The leg-1 matrix with each envelope `visible` written back as the bare string an author writes. */
254+
const authoredFixtures: Fixture[] = fixtures
255+
.filter((f) => f.value.visible !== undefined)
256+
.map((f) => ({
257+
...f,
258+
label: `${f.label}, \`visible\` written as the bare string`,
259+
value: { ...f.value, visible: (f.value.visible as { source: string }).source },
260+
}));
261+
262+
describe('both spellings — the export reads `visible` as authored and as parsed, and answers the same', () => {
252263
const parsedVisible = EvaluatedExpressionInputSchema.parse(CARD_ENTRY.visible);
253264

254265
it('the spec normalises the bare string to the envelope the check reads', () => {
@@ -265,13 +276,33 @@ describe('the input contract — the export judges the entry after the spec\'s p
265276
expect(runExport({ ...CARD_ENTRY, visible: parsedVisible })).toEqual(runParse(CARD_ENTRY));
266277
});
267278

268-
it('the export raises NOTHING on the bare-string spelling — a mirror must hand it the parsed entry', () => {
269-
expect(runExport(CARD_ENTRY)).toEqual([]);
279+
it('the export raises the door\'s identical issue (code, path and message) on the card\'s entry AS WRITTEN', () => {
280+
const direct = runExport(CARD_ENTRY);
281+
expect(direct).toHaveLength(1);
282+
expect(direct).toEqual(runParse(CARD_ENTRY));
283+
});
284+
285+
it.each(authoredFixtures)('$label — the direct call refuses at exactly the declared paths, issue for issue with the door', ({ value, refusesAt }) => {
286+
const direct = runExport(value);
287+
expect(direct.map((i) => i.path).sort()).toEqual([...refusesAt].sort());
288+
expect(runParse(value)).toEqual(direct);
270289
});
271290

272-
it('the control — the same entry with a `dataSource` — is accepted by the door and by the export', () => {
291+
it('the authored matrix has both kinds of path, so agreement is not vacuous', () => {
292+
expect(authoredFixtures.filter((f) => f.refusesAt.length > 0).length).toBeGreaterThanOrEqual(4);
293+
expect(authoredFixtures.filter((f) => f.refusesAt.length === 0).length).toBeGreaterThanOrEqual(3);
294+
});
295+
296+
it('the control — the same bare string with a `dataSource` — draws nothing, from the door or the export', () => {
273297
const withSource = { ...CARD_ENTRY, dataSource: API_BINDING };
274298
expect(NavigationItemSchema.safeParse(withSource).success).toBe(true);
299+
expect(runExport(withSource)).toEqual([]);
275300
expect(runExport({ ...withSource, visible: parsedVisible })).toEqual([]);
276301
});
302+
303+
it('a boolean or absent `visible` judges nothing', () => {
304+
expect(runExport({ ...CARD_ENTRY, visible: true })).toEqual([]);
305+
expect(runExport({ ...CARD_ENTRY, visible: false })).toEqual([]);
306+
expect(runExport({ id: CARD_ENTRY.id, type: CARD_ENTRY.type, objectName: CARD_ENTRY.objectName })).toEqual([]);
307+
});
277308
});

‎packages/spec/src/ui/app.zod.ts‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,17 @@ function celReadsRootData(source: string): boolean {
10171017
return reads;
10181018
}
10191019

1020+
/**
1021+
* An authored bare-string `visible` as the envelope the spec's parse makes of
1022+
* it. It goes through {@link EvaluatedExpressionInputSchema}, the slot's own
1023+
* schema, so the shorthand normalisation is reused and never restated here.
1024+
* `undefined` when that schema refuses the string (a blank one).
1025+
*/
1026+
function authoredVisibleEnvelope(source: string): unknown {
1027+
const parsed = EvaluatedExpressionInputSchema.safeParse(source);
1028+
return parsed.success ? parsed.data : undefined;
1029+
}
1030+
10201031
/**
10211032
* A navigation entry whose `visible` reads `data` must declare `dataSource` —
10221033
* the narrowing arm of #24332's ruling, question 2 (letter A).
@@ -1029,8 +1040,8 @@ function celReadsRootData(source: string): boolean {
10291040
* parsed, validated and shipped as an entry the console never shows, with the
10301041
* author told nothing. Refused at the entry's own `visible`, with the fix.
10311042
*
1032-
* Judged on the PARSED entry, where `visible` is always the `{ dialect, source }`
1033-
* envelope; only the `cel` dialect is read. Mounted once, on
1043+
* Judged on the `{ dialect, source }` envelope; only the `cel` dialect is read
1044+
* (see the input paragraph below for the two spellings). Mounted once, on
10341045
* {@link NavigationItemSchema} itself, so it reaches every navigation door —
10351046
* `app.navigation`, `app.areas[].navigation`, every `children` array at any
10361047
* depth, and a navigation contribution's `items`. The exported per-branch
@@ -1046,20 +1057,30 @@ function celReadsRootData(source: string): boolean {
10461057
* Exporting it moves no accept set, and its issue (`custom` at `visible`, the
10471058
* message above) is the same at every door that runs it.
10481059
*
1049-
* ⚠️ What it reads is the entry AFTER the spec's parse: `visible` as the
1050-
* `{ dialect, source }` envelope {@link EvaluatedExpressionInputSchema} turns
1051-
* either authored spelling into. A bare-string `visible` (the authored
1052-
* shorthand, before that normalisation) is not read and raises nothing, so a
1053-
* mirror whose own `visible` stays the authored string refuses nothing by
1054-
* attaching this as it is. The mount, the export, the barrel identity and that
1055-
* input contract are pinned by
1060+
* It accepts both spellings of `visible`, so it answers the same on an entry
1061+
* as authored and on the entry the spec's parse produced:
1062+
*
1063+
* - the PARSED `{ dialect, source }` envelope, which is all the spec's own door
1064+
* ever hands it;
1065+
* - the AUTHORED bare string (`'data.total == 1'`), which a mirror whose own
1066+
* `visible` stays the authored string hands it. It is normalised by
1067+
* {@link EvaluatedExpressionInputSchema}, the slot's own schema, whose string
1068+
* arm turns it into `{ dialect: 'cel', source }`. It is the very normaliser
1069+
* the parse runs, reused rather than restated, so a blank string it refuses
1070+
* is judged by nothing here either.
1071+
*
1072+
* A boolean or absent `visible` judges nothing. So a mirror attaches this as it
1073+
* is, by identifier. The mount, the export, the barrel identity and both
1074+
* spellings are pinned by
10561075
* `app-nav-visible-data-needs-data-source-export.test.ts`.
10571076
*/
10581077
export function navItemVisibleDataNeedsDataSource(item: unknown, ctx: z.RefinementCtx): void {
10591078
if (!item || typeof item !== 'object') return;
10601079
const entry = item as { visible?: unknown; dataSource?: unknown };
10611080
if (entry.dataSource !== undefined) return;
1062-
const visible = entry.visible as { dialect?: unknown; source?: unknown } | undefined;
1081+
const visible = (typeof entry.visible === 'string'
1082+
? authoredVisibleEnvelope(entry.visible)
1083+
: entry.visible) as { dialect?: unknown; source?: unknown } | undefined;
10631084
if (!visible || visible.dialect !== 'cel' || typeof visible.source !== 'string') return;
10641085
if (!celReadsRootData(visible.source)) return;
10651086
ctx.addIssue({

0 commit comments

Comments
 (0)