Repository navigation
docs(skills): a vetted overlap-check hook template for insert and update - #33292
objectstack-fleet[bot] wants to merge 5 commits into
Conversation
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>
📓 Docs Drift CheckNothing 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): |
|
domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb Contract reviewServed-tier: 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 ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentskills/objectstack-data/references/data-hooks.md — the template against the live runtime. Each call holds on
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 ( Findings.
Docs page agreement ( 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: 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>
|
domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb Contract reviewServed-tier: 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 ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentRound-1 F2 (visibility) — resolved, and the elevation is the right one.
Round-1 F1 (date-only ranges) — resolved for the canonical spelling; one residue.
The deletion that pays for the delta. The Nits.
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 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 ( Implemented-by: 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>
|
domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb Contract reviewServed-tier: 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 ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentF1r (the date arm chosen by spelling) — resolved.
What happens when an author forgets the flag. On N4 (renaming) — closed. The skill says "renaming its 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: " F2 (elevation), re-derived on this head. Unchanged by the delta and still right: Nits.
Docs page agreement (read, not reviewed). Template byte-identical. The date bullet no longer claims a ten-character value; it says a 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):
Implemented-by: 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 |
|
domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb 维护者速读
席位核对Verdict: ACCEPT, held for the maintainer · head
|
Requested by Leehom · project thread
domain:skills#1 · os-project-manager · session_01FEkQ5kKeYvkiYDEFCNhNhU
Fixes #32979
Clause-②: no
Metadata-protocol: none
维护者速读
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、同一批量插入或更新内的行互相看不见、没有开始或结束的行不检查也不计入)。$in白名单。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 沙箱跑模板本身)。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 (sandboxedbody): one when-to-use line, the template, and one known-limits paragraph. The template is abeforeInsert+beforeUpdatehook thatObject.assign({}, ctx.previous, ctx.input)),id: { $ne: ctx.previous.id }),$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,datetimefields (start_at $lt end,end_at $gt start) and, when the author sets the template'sDATE_ONLYconstant fordatefields, an inclusive end day ($lte/$gte). The kind is stated, not read off the value: abefore*hook sees the value as the caller wrote it, and adatefield also takes a full ISO instant or aDate. 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),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'sctx.apiis elevated, the write itself runs as the saver,ctx.object, so the body names no object; the lead-in says to rename the hook'sname(the refusal's translation key follows it), itsobjectand its fields,refusalsentry (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.
object,events,priority,asyncandonErrorsubsections under Key Properties are deleted, and so are thepreviousexample under Hook Context API (it restated the sandboxctxtable'sctx.previousrow and theHookContextinterface'spreviousmember) and the Cross-Object API note there (it restated thectx.api.object(name)section: the repository bound to the current transaction, andwhereas the query key). Each restated what the file already says one screen up or below: thedefineHookexample'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: trueruns after commit;onErrordefaults to'abort'for every hook, set'log'on side-effect hooks), and Best Practices (object: '*'sparingly;async: truefor non-critical work). No link in the repo points at their anchors (git grepfor 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 (therunAsbullet 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
exclusionskey, no engine, data-layer or runtime change.Why each call in the template is the one on
mainRead on
origin/maind4fbb4d:events: ['beforeInsert', 'beforeUpdate']packages/spec/src/data/hook.zod.tsL389-L390 (the event enum)ctx.previousis the stored row on update,undefinedon insertpackages/runtime/src/sandbox/body-runner.tsL1481-L1483 (buildSandboxContext)ctx.inputon update carries only the changed fieldsunwrapProxyToPlain(engineCtx.input), L1411); taught at data-hooks.md'sctx.inputrowrunAs: 'system'elevates only the hook'sctx.api(isSystem: true,userIdkept)packages/spec/src/data/hook.zod.tsL720 (runAs),packages/objectql/src/hook-run-as.tsL24-L28 andderiveHookApiL196; pinned for a body bypackages/runtime/src/sandbox/hook-run-as.integration.test.tsctx.objectis the object the hook fired onctxtablectx.api.object(name).count({ where }), gated byapi.readbody-runner.tsL1167-L1168 (count),packages/runtime/src/sandbox/quickjs-runner.tsL705 (the read set the gate grants toapi.read)refusals+throw Object.assign(new Error(), { code })answered with the declared entryhook.zod.tsL665 (refusals),body-runner.tsL534-L539 (catch hands the throw toresolveHookRefusal),packages/runtime/src/sandbox/hook-refusal-catalogue.tsL160Tests
packages/runtime/src/sandbox/overlap-hook-template.integration.test.tsruns the template's hook in composition (realObjectQL, real SQLite driver, the realQuickJSScriptRunner,bindHooksToEngine; the harness ofhook-capability-denial-is-a-refusal.integration.test.ts) on aleave_requestobject withdatetimefields and, bound unchanged but forname,objectandDATE_ONLY = true, on aleave_dayobject withdatefields, under a middleware that stands in for private sharing (an unelevated read ofleave_requestsees only rows its user filed). It reads every refusal offmapDataError(status 409,declaredCode: 'OVERLAP', the declared sentence):start_atand moves into overlap is refused (stored row merged), and reviving a rejected row into overlap is refused;datefields: 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;datefields, written the long way: a day written as a full ISO instant, or as aDate, over a booked day is refused; the next day written that way is accepted and stored as2026-11-11;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.mjson the test's copy (the three copies are byte-identical), then restored; the test file's blob matchedHEADafter every run:start_at: DATE_ONLY ?tostart_at: String(r.end_at).length === 10 ?DATE_ONLY ?inclusive arm onstart_atDATE_ONLY ?inclusive arm onend_atwhere.id = { $ne: ctx.previous.id }$ltto$lteon the datetimestart_atarm$nin: offto$in: ['approved']Object.assign({}, ctx.previous, ctx.input)toObject.assign({}, ctx.input)runAs: 'system'lineReadings taken while writing it: the first two are probes, not kept. A row with a
nullstatus is counted by$nin(the engine does not drop it as SQLNOT INwould), and a+08:00input 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 toYYYY-MM-DDin the hook, and the mutation removing that slice stayed green (8 passed), because the drivers already read adatecomparand by the field's storage rule (temporalStorageFormin@objectstack/core, called fromdriver-sql,driver-turso,driver-memoryanddriver-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 pluscheck:test-typecheck): exit 0.Acceptance notes
skills/:@objectstack/runtime's suites read nothing outside the package, and the claim's file surface does not includescripts/cross-package-test-inputs.mjsorpackages/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.plugin-securityitself: the middleware filters exactly where the security middleware does (opCtx.context.isSystem,hook-run-as.tsL24-L27 says it short-circuits on that flag before every row-level gate). class: observation · reach: none · carrier: none.Skills readings
Measured with
node scripts/check-skills-token-ratchet.mjsonorigin/maind4fbb4d and on head e417fbf:skills/objectstack-data/references/data-hooks.mdlinesSKILL.mdfiles (lines)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):
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 aDateand 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 adatecomparand as the stored day, so a slice in the hook changed nothing a test can see (see Tests). The docs sentence "Adatevalue isYYYY-MM-DD, ten characters" is gone; the docs bullet now says the flag is needed because abefore*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 "fordatefields the author setsDATE_ONLY, which counts the end day as booked".name,objectand fields (datefields: setDATE_ONLY)"; the docs say "rename itsname(the refusal's translation key follows it), itsobjectand its fields".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 (--prreads it).Tests 8 passed (8);pnpm --filter @objectstack/runtime typecheckexit 0 (withNODE_OPTIONS=--max-old-space-size=4096; the default heap rancheck:test-typecheck's tsc out of memory, no diagnostics);pnpm check:query-options-erasureexit 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/objectstackat 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/runtimeclosure,@objectstack/client-react..., formula and lint underos-verify-lock. 96 exit 0 on the first pass, includingcheck:skill-examples(theos:checkblock type-checks),check:doc-anchors,check:doc-formula-expressions, the skills token ratchet andcheck:query-options-erasure.pnpm check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET, packages withoutdist/); afterturbo run buildof the whole tree under the lock (exit 0) it exits 0: 123 require entry points across 72 packages load.--ranreconciliation: "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 filepm-orders/skills/32979-gates-ran-r2.txt.Signed: domain:skills#1 · os-project-manager · session_01FEkQ5kKeYvkiYDEFCNhNhU
Generated by Claude Code