Skip to content

Commit f4755a6

Browse files
docs(skills): objectstack-data says which hook doors provide ctx.title (#24289) (#24504)
Fixes #24289 Clause-②: no **Tier H — the maintainer merges this PR.** The diff touches `skills/**`, a governed surface (AGENTS.md Prime Directive #14). No AI seat readies, queues, arms auto-merge on, or approves it. ## What this does This carries out ruling letter E on #24289 (comment 6261581954), item 2: the hook context type stays as it is, and the docs say which hook doors provide `ctx.title`. `skills/objectstack-data/references/data-hooks.md` listed `ctx.title(field?)` in the sandbox `ctx` table with no qualifier, so it read as a member of every hook context. One row changes, the `ctx.title` row. It keeps what it taught (the no-argument form, a formula `nameField`, the lookup form under `api.read`, `null` and never the id) and adds a per-door note: - `body`: has it (the recommended form). - a deprecated `handler`: gets it at runtime, but `HookContext` does not declare it, so an annotated handler fails with TS2339. Use `body`. - `ql.registerHook` and a bare `ObjectQL` `bindHooks`: do not provide it. Under ruling item 1 nothing else changes: `HookContextSchema` gets no `title`, there is no new context type, and there is no engine work. ## Each door, checked against `main` (`ce2e08ee3`, and again after merging `959ee317b`) | Door | Reading | Where | |:--|:--|:--| | `body` (sandbox) | `buildSandboxContext(..., buildTitleSurface(engineCtx, opts.ql, ...))` installs the accessor on every hook body | `packages/runtime/src/sandbox/body-runner.ts` (`boundBodyHandler`) | | `handler`, inline or named, when the runtime binds the stack's hooks | `bindAppArtifactHandlers` passes `wrapHandler: (handler, hook) => withInProcessHookTitle(handler, ql, ...)` to `ql.bindHooks`. The binder applies it after a string `handler` resolves, so inline and named handlers take the same wrap (PRs #24356 and #24423) | `packages/runtime/src/app-artifact-handlers.ts`; `packages/objectql/src/hook-binder.ts` (`wrapHandler`) | | `HookContext` type | `HookContextSchema` declares no `title` (zero hits for `title` in the file) | `packages/spec/src/data/hook.zod.ts` | | `ql.registerHook` | the engine never adds `title` to a hook context. The only producers of the accessor are `buildTitleSurface` and `withInProcessHookTitle` in `packages/runtime`, and `registerHook` goes through neither | `packages/objectql/src/engine.ts` | | bare `ObjectQL.bindHooks` | without `wrapHandler`, `resolveHandler` returns the resolved handler as it is (`if (!fn \|\| typeof opts.wrapHandler !== 'function') return fn;`), so nothing installs `title` | `packages/objectql/src/hook-binder.ts` | This is a code reading. No new runtime probe was run. The PR #24423 dev measured the last two doors with a bare-`ObjectQL` probe (`registerHook`, `bindInline` and `bindNamed` each read `typeof ctx.title === 'undefined'`). That reading is in the card's report comment 6257914461. ## Token and line budget (`skills/**`) - `skills/objectstack-data/references/data-hooks.md`: **998 → 998 lines**. Tokens (`check-skills-token-ratchet.mjs` `pricedTokens`) **10059 → 10064**, ceiling 10066 (headroom 2). The ceiling is unchanged. - Whole package, the sum of all `skills/**/SKILL.md`: **4394 → 4394 lines**. No `SKILL.md` is touched. - The new text is paid for by deleting text from the same row, not by re-wrapping it. Removed: the row's copy of the capability table's rule that the no-argument form needs no capability (the `api.read` row in "Capabilities — the complete list" already says so), and wordier phrasing ("including when it is", "evaluated server-side", "one `findOne`", two `await`s, "it never falls back to"). ## Changeset `.changeset/24289-hook-ctx-title-doors.md`: `'@objectstack/skills': patch`. That package publishes `skills/**` as its `dist/skills` (`packages/skills/scripts/sync-catalog.mjs`), so a text change here ships. The earlier skills docs PRs #24174 and #24262 shipped the same patch. No `skip-changeset`. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) derived 42 commands at `953159abe`, and the same 42 after the merge. I also ran `node scripts/check-changeset-fixed.mjs`, a roster family whose roster sits under `.changeset`. - `--ran`: **42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN**. Every exit code was recorded before any pipe. - First pass at `dcd4a8368` / `eee9ae8de`: `check-skills-token-ratchet` exited 1 (`data-hooks.md` 10127 / 10066, over by 61). The row was trimmed in `953159abe`. Five gates exited 3 on prerequisites (no `dist`). After a full `turbo run build --filter='!@objectstack/docs'`, all 43 exited 0. - At the final head `260ca2ea6`, the merge of `origin/main` `959ee317b`, I rebuilt (`turbo run build --filter='!@objectstack/docs'`, exit 0) and re-ran all 43: each exited 0, and `--ran` gave 42 / 42 / 0 / 0. `check-skills-token-ratchet` read `data-hooks.md` at 10064 / 10066. - Not run locally: `pnpm lint`, `test:repo` and CI's type-check lanes. CI owns those. ## Acceptance notes - **Wording difference from the ruling's letter:** ruling item 2 says `handler` is "available at runtime since PRs #24356 and #24423". The skill text says "gets it at runtime" and does not cite the PRs. Published skill text is customer-facing and carries no tracker numbers (AGENTS.md: the lesson goes into the text). The PRs are cited here instead. - **Edit scope:** the row is the only skill text changed. The "Hook Context API" blockquote above `interface HookContext` still calls a `body`'s `ctx` a "capability-gated subset" of the in-process context. `ctx.title` is one member a `body` has that `HookContext` does not declare. This PR leaves that blockquote alone, because ruling E scopes the edit to the context table row. - **Out of this PR:** ruling item 3, the pointer on objectstack-ai/hotcrm#2086, is not done here. That card is outside this dispatch's write budget. --- ## 维护者速读(草稿) **改了什么:** 只改技能文档 `data-hooks.md` 里 `ctx.title` 那一行,写明各个入口能不能用它:`body` 能用(推荐写法);已弃用的 `handler` 运行时能用,但 `HookContext` 类型里没有声明,写了类型注解会编译报错,建议改用 `body`;`ql.registerHook` 和不经 runtime 直接用 `ObjectQL` 绑定的钩子拿不到它。另加一个 `@objectstack/skills` 的 patch changeset。 **为什么改:** 按裁决 E(#24289 评论 6261581954)执行。原文没有任何限定,读起来像每个钩子上下文都有 `ctx.title`,实际并非如此。现在文档写的就是运行时真实提供的。 **风险与代价(含回滚):** 纯文档,不改类型、不改引擎,运行时行为不变。这一行的 token 由删掉同一行里的重复内容来抵,文件 token 仍在上限内(10064 / 10066),行数不变。回滚就是 revert 这个 PR。 **席位意见:** **你要做的:** 审阅这一行的措辞,同意就合并(Tier H,只能由你合并)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c1f4093 commit f4755a6

2 files changed

Lines changed: 10 additions & 1 deletion

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): `objectstack-data` says which hook doors provide `ctx.title` — a `body` has it, a deprecated `handler` gets it at runtime without `HookContext` declaring it, and `ql.registerHook` and a bare `ObjectQL` `bindHooks` do not provide it (#24289)
6+
7+
Clause-②: no
8+
9+
`skills/objectstack-data/references/data-hooks.md` listed `ctx.title(field?)` in the sandbox `ctx` table with no qualifier, so it read as a member of every hook context. It is not: a sandboxed `body` has it (the recommended form); an inline or named `handler`, which is deprecated, is handed it at runtime when the runtime binds the stack's hooks, but `HookContext` does not declare it, so a handler annotated with that type fails to compile; a hook registered with `ql.registerHook(...)`, or bound through `ObjectQL.bindHooks` with no runtime, gets no `ctx.title` at all. The row now says so. Text only; nothing an author writes changes, and no hook context changes.

‎skills/objectstack-data/references/data-hooks.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ The sandbox is handed a **JSON snapshot** of these (built by
366366
| `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. |
367367
| `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). |
368368
| `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. |
369-
| `ctx.title` | `(field?) => Promise<string \| null>` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. |
369+
| `ctx.title` | `(field?) => Promise<string \| null>` | **Name the record, not its id.** `ctx.title()` resolves this object's `nameField`, a **formula** included (no extra read); `ctx.title('account_id')` resolves a lookup's title (needs `api.read`). `null` when there is no title, never the id. **Per door:** `body` has it; a deprecated `handler` gets it at runtime, undeclared on `HookContext` (TS2339 when annotated) — use `body`; `ql.registerHook` and a bare `ObjectQL` `bindHooks` lack it. |
370370

371371
(Action bodies additionally receive `ctx.recordId` and `ctx.record`, and their
372372
wrap is `(async (input, ctx) => { … })(input, ctx)` — the action params arrive as

0 commit comments

Comments
 (0)