diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 14b9d38fe4..18d3761815 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1071,6 +1071,32 @@ jobs:
- name: Docs anchors resolve to real headings
run: pnpm check:doc-anchors
+ # #12236 one
per doc page. `DocsTitle` renders the frontmatter `title`
+ # as the page's unconditionally (apps/docs/app/[lang]/docs/[[...slug]]/
+ # page.tsx), so a body-level `# ` heading compiles to a SECOND one inside
+ # `DocsBody`. Measured on the dev server at /docs/data-modeling/objects
+ # before the sweep: two , both reading "Object Metadata". 195 of the 403
+ # pages were in that state, 138 of them with the two identical.
+ #
+ # It lives next to check:doc-anchors because the two share the corpus and
+ # the heading model — this gate imports `stripFrontmatter` and
+ # `flattenHeadingText` from that one rather than re-modelling either, which
+ # is what makes the delete-vs-demote remedy right on the nine pages whose
+ # two headings differ only by inline code markup.
+ #
+ # The gate strips FENCED CODE before looking for a heading, and that is the
+ # load-bearing half: a naive `^# ` scan reports 205 files, ten of which are
+ # shell/YAML comments inside working examples. A fence-blind rule does not
+ # over-report by ten, it instructs the fixer to corrupt ten snippets.
+ #
+ # Two subtrees are excluded, each with a named owner (#12249 generated
+ # reference pages, #12250 release notes) and each self-retiring: the gate
+ # FAILS with DEAD-EXCLUSION when an excluded tree becomes clean, so the
+ # carve-outs cannot outlive their reasons. A run that judges zero pages
+ # fails too (#4690).
+ - name: One `` per docs page
+ run: pnpm check:docs-single-h1
+
# #9014 docs redirects: `apps/docs/redirects.mjs` is a 92-entry table of
# PERMANENT (308) redirects, and before this step nothing in the repo read
# it — `dispatch-gates.mjs apps/docs/redirects.mjs` named no family over 99
diff --git a/content/docs/ai/actions-as-tools.mdx b/content/docs/ai/actions-as-tools.mdx
index 1396e2ae9e..8b937dbc97 100644
--- a/content/docs/ai/actions-as-tools.mdx
+++ b/content/docs/ai/actions-as-tools.mdx
@@ -3,8 +3,6 @@ title: Actions as Tools
description: Expose declarative Action metadata as AI-callable tools with explicit opt-in, HITL approval, and permission-aware execution
---
-# Actions as Tools
-
Part of the [AI module](/docs/ai) — how existing Action metadata becomes LLM-callable, and the guardrails around it.
This is what makes the app you built **AI-operable**: connect an MCP client (Claude Code, Cursor, …) and your business Actions become callable tools — so an agent can "resolve this ticket" or "convert this lead" through the same logic and permissions as the Console button.
diff --git a/content/docs/ai/agents.mdx b/content/docs/ai/agents.mdx
index fbd1c5fe7e..d2b0540c43 100644
--- a/content/docs/ai/agents.mdx
+++ b/content/docs/ai/agents.mdx
@@ -3,8 +3,6 @@ title: AI Agents
description: The two platform agents (ask and build), how skills extend them, and the shape of an AgentSchema record
---
-# AI Agents
-
Part of the [AI module](/docs/ai). In the **open edition**, agents, tools, and
skills are **typed metadata**. You author **skills** (`defineSkill`) — agents are
platform-owned and tool records are optional (see below) — from the open
diff --git a/content/docs/ai/connect-mcp.mdx b/content/docs/ai/connect-mcp.mdx
index a79af08779..3a970420d3 100644
--- a/content/docs/ai/connect-mcp.mdx
+++ b/content/docs/ai/connect-mcp.mdx
@@ -3,8 +3,6 @@ title: Connect an MCP Client
description: Point Claude Code, Claude Desktop, or any MCP client at your running app — OAuth or API key — and verify the agent can see and operate it.
---
-# Connect an MCP Client
-
Every ObjectStack deployment is already an MCP server. The runtime serves the
[Model Context Protocol](https://modelcontextprotocol.io) at **`/api/v1/mcp`**
— on by default, no plugin to install, no configuration step. Your objects and
diff --git a/content/docs/ai/index.mdx b/content/docs/ai/index.mdx
index 033f42ed4d..cf508fea1e 100644
--- a/content/docs/ai/index.mdx
+++ b/content/docs/ai/index.mdx
@@ -3,8 +3,6 @@ title: AI Overview
description: Complete guide to leveraging AI agents, RAG pipelines, and intelligent automation in ObjectStack
---
-# AI Overview
-
AI in ObjectStack is a **cross-protocol capability layer**: agents, tools, and knowledge retrieval sit on top of the same objects, actions, permissions, and automation that power the rest of the platform. This module covers the architecture and each of its moving parts.
diff --git a/content/docs/ai/knowledge-rag.mdx b/content/docs/ai/knowledge-rag.mdx
index d8946214ae..1a35b357f8 100644
--- a/content/docs/ai/knowledge-rag.mdx
+++ b/content/docs/ai/knowledge-rag.mdx
@@ -3,8 +3,6 @@ title: Knowledge & RAG
description: The Knowledge Protocol — permission-aware RAG for agents via pluggable adapter plugins (memory, RAGFlow, custom)
---
-# Knowledge & RAG
-
Part of the [AI module](/docs/ai) — how agents retrieve knowledge through the Knowledge Protocol and its adapter plugins.
ObjectStack ships a Knowledge Protocol that lets you retrieve from pluggable backends (RAGFlow, LlamaIndex, Dify, custom pgvector, …) with one call: `KnowledgeService.search(query, { sourceIds?, topK? })`. The framework defines the contract and runs permission-aware filtering; the adapter plugin does the actual retrieval. See [the protocol design](/docs/protocol/knowledge) for the rationale.
diff --git a/content/docs/ai/natural-language-queries.mdx b/content/docs/ai/natural-language-queries.mdx
index 7d9f3aea4a..4a55e1f2a1 100644
--- a/content/docs/ai/natural-language-queries.mdx
+++ b/content/docs/ai/natural-language-queries.mdx
@@ -3,8 +3,6 @@ title: Natural Language Queries
description: How agents query live data through the open MCP data tools (query_records, get_record, aggregate_records) under RLS
---
-# Natural Language Queries
-
Part of the [AI module](/docs/ai) — how natural-language questions become ObjectQL queries at runtime.
Natural-language querying is **open**. Point your own AI — Claude, Cursor, any
diff --git a/content/docs/ai/skills-reference.mdx b/content/docs/ai/skills-reference.mdx
index 39f6307afc..657b24c9bb 100644
--- a/content/docs/ai/skills-reference.mdx
+++ b/content/docs/ai/skills-reference.mdx
@@ -3,8 +3,6 @@ title: AI Skills Reference
description: Reference for every ObjectStack AI skill — domain-scoped knowledge modules that teach AI assistants the protocol's schemas and constraints
---
-# AI Skills Reference
-
ObjectStack ships a set of **domain-specific skills** that teach AI assistants (Claude Code, GitHub Copilot, Cursor, …) the protocol's schemas, patterns, and constraints — plus **process** skills that teach how work moves through a project rather than what to author. This guide is the complete catalog.
diff --git a/content/docs/ai/skills.mdx b/content/docs/ai/skills.mdx
index 5a32d81dfd..d3ad01a399 100644
--- a/content/docs/ai/skills.mdx
+++ b/content/docs/ai/skills.mdx
@@ -5,8 +5,6 @@ description: How ObjectStack uses structured AI skills to enable intelligent cod
import { Bot, Brain, Cpu, Database, Globe, Layout, Shield, Workflow, Wrench, Languages, Zap, Puzzle, BookOpen, Target, ArrowRight } from 'lucide-react';
-# AI Skills System
-
ObjectStack introduces a **Skills System** — structured, domain-specific knowledge modules that enable AI assistants (GitHub Copilot, Claude Code, Cursor, etc.) to understand and generate protocol-compliant code. Skills are what make [building with Claude Code](/docs/getting-started/build-with-claude-code) reliable: they teach the agent the protocol's rules up front, so it authors correct metadata instead of guessing.
diff --git a/content/docs/ai/tools.mdx b/content/docs/ai/tools.mdx
index eeb58b35f1..8936bb74d9 100644
--- a/content/docs/ai/tools.mdx
+++ b/content/docs/ai/tools.mdx
@@ -3,8 +3,6 @@ title: Tool Records
description: The three ways a capability reaches an agent, and the narrow case where authoring a tool record is the right answer
---
-# Tool Records
-
Part of the [AI module](/docs/ai). `tool` is an authorable metadata kind —
`ToolSchema`, declared as `defineStack({ tools })` — and it is the **least
likely** answer to "how do I give my agent a new capability".
diff --git a/content/docs/api/client-sdk.mdx b/content/docs/api/client-sdk.mdx
index dd20c154a9..e5309f72f3 100644
--- a/content/docs/api/client-sdk.mdx
+++ b/content/docs/api/client-sdk.mdx
@@ -3,8 +3,6 @@ title: Client SDK
description: The official TypeScript client for ObjectStack — auto-discovery, typed metadata, CRUD, batch operations, and service-aware feature detection.
---
-# Client SDK
-
The `@objectstack/client` is the official TypeScript client for ObjectStack. It provides a typed, protocol-aware interface that automatically adapts to your server's available services.
## Features
diff --git a/content/docs/api/data-api.mdx b/content/docs/api/data-api.mdx
index cc6698d0ed..4e76c19bf8 100644
--- a/content/docs/api/data-api.mdx
+++ b/content/docs/api/data-api.mdx
@@ -3,8 +3,6 @@ title: Data API
description: REST endpoints for CRUD, batch operations, record cloning, and analytics queries.
---
-# Data API
-
Record CRUD, batch operations, and analytics queries over REST. All paths are relative to the base URL (defaults to `/api/v1`) — see the [API Overview](/docs/api) for discovery and service availability.
## Data Operations
diff --git a/content/docs/api/data-flow.mdx b/content/docs/api/data-flow.mdx
index f18e8ef9f7..99b9b3f93b 100644
--- a/content/docs/api/data-flow.mdx
+++ b/content/docs/api/data-flow.mdx
@@ -3,8 +3,6 @@ title: Data Flow Diagrams
description: How data moves through the ObjectStack system — from defineStack() configuration to database queries, realtime events, and metadata resolution
---
-# Data Flow Diagrams
-
This guide provides detailed diagrams showing how data moves through every layer of the ObjectStack system — from developer configuration to database operations and back to the client.
diff --git a/content/docs/api/declarative-endpoints.mdx b/content/docs/api/declarative-endpoints.mdx
index 9522f836a1..f843e479dc 100644
--- a/content/docs/api/declarative-endpoints.mdx
+++ b/content/docs/api/declarative-endpoints.mdx
@@ -3,8 +3,6 @@ title: Declarative Endpoints
description: "Expose your app to systems outside the platform by declaring an apis: endpoint as metadata — which channel to pick, the four publish gates, and the obligation that comes with an anonymous endpoint."
---
-# Declarative Endpoints
-
A stack can publish an HTTP endpoint as **metadata** instead of writing a handler: a URL,
a method, a policy block, and the pipeline it delegates to. That is the `apis:` block of
`defineStack`, and its entries are **live from protocol 17** — an endpoint that passes its
diff --git a/content/docs/api/environment-routing.mdx b/content/docs/api/environment-routing.mdx
index 09c3ecdfee..1c26bca997 100644
--- a/content/docs/api/environment-routing.mdx
+++ b/content/docs/api/environment-routing.mdx
@@ -3,8 +3,6 @@ title: Environment-Scoped Routing
description: Route REST, metadata, automation, AI, and package calls through /api/v1/environments/:environmentId/....
---
-# Environment-Scoped Routing
-
Environment-scoped routing makes the target runtime explicit in the URL:
```text
diff --git a/content/docs/api/error-catalog.mdx b/content/docs/api/error-catalog.mdx
index de607632f4..bf5911fc17 100644
--- a/content/docs/api/error-catalog.mdx
+++ b/content/docs/api/error-catalog.mdx
@@ -3,8 +3,6 @@ title: Error Code Catalog
description: Complete reference for all ObjectStack error codes with causes, fixes, and retry strategies
---
-# Error Code Catalog
-
ObjectStack uses a structured error system with **9 error categories** and **50 standardized error codes**. Every error includes a machine-readable code, HTTP status mapping, and retry guidance.
diff --git a/content/docs/api/error-handling-client.mdx b/content/docs/api/error-handling-client.mdx
index 0f02b2c2eb..bf57473e94 100644
--- a/content/docs/api/error-handling-client.mdx
+++ b/content/docs/api/error-handling-client.mdx
@@ -3,8 +3,6 @@ title: Client-Side Error Handling
description: Best practices for handling ObjectStack errors in client applications — parsing, display, retry strategies, and React patterns
---
-# Client-Side Error Handling
-
This guide covers best practices for handling ObjectStack API errors in client applications — from parsing error responses to displaying validation errors in forms and implementing retry strategies.
diff --git a/content/docs/api/error-handling-server.mdx b/content/docs/api/error-handling-server.mdx
index 43dc0d6461..c17356122b 100644
--- a/content/docs/api/error-handling-server.mdx
+++ b/content/docs/api/error-handling-server.mdx
@@ -3,8 +3,6 @@ title: Server-Side Error Handling
description: Best practices for handling and throwing errors in ObjectStack plugins — custom errors, hook propagation, validation, transactions, and safeParsePretty
---
-# Server-Side Error Handling
-
This guide covers best practices for handling and throwing errors within ObjectStack plugins and server-side code — from creating custom errors with proper codes to transaction rollback and integration with `safeParsePretty()`.
diff --git a/content/docs/api/index.mdx b/content/docs/api/index.mdx
index d509e666d7..6e2b236923 100644
--- a/content/docs/api/index.mdx
+++ b/content/docs/api/index.mdx
@@ -3,8 +3,6 @@ title: API Overview
description: How ObjectStack generates its API surface from metadata — discovery, error handling wire formats, protocol types, and a map of the API & SDK docs.
---
-# API Overview
-
ObjectStack generates its entire API surface — REST endpoints, realtime protocols, and the client SDK — from your metadata: define an object once and its endpoints exist. This module documents those surfaces and how to consume them.
ObjectStack exposes a fully typed REST API. All endpoints use JSON request/response bodies. The API is **service-driven** — routes are only available when the corresponding plugin is installed. Use the [Discovery endpoint](#discovery) to determine what services are available at runtime.
diff --git a/content/docs/api/metadata-api.mdx b/content/docs/api/metadata-api.mdx
index f4b8c6816b..605319e398 100644
--- a/content/docs/api/metadata-api.mdx
+++ b/content/docs/api/metadata-api.mdx
@@ -3,8 +3,6 @@ title: Metadata & Package API
description: REST endpoints for object schemas, metadata types, UI views, and package management.
---
-# Metadata & Package API
-
Manage object schemas, metadata types, UI views, and installed packages over REST. All paths are relative to the base URL (defaults to `/api/v1`) — see the [API Overview](/docs/api) for discovery and service availability.
## Metadata
diff --git a/content/docs/api/plugin-endpoints.mdx b/content/docs/api/plugin-endpoints.mdx
index f8ba211b2a..f28d949c20 100644
--- a/content/docs/api/plugin-endpoints.mdx
+++ b/content/docs/api/plugin-endpoints.mdx
@@ -3,8 +3,6 @@ title: Plugin Endpoints
description: REST endpoints that become available when the corresponding plugin is installed — auth, workflow, automation, views, realtime, notifications, AI, i18n, and file storage.
---
-# Plugin Endpoints
-
These REST endpoints are only available when the corresponding plugin is installed. Check the [discovery manifest](/docs/api#discovery) `services` map before calling them; all paths are relative to the base URL (defaults to `/api/v1`).
This page is a **catalog of routes the platform serves for you** — it is not about authoring an
diff --git a/content/docs/api/wire-format.mdx b/content/docs/api/wire-format.mdx
index 9045b6e39c..a031992f51 100644
--- a/content/docs/api/wire-format.mdx
+++ b/content/docs/api/wire-format.mdx
@@ -3,8 +3,6 @@ title: Wire Format & JSON Examples
description: Complete HTTP request/response examples for ObjectStack API operations — CRUD, queries, metadata, errors, and batch operations
---
-# Wire Format & JSON Examples
-
This page provides complete HTTP request and response examples for every common ObjectStack API operation. All examples use a `task` object with realistic data.
diff --git a/content/docs/automation/approvals.mdx b/content/docs/automation/approvals.mdx
index ffe7a2763f..af603b221c 100644
--- a/content/docs/automation/approvals.mdx
+++ b/content/docs/automation/approvals.mdx
@@ -3,8 +3,6 @@ title: Approval workflow
description: Route a record for sign-off — who can configure the automation, and the run-identity decision that keeps an approval flow from quietly bypassing row-level security.
---
-# Approval workflow
-
## Scenario
> A record (an invoice, a discount, a leave request) must be **routed for approval** before it proceeds. Who is allowed to build that automation, and how do I make sure it runs safely?
diff --git a/content/docs/automation/connectors.mdx b/content/docs/automation/connectors.mdx
index c9148dc81a..e22720d1cb 100644
--- a/content/docs/automation/connectors.mdx
+++ b/content/docs/automation/connectors.mdx
@@ -3,8 +3,6 @@ title: Connectors
description: Call external systems from flows — plugin-registered connectors, and declarative provider-bound instances (rest / openapi / mcp) authored as pure metadata with reference-based credentials.
---
-# Connectors
-
> **Status:** Shipped · **Audience:** App authors (human and AI), integration
> engineers
>
diff --git a/content/docs/automation/email-templates.mdx b/content/docs/automation/email-templates.mdx
index 633da0fa13..b127636ca3 100644
--- a/content/docs/automation/email-templates.mdx
+++ b/content/docs/automation/email-templates.mdx
@@ -3,8 +3,6 @@ title: Email Templates
description: Author a localizable outbound mail template as metadata, and reach it from a flow's notify node or from services.email.
---
-# Email Templates
-
An **email template** is a named, localizable subject + body that lives as
metadata. Your app declares it; the email service resolves it by
`(name, locale)` at send time and renders its `{{placeholders}}` against a
diff --git a/content/docs/automation/flows.mdx b/content/docs/automation/flows.mdx
index 9880dba07f..6d7e581fdb 100644
--- a/content/docs/automation/flows.mdx
+++ b/content/docs/automation/flows.mdx
@@ -3,8 +3,6 @@ title: Flow Metadata
description: Build visual automation with decision trees, data operations, HTTP requests, and screen interactions
---
-# Flow Metadata
-
A **Flow** is a visual automation that orchestrates business logic through connected nodes. Flows support decision branching, data operations (CRUD), HTTP integrations, script execution, user screens, and subflow composition.
## Basic Structure
diff --git a/content/docs/automation/hook-bodies.mdx b/content/docs/automation/hook-bodies.mdx
index 625260a6a6..01a7af2b6b 100644
--- a/content/docs/automation/hook-bodies.mdx
+++ b/content/docs/automation/hook-bodies.mdx
@@ -3,7 +3,7 @@ title: Hook & Action Bodies (L1 / L2)
description: How hook handlers and script-action bodies travel through ObjectStack as pure metadata, and the spec they must conform to.
---
-# Hook & Action Bodies
+## Hook & Action Bodies
ObjectStack treats every hook handler and every `type: 'script'` action as **pure metadata**. In the self-contained (body-only) form there is no separate `.mjs` file shipped alongside the project artifact, no dynamic `import()` at runtime, and no filesystem dependency on the cloud — though today a legacy `objectstack-runtime.{hash}.mjs` back-compat bundle can still ship, and does get dynamically imported at boot, for any handler that hasn't been lowered to a metadata `body` yet (see [Migration](#migration) below). A body is either:
diff --git a/content/docs/automation/hooks.mdx b/content/docs/automation/hooks.mdx
index 0f7dd1a07a..2bffbdba39 100644
--- a/content/docs/automation/hooks.mdx
+++ b/content/docs/automation/hooks.mdx
@@ -3,8 +3,6 @@ title: Hooks
description: Run custom code at interception points in the ObjectQL execution pipeline — before/after insert, update, delete, and find
---
-# Hooks
-
Hooks are the data-layer "logic layer": they run custom code at interception
points in the ObjectQL execution pipeline (before/after insert, update, delete,
find, etc.). The construct is `Hook`, imported from `@objectstack/spec/data`.
diff --git a/content/docs/automation/index.mdx b/content/docs/automation/index.mdx
index 39aa834a87..e58eb42655 100644
--- a/content/docs/automation/index.mdx
+++ b/content/docs/automation/index.mdx
@@ -3,8 +3,6 @@ title: Automation
description: Hooks, flows, workflows, approvals, scheduled jobs, durable webhooks, and connectors — the process engine that reacts to your data.
---
-# Automation
-
Automation is ObjectStack's process engine: you attach business logic to the data model **declaratively** — as metadata — instead of scattering it through application code. It is a cross-protocol capability: automations are declared in the Automation Protocol namespace and executed by the ObjectStack runtime against ObjectQL data.
The smallest useful automation is a hook (from the CRM example app):
diff --git a/content/docs/automation/jobs.mdx b/content/docs/automation/jobs.mdx
index 59bde45ac4..a3597f1b69 100644
--- a/content/docs/automation/jobs.mdx
+++ b/content/docs/automation/jobs.mdx
@@ -3,8 +3,6 @@ title: Scheduled Jobs
description: Run a TypeScript function on a cron, interval, or one-off schedule — and decide when a job is the right tool instead of a schedule-triggered flow.
---
-# Scheduled Jobs
-
A **job** runs one named function in your bundle on a schedule. You declare the
schedule as metadata; the platform's job service owns the timing, the retries,
the per-attempt time limit, and the run history.
diff --git a/content/docs/automation/webhooks.mdx b/content/docs/automation/webhooks.mdx
index 8a59da4ca6..10d0784933 100644
--- a/content/docs/automation/webhooks.mdx
+++ b/content/docs/automation/webhooks.mdx
@@ -3,8 +3,6 @@ title: Webhook Delivery
description: How ObjectStack reliably ships outbound HTTP notifications — from event to receiver — with at-least-once guarantees, signed payloads, and observable retries.
---
-# Webhook Delivery
-
> **Status:** Shipped · **Audience:** Plugin authors, runtime engineers,
> integration partners
>
diff --git a/content/docs/automation/workflows.mdx b/content/docs/automation/workflows.mdx
index c8fc92f37b..0750ac05cb 100644
--- a/content/docs/automation/workflows.mdx
+++ b/content/docs/automation/workflows.mdx
@@ -3,8 +3,6 @@ title: Workflow Metadata
description: Use Flow for automation and state machines for strict lifecycle transitions.
---
-# Workflow Metadata
-
ObjectStack no longer has a standalone Salesforce-style Workflow Rule authoring
type. Use:
diff --git a/content/docs/concepts/architecture.mdx b/content/docs/concepts/architecture.mdx
index f3b6564f7f..80329621d6 100644
--- a/content/docs/concepts/architecture.mdx
+++ b/content/docs/concepts/architecture.mdx
@@ -5,7 +5,7 @@ description: How the Data, System, and UI protocols work together as one cohesiv
import { Database, Layout, Cpu, ArrowRight, CheckCircle, Workflow, Bot, Cloud } from 'lucide-react';
-# The Protocol Stack
+## The Protocol Stack
The architecture is built on foundational protocols that work together as a unified system:
diff --git a/content/docs/concepts/design-principles.mdx b/content/docs/concepts/design-principles.mdx
index 4b3176b584..f8493b0e24 100644
--- a/content/docs/concepts/design-principles.mdx
+++ b/content/docs/concepts/design-principles.mdx
@@ -5,8 +5,6 @@ description: The unshakable core principles that govern ObjectStack as an AI-nat
import { Scale, Code2, Database, ScrollText } from 'lucide-react';
-# Design Principles
-
ObjectStack exists to make enterprise application development explicit enough for both humans and AI agents to operate safely: **structured business metadata**.
To maintain a healthy, decoupled, and future-proof ecosystem, we uphold the following core principles. These are not suggestions; they are the constraints that enable our freedom.
diff --git a/content/docs/concepts/index.mdx b/content/docs/concepts/index.mdx
index d72da067d2..0b9dfc3729 100644
--- a/content/docs/concepts/index.mdx
+++ b/content/docs/concepts/index.mdx
@@ -5,8 +5,6 @@ description: The foundational ideas behind ObjectStack — metadata-driven devel
import { Scale, Code2, Database, ScrollText, Laptop } from 'lucide-react';
-# Core Concepts
-
Before diving into code, understanding these foundational ideas will make everything else click.
## Find Your Path
diff --git a/content/docs/concepts/metadata-driven.mdx b/content/docs/concepts/metadata-driven.mdx
index 3e313bd46c..ddfe69a2f5 100644
--- a/content/docs/concepts/metadata-driven.mdx
+++ b/content/docs/concepts/metadata-driven.mdx
@@ -5,7 +5,7 @@ description: Understanding how structured metadata becomes APIs, UI, workflows,
import { Database, Code, Link, Laptop } from 'lucide-react';
-# What is Metadata-Driven Development?
+## What is Metadata-Driven Development?
Metadata-driven development is a paradigm shift where **business intent is defined by declarative, analyzable metadata instead of scattered imperative code.**
diff --git a/content/docs/concepts/metadata-lifecycle.mdx b/content/docs/concepts/metadata-lifecycle.mdx
index 4cf009faa4..1725a24f91 100644
--- a/content/docs/concepts/metadata-lifecycle.mdx
+++ b/content/docs/concepts/metadata-lifecycle.mdx
@@ -3,8 +3,6 @@ title: Metadata Lifecycle & HMR
description: How metadata flows through Repository → Change Log → Cache → Registry — the canonical event stream that powers Studio HMR, REST writes, and future cloud editing.
---
-# Metadata Lifecycle & HMR
-
This page documents the metadata data path introduced by [ADR-0008](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0008-metadata-repository-and-change-log.md) and refined by [ADR-0005](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0005-metadata-customization-overlay.md). It is the canonical event stream that powers Studio Hot Module Replacement (HMR), REST writes, and future cloud editing.
diff --git a/content/docs/concepts/north-star.mdx b/content/docs/concepts/north-star.mdx
index 17c76b83cd..ec5e3eced4 100644
--- a/content/docs/concepts/north-star.mdx
+++ b/content/docs/concepts/north-star.mdx
@@ -3,8 +3,6 @@ title: North Star
description: ObjectStack's current product and architecture direction.
---
-# North Star
-
ObjectStack is the metadata-native backend for business software that humans and
AI agents can both operate safely. The platform makes the business system
explicit: data models, views, flows, permissions, actions, AI tools, and runtime
diff --git a/content/docs/data-modeling/analytics.mdx b/content/docs/data-modeling/analytics.mdx
index 94099a556c..256bc32296 100644
--- a/content/docs/data-modeling/analytics.mdx
+++ b/content/docs/data-modeling/analytics.mdx
@@ -3,8 +3,6 @@ title: Analytics Datasets
description: The dataset semantic layer (ADR-0021) — define a metric once, bind reports and dashboards to it by name.
---
-# Analytics Datasets
-
> **Related ADR:** [ADR-0021 — Analytics: one semantic `dataset` layer](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0021-analytics-dataset-semantic-layer.md)
A **dataset** is a named, reusable analytical definition — a base object, the
diff --git a/content/docs/data-modeling/drivers.mdx b/content/docs/data-modeling/drivers.mdx
index 1d9153446f..28d80c5e42 100644
--- a/content/docs/data-modeling/drivers.mdx
+++ b/content/docs/data-modeling/drivers.mdx
@@ -3,8 +3,6 @@ title: Database Drivers
description: Configuration reference for supported database drivers
---
-# Database Drivers
-
ObjectStack supports multiple database backends through a unified driver interface.
Drivers can be selected in two ways:
diff --git a/content/docs/data-modeling/external-datasources.mdx b/content/docs/data-modeling/external-datasources.mdx
index bb879d9ffd..6837030e8f 100644
--- a/content/docs/data-modeling/external-datasources.mdx
+++ b/content/docs/data-modeling/external-datasources.mdx
@@ -3,8 +3,6 @@ title: External Datasources (Federation)
description: Declare an external database as a datasource and query its tables as ObjectStack objects — visible, auto-connected, and queryable with zero app code.
---
-# External Datasources (Federation)
-
ObjectStack can treat a **mature external database** — one it does not own — as a
read-only (or, with explicit opt-in, writable) datasource, and expose its tables
as normal objects. This is *federation*: the data stays in the remote database;
diff --git a/content/docs/data-modeling/field-type-decision-tree.mdx b/content/docs/data-modeling/field-type-decision-tree.mdx
index 29095e2417..8f4b9241db 100644
--- a/content/docs/data-modeling/field-type-decision-tree.mdx
+++ b/content/docs/data-modeling/field-type-decision-tree.mdx
@@ -3,8 +3,6 @@ title: Field Type Decision Tree
description: Interactive guide for choosing the right ObjectStack field type — decision flowchart, quick-reference table, and use case mapping
---
-# Field Type Decision Tree
-
Not sure which field type to use? Follow this decision tree to find the right type for your data, then check the quick-reference table for details.
diff --git a/content/docs/data-modeling/field-types.mdx b/content/docs/data-modeling/field-types.mdx
index 91699ba91c..714c553980 100644
--- a/content/docs/data-modeling/field-types.mdx
+++ b/content/docs/data-modeling/field-types.mdx
@@ -3,8 +3,6 @@ title: Field Type Gallery
description: Complete reference for every ObjectStack field type with per-type configuration properties
---
-# Field Type Gallery
-
ObjectStack provides a **comprehensive set of field types** covering every data modeling need — from basic text and numbers to AI vectors and rich media. This guide organizes them by category with per-type configuration details.
diff --git a/content/docs/data-modeling/fields.mdx b/content/docs/data-modeling/fields.mdx
index 3fcc2c68ac..8238716e55 100644
--- a/content/docs/data-modeling/fields.mdx
+++ b/content/docs/data-modeling/fields.mdx
@@ -3,8 +3,6 @@ title: Field Metadata
description: Configure field types and properties — text, numbers, dates, relationships, files, and more
---
-# Field Metadata
-
A **Field** defines an individual property within an Object. ObjectStack provides a comprehensive set of field types covering text, numbers, dates, selections, relationships, files, calculations, and specialized types like vectors and QR codes.
## Basic Usage
diff --git a/content/docs/data-modeling/formulas.mdx b/content/docs/data-modeling/formulas.mdx
index 7811580455..f80ffaf782 100644
--- a/content/docs/data-modeling/formulas.mdx
+++ b/content/docs/data-modeling/formulas.mdx
@@ -3,7 +3,7 @@ title: Expressions (CEL)
description: Canonical CEL-based expression language for formulas, predicates, conditions, and dynamic seed values
---
-# Expressions
+## Expressions
ObjectStack uses a **single canonical expression language** for every place a
piece of metadata needs to compute a value or evaluate a condition:
diff --git a/content/docs/data-modeling/import-mappings.mdx b/content/docs/data-modeling/import-mappings.mdx
index 84047ecb57..587284ae31 100644
--- a/content/docs/data-modeling/import-mappings.mdx
+++ b/content/docs/data-modeling/import-mappings.mdx
@@ -3,8 +3,6 @@ title: Import Mappings
description: Named, reusable source-column to field projections for CSV/JSON/xlsx import, and the mappingName request that applies one.
---
-# Import Mappings
-
An **import mapping** is a named, reusable projection from the columns of a source
file onto the fields of one object. `mapping` is a first-class metadata kind, so a
mapping either ships inside a package or is saved at runtime — and either way
diff --git a/content/docs/data-modeling/index.mdx b/content/docs/data-modeling/index.mdx
index fc69a519cb..a0a490c96f 100644
--- a/content/docs/data-modeling/index.mdx
+++ b/content/docs/data-modeling/index.mdx
@@ -3,8 +3,6 @@ title: Data Modeling
description: Objects, fields, relationships, validation, CEL formulas, and a compiled query AST — the ObjectQL layer every other module builds on.
---
-# Data Modeling
-
Every ObjectStack application starts here: you declare **objects** (business entities), their **fields**, **relationships**, and **validation rules** as typed metadata, and the platform derives the database schema, REST API, permission surface, and UI from that single definition. This module is the practical documentation for the **ObjectQL layer** — the Data Protocol specified normatively in the [ObjectQL spec](/docs/protocol/objectql).
A real object definition looks like this (from the CRM example app):
diff --git a/content/docs/data-modeling/indexing.mdx b/content/docs/data-modeling/indexing.mdx
index 24b2ded18d..410160a0e1 100644
--- a/content/docs/data-modeling/indexing.mdx
+++ b/content/docs/data-modeling/indexing.mdx
@@ -3,8 +3,6 @@ title: Database Indexing
description: Optimize query performance with single-field, unique, compound, and lookup indexes
---
-# Database Indexing
-
Optimize query performance with indexes:
{/* os:check */}
diff --git a/content/docs/data-modeling/object-extensions.mdx b/content/docs/data-modeling/object-extensions.mdx
index 46bad08c7f..302a1783e8 100644
--- a/content/docs/data-modeling/object-extensions.mdx
+++ b/content/docs/data-modeling/object-extensions.mdx
@@ -3,8 +3,6 @@ title: Object Extensions
description: Add fields, validations and indexes to an object another package owns, without forking it — declaration, merge order, and what an extension may not contribute.
---
-# Object Extensions
-
Every object has exactly **one owning package**. That package defines the table, the
primary key and the core fields, and no second package may claim the same name — the
registry refuses it outright.
diff --git a/content/docs/data-modeling/objects.mdx b/content/docs/data-modeling/objects.mdx
index 90161bd922..3d74990d40 100644
--- a/content/docs/data-modeling/objects.mdx
+++ b/content/docs/data-modeling/objects.mdx
@@ -3,8 +3,6 @@ title: Object Metadata
description: Define business entities with ObjectSchema — the core building block of every ObjectStack application
---
-# Object Metadata
-
An **Object** is the foundational metadata type in ObjectStack. It defines a business entity — its fields, capabilities, indexes, and behaviors. Each Object maps to a database table/collection and automatically gets CRUD APIs, UI forms, and query support.
## Basic Structure
diff --git a/content/docs/data-modeling/queries.mdx b/content/docs/data-modeling/queries.mdx
index d8c92eea56..4cd657c5f6 100644
--- a/content/docs/data-modeling/queries.mdx
+++ b/content/docs/data-modeling/queries.mdx
@@ -3,8 +3,6 @@ title: Query Syntax Cheat Sheet
description: One-page reference for ObjectStack QuerySchema — filters, sorts, pagination, aggregations, and expand
---
-# Query Syntax Cheat Sheet
-
Quick reference for building queries with the ObjectStack QuerySchema.
diff --git a/content/docs/data-modeling/relationships.mdx b/content/docs/data-modeling/relationships.mdx
index 1c05618e56..7fea4f7ce2 100644
--- a/content/docs/data-modeling/relationships.mdx
+++ b/content/docs/data-modeling/relationships.mdx
@@ -3,8 +3,6 @@ title: "Relationships & Lookups"
description: Model relationships between objects — lookups, filtered lookups, self-referencing hierarchies, and related lists
---
-# Relationships & Lookups
-
### Lookup (Many-to-One)
Creates a reference to another object:
diff --git a/content/docs/data-modeling/schema-design.mdx b/content/docs/data-modeling/schema-design.mdx
index c2bed2ca48..444a5c4187 100644
--- a/content/docs/data-modeling/schema-design.mdx
+++ b/content/docs/data-modeling/schema-design.mdx
@@ -3,8 +3,6 @@ title: Schema Design
description: Design robust object schemas in ObjectStack — object metadata, enable flags, field groups, and enterprise best practices
---
-# Schema Design
-
Complete guide to designing robust data models in ObjectStack following enterprise best practices.
---
diff --git a/content/docs/data-modeling/seed-data.mdx b/content/docs/data-modeling/seed-data.mdx
index 6a08f1b002..df9654b89a 100644
--- a/content/docs/data-modeling/seed-data.mdx
+++ b/content/docs/data-modeling/seed-data.mdx
@@ -3,8 +3,6 @@ title: Seed Data & Fixtures
description: Populate ObjectStack objects with bootstrap data, reference records, and demo fixtures using defineSeed()
---
-# Seed Data & Fixtures
-
`defineSeed()` is the canonical way to define seed data in ObjectStack. It provides
compile-time type safety by inferring valid field keys directly from your object
definition, so typos in record field names are caught before the code runs.
diff --git a/content/docs/data-modeling/validation-rules.mdx b/content/docs/data-modeling/validation-rules.mdx
index ef13e81639..28af424f20 100644
--- a/content/docs/data-modeling/validation-rules.mdx
+++ b/content/docs/data-modeling/validation-rules.mdx
@@ -3,8 +3,6 @@ title: Field Validation Rules
description: Default validation behavior, required properties, and constraints for each ObjectStack field type
---
-# Field Validation Rules
-
Every ObjectStack field type has built-in validation behavior that runs automatically at the schema level. This reference documents the **default constraints**, **required properties**, and **validation semantics** for each field type.
diff --git a/content/docs/data-modeling/validation.mdx b/content/docs/data-modeling/validation.mdx
index 5b81813359..0291807e9c 100644
--- a/content/docs/data-modeling/validation.mdx
+++ b/content/docs/data-modeling/validation.mdx
@@ -3,8 +3,6 @@ title: Validation Metadata
description: Define data integrity rules — formula conditions, format, cross-field checks, state machine transitions, and more
---
-# Validation Metadata
-
**Validation rules** enforce data integrity at the platform level. They run automatically on the write path (insert/update), preventing invalid data from being saved. A validation rule is a deterministic, synchronous, side-effect-free predicate over a single record — it must be decidable from the incoming write (and, on update, the prior record) with no I/O. ObjectStack supports 6 validation types: `script`, `state_machine`, `format`, `cross_field`, `json_schema`, and `conditional`.
diff --git a/content/docs/deployment/backup-restore.mdx b/content/docs/deployment/backup-restore.mdx
index 3b1c0e356e..2165c017df 100644
--- a/content/docs/deployment/backup-restore.mdx
+++ b/content/docs/deployment/backup-restore.mdx
@@ -3,8 +3,6 @@ title: Backup & Restore
description: What state an ObjectStack deployment actually holds, how to back up each piece per database driver, and the restore drill to rehearse before you need it.
---
-# Backup & Restore
-
The [go-live checklist](/docs/deployment/production-readiness#go-live-checklist)
requires a documented, tested backup/restore drill. This page is that drill:
what to back up, how, and how to prove the restore works.
diff --git a/content/docs/deployment/cli.mdx b/content/docs/deployment/cli.mdx
index e101d9c9e1..b2ccecb7ed 100644
--- a/content/docs/deployment/cli.mdx
+++ b/content/docs/deployment/cli.mdx
@@ -3,7 +3,7 @@ title: Command Line Interface
description: Complete guide for using the ObjectStack CLI to build metadata-driven applications
---
-# @objectstack/cli
+## @objectstack/cli
Command Line Interface for building metadata-driven applications with the ObjectStack Protocol.
diff --git a/content/docs/deployment/environment-variables.mdx b/content/docs/deployment/environment-variables.mdx
index 18468c1270..08d814e15b 100644
--- a/content/docs/deployment/environment-variables.mdx
+++ b/content/docs/deployment/environment-variables.mdx
@@ -3,8 +3,6 @@ title: Environment Variables
description: Canonical list of OS_* environment variables read by the ObjectStack runtime, CLI, and built-in plugins.
---
-# Environment Variables
-
All ObjectStack-owned environment variables use the `OS_` prefix. Variables are
read at startup unless noted otherwise. Boolean variables accept `true` / `false`
(case-insensitive); flag-style variables also accept `1` / `0` where noted.
diff --git a/content/docs/deployment/index.mdx b/content/docs/deployment/index.mdx
index de72249fa6..7726c47fac 100644
--- a/content/docs/deployment/index.mdx
+++ b/content/docs/deployment/index.mdx
@@ -3,8 +3,6 @@ title: Deployment Overview
description: Two things deploy on this platform and they move on separate clocks — the platform runtime you operate, and the metadata app you build. Which one you are doing decides which pages in this section are yours.
---
-# Deployment Overview
-
Two different things get deployed here, and they run on **separate clocks**:
| | **The platform runtime** | **Your metadata app** |
diff --git a/content/docs/deployment/production-readiness.mdx b/content/docs/deployment/production-readiness.mdx
index c6248b9673..0384394453 100644
--- a/content/docs/deployment/production-readiness.mdx
+++ b/content/docs/deployment/production-readiness.mdx
@@ -3,8 +3,6 @@ title: Production Readiness
description: Security headers, rate limiting, observability, and the go-live checklist for ObjectStack runtimes.
---
-# Production Readiness
-
`@objectstack/runtime` ships first-class primitives for the cross-cutting
concerns every production deployment needs: HTTP hardening, rate limiting,
metrics, error reporting, and request-id correlation. Everything is
diff --git a/content/docs/deployment/publish-and-preview.mdx b/content/docs/deployment/publish-and-preview.mdx
index a056cec01d..6a1f3d7919 100644
--- a/content/docs/deployment/publish-and-preview.mdx
+++ b/content/docs/deployment/publish-and-preview.mdx
@@ -3,8 +3,6 @@ title: Publish, Versioning & Preview
description: A metadata app is versioned in your catalog while the platform moves on its own release train. Compile the app into an artifact, then pick how it reaches a running platform — installed from the catalog, or pinned as the runtime's boot artifact.
---
-# Publish, Versioning & Preview
-
## Your app and the platform move on separate clocks
A metadata app is **not part of the platform**. It compiles to its own immutable
diff --git a/content/docs/deployment/seed-tenancy-repair.mdx b/content/docs/deployment/seed-tenancy-repair.mdx
index 95e8c1009d..e07f529ea8 100644
--- a/content/docs/deployment/seed-tenancy-repair.mdx
+++ b/content/docs/deployment/seed-tenancy-repair.mdx
@@ -3,8 +3,6 @@ title: Seed Tenancy Repair
description: The automatic repair that stamps organization_id on untenanted seed rows and merges the __global__ autonumber counter — when it runs, what it changes, what it deliberately leaves alone, and the manual remedy for a multi-organization install.
---
-# Seed Tenancy Repair
-
ObjectStack ships one repair that **rewrites stored rows without an operator
asking it to**. Every other row-rewriting migration on this platform is an
explicit `os migrate … --apply` with a preview mode; this one is not. It has no
diff --git a/content/docs/deployment/self-hosting.mdx b/content/docs/deployment/self-hosting.mdx
index 36d3121cf5..1ed1ce73e1 100644
--- a/content/docs/deployment/self-hosting.mdx
+++ b/content/docs/deployment/self-hosting.mdx
@@ -3,8 +3,6 @@ title: Self-Hosted Deployment
description: Run a compiled ObjectStack app on your own infrastructure with the official Docker image — plus Compose with Postgres, Kubernetes, and the bare Node.js fallback, including health checks, reverse-proxy wiring, and the secrets you must pin.
---
-# Self-Hosted Deployment
-
This guide takes the artifact produced by `os build` / `os compile` and runs it
on infrastructure **you** operate. **Docker is the standard path** — the
platform publishes an official runtime image on every release, and Compose and
diff --git a/content/docs/deployment/single-project-mode.mdx b/content/docs/deployment/single-project-mode.mdx
index 1420b2919b..9dc324a5b9 100644
--- a/content/docs/deployment/single-project-mode.mdx
+++ b/content/docs/deployment/single-project-mode.mdx
@@ -3,8 +3,6 @@ title: Single-Environment Mode
description: Run ObjectStack as a one-environment local stack without a Cloud control plane.
---
-# Single-Environment Mode
-
Single-environment mode is the default boot shape for this repository's
framework runtime and examples. One process owns one active environment, loads
the configured metadata, and serves REST plus the published console bundle.
diff --git a/content/docs/deployment/tenancy-modes.mdx b/content/docs/deployment/tenancy-modes.mdx
index d0b9997e38..03296762ab 100644
--- a/content/docs/deployment/tenancy-modes.mdx
+++ b/content/docs/deployment/tenancy-modes.mdx
@@ -3,8 +3,6 @@ title: Tenancy Postures & Membership
description: The three tenancy postures (single / group / isolated), how OS_TENANCY_POSTURE resolves, the membership policy for new users, and the degraded-tenancy boot guard.
---
-# Tenancy Postures & Membership
-
An ObjectStack deployment runs in one of **three tenancy postures**. The posture
is the single fact that decides what the authorization kernel's Layer 0
organization wall enforces, how new users are placed into an organization, and
diff --git a/content/docs/deployment/troubleshooting.mdx b/content/docs/deployment/troubleshooting.mdx
index 7bf626f44e..db62e4fe3b 100644
--- a/content/docs/deployment/troubleshooting.mdx
+++ b/content/docs/deployment/troubleshooting.mdx
@@ -3,8 +3,6 @@ title: Troubleshooting & FAQ
description: Solutions to common ObjectStack issues — validation failures, query problems, configuration mistakes, and more
---
-# Troubleshooting & FAQ
-
Solutions to the most common issues encountered when working with ObjectStack.
---
diff --git a/content/docs/deployment/validating-metadata.mdx b/content/docs/deployment/validating-metadata.mdx
index 04ef2d6a65..a67b2ff312 100644
--- a/content/docs/deployment/validating-metadata.mdx
+++ b/content/docs/deployment/validating-metadata.mdx
@@ -3,8 +3,6 @@ title: Validating Metadata
description: Why ObjectStack metadata mistakes fail silently at runtime, and the one command that catches them at author time — run it after every metadata edit.
---
-# Validating Metadata
-
ObjectStack metadata is data, not code paths — so most mistakes are **not** caught
by the TypeScript compiler. They pass `tsc`, load fine, and then fail **silently
at runtime**. The fix is one command you run after every metadata edit:
diff --git a/content/docs/getting-started/build-with-claude-code.mdx b/content/docs/getting-started/build-with-claude-code.mdx
index 98dc5cb1ef..cd473f894b 100644
--- a/content/docs/getting-started/build-with-claude-code.mdx
+++ b/content/docs/getting-started/build-with-claude-code.mdx
@@ -3,8 +3,6 @@ title: Build with Claude Code
description: The core ObjectStack workflow — Claude Code authors the metadata, you verify in the visual Console, guardrails catch mistakes, and the app you build is itself AI-operable over MCP.
---
-# Build with Claude Code
-
This is the **main way you build on ObjectStack**: you describe what you want in
plain language, **Claude Code (or Cursor, Copilot, …) writes the typed metadata**,
a validation gate catches the mistakes that fail silently at runtime, and **you
diff --git a/content/docs/getting-started/common-patterns.mdx b/content/docs/getting-started/common-patterns.mdx
index 9851d24186..97364d9c2e 100644
--- a/content/docs/getting-started/common-patterns.mdx
+++ b/content/docs/getting-started/common-patterns.mdx
@@ -3,7 +3,7 @@ title: Common Patterns
description: Top 10 patterns for building applications with ObjectStack — CRUD, views, flows, agents, security, and more
---
-# Common Patterns Guide
+## Common Patterns Guide
This guide covers the most common patterns you will use when building applications with ObjectStack. Each pattern includes a complete, copy-pasteable example.
diff --git a/content/docs/getting-started/examples.mdx b/content/docs/getting-started/examples.mdx
index 464dd9f732..39f9b6f476 100644
--- a/content/docs/getting-started/examples.mdx
+++ b/content/docs/getting-started/examples.mdx
@@ -5,8 +5,6 @@ description: Run and explore the built-in example applications to learn ObjectSt
import { CheckSquare, Building2, BarChart3, Server } from 'lucide-react';
-# Example Apps
-
The monorepo ships three ready-to-run examples in `examples/` that progressively demonstrate ObjectStack features — from a simple Todo app to a full CRM and a kitchen-sink reference. For a larger external enterprise reference, see the [HotCRM repository](https://github.com/objectstack-ai/hotcrm).
diff --git a/content/docs/getting-started/how-ai-development-works.mdx b/content/docs/getting-started/how-ai-development-works.mdx
index 6be7726fa6..52d0cf8bd1 100644
--- a/content/docs/getting-started/how-ai-development-works.mdx
+++ b/content/docs/getting-started/how-ai-development-works.mdx
@@ -3,8 +3,6 @@ title: How AI Development Works
description: The division of labor behind ObjectStack — AI authors the typed metadata, you verify in the visual UI, and layered guardrails keep the AI from shipping mistakes.
---
-# How AI Development Works
-
ObjectStack is built for a specific way of working: **an AI agent writes the
application; a human verifies it in a visual UI; and a stack of guardrails keeps
the AI from shipping mistakes.** This page explains why that division of labor is
diff --git a/content/docs/getting-started/quick-reference.mdx b/content/docs/getting-started/quick-reference.mdx
index 44153c3c61..0f171c89a3 100644
--- a/content/docs/getting-started/quick-reference.mdx
+++ b/content/docs/getting-started/quick-reference.mdx
@@ -4,8 +4,6 @@ description: Fast lookup table for all ObjectStack protocols
---
-# Quick Reference Guide
-
Fast lookup for the ObjectStack protocols organized by category.
diff --git a/content/docs/getting-started/quick-start.mdx b/content/docs/getting-started/quick-start.mdx
index 612137850b..a102110a45 100644
--- a/content/docs/getting-started/quick-start.mdx
+++ b/content/docs/getting-started/quick-start.mdx
@@ -3,8 +3,6 @@ title: Anatomy of an ObjectStack App
description: A tour of the metadata an AI agent writes for you — objects, actions, views, apps — and how to read it so you can verify the result in the Console.
---
-# Anatomy of an ObjectStack App
-
An ObjectStack app *is* its metadata: a few hundred lines of typed definitions for
objects, actions, views, apps, automation, and permissions. In the
[AI-first workflow](/docs/getting-started/build-with-claude-code), **an agent
diff --git a/content/docs/getting-started/your-first-project.mdx b/content/docs/getting-started/your-first-project.mdx
index f0c1db5fe3..f2b8ceae94 100644
--- a/content/docs/getting-started/your-first-project.mdx
+++ b/content/docs/getting-started/your-first-project.mdx
@@ -3,8 +3,6 @@ title: Your First Project
description: Scaffold a standalone ObjectStack project with npm, understand what was generated, extend the data model, call the REST API, and build a deployable artifact — no monorepo checkout, no AI agent required.
---
-# Your First Project
-
This is the hands-on path for developers building **on** ObjectStack from the
published npm packages — you never clone the framework repository. In about ten
minutes you will scaffold a project, run it, add a field by hand, call the
diff --git a/content/docs/index.mdx b/content/docs/index.mdx
index ab66088b98..93e81c18ed 100644
--- a/content/docs/index.mdx
+++ b/content/docs/index.mdx
@@ -3,7 +3,7 @@ title: Documentation
description: Technical documentation for ObjectStack.
---
-# ObjectStack Documentation
+## ObjectStack Documentation
ObjectStack is an **AI-native business backend protocol** for structured, auditable
business applications. The way you build it: **an AI agent (Claude Code) authors your
diff --git a/content/docs/kernel/architecture.mdx b/content/docs/kernel/architecture.mdx
index eed58bf653..3ecf5cfcb8 100644
--- a/content/docs/kernel/architecture.mdx
+++ b/content/docs/kernel/architecture.mdx
@@ -3,7 +3,7 @@ title: Architecture
description: Deep dive into the ObjectKernel architecture
---
-# Core Architecture
+## Core Architecture
The ObjectKernel architecture follows a strict **Life-Cycle** process to ensure system stability.
diff --git a/content/docs/kernel/cluster.mdx b/content/docs/kernel/cluster.mdx
index fbf4ee79e1..3961b44d68 100644
--- a/content/docs/kernel/cluster.mdx
+++ b/content/docs/kernel/cluster.mdx
@@ -3,8 +3,6 @@ title: Cluster Semantics
description: How ObjectStack stays correct from a single laptop to a multi-node cluster — event scope, service scope, leader election, and metadata versioning.
---
-# Cluster Semantics
-
> **Status:** Accepted · **Audience:** Plugin authors, runtime engineers
>
> **TL;DR** — ObjectStack runs identically on a laptop and on a 10-node cluster. To
diff --git a/content/docs/kernel/contracts/auth-service.mdx b/content/docs/kernel/contracts/auth-service.mdx
index a08d7993b1..18f7096645 100644
--- a/content/docs/kernel/contracts/auth-service.mdx
+++ b/content/docs/kernel/contracts/auth-service.mdx
@@ -3,8 +3,6 @@ title: IAuthService Contract
description: Reference for the Auth Service contract — authentication, session verification, user resolution, and logout
---
-# IAuthService Contract
-
The Auth Service handles **authentication** — verifying user identity through credentials, tokens, or external providers. It does not handle authorization (permissions); that is governed separately by the security layer (PermissionSets, sharing rules) in `plugin-security`.
diff --git a/content/docs/kernel/contracts/cache-service.mdx b/content/docs/kernel/contracts/cache-service.mdx
index 0dace2dd34..cf39548cb3 100644
--- a/content/docs/kernel/contracts/cache-service.mdx
+++ b/content/docs/kernel/contracts/cache-service.mdx
@@ -3,8 +3,6 @@ title: ICacheService Contract
description: Reference for the Cache Service contract — key-value caching with TTL support
---
-# ICacheService Contract
-
The Cache Service provides a **key-value caching** layer used throughout ObjectStack for metadata caching, query result caching, and rate limiting. Implementations can range from in-memory Maps to Redis clusters. It does **not** store sessions: the session of record is always the `sys_session` table, because ADR-0069 D4's session controls (idle timeout, absolute lifetime, concurrent-session cap) revoke a session by writing that row. A host may opt in explicitly with `cacheSecondaryStorage()` from `@objectstack/plugin-auth`, but should know what it buys: better-auth then answers session lookups from the cached snapshot without reading the database, so **opting in disables all three D4 session controls** — a revoked session stays usable until its cached copy expires.
diff --git a/content/docs/kernel/contracts/data-engine.mdx b/content/docs/kernel/contracts/data-engine.mdx
index 1728dae619..967eb870f6 100644
--- a/content/docs/kernel/contracts/data-engine.mdx
+++ b/content/docs/kernel/contracts/data-engine.mdx
@@ -3,8 +3,6 @@ title: IDataEngine Contract
description: Reference for the Data Engine contract — the core data persistence layer for CRUD operations, queries, aggregations, and transactions
---
-# IDataEngine Contract
-
The Data Engine is the **core persistence layer** of ObjectStack. Every data operation — inserts, finds, updates, deletes, counts, and aggregations — flows through this contract. The Kernel delegates to the Data Engine after applying security, validation, and hooks.
diff --git a/content/docs/kernel/contracts/index.mdx b/content/docs/kernel/contracts/index.mdx
index 65dac74a19..d39db3841b 100644
--- a/content/docs/kernel/contracts/index.mdx
+++ b/content/docs/kernel/contracts/index.mdx
@@ -3,8 +3,6 @@ title: Service Contracts Overview
description: Reference for all ObjectStack service contracts — TypeScript interfaces that define the API between the Kernel and plugins
---
-# Service Contracts Overview
-
Service contracts are **TypeScript interfaces** that define the boundaries between the ObjectStack Kernel and its plugins. Every plugin implements one or more contracts, and the Kernel consumes them through dependency injection.
diff --git a/content/docs/kernel/contracts/metadata-service.mdx b/content/docs/kernel/contracts/metadata-service.mdx
index 617e1b29fe..4a730bc1b8 100644
--- a/content/docs/kernel/contracts/metadata-service.mdx
+++ b/content/docs/kernel/contracts/metadata-service.mdx
@@ -3,8 +3,6 @@ title: IMetadataService Contract
description: Reference for the Metadata Service contract — CRUD operations for object and field definitions, schema registry, overlay management, and import/export
---
-# IMetadataService Contract
-
The Metadata Service manages all object and field definitions at runtime. It serves as the **schema registry** — plugins, the Kernel, and the API layer all query this service to discover what objects exist and what fields they contain.
diff --git a/content/docs/kernel/contracts/storage-service.mdx b/content/docs/kernel/contracts/storage-service.mdx
index f689456cce..a4ed486941 100644
--- a/content/docs/kernel/contracts/storage-service.mdx
+++ b/content/docs/kernel/contracts/storage-service.mdx
@@ -3,8 +3,6 @@ title: IStorageService Contract
description: Reference for the Storage Service contract — file upload, download, deletion, metadata, and signed URL generation
---
-# IStorageService Contract
-
The Storage Service provides a unified interface for **file management** — uploading, downloading, and organizing files across different storage backends (local filesystem and S3-compatible object storage).
diff --git a/content/docs/kernel/events.mdx b/content/docs/kernel/events.mdx
index f119cd3f43..58260ec497 100644
--- a/content/docs/kernel/events.mdx
+++ b/content/docs/kernel/events.mdx
@@ -3,8 +3,6 @@ title: Events & Hooks
description: System-wide event bus for loose coupling between plugins
---
-# Events & Hooks
-
ObjectStack has **two distinct hook systems**. They look similar but use different APIs, payloads, ordering, and error semantics — pick the right one for the job:
- **Kernel lifecycle hooks** — `ctx.hook(name, handler)` / `ctx.trigger(name, ...args)` on the `PluginContext`. Used for system bootstrap events and custom plugin-to-plugin events, matched by **exact name**.
diff --git a/content/docs/kernel/index.mdx b/content/docs/kernel/index.mdx
index f227739c77..7e362d6567 100644
--- a/content/docs/kernel/index.mdx
+++ b/content/docs/kernel/index.mdx
@@ -3,8 +3,6 @@ title: Kernel & Services
description: The ObjectKernel runtime — plugin host, event bus, service registry, and the versioned services.* APIs your hooks and plugins call.
---
-# Kernel & Services
-
The kernel is ObjectStack's runtime: it loads your metadata artifact, hosts plugins, wires up services, and enforces the lifecycle that keeps the system stable. This module documents the **ObjectOS layer** in practice — the [System Protocol](/docs/protocol/kernel) is its normative spec. If you write hooks or plugins, this is where the capability surface you call — `ctx.api`, `ctx.getService(...)`, and the `services.*` contract they implement — is documented.
## How it fits together
diff --git a/content/docs/kernel/runtime-services/audit-service.mdx b/content/docs/kernel/runtime-services/audit-service.mdx
index 94cf5fd581..a5d01e85a5 100644
--- a/content/docs/kernel/runtime-services/audit-service.mdx
+++ b/content/docs/kernel/runtime-services/audit-service.mdx
@@ -3,8 +3,6 @@ title: services.audit
description: Write ingress for audit events that are not CRUD — today, auth session events.
---
-# `services.audit`
-
- **Stability:** `experimental`
- **Canonical source:** `packages/plugins/plugin-audit/src/auth-event-audit.ts`
- **Registry slot:** `audit` — resolve with `ctx.getService('audit')`.
diff --git a/content/docs/kernel/runtime-services/data-service.mdx b/content/docs/kernel/runtime-services/data-service.mdx
index 97d7bf031a..86c7904a07 100644
--- a/content/docs/kernel/runtime-services/data-service.mdx
+++ b/content/docs/kernel/runtime-services/data-service.mdx
@@ -3,8 +3,6 @@ title: services.data
description: CRUD runtime helper API for records (`query`, `get`, `find`, `create`, `update`, `delete`).
---
-# `services.data`
-
- **Stability:** `stable`
- **Canonical source:** `packages/client/src/index.ts` — the `ObjectStackClient.data`
surface (see [Canonical source](#canonical-source) for why this page names the SDK
diff --git a/content/docs/kernel/runtime-services/email-service.mdx b/content/docs/kernel/runtime-services/email-service.mdx
index e1200cb603..0d51c57428 100644
--- a/content/docs/kernel/runtime-services/email-service.mdx
+++ b/content/docs/kernel/runtime-services/email-service.mdx
@@ -3,8 +3,6 @@ title: services.email
description: Outbound email delivery and template rendering APIs.
---
-# `services.email`
-
- **Stability:** `stable`
- **Canonical source:** `packages/spec/src/contracts/email-service.ts`
- **Registry slot:** `email` — resolve with `ctx.getService('email')`.
diff --git a/content/docs/kernel/runtime-services/examples.mdx b/content/docs/kernel/runtime-services/examples.mdx
index 7bd93f7bf5..5042f95060 100644
--- a/content/docs/kernel/runtime-services/examples.mdx
+++ b/content/docs/kernel/runtime-services/examples.mdx
@@ -3,8 +3,6 @@ title: Runtime Service Examples
description: Practical examples for flow nodes, hooks, and plugin event subscriptions.
---
-# Runtime Service Examples
-
These pages document the `services.*` **contract surface** — the signatures, not a
diff --git a/content/docs/kernel/runtime-services/index.mdx b/content/docs/kernel/runtime-services/index.mdx
index 1136b50c2b..e22e54eb8f 100644
--- a/content/docs/kernel/runtime-services/index.mdx
+++ b/content/docs/kernel/runtime-services/index.mdx
@@ -3,8 +3,6 @@ title: Runtime Service APIs
description: Reference entry for runtime `services.*` APIs used by flow nodes, hooks, and plugins.
---
-# Runtime Service APIs
-
**Binding note.** These pages document the stable `services.*` contract surface
(signatures match the client SDK and `packages/spec/src/contracts/`). In this
diff --git a/content/docs/kernel/runtime-services/queue-service.mdx b/content/docs/kernel/runtime-services/queue-service.mdx
index 82d3213c68..05173d6a2a 100644
--- a/content/docs/kernel/runtime-services/queue-service.mdx
+++ b/content/docs/kernel/runtime-services/queue-service.mdx
@@ -3,8 +3,6 @@ title: services.queue
description: Async queue publish/subscribe and DLQ operations.
---
-# `services.queue`
-
- **Stability:** `stable`
- **Canonical source:** `packages/spec/src/contracts/queue-service.ts`
- **Registry slot:** `queue` — resolve with `ctx.getService('queue')`.
diff --git a/content/docs/kernel/runtime-services/settings-service.mdx b/content/docs/kernel/runtime-services/settings-service.mdx
index e75d0c23a6..dc3df073c5 100644
--- a/content/docs/kernel/runtime-services/settings-service.mdx
+++ b/content/docs/kernel/runtime-services/settings-service.mdx
@@ -3,8 +3,6 @@ title: services.settings
description: Namespace-based configuration service with OS env/global/tenant/user/default cascade.
---
-# `services.settings`
-
- **Stability:** `stable`
- **Canonical source:** `packages/services/service-settings/src/settings-service.ts`
- **Registry slot:** `settings` — resolve with `ctx.getService('settings')`.
diff --git a/content/docs/kernel/runtime-services/sharing-service.mdx b/content/docs/kernel/runtime-services/sharing-service.mdx
index 50cc041e6b..41b34c06cb 100644
--- a/content/docs/kernel/runtime-services/sharing-service.mdx
+++ b/content/docs/kernel/runtime-services/sharing-service.mdx
@@ -3,8 +3,6 @@ title: services.sharing
description: Record-level sharing and editability checks.
---
-# `services.sharing`
-
- **Stability:** `stable`
- **Canonical source:** `packages/spec/src/contracts/sharing-service.ts`
- **Registry slot:** `sharing` — resolve with `ctx.getService('sharing')`.
diff --git a/content/docs/kernel/runtime-services/sms-service.mdx b/content/docs/kernel/runtime-services/sms-service.mdx
index 66d78a5796..c6db503dca 100644
--- a/content/docs/kernel/runtime-services/sms-service.mdx
+++ b/content/docs/kernel/runtime-services/sms-service.mdx
@@ -3,8 +3,6 @@ title: services.sms
description: Outbound SMS delivery through pluggable providers (Aliyun SMS, Twilio).
---
-# `services.sms`
-
- **Stability:** `stable`
- **Canonical source:** `packages/spec/src/contracts/sms-service.ts`
- **Registry slot:** `sms` — resolve with `ctx.getService('sms')`.
diff --git a/content/docs/kernel/runtime-services/storage-service.mdx b/content/docs/kernel/runtime-services/storage-service.mdx
index 0f9be1fad7..8b996b2ec9 100644
--- a/content/docs/kernel/runtime-services/storage-service.mdx
+++ b/content/docs/kernel/runtime-services/storage-service.mdx
@@ -3,8 +3,6 @@ title: services.storage
description: File/object storage contract for upload/download and presigned URL workflows.
---
-# `services.storage`
-
- **Stability:** `stable`
- **Canonical source:** `packages/spec/src/contracts/storage-service.ts`
- **Registry slot:** `storage` — resolve with `ctx.getService('storage')`.
diff --git a/content/docs/kernel/runtime-services/versioning.mdx b/content/docs/kernel/runtime-services/versioning.mdx
index f140aa1fa8..fce76d5c60 100644
--- a/content/docs/kernel/runtime-services/versioning.mdx
+++ b/content/docs/kernel/runtime-services/versioning.mdx
@@ -3,8 +3,6 @@ title: Runtime Service API Versioning
description: Stability labels and how to track runtime service API breaking changes.
---
-# Runtime Service API Versioning
-
## Stability Labels
- `stable`: backward-compatible within the current major version.
diff --git a/content/docs/kernel/services-checklist.mdx b/content/docs/kernel/services-checklist.mdx
index 0148603842..9d42d30132 100644
--- a/content/docs/kernel/services-checklist.mdx
+++ b/content/docs/kernel/services-checklist.mdx
@@ -3,8 +3,6 @@ title: Kernel Services Checklist
description: Complete inventory of ObjectStack kernel services with protocol methods, implementation status, and development requirements.
---
-# Kernel Services Checklist
-
**This page is hand-maintained.** The per-slot Provider column mirrors
`CORE_SERVICE_PROVIDER` (`packages/spec/src/system/core-services.zod.ts`), which
diff --git a/content/docs/kernel/services.mdx b/content/docs/kernel/services.mdx
index 2e566d2daf..337b52caa1 100644
--- a/content/docs/kernel/services.mdx
+++ b/content/docs/kernel/services.mdx
@@ -3,8 +3,6 @@ title: Service Registry
description: Dependency Injection mechanism for loose coupling between plugins
---
-# Service Registry
-
ObjectStack uses a lightweight **Service Locator pattern** for Dependency Injection. Services are the primary way plugins expose and consume functionality.
## Concepts
diff --git a/content/docs/permissions/access-matrix.mdx b/content/docs/permissions/access-matrix.mdx
index c57482c8ae..2037f301a8 100644
--- a/content/docs/permissions/access-matrix.mdx
+++ b/content/docs/permissions/access-matrix.mdx
@@ -3,8 +3,6 @@ title: Access-Matrix Snapshot Gate
description: A committed (permission set × object) capability matrix that fails the build on any grant drift, with the semantic diff as the review artifact (ADR-0090 D6).
---
-# Access-Matrix Snapshot Gate
-
AI drafts permission metadata on this platform; the one thing it must never do
is *silently* change who can do what. The access-matrix gate makes every
capability change a **visible, reviewable build event**: the
diff --git a/content/docs/permissions/access-recipes.mdx b/content/docs/permissions/access-recipes.mdx
index bbae0b22c6..62f7b046d9 100644
--- a/content/docs/permissions/access-recipes.mdx
+++ b/content/docs/permissions/access-recipes.mdx
@@ -3,8 +3,6 @@ title: Who can see data / automation / interface
description: Map a concrete access requirement onto the platform's layers — object CRUD, field-level security, row-level security, capabilities, app/nav gating, and the run-identity of automations.
---
-# Who can see data / automation / interface
-
## Scenario
> I have a real access requirement — "sales reps see only their own deals; managers see the team's; finance can export; nobody but admins touches the automations." How do these map onto the platform's authorization layers?
diff --git a/content/docs/permissions/administrator-guide.mdx b/content/docs/permissions/administrator-guide.mdx
index 7d38d5536f..8a8107fea4 100644
--- a/content/docs/permissions/administrator-guide.mdx
+++ b/content/docs/permissions/administrator-guide.mdx
@@ -3,8 +3,6 @@ title: Administrator Guide
description: The task-first operations manual for customer system administrators — onboard a tenant in four steps (build the org tree, add people, assign positions, verify), with the 90% rule — daily administration is assigning positions; the capability plumbing ships built-in.
---
-# Administrator Guide
-
You are the **system administrator** of an ObjectStack tenant. You opened
Setup for the first time and found dozens of positions and permission sets you
never created. This page is the *"I'm the admin — what do I actually do?"*
diff --git a/content/docs/permissions/attachments-access.mdx b/content/docs/permissions/attachments-access.mdx
index c7faba4edf..d4bc791ca4 100644
--- a/content/docs/permissions/attachments-access.mdx
+++ b/content/docs/permissions/attachments-access.mdx
@@ -3,8 +3,6 @@ title: Attachments Access
description: How access to record attachments is decided — the parent-derived read/create/delete model, authenticated downloads, the enable.files opt-in gate, and the storage-byte lifecycle. Covers sys_attachment and sys_file.
---
-# Attachments Access
-
The generic **Attachments** surface (Salesforce "Notes & Attachments" parity)
separates *file storage* from *where a file is attached*:
diff --git a/content/docs/permissions/authentication.mdx b/content/docs/permissions/authentication.mdx
index a36cf7a943..e5e88119b2 100644
--- a/content/docs/permissions/authentication.mdx
+++ b/content/docs/permissions/authentication.mdx
@@ -3,7 +3,7 @@ title: Authentication
description: "Complete guide to implementing authentication in ObjectStack using plugin-auth with Better-Auth"
---
-# Authentication Guide
+## Authentication Guide
Complete guide to implementing authentication in ObjectStack applications using the `@objectstack/plugin-auth` package powered by Better-Auth.
diff --git a/content/docs/permissions/authorization.mdx b/content/docs/permissions/authorization.mdx
index 129d6958fe..80a8e78711 100644
--- a/content/docs/permissions/authorization.mdx
+++ b/content/docs/permissions/authorization.mdx
@@ -3,8 +3,6 @@ title: Authorization Architecture
description: The one-page map of ObjectStack authorization — the enforcement chain, combination semantics, package provenance, lifecycle coverage, and the CI governance — with its limits — behind "declared" equals "enforced". Stitches ADR-0049/0054/0056/0057/0066/0068/0069/0078/0086 into a single narrative.
---
-# Authorization Architecture
-
This page is the consolidated overview of how authorization works across the
platform. Every decision summarized here is owned by an ADR (index at the
bottom) — this page adds no new decisions; it exists so you don't have to read
diff --git a/content/docs/permissions/capabilities.mdx b/content/docs/permissions/capabilities.mdx
index 9c6ce5c6fc..bb2c9b9b62 100644
--- a/content/docs/permissions/capabilities.mdx
+++ b/content/docs/permissions/capabilities.mdx
@@ -3,8 +3,6 @@ title: "Declaring Capabilities"
description: "How a package DEFINES an authorization capability with defineCapability — the declaration half of ADR-0066 D1 — and how that name travels from source to the sys_capability catalogue to a permission-set grant to a requiredPermissions check."
---
-# Declaring Capabilities
-
Every other page in this module is about **consuming** a capability: granting
one through a permission set, requiring one on an object or an action, reading
one back in an access matrix. This page is about the other end — how a package
diff --git a/content/docs/permissions/delegated-administration.mdx b/content/docs/permissions/delegated-administration.mdx
index 1d61949068..79403d0c19 100644
--- a/content/docs/permissions/delegated-administration.mdx
+++ b/content/docs/permissions/delegated-administration.mdx
@@ -3,8 +3,6 @@ title: Delegated Administration
description: Administration itself as a scoped grant — a business-unit subtree, an action set, and an assignable-set allowlist, with self-escalation structurally impossible (ADR-0090 D12).
---
-# Delegated Administration
-
A group with fifty subsidiaries cannot manage every grant from headquarters —
but handing each subsidiary a full admin is worse. ADR-0090 D12's answer:
**administration itself becomes a scoped capability**. A delegate can onboard
diff --git a/content/docs/permissions/explain.mdx b/content/docs/permissions/explain.mdx
index 22ce906a27..8c1aa172c3 100644
--- a/content/docs/permissions/explain.mdx
+++ b/content/docs/permissions/explain.mdx
@@ -3,8 +3,6 @@ title: Explain Engine
description: Ask the runtime why a decision came out the way it did — every pipeline layer, with contributor attribution, over the kernel service or REST (ADR-0090 D6).
---
-# Explain Engine
-
Authorization here is a nine-layer pipeline, and a nine-layer pipeline nobody
can interrogate is a support ticket generator. The explain engine
(ADR-0090 D6) is the first-class answer to *"why can 张三 PATCH 李四's
diff --git a/content/docs/permissions/field-level-security.mdx b/content/docs/permissions/field-level-security.mdx
index bcc741d1d3..c29e4a8dfd 100644
--- a/content/docs/permissions/field-level-security.mdx
+++ b/content/docs/permissions/field-level-security.mdx
@@ -3,8 +3,6 @@ title: "Field-Level Security"
description: "Control visibility and editability of specific fields — readable/editable rules, hidden vs. read-only semantics, and server-side enforcement of declared rules."
---
-# Field-Level Security
-
Control visibility and editability of specific fields.
## Field Permissions in Permission Sets
diff --git a/content/docs/permissions/index.mdx b/content/docs/permissions/index.mdx
index f0d51548e8..49ed30ad3d 100644
--- a/content/docs/permissions/index.mdx
+++ b/content/docs/permissions/index.mdx
@@ -3,8 +3,6 @@ title: "Permissions & Identity"
description: "Authentication, authorization, and record- and field-level access control in ObjectStack — a cross-protocol capability enforced by the ObjectStack runtime and declared as ObjectQL security metadata."
---
-# Permissions & Identity
-
This module covers authentication, authorization, and record- and field-level
access control. It is a cross-protocol capability: enforced by the ObjectStack
runtime, declared as ObjectQL security metadata.
diff --git a/content/docs/permissions/permission-metadata.mdx b/content/docs/permissions/permission-metadata.mdx
index de1232a132..33d49dc59d 100644
--- a/content/docs/permissions/permission-metadata.mdx
+++ b/content/docs/permissions/permission-metadata.mdx
@@ -3,8 +3,6 @@ title: Permission Metadata
description: Define access control with permission sets — object CRUD, field security, tab visibility, and row-level security
---
-# Permission Metadata
-
A **Permission Set** defines what a user can do within the application. It controls object-level CRUD operations, field-level visibility, tab/app access, and row-level security policies.
## Basic Structure
diff --git a/content/docs/permissions/permission-sets.mdx b/content/docs/permissions/permission-sets.mdx
index 2666720366..05081b0250 100644
--- a/content/docs/permissions/permission-sets.mdx
+++ b/content/docs/permissions/permission-sets.mdx
@@ -3,8 +3,6 @@ title: "Permission Sets"
description: "The only capability container — object CRUD + FLS + scope depth + capabilities, union-merged across everything a user holds. Covers the built-in sets, assignment tables, access depth, the isDefault suggestion, and delegated-admin scopes."
---
-# Permission Sets
-
A **permission set** is the *only* capability container in the platform
(ADR-0090): a named bundle of object CRUD grants, field-level security, scope
depth, tab visibility, system capabilities, and (optionally) RLS policies.
diff --git a/content/docs/permissions/permissions-matrix.mdx b/content/docs/permissions/permissions-matrix.mdx
index 80d42a0d1d..4b77ab6ba8 100644
--- a/content/docs/permissions/permissions-matrix.mdx
+++ b/content/docs/permissions/permissions-matrix.mdx
@@ -3,8 +3,6 @@ title: Security Permissions Matrix
description: Visual reference for ObjectStack's security model — permission types, the object × permission-set matrix, field-level security, sharing rules, business-unit depth, and the access-matrix snapshot gate
---
-# Security Permissions Matrix
-
This page provides a comprehensive visual reference for ObjectStack's security model — from object-level permissions to field-level security, sharing rules, and business-unit depth.
diff --git a/content/docs/permissions/positions.mdx b/content/docs/permissions/positions.mdx
index 4781387077..83a159e74b 100644
--- a/content/docs/permissions/positions.mdx
+++ b/content/docs/permissions/positions.mdx
@@ -3,8 +3,6 @@ title: "Positions"
description: "Positions (岗位) are flat capability-distribution groups — users hold positions, positions bind permission sets. The visibility hierarchy lives on business units, never here. Includes the built-in identity positions and the everyone/guest audience anchors."
---
-# Positions
-
A **position** (岗位) is a named, assignable distribution group: users hold
positions (`sys_user_position` rows), positions bind permission sets
(`sys_position_permission_set` rows), and a user's capability is the union of
diff --git a/content/docs/permissions/profiles.mdx b/content/docs/permissions/profiles.mdx
index 12f75b347b..7ba43de04a 100644
--- a/content/docs/permissions/profiles.mdx
+++ b/content/docs/permissions/profiles.mdx
@@ -3,7 +3,7 @@ title: "Profiles (removed)"
description: "The Profile concept was removed by ADR-0090 D2. Baseline access is now authored with the everyone audience anchor, package isDefault suggestions, and ordinary permission sets distributed via positions."
---
-# Profiles — removed (ADR-0090 D2)
+## Profiles — removed (ADR-0090 D2)
ObjectStack **no longer has a Profile concept**. It was removed pre-launch
(ADR-0090 D2) because it duplicated permission sets while adding a second
diff --git a/content/docs/permissions/record-view-auditing.mdx b/content/docs/permissions/record-view-auditing.mdx
index 3fd36bb1f1..875bf00f93 100644
--- a/content/docs/permissions/record-view-auditing.mdx
+++ b/content/docs/permissions/record-view-auditing.mdx
@@ -3,8 +3,6 @@ title: Record-View Auditing
description: "Who viewed this record, and when — the `read` action in sys_audit_log: its per-object opt-in, the four edges of its scope, and what a view row deliberately does not carry."
---
-# Record-View Auditing
-
Every other layer in this module answers *who is allowed to see this record*.
This page answers the question that comes after it, and that every regulated
industry review opens with: **who actually opened it, and when?**
diff --git a/content/docs/permissions/rls.mdx b/content/docs/permissions/rls.mdx
index 37cf4dc1d0..0bad5c5573 100644
--- a/content/docs/permissions/rls.mdx
+++ b/content/docs/permissions/rls.mdx
@@ -3,8 +3,6 @@ title: Row-Level Security (RLS)
description: Declarative per-row policies compiled into query filters — the grammar, the context variables, how policies compose, and the fail-closed contract.
---
-# Row-Level Security (RLS)
-
RLS answers a question the other layers can't: *which rows* of an object may
this user touch? Object permissions say "can read tasks"; the OWD baseline says
"tasks are private"; RLS says **"only the tasks assigned to you"** — as a
diff --git a/content/docs/permissions/sharing-rules.mdx b/content/docs/permissions/sharing-rules.mdx
index d587d676b5..0fd68bd8c8 100644
--- a/content/docs/permissions/sharing-rules.mdx
+++ b/content/docs/permissions/sharing-rules.mdx
@@ -3,7 +3,7 @@ title: "Sharing Rules"
description: "Record-level access: the organization-wide default (OWD) baseline per object, the external sharing dial, criteria sharing rules and recipient types, and the RLS-safe analytics read scope."
---
-# Sharing & Organization-Wide Defaults
+## Sharing & Organization-Wide Defaults
Record-level access starts from each object's **organization-wide default
(OWD)** and can only be **widened** from there — by scope-depth grants,
diff --git a/content/docs/permissions/sso.mdx b/content/docs/permissions/sso.mdx
index c2769438ee..15f26da01d 100644
--- a/content/docs/permissions/sso.mdx
+++ b/content/docs/permissions/sso.mdx
@@ -3,8 +3,6 @@ title: Social & Enterprise SSO
description: Enable Google sign-in in open source, and extend auth providers through packages.
---
-# Social & Enterprise SSO
-
ObjectStack open source ships two built-in social sign-in implementations, both wired by `os serve` from deployment env vars:
- **Google OAuth**: configure in Setup → Authentication, or from
diff --git a/content/docs/permissions/system-context.mdx b/content/docs/permissions/system-context.mdx
index 2c322aed06..8ca05799f6 100644
--- a/content/docs/permissions/system-context.mdx
+++ b/content/docs/permissions/system-context.mdx
@@ -3,8 +3,6 @@ title: System Context (isSystem)
description: The authoritative table of every platform behaviour keyed off `ExecutionContext.isSystem` — what an elevated write gets, what it loses, and what the flag deliberately does NOT do. Built by census over the whole repo, not by recall.
---
-# System Context (`isSystem`)
-
`ExecutionContext.isSystem` is the platform's one elevation flag. Setting it on a
write or read means "this operation is the engine acting on its own behalf" —
the seed loader replaying package fixtures, a plugin's boot reconciler, a
diff --git a/content/docs/plugins/adding-a-metadata-type.mdx b/content/docs/plugins/adding-a-metadata-type.mdx
index 6248bf5ad2..0d8aac4c17 100644
--- a/content/docs/plugins/adding-a-metadata-type.mdx
+++ b/content/docs/plugins/adding-a-metadata-type.mdx
@@ -3,8 +3,6 @@ title: Adding a Metadata Type
description: How to register a new metadata type so it shows up in the Studio app's Metadata Admin with full CRUD, overlay diffing, and (optionally) a custom editor.
---
-# Adding a Metadata Type
-
The **Metadata Admin** engine (Studio app → *All Metadata Types*) automatically
renders a directory tile, list page, schema-driven form, layered diff view,
quick-find palette, and version history for **every** type registered in the
diff --git a/content/docs/plugins/anatomy.mdx b/content/docs/plugins/anatomy.mdx
index a7525c3c03..af4f0da0ee 100644
--- a/content/docs/plugins/anatomy.mdx
+++ b/content/docs/plugins/anatomy.mdx
@@ -3,8 +3,6 @@ title: Plugin Anatomy
description: The Plugin contract — interface, context, and lifecycle phases every ObjectStack plugin implements
---
-# Plugin Anatomy
-
Plugins are the building blocks of ObjectStack. A plugin is a plain JavaScript/TypeScript object (or class) that conforms to the `Plugin` interface.
## Anatomy of a Plugin
diff --git a/content/docs/plugins/development.mdx b/content/docs/plugins/development.mdx
index 5fe711e711..0324f7bb42 100644
--- a/content/docs/plugins/development.mdx
+++ b/content/docs/plugins/development.mdx
@@ -3,7 +3,7 @@ title: Plugin Development
description: Step-by-step guide to creating, testing, and publishing ObjectStack plugins
---
-# Plugin Development Tutorial
+## Plugin Development Tutorial
This guide walks you through creating an ObjectStack plugin from scratch — from project setup to testing and registration.
diff --git a/content/docs/plugins/index.mdx b/content/docs/plugins/index.mdx
index a694603244..ee038ae916 100644
--- a/content/docs/plugins/index.mdx
+++ b/content/docs/plugins/index.mdx
@@ -3,8 +3,6 @@ title: Plugin System
description: Overview of the ObjectStack plugin architecture — plugin types, configuration, management, and built-in plugins.
---
-# Plugin System
-
ObjectStack is built on a **microkernel architecture** where nearly everything beyond the core data engine is delivered as a **plugin**. This guide covers how to create, configure, and manage plugins using the CLI and configuration.
diff --git a/content/docs/plugins/packages.mdx b/content/docs/plugins/packages.mdx
index aecb72437a..ee7ab1d360 100644
--- a/content/docs/plugins/packages.mdx
+++ b/content/docs/plugins/packages.mdx
@@ -3,8 +3,6 @@ title: Package Overview
description: Complete guide to all ObjectStack packages, services, drivers, plugins, and adapters
---
-# Package Overview
-
ObjectStack is organized into **72 package manifests** across multiple categories. This guide provides an overview of the framework packages, services, drivers, plugins, and adapters in the [framework repository](https://github.com/objectstack-ai/objectstack/tree/main/packages).
### Package categories at a glance
diff --git a/content/docs/protocol/backward-compatibility.mdx b/content/docs/protocol/backward-compatibility.mdx
index 0452067156..9f5372f1c3 100644
--- a/content/docs/protocol/backward-compatibility.mdx
+++ b/content/docs/protocol/backward-compatibility.mdx
@@ -3,8 +3,6 @@ title: Backward Compatibility Policy
description: Versioning strategy, deprecation timelines, breaking change policy, and migration support for ObjectStack
---
-# Backward Compatibility Policy
-
ObjectStack follows strict backward compatibility guarantees to ensure predictable upgrades and long-term stability for production deployments.
diff --git a/content/docs/protocol/diagram.mdx b/content/docs/protocol/diagram.mdx
index 391db33be4..ec1b9f1c42 100644
--- a/content/docs/protocol/diagram.mdx
+++ b/content/docs/protocol/diagram.mdx
@@ -3,8 +3,6 @@ title: Protocol Relationship Diagram
description: Visual architecture diagram showing how ObjectStack's protocol layers connect — Data, UI, System, Automation, AI, API, and Security
---
-# Protocol Relationship Diagram
-
ObjectStack is composed of seven protocol layers that work together to form a complete application platform. This page provides visual diagrams showing how these layers connect and how data flows through the system.
diff --git a/content/docs/protocol/index.mdx b/content/docs/protocol/index.mdx
index 2e35c6c842..c87754654f 100644
--- a/content/docs/protocol/index.mdx
+++ b/content/docs/protocol/index.mdx
@@ -5,8 +5,6 @@ description: The formal ObjectStack protocol for Data, UI, and System layers
import { Database, Layout, Cpu } from 'lucide-react';
-# Protocol Specification
-
The ObjectStack Protocol defines three interconnected layers that together describe a complete enterprise application:
diff --git a/content/docs/protocol/kernel/config-resolution.mdx b/content/docs/protocol/kernel/config-resolution.mdx
index 4d2e23b2b8..cc73f628f5 100644
--- a/content/docs/protocol/kernel/config-resolution.mdx
+++ b/content/docs/protocol/kernel/config-resolution.mdx
@@ -3,8 +3,6 @@ title: Configuration Resolution
description: How a setting resolves — the env → global → tenant → user → default cascade, its storage, and the SettingsService API
---
-# Configuration Resolution
-
**This page describes the shipped contract.** Every symbol below exists in the
repository today: the resolver is `SettingsService`
diff --git a/content/docs/protocol/kernel/error-handling.mdx b/content/docs/protocol/kernel/error-handling.mdx
index 287e058530..74ca26bbf2 100644
--- a/content/docs/protocol/kernel/error-handling.mdx
+++ b/content/docs/protocol/kernel/error-handling.mdx
@@ -5,8 +5,6 @@ description: Global error codes, response formats, and debugging strategies for
import { AlertCircle, Bug, Shield, Info, AlertTriangle, XCircle, Radio, Zap } from 'lucide-react';
-# Error Handling
-
The **Error Handling Protocol** defines standardized error codes, response formats, and debugging strategies across all ObjectStack APIs (HTTP, WebSocket).
## Why Standardized Errors Matter
diff --git a/content/docs/protocol/kernel/http-protocol.mdx b/content/docs/protocol/kernel/http-protocol.mdx
index 438c90e3ce..b4880e72c2 100644
--- a/content/docs/protocol/kernel/http-protocol.mdx
+++ b/content/docs/protocol/kernel/http-protocol.mdx
@@ -5,8 +5,6 @@ description: Standard REST mapping rules, CRUD operations, and request/response
import { Radio, Code, Database, Lock, Zap, CheckCircle, AlertCircle } from 'lucide-react';
-# HTTP API
-
The **HTTP API** defines how ObjectStack maps data operations to RESTful HTTP endpoints. Every object you define automatically gets a complete set of CRUD (Create, Read, Update, Delete) operations with consistent request/response formats.
## Core Principles
diff --git a/content/docs/protocol/kernel/i18n-standard.mdx b/content/docs/protocol/kernel/i18n-standard.mdx
index 713930d8db..9f41798a29 100644
--- a/content/docs/protocol/kernel/i18n-standard.mdx
+++ b/content/docs/protocol/kernel/i18n-standard.mdx
@@ -5,7 +5,7 @@ description: Translation bundles, locale resolution, pluralization, date/number
import { Globe, Languages, Calendar, Hash, FileText, MapPin } from 'lucide-react';
-# Internationalization (i18n) Standard
+## Internationalization (i18n) Standard
**Protocol spec, partial implementation.** The runtime i18n service
diff --git a/content/docs/protocol/kernel/lifecycle.mdx b/content/docs/protocol/kernel/lifecycle.mdx
index 237a27f9e0..4c2840bd40 100644
--- a/content/docs/protocol/kernel/lifecycle.mdx
+++ b/content/docs/protocol/kernel/lifecycle.mdx
@@ -5,8 +5,6 @@ description: Boot sequence, plugin installation, zero-downtime upgrades, and rol
import { Zap, Power, Package, RefreshCw, ArrowLeft, CheckCircle, XCircle, AlertTriangle } from 'lucide-react';
-# System Lifecycle
-
**Protocol spec.** The boot sequence, schema-evolution model, and graceful
shutdown described below reflect the current `@objectstack/core` and CLI
diff --git a/content/docs/protocol/kernel/plugin-spec.mdx b/content/docs/protocol/kernel/plugin-spec.mdx
index 58fef36f04..04367a2185 100644
--- a/content/docs/protocol/kernel/plugin-spec.mdx
+++ b/content/docs/protocol/kernel/plugin-spec.mdx
@@ -5,8 +5,6 @@ description: Manifest structure, directory layout, dependency management, and di
import { Package, FileCode, Folder, Box, Link2, Shield, Settings } from 'lucide-react';
-# Plugin Package Specification
-
**Protocol spec.** This page describes the **target** plugin packaging
specification — an ergonomic manifest wrapper, semantic-version dependency
diff --git a/content/docs/protocol/kernel/realtime-protocol.mdx b/content/docs/protocol/kernel/realtime-protocol.mdx
index 6da9dc7ff8..b7ef312e41 100644
--- a/content/docs/protocol/kernel/realtime-protocol.mdx
+++ b/content/docs/protocol/kernel/realtime-protocol.mdx
@@ -5,8 +5,6 @@ description: WebSocket subscriptions, Server-Sent Events, and event-driven commu
import { Zap, Radio, Activity, Bell, Users, Gauge, AlertCircle } from 'lucide-react';
-# Real-Time Protocols
-
The **Real-Time Protocol** describes how live data synchronization is intended to work between clients and servers. Get instant updates when data changes without polling.
diff --git a/content/docs/protocol/knowledge.mdx b/content/docs/protocol/knowledge.mdx
index 6c32b95c28..7bead96e55 100644
--- a/content/docs/protocol/knowledge.mdx
+++ b/content/docs/protocol/knowledge.mdx
@@ -3,8 +3,6 @@ title: Knowledge Protocol
description: Pluggable RAG / knowledge retrieval for ObjectStack agents — protocol + adapter model.
---
-# Knowledge Protocol
-
ObjectStack does not ship its own RAG engine. The market has many mature
options (RAGFlow, LlamaIndex, Dify, Vectara, pgvector + custom pipelines)
and competing with them on chunking, embeddings, hybrid retrieval, and
diff --git a/content/docs/protocol/objectql/security.mdx b/content/docs/protocol/objectql/security.mdx
index 9a10b8bc91..db0f8d3393 100644
--- a/content/docs/protocol/objectql/security.mdx
+++ b/content/docs/protocol/objectql/security.mdx
@@ -5,7 +5,7 @@ description: Comprehensive security model - ACL, field-level security, row-level
import { Shield, Lock, Users, Eye } from 'lucide-react';
-# Security Protocol
+## Security Protocol
ObjectStack implements a **multi-layered security model** that enforces access control at the data layer, before queries execute. Security is declarative—defined as metadata (permission sets, row-level policies, sharing rules), not scattered in application code.
diff --git a/content/docs/protocol/objectql/state-machine.mdx b/content/docs/protocol/objectql/state-machine.mdx
index 4b69171dd2..0b0617b2cd 100644
--- a/content/docs/protocol/objectql/state-machine.mdx
+++ b/content/docs/protocol/objectql/state-machine.mdx
@@ -3,7 +3,7 @@ title: State Machine (Lifecycle)
description: Define strict business logic constraints prevents AI hallucinations by enforcing valid transitions.
---
-# State Machine Protocol
+## State Machine Protocol
The **State Machine** (`state_machine` validation rule) lets you define the "Constitution" of a record's lifecycle: the legal `status` transitions a record may take. It is a **flat, textbook finite-state-machine** transition table that the write path enforces.
diff --git a/content/docs/ui/actions.mdx b/content/docs/ui/actions.mdx
index d3246d49f8..0ad92acebf 100644
--- a/content/docs/ui/actions.mdx
+++ b/content/docs/ui/actions.mdx
@@ -3,8 +3,6 @@ title: Actions
description: Declarative buttons with server-side behavior — define once, bind to lists, records, and navigation, permission-check on both surfaces, and optionally expose to AI.
---
-# Actions
-
An action is a **button declared as metadata**: where it appears
(`locations`), when it's visible (`visible`), who may run it
(`requiredPermissions`), and what it executes — an inline sandboxed script, a
diff --git a/content/docs/ui/apps.mdx b/content/docs/ui/apps.mdx
index becc2169bf..6dbcb4124c 100644
--- a/content/docs/ui/apps.mdx
+++ b/content/docs/ui/apps.mdx
@@ -3,8 +3,6 @@ title: App Metadata
description: Define application containers with navigation, branding, and access control
---
-# App Metadata
-
An **App** is a logical container that bundles objects, views, pages, and dashboards into a cohesive application experience. It defines the navigation structure, branding, and access permissions.
## Basic Structure
diff --git a/content/docs/ui/audience-based-interfaces.mdx b/content/docs/ui/audience-based-interfaces.mdx
index faa76ea42c..a8bdee9672 100644
--- a/content/docs/ui/audience-based-interfaces.mdx
+++ b/content/docs/ui/audience-based-interfaces.mdx
@@ -3,8 +3,6 @@ title: Audience-based interfaces
description: The same data serves different audiences. Give end users a curated app/page and keep builder surfaces — Studio, raw object tables, automation config — out of their view. Separate consumer and builder paths by default.
---
-# Audience-based interfaces
-
## Scenario
> The same object is touched by two very different audiences: **builders** who design the schema and **end users** who just enter and read data. How do I give each the right surface — without end users seeing (and being confused by) the builder chrome?
diff --git a/content/docs/ui/create-vs-edit-form.mdx b/content/docs/ui/create-vs-edit-form.mdx
index 3e6314731a..bc300adc36 100644
--- a/content/docs/ui/create-vs-edit-form.mdx
+++ b/content/docs/ui/create-vs-edit-form.mdx
@@ -3,8 +3,6 @@ title: Create form ≠ edit form
description: The new-record form asks 5 fields; the full edit form shows 40 grouped into sections. Derive both from one flat field set; only hand-shape the create form when layout or flow genuinely diverges.
---
-# Create form ≠ edit form
-
## Scenario
> The form for **creating** a record should ask only a handful of fields (the essentials). The form for **editing** an existing record shows the full record, grouped into sections. How do I model this without maintaining two field lists that drift apart?
diff --git a/content/docs/ui/dashboards.mdx b/content/docs/ui/dashboards.mdx
index 08164f6dfd..e8942c5745 100644
--- a/content/docs/ui/dashboards.mdx
+++ b/content/docs/ui/dashboards.mdx
@@ -3,8 +3,6 @@ title: Dashboard Metadata
description: Build analytics dashboards with chart widgets, global filters, and auto-refresh
---
-# Dashboard Metadata
-
A **Dashboard** defines an analytics page with chart widgets, key metrics, and data visualizations. Dashboards support configurable layouts, global date filters, and auto-refresh.
## Basic Structure
diff --git a/content/docs/ui/doc-pages.mdx b/content/docs/ui/doc-pages.mdx
index bc40c78265..8e9d478cfe 100644
--- a/content/docs/ui/doc-pages.mdx
+++ b/content/docs/ui/doc-pages.mdx
@@ -3,8 +3,6 @@ title: Doc Metadata
description: Ship package documentation as metadata — flat src/docs/*.md files compiled into the manifest and rendered in the console, ordered by a book navigation spine
---
-# Doc Metadata
-
A **Doc** is a single page of package documentation. You write plain
Markdown files in a flat `src/docs/` directory; `os build` compiles each
one into a `doc` metadata item that ships inside the package artifact and
diff --git a/content/docs/ui/field-grouping-and-order.mdx b/content/docs/ui/field-grouping-and-order.mdx
index abe7db36cc..c4abacabde 100644
--- a/content/docs/ui/field-grouping-and-order.mdx
+++ b/content/docs/ui/field-grouping-and-order.mdx
@@ -3,8 +3,6 @@ title: Field grouping & order
description: The data model is a flat field set, but forms need sections. Where grouping actually lives — semantic field.group vs form sections vs a table's row grouping — and why those three "groups" are different things.
---
-# Field grouping & order
-
## Scenario
> From the object-definition angle, fields are **flat**. In a table they're flat columns too. But a form needs them **grouped** into sections. Where does the grouping live — on the field, or on the form? And do I have to re-group on every form?
diff --git a/content/docs/ui/forms.mdx b/content/docs/ui/forms.mdx
index 8db251f4a4..a227915a07 100644
--- a/content/docs/ui/forms.mdx
+++ b/content/docs/ui/forms.mdx
@@ -3,7 +3,7 @@ title: Forms (Public + Internal)
description: Render any FormView either publicly (anonymous, /f/:slug) or internally (authed operators, /forms/:name). The same metadata drives both, with URL prefill, configurable post-submit behavior, and declarative open-form actions.
---
-# Forms
+## Forms
ObjectStack Forms are Airtable-style **metadata-driven** forms with two render modes that share **one** spec and **one** renderer:
diff --git a/content/docs/ui/index.mdx b/content/docs/ui/index.mdx
index d9e11ec51b..3a8454334d 100644
--- a/content/docs/ui/index.mdx
+++ b/content/docs/ui/index.mdx
@@ -3,8 +3,6 @@ title: UI Engine
description: Apps, views, dashboards, branding, and public forms — server-driven UI declared as metadata and rendered by the ObjectUI runtime.
---
-# UI Engine
-
The UI engine turns metadata into user interfaces: you declare **apps**, **views**, **pages**, and **dashboards** as data, and any compliant renderer draws them. This module is the practical documentation for the **ObjectUI layer** — the UI Protocol specified normatively in the [ObjectUI spec](/docs/protocol/objectui). Because the UI is data, it ships inside the same artifact as your schema, respects the same permissions, and can be generated or modified by AI agents through the same typed surface.
An app is a navigation shell over your objects (from the CRM example app):
diff --git a/content/docs/ui/pages.mdx b/content/docs/ui/pages.mdx
index 47cadf0fc4..38192b10eb 100644
--- a/content/docs/ui/pages.mdx
+++ b/content/docs/ui/pages.mdx
@@ -3,8 +3,6 @@ title: Page Metadata
description: Build custom pages with component-based layouts, variables, and event handling
---
-# Page Metadata
-
A **Page** defines a custom UI layout using components, regions, and variables. Unlike Views which are bound to a single Object, Pages are flexible containers that can combine multiple components, embed views, and manage local state.
## Basic Structure
diff --git a/content/docs/ui/public-data-collection.mdx b/content/docs/ui/public-data-collection.mdx
index 0d9e91a411..e003460986 100644
--- a/content/docs/ui/public-data-collection.mdx
+++ b/content/docs/ui/public-data-collection.mdx
@@ -3,8 +3,6 @@ title: Collect data from the public
description: Expose one form to anonymous visitors (web-to-lead, contact-us, intake) without opening the underlying base. Authorization is derived from the form's own declaration; only whitelisted fields are accepted.
---
-# Collect data from the public
-
## Scenario
> I want anonymous visitors to submit a form — a "Contact us" / "Request a demo" / intake form — that creates a record. But I must **not** give them access to the base, and they must only be able to set the fields on the form.
diff --git a/content/docs/ui/react-pages.mdx b/content/docs/ui/react-pages.mdx
index b4ec8fb31a..192c750dc5 100644
--- a/content/docs/ui/react-pages.mdx
+++ b/content/docs/ui/react-pages.mdx
@@ -3,8 +3,6 @@ title: React Pages
description: Author a page body as real React (kind:'react') or as constrained JSX that is parsed and never executed (kind:'html') — the two source-authoring tiers, and how to choose
---
-# React Pages
-
Most pages are a **schema tree**: `regions[].components[]` of JSON nodes, described in
[Page Metadata](/docs/ui/pages). Two page kinds let you write the body as a **source
string** instead, for layouts and interactions the fixed schema cannot express.
diff --git a/content/docs/ui/reports.mdx b/content/docs/ui/reports.mdx
index 24b1c919bb..54ce5bdaca 100644
--- a/content/docs/ui/reports.mdx
+++ b/content/docs/ui/reports.mdx
@@ -3,8 +3,6 @@ title: Report Metadata
description: Analytics reports as metadata — the four report shapes, dataset binding, drill-through, and how a report differs from a list view and a dashboard widget
---
-# Report Metadata
-
A **Report** is an analytics artifact. It groups and aggregates the rows of a
[dataset](/docs/data-modeling/analytics) into a pivot — grouped down-axis rows,
optionally pivoted across a second axis, with measures in the cells — and gives that
diff --git a/content/docs/ui/setup-app.mdx b/content/docs/ui/setup-app.mdx
index 54ecd774a9..ca6baf5dbd 100644
--- a/content/docs/ui/setup-app.mdx
+++ b/content/docs/ui/setup-app.mdx
@@ -3,8 +3,6 @@ title: Setup App
description: The platform's built-in administration UI — what it is, what it shows, and how it is wired
---
-# Setup App
-
The **Setup App** (`/apps/setup`) is the built-in administration console
for every ObjectStack project. It surfaces the platform `sys_*` objects,
the System Overview dashboard, and a set of settings pages behind a
diff --git a/content/docs/ui/translations.mdx b/content/docs/ui/translations.mdx
index 0c48219455..94176a9c6e 100644
--- a/content/docs/ui/translations.mdx
+++ b/content/docs/ui/translations.mdx
@@ -3,8 +3,6 @@ title: Translations
description: Labels and UI text as metadata — one bundle per locale, resolved per request, with CLI tooling to draft and to gate coverage in CI.
---
-# Translations
-
Every label in an ObjectStack app — object and field names, picklist options,
view titles, action buttons, app navigation — is metadata, not a string baked
into a component. Adding a language means adding a **bundle**, not touching the
diff --git a/content/docs/ui/views.mdx b/content/docs/ui/views.mdx
index 2acebdacce..ff0068ccae 100644
--- a/content/docs/ui/views.mdx
+++ b/content/docs/ui/views.mdx
@@ -3,8 +3,6 @@ title: View Metadata
description: Configure list views and form views — grid, kanban, calendar, gantt, and more
---
-# View Metadata
-
A **View** defines how records of an Object are displayed to users. ObjectStack supports two main view categories: **List Views** for browsing records and **Form Views** for editing individual records.
## The `defineView` Container
diff --git a/package.json b/package.json
index fa949f9228..f80db02cce 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"check:stack-collection-maps": "node scripts/check-stack-collection-maps.mjs --self-test && node scripts/check-stack-collection-maps.mjs",
"check:doc-authoring": "node scripts/check-doc-authoring.mjs --self-test && node scripts/check-doc-authoring.mjs",
"check:doc-anchors": "node scripts/check-doc-anchors.mjs --self-test && node scripts/check-doc-anchors.mjs",
+ "check:docs-single-h1": "node scripts/check-docs-single-h1.mjs --self-test && node scripts/check-docs-single-h1.mjs",
"check:docs-audit-scope": "node scripts/docs-audit/affected-docs.mjs --self-test && node scripts/docs-audit/check-audit-scope.mjs --self-test && node scripts/docs-audit/check-audit-scope.mjs",
"check:docs-redirects": "node scripts/check-docs-redirects.mjs --self-test && node scripts/check-docs-redirects.mjs",
"check:docs-image-tag": "node scripts/check-docs-image-tag.mjs --self-test && node scripts/check-docs-image-tag.mjs",
diff --git a/scripts/check-docs-single-h1.mjs b/scripts/check-docs-single-h1.mjs
new file mode 100644
index 0000000000..4a3b92bfaf
--- /dev/null
+++ b/scripts/check-docs-single-h1.mjs
@@ -0,0 +1,437 @@
+#!/usr/bin/env node
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+//
+// check-docs-single-h1 (#12236) — no page under `content/docs/**` may contain a
+// body-level `# ` heading, because the site already renders one `` per page
+// from the frontmatter `title` and a second one is not authorable, it is a bug.
+//
+// node scripts/check-docs-single-h1.mjs
+// node scripts/check-docs-single-h1.mjs --list # every page and its verdict
+// node scripts/check-docs-single-h1.mjs --self-test # prove the battery can go red
+//
+// ## The invariant, and where the other `` comes from
+//
+// `apps/docs/app/[lang]/docs/[[...slug]]/page.tsx` renders every doc page as
+//
+// {page.data.title}
+// ...
+//
+//
+// `DocsTitle` is an ``. So the frontmatter `title` IS the page's heading-one,
+// unconditionally, on all 403 pages — and any `# ` the author writes in the body
+// compiles to a SECOND `` inside `DocsBody`. Measured on the dev server at
+// `/docs/data-modeling/objects` before this gate landed: two `` elements,
+// both reading `Object Metadata`, because the page's frontmatter said
+// `title: Object Metadata` and its body opened with `# Object Metadata`.
+//
+// That was true of 195 of the 403 pages, and on 138 of those the two `` were
+// the same rendered string. Two `` split the strongest on-page signal a
+// document has and a screen reader announces the title twice.
+//
+// The remedy has two shapes and this gate names which one applies (see
+// `remedyFor`): when the body heading renders the same text as the title, DELETE
+// it — the page loses nothing. When it says something different, DEMOTE it to
+// `## `, which keeps the wording and keeps the slug, so no inbound `#anchor`
+// breaks. Levels below one are never a finding: this gate has exactly one
+// opinion and `check:doc-anchors` owns the rest of the heading surface.
+//
+// ## Why fenced code is stripped FIRST, and why that is the whole gate
+//
+// This is the measurement that decides whether this file is a gate or a hazard.
+// A naive `^# ` scan over `content/docs/**` reports 205 files. The real number is
+// 195. The ten-file gap is entirely shell and YAML comments inside fenced code
+// blocks — they open a line with `# ` and render as CODE, not as a heading:
+//
+// content/docs/getting-started/index.mdx # Install pnpm globally
+// content/docs/protocol/objectql/index.mdx # customer.object.yml
+// content/docs/upgrading.mdx # docker-compose.yml, or your ...
+// ...and seven more
+//
+// A fence-blind gate does not merely over-report by ten. It instructs an author
+// — or a script-driven sweep, which is how a 195-file change gets made — to
+// delete or demote a line inside a working example, silently corrupting ten
+// copy-pasteable snippets to satisfy a check about HTML that those lines never
+// produced. The false positive here is more expensive than the defect.
+//
+// So `stripFencedBlocks` runs before anything looks for a heading, and the
+// self-test carries a control for both fence spellings (``` and ~~~).
+//
+// ## Why nothing here re-models Markdown
+//
+// Three things had to be decided — where the frontmatter ends, where a fenced
+// block ends, and what text a heading renders as — and this repo already owns an
+// answer to each. `stripFrontmatter` and `flattenHeadingText` are imported from
+// `check-doc-anchors.mjs` and `stripFencedBlocks` from `check-adr-links.mjs`;
+// none of the three is retyped here.
+//
+// That is not tidiness, it is the correctness argument. `flattenHeadingText` was
+// diffed against the real fumadocs pipeline over the whole corpus (397 files,
+// 6955 headings, zero disagreements — see that file's header) and it is what
+// makes the delete-vs-demote call right on nine pages where the two headings
+// differ ONLY by inline markup:
+//
+// content/docs/kernel/runtime-services/audit-service.mdx
+// title: services.audit h1: `services.audit`
+//
+// Byte-compared, those two strings differ and the remedy reads "demote" — which
+// would leave a `## services.audit` sitting directly under an `services.audit`,
+// preserving the duplicate this gate exists to remove, one level down. Compared
+// as RENDERED TEXT they are equal and the remedy is "delete", which is correct.
+// A second, private heading-text model in this file would have gotten those nine
+// wrong, and a gate disagreeing with the renderer about what a page says is the
+// same class of defect as no gate at all.
+//
+// ## Scope: two trees are excluded, and the exclusions are self-retiring
+//
+// `EXCLUSIONS` carves out two SUBTREES — never individual files. A file
+// allowlist is where new failures go to be forgotten; a subtree with a named
+// owner is a statement about who fixes that tree.
+//
+// - `content/docs/references/**` — 100% generated by
+// `packages/spec/scripts/build-docs.ts` from JSDoc file headers in
+// `packages/spec/src/**`. Its own required gate (`check:docs`) regenerates
+// the tree and fails on any difference, so a hand-edit here does not merely
+// get reverted at the next generator run — it ships CI-red immediately. The
+// 38 pages that still violate are owned by #12249, in the generator.
+// - `content/docs/releases/**` — `CLAUDE.md` puts a hard stop on this
+// directory, and three of its four violations are not mechanical: the `# `
+// headings in `v15`/`v16`/`v17.mdx` are real top-level dividers with `##`
+// children, so demoting one makes it a sibling of its own subsections. Owned
+// by #12250.
+//
+// **An exclusion whose tree is CLEAN is a failure, not a pass.** When #12249 or
+// #12250 lands, this gate goes red with `DEAD-EXCLUSION` and asks for its own
+// carve-out to be deleted. Without that limb the two exclusions would outlive
+// their reasons and quietly shrink the gate's scope by 42 pages forever — the
+// vacuous-green shape (#4690) that this repo treats as worse than no check.
+//
+// The same reasoning gives the corpus-level limb: a run that reads ZERO pages
+// exits 1. "Nothing differs" must never be reachable by checking nothing.
+//
+// ## Why this cannot be a `check:doc-authoring` rule
+//
+// `check-doc-authoring.mjs` judges CODE BLOCKS in docs; this judges the prose
+// around them, and its central operation — strip the code blocks first — is that
+// gate's population inverted. Sharing a file would mean one scanner holding both
+// a "look only inside fences" and a "look only outside fences" mode, which is
+// two gates in a trench coat.
+//
+// ## Cost
+//
+// One filesystem read of 403 files and a line scan. Sub-second, no build, no
+// network. It reads `yaml` only to name the remedy — see `remedyFor` — and the
+// finding itself does not depend on the frontmatter parsing at all.
+
+import { readFileSync, readdirSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, join, relative, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { requireDependency } from './import-prerequisite.mjs';
+const { parse } = await requireDependency('yaml', () => import('yaml'), import.meta.url, {
+ measures: 'any page under content/docs/** contains a body-level `# ` heading',
+});
+
+import { stripFencedBlocks } from './check-adr-links.mjs';
+import { flattenHeadingText, stripFrontmatter } from './check-doc-anchors.mjs';
+import { isEntrypoint } from './invoked-as.mjs';
+
+const HERE = resolve(fileURLToPath(import.meta.url), '..');
+const REPO_ROOT = resolve(HERE, '..');
+
+/**
+ * The population this gate sweeps, as the repo-relative subtree it really reads.
+ *
+ * Spelled as a subtree WITH a separator on purpose: `scripts/pm/dispatch-gates.mjs`
+ * derives a dispatch's gate list by scanning each gate for the path literals it
+ * operates on, and refuses a literal with no separator as a WORD rather than a
+ * path. Spelled `'content'` this gate would contribute no hint at all and would
+ * score `silent` for every card editing a doc page — the same gap #9626 closed
+ * for `check:doc-anchors`.
+ */
+const PAGE_GLOB = 'content/docs/**';
+
+/** The tree `PAGE_GLOB` names, derived from it so the two cannot drift apart. */
+const PAGE_ROOT = PAGE_GLOB.slice(0, PAGE_GLOB.lastIndexOf('/'));
+
+/** `.mdx` only: `defineDocs` compiles these, and `content/docs` holds nothing else. */
+const PAGE_EXTENSION = '.mdx';
+
+/**
+ * Subtrees this gate does not judge, each with the issue that will make it
+ * empty. Read the header's "Scope" section before adding a third: an entry
+ * whose tree is clean FAILS, so every line here has a finite life.
+ */
+const EXCLUSIONS = [
+ {
+ prefix: 'content/docs/references/',
+ owner: '#12249',
+ why: 'generated by packages/spec/scripts/build-docs.ts from JSDoc file headers; `check:docs` fails on any hand-edit',
+ },
+ {
+ prefix: 'content/docs/releases/',
+ owner: '#12250',
+ why: 'CLAUDE.md stops code PRs at this directory, and three of the four violations need a cascading demotion',
+ },
+];
+
+/** ATX heading: CommonMark allows up to three leading spaces. */
+const ATX_HEADING = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$/;
+
+/** Fumadocs' custom-id suffix — `## Heading [#custom-id]`, remark-heading.ts. */
+const CUSTOM_ID = /\s*\[#([^\]]+?)]\s*$/;
+
+/* ------------------------------------------------------------------ scanning */
+
+/** Every `.mdx` under `dir`, recursively, in a stable order. */
+export function listPages(dir, out = []) {
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
+ const full = join(dir, entry.name);
+ if (entry.isDirectory()) listPages(full, out);
+ else if (entry.name.endsWith(PAGE_EXTENSION)) out.push(full);
+ }
+ return out;
+}
+
+/**
+ * Every body-level `# ` heading one page contains, as `{ line, raw, text }`.
+ *
+ * Frontmatter and fenced code are blanked first, both line-count preserving, so
+ * a reported line number indexes the real file. See the header for why the fence
+ * pass is the load-bearing half.
+ */
+export function bodyH1s(markdown) {
+ const lines = stripFencedBlocks(stripFrontmatter(markdown)).split('\n');
+ const found = [];
+ lines.forEach((line, i) => {
+ const m = ATX_HEADING.exec(line);
+ if (!m || m[1].length !== 1) return;
+ const raw = (m[2] ?? '').replace(/\s+#+\s*$/, '');
+ found.push({ line: i + 1, raw: line, text: flattenHeadingText(raw.replace(CUSTOM_ID, '')) });
+ });
+ return found;
+}
+
+/** The frontmatter `title`, or `null` when there is none this gate can read. */
+export function frontmatterTitle(markdown) {
+ const m = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(\r?\n|$)/.exec(markdown);
+ if (!m) return null;
+ try {
+ const doc = parse(m[1]);
+ return doc && typeof doc.title === 'string' ? doc.title : null;
+ } catch {
+ // A page whose frontmatter does not parse is `check:doc-frontmatter`'s
+ // finding, not this gate's. The heading is still reported; only the remedy
+ // hint degrades.
+ return null;
+ }
+}
+
+/**
+ * Which of the two fixes this heading wants.
+ *
+ * A HINT attached to a finding, never part of the verdict: the finding is "this
+ * page contains a body `# ` heading", which is true whatever the frontmatter
+ * says. Comparison is on RENDERED text, trimmed and case-folded — see the
+ * header for the nine pages that turn on exactly this.
+ */
+export function remedyFor(headingText, title) {
+ const norm = (s) => (s ?? '').trim().toLowerCase();
+ if (title !== null && norm(headingText) === norm(title)) {
+ return { kind: 'delete', detail: 'renders the same text as the frontmatter title — delete the line' };
+ }
+ return { kind: 'demote', detail: 'says something the title does not — demote it to `## ` (keeps the wording and the slug)' };
+}
+
+/** The exclusion covering `relPath`, or `null`. */
+function exclusionFor(relPath) {
+ return EXCLUSIONS.find((e) => relPath.startsWith(e.prefix)) ?? null;
+}
+
+/**
+ * Judge one tree. Returns `{ findings, excluded, scanned }` — `excluded` is
+ * keyed by exclusion prefix so the caller can enforce the dead-exclusion rule.
+ */
+export function scanTree(root, repoRoot = REPO_ROOT) {
+ const findings = [];
+ const excluded = new Map(EXCLUSIONS.map((e) => [e.prefix, []]));
+ let scanned = 0;
+
+ for (const abs of listPages(root)) {
+ const rel = relative(repoRoot, abs).split('\\').join('/');
+ const src = readFileSync(abs, 'utf8');
+ const headings = bodyH1s(src);
+ const exclusion = exclusionFor(rel);
+ if (exclusion) {
+ if (headings.length) excluded.get(exclusion.prefix).push({ rel, headings });
+ continue;
+ }
+ scanned += 1;
+ if (!headings.length) continue;
+ const title = frontmatterTitle(src);
+ for (const h of headings) {
+ findings.push({ rel, line: h.line, raw: h.raw.trim(), ...remedyFor(h.text, title) });
+ }
+ }
+ return { findings, excluded, scanned };
+}
+
+/* ---------------------------------------------------------------------- main */
+
+function main(argv) {
+ const root = join(REPO_ROOT, PAGE_ROOT);
+ const { findings, excluded, scanned } = scanTree(root);
+
+ if (argv.includes('--list')) {
+ for (const abs of listPages(root)) {
+ const rel = relative(REPO_ROOT, abs).split('\\').join('/');
+ const ex = exclusionFor(rel);
+ const n = bodyH1s(readFileSync(abs, 'utf8')).length;
+ console.log(`${ex ? 'EXCLUDED' : n ? 'FINDING ' : 'ok '} ${rel}${n ? ` (${n} body h1)` : ''}`);
+ }
+ return 0;
+ }
+
+ // Anti-vacuity: reading nothing must never read as success.
+ if (scanned === 0) {
+ console.error(
+ `✗ check-docs-single-h1 read ZERO judgeable pages under ${PAGE_ROOT}/.\n` +
+ ` Nothing was measured, so this exit code says nothing about the corpus.\n` +
+ ` Either the tree moved, or every page fell into an EXCLUSIONS subtree.`,
+ );
+ return 1;
+ }
+
+ let failed = false;
+
+ if (findings.length) {
+ failed = true;
+ console.error(`✗ ${findings.length} body-level \`# \` heading(s) in ${PAGE_ROOT}/ — each renders a SECOND :\n`);
+ for (const f of findings) {
+ console.error(` ${f.rel}:${f.line}`);
+ console.error(` ${f.raw}`);
+ console.error(` → ${f.kind.toUpperCase()}: ${f.detail}\n`);
+ }
+ console.error(
+ ` \`DocsTitle\` already renders the frontmatter \`title\` as this page's \n` +
+ ` (apps/docs/app/[lang]/docs/[[...slug]]/page.tsx). A body \`# \` is a second one.\n`,
+ );
+ }
+
+ // A carve-out that no longer carves anything out must be deleted, in the same
+ // change that made it empty. See the header's "Scope" section.
+ for (const e of EXCLUSIONS) {
+ const hits = excluded.get(e.prefix);
+ if (hits.length) {
+ console.error(`ℹ EXCLUDED ${e.prefix}** — ${hits.length} page(s) still carry a body \`# \` heading, owned by ${e.owner}`);
+ continue;
+ }
+ failed = true;
+ console.error(
+ `✗ DEAD-EXCLUSION: ${e.prefix}** is clean, so its EXCLUSIONS entry checks nothing.\n` +
+ ` ${e.owner} appears to have landed. Delete that entry from scripts/check-docs-single-h1.mjs\n` +
+ ` in the same change, so the gate's scope grows back with the fix.\n`,
+ );
+ }
+
+ if (failed) return 1;
+ console.log(
+ `✓ check-docs-single-h1: ${scanned} page(s) under ${PAGE_ROOT}/ carry no body-level \`# \` heading ` +
+ `(${EXCLUSIONS.length} subtree(s) excluded, see --list).`,
+ );
+ return 0;
+}
+
+/* ----------------------------------------------------------------- self-test */
+
+/**
+ * Positive AND negative controls over a real temp tree, running the real
+ * `scanTree`. A live corpus that is green cannot tell a working gate from a
+ * blind one, and this gate lands with its corpus green by construction.
+ */
+export function selfTest() {
+ const cases = [];
+ const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail });
+
+ const dir = mkdtempSync(join(tmpdir(), 'docs-single-h1-'));
+ try {
+ const write = (rel, body) => {
+ const abs = join(dir, rel);
+ mkdirSync(dirname(abs), { recursive: true });
+ writeFileSync(abs, body);
+ };
+ const fm = (title) => `---\ntitle: ${title}\n---\n\n`;
+
+ write('content/docs/clean.mdx', `${fm('Clean Page')}Prose with no heading-one.\n\n## A section\n`);
+ write('content/docs/dup.mdx', `${fm('Object Metadata')}# Object Metadata\n\nProse.\n`);
+ write('content/docs/differs.mdx', `${fm('Command Line Interface')}# @objectstack/cli\n\nProse.\n`);
+ // THE control: the ten real pages whose only `# ` line is a shell comment.
+ write('content/docs/fenced.mdx', `${fm('Fenced')}Prose.\n\n\`\`\`bash\n# Install pnpm globally\nnpm i -g pnpm\n\`\`\`\n`);
+ write('content/docs/fenced-tilde.mdx', `${fm('Tilde')}Prose.\n\n~~~yaml\n# customer.object.yml\nname: customer\n~~~\n`);
+ // Inline-markup-only difference: renders identically, so DELETE, not demote.
+ write('content/docs/inline-code.mdx', `${fm('services.audit')}# \`services.audit\`\n\nProse.\n`);
+ // Not headings: no space after the hash, and a level-two heading.
+ write('content/docs/not-a-heading.mdx', `${fm('Nope')}#NotAHeading\n\n## A real section\n`);
+ // CommonMark allows up to three leading spaces.
+ write('content/docs/indented.mdx', `${fm('Indented')} # Indented Heading\n\nProse.\n`);
+ // A `# ` in the frontmatter block itself is not body content.
+ write('content/docs/fm-hash.mdx', `---\ntitle: Hashy\ndescription: "# not a heading"\n---\n\nProse.\n`);
+ // Both excluded subtrees, each dirty — the live shape.
+ write('content/docs/references/gen.mdx', `${fm('Gen')}# Why this lives in \`spec\`\n\nProse.\n`);
+ write('content/docs/releases/v9.mdx', `${fm('v9.0.0')}# 9.0.0 in detail\n\nProse.\n`);
+
+ const root = join(dir, 'content/docs');
+ const { findings, excluded, scanned } = scanTree(root, dir);
+ const at = (rel) => findings.filter((f) => f.rel === rel);
+
+ t('a page with no body h1 is not a finding', at('content/docs/clean.mdx').length === 0);
+ t('a body h1 repeating the title is a DELETE finding', at('content/docs/dup.mdx')[0]?.kind === 'delete', JSON.stringify(at('content/docs/dup.mdx')));
+ t('a body h1 differing from the title is a DEMOTE finding', at('content/docs/differs.mdx')[0]?.kind === 'demote', JSON.stringify(at('content/docs/differs.mdx')));
+ t('a `# ` inside a ``` fence is NOT a finding', at('content/docs/fenced.mdx').length === 0, JSON.stringify(at('content/docs/fenced.mdx')));
+ t('a `# ` inside a ~~~ fence is NOT a finding', at('content/docs/fenced-tilde.mdx').length === 0, JSON.stringify(at('content/docs/fenced-tilde.mdx')));
+ t(
+ 'a heading differing from the title ONLY by inline code is a DELETE finding',
+ at('content/docs/inline-code.mdx')[0]?.kind === 'delete',
+ JSON.stringify(at('content/docs/inline-code.mdx')),
+ );
+ t('`#NoSpace` is not a heading', at('content/docs/not-a-heading.mdx').length === 0);
+ t('a `## ` heading is never a finding', findings.every((f) => f.raw.startsWith('#') && !f.raw.startsWith('##')));
+ t('an h1 indented up to three spaces IS a finding', at('content/docs/indented.mdx').length === 1, JSON.stringify(at('content/docs/indented.mdx')));
+ t('a `# ` inside the frontmatter block is not body content', at('content/docs/fm-hash.mdx').length === 0);
+ t('the reported line number indexes the real file', at('content/docs/dup.mdx')[0]?.line === 5, JSON.stringify(at('content/docs/dup.mdx')));
+ t('an excluded subtree yields no findings', findings.every((f) => !exclusionFor(f.rel)));
+ t('...but its violations are still counted', excluded.get('content/docs/references/').length === 1 && excluded.get('content/docs/releases/').length === 1);
+ t('only judgeable pages are counted as scanned', scanned === 9, `scanned=${scanned}`);
+
+ // The dead-exclusion limb: a clean excluded tree must be reported empty so
+ // `main` can fail on it.
+ rmSync(join(dir, 'content/docs/references/gen.mdx'));
+ const after = scanTree(root, dir);
+ t('a CLEAN excluded subtree reports zero — the dead-exclusion trigger', after.excluded.get('content/docs/references/').length === 0);
+
+ // Anti-vacuity: a tree of nothing but excluded pages scans zero pages.
+ const empty = mkdtempSync(join(tmpdir(), 'docs-single-h1-empty-'));
+ try {
+ mkdirSync(join(empty, 'content/docs/releases'), { recursive: true });
+ writeFileSync(join(empty, 'content/docs/releases/v1.mdx'), `${fm('v1')}# 1.0.0 in detail\n`);
+ t('a corpus of only excluded pages scans ZERO', scanTree(join(empty, 'content/docs'), empty).scanned === 0);
+ } finally {
+ rmSync(empty, { recursive: true, force: true });
+ }
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+
+ const failed = cases.filter((c) => !c.ok);
+ for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
+ if (failed.length) {
+ console.error(`✗ check-docs-single-h1 self-test: ${failed.length} of ${cases.length} case(s) failed.`);
+ return 1;
+ }
+ console.log(`✓ check-docs-single-h1 self-test: ${cases.length} cases pass (both fence spellings, inline-code equality, indentation, and both anti-vacuity limbs).`);
+ return 0;
+}
+
+if (isEntrypoint(import.meta.url)) {
+ process.exit(process.argv.includes('--self-test') ? selfTest() : main(process.argv.slice(2)));
+}