feat(lint): field-typed equality/membership arm for filter-preset-comparand on declared date/datetime fields - #16347
Conversation
…parand On a declared date / datetime field, refuse a dashboard date-range preset name in every comparand position (bare, $eq/$ne, $in/$nin, and the view-rule and triple spellings), binding each authored filter to the object its conditions address through the stack's own object graph. The field-agnostic schema door keeps its ordering-only boundary. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
…eset-comparand-field-typed
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
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 d7d982c7aa983e16004d25cd1bde9d924fbe172c && git checkout d7d982c7aa983e16004d25cd1bde9d924fbe172c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 199fec477094f49f4a0be11f30e95792817ef91b b44c4877f7978f7baf8f53d04d9fdfce6307d137 && git checkout -B drift-repro 199fec477094f49f4a0be11f30e95792817ef91b && git merge --no-ff b44c4877f7978f7baf8f53d04d9fdfce6307d137
node scripts/docs-audit/affected-docs.mjs --json 199fec477094f49f4a0be11f30e95792817ef91b
|
… in the shared message's TSDoc The rule's registration comment said it needs no resolution context; arm 2 reads the stack's objects and datasets when present. The shared refusal wording's TSDoc said "ordering comparand"; it is now also the wording for the field-typed equality/membership positions. The TSDoc publishes in dist/*.d.ts (measured), so the spec change carries a patch changeset. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
…-② gate Check Changeset reads clause-② at PR level and requires at least minor on every package the PR grows. The spec diff is a TSDoc comment only; the public-surface widening of this PR is in @objectstack/lint, already minor. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
…object, never the parent form The public-lookup route runs a publicPicker filter on the referenced object (picker.object, else the field's reference). The field-typed arm's binding fell through to the view's own object, so a parent date column and a referenced select column sharing a name produced a false refusal on a legitimate picker filter. The picker is now a claiming reader: it resolves the referenced object or leaves the position unjudged. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
落地前检三条 ·
|
| 载体 | 读回 |
|---|---|
| 卡 #16106 | 5 → 4:bug · priority:p2 · pm:dispatched · domain:spec |
| PR #16347 | 6 → 5:documentation · size/l · tests · tooling · protocol:data |
两边都先读当前集再写合并集 —— ⛔ 不猜。
③ 检查 —— ⚠️ 绿是真的,但它是过时聚合器判的
Test Core (5/6) cancelled 16:20:54Z (30m01s)
Test Core (必需) success 16:21:16Z
⛔ 我不把这个 success 当作分片跑过了。 PR #16316(Part of #16157,汇总修复)在 14:48:55Z 落地为 b9a14dd64f,而这个分支的基线早于它。带点亮对照实测:
origin/main scripts/check-shard-attestation.mjs "an untested shard is not a passing shard" → 1 ← 对照亮
3b6ba7b409 同一文件 → 0
⇒ 这条分支跑的是旧聚合器 —— 就是那个把被杀分片当作「不欠 attestation」、用空集满足名册检查的版本。对照:兄弟 PR #16342 合并过修复后的 main,同样的 5/6 cancelled,它的汇总诚实地判红,并逐字打印 "an untested shard is not a passing shard, see #16157"。
落地判断
转 ready、挂 auto-merge,理由是机制而非侥幸:合并队列的构建跑在 gh-readonly-queue/main/… = 当前 main(已含修复)+ 本 PR,所以队列会用诚实的聚合器重判。
- 分片在队列里跑进墙内 → 诚实落地,六个分片都真的执行过;
- 分片再被杀 → 汇总红,本 PR 被踢出队列并保持 open —— 那是正确结果,我按阻塞处理,⛔ 不当作可披露的成本。
⇒ 无论哪一种,把关的都是诚实的那一版。⛔ 错的做法是让这个过时的绿成为它落地的理由。
5560401741)。这一张不会再是第七张,因为队列现在会重判它。
两条 advisory,⛔ 不在本 PR 修
复核给的两条都不阻塞,我另行立卡:① picker 分支按属性名匹配而非 schema 位置(今天 11 处子片段命中全在 view.zod.ts,但未来别处出现同名键会静默继承此分支);② 点号 field 与嵌套 picker 上 lint 与路由发散(lint 跟随跳转,路由做顶层直查),发散方向永不产生假拒绝。
domain:spec PM 派发席 · 席位帖 #6017 · R4 · 落地前检 ①②③ 逐条读数
Generated by Claude Code
Generated by Claude Code
…field-typed Brings the honest shard aggregator from #16316 onto this branch: before this merge the branch carried the pre-fix scripts/check-shard-attestation.mjs, so the aggregate `Test Core` check reported success over a killed shard. After the merge `Test Core` reports red whenever a shard is cancelled — the intended outcome, not a regression. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
…ion is made `Check Changeset`'s level axis applied a PR-scoped declaration to every package whose `packages/*/src/**` the diff moved, and asserted about each of them that it had been widened. Measured on two PRs from one dispatch round: * PR #16342 @ 273247e -- spec `patch` + runtime `patch`, six new published STACK_* error codes. A correct fire. * PR #16347 @ 23443ce -- the widening is in `@objectstack/lint`, graded `minor` and never named by the gate; `@objectstack/spec` received one re-worded TSDoc comment, was graded `patch`, and was refused with the premise "a purely additive widening of a published package's public surface" -- false of that package. Clause 2 is declared once, for the PR: the carrier is a PR label and the `Clause-2:` line is a PR-body line, and neither names a package. The predicate is now the strongest thing that declaration entails, asked at its own grain: a PR that declares clause-2 `yes` must grade AT LEAST ONE package whose `packages/*/src/**` it moves at `minor` or above. Nothing reads the content of a diff hunk -- no tolerance, no allowlist, no comment-only skip. Only the quantifier moved. Two new verdicts keep the readings from collapsing into a tick: `discharged` (some moved packages are graded `patch`, one carries the level, and the gate prints what it is deliberately not refusing plus the residual it cannot see) beside `clean` (there was nothing of that shape at all). The `not-measured-*` materiality split reads the same single `refusable` predicate, so the unread lane cannot drift from the enforcing lane. The refusal message no longer claims each listed package was widened. It states what it can read, says out loud that it cannot tell which package received the act, and asks the author to raise the one that actually grew. Self-test: 179 -> 204 assertions; new battery "The GRAIN: a PR-scoped declaration judged at PR scope" at 25 cases; roster floor 14 -> 15. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
…ion is made (objectstack-ai#16945) `Check Changeset`'s level axis applied a PR-scoped declaration to every package whose `packages/*/src/**` the diff moved, and asserted about each of them that it had been widened. Measured on two PRs from one dispatch round: * PR objectstack-ai#16342 @ 273247e -- spec `patch` + runtime `patch`, six new published STACK_* error codes. A correct fire. * PR objectstack-ai#16347 @ 23443ce -- the widening is in `@objectstack/lint`, graded `minor` and never named by the gate; `@objectstack/spec` received one re-worded TSDoc comment, was graded `patch`, and was refused with the premise "a purely additive widening of a published package's public surface" -- false of that package. Clause 2 is declared once, for the PR: the carrier is a PR label and the `Clause-2:` line is a PR-body line, and neither names a package. The predicate is now the strongest thing that declaration entails, asked at its own grain: a PR that declares clause-2 `yes` must grade AT LEAST ONE package whose `packages/*/src/**` it moves at `minor` or above. Nothing reads the content of a diff hunk -- no tolerance, no allowlist, no comment-only skip. Only the quantifier moved. Two new verdicts keep the readings from collapsing into a tick: `discharged` (some moved packages are graded `patch`, one carries the level, and the gate prints what it is deliberately not refusing plus the residual it cannot see) beside `clean` (there was nothing of that shape at all). The `not-measured-*` materiality split reads the same single `refusable` predicate, so the unread lane cannot drift from the enforcing lane. The refusal message no longer claims each listed package was widened. It states what it can read, says out loud that it cannot tell which package received the act, and asks the author to raise the one that actually grew. Self-test: 179 -> 204 assertions; new battery "The GRAIN: a PR-scoped declaration judged at PR scope" at 25 cases; roster floor 14 -> 15. Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU Co-authored-by: Claude <[email protected]>
…ot by key name (objectstack-ai#18524) Fixes objectstack-ai#16403 Clause-②: no ## What changed `bindAncestors` (`packages/lint/src/validate-preset-comparands.ts`) matched the public-lookup picker reader on the property NAME: ```ts if (key === PUBLIC_PICKER_KEY) { ``` `walkAuthoredFilters` / `scanForFilters` recognise a filter by KEY at any depth on all eight scanned collections, so that reader was reachable from any node spelling `publicPicker`, at any depth, on any surface — and its unresolvable exit is `undefined`, i.e. no bound object, so arm 2's field-type oracle answers `false` for every key and the whole filter subtree is judged by nobody. The branch is now entered by SCHEMA POSITION: ```ts const formField = key === PUBLIC_PICKER_KEY ? strName(chain[i - 1]?.node.field) : undefined; if (formField) { ``` `publicPicker` is declared exactly once as a schema key — `FormFieldBaseSchema` in `packages/spec/src/ui/view.zod.ts`, where the enclosing record is a form field whose `field` is REQUIRED. So the enclosing `field` identifies the position, and it is the same read the branch already had to make one line later. Outside that position the node falls through to the ordinary nearest-ancestor readers, exactly like every other key this walk does not recognise. ## Coupling: which size was chosen, and what it costs The triage authorised either a comment alone or a `chain[i-1].node.field` guard, and asked for the cost of the choice to be stated. **The guard was chosen**, for one reason: it introduces **no new coupling** between `packages/lint` and the form-view schema. The read it adds at the entry is byte-for-byte the read the branch already performed at its second line — the guard moves it, it does not invent it. A comment alone would have left the rule's behaviour dependent on a key NAME staying globally unique, which is not a property any gate holds. What it costs, stated plainly: - The guard reaches only ONE case: a node reached under `publicPicker` whose enclosing ancestor declares no string `field`, AND which declares no `object` of its own. Every other shape is unchanged (an `object` on the node is read by the picker reader's override and by the ordinary `r.object` reader alike, to the same value). - In that one case the node is now bound by the ordinary nearest-ancestor readers instead of leaving unbound. That is the rule's general contract — the same contract every other unrecognised key in the config already runs under — rather than a name-triggered exemption from it. It is not a new class of risk; it is the removal of a name-shaped exception to the existing one. - A malformed form field (one that omits the required `field`) therefore also falls through. `FormFieldBaseSchema` requires `field`, so such metadata is refused at the schema door; this rule is not the door that holds it. ## No live defect today — and why the gap was still worth closing `publicPicker` has exactly one schema declaration today, and there the reader is correct. What the guard changes is the failure mode the day a second schema declares the same name: that schema's filters would have inherited this branch silently. Under-reporting is this rule's only permitted failure direction, so the hole could never have VIOLATED the invariant — it would have quietly spent it, where no test asking "was the invariant violated?" can see it. ## Evidence **Negative control (triage item 3) — the objectstack-ai#16106 B1 false refusal stays closed.** The one real picker still enters the branch and still resolves to the referenced object. Measured, `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-preset-comparands.test.ts` on the fixed tree: `Test Files 1 passed (1) · Tests 22 passed (22)`, both B1 tests among them — - `[B1] stays QUIET on a publicPicker filter over a referenced select column that shares its name with a parent date column` — PASS (a form field's picker filter over `crm_account.close_date: select` reports nothing, where the parent `crm_opportunity.close_date` is a `date`). - `[B1] POSITIVE CONTROL: the same picker filter is still refused when the REFERENCED object declares the field as a date` — PASS, reporting at `views[0].sections[0].fields[0].publicPicker.filter[0].value` through the field's `reference` and through the `object` override alike. **Reverse verification.** The fix was committed first; the guard was then reverted in place, on disk, and the same suite re-run. Direction: RED, exactly one test. ``` pre-mutation : guarded-entry lines 1 · name-only-entry lines 0 post-mutation : guarded-entry lines 0 · name-only-entry lines 1 · mutated blob != HEAD blob ablated run : Tests 1 failed | 21 passed (22) FAIL [objectstack-ai#16403] a `publicPicker` key outside the declared form-field position … - Expected ["dashboards[0].widgets[0].publicPicker.filter.close_date"] + Received [] restore : git diff HEAD empty · porcelain clean · blob == HEAD blob · guarded-entry lines back to 1 ``` The received `[]` is the gap itself: the picker reader claimed the node on its key, found no enclosing `field`, and left through the `undefined` exit, taking the subtree out of arm 2 without a word. The two B1 tests stayed GREEN under the ablation, which is the other half of the reading — the guard is orthogonal to the behaviour objectstack-ai#16347 fixed. **Package verification.** `pnpm --filter @objectstack/lint test` — `Test Files 103 passed (103) · Tests 3845 passed (3845)`. `pnpm --filter @objectstack/lint typecheck` — exit 0. `pnpm lint` (repo-wide `eslint . --no-inline-config`) — exit 0, whole population, no narrowing claimed. ## 验收备注 (Acceptance notes) The triage ruling (`5579122478`), quoted verbatim — Chinese decisions are not translated: > 1. 项 (1)(属性名匹配)与项 (2)(lint 跟了一跳 route 不跟)**可分开落地**,⛔ 不必同一个 PR。**项 (1) 是本卡主项**。 > 2. ⛔ **不动 picker 分支的 `undefined` 出口** —— 它被 objectstack-ai#16106 复审第三条消融腿(P3)的 QUIET pin 钉住,且是有意的。⭐ 分诊把修法的形状定死了:**出口被钉住了,入口没有** ⇒ 要加的是「什么样的节点算 picker」的约束。 > 3. **若加 `chain[i-1].node.field` 守卫**:**阴性对照必测** —— `view.zod.ts` 上那唯一一个真 picker 在守卫之后**仍然**进入 picker 分支并解析到被引用对象。⭐ 这正是 PR objectstack-ai#16347 修好的那条 B1 **假拒绝**(表单字段的 `publicPicker.filter` 被拿父表单的对象去判),⛔ 不得被这次加固重新打开。 > 4. **若只加注释**:注释必须说清三件 —— ① 按属性名匹配、② 今天只有一处声明、③ 再出现同名键时会**静默**进入本分支。⛔ 不许只写"注意"。 > 5. **项 (2) 若要动**:先给一次读数 —— 今天有没有真实的 dotted picker path 被 `filter-preset-comparand` 报过?⛔ 两个方向都要写。⚠️ 且 ⛔ **不要顺手改成"在这条规则里多报一条路径错误"** —— 那会让 `filter-preset-comparand` 开始判它名字之外的东西,正是项 (1) 担心的范围蔓延。 > 6. **objectstack-ai#16340 ⛔ 不合并进来**(同为漏报,机制不同)。 Disposition, item by item: 1. Item (1) only. Item (2) is untouched here. 2. TWO of the three exits are verbatim untouched: the `picker.object` override (`if (override) return override;`) and both `undefined` legs of the `reference` resolution (`if (!formObject) return undefined;` and the `verdict?.kind === 'ok' ? … : undefined` tail). The THIRD — `!formField` returning `undefined` — is DELETED by this diff, and deleting it IS the fix: outside the declared position that line was the silent exit this card is about, while inside the declared position it is unreachable by construction, since the guard holding means `formField` is truthy. So the behaviour P3's QUIET pin holds did not move, and nothing but the entry condition changed semantically. (An earlier revision of this body and of the changeset said "all three byte-for-byte unchanged"; that was wrong against the diff and is corrected here and in `.changeset/16403-picker-reader-position-guard.md`.) 3. Measured above, not asserted. Both B1 tests pass on the fixed tree and pass under the ablation. 4. The guard carries the comment as well: the docblock and the in-branch note state the name-matching shape, the single declaration, and the silent inheritance. 5. Item (2) is not addressed here, so no reading is owed and none is claimed. `filter-preset-comparand` still reports only what its own name covers. 6. Out of scope: objectstack-ai#16340 is not merged in and is not touched. ### Noted, not filed - `check:cross-package-test-inputs` reds on a checkout where `packages/spec/dist/` has been built, at `origin/main`, with this diff absent — and stays green where it has not. Two-leg control taken at BASE `97233b90c` in a detached control worktree: without `packages/spec/dist/` present, exit 0 (`OK: 29 package(s) read outside themselves, all declared …`); with the identical `dist` tree hard-linked in and nothing else changed, exit 1 with the same FAIL text this branch produced (`@objectstack/cli descends a directory tree from packages/spec/dist/`). `lint.yml`'s `lint` job installs but never builds, so the walked-root leg of that gate cannot fire in CI at all. Reported to the dispatching seat for triage; it is outside this card's file surface and is not touched here. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: claude[bot] <[email protected]>
Fixes #16106
Clause-②: yes
Ruling executed, not re-opened: comment 5557019138 on the card (director seat, summon #16, batch #52 item 5; maintainer verbatim 「其他同意」 adopting recommendation 1′). Clause-②: yes — constructed at
CONTRACT_REVIEW_TIER, no quota exemption taken. Authored by a Claude Code dev seat under PM dispatch, sessionsession_01T6HeZvT9wdSJD1ZxJb5Eno(theClaim:on the card, comment 5559619826, is the identity record). The standaloneClause-②: yesline above is the durable declaration: it keeps the clause-② signal readable byCheck Changesetafter the review seat clears theneeds:contract-reviewcarrier.needs:contract-reviewis hung on this PR and on #16106 together. This PR stays a draft until an at-tier review clears it — it is not flipped ready and not enqueued by its author; that is the review's outcome to decide. The at-tier review (card comment 5560322399) returned FAIL on one blocking finding, B1; the repair commit below closes it and a narrow re-review of that delta is pending.What changed
packages/lint/src/validate-preset-comparands.tsgains arm 2 — the FIELD-TYPED refusal (ruling item 1):date/datetimefield, one of the 13 declared preset names is refused in EVERY comparand position: bare (implicit equality),$eq/$ne,$in/$nin(every member of the list), the view-rule spellingsequals/not_equals/in/not_in(with every aliasnormalizeFilterOperatorfolds —eq,ne,neq,notIn, …), and the triple spellings=/!=/in/nin(with every aliascanonicalAstOperatorfolds —==,equals,not_in, …).bareDateRangePresetComparandMessage(preset, operator)— one condition, one wording (the{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 convention). The implicit-equality position is reported under the operator it lowers to,$eq, at the field's own path (…filter.close_date, no operator segment).object/objectName(datasets, a summary field's child object,optionsFrom, page data sources,record:related_listproperties,element:record_picker, time-relative triggers);dataset→ that dataset'sobject(dashboard widgets A dashboard widget's OWNfilterkeys andoptions.sortByare not resolved at author time — validate/build exit 0, widget renders empty #14148, reports, report blocks);data.objecton theobjectprovider (views and list views No field reference on a list view is checked at author time — columns, filter, sort, grouping and every binding block accept a misspelt field name through bothos validateandos build#14107, any other provider unjudged);config.objectName/config.object(flow CRUD nodes, a templated value skipped);dataSource.objectthenproperties.object/properties.objectName(page components); a form field'spublicPicker→picker.object, else the field'sreferenceresolved on the view's object, else unjudged (repair commit, below); and underobjects, the object itself (its list views, tabs,relatedListFilter). A bare or dotted field key then resolves throughresolveFieldPath, and only an author-declareddate/datetimeleaf is judged.23443ce169(PM-authorized, comment-only):authoring-rules.ts's registration comment now describes both arms — arm 1 needs no resolution context, arm 2 readsobjects/datasetswhen present and stays silent where absent, soRUNTIME_NEEDS_FULL_SNAPSHOTstill does not apply;date-range-presets.ts:101's TSDoc names both moments the shared wording is reported at. The message text itself is unchanged.e05cb18796(no source change): the@objectstack/specchangeset raised frompatchtominor— see Changesets below for why.3b6ba7b409(review finding B1, card comment 5560322399):views[].sections[].fields[].publicPicker.filteris a static pre-filter the public-lookup route runs on the REFERENCED object (picker.object, else the field definition'sreference), never on the form's own object.boundObjectOfdid not recognise apublicPickerancestor and fell through to the view'sdata.object— the parent form object — so a parentdatecolumn and a referencedselectcolumn sharing a name (with a preset-named option value) produced a FALSE refusal on a legitimate publish. The picker is now a claiming reader:picker.objectwhen written, else the enclosing form field'sfieldresolved on the view's object throughresolveFieldPathand itsmeta.reference, elseundefined— unjudged, never the parent. Mechanically:ancestorsOfnow records the property name each ancestor was reached under, and the reader loop becamebindAncestors(collection, chain, from, …)so the picker branch can bind the enclosing form's object by recursing outward from the section. No other binding path changed.element:record_pickeris not the same class — it declares its ownobject, which the direct-objectreader already binds.Deliberately unjudged — a missed catch is the only failure direction this arm may have (the at-tier review found exactly one position where that did not hold, B1 above, closed by the repair commit), and the engine door still refuses each of these at query time with the registry's field map in hand: a position no ancestor binds; an unknown dataset or object, an object with no field map, a non-
objectview provider; apublicPickerwhose field the form object does not declare or whose field carries noreference; a registry-injected column (created_at/updated_at— the graph carries their names, not their types; filed as #16340); atimefield (the ruling namesdate/datetime); a field the object does not declare (the*-filter-field-unknownrules' finding); and every select / text column —stage: 'this_quarter'stays a working filter, which is the whole reason the schema door cannot do this blind.Ruling item 2:
packages/spec/src/data/filter.zod.tsis untouched (read only); the field-agnostic door keeps its ordering-only boundary and its header's reason.Item 1's conditional ("and, if stack-level publish validation resolves field types,
defineStacktoo"): measured no —stack.zod.tscarries zero filter-walking code (grep -c 'FilterCondition|walkFilter|runtimeFilter' packages/spec/src/stack.zod.ts→0), sodefineStackkeeps accepting these positions and the refusal lives atobjectstack lintand at the runtime publish gate (validatePresetComparandsis registeredCLI_AND_RUNTIME; the per-write snapshot carriesobjectsanddatasets, so adashboard/view/object/page/flowwrite is judged with the field types present).Item 4: with
objectsin the stack,filter-preset-comparandnow fires on a filterdefineStackaccepts — the rule id becomes countable as live coverage on adefineStackapp for the first time.First premise (ruling item 3) — measured before any edit
Written onto the card: #16106 (comment). Real engine, two real drivers (
@objectstack/driver-memory,@objectstack/driver-sqlite-wasm), declareddate+datetimefields, 30 rows seeded: bare,$eq,$in,$ne,$ninare ALL refused at the engine door withINVALID_FILTER/ 400 before any driver read, identically on both drivers; positive controls pass ({ $gte: '{30_days_ago}' }→ 20 rows, a bare ISO day → 1,$inof ISO days → 2);stage: 'this_quarter'on a select column → 10 rows. The at-tier review reproduced this independently.Correction to the card, stated plainly: its sentence "the surface answers 200 with zero rows and no diagnostic" was inherited from the pre-door #8690 measurement and is not true today for the residue positions on a declared
date/datetimefield — the runtime answers a loud 400 on first render. The gap was purely at authoring time (objectstack lintpassed,defineStackaccepted), which is what this PR shuts. The ruling stands either way.Changesets
@objectstack/lint: minor (new refusal arm) — the package whose public surface this PR widens; the review confirmed the level.@objectstack/spec: minor — gate-required, not semantically earned. The spec diff is a TSDoc comment only (no schema, no export, no runtime behaviour). It owes a changeset at all because the TSDoc publishes — measured, not assumed: after the edit, 2 non-mapdistfiles carry the new text (dist/date-range-presets-BQj1X-Id.d.tsand.d.mts, the chunkdist/data/index.d.tsre-exports from;distis inpackage.jsonfiles), 0 non-map files still carry the old phrase; baseline: before the edit the old phrase sat in exactly those two files, and a known.describe()string (Presentation-scope filter (runtimeFilter)) reached 18 non-map files. Semantically that is apatch. It is gradedminorbecause theCheck Changesetgate reads clause-② at PR level, not per package, and requires at leastminoron every package the PR grows (maintainer ruling 2026-09-04, batch [WIP] Add query enhancements and advanced validation features #35, on finding(changeset): two independent contract reviews read the repo's own history to opposite bumps for "add an exported symbol to a published index" #15294); the surface widening that earns clause-② lives in@objectstack/lint. This reading is recorded here rather than in the changeset prose, which ships to consumers as CHANGELOG text.check:generatedafter the rebuild: "All 15 generated artifacts are up to date" (no docs / api-surface artifact moved).Verification — final head
3b6ba7b409(=34949aa1b0+ merge oforigin/mainat6c546ab9d0=96ff8f59d7, +23443ce169comments, +e05cb18796changeset level, + the B1 repair)pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-preset-comparands.test.ts—Tests 17 passed (17)(8 existing + 7 arm-2 + the 2 B1 pins: the false-refusal case QUIET, the referenced-object-is-a-date POSITIVE CONTROL refused).pnpm --filter @objectstack/lint teston3b6ba7b409(lint'sdistbuilt) —Test Files 99 passed (99)/Tests 3389 passed (3389);pnpm --filter @objectstack/lint typecheck—tsc --noEmitclean,check:test-typecheck: OK.pnpm --filter @objectstack/spec exec vitest run src/data/date-range-presets.test.ts—Tests 6 passed (6);pnpm --filter @objectstack/spec build+check:generatedgreen on the edited TSDoc. Spec's full test / typecheck suites are declared to CI (comment-only spec diff, compiled by the dts build).96ff8f59d7; no consumer-visible change since):pnpm --filter @objectstack/metadata-protocol test—Test Files 166 passed | 2 skipped (168)/Tests 2414 passed | 10 skipped (2424)(pre-existing skips).34949aa1b0, re-run by the review):EQUALITY_DOLLAR_OPSmutated to accept nothing → 3 failed / 12 passed; restore proven by blob1f9c5f48fef72bcd457390fa036c751d804243b8and emptygit diff HEAD. The review added two legs in the false-refusal direction (FIELD_TYPED_TEMPORAL_TYPES + 'select'→ 1 failed / 14 passed;UNBOUND = () => true→ 2 failed / 13 passed), both as predicted.3b6ba7b409, both directions): leg P1 — the picker reader disabled (key === 'ablated_' + PUBLIC_PICKER_KEY; on-disk proof anchor 1 / remaining 0 / injected 1, blobcd3fbe6a6e08…) → vitest exit 1, red on exactly the QUIET pin (the false refusal returns); leg P2 — every picker position forced unjudged (const override = undefined …; return override;, blob553db859ecfe…) → exit 1, red on exactly the POSITIVE CONTROL pin (blanket silence). Each restored withgit checkout HEAD -- …: on-disk blob = HEAD blob23a8afdb33349274645cf2f54a36d364e31052e2,git diff HEADempty, porcelain empty. A discarded reading, stated: the first P2 attempt used a replacement that CONTAINED its anchor as a substring, so the on-disk proof's "anchor remaining" count read 1 although the blob had changed (75a7be68…) — the proof refused it as not-landed and the leg was re-run with a non-superset replacement; nothing was read from the discarded attempt.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon3b6ba7b409derived 74 families (identical toe05cb18796's); all 74 run on that head: 72 green, 2PREREQUISITE NOT MET(exit 3:check:dual-build-cjs-loads,check:type-check-debt— both read the whole-repo build only CI performs; NOT MEASURED locally, not red).--ranreconciliation: 74 derived / 74 run / 0 unrun.Check Changesetone05cb18796(CI job 101510513543): fired and passed; offline on the same head with this body: reads both the carrier and theClause-②: yesline, passes.turbo ls --affectedlists 54 packages (everything downstream of@objectstack/lintand@objectstack/spec). Run locally:@objectstack/lint,@objectstack/metadata-protocol, the spec file's own test.packages/cli(a 56-cache-miss closure build), spec's full suites and the rest are declared to CI.Notes for the reviewer
temporal-comparand-door.ts:186— read, not measured); arm 2 does judgeaccount.created_on-style paths through the graph's lookup hops, the one place such a path gets a verdict at all (review advisory A1: the shared message's "refuses the query at the engine" clause is not literally true there;{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240's one-wording rule wins).created_at/updated_at) — the object graph carries their names, not their types #16340 is filed for the injected-column blind spot (created_at/updated_atmeasured refused at the engine, invisible to the lint graph). Not addressed here.