From 875f98107ae4a270851a681f679271abea67b43e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 21:35:52 +0000 Subject: [PATCH 1/2] fix(spec): give `data/query.zod.ts` a module header so its pointer rows name the query AST The skill reference indexes and the generated `data/query` reference page described the file carrying the whole `QueryAST` as "Sort Node": the generators publish the module's own doc block, this file had none, and `SortNodeSchema`'s block qualified because the file's rationale comments separate it from its schema. Adds one short file-level block reusing the sentence `QueryAST`'s type block already carried, regenerates the four published indexes and the reference page, and pins the row. `SortNode`'s block is untouched. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE --- .changeset/query-zod-module-description.md | 29 +++++ content/docs/references/data/query.mdx | 8 +- .../spec/scripts/query-pointer-row.test.ts | 110 ++++++++++++++++++ packages/spec/src/data/query.zod.ts | 13 ++- skills/objectstack-api/references/_index.md | 2 +- skills/objectstack-data/references/_index.md | 2 +- skills/objectstack-query/references/_index.md | 2 +- skills/objectstack-ui/references/_index.md | 2 +- 8 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 .changeset/query-zod-module-description.md create mode 100644 packages/spec/scripts/query-pointer-row.test.ts 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..034b55cbab 100644 --- a/packages/spec/src/data/query.zod.ts +++ b/packages/spec/src/data/query.zod.ts @@ -7,13 +7,22 @@ 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 doc +// block that documents no symbol (`scripts/lib/file-description.ts`) 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 From 01a03f98e0f8c2de5f1f1384b540be8e777055d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 22:15:33 +0000 Subject: [PATCH 2/2] docs(spec): name the header-zone condition in query.zod.ts's selector warning The warning comment beside `SortNode`'s block said the generator publishes "the FIRST doc block that documents no symbol". That is the condition this card's defect turned on, but it is only two of the selector's three: a block documenting no symbol is published only while it is still in the HEADER ZONE, which the first declaration closes (`scripts/lib/file-description.ts`). Left as it was, the next author could put a block documenting nothing below a declaration and expect it on the page. Comment-only: no doc block changes, so no generated artifact moves. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE --- packages/spec/src/data/query.zod.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/spec/src/data/query.zod.ts b/packages/spec/src/data/query.zod.ts index 034b55cbab..341bb947a4 100644 --- a/packages/spec/src/data/query.zod.ts +++ b/packages/spec/src/data/query.zod.ts @@ -20,9 +20,10 @@ import { strictObject } from '../shared/strict-object'; * Represents "Order By" — one `{ field, order }` pair. Unknown keys are * REJECTED (#4721); spell the direction `order`, never `direction`. */ -// ⚠️ Keep the file header above short: `build-docs.ts` publishes the FIRST doc -// block that documents no symbol (`scripts/lib/file-description.ts`) 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) ── //