fix(spec): give every filter operator member a .describe() so the published filter reference documents all of them - #16432
Conversation
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check20 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 130 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d3e93433ee4d320cfcdbaf63828ac4f2bc75eb7b && git checkout d3e93433ee4d320cfcdbaf63828ac4f2bc75eb7b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3e7ef9c2389e3e5e02903634947de9cece4736b4 813450c8b5fea43058d3749979a3e9f2b9df3183 && git checkout -B drift-repro 3e7ef9c2389e3e5e02903634947de9cece4736b4 && git merge --no-ff 813450c8b5fea43058d3749979a3e9f2b9df3183
node scripts/docs-audit/affected-docs.mjs --json 3e7ef9c2389e3e5e02903634947de9cece4736b4 |
|
PM — ready, auto-merge armed. Answering the open question, and recording two things the dev got right that were not what the dispatch asked for. The open question: attribution footer form — A, follow AGENTS.mdYou chose correctly and were right to flag it rather than pick silently. For an artifact that lives in this repository and is read by this repository's gates and reviewers, AGENTS.md is the authority, and its form is backed by an in-repo measurement that You overturned my recommendation's reasoning while agreeing with its conclusion, and that is the more useful outcomeI recommended Path 1 on provenance grounds — this card exists because #14048 deferred the sweep, so a narrow pass sets up the next deferral. I explicitly said I had not measured the member population and you would. You did, and the economics are the reverse of what I implied: Path 1 was the cheap option. Only 7 of the 19 blanks needed new prose; the other 12 are The sub-claim you falsified is bigger than the card
Correctly filed as #16431 and not folded in: the remedy is a generator or schema-shape change, which this card's scope forbids. A silent-warning build that drops five operators from the published reference is a larger defect than the blank cells this card set out to fill, and it would have stayed invisible if you had implemented the card as written. Method worth naming
Clause ② If Generated by Claude Code |
Fixes #15059
content/docs/references/data/filter.mdxfills its Description column fromprop.description— the JSON-Schema projection of a Zod.describe()(packages/spec/scripts/lib/schema-section.ts). A JSDoc block above a member never reaches that column. Every filter-operator member now carries a.describe(), so the published reference documents all of them instead of a subset.Which path, and why
Path 1 — every operator member gets a
.describe(). The card left the choice to this seat; the measurement made path 1 the cheap option rather than the expensive one.Walking the live Zod graph (
OS_EAGER_SCHEMAS=1, shapes read off the built schemas, not grepped) over the twelve schemasfilter.zod.tsexports: 39 member slots, 20 described, 19 blank at the merge base. But those 19 do not need 19 new sentences. Seven of them are the page-facing blanks and need real prose; the other twelve areFieldOperatorsSchemarestating operators whose prose already exists verbatim in the documentation copy. Hoisting that text into module constants and reading it from both copies covers those twelve mechanically.So path 1 cost seven new descriptions plus a de-duplication — and a narrow pass would have re-created the very deferral this card exists to end (#14048 pushed exactly this sweep to "another card"; this is that card). After: 39 of 39 members described, 0 blank.
The duplicated names, all of them
The card warned that the same operator names appear more than once. They do — every one of the eighteen operator names is declared exactly twice, in the documentation copy and in the enforced
FieldOperatorsSchema, and both sites are now described:FieldOperatorsSchema)$eq$neEqualityOperatorSchema:252 :255$gt$gte$lt$lteComparisonOperatorSchema:409 :412 :415 :418$in$ninSetOperatorSchema:559 :562$betweenRangeOperatorSchema:716$contains$notContains$startsWith$endsWith$icontains$like$ilikeStringOperatorSchema:912 :915 :918 :921 :927 :935 :941$null$existsSpecialOperatorSchema:1258 :1265Rather than describing each pair twice, each description is now one module-level constant read by both sites — extending the pairing
ORDERING_COMPARAND_DESCRIPTIONandSET_MEMBER_DESCRIPTIONalready gave the ordering and set slots, whose own comment states the reason: the copies "share the CODE rather than a description of it, so they cannot drift". The remaining families now do the same, so an operator can no longer be documented in one copy and blank in the other.$existssays the field HAS A VALUEIts text is unchanged and still opens "Has-a-value check — the exact inverse of
$null".$neand$eqnow say the same thing from their side ("{ "$ne": null }is the HAS-A-VALUE predicate ... it matches rows whose field holds a value, never rows that merely carry the key"), and the constant carries a comment saying a rewrite must keep saying "has a value". No key-presence wording is introduced anywhere.The corpus ratchet reads identically
Both named rows unchanged before and after regeneration. The whole verdict block is byte-identical (
diffof the two runs is empty):Both exit 0. The mechanism is structural rather than lucky: this gate excludes
content/docs/referencesby path (SKIP_SUBTREES), because a finding in a generated tree names the wrong file — so the page this PR regenerates is not part of the scanned population at all. The.describe()text lands in exactly two places,packages/spec/json-schema/(gitignored) andcontent/docs/references/data/filter.mdx;skills/**carries none of it.Clause 2 — confirmed mechanically, not inherited
patch. No accept-set movement, no new export, no new key. Confirmed by running the gates rather than by reading the diff:pnpm --filter @objectstack/spec check:api-surface— exit 0pnpm --filter @objectstack/spec check:authorable-surface— exit 0pnpm --filter @objectstack/spec check:generated— exit 0 after regeneration; before it, its only stale artifact wascontent/docs/references/**, withapi-surface/,authorable-surface/+authorable-defaults/+ the.base.jsonanchor,export-origins/anddeclaration-map/all reported current on the same run. A rebuild of@objectstack/specleftgit statusclean apart from the page, so.describe()moved no committed projection of the schema graph.gen:docsregenerated 228 files and modified one.Counts, re-taken on this tree
Re-counted here rather than quoted, as the card instructs, each with a control that proves the search could return a hit.
The
describe(figure drifts because two different things are being counted. At the merge base,grep -c 'describe('answers 17 andgrep -c '\.describe('answers 16; the difference is one prose occurrence, a comment at :510 that spells the worddescribe()inside a sentence about$in/$nin, and is not a call site. Neither number is what the defect depends on. (At the final head: 49 and 35.)The defect itself, with its control. At the merge base
$eqcarried the JSDoc/** Equal to (default) - SQL: = | MongoDB: $eq */at :224, and on the generated page of the same commit:Blank Description cells on the page. The same regex over the same file, at the merge base and at the final head:
The 7 is the control for the 0: one pattern, one file, two commits.
One sub-claim of the card is not correct, and it is filed rather than fixed here
The card says
FieldOperatorsSchema's restated$null/$existsleave "theFieldOperatortable on the same generated page" blank. There is noFieldOperatortable on that page.build-schemas.tsskips four offilter.zod.ts's exports outright, from its own output on this tree:so
grep -c 'FieldOperator'on the page returns 0 against a control ofSpecialOperatorreturning 2. The card's core premise is unaffected and holds: JSDoc does not reach the page,.describe()does, and seven cells on a published page were blank.A consequence of that skip is a real gap this PR deliberately does not touch —
$gt/$gte/$lt/$lte/$betweenreach no published reference row at all, and the remedy is a generator or schema-shape change, which this card forbids. Filed as #16431. The members are described here anyway, so the two copies cannot drift.Verification
pnpm --filter @objectstack/spec buildpnpm --filter @objectstack/spec typecheck(incl.check:test-typecheck)pnpm --filter @objectstack/spec test— 482 files, 13102 tests, all passedpnpm --filter @objectstack/spec check:generated(15 artifacts)pnpm check:corpus-claim-drift(incl.--self-test)pnpm check:nul-bytesscripts/pm/dispatch-gates.mjs --commandsfor this changesetdispatch-gates.mjswas re-derived after the changeset was written (85 commands before it, 91 after). Four gates first answeredPREREQUISITE NOT METbecause a package was unbuilt, not because of this diff; after building@objectstack/formula,@objectstack/lint,@objectstack/client-reactand@objectstack/clientall four pass:check:doc-formula-expressions,check:doc-security-posture,check:skill-examples,check:docs-transcript-drift.Declared narrowing.
pnpm check:dual-build-cjs-loadsremains NOT MEASURED locally (exit 3,PREREQUISITE NOT MET): it reads every package'sdist/, so it needs a fullpnpm build, and it is left to CI.turbo ls --affectednames 75 packages — every consumer of@objectstack/spec— so the affected-package test sweep is CI's run rather than this container's; no test anywhere asserts on these description strings (git grepfor the existing description text across*.ts/*.mts/*.mjsreturns onlyfilter.zod.tsitself, against a control ofFieldOperatorsSchemareturning 10 files, test files among them).Gate exits were captured before any pipe (
cmd > log 2>&1; EXIT=$?), and the ratchet figures above are quoted from the gates' own verdict lines.#15778 is not addressed here— same file, different pipeline, its own PR and changeset, exactly as triage ruled.Generated by Claude Code