Draft
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
29 changes: 29 additions & 0 deletions .changeset/query-zod-module-description.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
"@objectstack/spec": patch
---

fix(spec): `data/query.zod.ts` now describes the query AST, not one sort node

The published skill reference indexes and the generated `data/query` reference
page opened on "Sort Node" — the description of a single `{ field, order }`
pair — for the file that carries the entire `QueryAST`.

The generators publish the module's OWN doc block: top-level, in the header
zone, documenting no symbol. `query.zod.ts` had no block of its own, and
`SortNodeSchema`'s qualified, because the file's rationale comments sit between
that block and its schema, so nothing attached it to a symbol. The mechanism was
understood when the file was written — a warning comment sits directly under
that block saying the first block becomes the page description. What was not
noticed is the ORDERING: the first block belonged to a symbol, and a comment
warning about a rule is not the same as satisfying it.

The file now opens with a short header of its own, reusing the sentence
`QueryAST`'s own type block already carried. Four published skill indexes
(`objectstack-query`, `objectstack-data`, `objectstack-api`, `objectstack-ui`)
and the reference page name the query AST as a result. `SortNode`'s block is
untouched and still documents the schema it belongs to; the warning comment
beside it now names which block is published and which selector picks it.

What the wrong row cost, in the skills' own terms: the skill tells an agent to
read the source for exact field shapes, so a pointer labelled "Sort Node" makes
it skip the one file that carries the AST.
8 changes: 5 additions & 3 deletions content/docs/references/data/query.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,11 @@ description: Query protocol schemas

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Sort Node
Represents "Order By" — one `{ field, order }` pair. Unknown keys are
REJECTED (#4721); spell the direction `order`, never `direction`.
QueryAST — Abstract Syntax Tree for data queries.

The query AST every data read is expressed in: `where` predicates, `fields`
projection, `orderBy` sort nodes, `expand` traversal, pagination and
aggregation.

<Callout type="info">
**Source:** `packages/spec/src/data/query.zod.ts`
Expand Down
110 changes: 110 additions & 0 deletions packages/spec/scripts/query-pointer-row.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Pin for WHAT the published pointer row for `data/query.zod.ts` names — the
* query AST, not one sort node.
*
* `build-skill-references.ts` describes each source by the module's own doc
* block (`lib/file-description.ts` selects it: top-level, in the header zone,
* documenting no symbol). `query.zod.ts` had no block of its own, and
* `SortNodeSchema`'s block qualified — the file's rationale comments sit
* between that block and its schema, so nothing attached it to a symbol. Four
* published indexes (`objectstack-query`, `-data`, `-api`, `-ui`) therefore
* labelled the file carrying the whole `QueryAST` "Sort Node", and the public
* reference page `content/docs/references/data/query.mdx` opened on it. The
* skill tells an agent to Read the source for exact field shapes, so that row
* cost the one read it exists to route: an agent looking for the query AST
* skips the only file that has it.
*
* No gate could see it. `check:skill-refs` and `check:docs` compare the
* artifact against the generator, and the generator reproduced the wrong block
* faithfully — the same blind spot #5059 and #12201 found one layer up, so the
* answer is the same one: pin the fact the artifact must state, not the
* pipeline that states it.
*
* Two legs, and they fail DIFFERENTLY, which is why both exist. The SOURCE leg
* reds the moment the file header is deleted or demoted below another block —
* no regeneration needed. The CORPUS leg stays green through that (it reads
* checked-in bytes, which only move when someone regenerates) and reds on the
* state this card actually found: an index regenerated from a file with no
* header of its own. MEASURED both ways in the fix's reverse verification.
*/

import fs from 'fs';
import path from 'path';
import url from 'url';

import { describe, expect, it } from 'vitest';

import { findModuleDocBlock } from './lib/file-description';

const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(HERE, '../../..');
const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills');
const QUERY_SOURCE = path.resolve(HERE, '../src/data/query.zod.ts');

/**
* The module's own opening sentence — the one `QueryAST`'s type block already
* carried further down the file. Spelled out rather than derived from the
* source: deriving it would re-assert the generator's rule and say nothing
* about WHICH subject the row names, which is the whole defect.
*/
const QUERY_AST_SENTENCE = 'QueryAST — Abstract Syntax Tree for data queries.';

/** The pointer path the generator writes for this source in every index. */
const POINTER = 'node_modules/@objectstack/spec/src/data/query.zod.ts';

/** First prose line of the block the generator would publish for a source. */
const firstDescriptionLine = (source: string): string | null => {
const block = findModuleDocBlock(source);
if (block === null) return null;
const lines = block
.split('\n')
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
.filter((line) => line && !line.startsWith('@') && !line.startsWith('```'));
return lines[0] ?? null;
};

describe('data/query.zod.ts — the module block describes the module', () => {
it('opens on the QueryAST sentence, not on `SortNode`', () => {
const source = fs.readFileSync(QUERY_SOURCE, 'utf-8');
expect(firstDescriptionLine(source)).toBe(QUERY_AST_SENTENCE);
});

it('still carries `SortNode`s own block — the fix adds a header, it does not move a symbol doc', () => {
// Passing by deleting the symbol's documentation would satisfy the row and
// lose what the block says about `direction` vs `order` (#4721).
expect(fs.readFileSync(QUERY_SOURCE, 'utf-8')).toContain(' * Sort Node');
});
});

describe('published catalog — every pointer row for the query AST names it', () => {
/** Every checked-in skill-index row pointing at `data/query.zod.ts`. */
const publishedRows = (): { file: string; description: string }[] => {
const rows: { file: string; description: string }[] = [];
for (const skill of fs.readdirSync(SKILLS_DIR)) {
const index = path.resolve(SKILLS_DIR, skill, 'references/_index.md');
if (!fs.existsSync(index)) continue;
for (const line of fs.readFileSync(index, 'utf-8').split('\n')) {
const match = /^- `([^`]+)` — (.+)$/.exec(line);
if (match && match[1] === POINTER) {
rows.push({ file: path.relative(REPO_ROOT, index), description: match[2].trim() });
}
}
}
return rows;
};

it('finds the rows at all', () => {
// Nothing parsed means nothing compared, and "no bad row" would read as
// green — the failure mode this whole file exists to refuse.
expect(publishedRows().length).toBeGreaterThan(0);
});

it('reads the QueryAST sentence on every one of them', () => {
const offenders = publishedRows()
.filter((row) => row.description !== QUERY_AST_SENTENCE)
.map((row) => `${row.file}: ${row.description}`);
expect(offenders).toEqual([]);
});
});
14 changes: 12 additions & 2 deletions packages/spec/src/data/query.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,13 +7,23 @@ import { lazySchema } from '../shared/lazy-schema';
import { retiredKey } from '../shared/retired-key';
import { strictObject } from '../shared/strict-object';

/**
* QueryAST — Abstract Syntax Tree for data queries.
*
* The query AST every data read is expressed in: `where` predicates, `fields`
* projection, `orderBy` sort nodes, `expand` traversal, pagination and
* aggregation.
*/

/**
* Sort Node
* Represents "Order By" — one `{ field, order }` pair. Unknown keys are
* REJECTED (#4721); spell the direction `order`, never `direction`.
*/
// ⚠️ Keep the block above short: `build-docs.ts` takes the FIRST JSDoc block in
// the file as this page's description, so the rationale below is line comments.
// ⚠️ Keep the file header above short: `build-docs.ts` publishes the FIRST
// HEADER-ZONE doc block that documents no symbol — the zone ends at the first
// declaration, so a later block cannot take over (`scripts/lib/file-description.ts`
// selects it) — as this page's description, so the rationale below is line comments.
//
// ─── Why this one schema is strict while the rest of the file is not (#4721) ──
//
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-api/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: ExecutionContextSchema
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object,
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-data/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-query/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ from `node_modules` — there is no local copy in the skill bundle.

- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.

## Transitive dependencies

Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-ui/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
- `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Draft
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
29 changes: 29 additions & 0 deletions .changeset/query-zod-module-description.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
"@objectstack/spec": patch
---

fix(spec): `data/query.zod.ts` now describes the query AST, not one sort node

The published skill reference indexes and the generated `data/query` reference
page opened on "Sort Node" — the description of a single `{ field, order }`
pair — for the file that carries the entire `QueryAST`.

The generators publish the module's OWN doc block: top-level, in the header
zone, documenting no symbol. `query.zod.ts` had no block of its own, and
`SortNodeSchema`'s qualified, because the file's rationale comments sit between
that block and its schema, so nothing attached it to a symbol. The mechanism was
understood when the file was written — a warning comment sits directly under
that block saying the first block becomes the page description. What was not
noticed is the ORDERING: the first block belonged to a symbol, and a comment
warning about a rule is not the same as satisfying it.

The file now opens with a short header of its own, reusing the sentence
`QueryAST`'s own type block already carried. Four published skill indexes
(`objectstack-query`, `objectstack-data`, `objectstack-api`, `objectstack-ui`)
and the reference page name the query AST as a result. `SortNode`'s block is
untouched and still documents the schema it belongs to; the warning comment
beside it now names which block is published and which selector picks it.

What the wrong row cost, in the skills' own terms: the skill tells an agent to
read the source for exact field shapes, so a pointer labelled "Sort Node" makes
it skip the one file that carries the AST.
8 changes: 5 additions & 3 deletions content/docs/references/data/query.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,11 @@ description: Query protocol schemas

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Sort Node
Represents "Order By" — one `{ field, order }` pair. Unknown keys are
REJECTED (#4721); spell the direction `order`, never `direction`.
QueryAST — Abstract Syntax Tree for data queries.

The query AST every data read is expressed in: `where` predicates, `fields`
projection, `orderBy` sort nodes, `expand` traversal, pagination and
aggregation.

<Callout type="info">
**Source:** `packages/spec/src/data/query.zod.ts`
Expand Down
110 changes: 110 additions & 0 deletions packages/spec/scripts/query-pointer-row.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Pin for WHAT the published pointer row for `data/query.zod.ts` names — the
* query AST, not one sort node.
*
* `build-skill-references.ts` describes each source by the module's own doc
* block (`lib/file-description.ts` selects it: top-level, in the header zone,
* documenting no symbol). `query.zod.ts` had no block of its own, and
* `SortNodeSchema`'s block qualified — the file's rationale comments sit
* between that block and its schema, so nothing attached it to a symbol. Four
* published indexes (`objectstack-query`, `-data`, `-api`, `-ui`) therefore
* labelled the file carrying the whole `QueryAST` "Sort Node", and the public
* reference page `content/docs/references/data/query.mdx` opened on it. The
* skill tells an agent to Read the source for exact field shapes, so that row
* cost the one read it exists to route: an agent looking for the query AST
* skips the only file that has it.
*
* No gate could see it. `check:skill-refs` and `check:docs` compare the
* artifact against the generator, and the generator reproduced the wrong block
* faithfully — the same blind spot #5059 and #12201 found one layer up, so the
* answer is the same one: pin the fact the artifact must state, not the
* pipeline that states it.
*
* Two legs, and they fail DIFFERENTLY, which is why both exist. The SOURCE leg
* reds the moment the file header is deleted or demoted below another block —
* no regeneration needed. The CORPUS leg stays green through that (it reads
* checked-in bytes, which only move when someone regenerates) and reds on the
* state this card actually found: an index regenerated from a file with no
* header of its own. MEASURED both ways in the fix's reverse verification.
*/

import fs from 'fs';
import path from 'path';
import url from 'url';

import { describe, expect, it } from 'vitest';

import { findModuleDocBlock } from './lib/file-description';

const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(HERE, '../../..');
const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills');
const QUERY_SOURCE = path.resolve(HERE, '../src/data/query.zod.ts');

/**
* The module's own opening sentence — the one `QueryAST`'s type block already
* carried further down the file. Spelled out rather than derived from the
* source: deriving it would re-assert the generator's rule and say nothing
* about WHICH subject the row names, which is the whole defect.
*/
const QUERY_AST_SENTENCE = 'QueryAST — Abstract Syntax Tree for data queries.';

/** The pointer path the generator writes for this source in every index. */
const POINTER = 'node_modules/@objectstack/spec/src/data/query.zod.ts';

/** First prose line of the block the generator would publish for a source. */
const firstDescriptionLine = (source: string): string | null => {
const block = findModuleDocBlock(source);
if (block === null) return null;
const lines = block
.split('\n')
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
.filter((line) => line && !line.startsWith('@') && !line.startsWith('```'));
return lines[0] ?? null;
};

describe('data/query.zod.ts — the module block describes the module', () => {
it('opens on the QueryAST sentence, not on `SortNode`', () => {
const source = fs.readFileSync(QUERY_SOURCE, 'utf-8');
expect(firstDescriptionLine(source)).toBe(QUERY_AST_SENTENCE);
});

it('still carries `SortNode`s own block — the fix adds a header, it does not move a symbol doc', () => {
// Passing by deleting the symbol's documentation would satisfy the row and
// lose what the block says about `direction` vs `order` (#4721).
expect(fs.readFileSync(QUERY_SOURCE, 'utf-8')).toContain(' * Sort Node');
});
});

describe('published catalog — every pointer row for the query AST names it', () => {
/** Every checked-in skill-index row pointing at `data/query.zod.ts`. */
const publishedRows = (): { file: string; description: string }[] => {
const rows: { file: string; description: string }[] = [];
for (const skill of fs.readdirSync(SKILLS_DIR)) {
const index = path.resolve(SKILLS_DIR, skill, 'references/_index.md');
if (!fs.existsSync(index)) continue;
for (const line of fs.readFileSync(index, 'utf-8').split('\n')) {
const match = /^- `([^`]+)` — (.+)$/.exec(line);
if (match && match[1] === POINTER) {
rows.push({ file: path.relative(REPO_ROOT, index), description: match[2].trim() });
}
}
}
return rows;
};

it('finds the rows at all', () => {
// Nothing parsed means nothing compared, and "no bad row" would read as
// green — the failure mode this whole file exists to refuse.
expect(publishedRows().length).toBeGreaterThan(0);
});

it('reads the QueryAST sentence on every one of them', () => {
const offenders = publishedRows()
.filter((row) => row.description !== QUERY_AST_SENTENCE)
.map((row) => `${row.file}: ${row.description}`);
expect(offenders).toEqual([]);
});
});
14 changes: 12 additions & 2 deletions packages/spec/src/data/query.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,13 +7,23 @@ import { lazySchema } from '../shared/lazy-schema';
import { retiredKey } from '../shared/retired-key';
import { strictObject } from '../shared/strict-object';

/**
* QueryAST — Abstract Syntax Tree for data queries.
*
* The query AST every data read is expressed in: `where` predicates, `fields`
* projection, `orderBy` sort nodes, `expand` traversal, pagination and
* aggregation.
*/

/**
* Sort Node
* Represents "Order By" — one `{ field, order }` pair. Unknown keys are
* REJECTED (#4721); spell the direction `order`, never `direction`.
*/
// ⚠️ Keep the block above short: `build-docs.ts` takes the FIRST JSDoc block in
// the file as this page's description, so the rationale below is line comments.
// ⚠️ Keep the file header above short: `build-docs.ts` publishes the FIRST
// HEADER-ZONE doc block that documents no symbol — the zone ends at the first
// declaration, so a later block cannot take over (`scripts/lib/file-description.ts`
// selects it) — as this page's description, so the rationale below is line comments.
//
// ─── Why this one schema is strict while the rest of the file is not (#4721) ──
//
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-api/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: ExecutionContextSchema
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object,
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-data/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-query/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ from `node_modules` — there is no local copy in the skill bundle.

- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.

## Transitive dependencies

Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-ui/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
- `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
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
29 changes: 29 additions & 0 deletions .changeset/query-zod-module-description.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
"@objectstack/spec": patch
---

fix(spec): `data/query.zod.ts` now describes the query AST, not one sort node

The published skill reference indexes and the generated `data/query` reference
page opened on "Sort Node" — the description of a single `{ field, order }`
pair — for the file that carries the entire `QueryAST`.

The generators publish the module's OWN doc block: top-level, in the header
zone, documenting no symbol. `query.zod.ts` had no block of its own, and
`SortNodeSchema`'s qualified, because the file's rationale comments sit between
that block and its schema, so nothing attached it to a symbol. The mechanism was
understood when the file was written — a warning comment sits directly under
that block saying the first block becomes the page description. What was not
noticed is the ORDERING: the first block belonged to a symbol, and a comment
warning about a rule is not the same as satisfying it.

The file now opens with a short header of its own, reusing the sentence
`QueryAST`'s own type block already carried. Four published skill indexes
(`objectstack-query`, `objectstack-data`, `objectstack-api`, `objectstack-ui`)
and the reference page name the query AST as a result. `SortNode`'s block is
untouched and still documents the schema it belongs to; the warning comment
beside it now names which block is published and which selector picks it.

What the wrong row cost, in the skills' own terms: the skill tells an agent to
read the source for exact field shapes, so a pointer labelled "Sort Node" makes
it skip the one file that carries the AST.
8 changes: 5 additions & 3 deletions content/docs/references/data/query.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,11 @@ description: Query protocol schemas

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Sort Node
Represents "Order By" — one `{ field, order }` pair. Unknown keys are
REJECTED (#4721); spell the direction `order`, never `direction`.
QueryAST — Abstract Syntax Tree for data queries.

The query AST every data read is expressed in: `where` predicates, `fields`
projection, `orderBy` sort nodes, `expand` traversal, pagination and
aggregation.

<Callout type="info">
**Source:** `packages/spec/src/data/query.zod.ts`
Expand Down
110 changes: 110 additions & 0 deletions packages/spec/scripts/query-pointer-row.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Pin for WHAT the published pointer row for `data/query.zod.ts` names — the
* query AST, not one sort node.
*
* `build-skill-references.ts` describes each source by the module's own doc
* block (`lib/file-description.ts` selects it: top-level, in the header zone,
* documenting no symbol). `query.zod.ts` had no block of its own, and
* `SortNodeSchema`'s block qualified — the file's rationale comments sit
* between that block and its schema, so nothing attached it to a symbol. Four
* published indexes (`objectstack-query`, `-data`, `-api`, `-ui`) therefore
* labelled the file carrying the whole `QueryAST` "Sort Node", and the public
* reference page `content/docs/references/data/query.mdx` opened on it. The
* skill tells an agent to Read the source for exact field shapes, so that row
* cost the one read it exists to route: an agent looking for the query AST
* skips the only file that has it.
*
* No gate could see it. `check:skill-refs` and `check:docs` compare the
* artifact against the generator, and the generator reproduced the wrong block
* faithfully — the same blind spot #5059 and #12201 found one layer up, so the
* answer is the same one: pin the fact the artifact must state, not the
* pipeline that states it.
*
* Two legs, and they fail DIFFERENTLY, which is why both exist. The SOURCE leg
* reds the moment the file header is deleted or demoted below another block —
* no regeneration needed. The CORPUS leg stays green through that (it reads
* checked-in bytes, which only move when someone regenerates) and reds on the
* state this card actually found: an index regenerated from a file with no
* header of its own. MEASURED both ways in the fix's reverse verification.
*/

import fs from 'fs';
import path from 'path';
import url from 'url';

import { describe, expect, it } from 'vitest';

import { findModuleDocBlock } from './lib/file-description';

const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(HERE, '../../..');
const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills');
const QUERY_SOURCE = path.resolve(HERE, '../src/data/query.zod.ts');

/**
* The module's own opening sentence — the one `QueryAST`'s type block already
* carried further down the file. Spelled out rather than derived from the
* source: deriving it would re-assert the generator's rule and say nothing
* about WHICH subject the row names, which is the whole defect.
*/
const QUERY_AST_SENTENCE = 'QueryAST — Abstract Syntax Tree for data queries.';

/** The pointer path the generator writes for this source in every index. */
const POINTER = 'node_modules/@objectstack/spec/src/data/query.zod.ts';

/** First prose line of the block the generator would publish for a source. */
const firstDescriptionLine = (source: string): string | null => {
const block = findModuleDocBlock(source);
if (block === null) return null;
const lines = block
.split('\n')
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
.filter((line) => line && !line.startsWith('@') && !line.startsWith('```'));
return lines[0] ?? null;
};

describe('data/query.zod.ts — the module block describes the module', () => {
it('opens on the QueryAST sentence, not on `SortNode`', () => {
const source = fs.readFileSync(QUERY_SOURCE, 'utf-8');
expect(firstDescriptionLine(source)).toBe(QUERY_AST_SENTENCE);
});

it('still carries `SortNode`s own block — the fix adds a header, it does not move a symbol doc', () => {
// Passing by deleting the symbol's documentation would satisfy the row and
// lose what the block says about `direction` vs `order` (#4721).
expect(fs.readFileSync(QUERY_SOURCE, 'utf-8')).toContain(' * Sort Node');
});
});

describe('published catalog — every pointer row for the query AST names it', () => {
/** Every checked-in skill-index row pointing at `data/query.zod.ts`. */
const publishedRows = (): { file: string; description: string }[] => {
const rows: { file: string; description: string }[] = [];
for (const skill of fs.readdirSync(SKILLS_DIR)) {
const index = path.resolve(SKILLS_DIR, skill, 'references/_index.md');
if (!fs.existsSync(index)) continue;
for (const line of fs.readFileSync(index, 'utf-8').split('\n')) {
const match = /^- `([^`]+)` — (.+)$/.exec(line);
if (match && match[1] === POINTER) {
rows.push({ file: path.relative(REPO_ROOT, index), description: match[2].trim() });
}
}
}
return rows;
};

it('finds the rows at all', () => {
// Nothing parsed means nothing compared, and "no bad row" would read as
// green — the failure mode this whole file exists to refuse.
expect(publishedRows().length).toBeGreaterThan(0);
});

it('reads the QueryAST sentence on every one of them', () => {
const offenders = publishedRows()
.filter((row) => row.description !== QUERY_AST_SENTENCE)
.map((row) => `${row.file}: ${row.description}`);
expect(offenders).toEqual([]);
});
});
14 changes: 12 additions & 2 deletions packages/spec/src/data/query.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,13 +7,23 @@ import { lazySchema } from '../shared/lazy-schema';
import { retiredKey } from '../shared/retired-key';
import { strictObject } from '../shared/strict-object';

/**
* QueryAST — Abstract Syntax Tree for data queries.
*
* The query AST every data read is expressed in: `where` predicates, `fields`
* projection, `orderBy` sort nodes, `expand` traversal, pagination and
* aggregation.
*/

/**
* Sort Node
* Represents "Order By" — one `{ field, order }` pair. Unknown keys are
* REJECTED (#4721); spell the direction `order`, never `direction`.
*/
// ⚠️ Keep the block above short: `build-docs.ts` takes the FIRST JSDoc block in
// the file as this page's description, so the rationale below is line comments.
// ⚠️ Keep the file header above short: `build-docs.ts` publishes the FIRST
// HEADER-ZONE doc block that documents no symbol — the zone ends at the first
// declaration, so a later block cannot take over (`scripts/lib/file-description.ts`
// selects it) — as this page's description, so the rationale below is line comments.
//
// ─── Why this one schema is strict while the rest of the file is not (#4721) ──
//
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-api/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: ExecutionContextSchema
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object,
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-data/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-query/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ from `node_modules` — there is no local copy in the skill bundle.

- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.

## Transitive dependencies

Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-ui/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
- `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
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
29 changes: 29 additions & 0 deletions .changeset/query-zod-module-description.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
"@objectstack/spec": patch
---

fix(spec): `data/query.zod.ts` now describes the query AST, not one sort node

The published skill reference indexes and the generated `data/query` reference
page opened on "Sort Node" — the description of a single `{ field, order }`
pair — for the file that carries the entire `QueryAST`.

The generators publish the module's OWN doc block: top-level, in the header
zone, documenting no symbol. `query.zod.ts` had no block of its own, and
`SortNodeSchema`'s qualified, because the file's rationale comments sit between
that block and its schema, so nothing attached it to a symbol. The mechanism was
understood when the file was written — a warning comment sits directly under
that block saying the first block becomes the page description. What was not
noticed is the ORDERING: the first block belonged to a symbol, and a comment
warning about a rule is not the same as satisfying it.

The file now opens with a short header of its own, reusing the sentence
`QueryAST`'s own type block already carried. Four published skill indexes
(`objectstack-query`, `objectstack-data`, `objectstack-api`, `objectstack-ui`)
and the reference page name the query AST as a result. `SortNode`'s block is
untouched and still documents the schema it belongs to; the warning comment
beside it now names which block is published and which selector picks it.

What the wrong row cost, in the skills' own terms: the skill tells an agent to
read the source for exact field shapes, so a pointer labelled "Sort Node" makes
it skip the one file that carries the AST.
8 changes: 5 additions & 3 deletions content/docs/references/data/query.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,11 @@ description: Query protocol schemas

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Sort Node
Represents "Order By" — one `{ field, order }` pair. Unknown keys are
REJECTED (#4721); spell the direction `order`, never `direction`.
QueryAST — Abstract Syntax Tree for data queries.

The query AST every data read is expressed in: `where` predicates, `fields`
projection, `orderBy` sort nodes, `expand` traversal, pagination and
aggregation.

<Callout type="info">
**Source:** `packages/spec/src/data/query.zod.ts`
Expand Down
110 changes: 110 additions & 0 deletions packages/spec/scripts/query-pointer-row.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Pin for WHAT the published pointer row for `data/query.zod.ts` names — the
* query AST, not one sort node.
*
* `build-skill-references.ts` describes each source by the module's own doc
* block (`lib/file-description.ts` selects it: top-level, in the header zone,
* documenting no symbol). `query.zod.ts` had no block of its own, and
* `SortNodeSchema`'s block qualified — the file's rationale comments sit
* between that block and its schema, so nothing attached it to a symbol. Four
* published indexes (`objectstack-query`, `-data`, `-api`, `-ui`) therefore
* labelled the file carrying the whole `QueryAST` "Sort Node", and the public
* reference page `content/docs/references/data/query.mdx` opened on it. The
* skill tells an agent to Read the source for exact field shapes, so that row
* cost the one read it exists to route: an agent looking for the query AST
* skips the only file that has it.
*
* No gate could see it. `check:skill-refs` and `check:docs` compare the
* artifact against the generator, and the generator reproduced the wrong block
* faithfully — the same blind spot #5059 and #12201 found one layer up, so the
* answer is the same one: pin the fact the artifact must state, not the
* pipeline that states it.
*
* Two legs, and they fail DIFFERENTLY, which is why both exist. The SOURCE leg
* reds the moment the file header is deleted or demoted below another block —
* no regeneration needed. The CORPUS leg stays green through that (it reads
* checked-in bytes, which only move when someone regenerates) and reds on the
* state this card actually found: an index regenerated from a file with no
* header of its own. MEASURED both ways in the fix's reverse verification.
*/

import fs from 'fs';
import path from 'path';
import url from 'url';

import { describe, expect, it } from 'vitest';

import { findModuleDocBlock } from './lib/file-description';

const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(HERE, '../../..');
const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills');
const QUERY_SOURCE = path.resolve(HERE, '../src/data/query.zod.ts');

/**
* The module's own opening sentence — the one `QueryAST`'s type block already
* carried further down the file. Spelled out rather than derived from the
* source: deriving it would re-assert the generator's rule and say nothing
* about WHICH subject the row names, which is the whole defect.
*/
const QUERY_AST_SENTENCE = 'QueryAST — Abstract Syntax Tree for data queries.';

/** The pointer path the generator writes for this source in every index. */
const POINTER = 'node_modules/@objectstack/spec/src/data/query.zod.ts';

/** First prose line of the block the generator would publish for a source. */
const firstDescriptionLine = (source: string): string | null => {
const block = findModuleDocBlock(source);
if (block === null) return null;
const lines = block
.split('\n')
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
.filter((line) => line && !line.startsWith('@') && !line.startsWith('```'));
return lines[0] ?? null;
};

describe('data/query.zod.ts — the module block describes the module', () => {
it('opens on the QueryAST sentence, not on `SortNode`', () => {
const source = fs.readFileSync(QUERY_SOURCE, 'utf-8');
expect(firstDescriptionLine(source)).toBe(QUERY_AST_SENTENCE);
});

it('still carries `SortNode`s own block — the fix adds a header, it does not move a symbol doc', () => {
// Passing by deleting the symbol's documentation would satisfy the row and
// lose what the block says about `direction` vs `order` (#4721).
expect(fs.readFileSync(QUERY_SOURCE, 'utf-8')).toContain(' * Sort Node');
});
});

describe('published catalog — every pointer row for the query AST names it', () => {
/** Every checked-in skill-index row pointing at `data/query.zod.ts`. */
const publishedRows = (): { file: string; description: string }[] => {
const rows: { file: string; description: string }[] = [];
for (const skill of fs.readdirSync(SKILLS_DIR)) {
const index = path.resolve(SKILLS_DIR, skill, 'references/_index.md');
if (!fs.existsSync(index)) continue;
for (const line of fs.readFileSync(index, 'utf-8').split('\n')) {
const match = /^- `([^`]+)` — (.+)$/.exec(line);
if (match && match[1] === POINTER) {
rows.push({ file: path.relative(REPO_ROOT, index), description: match[2].trim() });
}
}
}
return rows;
};

it('finds the rows at all', () => {
// Nothing parsed means nothing compared, and "no bad row" would read as
// green — the failure mode this whole file exists to refuse.
expect(publishedRows().length).toBeGreaterThan(0);
});

it('reads the QueryAST sentence on every one of them', () => {
const offenders = publishedRows()
.filter((row) => row.description !== QUERY_AST_SENTENCE)
.map((row) => `${row.file}: ${row.description}`);
expect(offenders).toEqual([]);
});
});
14 changes: 12 additions & 2 deletions packages/spec/src/data/query.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,13 +7,23 @@ import { lazySchema } from '../shared/lazy-schema';
import { retiredKey } from '../shared/retired-key';
import { strictObject } from '../shared/strict-object';

/**
* QueryAST — Abstract Syntax Tree for data queries.
*
* The query AST every data read is expressed in: `where` predicates, `fields`
* projection, `orderBy` sort nodes, `expand` traversal, pagination and
* aggregation.
*/

/**
* Sort Node
* Represents "Order By" — one `{ field, order }` pair. Unknown keys are
* REJECTED (#4721); spell the direction `order`, never `direction`.
*/
// ⚠️ Keep the block above short: `build-docs.ts` takes the FIRST JSDoc block in
// the file as this page's description, so the rationale below is line comments.
// ⚠️ Keep the file header above short: `build-docs.ts` publishes the FIRST
// HEADER-ZONE doc block that documents no symbol — the zone ends at the first
// declaration, so a later block cannot take over (`scripts/lib/file-description.ts`
// selects it) — as this page's description, so the rationale below is line comments.
//
// ─── Why this one schema is strict while the rest of the file is not (#4721) ──
//
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-api/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: ExecutionContextSchema
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object,
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-data/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-query/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ from `node_modules` — there is no local copy in the skill bundle.

- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.

## Transitive dependencies

Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-ui/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
- `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Draft
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
29 changes: 29 additions & 0 deletions .changeset/query-zod-module-description.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
"@objectstack/spec": patch
---

fix(spec): `data/query.zod.ts` now describes the query AST, not one sort node

The published skill reference indexes and the generated `data/query` reference
page opened on "Sort Node" — the description of a single `{ field, order }`
pair — for the file that carries the entire `QueryAST`.

The generators publish the module's OWN doc block: top-level, in the header
zone, documenting no symbol. `query.zod.ts` had no block of its own, and
`SortNodeSchema`'s qualified, because the file's rationale comments sit between
that block and its schema, so nothing attached it to a symbol. The mechanism was
understood when the file was written — a warning comment sits directly under
that block saying the first block becomes the page description. What was not
noticed is the ORDERING: the first block belonged to a symbol, and a comment
warning about a rule is not the same as satisfying it.

The file now opens with a short header of its own, reusing the sentence
`QueryAST`'s own type block already carried. Four published skill indexes
(`objectstack-query`, `objectstack-data`, `objectstack-api`, `objectstack-ui`)
and the reference page name the query AST as a result. `SortNode`'s block is
untouched and still documents the schema it belongs to; the warning comment
beside it now names which block is published and which selector picks it.

What the wrong row cost, in the skills' own terms: the skill tells an agent to
read the source for exact field shapes, so a pointer labelled "Sort Node" makes
it skip the one file that carries the AST.
8 changes: 5 additions & 3 deletions content/docs/references/data/query.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,11 @@ description: Query protocol schemas

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Sort Node
Represents "Order By" — one `{ field, order }` pair. Unknown keys are
REJECTED (#4721); spell the direction `order`, never `direction`.
QueryAST — Abstract Syntax Tree for data queries.

The query AST every data read is expressed in: `where` predicates, `fields`
projection, `orderBy` sort nodes, `expand` traversal, pagination and
aggregation.

<Callout type="info">
**Source:** `packages/spec/src/data/query.zod.ts`
Expand Down
110 changes: 110 additions & 0 deletions packages/spec/scripts/query-pointer-row.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Pin for WHAT the published pointer row for `data/query.zod.ts` names — the
* query AST, not one sort node.
*
* `build-skill-references.ts` describes each source by the module's own doc
* block (`lib/file-description.ts` selects it: top-level, in the header zone,
* documenting no symbol). `query.zod.ts` had no block of its own, and
* `SortNodeSchema`'s block qualified — the file's rationale comments sit
* between that block and its schema, so nothing attached it to a symbol. Four
* published indexes (`objectstack-query`, `-data`, `-api`, `-ui`) therefore
* labelled the file carrying the whole `QueryAST` "Sort Node", and the public
* reference page `content/docs/references/data/query.mdx` opened on it. The
* skill tells an agent to Read the source for exact field shapes, so that row
* cost the one read it exists to route: an agent looking for the query AST
* skips the only file that has it.
*
* No gate could see it. `check:skill-refs` and `check:docs` compare the
* artifact against the generator, and the generator reproduced the wrong block
* faithfully — the same blind spot #5059 and #12201 found one layer up, so the
* answer is the same one: pin the fact the artifact must state, not the
* pipeline that states it.
*
* Two legs, and they fail DIFFERENTLY, which is why both exist. The SOURCE leg
* reds the moment the file header is deleted or demoted below another block —
* no regeneration needed. The CORPUS leg stays green through that (it reads
* checked-in bytes, which only move when someone regenerates) and reds on the
* state this card actually found: an index regenerated from a file with no
* header of its own. MEASURED both ways in the fix's reverse verification.
*/

import fs from 'fs';
import path from 'path';
import url from 'url';

import { describe, expect, it } from 'vitest';

import { findModuleDocBlock } from './lib/file-description';

const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(HERE, '../../..');
const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills');
const QUERY_SOURCE = path.resolve(HERE, '../src/data/query.zod.ts');

/**
* The module's own opening sentence — the one `QueryAST`'s type block already
* carried further down the file. Spelled out rather than derived from the
* source: deriving it would re-assert the generator's rule and say nothing
* about WHICH subject the row names, which is the whole defect.
*/
const QUERY_AST_SENTENCE = 'QueryAST — Abstract Syntax Tree for data queries.';

/** The pointer path the generator writes for this source in every index. */
const POINTER = 'node_modules/@objectstack/spec/src/data/query.zod.ts';

/** First prose line of the block the generator would publish for a source. */
const firstDescriptionLine = (source: string): string | null => {
const block = findModuleDocBlock(source);
if (block === null) return null;
const lines = block
.split('\n')
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
.filter((line) => line && !line.startsWith('@') && !line.startsWith('```'));
return lines[0] ?? null;
};

describe('data/query.zod.ts — the module block describes the module', () => {
it('opens on the QueryAST sentence, not on `SortNode`', () => {
const source = fs.readFileSync(QUERY_SOURCE, 'utf-8');
expect(firstDescriptionLine(source)).toBe(QUERY_AST_SENTENCE);
});

it('still carries `SortNode`s own block — the fix adds a header, it does not move a symbol doc', () => {
// Passing by deleting the symbol's documentation would satisfy the row and
// lose what the block says about `direction` vs `order` (#4721).
expect(fs.readFileSync(QUERY_SOURCE, 'utf-8')).toContain(' * Sort Node');
});
});

describe('published catalog — every pointer row for the query AST names it', () => {
/** Every checked-in skill-index row pointing at `data/query.zod.ts`. */
const publishedRows = (): { file: string; description: string }[] => {
const rows: { file: string; description: string }[] = [];
for (const skill of fs.readdirSync(SKILLS_DIR)) {
const index = path.resolve(SKILLS_DIR, skill, 'references/_index.md');
if (!fs.existsSync(index)) continue;
for (const line of fs.readFileSync(index, 'utf-8').split('\n')) {
const match = /^- `([^`]+)` — (.+)$/.exec(line);
if (match && match[1] === POINTER) {
rows.push({ file: path.relative(REPO_ROOT, index), description: match[2].trim() });
}
}
}
return rows;
};

it('finds the rows at all', () => {
// Nothing parsed means nothing compared, and "no bad row" would read as
// green — the failure mode this whole file exists to refuse.
expect(publishedRows().length).toBeGreaterThan(0);
});

it('reads the QueryAST sentence on every one of them', () => {
const offenders = publishedRows()
.filter((row) => row.description !== QUERY_AST_SENTENCE)
.map((row) => `${row.file}: ${row.description}`);
expect(offenders).toEqual([]);
});
});
14 changes: 12 additions & 2 deletions packages/spec/src/data/query.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,13 +7,23 @@ import { lazySchema } from '../shared/lazy-schema';
import { retiredKey } from '../shared/retired-key';
import { strictObject } from '../shared/strict-object';

/**
* QueryAST — Abstract Syntax Tree for data queries.
*
* The query AST every data read is expressed in: `where` predicates, `fields`
* projection, `orderBy` sort nodes, `expand` traversal, pagination and
* aggregation.
*/

/**
* Sort Node
* Represents "Order By" — one `{ field, order }` pair. Unknown keys are
* REJECTED (#4721); spell the direction `order`, never `direction`.
*/
// ⚠️ Keep the block above short: `build-docs.ts` takes the FIRST JSDoc block in
// the file as this page's description, so the rationale below is line comments.
// ⚠️ Keep the file header above short: `build-docs.ts` publishes the FIRST
// HEADER-ZONE doc block that documents no symbol — the zone ends at the first
// declaration, so a later block cannot take over (`scripts/lib/file-description.ts`
// selects it) — as this page's description, so the rationale below is line comments.
//
// ─── Why this one schema is strict while the rest of the file is not (#4721) ──
//
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-api/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: ExecutionContextSchema
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object,
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-data/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-query/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ from `node_modules` — there is no local copy in the skill bundle.

- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.

## Transitive dependencies

Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-ui/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
- `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
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
29 changes: 29 additions & 0 deletions .changeset/query-zod-module-description.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
"@objectstack/spec": patch
---

fix(spec): `data/query.zod.ts` now describes the query AST, not one sort node

The published skill reference indexes and the generated `data/query` reference
page opened on "Sort Node" — the description of a single `{ field, order }`
pair — for the file that carries the entire `QueryAST`.

The generators publish the module's OWN doc block: top-level, in the header
zone, documenting no symbol. `query.zod.ts` had no block of its own, and
`SortNodeSchema`'s qualified, because the file's rationale comments sit between
that block and its schema, so nothing attached it to a symbol. The mechanism was
understood when the file was written — a warning comment sits directly under
that block saying the first block becomes the page description. What was not
noticed is the ORDERING: the first block belonged to a symbol, and a comment
warning about a rule is not the same as satisfying it.

The file now opens with a short header of its own, reusing the sentence
`QueryAST`'s own type block already carried. Four published skill indexes
(`objectstack-query`, `objectstack-data`, `objectstack-api`, `objectstack-ui`)
and the reference page name the query AST as a result. `SortNode`'s block is
untouched and still documents the schema it belongs to; the warning comment
beside it now names which block is published and which selector picks it.

What the wrong row cost, in the skills' own terms: the skill tells an agent to
read the source for exact field shapes, so a pointer labelled "Sort Node" makes
it skip the one file that carries the AST.
8 changes: 5 additions & 3 deletions content/docs/references/data/query.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,11 @@ description: Query protocol schemas

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Sort Node
Represents "Order By" — one `{ field, order }` pair. Unknown keys are
REJECTED (#4721); spell the direction `order`, never `direction`.
QueryAST — Abstract Syntax Tree for data queries.

The query AST every data read is expressed in: `where` predicates, `fields`
projection, `orderBy` sort nodes, `expand` traversal, pagination and
aggregation.

<Callout type="info">
**Source:** `packages/spec/src/data/query.zod.ts`
Expand Down
110 changes: 110 additions & 0 deletions packages/spec/scripts/query-pointer-row.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Pin for WHAT the published pointer row for `data/query.zod.ts` names — the
* query AST, not one sort node.
*
* `build-skill-references.ts` describes each source by the module's own doc
* block (`lib/file-description.ts` selects it: top-level, in the header zone,
* documenting no symbol). `query.zod.ts` had no block of its own, and
* `SortNodeSchema`'s block qualified — the file's rationale comments sit
* between that block and its schema, so nothing attached it to a symbol. Four
* published indexes (`objectstack-query`, `-data`, `-api`, `-ui`) therefore
* labelled the file carrying the whole `QueryAST` "Sort Node", and the public
* reference page `content/docs/references/data/query.mdx` opened on it. The
* skill tells an agent to Read the source for exact field shapes, so that row
* cost the one read it exists to route: an agent looking for the query AST
* skips the only file that has it.
*
* No gate could see it. `check:skill-refs` and `check:docs` compare the
* artifact against the generator, and the generator reproduced the wrong block
* faithfully — the same blind spot #5059 and #12201 found one layer up, so the
* answer is the same one: pin the fact the artifact must state, not the
* pipeline that states it.
*
* Two legs, and they fail DIFFERENTLY, which is why both exist. The SOURCE leg
* reds the moment the file header is deleted or demoted below another block —
* no regeneration needed. The CORPUS leg stays green through that (it reads
* checked-in bytes, which only move when someone regenerates) and reds on the
* state this card actually found: an index regenerated from a file with no
* header of its own. MEASURED both ways in the fix's reverse verification.
*/

import fs from 'fs';
import path from 'path';
import url from 'url';

import { describe, expect, it } from 'vitest';

import { findModuleDocBlock } from './lib/file-description';

const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(HERE, '../../..');
const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills');
const QUERY_SOURCE = path.resolve(HERE, '../src/data/query.zod.ts');

/**
* The module's own opening sentence — the one `QueryAST`'s type block already
* carried further down the file. Spelled out rather than derived from the
* source: deriving it would re-assert the generator's rule and say nothing
* about WHICH subject the row names, which is the whole defect.
*/
const QUERY_AST_SENTENCE = 'QueryAST — Abstract Syntax Tree for data queries.';

/** The pointer path the generator writes for this source in every index. */
const POINTER = 'node_modules/@objectstack/spec/src/data/query.zod.ts';

/** First prose line of the block the generator would publish for a source. */
const firstDescriptionLine = (source: string): string | null => {
const block = findModuleDocBlock(source);
if (block === null) return null;
const lines = block
.split('\n')
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
.filter((line) => line && !line.startsWith('@') && !line.startsWith('```'));
return lines[0] ?? null;
};

describe('data/query.zod.ts — the module block describes the module', () => {
it('opens on the QueryAST sentence, not on `SortNode`', () => {
const source = fs.readFileSync(QUERY_SOURCE, 'utf-8');
expect(firstDescriptionLine(source)).toBe(QUERY_AST_SENTENCE);
});

it('still carries `SortNode`s own block — the fix adds a header, it does not move a symbol doc', () => {
// Passing by deleting the symbol's documentation would satisfy the row and
// lose what the block says about `direction` vs `order` (#4721).
expect(fs.readFileSync(QUERY_SOURCE, 'utf-8')).toContain(' * Sort Node');
});
});

describe('published catalog — every pointer row for the query AST names it', () => {
/** Every checked-in skill-index row pointing at `data/query.zod.ts`. */
const publishedRows = (): { file: string; description: string }[] => {
const rows: { file: string; description: string }[] = [];
for (const skill of fs.readdirSync(SKILLS_DIR)) {
const index = path.resolve(SKILLS_DIR, skill, 'references/_index.md');
if (!fs.existsSync(index)) continue;
for (const line of fs.readFileSync(index, 'utf-8').split('\n')) {
const match = /^- `([^`]+)` — (.+)$/.exec(line);
if (match && match[1] === POINTER) {
rows.push({ file: path.relative(REPO_ROOT, index), description: match[2].trim() });
}
}
}
return rows;
};

it('finds the rows at all', () => {
// Nothing parsed means nothing compared, and "no bad row" would read as
// green — the failure mode this whole file exists to refuse.
expect(publishedRows().length).toBeGreaterThan(0);
});

it('reads the QueryAST sentence on every one of them', () => {
const offenders = publishedRows()
.filter((row) => row.description !== QUERY_AST_SENTENCE)
.map((row) => `${row.file}: ${row.description}`);
expect(offenders).toEqual([]);
});
});
14 changes: 12 additions & 2 deletions packages/spec/src/data/query.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,13 +7,23 @@ import { lazySchema } from '../shared/lazy-schema';
import { retiredKey } from '../shared/retired-key';
import { strictObject } from '../shared/strict-object';

/**
* QueryAST — Abstract Syntax Tree for data queries.
*
* The query AST every data read is expressed in: `where` predicates, `fields`
* projection, `orderBy` sort nodes, `expand` traversal, pagination and
* aggregation.
*/

/**
* Sort Node
* Represents "Order By" — one `{ field, order }` pair. Unknown keys are
* REJECTED (#4721); spell the direction `order`, never `direction`.
*/
// ⚠️ Keep the block above short: `build-docs.ts` takes the FIRST JSDoc block in
// the file as this page's description, so the rationale below is line comments.
// ⚠️ Keep the file header above short: `build-docs.ts` publishes the FIRST
// HEADER-ZONE doc block that documents no symbol — the zone ends at the first
// declaration, so a later block cannot take over (`scripts/lib/file-description.ts`
// selects it) — as this page's description, so the rationale below is line comments.
//
// ─── Why this one schema is strict while the rest of the file is not (#4721) ──
//
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-api/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: ExecutionContextSchema
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object,
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-data/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-query/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ from `node_modules` — there is no local copy in the skill bundle.

- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.

## Transitive dependencies

Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-ui/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
- `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Draft
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
29 changes: 29 additions & 0 deletions .changeset/query-zod-module-description.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
"@objectstack/spec": patch
---

fix(spec): `data/query.zod.ts` now describes the query AST, not one sort node

The published skill reference indexes and the generated `data/query` reference
page opened on "Sort Node" — the description of a single `{ field, order }`
pair — for the file that carries the entire `QueryAST`.

The generators publish the module's OWN doc block: top-level, in the header
zone, documenting no symbol. `query.zod.ts` had no block of its own, and
`SortNodeSchema`'s qualified, because the file's rationale comments sit between
that block and its schema, so nothing attached it to a symbol. The mechanism was
understood when the file was written — a warning comment sits directly under
that block saying the first block becomes the page description. What was not
noticed is the ORDERING: the first block belonged to a symbol, and a comment
warning about a rule is not the same as satisfying it.

The file now opens with a short header of its own, reusing the sentence
`QueryAST`'s own type block already carried. Four published skill indexes
(`objectstack-query`, `objectstack-data`, `objectstack-api`, `objectstack-ui`)
and the reference page name the query AST as a result. `SortNode`'s block is
untouched and still documents the schema it belongs to; the warning comment
beside it now names which block is published and which selector picks it.

What the wrong row cost, in the skills' own terms: the skill tells an agent to
read the source for exact field shapes, so a pointer labelled "Sort Node" makes
it skip the one file that carries the AST.
8 changes: 5 additions & 3 deletions content/docs/references/data/query.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,11 @@ description: Query protocol schemas

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Sort Node
Represents "Order By" — one `{ field, order }` pair. Unknown keys are
REJECTED (#4721); spell the direction `order`, never `direction`.
QueryAST — Abstract Syntax Tree for data queries.

The query AST every data read is expressed in: `where` predicates, `fields`
projection, `orderBy` sort nodes, `expand` traversal, pagination and
aggregation.

<Callout type="info">
**Source:** `packages/spec/src/data/query.zod.ts`
Expand Down
110 changes: 110 additions & 0 deletions packages/spec/scripts/query-pointer-row.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Pin for WHAT the published pointer row for `data/query.zod.ts` names — the
* query AST, not one sort node.
*
* `build-skill-references.ts` describes each source by the module's own doc
* block (`lib/file-description.ts` selects it: top-level, in the header zone,
* documenting no symbol). `query.zod.ts` had no block of its own, and
* `SortNodeSchema`'s block qualified — the file's rationale comments sit
* between that block and its schema, so nothing attached it to a symbol. Four
* published indexes (`objectstack-query`, `-data`, `-api`, `-ui`) therefore
* labelled the file carrying the whole `QueryAST` "Sort Node", and the public
* reference page `content/docs/references/data/query.mdx` opened on it. The
* skill tells an agent to Read the source for exact field shapes, so that row
* cost the one read it exists to route: an agent looking for the query AST
* skips the only file that has it.
*
* No gate could see it. `check:skill-refs` and `check:docs` compare the
* artifact against the generator, and the generator reproduced the wrong block
* faithfully — the same blind spot #5059 and #12201 found one layer up, so the
* answer is the same one: pin the fact the artifact must state, not the
* pipeline that states it.
*
* Two legs, and they fail DIFFERENTLY, which is why both exist. The SOURCE leg
* reds the moment the file header is deleted or demoted below another block —
* no regeneration needed. The CORPUS leg stays green through that (it reads
* checked-in bytes, which only move when someone regenerates) and reds on the
* state this card actually found: an index regenerated from a file with no
* header of its own. MEASURED both ways in the fix's reverse verification.
*/

import fs from 'fs';
import path from 'path';
import url from 'url';

import { describe, expect, it } from 'vitest';

import { findModuleDocBlock } from './lib/file-description';

const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(HERE, '../../..');
const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills');
const QUERY_SOURCE = path.resolve(HERE, '../src/data/query.zod.ts');

/**
* The module's own opening sentence — the one `QueryAST`'s type block already
* carried further down the file. Spelled out rather than derived from the
* source: deriving it would re-assert the generator's rule and say nothing
* about WHICH subject the row names, which is the whole defect.
*/
const QUERY_AST_SENTENCE = 'QueryAST — Abstract Syntax Tree for data queries.';

/** The pointer path the generator writes for this source in every index. */
const POINTER = 'node_modules/@objectstack/spec/src/data/query.zod.ts';

/** First prose line of the block the generator would publish for a source. */
const firstDescriptionLine = (source: string): string | null => {
const block = findModuleDocBlock(source);
if (block === null) return null;
const lines = block
.split('\n')
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
.filter((line) => line && !line.startsWith('@') && !line.startsWith('```'));
return lines[0] ?? null;
};

describe('data/query.zod.ts — the module block describes the module', () => {
it('opens on the QueryAST sentence, not on `SortNode`', () => {
const source = fs.readFileSync(QUERY_SOURCE, 'utf-8');
expect(firstDescriptionLine(source)).toBe(QUERY_AST_SENTENCE);
});

it('still carries `SortNode`s own block — the fix adds a header, it does not move a symbol doc', () => {
// Passing by deleting the symbol's documentation would satisfy the row and
// lose what the block says about `direction` vs `order` (#4721).
expect(fs.readFileSync(QUERY_SOURCE, 'utf-8')).toContain(' * Sort Node');
});
});

describe('published catalog — every pointer row for the query AST names it', () => {
/** Every checked-in skill-index row pointing at `data/query.zod.ts`. */
const publishedRows = (): { file: string; description: string }[] => {
const rows: { file: string; description: string }[] = [];
for (const skill of fs.readdirSync(SKILLS_DIR)) {
const index = path.resolve(SKILLS_DIR, skill, 'references/_index.md');
if (!fs.existsSync(index)) continue;
for (const line of fs.readFileSync(index, 'utf-8').split('\n')) {
const match = /^- `([^`]+)` — (.+)$/.exec(line);
if (match && match[1] === POINTER) {
rows.push({ file: path.relative(REPO_ROOT, index), description: match[2].trim() });
}
}
}
return rows;
};

it('finds the rows at all', () => {
// Nothing parsed means nothing compared, and "no bad row" would read as
// green — the failure mode this whole file exists to refuse.
expect(publishedRows().length).toBeGreaterThan(0);
});

it('reads the QueryAST sentence on every one of them', () => {
const offenders = publishedRows()
.filter((row) => row.description !== QUERY_AST_SENTENCE)
.map((row) => `${row.file}: ${row.description}`);
expect(offenders).toEqual([]);
});
});
14 changes: 12 additions & 2 deletions packages/spec/src/data/query.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,13 +7,23 @@ import { lazySchema } from '../shared/lazy-schema';
import { retiredKey } from '../shared/retired-key';
import { strictObject } from '../shared/strict-object';

/**
* QueryAST — Abstract Syntax Tree for data queries.
*
* The query AST every data read is expressed in: `where` predicates, `fields`
* projection, `orderBy` sort nodes, `expand` traversal, pagination and
* aggregation.
*/

/**
* Sort Node
* Represents "Order By" — one `{ field, order }` pair. Unknown keys are
* REJECTED (#4721); spell the direction `order`, never `direction`.
*/
// ⚠️ Keep the block above short: `build-docs.ts` takes the FIRST JSDoc block in
// the file as this page's description, so the rationale below is line comments.
// ⚠️ Keep the file header above short: `build-docs.ts` publishes the FIRST
// HEADER-ZONE doc block that documents no symbol — the zone ends at the first
// declaration, so a later block cannot take over (`scripts/lib/file-description.ts`
// selects it) — as this page's description, so the rationale below is line comments.
//
// ─── Why this one schema is strict while the rest of the file is not (#4721) ──
//
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-api/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: ExecutionContextSchema
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object,
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-data/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-query/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ from `node_modules` — there is no local copy in the skill bundle.

- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.

## Transitive dependencies

Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-ui/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
- `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Draft
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
29 changes: 29 additions & 0 deletions .changeset/query-zod-module-description.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
---
"@objectstack/spec": patch
---

fix(spec): `data/query.zod.ts` now describes the query AST, not one sort node

The published skill reference indexes and the generated `data/query` reference
page opened on "Sort Node" — the description of a single `{ field, order }`
pair — for the file that carries the entire `QueryAST`.

The generators publish the module's OWN doc block: top-level, in the header
zone, documenting no symbol. `query.zod.ts` had no block of its own, and
`SortNodeSchema`'s qualified, because the file's rationale comments sit between
that block and its schema, so nothing attached it to a symbol. The mechanism was
understood when the file was written — a warning comment sits directly under
that block saying the first block becomes the page description. What was not
noticed is the ORDERING: the first block belonged to a symbol, and a comment
warning about a rule is not the same as satisfying it.

The file now opens with a short header of its own, reusing the sentence
`QueryAST`'s own type block already carried. Four published skill indexes
(`objectstack-query`, `objectstack-data`, `objectstack-api`, `objectstack-ui`)
and the reference page name the query AST as a result. `SortNode`'s block is
untouched and still documents the schema it belongs to; the warning comment
beside it now names which block is published and which selector picks it.

What the wrong row cost, in the skills' own terms: the skill tells an agent to
read the source for exact field shapes, so a pointer labelled "Sort Node" makes
it skip the one file that carries the AST.
8 changes: 5 additions & 3 deletions content/docs/references/data/query.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,11 @@ description: Query protocol schemas

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Sort Node
Represents "Order By" — one `{ field, order }` pair. Unknown keys are
REJECTED (#4721); spell the direction `order`, never `direction`.
QueryAST — Abstract Syntax Tree for data queries.

The query AST every data read is expressed in: `where` predicates, `fields`
projection, `orderBy` sort nodes, `expand` traversal, pagination and
aggregation.

<Callout type="info">
**Source:** `packages/spec/src/data/query.zod.ts`
Expand Down
110 changes: 110 additions & 0 deletions packages/spec/scripts/query-pointer-row.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Pin for WHAT the published pointer row for `data/query.zod.ts` names — the
* query AST, not one sort node.
*
* `build-skill-references.ts` describes each source by the module's own doc
* block (`lib/file-description.ts` selects it: top-level, in the header zone,
* documenting no symbol). `query.zod.ts` had no block of its own, and
* `SortNodeSchema`'s block qualified — the file's rationale comments sit
* between that block and its schema, so nothing attached it to a symbol. Four
* published indexes (`objectstack-query`, `-data`, `-api`, `-ui`) therefore
* labelled the file carrying the whole `QueryAST` "Sort Node", and the public
* reference page `content/docs/references/data/query.mdx` opened on it. The
* skill tells an agent to Read the source for exact field shapes, so that row
* cost the one read it exists to route: an agent looking for the query AST
* skips the only file that has it.
*
* No gate could see it. `check:skill-refs` and `check:docs` compare the
* artifact against the generator, and the generator reproduced the wrong block
* faithfully — the same blind spot #5059 and #12201 found one layer up, so the
* answer is the same one: pin the fact the artifact must state, not the
* pipeline that states it.
*
* Two legs, and they fail DIFFERENTLY, which is why both exist. The SOURCE leg
* reds the moment the file header is deleted or demoted below another block —
* no regeneration needed. The CORPUS leg stays green through that (it reads
* checked-in bytes, which only move when someone regenerates) and reds on the
* state this card actually found: an index regenerated from a file with no
* header of its own. MEASURED both ways in the fix's reverse verification.
*/

import fs from 'fs';
import path from 'path';
import url from 'url';

import { describe, expect, it } from 'vitest';

import { findModuleDocBlock } from './lib/file-description';

const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(HERE, '../../..');
const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills');
const QUERY_SOURCE = path.resolve(HERE, '../src/data/query.zod.ts');

/**
* The module's own opening sentence — the one `QueryAST`'s type block already
* carried further down the file. Spelled out rather than derived from the
* source: deriving it would re-assert the generator's rule and say nothing
* about WHICH subject the row names, which is the whole defect.
*/
const QUERY_AST_SENTENCE = 'QueryAST — Abstract Syntax Tree for data queries.';

/** The pointer path the generator writes for this source in every index. */
const POINTER = 'node_modules/@objectstack/spec/src/data/query.zod.ts';

/** First prose line of the block the generator would publish for a source. */
const firstDescriptionLine = (source: string): string | null => {
const block = findModuleDocBlock(source);
if (block === null) return null;
const lines = block
.split('\n')
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
.filter((line) => line && !line.startsWith('@') && !line.startsWith('```'));
return lines[0] ?? null;
};

describe('data/query.zod.ts — the module block describes the module', () => {
it('opens on the QueryAST sentence, not on `SortNode`', () => {
const source = fs.readFileSync(QUERY_SOURCE, 'utf-8');
expect(firstDescriptionLine(source)).toBe(QUERY_AST_SENTENCE);
});

it('still carries `SortNode`s own block — the fix adds a header, it does not move a symbol doc', () => {
// Passing by deleting the symbol's documentation would satisfy the row and
// lose what the block says about `direction` vs `order` (#4721).
expect(fs.readFileSync(QUERY_SOURCE, 'utf-8')).toContain(' * Sort Node');
});
});

describe('published catalog — every pointer row for the query AST names it', () => {
/** Every checked-in skill-index row pointing at `data/query.zod.ts`. */
const publishedRows = (): { file: string; description: string }[] => {
const rows: { file: string; description: string }[] = [];
for (const skill of fs.readdirSync(SKILLS_DIR)) {
const index = path.resolve(SKILLS_DIR, skill, 'references/_index.md');
if (!fs.existsSync(index)) continue;
for (const line of fs.readFileSync(index, 'utf-8').split('\n')) {
const match = /^- `([^`]+)` — (.+)$/.exec(line);
if (match && match[1] === POINTER) {
rows.push({ file: path.relative(REPO_ROOT, index), description: match[2].trim() });
}
}
}
return rows;
};

it('finds the rows at all', () => {
// Nothing parsed means nothing compared, and "no bad row" would read as
// green — the failure mode this whole file exists to refuse.
expect(publishedRows().length).toBeGreaterThan(0);
});

it('reads the QueryAST sentence on every one of them', () => {
const offenders = publishedRows()
.filter((row) => row.description !== QUERY_AST_SENTENCE)
.map((row) => `${row.file}: ${row.description}`);
expect(offenders).toEqual([]);
});
});
14 changes: 12 additions & 2 deletions packages/spec/src/data/query.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,13 +7,23 @@ import { lazySchema } from '../shared/lazy-schema';
import { retiredKey } from '../shared/retired-key';
import { strictObject } from '../shared/strict-object';

/**
* QueryAST — Abstract Syntax Tree for data queries.
*
* The query AST every data read is expressed in: `where` predicates, `fields`
* projection, `orderBy` sort nodes, `expand` traversal, pagination and
* aggregation.
*/

/**
* Sort Node
* Represents "Order By" — one `{ field, order }` pair. Unknown keys are
* REJECTED (#4721); spell the direction `order`, never `direction`.
*/
// ⚠️ Keep the block above short: `build-docs.ts` takes the FIRST JSDoc block in
// the file as this page's description, so the rationale below is line comments.
// ⚠️ Keep the file header above short: `build-docs.ts` publishes the FIRST
// HEADER-ZONE doc block that documents no symbol — the zone ends at the first
// declaration, so a later block cannot take over (`scripts/lib/file-description.ts`
// selects it) — as this page's description, so the rationale below is line comments.
//
// ─── Why this one schema is strict while the rest of the file is not (#4721) ──
//
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-api/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: ExecutionContextSchema
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object,
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-data/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-query/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,7 @@ from `node_modules` — there is no local copy in the skill bundle.

- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.

## Transitive dependencies

Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-ui/references/_index.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@ from `node_modules` — there is no local copy in the skill bundle.
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema
- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification
- `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node
- `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries.
- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010)
- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum
- `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol
Expand Down
Loading