diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index d2220124bc8..f2fcaf435dc 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -628,7 +628,17 @@ export const CEILINGS = new Map([ // objectstack-query ['skills/objectstack-query/rules/aggregation.md', 2357], - ['skills/objectstack-query/rules/filters.md', 2149], + // 2149 -> 2482: the now-anchored comparand `{ $now: true, addDays }` — the + // shape, the one example (overdue by each record's own grace days, the + // offset column holding negative days), where it is accepted (the six scalar + // comparisons on a date / datetime field) and what is refused — which the + // file did not teach, so an author reached for a stored formula field and a + // hook. +334 tokens on a landed 2148, 1 absorbed by headroom, ceiling +333. + // Nothing duplicated in the file to cut first (measured: no passage of the + // file restates another). Maintainer ruling 2026-10-27 on the decision card + // 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A, verbatim and + // untranslated: 「A:先削重复,余额按实测提高」. + ['skills/objectstack-query/rules/filters.md', 2482], ['skills/objectstack-query/rules/pagination.md', 1382], // objectstack-automation @@ -803,7 +813,13 @@ export const CEILINGS = new Map([ // ceiling +247, pinned AT the measurement. // ceil($(wc -c < skills/objectstack-platform/evals/config-plugins-ops.json) / 4) == ceil(5831 / 4) ['skills/objectstack-platform/evals/config-plugins-ops.json', 1458], - ['skills/objectstack-query/evals/filters-pagination-search.json', 955], + // 955 -> 1175: one regression case for the now-anchored comparand (the + // overdue-by-grace-days prompt) — expected `{ $now: true, addDays: { $field: + // 'grace_days_offset' } }` as the whole comparand of `$lt`, and NO stored + // formula field or `subDays`. +220 tokens, zero headroom to absorb any of + // it, ceiling +220; nothing duplicated in the file to cut first. Same ruling + // as the `rules/filters.md` row above (2026-10-27, tap A: 「A:先削重复,余额按实测提高」). + ['skills/objectstack-query/evals/filters-pagination-search.json', 1175], // 1505 -> 1789 (#26295): two eval prompts — list grouping with `orderBy`, and the copy hole // + `onSuccess` pair — +284 tokens in the file's shape; fixtures, nothing to delete, the // whole addition raised and pinned AT the measurement. Maintainer ruling 2026-10-27 (Warren, decision card 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, diff --git a/skills/objectstack-query/SKILL.md b/skills/objectstack-query/SKILL.md index 7f885f77f85..4c1b12c094c 100644 --- a/skills/objectstack-query/SKILL.md +++ b/skills/objectstack-query/SKILL.md @@ -86,7 +86,7 @@ procedure and the full tombstone register are **objectstack-upgrade**. ## Quick Reference — Detailed Rules -- **[Filters](./rules/filters.md)** — all operators, logical combinations, filtering by a related record, date macros and session tokens +- **[Filters](./rules/filters.md)** — all operators, logical combinations, filtering by a related record, now-anchored comparands, date macros and session tokens - **[Aggregation](./rules/aggregation.md)** — groupBy, date bucketing, functions, `having`, per-measure `filter` - **[Pagination](./rules/pagination.md)** — offset vs keyset, best practices, performance @@ -195,7 +195,9 @@ the two-step route past them: **[filter rules → Relation Filters](./rules/filt ### Cross-field comparisons `{ $field: '...' }` compares two columns of the same row, in a **comparison** -position only — see **[filter rules → Field References](./rules/filters.md)**. +position only; `{ $now: true, addDays: OFFSET }` is the instant the query runs +at, shifted by whole days a number field of the record holds — see **[filter +rules → Field References / Now-Anchored Comparands](./rules/filters.md)**. ## Sorting diff --git a/skills/objectstack-query/evals/filters-pagination-search.json b/skills/objectstack-query/evals/filters-pagination-search.json index 13c60ed0ccf..ee40fea8a31 100644 --- a/skills/objectstack-query/evals/filters-pagination-search.json +++ b/skills/objectstack-query/evals/filters-pagination-search.json @@ -50,6 +50,16 @@ "must_contain": ["context", "isSystem: true", "limit: 1"], "must_not_contain": [] } + }, + { + "id": 6, + "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.", + "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).", + "files": [], + "assertions": { + "must_contain": ["$now: true", "addDays", "$field: 'grace_days_offset'", "$lt"], + "must_not_contain": ["subDays", "type: 'formula'"] + } } ] } diff --git a/skills/objectstack-query/rules/filters.md b/skills/objectstack-query/rules/filters.md index aa959ee5998..328aedfc6ae 100644 --- a/skills/objectstack-query/rules/filters.md +++ b/skills/objectstack-query/rules/filters.md @@ -54,6 +54,32 @@ Legal in a **comparison** position only. As an `$in` / `$nin` member or a `$between` endpoint it is refused at parse — no evaluation path resolves a reference there. +## Now-Anchored Comparands + +> ✅ **Enforced.** `{ $now: true, addDays: OFFSET }` is the instant the query +> runs at, shifted by whole days — legal exactly where a `{ $field }` reference +> is: the whole comparand of `$eq` / `$ne` / `$gt` / `$gte` / `$lt` / `$lte` on +> a `date` or `datetime` field. The engine pins `$now` to ONE instant per query. + +```typescript +// ✅ Overdue by each record's own grace period: due_at < now + grace_days_offset, +// the number field holding the grace period as NEGATIVE days +where: { + due_at: { $lt: { $now: true, addDays: { $field: 'grace_days_offset' } } } +} +``` + +`addDays` is **required** and always ADDED: an integer of any sign (`-30` +is thirty days ago keeping the time of day, where `{30_days_ago}` names a +calendar day; for the bare instant write `{now}`), or a `{ $field }` naming +a **number** field of the same record (NULL counts as zero days). The filter +cannot negate a positive `grace_days` column: store the signed offset. Refused +with `INVALID_FILTER` / 400: an anchor without `addDays`, a fractional or +string offset, any extra key (there is no `subDays`), a target that is not +`date` / `datetime`, and any `$in` / `$nin` / `$between` position; SQL +push-down also refuses a dotted or non-number offset field. The filter alone +answers it — not a stored formula field plus a hook. + ## Relation Filters A condition on a related record's fields beneath a relation field (`lookup`,