diff --git a/.changeset/query-zod-module-description.md b/.changeset/query-zod-module-description.md new file mode 100644 index 0000000000..7a82fd700a --- /dev/null +++ b/.changeset/query-zod-module-description.md @@ -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. diff --git a/content/docs/references/data/query.mdx b/content/docs/references/data/query.mdx index aa1d0329f4..85a7bd909d 100644 --- a/content/docs/references/data/query.mdx +++ b/content/docs/references/data/query.mdx @@ -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. **Source:** `packages/spec/src/data/query.zod.ts` diff --git a/packages/spec/scripts/query-pointer-row.test.ts b/packages/spec/scripts/query-pointer-row.test.ts new file mode 100644 index 0000000000..6186905edb --- /dev/null +++ b/packages/spec/scripts/query-pointer-row.test.ts @@ -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([]); + }); +}); diff --git a/packages/spec/src/data/query.zod.ts b/packages/spec/src/data/query.zod.ts index 7e40486284..341bb947a4 100644 --- a/packages/spec/src/data/query.zod.ts +++ b/packages/spec/src/data/query.zod.ts @@ -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) ── // diff --git a/skills/objectstack-api/references/_index.md b/skills/objectstack-api/references/_index.md index 9aa8e71cf7..519c9ff728 100644 --- a/skills/objectstack-api/references/_index.md +++ b/skills/objectstack-api/references/_index.md @@ -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, diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md index 1526a53b26..187e605343 100644 --- a/skills/objectstack-data/references/_index.md +++ b/skills/objectstack-data/references/_index.md @@ -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 diff --git a/skills/objectstack-query/references/_index.md b/skills/objectstack-query/references/_index.md index 3cea130c09..0287454b4e 100644 --- a/skills/objectstack-query/references/_index.md +++ b/skills/objectstack-query/references/_index.md @@ -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 diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index 02f1bb86c5..96831e8bd9 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -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