Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .changeset/doc-component-type-ratchet-4823.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
---

Tooling + docs-only (objectui#4823). Every `type` string literal in a `content/docs/**.mdx`
code block must now name a component the repository actually registers, enforced by a new CI
gate — `pnpm check:doc-types`, `scripts/check-doc-component-types.mjs`, in its own
`doc-component-types.yml` workflow.

The catalog side has had this ratchet since objectui#4616:
`examples/schema-catalog/test/catalog-gallery-render.test.tsx` renders every catalog entry and
fails if any paints the registry's "Unknown component type" panel (OBJUI-001). The teaching
surface had no equivalent — a fenced snippet in `content/docs/**` is not rendered, not parsed
and not compared against anything — so a page could teach a `type` that does not exist and
every check in the repo stayed green. The same defect landed three times on that surface
(objectui#4786 `stats-card`, objectui#4796 `plugin:grid` and `plugin:map`), each found by a
human probe rather than by a check.

The registered-key universe is derived from the register calls themselves on every run — no
hard-coded list and no build step, so the gate is a checkout plus one `node` call and can
therefore run unfiltered, which matters because the change that introduces this defect is
docs-only and `ci.yml`'s gates skip those by design.

The first full scan read 558 `type` literals across 143 pages against 661 derived keys and
found three more instances of the same shape, fixed here: `content/docs/utilities/runner.mdx`
and `content/docs/utilities/vscode-extension.mdx` taught `heading`, which nothing registers
(now `h1`, which `html-elements.tsx` registers and which renders the node's `children`), and
`content/docs/plugins/plugin-form.mdx` taught a `multi-step-form` type that appears nowhere in
the repo outside that snippet (now the `object-form` + `formType: 'wizard'` + `sections` shape
that `WizardFormSchema` itself declares).

No published behaviour changes — repo tooling plus three documentation snippets — so this
declares "no release" rather than a bump.
79 changes: 79 additions & 0 deletions .github/workflows/doc-component-types.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Doc Component Types

# Why this is its own workflow instead of a step in `ci.yml` or `lint.yml`: the
# defect this gate exists for arrives in a DOCS-ONLY pull request, and that is
# precisely the shape both of those workflows skip. `ci.yml`'s `type-check` job
# decides whether to run its expensive steps with a `git diff` that excludes
# `content/**`, `'**/*.md'`, `docs/**` and `apps/site/**` — so a PR that edits
# only `content/docs/**.mdx` reports the context and runs none of the gates
# inside it. A gate against a wrong `type` in a teaching snippet, wired there,
# would be blind to every change that can introduce one.
#
# This is the fifth instance of the shape in this repo and the reasoning is
# borrowed, not invented: `docs-links.yml`'s header records the link check
# spending #3213 to #3448 inside `ci.yml`'s `docs` job, unable to see the one
# class of PR most likely to break a link; `control-bytes.yml`'s header names the
# consequence — a gate that cannot see a markdown-only change "rebuilds the hole
# it exists to close". `changeset-guard.yml` and `skills-paths.yml` are the third
# and fourth.
#
# Hence: no `paths` and no `paths-ignore` here, deliberately.
# `scripts/__tests__/check-doc-component-types.test.ts` fails if either is ever
# added, and fails too if a second workflow starts running the same script — one
# gate, one home.
#
# It needs no install and no build. The script reads the checkout with `node:fs`
# only: 143 mdx files for the snippets, and the `packages/` + `apps/` sources for
# the registered-key universe it compares them against. A few seconds. Keep it
# that way if you add checks to it — the moment this needs `pnpm install` it
# stops being cheap enough to run unfiltered, and the filter is the hole.

on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
# Merge queue (objectui#3523 — see `ci.yml`'s trigger block for the full note
# and the measurements behind it). A required check that does not report on a
# queue build stalls the queue until the ruleset's 60-minute timeout fails it,
# so an unfiltered gate that could become required subscribes here from the
# start. `types:` is named although `checks_requested` is currently the only
# activity type GitHub defines for `merge_group`.
merge_group:
types: [checks_requested]
workflow_dispatch:

concurrency:
group: doc-component-types-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
doc-component-types:
name: Doc Component Type Check
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22.x'

# A `type` string in a `content/docs/**.mdx` code block is not rendered,
# not parsed and not compared against anything, so a snippet can name a
# component that does not exist and every check in the repo stays green —
# while a reader who copies it gets the renderer's red "Unknown component
# type" panel (OBJUI-001). That defect landed three times before this gate
# (objectui#4786 `stats-card`, objectui#4796 `plugin:grid` and
# `plugin:map`), each found by a human probe. The catalog side has had the
# equivalent ratchet since objectui#4616
# (`examples/schema-catalog/test/catalog-gallery-render.test.tsx`); this is
# the missing half. Reads the checkout and nothing else, so no install.
- name: Check documented component types against the registry
run: node scripts/check-doc-component-types.mjs
52 changes: 52 additions & 0 deletions content/docs/guide/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ one has its own section below.
| `control-bytes.yml` | Control Byte Scan | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** |
| `docs-links.yml` | Internal Docs Link Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** |
| `skills-paths.yml` | Skill Guide Path Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a path stated in a `skills/` guide does not exist |
| `doc-component-types.yml` | Doc Component Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a `content/docs/**.mdx` snippet teaches a `type` nothing registers |
| `performance-budget.yml` | Bundle Analysis | Push / PR touching `packages/**`, `apps/console/**`, `pnpm-lock.yaml` | **Yes** — the console entry gzip budget |
| `live-e2e.yml` | Live E2E (informational) | PR to `main`, `develop` (code paths); nightly cron `30 6 * * *`; manual | No — informational lane, `continue-on-error` |
| `labeler.yml` | Auto Label PRs | PR `opened`, `synchronize`, `reopened` | No |
Expand Down Expand Up @@ -538,6 +539,57 @@ the sentence's whole point is that the path does not exist. Run it locally with
`pnpm check:skills-paths`, or `node scripts/check-skills-paths.mjs --list` to see every candidate and
how it was classified.

## Documented Component Types (`doc-component-types.yml`)

**Triggers:** Push and PR to `main`/`develop`, merge-queue builds, plus manual dispatch — with **no
path filter at all**, and here the reason is sharper than in the three sections above. `ci.yml`'s
`type-check` job decides whether to run its gates with a `git diff` that *excludes* `content/**`, so
a pull request editing only `content/docs/**.mdx` reports that context and runs nothing inside it —
and a docs-only pull request is exactly the change that introduces the defect this gate exists for.
It appears in the checks list as **Doc Component Type Check**.

Runs `scripts/check-doc-component-types.mjs`, which reads every fenced code block under
`content/docs/**` and asks, of each `type` string literal in one, whether the repository registers a
component under that name.

**Why the teaching surface needed its own ratchet.** The catalog side has had one since
[#4616](https://github.com/objectstack-ai/objectui/issues/4616):
`examples/schema-catalog/test/catalog-gallery-render.test.tsx` renders every catalog entry and fails
if any paints the registry's `Unknown component type` panel (OBJUI-001). A snippet in the docs is
rendered by nothing, parsed by nothing and compared against nothing, so it could name any string at
all and every check stayed green — while a reader who copied it got the red panel. The same defect
landed three times that way, each found by a human probe:
[#4786](https://github.com/objectstack-ai/objectui/issues/4786) taught `stats-card`, and
[#4796](https://github.com/objectstack-ai/objectui/issues/4796) taught `plugin:grid` and
`plugin:map` (the registered names are `object-grid` and `object-map`).

**Where the key list comes from.** Nowhere — it is derived from the `ComponentRegistry.register(…)`
and `registerLazy(…)` calls themselves on every run, including the loop forms and two helpers that
register from a collection, with `namespace` and `skipFallback` read out of each call's own balanced
argument span. There is no hard-coded enumeration to drift, and no build step, which is what keeps
the whole run to a checkout plus one `node` call. A registration whose key the derivation cannot
resolve **fails the gate** rather than being skipped: a key silently missing from the universe turns
*correct* documentation red, which is the failure mode that gets gates deleted.

**How a snippet is judged.** `type` is not one vocabulary in these pages — measured across 143 files
and 558 literals, the corpus spells action schemas, block schemas, theme and report schemas, field
and JSON-Schema data types, validation rules and navigation items all under the same key. A
structural discriminator was built and rejected on measurement (a TypeScript annotation reads exactly
like an object key to a brace tracker, and `items` carries navigation entries on one page and
renderable children on another, so any global rule is a silent false green somewhere). So the rule is
flat: every literal is a candidate component key, and a value outside the derived universe must be
**declared** in the script's `DOC_TYPE_EXEMPTIONS` — keyed by (file, value), with a written reason
naming the vocabulary it really belongs to. A whole-file exemption is deliberately not offered:
`blocks/block-schema.mdx` carries `type: 'block'` and `type: 'div'` in the same document.

Entries are re-derived per run, so one whose page stopped spelling that type fails as a stale
exemption rather than quietly widening the hole.

**If it fails:** it prints every `file:line — type '<value>'` with the offending source line. Either
spell the registered key (`grep -rn "ComponentRegistry.register(" packages/` for the real name), or —
if the value belongs to another vocabulary — add the declaration with its reason. Run it locally with
`pnpm check:doc-types`.

## Link Checking (`check-links.yml`)

**Trigger:** Weekly cron (`17 4 * * 0` — Sundays, off the top of the hour, when the scheduled-run
Expand Down
30 changes: 18 additions & 12 deletions content/docs/plugins/plugin-form.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -205,23 +205,29 @@ Object.entries(formComponents).forEach(([type, component]) => {

### Multi-Step Form

Multi-step is a **mode of the object-bound form**, not a component of its own — there is
no `multi-step-form` type. The registered type is `object-form`, and `formType: 'wizard'`
turns its `sections` into steps; `WizardFormSchema` (exported by
`@object-ui/plugin-form`) declares the shape. Because the wizard resolves its fields from
the object's own metadata, a section lists field **names** rather than field definitions.

```json
{
"type": "multi-step-form",
"steps": [
"type": "object-form",
"objectName": "contact",
"mode": "create",
"formType": "wizard",
"showStepIndicator": true,
"sections": [
{
"title": "Personal Info",
"fields": [
{ "name": "firstName", "type": "input", "label": "First Name", "required": true },
{ "name": "lastName", "type": "input", "label": "Last Name", "required": true }
]
"name": "personal",
"label": "Personal Info",
"fields": ["first_name", "last_name"]
},
{
"title": "Contact Info",
"fields": [
{ "name": "email", "type": "input", "inputType": "email", "label": "Email", "required": true },
{ "name": "phone", "type": "input", "inputType": "tel", "label": "Phone" }
]
"name": "contact_details",
"label": "Contact Info",
"fields": ["email", "phone"]
}
]
}
Expand Down
3 changes: 1 addition & 2 deletions content/docs/utilities/runner.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,7 @@ serve it from your own backend and load it with `?api=<base>`.
"className": "p-8 space-y-6",
"children": [
{
"type": "heading",
"level": 1,
"type": "h1",
"children": "Sales Dashboard"
},
{
Expand Down
2 changes: 1 addition & 1 deletion content/docs/utilities/vscode-extension.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const schema = {
"type": "div",
"className": "p-4",
"children": [
{ "type": "heading", "level": 1, "children": "Hello World" }
{ "type": "h1", "children": "Hello World" }
]
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"check:i18n-drift": "node scripts/check-i18n-en-drift.mjs",
"check:i18n-dead-keys": "node scripts/check-i18n-dead-keys.mjs",
"check:skills-paths": "node scripts/check-skills-paths.mjs",
"check:doc-types": "node scripts/check-doc-component-types.mjs",
"cli": "node packages/cli/dist/cli.js",
"objectui": "node packages/cli/dist/cli.js",
"create-plugin": "node packages/create-plugin/dist/index.js",
Expand Down
Loading
Loading