docs(skills): $exists means HAS A VALUE — retire the MongoDB $exists claim from objectstack-query - #13577
Conversation
…claim (#13539) GOVERNED SURFACE — published, customer-facing skill text. Both rows taught key-presence, and `objectstack-query/SKILL.md` named the mapping outright: "MongoDB `$exists`". Since 9dac1ae (PR #13529) `driver-mongodb`'s translateFilter never emits MongoDB's `$exists` — it lowers the operator to `{$ne: null}` / `{$eq: null}` — so a customer following that line writes a filter whose behaviour differs from the promise on any null-valued field. Established from code and from an executed test, not from another document: - mongodb-filter.ts `case '$exists'` puts `$ne: null` / `$eq: null`; - sql-driver.ts `case '$exists'` compiles whereNotNull / whereNull; - objectql having-filter.ts evaluates `value !== undefined && value !== null`; - 14/14 green in driver-mongodb/src/mongodb-exists-has-value-translation.test.ts, which pins `translateFilter({name: {$exists: true}})` to `{name: {$ne: null}}`. Written to the published token ratchet rather than around it: both files carry zero headroom, so the correction is byte-neutral-or-shrinking rather than an added warning. SKILL.md 22208 -> 22207 bytes, filters.md 8593 -> 8596; both files unchanged in tokens and lines, bundle total unchanged at 186751. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Ownership note from
|
|
Contract review PASS at head Verified this round against origin/main
Generated by Claude Code |
Part of #13539 — deliberately not a closing keyword. This PR carries 2 of that card's 6 sites; the
content/docs/**half ships as #13581, and thepackages/spec/src/data/filter.zod.tsJSDoc site belongs to thedomain:specseat. Merging this must not close the card while those remain.Session, for durable attribution:
https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC⛔ Governed surface — human merge, deliberately parked
skills/**is the published, customer-facing surface. This PR is a draft on purpose: auto-merge is not armed, it is not marked ready, and the dispatching seat will not arm it. It waits for a maintainer to merge by hand. A draft awaiting the maintainer's own merge is the correct terminal state here, not a stuck PR.The card's triage ruled three surfaces, three merge regimes, three PRs, ⛔ not one bundle — precisely so the two customer-reachable lines are not held behind the slowest regime. That is why this is its own PR.
The defect — two published lines that are false
skills/objectstack-query/SKILL.md, "Null & Existence Operators":skills/objectstack-query/rules/filters.md, "Operator Reference":The sharpest one is the SKILL.md row: it told authors
$existsmaps to MongoDB$exists. Since9dac1ae017(PR #13529)driver-mongodb'stranslateFilternever emits MongoDB's$existsat all. A customer following that line writes a filter whose behaviour differs from the promise on every null-valued field.Why it is false — established from code, then from an executed test
Not from another document. The implementing predicates:
packages/drivers/driver-mongodb/src/mongodb-filter.ts,case '$exists'→put(value === true ? '$ne' : '$eq', null)— the same spelling the$nullarm emits;packages/drivers/driver-sql/src/sql-driver.ts,case '$exists'→whereNotNull/whereNull;packages/objectql/src/having-filter.ts,case '$exists'→value !== undefined && value !== null;packages/spec/src/data/filter-logic-conformance.tsstates the settled semantic verbatim: "$existsmeans "has a value" (!= null), never key-presence".Executed, not merely read — 14/14 green on
packages/drivers/driver-mongodb/src/mongodb-exists-has-value-translation.test.ts, which pinstranslateFilter({name: {$exists: true}})to{name: {$ne: null}}and asserts that document is exactly what$nullemits.Written to the published token ratchet, not around it
Both files sit at zero headroom under
scripts/check-skills-token-ratchet.mjs, so this correction is byte-neutral-or-shrinking rather than an added warning paragraph. No ceiling was raised — raising one is a maintainer's call, not a dev's. That constraint is why the nuance ("not key presence", "MongoDB never sees an$exists") is spelled out in the docs half rather than added here.SKILL.mdlines (whole file)rules/filters.mdlines (whole file)skills/objectstack-query/**package lines (all.md)SKILL.mdtokens (ceiling 5552)rules/filters.mdtokens (ceiling 2149)Bytes moved −1 and +3 respectively; token and line counts are unchanged in both directions.
Gates — run locally at head
2f2451c0dThe family was derived from the real diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, not recalled. All 13 derived families green, pluscheck:skill-exampleswhich reads this very corpus:node scripts/check-test-completeness.mjsexits 3 = PREREQUISITE NOT MET by its own text (it grades a savedturbo run testlog and none was named): recorded as NOT MEASURED, not as a red.ESLint was not run repo-wide: its population, read from
eslint.config.mjsitself, is**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}and this diff is.mdonly, so zero of these files are in that gate's population in either direction.No changeset: this PR releases nothing from any package, which is the repo's live convention for a pure
skills/**change — measured, the last 12 pure-skills/**commits onmaincarry zero changesets.skip-changesetis applied.Clause ② carrier
needs:contract-reviewis attached to this PR and to #13539 in the same pass, under the standing instruction that the card's clause-② determination is unchanged and the same write that creates a PR re-attaches both carriers. Measured honestly: this diff touches zeropackages/spec/**paths, so the path limb is not fired by these bytes — the carrier is on the card's determination, not on this diff. ⛔ Neither this PR nor the dispatching seat may self-clear it.What is not here, on purpose
packages/spec/src/data/filter.zod.ts:954— the JSDoc site.packages/spechas a single owner; ⛔ not touched.content/docs/references/data/filter.mdxis generated from it and inherits whatever wording that seat lands.content/docs/data-modeling/queries.mdxalready reads "Field has a value" and was left alone — it is the target wording, not another site to sweep.Generated by Claude Code