Skip to content

Commit f9e871a

Browse files
skills(objectstack-query): teach the now-anchored comparand { $now: true, addDays } (#26407)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8DnAiPBrJBxA6FJG4ChPE3h)_ Fixes #26301 Clause-②: no Metadata-protocol: none Skills-docs: updated here ## 维护者速读 - **改了什么**:`skills/objectstack-query/rules/filters.md` 在 Field References 之后新增一节「Now-Anchored Comparands」:`{ $now: true, addDays: OFFSET }` 的形状、唯一示例(按每条记录自己的宽限天数判逾期,偏移列存负数天)、哪里接受(六个标量比较、date/datetime 字段)、哪些被拒(无 `addDays`、小数或字串偏移、多余键、非日期目标、`$in`/`$nin`/`$between` 位置,SQL 下推拒点路径或非数值偏移列),并点明不必用存储公式字段加钩子。`SKILL.md` 的规则清单与「Cross-field comparisons」各加一句指向它(在余量内,不抬上限)。eval 文件加第 6 题。两行 token 上限按实测抬高,裁决原文写在行旁。 - **为什么改**:该比较数自 18.0 起已在过滤语法里,但技能只分别教了 `$field` 与日期宏;AI 作者要「按每条记录自己的宽限天数判逾期」时会去建存储公式字段再写钩子。 - **风险与回滚**:只改发布技能包的文字与一个 eval 夹具,不改运行时;回滚即 revert 本 PR(两行上限随之回落)。 - **先看**:① `skills/objectstack-query/rules/filters.md` 新节(接受/拒绝清单对照 `packages/spec/src/data/filter.zod.ts` 的 `NowAnchorSchema` 与 `filter.test.ts` 的 #25808 用例);② `scripts/check-skills-token-ratchet.mjs` 两行上限及注释引的裁决;③ `evals/filters-pagination-search.json` 第 6 题。 - **两分钟验证**:`node scripts/check-skills-token-ratchet.mjs` → 末行 `81 authored bundle file(s) within their ceilings`,其中 `rules/filters.md is 2482 tokens (ceiling 2482; headroom 0)`、`filters-pagination-search.json is 1175 tokens (ceiling 1175; headroom 0)`。 - **需要你做的**:Tier H(`skills/**`),请 approve 或直接 Merge when ready。 ## What changes Card #26301, the audit's row Q1 (the comparand `{ $now: true, addDays: FIELD }`, in the filter grammar since the 18.0 floor, taught nowhere in `skills/objectstack-query/**`): 1. **`rules/filters.md` — one subsection, "Now-Anchored Comparands"**, placed directly under Field References and before Relation Filters. Why there and not just before Date Filtering Patterns: the anchor is legal exactly where a `{ $field }` reference is and borrows its `addDays` grammar, so the author who has just read how a comparand can be another column meets its now-anchored twin on the next screen; the date-token section teaches placeholders that take a literal N only. The section carries, in the spec's words (`NowAnchorSchema`, `filter.zod.ts`, and the #25808 cases in `filter.test.ts` / `engine-now-anchor.test.ts`): the shape; the one example (overdue by each record's own grace period, `due_at < now + grace_days_offset`, the offset column holding NEGATIVE days); that `addDays` is required and always added (an integer of any sign, `-30` keeping the time of day where `{30_days_ago}` names a calendar day; `{now}` for the bare instant; or a `{ $field }` naming a number field of the same record, NULL counting as zero days); that the filter cannot negate a positive column, so the signed offset is what gets stored; what is refused with `INVALID_FILTER` / 400 (no `addDays`, a fractional or string offset, an extra key — there is no `subDays` — a target that is not `date` / `datetime`, any `$in` / `$nin` / `$between` position; SQL push-down also refuses a dotted or non-number offset field); and that the filter alone answers the question, not a stored formula field plus a hook (the card's "who pays"). 2. **`SKILL.md` — two pointers**, inside the file's headroom (priced 4124 → 4168, ceiling 5547): the rules list names "now-anchored comparands" among what `filters.md` covers, and the Cross-field comparisons entry gains one clause naming the anchor beside `{ $field }`, pointing at the new section. Line 1 (the version stamp PR #25640 owns) is untouched. 3. **Eval 6** in `evals/filters-pagination-search.json`, in the file's existing shape (the earlier five entries are byte-identical): the overdue-by-grace-days prompt, with the schema stated (a `datetime` and a number field holding negative days); expected output is the anchor as the whole comparand of `$lt`, and names the three wrong routes (a stored formula field plus a hook, `{N_days_ago}`, a `{ $field: 'due_at', addDays }` column-to-column comparison). `must_contain` pins `$now: true`, `addDays`, `$field: 'grace_days_offset'`, `$lt`; `must_not_contain` pins `subDays` and `type: 'formula'`. 4. **`references/_index.md`** is generator-owned (`gen:skill-refs`) and already lists `data/filter.zod.ts`; not touched. **Token-ratchet rows** (`scripts/check-skills-token-ratchet.mjs`), per the maintainer's ruling on the seat's decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap **A** (2026-10-27 01:21Z), verbatim and untranslated: 「A:先削重复,余额按实测提高」. Step one, cut duplicates first: measured on `origin/main` `a66c9ff4`, `rules/filters.md` carries no passage that restates another in the file (the operator table's `case-blind` cells and the Case-sensitivity paragraph are a table row and its rule, not a restatement; the Relation Filters limits and the two-step route are stated once each; `$field`'s comparison-position rule appears once here and once in `SKILL.md`, a different file), and the eval file repeats nothing — so there was nothing to cut and no surviving-home list to give. Step two, raise by the measured remainder: `rules/filters.md` 2149 → 2482 (+334 landed on 2148, 1 absorbed by headroom, ceiling +333); `evals/filters-pagination-search.json` 955 → 1175 (+220, zero headroom). Both rows re-pinned at zero headroom; the comments beside them quote the ruling. **Two readings** (lines, and tokens as the ratchet counts them, `ceil(utf8 bytes / 4)`; the `SKILL.md` token figures here are the raw file, the ratchet's priced count excludes the version line): | File | Lines before → after | Tokens before → after | |:--|:--|:--| | `rules/filters.md` | 185 → 211 (+26) | 2148 → 2482 (+334) | | `SKILL.md` | 401 → 403 (+2) | 4129 → 4173 (+44) | | `evals/filters-pagination-search.json` (not .md, reported for the raise) | 55 → 65 (+10) | 955 → 1175 (+220) | | **Whole package** (`SKILL.md` + `references/**/*.md` + `rules/**/*.md`, 5 files) | **1063 → 1091 (+28)** | **9660 → 10038 (+378)** | Nothing removed: the card names no stale line in this package. Sequencing (zero verbs beside these numbers): PR #25640 owns line 1 of every `skills/*/SKILL.md`, untouched here; the sibling audit PRs #26385, #26388, #26390, #26392 and #26393 edit other packages' rows of the ratchet script, and this branch was cut from `a66c9ff4`, after #26392 landed; no open PR touches `skills/objectstack-query/**`. `skip-changeset`: the published skills packages carry no `package.json`; nothing is released from any package. ## Tests - `node scripts/check-skills-token-ratchet.mjs` — exit 0: `81 authored bundle file(s) within their ceilings`, the two raised rows at headroom 0, `SKILL.md` at headroom 1379 (no raise). - `node scripts/check-skills-token-ratchet.mjs --self-test` — exit 0, `79 cases pass`. - `pnpm --filter @objectstack/spec run check:skill-docs`, `check:skill-refs`, `check:skill-examples` — each exit 0 (`check:skill-examples` after building `@objectstack/spec`, `@objectstack/client` and `@objectstack/client-react` under `scripts/pm/os-verify-lock.sh`, slot `issue-26301`, 312 s for the whole closure including `@objectstack/formula` and `@objectstack/lint`); the new code block in `filters.md` is a `where:` fragment and is deliberately not marked `os:check`, like every other fragment in the file. - Eval 6 is a fixture (the repo has no eval runner): its assertions were checked by script against its expected output — every `must_contain` string occurs in it, neither `must_not_contain` string does. - Prose cross-checked against the contract, not paraphrased from memory: `NowAnchorSchema` (`packages/spec/src/data/filter.zod.ts`), its #25808 cases in `filter.test.ts` (the six operators accept it; `$in` / `$nin` / `$between` refuse it; missing, fractional and string offsets and an extra key refuse), `engine-now-anchor.test.ts` (the resolver pins `$now`; a numeric target and a text or undeclared offset column refuse) and the `$now` section of `content/docs/protocol/objectql/query-syntax.mdx` (NULL offset = zero days; a dotted offset is refused on SQL push-down). No ablation: no tested behaviour changed. ## Acceptance notes - `addDays` on a `{ $field }` reference (`{ $field: 'due_date', addDays: { $field: 'grace_days' } }`, in the grammar since 18.0's floor as well) is likewise untaught in Field References; the audit's row Q1 cites that section as teaching `$field` alone and names only the anchor. Observation only, outside this card's "Wanted"; not filed (read-only inference, no public-door probe). - The dispatch order's "`skills/**` inside the package's ceilings" cannot hold at the measured headroom (`filters.md` 1, eval 0); the claim's file surface already names the two ratchet rows under the ruling above. - The PR body's first two lines are the platform's attribution block (the os-dev exception for a harness-written attribution comment); `Fixes #26301` follows immediately. `Skills-docs: updated here` is written because it is true, not because it is owed (`Metadata-protocol: none`). ## Verification Gate list derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the final tree (`a66c9ff4` base, head `13e75c8c`), each run locally with the exit code captured before any pipe; `--ran` reconciliation below. **Reconciliation** (`node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack`, exit 0): `52 derived, 51 run, 1 NOT-MEASURED, 0 UNRUN`. The one NOT MEASURED is `pnpm check:dual-build-cjs-loads`: its prerequisite is a whole-repo build (28 packages without `dist/` on this worktree after the targeted closure); this diff touches no package source, and CI's `Lint & Repo Gates` runs it — a declared narrowing. `check:pm-dispatch-gates` ran detached on the final head: under six minutes of wall (launched after the 51-gate pass ended at 02:32Z, finished 02:38Z), 1901 cases pass (fast tier; 7 slow sections deferred), exit 0. No gate was red on any head. <details><summary>Every derived command with its exit code on 13e75c8 (54 lines: the 52 derived, plus `check:skill-refs` and `check:skill-examples` the card asks for)</summary> - `node scripts/check-ci-filter-parity.mjs` — exit 0 - `node scripts/check-closing-keyword-parity.mjs` — exit 0 - `node scripts/check-closing-keyword-parity.mjs --self-test` — exit 0 - `node scripts/check-comment-mask-corpus.mjs` — exit 0 - `node scripts/check-declaration-mirrors.mjs` — exit 0 - `node scripts/check-declaration-mirrors.mjs --self-test` — exit 0 - `node scripts/check-doc-route-spelling.mjs --advisory` — exit 0 - `node scripts/check-doc-route-spelling.mjs --self-test` — exit 0 - `node scripts/check-dts-emitted.mjs --self-test` — exit 0 - `node scripts/check-scripts-symbol-anchors.mjs` — exit 0 - `node scripts/check-scripts-symbol-anchors.mjs --self-test` — exit 0 - `node scripts/check-self-test-wired.mjs` — exit 0 - `node scripts/check-self-test-wired.mjs --self-test` — exit 0 - `node scripts/check-self-test-workflow-commands.mjs` — exit 0 - `node scripts/check-self-test-workflow-commands.mjs --self-test` — exit 0 - `node scripts/check-skills-token-ratchet.mjs` — exit 0 - `node scripts/check-skills-token-ratchet.mjs --self-test` — exit 0 - `node scripts/check-whole-set-label-write.mjs` — exit 0 - `node scripts/check-whole-set-label-write.mjs --self-test` — exit 0 - `node scripts/ci/assert-turbo-remote-cache.mjs --self-test` — exit 0 - `node scripts/ci/prune-turbo-cache.mjs --self-test` — exit 0 - `node scripts/pm/bare-root-worklist.mjs --self-test` — exit 0 - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` — exit 0 - `pnpm --filter @objectstack/spec run check:skill-docs` — exit 0 - `pnpm check:agent-test-spelling` — exit 0 - `pnpm check:bash32-floor` — exit 0 - `pnpm check:cli-command-ids` — exit 0 - `pnpm check:corpus-claim-drift` — exit 0 - `pnpm check:cross-package-test-inputs` — exit 0 - `pnpm check:doc-authoring` — exit 0 - `pnpm check:driver-memory-census` — exit 0 - `pnpm check:dts-closure` — exit 0 - `pnpm check:dual-build-cjs-loads` — NOT MEASURED (exit 3, PREREQUISITE NOT MET) - `pnpm check:entry-guard` — exit 0 - `pnpm check:gitlink-declared` — exit 0 - `pnpm check:lean-entry-closure` — exit 0 - `pnpm check:merge-driver` — exit 0 - `pnpm check:nul-bytes` — exit 0 - `pnpm check:parse-guard` — exit 0 - `pnpm check:pm-governed-merges` — exit 0 - `pnpm check:pm-skill-id-lint` — exit 0 - `pnpm check:pnpm-filter-targets` — exit 0 - `pnpm check:pnpm-flag-spellings` — exit 0 - `pnpm check:ratchet-remedy-authority` — exit 0 - `pnpm check:refd-timer-probe` — exit 0 - `pnpm check:role-word` — exit 0 - `pnpm check:skill-compatibility` — exit 0 - `pnpm check:skill-frame-sync` — exit 0 - `pnpm check:skill-identifier-liveness` — exit 0 - `pnpm check:sourcemap-no-sources-content` — exit 0 - `pnpm check:watch-hint-literal` — exit 0 - `pnpm check:pm-dispatch-gates` — exit 0 - `pnpm --filter @objectstack/spec run check:skill-refs` — exit 0 - `pnpm --filter @objectstack/spec run check:skill-examples` — exit 0 </details> <details><summary>Token ratchet on 13e75c8</summary> - `skills/objectstack-query/SKILL.md is 4168 tokens (ceiling 5547; headroom 1379). Its release-stamp line (version: "18.1.0", 20 bytes) is written by the version script, not an author, and is not priced.` - `skills/objectstack-query/evals/filters-pagination-search.json is 1175 tokens (ceiling 1175; headroom 0).` - `skills/objectstack-query/rules/filters.md is 2482 tokens (ceiling 2482; headroom 0).` - `81 authored bundle file(s) within their ceilings; 10 generator-owned file(s) measured, not ratcheted.` - `--self-test`: `79 cases pass.` </details> <details><summary>check:skill-examples on 13e75c8</summary> `264 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them` </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbX21W6nMhxhqhmbFEH)_ Co-authored-by: objectstack-fleet[bot] <noreply@anthropic.com>
1 parent 775a000 commit f9e871a

4 files changed

Lines changed: 58 additions & 4 deletions

File tree

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,17 @@ export const CEILINGS = new Map([
628628

629629
// objectstack-query
630630
['skills/objectstack-query/rules/aggregation.md', 2357],
631-
['skills/objectstack-query/rules/filters.md', 2149],
631+
// 2149 -> 2482: the now-anchored comparand `{ $now: true, addDays }` — the
632+
// shape, the one example (overdue by each record's own grace days, the
633+
// offset column holding negative days), where it is accepted (the six scalar
634+
// comparisons on a date / datetime field) and what is refused — which the
635+
// file did not teach, so an author reached for a stored formula field and a
636+
// hook. +334 tokens on a landed 2148, 1 absorbed by headroom, ceiling +333.
637+
// Nothing duplicated in the file to cut first (measured: no passage of the
638+
// file restates another). Maintainer ruling 2026-10-27 on the decision card
639+
// 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A, verbatim and
640+
// untranslated: 「A:先削重复,余额按实测提高」.
641+
['skills/objectstack-query/rules/filters.md', 2482],
632642
['skills/objectstack-query/rules/pagination.md', 1382],
633643

634644
// objectstack-automation
@@ -803,7 +813,13 @@ export const CEILINGS = new Map([
803813
// ceiling +247, pinned AT the measurement.
804814
// ceil($(wc -c < skills/objectstack-platform/evals/config-plugins-ops.json) / 4) == ceil(5831 / 4)
805815
['skills/objectstack-platform/evals/config-plugins-ops.json', 1458],
806-
['skills/objectstack-query/evals/filters-pagination-search.json', 955],
816+
// 955 -> 1175: one regression case for the now-anchored comparand (the
817+
// overdue-by-grace-days prompt) — expected `{ $now: true, addDays: { $field:
818+
// 'grace_days_offset' } }` as the whole comparand of `$lt`, and NO stored
819+
// formula field or `subDays`. +220 tokens, zero headroom to absorb any of
820+
// it, ceiling +220; nothing duplicated in the file to cut first. Same ruling
821+
// as the `rules/filters.md` row above (2026-10-27, tap A: 「A:先削重复,余额按实测提高」).
822+
['skills/objectstack-query/evals/filters-pagination-search.json', 1175],
807823
// 1505 -> 1789 (#26295): two eval prompts — list grouping with `orderBy`, and the copy hole
808824
// + `onSuccess` pair — +284 tokens in the file's shape; fixtures, nothing to delete, the
809825
// whole addition raised and pinned AT the measurement. Maintainer ruling 2026-10-27 (Warren, decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」,

‎skills/objectstack-query/SKILL.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ procedure and the full tombstone register are **objectstack-upgrade**.
8686

8787
## Quick Reference — Detailed Rules
8888

89-
- **[Filters](./rules/filters.md)** — all operators, logical combinations, filtering by a related record, date macros and session tokens
89+
- **[Filters](./rules/filters.md)** — all operators, logical combinations, filtering by a related record, now-anchored comparands, date macros and session tokens
9090
- **[Aggregation](./rules/aggregation.md)** — groupBy, date bucketing, functions, `having`, per-measure `filter`
9191
- **[Pagination](./rules/pagination.md)** — offset vs keyset, best practices, performance
9292

@@ -195,7 +195,9 @@ the two-step route past them: **[filter rules → Relation Filters](./rules/filt
195195
### Cross-field comparisons
196196

197197
`{ $field: '...' }` compares two columns of the same row, in a **comparison**
198-
position only — see **[filter rules → Field References](./rules/filters.md)**.
198+
position only; `{ $now: true, addDays: OFFSET }` is the instant the query runs
199+
at, shifted by whole days a number field of the record holds — see **[filter
200+
rules → Field References / Now-Anchored Comparands](./rules/filters.md)**.
199201

200202
## Sorting
201203

‎skills/objectstack-query/evals/filters-pagination-search.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@
5050
"must_contain": ["context", "isSystem: true", "limit: 1"],
5151
"must_not_contain": []
5252
}
53+
},
54+
{
55+
"id": 6,
56+
"prompt": "Find tasks that are overdue by each record's own grace period: `due_at` is a datetime and `grace_days_offset` is a number field holding the grace period as negative days.",
57+
"expected_output": "`engine.find('task', { where: { due_at: { $lt: { $now: true, addDays: { $field: 'grace_days_offset' } } } } })` — the now-anchored comparand: `$now: true` is the instant the query runs at, `addDays` reads the signed whole-day offset off the same record (`-N` is N days ago). Not a stored formula field plus a hook, not `{N_days_ago}` (a literal N only), not `{ $field: 'due_at', addDays: ... }` (that compares two columns, never now).",
58+
"files": [],
59+
"assertions": {
60+
"must_contain": ["$now: true", "addDays", "$field: 'grace_days_offset'", "$lt"],
61+
"must_not_contain": ["subDays", "type: 'formula'"]
62+
}
5363
}
5464
]
5565
}

‎skills/objectstack-query/rules/filters.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,32 @@ Legal in a **comparison** position only. As an `$in` / `$nin` member or a
5454
`$between` endpoint it is refused at parse — no evaluation path resolves a
5555
reference there.
5656

57+
## Now-Anchored Comparands
58+
59+
> ✅ **Enforced.** `{ $now: true, addDays: OFFSET }` is the instant the query
60+
> runs at, shifted by whole days — legal exactly where a `{ $field }` reference
61+
> is: the whole comparand of `$eq` / `$ne` / `$gt` / `$gte` / `$lt` / `$lte` on
62+
> a `date` or `datetime` field. The engine pins `$now` to ONE instant per query.
63+
64+
```typescript
65+
// ✅ Overdue by each record's own grace period: due_at < now + grace_days_offset,
66+
// the number field holding the grace period as NEGATIVE days
67+
where: {
68+
due_at: { $lt: { $now: true, addDays: { $field: 'grace_days_offset' } } }
69+
}
70+
```
71+
72+
`addDays` is **required** and always ADDED: an integer of any sign (`-30`
73+
is thirty days ago keeping the time of day, where `{30_days_ago}` names a
74+
calendar day; for the bare instant write `{now}`), or a `{ $field }` naming
75+
a **number** field of the same record (NULL counts as zero days). The filter
76+
cannot negate a positive `grace_days` column: store the signed offset. Refused
77+
with `INVALID_FILTER` / 400: an anchor without `addDays`, a fractional or
78+
string offset, any extra key (there is no `subDays`), a target that is not
79+
`date` / `datetime`, and any `$in` / `$nin` / `$between` position; SQL
80+
push-down also refuses a dotted or non-number offset field. The filter alone
81+
answers it — not a stored formula field plus a hook.
82+
5783
## Relation Filters
5884

5985
A condition on a related record's fields beneath a relation field (`lookup`,

0 commit comments

Comments
 (0)