feat(spec): comparand-type door — the accepted literal comparand set, enforced once at the shared compile face for all five drivers (#7872) - #8234
Conversation
…rced at the shared compile face (#7872) The measured superset string | number | bigint | boolean | null | Date is defined in packages/spec (filter-comparand-type.ts), enforced by parseFilterAST on everything it returns and by the engine's lowering seam on the object form; everything else is refused with the INVALID_FILTER / 400 envelope naming the accepted set. bigint is narrowed copy-on-write to its exact number (refused beyond 2^53), which kills the driver-memory mingo crash cell without touching the frozen driver; the mongo silent-edit cell ({qty: undefined} wiring to {} = match everything) becomes a refusal at the door. SQL-family allowlists source their set membership and wording from the door instead of local copies. FILTER_COMPARAND_TYPE_CASES conformance table added and consumed by all five driver suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
…d-type door exports (#7872) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
…7872 door Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 4 package(s): 113 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…tree (#7872 relay lap over #8230/#8232) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
|
os-regen relay lap over #8230 / #8232 — complete. Merge sha Sibling-entry assertions on the regenerated artifacts:
Gates and suites on the new head: PR stays draft; over to the PM for gate verification on Generated by Claude Code Generated by Claude Code |
…erased engine option bags (#7872) The door suite's registerObject(deal) call was missing the required packageId (the +1 over objectql's frozen TEST_DEBT 355); and its option bags used bare 'as any', which the query-options-erasure ratchet counts (240 -> 245). The bags now type-check uncast — FilterCondition's index signature admits the deliberately off-contract comparands, which is the point of the door — except the one FilterArray case, which carries the sanctioned 'as unknown as EngineQueryOptions' naming the contract it bypasses (not counted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
|
Patch round for the two red farm jobs — fixed, new head
Behavior re-verified after the edits: door suite + lowering suite — 2 files / 58 tests green (exit 0). Diff is one test file; no source, no artifacts, no changeset delta. PR stays draft on Generated by Claude Code Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31653423757 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
os-regen merge lap for landing: merge origin/main (committed first, per scripts/pm/os-regen-merge.sh), then wholesale regen of the os-regen'd references/api pages from the merged tree. Sibling assertions on the regenerated tree: #8230 retirement state (def names only in retirement prose + migration entry), #8232 currency fraction-digit guard, #8234's 13 comparand-door exports in api-surface/data.json, #8236's groupBy union row all survive; MONGODB_MULTI_TENANT_UNSUPPORTED stays absent from the ledger and the references ErrorCode union reads main-minus-one (+265 -> +264; the union grew by 2 on main since the previous lap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
Fixes #7872
Implements the maintainer ruling on this card (comment 5265944890, 「同意你的决策」): the shared filter-compilation face in
packages/specdefines the accepted literal comparand-type set as the measured superset —string | number | bigint | boolean | null | Date(#7956's divergence matrix; the "any JSON-representable literal" wording was rejected because bigint is not JSON-representable yet accepted by 4/5 drivers) and refuses everything else loudly at the compile face, withcode: INVALID_FILTER+status: 400+ guidance naming the accepted set.One PM mechanism assumption was falsified — the measured call graph, and the adjusted wiring
The dispatch assumed
parseFilterASTis on every driver's path. Measured on this branch's base:metadata-protocol/protocol.ts~6782) runsparseFilterASTonly behindisFilterAST, which answersfalsefor every non-array — so an object-form wire filter never passes through it.objectql/engine.tslowerWhereFilterArray) routes only the array branch throughparseFilterAST; the non-array branch returned after the 数据 API:集合算子not_in/in的比较值是标量(非数组)时答 500 DATABASE_ERROR,而不是带信封的 400 —— 而这个形状是 spec 合法的 ViewFilterRule #5869 shape gate.lowerWhereFilterArray— the same measurement 数据 API:集合算子not_in/in的比较值是标量(非数组)时答 500 DATABASE_ERROR,而不是带信封的 400 —— 而这个形状是 spec 合法的 ViewFilterRule #5869 recorded when it placed the list-shape gate there.The object form is exactly how the #7956 matrix (and the mongo worst cell) arrives, so a door wired in
parseFilterASTalone would have missed the dominant form — theNormalizedFilterSchemalesson again. The adjusted wiring keeps the ruling's substance intact:packages/spec(data/filter-comparand-type.ts— the set, the predicate, the walk, the refusal envelope, the shared sentence);parseFilterASTjudges everything it returns (object passthrough included), making it the compile face the ruling names — this covers Door 1's array lowering, analytics' normalizer, and every direct caller;normalizeFilterComparandTypes, imported from spec), beside the 数据 API:集合算子not_in/in的比较值是标量(非数组)时答 500 DATABASE_ERROR,而不是带信封的 400 —— 而这个形状是 spec 合法的 ViewFilterRule #5869 gate, on all six filter-collecting verbs (find/findOne/count/aggregate/update/delete).Net: every platform path to every driver passes the door;
parseFilterASTis now on the object form's path wherever it previously was at all. Flagged here rather than silently absorbed; if the PM reads assumption 1's STOP clause as outranking this, the wiring commit is cleanly separable.The bigint cell — how the ruling's two sentences were reconciled
The ruling both (a) puts
bigintin the accepted set and (b) says the memory BigInt crash cell "becomes a refusal". A bigint passed through verbatim still crashes mingo (Query.compile→JSON.stringify), and driver-local patches are frozen (#5499) — so (a) and (b) cannot both be literal. The approved recommendation's own sentence ("mongo's silent-edit cell and memory's crash cell both die at the door without touching frozen drivers") is implementable, and that is what this PR does:RemoteTransport's Date → ISO — so no driver path ever sees a bigint, and the memory cell becomes a working compile (the conformance table pins{qty: {$eq: BigInt(100)}}matching its row on all five drivers);remote-transport-comparand-refusal.test.ts's pin is untouched).The
#6915citation in the ruling is a mis-cite (that card is driver-memory tenant isolation); the BigInt crash cell is this card's own memory×BigInt cell. Both readings are surfaced in the report'sopen_questionsfor the maintainer to confirm.Both directions pinned —
FILTER_COMPARAND_TYPE_CASES, run by all five driversNew conformance table in
spec/data(registered inscripts/check-driver-conformance.mjs; the matrix is now 45 covered / 0 DEBT / 0 exempt):translateFilter+ the mongodb package's own BSON round trip (driver-mongodb). Stated in that suite as the accepted substitute.undefined(operator form, implicit form — the mongo worst cell — and as an$inmember), function, Symbol, Map, Set, class instance, plain object in a scalar slot, oversized bigint — all refused at the door with the envelope, and the suites assert no driver ever runs (the engine suite pins zero driver reads/writes on refusal).{qty: undefined}still BSON-encodes to{}= match everything on the direct path. Both notes say when to retire the pins.SQL-family allowlists reconciled to the door, not duplicated
driver-sqlisBindableComparand/isRenderableTextComparandnow delegate type membership toisAcceptedFilterComparand; refusal messages quoteACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE. Two recorded driver-local extras stay, each with its reason in place: theundefined → truearm (unreachable behind the drivers:undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 walk, which owns the purpose-written undefined refusal) andArrayBuffer.isView(blob bindables — a direct-caller surface the engine door does not admit; nothing on engine paths uses them, measured by grep and by the suites).driver-tursoserializeComparanddelegates the scalar membership to the same predicate; its pinned refusal wording ("must be a string, number, bigint, boolean, null or Date") is now the shared constant, byte-identical, so reconciling changed no message.Boundaries the door deliberately does not judge (each has a recorded owner)
FieldReference({ $field: … }) at any position — [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222 / [spec]FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596 / [spec]parseFilterASTlowers['a', '=', { $field: 'b' }]to a bare field spec no backend reads as equality #7597 own its per-position fate.$keys) — the 数据 API:集合算子not_in/in的比较值是标量(非数组)时答 500 DATABASE_ERROR,而不是带信封的 400 —— 而这个形状是 spec 合法的 ViewFilterRule #5869 boundary, kept verbatim.$in/$nin/$between— unmeasured, unruled, answered per driver today.$wat,$regex) — the downstream refusals carry the specific prescriptions (RETIRED_FILTER_OPERATORS); a generic type refusal would preempt a better message.{ $in: 'won' }) — the engine's 数据 API:集合算子not_in/in的比较值是标量(非数组)时答 500 DATABASE_ERROR,而不是带信封的 400 —— 而这个形状是 spec 合法的 ViewFilterRule #5869 shape gate, whose driver-mongodb 带着 #5328 的同一条缝:形状错误的$between/ 非数组$and、$or/ 非对象$not全被静默吞掉,且它的算子拒收没有 ADR-0112 信封 #5346/driver-sql 把**文档级**未声明$op({$where:…}、{$nor:[…]})当成列名编译,静默返回空结果集 —— #5324 的文档级一半在 SQL 侧还在 #5348 wording is pinned by consumers.Changeset: minor, argued from the diff
Precedent #3746 (acceptance narrowing at a validation face, minor) fits what this diff does: no authorable key is removed, no export is removed, and every newly-refused input was measured answering a crash, a silent wrong row count, or a silent match-all — not a documented capability. The one previously-"working" behavior that narrows is mongo/memory deep-equality via
$eq: {plain object}through engine paths (direct driver calls unchanged) — measured divergent 3-vs-2 across drivers, which is the divergence the ruling closes. #4583-major (removal of a declared key with migration prescription) does not describe this diff. Not declared-breaking ⇒ no ADR-0087 marker required; the changeset carries the full caller-facing delta anyway.Verification
All local runs serialized on the shared verify lock,
NODE_OPTIONS=--max-old-space-size=4096, scoped filters.Pre-merge batch (base 37b82ed):
@objectstack/spectest — full suite green (386 files; earlier baseline run: 10236 tests, the two reds it surfaced — a 521-char refusal message and the stale export snapshots — were fixed/regenerated and the suite re-ran green).@objectstack/objectqltest — 191/192 green; the one red wasengine-filter-array-lowering.test.ts's pass-through pin for{ $eq: { $in: 'not-an-operator' } }, a fixture that pinned exactly the limb the ruling re-judges (plain object in a scalar slot). Re-judged per the fixture-triage rule: the case now asserts the door's TYPE refusal atwhere.stage.$eqand that the message never misreads the inner$indata key as a malformed operator (not.toContain('requires an ARRAY')) — the old pin's surviving concern, kept.@objectstack/metadata-protocol(Door 1), all five driver suites,@objectstack/service-analytics,@objectstack/runtime+@objectstack/rest— green (runtime: 145 files / 2195 tests).check:driver-conformance— 45 covered / 0 DEBT / 0 exempt (was 40/0/0; the five new cells are the door column, all covered in this PR).check:nul-bytes— green (7424 files).check:generated— 2 of 13 stale (api-surface/,export-origins/— the 13 new exports), regenerated with--fix, additions only.Post-merge lap (merged
origin/main@ fa48973; incoming touchedobjectql/src/engine.ts— the same file as the door wiring — andspec/src/contracts/*): rebuilt the closure, then re-ran the moved packages — speccheck:generatedgreen (artifacts still current), spec test 386/386 files green, objectql test 193/193 files green (the re-judged pin and the new door suite included).Examples validate ×3: all green (
app-crm,app-showcase,app-todo— "Validation passed"; todo's two draft-flow warnings pre-exist this branch). The first two attempts failed on this worktree's own build state (packages/cliandconnector-mcphad no dist —command validate not found/ missing module), not on the diff; building the examples' dependency closures by package name fixed both.Out-of-scope findings
service-analyticscarries a third and fourth copy of the same six-type membership (comparand-shape.ts:116/:142, message twins inread-scope-sql.ts) — outside this dispatch's declared file surface, filed as [finding] service-analytics carries its own copies of the comparand-type allow-list the #7872 door now single-sources — reconcile membership and message wording to the door #8186 for reconciliation to the door.Generated by Claude Code