Skip to content

docs(skills): a vetted overlap-check hook template for insert and update - #33292

Draft
objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/project-thread-0sjbar
Draft

objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/project-thread-0sjbar

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

domain:skills#1 · os-project-manager · session_01FEkQ5kKeYvkiYDEFCNhNhU

Fixes #32979

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:按裁决 6876330213「脚本模板,内核不动」,在已发布的数据技能 skills/objectstack-data/references/data-hooks.md 加了第 5 个模式「同一人的时间段不能重叠」的 hook 模板,文档页 content/docs/automation/hooks.mdx 加同一模板和逐行原因。模板同时适用日期时间字段(前闭后开,18:00 结束与 18:00 开始不算重叠)和纯日期字段(作者把模板里的 DATE_ONLY 设为 true 后结束日算在内,同一天请两次会被拒;不论日期写成 2026-11-10、完整 ISO 时刻还是 Date 都一样),并以 runAs: 'system' 计数,保存人看不到的记录(如代人提交、私有共享)也算在内。两处都写明已知缺口(并发两次保存可同时通过、回收站恢复不跑插入 hook、关闭自动化的导入跳过 hook、同一批量插入或更新内的行互相看不见、没有开始或结束的行不检查也不计入)。
  • 为什么改:平台没有跨记录的重叠校验,裁决定为暂用 hook 脚本;AI 作者此前没有经过验证的写法,容易漏掉更新、漏掉排除自身、或把状态写成 $in 白名单。
  • 风险与回滚:只改教学文本和一个测试文件,不动 spec、引擎、数据层或运行时;技能文件为腾出 token 删掉了 Key Properties 下五个重复小节(object/events/priority/async/onError)、Hook Context API 下重复的 previous 示例和 Cross-Object API 小注,文件净 −45 token、−59 行,未抬天花板。回滚即还原本 PR。
  • 先看:① skills/objectstack-data/references/data-hooks.md 新增的 pattern 5 一段;② 同文件被删的七处重复(五个小节、previous 示例、Cross-Object API 小注);③ packages/runtime/src/sandbox/overlap-hook-template.integration.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run --project local src/sandbox/overlap-hook-template.integration.test.ts,预期 8 个用例全绿(真实 SQLite + QuickJS 沙箱跑模板本身)。
  • 需要你做的:受管面 Tier H(skills/**):由你合并,或授权账号 APPROVED 后由席位落地。

What changes

  • skills/objectstack-data/references/data-hooks.md (the file that already teaches data hooks): Common Patterns gains 5. No two of mine overlap (sandboxed body): one when-to-use line, the template, and one known-limits paragraph. The template is a beforeInsert + beforeUpdate hook that
    • merges the stored row under the patch on update (Object.assign({}, ctx.previous, ctx.input)),
    • leaves the record itself out (id: { $ne: ctx.previous.id }),
    • names the ignored statuses with $nin, so a status added later, or a row with no status, still counts, and does not check a record saved in an ignored status,
    • uses half-open ranges for datetime fields (start_at $lt end, end_at $gt start) and, when the author sets the template's DATE_ONLY constant for date fields, an inclusive end day ($lte / $gte). The kind is stated, not read off the value: a before* hook sees the value as the caller wrote it, and a date field also takes a full ISO instant or a Date. The template does not cut the values to the day itself; the driver reads each comparand by the field's own storage rule (measured below),
    • declares runAs: 'system', so the count also sees rows the saver cannot read (a delegate under private sharing, someone else's room booking); only the hook's ctx.api is elevated, the write itself runs as the saver,
    • counts in ctx.object, so the body names no object; the lead-in says to rename the hook's name (the refusal's translation key follows it), its object and its fields,
    • refuses through a declared refusals entry (OVERLAP, status 409), so the message is fixed and translatable.
      The limits stated next to it: the ruling's three (recycle-bin restore runs no insert hook; an import with automations off skips hooks; rows of one bulk insert do not see each other, widened to bulk update), the read-then-write race (two concurrent saves can both pass), which the ruling's own "without a concurrency guarantee" names, and a row with no start or end is neither checked nor counted.
  • Paid for in the same file, no ceiling raise: the object, events, priority, async and onError subsections under Key Properties are deleted, and so are the previous example under Hook Context API (it restated the sandbox ctx table's ctx.previous row and the HookContext interface's previous member) and the Cross-Object API note there (it restated the ctx.api.object(name) section: the repository bound to the current transaction, and where as the query key). Each restated what the file already says one screen up or below: the defineHook example's own comments (// string | string[] | '*', the event list, priority "lower runs first; System: 0-99, App: 100-999, User: 1000+", async "after* events only"), the Before vs After table's Transaction and Error-handling rows (async: true runs after commit; onError defaults to 'abort' for every hook, set 'log' on side-effect hooks), and Best Practices (object: '*' sparingly; async: true for non-critical work). No link in the repo points at their anchors (git grep for each anchor: 0 hits).
  • content/docs/automation/hooks.mdx (the record hooks page): a new section Recipe — refuse overlapping time ranges before Hook Context, with the same template byte for byte, one bullet per rule saying why it is there (the runAs bullet links the page's own Elevation section), and the limits. Room and shift bookings are named as the same rule grouped by the room or shift.
  • packages/runtime/src/sandbox/overlap-hook-template.integration.test.ts: the proving test (below).
  • .changeset/32979-skills-overlap-hook-template.md: '@objectstack/skills': patch (skills/** ships in that package).

No spec key, no exclusions key, no engine, data-layer or runtime change.

Why each call in the template is the one on main

Read on origin/main d4fbb4d:

Template call Where the runtime defines it
events: ['beforeInsert', 'beforeUpdate'] packages/spec/src/data/hook.zod.ts L389-L390 (the event enum)
ctx.previous is the stored row on update, undefined on insert packages/runtime/src/sandbox/body-runner.ts L1481-L1483 (buildSandboxContext)
ctx.input on update carries only the changed fields the same snapshot (unwrapProxyToPlain(engineCtx.input), L1411); taught at data-hooks.md's ctx.input row
runAs: 'system' elevates only the hook's ctx.api (isSystem: true, userId kept) packages/spec/src/data/hook.zod.ts L720 (runAs), packages/objectql/src/hook-run-as.ts L24-L28 and deriveHookApi L196; pinned for a body by packages/runtime/src/sandbox/hook-run-as.integration.test.ts
ctx.object is the object the hook fired on data-hooks.md's sandbox ctx table
ctx.api.object(name).count({ where }), gated by api.read body-runner.ts L1167-L1168 (count), packages/runtime/src/sandbox/quickjs-runner.ts L705 (the read set the gate grants to api.read)
refusals + throw Object.assign(new Error(), { code }) answered with the declared entry hook.zod.ts L665 (refusals), body-runner.ts L534-L539 (catch hands the throw to resolveHookRefusal), packages/runtime/src/sandbox/hook-refusal-catalogue.ts L160

Tests

packages/runtime/src/sandbox/overlap-hook-template.integration.test.ts runs the template's hook in composition (real ObjectQL, real SQLite driver, the real QuickJSScriptRunner, bindHooksToEngine; the harness of hook-capability-denial-is-a-refusal.integration.test.ts) on a leave_request object with datetime fields and, bound unchanged but for name, object and DATE_ONLY = true, on a leave_day object with date fields, under a middleware that stands in for private sharing (an unelevated read of leave_request sees only rows its user filed). It reads every refusal off mapDataError (status 409, declaredCode: 'OVERLAP', the declared sentence):

  1. overlapping insert refused, nothing written;
  2. fail closed: a sibling with no status, or a status the list does not name, still counts;
  3. adjacent ranges (half-open), another owner, a rejected sibling and a rejected write all accepted;
  4. an update whose patch carries only start_at and moves into overlap is refused (stored row merged), and reviving a rejected row into overlap is refused;
  5. updating the record itself (other field, or a range that still overlaps only its own stored row) is not refused;
  6. date fields: the same day twice and a range ending on a booked day are refused, the next day and the day before are accepted, and an update that keeps the stored end and moves the start into another row's days is refused;
  7. date fields, written the long way: a day written as a full ISO instant, or as a Date, over a booked day is refused; the next day written that way is accepted and stored as 2026-11-11;
  8. a delegate whose own read does not see the owner's row (count 0 under the stand-in) is still refused, because the count runs runAs: 'system'.

On head e417fbf: Test Files 1 passed (1) · Tests 8 passed (8).

The test can fail: one mutation per rule, each run on e417fbf through node scripts/ablation-replace.mjs on the test's copy (the three copies are byte-identical), then restored; the test file's blob matched HEAD after every run:

Mutation Result
pick the arm by spelling again: start_at: DATE_ONLY ? to start_at: String(r.end_at).length === 10 ? 1 failed (case 7)
delete the DATE_ONLY ? inclusive arm on start_at 2 failed (cases 6, 7)
delete the DATE_ONLY ? inclusive arm on end_at 2 failed (cases 6, 7)
drop where.id = { $ne: ctx.previous.id } 2 failed (cases 5, 6)
$lt to $lte on the datetime start_at arm 1 failed (case 3)
$nin: off to $in: ['approved'] 1 failed (case 2)
Object.assign({}, ctx.previous, ctx.input) to Object.assign({}, ctx.input) 2 failed (cases 4, 6)
delete the runAs: 'system' line 1 failed (case 8)

Readings taken while writing it: the first two are probes, not kept. A row with a null status is counted by $nin (the engine does not drop it as SQL NOT IN would), and a +08:00 input is normalised before the comparison (19:00+08:00 to 20:00+08:00 overlaps 10:30Z to 13:30Z and is refused; kept in case 1). The third shaped round 2: a first cut of the template sliced both ends to YYYY-MM-DD in the hook, and the mutation removing that slice stayed green (8 passed), because the drivers already read a date comparand by the field's storage rule (temporalStorageForm in @objectstack/core, called from driver-sql, driver-turso, driver-memory and driver-mongodb). The slice was dropped: it proved nothing and would have mangled a value that is not a string.

pnpm --filter @objectstack/runtime typecheck (tsc plus check:test-typecheck): exit 0.

Acceptance notes

  • The test holds a copy of the template, not a read of skills/: @objectstack/runtime's suites read nothing outside the package, and the claim's file surface does not include scripts/cross-package-test-inputs.mjs or packages/runtime/vitest.repo-tests.json, which a read would need. The three copies (skills, docs, test) are byte-identical on this head; the test's header says to edit it with the template. class: observation · reach: none · carrier: none.
  • Elevation is measured against a stand-in for sharing, not plugin-security itself: the middleware filters exactly where the security middleware does (opCtx.context.isSystem, hook-run-as.ts L24-L27 says it short-circuits on that flag before every row-level gate). class: observation · reach: none · carrier: none.
  • Docs page budget: +79 lines (379 to 458) against the order's +80.

Skills readings

Measured with node scripts/check-skills-token-ratchet.mjs on origin/main d4fbb4d and on head e417fbf:

before after delta
skills/objectstack-data/references/data-hooks.md lines 943 884 −59
same file, tokens (ceiling 10011) 10007 9962 −45
all SKILL.md files (lines) unchanged unchanged 0 (no SKILL.md touched)
bundle total (whole shipped tree, tokens) 244205 244160 −45

New text is paid by deleting restatement in the same file; no ceiling moves, so no raise path is used.

Verification

Round 2 answers delta review 6881070935 (PASS on da1e590 with one should-fix and one nit):

  • F1r (the date arm picked by spelling). The template no longer reads the field kind off the value. The author states it: const DATE_ONLY = false; // true when start_at / end_at are date fields, and the arm follows the constant. Case 7 writes the day as a full ISO instant and as a Date and is still refused; the mutation that puts the old spelling test back turns case 7 red. The normalisation the review offered as part of that option was tried and dropped on measurement: the drivers already read a date comparand as the stored day, so a slice in the hook changed nothing a test can see (see Tests). The docs sentence "A date value is YYYY-MM-DD, ten characters" is gone; the docs bullet now says the flag is needed because a before* hook sees the value as written, and that the query reads each value as the day it stores. The changeset's "the same day cannot be booked twice" now reads "for date fields the author sets DATE_ONLY, which counts the end day as booked".
  • N4 (renaming). Both lead-ins name what to rename: the skill says "renaming its name, object and fields (date fields: set DATE_ONLY)"; the docs say "rename its name (the refusal's translation key follows it), its object and its fields".
  • N1 and N2 stay as the accepted deviations.
  • Budgets: the skills file is 9962 / 10011 tokens and −59 lines against main; the extra round-2 cost was paid by deleting the Cross-Object API note (restatement of the ctx.api.object(name) section). The docs page is +79 lines (budget +80). The three copies are byte-identical on e417fbf.

Checks on e417fbf:

  • node scripts/pm/dispatch-gates.mjs --tier: MANDATORY CONTRACT_REVIEW_TIER (skills/**, no one-line exit).
  • node scripts/pm/check-governed-merges.mjs --branch claude/project-thread-0sjbar, exit 3: GOVERNED, landing tier H (人合), skills/** ×1 (skills/objectstack-data/references/data-hooks.md); the other three paths are not on the register; 479 changed lines (+378 / −101), under the 3000 threshold; head repo NOT MEASURED (--pr reads it).
  • Proving test Tests 8 passed (8); pnpm --filter @objectstack/runtime typecheck exit 0 (with NODE_OPTIONS=--max-old-space-size=4096; the default heap ran check:test-typecheck's tsc out of memory, no diagnostics); pnpm check:query-options-erasure exit 0, test surface 236 (at the ceiling, unchanged).
Gate list on e417fbf: 97 derived, 97 exit 0

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at e417fbf derived the same 97 commands as round 1; each was run on that head with its exit code captured before any pipe, after building the @objectstack/runtime closure, @objectstack/client-react..., formula and lint under os-verify-lock. 96 exit 0 on the first pass, including check:skill-examples (the os:check block type-checks), check:doc-anchors, check:doc-formula-expressions, the skills token ratchet and check:query-options-erasure. pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET, packages without dist/); after turbo run build of the whole tree under the lock (exit 0) it exits 0: 123 require entry points across 72 packages load.

--ran reconciliation: "97 derived famil(ies) accounted for — 97 run, 0 NOT-MEASURED (a DERIVED zero — all 97 recorded an exit code and none of them is 3)", exit 0. Record: project file pm-orders/skills/32979-gates-ran-r2.txt.

Signed: domain:skills#1 · os-project-manager · session_01FEkQ5kKeYvkiYDEFCNhNhU


Generated by Claude Code

The data skill's hook reference and the record hooks docs page carry a
template that refuses two live records of one owner over overlapping time:
stored row merged under the patch on update, the record itself excluded,
ignored statuses named with $nin, half-open ranges, a declared refusal.
Its four known limits sit next to it. The runtime test runs the template
on a real SQLite driver and the QuickJS sandbox. Restating subsections
under Key Properties are removed to pay for the new text.

Claude-Session: https://claude.ai/code/session_01FEkQ5kKeYvkiYDEFCNhNhU
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 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 d3f3f4abf9f392a49fecf77f2175424fe1680bef → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 500642ac9204b77e8f1f8c67fd1f92244953e1de
Local-runs: none
Scope: skills/objectstack-data/references/data-hooks.md + packages/runtime/src/sandbox/overlap-hook-template.integration.test.ts

Read for this record: card #32979 (body and all 13 comments, ruling 6876330213 included), PR #33292 (body, file list, net diff), the head's check-runs, and the cited code on origin/main (47a3b95; the PR's merge base is d4fbb4d, and nothing under skills/objectstack-data, packages/runtime/src/sandbox, packages/objectql/src, packages/spec/src/data/hook.zod.ts or content/docs/automation/hooks.mdx moved between the two). content/docs/automation/hooks.mdx and the changeset are outside ④ and were read only for agreement with the skill text.

① Derived judgments

  • Published surface: one skill reference file, teaching text only. No spec key, no engine, driver or runtime source in the diff. Right for ruling 6876330213 ("script template, kernel untouched").
  • Accept-set: unchanged. The template uses only what main already accepts: the events beforeInsert / beforeUpdate, refusals with status: 409 (schema allows 400 to 499), capability api.read, ctx.api.object(n).count({ where }), $nin / $ne / $lt / $gt. Right.
  • Token ceiling not raised (the PR reports 10005 of 10011 on the file, paid by deletion in the same file). Right as a route; what the deletion costs is judged in ④.
  • Gates. On this head 17 check-runs are success, 15 skipped, 0 failing, and 18 are still in progress at the time of this record: Test Core 1/8 to 8/8, Build Core, Build Docs, Type Check (workspace, consumer gates), Lint (ESLint, query-options erasure, slot lookup and corpus, repo and tooling gates, docs and code guards, CI/release/engine gates). Already concluded green: Check PR Size, Governed Surface Queue Guard, Spec property liveness, PR-body guards, Check Documentation Links, Lint PM dispatch-gates, tooling self-tests, Dogfood Regression Gate, Test UI. The proving test runs in Test Core and the os:check block in the lint/type families, so their verdict is not in yet. This record is a design verdict; it does not stand in for the pending runs.

② Semver level

.changeset/32979-skills-overlap-hook-template.md declares '@objectstack/skills': patch. skills/** ships in that package (packages/skills/package.json). The diff publishes a new teaching pattern and removes restated prose; no schema, no behaviour. Patch is the right level.

Clause-②: no on the PR body and in the changeset: correct. Nothing a consumer could declare yesterday is refused today; the five removed subsections are documentation of keys that stay accepted unchanged.

③ Boundary flags

  • open_questions: empty in the dev report. Nothing to answer.
  • Deviation "plain Signed: line instead of a trailing HTML comment": process only, no effect on the contract. Accepted.
  • Deviation "pr_create sent no labels": process only. Accepted.
  • Deviation "four mutation runs and one probe ran outside os-verify-lock": they are the dev's evidence, not a gate; the gate verdict is the head's check-runs. Accepted.
  • Out-of-scope finding "the test holds a copy of the template": answered in ④ N2 (the three copies are byte-identical on this head; drift is held by convention only).
  • Out-of-scope finding "ctx.api reads as the saver, so rows the saver cannot read are missed": the dev left it out of the skill text. It is not out of scope for a customer agent loading the skill; raised as ④ F2.

④ Design judgment

skills/objectstack-data/references/data-hooks.md — the template against the live runtime. Each call holds on main:

  • events: ['beforeInsert', 'beforeUpdate'] are the engine's event names (engine-insert.ts, engine-update.ts dispatch sites).
  • ctx.previous is undefined on insert and the stored row on update (body-runner.ts, buildSandboxContext); on a predicate update the hook is dispatched once per matched row with that row's pre-image, so ctx.previous.id names the row there too. ctx.input is the patch only. The merge Object.assign({}, ctx.previous, ctx.input) is therefore the right record on both events, and on insert it already carries engine defaults (the hook runs over the defaulted row), so a defaulted status is judged.
  • ctx.api.object(n).count({ where }) exists and is gated by api.read, the one capability declared.
  • $nin is null-safe by platform contract on the SQL driver (applyNullSafeNegative), and the memory, MongoDB and OData drivers state the same reading, so "a row with no status still counts" is not a SQLite accident.
  • Datetime comparands are canonicalised by the driver's filter coercion, so a patch value with an offset and a stored value compare correctly.
  • The refusal: refusals is on HookSchema, the body throws the code alone, resolveHookRefusal answers with the declared entry. The literal code thrown is declared, so the parse-time check passes.
  • The three limits the ruling names are true on main: restore runs no insert hook (engine-top-level.ts, ADR-0148 D2), an import with automations off passes skipAutomations, and a batch insert dispatches every row's beforeInsert before any row is written.

Against the ruling, item by item. Skills and docs both carry it: yes. Insert and update: yes. Merge previous values on update: yes. Exclude the record itself: yes (id $ne ctx.previous.id). Status filter as $ne / $nin: yes ($nin), and the docs page names both spellings and forbids $in. Fixed message: yes, a declared refusals entry. The three named gaps: all three stated, plus the read-then-write race the ruling's "without a concurrency guarantee" implies. The ruling is met.

Findings.

  • F1 — should-fix. Date-only ranges pass silently after the rename the skill invites. The skill says "use this hook, renaming object and fields". With date fields and an inclusive end day, a one-day leave has start == end, and start $lt end is false for two identical one-day leaves: both are saved. That is the card's own second symptom ("the same day requested twice" on leave), on the template's own example object. The half-open comment speaks only of 18:00. The test uses datetime fields only, so nothing pins this either way. One clause next to the template is enough (for inclusive date-only ends compare with $lte / $gte, or store an exclusive end); without it a customer agent ships a check that never fires for the most common leave shape.
  • F2 — should-fix. The count sees only what the saver can read, and the limits do not say so. A hook's ctx.api runs as the triggering user under the default runAs: 'inherit' (hook-wrappers.ts: inherit leaves the engine-built api in place). When the saver cannot read the sibling rows (an approver or delegate reviving someone's request under private sharing, or any group key other than the saver's own rows, such as a room), the count misses them and the write passes. Unlike the four stated limits this one a hook can cover: runAs: 'system'. The "Known limits" paragraph presents itself as the list of what the hook does not cover, and runAs is not taught anywhere in objectstack-data/references/data-hooks.md. Read on main, not measured (the test harness has no security plugin, as the PR notes). One clause in the limits closes it.
  • N1 — nit. The five deleted subsections: no rule is lost, three small facts keep only a weaker home. events and onError are fully restated (lifecycle table; Before vs After table rows; the Error Handling section; the fillPositionOnHire example). async keeps "after* events only" (example comment), "within the transaction unless async: true, then after commit" (table) and "async: true for non-critical background work" (Best Practices). What has no verbatim home: that priority: 100 is the default (the example shows the value without the word), that object: '*' costs performance (Best Practices keeps the rule, not the reason), and the explicit "keep related-record and dependent-data writes synchronous" list (now only implied by "non-critical"). No anchor to the removed headings is referenced anywhere in the head tree.
  • N2 — nit. What the proving test does not pin. Its five cases do pin every ruled item: overlapping insert refused with nothing written (409, declaredCode OVERLAP, the declared sentence); null and unlisted statuses count; adjacent ranges, another owner and ignored statuses accepted; a patch carrying start_at alone judged with the stored row, and a revive refused; self-update not refused. Not pinned: the template is bound as a plain object, not through defineHook, so the schema parse is covered only by the os:check type-check; no case uses an offset timestamp, the shape of the card's evidence (the PR reports a probe, not kept); and the copy is held equal to the skill by a header comment only. The three copies are byte-identical on this head.
  • N3 — nit. Unstated edges of the same family as the stated limits. Rows of one bulk update do not see each other either (the limit names bulk insert only); a row with no end, stored or in the write, is neither checked nor counted.

Docs page agreement (content/docs/automation/hooks.mdx, read, not reviewed). The template is byte-identical to the skill's; the five reasons and the four limits agree with the skill text and with the code. One wording point for the prose face: the opening sentence lists "room and shift bookings" under "two live records of the same owner", where the group key is the room, which is exactly the case F2 describes.

F1 and F2 each earn a delta review of their fix. Neither is a must-fix: the template is correct for the datetime, owner-keyed case the ruling describes and the test proves.

Implemented-by: claude/project-thread-0sjbar
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

VERDICT: PASS

Adopted by the seat 2026-11-14T16:01Z · isolated sub-agent at CONTRACT_REVIEW_TIER · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z

Contract review round 1. The template now compares a date range with its
end day inclusive (the same day twice is refused) while a datetime range
stays half-open, declares runAs system so its count sees rows the saver
cannot read, and counts in ctx.object so only field names need renaming.
The limits name bulk updates and rows with no start or end. The proving
test adds a date-field case and a delegate-under-sharing case. The
previous-state example under Hook Context API, which restated the sandbox
ctx table, is removed to pay for the new text.

Claude-Session: https://claude.ai/code/session_01FEkQ5kKeYvkiYDEFCNhNhU
Co-Authored-By: Claude <noreply@anthropic.com>
The delegate case passed its context through an options bag cast to any,
which the query-options erasure ratchet counts as a new test-surface
site. Typed as ExecutionContext, the cast is gone and the count returns
to its ceiling.

Claude-Session: https://claude.ai/code/session_01FEkQ5kKeYvkiYDEFCNhNhU
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: da1e5906e6c9d50e1f9b353628522708ecbeb231
Local-runs: none
Scope: skills/objectstack-data/references/data-hooks.md + packages/runtime/src/sandbox/overlap-hook-template.integration.test.ts

Delta review, round 1, of the rework that answers record 6880254594 (PASS on 500642a with two should-fix). Read for this record: card #32979 and its comments, PR #33292 (the patched body, file list, net diff against main), the delta 500642a..da1e590 (commits 5b24166 and da1e590), the head's check-runs, and the cited code on origin/main (87330e8; merge base d4fbb4d). Every face below is re-derived from the diff and the code, not carried over. content/docs/automation/hooks.mdx and the changeset are outside ④ and were read for agreement with the skill text.

① Derived judgments

  • Published surface: one skill reference file, teaching text only. Four files in the PR, as before; no spec, engine, driver or runtime source. Right for ruling 6876330213.
  • Accept-set: unchanged. The template now also uses runAs: 'system' (an enum member of HookSchema.runAs), ctx.object (on the sandbox ctx), and $lte / $gte. All are accepted on main today. Right.
  • Token ceiling not raised. The file is paid for by deletion in the same file (the PR reports 9985 of 10011). Right as a route; what the new deletion costs is judged in ④.
  • Main has moved under the PR in one relevant place. Since the merge base, main took fix(driver-sql): a date field still on a date-time column filters as its calendar day on SQLite and MySQL; operator note for Postgres days stored early #33270 in driver-sql (a date field still on a date-time column filters as its calendar day). It changes no call the template makes; it is the same comparand rule the date arm relies on.
  • Gates. All 57 check-runs on this head have concluded: 40 success, 17 skipped, 0 failing, 0 pending. Green among them: Test Core 1/8 to 8/8 and its aggregate (the proving test runs there), Build Core, Build Docs, the Type Check families, Lint query-options erasure (red on 5b24166, green here), Check Changeset, Governed Surface Queue Guard, Check PR Size, PR-body guards. The derived gate families are answered.

② Semver level

'@objectstack/skills': patch, Clause-②: no, Metadata-protocol: none: all still correct. The delta adds teaching text and removes one more restating example; nothing a consumer could declare is refused. The changeset prose was updated to the new template and agrees with it, with one overstatement noted under F1.

③ Boundary flags

  • The dev's round-1 report carries no open question. Nothing to answer.
  • Acceptance note "elevation is measured against a stand-in for sharing, not plugin-security": accepted as stated, and named again under F2. The stand-in keys on context.isSystem, the flag the security middleware short-circuits on.
  • Acceptance note "the test holds a copy of the template": the three copies (skill, docs, test) are byte-identical on this head; drift is still held by a header comment only (round-1 N2, unchanged).
  • The CI red on 5b24166 (query-options erasure ratchet) is answered by typing the delegate context as ExecutionContext; that check-run is green on this head.

④ Design judgment

Round-1 F2 (visibility) — resolved, and the elevation is the right one.

  • runAs: 'system' derives the hook's ctx.api as the triggering context with isSystem: true (engine-scoped-context.ts, withRunAs). It never touches the context of the write that fired the hook, so the save itself is still judged as the saver.
  • The body declares api.read only, so the elevated api cannot write: an insert, update or delete from this body is refused by the capability gate whatever its identity.
  • The derivation spreads the triggering context, so tenantId rides along and the driver's native read scope (engine-driver-options.ts, driverReadScope) still names the saver's organization. The count does not cross organizations.
  • What leaves the elevated read is one bit: the fixed OVERLAP sentence. A saver learns that a conflicting row exists that they may not be able to open. For this rule that is the point, but it is new behaviour of a copied template and the maintainer quick-read should say so.
  • Test case 7 pins it: the delegate's own count is 0 under the stand-in, and the insert is still refused. Read on main, and measured against the stand-in only; plugin-security and plugin-sharing are not in the harness.
  • ctx.api.object(ctx.object) is sound: ctx.object is the target object name on the sandbox ctx, and the same source is bound to two objects in the test.

Round-1 F1 (date-only ranges) — resolved for the canonical spelling; one residue.

  • For a date value spelled YYYY-MM-DD the template now compares with $lte / $gte, and case 6 pins the same day twice, a range ending on a booked day, adjacent days accepted, and an update that keeps the stored end.
  • F1r — should-fix. The date arm is chosen by the spelling of the value, and the hook sees the caller's spelling, not the stored one. A date field accepts a Date and any string with a leading YYYY-MM-DD (record-validator.ts: "a date string also carries a leading YYYY-MM-DD"); the collapse to the ten-character day (temporalStorageForm) is applied by the drivers, after beforeInsert / beforeUpdate. So a date end written as a full ISO instant (the day followed by a midnight time and Z), or as a Date (which reaches the sandbox as that same string), has length 24: day is false, the half-open arm runs, the comparand collapses to the same day in the filter, and start $lt end is false for a same-day duplicate. Both rows save. That is the round-1 failure, now conditional on how the client spells the day. Read on main, not measured; case 6 writes ten-character strings only. The docs sentence "A date value is YYYY-MM-DD, ten characters" and the changeset's "the same day cannot be booked twice" are true of the stored value, not of ctx.input. A fix that does not depend on spelling: let the author state the field kind (a constant in the template), or say in the limits that the end must arrive as a ten-character day.

The deletion that pays for the delta. The previous example under Hook Context API is gone. Nothing it taught is lost: the HookContext interface in the same section keeps previous with "Previous state (update/delete)", the sandbox ctx table keeps "Pre-write record on update/delete, undefined on insert", and patterns 1 to 3 read ctx.previous in running code. No anchor to the removed heading is referenced in the head tree.

Nits.

  • N1 (round 1), unchanged. Three small facts from the five deleted Key Properties subsections keep only a weaker home (that priority: 100 is the default; the performance reason for object: '*'; the "keep related and dependent writes synchronous" list). No rule lost. An accepted deviation or a follow-up card.
  • N2 (round 1), partly open. The template is still bound as a plain object, not through defineHook, and no case uses an offset timestamp.
  • N3 (round 1), closed. The limits now name bulk insert and bulk update, and rows with no start or end.
  • N4, new. The lead-in now reads "renaming its fields". The object key and the hook name (and with it the translation key) still have to be renamed too; only the ctx.api.object(...) call no longer does.

Docs page agreement (read, not reviewed). Template byte-identical to the skill's. The intro now groups room and shift bookings by the room or the shift, which closes the round-1 wording point. The runAs bullet agrees with the page's own Elevation section and with the code. The date bullet carries the F1r overstatement.

F1r earns a delta review of its fix. It is not a must-fix: the template is correct for datetime ranges and for dates spelled as days, both proven by the test, and it is strictly better than the head round 0 passed.

For the seat's ACCEPT and the maintainer quick-read: (1) the copied template now reads elevated by default (runAs: 'system', read-only, same organization), and its refusal tells a saver that a row they may not see exists; (2) the date arm holds only when the end arrives as a ten-character day (F1r); (3) elevation is proven against a stand-in, not the security plugins; (4) the round-1 nits N1, N2 and the new N4 are left open.

Implemented-by: claude/project-thread-0sjbar
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

VERDICT: PASS

Adopted by the seat 2026-11-14T16:57Z · isolated sub-agent at CONTRACT_REVIEW_TIER · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z

…e spelling

A before* hook sees a date value as the caller wrote it (a Date, or any
string with a leading YYYY-MM-DD); drivers cut it to the day only after the
hook runs. The template now takes a DATE_ONLY constant from the author and,
when it is set, cuts both ends to their leading day before comparing. The
lead-ins say the hook's name and object are renamed with the fields. The
proving test adds an end written as a full ISO instant and as a Date.

The token cost is paid by deleting the Cross-Object API note, which restated
the ctx.api.object(name) section of the same file.

Claude-Session: https://claude.ai/code/session_01FEkQ5kKeYvkiYDEFCNhNhU
Co-Authored-By: Claude <noreply@anthropic.com>
The drivers read a date comparand by the field's own storage rule, so a
value written as a full ISO instant or a Date already matches the stored day;
the template's own slice was redundant (a mutation removing it stayed green)
and would have mangled a non-string value. DATE_ONLY stays: it is what the
value's spelling cannot tell.

Claude-Session: https://claude.ai/code/session_01FEkQ5kKeYvkiYDEFCNhNhU
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e417fbfcfccaec33e34313e7dd8045c26ba08ddf
Local-runs: none
Scope: skills/objectstack-data/references/data-hooks.md + packages/runtime/src/sandbox/overlap-hook-template.integration.test.ts

Delta review, round 2, of the rework that answers record 6881070935 (PASS on da1e590 with one should-fix, F1r, and nit N4). Read for this record: card #32979 and its comments, PR #33292 (the patched body, file list, net diff against main), the delta da1e590..e417fbf (commits 7f6e8db and e417fbf), the head's check-runs, and the cited code on origin/main (6538ba4; merge base d4fbb4d). Every face is re-derived from the diff and the code. content/docs/automation/hooks.mdx and the changeset are outside ④ and were read for agreement with the skill text.

① Derived judgments

  • Published surface: one skill reference file, teaching text only. Four files in the PR; no spec, engine, driver or runtime source. Right for ruling 6876330213.
  • Accept-set: unchanged. The template uses beforeInsert / beforeUpdate, runAs: 'system', refusals with status 409, capability api.read, ctx.object, ctx.api.object(n).count({ where }) and $nin / $ne / $lt / $gt / $lte / $gte. All accepted on main today.
  • Token ceiling not raised. Paid by deletion in the same file (the PR reports 9962 of 10011; the file is 884 lines on this head). What the new deletion costs is judged in ④.
  • Main drift. Since the last record main moved only in objectql's authored-action binding (plugin.ts, action-handler-coordinate.ts); no hook, sandbox, driver or temporal code the template touches.
  • Gates. All 57 check-runs on this head have concluded: 40 success, 17 skipped, 0 failing, 0 pending. Green among them: Test Core 1/8 to 8/8 and its aggregate (the proving test runs there), Build Core, Build Docs, the Type Check families, Lint query-options erasure, Check Changeset, Governed Surface Queue Guard, Check PR Size, PR-body guards. The derived gate families are answered.

② Semver level

'@objectstack/skills': patch, Clause-②: no, Metadata-protocol: none: correct. The delta changes teaching text and removes one more restating note; nothing a consumer could declare is refused. The changeset now reads "for date fields the author sets DATE_ONLY, which counts the end day as booked": that is what the code does, and it no longer promises the result unconditionally.

③ Boundary flags

  • The dev's round-2 report carries no open question.
  • The dev reports dropping an in-hook slice of both ends to the day, because removing it left the test green. Judged in ④: the reason holds in the drivers, so the slice would have been dead code.
  • Acceptance notes carried from round 1 (elevation measured against a stand-in; the test holds a copy of the template): unchanged and still accepted. The three copies (skill, docs, test) are byte-identical on this head.

④ Design judgment

F1r (the date arm chosen by spelling) — resolved.

  • The template now opens with const DATE_ONLY = false; and a comment saying to set it when the two fields are date fields; both comparison arms follow the constant. Nothing reads the field kind off the value any more.
  • With the flag set, the comparands may still arrive as a full ISO instant or as a Date (which the sandbox sees as that same string). That is sound, because the comparand and the stored value pass through one rule: a comparand on a declared date field is put into the calendar-day storage form before it is compared. Read in each driver the platform stores through: driver-sql (coerceFilterValue ends in toDateOnly, which is temporalStorageForm(value, 'date')), driver-turso (a SqlDriver subclass), driver-memory (coerceTemporalValue calls temporalStorageForm), driver-mongodb (translateFilter is handed the field's temporal kind and coerces with storageDateValue). So $lte / $gte compare day against day on all four, and the dev's reason for dropping the in-hook slice is true.
  • The one driver where this is not the platform's rule is driver-odata: a federated object's comparison is the remote's. A booking object that lives in a remote OData source is outside what the template was proven on. Not a finding against a template for stored objects; named for the quick-read.
  • Test case 7 pins it on SQLite: a day written as a full ISO instant and as a Date is refused against a stored ten-character day, the next day written the long way is accepted, and it reads back as its day. Case 6 runs with the flag set by exactly one replacement of the template's own line, which also proves the line exists once. Case 1 gained the offset-timestamp case round 1 asked for (N2, that half closed).

What happens when an author forgets the flag. On date fields with DATE_ONLY left false, the half-open arm runs and a same-day duplicate is saved: the round-0 failure, silently. The opposite mistake (flag set on datetime fields) refuses adjacent bookings, which is visible. The default therefore fails toward the silent side. It is stated at both places an author reads (the lead-in and the first line of the body), and the default is right for the template's own datetime example, so this is graded a nit (N5), not a defect of the template.

N4 (renaming) — closed. The skill says "renaming its name, object and fields"; the docs add that the refusal's translation key follows the name. Both true: the key is built from the hook name, and the body names no object.

The deletion that pays for the delta. The three-line Cross-Object API note under Hook Context API is gone. What it said keeps a home in the same file: "ctx.api.object('...') returns a repository bound to the current org / user / transaction" and "Query shape — the key is where" in the sandbox section, and api?: ScopedContext; // Cross-object CRUD API in the HookContext interface. Only the explicit words "the same repository in both forms" are lost, about a deprecated form. No anchor to the removed heading is referenced in the head tree. No rule lost.

F2 (elevation), re-derived on this head. Unchanged by the delta and still right: runAs: 'system' elevates the hook's ctx.api only, the body holds api.read only, the organization scope rides along, and the one thing that leaves is the fixed OVERLAP sentence. Proven against a stand-in, not the security plugins.

Nits.

  • N1 (round 0), unchanged. Three small facts from the deleted Key Properties subsections keep only a weaker home (that priority: 100 is the default; the performance reason for object: '*'; the "keep related and dependent writes synchronous" list).
  • N2 (round 0), half open. The template is still bound as a plain object in the test, not through defineHook; the schema parse is covered by the os:check type-check only.
  • N5, new. A forgotten DATE_ONLY on date fields fails silently (above). One clause in the limits would name it; the text is otherwise sufficient.

Docs page agreement (read, not reviewed). Template byte-identical. The date bullet no longer claims a ten-character value; it says a before* hook sees the value as written, that a date field also takes a full instant or a Date, and that the query reads each value as the day it stores. Each sentence holds against the code above. The "body names no object" bullet is true.

No must-fix and no should-fix on this head. Nits only: the round ends here.

For the seat's ACCEPT and the maintainer quick-read (plain facts):

  • What changed: the data skill and the hooks docs page gain one vetted hook template that refuses two records of one owner over overlapping time, on insert and on update. Datetime ranges are half-open. For date fields the author flips one constant, DATE_ONLY, and the end day counts as booked.
  • Why: the ruling keeps this rule in hook scripts, and AI authors had no proven form; the usual mistakes were skipping update, colliding with the record itself, and whitelisting statuses.
  • Risk: teaching text and one test file only. Three things to know. (1) Every copy reads elevated by default (runAs: 'system', read-only, same organization), and the refusal tells a saver that a conflicting row exists even if they cannot open it. (2) An author who forgets DATE_ONLY on date fields gets no refusal for a same-day duplicate. (3) The stated limits still apply: two concurrent saves, recycle-bin restore, imports with automations off, rows of one bulk write, rows with no start or end.
  • Where to look first: pattern 5 in skills/objectstack-data/references/data-hooks.md; then the deleted passages in the same file (five Key Properties subsections, the previous example, the Cross-Object API note); then packages/runtime/src/sandbox/overlap-hook-template.integration.test.ts, eight cases.
  • Left open as accepted deviations or a follow-up card: N1, N2, N5; elevation proven against a stand-in rather than the security plugins; federated (OData) objects not covered by the proof.

Implemented-by: claude/project-thread-0sjbar
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

VERDICT: PASS

Adopted by the seat 2026-11-14T17:58Z · isolated sub-agent at CONTRACT_REVIEW_TIER · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

维护者速读

  • 改了什么:objectstack-data 技能与 hooks 文档页各加一份经测试验证的 hook 模板,拒绝同一所有者在时间段重叠的两条记录(新增与更新都拦)。日期时间区间按半开计;若字段是 date 类型,作者把常量 DATE_ONLY 改为 true,结束日当天也算占用。
  • 为什么改:按裁定 6876330213,此类规则留在 hook 脚本里,但 AI 作者此前没有可照抄的正确写法,常见错误是漏拦更新、与自身记录冲突、按状态白名单放行。
  • 风险与回滚:只动技能文字、文档页、一份测试和 changeset,不动 schema 与运行时;回滚即还原本 PR。需知三点:① 模板默认 runAs: 'system'(只读、同组织),拒绝提示会让保存者知道有冲突记录存在,即使他打不开;② 日期字段忘记把 DATE_ONLY 设为 true 时,同日重复不会被拦;③ 并发双写、回收站恢复、关自动化导入、同一批量写内的行、无起止时间的行仍不在拦截范围,模板 limits 已写明。
  • 先看:skills/objectstack-data/references/data-hooks.md 的 Pattern 5,以及同文件为抵 token 预算删掉的段落(五个 Key Properties 小节、previous 示例、Cross-Object API 注);再看 packages/runtime/src/sandbox/overlap-hook-template.integration.test.ts 的 8 个用例。
  • 两分钟验证:pnpm vitest run packages/runtime/src/sandbox/overlap-hook-template.integration.test.ts,8 例全过;三处模板逐字一致。
  • 需要你做的:受管面 Tier H(skills/**),契约复审 PASS 评论 6881830735(Head-sha e417fbfcfccaec33e34313e7dd8045c26ba08ddf)已在案,请批准或直接合并。
  • 席位意见:可以合。经两轮返工,两条应修(提权读、日期写法)均已修复并有用例钉住;复审独立核对了四个存储驱动都按日比较日期比较值。

席位核对

Verdict: ACCEPT, held for the maintainer · head e417fbfcfccaec33e34313e7dd8045c26ba08ddf · card #32979 · report 6880159018 · contract review 6881830735 (PASS on this head, CONTRACT_REVIEW_TIER; delta of 6881070935 on da1e5906, which was the delta of 6880254594 on 500642ac; Ruling-ref for the in-seat review: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb option A).

  • Shape: draft → main, Fixes #32979. Four files (+378 / -101): skills/objectstack-data/references/data-hooks.md (+42 / -101), content/docs/automation/hooks.mdx (+79, budget +80), packages/runtime/src/sandbox/overlap-hook-template.integration.test.ts (+244, 8 cases), one '@objectstack/skills': patch changeset. GOVERNED, Tier H (check-governed-merges --pr 33292: skills/** ×1). Clause-②: no; Metadata-protocol: none.
  • Round-1 should-fixes: F1 (date-only ranges) and F2 (the overlap read must see rows the saver cannot) fixed on da1e5906; F2 by runAs: 'system', which elevates ctx.api only, read-only, with the organization scope kept.
  • Round-2 should-fix F1r (the date arm keyed on the caller's spelling): fixed by the author-set constant DATE_ONLY; case 7 writes the day as a full ISO instant and as a Date and both are refused. The dropped in-hook slice is justified: driver-sql, driver-turso, driver-memory and driver-mongodb all put a date comparand into calendar-day form before comparing.
  • N4 (rename name, object and the fields): fixed.
  • Token budget: data-hooks.md 9962 / 10011 tokens with no ceiling raise, paid by deleting restating passages; the review found no rule lost (previous and ctx.api.object remain taught in the HookContext interface, the sandbox ctx table and patterns 1 to 3). The three template copies are byte-identical.
  • CI red on 5b241662 (query-options erasure ratchet, 236 → 237): fixed on da1e5906 by typing the delegate context as ExecutionContext; green since.
  • Gates: the dev reports 97 derived, 97 exit 0, --ran 0 NOT-MEASURED; 8 mutations each red. Check-runs on the head: 57, 40 success, 17 skipped, none failing or pending. Mergeable clean.
  • Accepted deviations (review nits): N1, three small facts from the deleted Key Properties subsections keep only a weaker home; N2, the test binds the template as a plain object rather than through defineHook; N5, an author who leaves DATE_ONLY false on date fields gets a same-day duplicate saved silently (the flag is named in the lead-in and on the body's first line). Elevation is proven against a middleware stand-in keyed on context.isSystem, not against the security plugins. Federated (driver-odata) objects are outside the proof; their comparison is the remote's.
  • Landing: Tier H. The seat does not flip this PR ready or enqueue it. It needs the maintainer's merge, or an authorised APPROVED review after which the seat lands it.

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 14, 2026

This branch has not been deployed

No deployments
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 pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skills: a vetted hook template refuses two records of one person over overlapping time — ruled: no exclusions key, no kernel change (6876330213)

2 participants