Skip to content

Restructure documentation following industry best practices - #67

Merged
hotlong merged 3 commits into
mainfrom
copilot/optimize-docs-structure
Jan 22, 2026
Merged

hotlong merged 3 commits into
mainfrom
copilot/optimize-docs-structure

Conversation

Copilot AI commented Jan 22, 2026 •

Copy link
Copy Markdown
Contributor

Documentation lacked progressive disclosure and role-based onboarding. Structure did not follow patterns from React, Stripe, Kubernetes docs.

Changes

New Sections

  • Quick Start (/docs/quick-start) - 4 role-based paths (backend, frontend, architect, tutorial)
  • Tutorials (/docs/tutorials) - Learning path framework with categorized hands-on guides
  • FAQ (/docs/faq) - 20+ common questions
  • Troubleshooting (/docs/troubleshooting) - Debugging guide with solutions

Enhanced Navigation

  • Index pages for all specification sections (Data, UI, System protocols)
  • Main specifications overview with protocol versioning info
  • Updated root navigation: quick-start → tutorials → guides → concepts → specifications → references

Content Examples

Role-based quick start pattern:

/docs/quick-start
  ├── index.mdx (role selector)
  ├── build-first-app.mdx (10min tutorial)
  ├── backend-developers.mdx (ORM comparison, query patterns)
  ├── frontend-developers.mdx (SDUI, view types)
  └── architects.mdx (architecture deep dive, multi-tenancy)

Specification index structure:

/docs/specifications
  ├── index.mdx (protocol overview)
  ├── /data/index.mdx (ObjectQL overview)
  ├── /ui/index.mdx (ObjectUI overview)
  └── /server/index.mdx (ObjectOS overview)

Structure Change

  /docs
+   /quick-start (5 files)
+   /tutorials (framework)
    /guides
    /concepts
-   /specifications (3 protocols)
+   /specifications (3 protocols + 4 index pages)
    /references
+   faq.mdx
+   troubleshooting.mdx

Files: +21 created, 7 updated. Builds successfully (588 static pages).

Original prompt

从行业最佳实践角度审视,并优化现有/content/docs, 官网fumadocs目录结构,增加必要的文档调整或修改现有的文档


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel

vercel Bot commented Jan 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
spec Ready Ready Preview, Comment Jan 22, 2026 11:48am

Request Review

- Add Quick Start section with role-based guides
- Add Tutorials landing page
- Add FAQ and Troubleshooting sections
- Add index pages for all specification sections
- Improve navigation hierarchy
- Update main index with new sections

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Remove unsupported Tabs/Tab components and use simple section headers instead. Build now completes successfully.

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Optimize documentation structure for best practices Restructure documentation following industry best practices Jan 22, 2026
Copilot AI requested a review from hotlong January 22, 2026 11:46
@github-actions github-actions Bot added documentation Improvements or additions to documentation size/xl labels Jan 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR is very large. Consider breaking it into smaller PRs for easier review.

@hotlong
hotlong marked this pull request as ready for review January 22, 2026 12:24
Copilot AI review requested due to automatic review settings January 22, 2026 12:24
@hotlong
hotlong merged commit a7cb2cf into main Jan 22, 2026
14 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR restructures the documentation to follow industry best practices, implementing progressive disclosure and role-based onboarding patterns inspired by React, Stripe, and Kubernetes documentation.

Changes:

  • Introduced Quick Start section with 4 role-based learning paths (backend developers, frontend developers, architects, and a 10-minute tutorial)
  • Added Tutorials framework with categorized hands-on guides and learning paths
  • Created FAQ and Troubleshooting sections for better self-service support
  • Enhanced Specifications with index pages providing clear navigation and protocol versioning information

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
content/docs/meta.json Added new top-level sections (quick-start, tutorials, faq, troubleshooting) to documentation navigation
content/docs/index.mdx Updated landing page to feature new Quick Start and Tutorials sections alongside existing documentation
content/docs/quick-start.mdx New role selector page introducing ObjectStack and directing users to appropriate learning paths
content/docs/quick-start/build-first-app.mdx Complete 10-minute tutorial for building a task management application
content/docs/quick-start/backend-developers.mdx Backend-focused introduction covering data modeling, business logic, and API generation
content/docs/quick-start/frontend-developers.mdx Frontend-focused introduction covering UI configuration and server-driven UI concepts
content/docs/quick-start/architects.mdx Architecture deep dive covering design philosophy, industry comparisons, and technical decisions
content/docs/tutorials.mdx Tutorial framework index with categorized learning paths and progression guidance
content/docs/faq.mdx Comprehensive FAQ covering general, technical, development, and business questions
content/docs/troubleshooting.mdx Debugging guide with common issues and solutions across installation, runtime, and development
content/docs/specifications/index.mdx Protocol specifications overview with versioning information and audience guidance
content/docs/specifications/data/index.mdx Data protocol (ObjectQL) overview and navigation hub
content/docs/specifications/ui/index.mdx UI protocol (ObjectUI) overview and navigation hub
content/docs/specifications/server/index.mdx System protocol (ObjectOS) overview and navigation hub
Comments suppressed due to low confidence (1)

content/docs/troubleshooting.mdx:1

  • This creates a circular reference - the troubleshooting guide is linking to itself at /docs/guides/troubleshooting instead of the correct path /docs/troubleshooting.
---

## Get Help

Stuck on a tutorial?
- Check the [Troubleshooting Guide](/docs/guides/troubleshooting)

Copilot AI Jan 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The troubleshooting guide link points to /docs/guides/troubleshooting but the actual file is at /docs/troubleshooting.

Suggested change
- Check the [Troubleshooting Guide](/docs/guides/troubleshooting)
- Check the [Troubleshooting Guide](/docs/troubleshooting)

Copilot uses AI. Check for mistakes.
Stuck on a tutorial?
- Check the [Troubleshooting Guide](/docs/guides/troubleshooting)
- Ask in [GitHub Discussions](https://github.com/objectstack-ai/spec/discussions)
- Review the [FAQ](/docs/guides/faq)

Copilot AI Jan 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FAQ link points to /docs/guides/faq but the actual file is at /docs/faq.

Suggested change
- Review the [FAQ](/docs/guides/faq)
- Review the [FAQ](/docs/faq)

Copilot uses AI. Check for mistakes.
os-bill added a commit that referenced this pull request Sep 10, 2026
…chema prose (#17203)

Six prose faces of the UI schemas told an author a CEL predicate could name
`app` — that the shipping renderer mounts it alongside `features` and
`os.user`. It does not, and never contractually did: `SCOPE_ROOTS` has never
declared `app`, ADR-0068 has never ruled it, and decision batch #67 ruled
option B, which ObjectUI shipped by dropping the binding.

Deletes the `app` token from all six faces, leaving `features`, `os.user`,
`data`, `current_user`, `record` and `user` in place and in order, and the
"renderer behaviour, NOT contract-guaranteed" framing verbatim:

  - ui/page.zod.ts       — "Ambient roots" docblock + the published
                            `.describe()` on `PageComponentSchema.visibleWhen`
  - ui/action.zod.ts     — param-level `visible` docblock + the action-level
                            `visible` docblock, which stated the same claim
                            unbackticked (`record/user/app/features`)
  - ui/component.zod.ts  — the `page:tabs` ambient-root resolution example
                            and its "also mounts the ambient …" sentence

The two latter faces were invisible to the token-co-occurrence probe that
found the first three; the probe here searched by the claim instead.

Regenerates content/docs/references/ui/page.mdx, which republishes the
`.describe()` verbatim, and adds a pin test over all six faces with lit and
dark probe controls.

No accept set moves: `SCOPE_ROOTS` is untouched and every schema parses
exactly what it parsed before.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…chema prose (objectstack-ai#17203) (objectstack-ai#17342)

Six prose faces of the UI schemas told an author a CEL predicate could name
`app` — that the shipping renderer mounts it alongside `features` and
`os.user`. It does not, and never contractually did: `SCOPE_ROOTS` has never
declared `app`, ADR-0068 has never ruled it, and decision batch objectstack-ai#67 ruled
option B, which ObjectUI shipped by dropping the binding.

Deletes the `app` token from all six faces, leaving `features`, `os.user`,
`data`, `current_user`, `record` and `user` in place and in order, and the
"renderer behaviour, NOT contract-guaranteed" framing verbatim:

  - ui/page.zod.ts       — "Ambient roots" docblock + the published
                            `.describe()` on `PageComponentSchema.visibleWhen`
  - ui/action.zod.ts     — param-level `visible` docblock + the action-level
                            `visible` docblock, which stated the same claim
                            unbackticked (`record/user/app/features`)
  - ui/component.zod.ts  — the `page:tabs` ambient-root resolution example
                            and its "also mounts the ambient …" sentence

The two latter faces were invisible to the token-co-occurrence probe that
found the first three; the probe here searched by the claim instead.

Regenerates content/docs/references/ui/page.mdx, which republishes the
`.describe()` verbatim, and adds a pin test over all six faces with lit and
dark probe controls.

No accept set moves: `SCOPE_ROOTS` is untouched and every schema parses
exactly what it parsed before.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…re, not that the renderer mounts it (objectstack-ai#18176)

Fixes objectstack-ai#17330

Clause-②: no
The diff narrows nothing and widens no published or accept surface. It
renames a package-internal constant (measured absent from this package’s
published entry), re-founds a docblock on decision batch objectstack-ai#67, and
rewrites diagnostic prose. The engine’s declared scope list is
untouched, no schema moves, no error code is added and no export
changes; the one package the diff moves under the source tree is graded
patch, which is the shape a yes would refuse. Recorded twice already —
the PM’s claim comment on objectstack-ai#17330 declares the same value, and so does
the dev report.

`FIELD_RULE_AMBIENT_ROOTS` is renamed `FIELD_RULE_NOWHERE_BOUND_ROOTS`
and keeps its single member `app`. The membership was never the false
part — the name, the docblock and one clause of the message were.

## What was wrong

objectstack-ai#13935 added `app` to this locally-assembled vocabulary on the premise
that objectui's app-shell bound it at the renderer, so the honest
verdict was "bound somewhere, just not here". **Decision batch objectstack-ai#67
(2026-09-07) ruled option B** — the engine's `SCOPE_ROOTS` is the
contract and ObjectUI aligns to it. ObjectUI shipped that, its scope
builder no longer binds `app`, and the producer-side option-A card was
closed `not_planned` under the same ruling. So the constant's name
asserts a fact that stopped being true, and its docblock cites a source
that no longer supports it.

### Which version of the card's strongest claim this PR asserts

The card and triage say the cited spec docblock was **deleted**.
Measured on `origin/main` and **confirmed independently by the
dispatching seat**, that is slightly wrong and the accurate version is
stronger:

- The `## Ambient roots — renderer behaviour, NOT contract-guaranteed`
section **still exists**, at `packages/spec/src/ui/page.zod.ts:365`.
- What changed is its **content**: it now names only `features`,
`os.user` and `data`. Backticked `app` occurs **0** times in that
section (lit control: backticked `features` occurs **1**); the only two
`app` hits in it are `app-shell`.

⇒ The constant is **not sourceless — it is contradicted**: its cited
source of truth still exists and no longer supports its membership of
`app`. This PR asserts that version. No commit naming the PR the card
blames appears in the recent history of `main`, so this PR does not
assert that it landed.

## Why the obvious fix is the wrong one

Emptying the constant is the card's literal ask. Measured, it makes a
**worse** message reachable, so it is not what shipped.

With the root gone from the judged vocabulary, both `app` spellings fall
through to `@objectstack/formula`'s generic bare-reference check, whose
prescription is to rewrite the root as a member of the record. Following
that earns ``unknown field `app` `` from the field-existence pass one
line up — the exact two-step wrong correction objectstack-ai#13935 existed to remove,
on the exact root, and the exact spelling this rule's own surviving
message refuses by name. It also turns the suppression and the whole
message tier into unreachable branches: with an empty array
`isBareReferenceToAny` can never return true.

So the membership stays and only its **grounds** move.

## The before and after an author reads

Same instrument for both readings: a driver calling
`validateStackExpressions` on the fixture shape the test helper uses,
run against this package's source. The shared leading clause is elided
with `…`; the differing tail is verbatim.

**Bare `app`** (sources `app` and `app == 'x'`) and **dotted
`app.theme`** (and `app.locale`) produce **byte-identical** text on each
side, because the tier is chosen by ROOT — so both spellings are quoted
once and both are pinned separately.

**Before:**

> … `` `app` is NOT declared platform-wide — it is an AMBIENT root,
mounted only by the renderer (objectui app-shell's `ExpressionProvider`
binds it beside `current_user` / `user` / `ctx` / `os` / `data` /
`features`, and the spec's page-component schema records that ambient
set as renderer behaviour, explicitly NOT contract-guaranteed). So it
resolves in a form VIEW's own field predicate and on no server path at
all, while a field-level object rule is server-enforced. ⛔ Do NOT write
`record.app`: … Rewrite the predicate against `record` (…), **or leave
the `app`-dependent decision on the view's own field predicate where
`app` IS bound — renderer-only, enforcing nothing server-side.** ``

**After:**

> … `` `app` is NOT declared platform-wide, and no evaluation site binds
it — not this one, and not any other: it is absent from the engine's
declared scope, and decision batch objectstack-ai#67 ruled that declared scope to be
the contract, so the renderer that once mounted it beside `current_user`
/ `user` / `ctx` / `os` / `data` / `features` was aligned to the same
set and no longer binds it either. The predicate therefore faults
wherever it is written, **and there is no surface to move it to.** ⛔ Do
NOT write `record.app`: `app` is not a field on this object, so that
spelling only trades this diagnostic for an `unknown field` error on
`app`. Rewrite the predicate against `record` (plus `previous`, and
`parent` on a master-detail line item) — gate on record state, not on
this root. ``

The bolded tail is the whole repair: the old text ended by offering a
destination that no longer binds the root, which is the same class of
error the tier exists to stop.

**What does not change:** before and after, both spellings yield exactly
**1** issue at `severity: 'error'`. No verdict moves and nothing that
used to lint clean stops doing so.

## Acceptance bar

⛔ On no path may an author read the record-rewrite prescription for this
root. Probed over seven cases:

- occurrences of that prescription on any `app` predicate: **0**
- lit control, the same construct for an ordinary bare identifier
(`nope`): **fires**
- control `record.app == 1` still earns `unknown field \`app\` on
\`showcase_deal\`` — which is why the refusal is kept in the message
rather than dropped

Four new pins assert this on the bare and the dotted spelling. Ablating
the fix (emptying the constant) turns **all four red**, alongside the
five pre-existing objectstack-ai#13935 pins — mutation proved on disk by blob hash
before the run was believed, restored with `git checkout HEAD -- path`
and re-proved byte-identical after.

## Blast radius

An `app.`-rooted field-rule predicate occurs **exactly once** repo-wide
and it is this package's own per-OPTION test fixture, not shipped app
metadata (lit control: **396** `*When` slots in the same corpus).
`app.(theme|locale|id|name)` across `content` + `examples`: **0**. So no
shipped metadata is mis-advised today, either way — which is what kept
this change small rather than a redesign of the tier.

## Not a published break

`FIELD_RULE_AMBIENT_ROOTS` was never re-exported from this package's
entry — `src/index.ts` exports `validateStackExpressions`,
`fieldRuleRootIssue` and `FIELD_RULE_BOUND_ROOTS` only, with zero
star-exports, and the built `dist/index.d.ts` carries **0** occurrences
of the old name (lit control `FIELD_RULE_BOUND_ROOTS`: **3**, including
the export statement). The rename is internal and no consumer import
moves.

## Changeset

A real `patch`, not `skip-changeset`, judged on the publish surface:
`packages/lint` ships `dist`, and the diagnostic text an author observes
is part of what it publishes. A user-visible behaviour change in a
released package takes a changeset. Nothing removed or renamed is
authorable or exported, so no migration mapping and no ADR-0087
disposition is owed.

## Verification

- `pnpm --filter @objectstack/lint test` — **103 files / 3826 tests
passed**
- `pnpm --filter @objectstack/lint typecheck` — pass, including
`check:test-typecheck`
- `pnpm --filter @objectstack/lint check:doc-formula-expressions` — pass
(the second consumer of `fieldRuleRootIssue`)
- Gate sweep derived from the real diff via
`scripts/pm/dispatch-gates.mjs --commands`: **61 families, 61 run, 0
NOT-MEASURED, 0 UNRUN**, reconciled with `--ran` carrying each exit code
captured before any pipe. Two families first answered `exit 3`
(PREREQUISITE NOT MET — unbuilt `dist`); a full `pnpm build` fixed the
prerequisite and the **whole sweep was re-run** rather than patched.
- `eslint . --no-inline-config` over the whole repo — **6752 files, 0
errors, 0 warnings**

## Out of scope, filed separately

`packages/lint/CHANGELOG.md` claims `FIELD_RULE_AMBIENT_ROOTS` and
`FIELD_RULE_JUDGED_ROOTS` are "exported beside"
`FIELD_RULE_BOUND_ROOTS`; neither is on the published entry. That is
released text and per AGENTS.md is amended in a dedicated docs-only PR,
⛔ never as a rider here. Reported on the card and taken by the triage
seat.


---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ecision in words instead of a tracker number (stage 22) (objectstack-ai#21931)

Part of objectstack-ai#20749
Clause-②: no

Stage 22 of this card: the next area of class (e), the test strings
shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513.
This stage takes the third name-ordered `ui/` group: the 29 id-bearing
test files directly under `packages/spec/src/ui/` from
`dataset-filter-nested-relation-list.test.ts` to
`view-inline-object-binding.test.ts`. Those files carried 96 messages
and 102 tracker ids, citing 52 records. 100 of those ids now either
state what their record decided, in words (form D), or are dropped where
the title already says it. Two stay: they are needles, ids that an
assertion reads in another file's text (below). Text only: no assertion,
identifier, test count or code comment changes, and no file is renamed.

## Census at the base (`be97cf3c93`)

Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`),
`census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs`
(md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5
`dda605c54745b4a60cc14c9a686e4eff`), byte-identical to the copies stages
10 to 21 used. A literal counts as a test title when its folded message
is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` /
`.only` chains included. Everything else is an "other" string.

The worktree was cut from `origin/main` at `be97cf3c93`, four commits
past the claim's `3dbd084209`. At the claim's base both instruments read
**665 messages / 702 ids**, the seat's reading and stage 21's head
reading. At `be97cf3c93` they read **665 / 704 in 154 files**: the two
extra ids are in `system/metadata-form-zod-reconciliation.test.ts` (22
to 24 ids), a ledger `why` string that objectstack-ai#21901 rewrote. No `ui/` file
moved.

| directory | files | messages / ids | titles | other |
|:--|--:|--:|--:|--:|
| `ui/` (this PR: 29 of the 48 files) | 48 | 201 / 213 | 186 / 198 | 15
/ 15 |
| `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 |
| `system/` | 34 | 154 / 167 | 128 / 138 | 26 / 29 |
| (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 |
| `ai/` | 1 | 2 / 2 | 0 | 2 / 2 |
| `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 |
| **total** | **154** | **665 / 704** | **612 / 648** | **53 / 56** |

The group reads **96 messages / 102 ids in 29 files**, the seat's
figures file for file:

| file (under `ui/`) | messages / ids | titles | other |
|:--|--:|--:|--:|
| `dataset-filter-nested-relation-list.test.ts` | 5 / 5 | 5 / 5 | 0 |
| `door-reachability.testkit.test.ts` | 4 / 4 | 3 / 3 | 1 / 1 |
| `expression-scope-app-root.pin.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `form-layout-inline-grid-retired.test.ts` | 4 / 4 | 3 / 3 | 1 / 1 |
| `form-option-enum-derive.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `i18n-label-resolver.test.ts` | 5 / 6 | 5 / 6 | 0 |
| `i18n.test.ts` | 6 / 6 | 5 / 5 | 1 / 1 |
| `inline-action-type.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `inline-action.test.ts` | 3 / 3 | 3 / 3 | 0 |
| `interaction-config-retirement.test.ts` | 4 / 4 | 2 / 2 | 2 / 2 |
| `joined-report-block-type.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `master-detail-detail-sort-field-retirement.test.ts` | 1 / 1 | 1 / 1 |
0 |
| `notification-embed-retirement.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `notification.test.ts` | 4 / 4 | 3 / 3 | 1 / 1 |
| `page.test.ts` | 2 / 3 | 2 / 3 | 0 |
| `react-blocks.test.ts` | 3 / 4 | 3 / 4 | 0 |
| `report-joined-block-dataset.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `report.test.ts` | 3 / 3 | 3 / 3 | 0 |
| `responsive.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `section-group-reference.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `strictness-batch14.test.ts` | 4 / 5 | 3 / 4 | 1 / 1 |
| `view-authoring-wire-split.test.ts` | 11 / 11 | 11 / 11 | 0 |
| `view-console-round-trip-keys.test.ts` | 5 / 5 | 5 / 5 | 0 |
| `view-field-order-composition.pin.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `view-filter-rule-value-shape.test.ts` | 8 / 9 | 8 / 9 | 0 |
| `view-filter-rule-wire-id.test.ts` | 4 / 5 | 4 / 5 | 0 |
| `view-form-features-root.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `view-gantt-tree-config-closed-15469.test.ts` | 4 / 4 | 4 / 4 | 0 |
| `view-inline-object-binding.test.ts` | 4 / 4 | 4 / 4 | 0 |
| **29 files** | **96 / 102** | **89 / 95** | **7 / 7** |

Seventeen more test files sit in the same name range and carry no id.
The seven "other" strings are the two needles below and five strings
rewritten and declared to the text-only tool: the expect messages at
`door-reachability.testkit.test.ts:216`, `i18n.test.ts:166-167` (the id
is on `:167`), `interaction-config-retirement.test.ts:148` and `:189`,
and the door name at `form-layout-inline-grid-retired.test.ts:61`, which
`describe(door.name, …)` prints as a title.

- **Controls.** Lit: `ui/view.test.ts`, outside the group, reads 43 ids
at the base and at the head. Dark: `view-authoring-wire-split.test.ts`
reads 0 at the head while 11 of its comment lines still carry a number.
Planted in scratch copies of head files: an id put into an
`inline-action-type.test.ts` title reads 1 / 1, and an id put into a
`report.test.ts` comment reads 0.
- **A wider pattern** (any `#` plus digits) reads the same as the gate
pattern in all 29 files at the base.
- **At the head:** 571 messages / 604 ids in 127 files. The 29 files
read 2 / 2 (the two needles), `ui/` reads 107 / 113, and no other file
moved.

## How the area was chosen

`ui/` has no subdirectory test file with an id, so it is taken in
name-ordered file groups near the ~100-id bound. Stage 21's re-cut named
this group at 102 ids, and this census reads 102, so no re-cut was
needed.

**Named for the next stages** (cut from the head census, 571 / 604):
- `ui/` 113 ids. 106 sit in the last group, the 16 files from
`view-item-config-type.test.ts` to `widget.test.ts` (100 messages / 106
ids; `view.test.ts` alone 43, `view-strictness-batch18.test.ts` 11,
`view-overlay-viewkind-arm.test.ts` 10). The other 7 are kept items:
stage 20's `component-props-unknown-members.pin.test.ts:322`, stage 21's
four colour literals, and this stage's two needles.
- `api/` 201, two stages. `system/` 167, two. The files directly in
`src/`, 120, one.
- The needles: the three docblock needles (`ai/build-progress.test.ts`
x2, `contracts/approval-service.test.ts`), the kept `:322`, and this
stage's two. One stage, with an at-tier review.

## The two needles, kept

- **`notification.test.ts:123`**, `source.indexOf('// [objectstack-ai#4610]')`. The
`./ui notification tombstone` pins read `ui/notification.zod.ts` and
slice it at this anchor, the comment that opens the tombstone at
`ui/notification.zod.ts:94`; `:134` then asserts the slice starts with
it. The id is the anchor text of a source comment, so it can only leave
together with that comment.
- **`strictness-batch14.test.ts:395`**,
`expect(source).toContain('objectstack-ai#5015')`. It reads `ui/notification.zod.ts`
and `ui/sharing.zod.ts` and asserts that both record the retirement by
citing the record. Those citations sit in source comments at
`ui/notification.zod.ts:48` and `:103`, and `ui/sharing.zod.ts:22` and
`:106`.

The five other "other" strings are failure messages of assertions whose
expected values carry no id, plus one door name. None is a needle.

## What each id became

- **24 literals (29 ids)** now state a decision in words.
- **22 literals (22 ids)** get their subject back in words, where the
number stood for a thing.
- **48 literals (49 ids)** drop a number the title already explains.

Every cited record was read with its comments through REST: 51 answer
200 and 1 answers 404. Three citations are cross-repo, `objectui#3907`,
`ui#6206-B` and `objectui#6262`, and were read from objectui. Two
records closed with no comment, objectstack-ai#3916 and objectstack-ai#4413; their decisions were
read from what landed: `f752ee3` ("give reports a sort declaration")
with `a831df1` ("`report.order` is live"), and `ebb209c` ("withdraw the
`record:*` blocks from the react tier — no renderer read the props it
published"). objectstack-ai#11284 answers 404; it was read from its landing commit
`5383fa6` (PR objectstack-ai#11695) and that commit's CHANGELOG entry.

Where a record's first decision was later corrected, the title follows
the corrected one:
- **objectstack-ai#15184:** its first ruling retired `fieldOrder`; ruling B superseded
it on a measured false premise (keep the key, declare the `columns` x
`hiddenFields` x `fieldOrder` composition). The title reads "the
list-view field composition is declared, not implied", which is ruling
B.
- **objectstack-ai#6227 and objectstack-ai#19514:** objectstack-ai#6227 recorded `equals` + array as accepted;
objectstack-ai#19514 reversed that on measurement. The two titles say so, in that
order.
- **objectstack-ai#20456:** not every census key is declared (a key the census mapped
to an existing spelling stays undeclared, which `:147` pins), so the
census describe names what the census records, not "every key is
declared".

**Stated in words:**

| record | literal (under `ui/`) | now reads | the decision |
|:--|:--|:--|:--|
| objectstack-ai#20080 | `dataset-filter-nested-relation-list.test.ts:116` | "§1 —
both analytics carriers refuse a list inside a nested relation, at save"
| Remedy A (triage `5825670610`): the two analytics carriers refuse the
list when the filter is saved, not when it is charted; the shared
`FilterConditionSchema` stays as ruled. |
| objectstack-ai#5056 | `door-reachability.testkit.test.ts:156` | "regression — the
any-one-shared-property bridge stays dead; a share of the shape decides"
| The derived-clone bridge stops firing on any one shared property and
requires a whole-shape overlap of at least 0.5. |
| objectstack-ai#5828 | `door-reachability.testkit.test.ts:216` (expect message) |
"the residual false-reachable case — no threshold excludes it" | Closed
not planned: no threshold separates a small all-shared-leaf shape from a
real derivation that also scores 1.0, so the `KNOWN BOUND` pin is the
record. |
| objectstack-ai#17203 | `expression-scope-app-root.pin.test.ts:84` | "no UI prose
face advertises `app` as an expression-scope root — the renderer no
longer mounts it" | Option B of decision batch objectstack-ai#67: objectui stopped
binding `app`, and the engine's `SCOPE_ROOTS` is the contract. |
| objectstack-ai#6761, objectstack-ai#6765 | `i18n-label-resolver.test.ts:281` | "resolveI18nLabel —
rule parity with objectui pickLocalized (ruled: one shared resolver, on
the server)" | Maintainer ruling B on objectstack-ai#6761: an inline locale map is
resolved to a string server-side, by one shared resolver in
`packages/spec`; objectstack-ai#6765 is that resolver, held to `pickLocalized`'s rule.
|
| `objectui#3907` | `i18n-label-resolver.test.ts:340` | "… the rule
departures converged once objectui read only own, string-valued entries;
one departure survives" | `pickLocalized` gained the own-property check
and the string filter on every limb. |
| objectstack-ai#10492 | `i18n.test.ts:99` | "rejects a lone `key`, which used to
parse as a locale map" | A lone `{ key }` parsed as a map for a language
called `key`; it is refused by name, under the retired key-reference
ruling. |
| objectstack-ai#6828 | `inline-action.test.ts:225` | "object-form `params` prescribes
per action type — its url meaning is retired, not re-keyed" | Maintainer
ruling 2026-08-10: retire the third meaning; no new key, and the refusal
guidance branches by action type. |
| objectstack-ai#4988 | `interaction-config-retirement.test.ts:60`, `:189` (expect
message) | "ui/ interaction config family retirement — renderer
behaviour, not authored metadata"; "… being undone — these are renderer
behaviour, not authored metadata" | Maintainer ruling A (2026-08-04):
the five files are retired; they are renderer built-in behaviour, not
per-page metadata. |
| objectstack-ai#21768 | `master-detail-detail-sort-field-retirement.test.ts:489` |
"the narrowing exempts only an inline grid field's own `sortField`, a
key the grid widget declares" | The `object-form` runtime form field
declares the grid widget's eight camelCase keys, `sortField` among them.
|
| objectstack-ai#4610 | `notification.test.ts:78` | "does not re-expose the bare
Notification/NotificationConfig names from ./ui — the bare
`Notification` belongs to ./api alone" | The `./ui` names were deleted;
`./api`'s `Notification` is the live contract. |
| objectstack-ai#11027 | `page.test.ts:494` | "PageComponentSchema — retired
`responsive`, which no renderer read" | Maintainer ruling B
(2026-08-22): retire it, since its renderer hook had zero callers. |
| `ui#6206-B`, objectstack-ai#15442 | `page.test.ts:696` | "ElementDataSourceSchema
`filter` — one filter orthography platform-wide, the ViewFilterRule
array" | Ruling B on objectui#6206 (one orthography), and ruling A on
objectstack-ai#15442: the binding-level `dataSource.filter` converges on
`ViewFilterRule[]`. |
| objectstack-ai#4413 | `react-blocks.test.ts:92` | "REACT_BLOCKS — the record:*
family is out, since no renderer read the props it published" |
`ebb209c`: the `record:*` blocks are withdrawn from the react tier. |
| objectstack-ai#11284 | `react-blocks.test.ts:147` | "REACT_BLOCKS — vocabulary
converges on the metadata tier, and the ListView alias retirement" |
`5383fa6`: the react tier adopts the metadata-tier spelling. objectstack-ai#14791 in
the same literal is dropped: the title names its retirement. |
| objectstack-ai#3916 | `report.test.ts:334` | "Report ordering — a report declares
its own sort" | `f752ee3`: the time axis is ordered by default, and a
report gets its own sort declaration. |
| objectstack-ai#13855 | `section-group-reference.test.ts:86`, `:181` | "… the
field-group reference form, members derived from the group" | Maintainer
ruling B (2026-08-31): a section names a field group and inherits its
members through `deriveFieldGroupLayout`. |
| objectstack-ai#5011 (and objectstack-ai#4001) | `strictness-batch14.test.ts:206` | "dashboard
compareTo: no longer a union but the executor contract — the strictness
arm-error limit does not apply to it" | Maintainer ruling 2026-08-04:
`compareTo` converges on the executor's `{ kind, dimension? }`. objectstack-ai#4001
becomes its subject, the strictness campaign. |
| objectstack-ai#5074 | `view-authoring-wire-split.test.ts:105` | "the two doors,
which is the whole point of the split: strict at authoring, reopened on
the wire" | Maintainer ruling A (2026-08-04): a strict authoring shape,
and a reopened wire member in the union. |
| objectstack-ai#19514 | `view-filter-rule-value-shape.test.ts:249` | "the scalar arm,
in both directions: a single-valued operator refuses an array" | The
protocol half of objectui#9050's ruling C′. |
| objectstack-ai#5114, objectstack-ai#5074 | `view-filter-rule-wire-id.test.ts:107` | "a
console-written filter row, judged per door: refused by name when
authored, stripped on the wire" | objectstack-ai#5114's provisional reopen ended when
objectstack-ai#5074's split landed. |
| objectstack-ai#15811 | `view-form-features-root.test.ts:208` | "an AST-only envelope
no longer reaches this scanner — an evaluated slot requires a `source`,
so it is refused one layer up" | Ruling A (decision batch objectstack-ai#122): every
engine-evaluated expression slot requires a non-blank `source`. |

**Subject back in words** (22 literals): "objectstack-ai#5056 premise" becomes "the
derived-clone bridge premise"; "the objectstack-ai#5068 props gate" becomes "the props
gate"; "the objectstack-ai#19331 shape" becomes "as its form row writes it"
(`object.form.ts`'s labelled `sharingModel` select); "the producer call
shape objectstack-ai#6761 needs" becomes "… the dataset compiler needs"; "the one
departure objectui#3907 did NOT touch" becomes "… the objectui map-limb
fix did NOT touch"; "the calls that caused objectstack-ai#6761" becomes "the calls
behind the dropped dataset label"; "retired at objectstack-ai#4988" becomes "retired
with the interaction-config family"; "after objectstack-ai#4988" becomes "after the
family retirement"; "objectstack-ai#4738 left it to ./ui alone" becomes "the
connector-side rename left it to ./ui alone"; "the objectstack-ai#4610 note" becomes
"the tombstone note"; "(objectstack-ai#5015 took the other half)" becomes
"(EmbedConfig, the other half, was retired)"; "objectstack-ai#4721, the silently
REVERSED sort" becomes "it once parsed as a silently REVERSED sort"; the
four `objectstack-ai#5599 —` titles become "the identity precondition …" / "identity
precondition — …" where the title needs the subject (`:272`, `:278`,
`:298`); "the census record (objectstack-ai#20456)" becomes "the census record of the
keys the console reads back"; "objectstack-ai#6227 — the reported shape" becomes "the
reported shape — a set operator carrying a scalar —"; "recorded as
ACCEPTED at objectstack-ai#6227" becomes "recorded as ACCEPTED by the first
value-shape rule"; "objectstack-ai#6227 — the refinement" becomes "the value-shape
refinement"; "the card's probe … (objectstack-ai#15469)" becomes "the probe that found
the gap"; "the objectstack-ai#14471 typo" becomes "the `colourField` typo", the key
the test writes; "objectstack-ai#6391's union membership" becomes "its union
membership".

**Dropped where already stated** (48 literals, 49 ids). A number goes
only where the title already says its decision. Examples: the three
`objectstack-ai#20080 §2` / `§3` / `§4` prefixes (the `§n` markers stay: the file's
own header numbers its sections with them); "[objectstack-ai#19920] InlineAction is an
inline action body, not unknown"; "… the retired arms are refused with
the prescription (objectstack-ai#20221)"; "InlineActionSchema — `bodyExtra` is the
payload key, `params` is not (objectstack-ai#5777)"; "ListView: objectName / viewType
are RETIRED — … (objectstack-ai#14791)"; the six `objectstack-ai#5074 —` prefixes beyond the first;
the four `[objectstack-ai#7741]` / `objectstack-ai#5114 —` prefixes; "what stays accepted (the objectstack-ai#5685
side: never stricter than the runtime)", which keeps "(never stricter
than the runtime)", objectstack-ai#5685's ruling in words. The batch labels `批 14`, `批
16` and `(batch 13)` stay in the earlier stages' form, and `ADR-0089
D3a` stays as a decision-record citation.

**No file is renamed.** `view-gantt-tree-config-closed-15469.test.ts`
keeps its name; its four title strings are rewritten.

## Readers

- **Test-name filters:** none. No tracked script, workflow or package
config passes `-t` / `--testNamePattern` (the 31 hits are `mapfile -t`,
`docker build -t`, `type -t`, a `create-objectstack -t` template flag
and a self-test's probe strings).
- **Snapshots:** none. No `__snapshots__` directory is tracked under
`packages/spec`, and none of the 29 files calls a snapshot matcher.
- **Projects:** `master-detail-detail-sort-field-retirement.test.ts` is
in the `repo` project (`packages/spec/vitest.repo-tests.json:50`); its
base and head runs below include it. The other 28 run in `local`.
- **By substring:** every old literal, its id-bearing fragment and a
window around each id (283 needles) was searched with `git grep` at the
base, across the tracked tree outside its own file. No gate, doc,
filter, snapshot, QA checklist entry or `scripts/check-*.mjs` self-test
reads one. The 4 hits are two code comments that quote the
`page.test.ts:494` title verbatim: `ui/dashboard.test.ts:585` and
`ui/responsive.test.ts:14`, both reading ("[objectstack-ai#11027] PageComponentSchema
— retired `responsive`"). Code comments are not this card's share. The
new title keeps "PageComponentSchema — retired `responsive`" as its
prefix, so a reader following either comment still finds it.

## Text-only proof

Stage 10's scratch tool (`textonly10.cjs`, md5
`d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file
on three legs:
1. **Skeleton:** the full AST, with string pieces masked. It must be
identical.
2. **Comments:** every comment, byte-equal.
3. **Strings:** each changed string leaf must sit in a test-call title
position or on a declared line, must carry a tracker id before, and must
carry no `#` plus digits after. This stage declares five lines:
`door-reachability.testkit.test.ts:216`,
`form-layout-inline-grid-retired.test.ts:61`, `i18n.test.ts:167`, and
`interaction-config-retirement.test.ts:148` and `:189`.

- **Result:** 29 of 29 files SAME on all three legs, with the per-file
counts predicted in writing before the run.
- **Totals:** 94 changed string leaves in 94 literals: 89 titles and 5
declared. The diff's `+` and `-` lines are exactly the 94 planned lines
as multisets, and every file keeps its line count.
- **Controls (14 of 14 as predicted on the first run, on scratch copies,
each anchor hit once):** identifier rename DIFF; numeric literal DIFF;
comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a
rewritten title given a new id VIOLATION; a title that was id-free at
base edited VIOLATION; one title reverted to base SAME; an `it.each` row
given an id VIOLATION; an undeclared expect message changed VIOLATION; a
title re-split into a `+` chain DIFF; a declared expect message reverted
to base SAME; a declared expect message given a new id VIOLATION; a kept
needle edited VIOLATION; a kept needle's id dropped VIOLATION.
- **Templates and tables:** one `.each` title changes,
`view-filter-rule-value-shape.test.ts:255`, a `%s` template (`refuses %s
— …`): its placeholder and rows are untouched, and the printed names
below match the plan. One template-literal title loses only its tail
(`form-layout-inline-grid-retired.test.ts:141`).

**Test counts:** the 29 files were run at the base, in a separate base
worktree, and at the head, with `--project local --project repo`. Both
sides read 858 tests in 29 files, all passed, with the same count and
status sequence per file in 29 of 29. 596 full test names change, and
each changed name equals the base name with the planned replacements
applied (0 mismatches). No full name repeats on either side.

## Changeset: `skip-changeset`

Measured, not assumed:
- `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the
29 touched files are in it, and no `*.test.ts` at all. The controls
`src/ui/view.zod.ts`, `src/ui/report.zod.ts` and `dist/index.mjs` are in
it.
- In the built `dist/`, two new phrases and an old one each read in 0
files. The control `Unrecognized key` reads in 42.

So this PR publishes nothing, and no changeset is added.

## Verification (at `612375dc0c`)

- `pnpm turbo run build` over all packages: 71 / 71, through the shared
verify lock (`VERDICT command-exit 0`).
- `@objectstack/spec`:
  - `vitest run --project local`: 619 files, 18471 passed, 1 todo.
- `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246
errors / 135 pinned signatures held). Its program holds all 29 group
files, counted by path with `tsc --listFilesOnly -p tsconfig.test.json`.
- `check:generated`: all 15 generated artifacts up to date, against the
`dist/` the build above wrote.
- **Gates:** `dispatch-gates --commands` derived 79 families, the same
set as stage 21, and all 79 exit 0. `--ran` reconciles: 79 derived, 79
run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded.
The five roster families whose rosters sit under a touched directory
were also run, and each exits 0: `check:meta-url-spelling`,
`check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`
and `check:filter-alias-parity`.
- **ESLint, a proven narrowing:** `--no-inline-config` over the 29 files
reads 0 errors and 0 warnings. The population comes from ESLint's own
config: 29 configured, 0 ignored. No file sets `parserOptions.project`
or `projectService`, so no untouched file's verdict can move.
- `check-governed-merges --test`: NOT governed, 188 changed lines (+94 /
-94).
- A control-byte scan over the 29 changed files finds none.

## `main` since the base

Re-fetched just before this PR opened, `origin/main` was two commits
past the base (`faf8dce482`: objectstack-ai#21917, objectstack-ai#21906). Neither touches
`packages/spec` or any of the 29 files, so `main` was not merged. `git
merge-tree` onto `faf8dce482` is clean, and none of the 8 open PRs
touches any of the 29 files.

## Acceptance notes

- **The two needles** stay, as above. They leave with their source
comments, in the needles' stage.
- **The census base moved by two ids** after the claim: objectstack-ai#21901
(`8e35895832`) rewrote a `why` string in
`system/metadata-form-zod-reconciliation.test.ts`, which now carries 24
ids where it carried 22. It is a ledger value, not a title, and it rides
the `system/` stages.
- **Same-id test titles in this card's later stages** go with those
stages: 34 lines in `packages/spec/src`, for example
`api/api-error-code-type.test.ts:71` ("[objectstack-ai#19920] …"),
`system/i18n-resolver.test.ts:2652` ("(objectstack-ai#5377)"),
`ui/view-metadata-schema.test.ts:215` ("identity precondition (objectstack-ai#5599)"),
`ui/view-strictness-batch18.test.ts:364` ("[RESOLVED at objectstack-ai#5074] …") and
`ui/view-union-diagnostics.test.ts:62` ("[objectstack-ai#6391] …").
- **Same-id test titles in other packages** stay: 38 lines in 9 packages
(`lint` 8, `objectql` 8, `service-analytics` 7, `cli` 4,
`metadata-protocol` 4, `spec/scripts` 3, `plugin-security` 2,
`plugin-sharing` 1, `service-automation` 1), each package's share under
the objectstack-ai#20513 lane children. Three of them cite `objectstack-ai#6262` (`objectql`) and
two cite `objectstack-ai#6206` (`plugin-security`, `plugin-sharing`): those are
objectstack records, different from the objectui records this group
cites.
- **Code comments with live ids** remain in these files and their
sources, for example the `[objectstack-ai#4610]` / `[objectstack-ai#5781]` banners in
`notification.test.ts`, the `objectstack-ai#5056` section headers in
`door-reachability.testkit.test.ts`, and the two comments above that
quote the old `page.test.ts:494` title. Code comments are not this
card's share.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_

Co-authored-by: Claude <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 1fbb8ab6 Deployed Jan 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants