diff --git a/.changeset/module-doc-block-selection.md b/.changeset/module-doc-block-selection.md
new file mode 100644
index 0000000000..8c659118fd
--- /dev/null
+++ b/.changeset/module-doc-block-selection.md
@@ -0,0 +1,15 @@
+---
+'@objectstack/spec': patch
+---
+
+Reference pages and skill indexes no longer open with one schema's comment: a
+doc block sitting inside a module's import list is selected as the module
+description only when it carries an explicit `@module` marker. The two genuine
+headers in that position (`shared/mapping`, `system/cache`) now carry the
+marker; the eight modules whose "description" was a detached symbol comment
+(`ai/agent`, `data/datasource`, `data/hook`, `security/permission`,
+`ui/action`, `ui/app`, `ui/component`, `ui/page`) had the comment moved back to
+the declaration it documents — restoring editor hover for
+`AIModelConfigSchema`, `DriverType`, `ActionParamSchema` and
+`PageRegionSchema` — and their pages honestly print no module description
+instead of a wrong one. No schema behavior changes.
diff --git a/content/docs/references/ai/agent.mdx b/content/docs/references/ai/agent.mdx
index 5664bd01bb..880bc15536 100644
--- a/content/docs/references/ai/agent.mdx
+++ b/content/docs/references/ai/agent.mdx
@@ -5,8 +5,6 @@ description: Agent protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-AI Model Configuration
-
**Source:** `packages/spec/src/ai/agent.zod.ts`
diff --git a/content/docs/references/data/datasource.mdx b/content/docs/references/data/datasource.mdx
index a346d23ce3..8f2c8e67ba 100644
--- a/content/docs/references/data/datasource.mdx
+++ b/content/docs/references/data/datasource.mdx
@@ -5,9 +5,6 @@ description: Datasource protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-Driver Identifier
-Can be a built-in driver or a plugin-contributed driver (e.g., "com.vendor.snowflake").
-
**Source:** `packages/spec/src/data/datasource.zod.ts`
diff --git a/content/docs/references/data/hook.mdx b/content/docs/references/data/hook.mdx
index 68b61c4fc1..9aa788bd38 100644
--- a/content/docs/references/data/hook.mdx
+++ b/content/docs/references/data/hook.mdx
@@ -5,9 +5,6 @@ description: Hook protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-Hook Lifecycle Events
-Defines the interception points in the ObjectQL execution pipeline.
-
**Source:** `packages/spec/src/data/hook.zod.ts`
diff --git a/content/docs/references/security/permission.mdx b/content/docs/references/security/permission.mdx
index e2e81725db..d98e3febd5 100644
--- a/content/docs/references/security/permission.mdx
+++ b/content/docs/references/security/permission.mdx
@@ -5,14 +5,6 @@ description: Permission protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-Entity (Object) Level Permissions
-Defines CRUD + VAMA (View All / Modify All) + Lifecycle access.
-
-Refined with enterprise data lifecycle controls:
-- Transfer (Ownership change)
-- Restore (Soft delete recovery)
-- Purge (Hard delete / Compliance)
-
**Source:** `packages/spec/src/security/permission.zod.ts`
diff --git a/content/docs/references/shared/mapping.mdx b/content/docs/references/shared/mapping.mdx
index cba9e4ef28..81cf20989c 100644
--- a/content/docs/references/shared/mapping.mdx
+++ b/content/docs/references/shared/mapping.mdx
@@ -5,6 +5,8 @@ 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/system/cache.mdx b/content/docs/references/system/cache.mdx
index f2ae75b68b..1541c5d040 100644
--- a/content/docs/references/system/cache.mdx
+++ b/content/docs/references/system/cache.mdx
@@ -5,6 +5,8 @@ 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/action.mdx b/content/docs/references/ui/action.mdx
index af20ace4c8..baa6002cf1 100644
--- a/content/docs/references/ui/action.mdx
+++ b/content/docs/references/ui/action.mdx
@@ -5,39 +5,6 @@ description: Action protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-Action Parameter Schema
-
-Defines inputs required before executing an action.
-
-Two declaration modes:
-
-1. **Field-backed** (preferred) — reference an existing object field; the
- runtime resolves the field's label (i18n), type, validation rules,
- options, placeholder, help text, and widget mapping from object
- metadata. Cross-object references use `objectOverride`.
-
- ```ts
- params: [
- { field: 'email' }, // same object
- { field: 'role', objectOverride: 'sys_member' }, // different object
- ]
- ```
-
-2. **Inline** (legacy / bespoke) — declare `name`, `label`, `type` etc.
- inline when no matching object field exists. Inline values may also be
- used alongside `field` to override individual properties. A `lookup` /
- `master_detail` param declared this way MUST name its target object via
- `reference` — there is no field to inherit it from:
-
- ```ts
- params: [
- { name: 'inspector', label: 'Inspector', type: 'lookup', reference: 'sys_user' },
- ]
- ```
-
-`name` is required unless `field` is provided (in which case it defaults
-to the field name and is used as the request-body key).
-
**Source:** `packages/spec/src/ui/action.zod.ts`
diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx
index 40a3236a4a..16ac1a9440 100644
--- a/content/docs/references/ui/app.mdx
+++ b/content/docs/references/ui/app.mdx
@@ -5,21 +5,6 @@ description: App protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-Base Navigation Item Schema
-Shared properties for all navigation types.
-
-**NAMING CONVENTION:**
-Navigation item IDs are used in URLs and configuration and must be lowercase snake_case.
-
-@example Good IDs
-- 'menu_accounts'
-- 'page_dashboard'
-- 'nav_settings'
-
-@example Bad IDs (will be rejected)
-- 'MenuAccounts' (PascalCase)
-- 'Page Dashboard' (spaces)
-
**Source:** `packages/spec/src/ui/app.zod.ts`
diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx
index 3bdc82eedc..50cfcd3867 100644
--- a/content/docs/references/ui/component.mdx
+++ b/content/docs/references/ui/component.mdx
@@ -5,8 +5,6 @@ description: Component protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-Empty Properties Schema
-
**Source:** `packages/spec/src/ui/component.zod.ts`
diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx
index d03ef7ec13..8b2ac56291 100644
--- a/content/docs/references/ui/page.mdx
+++ b/content/docs/references/ui/page.mdx
@@ -5,9 +5,6 @@ description: Page protocol schemas
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
-Page Region Schema
-A named region in the template where components are dropped.
-
**Source:** `packages/spec/src/ui/page.zod.ts`
diff --git a/packages/spec/scripts/file-description.test.ts b/packages/spec/scripts/file-description.test.ts
index eb799771df..28358e1d0a 100644
--- a/packages/spec/scripts/file-description.test.ts
+++ b/packages/spec/scripts/file-description.test.ts
@@ -245,7 +245,8 @@ describe('findModuleDocBlock — a block documents a symbol, or it documents the
* The tightening applies only INSIDE the import block and stops at a comment of
* any kind, which is what the four `keeps` cases below pin: without the first
* limb, three real headers written above their imports go blank; without the
- * second, `api/analytics` and `system/cache` do.
+ * second, `api/analytics` does (and `system/cache` did, until #13334 put its
+ * selection on the `@module` marker instead — the describe after this one).
*
* MEASURED over `packages/spec/src` (193 sources, base `3322527f`): 28 pages
* lose a misattributed opening, 0 change to a different block, 165 are
@@ -330,15 +331,18 @@ describe('findModuleDocBlock — #13263: an import injected between a block and
expect(opening(findModuleDocBlock(source))).toBe('Package Documentation Metadata Protocol (ADR-0046)');
});
- it('keeps a header inside the imports when the next schema carries its own JSDoc', () => {
- // `system/cache.zod.ts` — its block names itself ("This File") and the
- // declaration beyond the injected import is already documented, so that
- // import says nothing about what the block documents.
+ it('keeps a marked header inside the imports when the next schema carries its own JSDoc', () => {
+ // `system/cache.zod.ts`, as #13334 left it: a block inside the import list
+ // with a comment on the far side is the position no structural signal can
+ // decide (see the #13334 describe below), so the real header now carries
+ // `@module` — and that marker, not the far-side comment, is what selects it.
const source = [
"import { z } from 'zod';",
"import { CronExpressionInputSchema } from '../shared/expression.zod';",
'',
'/**',
+ ' * @module system/cache',
+ ' *',
' * Application-Level Cache Protocol',
' */',
"import { lazySchema } from '../shared/lazy-schema';",
@@ -347,12 +351,15 @@ describe('findModuleDocBlock — #13263: an import injected between a block and
"export const CacheStrategySchema = z.enum(['lru']);",
'',
].join('\n');
- expect(opening(findModuleDocBlock(source))).toBe('Application-Level Cache Protocol');
+ expect(opening(findModuleDocBlock(source))).toBe('@module system/cache');
});
it('keeps a block with no declaration on the far side at all', () => {
// A module that is nothing but re-exports — the plumbing runs to the end of
// the file, so there is no symbol for the block to have been torn from.
+ // #13334's marker gate deliberately leaves this shape alone: with no symbol
+ // anywhere beyond, the block cannot be a detached symbol doc, so it selects
+ // bare.
const source = [
"import { z } from 'zod';",
'',
@@ -367,6 +374,132 @@ describe('findModuleDocBlock — #13263: an import injected between a block and
});
});
+/**
+ * #13334 — inside the import list, only `@module` selects.
+ *
+ * The position #13263 could not decide: a block with imports before it, an
+ * import the FIRST thing after it, and a comment beyond that plumbing. Two
+ * genuine headers (`system/cache`, `shared/mapping`) and eight detached symbol
+ * docs (`ai/agent`, `data/datasource`, `data/hook`, `security/permission`,
+ * `ui/action`, `ui/app`, `ui/component`, `ui/page`) sat there structurally
+ * identical — #13334 measured every positional signal (line number, imports
+ * before, block length, whether the next declaration has its own JSDoc) and
+ * none separates them; only the prose differs, and a prose pattern check is the
+ * approach `file-description.ts`'s own header rejects with the measurement
+ * behind the rejection. So the rule asks the author: the explicit `@module`
+ * marker — already this repo's spelling for a module header, fourteen sources
+ * above their imports open with it — decides, in both directions.
+ */
+describe('findModuleDocBlock — #13334: inside the import list, only `@module` selects', () => {
+ // `ui/page.zod.ts` as #13263 left it, reduced: the block reads like (and is)
+ // one schema's doc, the injected import sits against it, and the next thing
+ // beyond the plumbing is another doc block. Under the far-side-comment rule
+ // this was SELECTED — the eighth published page opening with `Page Region
+ // Schema`.
+ const unmarked = [
+ "import { z } from 'zod';",
+ '',
+ '/**',
+ ' * Page Region Schema',
+ ' * A named region in the template where components are dropped.',
+ ' */',
+ "import { lazySchema } from '../shared/lazy-schema';",
+ '',
+ '/** Shared history for this file. */',
+ "const PAGE_HISTORY = 'Until this shape was closed…';",
+ '',
+ 'export const PageRegionSchema = lazySchema(() => strictObject({}));',
+ '',
+ ].join('\n');
+
+ it('rejects an unmarked block inside the import list, comment on the far side or not', () => {
+ expect(findModuleDocBlock(unmarked)).toBeNull();
+ });
+
+ it('selects the byte-identical block once it carries `@module` — the minimal pair', () => {
+ const marked = unmarked.replace(' * Page Region Schema', ' * @module ui/page\n *\n * Page Region Schema');
+ expect(opening(findModuleDocBlock(marked))).toBe('@module ui/page');
+ });
+
+ it('lets `@module` decide even with a declaration on the far side of the plumbing', () => {
+ // The marker is the author's explicit declaration, and explicit beats
+ // inferred: a marked header whose next schema happens to be undocumented
+ // must not go blank the day that schema loses its JSDoc.
+ const source = [
+ "import { z } from 'zod';",
+ '',
+ '/**',
+ ' * @module system/example',
+ ' *',
+ ' * Example Protocol',
+ ' */',
+ "import { lazySchema } from '../shared/lazy-schema';",
+ '',
+ 'export const ExampleSchema = lazySchema(() => z.object({}));',
+ '',
+ ].join('\n');
+ expect(opening(findModuleDocBlock(source))).toBe('@module system/example');
+ });
+
+ it('reads an `@module` inside a fenced example as content, never as the marker', () => {
+ // The predicate runs through `classifyLines`, the same model of "which
+ // lines are code" the renderer uses — an author ILLUSTRATING the convention
+ // must not thereby publish the block they illustrated it in.
+ const source = [
+ "import { z } from 'zod';",
+ '',
+ '/**',
+ ' * Widget Config Schema',
+ ' *',
+ ' * ```ts',
+ ' * @module not/a/marker',
+ ' * ```',
+ ' */',
+ "import { lazySchema } from '../shared/lazy-schema';",
+ '',
+ '/** The widget. */',
+ 'export const WidgetSchema = lazySchema(() => z.object({}));',
+ '',
+ ].join('\n');
+ expect(findModuleDocBlock(source)).toBeNull();
+ });
+
+ it('reads a mid-sentence mention of the tag as prose about it, never as the marker', () => {
+ const source = [
+ "import { z } from 'zod';",
+ '',
+ '/**',
+ ' * Widget Config Schema — needs no @module marker to look like one.',
+ ' */',
+ "import { lazySchema } from '../shared/lazy-schema';",
+ '',
+ '/** The widget. */',
+ 'export const WidgetSchema = lazySchema(() => z.object({}));',
+ '',
+ ].join('\n');
+ expect(findModuleDocBlock(source)).toBeNull();
+ });
+
+ it('still needs no marker ABOVE the imports — the gate is scoped to the ambiguous position', () => {
+ // ~180 real headers sit outside the import list and stay untouched; the
+ // fixture beside `keeps a module header written above the imports` pins the
+ // same thing, and this one pins it against the gate specifically.
+ const source = [
+ '/**',
+ ' * Widget Protocol',
+ ' */',
+ '',
+ "import { z } from 'zod';",
+ "import { lazySchema } from '../shared/lazy-schema';",
+ '',
+ '/** The widget. */',
+ 'export const WidgetSchema = lazySchema(() => z.object({}));',
+ '',
+ ].join('\n');
+ expect(opening(findModuleDocBlock(source))).toBe('Widget Protocol');
+ });
+});
+
describe('renderFileDescription', () => {
// `fromCategory` is the directory the rendered module lives in (#6484); these
// cases reference `automation/` and are written as if from there.
@@ -1222,6 +1355,80 @@ describe('corpus — no reference source donates a symbol comment to its page',
expect(openingOf('kernel/execution-context.zod.ts')).toBeNull();
expect(openingOf('data/field.zod.ts')).toBeNull();
});
+
+ /**
+ * #13334's corpus limb — the half that cannot rot.
+ *
+ * Re-derives the marker gate from the real tree: a SELECTED block that sits
+ * inside the import list — plumbing somewhere before it, plumbing the first
+ * thing after it — must carry `@module` on a prose line, unless nothing but
+ * plumbing and blanks follows it to the end of the file. A source that
+ * drifts back into the ambiguous position (the lazify codemod's insertion
+ * point is exactly there) cannot quietly re-acquire a wrong page opening:
+ * unmarked, it renders no description at all.
+ */
+ it('never selects an unmarked block inside the import list (#13334)', () => {
+ const PLUMBING = /^(?:import\b|export\s*(?:\*|\{|type\s*\{))/;
+ const offenders: string[] = [];
+ for (const file of zodFiles) {
+ const source = fs.readFileSync(file, 'utf-8');
+ const block = findModuleDocBlock(source);
+ if (block === null) continue;
+ const marker = `/**${block}*/`;
+ const at = source.indexOf(marker);
+ if (at < 0) continue; // already reported by the first corpus case
+ const lines = source.split('\n');
+ const startLine = source.slice(0, at).split('\n').length - 1;
+ const endLine = startLine + marker.split('\n').length - 1;
+ if (!lines.slice(0, startLine).some(l => PLUMBING.test(l))) continue; // above the imports
+ const next = lines.slice(endLine + 1).find(l => l.trim() !== '') ?? '';
+ if (!PLUMBING.test(next)) continue; // a comment or declaration against the block decides for itself
+ // The same walk the selector runs: over blanks and plumbing, does
+ // anything but the end of the file lie beyond? A comment or a
+ // declaration both count — only a pure re-export tail selects bare.
+ const runsToEof = lines.slice(endLine + 1).every(
+ l => l.trim() === '' || (PLUMBING.test(l) || (!l.trimStart().startsWith('/') && !/^[A-Za-z_$@]/.test(l))),
+ );
+ const marked = block.split('\n').some(l => /^@module\b/.test(l.replace(/^\s*\*\s?/, '')));
+ if (!marked && !runsToEof) {
+ offenders.push(path.relative(SRC_DIR, file));
+ }
+ }
+ expect(offenders).toEqual([]);
+ });
+
+ it('opens `system/cache` and `shared/mapping` with their marked headers (#13334)', () => {
+ // The two genuine headers the ambiguous position held; each now carries
+ // the one-line `@module` marker, and the gate is what makes that marker
+ // load-bearing rather than decorative.
+ const openingOf = (rel: string) =>
+ opening(findModuleDocBlock(fs.readFileSync(path.join(SRC_DIR, rel), 'utf-8')));
+ expect(openingOf('system/cache.zod.ts')).toBe('@module system/cache');
+ expect(openingOf('shared/mapping.zod.ts')).toBe('@module shared/mapping');
+ });
+
+ it('publishes no description for the eight modules whose opening was one schema\'s doc (#13334)', () => {
+ // The corpus half of route ② — each of the eight had a symbol's comment
+ // wedged into its import list by the lazify codemod, published as the
+ // page's opening and as the module's skill-index pointer row. The comments
+ // moved back to the declarations they document (or to the family they
+ // head), so no block sits in the header zone documenting nothing: the page
+ // honestly prints no description. 宁可缺,不要错.
+ const openingOf = (rel: string) =>
+ opening(findModuleDocBlock(fs.readFileSync(path.join(SRC_DIR, rel), 'utf-8')));
+ for (const rel of [
+ 'ai/agent.zod.ts',
+ 'data/datasource.zod.ts',
+ 'data/hook.zod.ts',
+ 'security/permission.zod.ts',
+ 'ui/action.zod.ts',
+ 'ui/app.zod.ts',
+ 'ui/component.zod.ts',
+ 'ui/page.zod.ts',
+ ]) {
+ expect(openingOf(rel), rel).toBeNull();
+ }
+ });
});
/**
@@ -1422,7 +1629,11 @@ describe('corpus — every rendered description is well-formed markdown', () =>
};
it('finds descriptions to check', () => {
- expect(described.length).toBeGreaterThan(150);
+ // 146 at #13334 (154 before it — the marker gate blanked the eight modules
+ // whose "description" was one schema's detached comment; the diff of every
+ // per-file verdict, old selector vs new over all 208 sources, is in that
+ // card's PR: 200 identical, 8 SELECTED→null, 0 to a different block).
+ expect(described.length).toBeGreaterThan(140);
});
it('never cuts an inline code span in half (#5553)', () => {
diff --git a/packages/spec/scripts/lib/file-description.ts b/packages/spec/scripts/lib/file-description.ts
index b16f90ef14..44172b4dd8 100644
--- a/packages/spec/scripts/lib/file-description.ts
+++ b/packages/spec/scripts/lib/file-description.ts
@@ -33,8 +33,10 @@
* and this repo writes module headers on either side of them); the first
* `const`/`export const`/… does.
* 3. **Documenting nothing** — the block is not immediately followed by a
- * declaration, and — when the block sits INSIDE the import block — no
- * declaration sits on the far side of that plumbing either.
+ * declaration, and — when the block sits INSIDE the import list, an import
+ * the first thing on its far side — it carries the explicit `@module`
+ * marker (see the #13334 section below; a far side that is nothing but
+ * plumbing to the end of the file needs no marker).
*
* (3) is the load-bearing one, and it is simply TSDoc's own rule read back: a
* doc block belongs to the declaration it immediately precedes, which is why
@@ -76,18 +78,31 @@
* `cloud/template-manifest` and `system/doc` — three real headers whose
* imports happen to be followed directly by a declaration — lose their
* opening paragraph.
- * - **A comment of any kind on the far side still ends the preamble.** A `// ═══`
- * banner or a second doc block means the block did NOT sit against the
- * declaration before the codemod ran, so the injection tells us nothing.
- * `api/analytics` (banner) and `system/cache` (the next schema's own JSDoc)
- * keep their headers through exactly this clause.
- *
- * What it deliberately does NOT decide is the block that sits inside the import
- * list with a comment on the far side: `system/cache` and `shared/mapping` are
- * genuine headers there and seven others are detached symbol docs, and no
- * positional or structural signal separates them — only the prose does. That
- * residue needs an explicit `@module` marker or a corpus pass, not a cleverer
- * detector; #13263 records the reading, module by module.
+ * - **A comment directly against the block's far side still ends the preamble.**
+ * A `// ═══` banner or a second doc block as the FIRST thing after the block
+ * means the block did NOT sit against an injected import at all, so the
+ * codemod's ambiguity never arises. `api/analytics` (banner) keeps its header
+ * through exactly this clause.
+ *
+ * ## Inside the import list, only `@module` selects (#13334)
+ *
+ * What position cannot decide is the block whose far side opens with an IMPORT
+ * and whose walk beyond that plumbing then meets a comment: `system/cache` and
+ * `shared/mapping` were genuine headers there and eight others were detached
+ * symbol docs, and every positional signal was measured and separates nothing —
+ * line number, imports-before count, block length, whether the next declaration
+ * has its own JSDoc; only the prose differs (#13334 carries the table). A
+ * first-sentence pattern check is the approach this header already rejects, so
+ * the rule asks the author instead: a block sitting inside the import list —
+ * imports before it, an import the first thing after it — is selected only when
+ * it carries an explicit `@module` marker on a prose line of its own. The
+ * marker is this repo's existing spelling for "this block is the module's"
+ * (fourteen headers above their imports already open with it), and it decides
+ * in BOTH directions: with it the block is the header whatever lies beyond the
+ * plumbing, without it the block is not selected even though a comment on the
+ * far side would once have kept it. One exception needs no marker: plumbing
+ * that runs to the end of the file leaves no symbol the block could have been
+ * torn from (a pure re-export module), so the block still selects bare.
*
* When no block qualifies, the module has no description and the page prints
* none. 宁可缺,不要错 — a missing paragraph is a gap the reader can see, while
@@ -300,37 +315,51 @@ function nextNonBlankLine(lines: readonly string[], from: number): number | null
}
/**
- * Is there a declaration on the far side of the plumbing that starts at `from`
- * — i.e. does everything between hold nothing but blank lines and imports?
+ * What the walk from `from`, over blank lines and plumbing, reaches first.
*
- * Only ever asked of a block that sits INSIDE the import block, where the one
- * thing known to put an import between a doc block and its symbol is the lazify
- * codemod (see the module comment). Answering `true` there restores the verdict
- * the block had before that import was injected.
+ * Only ever asked of a block that sits INSIDE the import list (see the module
+ * comment's #13334 section), where the answer routes an UNMARKED block:
+ * `'eof'` selects it — the plumbing runs to the end of the file, so there is
+ * no symbol the block could have been torn from (a pure re-export module) —
+ * while `'declaration'` (the #13263 codemod shape) and `'comment'` (the
+ * position #13334 measured as undecidable) both refuse it, leaving `@module`
+ * as the only thing that selects there.
*
* A comment of any kind — a `// ═══` banner, a second doc block, an import's
- * own explanatory note — answers `false` instead of being walked over. That is
- * the same boundary `nextNonBlankLine` draws and for the same reason: it means
- * the block was NOT sitting against the declaration beforehand either, so the
- * injected import carries no information about what the block documents. It is
- * what keeps `api/analytics` (banner) and `system/cache` (the next schema's own
- * JSDoc) opening with their real module headers.
+ * own explanatory note — stops the walk instead of being walked over. That is
+ * the same boundary `nextNonBlankLine` draws and for the same reason.
*
* Continuation and closing lines of a multi-line import (` Foo,`,
* `} from './x';`) are plumbing too — they open with neither an identifier
* character nor a comment delimiter, exactly as `findModuleDocBlock`'s own walk
* reads them.
*/
-function declarationBeyondPlumbing(lines: readonly string[], from: number): boolean {
+function beyondPlumbing(lines: readonly string[], from: number): 'declaration' | 'comment' | 'eof' {
for (let i = from; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '') continue;
- if (line.trimStart().startsWith('/')) return false; // a comment ends the preamble
+ if (line.trimStart().startsWith('/')) return 'comment';
if (MODULE_PLUMBING.test(line)) continue;
if (!startsDeclaration(line)) continue; // continuation / closing punctuation
- return true;
+ return 'declaration';
}
- return false;
+ return 'eof';
+}
+
+/**
+ * Does the block carry the `@module` marker — this repo's existing spelling
+ * for "this block documents the module" — on a prose line of its own?
+ *
+ * Judged on gutter-stripped lines through the same `classifyLines` model the
+ * renderer uses, so an `@module` shown inside a fenced example (an author
+ * illustrating the convention) is content, never a marker. The line must OPEN
+ * with the tag: fourteen real headers write it exactly that way (`@module
+ * ui/sharing`), and a mid-sentence mention of the tag is prose about it.
+ */
+function hasModuleMarker(inner: string): boolean {
+ const lines = stripDocGutter(inner);
+ const kind = classifyLines(lines);
+ return lines.some((line, i) => kind[i] === 'prose' && /^@module\b/.test(line));
}
/**
@@ -364,11 +393,25 @@ export function findModuleDocBlock(source: string): string | null {
if (end >= lines.length) return null; // unterminated — nothing to trust
const next = nextNonBlankLine(lines, end + 1);
if (next !== null && startsDeclaration(lines[next])) return null; // documents a symbol
- // …and the same verdict when only injected plumbing stands between the
- // two: the import moved, the attachment did not.
- if (insideImportBlock && declarationBeyondPlumbing(lines, end + 1)) return null;
const raw = lines.slice(i, end + 1).join('\n');
- return raw.slice(raw.indexOf('/**') + 3, raw.lastIndexOf('*/'));
+ const inner = raw.slice(raw.indexOf('/**') + 3, raw.lastIndexOf('*/'));
+ // INSIDE the import list — imports before the block, an import the first
+ // thing after it — position cannot say what the block documents (#13334;
+ // the measurement is in the module comment). Only the author's explicit
+ // `@module` marker selects it there, except when the plumbing runs to the
+ // end of the file and no symbol exists for the block to have been torn
+ // from. This subsumes #13263's far-side rule: an unmarked block with a
+ // declaration beyond the plumbing is still refused.
+ if (
+ insideImportBlock &&
+ next !== null &&
+ MODULE_PLUMBING.test(lines[next]) &&
+ !hasModuleMarker(inner) &&
+ beyondPlumbing(lines, next) !== 'eof'
+ ) {
+ return null;
+ }
+ return inner;
}
if (line.startsWith('/*')) { i = endOfBlockComment(lines, i) + 1; continue; }
diff --git a/packages/spec/src/ai/agent.zod.ts b/packages/spec/src/ai/agent.zod.ts
index ee4a886094..690dc4e58e 100644
--- a/packages/spec/src/ai/agent.zod.ts
+++ b/packages/spec/src/ai/agent.zod.ts
@@ -5,10 +5,6 @@ import { retiredKey } from '../shared/retired-key';
import { ProtectionSchema } from '../shared/protection.zod';
import { MetadataProtectionFields } from '../kernel/metadata-protection.zod';
import { StateMachineSchema } from '../automation/state-machine.zod';
-
-/**
- * AI Model Configuration
- */
import { lazySchema } from '../shared/lazy-schema';
import { strictObject } from '../shared/strict-object';
@@ -24,6 +20,9 @@ const AGENT_HISTORY =
'Until this shape was closed these were dropped silently — the agent still registered '
+ 'and still answered, minus whatever the key was meant to configure or constrain.';
+/**
+ * AI Model Configuration
+ */
export const AIModelConfigSchema = lazySchema(() => strictObject({
surface: 'this model configuration',
history: AGENT_HISTORY,
diff --git a/packages/spec/src/data/datasource.zod.ts b/packages/spec/src/data/datasource.zod.ts
index c004d5ab8c..0581ad4065 100644
--- a/packages/spec/src/data/datasource.zod.ts
+++ b/packages/spec/src/data/datasource.zod.ts
@@ -1,11 +1,6 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
-
-/**
- * Driver Identifier
- * Can be a built-in driver or a plugin-contributed driver (e.g., "com.vendor.snowflake").
- */
import { lazySchema } from '../shared/lazy-schema';
import { strictObject } from '../shared/strict-object';
import { MetadataProtectionFields } from '../kernel/metadata-protection.zod';
@@ -173,6 +168,10 @@ const RETIRED_READ_REPLICAS =
+ '`config` at that endpoint, which is the only read-scaling path that works today. '
+ 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.';
+/**
+ * Driver Identifier
+ * Can be a built-in driver or a plugin-contributed driver (e.g., "com.vendor.snowflake").
+ */
export const DriverType = z.string().describe('Underlying driver identifier');
export type DriverType = z.input;
diff --git a/packages/spec/src/data/hook.zod.ts b/packages/spec/src/data/hook.zod.ts
index d14db06138..079adc8dde 100644
--- a/packages/spec/src/data/hook.zod.ts
+++ b/packages/spec/src/data/hook.zod.ts
@@ -2,11 +2,6 @@
import { z } from 'zod';
import { ExpressionInputSchema } from '../shared/expression.zod';
-
-/**
- * Hook Lifecycle Events
- * Defines the interception points in the ObjectQL execution pipeline.
- */
import { lazySchema } from '../shared/lazy-schema';
import { retiredKey } from '../shared/retired-key';
import { strictObject } from '../shared/strict-object';
@@ -68,6 +63,11 @@ const hookTargetError =
+ "`object: 'account'` or `object: ['account', 'contact']` — or, if firing on "
+ "every object really is the intent, write the wildcard explicitly: `object: '*'`.";
+/**
+ * Hook Lifecycle Events
+ * Defines the interception points in the ObjectQL execution pipeline.
+ */
+
/**
* The lifecycle events a hook can subscribe to.
*
diff --git a/packages/spec/src/security/permission.zod.ts b/packages/spec/src/security/permission.zod.ts
index 1c726da8af..fd6a64e59f 100644
--- a/packages/spec/src/security/permission.zod.ts
+++ b/packages/spec/src/security/permission.zod.ts
@@ -6,19 +6,10 @@ import { RowLevelSecurityPolicySchema } from './rls.zod';
import { ApiOperationSchema } from '../data/object.zod';
import { ProtectionSchema } from '../shared/protection.zod';
import { MetadataProtectionFields } from '../kernel/metadata-protection.zod';
-
-/**
- * Entity (Object) Level Permissions
- * Defines CRUD + VAMA (View All / Modify All) + Lifecycle access.
- *
- * Refined with enterprise data lifecycle controls:
- * - Transfer (Ownership change)
- * - Restore (Soft delete recovery)
- * - Purge (Hard delete / Compliance)
- */
import { lazySchema } from '../shared/lazy-schema';
import { acceptRetiredDefaultResidue, retiredKey } from '../shared/retired-key';
import { strictObject } from '../shared/strict-object';
+
/**
* [ADR-0057 D1] Object access DEPTH — the Dataverse "access level" axis,
* layered on top of OWD. Widens the owner-match for owner-scoped objects.
@@ -43,6 +34,16 @@ export type ObjectAccessScope = z.input;
* a recognisable spelling of it.
*/
+/**
+ * Entity (Object) Level Permissions
+ * Defines CRUD + VAMA (View All / Modify All) + Lifecycle access.
+ *
+ * Refined with enterprise data lifecycle controls:
+ * - Transfer (Ownership change)
+ * - Restore (Soft delete recovery)
+ * - Purge (Hard delete / Compliance)
+ */
+
/**
* Semantic near-misses for object-permission bits — mostly the bare CRUD verbs
* (Salesforce object-permission vocabulary) an author reaches for before
diff --git a/packages/spec/src/shared/mapping.zod.ts b/packages/spec/src/shared/mapping.zod.ts
index 3fc3a765e9..506db6ca70 100644
--- a/packages/spec/src/shared/mapping.zod.ts
+++ b/packages/spec/src/shared/mapping.zod.ts
@@ -4,6 +4,8 @@ import { z } from 'zod';
import { retiredKey } from './retired-key';
/**
+ * @module shared/mapping
+ *
* Base Field Mapping Protocol
*
* Shared by: Connector
diff --git a/packages/spec/src/system/cache.zod.ts b/packages/spec/src/system/cache.zod.ts
index e1585142a2..5f353d1277 100644
--- a/packages/spec/src/system/cache.zod.ts
+++ b/packages/spec/src/system/cache.zod.ts
@@ -4,8 +4,10 @@ import { z } from 'zod';
import { CronExpressionInputSchema } from '../shared/expression.zod';
/**
+ * @module system/cache
+ *
* Application-Level Cache Protocol
- *
+ *
* Multi-tier caching strategy for application data.
* Supports Memory, Redis, Memcached, and CDN.
*
diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts
index 2fc47a80f3..d4bdb99391 100644
--- a/packages/spec/src/ui/action.zod.ts
+++ b/packages/spec/src/ui/action.zod.ts
@@ -23,41 +23,6 @@ import { PUBLIC_AUTH_FEATURE_NAMES, lowerRequiresFeature } from '../kernel/publi
import { strictUnknownKeyError } from '../shared/suggestions.zod';
import { strictObject } from '../shared/strict-object';
import { MetadataProtectionFields } from '../kernel/metadata-protection.zod';
-
-/**
- * Action Parameter Schema
- *
- * Defines inputs required before executing an action.
- *
- * Two declaration modes:
- *
- * 1. **Field-backed** (preferred) — reference an existing object field; the
- * runtime resolves the field's label (i18n), type, validation rules,
- * options, placeholder, help text, and widget mapping from object
- * metadata. Cross-object references use `objectOverride`.
- *
- * ```ts
- * params: [
- * { field: 'email' }, // same object
- * { field: 'role', objectOverride: 'sys_member' }, // different object
- * ]
- * ```
- *
- * 2. **Inline** (legacy / bespoke) — declare `name`, `label`, `type` etc.
- * inline when no matching object field exists. Inline values may also be
- * used alongside `field` to override individual properties. A `lookup` /
- * `master_detail` param declared this way MUST name its target object via
- * `reference` — there is no field to inherit it from:
- *
- * ```ts
- * params: [
- * { name: 'inspector', label: 'Inspector', type: 'lookup', reference: 'sys_user' },
- * ]
- * ```
- *
- * `name` is required unless `field` is provided (in which case it defaults
- * to the field name and is used as the request-body key).
- */
import { lazySchema } from '../shared/lazy-schema';
/**
@@ -167,6 +132,40 @@ const actionParamOptionUndeclaredAnywhere = (key: 'icon' | 'disabled'): string =
+ `\`SelectOptionMetadata\` type, which no metadata path populates and no widget reads. An `
+ `action param's options are \`{ label, value, visibleWhen }\`; drop the key.`;
+/**
+ * Action Parameter Schema
+ *
+ * Defines inputs required before executing an action.
+ *
+ * Two declaration modes:
+ *
+ * 1. **Field-backed** (preferred) — reference an existing object field; the
+ * runtime resolves the field's label (i18n), type, validation rules,
+ * options, placeholder, help text, and widget mapping from object
+ * metadata. Cross-object references use `objectOverride`.
+ *
+ * ```ts
+ * params: [
+ * { field: 'email' }, // same object
+ * { field: 'role', objectOverride: 'sys_member' }, // different object
+ * ]
+ * ```
+ *
+ * 2. **Inline** (legacy / bespoke) — declare `name`, `label`, `type` etc.
+ * inline when no matching object field exists. Inline values may also be
+ * used alongside `field` to override individual properties. A `lookup` /
+ * `master_detail` param declared this way MUST name its target object via
+ * `reference` — there is no field to inherit it from:
+ *
+ * ```ts
+ * params: [
+ * { name: 'inspector', label: 'Inspector', type: 'lookup', reference: 'sys_user' },
+ * ]
+ * ```
+ *
+ * `name` is required unless `field` is provided (in which case it defaults
+ * to the field name and is used as the request-body key).
+ */
export const ActionParamSchema = lazySchema(() => strictObject(
{
surface: 'this action param',
diff --git a/packages/spec/src/ui/app.zod.ts b/packages/spec/src/ui/app.zod.ts
index fe8562b7da..7804ae2c07 100644
--- a/packages/spec/src/ui/app.zod.ts
+++ b/packages/spec/src/ui/app.zod.ts
@@ -6,23 +6,6 @@ import { ExpressionInputSchema } from '../shared/expression.zod';
import { I18nLabelSchema } from './i18n.zod';
import { retiredKey } from '../shared/retired-key';
import { strictObject, type StrictObjectOptions } from '../shared/strict-object';
-
-/**
- * Base Navigation Item Schema
- * Shared properties for all navigation types.
- *
- * **NAMING CONVENTION:**
- * Navigation item IDs are used in URLs and configuration and must be lowercase snake_case.
- *
- * @example Good IDs
- * - 'menu_accounts'
- * - 'page_dashboard'
- * - 'nav_settings'
- *
- * @example Bad IDs (will be rejected)
- * - 'MenuAccounts' (PascalCase)
- * - 'Page Dashboard' (spaces)
- */
import { lazySchema } from '../shared/lazy-schema';
import { MetadataProtectionFields } from '../kernel/metadata-protection.zod';
import { ProtectionSchema } from '../shared/protection.zod';
@@ -256,6 +239,23 @@ const navItemSurface = (variant: NavItemVariant): StrictObjectOptions => ({
'an entry that should have been gated).',
});
+/**
+ * Base Navigation Item Schema
+ * Shared properties for all navigation types.
+ *
+ * **NAMING CONVENTION:**
+ * Navigation item IDs are used in URLs and configuration and must be lowercase snake_case.
+ *
+ * @example Good IDs
+ * - 'menu_accounts'
+ * - 'page_dashboard'
+ * - 'nav_settings'
+ *
+ * @example Bad IDs (will be rejected)
+ * - 'MenuAccounts' (PascalCase)
+ * - 'Page Dashboard' (spaces)
+ */
+
/**
* Shared shape of every navigation item — spread into the nine branches below.
*
diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts
index 14ed30b8dc..c09d708b0a 100644
--- a/packages/spec/src/ui/component.zod.ts
+++ b/packages/spec/src/ui/component.zod.ts
@@ -6,6 +6,18 @@ import { ViewFilterRuleSchema, ViewDataSchema } from './view.zod';
import { InlineActionSchema, ActionLocationSchema } from './action.zod';
import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod';
import { FeedItemType, FeedFilterMode } from '../data/feed.zod';
+import { lazySchema } from '../shared/lazy-schema';
+import { ExpressionInputSchema } from '../shared/expression.zod';
+import { retiredKey } from '../shared/retired-key';
+// `element:record_picker`'s flat `sort` shorthand is the SAME contract as
+// `ElementDataSourceSchema.sort` (page.zod.ts) — one shape, imported from the
+// shared source rather than re-spelled here (#6276).
+import { SortItemSchema } from '../shared/enums.zod';
+import { strictObject } from '../shared/strict-object';
+import type { KeySetGuidance } from '../shared/suggestions.zod';
+// [#13855] The section → field-group reference form, shared with
+// `FormSectionSchema` (view.zod.ts) so one mixing rule serves both escape hatches.
+import { SectionGroupKeySchema, sectionGroupReferenceRefinement } from '../shared/section-group-reference';
// ---------------------------------------------------------------------------
// CLOSED AGAINST UNKNOWN KEYS as of #4001 batch A -- all 31 object sites.
@@ -210,22 +222,6 @@ import { FeedItemType, FeedFilterMode } from '../data/feed.zod';
// ---------------------------------------------------------------------------
-/**
- * Empty Properties Schema
- */
-import { lazySchema } from '../shared/lazy-schema';
-import { ExpressionInputSchema } from '../shared/expression.zod';
-import { retiredKey } from '../shared/retired-key';
-// `element:record_picker`'s flat `sort` shorthand is the SAME contract as
-// `ElementDataSourceSchema.sort` (page.zod.ts) — one shape, imported from the
-// shared source rather than re-spelled here (#6276).
-import { SortItemSchema } from '../shared/enums.zod';
-import { strictObject } from '../shared/strict-object';
-import type { KeySetGuidance } from '../shared/suggestions.zod';
-// [#13855] The section → field-group reference form, shared with
-// `FormSectionSchema` (view.zod.ts) so one mixing rule serves both escape hatches.
-import { SectionGroupKeySchema, sectionGroupReferenceRefinement } from '../shared/section-group-reference';
-
/**
* What silently happened to an undeclared prop before these shapes were closed
* — the one sentence every rejection on this file carries.
@@ -316,6 +312,10 @@ const COMPONENT_LEVEL_GUIDANCE: readonly KeySetGuidance[] = [
COMPONENT_NODE_KEYS_GUIDANCE,
];
+/**
+ * Empty Properties Schema
+ */
+
/**
* A component that declares no props at all — `app:launcher`, `nav:menu`,
* `nav:breadcrumb`, `global:search`, `global:notifications`, `user:profile`,
diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts
index 2183197394..bd5bf6c2b7 100644
--- a/packages/spec/src/ui/page.zod.ts
+++ b/packages/spec/src/ui/page.zod.ts
@@ -19,10 +19,6 @@ import {
ListColumnSchema,
} from './view.zod';
-/**
- * Page Region Schema
- * A named region in the template where components are dropped.
- */
import { lazySchema } from '../shared/lazy-schema';
import { strictObject } from '../shared/strict-object';
import { MetadataProtectionFields } from '../kernel/metadata-protection.zod';
@@ -39,6 +35,10 @@ const PAGE_HISTORY =
'Until this shape was closed these were dropped silently — the page still rendered, '
+ 'without whatever the key was meant to configure.';
+/**
+ * Page Region Schema
+ * A named region in the template where components are dropped.
+ */
export const PageRegionSchema = lazySchema(() => strictObject({
surface: 'this page region',
history: PAGE_HISTORY,
diff --git a/skills/objectstack-ai/references/_index.md b/skills/objectstack-ai/references/_index.md
index ad27f6e764..aae04aa4f2 100644
--- a/skills/objectstack-ai/references/_index.md
+++ b/skills/objectstack-ai/references/_index.md
@@ -9,7 +9,7 @@ from `node_modules` — there is no local copy in the skill bundle.
## Core schemas
-- `node_modules/@objectstack/spec/src/ai/agent.zod.ts` — AI Model Configuration
+- `node_modules/@objectstack/spec/src/ai/agent.zod.ts` — Exports: AIModelConfigSchema, StructuredOutputFormatSchema, TransformPipelineStepSchema, StructuredOutputConfigSchema, AgentSchema
- `node_modules/@objectstack/spec/src/ai/conversation.zod.ts` — AI Conversation Memory Protocol
- `node_modules/@objectstack/spec/src/ai/embedding.zod.ts` — Embedding & Vector Store Primitives
- `node_modules/@objectstack/spec/src/ai/knowledge-document.zod.ts` — Knowledge Document / Chunk / Hit — canonical shapes shared by every
diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md
index 107b66d09d..a87f7fb377 100644
--- a/skills/objectstack-data/references/_index.md
+++ b/skills/objectstack-data/references/_index.md
@@ -9,13 +9,13 @@ from `node_modules` — there is no local copy in the skill bundle.
## Core schemas
-- `node_modules/@objectstack/spec/src/data/datasource.zod.ts` — Driver Identifier
+- `node_modules/@objectstack/spec/src/data/datasource.zod.ts` — Exports: DriverType, DriverDefinitionSchema, SchemaModeSchema, ExternalDatasourceSettingsSchema, DatasourceSchema
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
-- `node_modules/@objectstack/spec/src/data/hook.zod.ts` — Hook Lifecycle Events
+- `node_modules/@objectstack/spec/src/data/hook.zod.ts` — Exports: HookEvent, HookSchema, HookContextSchema
- `node_modules/@objectstack/spec/src/data/object.zod.ts` — Exports: ApiMethod, ApiOperationSchema, ObjectCapabilities, IndexSchema, TenancyConfigSchema
- `node_modules/@objectstack/spec/src/data/seed.zod.ts` — Exports: SeedMode, SeedSchema, leadSeed
- `node_modules/@objectstack/spec/src/data/validation.zod.ts` — ObjectStack Validation Protocol
-- `node_modules/@objectstack/spec/src/security/permission.zod.ts` — Entity (Object) Level Permissions
+- `node_modules/@objectstack/spec/src/security/permission.zod.ts` — Exports: ObjectAccessScopeSchema, ObjectPermissionSchema, EffectiveObjectPermissionSchema, AdminScopeSchema, FieldPermissionSchema
## Transitive dependencies
@@ -45,7 +45,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/shared/suggestions.zod.ts` — "Did you mean?" Suggestion Utilities
- `node_modules/@objectstack/spec/src/system/deploy-bundle.zod.ts` — Deploy Bundle Protocol
- `node_modules/@objectstack/spec/src/ui/action-params.zod.ts` — The action DISPATCH contract: what the platform validates on the way in, and
-- `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema
+- `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Exports: ActionParamSchema, ActionType, ActionLocationSchema, ActionAiSchema, ActionSchema
- `node_modules/@objectstack/spec/src/ui/bulk-action.zod.ts` — Bulk Action Schemas
- `node_modules/@objectstack/spec/src/ui/chart.zod.ts` — Unified Chart Type Taxonomy
- `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape.
diff --git a/skills/objectstack-platform/references/_index.md b/skills/objectstack-platform/references/_index.md
index 14638841fb..f67bd317f5 100644
--- a/skills/objectstack-platform/references/_index.md
+++ b/skills/objectstack-platform/references/_index.md
@@ -9,7 +9,7 @@ from `node_modules` — there is no local copy in the skill bundle.
## Core schemas
-- `node_modules/@objectstack/spec/src/data/datasource.zod.ts` — Driver Identifier
+- `node_modules/@objectstack/spec/src/data/datasource.zod.ts` — Exports: DriverType, DriverDefinitionSchema, SchemaModeSchema, ExternalDatasourceSettingsSchema, DatasourceSchema
- `node_modules/@objectstack/spec/src/data/seed.zod.ts` — Exports: SeedMode, SeedSchema, leadSeed
- `node_modules/@objectstack/spec/src/kernel/context.zod.ts` — Exports: RuntimeMode, KernelContextSchema, TenantRuntimeContextSchema
- `node_modules/@objectstack/spec/src/kernel/manifest.zod.ts` — Exports: PluginPermissionsSchema, ManifestPermissionsSchema, PluginEnginesSchema, PluginRuntimeSchema, PluginPackagingSchema
@@ -44,8 +44,8 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/shared/suggestions.zod.ts` — "Did you mean?" Suggestion Utilities
- `node_modules/@objectstack/spec/src/system/tenant.zod.ts` — Tenant Schema (Multi-Tenant Architecture)
- `node_modules/@objectstack/spec/src/ui/action-params.zod.ts` — The action DISPATCH contract: what the platform validates on the way in, and
-- `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema
-- `node_modules/@objectstack/spec/src/ui/app.zod.ts` — Base Navigation Item Schema
+- `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Exports: ActionParamSchema, ActionType, ActionLocationSchema, ActionAiSchema, ActionSchema
+- `node_modules/@objectstack/spec/src/ui/app.zod.ts` — Exports: ObjectNavItemSchema, DashboardNavItemSchema, PageNavItemSchema, UrlNavItemSchema, ReportNavItemSchema
- `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape.
## How to read these
diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md
index 8fed011033..8efc51b9d7 100644
--- a/skills/objectstack-ui/references/_index.md
+++ b/skills/objectstack-ui/references/_index.md
@@ -9,13 +9,13 @@ from `node_modules` — there is no local copy in the skill bundle.
## Core schemas
-- `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema
-- `node_modules/@objectstack/spec/src/ui/app.zod.ts` — Base Navigation Item Schema
+- `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Exports: ActionParamSchema, ActionType, ActionLocationSchema, ActionAiSchema, ActionSchema
+- `node_modules/@objectstack/spec/src/ui/app.zod.ts` — Exports: ObjectNavItemSchema, DashboardNavItemSchema, PageNavItemSchema, UrlNavItemSchema, ReportNavItemSchema
- `node_modules/@objectstack/spec/src/ui/chart.zod.ts` — Unified Chart Type Taxonomy
-- `node_modules/@objectstack/spec/src/ui/component.zod.ts` — Empty Properties Schema
+- `node_modules/@objectstack/spec/src/ui/component.zod.ts` — Exports: PageContainerProps, PageHeaderProps, PageTabsProps, PageCardProps, RecordDetailsProps
- `node_modules/@objectstack/spec/src/ui/dashboard.zod.ts` — Exports: WidgetColorVariantSchema, WidgetActionTypeSchema, DashboardHeaderActionSchema, DashboardHeaderSchema, DashboardWidgetOptionsSchema
- `node_modules/@objectstack/spec/src/ui/dataset.zod.ts` — Analytics Dataset — the one semantic layer (ADR-0021).
-- `node_modules/@objectstack/spec/src/ui/page.zod.ts` — Page Region Schema
+- `node_modules/@objectstack/spec/src/ui/page.zod.ts` — Exports: PageRegionSchema, PageComponentType, ElementDataSourceSchema, PageComponentSchema, PageVariableSchema
- `node_modules/@objectstack/spec/src/ui/report.zod.ts` — Exports: ReportType, ReportChartSchema, ReportSortSchema, JoinedReportBlockSchema, ReportSchema
- `node_modules/@objectstack/spec/src/ui/view.zod.ts` — View protocol schemas — the `view` metadata type and its three persisted body spellings.
- `node_modules/@objectstack/spec/src/ui/widget.zod.ts` — Exports: FieldWidgetPropsSchema