Skip to content

fix(core, driver-memory): one reading of the dateRange array arm — an ARITY case in the conformance kit, and the cube face stops dropping the window - #17694

Merged
os-musk merged 5 commits into
mainfrom
claude/issue-17596-date-range-arity
Sep 11, 2026
Merged

fix(core, driver-memory): one reading of the dateRange array arm — an ARITY case in the conformance kit, and the cube face stops dropping the window#17694
os-musk merged 5 commits into
mainfrom
claude/issue-17596-date-range-arity

Conversation

@os-musk

@os-musk os-musk commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17596

Clause-②: yes

Two halves, both in scope per the triage ruling on the card (5628469889), which named the #5499 escalation for driver-memory's window-construction seam — and only that seam. generateSql() (#17301) is untouched; packages/spec (#17598) is untouched.

Half 2 first: the probe, because the card filed a SOURCE READING

The card said so in its own words, so the first act was driving the real face rather than reading it. MemoryAnalyticsService.query, four rows spanning 2020…2099, one authored document, on 49cd71548:

dateRange rows selected pipeline emitted
['2026-01-01', '2026-01-01'] (control) b_target — the one day $match + $group
['2026-01-01'] all four, 2020 and 2099 included byte-identical to a query with no dateRange at all
[] all four same
['2026-01-01', '2026-01-31', '2026-02-01'] all four same
[null, null] none $gte: 'null', which no instant sorts inside
no dateRange (control) all four $group only

The reading is confirmed, and the last column is the sharpest statement of it: for three shapes the face produced exactly the pipeline it produces when the caller asked for no time window at all. Nothing downstream — not a status, not a field, not the dump — could tell a silently widened dashboard from a correct one. That is the "plot all of history" shape #3650 was filed about, on the arm #16322 did not repair.

Half 1: the kit's ARITY case

analyticsDateRangeConformanceFindings had exactly one array case — the two-element window — so the arity was governed nowhere. It now exports ANALYTICS_DATE_RANGE_NOT_A_WINDOW (four shapes: one element, empty, three elements, two null bounds) and holds every registered face to the rule PR #17593 already landed on the service-analytics faces: refused with ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400. No second rule was invented.

  • ⛔ No existing case was weakened. The two-element window case is untouched and is this case's control — "refuse every array" cannot pass.
  • The envelope judgement is now one closure shared by the string arm and the array arm, so the two cannot drift into two envelopes for one condition.
  • AnalyticsDateRangeFace.lower takes string | readonly unknown[]. [] and [null, null] are shapes a real caller reaches a face with (POST /analytics/dataset/query types its selection from AnalyticsQuery and never Zod-parses it). Existing runners need no change: the declaration is a method, so its parameter is bivariant.

Order of operations — the case was made RED before the fix existed

Zone 3's order, followed, because adding the case and the fix in one motion leaves nobody able to tell whether the case would ever have caught anything.

  1. Kit case alone, at 1dd11a1bdadriver-memory conformance: 8 findings, all from the new case, including refused ["2026-01-01"] with code undefined … (no window in the memory pipeline dump: … ) and raised 2 different envelopes for one condition.
  2. Same commit, service-analytics conformance + its own arity pin: 30 passed. The rule already existed there, which is the evidence that the case asserts it rather than a second one.
  3. Then the driver fix, at 68a558e52f.

The driver fix

The array arm is judged at the discriminator and either yields two bounds or throws the shared ADR-0112 envelope, built from @objectstack/core's one constructor so the code + status pair keeps a single origin. if (range.length === 2) — the line that dropped the window — is gone, and ResolvedDateRange.bounds is now a tuple, so it cannot come back as a silent drop. The message names what arrived, why it is not a window, the two-element contract and the single-day spelling to write instead.

Verification

Every heavy run went through scripts/pm/os-verify-lock.sh (slot issue-17596); verdict lines read, never a bare $?.

Tests, on the final head:

  • @objectstack/driver-memory full suite: 53 files, 1242 tests, all passing (after the fix; the same suite carried the 8 findings before it).
  • @objectstack/core full suite: 51 files, 1311 tests, all passing.
  • @objectstack/service-analytics conformance + arity pin: 30 passing — the other registered face, unchanged and still green.
  • typecheck on both touched packages: exit 0.
  • New: memory-analytics-date-range-array-arity.test.ts — the ROW-level half the conformance runner cannot see, with its population imported from the kit so a shape added there gains a row-level reading here automatically. Its controls: a two-element window still selects exactly that day with driver-memory / driver-mongodb:裸日期 $lte 上界在 datetime 值上同样丢当天数据(#3777 的非 SQL 驱动对齐) #4042's half-open widening; a preset still resolves; and no dateRange still selects all of history — so "refused" and "dropped" are distinguishable rather than both reading as green.

Ablation — does the new case actually reach the OTHER registered face? service-analytics resolves @objectstack/core through its exports, i.e. dist/, with no vitest alias back to source, so a green there is worthless until proven to run the new bytes. Both legs, from the committed state:

  • Mutation leg: a REAL two-bound window smuggled into the not-a-window population; on-disk marker count 1; @objectstack/core rebuilt; ablation-dist-preflight.mjs @objectstack/core '2044-01-01T00:00:00.000Z' exit 0 (marker present in the built artifact); service-analytics conformance → 3 of 8 red, each naming the new case, one per face.
  • Restore leg: git checkout HEAD -- the kit; restored blob 3b5f89d13a825240e3c9e5a375d6d188bdfc1baa equals the HEAD blob byte for byte; git diff HEAD empty; rebuilt; preflight --absent exit 0 (marker gone from all 12 built files, tree clean); conformance → 8 passing.
  • The script carried trap … EXIT INT TERM with absolute paths throughout. No ablation artifact remains.

The mutation leg also earned its keep: it showed the finding text calling [null, null] an arity problem, which is the wrong half of the contract. Corrected in 7fdb888aee.

Gates. Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (never a hand-written list) and reconciled with --ran: 60 derived, 57 run green, 3 NOT MEASUREDcheck:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt, each exiting 3, PREREQUISITE NOT MET, all three because they read a whole-repo dist/ this worktree does not have. ⛔ Not read as passes. CI builds the closure and owns them. Ratchet-sensitive gates were re-run on the final head after the last commit: nul-bytes, type-check-coverage, cross-package-test-inputs, test-source-alias, published-files, driver-memory-census, driver-conformance, the three changeset gates and comment-mask-adoption — all exit 0.

Lint, narrowed and declared. ① Population: a repo-wide run lints 6634 files, read from eslint's own config via lintFiles(['.']), not estimated. ② This run: 4 files — every lintable path in the diff — 0 errors, 0 warnings, exit 0, file count read from --format json. ③ Invariance: this repo runs one eslint.config.mjs which "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", so this diff cannot move the verdict on any file it does not contain. The narrowing is therefore a measurement, not a skipped run.

Clause-②

Clause-②: yes, by the mechanical floor: the diff adds a new exported symbol to a published package (ANALYTICS_DATE_RANGE_NOT_A_WINDOW from @objectstack/core, re-exported by export *) and widens a published interface's parameter type. needs:contract-review is attached to both carriers.

Changeset

@objectstack/core: minor — it gains a new exported symbol on its index (ANALYTICS_DATE_RANGE_NOT_A_WINDOW), and a purely additive widening of a published package's public surface takes at least minor whatever the commit type says (maintainer ruling 2026-09-04, decision batch #35, on #15294). Verified with a control: the symbol has 0 hits on origin/main where its sibling ANALYTICS_DATE_RANGE_EXPLICIT_WINDOW has 4.

@objectstack/driver-memory: patch — its public surface is byte-unchanged (no new export; explicitDateRangeWindow is module-local, and src/index.ts is not in the diff). Its behaviour does change, from selecting every row to refusing with 400 ANALYTICS_DATE_RANGE_UNRECOGNIZED, and that is a patch because the old behaviour was a defect and never a contract: the spec's own refusal wording already said an explicit window is the two-element array, and the #16322 migration table already told authors to write a single day as two bounds. The changeset carries the author-facing migration: ['2026-01-01'] becomes ['2026-01-01', '2026-01-01'], which selected exactly that day before this change too.

Acceptance notes

  • Two spellings of one rule, held equal by the kit rather than by code. service-analytics's date-range-array-arm.ts and driver-memory's new explicitDateRangeWindow now implement the same arity-and-bounds judgement with their own package-specific sentences. That is the kit's declared architecture (the cases and rules live in packages/core; each face's lowering lives in its own package), and unifying the implementation would have pulled a third package into a card triage measured as two. Noted, not filed. Carrier: the next card that adds or moves an analytics face — it will find both spellings through the kit's case.
  • The face census holds at five. Grepped for other array-arm readers: driver-sql/src/sql-driver.ts still has no dateRange code (comments only), and rest/src/analytics-selection-door.ts and runtime/src/domains/analytics.ts classify the refusal rather than lowering a window. No sixth face, so no further package is owed the case.
  • driver-memory's conformance runner used to throw a bare "no window in the memory pipeline dump" for a face that emitted no predicate at all. Since the kit now quotes that text whenever a thrown thing carries no ADR-0112 code, it was reworded to name the consequence — every row selected — rather than the harness's disappointment.

Generated by Claude Code

The shared conformance kit had exactly one array-arm case — a two-element
window — so the arity itself was governed nowhere and each face was free to
invent a reading for `['2026-01-01']`, `[]`, `[a, b, c]` and `[null, null]`.

Adds `ANALYTICS_DATE_RANGE_NOT_A_WINDOW` and the case that holds every
REGISTERED face to the rule PR #17593 already landed on the service-analytics
faces: a non-two-bound array is refused with the ADR-0112
ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400 envelope. No existing case is
weakened — the two-element window case is this one's control.

Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM
Co-authored-by: Claude <noreply@anthropic.com>
…window

MEASURED end to end on `49cd71548`, four rows spanning 2020…2099: the cube
face emitted NO time predicate at all for `['2026-01-01']`, `[]` and
`['2026-01-01','2026-01-31','2026-02-01']` — a pipeline byte-identical to one
with no `dateRange` — so the query read ALL of history, and `[null, null]`
compared against the string `'null'` and selected nothing.

The array arm is now judged at the discriminator and yields two bounds or
throws the shared ADR-0112 ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400 envelope,
which is the rule the kit's new ARITY case holds every registered face to. The
`if (range.length === 2)` guard — the line that dropped the window — is gone,
and `ResolvedDateRange.bounds` is a tuple so it cannot come back.

Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM
Co-authored-by: Claude <noreply@anthropic.com>
Two bounds is necessary, not sufficient: calling `[null, null]` an arity
problem sends the next reader to the wrong line. Surfaced by this change's own
ablation, where a real window smuggled into the not-a-window population was
reported as "every other arity".

Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/driver-memory, touching 6 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17/17-4.mdx (via ANALYTICS_DATE_RANGE_UNRECOGNIZED (literal, a string literal in analyticsDateRangeConformanceFindings))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 88a933088e93067b4df4b20380ab9a1ceed2ed17packageMentionDocs.

Which tree this was computed on

This run read content/docs from 475c1537ab7dfaec0fd29d6b99a133ed7d8310aa — the merge of head 4ab57672f6fce3145a1e1248cec8c228b42a943f into base 88a933088e93067b4df4b20380ab9a1ceed2ed17, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 475c1537ab7dfaec0fd29d6b99a133ed7d8310aa && git checkout 475c1537ab7dfaec0fd29d6b99a133ed7d8310aa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 88a933088e93067b4df4b20380ab9a1ceed2ed17 4ab57672f6fce3145a1e1248cec8c228b42a943f && git checkout -B drift-repro 88a933088e93067b4df4b20380ab9a1ceed2ed17 && git merge --no-ff 4ab57672f6fce3145a1e1248cec8c228b42a943f

node scripts/docs-audit/affected-docs.mjs --json 88a933088e93067b4df4b20380ab9a1ceed2ed17

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 88a933088e93067b4df4b20380ab9a1ceed2ed17 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 11, 2026
A purely additive widening of a published package's public surface takes at
least `minor` (maintainer ruling 2026-09-04, decision batch #35, on #15294):
`ANALYTICS_DATE_RANGE_NOT_A_WINDOW` is absent on `origin/main`, exported from
`analytics-date-range-conformance.ts` and re-exported by the package entry.

`@objectstack/driver-memory` stays `patch` and the changeset now says why: its
public surface is byte-unchanged, and the behaviour it stops producing was a
defect the contract never admitted.

Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM
Co-authored-by: Claude <noreply@anthropic.com>

os-musk commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Head reviewed: 4ab57672f6. Card #17596 · domain:engine execution PM seat · R1 · 2026-09-11.
Tier: 默认判断档自审加门禁 (SKILL.md:641), ⛔ outside the downgrade fuse. ⛔ Every reading below was taken by this seat from the tree, ⛔ none quoted from the round's report.

① Derived judgments

# change judgment
1 new export ANALYTICS_DATE_RANGE_NOT_A_WINDOW on @objectstack/core Clause-②: yes is correct, on the mechanical floor. Verified: absent on origin/main; exported at analytics-date-range-conformance.ts:147; re-exported from the package entry by index.ts:92 (export *). 「新导出符号…恒 yes」.
2 the kit's new ARITY case asserts the existing ADR-0112 rule (ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400) correct — ⛔ not a second rule invented. That code already exists (analytics-date-range.ts:226, and the kit already compares against it at :197). This is the order's Zone 1.7.
3 driver-memory's face refuses at the discriminator; the if (range.length === 2) guard is gone; ResolvedDateRange.bounds becomes a tuple behaviour change; published surface byte-unchanged (no new export, no new accepted key). The tuple is what makes a silent drop unrepresentable rather than merely unlikely.
4 red-before proof — the kit case alone produced 8 findings on driver-memory and 30 passes on service-analytics, before the driver fix existed the case is provably not vacuous, and it is the order's Zone 3 sequencing followed exactly. ⭐ Without this the case and its fix would have landed together and nobody could tell whether the case would ever catch anything.
5 scope held. packages/spec untouched (#17598 in the decision box) and generateSql() untouched (#17301 still on hold) — verified from the changed-file list, 5 files in packages/core + packages/drivers/driver-memory only. Two packages, one lane.

② Semver grading

@objectstack/core: minor ✅ — the mechanical floor above requires at least minor; the level was patch at head 7fdb888aee and Check Changeset correctly refused it. ⭐ That red was this seat's ruling to answer, and the answer was path 1 (raise the level), ⛔ not path 2 (walk back the declaration).

@objectstack/driver-memory: patch ⇒ ✅ judged sound, and its load-bearing premise verified by this seat: the changeset argues the refused shapes 「had no correct answer to lose」 because the contract never admitted them. Measured — packages/spec/src/data/analytics.zod.ts:285 reads verbatim 「an explicit window is the two-element array [start, end] of ISO dates」. ⇒ a release that stops answering a shape the contract never admitted is a fix, not a feature.
⚠️ Bound on that verification: this seat checked the spec-wording limb only. The changeset also cites a #16322 migration table telling authors to write a single day as two bounds — ⛔ that limb was not re-measured here, and the grading does not depend on it.

③ Boundary flags

The round's only open question — --pair 17694 exit 4 — is answered, and the fault was the ORDER's. The declaration limb reads the card's claim comment, and this seat's order had put the key inline inside item 8 rather than on a line of its own, so the card carried no judgement at all. ⇒ this seat wrote the line-anchored Clause-②: yes with its basis; --pair 17694 now exit 0 on this head. ⭐ The round was right to refuse to fill it in on the claiming seat's behalf — 「the declaration IS the judgement」.
And chasing it found worse: this seat's other dispatches carried a boilerplate bullet whose yes|no placeholder the reader parses as a declared yes, so --pair 17689 had passed on an instruction rather than a judgement. Corroborated onto the existing card #17098 (5636056726); ⛔ no duplicate filed.

Three out_of_scope_findings, all accepted as noted-not-filed: the two package-local spellings of one arity rule (the kit's declared architecture — unifying them would pull in a third package); the platform-wide face census holding at five, measured (driver-sql carries no dateRange code; rest and runtime classify rather than lower) ⇒ ⛔ no sixth face is owed the case; and the runner's bare error text reworded in place, inside half 2's own package.

Independence pair

Implemented-by: claude/issue-17596-date-range-arity
Reviewed-by: session_01RuoNSXUbBoWHkNS4AknTrM

⚠️ Declared: NOT an independent review. The executor was a mode:subagent dev of the reviewing session — it records its branch because a subagent has no session of its own, so the pair does ⛔ not mechanically read as 「两者同 session」, and letting that stand as independence would be dishonest. ⭐ This is the in-seat default-tier self-review plus gates this lane runs by design (SKILL.md:641; #17294), ⛔ not a 达档 ruling.

Verdict

✅ PASS

Landing pre-checks, all three by this seat: ① this comment · ② --pair 17694 exit 0 · ③ 33 distinct check names, 0 non-green (latest run per name; ⛔ not the required subset).
⚠️ The head moved 7fdb888aee4ab57672f6 after the first review pass; ⭐ the delta is one file, the changeset, +14/−1 — verified by git diff --stat, so nothing else re-entered review.

⇒ Both carriers stripped in this same stroke.

domain:engine 执行 PM 席 · R1 · session_01RuoNSXUbBoWHkNS4AknTrM · 2026-09-11


Generated by Claude Code

@os-musk
os-musk marked this pull request as ready for review September 11, 2026 15:02
@os-musk
os-musk enabled auto-merge September 11, 2026 15:02
@os-musk
os-musk added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit e7ff9c2 Sep 11, 2026
50 checks passed
@os-musk
os-musk deleted the claude/issue-17596-date-range-arity branch September 11, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants