Skip to content

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

@yinlianghui-tw

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 #4644 indexed, #4687 distance_metric and #4676 placeholder took three different correct resolutions.

The key

formula is not in FieldSchema's accept set. Measured against the installed @objectstack/spec 17.2.0:

FieldSchema.safeParse({ type:'formula', label:'Tax', formula:'price * quantity' })
  => success = false
  => [{ code: 'unrecognized_keys', keys: ['formula'] }]

The spec carries two keys for this concept, and that is what makes it non-mechanical:

expression — Formula expression (CEL). e.g. F`record.amount * 0.1`
returnType — Inferred value type of a formula field (number/text/boolean/date)

Why this is live

  • packages/plugin-designer/src/FieldDesigner.tsx:312 — live textarea control, visibleWhen: "record.type == 'formula'", placeholder e.g. price * quantity.
  • packages/plugin-designer/src/FieldDesigner.tsx:222,243 — create and update paths both write it.
  • fromDesignerField (MetadataFieldsPage.tsx) and toFieldPayload (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/:name returns a hard 422 INVALID_METADATA that 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:

  1. Expression language. The designer's placeholder is price * quantity; the spec's expression is CEL, documented as F`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.
  2. 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.
  3. The formula field type itself (FieldDesigner.tsx:91,120) needs to be checked against the spec's FieldType vocabulary in the same pass.

The read direction is broken symmetrically too: toDesignerField reads raw.formula, but a spec-parsed server sends expression, so an existing formula field loads with an empty box.

Verification

pnpm check:designer-field-key-parity reports it; it is recorded in that gate's KNOWN_UNPARSEABLE_KEYS ledger 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

Activity

  1. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    Contributor

    Triage: lands in packages/plugin-designer + MetadataService.ts; routed pm: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 rename formula→expression would 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, returnType handled), 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

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 25, 2026
  3. added theissue type on Aug 25, 2026
  4. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    Collaborator

    Claim: 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 the formula ledger entry in scripts/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 as 2cf69e4 (PR #6225), and #6223 landed as d18a0d3 (PR #6253); both are on origin/main, so packages/plugin-designer/src is free. ⚠️ #6281 is open on packages/app-shell but on views/studio-design/StudioDesignSurface.tsx — verified by reading its changed-file list, ⛔ not its title — so it does not touch services/MetadataService.ts. Sibling cards this wave are disjoint.

    Stale-premise check — run against origin/main @ ba306e3

    • ✅ The control is still live: FieldDesigner.tsx:312 still renders the formula textarea gated on record.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's expression is 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 names MetadataFieldsPage.tsx and MetadataService.ts:105 without package prefixes; on today's tree they are packages/plugin-designer/src/MetadataFieldsPage.tsx (3 formula hits) and packages/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 expression together with teaching CEL (placeholder, help text, and a decision on returnType), or remove the control until formula authoring is properly designed.

    ⛔ A bare key rename is explicitly refused. The spec's expression is CEL — documented root and syntax differ from the designer's current price * quantity placeholder — 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)

    1. ⭐ Reproduce the refusal first, on the installed spec: parse a field object carrying formula through FieldSchema and confirm unrecognized_keys: ['formula']. If it now parses, ⛔ stop and report — the premise has moved under the 17.2.0 pin.
    2. The card claims the read direction is broken symmetrically (toDesignerField reads raw.formula, a spec-parsed server sends expression, so an existing formula field loads empty). Measure that, ⛔ do not assert it from the card.
    3. The formula field type itself (FieldDesigner.tsx:91,120) needs checking against spec's FieldType vocabulary in the same pass — if the type is also refused, the card's shape changes and that is worth reporting before you build.
    4. returnType is 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: git first (free), then the rendered issue page at https://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

  5. claude commented on Aug 25, 2026

    @claude
    Contributor

    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 on origin/main @ 7ccb53a (⚠️ main moved from the PM's ba306e3 during dispatch; every premise below was re-derived on 7ccb53a).
    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.formula
    • packages/plugin-designer/src/FieldDesigner.tsx — control + both write paths + edit seed
    • packages/plugin-designer/src/MetadataFieldsPage.tsx — ServerFieldSchema, both converters, RETIRED_FIELD_KEYS
    • packages/app-shell/src/services/MetadataService.ts — FieldMetadataPayload, toFieldPayload
    • ⭐ packages/app-shell/src/views/metadata-admin/previews/object-fields-bridge.ts — a THIRD formula write site neither the card nor the parity gate names. It is not in the gate's PAYLOAD_SHAPES, so the gate is green over it. It is in scope by necessity rather than by choice: it consumes DesignerFieldDefinition, so removing that property forces this file to change.
    • scripts/check-designer-field-key-parity.mjs — the formula ledger entry (ratchets both ways)
    • new tests + changeset

    Premise: HOLDS. Measured on the installed @objectstack/spec 17.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.ts declares the key at :82 and writes it at :129 — line 105 is neither. FieldDesigner.tsx :222/:243/:312/:351 all re-verified on 7ccb53a.

    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 level
    

    Only 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.ts states that under scope: '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; a type: 'textarea' in a JSON DrawerForm schema 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-shell depends on @object-ui/plugin-designer (its package.json lists it).
    • @object-ui/plugin-designer does not depend on @objectstack/formula at 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 formula returns 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's ObjectFieldInspector authors expression through CelPredicateField with live lint and stamps returnType from inferred CEL type, labelled Formula (CEL). Its own comment records that it "replaces the formula textarea that previously edited a formula key 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 formula field TYPE is kept: measured, FieldSchema.safeParse({ type:'formula', label:'Tax' }) succeeds — formula is a valid spec FieldType. 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-report comment here before the return message.


    Generated by Claude Code

  6. claude commented on Aug 25, 2026

    @claude
    Contributor

    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

  7. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    Collaborator

    ✅ ACCEPT — PR #6312. Route (B) taken against my weak lean, and the rebuttal is correct.

    Reviewer of record: domain:ui @ objectui execution seat, PM session session_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:

    1. The rename is worse than the card stated. FieldSchema judges the key name, never the expression language: expression: 'record.price * record.quantity' ✅, expression: 'price * quantity' ✅, and expression: '!!!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's celAuthoring.ts already records as silently evaluating to null under the record scope formulas bind. Loud 422 → silent wrong answer. Exactly the trade my ruling forbade.
    2. (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/formula dependency at all. Building a second formula-authoring surface there is a Feature, which triage and I both fenced.
    3. ⭐ My lean's premise was simply false. "Removing takes a capability away from authors using it" — every save carrying formula returned 422, so no author can ever have used this control successfully. It has only ever failed. And the real capability already exists: metadata-admin's ObjectFieldInspector authors expression through CelPredicateField with live lint and stamps returnType from 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/main vs branch)
    textarea control removed name: 'formula' 1 → 0
    …and that zero is a reading control: sibling controls (referenceTo/label) 2 → 2 — the query works
    designer write paths cleared FieldDesigner formula: 4 → 1, MetadataFieldsPage 2 → 0, MetadataService 1 → 0
    the surviving 1 is deliberate it is the field-type map entry formula: { label: 'Formula', Icon: Code } — kept on purpose
    field TYPE kept ✅ still present; measured valid: safeParse({type:'formula',label}) succeeds
    tombstone present ✅ RETIRED_FIELD_KEYS = ['indexed','referenceTo','isSystem','formula']
    published type DesignerFieldDefinition.formula gone, replaced by a tombstone comment stating why it was not renamed
    scope 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 — and carryOver spreads the previous server def verbatim, so an already-blocked object would stay blocked forever. RETIRED_FIELD_KEYS is what makes the fix reach objects that are already broken, not just prevent new ones. ⭐ And it drops rather than migrating to expression — 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 stored expression rides through untouched. ✅ Correct on both counts.

    returnType omitted 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, and FieldSchema's accept set is untouched, so Clause-② still reads no. The changeset is minor on all three packages, which is the repo's rule for a public-type break (⛔ never major). ✅

    Self-corrections — all three recorded rather than buried

    • ⭐ Its own claim comment was wrong and it says so: it called object-fields-bridge.ts a live third write site; measured, it is dead code, zero importers repo-wide (positive control: sibling object-fields-io has 10+), so that edit is behaviourally inert. Filed as finding: metadata-admin/previews/object-fields-bridge.ts has zero importers, and its serializeDesignerField would 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 -cF with 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/returnType and 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.js sets no parserOptions.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 is undefined is a key zod's strict object counts and JSON.stringify drops. 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 lint is turbo run lint (per-package eslint .), ⛔ not eslint . --no-inline-config. Carried into later dispatch words this round.

    Known leftover — correctly declared, not hidden

    appDesigner.fieldDesigner.formula is now a dead i18n key in ten locale packs plus the defaults map. Left in place and filed as #6310, because check:i18n-dead-keys is 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 behind main and 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 in docs/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

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions