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
14 changes: 14 additions & 0 deletions .changeset/module-marker-not-page-prose.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 0 additions & 2 deletions content/docs/references/automation/bpmn-interop.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/automation/builtin-node-config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/automation/control-flow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/automation/flow-function.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/automation/io-node-config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/automation/node-executor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/automation/schemaless-node-config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/automation/state-machine.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/shared/mapping.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/studio/flow-builder.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/studio/object-designer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/studio/plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/system/cache.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/ui/sharing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
119 changes: 119 additions & 0 deletions packages/spec/scripts/file-description.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,106 @@ describe('renderFileDescription — #10924: the os:check marker is machinery, no
});
});

/**
* #13796 — the `@module` marker is machinery, not content.
*
* `@module <path>` 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.
*
Expand Down Expand Up @@ -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
Expand Down
52 changes: 47 additions & 5 deletions packages/spec/scripts/lib/file-description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,47 @@ function stripDocGutter(block: string): string[] {
*/
const SKILL_EXAMPLE_MARKER = '<!-- os:check -->';

/**
* The `@module <path>` 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';

Expand Down Expand Up @@ -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);

Expand Down
Loading