Repository navigation
docs(content): apply the approved search-intent title rule to 169 authored pages, short nav labels kept via navTitle - #20170
Merged
objectstack-fleet[bot] merged 5 commits intoSep 27, 2026
Conversation
…eeping short nav labels via navTitle Every authored page still carrying its pre-rule title now takes the approved search-intent title (`<primary keyword> — <qualifier>`, 36-46 characters so the rendered title with the ` | ObjectStack` suffix lands in 50-60), and declares `navTitle:` with its previous short title so the page tree -- the sidebar and the footer previous/next links -- reads exactly as before. 176 rows are applied verbatim from the approved table; one authored page the table never listed (permissions/tenant-audit-census.mdx) is authored per the rule and flagged for maintainer voice review. Claude-Session: https://claude.ai/code/session_018bR89KaSkoZVqgBnUYXtyD Co-authored-by: Claude <noreply@anthropic.com>
…le" from the administrator guide title check:runtime-services-index holds each runtime-services page's title to its `services.<name>` accessor, so the eight approved rows for that chapter cannot land from frontmatter alone; they are restored to base and left for a maintainer ruling. check:role-word rejects the reserved word in the approved administrator-guide title, so that row takes "permissions and access". Claude-Session: https://claude.ai/code/session_018bR89KaSkoZVqgBnUYXtyD Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 27, 2026
Closed
…, so the chapter takes its approved titles check:runtime-services-index held each kernel/runtime-services/NAME-service.mdx to `title: services.NAME`, the premise its five enumerations rest on. The site-wide title rule lengthens `title` for the search-facing surfaces, so the premise moves to the page's `navTitle` -- the page-tree label, which on these pages is the bare accessor. It does not loosen: an absent, blank, body-only or mismatched navTitle is red, the old title-only spelling is red, and each finding names the fix. The eight pages take their approved titles verbatim and declare `navTitle: services.NAME`. Self-test: new floored battery 'The accessor premise lives in navTitle' (7 cases), roster floor 8 -> 9. Claude-Session: https://claude.ai/code/session_018bR89KaSkoZVqgBnUYXtyD Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
维护者速读 · 需要你过目两行标题 · 2026-09-27T05:53Z本 PR 交付 #12237(p1)。席位复核结论 ACCEPT(#12237 评论 改了什么
为什么改
风险与代价(含回滚)
席位意见:下面两行不在你批准的表里,席位建议都通过。
四棱:
你要做的(一个动作):在本 PR 回复「同意」,或者点名要改哪一行、改成什么。收到后,席位把 PR 转为 ready,经合并队列落地。 Generated by Claude Code |
This was referenced Sep 27, 2026
Contributor
Author
Maintainer ruling recorded: both rows approved, land it · 2026-09-27T14:42Z
谁的指令: the maintainer
Generated by Claude Code |
3 tasks
objectstack-fleet
Bot
deleted the
claude/issue-12237-title-rule-rollout
branch
September 27, 2026 15:13
Contributor
Author
|
Maintainer's word recorded — this PR lands. Director seat (summon #30 续) ·
Generated by Claude Code |
This was referenced Sep 27, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…on the dry run, the commit and validate alike (objectstack-ai#20208) Fixes objectstack-ai#20150 Clause-②: yes (narrowing) ## What this does `ImportFieldMappingSchema.target` is declared as "Target object field(s)". A mapping whose target named no field of its `targetObject` passed `objectstack validate`, answered `ok` on every row of the dry run, and then failed every row on the commit with `INVALID_FIELD`. The dry run promised what the commit refused (objectstack-ai#4633's contract is that the dry run predicts the write). One verdict now decides what a target may name, and both doors call it: - **`@objectstack/spec`** (`packages/spec/src/data/import-mapping-target.ts`, re-exported from `mapping.zod.ts` beside the schema it judges): `unknownImportMappingTargets(fieldMapping, objectDef)`, built on `indexImportMappingTargets` + `judgeImportMappingTarget` + `importMappingEntryTargets`. It lives in the spec because `@objectstack/rest` and `@objectstack/lint` both depend on the spec and neither depends on the other (triage `5853335141`). - **The import door** (`packages/rest/src/import-prepare.ts`, `import-mapping.ts`): `prepareImportRequest` resolves the object definition first and refuses a named mapping with a target that names no field BEFORE any row, with `400 INVALID_FIELD`. Both the dry run and the commit arrive through that function, so they give the same answer (the async import-job route shares it too). `rest-server.ts` is untouched. - **The author-time check** (`packages/lint/src/validate-mapping-target-fields.ts`): a new reference-integrity suite member, `validateMappingTargetFields`, rule `mapping-target-field-unknown`, severity `error`, located at `mappings[i].fieldMapping[j].target` and naming the mapping and the object. The suite is the one table `os validate`, `os lint` and `os build` share, so there is no validate-only copy in `packages/cli`. ### What a target may name (and the false-refusal trap) A target resolves when it names (1) a declared field, (2) a column the platform provisions on THAT object, from `resolveInjectedSystemColumns` (`owner_id` only where the object carries it, and so on), or (3) `id` / `created_at` / `updated_at`, which the engine's write door (`PLATFORM_PROVISIONED_COLUMNS` in `undeclaredWriteFieldErrors`) admits on every object, `systemFields: false` included. Row 3 is what keeps the new refusal from being narrower than the write: measured below, a `created_at` target on a `systemFields: false` object is written by the commit. An object with no readable, non-empty field map is not judged at all (no opinion, never a refusal), the engine's own position on the same input. Measured at the door (probe against the real engine + protocol + sqlite, HEAD `eab27ef780`): the object definition `getMetaItem` serves carries the injected columns (`created_at, created_by, organization_id, owner_id, owning_business_unit_id, updated_at, updated_by`) and the fields an `objectExtensions` entry merges in. The lint side reads the authored stack, so it folds `objectExtensions` fields (from the stack and from `packages[].manifest`) into the object before judging, and it skips a mapping whose `targetObject` this stack does not define, the same skip its field-existence siblings take. ### Where objectstack-ai#20149's arm goes objectstack-ai#20149 is not addressed here (ruled A, `Blocked-by` this card). Its compound-part capability extends this verdict by ONE arm in `judgeImportMappingTarget`, at the marked comment, returning a new `kind: 'part'` member of `ImportMappingTargetVerdict`; it reads the head field's definition from `ImportMappingTargetIndex.fields`, which carries the declared definitions for exactly that purpose. It never becomes a second predicate. Until it lands, `mailing_address.street` names no field and is refused, which is this card's pin. ## Before and after (measured) Before, with the door's refusal ablated at HEAD (the same door origin/main runs), mapping target `mailing_address.street` on object `task`: - dry run: `200`, `ok: 2, errors: 0` - commit: `200`, `ok: 0, errors: 2`, each row `INVALID_FIELD` "Unknown field 'mailing_address.street' on object 'task'" After: both answer `400` `{ code: 'INVALID_FIELD' }`, byte-identical bodies, nothing written. The refusal reuses the code the commit already carried, so no error-code ledger entry is added. ## Tests The card's pin is in `packages/rest/src/import-integration.test.ts` (real engine, real protocol, sqlite): a target that names no field is refused on the dry run and on the commit alike; each element of a `split` target is judged; a valid mapping is the control (dry run and commit agree on `ok`); and a platform column the door accepts (`created_at` / `updated_at` on a `systemFields: false` object, `owner_id` on a default one) stays green at both ends. The lint rule is red on the bad mapping and green on the control (`validate-mapping-target-fields.test.ts`), and the suite membership pin gains the member with a live finding (`reference-integrity-suite.test.ts`). The verdict itself is pinned in `packages/spec/src/data/import-mapping-target.test.ts`. Local results at `f35d38b16d` (all through `os-verify-lock.sh`, shared box): - `@objectstack/spec`: `vitest run --project local` 541 files / 15849 tests pass; `typecheck` (tsc, scripts, test layer) exit 0; `check:generated` all 15 artifacts up to date after `gen:api-surface` + `gen:export-origins` (committed). - `@objectstack/lint`: `vitest run` 109 files / 4183 tests pass; `typecheck` exit 0. - `@objectstack/rest`: `vitest run --project local` 197 files / 3347 pass, 1 skipped; `typecheck` exit 0. - `@objectstack/cli` (unit): `test/validate-build-gate-parity.test.ts` 22 pass. The `integration` layer (the spawned-CLI `authoring-rule-command-parity`) is declared to CI. - Gates: `dispatch-gates.mjs --commands` derived 85 families; `--ran` reconciles 83 run green, 2 NOT MEASURED, 0 unrun. NOT MEASURED: `check:dual-build-cjs-loads` and `check:type-check-debt`, both PREREQUISITE NOT MET (exit 3, they read a whole-repo build this worktree does not have). - ESLint over the 12 changed `.ts` files (`--no-inline-config`): 0 errors, 0 warnings. The repo-wide `pnpm lint` is CI's. ### Ablation (one-shot, restored and proven byte-identical to HEAD) Each leg mutated through `scripts/ablation-replace.mjs` (anchor hit 1, blob changed, restored by `git checkout HEAD`, hash equal to the HEAD blob after): - A, the door stops refusing (`import-prepare.ts`): the two refusal tests in `import-integration.test.ts` go red; the controls stay green. - B, the lint rule stops reporting: 3 rule tests and the suite's "every member runs" test go red. - C, the spec verdict admits every name: 3 verdict tests go red. ## Acceptance notes - `content/docs/data-modeling/import-mappings.mdx` is not edited: its "Rejections before any row is read" table does not list the new `INVALID_FIELD` row and its build-validation list does not mention the new lint rule. No sentence in it turns false, so it is outside this card's surface (its frontmatter is held by PR objectstack-ai#20170). Carrier: none. - The engine keeps its own `PLATFORM_PROVISIONED_COLUMNS` literal in `@objectstack/objectql`; the spec constant `IMPORT_TARGET_ALWAYS_ADDRESSABLE_COLUMNS` mirrors it. Converging the engine onto the spec constant is outside this surface. Carrier: none. - The runtime publish gate does not judge `mapping` writes: the new member keeps the frozen `flow` default, and a flow snapshot carries no `mappings`. The import door refuses a bad mapping at use either way. - `upsertKey` naming no field is not judged here (the card is about `target`). --- _Generated by [Claude Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…gent chat route (objectstack-ai#20220) Fixes objectstack-ai#19958 Clause-②: no Docs only. One section of `content/docs/protocol/kernel/error-handling.mdx` is rewritten: the `QUOTA_EXCEEDED` entry. No runtime, spec or error-code-ledger change. `QUOTA_EXCEEDED` stays registered, as the narrowed ruling on objectstack-ai#17707 (`5807013768`) keeps it. ## What the page said, and what it says now Removed (it was false): > **Envelope:** none — no producer emits it > > **Not emitted today.** `QUOTA_EXCEEDED` is a registered member of the standard error-code catalog (…), but no ObjectStack producer emits it. No response carries this code, and none carries a quota `details` bag — do not write a client branch against it. Added (each sentence rests on a measurement below): > **Envelope:** nested — emitted by one route only, the ObjectOS agent chat route, and only to a JSON-mode request > > **One producer.** `POST /api/v1/ai/agents/:agentName/chat` answers `QUOTA_EXCEEDED` when the deployment has switched on its per-user daily chat-turn cap and the calling user has spent that cap. […] No other route emits this code. > > **JSON mode only.** The route streams by default. A request whose `stream` flag is absent or `true` gets the refusal as an ordinary assistant text message on HTTP 200, and no error code reaches it. Only a request with `stream: false` gets the 429 body below. > > `error.details.resetAt` is an ISO-8601 timestamp: the moment the cap lifts. It is the only recovery time the response carries. The route sets no `Retry-After` header and sends no `retryAfterSeconds`. Kept as it was: the SMS sentence (`the SMS daily quota answers TOO_MANY_REQUESTS`) and the pointer to `RATE_LIMIT_EXCEEDED` for request pacing. The SMS sentence still holds on `main`: `packages/services/service-sms/src/sms-daily-quota.ts:85` is `SMS_QUOTA_EXCEEDED_CODE = 'TOO_MANY_REQUESTS'`. The only change there is its first word, "Quota enforcement that does exist" → "Other quota enforcement". ## The producer, measured (cloud `main` `48d70663`) The producer and reader are cited here, not in the doc, following the dispatch's route. - `packages/service-ai/src/routes/agent-routes.ts:572-575`: `return sendError(429, 'QUOTA_EXCEEDED', message, { details: { resetAt: decision.resetAt }, category: 'rate_limit' })`. - `agent-routes.ts:527`: `const wantStreamMode = body.stream !== false;`. At `:552-565` a streaming request gets `status: 200` with the copy as one `text-delta` part, so there is no code on that path. - `packages/service-ai/src/routes/envelope.ts:188-198`: `sendError` writes `{ success: false, error: { code, message, httpStatus: status, ...extra } }`. - `packages/service-ai/src/plugin.ts:1154-1159`: the gate exists only when the deployment sets its daily-turn knob to a positive number, and the only implementation wired is `DailyMessageQuota`. Its refusal always sets `resetAt` (`quota/agent-chat-quota.ts:92-96`). - No `Retry-After` on this path. `sendError` returns status and body only, and no cloud writer adds that header to this route. - Pinned in cloud by `packages/service-ai/src/__tests__/agent-error-envelope.conformance.test.ts:300-315` (status 429, code, `details` equal to `{ resetAt }`). - Control: objectstack `main` has no producer of this code. `git grep QUOTA_EXCEEDED` at `e7f69dbb` gives 40 lines: docs, generated references, the registration in `packages/spec/src/api/errors.zod.ts:106`, a ledger test, a status baseline, and the SMS `SMS_QUOTA_EXCEEDED_*` constants whose value is `TOO_MANY_REQUESTS`. ## The readers, measured objectui, pin `f8a9d0fb` (current `.objectui-sha`) and `main` `25c7d584`. `tool-display.ts`, `tool-display.test.ts` and `useObjectChat.ts` are byte-identical between the two. - `packages/plugin-chatbot/src/tool-display.ts:314-326` and `:336`: `parseAiQuotaError` deliberately does NOT recognize `QUOTA_EXCEEDED`. - The 429 is routed by `isUnsentSendError` (`:404`) and `isRateLimitError` (`:423`). They key on the HTTP status tagged by `sendAwareFetch`, or on a raw-text regex. - objectui reads no field of this body: not `code`, not `details.resetAt`, not `category`. The card's "objectui branches on it (`error.details.resetAt`, `category`)" comes from a comment at `:317` that describes the producer. No read in objectui matches it. - objectui's chat defaults to `streamingEnabled = true` (`useObjectChat.ts:768`, sent as `stream` at `:931`). Its default path therefore receives the in-band text on HTTP 200. The 429 branch is reached only when a chatbot schema sets `streamingEnabled: false`, and it is pinned by fixture (`tool-display.test.ts:365-390`). `@objectstack/client`, this repo at `e7f69dbb`: - `client.ai.agents.chat()` sends `stream: false` (`packages/client/src/index.ts:6648-6653`). It throws an error carrying `code`, `category`, `details` and `httpStatus` (from `res.status`) (`:7376-7405`). - `chatStream()` sends `stream: true` (`:6662-6667`). Field mismatches (dispatch Zone 2 item 2): - **Sent by the producer, ignored by the reader:** - objectui ignores all of `code`, `details.resetAt`, `category` and `httpStatus`, and reads `message` only through the regex probe. - The SDK reads every sent field. - **Read by a reader, not sent:** the SDK's `error.retryable`, which is `undefined` here. ## Choices this PR settled - **"ObjectOS", not "hosted".** Triage's wording was 托管的 AI agent 对话路由. The docs' own term for where this route lives is the callout in `content/docs/ai/index.mdx`: the in-product chat runtime "ships in **ObjectOS**". The section links there. - **The example shows `httpStatus: 429`.** Triage ruled 「⛔ 不要自行补充字段」. `httpStatus` is not an added field: the producer's `sendError` writes it on every body (`envelope.ts:198`), and leaving it out would misdescribe the wire. The field list under the example names only what a client acts on: `code`, `details.resetAt`, `category` and `message`. `retryAfterSeconds` and `Retry-After` are named only as absent. - **The example `message` is illustrative.** It is the English half of `DailyMessageQuota`'s copy with an example limit of 50. The real copy is bilingual, Chinese first. The doc says to display `message`, never to parse it. - **The "JSON mode only" paragraph was not in triage's text.** It is measured and it changes what a client can rely on: a streaming client never sees the code. Dispatch Zone 2 item 3 asked for the one emitting route, not a platform-wide promise. This paragraph narrows that route to its one mode. ## Acceptance notes - **`content/docs/api/error-catalog.mdx` disagrees with the rewritten section. It is not edited here (claim file surface).** - `:507-510`: the entry sends readers to "check `retryAfterSeconds`", which this producer never sends; `details.resetAt` is the field. Its cause line says "API usage quota for the current period", while the only producer is a per-user daily chat-turn cap. - The 429 row at `:913` (`rate_limit`) agrees. - Open PR objectstack-ai#19957 has that entry only as unchanged context, and PR objectstack-ai#20170 does not touch it. Both leave the disagreement in place. - **The page's general "Nested envelope" field list** says no route-module body carries `httpStatus`, and it does not list `category`. That is true of the writer it names, `packages/types/src/response-envelope.ts`. The ObjectOS route writes through its own `sendError`, which carries both. Not edited. - **The `fetchWithRetry` example under `RATE_LIMIT_EXCEEDED` retries any 429 by `Retry-After`.** Against this 429 it would wait zero seconds and retry. The new section warns against that. The loop under "Implement Retry Logic" keys on the code and lets `QUOTA_EXCEEDED` throw, which is right. Its comment "it is present on every 429" is inaccurate for this code's 429, but the comment sits inside the `RATE_LIMIT_EXCEEDED` branch, so behaviour is unaffected. Carrier: none. - **Card pin moved.** The card cited objectui `62597c5880`; `.objectui-sha` is now `f8a9d0fb`. The cited lines are unchanged at both. ## Verification (head `6e0bb38e`) - **Derived gate set.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` gives 41 commands on the actual changed paths, identical to the dispatch lead. - **Readings.** All 41 ran, every exit code was recorded to disk before it was read, and all 41 read exit 0. - **Prerequisite refusals.** Four lines first refused with PREREQUISITE NOT MET (exit 3): `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples` and `check:docs-transcript-drift`. `check:docs` was held back until spec was built. None of those refusals counted as a reading. The lines were re-run after builds under `scripts/pm/os-verify-lock.sh`: - `turbo run build` over `@objectstack/lint...`, `@objectstack/formula` and `@objectstack/spec`: VERDICT command-exit 0. - The same over `@objectstack/client-react...` and `@objectstack/client...`: VERDICT command-exit 0. `check:skill-examples` needed this second build because it refused again, on missing client declarations. - `check:skill-examples` then read "259 prose examples type-check across 3 surface(s)". - **Reconciliation.** `dispatch-gates.mjs --ran ran.list` prints: "✓ dispatch-gates --ran: 41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED (a DERIVED zero — all 41 recorded an exit code and none of them is 3)." - **Control bytes.** `pnpm check:nul-bytes` exits 0, and the control-byte self-scan of the edited file has no hits. - **Tests.** No test and no pin was added or changed: the change is prose only, and nothing is accepted or refused differently. - **Changeset.** `skip-changeset`: 0 of 69 non-private workspace packages list a `files[]` entry reaching `content/docs`. - **Newer `main`.** `origin/main` moved to `7e7fab73`, and no commit since BASE `e7f69dbb` touches either doc. --- _Generated by [Claude Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_ Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…rt of a compound field (mailing_address.street) (objectstack-ai#20246) Fixes objectstack-ai#20149 Clause-②: yes ## What this does The maintainer's ruling on objectstack-ai#20149 (comment 5852138019, 「同意」): > **Ruled: A — a mapping target may name a declared part of a compound field (`mailing_address.street`); the importer assembles the parts into one value; the part names are the closed set the value schema declares.** An import mapping could write each source column to one flat field only, so nothing could build an `address` value from the street / city / state / postal code / country columns a spreadsheet carries. Now a `fieldMapping[].target` may name `field.part`, and the import door assembles every part one row maps into ONE value under the field's key, before the engine sees the row. It extends the ONE verdict objectstack-ai#20150 built, at the arm that PR marked. There is no second predicate. - **`@objectstack/spec`** (`packages/spec/src/data/import-mapping-target.ts`): - `judgeImportMappingTarget` gains the `{ kind: 'part', target, field, part }` arm. - `indexImportMappingTargets` carries each compound field's parts on a new `parts` map, read from the field's stored value schema (`valueSchemaFor`). The part names are never listed by hand. - A dotted target that stays refused answers `unknown` with a `head` (what the text before the dot names, and its parts when it is compound), so every door can name the legal parts. - `unknownImportMappingTargets` gives each refused target a `reason`: `unknown`, or `collides` for a part of a field the same mapping also writes whole. - `ImportFieldMappingSchema.target` declares the part path in its `.describe()` and docblock. The generated reference page, `api-surface/` and `export-origins/` are regenerated. - **`@objectstack/rest`** (`import-mapping.ts`, `import-prepare.ts`): - `applyMappingToRows` takes the object definition, `trimWhitespace` and `nullValues`, and assembles parts from every transform: `none`, `map`, `constant`, `join`, and each element of a `split`. - `refuseUnknownMappingTargets` names the legal parts in its refusal. It still answers `400 INVALID_FIELD`, before any row. - `isBlank` is exported from `import-coerce.ts`, so a blank part is judged by the same rule as a blank cell. - **`@objectstack/lint`** (`validate-mapping-target-fields.ts`): the message "a dotted path into a field's value is not a target" is gone, because it is false for a declared part. The rule now reports the three refused cases, each with the legal parts. - **Docs**: in `content/docs/data-modeling/import-mappings.mdx` the `target` row said "Target field name(s)", which is now false. It is rewritten. Only the body is edited; the frontmatter is held by PR objectstack-ai#20170. ## Decisions, each from a measurement **H1: which fields are compound.** I checked every one of the 49 `FieldType` values through `valueSchemaFor`. Two stored value schemas are closed objects (`catchall: never`): - `address`: seven parts (`street`, `city`, `state`, `postalCode`, `country`, `countryCode`, `formatted`). Every part is an optional string. - `location`: `lat` and `lng` are required numbers; `altitude` and `accuracy` are optional numbers. A field counts as compound when its value schema is a closed object whose every part is an optional string. That selects `address` only. **`location` is excluded, as the ruling asked me to decide and record:** - `LocationValueSchema` refuses `{ lat: '37.7', lng: '-122.4' }` (strings) and `{ lat: 37.7 }` (no `lng`). - On the ADR-0104 warn-first write path, the engine admits a value that fails its value schema and only warns. I measured this below with an address part holding a number. - So a location assembled from text cells would be stored with the wrong type, and nothing would refuse it. With optional-string parts, every subset of text cells is a valid value by construction. The census test pins the whole set against `FieldType.options`. **H2: what one row assembles.** Measured through the real engine (sqlite, JSON rows, no mapping) at `055d4b66e9`, with the same result on the dry run and the commit: | value sent | dry run | commit | read back | |:---|:---|:---|:---| | `{ street, city }` (partial) | ok | ok | `{ street, city }` | | `{ street: '', city }` (empty part) | ok | ok | `{ street: '', city }` | | `{ postalCode: 12345 }` (wrong type) | ok, with warning `invalid_type` | ok | `{ postalCode: 12345 }` | | update `{ city }` over `{ street, city }` | | ok | `{ city }` (the stored value is replaced whole) | The assembly rule, stated in `applyMappingToRows`' docblock and the docs row: - A blank part cell contributes nothing. Blank means empty, whitespace, or a `nullValues` token. - The schema would accept `street: ''`, but a blank flat cell leaves its field unset, and a blank part does the same one level down. - A string part is trimmed under `trimWhitespace`, as a flat text cell is. Coercion never reaches inside a compound value, so the trim happens in the assembly. - If every part a row maps is blank, the field is left unset. - Otherwise the assembled object is the field's whole new value. On an update it replaces the stored one. **Whole field and part together.** When a mapping writes `mailing_address` and also `mailing_address.street`, both write the same key of one row. They are refused as `reason: 'collides'` at all three doors, naming where the whole field is written. A repeated part target follows the same last-write rule as a repeated flat target. **One door.** The dry run and the commit both reach the assembly through `prepareImportRequest`, so they judge the same assembled row. `objectstack validate` asks the same spec verdict. **H5: census of dotted targets** at base `3875ae6773`: - The repo's one import mapping, `examples/app-showcase` `showcase_inquiry_feed` (5 entries), has no dotted target. - No `skills/` or docs page carries a dotted `fieldMapping` target. - Every dotted `target:` hit in the tree belongs to another schema: action or form targets, API endpoint `inputMapping` / `outputMapping`, and the seed loader. - hotcrm (the card's origin, hotcrm#1836) is NOT MEASURED from here. ## Pins (the pin sweep) - **Flipped:** - objectstack-ai#20150 pinned `mailing_address.street` as `unknown` in `import-mapping-target.test.ts`, and as a finding in `validate-mapping-target-fields.test.ts`, on an object that declares that address field. - Both now assert the new meaning: all seven parts answer `part`, and the card's full address template is green at `validate`. - The objectstack-ai#20150 integration pin keeps refusing `mailing_address.street` on `task`, which declares no such field. Its comment now says why. - **New, real engine** (`import-integration.test.ts`, sqlite): - The customer template's five columns over three rows (full, partial, all blank). - The dry run and the commit agree row for row: `ok 3` each. - Read back: one address object, a two-part address, and an unset field. - An unknown part and a dotted path on a text field are refused on both paths with identical bodies and the part list named, and nothing is written. - The whole-plus-part collision is refused on both paths. - The control mapping with no dotted target passes on both. - **Unit:** the assembly rule in `import-mapping.test.ts` (blank, trim, `trimWhitespace: false`, every transform, two compound fields kept apart, no object definition means no part reading). The door's three refusal texts. The lint rule's three refusals. ## Tests (all through `os-verify-lock.sh`, shared box; tree `f21f41dbea`) - `@objectstack/spec`: `vitest run --project local`: 542 files, 15945 passed, 2 todo. `typecheck` (tsc, scripts, test layer): exit 0. - `@objectstack/rest`: `vitest run --project local`: 199 files, 3569 passed, 1 skipped. `typecheck` (tsc and test layer): exit 0. The `repo` project is declared to CI. - `@objectstack/lint`: `vitest run`: 109 files, 4236 passed. `typecheck`: exit 0. - `@objectstack/cli` (unit): `test/validate-build-gate-parity.test.ts`: 22 passed. The `integration` layer is declared to CI. - ESLint over the 10 changed `.ts` files (`--no-inline-config --format json`): 10 files, 0 errors, 0 warnings. - The population is the config's `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus the build directories, which all 10 files fall under. - `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`), so this diff cannot move the verdict on any file it does not touch. - The repo-wide `pnpm lint` is CI's. ### Ablations (one-shot; each through `scripts/ablation-replace.mjs`, anchor hit 1, blob changed, restored to the HEAD blob with `git diff HEAD` empty) - **A.** The spec part arm answers nothing (`import-mapping-target.ts`): 3 red, 18 green in `import-mapping-target.test.ts`. The red tests are the part arm, the address-by-parts mapping and the collision. - **B.** The rest assembly is off (`import-mapping.ts`): 5 red, 57 green. - 4 red are the assembly unit tests; the control stays green. - The fifth is the integration test. With assembly off, the dry run still answered `ok 3` while the commit answered `ok 1, errors 2`. That is the card's own dry-run-versus-commit defect, reproduced. - **C.** The lint dotted reason is dropped (`validate-mapping-target-fields.ts`): 2 red, 11 green (the unknown-part and no-parts refusals). ## Gates `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 108 families at `f21f41dbea`, and `--ran` reconciles them: 105 run green, 3 NOT MEASURED, 0 unrun. - NOT MEASURED, each exit 3 (PREREQUISITE NOT MET): - `check:skill-examples` needs a `client-react` build. The shared lock never granted one inside its budget. - `check:dual-build-cjs-loads` and `check:type-check-debt` read a whole-repo build that this worktree does not have. - `check:generated`: all 15 artifacts are up to date after `gen:api-surface`, `gen:export-origins` and `gen:docs`, which are committed. - `check:adr-0087-registration`: green. The changeset declares a widening only (`Clause-②: yes`), so no disposition marker applies. - A mergeability probe against `origin/main` `ab820016b3`, from a bare clone with no regen driver: clean. ## Acceptance notes - Only `address` is compound for import. A `location` field still imports whole, as one JSON object. Adding it would need a per-part number coercion and a required-part check. Carrier: none. - A JSON-format row can put a non-string value in a part, such as a number for `postalCode`. That value passes through to the engine's value-shape check, which admits it with a warning on a warn-first deployment (measured above). This is the posture every structured value on the import path already has. Carrier: none. - On an update, the assembled value replaces the stored address whole (measured). Merging parts into the stored value would be a new decision. It is documented in the docs row and the changeset. Carrier: none. - The collision refusal travels under the existing rule id `mapping-target-field-unknown` and the existing door code `INVALID_FIELD`. No new rule id or error code was minted. - The spec verdict is shared. The door and the lint rule each phrase their own sentence from it (`head.parts`, `index.parts`), as objectstack-ai#20150 left them. --- _Generated by [Claude Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…s validate` and the published per-type schemas (objectstack-ai#19098) (objectstack-ai#20266) Fixes objectstack-ai#19098 Fixes objectstack-ai#20270 Clause-②: no (narrowing) Executes maintainer ruling `5856790152` on objectstack-ai#19098 (batch objectstack-ai#227 item 5, **letter C**, maintainer 「同意」, confirmed `5856865990`): `os generate schema` is retired, not repaired. No file is generated in its place, and nothing in `packages/spec` moves (ruling item 4). ## What changed **`packages/cli/src/commands/generate.ts`** - `RETIRED_GENERATORS` gains `schema` in the `os g agent` shape. The refusal reads "`os g schema` was retired" and cites the maintainer ruling. It names `os validate` (the real parse: `ObjectStackDefinitionSchema.safeParse`), and names the per-type schemas `@objectstack/spec` publishes at `node_modules/@objectstack/spec/json-schema/CATEGORY/TYPE.json`, which carry the published projection plus `x-dropped-refinements`. It exits 1. - The ruling's id lives in the ledger's code comment, not in the refusal. Text an author is shown carries no tracker number (AGENTS.md; `check:doc-authoring`'s cross-package prose-id leg ratchets `#NNN` tokens in `packages/**` strings, and `generate.ts` has no ledger row). - **Deleted, not left dead:** `runSchemaGeneration`, with its three `z.toJSONSchema(ObjectStackDefinitionSchema…)` rungs and both ladder notices; `KNOWN_UNSUPPORTED_JSON_SCHEMA_PATTERNS`; `isKnownUnsupportedJsonSchema`; and the `case 'schema'` route. - Nothing else was schema-only. `-o`, `--dry-run` and `--format` serve `types`, `client` and `migration`. `z` and `ObjectStackDefinitionSchema` were dynamic imports inside the deleted function. `zod` stays a dependency, used by `compile`, `validate` and `format`. The `type` argument's help text derives from `GENERATORS` and never listed `schema`. - **The ledger door moved (PM mechanism assumption 2, measured and falsified).** The lookup sat inside `runMetadataGeneration`, which runs only after two earlier steps: the sub-command `switch` (where `schema` returned first) and the NAME requirement (where a nameless call stopped). So a `schema` entry alone could never be reached from `os generate schema`. - BEFORE, at BASE `0d3ec4713` on the dev runner, `os g agent` with no name printed `Missing required argument` and exited 1. - The lookup now runs first in `Generate.run`, through `refuseRetiredGenerator`, as an own-key read (the package's `Object.prototype.hasOwnProperty.call` idiom). - Consequences, both pinned. `os g agent` with no name now prints the agent retirement. `os g constructor NAME` used to print "`os g constructor` was retired — undefined" and crash with `TypeError: retired.detail is not iterable`. It now falls through to the ordinary checks: exit 1, "No file naming convention is declared for type: constructor". **Tests** - `packages/cli/test/generate-schema-retired.e2e.test.ts` (new) is the retirement pin, in the `agent` precedent's shape: a real child process through `bin/run-dev.js` plus tsx, with assertions on stdout content and the exit status. It covers: - `os generate schema` (no name) and `os g schema -o custom.schema.json` both exit 1 and write nothing (directory listing `[]`); - "was retired", with neither "Unknown type:" nor "Missing required argument"; - "maintainer ruling", `os validate`, and `@objectstack/spec/json-schema/`; - the alias output is byte-equal to the documented spelling's; - `os g agent` (no name) answers the agent retirement; - `os g constructor thing` is not taken for a retired type; - control: `os g object customer --dry-run` still previews. - `packages/cli/test/generate-schema-writes-json-schema.e2e.test.ts` (the objectstack-ai#17873 pin, groups (a) to (e)) is **deleted**. Every assertion in it was about the document the ruling withdrew: the file exists, parses, declares its draft, the four members' fragments, and the input direction. Nothing in it survives the retirement. - Other tests that exercised the command: none. `git grep` for `generate schema`, `runSchemaGeneration`, `objectstack.schema.json` and `isKnownUnsupportedJsonSchema` over `packages/cli/src` and `packages/cli/test` finds only the deleted file. The ladder PR (objectstack-ai#17903) added no pin of its own beyond that file. - `packages/cli/test/generate-refuses-retired-generator.test.ts` (new, patch round 1) is the per-PR guard for the door. It spawns the CLI and is not named `.e2e`, so it runs in the queue's `integration` project. It pins: - `os generate schema` and `os g schema -o custom.schema.json`: exit 1, nothing written, the retirement answered (neither "Missing required argument" nor "Unknown type:"), naming the ruling, `os validate` and `@objectstack/spec/json-schema/`; - `os g constructor thing`: an own-key miss (exit 1, not "was retired", no `TypeError`, nothing written); - control: `os g object customer --dry-run` exits 0 and previews exactly what the exported object template emits. - `packages/cli/test/generate-agent-retired.e2e.test.ts` (objectstack-ai#20270, its own commit `a0cec66e9`): the live-generator control asserted `import * as Data from '@objectstack/spec/data'`, which the object template stopped emitting in `0bd11261e`, so this nightly case was red on main. It now asserts the template's current shape, `ObjectSchema` among the named imports from `@objectstack/spec/data` plus the `ObjectSchema.create({` call, never one exact import line. The case is neither skipped nor quarantined. **Docs (ruling item 2).** The sweep is `git grep -n "generate schema" -- content/docs`, run at BASE: | hit | disposition | |:--|:--| | `content/docs/api/data-flow.mdx:200`, the JSON Schema row ("Autocomplete and validation for `objectstack.config.ts` (via `os generate schema`)") | **removed** | | `content/docs/references/api/plugin-rest-api.mdx:107` | kept: a substring of "Auto-generate schemas", the REST plugin's `generateSchemas` option, in an auto-generated reference; it is not the command | | `content/docs/references/api/plugin-rest-api.mdx:282` | kept: same | - One docs edit goes beyond the grep-found set, declared here. The Mermaid node `D[JSON Schema]` and its two edges, in the same section directly above the row, pictured the removed row's output ("Build Time: generate, then JSON Schema, then feed IDE Autocomplete"). They are removed, and `TypeScript Types` now feeds `IDE Autocomplete` alone. - The page's front matter is untouched. objectstack-ai#20170 landed its title-rule hunk on it, and the merge was clean. - `content/docs/deployment/cli.mdx` (patch round 1) gains a warn-type Callout, "`os generate schema` is retired", in the `os generate` section beside the `os g agent` one, the same shape as the two prior CLI retirements. It points at `os validate` (linked to the page's own `os validate` heading) and at the per-type schemas `@objectstack/spec` publishes. The refusal's `Docs:` link lands on this page. The page never listed a `schema` sub-type, so nothing else on it changes. - Also swept, with nothing to change: - `packages/cli/README.md` carries only the generic `os generate` row; - `skills/` has 0 hits; - `content/docs/protocol/diagram.mdx:243-255` (see Acceptance notes). **Changeset** `.changeset/19098-retire-generate-schema.md`: - `@objectstack/cli: minor`, with a **BREAKING** banner and `Clause-②: no (narrowing)`; - the ADR-0087 marker in the `os g agent` wording (`not-required (no-migration-prescription)`); - reach outside the repository stated as NOT MEASURED (no telemetry); - the refusal's pointer given as the migration: delete the call and any editor mapping of `objectstack.schema.json`, run `os validate`, and point editors at the per-type schemas. ## Verification, patch round 1 (at HEAD `2cb9e9613` unless stated) - **Merge.** `origin/main` `17bd31877` merged by merge as `9f702ee94` (`scripts/pm/os-regen-merge.sh`). No incoming file is one of this PR's. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 93 commands: round 0's 92 plus `pnpm check:cli-examples-parity`, from the `cli.mdx` edit. All 93 exited 0 at `2cb9e9613`. `--ran` with the recorded exit codes reads "93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero). - **Named gates:** - `pnpm lint`: exit 0 (118 s); - `node scripts/check-issue-citations.mjs --base origin/main` (`ae8e3ca01`): exit 0; - `pnpm check:docs-audit-scope` and `node scripts/docs-audit/check-affected-docs.mjs`: exit 0 each; - `pnpm check:docs-transcript-drift` and `pnpm check:cli-examples-parity`: exit 0 each; - `pnpm --filter @objectstack/cli typecheck`: `VERDICT command-exit 0`, with the new pin inside the `tsconfig.test.json` program (`--listFilesOnly`: 1 hit). - **Per-PR pin.** `pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/generate-refuses-retired-generator.test.ts`: 7/7. It sits in the queue's `integration` population (54 to 55 files) and is absent from the nightly population. The partition pin `test/vitest-tiers-partition.test.ts` passes 22/22. - **Nightly.** `OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/generate-schema-retired.e2e.test.ts test/generate-agent-retired.e2e.test.ts`: 2 files, 19/19. That includes "the generators that were not retired still work", the objectstack-ai#20270 case. - **Shape proof for objectstack-ai#20270.** The two new assertions accept the current emission: through the exported template, the `ObjectSchema` import matches and the `create` call matches. They reject the pre-objectstack-ai#20195 emission taken from `0bd11261e^`: neither matches. - **Ablations on the per-PR file.** These ran on committed state `a2dda36c2`. `generate.ts` and the per-PR file are byte-identical at `2cb9e9613`. | leg | mutation | on-disk proof | per-PR pin result | restore | |:--|:--|:--|:--|:--| | A | `schema` ledger key renamed to `schema_ablated` | anchor 1 to 0; blob `62cb7ec8b8ed` to `7a1f27dd8cfb`; grep `schema-entry=0 ablated-entry=1` | **2 failed**, 5 passed: "is the retirement, not a missing name and not an unknown type"; "names the ruling, `os validate` and the per-type schemas" | blob equals HEAD `62cb7ec8b8ed`, `git diff HEAD` empty | | B | own-key read reverted to `RETIRED_GENERATORS[args.type]` | anchor 1 to 0; blob to `26ec87322f84`; grep `own-key=0 bracket=1` | **1 failed**, 6 passed: the `os g constructor thing` case | same | After both legs, the restored per-PR run passed 7/7. - **History.** The objectstack-ai#20270 fix is its own commit, `a0cec66e9`. A first push, `a2dda36c2`, had carried a first version of it together with the per-PR pin and the callout. Rewriting that pushed commit was refused by the session's permission classifier, so the split line was merged with the pushed head instead (`2cb9e9613`, whose tree is identical to `a0cec66e9`'s). Nothing pushed was discarded. ## Verification, round 0 (at HEAD `c1036ebb9` unless stated) - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 92 commands, and all 92 exited 0 at `c1036ebb9`. `--ran` with the recorded exit codes reads "92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero). - `check-adr-0087-registration --base origin/main` reads `[BREAKING+clause-②-narrowing] not-required (no-migration-prescription)`. - `check-changeset-no-major` reads "no `major` bump". Its level axis reads the PR body, so it is judged in CI. - **`pnpm lint`** (repo-wide, `eslint . --no-inline-config`): exit 0, 130 s. - **`pnpm --filter @objectstack/cli typecheck`**: `VERDICT command-exit 0`. That is `tsc --noEmit` plus `check:test-typecheck`, and the new pin is inside the `tsconfig.test.json` program (`--listFilesOnly`: 1 hit). - **Board check.** `node scripts/check-issue-citations.mjs --base origin/main` (`a9fb83ef0`) exits 0: 73 citations judged, 71 resolve, 2 cross-repo. - **`unit` layer**, run at merge commit `5c7c5bee3`: - `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2` ran 229 files: 227 passed and 2 failed as prerequisite refusals ("packages/cli is not built"). - After the CLI build, those 2 files passed (29 tests). - `c1036ebb9` differs from `5c7c5bee3` only in the new pin, which is not in the unit population. - **Integration tier.** The retirement pin is `*.e2e.test.ts` like the `agent` precedent, so it is **nightly-tier**: the per-PR queue run does not select it. - Run locally as `OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2` over the new pin plus `generate-agent-retired`, `generate-skill` and `generate-object-namespace-prefix`. - Result: 38 passed and 1 failed. The failure is the pre-existing stale control in `generate-agent-retired.e2e.test.ts` (see Acceptance notes); the new pin is 10/10. - **Reach checks** on the dev runner, BEFORE at BASE and AFTER at HEAD: - `os generate schema -o out.json` went from exit 0 and a 3,335,734-byte file written to exit 1, the refusal, and nothing written; - `os g agent` went from "Missing required argument" to the agent retirement; - `os g constructor foo` went from a `TypeError` crash to the ordinary refusal. **Ablation.** This is a one-shot proof, run against the committed state `c1036ebb9` through `scripts/ablation-replace.mjs` under a script-level `trap` restore. The subject resolves from `src/` via tsx, so no dist is involved. | leg | mutation | on-disk proof | pin result | restore | |:--|:--|:--|:--|:--| | A | `schema` ledger key renamed to `schema_ablated` (the lookup misses it) | anchor 1 to 0; blob `62cb7ec8b8ed` to `7a1f27dd8cfb`; grep `schema-entry=0 ablated-entry=1` | **4 failed**, 6 passed: "was RETIRED", "names the ruling", `os validate`, per-type schemas | blob equals HEAD `62cb7ec8b8ed`; `git diff HEAD` empty | | B | own-key read reverted to `RETIRED_GENERATORS[args.type]` | anchor 1 to 0; blob to `26ec87322f84`; grep `own-key=0 bracket=1` | **1 failed**, 9 passed: the `constructor` own-keys assertion | same | In leg A the exit-1 and writes-nothing assertions stayed green, because the missing-name path also exits 1 without writing. That is exactly why the pin asserts the refusal's content, as the `agent` precedent does. ## Acceptance notes 1. Now repaired here (objectstack-ai#20270, commit `a0cec66e9`): the live-generator control in `packages/cli/test/generate-agent-retired.e2e.test.ts` asserted `import * as Data from '@objectstack/spec/data'`, which the object template stopped emitting in `0bd11261e`. - Triage's note-2 sweep covered all 73 nightly-tier files under `packages/cli` for pre-objectstack-ai#20195 template text (`import * as Data from '@objectstack/spec/data'`, `Data.ServiceObject`). That line was the only nightly hit; the control, the same sweep at `9f702ee94`, finds it. - Two per-PR files carry the old shape on purpose and are not changed: `scaffold-emission-typechecks.test.ts` (a tsc canary fixture) and `scaffold-object-declaration-shape.test.ts` (it refuses the pre-ruling literal). 2. `.changeset/sour-moons-smile.md`, pending and unreleased, records the objectstack-ai#17903 ladder repair this PR deletes. It is left unchanged. - Rewriting it here is the case `check-empty-changeset.mjs` classes as a DELIBERATE CORRECTION (objectstack-ai#18160, ruling D on objectstack-ai#17712). The gate goes red on any modification or deletion of a changeset from the merge base, and its remedy is "do NOT restore it -- say so on the PR and get it confirmed". - The seat answered the open question with A: leave the file. The new changeset tells the reader that this retirement supersedes that repair, and the release owner may drop the file at compilation. 3. The docblock at `packages/spec/scripts/lib/refinement-projection.ts` (the "Producers outside `packages/spec`'s own artifacts" bullet) still names the CLI's `os generate` as a direct `z.toJSONSchema` producer. It goes stale once this lands, but `packages/spec` is untouched by ruling item 4. 4. The `GENERATORS[type]` lookup in `runMetadataGeneration` is still an inherited-key read. So `os g constructor NAME` answers "No file naming convention is declared for type: constructor" rather than "Unknown type". The answer is misleading but does not crash, and it is not changed here. 5. Reach outside this repository is NOT MEASURED (no telemetry). Inside it there is no reader and no editor mapping of `objectstack.schema.json`. 6. `content/docs/protocol/diagram.mdx:243-255` carries a `D[JSON Schema]` node feeding IDE autocomplete, and a row "Generated JSON Schema files for IDE validation". It does not name the command, and the plural "files" reads as the per-type schemas `@objectstack/spec` still publishes, so it is left unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
veigajoao
pushed a commit
to veigajoao/objectstack
that referenced
this pull request
Sep 29, 2026
…ebar labels kept via navTitle (objectstack-ai#20401) Fixes objectstack-ai#15403 Clause-②: no ## Summary The generated reference pages under `content/docs/references/**` now carry the docs page-title rule, emitted by the generator itself, and every one keeps its sidebar label byte-identical through `navTitle`. - **One pure function holds the rule:** `packages/spec/scripts/lib/page-title.ts`. It builds each title from data the generator already holds (the module display name and the category's declared title) plus fixed words, picks the longest candidate inside the band, and refuses (never truncates) when no candidate fits. - **Three emission sites use it:** module pages and category index pages in `packages/spec/scripts/build-docs.ts`, and the root index in `packages/spec/scripts/lib/root-index.ts`. - **`navTitle` = the page's previous `title`, verbatim**, so `navLabel()` in `apps/docs/lib/nav-title.ts` returns the same string as before. The page tree is unchanged (replayed below, with a control). - **211 generated pages regenerated**, frontmatter only: each is `+2/-1` (the `title:` line replaced, a `navTitle:` line added). No `description:`, no body, no `meta.json`. ## The rule, and how the emitter expresses it The rule is the maintainer's, recorded on objectstack-ai#12237 in comment `5419555720`, verbatim 「其他同意」. Its text as landed is the `## The rule` section of PR objectstack-ai#20170: - shape `PRIMARY-KEYWORD — QUALIFIER`, separator ` — `; - frontmatter `title` is 36–46 characters, so the rendered title with the 14-character ` | ObjectStack` suffix is 50–60, none over 60; - `ObjectStack` never inside a title; - short navigation labels go in `navTitle`, whose fallback to `title` lives only in `navLabel()`. The generator holds only the module display name (3–29 characters on this tree), the category's declared title (11–20 for categories with pages) and fixed words. No single template lands every page inside an 11-character band, so each page kind has a short ladder, longest first, and the title is the first candidate inside the band: | page kind | candidate (longest first) | length | |---|---|--:| | module page | `NAME schema — CATEGORY property reference`, offered only when the page renders a `### Properties` table | name + category + 29 | | | `NAME schema — CATEGORY reference` | name + category + 20 | | | `NAME — CATEGORY reference` | name + category + 13 | | | `NAME — CATEGORY` | name + category + 3 | | category index | `CATEGORY — complete schema reference` | category + 28 | | | `CATEGORY — schema reference` | category + 19 | | root index | `Protocol reference — every schema by module` | 43 | The module rungs overlap end to end and cover every name + category length from 7 to 43; the longest pair on the tree is 41 (`Schemaless Node Config` in `Automation Protocol`). The first module rung is conditional: `property reference` is offered only when at least one of the page's schemas renders a `### Properties` table (`rendersPropertiesTable` in `lib/schema-section.ts`, built on `declaresProperties`, the one expression the section renderer itself branches on). A page without one, such as the enum-only `data/feed`, starts at the second rung; without the first rung the ladder covers name + category lengths 16 to 43, and the shortest property-less pair on the tree is 17 (`Feed` in `Data Protocol`). The category rungs cover category titles of 8 to 27 characters; the longest declared title is 25. The category in every module title keeps same-named modules apart (`Plugin` is both a `kernel` and a `studio` page). A page no rung fits stops `gen:docs` with a message naming the page and every candidate with its length. Rung usage on this tree: module pages 18 / 103 / 60 / 15 (rungs 1–4), category index pages 11 / 3, root index 1. All 18 rung-1 pages render a `### Properties` table; the 14 module pages that render none are all on rungs 2–4. ## Measured, before and after Population re-derived at base `862b6ce8` and at head: `content/docs/**/*.mdx` = **406**; pages carrying the generator's `AUTO-GENERATED — DO NOT EDIT` banner = **211**, all under `content/docs/references/**` (196 module pages, 14 category index pages, 1 root index). Rendered = `title` + 14. | | pages | min | median | max | in 36–46 | outside | rendered over 60 | with ` — ` | with `ObjectStack` | duplicate titles | `navTitle` | |---|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:| | before (`origin/main`) | 211 | 3 | 11 | 29 | 0 | 211 | 0 | 0 | 0 | 6 | 0 | | after (this head) | 211 | 37 | 43 | 46 | 211 | 0 | 0 | 211 | 0 | 0 | 211 | After: rendered 51–60. Duplicate titles across the whole corpus (406 pages, authored and generated): **0**. **The card body's 225 of 405 does not reproduce, and the difference is not generator output.** The generated set is 211; the 14 others in that count are `content/docs/releases/**` pages, which carry no generator banner and are release-owned (compiled by hand at release time). No other emitter writes a `title:` into `content/docs/**`: `build-skill-docs.ts` rewrites only the region between its markers in `content/docs/ai/skills-reference.mdx`, whose authored title already follows the rule (PR objectstack-ai#20170). The only `title:` emissions in the repo's generators are the three this PR changes (`git grep` over `scripts/`, `packages/*/scripts/`, `apps/docs/scripts/`). ## The page tree is unchanged Replayed with the site's own machinery: `fumadocs-core` 16.14.4 `loader()` (the `apps/docs` dependency) with the site's `i18n` settings and the site's `navTitlePlugin()`, over every `.mdx` frontmatter and `meta.json` under `content/docs`, once from `origin/main` and once from this head. Serialized page tree (folders, separators, pages, folder index pages): **449 nodes before, 449 after, `diff` exit 0.** Control, so the replay can see a title change at all: the same replay of this head WITHOUT `navTitlePlugin()` differs from `origin/main` in **388** nodes, exactly the 211 generated pages plus the 177 authored pages PR objectstack-ai#20170 gave a `navTitle`. Folder labels come from each category's `meta.json` `title`, which this PR does not touch. The category and root `index.mdx` pages carry `navTitle` anyway because the footer previous/next list walks folder index pages. ## Examples | page | before | after | chars / rendered | |---|---|---|--:| | `references/ai/mcp.mdx` | Mcp | Mcp schema — AI Protocol property reference | 43 / 57 | | `references/ai/agent.mdx` | Agent | Agent schema — AI Protocol property reference | 45 / 59 | | `references/data/feed.mdx` | Feed | Feed schema — Data Protocol reference | 37 / 51 | | `references/data/object.mdx` | Object | Object schema — Data Protocol reference | 39 / 53 | | `references/automation/flow.mdx` | Flow | Flow schema — Automation Protocol reference | 43 / 57 | | `references/kernel/plugin.mdx` | Plugin | Plugin schema — Kernel Protocol reference | 41 / 55 | | `references/studio/plugin.mdx` | Plugin | Plugin schema — Studio Protocol reference | 41 / 55 | | `references/kernel/plugin-registry.mdx` | Plugin Registry | Plugin Registry — Kernel Protocol reference | 43 / 57 | | `references/kernel/metadata-protection.mdx` | Metadata Protection | Metadata Protection — Kernel Protocol | 37 / 51 | | `references/ui/expression-bindable-text-keys.mdx` | Expression Bindable Text Keys | Expression Bindable Text Keys — UI Protocol | 43 / 57 | | `references/automation/schemaless-node-config.mdx` | Schemaless Node Config | Schemaless Node Config — Automation Protocol | 44 / 58 | | `references/ai/index.mdx` | AI Protocol | AI Protocol — complete schema reference | 39 / 53 | | `references/automation/index.mdx` | Automation Protocol | Automation Protocol — schema reference | 38 / 52 | | `references/identity/index.mdx` | Identity Protocol | Identity Protocol — complete schema reference | 45 / 59 | | `references/index.mdx` | Protocol Reference | Protocol reference — every schema by module | 43 / 57 | The complete table is at the end. ## Wording for maintainer voice review The rule is approved; the fixed words the generator adds are new public text and are not in the approved table of PR objectstack-ai#12312: `schema`, `property reference`, `reference`, `complete schema reference`, `schema reference`, and the root title `Protocol reference — every schema by module`. Rewording any of them is a one-line change in `lib/page-title.ts` plus `gen:docs`; the band, the unit test and `check:docs` hold the result either way. Non-blocking: the seat answered land-as-shipped in comment 5865474678 on objectstack-ai#15403, and `property reference` is now claimed only by pages that render a property table. ## File surface - `packages/spec/scripts/lib/page-title.ts` (new): the rule, the ladders, the refusal, the frontmatter lines. - `packages/spec/scripts/page-title.test.ts` (new): the pin (`local` vitest project; reads nothing outside the package). - `packages/spec/scripts/build-docs.ts`: the two emission sites (module pages, category index pages), located by symbol. - `packages/spec/scripts/lib/schema-section.ts`: `declaresProperties()` (an object that declares properties, the one spelling of that condition, now also used by the renderer's root and union-arm branches) and `rendersPropertiesTable()` (whether a schema renders at least one `### Properties` table), read by the module-page title. Rendered output unchanged: regenerating moved exactly one page, `data/feed.mdx`, and only its `title:` line. - `packages/spec/scripts/lib/root-index.ts`: **one site beyond the two the dispatch named.** It is the generator's third `title:` emission (the root `references/index.mdx`, previously `title: Protocol Reference`, 18 characters). Leaving it would have left one generated page outside the rule. - `content/docs/references/**`: 211 pages, regenerated by `gen:docs`, never hand-edited. - Not touched: authored pages, the `description:` emission, `check-generated.ts` (it regenerates and compares; nothing in it reads the title line), `build-skill-docs.ts` (emits no title). ## Changeset: `skip-changeset` Rule applied: a changeset is owed when a package's published `files[]` content moves. `@objectstack/spec` publishes `dist`, `json-schema`, `liveness`, `prompts`, `llms.txt`, `README.md`, `src/**/*.zod.ts`, `CHANGELOG.md`, `api-surface`, `spec-changes.json`; `scripts/` is not among them, and `apps/docs` (which renders `content/docs`) is private. Measured after the build: the new symbols (`titleFrontmatter`, `navTitle`, `property reference`) have **0** hits across all ten of those paths, while the positive control `ObjectSchema` hits in 9 of the 10 (all but `json-schema`). ## Verification Head `1ba9d84c`, REWORK round 1: `41b0f683` (the conditional rung), `0ea42f45` (`data/feed.mdx` regenerated), then a merge of `origin/main` at `df3ba164` whose two deferred pages, `security/{permission,rls}.mdx`, were regenerated on the merged tree in `1ba9d84c`. Round 0 was head `a61335f9`. - `pnpm --filter @objectstack/spec test`: 561 files, 16546 passed, 1 todo. - `pnpm --filter @objectstack/spec test:repo`: 35 files, 634 passed. - Type check: `check:scripts-typecheck` (`tsc -p tsconfig.scripts.json`, the program holding every file this diff edits, `page-title.test.ts` included) exit 0 at `1ba9d84c`. The full spec `typecheck` was green locally at round 0's pre-merge head `0a241893`, and `Type Check · workspace` ran it green in CI on `a61335f9`. - The new files are in the scripts tsc program (`tsc -p tsconfig.scripts.json --listFiles` lists `lib/page-title.ts` and `page-title.test.ts`). - Gates derived by `node scripts/pm/dispatch-gates.mjs --commands` over this diff at `1ba9d84c` (216 paths, the same 89 families as round 0), each exit code recorded, then `--ran`: **89 derived, 85 run (all exit 0), 4 NOT MEASURED, 0 UNRUN.** Among the 85: `check:docs`, `check:generated` (all 15 generated artifacts up to date), `check:doc-frontmatter`, `check:docs-single-h1`, `check:doc-anchors`, `check:nul-bytes`, `check:quick-reference-counts`. - NOT MEASURED, prerequisite refused (exit 3): `check:skill-examples` (needs the 36-package `@objectstack/client-react` closure built), `check:dual-build-cjs-loads` (needs every package built), `check:type-check-debt` (needs a 30-package closure built). - NOT MEASURED, timed out: `check:pm-dispatch-gates`. Its self-test of `scripts/pm/dispatch-gates.mjs`, which this diff does not edit, did not finish in a 420 s and then a 560 s window on the shared box. Not re-run in round 1; `Lint & Repo Gates` ran it green in CI on `a61335f9`. - **Ablation 1, the unit test can fail:** `scripts/ablation-replace.mjs` changed `TITLE_MAX = 46` to `60` in `lib/page-title.ts` (anchor 1 to 0, blob `3fe85286f82d` to `ee55509927c8`). `page-title.test.ts` went **12 failed / 13 passed**. Restored under an EXIT/INT/TERM trap to `3fe85286f82d` = HEAD blob, `git diff HEAD` empty. - **Ablation 2, `check:docs` sees the emission:** the first module rung's `property reference` changed to `field reference` (blob `3fe85286f82d` to `b8f4c6d26269`). `check:docs` exited **1** with **49** pages out of date: the 19 rung-1 pages, plus the pages the shorter word lets onto rung 1. Restored the same way. - **Ablation 3, the rung condition is pinned:** `...(documentsProperties ? [` changed to `...(true ? [` in `lib/page-title.ts` (anchor 1 to 0, blob `38277b23f728` to `90e0cbe18d3b`). `page-title.test.ts` went **3 failed / 38 passed**: the `data/feed` row, the condition pin, and the short property-less refusal. Restored to `38277b23f728` = HEAD blob, `git diff HEAD` empty. - **The predicate agrees with the renderer on every published schema:** over all 1523 JSON Schemas under `packages/spec/json-schema/`, `rendersPropertiesTable(name, schema)` equals whether `renderSchemaSection(name, schema)` emits a `### Properties` heading: 1219 true, **0 disagreements**. The unit pin holds the same agreement shape by shape (11 JSON Schema shapes covering every renderer branch). ## Acceptance notes - **Module display names are title-cased from the file slug, so abbreviations come out wrong:** `Mcp`, `Rls`, `Scim`, `Odata`, `Http`, `I18n`, `Driver Sql`, `Cli Extension`, `Io Node Config`, `Bpmn Interop`, `Package Api`, `Rest Server`, `Events Dlq`, and others. This is the `Qa Protocol` defect class that `lib/category-title.ts` fixed for category titles, one level down, and it predates this PR (these strings were the whole title before; they are the `navTitle` now, and the primary keyword of the new title). Out of scope here: fixing it changes nav labels, which this card keeps byte-identical. Noted, not filed. - The card body's 225 / 405 reading included 14 `releases/**` pages (see *Measured*); nothing here depends on them. - `api/protocol.mdx` renders 6 `### Properties` tables with no rows (object schemas declaring an empty `properties`). Pre-existing renderer output; that page is on rung 2 and carries non-empty tables as well. Noted, not filed. - The optional routing of `rootIndexTitle()` through `pageTitleOrExit` was not taken: `pageTitleOrExit` lives in `build-docs.ts`, so routing it means passing the title into `renderRootIndex` (its input type and the `root-index.test.ts` fixtures), more than one line. The root title is a fixed 43-character string pinned by the unit test. ## The complete table All 211 generated pages. `navTitle` = before, verbatim, on every row. | page | before | after | chars / rendered | `navTitle` | |---|---|---|--:|---| | `references/index.mdx` | Protocol Reference | Protocol reference — every schema by module | 43 / 57 | = before | | `references/ai/index.mdx` | AI Protocol | AI Protocol — complete schema reference | 39 / 53 | = before | | `references/ai/agent.mdx` | Agent | Agent schema — AI Protocol property reference | 45 / 59 | = before | | `references/ai/build-progress.mdx` | Build Progress | Build Progress schema — AI Protocol reference | 45 / 59 | = before | | `references/ai/conversation.mdx` | Conversation | Conversation schema — AI Protocol reference | 43 / 57 | = before | | `references/ai/embedding.mdx` | Embedding | Embedding schema — AI Protocol reference | 40 / 54 | = before | | `references/ai/knowledge-document.mdx` | Knowledge Document | Knowledge Document — AI Protocol reference | 42 / 56 | = before | | `references/ai/knowledge-source.mdx` | Knowledge Source | Knowledge Source — AI Protocol reference | 40 / 54 | = before | | `references/ai/mcp.mdx` | Mcp | Mcp schema — AI Protocol property reference | 43 / 57 | = before | | `references/ai/model-registry.mdx` | Model Registry | Model Registry schema — AI Protocol reference | 45 / 59 | = before | | `references/ai/skill.mdx` | Skill | Skill schema — AI Protocol property reference | 45 / 59 | = before | | `references/ai/solution-blueprint.mdx` | Solution Blueprint | Solution Blueprint — AI Protocol reference | 42 / 56 | = before | | `references/ai/tool.mdx` | Tool | Tool schema — AI Protocol property reference | 44 / 58 | = before | | `references/ai/usage.mdx` | Usage | Usage schema — AI Protocol property reference | 45 / 59 | = before | | `references/api/index.mdx` | API Protocol | API Protocol — complete schema reference | 40 / 54 | = before | | `references/api/analytics.mdx` | Analytics | Analytics schema — API Protocol reference | 41 / 55 | = before | | `references/api/auth-endpoints.mdx` | Auth Endpoints | Auth Endpoints schema — API Protocol reference | 46 / 60 | = before | | `references/api/auth.mdx` | Auth | Auth schema — API Protocol property reference | 45 / 59 | = before | | `references/api/automation-api.mdx` | Automation Api | Automation Api schema — API Protocol reference | 46 / 60 | = before | | `references/api/batch.mdx` | Batch | Batch schema — API Protocol property reference | 46 / 60 | = before | | `references/api/contract.mdx` | Contract | Contract schema — API Protocol reference | 40 / 54 | = before | | `references/api/discovery.mdx` | Discovery | Discovery schema — API Protocol reference | 41 / 55 | = before | | `references/api/dispatcher.mdx` | Dispatcher | Dispatcher schema — API Protocol reference | 42 / 56 | = before | | `references/api/documentation.mdx` | Documentation | Documentation schema — API Protocol reference | 45 / 59 | = before | | `references/api/endpoint.mdx` | Endpoint | Endpoint schema — API Protocol reference | 40 / 54 | = before | | `references/api/error-code-ledger.mdx` | Error Code Ledger | Error Code Ledger — API Protocol reference | 42 / 56 | = before | | `references/api/errors.mdx` | Errors | Errors schema — API Protocol reference | 38 / 52 | = before | | `references/api/events.mdx` | Events | Events schema — API Protocol reference | 38 / 52 | = before | | `references/api/export.mdx` | Export | Export schema — API Protocol reference | 38 / 52 | = before | | `references/api/http-cache.mdx` | Http Cache | Http Cache schema — API Protocol reference | 42 / 56 | = before | | `references/api/metadata.mdx` | Metadata | Metadata schema — API Protocol reference | 40 / 54 | = before | | `references/api/misc.mdx` | Misc | Misc schema — API Protocol property reference | 45 / 59 | = before | | `references/api/odata.mdx` | Odata | Odata schema — API Protocol property reference | 46 / 60 | = before | | `references/api/package-api-assembled.mdx` | Package Api Assembled | Package Api Assembled — API Protocol reference | 46 / 60 | = before | | `references/api/package-api.mdx` | Package Api | Package Api schema — API Protocol reference | 43 / 57 | = before | | `references/api/package-lifecycle.mdx` | Package Lifecycle | Package Lifecycle — API Protocol reference | 42 / 56 | = before | | `references/api/plugin-rest-api.mdx` | Plugin Rest Api | Plugin Rest Api — API Protocol reference | 40 / 54 | = before | | `references/api/protocol.mdx` | Protocol | Protocol schema — API Protocol reference | 40 / 54 | = before | | `references/api/query-adapter.mdx` | Query Adapter | Query Adapter schema — API Protocol reference | 45 / 59 | = before | | `references/api/realtime-shared.mdx` | Realtime Shared | Realtime Shared — API Protocol reference | 40 / 54 | = before | | `references/api/realtime.mdx` | Realtime | Realtime schema — API Protocol reference | 40 / 54 | = before | | `references/api/rest-server.mdx` | Rest Server | Rest Server schema — API Protocol reference | 43 / 57 | = before | | `references/api/router.mdx` | Router | Router schema — API Protocol reference | 38 / 52 | = before | | `references/api/sortability.mdx` | Sortability | Sortability schema — API Protocol reference | 43 / 57 | = before | | `references/api/storage.mdx` | Storage | Storage schema — API Protocol reference | 39 / 53 | = before | | `references/api/versioning.mdx` | Versioning | Versioning schema — API Protocol reference | 42 / 56 | = before | | `references/api/websocket.mdx` | Websocket | Websocket schema — API Protocol reference | 41 / 55 | = before | | `references/automation/index.mdx` | Automation Protocol | Automation Protocol — schema reference | 38 / 52 | = before | | `references/automation/approval.mdx` | Approval | Approval — Automation Protocol reference | 40 / 54 | = before | | `references/automation/bpmn-interop.mdx` | Bpmn Interop | Bpmn Interop — Automation Protocol reference | 44 / 58 | = before | | `references/automation/builtin-node-config.mdx` | Builtin Node Config | Builtin Node Config — Automation Protocol | 41 / 55 | = before | | `references/automation/control-flow.mdx` | Control Flow | Control Flow — Automation Protocol reference | 44 / 58 | = before | | `references/automation/execution.mdx` | Execution | Execution — Automation Protocol reference | 41 / 55 | = before | | `references/automation/flow-function.mdx` | Flow Function | Flow Function — Automation Protocol reference | 45 / 59 | = before | | `references/automation/flow.mdx` | Flow | Flow schema — Automation Protocol reference | 43 / 57 | = before | | `references/automation/io-node-config.mdx` | Io Node Config | Io Node Config — Automation Protocol reference | 46 / 60 | = before | | `references/automation/node-executor.mdx` | Node Executor | Node Executor — Automation Protocol reference | 45 / 59 | = before | | `references/automation/schedule-organization.mdx` | Schedule Organization | Schedule Organization — Automation Protocol | 43 / 57 | = before | | `references/automation/schemaless-node-config.mdx` | Schemaless Node Config | Schemaless Node Config — Automation Protocol | 44 / 58 | = before | | `references/automation/state-machine.mdx` | State Machine | State Machine — Automation Protocol reference | 45 / 59 | = before | | `references/automation/time-relative-trigger.mdx` | Time Relative Trigger | Time Relative Trigger — Automation Protocol | 43 / 57 | = before | | `references/automation/webhook.mdx` | Webhook | Webhook schema — Automation Protocol reference | 46 / 60 | = before | | `references/data/index.mdx` | Data Protocol | Data Protocol — complete schema reference | 41 / 55 | = before | | `references/data/analytics.mdx` | Analytics | Analytics schema — Data Protocol reference | 42 / 56 | = before | | `references/data/context-tokens.mdx` | Context Tokens | Context Tokens — Data Protocol reference | 40 / 54 | = before | | `references/data/data-engine.mdx` | Data Engine | Data Engine schema — Data Protocol reference | 44 / 58 | = before | | `references/data/datasource.mdx` | Datasource | Datasource schema — Data Protocol reference | 43 / 57 | = before | | `references/data/date-macros.mdx` | Date Macros | Date Macros schema — Data Protocol reference | 44 / 58 | = before | | `references/data/document.mdx` | Document | Document schema — Data Protocol reference | 41 / 55 | = before | | `references/data/driver-common.mdx` | Driver Common | Driver Common schema — Data Protocol reference | 46 / 60 | = before | | `references/data/driver-memory.mdx` | Driver Memory | Driver Memory schema — Data Protocol reference | 46 / 60 | = before | | `references/data/driver-mongo.mdx` | Driver Mongo | Driver Mongo schema — Data Protocol reference | 45 / 59 | = before | | `references/data/driver-mysql.mdx` | Driver Mysql | Driver Mysql schema — Data Protocol reference | 45 / 59 | = before | | `references/data/driver-nosql.mdx` | Driver Nosql | Driver Nosql schema — Data Protocol reference | 45 / 59 | = before | | `references/data/driver-postgres.mdx` | Driver Postgres | Driver Postgres — Data Protocol reference | 41 / 55 | = before | | `references/data/driver-sql.mdx` | Driver Sql | Driver Sql schema — Data Protocol reference | 43 / 57 | = before | | `references/data/driver-sqlite.mdx` | Driver Sqlite | Driver Sqlite schema — Data Protocol reference | 46 / 60 | = before | | `references/data/driver-turso.mdx` | Driver Turso | Driver Turso schema — Data Protocol reference | 45 / 59 | = before | | `references/data/driver.mdx` | Driver | Driver schema — Data Protocol reference | 39 / 53 | = before | | `references/data/external-catalog.mdx` | External Catalog | External Catalog — Data Protocol reference | 42 / 56 | = before | | `references/data/feed.mdx` | Feed | Feed schema — Data Protocol reference | 37 / 51 | = before | | `references/data/field-value.mdx` | Field Value | Field Value schema — Data Protocol reference | 44 / 58 | = before | | `references/data/field.mdx` | Field | Field schema — Data Protocol reference | 38 / 52 | = before | | `references/data/filter.mdx` | Filter | Filter schema — Data Protocol reference | 39 / 53 | = before | | `references/data/hook-body.mdx` | Hook Body | Hook Body schema — Data Protocol reference | 42 / 56 | = before | | `references/data/hook.mdx` | Hook | Hook schema — Data Protocol property reference | 46 / 60 | = before | | `references/data/mapping.mdx` | Mapping | Mapping schema — Data Protocol reference | 40 / 54 | = before | | `references/data/object.mdx` | Object | Object schema — Data Protocol reference | 39 / 53 | = before | | `references/data/query.mdx` | Query | Query schema — Data Protocol reference | 38 / 52 | = before | | `references/data/seed-loader.mdx` | Seed Loader | Seed Loader schema — Data Protocol reference | 44 / 58 | = before | | `references/data/seed.mdx` | Seed | Seed schema — Data Protocol property reference | 46 / 60 | = before | | `references/data/validation.mdx` | Validation | Validation schema — Data Protocol reference | 43 / 57 | = before | | `references/identity/index.mdx` | Identity Protocol | Identity Protocol — complete schema reference | 45 / 59 | = before | | `references/identity/eval-user.mdx` | Eval User | Eval User schema — Identity Protocol reference | 46 / 60 | = before | | `references/identity/identity.mdx` | Identity | Identity schema — Identity Protocol reference | 45 / 59 | = before | | `references/identity/organization.mdx` | Organization | Organization — Identity Protocol reference | 42 / 56 | = before | | `references/identity/position.mdx` | Position | Position schema — Identity Protocol reference | 45 / 59 | = before | | `references/identity/scim.mdx` | Scim | Scim schema — Identity Protocol reference | 41 / 55 | = before | | `references/integration/index.mdx` | Integration Protocol | Integration Protocol — schema reference | 39 / 53 | = before | | `references/integration/connector.mdx` | Connector | Connector — Integration Protocol reference | 42 / 56 | = before | | `references/kernel/index.mdx` | Kernel Protocol | Kernel Protocol — complete schema reference | 43 / 57 | = before | | `references/kernel/cli-extension.mdx` | Cli Extension | Cli Extension — Kernel Protocol reference | 41 / 55 | = before | | `references/kernel/cluster.mdx` | Cluster | Cluster schema — Kernel Protocol reference | 42 / 56 | = before | | `references/kernel/context.mdx` | Context | Context schema — Kernel Protocol reference | 42 / 56 | = before | | `references/kernel/dependency-resolution.mdx` | Dependency Resolution | Dependency Resolution — Kernel Protocol | 39 / 53 | = before | | `references/kernel/events-bus.mdx` | Events Bus | Events Bus schema — Kernel Protocol reference | 45 / 59 | = before | | `references/kernel/events-core.mdx` | Events Core | Events Core schema — Kernel Protocol reference | 46 / 60 | = before | | `references/kernel/events-dlq.mdx` | Events Dlq | Events Dlq schema — Kernel Protocol reference | 45 / 59 | = before | | `references/kernel/events-handlers.mdx` | Events Handlers | Events Handlers — Kernel Protocol reference | 43 / 57 | = before | | `references/kernel/events-integrations.mdx` | Events Integrations | Events Integrations — Kernel Protocol | 37 / 51 | = before | | `references/kernel/events-queue.mdx` | Events Queue | Events Queue — Kernel Protocol reference | 40 / 54 | = before | | `references/kernel/execution-context.mdx` | Execution Context | Execution Context — Kernel Protocol reference | 45 / 59 | = before | | `references/kernel/manifest.mdx` | Manifest | Manifest schema — Kernel Protocol reference | 43 / 57 | = before | | `references/kernel/metadata-loader.mdx` | Metadata Loader | Metadata Loader — Kernel Protocol reference | 43 / 57 | = before | | `references/kernel/metadata-plugin.mdx` | Metadata Plugin | Metadata Plugin — Kernel Protocol reference | 43 / 57 | = before | | `references/kernel/metadata-protection.mdx` | Metadata Protection | Metadata Protection — Kernel Protocol | 37 / 51 | = before | | `references/kernel/package-artifact.mdx` | Package Artifact | Package Artifact — Kernel Protocol reference | 44 / 58 | = before | | `references/kernel/package-registry.mdx` | Package Registry | Package Registry — Kernel Protocol reference | 44 / 58 | = before | | `references/kernel/package-upgrade.mdx` | Package Upgrade | Package Upgrade — Kernel Protocol reference | 43 / 57 | = before | | `references/kernel/plugin-capability.mdx` | Plugin Capability | Plugin Capability — Kernel Protocol reference | 45 / 59 | = before | | `references/kernel/plugin-lifecycle-advanced.mdx` | Plugin Lifecycle Advanced | Plugin Lifecycle Advanced — Kernel Protocol | 43 / 57 | = before | | `references/kernel/plugin-loading.mdx` | Plugin Loading | Plugin Loading — Kernel Protocol reference | 42 / 56 | = before | | `references/kernel/plugin-registry.mdx` | Plugin Registry | Plugin Registry — Kernel Protocol reference | 43 / 57 | = before | | `references/kernel/plugin-security-advanced.mdx` | Plugin Security Advanced | Plugin Security Advanced — Kernel Protocol | 42 / 56 | = before | | `references/kernel/plugin-security.mdx` | Plugin Security | Plugin Security — Kernel Protocol reference | 43 / 57 | = before | | `references/kernel/plugin-structure.mdx` | Plugin Structure | Plugin Structure — Kernel Protocol reference | 44 / 58 | = before | | `references/kernel/plugin-validator.mdx` | Plugin Validator | Plugin Validator — Kernel Protocol reference | 44 / 58 | = before | | `references/kernel/plugin-versioning.mdx` | Plugin Versioning | Plugin Versioning — Kernel Protocol reference | 45 / 59 | = before | | `references/kernel/plugin.mdx` | Plugin | Plugin schema — Kernel Protocol reference | 41 / 55 | = before | | `references/kernel/service-registry.mdx` | Service Registry | Service Registry — Kernel Protocol reference | 44 / 58 | = before | | `references/kernel/startup-orchestrator.mdx` | Startup Orchestrator | Startup Orchestrator — Kernel Protocol | 38 / 52 | = before | | `references/marketplace/index.mdx` | Marketplace Protocol | Marketplace Protocol — schema reference | 39 / 53 | = before | | `references/marketplace/marketplace.mdx` | Marketplace | Marketplace — Marketplace Protocol reference | 44 / 58 | = before | | `references/marketplace/package-version.mdx` | Package Version | Package Version — Marketplace Protocol | 38 / 52 | = before | | `references/marketplace/package.mdx` | Package | Package — Marketplace Protocol reference | 40 / 54 | = before | | `references/marketplace/template-manifest.mdx` | Template Manifest | Template Manifest — Marketplace Protocol | 40 / 54 | = before | | `references/qa/index.mdx` | QA Protocol | QA Protocol — complete schema reference | 39 / 53 | = before | | `references/qa/testing.mdx` | Testing | Testing schema — QA Protocol reference | 38 / 52 | = before | | `references/security/index.mdx` | Security Protocol | Security Protocol — complete schema reference | 45 / 59 | = before | | `references/security/explain.mdx` | Explain | Explain schema — Security Protocol reference | 44 / 58 | = before | | `references/security/misc.mdx` | Misc | Misc schema — Security Protocol reference | 41 / 55 | = before | | `references/security/permission.mdx` | Permission | Permission — Security Protocol reference | 40 / 54 | = before | | `references/security/rls.mdx` | Rls | Rls schema — Security Protocol reference | 40 / 54 | = before | | `references/security/sharing.mdx` | Sharing | Sharing schema — Security Protocol reference | 44 / 58 | = before | | `references/shared/index.mdx` | Shared Protocol | Shared Protocol — complete schema reference | 43 / 57 | = before | | `references/shared/duration.mdx` | Duration | Duration schema — Shared Protocol reference | 43 / 57 | = before | | `references/shared/enums.mdx` | Enums | Enums schema — Shared Protocol reference | 40 / 54 | = before | | `references/shared/epoch.mdx` | Epoch | Epoch schema — Shared Protocol reference | 40 / 54 | = before | | `references/shared/expression.mdx` | Expression | Expression schema — Shared Protocol reference | 45 / 59 | = before | | `references/shared/http.mdx` | Http | Http schema — Shared Protocol reference | 39 / 53 | = before | | `references/shared/identifiers.mdx` | Identifiers | Identifiers schema — Shared Protocol reference | 46 / 60 | = before | | `references/shared/mapping.mdx` | Mapping | Mapping schema — Shared Protocol reference | 42 / 56 | = before | | `references/shared/metadata-types.mdx` | Metadata Types | Metadata Types — Shared Protocol reference | 42 / 56 | = before | | `references/shared/protection.mdx` | Protection | Protection schema — Shared Protocol reference | 45 / 59 | = before | | `references/shared/value-domain.mdx` | Value Domain | Value Domain — Shared Protocol reference | 40 / 54 | = before | | `references/studio/index.mdx` | Studio Protocol | Studio Protocol — complete schema reference | 43 / 57 | = before | | `references/studio/flow-builder.mdx` | Flow Builder | Flow Builder — Studio Protocol reference | 40 / 54 | = before | | `references/studio/object-designer.mdx` | Object Designer | Object Designer — Studio Protocol reference | 43 / 57 | = before | | `references/studio/plugin.mdx` | Plugin | Plugin schema — Studio Protocol reference | 41 / 55 | = before | | `references/system/index.mdx` | System Protocol | System Protocol — complete schema reference | 43 / 57 | = before | | `references/system/app-install.mdx` | App Install | App Install schema — System Protocol reference | 46 / 60 | = before | | `references/system/auth-config.mdx` | Auth Config | Auth Config schema — System Protocol reference | 46 / 60 | = before | | `references/system/book.mdx` | Book | Book schema — System Protocol reference | 39 / 53 | = before | | `references/system/cache.mdx` | Cache | Cache schema — System Protocol reference | 40 / 54 | = before | | `references/system/collaboration.mdx` | Collaboration | Collaboration — System Protocol reference | 41 / 55 | = before | | `references/system/core-services.mdx` | Core Services | Core Services — System Protocol reference | 41 / 55 | = before | | `references/system/deploy-bundle.mdx` | Deploy Bundle | Deploy Bundle — System Protocol reference | 41 / 55 | = before | | `references/system/dev-login.mdx` | Dev Login | Dev Login schema — System Protocol reference | 44 / 58 | = before | | `references/system/disaster-recovery.mdx` | Disaster Recovery | Disaster Recovery — System Protocol reference | 45 / 59 | = before | | `references/system/doc.mdx` | Doc | Doc schema — System Protocol reference | 38 / 52 | = before | | `references/system/email-config.mdx` | Email Config | Email Config — System Protocol reference | 40 / 54 | = before | | `references/system/email-template.mdx` | Email Template | Email Template — System Protocol reference | 42 / 56 | = before | | `references/system/encryption.mdx` | Encryption | Encryption schema — System Protocol reference | 45 / 59 | = before | | `references/system/environment-artifact.mdx` | Environment Artifact | Environment Artifact — System Protocol | 38 / 52 | = before | | `references/system/http-server.mdx` | Http Server | Http Server schema — System Protocol reference | 46 / 60 | = before | | `references/system/job.mdx` | Job | Job schema — System Protocol reference | 38 / 52 | = before | | `references/system/license.mdx` | License | License schema — System Protocol reference | 42 / 56 | = before | | `references/system/logging.mdx` | Logging | Logging schema — System Protocol reference | 42 / 56 | = before | | `references/system/metadata-persistence.mdx` | Metadata Persistence | Metadata Persistence — System Protocol | 38 / 52 | = before | | `references/system/metrics.mdx` | Metrics | Metrics schema — System Protocol reference | 42 / 56 | = before | | `references/system/migration.mdx` | Migration | Migration schema — System Protocol reference | 44 / 58 | = before | | `references/system/notification.mdx` | Notification | Notification — System Protocol reference | 40 / 54 | = before | | `references/system/object-storage.mdx` | Object Storage | Object Storage — System Protocol reference | 42 / 56 | = before | | `references/system/registry-config.mdx` | Registry Config | Registry Config — System Protocol reference | 43 / 57 | = before | | `references/system/search-engine.mdx` | Search Engine | Search Engine — System Protocol reference | 41 / 55 | = before | | `references/system/security-context.mdx` | Security Context | Security Context — System Protocol reference | 44 / 58 | = before | | `references/system/settings-client.mdx` | Settings Client | Settings Client — System Protocol reference | 43 / 57 | = before | | `references/system/settings-manifest.mdx` | Settings Manifest | Settings Manifest — System Protocol reference | 45 / 59 | = before | | `references/system/stack-server.mdx` | Stack Server | Stack Server — System Protocol reference | 40 / 54 | = before | | `references/system/supplier-security.mdx` | Supplier Security | Supplier Security — System Protocol reference | 45 / 59 | = before | | `references/system/tenant.mdx` | Tenant | Tenant schema — System Protocol reference | 41 / 55 | = before | | `references/system/tracing.mdx` | Tracing | Tracing schema — System Protocol reference | 42 / 56 | = before | | `references/system/translation.mdx` | Translation | Translation schema — System Protocol reference | 46 / 60 | = before | | `references/system/worker.mdx` | Worker | Worker schema — System Protocol reference | 41 / 55 | = before | | `references/ui/index.mdx` | UI Protocol | UI Protocol — complete schema reference | 39 / 53 | = before | | `references/ui/action-params.mdx` | Action Params | Action Params schema — UI Protocol reference | 44 / 58 | = before | | `references/ui/action.mdx` | Action | Action schema — UI Protocol property reference | 46 / 60 | = before | | `references/ui/app.mdx` | App | App schema — UI Protocol property reference | 43 / 57 | = before | | `references/ui/bulk-action.mdx` | Bulk Action | Bulk Action schema — UI Protocol reference | 42 / 56 | = before | | `references/ui/chart.mdx` | Chart | Chart schema — UI Protocol property reference | 45 / 59 | = before | | `references/ui/component.mdx` | Component | Component schema — UI Protocol reference | 40 / 54 | = before | | `references/ui/dashboard.mdx` | Dashboard | Dashboard schema — UI Protocol reference | 40 / 54 | = before | | `references/ui/dataset.mdx` | Dataset | Dataset schema — UI Protocol reference | 38 / 52 | = before | | `references/ui/expression-bindable-text-keys.mdx` | Expression Bindable Text Keys | Expression Bindable Text Keys — UI Protocol | 43 / 57 | = before | | `references/ui/i18n.mdx` | I18n | I18n schema — UI Protocol property reference | 44 / 58 | = before | | `references/ui/notification.mdx` | Notification | Notification schema — UI Protocol reference | 43 / 57 | = before | | `references/ui/page.mdx` | Page | Page schema — UI Protocol property reference | 44 / 58 | = before | | `references/ui/report.mdx` | Report | Report schema — UI Protocol property reference | 46 / 60 | = before | | `references/ui/responsive.mdx` | Responsive | Responsive schema — UI Protocol reference | 41 / 55 | = before | | `references/ui/sharing.mdx` | Sharing | Sharing schema — UI Protocol reference | 38 / 52 | = before | | `references/ui/view.mdx` | View | View schema — UI Protocol property reference | 44 / 58 | = before | --- _Generated by [Claude Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12237
Clause-②: no
Summary
Applies the maintainer-approved page-title rule to the authored docs pages that still carried their pre-rule title, and keeps every navigation label exactly as it was through
navTitle.navTitle:line holding the page's previous title.check:role-word), and 1 authored page the table never listed authored per the rule. Both are listed under Needs maintainer voice review below.check:runtime-services-indexused to hold eachkernel/runtime-services/NAME-service.mdxtotitle: services.NAME. It now reads that accessor premise from the page'snavTitle, so the chapter's 8 approved titles land verbatim withnavTitle: services.NAME. See The runtime-services gate below. (PM answer to the round-1 open question, review comment5852831074on docs content: page titles carry no search intent — median 14 characters, 325 of 403 under 20 #12237.)+2/-1(title:replaced,navTitle:added). Nodescription:, no page body, nothing underreferences/**orreleases/**, nothing inapps/docs/**or anymeta.json. Plus one gate script,scripts/check-runtime-services-index.mjs(file surface amended by the seat for this round).⛔ Draft, and it stays open for maintainer review — do not self-merge (card body). The landing path after review is the seat's call.
The rule
title:string is 36–46 characters, so the rendered title with the 14-character| ObjectStacksuffix lands in 50–60; no rendered title over 60. (PM ruling on the card, comment5414218147, Q2: the band is authoritative.)—throughout;ObjectStacknever inside a title, because the suffix carries it; one declared exception,getting-started/index.mdxkeepsWhat is ObjectStack?. (Maintainer ruling recorded in comment5419555720, verbatim: 「其他同意」 — the rule and the full before/after table in PR docs(content): propose a search-intent title rule, land it on the four pages with no sidebar cost #12312 approved as they stand.)domain:spec), per the triage split in comment5541938216.navTitle(landed with PR docs(site): give a doc page a short sidebar label (navTitle) distinct from its title #14055). The fallback totitlelives only innavLabel()inapps/docs/lib/nav-title.ts.What changes on the site, and what does not
Measured in
apps/docsat this head. A page'stitleis read at 12 read points on 5 faces, plus the site search index. All of these take the longer title:app/[lang]/docs/[[...slug]]/page.tsxgenerateMetadata:title(232),openGraph.title(237), OG imagealt(248),twitter.title(254)page.tsx:171DocsTitleTechArticleheadlineandname(page.tsx:144,145); theBreadcrumbListleaf crumb (page.tsx:116)llms.txt/llms-full.txt//llms.mdx/docs/…app/llms.txt/route.ts:10;lib/source.ts:66getLLMTextapp/og/docs/[...slug]/route.tsx:18app/api/search/route.ts,createFromSource(source)(fumadocs indexespage.data.title)Unchanged, by construction: the page tree, i.e. the sidebar entries and the footer previous/next links. Every page this PR lengthens is a page-tree node, and each one declares
navTitle:with its previous title verbatim (same YAML quoting), sonavLabel()returns the old string.How "page-tree node" was measured, not guessed: replaying fumadocs-core 16.14.4
buildFolder()over everymeta.json(all 19 authoredmeta.jsonlistpagesexplicitly, none uses a rest entry, none listsindex) gives 180 of 181 authored pages in the tree: 162 sidebar leaves and 18 folder index pages. A folder index page is not a sidebar row (its folder label comes frommeta.jsontitle), butflattenTree()putsfolder.indexinto the footer previous/next list, so it needsnavTitletoo. The one authored page outside the tree iscontent/docs/index.mdx, the global root (landed by #12312, not touched here).The JSON-LD breadcrumb does not pick up the short label:
docsTrail()callsgetBreadcrumbItems(…, { includePage: false })and pushespage.data.titleitself, pinned by leg D ofscripts/check-docs-nav-label.mjs(run green below).Statistics, re-derived
Population: every
content/docs/**/*.mdxoutsidereferences/**andreleases/**, 181 pages. Rendered length =title+ 14. Base84880f9, headc69d5c7.navTitledeclaredDuplicates were also checked against the 225 generated
references/**+releases/**titles: no new title collides with any page.Premise check on the approved table (PR #12312 body, section 6): it parses to 180 rows — 4 landed with #12312 and 176 not landed. On base
84880f9, 176 of 176 pages still carry exactly the table's before, 0 carry its after, 0 paths are missing. The generated-page test reads the file head for a marker, never prose: no authored page carries one (the nine prose hits of the word "generated" in a page head, e.g.api/index.mdx,ui/index.mdx, are body text). One authored page is absent from the table:permissions/tenant-audit-census.mdx(added after #12312; its frontmatter is authored, only a body region is generated byscripts/tenant-audit-census.mjs).Needs maintainer voice review — not covered by the approved table
Every row here is applied in this diff.
permissions/administrator-guide.mdxcheck:role-wordgate: "role" is a reserved word (ADR-0090 D3). Smallest change that clears it.permissions/tenant-audit-census.mdxnavTitle: Tenant-Audit Census.The runtime-services gate
scripts/check-runtime-services-index.mjschecks first that each chapter page declares the accessor its filename claims; the other five enumerations rest on that premise. It used to readtitle. The title rule lengthenstitlefor the search-facing surfaces, so the premise now readsnavTitle, which is the page-tree label and, on these 8 pages, the bare accessor.titleis not read at all.navTitleabsent, blank, present only in the body, or notservices.NAME. A page that still saystitle: services.NAMEwith nonavTitleis red too. Each finding names the fix (add \navTitle: services.sms` to the page's leading frontmatter block`).title:premise is rewritten to say what the gate now does and why.--self-test: new floored battery The accessor premise lives in navTitle, 7 cases (absent, wrong, old title-only spelling, body-only line and blank are red; a long title and a quoted value are green). Roster floor 8 to 9. The fixture's defaulttitleis now a long search-intent string, so every existing green case is also a green with a long title. 59 assertions.scripts/ablation-replace.mjssetnavTitle: services.smstonavTitle: services.textinsms-service.mdx(anchor 1 to 0, blob2001694b2859to67f9af393d4d). The gate exited 1:frontmatter navTitle is "services.text", expected "services.sms" to match the filename -- set \navTitle: services.sms`, or rename the page if the accessor really changed. Restored under an EXIT/INT/TERM trap to blob2001694b2859, which equals HEAD;git diff HEAD` empty.declares no navTitlefindings.Other readers of these pages'
title(git grepoverscripts/,packages/,apps/docs,.github/,content/docs): no other gate or script parses it.apps/docsreads it generically throughpage.data.title, which is the intended change.content/docs/kernel/index.mdxnames the accessors in its services table and does not read the page titles. One piece of stale prose: the header ofscripts/check-docs-single-h1.mjs(lines 72-73) usesaudit-service.mdxtitle: services.auditas a worked example. Its logic is unaffected and it is green, but the example no longer matches the page. That file is outside this PR's surface, so it is noted here and not edited.The complete table
All 177 authored pages this PR changes. Rendered =
title+| ObjectStack.navTitle= the page's previous title, declared verbatim. ✳️ = in the review section above.navTitleai/actions-as-tools.mdxai/agents.mdxai/connect-mcp.mdxai/index.mdxai/knowledge-rag.mdxai/natural-language-queries.mdxai/skills-reference.mdxai/skills.mdxai/tools.mdxapi/client-sdk.mdxapi/data-api.mdxapi/data-flow.mdxapi/declarative-endpoints.mdxapi/environment-routing.mdxapi/error-catalog.mdxapi/error-handling-client.mdxapi/error-handling-server.mdxapi/index.mdxapi/metadata-api.mdxapi/plugin-endpoints.mdxapi/wire-format.mdxautomation/approvals.mdxautomation/connectors.mdxautomation/email-templates.mdxautomation/flows.mdxautomation/hook-bodies.mdxautomation/hooks.mdxautomation/index.mdxautomation/jobs.mdxautomation/webhooks.mdxautomation/workflows.mdxbuild-without-code.mdxcapabilities/ai.mdxcapabilities/analytics.mdxcapabilities/approvals.mdxcapabilities/automation.mdxcapabilities/data.mdxcapabilities/forms.mdxcapabilities/index.mdxcapabilities/integrations.mdxcapabilities/permissions.mdxcapabilities/request-template.mdxcapabilities/views.mdxconcepts/architecture.mdxconcepts/design-principles.mdxconcepts/index.mdxconcepts/metadata-driven.mdxconcepts/metadata-lifecycle.mdxconcepts/north-star.mdxdata-modeling/analytics.mdxdata-modeling/drivers.mdxdata-modeling/external-datasources.mdxdata-modeling/field-type-decision-tree.mdxdata-modeling/field-types.mdxdata-modeling/fields.mdxdata-modeling/formulas.mdxdata-modeling/import-mappings.mdxdata-modeling/index.mdxdata-modeling/indexing.mdxdata-modeling/object-extensions.mdxdata-modeling/objects.mdxdata-modeling/queries.mdxdata-modeling/relationships.mdxdata-modeling/schema-design.mdxdata-modeling/seed-data.mdxdata-modeling/validation-rules.mdxdata-modeling/validation.mdxdeployment/backup-restore.mdxdeployment/cli.mdxdeployment/environment-variables.mdxdeployment/index.mdxdeployment/production-readiness.mdxdeployment/publish-and-preview.mdxdeployment/seed-tenancy-repair.mdxdeployment/self-hosting.mdxdeployment/single-project-mode.mdxdeployment/tenancy-modes.mdxdeployment/troubleshooting.mdxdeployment/validating-metadata.mdxgetting-started/build-with-claude-code.mdxgetting-started/common-patterns.mdxgetting-started/examples.mdxgetting-started/glossary.mdxgetting-started/how-ai-development-works.mdxgetting-started/index.mdxgetting-started/quick-reference.mdxgetting-started/quick-start.mdxgetting-started/your-first-project.mdxkernel/architecture.mdxkernel/cluster.mdxkernel/contracts/auth-service.mdxkernel/contracts/cache-service.mdxkernel/contracts/data-engine.mdxkernel/contracts/index.mdxkernel/contracts/metadata-service.mdxkernel/contracts/storage-service.mdxkernel/events.mdxkernel/index.mdxkernel/runtime-services/audit-service.mdxkernel/runtime-services/data-service.mdxkernel/runtime-services/email-service.mdxkernel/runtime-services/examples.mdxkernel/runtime-services/index.mdxkernel/runtime-services/queue-service.mdxkernel/runtime-services/settings-service.mdxkernel/runtime-services/sharing-service.mdxkernel/runtime-services/sms-service.mdxkernel/runtime-services/storage-service.mdxkernel/runtime-services/versioning.mdxkernel/services-checklist.mdxkernel/services.mdxpermissions/access-matrix.mdxpermissions/access-recipes.mdxpermissions/administrator-guide.mdx✳️permissions/attachments-access.mdxpermissions/authentication.mdxpermissions/authorization.mdxpermissions/capabilities.mdxpermissions/delegated-administration.mdxpermissions/explain.mdxpermissions/field-level-security.mdxpermissions/index.mdxpermissions/permission-metadata.mdxpermissions/permission-sets.mdxpermissions/permissions-matrix.mdxpermissions/positions.mdxpermissions/profiles.mdxpermissions/record-view-auditing.mdxpermissions/rls.mdxpermissions/sharing-rules.mdxpermissions/sso.mdxpermissions/system-context.mdxpermissions/tenant-audit-census.mdx✳️plugins/adding-a-metadata-type.mdxplugins/anatomy.mdxplugins/development.mdxplugins/index.mdxplugins/packages.mdxprotocol/backward-compatibility.mdxprotocol/diagram.mdxprotocol/index.mdxprotocol/kernel/config-resolution.mdxprotocol/kernel/error-handling.mdxprotocol/kernel/http-protocol.mdxprotocol/kernel/i18n-standard.mdxprotocol/kernel/index.mdxprotocol/kernel/lifecycle.mdxprotocol/kernel/metadata-service.mdxprotocol/kernel/plugin-spec.mdxprotocol/kernel/realtime-protocol.mdxprotocol/knowledge.mdxprotocol/objectql/query-syntax.mdxprotocol/objectql/schema.mdxprotocol/objectql/security.mdxprotocol/objectql/state-machine.mdxprotocol/objectql/types.mdxprotocol/objectui/actions.mdxprotocol/objectui/concept.mdxprotocol/objectui/layout-dsl.mdxprotocol/objectui/widget-contract.mdxui/actions.mdxui/apps.mdxui/audience-based-interfaces.mdxui/create-vs-edit-form.mdxui/dashboards.mdxui/doc-pages.mdxui/field-grouping-and-order.mdxui/forms.mdxui/index.mdxui/pages.mdxui/public-data-collection.mdxui/react-pages.mdxui/reports.mdxui/setup-app.mdxui/translations.mdxui/views.mdxupgrading.mdxVerification
All at head
c69d5c7,git status --porcelainempty.origin/mainwas merged in twice during the rework round (now at merge basee2c4e12); neither merge touched this PR's files.Gate list derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(178 paths vs merge basee2c4e12, three-dot; 640 changed lines, under the 5,000 human-merge threshold), then reconciled:--rananswered74 derived famil(ies) accounted for — 74 run, 0 NOT-MEASURED (a DERIVED zero — all 74 recorded an exit code and none of them is 3). Each command's exit code was captured before any pipe:Verdict lines:
check-doc-anchors: 376 internal #fragment link(s) across 411 source file(s) all resolve to a real heading(the card's acceptance gate)check-doc-frontmatter: 2 content root(s) verified, each against its own floor — content/docs 406, content/blog 3.check-docs-nav-label:navTitlenamed in code by 2 file(s) only; resolver executed; plugin wired into the docs loader; JSON-LD breadcrumb leg green. It readsapps/docs/**only, so anavTitle:frontmatter line is not judged by it; run as a guard that the mechanism this PR relies on is intact.check-runtime-services-index --self-test: 59 assertions …andcheck-runtime-services-index: 8 chapter page(s) vs meta.json "pages" …both green, with the 8 approved titles applied.For the record, round 1: on the verbatim table
check:role-wordandcheck:runtime-services-indexexited 1. Both were real findings: the first is handled by the re-worded row, the second by the gate change above.check:skill-examplesexited 3 (PREREQUISITE NOT MET, staleclient-reactdeclarations, nothing measured) until@objectstack/client-reactwas built. All green at this head.NOT MEASURED locally: the card's "link checker" is
.github/workflows/check-links.yml(lychee, advisory lane); lychee is not installed in this container. It runs on this PR in CI. This diff changes no link, link text or file path, only two frontmatter keys. A fullnext buildofapps/docswas not run locally;fumadocs-mdxregenerated the source index at exit 0, andcheck-doc-frontmatterparses every page with theyamlversion the build resolves.pnpm lintis not owed:.mdxis outside the ESLint population and no other file type is touched.No changeset: docs content only, publishes nothing.
skip-changesetapplied.Acceptance notes
—separator (for exampleai/natural-language-queries.mdx→Natural language queries over your data), although the ruling says "separator—throughout". The ruling approves the table "as they stand", so they are applied verbatim (PM answer in review comment5852831074); re-wording them is the maintainer's call.protocol/objectql/index.mdxandprotocol/objectui/index.mdxcarry nonavTitle, andflattenTree()puts a folder index into that list. docs(content): propose a search-intent title rule, land it on the four pages with no sidebar cost #12312 judged them "not in the page tree /meta.jsontitle wins", which holds for the sidebar only. Left as they are, per the PM answer in review comment5852831074.<h1>— 129 of them the same text twice #12236 trap note (76 demoted body H1s as a mapping-table input) is moot for the applied rows: the table was approved as it stands.references/**pages (e.g.Mapping,Sharing,Plugin); that is the generator half, docs(spec): carry the page-title rule into the docs GENERATOR — 225 of 405 pages are emitted, so a hand edit is reverted (split (b) of #12237) #15403.Generated by Claude Code