Commit fd96a84
fix(spec-docs): one shared def renders one optionality face on every reference page (#21478)
Fixes #21466
Clause-②: no
## What was wrong
A generated reference page gave two optionality answers for one shared
def. The card measured it on PR #21463's regeneration of
`content/docs/references/ui/component.mdx`:
`ObjectGridProps.grouping.fields[]` rendered `order?` / `collapsed?`,
and `data[provider='api'].read` / `.write` rendered `method?`, while the
same defs on the `ObjectKanbanProps`, `ObjectGanttProps`,
`ObjectMapProps` and `ObjectTreeProps` rows rendered them required.
## Mechanism: measured, and it is not evaluation order
The card's working hypothesis was an evaluation-order dependence: a
memoised conversion, a `.shape` getter or a def cache decided by
whichever caller came first. Measured, there is none:
- **Two-order harness.** The five props schemas go through the
generator's own `projectPublishedJsonSchema` and its output-to-input
fallback, under 2 import orders (`ui/view.zod` first or
`ui/component.zod` first) times 2 emission orders (grid first or grid
last), with `OS_EAGER_SCHEMAS=1` as `gen:schema` runs. All four orders
print byte-identical output, with 1 distinct output per tree. That holds
on `main` at `6210f8870a`, on #21463's head `38159d1362`, and on this
branch's final head.
- **What actually flips is the document's io mode.** On #21463's head,
`ui/ObjectGridProps.json` is the one props schema emitted "(input
shape)". Its output projection throws "Transforms cannot be represented
in JSON Schema", because one newly typed member carries a transform. So
`build-schemas.ts` falls back to `io: 'input'` for the whole document.
Input mode leaves `.default()` members out of `required`, output mode
lists them, and both keep `default`.
- **The renderer leaked that mode.** The Type column's `{ … }` shape
summary marked a key optional from `required` alone
(`scripts/lib/format-type.ts`). The Required column already read
`default` (`renderRequiredCell`, #8703's rule). Before this change,
#8703's rule covered only one of the two places a page states
optionality.
Corpus reading on `main` `6210f8870a`, with the old renderer: 1398
exports project in both io modes. When each section is rendered from
both projections, 215 of them differ, over 483 lines. Each of the 483
lines differs only by this marker.
## Fix
There is one predicate now, `isAuthorOmittable(prop, required)` in
`format-type.ts`: `default` decides, and `required` breaks the tie. Both
optionality positions read it: the shape summary's `key?:` marker and
`renderRequiredCell`, whose output is unchanged byte for byte.
What does not change:
- No emitted JSON Schema. Under #8703's design, the published files keep
describing the post-parse shape.
- No `packages/spec/src/**` change.
- No per-row patch, and no hand edit of a generated page.
Afterwards the same corpus measurement reads 0 of 1398 differing (0
lines) on `6210f8870a`, and 0 of 1392 on the final head `d5d88f55a5`.
The population is smaller on the final head because `ObjectGridProps` is
input-only there and `main`'s agent.lifecycle retirement removed some
exports.
## Regenerated pages (the second pin)
`check:generated --fix` on the base changed 459 rows on 86 pages. Each
changed row only adds `?` to defaulted members (932 markers in total).
No row removes one, and no other byte moved.
`main` was merged in twice, and each time the colliding generated page
was regenerated on the merged tree rather than text-merged:
- The first merge brought #21463 and regenerated `component.mdx`.
- The second brought `main` at `0b8239111f` (the agent.lifecycle
retirement plus one non-docs commit) and regenerated `agent.mdx`.
`automation/state-machine.mdx` stays deleted, as `main` has it.
Against `main` at `0b8239111f` (and identically against `6e33b67912`),
the references delta is 453 rows on 85 pages, 925 markers added. With
every `?:` normalised to `:`, each of the 85 pages is byte-identical to
main's copy. No row removes a marker.
On the final `component.mdx`:
- `method?:` appears 8 times and `method:` 0 times; `collapsed?:` 3
times and `collapsed:` 0 times.
- The grid's and the kanban's `grouping.fields` rows are byte-identical.
- The four `data[provider='api']` `read` rows are byte-identical, and so
are the four `write` rows.
The same JSON from #21463's head, rendered with the old renderer, gives
the card's exact rows (`order?` on the grid and `order` on the kanban).
Rendered with this branch's renderer, every shared-def row is identical.
## Pin
`packages/spec/scripts/schema-section.test.ts` has a new block, "one
shared def renders one face, whichever io mode its document took". Two
parent rows share a grouping def and a `{ url, method }` def through the
generator's own projection. One parent projects in output mode; the
other projects only in input mode, because it has a transform member.
- Two precondition cases prove the fixture really is one output-mode and
one input-mode document, and that their `required` arrays disagree. So
the identical rendering comes from the renderer, not the fixture.
- The render case requires the shared defs' rows to be byte-identical
and on the input face.
- A fourth case pins the marker on hand-written nodes.
Reverse verification used the committed fix at `3a62f4a391`. The
mutation went through `scripts/ablation-replace.mjs`: anchor count 1 to
0, blob changed. The restore was proven: blob equals HEAD and `git diff
HEAD` is empty. Putting back the old `required`-only marker turns 2 of
137 cases red: the render case and the hand-written summary case. The
two precondition cases stay green, as designed. The subject resolves to
source through a relative import, so no `dist/` leg applies.
## Gates
At the final head `d5d88f55a5`, after the second merge:
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: 602 files, 17769 passed, 1 todo.
- `check:generated` ("All 15 generated artifacts are up to date"),
`check:docs` and `check:nul-bytes`: exit 0.
At `ce1d8ceaf8`, before the second merge, which brought only main's
commits and one regenerated page:
- `pnpm --filter @objectstack/spec typecheck` (the build program,
`tsconfig.scripts.json` and the test program): exit 0. The three touched
TS files are in the `tsconfig.scripts.json` program (checked with
`--listFilesOnly`).
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran`: 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN, every one exit 0.
That includes `check:generated`, `check:docs` and `check:nul-bytes`.
This full union was not re-run at `d5d88f55a5`.
- Lint was narrowed to the 3 touched TS files: `eslint
--no-inline-config --format json` reads 3 files, 0 errors, 0 warnings.
The `.mdx` pages match no lint `files` glob in `eslint.config.mjs`. That
config enables no type-aware linting (no `parserOptions.project`), so
this diff cannot move a verdict on an untouched file. The repo-wide
`pnpm lint` is left to CI.
## Changeset
`skip-changeset`, measured:
- `@objectstack/spec`'s `files[]` is `dist`, `json-schema`, `liveness`,
`prompts`, `llms.txt`, `README.md`, `src/**/*.zod.ts`, `CHANGELOG.md`,
`api-surface` and `spec-changes.json`.
- This diff touches only `packages/spec/scripts/**` and
`content/docs/references/**`. The docs site, `apps/docs`, is private.
- After a build, `isAuthorOmittable` and `carriesDefault` match 0 files
under those paths. The positive control, `lazySchema`, matches 240.
- `check:generated` moved no published artifact.
## Acceptance notes
- The two-order harness and the corpus io-face measurement were one-time
readings from scratch scripts and are not committed. The committed pin
is the two-parent-row block above.
- Open edge, with no instances today: the predicate reads a member's own
`default`. A member spelled as a bare `$ref` to a defaulted def would
carry `default` only on the def, not on the property node. The corpus
reads 0 such members (0 differing lines in either column after the
change). Carrier: none.
- The io fallback is per document by design: one transform anywhere
moves the whole document to the input projection. After this change that
no longer shows on the page. It does still decide which JSON Schema face
`json-schema/` publishes for that document, and that is out of this
card's scope.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5555047 commit fd96a84
88 files changed
Lines changed: 618 additions & 457 deletions
File tree
- content/docs/references
- ai
- api
- automation
- data
- identity
- integration
- kernel
- marketplace
- security
- studio
- system
- ui
- packages/spec/scripts
- lib
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
| 51 | + | |
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
| 60 | + | |
61 | 61 | | |
62 | 62 | | |
63 | | - | |
| 63 | + | |
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
97 | | - | |
| 97 | + | |
98 | 98 | | |
99 | | - | |
| 99 | + | |
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| |||
179 | 179 | | |
180 | 180 | | |
181 | 181 | | |
182 | | - | |
| 182 | + | |
183 | 183 | | |
184 | | - | |
| 184 | + | |
185 | 185 | | |
186 | 186 | | |
187 | 187 | | |
| |||
228 | 228 | | |
229 | 229 | | |
230 | 230 | | |
231 | | - | |
| 231 | + | |
232 | 232 | | |
233 | | - | |
| 233 | + | |
234 | 234 | | |
235 | 235 | | |
236 | 236 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
| 57 | + | |
58 | 58 | | |
59 | | - | |
| 59 | + | |
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| |||
202 | 202 | | |
203 | 203 | | |
204 | 204 | | |
205 | | - | |
| 205 | + | |
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
| |||
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
215 | | - | |
| 215 | + | |
216 | 216 | | |
217 | | - | |
| 217 | + | |
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| |||
298 | 298 | | |
299 | 299 | | |
300 | 300 | | |
301 | | - | |
| 301 | + | |
302 | 302 | | |
303 | 303 | | |
304 | 304 | | |
| |||
340 | 340 | | |
341 | 341 | | |
342 | 342 | | |
343 | | - | |
| 343 | + | |
344 | 344 | | |
345 | 345 | | |
346 | 346 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
159 | | - | |
160 | | - | |
| 159 | + | |
| 160 | + | |
161 | 161 | | |
162 | 162 | | |
163 | 163 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
153 | | - | |
| 153 | + | |
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
190 | | - | |
| 190 | + | |
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
| |||
0 commit comments