Repository navigation
DELETE_RESTRICTED (409) message is shown verbatim to end users: English-only, leaks API names, and contains developer-facing advice #7307
Description
Activity
Triage:
pm:queue+domain:engine-core.- Class: concrete defect with a named location and a 4-step repro — the user-visible
messageofDELETE_RESTRICTEDis composed English-only with raw API names and a metadata-authoring hint. Not a design card: the engine already has the localization machinery this fix needs —setI18nService(packages/objectql/src/engine.ts:3701), per-requestExecutionContext.localeresolution (:3710-3726), and write-time localized validation messages shipped under Field validation messages are hardcoded English + API field name —penalty_amount must be ≥ 0reaches end users verbatim #3957/ADR-0053 (engine-validation-locale.test.tspins the call-site contract). Routing the restrict-branch message through the same translate path with resolved labels, and keeping the developer hint in the structured fields, is execution of that established pattern. - Routing anchor (verified on
origin/main@62b6a2f): the composed message lives atpackages/objectql/src/engine.ts:7845(Delete or reassign them first, or set deleteBehavior:'cascade' on ...),err.code = 'DELETE_RESTRICTED'at:7847, incascadeDeleteRelations's restrict branch —packages/objectql⇒domain:engine-core. Existing behavior pins inengine-cascade-delete.test.ts:105-110assert code/status/structured fields only, not the message text, so the copy change is free to move under them. - Dedup:
DELETE_RESTRICTEDmatches only this card in this repo (positive control: the search does return this card); objectui has no shadow (0 hits) — consistent with the card's own analysis that the console toast is a pass-through and the fix is producer-side. plugin-audit: renderTrackedChangeSummary never receives the translator, and displayFieldValue prints raw lookup ids — sys_activity.summary shipsRating Owner: ∅ → oBK25…onto every feed surface (re-routed from objectui#4055) #7230 / plugin-audit: select option labels insys_activity.summaryare never localized — the same defect class as #7230, one line over indisplayFieldValue#7289 / plugin-audit:activityMilestonessummary templates still interpolate raw lookup ids —{owner_id}rendersoBK25…after #7230 #7290 are the same family (unlocalized user-visible copy) but a different producer (plugin-audit), not duplicates. target:v17: not applied — display-class defect (the delete is correctly refused; the failure is the copy), same grading as the plugin-audit: renderTrackedChangeSummary never receives the translator, and displayFieldValue prints raw lookup ids — sys_activity.summary shipsRating Owner: ∅ → oBK25…onto every feed surface (re-routed from objectui#4055) #7230 family. Doesn't meet any of the four blocking classes.- Note for the implementer: the structured-fields half of the card ("keep
status/code/object/dependentObject/dependentCountexactly as-is") should be treated as a hard constraint — tests pin them.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Class: concrete defect with a named location and a 4-step repro — the user-visible
CLAIM —
domain:engine-coreseat (#6019). Sessionsession_01MwoubC3jL271FYt9rGXwxb· branchclaude/issue-7307-delete-restricted-user-copy.Race re-read done;
assigneesmeasured EMPTY via REST immediately before assigning.
Generated by Claude Code
Claim-format amendment — the 10:1xZ claim above predates this seat loading today's SKILL.md update (#7357/#7379); re-stating in the step-4 fixed shape so the half-state sweeper's H2 predicate can read it:
Claim: PM loop wave 7 (seat #6019)
Session:session_01MwoubC3jL271FYt9rGXwxb
Branch:claude/issue-7307-delete-restricted-user-copy
Worktree:objectstack-7307
Domain:domain:engine-coreLabels repaired in the same write:
pm:queue→pm:dispatched(H3 discipline).
Generated by Claude Code
- added a commit that references this issue
on Aug 10, 2026 ACCEPT — PR #7423. Ready + queued; #6017 declaration filed for the new spec catalog.
Verified against all six criteria by reading the body and diff:
- ✅ The consumer table came first and decided the shape. Seven consumers measured; exactly one reads
.message, and it shows it to an end user (Console rendersbody.errorverbatim, with noDELETE_RESTRICTEDbranch — 0 hits). That measurement, not taste, is what makes the two-audience split the right shape. No objectui card needed — fixed at the producer, Console is pass-through. - ✅ Two sentences, one wire code:
messagebecomes the localized user half, rendered from resolved labels (bundle → declared label → API name, so the API name is where the ladder ends, not starts);deleteBehaviorappears in no locale.code/status/object/dependentObject/dependentCountbyte-identical; ⛔ the ADR-0112 code does not split. - ✅ The developer hint survives in the developer channel —
developerMessagecarries the previous sentence byte for byte as a REST sibling field and in the engine's delete log; pinned both ways (present in the dev channel, absent from the user half). - ✅ Transport pins unchanged and extended, not replaced; an i18n service that throws still yields 409, not 500 — the refusal never regresses to a 500 for want of a translation.
- ✅ The catalog home is argued, not assumed: not
validation.field.*(aDELETE_RESTRICTEDnames no field of the acted-on object; filing there would mint an override key that lies), sooperation-message.tsaddressederrors.<key>, sharinginterpolateValidationMessageso the two catalogs cannot drift. Field validation messages are hardcoded English + API field name —penalty_amount must be ≥ 0reaches end users verbatim #3957's fix reached from the operation side. - ✅ Changeset
minor×3 with the fixed-group reasoning (check-changeset-no-major.mjs); ⛔ no error-code-ledger touch (no new/renamed code). Spec touch declared on [PM seat] domain:spec — 🟢 os-warren · session_019G5ZnRrTMiwcHG6iZM19zf #6017.
29→26 checks all green across both heads. Auto-merge armed; issue closes on merge. #7163 dispatches when this lands (
engine.tsserial queue).
Generated by Claude Code
- ✅ The consumer table came first and decided the shape. Seven consumers measured; exactly one reads
- added 3 commits that reference this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 9, 2026 - added a commit that references this issue
on Sep 17, 2026
Summary
When a delete is refused by referential integrity, the engine throws a
409 DELETE_RESTRICTEDwhose
messageis surfaced verbatim to end users by Console's toast. That message isEnglish-only, embeds machine object/field names, and hands the user a developer instruction
(
set deleteBehavior:'cascade' on ...) — none of which is actionable or comprehensible for abusiness user of a localized app.
The error transport itself is fine (
statusis set, so the message is not swallowed into ageneric 500). This is purely about the user-facing copy.
Where
packages/objectql—cascadeDeleteRelations, thebehavior === "restrict"branch(observed in
@objectstack/objectql@17.0.0-rc.5,dist/core.js):Actual behaviour
Deleting a
sys_business_unitreferenced by a required lookup produces this red toast inConsole, shown as-is to the operator in an otherwise fully Chinese UI:
Three separate problems for the end user:
os_tianshun_ehr_sporadic_application,apply_dept. The userknows these as 「零星申请」and「申报部门」; the raw API names mean nothing to them.
set deleteBehavior:'cascade' on ...is ametadata-authoring instruction. A business user cannot act on it, and it invites
support tickets asking where that setting lives.
Expected
A business user should get something like:「该 <对象label> 正被 1 条 <子对象label> 通过
「<字段label>」引用,且该字段为必填,无法清空。请先删除或改派这些记录。」 — i.e. localized,
using labels rather than API names, and without the metadata-authoring hint.
The structured fields (
code,object,dependentObject,dependentCount) are alreadypresent and are the right channel for developers/logs — the developer guidance belongs there
(or in a separate
detail/devHintproperty), not in the user-visiblemessage.Suggested direction
messagethrough the i18n layer with an overridable key (e.g.errors.deleteRestricted), interpolating resolved labels for object/field whereavailable and falling back to API names only when no label exists;
Delete or reassign them first, or set deleteBehavior:'cascade' on ...out ofmessageinto a developer-only field (or drop thedeleteBehaviorhalf of it entirely —the "delete or reassign them first" half is useful to end users);
status: 409and the structured fields exactly as they are — that part works well.Repro
required: true(soset_nullis coerced torestrict);Found while building an app on 17.0.0-rc.5; reported per "platform issues go to the platform repo".