From 32d9dcf3919fc711c7b15aa9d4cb21ad130bc5d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 07:02:41 +0000 Subject: [PATCH 1/2] fix(spec): drop the `@module` marker line from generated reference pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `@module ` tag selects a module's doc block (#13334) but is machinery for the selector, not prose. `renderFileDescription` emitted it verbatim, so fourteen published reference pages opened on the literal text `@module ui/sharing` instead of on their first sentence. `check:docs` could not see it: it compares the artifact against the source, and the artifact reproduced the marker faithfully. Mirrors the existing `os:check` marker drop in the same function — a prose-level filter applied before the render classification — rather than inventing a second convention, and matches the filter `build-skill-references.ts` has always had on the skill-index surface. The marker stays in the source and `findModuleDocBlock` still reads it. Scope is `@module` alone, not `^@\w+`: `@example ` and `@category ` carry prose a line-drop would take off the page. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21 --- .../spec/scripts/file-description.test.ts | 119 ++++++++++++++++++ packages/spec/scripts/lib/file-description.ts | 52 +++++++- 2 files changed, 166 insertions(+), 5 deletions(-) diff --git a/packages/spec/scripts/file-description.test.ts b/packages/spec/scripts/file-description.test.ts index 28358e1d0a..69f30f6b23 100644 --- a/packages/spec/scripts/file-description.test.ts +++ b/packages/spec/scripts/file-description.test.ts @@ -579,6 +579,106 @@ describe('renderFileDescription — #10924: the os:check marker is machinery, no }); }); +/** + * #13796 — the `@module` marker is machinery, not content. + * + * `@module ` is what tells `findModuleDocBlock` that a block documents + * the module (#13334). It is a prose line, and prose is emitted verbatim, so + * the marker became the OPENING LINE of every published page whose source + * carries it: fourteen on `main`, `content/docs/references/ui/sharing.mdx` + * opening on the literal text `@module ui/sharing` instead of on `Sharing & + * Embedding Protocol`. `check:docs` could not see it — that gate compares the + * artifact against the source, and the artifact reproduced the marker + * faithfully, exactly as it did for #5059 and #10924. + * + * The sibling surface never had it: `build-skill-references.ts` drops + * `@`-opening lines before taking a module's one-line description, so the skill + * index reads `Sharing & Embedding Protocol` for the same file. That is what + * makes this a filter missing on ONE surface rather than an open question about + * the tag. + * + * Both halves are pinned together on purpose. The drop belongs in the RENDERER + * and nowhere else: moved into `findModuleDocBlock` it would blank the eight + * modules #13334 selects by the marker alone, so "selection still reads it" is + * as much a part of this fix as "the page never shows it". + */ +describe('renderFileDescription — #13796: the `@module` marker is machinery, not content', () => { + const ctx = { fromCategory: 'ui', sourcePathToDocsRoute: () => null, sectionLevel: PAGE_SECTION_LEVEL }; + + const moduleBlock = (...body: string[]): string => + ['/**', ...body.map(l => (l === '' ? ' *' : ` * ${l}`)), ' */', '', "import { z } from 'zod';", ''].join('\n'); + + it('drops the marker and opens the page on the prose beneath it', () => { + // `ui/sharing.zod.ts`, reduced — the page the issue opened with. + const source = moduleBlock( + '@module ui/sharing', + '', + 'Sharing & Embedding Protocol', + '', + 'Public-link sharing of a form view.', + ); + const out = renderFileDescription(source, ctx); + expect(out.split('\n')[0]).toBe('Sharing & Embedding Protocol'); + expect(out).not.toContain('@module'); + // The drop takes the marker and nothing else — no page loses prose to it. + expect(out).toContain('Public-link sharing of a form view.'); + // …and the SELECTOR still reads the marker, which is the whole reason this + // block is the module's inside an import list (#13334). + expect(opening(findModuleDocBlock(source))).toBe('@module ui/sharing'); + }); + + it('drops a bare `@module` with no path, wherever in the block it sits', () => { + // `shared/metadata-collection.zod.ts` writes it last and without a path, + // so neither "first line" nor "has an argument" identifies the marker. + expect(renderFileDescription(moduleBlock('Metadata collections.', '', '@module'), ctx)).toBe( + 'Metadata collections.', + ); + }); + + it('keeps a marker shown INSIDE a fence — there it is an author illustrating the convention', () => { + // The prose-level condition, same as #10924's: stripping by text alone + // would silently edit a header that documents the convention itself. + const out = renderFileDescription( + moduleBlock('How a module header opens:', '', '```md', '@module ui/sharing', '```'), + ctx, + ); + expect(out).toContain('@module ui/sharing'); + }); + + it('keeps a mid-sentence mention — only a line that OPENS with the tag is the marker', () => { + // Judged with the same UNTRIMMED `^@module` test `hasModuleMarker` selects + // on, so the renderer drops exactly what the selector recognised. + const out = renderFileDescription(moduleBlock('Write the `@module` tag to mark this block.'), ctx); + expect(out).toContain('@module'); + }); + + it('leaves every other block tag alone — the filter is `@module`, not `^@\\w+`', () => { + // Scope, pinned. `@example Basic field mapping` is the caption of the fence + // beneath it and `@category Security` is a classification: a blanket + // tag-line drop would take that prose off the page, which is the one thing + // this fix may not do. How a tag WITH content should render is a different + // question and not this one. + const out = renderFileDescription( + moduleBlock( + '@module shared/mapping', + '', + 'Field mapping.', + '', + '@category Security', + '', + '@example Basic field mapping', + '```ts', + 'const m = 1;', + '```', + ), + ctx, + ); + expect(out).not.toContain('@module'); + expect(out).toContain('@category Security'); + expect(out).toContain('@example Basic field mapping'); + }); +}); + /** * #5553 — the block is rendered as the markdown it was written as. * @@ -1829,6 +1929,25 @@ describe('corpus — every rendered description is well-formed markdown', () => expect(shifted.filter(d => /^ {0,3}#(?:[ \t]|$)/m.test(withoutFences(d.untouched)))).toHaveLength(37); }); + it('never opens a page on the `@module` marker (#13796)', () => { + // The half that cannot rot: re-derived from the real `packages/spec/src` + // tree, so a source that acquires the marker tomorrow cannot re-acquire the + // defect with it. Deliberately an invariant (zero) rather than a count — + // the fourteen pages that carried it are the symptom, and a fifteenth + // marked module is a good thing, not a regression. + // + // Read on the rendered fragment, not on the emitted `.mdx`, for the reason + // the rest of this file is: `check:docs` reproduced the marker faithfully + // and stayed green through all fourteen. + const offenders: string[] = []; + for (const { rel, out } of described) { + for (const line of withoutFences(out).split('\n')) { + if (/^@module\b/.test(line)) offenders.push(`${rel}: ${line.trim()}`); + } + } + expect(offenders).toEqual([]); + }); + 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 44172b4dd8..2464af9a0b 100644 --- a/packages/spec/scripts/lib/file-description.ts +++ b/packages/spec/scripts/lib/file-description.ts @@ -466,6 +466,47 @@ function stripDocGutter(block: string): string[] { */ const SKILL_EXAMPLE_MARKER = ''; +/** + * The `@module ` marker, which SELECTS a block but is not part of it. + * + * `findModuleDocBlock` reads this tag as the author's explicit "this block + * documents the module" — `hasModuleMarker` above, and the module comment's + * #13334 section. That makes it machinery for the SELECTOR, and it stays in the + * source where the selector reads it. The renderer, though, emitted it verbatim + * like any other prose line, so fourteen published reference pages opened on the + * literal text `@module ui/sharing` instead of on their first sentence. + * + * The sibling surface never had the defect: `../build-skill-references.ts` drops + * `@`-opening lines before taking a module's one-line description, which is why + * the skill index reads `Sharing & Embedding Protocol` for the same file. This + * is the docs half of that rule, spelled here rather than restated there. + * + * Dropped rather than rewritten, for the same reason `SKILL_EXAMPLE_MARKER` is: + * it is machinery, not content, and the one thing it says — the module's own + * path — is what the page's route and title already say twice over. + * + * SCOPE: this tag, deliberately, and NOT `^@\w+` at large. The two other block + * tags that reach a page carry a payload a reader needs — `@example Basic field + * mapping` is the caption of the fence beneath it, `@category Security` is a + * classification — so a line-drop would take that prose with them, and "no page + * loses non-tag prose" is this fix's acceptance criterion. `@see` shows the + * shape those two want instead: `renderProse` REWRITES it into `See also: …` + * rather than dropping it. `@module` is the one tag whose entire content is the + * marker, so it is the one tag a drop is right for. + * + * Judged with the same UNTRIMMED `^@module\b` test `hasModuleMarker` selects + * on, so the renderer drops exactly what the selector recognised — never a + * mid-sentence mention of the tag, never an indented `@module` inside a list + * item, and never one shown inside a fenced example (the prose-level condition + * at the call site covers that, as it does for the marker above). + */ +const MODULE_MARKER = /^@module\b/; + +/** A machinery line the rendered page must never show, whichever kind it is. */ +function isMarkerLine(line: string): boolean { + return line.trim() === SKILL_EXAMPLE_MARKER || MODULE_MARKER.test(line); +} + /** What a line is, which decides which transforms may touch it. */ type LineKind = 'prose' | 'fenced' | 'indented'; @@ -745,14 +786,15 @@ export function renderFileDescription(source: string, ctx: FileDescriptionContex const block = findModuleDocBlock(source); if (block === null) return ''; - // Dropped BEFORE classification, so the marker never reaches the page and - // never separates a fence from the `@example` tag above it. Only at prose - // level: the same text inside a fenced block is an author illustrating the - // convention, which is content (see SKILL_EXAMPLE_MARKER). + // Dropped BEFORE classification, so neither marker reaches the page and the + // `os:check` one never separates a fence from the `@example` tag above it. + // Only at prose level: the same text inside a fenced block is an author + // illustrating the convention, which is content (see SKILL_EXAMPLE_MARKER and + // MODULE_MARKER). const gutterless = stripDocGutter(block); const markerKind = classifyLines(gutterless); const lines = withTagBlocksSeparated( - gutterless.filter((line, i) => !(markerKind[i] === 'prose' && line.trim() === SKILL_EXAMPLE_MARKER)), + gutterless.filter((line, i) => !(markerKind[i] === 'prose' && isMarkerLine(line))), ); const kind = classifyLines(lines); From b0e155aa7023fbe92beda32b2269537758de1e7d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 07:20:58 +0000 Subject: [PATCH 2/2] docs(spec): regenerate the fourteen reference pages the marker opened MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pnpm --filter @objectstack/spec gen:docs` after the renderer fix. Every page loses exactly two lines — its `@module ` marker and the blank line under it — and none gains any, so no page loses prose. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21 --- .changeset/module-marker-not-page-prose.md | 14 ++++++++++++++ .../docs/references/automation/bpmn-interop.mdx | 2 -- .../references/automation/builtin-node-config.mdx | 2 -- .../docs/references/automation/control-flow.mdx | 2 -- .../docs/references/automation/flow-function.mdx | 2 -- .../docs/references/automation/io-node-config.mdx | 2 -- .../docs/references/automation/node-executor.mdx | 2 -- .../automation/schemaless-node-config.mdx | 2 -- .../docs/references/automation/state-machine.mdx | 2 -- content/docs/references/shared/mapping.mdx | 2 -- content/docs/references/studio/flow-builder.mdx | 2 -- content/docs/references/studio/object-designer.mdx | 2 -- content/docs/references/studio/plugin.mdx | 2 -- content/docs/references/system/cache.mdx | 2 -- content/docs/references/ui/sharing.mdx | 2 -- 15 files changed, 14 insertions(+), 28 deletions(-) create mode 100644 .changeset/module-marker-not-page-prose.md diff --git a/.changeset/module-marker-not-page-prose.md b/.changeset/module-marker-not-page-prose.md new file mode 100644 index 0000000000..3163ca7fe1 --- /dev/null +++ b/.changeset/module-marker-not-page-prose.md @@ -0,0 +1,14 @@ +--- +'@objectstack/spec': patch +--- + +Reference pages no longer open with the `@module` marker line. That tag is what +tells the docs generator which doc block describes the module, but it is +machinery for the selector, not prose — and the renderer emitted it verbatim, so +fourteen published pages opened on the literal text `@module ui/sharing` instead +of on their first sentence. `renderFileDescription` now drops the marker at prose +level, exactly as it already drops the `check:skill-examples` opt-in marker, and +as the skill-index extractor has always done. The marker stays in the source and +still selects the block; only `@module` is dropped, because `@example` and +`@category` carry prose a line-drop would take off the page. No schema behavior +changes. diff --git a/content/docs/references/automation/bpmn-interop.mdx b/content/docs/references/automation/bpmn-interop.mdx index f1df189b8c..904bf4a26a 100644 --- a/content/docs/references/automation/bpmn-interop.mdx +++ b/content/docs/references/automation/bpmn-interop.mdx @@ -5,8 +5,6 @@ description: Bpmn Interop protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -@module automation/bpmn-interop - BPMN XML Interoperability Protocol Defines the specification for importing and exporting BPMN 2.0 XML diff --git a/content/docs/references/automation/builtin-node-config.mdx b/content/docs/references/automation/builtin-node-config.mdx index 2a4f991a06..463e2b89fd 100644 --- a/content/docs/references/automation/builtin-node-config.mdx +++ b/content/docs/references/automation/builtin-node-config.mdx @@ -5,8 +5,6 @@ description: Builtin Node 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/. */} -@module automation/builtin-node-config - Config contracts for the remaining flat builtins — the CRUD quartet (`get_record` / `create_record` / `update_record` / `delete_record`), `screen`, and `map` (#4045). Sibling of `io-node-config.zod.ts` diff --git a/content/docs/references/automation/control-flow.mdx b/content/docs/references/automation/control-flow.mdx index 072636a4f4..41f151d318 100644 --- a/content/docs/references/automation/control-flow.mdx +++ b/content/docs/references/automation/control-flow.mdx @@ -5,8 +5,6 @@ description: Control Flow protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -@module automation/control-flow - Structured control-flow constructs (ADR-0031) — the **native + AI-authored** flow model: a `loop` **container**, a `parallel` **block**, and structured `try/catch/retry`. Unlike BPMN's gateway/boundary/token graph (kept in the diff --git a/content/docs/references/automation/flow-function.mdx b/content/docs/references/automation/flow-function.mdx index ab9f4d51c6..83f1451ed5 100644 --- a/content/docs/references/automation/flow-function.mdx +++ b/content/docs/references/automation/flow-function.mdx @@ -5,8 +5,6 @@ description: Flow Function protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -@module automation/flow-function - The contract for a **named handler function a `script` node invokes** — contributed by `defineStack({ functions })` and resolved by name at execute time (#1870). diff --git a/content/docs/references/automation/io-node-config.mdx b/content/docs/references/automation/io-node-config.mdx index be53ac6bb6..a3f8790148 100644 --- a/content/docs/references/automation/io-node-config.mdx +++ b/content/docs/references/automation/io-node-config.mdx @@ -5,8 +5,6 @@ description: Io Node 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/. */} -@module automation/io-node-config - Config contracts for the flat IO builtins — `notify` and `http` (#4045). ## Provenance — written from the executors, not from the forms diff --git a/content/docs/references/automation/node-executor.mdx b/content/docs/references/automation/node-executor.mdx index b253293eab..99e0214598 100644 --- a/content/docs/references/automation/node-executor.mdx +++ b/content/docs/references/automation/node-executor.mdx @@ -5,8 +5,6 @@ description: Node Executor protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -@module automation/node-executor - Node Executor Plugin Protocol — Wait Node Pause/Resume Defines the specification for node executor plugins, with a focus on diff --git a/content/docs/references/automation/schemaless-node-config.mdx b/content/docs/references/automation/schemaless-node-config.mdx index 214db16a8d..3ef5a461ac 100644 --- a/content/docs/references/automation/schemaless-node-config.mdx +++ b/content/docs/references/automation/schemaless-node-config.mdx @@ -5,8 +5,6 @@ description: Schemaless Node 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/. */} -@module automation/schemaless-node-config - Config contracts for the **descriptor-schemaless** builtins whose designer form lives ONLY in objectui's hand-written `FLOW_NODE_CONFIG` table — `script`, `subflow` and `decision` (#4278). diff --git a/content/docs/references/automation/state-machine.mdx b/content/docs/references/automation/state-machine.mdx index 139862f2d0..8251baee11 100644 --- a/content/docs/references/automation/state-machine.mdx +++ b/content/docs/references/automation/state-machine.mdx @@ -5,8 +5,6 @@ description: State Machine protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -@module automation/state-machine - XState-inspired State Machine Protocol — hierarchical states, guarded transitions, entry/exit actions. Used to declare strict business-logic constraints and lifecycle management, so an AI author cannot "hallucinate" a diff --git a/content/docs/references/shared/mapping.mdx b/content/docs/references/shared/mapping.mdx index 81cf20989c..cba9e4ef28 100644 --- a/content/docs/references/shared/mapping.mdx +++ b/content/docs/references/shared/mapping.mdx @@ -5,8 +5,6 @@ description: Mapping protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -@module shared/mapping - Base Field Mapping Protocol Shared by: Connector diff --git a/content/docs/references/studio/flow-builder.mdx b/content/docs/references/studio/flow-builder.mdx index 128133543d..b4edd84d70 100644 --- a/content/docs/references/studio/flow-builder.mdx +++ b/content/docs/references/studio/flow-builder.mdx @@ -5,8 +5,6 @@ description: Flow Builder protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -@module studio/flow-builder - Studio Flow Builder Protocol Defines the specification for the visual Flow Builder (automation canvas) diff --git a/content/docs/references/studio/object-designer.mdx b/content/docs/references/studio/object-designer.mdx index 896f7804d1..9ff239dd52 100644 --- a/content/docs/references/studio/object-designer.mdx +++ b/content/docs/references/studio/object-designer.mdx @@ -5,8 +5,6 @@ description: Object Designer protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -@module studio/object-designer - Object Designer Protocol — Visual Field Editor, Relationship Mapper & ER Diagram Defines the specification for the Object Designer experience within ObjectStack Studio, diff --git a/content/docs/references/studio/plugin.mdx b/content/docs/references/studio/plugin.mdx index 91cc26f09b..f13ea2933b 100644 --- a/content/docs/references/studio/plugin.mdx +++ b/content/docs/references/studio/plugin.mdx @@ -5,8 +5,6 @@ description: 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/. */} -@module studio/plugin - Studio Plugin Protocol Defines the specification for Studio plugins — a VS Code-like extension model diff --git a/content/docs/references/system/cache.mdx b/content/docs/references/system/cache.mdx index 1541c5d040..f2ae75b68b 100644 --- a/content/docs/references/system/cache.mdx +++ b/content/docs/references/system/cache.mdx @@ -5,8 +5,6 @@ description: Cache protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -@module system/cache - Application-Level Cache Protocol Multi-tier caching strategy for application data. diff --git a/content/docs/references/ui/sharing.mdx b/content/docs/references/ui/sharing.mdx index ceb1ab1b28..50a901565e 100644 --- a/content/docs/references/ui/sharing.mdx +++ b/content/docs/references/ui/sharing.mdx @@ -5,8 +5,6 @@ description: Sharing protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -@module ui/sharing - Sharing & Embedding Protocol Public-link sharing of a form view. The module name is plural for historical