From 9ef577d3872e277f8f4bc8f552a34d0b8ac86755 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 11:19:10 +0000 Subject: [PATCH] docs(plugin-gantt): stop instructing the retired `navigation.view` in the published README (objectui#9987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README's record-navigation paragraph told an author to write the member that `@objectstack/spec` retired under ADR-0049 (objectstack-ai/objectstack#18619) — and then, in the same paragraph, explained the mechanism by which an undeclared key rejects the whole config. The first sentence caused what the second diagnosed. This file is listed in `files[]`, so the instruction shipped in the npm tarball. The instruction is replaced by the route the retirement actually leaves standing, carried in from `@object-ui/react`'s `useNavigationOverlay` docblock: assign a `record` page to the object and let `isDefault` pick the one that opens. Deleting the sentence alone was not enough — it answered a real authoring question, and a reader who loses the answer goes looking and finds the retired key elsewhere. The second sentence is kept byte-identical (modulo re-wrapping): it is the only place this README tells an author why a typo costs them the whole block. It just stops being a description of what the paragraph above it told them to do. `readme-navigation-example.test.ts` now measures the PROSE as well as the fence. The fence tests could never have caught this — they extract the ```json block, and the block never carried the key. The pinned `@objectstack/spec@17.4.0` still declares `view`, so nothing schema-derived can say it is retired in this tree; it is named by hand, exactly as `basePath` already is, and the absence carries its own control: the same detector is run over the sentence that used to carry the instruction and must find it there. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq --- .../9987-gantt-readme-navigation-view.md | 15 +++ packages/plugin-gantt/README.md | 16 ++-- .../src/readme-navigation-example.test.ts | 96 +++++++++++++++++++ 3 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 .changeset/9987-gantt-readme-navigation-view.md diff --git a/.changeset/9987-gantt-readme-navigation-view.md b/.changeset/9987-gantt-readme-navigation-view.md new file mode 100644 index 0000000000..e950ee066c --- /dev/null +++ b/.changeset/9987-gantt-readme-navigation-view.md @@ -0,0 +1,15 @@ +--- +--- + +Stop instructing the retired `navigation.view` in `packages/plugin-gantt/README.md`, +and extend `readme-navigation-example.test.ts` to measure the paragraph's PROSE and not +only its `json` fence (objectui#9987). No package is released by this change. + +Polarity, decided against this gate's own criterion rather than by habit: the changed +README **is** in `@object-ui/plugin-gantt`'s `files[]` and does ship in the tarball, but +`check-changeset-presence.mjs` subtracts `*.md` from published source on purpose — +"published-but-not-code sits with the READMEs" — and the file that actually put this +change in the gate's population is the test under `src/`, which that same header answers +with the empty-frontmatter exemption in one line. No runtime behaviour, no export and no +published contract field moved. Every package here is in one fixed group, so the +corrected README reaches npm with the next release either way. diff --git a/packages/plugin-gantt/README.md b/packages/plugin-gantt/README.md index 7461cf5744..3695c7b1ba 100644 --- a/packages/plugin-gantt/README.md +++ b/packages/plugin-gantt/README.md @@ -53,13 +53,15 @@ When used through `ObjectGantt` (the wiring the framework uses for the The destination route is **not** authorable here — `useNavigationOverlay` builds no URL out of this config, so page mode hands the record to the - host's `onNavigate` / `onRowClick` and the host owns where it lands. To - choose *which* detail view opens, use the declared `view` member (a - form-view name, e.g. `"summary_view"`). `navigation` is the spec's - `NavigationConfig`, and its schema refuses any key it does not declare: an - undeclared key rejects the whole config, so the `mode` beside it never - takes effect either. `@objectstack/spec`'s `NavigationConfigSchema` owns the - member list. + host's `onNavigate` / `onRowClick` and the host owns where it lands. *Which* + detail layout opens is not authorable here either: assign a `record` page to + the object and let `isDefault` pick the one that opens. Page assignment is + what resolves a detail layout; this block only decides **how** that detail + is surfaced (`mode`, `size`) — `@object-ui/react`'s `useNavigationOverlay` + docblock owns that account. `navigation` is the spec's `NavigationConfig`, + and its schema refuses any key it does not declare: an undeclared key + rejects the whole config, so the `mode` beside it never takes effect either. + `@objectstack/spec`'s `NavigationConfigSchema` owns the member list. ### Drag-and-drop rescheduling diff --git a/packages/plugin-gantt/src/readme-navigation-example.test.ts b/packages/plugin-gantt/src/readme-navigation-example.test.ts index dc8811a778..0255f301b1 100644 --- a/packages/plugin-gantt/src/readme-navigation-example.test.ts +++ b/packages/plugin-gantt/src/readme-navigation-example.test.ts @@ -53,6 +53,29 @@ * A `safeParse` that succeeds proves nothing on its own if the schema accepts * everything, so the historical shape is parsed alongside and must be REJECTED * by name. The pair is the measurement; neither half alone is one. + * + * ## The PROSE is pinned too, not only the fence (objectui#9987) + * + * The fence was clean and the paragraph around it was not. It INSTRUCTED the + * reader to write `navigation.view` -- the member objectstack#18619 retired + * under ADR-0049 -- and then, in the same breath, explained the mechanism by + * which an undeclared key rejects the whole config. The first sentence caused + * what the second diagnosed, and a reader who stops at the instruction never + * reaches the contradiction. This README ships inside the npm tarball + * (`files: ["dist", "README.md", ...]`), so the instruction was published. + * + * The fence tests above could not see it: they extract the ```json block and + * the block never carried the key. An instruction can live entirely in prose, + * which is why the prose is measured here too. + * + * WARNING: no schema this package installs can derive that key's retirement. + * The pinned `@objectstack/spec@17.4.0` still DECLARES `view`, so + * `declaredMembers()` reports it legal and every schema-derived statement about + * it is vacuous in this tree; the retirement lands it as a tombstone (typed + * `never`, raising a prescription at parse) only when the pin moves. It is + * therefore named by hand, exactly as `basePath` already is -- and the absence + * carries its own control: the same detector is run over the sentence that used + * to carry the instruction, in the same file, and must find it there. */ import { describe, it, expect } from 'vitest'; @@ -151,6 +174,42 @@ const EXAMPLE = readmeExample(); /** The historical shape, kept only as the control's input. */ const REJECTED_KEY = 'basePath'; +/** + * The retired member (objectstack#18619, ADR-0049) the README used to instruct. + * Named by hand because this tree's pinned spec still declares it -- see the + * warning in the header. + */ +const RETIRED_KEY = 'view'; + +/** The form-view name the retired instruction offered as its worked value. */ +const RETIRED_KEY_SAMPLE = 'summary_view'; + +/** + * The replacement route, by the identifiers that carry it. `@object-ui/react`'s + * `useNavigationOverlay` docblock states it: assign a `record` page to the + * object and let `isDefault` pick the one that opens. The README has to keep + * ANSWERING the question the retired instruction answered -- deleting the + * instruction alone sends the reader looking, and the retired key is what they + * find. These are identifiers, not wording: the sentence may be rewritten + * freely as long as it still hands the reader these two. + */ +const REPLACEMENT_ROUTE = ['record', 'isDefault'] as const; + +/** Every inline code span in a markdown fragment, in order. */ +function codeSpans(markdown: string): string[] { + return [...markdown.matchAll(/`([^`\n]+)`/g)].map((m) => m[1]); +} + +/** + * The sentence this section carried until objectui#9987, verbatim. It is the + * control's input and nothing else: it is what lights `codeSpans` so that the + * empty result below reads as a measurement rather than as a detector that + * cannot fire. + */ +const RETIRED_INSTRUCTION = + 'To choose *which* detail view opens, use the declared `view` member (a ' + + 'form-view name, e.g. `"summary_view"`).'; + function unrecognizedKeys(issues: readonly { code: string }[]): string[] { const out: string[] = []; for (const issue of issues) { @@ -207,4 +266,41 @@ describe('plugin-gantt README: the record-navigation example', () => { '`useNavigationOverlay` builds no URL out of this config; the host owns the route.', ).toBe(false); }); + + it('does not INSTRUCT the retired detail-view key, in prose or fence', () => { + const section = navigationSection(); + expect( + codeSpans(section).filter((span) => span === RETIRED_KEY), + `The section must not tell an author to write \`${RETIRED_KEY}\`. It is retired ` + + '(objectstack#18619, ADR-0049): once the spec pin moves it is typed `never` and a ' + + 'value reaching the parse raises the prescription — which is the paragraph’s own ' + + 'next sentence, turned on the reader who followed the paragraph’s first one.', + ).toEqual([]); + expect( + section.includes(RETIRED_KEY_SAMPLE), + `\`${RETIRED_KEY_SAMPLE}\` was the worked value the instruction offered. A sample ` + + 'left behind still teaches the key, with the member name only implied.', + ).toBe(false); + }); + + it('CONTROL: the same detector finds the key in the sentence that carried it', () => { + expect( + codeSpans(RETIRED_INSTRUCTION).filter((span) => span === RETIRED_KEY), + 'This control is what makes the empty result above a measurement. If `codeSpans` ' + + 'stops finding the key HERE — in the sentence that indisputably carries it — then ' + + 'the clean section above says nothing at all.', + ).toEqual([RETIRED_KEY]); + expect(RETIRED_INSTRUCTION.includes(RETIRED_KEY_SAMPLE)).toBe(true); + }); + + it('still answers the question the retired instruction answered', () => { + const spans = new Set(codeSpans(navigationSection())); + expect( + REPLACEMENT_ROUTE.filter((name) => !spans.has(name)), + 'Deleting the instruction is not the repair on its own: it answered a real authoring ' + + 'question — which detail layout opens — and a reader who loses the answer goes ' + + 'looking and finds the retired key elsewhere. The section must name the replacement ' + + 'route `@object-ui/react`\'s `useNavigationOverlay` docblock states.', + ).toEqual([]); + }); });