Repository navigation
docs(site): give a doc page a short sidebar label (navTitle) distinct from its title - #14055
Merged
Merged
Conversation
4 tasks
os-support-ai
marked this pull request as ready for review
September 1, 2026 01:54
os-support-ai
enabled auto-merge
September 1, 2026 01:54
Closed
4 tasks
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…hored pages, short nav labels kept via navTitle (objectstack-ai#20170) Fixes objectstack-ai#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`. - **175 approved rows applied verbatim** from the table in PR objectstack-ai#12312, each with a `navTitle:` line holding the page's previous title. - **1 approved row re-worded** because a required gate rejects the approved text (`check:role-word`), and **1 authored page the table never listed** authored per the rule. Both are listed under *Needs maintainer voice review* below. - **One gate premise moved, not loosened:** `check:runtime-services-index` used to hold each `kernel/runtime-services/NAME-service.mdx` to `title: services.NAME`. It now reads that accessor premise from the page's `navTitle`, so the chapter's 8 approved titles land verbatim with `navTitle: services.NAME`. See *The runtime-services gate* below. (PM answer to the round-1 open question, review comment `5852831074` on objectstack-ai#12237.) - 177 docs files, frontmatter only: every one is `+2/-1` (`title:` replaced, `navTitle:` added). No `description:`, no page body, nothing under `references/**` or `releases/**`, nothing in `apps/docs/**` or any `meta.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 ``` PRIMARY-KEYWORD — QUALIFIER ``` - The frontmatter `title:` string is **36–46 characters**, so the rendered title with the 14-character ` | ObjectStack` suffix lands in **50–60**; no rendered title over 60. (PM ruling on the card, comment `5414218147`, Q2: the band is authoritative.) - Separator ` — ` throughout; `ObjectStack` never inside a title, because the suffix carries it; one declared exception, `getting-started/index.mdx` keeps `What is ObjectStack?`. (Maintainer ruling recorded in comment `5419555720`, verbatim: 「其他同意」 — the rule and the full before/after table in PR objectstack-ai#12312 approved as they stand.) - Scope: authored pages only. The generator-emitted pages are objectstack-ai#15403 (`domain:spec`), per the triage split in comment `5541938216`. - Short navigation labels: `navTitle` (landed with PR objectstack-ai#14055). The fallback to `title` lives only in `navLabel()` in `apps/docs/lib/nav-title.ts`. ## What changes on the site, and what does not Measured in `apps/docs` at this head. A page's `title` is read at **12 read points on 5 faces**, plus the site search index. All of these take the longer title: | face | read points | |---|---| | SERP / Open Graph / Twitter metadata | `app/[lang]/docs/[[...slug]]/page.tsx` `generateMetadata`: `title` (232), `openGraph.title` (237), OG image `alt` (248), `twitter.title` (254) | | on-page h1 | `page.tsx:171` `DocsTitle` | | JSON-LD | `TechArticle` `headline` and `name` (`page.tsx:144,145`); the `BreadcrumbList` leaf crumb (`page.tsx:116`) | | `llms.txt` / `llms-full.txt` / `/llms.mdx/docs/…` | `app/llms.txt/route.ts:10`; `lib/source.ts:66` `getLLMText` | | Open Graph card image | `app/og/docs/[...slug]/route.tsx:18` | | site search results | `app/api/search/route.ts`, `createFromSource(source)` (fumadocs indexes `page.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), so `navLabel()` returns the old string. How "page-tree node" was measured, not guessed: replaying fumadocs-core 16.14.4 `buildFolder()` over every `meta.json` (all 19 authored `meta.json` list `pages` explicitly, none uses a rest entry, none lists `index`) 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 from `meta.json` `title`), but `flattenTree()` puts `folder.index` into the footer previous/next list, so it needs `navTitle` too. The one authored page outside the tree is `content/docs/index.mdx`, the global root (landed by objectstack-ai#12312, not touched here). The JSON-LD breadcrumb does not pick up the short label: `docsTrail()` calls `getBreadcrumbItems(…, { includePage: false })` and pushes `page.data.title` itself, pinned by leg D of `scripts/check-docs-nav-label.mjs` (run green below). ## Statistics, re-derived Population: every `content/docs/**/*.mdx` outside `references/**` and `releases/**`, **181 pages**. Rendered length = `title` + 14. Base `84880f9`, head `c69d5c7`. | | pages | median | min | max | in 50–60 | over 60 | under 50 | duplicate titles | `navTitle` declared | |---|--:|--:|--:|--:|--:|--:|--:|--:|--:| | before | 181 | 32 | 19 | 56 | 6 | 0 | 175 | 0 | 0 | | after | 181 | 54 | 50 | 60 | 181 | 0 | 0 | 0 | 177 | Duplicates were also checked against the 225 generated `references/**` + `releases/**` titles: no new title collides with any page. **Premise check on the approved table** (PR objectstack-ai#12312 body, section 6): it parses to **180 rows** — 4 landed with objectstack-ai#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 objectstack-ai#12312; its frontmatter is authored, only a body region is generated by `scripts/tenant-audit-census.mjs`). ## Needs maintainer voice review — not covered by the approved table Every row here is **applied in this diff**. | page | before | approved *after* | applied | rendered | why | |---|---|---|---|--:|---| | `permissions/administrator-guide.mdx` | Administrator Guide | Administrator guide to permissions and roles | **Administrator guide to permissions and access** | 59 | the approved text fails the required `check:role-word` gate: "role" is a reserved word (ADR-0090 D3). Smallest change that clears it. | | `permissions/tenant-audit-census.mdx` | Tenant-Audit Census | — (page not in the table) | **Tenant-audit census — tenancy write call sites** | 60 | authored page the table never listed; written per the rule. `navTitle: Tenant-Audit Census`. | ## The runtime-services gate `scripts/check-runtime-services-index.mjs` checks first that each chapter page declares the accessor its filename claims; the other five enumerations rest on that premise. It used to read `title`. The title rule lengthens `title` for the search-facing surfaces, so the premise now reads `navTitle`, which is the page-tree label and, on these 8 pages, the bare accessor. - Read from the page's **leading frontmatter block only**; one layer of matching YAML quotes is syntax, not value. The `title` is not read at all. - Still red: `navTitle` absent, blank, present only in the body, or not `services.NAME`. A page that still says `title: services.NAME` with no `navTitle` is red too. Each finding names the fix (`add \`navTitle: services.sms\` to the page's leading frontmatter block`). - The header prose that stated the `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 default `title` is now a long search-intent string, so every existing green case is also a green with a long title. 59 assertions. - **Ablation, trapped:** `scripts/ablation-replace.mjs` set `navTitle: services.sms` to `navTitle: services.text` in `sms-service.mdx` (anchor 1 to 0, blob `2001694b2859` to `67f9af393d4d`). 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 blob `2001694b2859`, which equals HEAD; `git diff HEAD` empty. - The live tree before the 8 pages were edited: the new gate went red with exactly 8 `declares no navTitle` findings. **Other readers of these pages' `title`** (`git grep` over `scripts/`, `packages/`, `apps/docs`, `.github/`, `content/docs`): no other gate or script parses it. `apps/docs` reads it generically through `page.data.title`, which is the intended change. `content/docs/kernel/index.mdx` names the accessors in its services table and does not read the page titles. One piece of stale prose: the header of `scripts/check-docs-single-h1.mjs` (lines 72-73) uses `audit-service.mdx` `title: services.audit` as 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. | page | before | after | rendered | `navTitle` | |---|---|---|--:|:-:| | `ai/actions-as-tools.mdx` | Actions as Tools | MCP tools — expose actions to AI agents | 53 | yes | | `ai/agents.mdx` | AI Agents | AI agents — declare tools, model and prompt | 57 | yes | | `ai/connect-mcp.mdx` | Connect an MCP Client | MCP client setup — Claude, Cursor and IDEs | 56 | yes | | `ai/index.mdx` | AI Overview | AI features — MCP tools, agents and RAG | 53 | yes | | `ai/knowledge-rag.mdx` | Knowledge & RAG | RAG — embeddings and knowledge retrieval | 54 | yes | | `ai/natural-language-queries.mdx` | Natural Language Queries | Natural language queries over your data | 53 | yes | | `ai/skills-reference.mdx` | AI Skills Reference | AI skill reference — every field explained | 56 | yes | | `ai/skills.mdx` | AI Skills System | AI skills — reusable instructions for agents | 58 | yes | | `ai/tools.mdx` | Tool Records | Tool records — govern what an agent may call | 58 | yes | | `api/client-sdk.mdx` | Client SDK | Client SDK — typed JavaScript data access | 55 | yes | | `api/data-api.mdx` | Data API | REST data API — CRUD over every object | 52 | yes | | `api/data-flow.mdx` | Data Flow Diagrams | Request data flow — from HTTP to driver | 53 | yes | | `api/declarative-endpoints.mdx` | Declarative Endpoints | Custom endpoints declared as metadata | 51 | yes | | `api/environment-routing.mdx` | Environment-Scoped Routing | Environment routing — dev, preview, prod | 54 | yes | | `api/error-catalog.mdx` | Error Code Catalog | API error codes — the complete catalog | 52 | yes | | `api/error-handling-client.mdx` | Client-Side Error Handling | Client error handling — retries and codes | 55 | yes | | `api/error-handling-server.mdx` | Server-Side Error Handling | Server error handling — throw the envelope | 56 | yes | | `api/index.mdx` | API Overview | REST and GraphQL APIs — generated per object | 58 | yes | | `api/metadata-api.mdx` | Metadata & Package API | Metadata API — read and publish packages | 54 | yes | | `api/plugin-endpoints.mdx` | Plugin Endpoints | Plugin endpoints — add routes from a plugin | 57 | yes | | `api/wire-format.mdx` | Wire Format & JSON Examples | API wire format — request and response JSON | 57 | yes | | `automation/approvals.mdx` | Approval workflow | Approval chains — multi-step sign-off rules | 57 | yes | | `automation/connectors.mdx` | Connectors | Connectors — call external systems safely | 55 | yes | | `automation/email-templates.mdx` | Email Templates | Email templates — merge fields and layouts | 56 | yes | | `automation/flows.mdx` | Flow Metadata | Flows — DAG automation as typed metadata | 54 | yes | | `automation/hook-bodies.mdx` | Hook & Action Bodies (L1 / L2) | Hook and action bodies — the L1/L2 rules | 54 | yes | | `automation/hooks.mdx` | Hooks | Record hooks — beforeInsert to afterDelete | 56 | yes | | `automation/index.mdx` | Automation | Automation — flows, triggers and schedules | 56 | yes | | `automation/jobs.mdx` | Scheduled Jobs | Scheduled jobs — cron automation metadata | 55 | yes | | `automation/webhooks.mdx` | Webhook Delivery | Webhooks — outbound delivery and retries | 54 | yes | | `automation/workflows.mdx` | Workflow Metadata | Workflow rules — declarative record logic | 55 | yes | | `build-without-code.mdx` | Build Without Code | Build business apps without writing code | 54 | yes | | `capabilities/ai.mdx` | AI Under Governance | AI under governance — permissioned agents | 55 | yes | | `capabilities/analytics.mdx` | Analytics & Dashboards | Analytics — dashboards, reports and charts | 56 | yes | | `capabilities/approvals.mdx` | Approvals | Approvals — routing, queues and audit trail | 57 | yes | | `capabilities/automation.mdx` | Automation — Processes That Run Themselves | Automation — processes that run themselves | 56 | yes | | `capabilities/data.mdx` | Manage Business Data | Business data — objects, fields and records | 57 | yes | | `capabilities/forms.mdx` | Forms & Data Quality | Forms and data quality — validation rules | 55 | yes | | `capabilities/index.mdx` | What Can It Do? | What can it do? — the capability overview | 55 | yes | | `capabilities/integrations.mdx` | Integrations & Everyday Work | Integrations — email, files and everyday work | 59 | yes | | `capabilities/permissions.mdx` | Permissions — Who Sees What | Permissions — who sees what, enforced | 51 | yes | | `capabilities/request-template.mdx` | How to Request Features | Request a feature — how to describe it | 52 | yes | | `capabilities/views.mdx` | Views — See Data Your Way | Views — lists, kanban, calendar and gantt | 55 | yes | | `concepts/architecture.mdx` | Protocol Architecture | Protocol architecture — the four layers | 53 | yes | | `concepts/design-principles.mdx` | Design Principles | Design principles behind the metadata spec | 56 | yes | | `concepts/index.mdx` | Core Concepts | Core concepts — metadata, runtime, protocol | 57 | yes | | `concepts/metadata-driven.mdx` | Metadata-Driven Development | Metadata-driven development explained | 51 | yes | | `concepts/metadata-lifecycle.mdx` | Metadata Lifecycle & HMR | Metadata lifecycle — load, publish and HMR | 56 | yes | | `concepts/north-star.mdx` | North Star | North star — why this framework exists | 52 | yes | | `data-modeling/analytics.mdx` | Analytics Datasets | Analytics datasets — modelling for reports | 56 | yes | | `data-modeling/drivers.mdx` | Database Drivers | Database drivers — Postgres, MySQL, Mongo | 55 | yes | | `data-modeling/external-datasources.mdx` | External Datasources (Federation) | External datasources — query without ETL | 54 | yes | | `data-modeling/field-type-decision-tree.mdx` | Field Type Decision Tree | Which field type should I use? A decision tree | 60 | yes | | `data-modeling/field-types.mdx` | Field Type Gallery | Field types — the complete visual gallery | 55 | yes | | `data-modeling/fields.mdx` | Field Metadata | Field metadata — every option explained | 53 | yes | | `data-modeling/formulas.mdx` | Expressions (CEL) | Formulas — CEL expressions on records | 51 | yes | | `data-modeling/import-mappings.mdx` | Import Mappings | Import mappings — load external data files | 56 | yes | | `data-modeling/index.mdx` | Data Modeling | Data modelling — objects, fields, relations | 57 | yes | | `data-modeling/indexing.mdx` | Database Indexing | Database indexes — declare and tune them | 54 | yes | | `data-modeling/object-extensions.mdx` | Object Extensions | Object extensions — extend without forking | 56 | yes | | `data-modeling/objects.mdx` | Object Metadata | Object metadata — define your data schema | 55 | yes | | `data-modeling/queries.mdx` | Query Syntax Cheat Sheet | ObjectQL query syntax — a cheat sheet | 51 | yes | | `data-modeling/relationships.mdx` | Relationships & Lookups | Relationships — lookups, master-detail | 52 | yes | | `data-modeling/schema-design.mdx` | Schema Design | Schema design — model a business domain | 53 | yes | | `data-modeling/seed-data.mdx` | Seed Data & Fixtures | Seed data — fixtures and demo datasets | 52 | yes | | `data-modeling/validation-rules.mdx` | Field Validation Rules | Validation rules — reject bad records | 51 | yes | | `data-modeling/validation.mdx` | Validation Metadata | Validation metadata — declare the checks | 54 | yes | | `deployment/backup-restore.mdx` | Backup & Restore | Backup and restore — protect tenant data | 54 | yes | | `deployment/cli.mdx` | Command Line Interface | Command line interface — the os CLI reference | 59 | yes | | `deployment/environment-variables.mdx` | Environment Variables | Environment variables — the full list | 51 | yes | | `deployment/index.mdx` | Deployment Overview | Deployment — ship a runtime to production | 55 | yes | | `deployment/production-readiness.mdx` | Production Readiness | Production readiness — the go-live list | 53 | yes | | `deployment/publish-and-preview.mdx` | Publish, Versioning & Preview | Publish, versioning and preview builds | 52 | yes | | `deployment/seed-tenancy-repair.mdx` | Seed Tenancy Repair | Repair seed tenancy after a bad import | 52 | yes | | `deployment/self-hosting.mdx` | Self-Hosted Deployment | Self-hosting — run your own deployment | 52 | yes | | `deployment/single-project-mode.mdx` | Single-Environment Mode | Single-environment mode — the simple setup | 56 | yes | | `deployment/tenancy-modes.mdx` | Tenancy Postures & Membership | Tenancy postures and membership models | 52 | yes | | `deployment/troubleshooting.mdx` | Troubleshooting & FAQ | Troubleshooting — common errors and fixes | 55 | yes | | `deployment/validating-metadata.mdx` | Validating Metadata | Validate metadata before you deploy it | 52 | yes | | `getting-started/build-with-claude-code.mdx` | Build with Claude Code | Build an app with Claude Code, step by step | 57 | yes | | `getting-started/common-patterns.mdx` | Common Patterns | Common patterns — proven metadata recipes | 55 | yes | | `getting-started/examples.mdx` | Example Apps | Example apps — CRM, showcase and more | 51 | yes | | `getting-started/glossary.mdx` | Glossary | Glossary — every metadata term defined | 52 | yes | | `getting-started/how-ai-development-works.mdx` | How AI Development Works | How AI-written app development works | 50 | yes | | `getting-started/index.mdx` | What is ObjectStack? | What is ObjectStack? — a 5-minute intro | 53 | yes | | `getting-started/quick-reference.mdx` | Quick Reference Guide | Quick reference — every metadata type | 51 | yes | | `getting-started/quick-start.mdx` | Anatomy of an ObjectStack App | Anatomy of an app — your first metadata | 53 | yes | | `getting-started/your-first-project.mdx` | Your First Project | Your first project — from zero to running | 55 | yes | | `kernel/architecture.mdx` | Architecture | Core architecture — kernel and services | 53 | yes | | `kernel/cluster.mdx` | Cluster Semantics | Cluster semantics — multi-node runtimes | 53 | yes | | `kernel/contracts/auth-service.mdx` | IAuthService Contract | IAuthService — the authentication contract | 56 | yes | | `kernel/contracts/cache-service.mdx` | ICacheService Contract | ICacheService — the cache service contract | 56 | yes | | `kernel/contracts/data-engine.mdx` | IDataEngine Contract | IDataEngine — the storage driver contract | 55 | yes | | `kernel/contracts/index.mdx` | Service Contracts Overview | Service contracts — the kernel interfaces | 55 | yes | | `kernel/contracts/metadata-service.mdx` | IMetadataService Contract | IMetadataService — the metadata contract | 54 | yes | | `kernel/contracts/storage-service.mdx` | IStorageService Contract | IStorageService — the file storage contract | 57 | yes | | `kernel/events.mdx` | Events & Hooks | Kernel events and hooks — the full list | 53 | yes | | `kernel/index.mdx` | Kernel & Services | Kernel and services — the runtime core | 52 | yes | | `kernel/runtime-services/audit-service.mdx` | services.audit | services.audit — the audit log service API | 56 | yes | | `kernel/runtime-services/data-service.mdx` | services.data | services.data — the record CRUD service API | 57 | yes | | `kernel/runtime-services/email-service.mdx` | services.email | services.email — the outbound mail API | 52 | yes | | `kernel/runtime-services/examples.mdx` | Runtime Service Examples | Runtime service examples — copyable code | 54 | yes | | `kernel/runtime-services/index.mdx` | Runtime Service APIs | Runtime service APIs — the services object | 56 | yes | | `kernel/runtime-services/queue-service.mdx` | services.queue | services.queue — the job queue service API | 56 | yes | | `kernel/runtime-services/settings-service.mdx` | services.settings | services.settings — the settings API | 50 | yes | | `kernel/runtime-services/sharing-service.mdx` | services.sharing | services.sharing — the record share API | 53 | yes | | `kernel/runtime-services/sms-service.mdx` | services.sms | services.sms — the text message service API | 57 | yes | | `kernel/runtime-services/storage-service.mdx` | services.storage | services.storage — the file store API | 51 | yes | | `kernel/runtime-services/versioning.mdx` | Runtime Service API Versioning | Runtime service API versioning policy | 51 | yes | | `kernel/services-checklist.mdx` | Kernel Services Checklist | Kernel services checklist for reviewers | 53 | yes | | `kernel/services.mdx` | Service Registry | Service registry — resolve and override | 53 | yes | | `permissions/access-matrix.mdx` | Access-Matrix Snapshot Gate | Access matrix snapshot gate explained | 51 | yes | | `permissions/access-recipes.mdx` | Who can see data / automation / interface | Access recipes — data, automation and UI | 54 | yes | | `permissions/administrator-guide.mdx` ✳️ | Administrator Guide | Administrator guide to permissions and access | 59 | yes | | `permissions/attachments-access.mdx` | Attachments Access | Attachment access — who can read a file | 53 | yes | | `permissions/authentication.mdx` | Authentication | Authentication — sessions, tokens, SSO | 52 | yes | | `permissions/authorization.mdx` | Authorization Architecture | Authorization architecture — how it decides | 57 | yes | | `permissions/capabilities.mdx` | Declaring Capabilities | Declaring capabilities in a permission set | 56 | yes | | `permissions/delegated-administration.mdx` | Delegated Administration | Delegated administration — scoped admin rights | 60 | yes | | `permissions/explain.mdx` | Explain Engine | Explain engine — why access was denied | 52 | yes | | `permissions/field-level-security.mdx` | Field-Level Security | Field-level security — hide and mask fields | 57 | yes | | `permissions/index.mdx` | Permissions & Identity | Permissions and identity — the overview | 53 | yes | | `permissions/permission-metadata.mdx` | Permission Metadata | Permission metadata — every option explained | 58 | yes | | `permissions/permission-sets.mdx` | Permission Sets | Permission sets — grant access in bundles | 55 | yes | | `permissions/permissions-matrix.mdx` | Security Permissions Matrix | Security permissions matrix reference | 51 | yes | | `permissions/positions.mdx` | Positions | Positions — org hierarchy for sharing | 51 | yes | | `permissions/profiles.mdx` | Profiles (removed) | Profiles (removed) — use permission sets | 54 | yes | | `permissions/record-view-auditing.mdx` | Record-View Auditing | Record view auditing — who read what | 50 | yes | | `permissions/rls.mdx` | Row-Level Security (RLS) | Row-level security (RLS) — filter by rule | 55 | yes | | `permissions/sharing-rules.mdx` | Sharing Rules | Sharing rules and organization-wide defaults | 58 | yes | | `permissions/sso.mdx` | Social & Enterprise SSO | SSO — social and enterprise sign-in setup | 55 | yes | | `permissions/system-context.mdx` | System Context (isSystem) | System context (isSystem) — bypass rules | 54 | yes | | `permissions/tenant-audit-census.mdx` ✳️ | Tenant-Audit Census | Tenant-audit census — tenancy write call sites | 60 | yes | | `plugins/adding-a-metadata-type.mdx` | Adding a Metadata Type | Add a custom metadata type from a plugin | 54 | yes | | `plugins/anatomy.mdx` | Plugin Anatomy | Plugin anatomy — files, hooks and exports | 55 | yes | | `plugins/development.mdx` | Plugin Development | Plugin development — build and test one | 53 | yes | | `plugins/index.mdx` | Plugin System | Plugin system — extend the runtime safely | 55 | yes | | `plugins/packages.mdx` | Package Overview | Package overview — what each one does | 51 | yes | | `protocol/backward-compatibility.mdx` | Backward Compatibility Policy | Backward compatibility policy for the spec | 56 | yes | | `protocol/diagram.mdx` | Protocol Relationship Diagram | Protocol relationship diagram explained | 53 | yes | | `protocol/index.mdx` | Protocol Specification | Protocol specification — the open format | 54 | yes | | `protocol/kernel/config-resolution.mdx` | Configuration Resolution | Configuration resolution order and layers | 55 | yes | | `protocol/kernel/error-handling.mdx` | Error Handling | Error handling — the response envelope | 52 | yes | | `protocol/kernel/http-protocol.mdx` | HTTP API | HTTP API protocol — routes and verbs | 50 | yes | | `protocol/kernel/i18n-standard.mdx` | Internationalization Standard | Internationalization standard for metadata | 56 | yes | | `protocol/kernel/index.mdx` | Kernel: The System Protocol | Kernel — the system protocol specification | 56 | yes | | `protocol/kernel/lifecycle.mdx` | System Lifecycle | System lifecycle — from boot to ready state | 57 | yes | | `protocol/kernel/metadata-service.mdx` | Metadata Service | Metadata service protocol specification | 53 | yes | | `protocol/kernel/plugin-spec.mdx` | Plugin Package Specification | Plugin package specification and manifest | 55 | yes | | `protocol/kernel/realtime-protocol.mdx` | Real-Time Protocols | Real-time protocols — websockets, SSE | 51 | yes | | `protocol/knowledge.mdx` | Knowledge Protocol | Knowledge protocol — RAG as metadata | 50 | yes | | `protocol/objectql/query-syntax.mdx` | Query Syntax | ObjectQL query syntax — the full specification | 60 | yes | | `protocol/objectql/schema.mdx` | Schema Definition | ObjectQL schema definition specification | 54 | yes | | `protocol/objectql/security.mdx` | Security & Access Control | ObjectQL security and access control | 50 | yes | | `protocol/objectql/state-machine.mdx` | State Machine (Lifecycle) | State machine — record lifecycle spec | 51 | yes | | `protocol/objectql/types.mdx` | Type System | ObjectQL type system — the specification | 54 | yes | | `protocol/objectui/actions.mdx` | Action Protocol | Action protocol — buttons as metadata | 51 | yes | | `protocol/objectui/concept.mdx` | UI as Data Concept | UI as data — the core ObjectUI concept | 52 | yes | | `protocol/objectui/layout-dsl.mdx` | Layout DSL | Layout DSL — arrange fields declaratively | 55 | yes | | `protocol/objectui/widget-contract.mdx` | Widget Contract | Widget contract — build a custom widget | 53 | yes | | `ui/actions.mdx` | Actions | Actions — permission-checked UI buttons | 53 | yes | | `ui/apps.mdx` | App Metadata | App metadata — navigation and branding | 52 | yes | | `ui/audience-based-interfaces.mdx` | Audience-based interfaces | Audience-based interfaces — one app, many | 55 | yes | | `ui/create-vs-edit-form.mdx` | Create form ≠ edit form | Create form vs edit form — the differences | 56 | yes | | `ui/dashboards.mdx` | Dashboard Metadata | Dashboard metadata — charts and KPIs | 50 | yes | | `ui/doc-pages.mdx` | Doc Metadata | Doc metadata — in-app documentation pages | 55 | yes | | `ui/field-grouping-and-order.mdx` | Field grouping & order | Field grouping and order in generated forms | 57 | yes | | `ui/forms.mdx` | Forms (Public + Internal) | Forms — public and internal data entry | 52 | yes | | `ui/index.mdx` | UI Engine | UI engine — render views from metadata | 52 | yes | | `ui/pages.mdx` | Page Metadata | Page metadata — build custom app screens | 54 | yes | | `ui/public-data-collection.mdx` | Collect data from the public | Collect data from the public with web forms | 57 | yes | | `ui/react-pages.mdx` | React Pages | React pages — escape hatch for custom UI | 54 | yes | | `ui/reports.mdx` | Report Metadata | Report metadata — grouped, filtered data | 54 | yes | | `ui/setup-app.mdx` | Setup App | Setup app — administer a running runtime | 54 | yes | | `ui/translations.mdx` | Translations | Translations — labels and UI text per locale | 58 | yes | | `ui/views.mdx` | View Metadata | View metadata — list, kanban, calendar | 52 | yes | | `upgrading.mdx` | Upgrading | Upgrade guide — move between major versions | 57 | yes | ## Verification All at head `c69d5c7`, `git status --porcelain` empty. `origin/main` was merged in twice during the rework round (now at merge base `e2c4e12`); 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 base `e2c4e12`, three-dot; 640 changed lines, under the 5,000 human-merge threshold), then reconciled: `--ran` answered `74 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: ``` exit 0 node scripts/check-ci-filter-parity.mjs exit 0 node scripts/check-closing-keyword-parity.mjs exit 0 node scripts/check-closing-keyword-parity.mjs --self-test exit 0 node scripts/check-comment-mask-corpus.mjs exit 0 node scripts/check-declaration-mirrors.mjs exit 0 node scripts/check-declaration-mirrors.mjs --self-test exit 0 node scripts/check-doc-frontmatter.mjs exit 0 node scripts/check-doc-frontmatter.mjs --self-test exit 0 node scripts/check-doc-route-spelling.mjs --advisory exit 0 node scripts/check-doc-route-spelling.mjs --self-test exit 0 node scripts/check-docs-section-name.mjs exit 0 node scripts/check-docs-section-name.mjs --self-test exit 0 node scripts/check-scripts-symbol-anchors.mjs exit 0 node scripts/check-scripts-symbol-anchors.mjs --self-test exit 0 node scripts/check-section-landing-index.mjs exit 0 node scripts/check-section-landing-index.mjs --self-test exit 0 node scripts/check-self-test-wired.mjs exit 0 node scripts/check-self-test-wired.mjs --self-test exit 0 node scripts/check-self-test-workflow-commands.mjs exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test exit 0 node scripts/check-system-context-census.mjs exit 0 node scripts/check-system-context-census.mjs --self-test exit 0 node scripts/check-tenant-audit-census.mjs exit 0 node scripts/check-tenant-audit-census.mjs --self-test exit 0 node scripts/check-whole-set-label-write.mjs exit 0 node scripts/check-whole-set-label-write.mjs --self-test exit 0 node scripts/docs-audit/check-drift-comment.mjs exit 0 node scripts/pm/bare-root-worklist.mjs --self-test exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions exit 0 pnpm --filter @objectstack/lint run check:doc-security-posture exit 0 pnpm --filter @objectstack/spec run check:docs exit 0 pnpm --filter @objectstack/spec run check:empty-state exit 0 pnpm --filter @objectstack/spec run check:liveness exit 0 pnpm --filter @objectstack/spec run check:skill-docs exit 0 pnpm --filter @objectstack/spec run check:skill-examples exit 0 pnpm --filter @objectstack/spec run check:strictness-ledger exit 0 pnpm --filter @objectstack/spec run check:variant-docs exit 0 pnpm --filter @objectstack/spec run check:yaml-examples exit 0 pnpm check:agent-test-spelling exit 0 pnpm check:bash32-floor exit 0 pnpm check:cli-command-ids exit 0 pnpm check:cli-examples-parity exit 0 pnpm check:corpus-claim-drift exit 0 pnpm check:cross-package-test-inputs exit 0 pnpm check:doc-anchors exit 0 pnpm check:doc-authoring exit 0 pnpm check:docs-audit-scope exit 0 pnpm check:docs-image-tag exit 0 pnpm check:docs-redirects exit 0 pnpm check:docs-single-h1 exit 0 pnpm check:docs-spec-enumerations exit 0 pnpm check:docs-transcript-drift exit 0 pnpm check:driver-memory-census exit 0 pnpm check:entry-guard exit 0 pnpm check:error-status-conformance exit 0 pnpm check:gitlink-declared exit 0 pnpm check:lockstep-package-count exit 0 pnpm check:merge-driver exit 0 pnpm check:nul-bytes exit 0 pnpm check:overlay-whitelist-table exit 0 pnpm check:parse-guard exit 0 pnpm check:pm-dispatch-gates exit 0 pnpm check:pm-widening-tells exit 0 pnpm check:pnpm-filter-targets exit 0 pnpm check:published-readme-links exit 0 pnpm check:quick-reference-counts exit 0 pnpm check:ratchet-remedy-authority exit 0 pnpm check:react-page-adapter-contract exit 0 pnpm check:refd-timer-probe exit 0 pnpm check:role-word exit 0 pnpm check:runtime-services-index exit 0 pnpm check:skill-identifier-liveness exit 0 pnpm check:vendor-version-stamps exit 0 pnpm check:watch-hint-literal exit 0 node scripts/check-docs-nav-label.mjs exit 0 node scripts/check-docs-nav-label.mjs --self-test ``` 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`: `navTitle` named in code by 2 file(s) only; resolver executed; plugin wired into the docs loader; JSON-LD breadcrumb leg green. It reads `apps/docs/**` only, so a `navTitle:` 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 …` and `check-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-word` and `check:runtime-services-index` exited **1**. Both were real findings: the first is handled by the re-worded row, the second by the gate change above. `check:skill-examples` exited **3** (`PREREQUISITE NOT MET`, stale `client-react` declarations, nothing measured) until `@objectstack/client-react` was 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 full `next build` of `apps/docs` was not run locally; `fumadocs-mdx` regenerated the source index at exit 0, and `check-doc-frontmatter` parses every page with the `yaml` version the build resolves. `pnpm lint` is not owed: `.mdx` is outside the ESLint population and no other file type is touched. No changeset: docs content only, publishes nothing. `skip-changeset` applied. ## Acceptance notes - **30 approved rows have no ` — ` separator** (for example `ai/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 comment `5852831074`); re-wording them is the maintainer's call. - **The two folder-index pages objectstack-ai#12312 lengthened show the long title in the footer previous/next links**: `protocol/objectql/index.mdx` and `protocol/objectui/index.mdx` carry no `navTitle`, and `flattenTree()` puts a folder index into that list. objectstack-ai#12312 judged them "not in the page tree / `meta.json` title wins", which holds for the sidebar only. Left as they are, per the PM answer in review comment `5852831074`. - The objectstack-ai#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. - Pre-existing duplicate titles exist among the generated `references/**` pages (e.g. `Mapping`, `Sharing`, `Plugin`); that is the generator half, objectstack-ai#15403. --- _Generated by [Claude Code](https://claude.ai/code/session_018bR89KaSkoZVqgBnUYXtyD)_ --------- 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 #12311
A doc page's frontmatter
titlewas the only string the site had. This addsnavTitle— the page tree's own short label — and pins the separation so the long, search-intent title stays on every surface that is not the sidebar. This card is the mechanism only: no page title is rewritten here, which is #12237's 176-row table sittingBlocked-by:this one.The measurement this was built on
Re-measured in this worktree on
origin/main@3b2a3f640, not carried over from the card.Frontmatter corpus —
content/docs/**/*.mdx: 405 pages; the only keys in use anywhere aretitle(405) anddescription(405); 0 pages carry any nav-label field. Independently reproduced bycheck-doc-frontmatter, which now prints it: "405 page(s) parse with yaml@2.9.0 — 190 hand-written + 215 generated under references/".Consumers of
page.data.title— the card said four, the card's own repair comment said six, and the code says eleven read sites across six surfaces, plus the page tree:title/ OG / twitter metadatapage.tsx:211,:216,:227(OG image alt),:233h1(DocsTitle)page.tsx:150TechArticleheadline/namepage.tsx:123,:124BreadcrumbListleafpage.tsx:95— and, until this PR, the page treellms.txt/llms-full.txt/.mdxapp/llms.txt/route.ts:10,lib/source.ts:55(getLLMText)app/og/docs/[...slug]/route.tsx:18lib/source.ts—loader()Two disagreements with the card's list are recorded rather than smoothed over.
generateMetadatais four reads, not one. Andlib/source.ts:55getLLMTextis a seventh consumer the card's table did not name — it is whatllms-full.txtand the/llms.mdx/docs/...endpoints emit, so the card's "llms.txt" row covers one of the three agent-facing endpoints.Does
fumadocs-core@16.14.4already ship this?No key. Yes hook. Read out of the installed package:
dist/source/schema.js—pageSchemais{ title, description, icon, full, _openapi };metaSchemais{ title, pages, pagesIndex, description, root, defaultOpen, collapsible, icon }. No nav label on either.dist/loader-*.js,buildFile()— the page-tree builder destructures exactly{ title, description, icon }off a page. Nothing else on a page can become a tree label.meta.jsonpagesentries parse into five forms only: a path,.../z...arest, a---Separator---, a!excludeor...extractprefix, and a[Name](url)link. The link form builds a node with no$refand a hand-written URL, bypassing page resolution and the loader's i18n URL generation — a raw link, not a per-page label override.sidebarTitle/navTitle/navLabel/sidebar_labelacrossdist/: zero hits.So the key had to be ours. What is not ours is the wiring:
PageTreeTransformeris fumadocs' own extension point, andtransformPageTree: { file }is exactly the shape its built-in icon plugin uses (iconPlugin,dist/icon-*.js). This is an extension through the library's published seam, not a transform bolted around it.That answer is now a live assertion rather than a note:
check-docs-nav-labelre-reads both stock schemas on every run and reds when fumadocs ships a first-class equivalent, saying migrate — so ours cannot quietly become the thing nobody migrated.What landed
apps/docs/source.config.ts—docsSchema = pageSchema.extend({ navTitle: z.string().optional() }), mirroring theblogSchemaextension two declarations below.apps/docs/lib/nav-title.ts(new) — the single resolution point and the mechanism's documentation.navLabel()returns the declared label when it is a non-blank string andtitleotherwise;navTitlePlugin()applies it to page-tree nodes. Thetitlefallback is stated there and at no read site — no??at a consumer, which is the shape AGENTS.md Prime Directive12rejects. Blank,null(what YAML gives a valuelessnavTitle:) and non-strings all fall back, because an invisible sidebar entry is worse than a long one.apps/docs/lib/source.ts— one line: the plugin joinslucideIconsPlugin()in the docsloader().apps/docs/app/[lang]/docs/[[...slug]]/page.tsx—getBreadcrumbItems(..., { includePage: false }). This is the one place the card's own acceptance criterion could have been silently violated, and it is worth reading closely.The leak the card did not name: the JSON-LD breadcrumb reads the tree
docsTrail()buildsBreadcrumbListfromsource.pageTree, not frompage.data.title— its docblock said so deliberately: "a crumb and its sidebar entry are the same node with the same label." WithincludePage: true, the leaf crumb was the page's tree node. Give that node a shortnavTitleand the short label lands in structured data a crawler reads — on the exact surface #12243 exists to improve, with nothing failing.includePage: falsemoves the leaf onto the function's existing tail push,{ name: page.data.title, url: canonical }. Behaviour-identical on today's corpus, and identical for a structural reason rather than by inspection — traced throughgetBreadcrumbItemsFromPath:[F, page-node]; after,[F]+ the tail push. The page node'snameispage.data.title, so the same pair either way.item.index === path[i+1], so before it was[page-node]and after it is[]+ the tail push. Same pair again.Ancestors are unaffected in both cases: they are folders, labelled from their own
meta.jsontitle, and all 35meta.jsonfiles undercontent/docsdeclare one.Pinning the separation
The whole value of the split is a string staying out of places nobody re-reads, and every way of losing it is silent — a consumer reading the key "because it is nicer", a second fallback appearing at a read site, a resolver that stops falling back (which costs all 405 pages their sidebar label), a plugin dropped from
loader()leaving the key declared and inert. None of those breaks a type, a link, a build or a test.scripts/check-docs-nav-label.mjs(new, wired intolint.yml) states the invariant conditionally and carries six legs:navTitleappears in code in exactly the two files that declare and resolve it. Comments are masked throughjs-comment-mask.mjs, so the mechanism's own prose can name the key freely while a read cannot hide in a comment.loader().title-consuming files still readpage.data.title.--self-test(25 assertions) observes every leg red as well as green. Leg B's mutants are real modules written to a temp dir and imported the same way the real one is: a resolver that stopped falling back, an inert plugin, one that clobbers a titleless page's path-derived name, and one exporting nothing. Legs A/C/D/E run over synthetic file maps — including two negative controls that matter, the key named in prose (silent, as intended) and the identifiernavTitlePlugin(silent — the word boundary is load-bearing).scripts/check-doc-frontmatter.mjsgains the docs root's key contract. That gate asserts set equality betweensource.config.ts's extensions and its own contracts, so extending the docs schema required it: its self-test previously asserted "the docs root is still handedpageSchemaunextended". Both roots are nowpageSchema.extend(...)and the parity leg runs over both, so an extension key added insource.config.tsand nowhere else reds on either root instead of arriving unowned.Verification
All commands run in the worktree; the union below was run on the final commit
a5eb6cc8d.node scripts/pm/dispatch-gates.mjs— full derived family, 41 commandsnode scripts/check-docs-nav-label.mjs --self-test25 assertionsnode scripts/check-doc-frontmatter.mjs --self-test85 assertionsnode scripts/check-doc-frontmatter.mjs405 page(s)parse;navTitlea string on the 0 that declare onepnpm --filter @objectstack/docs typecheck(fumadocs-mdx && next typegen && tsc --noEmit)pnpm lint— repo-wideeslint . --no-inline-config, not narrowedpnpm check:nul-bytes,check:docs-single-h1,check:doc-anchors,check:doc-authoringThe two exit-3 results are the repo's PREREQUISITE-NOT-MET code, and both say in their own verdict text that they are not findings:
check-test-completenessneeds a test-run log the family invocation cannot give it, andcheck:type-check-debt --re-measurerefuses without a built workspace closure. Neither is reachable from this diff — nothing here touches a ledgered package.Two things checked rather than assumed, because "typecheck passed" is a claim about a program and not about a file:
tsc --noEmit --listFilesnamessource.config.ts,lib/nav-title.ts,lib/source.tsandapp/[lang]/docs/[[...slug]]/page.tsx— the new file really is in the program..source/source.config.mjscarriesnavTitle: z.string().optional(), so the extension reached the docs build's own compiled config rather than only the TypeScript.No page changed. No
content/docs/**fixture was needed: leg B drives the resolver over a synthetic storage, andcheck-doc-frontmatterindependently reports 0 of 405 pages declaring the key — so "all 405 keep working" is two measurements rather than an argument.Where #12237 cites this from
apps/docs/lib/nav-title.ts's header, which carries the contract, a worked frontmatter example and the surface table. That was chosen over a new page undercontent/docs/**: that tree is the product documentation, and a contributor-facing authoring rule added there becomes a 406th page needing its ownmeta.jsonentry and its own title. The header sits at the resolution point, so it cannot drift from the code, and the frontmatter key is additionally typed incheck-doc-frontmatter's docs key contract — the gate that speaks to a frontmatter author when they get it wrong.Not addressed here
#12237remains open and is not touched by this PR — the 176 title rewrites are its own card, and its PR carries the rule plus the before/after table.Generated by Claude Code