Repository navigation
feat(spec): a hook declares its refusal catalogue, and its body refuses by code, answered in the caller's language - #26831
Conversation
Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
The save-time reading now masks comments and string contents first, so a throw or code written inside either is never read as one. Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
…opies The console's translation preview and loader group list, the i18n docs, the walk-parity fixture and the served hook property count move with the new group. Hook refusal pins read the declared code off declaredCode. Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
… ceiling Pays for the new section by deleting the error-handling examples the handler and onError sections already carry, and folds the testing pointer into the references list. Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
…e keys; offer refusals in the hook form Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
…of a record control that would stamp a refused key Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
…-ivr552 # Conflicts: # packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts
Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 157 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a252eb72bc469ef395419cb3019956476318a813 && git checkout a252eb72bc469ef395419cb3019956476318a813
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e6c440900072a2d2423474962db9c7db6011b845 a1dbd6eaa144abbeecd35b2dd82a46a51c45e2ae && git checkout -B drift-repro e6c440900072a2d2423474962db9c7db6011b845 && git merge --no-ff a1dbd6eaa144abbeecd35b2dd82a46a51c45e2ae
node scripts/docs-audit/affected-docs.mjs --json e6c440900072a2d2423474962db9c7db6011b845
|
…mitted error code Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read: card #26170 body and all 6 comments (decision 6430864109, brief 6431195984, ruling 6431311681 letter A, claim 6442821427, os-dev-report 6449646620); PR #26831 body, 52-path file list, the three-dot diff at this head (merge-base ① Derived judgmentsGate readings on this head. Green: Auto Label, Dogfood Regression Gate (umbrella), Build Docs, Build UI Deps, Plan Test Core shards, Flag docs affected, Check Documentation Links, Lint · CI/release/engine gates, Lint · tooling self-tests, Lint · query-options erasure, Lint · repo & tooling gates, Lint · slot lookup & corpus, Lint · ESLint, Type Check · source gates, Check PR Size, Spec property liveness, Check Changeset, Governed Surface Queue Guard, PR-body guards, filter (21). Red: Ruling A, exactly? Declaration shape ✓ — Save-time literal check — sound. Runtime — loud where the catalogue speaks. Translation and locale — fail-safe. No params — extensible later, with one caveat. The declared Off-surface files — each necessary, none creep. objectql Studio hook form. Skills paths — true, with one over-claim read elsewhere. Both ② Semver level
③ Boundary flagsFAIL-1 — the head's gate is red, and its tests are unmeasured. FAIL-2 — declared ≠ enforced on the in-process Non-blocking, answered or escalated:
Implemented-by: VERDICT: FAIL |
… hole in a refusal message (#26170) Contract review FAIL-2: a catalogue beside an in-process `handler` (or an expression body) was saved, its translations demanded by `os i18n`, and never answered. HookSchema now refuses it at `refusals`, naming the remedy. A refusal message is plain text; a `{{ }}` hole is refused by a pattern the published JSON Schema carries too, so a later params slot stays additive. The liveness note no longer claims the build lowers every handler to a body. The changeset names @object-ui/i18n and @object-ui/app-shell, which move. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem
Triage: this PR conflicts with
|
…-ivr552 # Conflicts: # content/docs/protocol/kernel/i18n-standard.mdx # packages/lint/src/validate-translation-references.test.ts # packages/spec/src/system/translation.zod.ts # ui/packages/i18n/src/utils/spec-translations.ts
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem
…groups now that `hooks` and `books` both stand Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfyVQuRsL6SRGuKCem
|
Main merged in; the conflict is resolved. New head
Generated by Claude Code |
|
Seat reading of the main merge · The triage note 6457569500 asked for a merge of
So the merge moves only regenerated counts and baselines. The PR's own accept set, published surface and semver level are unchanged. Contract review round 3 (6452063072) and ACCEPT 6453820397 stand for Landing state:
The seat follows CI on Generated by Claude Code |
Contract reviewServed-tier: Round 4 of 6449923427 / 6451717284 / 6452063072 (PASS on ① Derived judgmentsGate readings on this head (63 check-runs, all The merge The one line the merge left wrong — non-blocking, must fix. The regeneration The test change Round-3 judgments re-read on this head, unchanged. Ruling A shape: ② Semver levelUnchanged by the delta and right: ③ Boundary flagsNo FAIL. Every flag answered or escalated:
Implemented-by: VERDICT: PASS Generated by Claude Code |
Requested by Yibo Wang · project thread
Fixes #26170
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #26834
维护者速读
refusals: { CODE: { message, status? } }拒绝目录;沙箱 body 只按 code 拒绝,运行时用目录里的消息(经hooks.HOOK.refusals.CODE.message按调用方语言翻译)和状态码作答。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、宿主错误透传)。skills/**(受治理面),需维护者合并;拒绝消息暂不带参数({{ }}现在保存时即拒,以后加参数是纯增量),见 Acceptance notes。What changes
Ruling A (6431311681), as bound by the order:
@objectstack/specHookSchema.refusals: a record keyed by a SCREAMING_SNAKE code (HOOK_REFUSAL_CODE_PATTERN) ofHookRefusalSchema={ message, status? }, closed,status400–499 (default 400). Aliases namereason/text/description→messageandhttpStatus/statusCode→status.bodypath, sorefusalson a hook with nojsbody (an in-processhandler, an expression body) is refused at save, atrefusals, naming the remedy (move the refusing logic into ajsbody, or droprefusals).messageis plain text: a{{ }}hole is refused at save, by a pattern the published JSON Schema carries too, so a later params slot stays additive.refuseUndeclaredLiteralRefusalCodesuperRefine): when a hook declaresrefusalsand its js body throws a code literally ({ code: 'X' }in athrow, ore.code = 'X'on a name thrown bare) that the catalogue does not declare, the parse fails atbody.source. The reader (hook-refusal-codes.ts, an import-free leaf) masks comments and string contents first, so athroworcode:written inside either is never read.hooksgroup,hooks.HOOK.refusals.CODE.message, on the per-app and platform faces, andhookRefusalMessageKey. The group counts in prose move by one: fifteen → sixteen per-app and sixteen → seventeen on the platform face, after the main merge that brought feat(spec,rest,cli): a doc book's title and section titles are translatable —books.<name>in the translation bundle #26819'sbooksgroup. One TSDoc line (translation.zod.ts~:2082,TranslationDataSchema) still says "Fifteen groups"; it is fixed in a trailing docs PR after this one lands (contract review round 4, 6458660736).HookContext.session.localedeclares the caller's locale.hook/refusals(withmessageandstatuschildren) andtranslation/hooks.@objectstack/objectql:buildSessioncopiesExecutionContext.localeonto a session that exists (a locale alone never makes one).@objectstack/runtimehook-refusal-catalogue.ts:resolveHookRefusalanswers a sandboxed body's throw. A declared code becomes aSandboxErrorwith the translated (else declared) message asinnerMessageanduserMessage, 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 withresolveBundleLocaleagainst the i18n service's locales, read lazily at refusal time.body-runner.tscalls it in the hook catch;app-plugin.tsandapp-artifact-handlers.tspass the lazyi18nreader.quickjs-runner.ts: a marshalled host error carries a non-enumerable marker of themessageandcodeit crossed with, so an error actx.apicall raised and the body let leave unchanged passes through untouched (isHostRelayedSandboxError); one the body rewrote is the body's own.@objectstack/cli:os i18n extractwalkshooks[].refusals(one key per declared entry) andos i18n checkcounts them in a newhookcoverage bucket.@objectstack/lint:translation-target-unknownjudgeshooks.NAMEagainst the stack's hooks (sibling packages' included) andhooks.NAME.refusals.CODEagainst that hook's declared codes, with a nearest-name suggestion.hookscategory (en/zh labels) and the loader's group list gainshooks(ui/);i18n-standard.mdxandui/translations.mdx; the walk-parity fixture authors one hook with a catalogue;CARD_PROPERTY_COUNTS.hook22 → 23.skills/objectstack-data/references/data-hooks.mdteachesrefusals.@object-ui/i18nand@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,refusalsrefused beside ahandleror 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 offmapDataError: declared code with default and declared status, zh-CN translation,zh→zh-CNnegotiation, fallback, no service, undeclared and missing code as 500, a hook withoutrefusalsunchanged, 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).resolveHookRefusalcall replaced byerr, 9 of the 17 runtime tests fail; with the host-relay marker not recorded, the passthrough test fails.Acceptance notes
api.refuse), measured: the existing throw shape carriescodeacross the VM already (__errorInfo), so the body spells a refusalthrow 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.paramson the thrown error) is purely additive.codeis the status's registered code (VALIDATION_ERRORfor 400,RESOURCE_CONFLICTfor 409) and the declared code rides asdeclaredCode, the existing demotion contract. The skills doc says so.refusals; every existing hook is answered as before, so the change is non-breaking. A catalogue is served only on the sandboxedbodypath, sorefusalson a hook with nojsbody is refused at save rather than wired into the in-processhandlerwrap (that would widen past the card).packages/objectql/src/engine-execution-plumbing.ts(the producer ofsession.locale; the runtime had no other way to read the caller's locale),packages/runtime/src/app-plugin.tsandapp-artifact-handlers.ts(wiring the i18n reader), theui/translation-preview and loader lists (an exhaustivesatisfies Recordover the translation groups fails type-check otherwise), the i18n docs, the walk-parity fixture, and the metadata-protocol property count.refusalsis not offered. The form'srecordeditor stamps each entry's key into the entry, and a refusal entry is closed atmessage/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 Studio hook form:refusalscannot be authored — the record editor stamps the key into a closed entry; needs a key-only record control (follow-up owed by #26170) #26835.data-hooks.md998 → 972 lines; theskills/objectstack-datamarkdown package 3849 → 3823 lines. The new section was paid for under the file's token ceiling by deleting error-handling examples the handler andonErrorsections already carry (10063 of 10066 tokens).!in the title and no(narrowing)arm, so no full dogfood run is owed.Verification
At head
e55978368d. Main is merged throughscripts/pm/os-regen-merge.shthree times, last ata20824f366with #26859. Each merge took main's side of the generated artifacts and regenerated them as their own commit. Every run is underos-verify-lock.c19dbcd0d4):hook-refusals.test.ts: 52 passed.hook.test.ts, the translation tests and the ADR-0122 pin: 342 passed.spec typecheck: exit 0.check:generated --fix: wroteapi-surface,export-originsand thehook.mdxreference.check-skills-token-ratchet,check-changeset-fixed,check-changeset-no-majorandcheck-empty-changeset: exit 0.pnpm build: exit 0.spec check:generated: all 14 current after the regen commit.actionKeys.pin.test.ts: 19 of 19 (fix(ui): the action key inventory lists disabledReason #26859).Test Core (6/8)shard: 44 of 44 turbo tasks, on the8f7675f85btree.check:i18nexit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 130 families, and all 130 were run with each exit code recorded before any pipe. All 130 exited 0.--ranreconciliation: 130 derived, 130 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.78dc14528e, main merged atc8583f80ac):!, no narrowing arm).Generated by Claude Code