diff --git a/content/docs/references/api/dispatcher.mdx b/content/docs/references/api/dispatcher.mdx index 15cf69a75f..a4dcbe884a 100644 --- a/content/docs/references/api/dispatcher.mdx +++ b/content/docs/references/api/dispatcher.mdx @@ -5,7 +5,7 @@ description: Dispatcher protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# HttpDispatcher Protocol +## HttpDispatcher Protocol Defines how the ObjectStack HttpDispatcher routes incoming API requests to the correct kernel service based on URL prefix matching. diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx index b1ea120a07..3d70ea305d 100644 --- a/content/docs/references/api/package-api.mdx +++ b/content/docs/references/api/package-api.mdx @@ -5,7 +5,7 @@ description: Package Api protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Package API Protocol +## Package API Protocol REST API endpoint schemas for package lifecycle management. diff --git a/content/docs/references/api/versioning.mdx b/content/docs/references/api/versioning.mdx index 4fcc2825cf..c9a8fae0e9 100644 --- a/content/docs/references/api/versioning.mdx +++ b/content/docs/references/api/versioning.mdx @@ -5,7 +5,7 @@ description: Versioning protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# API Versioning Protocol +## API Versioning Protocol Defines how API versions are negotiated between client and server. Supports multiple versioning strategies and deprecation lifecycle management. diff --git a/content/docs/references/cloud/app-store.mdx b/content/docs/references/cloud/app-store.mdx index 1fe4636ee3..99f3ac328b 100644 --- a/content/docs/references/cloud/app-store.mdx +++ b/content/docs/references/cloud/app-store.mdx @@ -5,22 +5,22 @@ description: App Store protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# App Store Protocol (Customer Experience) +## App Store Protocol (Customer Experience) Defines schemas for the end-customer experience when browsing, evaluating, installing, and managing marketplace apps from within ObjectOS. -## Architecture Alignment +### Architecture Alignment - **Salesforce AppExchange (Customer)**: Browse apps, read reviews, 1-click install - **Shopify App Store (Merchant)**: App evaluation, trial, install, manage subscriptions - **Apple App Store (User)**: Ratings, reviews, featured collections, personalized recs -## Customer Journey +### Customer Journey ``` Discover → Evaluate → Install → Configure → Use → Rate/Review → Manage ``` -## Key Concepts +### Key Concepts - **Reviews & Ratings**: User-submitted ratings and reviews with moderation - **Collections & Recommendations**: Personalized discovery and curated picks - **Subscription Management**: Manage licenses, billing, and renewals diff --git a/content/docs/references/cloud/developer-portal.mdx b/content/docs/references/cloud/developer-portal.mdx index 6c97ac7b93..15d37bd079 100644 --- a/content/docs/references/cloud/developer-portal.mdx +++ b/content/docs/references/cloud/developer-portal.mdx @@ -5,7 +5,7 @@ description: Developer Portal protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Developer Portal Protocol +## Developer Portal Protocol Defines schemas for the developer-facing side of the marketplace ecosystem. Covers the complete developer journey: @@ -14,12 +14,12 @@ Covers the complete developer journey: Register → Create App → Develop → Validate → Build → Submit → Monitor → Iterate ``` -## Architecture Alignment +### Architecture Alignment - **Salesforce Partner Portal**: ISV registration, AppExchange publishing, Trialforce - **Shopify Partner Dashboard**: App management, analytics, billing - **VS Code Marketplace Management**: Extension publishing, statistics, tokens -## Identity Integration (better-auth) +### Identity Integration (better-auth) Authentication, organization management, and API keys are handled by the Identity module (`@objectstack/spec` Identity namespace), which follows the better-auth specification. This module only defines marketplace-specific @@ -35,7 +35,7 @@ extensions on top of the shared identity layer: There is no `Identity.ApiKeySchema` — #8715 retired it (it documented better-auth's apiKey plugin, which this platform does not load) -## Key Concepts +### Key Concepts - **Publisher Profile**: Links an Identity Organization to a marketplace publisher - **App Listing Management**: CRUD for marketplace listings (draft → published) - **Version Channels**: alpha / beta / rc / stable release channels diff --git a/content/docs/references/cloud/environment-artifact.mdx b/content/docs/references/cloud/environment-artifact.mdx index c42862dfdd..8fbd7a8827 100644 --- a/content/docs/references/cloud/environment-artifact.mdx +++ b/content/docs/references/cloud/environment-artifact.mdx @@ -5,7 +5,7 @@ description: Environment Artifact protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Environment Artifact Envelope — re-export (#4740, #4535 C10) +## Environment Artifact Envelope — re-export (#4740, #4535 C10) The envelope has exactly ONE declaration: `../system/environment-artifact.zod` (maintainer route A′ on #4740 — diff --git a/content/docs/references/cloud/environment-package.mdx b/content/docs/references/cloud/environment-package.mdx index a4ba268ec7..385ca26204 100644 --- a/content/docs/references/cloud/environment-package.mdx +++ b/content/docs/references/cloud/environment-package.mdx @@ -5,7 +5,7 @@ description: Environment Package protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Environment Package Installation Protocol +## Environment Package Installation Protocol Models `sys_package_installation` — the pairing between an Environment and a specific, immutable `sys_package_version` snapshot. diff --git a/content/docs/references/cloud/environment.mdx b/content/docs/references/cloud/environment.mdx index 4731452c71..6ca8c281d5 100644 --- a/content/docs/references/cloud/environment.mdx +++ b/content/docs/references/cloud/environment.mdx @@ -5,7 +5,7 @@ description: Environment protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Environment Protocol (runtime container) +## Environment Protocol (runtime container) An **Environment** is the runtime container of an organization's data. It owns a physically isolated database, a canonical hostname, a plan/quota diff --git a/content/docs/references/cloud/marketplace-admin.mdx b/content/docs/references/cloud/marketplace-admin.mdx index 6b7e5b8821..a96fbee061 100644 --- a/content/docs/references/cloud/marketplace-admin.mdx +++ b/content/docs/references/cloud/marketplace-admin.mdx @@ -5,17 +5,17 @@ description: Marketplace Admin protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Marketplace Administration Protocol +## Marketplace Administration Protocol Defines schemas for the platform (Cloud) side of marketplace operations. Covers the administrative workflows for managing and governing the marketplace. -## Architecture Alignment +### Architecture Alignment - **Salesforce AppExchange Admin**: Security review, ISV monitoring, partner management - **Apple App Store Connect Review**: Human review process, guidelines, rejection reasons - **Google Play Console**: Policy enforcement, quality gates, content moderation -## Key Concepts +### Key Concepts - **Review Process**: Structured workflow for submission review (automated + manual) - **Curation**: Featured apps, curated collections, editorial picks - **Governance**: Policy enforcement, takedown, compliance diff --git a/content/docs/references/cloud/marketplace.mdx b/content/docs/references/cloud/marketplace.mdx index b6d16017e6..4e633feacf 100644 --- a/content/docs/references/cloud/marketplace.mdx +++ b/content/docs/references/cloud/marketplace.mdx @@ -5,7 +5,7 @@ description: Marketplace protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Marketplace Protocol +## Marketplace Protocol Defines the core schemas for the plugin marketplace ecosystem, covering: - **Developer Side**: Package publishing, submission, and version releases @@ -14,13 +14,13 @@ Defines the core schemas for the plugin marketplace ecosystem, covering: This protocol defines the contract between plugin developers, the marketplace platform, and customers who install plugins. -## Architecture Alignment +### Architecture Alignment - **Salesforce AppExchange**: Security review, managed packages, listing profiles - **VS Code Marketplace**: Extension publishing, ratings, verified publishers - **npm Registry**: Package publishing, versioning, scoped packages - **Shopify App Store**: App review process, billing integration, merchant installs -## Developer Publishing Flow +### Developer Publishing Flow ``` 1. Develop → Build the project locally using ObjectStack CLI 2. Validate → Run `os validate` (schema + security checks) @@ -30,7 +30,7 @@ platform, and customers who install plugins. 6. Publish → Approved listing goes live on marketplace ``` -## Platform Management Flow +### Platform Management Flow ``` 1. Receive → Accept submissions from verified publishers 2. Scan → Automated security scan and compatibility check diff --git a/content/docs/references/data/context-tokens.mdx b/content/docs/references/data/context-tokens.mdx index 9e34c9f1ce..c6976670c2 100644 --- a/content/docs/references/data/context-tokens.mdx +++ b/content/docs/references/data/context-tokens.mdx @@ -8,7 +8,7 @@ description: Context Tokens protocol schemas Context Tokens — the declarative placeholders that resolve against the **caller's session** (who am I, which org am I in) rather than the clock. -# Why this lives in `spec` +## Why this lives in `spec` These are the sibling vocabulary to `{date-macros}`. Filter values in dashboards, views, reports and pages travel as JSON, so a user-scoped @@ -38,7 +38,7 @@ neither is an ERROR, not a null comparand: resolving to `null` degrades to `IS NULL` on most drivers and would hand back the rows the filter was written to exclude. -# Presentation scope, NOT a security boundary +## Presentation scope, NOT a security boundary This is the single most important thing to understand about these tokens. `{current_user_id}` scopes what a surface *shows*; it does not @@ -60,7 +60,7 @@ Never reach for a context token to keep a user away from data. Removing a `{current_user_id}` filter widens a *view*; it must never widen *access*. -# Where the tokens are honoured +## Where the tokens are honoured Filter values on every surface that resolves placeholders — object list views, dashboard widgets, reports, SDUI page components. Navigation @@ -69,7 +69,7 @@ such as `{active_package}`; those are nav-only and are NOT valid inside filter values, because filters are not evaluated with the sidebar's selector state. -# Out of scope +## Out of scope - `current_user.*` RLS expressions — see `@objectstack/plugin-security`. - `{date-macros}` — the clock-based sibling; see `./date-macros.zod.ts`. diff --git a/content/docs/references/data/date-macros.mdx b/content/docs/references/data/date-macros.mdx index 16ae5053f8..4776613267 100644 --- a/content/docs/references/data/date-macros.mdx +++ b/content/docs/references/data/date-macros.mdx @@ -8,7 +8,7 @@ description: Date Macros protocol schemas Date Macro Tokens — the declarative placeholders the UI substitutes into filter values before sending a query to the data engine. -# Why this lives in `spec` +## Why this lives in `spec` Filter values in dashboards, views, reports and pages travel as JSON. Because JSON cannot evaluate code, callers cannot write `daysAgo(30)` @@ -52,7 +52,7 @@ so the **set of recognised tokens is part of the platform contract** and must live here next to the rest of the JSON-DSL schemas, not inside any single UI implementation. -# Two flavours of token +## Two flavours of token 1. **Fixed tokens** — small, finite list (`{today}`, `{current_quarter_start}`, `{last_year_end}`, …). Enumerated by @@ -63,7 +63,7 @@ inside any single UI implementation. `DATE_MACRO_PARAM_RE`. Units: `minute(s)`, `hour(s)`, `day(s)`, `week(s)`, `month(s)`, `year(s)`. Directions: `ago`, `from_now`. -# Out of scope +## Out of scope - CEL expressions (`cel\`daysAgo(30)\``) run **server-side** in the formula engine. They are unrelated to these placeholders; see diff --git a/content/docs/references/data/seed-loader.mdx b/content/docs/references/data/seed-loader.mdx index 490d0ca592..1d9e7c2722 100644 --- a/content/docs/references/data/seed-loader.mdx +++ b/content/docs/references/data/seed-loader.mdx @@ -5,17 +5,17 @@ description: Seed Loader protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Seed Loader Protocol +## Seed Loader Protocol Defines the schemas for metadata-driven seed data loading with automatic relationship resolution, dependency ordering, and multi-pass insertion. -## Architecture Alignment +### Architecture Alignment - **Salesforce Data Loader**: External ID-based upsert with relationship resolution - **ServiceNow**: Sys ID and display value mapping during import - **Airtable**: Linked record resolution via display names -## Loading Flow +### Loading Flow ``` 1. Build object dependency graph from field metadata (lookup/master_detail) 2. Topological sort → determine insert order (parents before children) diff --git a/content/docs/references/data/validation.mdx b/content/docs/references/data/validation.mdx index 202c38bc91..498f7f7362 100644 --- a/content/docs/references/data/validation.mdx +++ b/content/docs/references/data/validation.mdx @@ -5,12 +5,12 @@ description: Validation protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# ObjectStack Validation Protocol +## ObjectStack Validation Protocol This module defines the validation schema protocol for ObjectStack, providing a comprehensive type-safe validation system similar to Salesforce's validation rules but with enhanced capabilities. -## Overview +### Overview Validation rules are applied at the data layer to ensure data integrity and enforce business logic. A validation rule is a **deterministic, synchronous, side-effect-free predicate over a single @@ -30,7 +30,7 @@ The system supports these validation types: 5. **JSON Schema Validation**: Validate a JSON field against a JSON Schema 6. **Conditional Validation**: Apply a nested rule based on a CEL condition -## Deliberately NOT validation rules +### Deliberately NOT validation rules These were once declared here but never enforced. Because the contract above rules them out (they need I/O or are client-side concerns), they were removed rather than left as silent @@ -51,7 +51,7 @@ no-ops. Use the layer that already does each one correctly: a `delete` event was a proven silent no-op — the enum value was removed rather than left advertised-but-unenforced (#3184; see docs/audits/2026-06-validationschema-property-liveness.md). -## Salesforce Comparison +### Salesforce Comparison ObjectStack validation rules are inspired by Salesforce validation rules but enhanced: - Salesforce: Formula-based validation with `Error Condition Formula` diff --git a/content/docs/references/identity/scim.mdx b/content/docs/references/identity/scim.mdx index 4a10b5f2b6..5442b93015 100644 --- a/content/docs/references/identity/scim.mdx +++ b/content/docs/references/identity/scim.mdx @@ -5,18 +5,18 @@ description: Scim protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# SCIM 2.0 Protocol Implementation +## SCIM 2.0 Protocol Implementation System for Cross-domain Identity Management (SCIM) 2.0 specification implementation for ObjectStack. -## Overview +### Overview SCIM 2.0 is an HTTP-based protocol for managing user and group identities across domains. It provides a standardized REST API for user provisioning, de-provisioning, and synchronization. -## Use Cases +### Use Cases 1. **Enterprise SSO Integration** - Integrate with Okta, Azure AD, OneLogin @@ -38,13 +38,13 @@ de-provisioning, and synchronization. - Track all identity changes - Meet SOX/HIPAA requirements for user management -## Specification References +### Specification References - **RFC 7643**: SCIM Core Schema - **RFC 7644**: SCIM Protocol - **RFC 7642**: SCIM Requirements -## Industry Implementations +### Industry Implementations - **Okta**: Leading SCIM provider - **Azure AD**: Microsoft's identity platform diff --git a/content/docs/references/kernel/cli-extension.mdx b/content/docs/references/kernel/cli-extension.mdx index c48f11b1e0..7ff473752d 100644 --- a/content/docs/references/kernel/cli-extension.mdx +++ b/content/docs/references/kernel/cli-extension.mdx @@ -5,14 +5,14 @@ description: Cli Extension protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# CLI Extension Protocol +## CLI Extension Protocol Defines the contract for plugins that extend the ObjectStack CLI with custom commands. This enables third-party packages (e.g., marketplace, cloud deployment tools) to register new CLI commands via oclif's built-in plugin system. -## How It Works (oclif Plugin Model) +### How It Works (oclif Plugin Model) 1. **Declare** — Plugin's `package.json` includes an `oclif` config section declaring its commands directory and any topics. @@ -21,7 +21,7 @@ built-in plugin system. 3. **Load** — oclif automatically discovers and registers all Command classes exported from the plugin's commands directory. -## Plugin Package Contract +### Plugin Package Contract The plugin must be a valid oclif plugin: @@ -57,7 +57,7 @@ export default class MarketplaceSearch extends Command { } ``` -## Migration from Commander.js +### Migration from Commander.js The previous plugin model required `contributes.commands` in the manifest and exported Commander.js `Command` instances. The new model uses oclif's diff --git a/content/docs/references/kernel/cluster.mdx b/content/docs/references/kernel/cluster.mdx index 1a10c51bdc..2e42592e54 100644 --- a/content/docs/references/kernel/cluster.mdx +++ b/content/docs/references/kernel/cluster.mdx @@ -5,7 +5,7 @@ description: Cluster protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Cluster Protocol +## Cluster Protocol Defines the runtime semantics required for ObjectStack to behave correctly when more than one Node.js process is involved. The protocol layer codifies diff --git a/content/docs/references/kernel/dependency-resolution.mdx b/content/docs/references/kernel/dependency-resolution.mdx index 505e90b5e4..000dd7ed47 100644 --- a/content/docs/references/kernel/dependency-resolution.mdx +++ b/content/docs/references/kernel/dependency-resolution.mdx @@ -5,18 +5,18 @@ description: Dependency Resolution protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Dependency Resolution Protocol +## Dependency Resolution Protocol Defines schemas for runtime dependency resolution when installing, upgrading, or managing packages. Provides a standardized way to express dependency conflicts, resolution results, and installation order. -## Architecture Alignment +### Architecture Alignment - **npm**: Dependency tree resolution with conflict detection - **Helm**: Dependency management with version constraints - **Salesforce**: Package dependency validation at install time -## Resolution Flow +### Resolution Flow ``` 1. Parse manifest.dependencies (SemVer ranges) 2. Check installed packages registry diff --git a/content/docs/references/kernel/metadata-customization.mdx b/content/docs/references/kernel/metadata-customization.mdx index bcf5239ad7..6dffc471ca 100644 --- a/content/docs/references/kernel/metadata-customization.mdx +++ b/content/docs/references/kernel/metadata-customization.mdx @@ -5,20 +5,20 @@ description: Metadata Customization protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Metadata Customization Layer Protocol +## Metadata Customization Layer Protocol Defines the overlay system for managing user customizations on top of package-delivered metadata. This protocol solves the critical challenge of separating "vendor-managed" metadata from "customer-customized" metadata, enabling safe package upgrades without losing user changes. -## Architecture Alignment +### Architecture Alignment - **Salesforce**: Managed vs Unmanaged metadata components - **ServiceNow**: Update Sets with collision detection - **WordPress**: Parent/child theme overlay model - **Kubernetes**: Strategic merge patch for resource customization -## Three-Layer Model +### Three-Layer Model ``` ┌─────────────────────────────────┐ │ User Layer (scope: user) │ ← Personal overrides (per-user) @@ -29,7 +29,7 @@ enabling safe package upgrades without losing user changes. └─────────────────────────────────┘ ``` -## Merge Resolution Order +### Merge Resolution Order Effective metadata = System ← merge(Platform) ← merge(User) Each layer only stores the delta (changed fields), not the full definition. diff --git a/content/docs/references/kernel/metadata-loader.mdx b/content/docs/references/kernel/metadata-loader.mdx index dd18be8103..26de3d765f 100644 --- a/content/docs/references/kernel/metadata-loader.mdx +++ b/content/docs/references/kernel/metadata-loader.mdx @@ -5,7 +5,7 @@ description: Metadata Loader protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Metadata Manager Configuration +## Metadata Manager Configuration How the runtime `MetadataManager` is wired: which datasource backs `sys_metadata`, what to fall back to when that datasource is unreachable, cache / watch / validation settings, and the persistence write gates. diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index 09aa14ab6d..3ebbbadc21 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -5,12 +5,12 @@ description: Metadata Plugin protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Metadata Plugin Protocol +## Metadata Plugin Protocol Defines the specification for the **Metadata Plugin** — the central authority responsible for managing ALL metadata across the ObjectStack platform. -## Architecture +### Architecture The Metadata Plugin consolidates all scattered metadata operations into a single, cohesive plugin that "takes over" the entire platform's metadata management: @@ -30,12 +30,12 @@ cohesive plugin that "takes over" the entire platform's metadata management: └──────────────────────────────────────────────────────────────────┘ ``` -## Alignment +### Alignment - **Salesforce**: Metadata API (deploy, retrieve, describe) - **ServiceNow**: System Dictionary + Metadata API - **Kubernetes**: API Server + CRD Registry -## References +### References - [kernel/metadata-loader.zod.ts](/docs/references/kernel/metadata-loader) — MetadataManager wiring (datasource, cache, write gates) - [kernel/metadata-customization.zod.ts](/docs/references/kernel/metadata-customization) — Overlay/merge protocol - [system/metadata-persistence.zod.ts](/docs/references/system/metadata-persistence) — Database record format + loader/watch envelope types diff --git a/content/docs/references/kernel/metadata-protection.mdx b/content/docs/references/kernel/metadata-protection.mdx index 2cc4aa1077..faabad30b7 100644 --- a/content/docs/references/kernel/metadata-protection.mdx +++ b/content/docs/references/kernel/metadata-protection.mdx @@ -5,7 +5,7 @@ description: Metadata Protection protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Metadata Protection Model — Phase 1 (ADR-0010) +## Metadata Protection Model — Phase 1 (ADR-0010) Phase 1 introduces the **item-level lock** (`_lock`) and the provenance / package tags that drive it. Later phases extend this diff --git a/content/docs/references/kernel/package-artifact.mdx b/content/docs/references/kernel/package-artifact.mdx index f5565a8562..13b32b2cc6 100644 --- a/content/docs/references/kernel/package-artifact.mdx +++ b/content/docs/references/kernel/package-artifact.mdx @@ -5,13 +5,13 @@ description: Package Artifact protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Package Artifact Format Protocol +## Package Artifact Format Protocol Defines the standard structure of a package artifact (.tgz) produced by the build pipeline. The marketplace uses these schemas to validate, store, and distribute package artifacts. -## Artifact Internal Structure +### Artifact Internal Structure ``` ├── manifest.json ← ManifestSchema serialized ├── metadata/ ← 30+ metadata types (JSON) @@ -32,7 +32,7 @@ and distribute package artifacts. └── signature.sig ← RSA-SHA256 package signature ``` -## Architecture Alignment +### Architecture Alignment - **Salesforce**: Managed Package .zip with metadata components - **npm**: .tgz with package.json + contents - **Helm**: Chart .tgz with Chart.yaml + templates diff --git a/content/docs/references/kernel/package-registry.mdx b/content/docs/references/kernel/package-registry.mdx index 96ae353ad6..32c31b10e3 100644 --- a/content/docs/references/kernel/package-registry.mdx +++ b/content/docs/references/kernel/package-registry.mdx @@ -5,11 +5,11 @@ description: Package Registry protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Package Registry Protocol +## Package Registry Protocol Defines the runtime state and lifecycle operations for installed packages. -## Key Distinction: App vs Package (ADR-0019) +### Key Distinction: App vs Package (ADR-0019) - **App (AppSchema)**: the one consumer-facing unit — what a tenant downloads, opens, and uninstalls. Only `type: app` packages are consumer-installable (see `isConsumerInstallable`), and a consumer package defines **at most one @@ -21,7 +21,7 @@ Defines the runtime state and lifecycle operations for installed packages. the .app bundle" — bundled within an App or operator-provisioned; a consumer never installs them directly. -## Architecture Alignment +### Architecture Alignment - **Salesforce**: Managed Packages with install/uninstall lifecycle - **VS Code**: Extension marketplace with enable/disable per-workspace - **Kubernetes**: Helm charts with release state tracking diff --git a/content/docs/references/kernel/package-upgrade.mdx b/content/docs/references/kernel/package-upgrade.mdx index ea3fb2afa3..180e04a7bd 100644 --- a/content/docs/references/kernel/package-upgrade.mdx +++ b/content/docs/references/kernel/package-upgrade.mdx @@ -5,19 +5,19 @@ description: Package Upgrade protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Package Upgrade Protocol +## Package Upgrade Protocol Defines the complete lifecycle for upgrading installed packages, including pre-upgrade analysis, snapshot/backup, execution, validation, and rollback capabilities. -## Architecture Alignment +### Architecture Alignment - **Salesforce**: Managed Package upgrade with push upgrades and subscriber control - **ServiceNow**: Update Sets with preview, commit, and back-out support - **Helm**: Helm upgrade with rollback to previous release - **Kubernetes**: Rolling update with readiness probes and automatic rollback -## Upgrade Flow +### Upgrade Flow ``` 1. PreCheck → Validate compatibility, check dependencies 2. Plan → Generate upgrade plan with metadata diff diff --git a/content/docs/references/kernel/plugin-capability.mdx b/content/docs/references/kernel/plugin-capability.mdx index 0305f80b30..3ea87f6b97 100644 --- a/content/docs/references/kernel/plugin-capability.mdx +++ b/content/docs/references/kernel/plugin-capability.mdx @@ -5,7 +5,7 @@ description: Plugin Capability protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Plugin Capability Protocol +## Plugin Capability Protocol Defines the standard way plugins declare their capabilities, implementations, and conformance levels to ensure interoperability across vendors. diff --git a/content/docs/references/kernel/plugin-lifecycle-advanced.mdx b/content/docs/references/kernel/plugin-lifecycle-advanced.mdx index 3f2db79127..d8b842acd0 100644 --- a/content/docs/references/kernel/plugin-lifecycle-advanced.mdx +++ b/content/docs/references/kernel/plugin-lifecycle-advanced.mdx @@ -5,7 +5,7 @@ description: Plugin Lifecycle Advanced protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Advanced Plugin Lifecycle — host-driven library vocabularies +## Advanced Plugin Lifecycle — host-driven library vocabularies Declares the INPUT contracts of the host-driven lifecycle classes exported by `@objectstack/core` — `PluginHealthMonitor` (reads `PluginHealthCheck`, diff --git a/content/docs/references/kernel/plugin-loading.mdx b/content/docs/references/kernel/plugin-loading.mdx index ea9cae4073..46ad18cfe0 100644 --- a/content/docs/references/kernel/plugin-loading.mdx +++ b/content/docs/references/kernel/plugin-loading.mdx @@ -5,7 +5,7 @@ description: Plugin Loading protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Plugin Loading Protocol +## Plugin Loading Protocol What remains of this module is the **observational** half: the lifecycle event a loader would emit (`PluginLoadingEventSchema`) and the per-plugin diff --git a/content/docs/references/kernel/plugin-registry.mdx b/content/docs/references/kernel/plugin-registry.mdx index 5e48238505..ff0125e8b6 100644 --- a/content/docs/references/kernel/plugin-registry.mdx +++ b/content/docs/references/kernel/plugin-registry.mdx @@ -5,7 +5,7 @@ description: Plugin Registry protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Plugin Registry Protocol +## Plugin Registry Protocol Defines the schema for the plugin discovery and registry system. This enables plugins from different vendors to be discovered, validated, diff --git a/content/docs/references/kernel/plugin-security-advanced.mdx b/content/docs/references/kernel/plugin-security-advanced.mdx index 6b66de7d41..7f244f83de 100644 --- a/content/docs/references/kernel/plugin-security-advanced.mdx +++ b/content/docs/references/kernel/plugin-security-advanced.mdx @@ -5,7 +5,7 @@ description: Plugin Security Advanced protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Plugin Security and Sandboxing Protocol +## Plugin Security and Sandboxing Protocol Defines comprehensive security mechanisms for plugin isolation, permission management, and threat protection in the ObjectStack ecosystem. diff --git a/content/docs/references/kernel/plugin-security.mdx b/content/docs/references/kernel/plugin-security.mdx index e8d6b81807..568528cc5d 100644 --- a/content/docs/references/kernel/plugin-security.mdx +++ b/content/docs/references/kernel/plugin-security.mdx @@ -5,7 +5,7 @@ description: Plugin Security protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Plugin Security & Dependency Resolution Protocol +## Plugin Security & Dependency Resolution Protocol Provides comprehensive security scanning, vulnerability management, and dependency resolution for the ObjectStack plugin ecosystem. diff --git a/content/docs/references/kernel/plugin-versioning.mdx b/content/docs/references/kernel/plugin-versioning.mdx index 92a5c5e441..f5d4c1e277 100644 --- a/content/docs/references/kernel/plugin-versioning.mdx +++ b/content/docs/references/kernel/plugin-versioning.mdx @@ -5,7 +5,7 @@ description: Plugin Versioning protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Plugin Versioning and Compatibility Protocol +## Plugin Versioning and Compatibility Protocol Defines comprehensive versioning, compatibility checking, and dependency resolution mechanisms for the plugin ecosystem. diff --git a/content/docs/references/security/rls.mdx b/content/docs/references/security/rls.mdx index a4e43ba397..02e8723136 100644 --- a/content/docs/references/security/rls.mdx +++ b/content/docs/references/security/rls.mdx @@ -5,18 +5,18 @@ description: Rls protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Row-Level Security (RLS) Protocol +## Row-Level Security (RLS) Protocol Implements fine-grained record-level access control inspired by PostgreSQL RLS and Salesforce Criteria-Based Sharing Rules. -## Overview +### Overview Row-Level Security (RLS) allows you to control which rows users can access in database tables based on their identity and positions. Unlike object-level permissions (CRUD), RLS provides record-level filtering. -## Use Cases +### Use Cases 1. **Multi-Tenant Data Isolation** - Users only see records from their organization @@ -41,7 +41,7 @@ object-level permissions (CRUD), RLS provides record-level filtering. - `using: "assigned_to_id in current_user.team_member_ids"` (the runtime pre-resolves `team_member_ids`, no subquery needed) -## PostgreSQL RLS Comparison +### PostgreSQL RLS Comparison PostgreSQL RLS Example: ```sql @@ -64,7 +64,7 @@ ObjectStack RLS Equivalent: } ``` -## Salesforce Sharing Rules Comparison +### Salesforce Sharing Rules Comparison Salesforce uses "Sharing Rules" and a visibility hierarchy for record-level access (our equivalent hierarchy is the business-unit tree, ADR-0090 D3). @@ -80,7 +80,7 @@ ObjectStack RLS: - Subquery-shaped needs are pre-resolved by the runtime (§7.3.1) - Multiple policies OR-combine for union (any-match-allows) semantics -## Best Practices +### Best Practices 1. **Always Define SELECT Policy**: Control what users can view 2. **Define INSERT/UPDATE CHECK Policies**: Prevent data leakage @@ -88,7 +88,7 @@ ObjectStack RLS: 4. **Test Thoroughly**: RLS can have complex interactions 5. **Monitor Performance**: Complex RLS policies can impact query performance -## Security Considerations +### Security Considerations 1. **Defense in Depth**: RLS is one layer; use with object permissions 2. **Default Deny**: If no policy matches, access is denied diff --git a/content/docs/references/shared/expression.mdx b/content/docs/references/shared/expression.mdx index 0546252581..932b4103c5 100644 --- a/content/docs/references/shared/expression.mdx +++ b/content/docs/references/shared/expression.mdx @@ -5,7 +5,7 @@ description: Expression protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Expression Protocol +## Expression Protocol Canonical wire format for all "expression"-shaped metadata across ObjectStack (formula fields, predicates, conditions, criteria, visibility rules, seed @@ -16,7 +16,7 @@ compile` normalization, `{ dialect, ast }`). String-only shorthand is accepted at *input* time for developer ergonomics; build emits the canonical envelope. -## Dialects +### Dialects | dialect | engine | use | |:---|:---|:---| diff --git a/content/docs/references/shared/protection.mdx b/content/docs/references/shared/protection.mdx index 318e9fbe5c..1186d1cf0b 100644 --- a/content/docs/references/shared/protection.mdx +++ b/content/docs/references/shared/protection.mdx @@ -5,7 +5,7 @@ description: Protection protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) +## Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) Public, type-safe author surface for package authors to declare how much of one of their metadata items the runtime (and the diff --git a/content/docs/references/system/core-services.mdx b/content/docs/references/system/core-services.mdx index aa068a2833..55ef1df0d2 100644 --- a/content/docs/references/system/core-services.mdx +++ b/content/docs/references/system/core-services.mdx @@ -5,7 +5,7 @@ description: Core Services protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Service Registry Protocol +## Service Registry Protocol Defines the standard built-in services that constitute the ObjectStack Kernel. This registry is used by the `ObjectKernel` and `HttpDispatcher` to: diff --git a/content/docs/references/system/environment-artifact.mdx b/content/docs/references/system/environment-artifact.mdx index d4240921d3..0afe0ec305 100644 --- a/content/docs/references/system/environment-artifact.mdx +++ b/content/docs/references/system/environment-artifact.mdx @@ -5,7 +5,7 @@ description: Environment Artifact protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Environment Artifact Envelope +## Environment Artifact Envelope THE single declaration of the environment artifact envelope (#4740, #4535 C10 — maintainer route A′). `@objectstack/spec/cloud` re-exports @@ -24,7 +24,7 @@ listing). This envelope wraps the compiled `ObjectStackDefinitionSchema` produced by `objectstack compile` together with control-plane assigned identity (`commitId`, `checksum`). -## Boundary +### Boundary - **Artifact (this schema):** compiled environment metadata plus provenance. Immutable, content-addressable via `commitId` and @@ -35,7 +35,7 @@ identity (`commitId`, `checksum`). See `content/docs/concepts/north-star.mdx` §6.3 for the runtime-inputs boundary. -## History (#4740) +### History (#4740) This file previously documented a richer "v0" envelope — a `{ algorithm, value }` checksum object, a category-bag `metadata`, inlined diff --git a/content/docs/references/system/registry-config.mdx b/content/docs/references/system/registry-config.mdx index ca3ab28985..6d1d28bc3b 100644 --- a/content/docs/references/system/registry-config.mdx +++ b/content/docs/references/system/registry-config.mdx @@ -5,7 +5,7 @@ description: Registry Config protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -# Registry Configuration Protocol +## Registry Configuration Protocol Defines the configuration for the ObjectStack Registry Service. Includes federation, synchronization, and storage settings. diff --git a/packages/spec/scripts/build-docs.ts b/packages/spec/scripts/build-docs.ts index f4626f9e41..8c6c199219 100644 --- a/packages/spec/scripts/build-docs.ts +++ b/packages/spec/scripts/build-docs.ts @@ -396,6 +396,27 @@ function generateMarkdown(schemaName: string, schema: any, category: string, _zo return renderSchemaSection(schemaName, schema, { schemaHref: schemaHrefFrom(category) }); } +/** + * The heading level a top-level section of a reference page sits at. + * + * A page's `

` is its frontmatter `title`: `DocsTitle` renders it + * unconditionally (`apps/docs/app/[lang]/docs/[[...slug]]/page.tsx`), so a `# ` + * anywhere in the body compiles to a SECOND `

`. Everything this generator + * puts in the body therefore starts one level down — `## TypeScript Usage` + * below, the `## ` sections in `lib/schema-section.ts`, and the + * module description. + * + * That last one is the one that could not state the number for itself (#12249). + * Its text is a JSDoc file header, written as if it were a standalone document + * and freely opening sections with `# `; 38 pages shipped two `

` because of + * it, which is why `content/docs/references/**` had to be carved out of + * `scripts/check-docs-single-h1.mjs`. The level travels to + * `renderFileDescription` as context for the same reason `fromCategory` does: + * only the page knows it, and the renderer hard-coding a 2 would put the page's + * heading contract in the module least able to state it. + */ +const PAGE_SECTION_LEVEL = 2; + function generateZodFileMarkdown(zodFile: string, schemas: Array<{name: string, content: any}>, category: string): string { const zodTitle = zodFile.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '); @@ -411,6 +432,7 @@ function generateZodFileMarkdown(zodFile: string, schemas: Array<{name: string, fileDesc = renderFileDescription(fs.readFileSync(sourcePath, 'utf-8'), { fromCategory: category, sourcePathToDocsRoute, + sectionLevel: PAGE_SECTION_LEVEL, }); } @@ -440,7 +462,7 @@ function generateZodFileMarkdown(zodFile: string, schemas: Array<{name: string, for (const gap of imports.gaps) importGaps.add(gap); if (imports.valueNames.length || imports.typeNames.length) { - md += `## TypeScript Usage\n\n`; + md += `${'#'.repeat(PAGE_SECTION_LEVEL)} TypeScript Usage\n\n`; md += `\`\`\`typescript\n`; if (imports.valueNames.length) { md += `import { ${imports.valueNames.join(', ')} } from '@objectstack/spec/${category}';\n`; diff --git a/packages/spec/scripts/file-description.test.ts b/packages/spec/scripts/file-description.test.ts index 2f52ae4bb4..3da18371e4 100644 --- a/packages/spec/scripts/file-description.test.ts +++ b/packages/spec/scripts/file-description.test.ts @@ -45,6 +45,17 @@ import { findModuleDocBlock, renderFileDescription } from './lib/file-descriptio const HERE = path.dirname(url.fileURLToPath(import.meta.url)); const SRC_DIR = path.resolve(HERE, '../src'); +/** + * What `build-docs.ts` passes as `sectionLevel` — a reference page's top-level + * sections sit at 2, because its `

` is the frontmatter `title` (#12249). + * + * Named once here so every case below renders the block the way the generator + * really does. The cases that are ABOUT the level state their own number + * inline instead, so that a change to the page layout cannot quietly rewrite + * what they assert. + */ +const PAGE_SECTION_LEVEL = 2; + /** First prose line of a selected block, the way a page renders it. */ const opening = (block: string | null) => block === null @@ -215,6 +226,7 @@ describe('renderFileDescription', () => { const ctx = { fromCategory: 'automation', sourcePathToDocsRoute: (t: string) => (t.includes('sync') ? '/docs/references/automation/sync' : null), + sectionLevel: PAGE_SECTION_LEVEL, }; it('renders nothing when the module has no description', () => { @@ -257,7 +269,7 @@ describe('renderFileDescription', () => { * page and broke the docs build. */ describe('renderFileDescription — #10924: the os:check marker is machinery, not content', () => { - const ctx = { fromCategory: 'studio', sourcePathToDocsRoute: () => null }; + const ctx = { fromCategory: 'studio', sourcePathToDocsRoute: () => null, sectionLevel: PAGE_SECTION_LEVEL }; const moduleBlock = (...body: string[]): string => ['/**', ...body.map(l => (l === '' ? ' *' : ` * ${l}`)), ' */', '', "import { z } from 'zod';", ''].join('\n'); @@ -300,7 +312,7 @@ describe('renderFileDescription — #5553: line layout is content, not decoratio // Nothing resolves here — these cases are about line layout, not routes — so // every path they contain takes the code-span fallback whatever `fromCategory` // says. - const ctx = { fromCategory: 'data', sourcePathToDocsRoute: () => null }; + const ctx = { fromCategory: 'data', sourcePathToDocsRoute: () => null, sectionLevel: PAGE_SECTION_LEVEL }; it('keeps an inline code span that wraps across two source lines', () => { // `automation/flow-function.zod.ts:13-15`, reduced — the example the issue @@ -458,6 +470,7 @@ describe('renderFileDescription — #6136: the bare-path rewriter skips formed l fromCategory: 'automation', sourcePathToDocsRoute: (t: string) => /integration\/connector\.zod\.ts$/.test(t) ? '/docs/references/integration/connector' : null, + sectionLevel: PAGE_SECTION_LEVEL, }; it('renders an untitled `{@link }` as ONE link', () => { @@ -550,6 +563,7 @@ describe('renderFileDescription — #6229: a bare path keeps its `../` prefix in const m = /(?:^|\/)(system|api)\/([\w-]+)\.zod\.ts$/.exec(t); return m ? `/docs/references/${m[1]}/${m[2]}` : null; }, + sectionLevel: PAGE_SECTION_LEVEL, }; const describedBy = (line: string) => @@ -654,6 +668,7 @@ describe('renderFileDescription — #6420: a bare path in parentheses still link const m = /(?:^|\/)(integration|automation)\/([\w-]+)\.zod\.ts$/.exec(t); return m ? `/docs/references/${m[1]}/${m[2]}` : null; }, + sectionLevel: PAGE_SECTION_LEVEL, }; const describedBy = (line: string) => @@ -785,7 +800,7 @@ describe('renderFileDescription — #6484: a same-directory path resolves agains const describedBy = (fromCategory: string, line: string) => renderFileDescription( ['/**', ` * ${line}`, ' */', '', "import { z } from 'zod';", ''].join('\n'), - { fromCategory, sourcePathToDocsRoute }, + { fromCategory, sourcePathToDocsRoute, sectionLevel: PAGE_SECTION_LEVEL }, ); it('links a same-directory path whose page exists — the published `api/realtime-shared` line', () => { @@ -1010,6 +1025,122 @@ describe('corpus — no reference source donates a symbol comment to its page', }); }); +/** + * #12249 — a module header's heading levels belong to the PAGE, not to the file. + * + * A `.zod.ts` header is written as if it were a standalone document, so its + * author opens a section with `# `. Embedded in a reference page that compiles + * to a SECOND `

`, because `DocsTitle` already renders the frontmatter + * `title` as the page's heading-one. Measured on `main` at `20b0fdb56`: 38 of + * the 190 described modules, 43 headings between them (41 distinct texts) — and + * the whole `content/docs/references/**` tree carved out of + * `scripts/check-docs-single-h1.mjs` because the fix could not live there. + * + * The renderer places the block's SHALLOWEST heading at `ctx.sectionLevel` and + * moves the rest with it. The three cases that shape pins are the three ways + * the obvious "rewrite every `# ` to `## `" goes wrong: it demotes blocks that + * were already correct, it collides two source levels into one wherever a block + * mixes them, and — the expensive one — it cannot tell a heading from a shell + * comment inside a fence. + * + * MEASURED (reverse verification), the ordinary direction: replacing the + * `withHeadingsAtSectionLevel` call in `renderFileDescription` with the + * identity turns 9 of these 70 cases red — every level-1 case here, plus both + * corpus limbs, the first of them reporting exactly the 43 headings the issue + * measured on `main`. The two cases about what the shift must NOT touch — + * `already start at level 2` and `#NoSpace` — stay green, and so do the 61 + * cases this file already had. That asymmetry is what says the pin measures the + * shift and not the renderer at large. + */ +describe('renderFileDescription — #12249: the fragment starts at the page\'s section level', () => { + const ctx = { fromCategory: 'data', sourcePathToDocsRoute: () => null, sectionLevel: PAGE_SECTION_LEVEL }; + + const moduleBlock = (...body: string[]): string => + ['/**', ...body.map(l => (l === '' ? ' *' : ` * ${l}`)), ' */', '', "import { z } from 'zod';", ''].join('\n'); + + it('demotes a level-1 heading to the page\'s section level', () => { + // `data/context-tokens.zod.ts:10` verbatim — the line the issue opened + // with, published as `content/docs/references/data/context-tokens.mdx:11`. + expect(renderFileDescription(moduleBlock('# Why this lives in `spec`', '', 'Prose.'), ctx)).toBe( + '## Why this lives in `spec`\n\nProse.', + ); + }); + + it('moves the whole block, keeping the nesting the author wrote', () => { + // The reason this is a renumbering and not a per-line rewrite: `# ` and + // `## ` in one block are two levels, and collapsing both onto `## ` would + // make a section its own subsection's sibling. + expect( + renderFileDescription(moduleBlock('# Top', '', 'Prose.', '', '## Under it', '', 'More prose.'), ctx), + ).toBe('## Top\n\nProse.\n\n### Under it\n\nMore prose.'); + }); + + it('leaves a description whose headings already start at level 2 byte-identical', () => { + // 26 of the 64 described modules with headings are in this state + // (`api/http-cache`, `automation/control-flow`, …). A blanket `#`→`##` + // would have regenerated all 26 pages to no purpose, and pushed + // `automation/control-flow`'s `###` to a fourth level. + const source = moduleBlock('## A section', '', 'Prose.', '', '### Nested', '', 'More prose.'); + expect(renderFileDescription(source, ctx)).toBe('## A section\n\nProse.\n\n### Nested\n\nMore prose.'); + // The identity render is the same string, which is what "untouched" means. + expect(renderFileDescription(source, { ...ctx, sectionLevel: 1 })).toBe(renderFileDescription(source, ctx)); + }); + + it('never touches a `# ` inside a fenced block — it is a shell comment, not a heading', () => { + // THE control, and the reason the shift lives beside `classifyLines` + // instead of running as a regex over the emitted string in `build-docs.ts`. + // A fence-blind pass rewrites working snippets to satisfy a rule about HTML + // those lines never produce — the false positive `check-docs-single-h1.mjs` + // measures at ten pages and calls more expensive than the defect itself. + const out = renderFileDescription( + moduleBlock('# Setup', '', '```bash', '# Install pnpm globally', 'npm i -g pnpm', '```'), + ctx, + ); + expect(out).toBe('## Setup\n\n```bash\n# Install pnpm globally\nnpm i -g pnpm\n```'); + }); + + it('never touches a `# ` inside an indented block, which is re-emitted as a fence', () => { + // `data/date-macros` and `data/context-tokens` write their examples this + // way, and both are among the 38. The re-fencing (#5553) happens after the + // shift, so the block has to be excluded by KIND, not by its final syntax. + const out = renderFileDescription(moduleBlock('# Macros', '', ' # not a heading', ' value: 1'), ctx); + expect(out).toBe('## Macros\n\n```\n# not a heading\nvalue: 1\n```'); + }); + + it('does not mistake `#NoSpace` or a `#5059` issue reference for a heading', () => { + const out = renderFileDescription(moduleBlock('#NotAHeading', '', '#5059 closed this.'), ctx); + expect(out).toBe('#NotAHeading\n\n#5059 closed this.'); + }); + + it('shifts a heading indented up to three spaces, which CommonMark still reads as one', () => { + // A fourth space would make it an indented code block, which + // `classifyLines` has already labelled `indented` by the time the shift + // runs — so three is the boundary the shift has to reach. Prose first + // because the fragment is trimmed as a whole, which would otherwise eat + // the indentation this case is about. + expect(renderFileDescription(moduleBlock('Prose.', '', ' # Indented'), ctx)).toBe('Prose.\n\n ## Indented'); + }); + + it('refuses rather than emitting seven hashes, which is not a heading at all', () => { + // Unreachable on the current corpus — no level-1-bearing description goes + // deeper than level 2 — and a loud failure is the right answer if a source + // ever gets there. Silently clamping to 6 would flatten two source levels + // into one; emitting `####### ` would publish the hashes as prose. + expect(() => renderFileDescription(moduleBlock('# Top', '', '###### Six deep'), ctx)).toThrow( + /would push a heading to level 7/, + ); + }); + + it('is the page that decides the level, not this module', () => { + // The seam itself: `sectionLevel` is context, so a page whose sections sit + // at 3 gets a fragment that starts at 3. Hard-coding 2 in the renderer + // would make this untestable and the contract unstated. + expect(renderFileDescription(moduleBlock('# Top', '', '## Under it'), { ...ctx, sectionLevel: 3 })).toBe( + '### Top\n\n#### Under it', + ); + }); +}); + /** * The corpus half of #5553 / #6136: re-derive both verdicts from the real * sources, so a future header cannot quietly re-acquire either defect. @@ -1074,7 +1205,7 @@ describe('corpus — every rendered description is well-formed markdown', () => // reference resolvable at all (#6484), and rendering the whole corpus // from one fixed category would test a context the generator never // constructs. - const ctx = { fromCategory: rel.split(path.sep)[0], sourcePathToDocsRoute }; + const ctx = { fromCategory: rel.split(path.sep)[0], sourcePathToDocsRoute, sectionLevel: PAGE_SECTION_LEVEL }; return { rel, out: renderFileDescription(fs.readFileSync(file, 'utf-8'), ctx) }; }) .filter(d => d.out !== ''); @@ -1237,6 +1368,55 @@ describe('corpus — every rendered description is well-formed markdown', () => expect(offenders).toEqual([]); }); + it('never emits a level-1 heading — the page already has one (#12249)', () => { + // The corpus half of the unit block above, and the issue's own acceptance + // criterion re-derived from the sources: a fragment embedded under the + // page's `

` may not open a second one. 38 of these descriptions did, + // 43 headings across them, which is what carved `content/docs/references/**` + // out of `scripts/check-docs-single-h1.mjs`. + // + // Asserted on the RENDERED fragment, not on the emitted `.mdx`: the gate + // that reads the tree is `check-docs-single-h1.mjs` and it reads the + // artifact, so it can only ever see this AFTER a `gen:docs` run committed + // the damage. This sees it at the source of the string. + const offenders: string[] = []; + for (const { rel, out } of described) { + for (const [i, line] of withoutFences(out).split('\n').entries()) { + if (/^ {0,3}#(?:[ \t]|$)/.test(line)) offenders.push(`${rel}:${i + 1} ${line.trim()}`); + } + } + expect(offenders).toEqual([]); + }); + + it('demotes only the 38 descriptions that needed it (#12249)', () => { + // The other half, and the reason this is a renumbering rather than a + // blanket `#`→`##`: 26 described modules already start their headings at + // level 2, and shifting those too would have moved 105 correct headings a + // level deeper and regenerated 26 pages that were never wrong. + // + // Re-derived from the SOURCES: a description needed the shift exactly when + // its own file header opens a heading at level 1. Rendering the same corpus + // at `sectionLevel: 1` — the identity, since no header goes shallower — + // gives the untouched string to compare against, so this counts real + // shifts rather than restating the fixture list. + const shifted = zodFiles + .map(file => { + const rel = path.relative(SRC_DIR, file); + const source = fs.readFileSync(file, 'utf-8'); + const base = { fromCategory: rel.split(path.sep)[0], sourcePathToDocsRoute }; + return { + rel, + untouched: renderFileDescription(source, { ...base, sectionLevel: 1 }), + emitted: renderFileDescription(source, { ...base, sectionLevel: PAGE_SECTION_LEVEL }), + }; + }) + .filter(d => d.untouched !== d.emitted); + + expect(shifted.length).toBe(38); + // …and every one of them was shifted because it opened at level 1. + expect(shifted.filter(d => /^ {0,3}#(?:[ \t]|$)/m.test(withoutFences(d.untouched)))).toHaveLength(38); + }); + it('keeps a description for every source that had one — #6134 selection is untouched', () => { // The rendering fix must not remove a page's opening paragraph; that is // #5059's acceptance criterion and it still binds. 185 sources carry a diff --git a/packages/spec/scripts/lib/file-description.ts b/packages/spec/scripts/lib/file-description.ts index 2e97a88d9d..2581ae656a 100644 --- a/packages/spec/scripts/lib/file-description.ts +++ b/packages/spec/scripts/lib/file-description.ts @@ -98,6 +98,47 @@ * keeping them would hand `{ $gte: '{last_quarter_start}' }` to MDX as an * expression and fail the docs build — the target dialect has one spelling for * a code block and this is it. + * + * ## Heading levels are the PAGE's, not the source file's (#12249) + * + * The second such place, and the same argument. A `.zod.ts` header is written + * as a standalone document, so its author opens a section with `# `; embedded + * in a reference page that `# ` compiles to a second `

`, because the page + * already renders one from its frontmatter `title` (`DocsTitle`, in + * `apps/docs/app/[lang]/docs/[[...slug]]/page.tsx`). Measured on `main`: 38 of + * the 190 described modules did that, 43 headings across them, and the whole + * `content/docs/references/**` tree had to be carved out of + * `scripts/check-docs-single-h1.mjs` because of it. + * + * So the fragment is renumbered on the way out: its SHALLOWEST heading is + * placed at `ctx.sectionLevel` and every other heading moves with it, keeping + * the relative structure the author wrote. Three properties this shape has that + * the obvious "demote every `# ` to `## `" does not: + * + * - **It only ever demotes.** A description whose headings already start at + * level 2 is left byte-identical — 26 of the 64 described modules with + * headings are in that state, and a blanket +1 would have pushed all of them + * a level deeper and regenerated 26 pages that were never wrong. + * - **It moves the block, not the level-1 lines.** `automation/control-flow` + * writes `##` with a `###` under it; renumbering the run keeps that nesting, + * whereas rewriting only the `# ` lines would collide two source levels into + * one output level wherever a block mixes them. + * - **It cannot silently corrupt code.** The shift is applied to `prose` lines + * only, so a `# Install pnpm globally` inside a fenced or indented block is + * left alone. That is the load-bearing half, and it is why this lives here + * rather than as a regex over the emitted string in `build-docs.ts`: + * `classifyLines` is already the one model in this pipeline that knows which + * lines are code, and a fence-blind pass over the same text would rewrite + * working snippets to satisfy a rule about HTML those lines never produce + * (the same false positive `check-docs-single-h1.mjs`'s header measures at + * ten pages, and calls more expensive than the defect). + * + * ATX only: no source writes a setext (`===` underline) heading, and both this + * renderer's consumers and `check-docs-single-h1.mjs` model headings the same + * ATX-only way. A shift that would push a heading past level 6 throws rather + * than emitting a seven-hash line, which CommonMark renders as paragraph text — + * unreachable on the current corpus (no h1-bearing description goes deeper than + * level 2) and a loud failure is the right answer if a source ever gets there. */ /** @@ -138,6 +179,20 @@ export interface FileDescriptionContext { * `/.zod.ts` lookup #4696 settled on. */ sourcePathToDocsRoute: (target: string) => string | null; + + /** + * The heading level a TOP-LEVEL section of the embedding page sits at — 2 for + * a reference page, whose `

` is the frontmatter `title`. + * + * Required for the reason `fromCategory` is (#6484): only the caller knows + * it. A `.zod.ts` header is written as if it were its own document and has no + * way to know what page it will be embedded in, while `build-docs.ts` emits + * the page's other top-level sections itself and therefore owns the number. + * Hard-coding `2` here would put the page's heading contract in the module + * that is least able to state it, and would drift silently the first time a + * page's layout changed. + */ + sectionLevel: number; } /** @@ -337,6 +392,60 @@ function classifyLines(lines: readonly string[]): LineKind[] { return kind; } +/** + * An ATX heading, and the three pieces a renumbering needs: the leading + * indent, the hash run, and where the rest of the line starts. + * + * CommonMark allows up to three leading spaces (a fourth makes it an indented + * code block, which `classifyLines` has already labelled `indented` by the time + * this runs). The trailing `(?=[ \t]|$)` is what keeps `#NotAHeading` and a + * `#5059` issue reference at the start of a line out of the match — a hash run + * is only a heading when a space or the end of the line closes it. + */ +const ATX_HEADING = /^( {0,3})(#{1,6})(?=[ \t]|$)/; + +/** + * The block's headings renumbered so its shallowest one sits at `sectionLevel`. + * + * See the module comment for why this only ever demotes, why it moves the whole + * block rather than the level-1 lines, and why it is scoped to `prose`. Returns + * the lines untouched when the block has no heading, or when its shallowest one + * is already at or below the page's section level. + */ +function withHeadingsAtSectionLevel( + lines: readonly string[], + kind: readonly LineKind[], + sectionLevel: number, +): string[] { + const headingLevel = (line: string, i: number): number | null => { + if (kind[i] !== 'prose') return null; + const m = ATX_HEADING.exec(line); + return m ? m[2].length : null; + }; + + const levels = lines.map(headingLevel).filter((l): l is number => l !== null); + if (levels.length === 0) return [...lines]; + + const shift = sectionLevel - Math.min(...levels); + if (shift <= 0) return [...lines]; // already at or below the page's level + + const deepest = Math.max(...levels) + shift; + if (deepest > 6) { + throw new Error( + `file-description: renumbering this module header to start at level ${sectionLevel} would ` + + `push a heading to level ${deepest}, and a run of seven hashes is not a heading at all. ` + + `Reduce the heading depth in the source's own file header instead.`, + ); + } + + return lines.map((line, i) => { + const level = headingLevel(line, i); + if (level === null) return line; + const m = ATX_HEADING.exec(line)!; + return `${m[1]}${'#'.repeat(level + shift)}${line.slice(m[1].length + m[2].length)}`; + }); +} + /** * A blank line before every JSDoc block tag that does not already have one. * @@ -517,25 +626,31 @@ export function renderFileDescription(source: string, ctx: FileDescriptionContex ); const kind = classifyLines(lines); + // Renumbered against the SAME classification the render loop below uses, so + // the shift and the "this line is code" verdict can never disagree. Adding + // hashes cannot change a line's kind — the indent is preserved, so a prose + // line stays prose (#12249). + const leveled = withHeadingsAtSectionLevel(lines, kind, ctx.sectionLevel); + // Prose is rendered a RUN of lines at a time, never line by line: a sentence, // an inline code span and a `{@link}` tag may each wrap across source lines, // and a transform applied per line cuts them in half (#5553). const out: string[] = []; - for (let i = 0; i < lines.length;) { - if (kind[i] === 'fenced') { out.push(lines[i]); i++; continue; } + for (let i = 0; i < leveled.length;) { + if (kind[i] === 'fenced') { out.push(leveled[i]); i++; continue; } if (kind[i] === 'indented') { const start = i; - while (i < lines.length && kind[i] === 'indented') i++; + while (i < leveled.length && kind[i] === 'indented') i++; // Re-emitted as a fence: MDX has no indented code blocks, so left as it // was authored this would be parsed as prose containing JSX expressions. - out.push('```', ...lines.slice(start, i).map(line => line.replace(/^ {4}/, '')), '```'); + out.push('```', ...leveled.slice(start, i).map(line => line.replace(/^ {4}/, '')), '```'); continue; } const start = i; - while (i < lines.length && kind[i] === 'prose') i++; - out.push(renderProse(lines.slice(start, i).join('\n'), ctx)); + while (i < leveled.length && kind[i] === 'prose') i++; + out.push(renderProse(leveled.slice(start, i).join('\n'), ctx)); } return out.join('\n').trim(); } diff --git a/scripts/check-docs-single-h1.mjs b/scripts/check-docs-single-h1.mjs index 4a3b92bfaf..fcc3f10b60 100644 --- a/scripts/check-docs-single-h1.mjs +++ b/scripts/check-docs-single-h1.mjs @@ -81,29 +81,34 @@ // 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 +// ## Scope: one tree is 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. +// `EXCLUSIONS` carves out 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. +// **An exclusion whose tree is CLEAN is a failure, not a pass.** When #12250 +// lands, this gate goes red with `DEAD-EXCLUSION` and asks for its own carve-out +// to be deleted. Without that limb an exclusion would outlive its reason and +// quietly shrink the gate's scope forever — the vacuous-green shape (#4690) +// that this repo treats as worse than no check. +// +// That limb has now been paid out once, which is the only evidence that it +// works. `content/docs/references/**` was the second entry here: 38 generated +// pages whose `# ` came from a JSDoc file header that +// `packages/spec/scripts/build-docs.ts` copied verbatim, so a hand-edit did not +// merely get reverted at the next generator run — `check:docs` regenerates the +// tree and fails on any difference, so it shipped CI-red immediately. #12249 +// fixed it where it was fixable, in the generator, which now renumbers a file +// header's headings to start at the page's section level; this gate went +// `DEAD-EXCLUSION` on the regenerated tree and the entry came out in that same +// change. Its 38 pages are judged here now, like every other page. // // 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. @@ -163,11 +168,6 @@ const PAGE_EXTENSION = '.mdx'; * 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', @@ -376,9 +376,12 @@ export function selfTest() { 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`); + // The excluded subtree, dirty — the live shape. write('content/docs/releases/v9.mdx', `${fm('v9.0.0')}# 9.0.0 in detail\n\nProse.\n`); + // NOT excluded any more (#12249): the generated tree is judged like every + // other page now. Kept as a fixture — with the carve-out gone this is the + // control proving the tree is back in scope rather than silently unread. + write('content/docs/references/gen.mdx', `${fm('Gen')}# Why this lives in \`spec\`\n\nProse.\n`); const root = join(dir, 'content/docs'); const { findings, excluded, scanned } = scanTree(root, dir); @@ -400,14 +403,20 @@ export function selfTest() { 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}`); + t('...but its violations are still counted', excluded.get('content/docs/releases/').length === 1); + t( + 'the generated references tree is judged, not excluded (#12249)', + at('content/docs/references/gen.mdx').length === 1, + JSON.stringify(at('content/docs/references/gen.mdx')), + ); + t('only judgeable pages are counted as scanned', scanned === 10, `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')); + // `main` can fail on it. This is the limb that retired the references + // carve-out in #12249, so it is the one that must keep working. + rmSync(join(dir, 'content/docs/releases/v9.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); + t('a CLEAN excluded subtree reports zero — the dead-exclusion trigger', after.excluded.get('content/docs/releases/').length === 0); // Anti-vacuity: a tree of nothing but excluded pages scans zero pages. const empty = mkdtempSync(join(tmpdir(), 'docs-single-h1-empty-'));