Skip to content

feat(spec): Operation Message Catalog gains refusal-situation keys for approvals recall and sharing write-denial - #12626

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-12493-operation-message-refusal-keys
Aug 27, 2026
Merged

feat(spec): Operation Message Catalog gains refusal-situation keys for approvals recall and sharing write-denial#12626
os-zhuang merged 2 commits into
mainfrom
claude/issue-12493-operation-message-refusal-keys

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#12493

Clause-②: yes; dispatched at review tier.

The spec-side half of the shape-A ruling on the approvals-recall card (triage 2026-08-26). Two measured hardcoded-English FORBIDDEN refusals gain user-situation keys in the Operation Message Catalog, in all four platform locales, so their emitters can later call renderOperationMessage instead of throwing raw English. Scope held: keys, catalog text, catalog-convention tests, changeset — zero edits outside packages/spec (plus the changeset file). Emitter call-site conversion stays with the consumer cards: #11993 remains open (its services half converts the approvals emitter after this lands) and #12260 remains open (probable second consumer, repair-pending; its consumer side is not implemented here).

New keys

  • record_write_denied — the user can see this record, but changing or deleting it is beyond their access. Measured emitter (recorded, not edited): packages/plugins/plugin-sharing/src/sharing-plugin.ts:1120, the by-id write gate throwing the FORBIDDEN insufficient-privileges string with the object API name and row id interpolated in; op there is update or delete, so the key covers both write verbs — the user's situation and remedy are the same, and the refused verb stays a developer fact on developerMessage/details. Deliberately NOT record_access_denied restated: that sentence ("You do not have access to this record") would be false on a row the read path already admitted — the user is typically looking at the record the write gate refuses.
  • approval_recall_not_submitter — the user asked to recall an approval request someone else submitted. Measured emitter (recorded, not edited): packages/plugins/plugin-approvals/src/approval-service.ts:2706. The copy names who CAN act — the submitter, or an administrator (the code path carries a privileged-admin override, so sending the user to an administrator is accurate).

Both sentences take no placeholders, re-derived per site per the family rule (the only nameable facts at both throw sites are object API names and opaque ids, which must not reach a toast). Both render through renderOperationMessage with the identical override/locale/fallback ladder as the record_access_denied family and are addressable for deployment overrides as errors.record_write_denied / errors.approval_recall_not_submitter. Module header updated: members list, situation table (two rows), and per-site placeholder derivations.

Tests

New describe block in packages/spec/src/system/operation-message.test.ts mirroring the existing family battery: caller-locale rendering (zh-CN + en pinned by value), base-language regional match (ja), en fallback for an uncarried locale, developer-vocabulary absence in every locale (family list plus FORBIDDEN and the insufficient-privileges phrase), distinctness from every sibling key per locale, placeholder-free catalog entries asserted on the entry (not the rendering), deployment-override wins, throwing-i18n-service containment. The pre-existing "every locale defines every key en defines" completeness test now also covers the new keys.

Verification (all through scripts/pm/os-verify-lock.sh; exit codes captured before any pipe; verdict lines quoted from the gates' own output)

Union re-run at the final head 8ee1941 (after merging origin/main forward — see landing-watch below):

  • pnpm --filter @objectstack/spec build — "check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present."
  • pnpm --filter @objectstack/spec test — "Test Files 435 passed (435) / Tests 11533 passed (11533)" at head 8ee1941.
  • pnpm --filter @objectstack/spec typecheck — clean, including "check:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json" (the new test file is inside the checked test program).
  • pnpm --filter @objectstack/spec check:generated — "All 14 generated artifacts are up to date." Zero baseline row deltas: the catalog ships as constants, not schemas, so no generated artifact moves.
  • pnpm --filter @objectstack/spec check:authorable-surface — green; "authorable-defaults/ verified against upstream e2d1f80 — 1255 default(s) unchanged". The baseRev-trails note is the documented informational line, not an error.
  • Gate derivation: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at head (answer taken from this checkout of objectstack at 999bf8b, re-confirmed after merge). All derived path-matched and convention-triggered families ran locally and are green, including check:doc-formula-expressions (after building its prerequisite packages), check:dev-prereqs (after the full package-workspace build), check:engine-double-contract ("OK — 689 pinned, 134 in the DEBT ledger, 3 exempt"), check:where-matcher ("303 matcher(s) discovered, 303 answer... correctly or refuse it loudly"), check:query-options-erasure ("ratchet holds... none new"), check:cross-package-test-inputs, and check:type-check-debt ("--re-measure: OK — 31 ledger entr(ies) re-measured, 1687 raw tsc error(s) total, none above its recorded number"). Ratchet families re-run at the merged head 8ee1941. Left to CI: the whole-repo eslint run (pnpm lint) and the workflow families whose population is unreachable from these paths — CI runs the full farm on every PR regardless.
  • pnpm check:nul-bytes — green.

Reverse verification

Direction predicted, then observed: turn red. With the catalog file reverted to the base revision (mutation proved on disk: 0 grep hits for the new key in the catalog, 6 in the kept test file) and the new tests kept, the targeted suite went RED — 7 failed / 27 passed, all seven failures in the new block (the override-wins case stays green by design: its translate stub answers regardless of the catalog). Restore proved by hash: git checkout HEAD on the path, git diff HEAD empty, disk blob hash equal to the HEAD blob hash; re-run 34/34 green. No build step is part of this loop — the test imports the module by relative source path, so no dist is on the resolution path.

Changeset

.changeset/operation-message-refusal-keys.md, minor on @objectstack/spec, following the landed published-surface precedent (the FLOW_INPUT_SCHEMA_INVALID ledger-row changeset): states plainly that this release ships catalog keys only, the emitters still throw raw English until the consumer halves land, and what operators see change when they do.

Landing-watch

origin/main moved mid-flight by two CLI-test commits (e2d1f80, 1ca763b — disjoint from packages/spec); merged forward (merge, not rebase) and the union plus ratchet families re-ran at the merged head 8ee1941 as reported above. Also disjoint from the in-flight #12497 surface (security/permission.zod.ts + registries) — no overlap with this diff.

Generated by Claude Code


Generated by Claude Code

…cord_write_denied and approval_recall_not_submitter
The spec-side half of the ruled shape-A localization path: two measured
hardcoded-English FORBIDDEN refusals (the sharing middleware's by-id write
denial, plugin-approvals' non-submitter recall) get situation keys in all
four platform locales so their emitters can convert to
renderOperationMessage in the consumer cards' own lanes. Keys only — no
emitter edits here.
Both sentences take no placeholders, re-derived per site per the family rule.
record_write_denied is deliberately not record_access_denied restated: the
sharing gate fires on rows the read path already admitted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xGvxcwPRTJfA7RfjXEYA4
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 0043c92240ffe5b53df43753f9ab1edd2446b474packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1d6e10f5190d6a507fa12c6da332fab22cac1736 — the merge of head 8ee19415799865a2d0acc259818196057ba7cc21 into base 0043c92240ffe5b53df43753f9ab1edd2446b474, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1d6e10f5190d6a507fa12c6da332fab22cac1736 && git checkout 1d6e10f5190d6a507fa12c6da332fab22cac1736
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0043c92240ffe5b53df43753f9ab1edd2446b474 8ee19415799865a2d0acc259818196057ba7cc21 && git checkout -B drift-repro 0043c92240ffe5b53df43753f9ab1edd2446b474 && git merge --no-ff 8ee19415799865a2d0acc259818196057ba7cc21
node scripts/docs-audit/affected-docs.mjs --json 0043c92240ffe5b53df43753f9ab1edd2446b474

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude