Repository navigation
Migrate the two in-repo hook examples (app-crm OpportunityStageHook, app-todo taskHook) to defineHook() so the platform's own examples match what the published data skill teaches (#14685 item 1, ruled A) #14850
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Sep 4, 2026 Triage: routed
domain:spec·Task·priority:p3. Ruled already — this is execution, not a fork.The lane table routes
examples/*by the subsystem the example exercises. What these two files exercise is the data-hook authoring contract:defineHook()is apackages/specexport (packages/spec/src/data/hook.zod.ts:1016) and the card's first instruction is to confirm the export path the package index actually exposes. ⇒domain:specowns the shape being adopted. ⛔ Notdomain:skills: the skill text is unchanged by ruling A, and the card says so.priority:p3— nothing is broken; the platform's own examples simply contradict what the published skill teaches.⚠️ The reason it is not p4 is the audience: the two real hooks in the repo are the only counter-examples an agent can read, and the card's own measurement is thatdefineHook(has 0 callers outsidepackages/specandpackages/cli. An agent that greps for usage finds the bare-literal form and copies it.Taskrather thanBug: nothing violates a contract. Both hooks are valid; they are written in the older of two valid spellings.⭐ The card is fully specified and its fences are good — carry them into the dispatch verbatim rather than restating:
- Keep every field of both hooks byte-identical inside the wrapper.
- If
ResolvedHookdiffers fromHookin a way the consumingdefineStack({ hooks })call or a test asserts on, ⛔ say so in the PR body with thefile:lineand do not widen the change to fix a schema.⚠️ This is the one branch that could turn an S card into something else; a dev that quietly "fixes" the schema has left the ruling. skip-changeset(examples release nothing), and let the gate say so rather than asserting it.
⛔ Out of scope and ruled so: the skill text, any other example, the
defineHookimplementation.⚠️ The threefile:linereadings are fromorigin/mainf3ae441fa2(2026-09-03). Locate both hook objects by symbol (OpportunityStageHook,taskHook) at dispatch; and re-run thedefineHook(caller count, because if another card has since adopted it the "0 callers" framing in the PR body would be wrong.
Generated by Claude Code
Claim: session_01MkQhmuuJAVDjmeWNixwDDH — branch
claude/issue-14850-define-hook-examplesClause-②: no
⚠️ TheClause-②line above is the dispatching seat's declaration and the round ⛔ re-derives it from what it actually ships. Reasoning: wrapping two example hook literals in an existing exported factory adds no authorable key and moves no accept set —defineHookis already exported and already what the published skill prescribes. If the round measures otherwise (for instance if adoptingResolvedHookforces a consuming declaration to change), it says so with the measurement and this line is corrected on the card, ⛔ not worked around.Dispatched by the
domain:specexecution seat at 2026-09-10T19:22Z. Theos-devround inherits this claim and assignee: ⛔ no secondClaim:, ⛔ never writes the assignee, ⛔ never yields the card.Declared file face:
examples/app-crm/src/hooks/opportunity.hook.ts,examples/app-todo/src/objects/task.hook.ts, plus a changeset. Read-only onpackages/spec/src/data/hook.zod.tsand the package index. Measured disjoint from the one other in-flight face in this lane (#17431:packages/spec/src/api/**,tsup.config.ts,package.json).Premise re-measured at claim time, ⛔ not inherited
origin/mainbea76c9280, read 2026-09-10T19:22Z. The card's table was taken atf3ae441fa2on 2026-09-03; all four rows still hold:examples/app-crm/.../opportunity.hook.ts 'export const OpportunityStageHook: Hook = {' : 1 examples/app-todo/.../task.hook.ts 'const taskHook: Hook = {' : 1 packages/spec/src/data/hook.zod.ts 'export function defineHook' : 1 defineHook( callers outside packages/spec and packages/cli : 0The 0 is the load-bearing one and it is what the ruling turns on: the platform's two real hooks are the only counter-examples an agent can read, and both contradict what the published skill teaches.
The ruling this card executes
Maintainer ruled A on 2026-09-03 (director record
5520452691, 「同意,然后执行契约复审」): thedefineHook()prescription stays in the published data skill, and one card migrates the two in-repo examples to match it. ⛔ The skill text is not touched by this card — the ruling keeps it as-is.
Generated by Claude Code
os-dev-report
{ "issue": 14850, "status": "done", "branch": "claude/issue-14850-define-hook-examples", "pr": "https://github.com/objectstack-ai/objectstack/pull/17532", "premise_still_valid": true, "summary": "Wrapped both in-repo hook examples in defineHook() — examples/app-crm/src/hooks/opportunity.hook.ts (OpportunityStageHook) and examples/app-todo/src/objects/task.hook.ts (taskHook) — three changed lines each (import, opening line, closing brace) with every field inside both objects byte-identical. All four premise rows re-measured green on my own merge base bea76c9280; the only drift is a line number (defineHook is at hook.zod.ts:1238, not the card's :1016 from f3ae441fa2), and everything was anchored by content. Export path measured rather than assumed: @objectstack/spec/data exposes defineHook as a runtime value (typeof function; positive control HookSchema function, negative control undefined) and re-exports it in dist/data/index.d.mts on the same line as Hook and ResolvedHook under a minified alias — which is why a naive grep for 'declare function defineHook' in that subpath finds nothing. ResolvedHook DOES differ from Hook (z.output vs z.input: HookSchema defaults priority=100 and async=false, so both are required on ResolvedHook and optional on Hook) and the difference is observable at runtime — post-change taskHook carries priority=100 async=false where the bare literal carried undefined for both — but no consuming site depends on it: allHooks becomes ResolvedHook[] and still satisfies defineStack({hooks}), the crm test reads .name, the todo test stringifies .handler (passed through by reference), and bindHooks re-parses idempotently. NO cast and no schema widening were needed anywhere. Carries the skip-changeset label rather than a changeset file, measured: both example packages are private:true, neither is in .changeset config fixed group, neither hook symbol appears in any published dist (positive control: defineHook found in packages/spec/dist/index.d.ts by the same grep), and pr-automation.yml names 'examples/' as the PREFERRED skip-changeset case. PR is draft, not enqueued, not armed, no approving review. Clause-② re-derived as 'no' and independently confirmed by check-clause2-carriers.mjs --pair 17532 (exit 0: declaration readable in the fixed spelling, both carriers agree, diff carries no widening tell).", "tests": "ALL verdicts are the command's own exit code captured before any pipe; all readings taken at final commit 6bb5dd1062, tree clean. TYPECHECK (load-bearing, examples as consumers): 'pnpm --filter @objectstack/example-crm --filter @objectstack/example-todo typecheck' VERDICT command-exit 0 — both 'tsc --noEmit' echoed in the output ('Scope: 2 of 81 workspace projects'), so this is not a zero-match filter run. TESTS: same filter pair, 'test' VERDICT command-exit 0 — app-crm 'Test Files 5 passed (5) / Tests 45 passed (45)', app-todo 'Test Files 4 passed (4) / Tests 106 passed (106)'. VALIDATE (objectstack validate, both apps): VERDICT command-exit 0, '✓ Validation passed' for each; remaining warnings are pre-existing field-liveness/flow-organization notes naming no hook. LINT: full repo-wide union 'pnpm lint' (= eslint . --no-inline-config) VERDICT command-exit 0 in 1m48s — NOT narrowed, so no narrowing evidence is owed. GATE FAMILIES: derived mechanically with 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' (no hand-built path list; its stderr names objectstack-ai/objectstack at 6bb5dd1062 and the --repo assertion holds); 33 families, all 33 run, 33 exit 0; reconciled with --ran using lines spelled COMMAND then a double colon then the word exit then CODE: '33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN … a DERIVED zero — all 33 recorded an exit code and none of them is 3'. Also ran, because this diff has a prose twin: 'pnpm check:skill-examples' exit 0 (258 marked blocks across 106 files type-check) and 'node scripts/check-corpus-claim-drift.mjs' exit 0. NOT MEASURED then MEASURED: 'pnpm check:dual-build-cjs-loads' first returned exit 3 'PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/' (ten packages outside this card's dependency closure). Classified NOT MEASURED, not red, and NOT reported as a failing check; rather than declare a narrowing I built the full tree ('turbo run build --filter=!@objectstack/docs', 73/73 successful) and re-ran it — exit 0, '104 published require entry point(s) across 67 package(s) load'. ABLATION A1 (type layer), predicted direction stated BEFORE running: RED. Injected an unknown key into the defineHook literal; mutation proved ON DISK before the run — anchor count 1, injected marker count 1, 'git diff --stat' showing '1 insertion(+)', blob moved 40131dacf5 to 86a0fe3465 — never inferred from an editor's exit code. OBSERVED RED, tsc exit 2: \"src/hooks/opportunity.hook.ts(14,3): error TS2353: Object literal may only specify known properties, and 'ablationUnknownKey' does not exist in type\". ABLATION A2 (runtime layer), three legs, predicted THROW under mutation and NO_THROW on the pre-change control: leg 0 post-change unmutated yields NO_THROW name=task_logic priority=100 async=false; leg 1 post-change with name broken to 'Task Logic' yields THROW ZodError invalid_format pattern /^[a-z_][a-z0-9_]*$/; leg 2 PRE-change bare-literal form checked out from bea76c9280 with the IDENTICAL mutation yields NO_THROW name='Task Logic' priority=undefined async=undefined. Leg 2 is the measurement of what the ruling buys: under the old spelling a schema-forbidden hook name is accepted silently at author time. RESTORATION: both ablations ran under 'trap … EXIT INT TERM' with absolute paths seeded from 'git rev-parse --show-toplevel', restoring via 'git checkout HEAD -- PATH' (never a bare 'git checkout --'); proved by BLOB EQUALITY against HEAD (40131dacf5e63039140f5e8ff2d3956af2798b30 and 34ffb8c48b1ec47e6d0214a0f89e43e928e89aac both matched), not by an exit code, with 'git diff HEAD' and 'git status --porcelain' each empty afterwards — leg 2 additionally staged content via 'git checkout bea76c9280 -- PATH', and the porcelain check confirms the index was reset too. Final 'git status --porcelain' empty; no ablation artifact left in the tree. HEAVY RUNS all went through 'scripts/pm/os-verify-lock.sh' with a stable OS_VERIFY_LOCK_SLOT=issue-14850, verdicts read from its own 'VERDICT command-exit' line. ONE SELF-INFLICTED RED, discarded as NOT MEASURED: my first closure build passed '--concurrency=2' after '--filter', which pnpm forwarded into @objectstack/cli's build script ('sh: 1: Syntax error: word unexpected', exit 2). That is my own flag misuse, not a repo defect; re-run correctly via turbo, exit 0.", "mcp_calls": "0 — no MCP GitHub call was made for the whole run; the card, its comments, PR creation, labelling and every read-back went through the repo-scoped REST channel (probed green at round start: GET /repos/objectstack-ai/objectstack HTTP 200), and code reads went through git.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: content/docs/automation/index.mdx:14 carries an os:check fenced block that reproduces OpportunityStageHook in the old bare-literal form and introduces it as '(from the CRM example app)' — a THIRD in-repo reading of the very hook this card migrates, so after this PR the prose page and the app it names use different spellings. Not filed because it clears none of the three filing classes: not (a) — copy the snippet and it compiles and runs, and both gates that read it are green here (check:skill-examples type-checks 258 marked blocks; check-corpus-claim-drift reports no unpinned claim site); not (b) — the snippet is already not a byte-exact quote (it drops the licence header and the docblock) and the gate that exists to pin corpus claims does not pin this one; not (c) — the bare literal is honoured by the runtime, which is the declared boundary of the metadata-trap class, so this is recorded as a boundary rather than an expansion of it. The bounded in-place-fix exemption also fails its fourth condition: content/docs/** would pull in a docs validation surface this change set does not otherwise schedule, and both the card and the dispatch fence this PR at two files. Successor named: whoever next edits content/docs/automation/index.mdx, or the docs half of the #14685 / #14292 programme. Surfaced in the PR's 验收备注 because 'you migrated the CRM hook but not the copy of it in the docs' is the first question a reviewer will reasonably ask.", "noted, not filed: CONFLICT between the dispatch order and the card, reported rather than silently resolved. The dispatch listed 'a changeset' among the deliverables and the seat's claim comment declared a file face of 'plus a changeset'; the card body says to apply skip-changeset and let the gate say so. Measurement resolves it to skip-changeset — both example packages are private:true, neither sits in .changeset/config.json's fixed group, neither hook symbol reaches any published dist (with a positive control proving the grep works), and pr-automation.yml's own guidance names 'examples/, tests-only, and the like) then apply the skip-changeset label, marked PREFERRED'. This also matters to the gate arithmetic: because no changeset file exists, the 13 families dispatch-gates reports as 'apply once this card's changeset exists' stay non-applicable rather than unrun. The label was written additively (POST .../labels, HTTP 200) and read back afterwards — ['size/s','skip-changeset'], nothing stripped by the size labeler's PUT.", "noted, not filed: line-number drift in the card's own table — defineHook now sits at packages/spec/src/data/hook.zod.ts:1238, not the :1016 recorded at f3ae441fa2. Content, not position, was used as the anchor throughout, so nothing depended on it; recorded only so the next reader of the card is not sent to the wrong line." ] }
Generated by Claude Code
Opened by the skills lane seat (session
session_01LraLgQVGq8egUwfYZpbYt1) as the post-ruling step of decision card #14685 item 1 (DATA-F-07), ruled A by the maintainer on 2026-09-03 (recorded by the director seat, comment 5520452691: 「同意,然后执行契约复审」 adopting the lane's recommendation). Unassigned; nodomain:*label by design — the files live underexamples/, notskills/, so triage routes it.The ruling, verbatim from the record
Measured at
origin/mainf3ae441fa2(2026-09-03T05:26Z)examples/app-crm/src/hooks/opportunity.hook.ts:9export const OpportunityStageHook: Hook = {— bare literalexamples/app-todo/src/objects/task.hook.ts:50const taskHook: Hook = {— bare literalpackages/spec/src/data/hook.zod.ts:1016export function defineHook(config: Hook): ResolvedHook {— the import-time-validated authoring path the skill prescribesdefineHook(outsidepackages/specandpackages/clipackages/**,examples/**,apps/**skills/objectstack-data/references/data-hooks.mdteachesdefineHook()(the prescription the ruling keeps); the two real hooks in the repo are the only counter-examples an agent can read.Scope
defineHook(...)and import it from@objectstack/spec(confirm the export path the package index actually exposes — read it, do not assume). Keep every field of both hooks byte-identical inside the wrapper.ResolvedHookdiffers fromHookin a way the consumingdefineStack({ hooks })call or a test asserts on, say so in the PR body with the file:line; do not widen the change to fix a schema.typecheck/testscripts by name, andos validateon each app if that is how they are checked) plusnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsover the change set.Fixesthis card. Not a governed surface. A changeset is not expected (examples release nothing) — applyskip-changesetand let the gate say so.⛔ Not in scope: the skill text (unchanged by ruling A), any other example, the
defineHookimplementation.Refs: #14685 (batch 3, item 1) · #14297 (the data-skill flight that deferred DATA-F-07) · #14292 (programme anchor) ·
packages/spec/src/data/hook.zod.ts:1016.