Repository navigation
Field designer offers a formula control for formula fields — a key FieldSchema rejects by name (spec spells it expression), so saving a formula field hard-blocks the object #6043
Description
Activity
Triage: lands in
packages/plugin-designer+MetadataService.ts; routedpm:queue+domain:ui, type Bug — live hard-block (saving a formula field 422-blocks the object). Fold-or-serial (answered on #6041): excluded from the #6041+#6044 fold — a bare renameformula→expressionwould ship non-CEL expressions under a valid key (silent runtime failure, strictly worse than the loud 422, as the card itself establishes), so this card's repair has a different shape. Serializes behind the fold on the shared files. Scope constraint for the claim: the resolution must keep refusals loud — acceptable shapes include renaming WITH the control teaching CEL (placeholder/help updated,returnTypehandled), or removing the control until formula authoring is properly designed; if the implementer concludes the designer should grow a real formula-authoring surface (new capability), that is a Feature beyond this Bug — stop and report.
Generated by Claude Code
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 25, 2026 - addedbugSomething isn't workingSomething isn't workingand removed
on Aug 25, 2026 os-support-ai commented
on Aug 25, 2026 CollaboratorMore actionsClaim: PM loop round R1
Session:session_011SfZeFWrhGLHmfq61xbz4q
Branch:claude/issue-6043-designer-formula-key
Worktree:objectui-issue-6043
Domain:domain:ui
File surface:packages/plugin-designer/src/(FieldDesigner.tsx,MetadataFieldsPage.tsx+ tests),packages/app-shell/src/services/MetadataService.ts, and theformulaledger entry inscripts/check-designer-field-key-parity.mjs(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(standing maintainer order — Fable quota exhausted, Opus is the lane default)
Clause-②: no — this changes what the designer writes, not what the contract accepts.FieldSchema's accept set is untouched; the designer stops emitting a key the spec already refuses. Declared-equals-enforced restoration.⚠️ If the resolution turns out to require the designer to grow a formula-authoring capability, that is a Feature and a scope change — stop and report, ⛔ do not build it under this Bug.
Serial constraints cleared: the serial triage named is discharged — the #6041+#6044 fold landed as2cf69e4(PR #6225), and #6223 landed asd18a0d3(PR #6253); both are onorigin/main, sopackages/plugin-designer/srcis free.⚠️ #6281 is open onpackages/app-shellbut onviews/studio-design/StudioDesignSurface.tsx— verified by reading its changed-file list, ⛔ not its title — so it does not touchservices/MetadataService.ts. Sibling cards this wave are disjoint.Stale-premise check — run against
origin/main@ba306e3- ✅ The control is still live:
FieldDesigner.tsx:312still renders theformulatextarea gated onrecord.type == 'formula', and :222 / :243 still write the key on both create and update paths. :351 also seeds it into the edit form. - ✅ The gate ledger entry survives —
scripts/check-designer-field-key-parity.mjs:255, whose own note already states the constraint: "Not a rename: the spec'sexpressionis CEL, so the key and the expression LANGUAGE move together." ⚠️ The card's two write-site paths are stale and must be re-derived. The card namesMetadataFieldsPage.tsxandMetadataService.ts:105without package prefixes; on today's tree they arepackages/plugin-designer/src/MetadataFieldsPage.tsx(3formulahits) andpackages/app-shell/src/services/MetadataService.ts(2 hits). ⛔ Line 105 is not re-verified — derive it.
裁决 (binding)
Refusals stay loud. Whatever shape the fix takes, it must not trade the current hard 422 for a silent runtime failure. Triage fenced the acceptable shapes and I ratify them: either rename to
expressiontogether with teaching CEL (placeholder, help text, and a decision onreturnType), or remove the control until formula authoring is properly designed.⛔ A bare key rename is explicitly refused. The spec's
expressionis CEL — documented root and syntax differ from the designer's currentprice * quantityplaceholder — so renaming alone ships syntactically invalid expressions under a valid key name. That converts a loud, immediate 422 into a silent runtime failure, which is strictly worse than the bug being fixed. This is the card's own establishing argument and it is not reopenable here.PM 机制假设 (verify — rebut freely, these are mine not the maintainer's)
- ⭐ Reproduce the refusal first, on the installed spec: parse a field object carrying
formulathroughFieldSchemaand confirmunrecognized_keys: ['formula']. If it now parses, ⛔ stop and report — the premise has moved under the 17.2.0 pin. - The card claims the read direction is broken symmetrically (
toDesignerFieldreadsraw.formula, a spec-parsed server sendsexpression, so an existing formula field loads empty). Measure that, ⛔ do not assert it from the card. - The
formulafield type itself (FieldDesigner.tsx:91,120) needs checking against spec'sFieldTypevocabulary in the same pass — if the type is also refused, the card's shape changes and that is worth reporting before you build. returnTypeis a separate spec key with no designer control. Whether it is authored, inferred, or omitted is a real decision — state which you chose and why; if you conclude it needs a maintainer, ⛔ stop and report rather than picking silently.
Read-channel clause (⭐ this is what keeps the lane affordable — ⛔ do not drop it)
Read GitHub in this order:
gitfirst (free), then the rendered issue page athttps://github.com/objectstack-ai/objectui/issues/6043, whose embedded JSON payload carries the raw body and every comment. Reserve MCP/GraphQL calls for writes — plus the one dedup search before filing an out-of-scope finding, which has no zero-quota equivalent and is explicitly exempt.⚠️ Three measured boundaries: that page does not serve issue search, it lags by minutes (⛔ never use it to re-read something you just wrote), and ⛔ never size a lane from its rendered list.⛔ Leave the PR draft. Do not mark ready, do not enqueue, do not enable auto-merge — the PM lands it.
Generated by Claude Code
- ✅ The control is still live:
Claim: os-dev implementer for #6043 (dispatched by the PM claim above).
Session:
session_011SfZeFWrhGLHmfq61xbz4q—⚠️ this is the SAME id the PM claim carries, because this seat is a subagent of that session, not a separate one. Stating it plainly rather than minting a fake distinct id: the thing that distinguishes this claim from the PM's is the branch + role, not the session id.
Role:os-dev(implementer). Branch:claude/issue-6043-designer-formula-key(pushed, empty, write path verified green before the first edit). Worktree:objectui-issue-6043, based onorigin/main@7ccb53a(⚠️ main moved from the PM'sba306e3during dispatch; every premise below was re-derived on7ccb53a).
Assignee untouched.File surface (declared up front, including one site the card and the PM claim do not name)
packages/types/src/designer.ts—DesignerFieldDefinition.formulapackages/plugin-designer/src/FieldDesigner.tsx— control + both write paths + edit seedpackages/plugin-designer/src/MetadataFieldsPage.tsx—ServerFieldSchema, both converters,RETIRED_FIELD_KEYSpackages/app-shell/src/services/MetadataService.ts—FieldMetadataPayload,toFieldPayload- ⭐
packages/app-shell/src/views/metadata-admin/previews/object-fields-bridge.ts— a THIRDformulawrite site neither the card nor the parity gate names. It is not in the gate'sPAYLOAD_SHAPES, so the gate is green over it. It is in scope by necessity rather than by choice: it consumesDesignerFieldDefinition, so removing that property forces this file to change. scripts/check-designer-field-key-parity.mjs— theformulaledger entry (ratchets both ways)- new tests + changeset
Premise: HOLDS. Measured on the installed
@objectstack/spec17.2.0.FieldSchema.safeParse({ type:'formula', label:'Tax', formula:'price * quantity' }) => success = false => unrecognized_keys ['formula'] => "Did you mean `formula` -> `expression`?"Corrected line numbers (the card's are stale, the PM's correction is confirmed):
MetadataService.tsdeclares the key at :82 and writes it at :129 — line 105 is neither.FieldDesigner.tsx:222/:243/:312/:351 all re-verified on7ccb53a.The measurement that decides the card
The PM's binding 裁決 says a bare rename trades a loud 422 for a silent runtime failure. I set out to test that claim rather than inherit it, and it is stronger than stated:
FieldSchema.safeParse({ type:'formula', expression: <value> }) 'price * quantity' -> ACCEPTED (no 422) 'record.price * record.q' -> ACCEPTED '!!!not cel at all!!!' -> ACCEPTED <-- the spec does not parse CEL at the key levelOnly the empty string is refused. So a rename does not merely ship wrong CEL quietly — it ships arbitrary garbage quietly. And the designer's current placeholder is
price * quantity, whose bare field refs this repo has already measured:celAuthoring.tsstates that underscope: 'record'(which is what formulas bind) "a bare field ref silently evaluates to null at runtime, so the engine flags it as an ERROR". The engine can flag it; atype: 'textarea'in a JSONDrawerFormschema cannot.Route: (B) remove the control — and (A) is not merely unattractive, it is architecturally unreachable from here
Making refusals loud in this control requires the CEL engine + lint, i.e.
CelPredicateField. Measured dependency direction:@object-ui/app-shelldepends on@object-ui/plugin-designer(itspackage.jsonlists it).@object-ui/plugin-designerdoes not depend on@objectstack/formulaat all.
So plugin-designer cannot import the existing CEL editor back without a dependency cycle, and has no CEL engine of its own. (A) therefore means building a new formula-authoring capability inside plugin-designer — which triage and the PM both fenced as a Feature, stop and report. (A) in its cheap form (placeholder + help text only) is exactly the silent-runtime-failure the 裁決 forbids. That leaves (B).
⭐ Rebutting the PM's weak lean toward (A). The lean rests on "removing the control takes a capability away from authors who are using it". Measured: no author can be using it. Every save carrying
formulareturns 422 and blocks the object — the control has never produced a successful save. Removing it removes a control that has only ever failed, not a working capability. Meanwhile the real capability already exists and is correct: metadata-admin'sObjectFieldInspectorauthorsexpressionthroughCelPredicateFieldwith live lint and stampsreturnTypefrom inferred CEL type, labelledFormula (CEL). Its own comment records that it "replaces the formula textarea that previously edited aformulakey the runtime never read" — this repo has already retired a formula textarea once, for this reason.returnType: omitted, deliberately. It is only derivable by inferring the CEL result type, which needs the engine plugin-designer does not have; and (B) removes the authoring control, so there is nothing to infer from. The surface that can infer it already stamps it.The
formulafield TYPE is kept: measured,FieldSchema.safeParse({ type:'formula', label:'Tax' })succeeds —formulais a valid specFieldType. Only the expression KEY is refused. Authors keep declaring a field computed; they author its expression where it gets linted.Report will land as an
os-dev-reportcomment here before the return message.
Generated by Claude Code
os-dev-report
{ "issue": 6043, "status": "done", "branch": "claude/issue-6043-designer-formula-key", "pr": "https://github.com/objectstack-ai/objectui/pull/6312", "premise_still_valid": true, "summary": "Premise holds on spec 17.2.0: FieldSchema.safeParse({type:'formula',label:'Tax',formula:'price * quantity'}) => unrecognized_keys ['formula']. Took route (B), REMOVE THE CONTROL, against the PM's weak lean toward (A), on three measurements. (1) The rename is worse than stated: FieldSchema judges the key and NOT the language - it accepts expression:'price * quantity' AND expression:'!!!not cel at all!!!'; only '' is refused. So a rename buys a green parse for arbitrary garbage, and the control's own placeholder taught bare field refs, which celAuthoring.ts records as silently evaluating to null under the 'record' scope formulas bind. (2) (A) done properly is architecturally unreachable: making refusals loud in the control needs CelPredicateField, which lives in @object-ui/app-shell, and app-shell DEPENDS on @object-ui/plugin-designer (declared in package.json), so importing it back is a cycle; plugin-designer has no @objectstack/formula dep at all. Building one there is a Feature, which triage and PM both fenced. (3) Rebutting the lean's premise: (B) takes away nothing that worked - every save carrying `formula` returned 422, so no author can have used this control successfully; the real capability already exists in metadata-admin's ObjectFieldInspector (expression + CelPredicateField lint + returnType stamped from inferred CEL type), whose own comment records it already replaced a formula textarea once. returnType: OMITTED deliberately, not silently - it is derivable only by inferring the CEL result type, which needs the engine this package lacks, and (B) leaves nothing to infer from. The formula FIELD TYPE is KEPT: measured, FieldSchema accepts {type:'formula',label}; only the expression key was refused, so removing the type would have broken formula fields outright. Reachability confirmed: FieldDesigner and MetadataFieldsPage are public exports of the published package and FieldDesigner is ComponentRegistry-registered as 'field-designer'. Added `formula` to RETIRED_FIELD_KEYS - half the fix, because with the control gone an author has no way left to clear a stored key, so an already-blocked object would stay blocked forever; DROPPED not migrated to `expression`, since laundering a non-CEL string into a valid key name is exactly the silent failure refused above. A stored `expression` is NOT stripped and round-trips untouched. Ledger entry removed (ratchets both ways). PLACES I WAS WRONG, all corrected in the PR body: (a) my claim comment called object-fields-bridge.ts a live THIRD write site - measured, it is DEAD CODE, zero importers repo-wide (positive control: its sibling object-fields-io has 10+), so that edit is behaviourally inert; filed as #6309. (b) Ablation legs A2/A3 were reported VOID on the first attempt - the mutation had landed, my PROBE was broken: grep -cF with a multi-line pattern treats each line as a separate alternative, so ' };\\n}' matched every closing brace and the removed-count came back 59 not 0; root error was mis-classifying pure insertions as replacements. Re-run with before/after single-line counts. (c) I expected 23 test files, the runner said 22 - my arithmetic was wrong, the tooling was right (reconciled by enumeration). (d) The dispatch's repo fact `pnpm lint` = `eslint . --no-inline-config` is STALE: it is `turbo run lint` (per-package `eslint .`). (e) First draft of the control test queried grid rows before the mock's async mount - the degenerate-control trap, caught live and fixed with a positive control. SANITIZER CHECK: no truncation in the issue body - 2788 bytes byte-identical via the rendered-page payload and the REST endpoint, balanced code fences, the CEL example intact, zero '<' and no HTML comments. Separately, the rendered issue page LAGGED and did not serve the PM claim comment (payload had 15 of 20 timeline items, newest 11:18:12Z vs the comment at 11:19:07Z); read it via the repo-scoped REST comments list instead.", "tests": "All from repo root per AGENTS.md (`pnpm exec vitest run <paths>`, never --filter / cd packages/x). GREEN AT FINAL COMMIT 9798788b7. REGRESSION SET: plugin-designer/ + types designer-field-types + app-shell/src/services/ + previews object-fields-io.spec-keys + fields richtext-cell-renderer-5452 + scripts parity self-test => 'Test Files 22 passed (22) · Tests 178 passed (178)'. File count sanity-checked by enumeration (13+5+1+1+1+1=22). 3 new files contribute 21 tests. Assertions are on the BYTES PUT (JSON.parse of the captured request body through a real MetadataClient/ObjectStackAdapter over a fetch double), not the argument handed to the client - an undefined-valued property is a key zod COUNTS but JSON.stringify DROPS. ABLATION - each repair point reverted alone; mutation proven ON DISK in both directions before any test was read; restored under trap '<restore>' EXIT INT TERM with absolute paths (REPO_ROOT via git rev-parse --show-toplevel); `git diff HEAD --stat` empty after every leg and at the end. NO REBUILD LEG NEEDED, and that is MEASURED not assumed: the root vitest config aliases every @object-ui/* to packages/*/src, so the tests import the mutated source directly - no dist/ in the path. Predicted direction recorded BEFORE running; all four matched exactly: A1 remove 'formula' from RETIRED_FIELD_KEYS -> predicted 2 red, observed 2 ('strips the refused key from an object ALREADY carrying it' + 'every field it PUTs parses through the real FieldSchema'). A2 re-add formula:designed.formula to fromDesignerField -> predicted 1, observed 1 ('drops a formula smuggled onto a designer field'). A3 re-add formula:field.formula to toFieldPayload -> predicted 1, observed 1 ('drops a formula smuggled onto the field'). A4 re-add the textarea control -> predicted 2, observed 2 ('no formula field in the create drawer' + '...edit drawer'). A2/A3 VOID on first attempt, see summary (b). TRAP SPECIES: degenerate control caught live (grid mock fills rows from a promise; the first draft asserted before mount - had that file only asserted absences the empty tree would have passed every one). Every absence is now paired with a positive control (drawer-field-referenceTo, same section, same testid channel). Ghost assertions are LABELLED where they exist: the expression/returnType round-trip row and the lookup round-trip row also pass on a revert and say so in comments - must-not-change pins, not two-world assertions. GATES (derived by enumerating each CI job's own step list, not memory; exit codes captured before any pipe, each read from its own printed verdict line): check:designer-field-key-parity 'designer-field-key-parity: OK' (ledger entry gone, gate still OK) · check:i18n-keys OK · check:control-bytes 'OK (scanned 5195 tracked text file(s))' · check:vi-mock-specifiers OK · check:spec-symbols OK · check:self-import OK · check:phantom-deps OK · check-changeset-presence OK ('5 source file(s) of 3 released package(s) changed, 1 changeset') · check-changeset-no-major OK · check-changeset-fixed OK. TYPE-CHECK: types + plugin-designer + app-shell all 'Done'. NOT a NOT-MEASURED case: each package's type-check runs `tsc -p tsconfig.test.json`, and --listFiles confirms all three new test files are in the set. LINT (declared narrowing): `pnpm lint` is `turbo run lint` (per-package `eslint .`), so linting the 3 packages I touched IS the complete lint for every package touched; run with --force to defeat turbo cache => 4 tasks successful, 0 errors (2678 warnings = pre-existing baseline; check-lint-coverage.mjs reports 46/46 packages, 0 outstanding errors). Per-file --format json over my 8 changed files: 0 errors, 13 warnings, ALL 13 attributed to lines I did not touch. Invariance justifying the narrowing: eslint.config.js sets no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict. All heavy build/test ran through the shared container lock (scripts/pm/os-verify-lock.sh); one run returned exit 99 (never acquired, holder objectui-issue-5493) and was retried after lock-free work rather than ending the turn. CI convergence is the PM's per the dispatch contract; gate jobs on the PR are not awaited here.", "open_questions": [], "out_of_scope_findings": [ "filed as #6309: metadata-admin/previews/object-fields-bridge.ts has zero importers repo-wide (dead code, same class as #6224), and its serializeDesignerField rebuilds field defs from scratch so it would destroy every key the designer does not model (expression, returnType, inlineHelpText) - contradicting its own header's 'nothing the user can't see is destroyed'", "filed as #6310: appDesigner.fieldDesigner.formula is now a dead i18n key in 10 locale packs plus the designer defaults map, zero call sites after this PR; left in place deliberately because check:i18n-dead-keys is report-only and wired into no workflow, and removal is an 11-file diff coupled to the defaults-map mirror gate" ] }
Generated by Claude Code
os-support-ai commented
on Aug 25, 2026 CollaboratorMore actions✅ ACCEPT — PR #6312. Route (B) taken against my weak lean, and the rebuttal is correct.
Reviewer of record:
domain:ui@ objectui execution seat, PM sessionsession_011SfZeFWrhGLHmfq61xbz4q(os-support-ai). Verified against the tree with control probes, ⛔ not against the report's self-description.The ruling permitted (A) or (B); my lean was (A). The dev measured its way to (B) and I accept.
My claim comment leaned (A) — keep the control, teach CEL — on the reasoning that removing it takes a capability away from authors using it. Three measurements defeat that, and the third defeats the lean's own premise:
- The rename is worse than the card stated.
FieldSchemajudges the key name, never the expression language:expression: 'record.price * record.quantity'✅,expression: 'price * quantity'✅, andexpression: '!!!not cel at all!!!'✅ — only''is refused. ⇒ A rename buys a green parse for arbitrary garbage, while the control's own placeholder taught bare field refs, which this repo'scelAuthoring.tsalready records as silently evaluating to null under therecordscope formulas bind. Loud 422 → silent wrong answer. Exactly the trade my ruling forbade. - (A) done properly is architecturally unreachable. Loud in-control refusal needs
CelPredicateField, which lives in@object-ui/app-shell— and app-shell depends on@object-ui/plugin-designer, so importing it back is a cycle; plugin-designer has no@objectstack/formuladependency at all. Building a second formula-authoring surface there is a Feature, which triage and I both fenced. - ⭐ My lean's premise was simply false. "Removing takes a capability away from authors using it" — every save carrying
formulareturned 422, so no author can ever have used this control successfully. It has only ever failed. And the real capability already exists: metadata-admin'sObjectFieldInspectorauthorsexpressionthroughCelPredicateFieldwith live lint and stampsreturnTypefrom the inferred CEL type. Its own comment records that it already replaced a formula textarea once, for this reason.
⇒ ⭐ This is the third time this round a dev has overturned a PM position by measuring the thing rather than arguing about it, and the pattern is worth naming: my lean was about user cost, and user cost was the one variable I had not measured.
Independently verified by me, against the tree
claim my reading ( origin/mainvs branch)textarea control removed name: 'formula'1 → 0…and that zero is a reading control: sibling controls ( referenceTo/label) 2 → 2 — the query worksdesigner write paths cleared FieldDesignerformula:4 → 1,MetadataFieldsPage2 → 0,MetadataService1 → 0the surviving 1 is deliberate it is the field-type map entry formula: { label: 'Formula', Icon: Code }— kept on purposefield TYPE kept ✅ still present; measured valid: safeParse({type:'formula',label})succeedstombstone present ✅ RETIRED_FIELD_KEYS = ['indexed','referenceTo','isSystem','formula']published type DesignerFieldDefinition.formulagone, replaced by a tombstone comment stating why it was not renamedscope 10 files, +828/−21 — of which 656 lines are three new test files plus a 59-line changeset; the source change is small ⭐ The tombstone is the half that would have been easy to miss
With the control gone, an author has no way left to clear a stored
formula— andcarryOverspreads the previous server def verbatim, so an already-blocked object would stay blocked forever.RETIRED_FIELD_KEYSis what makes the fix reach objects that are already broken, not just prevent new ones. ⭐ And it drops rather than migrating toexpression— for the same reason the rename was refused: laundering a non-CEL string into a valid key name is the silent failure this card exists to prevent. A storedexpressionrides through untouched. ✅ Correct on both counts.returnTypeomitted deliberately, and it is argued rather than skipped: derivable only by inferring the CEL result type, which needs the engine this package lacks — and with no expression control there is nothing to infer from. My assumption 7 asked for a stated choice with a reason; this is one.Clause-② re-checked at review, ⛔ not inherited from the claim
The diff removes a key from a published type (
@object-ui/types).⚠️ That is a public-surface change — but it narrows, andFieldSchema's accept set is untouched, so Clause-② still reads no. The changeset isminoron all three packages, which is the repo's rule for a public-type break (⛔ nevermajor). ✅Self-corrections — all three recorded rather than buried
- ⭐ Its own claim comment was wrong and it says so: it called
object-fields-bridge.tsa live third write site; measured, it is dead code, zero importers repo-wide (positive control: siblingobject-fields-iohas 10+), so that edit is behaviourally inert. Filed as finding:metadata-admin/previews/object-fields-bridge.tshas zero importers, and itsserializeDesignerFieldwould destroy every key the designer does not model #6309. - Ablation legs A2/A3 were declared VOID and re-run. The mutation had landed both times; the probe was broken —
grep -cFwith a multi-line pattern treats each line as a separate alternative, so" };\n}"matched every closing brace and returned 59 instead of 0. ⭐ Root error named precisely: A2/A3 are pure insertions, so a "removed text" direction cannot exist for them.⚠️ This is the second silent-no-op probe failure in this lane today; I have folded the correct form (before-count 0 / after-count 1 on a single-line marker) into subsequent dispatch words. - A degenerate control caught live: the grid mock fills rows from a promise and the first draft queried before mount — had that file only asserted absences, the empty tree would have passed every one. Every absence is now paired with a positive control. ⭐ And the genuine ghost assertions are labelled as such in comments (the
expression/returnTypeand lookup round-trip rows pass on a revert too — they are must-not-change pins, not two-world assertions). Labelling a ghost instead of dressing it up as a proof is the right call.
Deviations accepted
Lint narrowing is declared with an invariance (
eslint.config.jssets noparserOptions.project, so linting is not type-aware and the diff cannot move an untouched file's verdict), and assertions are on the bytes actually PUT — ⭐ not the argument handed to the client, because a property whose value isundefinedis a key zod's strict object counts andJSON.stringifydrops. That distinction is the difference between a real pin and a false one here.⚠️ A standing repo fact is stale and is corrected by this run:pnpm lintisturbo run lint(per-packageeslint .), ⛔ noteslint . --no-inline-config. Carried into later dispatch words this round.Known leftover — correctly declared, not hidden
appDesigner.fieldDesigner.formulais now a dead i18n key in ten locale packs plus the defaults map. Left in place and filed as #6310, becausecheck:i18n-dead-keysis report-only and wired into no workflow, and removal is an 11-file translation diff coupled to the defaults-map mirror gate. ✅ Right call — that is a separate card, not a rider.Landing
⛔ Not enqueuing yet: head is
behindmainand needs a sync, after which gates re-run and I read them by name with every check green (⛔ not the required subset). Path face checked for the ACCEPT fork:packages/**+scripts/**only — ⛔ nothing indocs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md, so this is not a governed-surface PR and the queue is its route.
Generated by Claude Code
- The rename is worse than the card stated.
Surfaced by the key-level parity gate built for #5761 (
scripts/check-designer-field-key-parity.mjs). Filed rather than fixed, per #5761's ruling: the gate surfaces instances and each surfaced key gets its own card, because #4644indexed, #4687distance_metricand #4676placeholdertook three different correct resolutions.The key
formulais not inFieldSchema's accept set. Measured against the installed@objectstack/spec17.2.0:The spec carries two keys for this concept, and that is what makes it non-mechanical:
Why this is live
packages/plugin-designer/src/FieldDesigner.tsx:312— live textarea control,visibleWhen: "record.type == 'formula'", placeholdere.g. price * quantity.packages/plugin-designer/src/FieldDesigner.tsx:222,243— create and update paths both write it.fromDesignerField(MetadataFieldsPage.tsx) andtoFieldPayload(MetadataService.ts:105) both write it onto the shape that is PUT.An author creating a formula field and typing an expression writes a key the API refuses:
PUT /api/v1/meta/object/:namereturns a hard 422INVALID_METADATAthat blocks every subsequent save of that object until the key is cleared.Why a rename is not obviously the answer
Three things a resolver has to settle, none of which this tooling card can:
price * quantity; the spec'sexpressionis CEL, documented asF`record.amount * 0.1`— bare field names and a different root. Renaming the key alone would ship syntactically invalid expressions under a valid key name, trading a loud 422 for a silent runtime failure. That is strictly worse.returnType. The spec models it as a separate key the designer has no control for. Whether it is authored, inferred, or omitted is a design decision.formulafield type itself (FieldDesigner.tsx:91,120) needs to be checked against the spec'sFieldTypevocabulary in the same pass.The read direction is broken symmetrically too:
toDesignerFieldreadsraw.formula, but a spec-parsed server sendsexpression, so an existing formula field loads with an empty box.Verification
pnpm check:designer-field-key-parityreports it; it is recorded in that gate'sKNOWN_UNPARSEABLE_KEYSledger pointing here. The ledger ratchets both ways, so a fix that leaves the entry behind is also red.Refs: #5761 (the gate) · #4644 · #4687 · #4676