…55) (objectstack-ai#10504)
Fixes objectstack-ai#8255
Clause-②: no
The docs half of the objectui#7928 ruling (decision batch objectstack-ai#70, option
A). The renderer half, objectui#8254, landed as PR objectstack-ai#9605 and honours a
spec-shaped named view, including `columns` in either union arm and
`filter` as rule objects. This PR makes the four pages teach that shape:
the three the card names, plus the CRUD guide folded in by the seat
decision `5827390159` (patch round 1, head `9c7f893d3`).
## What changed
- `packages/plugin-view/README.md` and
`content/docs/plugins/plugin-view.mdx` (the same three examples in
each): every named view now carries `columns`, the one key
`ObjectListViewSchema` requires. The label-only `all` view in the
ObjectView overview gains `columns: ['name', 'email']` (the same list as
the snippet's `table.columns`), and its inline comment now says each
view needs `label` and `columns`. `cheap` (Read/List) gains `columns:
['name', 'price']` and `admins` (View tabs) gains `columns: ['name',
'email']`, matching their sibling views.
- `content/docs/api/schema-reference.md`, the `ObjectViewSchema`
example: both views gain `columns` (the example's own `table.columns`
list), and `my-deals` spells its filter as a spec rule object, `{
"field": "owner", "operator": "equals", "value": "{current_user_id}" }`,
instead of the ObjectQL tuple. The value is the spec's `CONTEXT_TOKENS`
spelling (patch round 1; it was an invented `${currentUser.id}` nothing
resolves), and one sentence beside the example says the host that
renders the list resolves the token, with the `object-view` node path
tracked in objectui#10506. The view is again written `label`-first. The
old order only existed to keep a pinned line byte-identical, and that
pin now reads this example differently (see below).
- `content/docs/guide/building-crud-app.md` (patch round 1):
`TaskSchema.listViews.active.filter` becomes the rule object `{ field:
'status', operator: 'not_equals', value: 'Done' }` instead of the tuple.
- `.changeset/object-view-unmirrored-keys-7779.md` (patch round 1): the
one sentence this PR falsified now reads in the past tense (the spec
refused the named views the docs taught when the entry was written;
objectui#8255 has since rewritten them). The front matter is
byte-identical.
- `scripts/markdown-test-inputs.mjs` (patch round 1): the 7779 pin's
ledger row gains the guide page in `reads`, because `--audit` refuses an
unadjudicated document the pin now resolves.
- No renderer, type or schema change.
## Validation against the installed spec
Schema: `ObjectListViewSchema` from `@objectstack/spec/ui`,
**`@objectstack/spec@17.4.0`**, resolved from `packages/types` (the
`^17.4.0` range there, `^17.0.0` in `plugin-view`, same install). This
scratch script was not committed. It finds every fenced block in the
three pages that mentions `listViews` and parses it with the TypeScript
parser (a `json` body is wrapped in an initializer). For each
`listViews` property it turns the literal into a value and runs each
entry through `ObjectListViewSchema.safeParse`.
Before (base `39395455a`), 12 entries in 7 blocks, **8 refused**:
```
REFUSE packages/plugin-view/README.md:154 (typescript) listViews.all -> columns: invalid_union
ACCEPT packages/plugin-view/README.md:464 (typescript) listViews.all
REFUSE packages/plugin-view/README.md:464 (typescript) listViews.cheap -> columns: invalid_union
ACCEPT packages/plugin-view/README.md:622 (typescript) listViews.active
REFUSE packages/plugin-view/README.md:622 (typescript) listViews.admins -> columns: invalid_union
REFUSE content/docs/plugins/plugin-view.mdx:72 (typescript) listViews.all -> columns: invalid_union
ACCEPT content/docs/plugins/plugin-view.mdx:399 (typescript) listViews.all
REFUSE content/docs/plugins/plugin-view.mdx:399 (typescript) listViews.cheap -> columns: invalid_union
ACCEPT content/docs/plugins/plugin-view.mdx:523 (typescript) listViews.active
REFUSE content/docs/plugins/plugin-view.mdx:523 (typescript) listViews.admins -> columns: invalid_union
REFUSE content/docs/api/schema-reference.md:872 (json) listViews.all -> columns: invalid_union
REFUSE content/docs/api/schema-reference.md:872 (json) listViews.my-deals -> columns: invalid_union | filter.0: invalid_type
SUMMARY listViews blocks=7 entries accepted=4 refused=8 controls=pass
```
After (this branch), **all 12 accepted, controls refused**:
```
ACCEPT packages/plugin-view/README.md:154 (typescript) listViews.all
ACCEPT packages/plugin-view/README.md:464 (typescript) listViews.all
ACCEPT packages/plugin-view/README.md:464 (typescript) listViews.cheap
ACCEPT packages/plugin-view/README.md:623 (typescript) listViews.active
ACCEPT packages/plugin-view/README.md:623 (typescript) listViews.admins
ACCEPT content/docs/plugins/plugin-view.mdx:72 (typescript) listViews.all
ACCEPT content/docs/plugins/plugin-view.mdx:399 (typescript) listViews.all
ACCEPT content/docs/plugins/plugin-view.mdx:399 (typescript) listViews.cheap
ACCEPT content/docs/plugins/plugin-view.mdx:524 (typescript) listViews.active
ACCEPT content/docs/plugins/plugin-view.mdx:524 (typescript) listViews.admins
ACCEPT content/docs/api/schema-reference.md:872 (json) listViews.all
ACCEPT content/docs/api/schema-reference.md:872 (json) listViews.my-deals
CONTROL ok tuple filter: refused -> filter.0: invalid_type
CONTROL ok label-only: refused -> columns: invalid_union
CONTROL ok entry-level default: refused -> (root): unrecognized_keys ["default"]
CONTROL ok catalog shape: accepted
SUMMARY listViews blocks=7 entries accepted=12 refused=0 controls=pass
```
Patch round 1, over four pages (the CRUD guide added). Base `39395455a`:
`SUMMARY listViews blocks=8 entries accepted=5 refused=9 controls=pass`
(the extra refusal: `content/docs/guide/building-crud-app.md:130 (ts)
listViews.active -> filter.0: invalid_type`). Head `9c7f893d3`:
```
SUMMARY listViews blocks=8 entries accepted=14 refused=0 controls=pass
```
The card named three examples. Measured, **seven blocks carried eight
refused entries**: the `cheap` and `admins` views in both the README and
the mdx were refused too (no `columns`), so all of them are rewritten.
## The pin that held the old forms, and its successor
`packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts`
pinned the refused forms off disk as "still what the docs teach". That
was the input to the measurement keeping `listViews` ledgered. Rewriting
the docs turned it red, as designed. Before the successor was written it
failed one test out of 233 (`expected '# @object-ui/plugin-view…' to
contain 'listViews: { all: { label: 'All User…'`).
That test is replaced by its successor and is not weakened. The new test
reads every named view the four pages author off disk (three, plus the
CRUD guide from patch round 1), using the TypeScript parser, and
requires each one to parse under `ObjectListViewSchema`. It requires at
least one entry per page, so a page cannot pass vacuously. It also runs
a positive control through the same predicate: the retired label-only
and tuple forms must come back refused, and the spec shape must not. The
spec-side refusal assertions in the test before it are unchanged; only
its title and comments moved to the past tense. The file header sentence
that said the docs "teach" the refused forms is updated the same way.
The pin reads all four pages, and its `scripts/markdown-test-inputs.mjs`
ledger row names all four in `reads` (`--audit`: exit 0, `75 declared
entries, all present`).
This PR claims the pages validate. Per AGENTS.md objectstack-ai#9, that claim now
points at this pin as the instrument that re-derives it, not at the
scratch output above.
### Reverse verification (one-time, not committed)
Each leg ran through `ablation-replace.mjs`: the anchor had to hit, the
blob had to change on disk, and the restore was proven by blob == HEAD
plus an empty `git diff HEAD`.
- **Leg A**: drop `columns` from the README's `cheap` view. Predicted
red, naming that entry. Observed exit 1: `+
"packages/plugin-view/README.md:458 listViews.cheap"`. Restored, blob
`e7f0e491f2b4` == HEAD.
- **Leg B**: put the tuple filter back in `schema-reference.md`.
Predicted red naming `my-deals`, with the objectui#7923 pin still green.
Observed exit 1: `+ "content/docs/api/schema-reference.md:853
listViews.my-deals"`, `Test Files 1 failed | 1 passed (2)`. Restored,
blob `67508396a7c5` == HEAD.
- **Leg C**: set `columns: 'name'` on the README overview view, to prove
`check:doc-snippets` compiles that block. Observed exit 1:
`packages/plugin-view/README.md:154:43 TS2322: Type 'string' is not
assignable to type 'string[]'`. Restored.
- **Leg D** (patch round 1): put the tuple back in the CRUD guide.
Observed exit 1: `+ "content/docs/guide/building-crud-app.md:89
listViews.active"`, `Tests 1 failed | 228 passed (229)`. Restored: blob
`f390aef0ef41` == HEAD, `git diff HEAD` empty.
## Gates, at HEAD `9c7f893d3` (patch round 1 re-ran every gate below)
| gate | result |
|:--|:--|
| `pnpm check:doc-snippets` (after the gate's own `--build-filter`
closure, `Tasks: 35 successful, 35 total`) | exit 0: `Semantic phase:
673 of 673 block(s) judged, 0 failed.` |
| `pnpm check:doc-examples` | exit 0: `Every covered @example compiles,
or fails exactly as its ledger row declares.` No ledger row keys on
these examples (its three plugin-view rows are JSDoc blocks in the
ObjectView source) |
| `pnpm check:doc-fences` | exit 0 |
| `pnpm check:doc-types` | exit 0: `Every documented component type is
registered.` |
| `pnpm docs:check-links` | exit 0: `Links are valid across 17 scan
roots.` |
| `node scripts/check-doc-example-ids.mjs` · `pnpm check:readme-exports`
· `node scripts/check-shell-escape-residue.mjs` | exit 0 each |
| `pnpm check:control-bytes` | exit 0 |
| `pnpm check:new-line-citations` | exit 0: `0 new citation(s)` |
| `node scripts/check-changeset-presence.mjs` | exit 0. It owed a
changeset for the test file under `packages/types/src/`, answered with
the empty-frontmatter `.changeset/8255-docs-spec-shaped-list-view.md`.
The docs alone owed none |
| `node scripts/check-changeset-no-major.mjs` · `node
scripts/check-pending-changeset-literals.mjs` · `node
scripts/markdown-test-inputs.mjs --audit` | exit 0 each |
| doc-gate suites: 11 files under `scripts/__tests__/` (the
`check-doc-*` suites, `markdown-test-inputs`,
`check-pending-changeset-literals`) | exit 0: `Test Files 11 passed
(11)`, `Tests 634 passed (634)` |
| `@object-ui/types`, whole package: `pnpm exec vitest run
packages/types/` | exit 0: `Test Files 232 passed (232)`, `Tests 5156
passed (5156)` |
| `pnpm --filter @object-ui/types type-check` (includes
`tsconfig.test.json`; `--listFiles` shows the edited pin in the program,
1 hit) | exit 0 |
| `node scripts/check-governed-queue-guard.mjs --test` on the eight
paths | `NOT GOVERNED` |
| patch round 1 additions: `pnpm check:changeset-claims` (the 7779 entry
no longer flagged) · `node scripts/check-changeset-overwrite.mjs`
(report-only; 7779 front matter unchanged, `@object-ui/types: minor`) ·
the 7779 + 7923 pins and the `guideCrudAppRenders` readers in
plugin-detail and plugin-form (`Tests 249 passed (249)`) · 18
`scripts/__tests__` suites naming the edited gates (`Tests 991 passed
(991)`) | exit 0 each |
NOT MEASURED: the docs-site build (`Build Docs`), because CI owns it.
The mdx edit is inside a code fence.
## Premise check
- The card's line numbers were re-measured (see the output above).
- ⚠️ **The `default: true` half of the card's premise is already gone.**
PR objectstack-ai#7991 (objectui#7923) removed the entry-level flag and added the
node-level `defaultListView`, so it is not in this diff. The rest of the
premise held.
## Acceptance notes
- A present-tense paragraph in the **pending changeset
`.changeset/object-view-unmirrored-keys-7779.md` is falsified by this
PR**. It says the spec refuses "the named views this package's own
README and `content/docs/api/schema-reference.md` teach"
(`check:changeset-claims`, report-only, flags it). That file is outside
this claim's file surface, so it is left untouched here. It publishes
verbatim at the next release, and the open release PR objectstack-ai#5400 carries it.
⇒ **Corrected in patch round 1** (seat decision `5827390159`, option A):
the one sentence now reads in the past tense, and the front matter is
byte-identical.
- Comments that describe the docs as still teaching the refused forms:
the `listViews` ledger docblock in
`packages/types/src/zod/objectql.zod.ts` and the matching
`UnmirroredDeclared` entry comment in
`packages/types/src/__tests__/zod-mirror-parity.test.ts`. The owner is
objectui#7928's mirror flip, which rewrites both. The header of
`schema-reference-named-list-view-keys-7923.test.ts` says the filter
line is "held verbatim" by the 7779 pin and that the running dialect is
unmeasured; no carrier has been named for it. Its assertions stay green.
- `content/docs/guide/building-crud-app.md` also teaches a named view
the spec refuses: `TaskSchema.listViews.active` uses the tuple
`[['status', '!=', 'Done']]`, refused by the spec's
`ObjectSchema.listViews` at `active.filter.0`, while the rule-object
spelling parses. It was not one of this card's three pages. ⇒ **Folded
in by patch round 1** (seat decision `5827390159`): the rule-object
spelling now, and the page joins the pin.
- `my-deals` keeps its value `${currentUser.id}`, because only the
dialect was in scope. Nothing resolves that value on an `object-view`
node. `SchemaRenderer` does not evaluate `listViews` (the spec's
`expressionBindableTextKeysFor('object-view')` is empty), and
plugin-view, plugin-grid and plugin-list have zero
`resolveFilterPlaceholders` / `current_user_id` / `useFilterScope`
reads. For comparison, app-shell's `ObjectView.tsx` has 3. The spec's
context token is `{current_user_id}`. ⇒ **Patch round 1** (seat decision
`5827390159`, option B): the example now uses `{current_user_id}`, with
one sentence naming the host that resolves it; the `object-view` node
path's gap is filed as objectui#10506.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #7923
content/docs/api/schema-reference.md'sObjectViewSchemaexample authored anentry-level key that nothing declares and nothing reads, so the view it labelled
"default" never opened first — twenty rows above the same page's property table
that documents the correct key.
Before / after
"my-deals": { - "label": "My Deals", "filter": [["owner", "=", "${currentUser.id}"]], - "default": true + "label": "My Deals" } }, + "defaultListView": "my-deals", "table": {Why the two remaining members are reordered rather than left alone: the
sibling pin
packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts:529holds the filter line verbatim including its trailing comma —
— so deleting
"default": truewhilefilterstayed last would have dropped thatcomma and reddened a pin held by draft PR #7969.
filtertherefore keeps a memberafter it. No content was invented to fill the slot.
Premises, re-measured on
origin/maine97e8e8NamedListView(packages/types/src/objectql.ts:1983) declares nodefault;ObjectView.tsx:701readsschema.defaultListViewlabelpresent; renderer line quoted belowdefaultListViewcontent/docs/api/schema-reference.md:885defaultgit grep -n '"default": true' -- content/docsreturns exactly this one hit; the twelvedefault: truehits are all(default: true)prose in property comments, a different shapecheck:doc-snippets' scan surfaceUNGATED_DOCSentries; butTS_FENCE_LANGUAGES = {ts, tsx, typescript}, and this example is a```jsonfence, so it is collected and never compiled (#5250). That is exactly the hole the fixture pin fillsThe pin, and why it is a fixture pin and not a type annotation
packages/types/src/__tests__/schema-reference-named-list-view-keys-7923.test.tsparses both sides off disk on every run — the shape #7925 points at
(
packages/plugin-calendar/src/readme-calendar-view-schema.test.ts): thedeclaration's own top-level members are brace-matched out of the interface source
(brace matching, not a line regex, because
NamedListViewcarries JSDoc blocks andfour nested object members that a regex would miscount), and the example is located
by its
### ObjectViewSchemaheading and parsed as JSON. Restating the member listin the test would reproduce the original defect one layer up.
An annotation on the example would catch nothing.
BaseSchemacloses with[key: string]: any(#7927), so every invented key type-checks through the indexsignature — #7927 measured it directly (probe P5: renaming
titleFieldtotitleFielddinside an annotated block leftcheck:doc-snippetsat exit 0). Theannotation checks a key's type and never its name. What makes a name-level
comparison possible here at all is that
NamedListViewis a standalone interfacethat does not extend
BaseSchema, so its declared members are a genuinely closedlist.
The pin asserts four things: a live control (the declaration parsed to >30 members
including the required
label, and the doc parsed to exactly the two named views),the predicate itself (every authored
listViewskey is declared), the node-leveldefaultListViewnaming a key that exists, and a positive control that runs onevery invocation — it plants
defaultinto the parsed object and asserts thesame predicate function returns
['default'], so the harness cannot go quietlygreen. That control opens by asserting
NamedListViewdoes not declaredefault,which is the tripwire if that ever changes.
The pin imports only
vitestand node builtins and reads files off disk, so itneeds no build.
Reverse verification (post-commit, trap-protected, restore proven by blob hash)
Each leg mutated the committed doc, proved the mutation actually landed (on-disk
hash ≠ the
HEADblob hash87e58fa…, so no leg was a silent no-op), ran the pin,then restored with
git checkout HEAD -- ABSOLUTE_PATHand proved the restore byhash equality and an empty
git diff HEAD."default": trueback intomy-dealsdefaultauthors no key NamedListView does not declare…expected [ 'default' ] to deeply equal []"defaultListView"linemust spell \defaultListView` … expected 'undefined' to be 'string'`Error: "### ObjectViewSchema" heading not found … the example moved,Tests no testsTest Files 1 passed (1),Tests 4 passed (4)The positive control was among the 3 passing tests in every red leg, so it is
demonstrably executed rather than merely present.
Gates
object-view-unmirrored-keys-7779.test.ts(the sibling pin on the filter line)Test Files 2 passed (2),Tests 94 passed (94)pnpm check:doc-snippetsSemantic phase: 495 of 495 block(s) judged, 0 failed.(after its own scoped--build-filterbuild, 34 tasks, exit 0; the first run's exit 2 was "I could not run", not a verdict)pnpm check:doc-fencesevery TypeScript block in 227 document(s) is fenced ts/tsx/typescriptnode scripts/check-doc-links.mjsLinks are valid across 17 scan roots.pnpm check:doc-typesEvery documented component type is registered.pnpm check:control-bytesscanned 6430 tracked text file(s); skipped 85 binarynode scripts/check-changeset-presence.mjspnpm changeset:checkNo changeset declares a major bump.check-governed-queue-guard --test(all 3 paths)NOT GOVERNED — 3 path(s) checked against 5 governed surface(s)pnpm --filter @object-ui/types type-checktsc -p tsconfig.test.json --listFilesshows the new test IS in the program, so this is a measurement of it and not around itpnpm --filter @object-ui/types lint273 problems (0 errors, 273 warnings), all pre-existing; the new file contributes 0 errors, 0 warnings (--format json, 1 file)pnpm exec vitest run packages/types/Test Files 131 passed (131),Tests 2418 passed (2418)pnpm check:doc-example-readersnode scripts/check-doc-expression-carriage.mjspnpm check:docs-route-closureReaders of the page —
git grep -l 'schema-reference.md' -- scripts/ packages/ .github/ content/returns 16 sites; every test among them was run together and passed:Test Files 10 passed (10),Tests 354 passed (354)(object-view-unmirrored-keys-7779,component-docs-retired-handler-keys-7340,kanban-plugin-dialect-authoritative-7664,object-grid-title-mirrored,tree-view-data-retired-6951,zod-mirror-parity,table-column-contract,check-doc-component-types,check-doc-links, plus the new pin).Lint narrowing — type-aware linting is not enabled in
eslint.config.js(noproject/projectService), and markdown is not in eslint's file population, sothis diff cannot move the verdict on any file it does not contain; the whole
@object-ui/typespackage was linted anyway.Deviation from the dispatch
The dispatch predicted
check-changeset-presencewould owe nothing ("docs + testonly — expect none owed"). It owes one. The gate reads any file under a
released package's
src/, tests included:Answered the way the gate itself names as the legitimate pass in this repo — an
empty-frontmatter
.changeset/*.mddeclaring that nothing releases (⛔ not theskip-changesetlabel, which this repo does not honour):⛔ The filter dialect is untouched, deliberately
Line 863's
"filter": [["owner", "=", "${currentUser.id}"]],is byte-identicalto
origin/main. Triage split this card in two and ruled the dialect half(the ObjectQL tuple form here versus the
{ field, operator, value }objectspackages/plugin-view/README.mdandcontent/docs/plugins/plugin-view.mdxteach)out of scope until which spelling
mergeFilterNodesactually runs is measured —NamedListView.filteris an untyped array, so both are declared-legal and guessingwould risk making a correct page wrong. That is #2890's half. It was not measured
here. The new pin cannot and does not adjudicate it: it compares key names
against the declaration, and
filteris a declared member under either dialect.🤖 Generated with Claude Code
https://claude.ai/code/session_01MM7kaS4dPpYHV5BsMyu4tQ
Generated by Claude Code