From bcfd9b5d4687344c76523f6c784b0e537c4623f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 14:47:52 +0000 Subject: [PATCH 1/2] docs(spec): ObjectNavItem.recordId is tolerated with viewName, not mutually exclusive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recordId docblock declared an exclusivity the navigation guard does not enforce. recordId + viewName parses clean through NavigationItemSchema; it is the one legacy pair objectNavTargetExclusivity deliberately lets through, and that guard's own docblock says so. The parenthetical gave it away — "viewName is ignored if both are set" describes a precedence, not a refusal, so the sentence's second clause contradicted its first. An author who read "mutually exclusive" would avoid a combination the platform accepts, or report a bug when it parses. Silent in both directions. Rewrite the sentence to say the pair is tolerated, name the guard that tolerates it, and point at the test that pins it. No schema, no guard, no accept set moves, and the .describe() strings that reach the generated reference are untouched. app-nav-target-exclusivity-export.test.ts gains a fifth leg asserting the docblock against the accept set it describes, so the next copy of the false sentence goes red instead of shipping. Its extraction throws when the anchor moves, so it cannot pass vacuously. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH --- .../16875-nav-recordid-viewname-tolerated.md | 46 ++++++++++++++++++ .../app-nav-target-exclusivity-export.test.ts | 48 +++++++++++++++++++ packages/spec/src/ui/app.zod.ts | 12 ++++- 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 .changeset/16875-nav-recordid-viewname-tolerated.md diff --git a/.changeset/16875-nav-recordid-viewname-tolerated.md b/.changeset/16875-nav-recordid-viewname-tolerated.md new file mode 100644 index 0000000000..be176a153f --- /dev/null +++ b/.changeset/16875-nav-recordid-viewname-tolerated.md @@ -0,0 +1,46 @@ +--- +'@objectstack/spec': patch +--- + +`ObjectNavItem.recordId`'s docblock said it was "Mutually exclusive with `viewName`" — the guard tolerates that exact pair, deliberately + +The docblock read *"Mutually exclusive with `viewName` (viewName is ignored if +both are set)"*. The parenthetical was the tell: *"ignored if both are set"* +describes a **precedence**, not a refusal, so the sentence's own second clause +contradicted its first — and the code agrees with the second clause. +`recordId` + `viewName` parses clean through `NavigationItemSchema`; it is the +one legacy combination `objectNavTargetExclusivity` lets through, and that +guard's own docblock says so in as many words. + +**The harm direction is silent in both directions.** An author (or an agent) +who read "mutually exclusive" would avoid a combination the platform accepts, +or file a bug when it parses. Two docblocks in one file described one rule and +disagreed; the guard's was right. + +⛔ **No behaviour changes, and the asymmetry is not "unified".** The tolerance +is a recorded decision, and `app-nav-target-exclusivity-export.test.ts` already +pins `recordId` + `viewName` as accepted precisely so that making the target +fields pairwise exclusive goes red. This changeset corrects the **prose** only: +no schema, no guard, no accept set, no authorable key, no export moves. The +`.describe()` strings — the ones that reach `content/docs/references/` — are +untouched. + +The corrected docblock now says the pair is tolerated rather than refused, +names the guard that tolerates it, and points at the test that pins it. The +same test file gains a fifth leg asserting the docblock against the accept set +it describes, so the next copy of this sentence goes red instead of shipping: +prose is the only place the tolerated pair is documented, so nothing else was +watching it. + +**This is shipped, which is why it carries a changeset rather than +`skip-changeset`.** `@objectstack/spec`'s published `files[]` carries both +`dist` and `src/**/*.zod.ts`, and `src/ui/app.zod.ts` matches that glob — the +edited file is shipped as source verbatim. Measured on the built artifact as +well: the new sentence is present in **18** built files under `dist/` and the +old spelling in **0**, with two untouched sentences from the same region +(`navigate straight to the detail page` and the `filters` docblock's TRUE +`Mutually exclusive with \`recordId\` / \`viewName\``) present in **18** each as +the lit controls, so the zero is a reading and not a mistyped anchor. The +declaration files do not carry it — this is a field-level docblock inside a Zod +shape — which is why the reach is stated as the bundles and the shipped source +rather than as `.d.ts`. diff --git a/packages/spec/src/ui/app-nav-target-exclusivity-export.test.ts b/packages/spec/src/ui/app-nav-target-exclusivity-export.test.ts index 88644544fb..4fd859d73b 100644 --- a/packages/spec/src/ui/app-nav-target-exclusivity-export.test.ts +++ b/packages/spec/src/ui/app-nav-target-exclusivity-export.test.ts @@ -292,3 +292,51 @@ describe('`./index` (the `@objectstack/spec/ui` surface) exports the same functi expect(objectNavTargetExclusivity.length).toBe(2); }); }); + +// --------------------------------------------------------------------------- +// Leg 5 — the `recordId` docblock agrees with the accept set it describes +// --------------------------------------------------------------------------- + +/** + * The tolerated pair is the one an author learns about from PROSE, not from a + * refusal: nothing goes red when the docblock drifts away from it. The + * `recordId` docblock did drift — it declared the pair "mutually exclusive" + * while leg 1 pins it as accepted — so the prose gets a pin of its own beside + * the measurement it has to match. + * + * The extraction throws when its anchor moves, so this can never pass + * vacuously on a docblock it failed to find. The docblock is normalised the + * way a JSDoc block has to be before it can be searched: strip the leading + * `*` of each line first, THEN flatten whitespace — a claim wraps lines, and + * a raw search reads a confident 0 on text that is plainly there. + */ +describe('`recordId` docblock parity with the accept set (#16875)', () => { + const src = fs.readFileSync(path.join(HERE, 'app.zod.ts'), 'utf8'); + + const recordIdDocblock = ((): string => { + const m = src.match(/\/\*\*([\s\S]*?)\*\/\s*\n\s*recordId: z\.string\(\)\.optional\(\)/); + if (!m) throw new Error('recordId docblock not found in app.zod.ts — the anchor moved, re-locate it BY TEXT'); + return m[1].replace(/^[ \t]*\*[ \t]?/gm, '').replace(/\s+/g, ' ').trim(); + })(); + + it('the extraction is lit — it found a non-empty docblock that names `viewName`', () => { + expect(recordIdDocblock.length).toBeGreaterThan(80); + expect(recordIdDocblock).toContain('viewName'); + }); + + it('claims no exclusivity the guard does not enforce', () => { + expect(recordIdDocblock).not.toMatch(/mutually exclusive/i); + expect(recordIdDocblock).not.toMatch(/not combinable/i); + expect(recordIdDocblock).not.toMatch(/cannot be combined/i); + }); + + it('says the pair is tolerated — and the schema still accepts it', () => { + expect(recordIdDocblock).toMatch(/tolerated/i); + const r = NavigationItemSchema.safeParse({ + ...NAV, + recordId: '{current_user_id}', + viewName: 'all', + }); + expect(r.success).toBe(true); + }); +}); diff --git a/packages/spec/src/ui/app.zod.ts b/packages/spec/src/ui/app.zod.ts index d42e9f0392..c47671ab3b 100644 --- a/packages/spec/src/ui/app.zod.ts +++ b/packages/spec/src/ui/app.zod.ts @@ -390,8 +390,16 @@ export const ObjectNavItemSchema = lazySchema(() => strictObject(navItemSurface( * When set, navigate straight to the detail page of this specific * record instead of the object's list view. Supports template * variables `{current_user_id}` and `{current_org_id}` resolved by - * the shell at render time. Mutually exclusive with `viewName` - * (viewName is ignored if both are set). + * the shell at render time. + * + * Combining it with `viewName` is TOLERATED, not refused — it is the one + * legacy pair {@link objectNavTargetExclusivity} deliberately lets through + * (that guard's own docblock names it), and `viewName` is ignored when both + * are set. Read as an exclusivity, this sentence would send an author away + * from a combination the platform accepts, or have them file a bug when it + * parses. The tolerance is deliberate and pinned by + * `app-nav-target-exclusivity-export.test.ts`; the combinations that ARE + * refused live on `filters` and `runAction`. */ recordId: z.string().optional().describe( 'Navigate directly to this record id instead of the list view. Supports template vars: {current_user_id}, {current_org_id}.', From bfd149e080b9bf3044b5aebca972aaa90a6c3bc0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 15:04:07 +0000 Subject: [PATCH 2/2] docs(changeset): drop escaped backticks that would render as literal backslashes The measurement paragraph quoted a neighbouring docblock sentence inside a code span that already contained backticks, so the inner pair was escaped and markdown would render the backslashes verbatim. Name the claim instead of quoting it. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH --- .changeset/16875-nav-recordid-viewname-tolerated.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/16875-nav-recordid-viewname-tolerated.md b/.changeset/16875-nav-recordid-viewname-tolerated.md index be176a153f..a07cafb6b6 100644 --- a/.changeset/16875-nav-recordid-viewname-tolerated.md +++ b/.changeset/16875-nav-recordid-viewname-tolerated.md @@ -38,9 +38,9 @@ watching it. edited file is shipped as source verbatim. Measured on the built artifact as well: the new sentence is present in **18** built files under `dist/` and the old spelling in **0**, with two untouched sentences from the same region -(`navigate straight to the detail page` and the `filters` docblock's TRUE -`Mutually exclusive with \`recordId\` / \`viewName\``) present in **18** each as -the lit controls, so the zero is a reading and not a mistyped anchor. The +(`navigate straight to the detail page`, and the `filters` docblock's own TRUE +exclusivity claim over `recordId` / `viewName`) present in **18** each as the +lit controls, so the zero is a reading and not a mistyped anchor. The declaration files do not carry it — this is a field-level docblock inside a Zod shape — which is why the reach is stated as the bundles and the shipped source rather than as `.d.ts`.