Repository navigation
Refuse the declared relative-date preset vocabulary as a bare temporal comparand at publish time — the ruled C half of #8690, carved out for the spec seat #8793
Description
Activity
Residual carried onto this card from #8690's B half (PR #8808) — plus the constraint that makes it safe
PM session
session_01XeQRiAa7vYRVX5Fog7Zby8. Adding this before the card is claimed so the spec seat inherits a complete brief rather than discovering it mid-implementation.What #8808 shipped, and the gap it leaves
The B half's analytics decline (
NativeSQLStrategy.canHandlerefusing to take the raw-SQL fast path for an uninterpretable temporal comparand) was implemented without the "newStrategyContexthook" the ruling's text named — becauseStrategyContextis declared inpackages/spec/src/contracts/analytics-service.ts:272, which the B-half dispatch forbade and which is this card's package. Instead it classifies on the cube's own declared dimension type (type: 'time'), reached throughctx.getCubeand the strategy's existinglookupMember.The residual: a temporal column filtered without being a declared time dimension is not classified, so it keeps today's behaviour on the raw-SQL path. Strictly smaller than "every raw-SQL query bypasses the door", and it fails in the safe direction — a missed decline degrades to today's behaviour rather than producing a new wrong answer.
The B-half dev's own recommendation, which I am adopting: if that residual precision is wanted, the right shape is a declared hook on
StrategyContext— and it belongs here, with the card already openingpackages/spec, so one PR touches that package instead of two.⛔ The constraint that makes it safe — do not drop this
An optional hook whose absence is silent re-creates the exact defect #8690 closes. A host that forgets to wire it gets no decline, and the raw-SQL bypass returns with no signal. That is the reasoning that disqualified the "declare it locally in
service-analytics" option, and it applies just as hard to a barehook?:on the shared contract.So if this card adds the hook, its absence must be loud rather than silently permissive — and the cube-dimension classification #8808 shipped stays as the floor beneath it, so an unwired host degrades to that coverage rather than to none.
⚠️ One mechanism measured and REJECTED — do not re-propose itI suggested reusing the existing
StrategyContext.coerceTemporalFilterValue?(objectName, fieldName, value)(:412) as a no-new-surface substitute, detecting uninterpretability by whether the value came back unchanged. That is wrong, and the reason is worth recording so nobody re-derives it:- It yields a coerced value, not a declared kind — which is the fact the classifier actually needs.
- Identity cannot separate "uninterpretable" from "interpretable and already in canonical storage form" (an ISO instant, an epoch number legitimately round-trip unchanged).
- It is optional, so it inherits the silent-absence failure above.
- Decisive: its own contract returns the value unchanged on native-timestamp dialects —
StrategyContext's doc says "native-timestamp dialects (Postgres/MySQL) and non-temporal fields → the value is returned UNCHANGED", and thecoerceTemporalcall site repeats it. So an identity proxy would decline every analytics query on Postgres and MySQL while behaving differently on SQLite. A routing gate whose verdict flips with the backing dialect is precisely the "one filter, two answers depending on which driver the deployment runs" shape An unparseable date comparand on a datetime filter is passed through and compares false — HTTP 200, zero rows, no diagnostic — while an unknown{placeholder}is correctly rejected 400 (17.0.0 GA) #8690 exists to remove — it would have reintroduced that card's defect at the routing layer while closing it at the value layer.
Point 4 is the B-half dev's finding, not mine; I raised the mechanism and they measured it out. Recorded here because a future author looking for "a hook that already exists" will find the same one and needs the same answer.
Tier reminder
Unchanged: this card narrows an accept set on a published authoring surface, so
claude-fable-5is mandatory by the ruling's own last line, with the 2026-08-13 quota exemption (「fable 如果用完了,可以用 opus」) as the only downgrade path — and only when fable is measurably unavailable, recorded in the claim comment.
Generated by Claude Code
os-project-manager commented
on Aug 15, 2026 CollaboratorMore actionsClaim: PM loop round 2 (spec seat; batch-3 posture, Bug & v17 first — maintainer 2026-08-15)
Session:session_01Fgvh1iEJfxetei7aNVdtJt
Branch:claude/issue-8793-preset-comparand-refusal
Worktree:objectstack-issue-8793
Domain:domain:spec
File surface (region-level):packages/spec/src/data/filter.zod.ts+ the temporal-comparand region of the filter/publish door,packages/spec/src/contracts/analytics-service.ts(only if the declared StrategyContext hook is taken up — with the loud-absence constraint),packages/lint/**(the lint half the ruling names), tests, changeset; ADR-0087/registry entry if the narrowing requires one (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: claude-fable-5— mandatory tier per the #8690 ruling's own closing line (accept-set narrowing on a published authoring surface); fable measured available this session
Serial constraints cleared: lane in-flight is zero dev-side (PRs #8905/#8914 are in the merge queue, surfacesdata/object.zod.ts+plugin-audit andui/component.zod.ts— both disjoint from this card's filter/contracts/lint surface); shared generated surfaces under the four waiver disciplines (mergemainbefore PR, re-merge after any sibling lands, ⛔ no hand-edited anchors, merge queue + os-regen relay arbitrate); registry hot-file discipline live if an entry is required; premises re-verified onorigin/mainthis round: preset vocabulary declared atui/dashboard.zod.ts:664, zero refusal indata/today,FILTER_TOKEN_UNKNOWNloud path present inpackages/coreRuling being implemented: #8690 maintainer ruling
5299879288(delegated adjudication) — the C half only: refuse the declared relative-date preset vocabulary as a bare temporal comparand at publish time,packages/spec+@objectstack/lint. B half already shipped (PR #8808). ⛔ Scope boundary carried: the empty-string cell stays its own card.
Generated by Claude Code
- added 3 commits that reference this issue
on Aug 15, 2026 os-project-manager commented
on Aug 15, 2026 CollaboratorMore actions{ "issue": 8793, "status": "done", "branch": "claude/issue-8793-preset-comparand-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/8935", "premise_still_valid": true, "summary": "C half implemented per the #8690 ruling: the 13 declared date-range preset names are refused as bare ORDERING comparands at publish time — a superRefine walk on FilterConditionSchema in packages/spec/src/data/filter.zod.ts ($gt/$gte/$lt/$lte + $between endpoints, firing on every embedding carrier incl. DashboardSchema at saveMetaItem), plus a new gating lint rule filter-preset-comparand covering all three authored filter shapes (Mongo objects, view filter rules with alias fold, filter-array triples) on CLI and the runtime publish gate for dashboard/view/object/page/flow. Equality/membership deliberately not judged (picklist-collision false positives; the engine's field-typed door owns those at query time) and only the declared vocabulary, never a superset — the boundary is argued in the PR body from the #6227/#7596/#3574 precedents. DATE_RANGE_PRESETS re-homed to data/date-range-presets.ts (ui re-exports; cycle-free for the filter door) with DATE_RANGE_PRESET_MACRO_WINDOWS so every refusal names the exact macro window that works. ADR-0087 semantic entry filter-preset-ordering-comparand-refused registered (protocol 18, no D2 — the view-filter-rule-value-shaped-by-operator precedent). The optional StrategyContext hook was NOT taken: its consumer is NativeSQLStrategy in service-analytics, which this card's 裁决 forbids touching, and a spec-only declaration nothing reads is declared-but-unenforced surface — C-alone is the sanctioned complete deliverable; residual stays as #8808 shipped it, a follow-up pairing spec + service-analytics could take it up. os-regen relay run for sibling PR #8932's landing (main 2c86fe3): merge committed first, one hand-resolved registry.ts conflict (stacked union), full regen chain, and both sides asserted surviving (my semantic entry, #8932's identity-api-key-schema-retired entry AND its ApiKeySchema deletion). File-surface amendment to the claim: beyond the claimed filter.zod.ts + lint/** + tests + changeset, this PR also touches ui/dashboard.zod.ts (re-export only), data/index.ts, data/date-range-presets.ts (new), migrations/entries/semantic/18.* (new) and migrations/registry.ts (generated block — os-regen relay applies at landing).", "tests": "Union re-run after the final commit at 6354f45 (clean tree): @objectstack/spec test = 405 files / 10711 passed, typecheck clean, check:generated = all 13 artifacts up to date; @objectstack/lint test = 73 files / 2045 passed, tsc --noEmit clean. Ratchet family at 6354f45 on the FULLY BUILT workspace closure (turbo build ./packages/* first — the earlier local re-measure that disagreed with CI was pre-build): check:type-check-coverage OK, --re-measure = 33 entries / 1926 raw errors, none above recorded. CI's red at f0a5202 (lint TEST_DEBT 20→22) was one extensionless test import (TS2835 + 2 downstream TS7006), fixed at source in af7d667, ledger NOT raised — lint test layer now measures 19 vs recorded 20; the 1-error surplus pre-dates this branch (the #6376 class, reported by the gate itself every run — noted, not filed as a duplicate). Root gates all PASS at 6354f45: changeset-gate-self-tests, cross-package-test-inputs (pnpm+node), doc-formula-expressions, merge-driver, objectui-changeset, spec-parsed-alias, type-source-resolution, adr-0087-registration, changeset-no-major, empty-changeset, dev-prereqs, nul-bytes, query-options-erasure. Consumer sweep (downstream consumers of @objectstack/spec, at pre-merge tree f0a5202, those packages unchanged by my diff since): @objectstack/core 838 passed; @objectstack/objectql 3675 passed untouched; app-showcase/app-crm/app-todo `objectstack validate` all exit 0 (pre-existing advisories only). Refusal pins assert zod issue code+path+message (13 presets × 4 ordering ops + $between endpoints + $and/$or/$not + nested-relation paths + DashboardSchema embedding) with discriminating positive controls in the same tests ({30_days_ago}, ISO, Date, $field, equality/membership collisions, undeclared strings, empty string all still accepted). Fixture triage: zero re-spellings needed — every corpus preset sits in a still-legal dashboard position, pinned. dispatch-gates.mjs re-derived on actual changed paths added objectui-changeset/changeset-no-major/empty-changeset beyond the prompt's list; all run. CI convergence at 6354f45 is the PM's read per the 2026-08-10 ruling; gate status at report time: in_progress.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
os-project-manager commented
on Aug 15, 2026 CollaboratorMore actionsACCEPT — PR #8935 (
Fixes #8793), reviewed by the spec seat (sessionsession_01Fgvh1iEJfxetei7aNVfdtJt.replace — sessionsession_01Fgvh1iEJfxetei7aNVdtJt) against the diff, the report, and CI:- Ruling honoured (C half only): the 13 declared preset names refuse as bare ORDERING comparands at both authoring moments — schema
superRefineonFilterConditionSchema(firing through every embedding carrier, dashboard-embedding pin proves the nested path) + gating lint rulefilter-preset-comparandcovering the two shapes no schema parse touches (view rules with alias fold, filter-array triples) at CLI and runtime publish gates. B half untouched (objectql 3675 green, unchanged). Empty-string cell untouched, per the ruling's scope boundary. - The boundary is the load-bearing decision and it is argued, pinned, and correct: equality/membership deliberately unjudged (picklist collisions protected by existing pins; the engine's field-typed door owns those), only the declared 13 — never a superset; refusal messages name the offending value AND the exact working macro window (
DATE_RANGE_PRESET_MACRO_WINDOWSpinned member-by-member against the real macro vocabulary so a prescription can never name a rejected token). - StrategyContext hook correctly NOT taken: the loud-absence constraint requires a consumer-side change in
service-analytics, which the 裁决 forbids from this card — a spec-only declaration nothing reads would be declared-but-unenforced surface. C-alone was the sanctioned complete deliverable; the residual stays as fix(objectql): refuse an uninterpretable temporal filter comparand at the engine door (#8690) #8808's floor. Sound reasoning, recorded for the follow-up shape (spec + service-analytics paired dispatch). - Patch round closed at source: the lint TEST_DEBT +2 was one extensionless test import; fixed, ledger NOT raised (test layer now measures 19 vs recorded 20 — below the ratchet).
- os-regen relay for feat(spec): retire ApiKeySchema — sys_api_key has one declaration, the platform object (#8715, ADR-0049) #8932 executed: merge committed first, one hand-resolved registry conflict as stacked union, full regen chain, both sides asserted surviving (this card's semantic entry + feat(spec): retire ApiKeySchema — sys_api_key has one declaration, the platform object (#8715, ADR-0049) #8932's retired-def entry + the
ApiKeySchemadeletion). - File-surface amendment declared in the report (re-export in
ui/dashboard.zod.ts,data/index.ts, newdata/date-range-presets.ts, migrations entries + registry) — all mechanically entailed by the cycle-free re-homing, accepted. - CI at head
6354f45: all 29 check-run jobs green. Vocabulary re-homing keeps both import paths (check:dual-source-exportsclean). Fixture triage: zero re-spellings (the shippedsystem_overviewdefaultValue: 'last_7_days'keeps validating, pinned).
Marking ready + auto-merge.
migrations/registry.tsis in this diff — if #8371's PR lands a registry entry behind this, that branch runs the relay.
Generated by Claude Code
- Ruling honoured (C half only): the 13 declared preset names refuse as bare ORDERING comparands at both authoring moments — schema
- added a commit that references this issue
on Sep 28, 2026 - added 3 commits that reference this issue
on Oct 7, 2026
Cross-seat transfer from the
domain:metadata/engine-coreseat (PM sessionsession_01XeQRiAa7vYRVX5Fog7Zby8). Filed here rather than kept in-lane because it lands inpackages/spec+@objectstack/lint, andpackages/specroutes to thedomain:specseat regardless of who needs it.Source: #8690, and specifically its maintainer ruling
5299879288(delegated adjudication; delegation 2026-08-15 verbatim 「决策你直接帮我做」, batch confirmed 「同意」). This is not a new proposal — the decision is already made and this card exists only to put the ruled work in the right seat's queue at the right tier.What was ruled
This card is the C half only. The B half (refuse at the ObjectQL engine's filter collection point, plus a
NativeSQLStrategy.canHandledecline via a newStrategyContexthook) is dispatched separately on #8690 and lands inpackages/objectql+service-analytics. They are two refusals at two boundaries, not one refusal split across packages — the ruling lists them as separate deliverables, and the ruling's own ⛔ against "splitting the refusal across packages" applies within each half, not between them.The ruling's closing line: "C narrows an accept set on a published authoring surface ⇒
claude-fable-5tier mandatory for the C half." That is the standing non-discretionary clause, not a suggestion. The only sanctioned downgrade is the maintainer's 2026-08-13 quota exemption (「fable 如果用完了,可以用 opus」) — and only when fable is measurably unavailable, with the tier and reason recorded in the claim comment.The defect this closes
last_7_days/last_30_days/last_90_daysare real, declared preset names in the dashboard schema. The shipped console lowers them to{N_days_ago}/{today}macros before they reach the query API, so the console path is safe. Any other caller that has the preset name in hand — a saved report, an integration, an MCP client, an AI-authored query — sends the name itself.Measured end-to-end on
InMemoryDriverwith a declareddatetimefield, 51 rows seeded / 38 in-window:So the vocabulary is declared in one layer and unrecognised in the next, with no error at the boundary — a
{placeholder}the resolver does not know is rejected loudly with a list of resolvable tokens, while a declared preset name is bound as-is and compares false against every row.C is the half that closes this at authoring time, where an AI-generated dashboard is actually produced, rather than tolerating it at consume time. It does not close the ad-hoc integration/MCP path — that is what B is for.
⛔ Scope boundary carried from the ruling
Measured:
$gte ""binds as'', and because every canonical UTC text sorts at or above it, the predicate is satisfied by every non-null row — 51 of 51. (#8690's body table records 38 for that row; that is a transcription error. Its prose, "silently drops the constraint and returns everything", is the correct account.) Leave that cell exactly as it is.Why this is repair, not a feature
By the mechanical boundary test the accept set moves inward, restoring
declared = enforced: a vocabulary the platform itself declares becomes refused at the layer that cannot interpret it, instead of silently producing an empty result. The loud path already exists one branch over (FILTER_TOKEN_UNKNOWN), so this reuses a settled refusal identity rather than minting one.Suggested verification
Pin that a bare preset name in a temporal filter is refused at publish with a message naming the macro spelling that does work (
{30_days_ago}), and keep a discriminating control in the same test — a legitimate temporal comparand still publishing cleanly. A refusal pin with no positive control cannot show the rule is selective rather than blanket.Origin: carved out of #8690 by the dispatching seat under that card's ruling, so the mandatory tier and the
packages/specownership rule are both honoured rather than folded into an opus engine-core dispatch.