Repository navigation
Commit b3f7344
feat(spec): a hook declares its refusal catalogue, and its body refuses by code, answered in the caller's language (#26831)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBNwSiQjPk36YGpZnrxAs9yW)_
Fixes #26170
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #26834
## 维护者速读
- 改了什么:按裁决 A,Hook 新增 `refusals: { CODE: { message, status? } }` 拒绝目录;沙箱
body 只按 code 拒绝,运行时用目录里的消息(经 `hooks.HOOK.refusals.CODE.message`
按调用方语言翻译)和状态码作答。
- 为什么改:hook 的业务拒绝是用户最常读到的"不行",却是唯一没有翻译键的拒绝入口(校验规则、flow 拒绝都已有)。
- 风险与回滚:只对声明了 `refusals` 的 hook 生效,未声明的 hook 行为完全不变;没有 js body 的
hook(进程内 handler、表达式 body)声明 `refusals` 会在保存时被拒;回滚即 revert 本 PR。新增翻译分组
`hooks` 连带改了控制台翻译预览、i18n 文档和 walk-parity 夹具。
-
先看:`packages/runtime/src/sandbox/hook-refusal-catalogue.ts`、`packages/spec/src/data/hook-refusal-codes.ts`、`hook.zod.ts`
的 `refusals`。
- 两分钟验证:`cd packages/runtime && npx vitest run
src/sandbox/hook-refusal-catalogue.test.ts`(17 项:声明码、翻译、协商、未声明码
500、宿主错误透传)。
- 需要你做的:本 PR 触及 `skills/**`(受治理面),需维护者合并;拒绝消息暂不带参数(`{{ }}`
现在保存时即拒,以后加参数是纯增量),见 Acceptance notes。
## What changes
Ruling A (6431311681), as bound by the order:
- **`@objectstack/spec`**
- `HookSchema.refusals`: a record keyed by a SCREAMING_SNAKE code
(`HOOK_REFUSAL_CODE_PATTERN`) of `HookRefusalSchema` = `{ message,
status? }`, closed, `status` 400–499 (default 400). Aliases name
`reason`/`text`/`description` → `message` and `httpStatus`/`statusCode`
→ `status`.
- A catalogue is answered only on the sandboxed `body` path, so
`refusals` on a hook with no `js` body (an in-process `handler`, an
expression body) is refused at save, at `refusals`, naming the remedy
(move the refusing logic into a `js` body, or drop `refusals`).
- A refusal `message` is plain text: a `{{ }}` hole is refused at save,
by a pattern the published JSON Schema carries too, so a later params
slot stays additive.
- Save-time refusal (`refuseUndeclaredLiteralRefusalCode` superRefine):
when a hook declares `refusals` and its js body throws a code literally
(`{ code: 'X' }` in a `throw`, or `e.code = 'X'` on a name thrown bare)
that the catalogue does not declare, the parse fails at `body.source`.
The reader (`hook-refusal-codes.ts`, an import-free leaf) masks comments
and string contents first, so a `throw` or `code:` written inside either
is never read.
- Translation: a new `hooks` group, `hooks.HOOK.refusals.CODE.message`,
on the per-app and platform faces, and `hookRefusalMessageKey`. The
group counts in prose move fourteen → fifteen.
- `HookContext.session.locale` declares the caller's locale.
- Liveness rows `hook/refusals` (with `message` and `status` children)
and `translation/hooks`.
- **`@objectstack/objectql`**: `buildSession` copies
`ExecutionContext.locale` onto a session that exists (a locale alone
never makes one).
- **`@objectstack/runtime`**
- `hook-refusal-catalogue.ts`: `resolveHookRefusal` answers a sandboxed
body's throw. A declared code becomes a `SandboxError` with the
translated (else declared) message as `innerMessage` and `userMessage`,
the declared code, and the entry's status. Any other throw from the body
(an undeclared code, or none) becomes a 500 fault naming the hook and
the code. The locale is negotiated with `resolveBundleLocale` against
the i18n service's locales, read lazily at refusal time.
- `body-runner.ts` calls it in the hook catch; `app-plugin.ts` and
`app-artifact-handlers.ts` pass the lazy `i18n` reader.
- `quickjs-runner.ts`: a marshalled host error carries a non-enumerable
marker of the `message` and `code` it crossed with, so an error a
`ctx.api` call raised and the body let leave unchanged passes through
untouched (`isHostRelayedSandboxError`); one the body rewrote is the
body's own.
- **`@objectstack/cli`**: `os i18n extract` walks `hooks[].refusals`
(one key per declared entry) and `os i18n check` counts them in a new
`hook` coverage bucket.
- **`@objectstack/lint`**: `translation-target-unknown` judges
`hooks.NAME` against the stack's hooks (sibling packages' included) and
`hooks.NAME.refusals.CODE` against that hook's declared codes, with a
nearest-name suggestion.
- **Ripples of the new group**: the console translation preview gains a
`hooks` category (en/zh labels) and the loader's group list gains
`hooks` (`ui/`); `i18n-standard.mdx` and `ui/translations.mdx`; the
walk-parity fixture authors one hook with a catalogue;
`CARD_PROPERTY_COUNTS.hook` 22 → 23.
- **Skills**: `skills/objectstack-data/references/data-hooks.md` teaches
`refusals`.
- Changeset: minor for spec, runtime, objectql, cli, lint; patch for
`@object-ui/i18n` and `@object-ui/app-shell` (the loader's group list
and the translation preview move).
## Tests
- `packages/spec/src/data/hook-refusals.test.ts` (new, 52): the schema,
the save-time refusal, `refusals` refused beside a `handler` or an
expression body, a `{{ }}` hole refused, the literal reader (positive
and negative shapes, comments and strings masked), the sentences, the
translation path, `session.locale`.
- `packages/runtime/src/sandbox/hook-refusal-catalogue.test.ts` (new,
17): through the real factory and QuickJS runner, read off
`mapDataError`: declared code with default and declared status, zh-CN
translation, `zh` → `zh-CN` negotiation, fallback, no service,
undeclared and missing code as 500, a hook without `refusals` unchanged,
a crash unchanged, a host error passed through, a host error re-coded by
the body judged.
- `packages/cli/test/i18n-hook-refusal-coverage.test.ts` (new, 7):
walker, coverage bucket, skeleton parses through the schema.
- `packages/lint/src/validate-translation-references.test.ts` (+5): the
hooks arm.
-
`packages/objectql/src/engine-execution-plumbing.session-locale.test.ts`
(new, 3).
- Ablation (on-disk mutation, restored after): with the body runner's
`resolveHookRefusal` call replaced by `err`, 9 of the 17 runtime tests
fail; with the host-relay marker not recorded, the passthrough test
fails.
## Acceptance notes
- **The OPEN item (throw shape versus `api.refuse`), measured:** the
existing throw shape carries `code` across the VM already
(`__errorInfo`), so the body spells a refusal `throw Object.assign(new
Error(), { code: 'X' })`, and no new API was added. **Params are not
carried:** a declared message is plain text, translated whole, like a
validation rule's message (no parameter contract). I did not treat this
as a fork, because the ruling's catalogue has no param slot either. A
`{{ }}` hole is refused at save now, so a later params slot (e.g.
`params` on the thrown error) is purely additive.
- **On the wire** an author's code is not an ADR-0112 registered one, so
the envelope's `code` is the status's registered code
(`VALIDATION_ERROR` for 400, `RESOURCE_CONFLICT` for 409) and the
declared code rides as `declaredCode`, the existing demotion contract.
The skills doc says so.
- **Enforcement scope:** the undeclared-code refusal applies only to a
hook that declares `refusals`; every existing hook is answered as
before, so the change is non-breaking. A catalogue is served only on the
sandboxed `body` path, so `refusals` on a hook with no `js` body is
refused at save rather than wired into the in-process `handler` wrap
(that would widen past the card).
- **Outside the order's file surface, and why:**
`packages/objectql/src/engine-execution-plumbing.ts` (the producer of
`session.locale`; the runtime had no other way to read the caller's
locale), `packages/runtime/src/app-plugin.ts` and
`app-artifact-handlers.ts` (wiring the i18n reader), the `ui/`
translation-preview and loader lists (an exhaustive `satisfies Record`
over the translation groups fails type-check otherwise), the i18n docs,
the walk-parity fixture, and the metadata-protocol property count.
- **Studio hook form:** `refusals` is not offered. The form's `record`
editor stamps each entry's key into the entry, and a refusal entry is
closed at `message`/`status` (the key is the code), so a stamped key
would be refused. The form gate's ledger records this omission. Offering
it needs a key-only record control, filed as #26835.
- **Skills, line and token counts:** `data-hooks.md` 998 → 972 lines;
the `skills/objectstack-data` markdown package 3849 → 3823 lines. The
new section was paid for under the file's token ceiling by deleting
error-handling examples the handler and `onError` sections already carry
(10063 of 10066 tokens).
- No `!` in the title and no `(narrowing)` arm, so no full dogfood run
is owed.
## Verification
**At head `e55978368d`.** Main is merged through
`scripts/pm/os-regen-merge.sh` three times, last at `a20824f366` with
#26859. Each merge took main's side of the generated artifacts and
regenerated them as their own commit. Every run is under
`os-verify-lock`.
- **r1 change (`c19dbcd0d4`):**
- `hook-refusals.test.ts`: 52 passed.
- With `hook.test.ts`, the translation tests and the ADR-0122 pin: 342
passed.
- `spec typecheck`: exit 0.
- `check:generated --fix`: wrote `api-surface`, `export-origins` and the
`hook.mdx` reference.
- I first tried a refinement for the hole refusal. The build refused it,
because the published JSON Schema could not carry it
(dropped-refinements ledger, 5 sites). A pattern carries it, so no
ledger rows are added.
- `check-skills-token-ratchet`, `check-changeset-fixed`,
`check-changeset-no-major` and `check-empty-changeset`: exit 0.
- **On the merged tree:**
- Full `pnpm build`: exit 0.
- `spec check:generated`: all 14 current after the regen commit.
- `actionKeys.pin.test.ts`: 19 of 19 (#26859).
- The nine whole packages of CI's `Test Core (6/8)` shard: 44 of 44
turbo tasks, on the `8f7675f85b` tree.
- Suites: spec 22093 passed (736 files); runtime 7474 passed (461
files); cli 6460 passed (519 files); lint 7453 passed (169 files);
objectql 8846 passed (476 files). `check:i18n` exit 0.
- Dispatch gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 130 families, and all 130 were run
with each exit code recorded before any pipe. All 130 exited 0. `--ran`
reconciliation: 130 derived, 130 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.
- **CI at this head:** all 62 check-runs completed, 57 success and 5
opt-in skips.
- **Before r1** (head `78dc14528e`, main merged at `c8583f80ac`):
- Suites: spec 22063, runtime 7465, lint 7435, objectql 8846, cli i18n
13, ui 1445.
- Dispatch gates: 130 derived, 130 run, 0 NOT-MEASURED, 0 UNRUN.
- **Dogfood:** not owed (no `!`, no narrowing arm).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a90163d commit b3f7344
52 files changed
Lines changed: 1784 additions & 99 deletions
File tree
- .changeset
- content/docs
- protocol/kernel
- references
- api
- data
- system
- ui
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages
- cli
- src/utils
- test
- lint/src
- metadata-protocol/src
- objectql/src
- runtime/src
- sandbox
- spec
- api-surface
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- liveness
- state-counts
- src
- data
- system
- scripts/fixtures/i18n-walk-parity
- skills
- objectstack-data/references
- objectstack-platform/references
- ui/packages
- app-shell/src/views/metadata-admin
- previews
- i18n/src/utils
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
406 | 406 | | |
407 | 407 | | |
408 | 408 | | |
409 | | - | |
| 409 | + | |
410 | 410 | | |
411 | 411 | | |
412 | 412 | | |
| |||
496 | 496 | | |
497 | 497 | | |
498 | 498 | | |
499 | | - | |
| 499 | + | |
500 | 500 | | |
501 | 501 | | |
502 | 502 | | |
| |||
1077 | 1077 | | |
1078 | 1078 | | |
1079 | 1079 | | |
| 1080 | + | |
| 1081 | + | |
1080 | 1082 | | |
1081 | 1083 | | |
1082 | 1084 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1782 | 1782 | | |
1783 | 1783 | | |
1784 | 1784 | | |
| 1785 | + | |
1785 | 1786 | | |
1786 | 1787 | | |
1787 | 1788 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
17 | | - | |
| 16 | + | |
| 17 | + | |
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| 45 | + | |
45 | 46 | | |
46 | 47 | | |
47 | 48 | | |
| |||
79 | 80 | | |
80 | 81 | | |
81 | 82 | | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
82 | 90 | | |
83 | 91 | | |
84 | 92 | | |
| |||
126 | 134 | | |
127 | 135 | | |
128 | 136 | | |
| 137 | + | |
129 | 138 | | |
130 | 139 | | |
131 | 140 | | |
| |||
164 | 173 | | |
165 | 174 | | |
166 | 175 | | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | | - | |
| 26 | + | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
135 | | - | |
| 135 | + | |
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
| |||
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
162 | | - | |
| 162 | + | |
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
| |||
0 commit comments