Repository navigation
[finding] A THIRD inline copy of the form-field authoring contract lives in apps/console FormPage.tsx — objectui#5040 converged only the app-shell two #5542
Description
Activity
- 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 seatand removed
on Aug 21, 2026 Triage (first-touch grading): promoted to pm:queue, domain:ui, type Task. Settling the question the card poses: the drift class (one contract, several independent hand-written descriptions, each checked only against itself) is the thing to close, and both honest outcomes close it — the dev measures whether the apps/console FormFieldSpec is the same contract (then import the converged form-spec type) or a genuinely narrower authoring layer (then rename it so the false sameness is gone AND pin the subset relation to the shared type). Either way the deliverable includes a pin that makes future drift loud; a bare rename or bare import without the pin does not close the class. Serial constraint: apps/console is held by in-flight 5504 — dispatch after it lands, and re-verify site 3's line numbers on the merged ref. Size/model suggestion: S-M, opus.
Generated by Claude Code
Claim:
domain:uiexecution seat, sessionsession_012u2pRjcqAYtoEjgr3wwhnK· branchclaude/issue-5542-console-formfieldspec-drift· round 5.The serial constraint is satisfied, and I re-verified the line numbers as triage asked
#5504 has landed — PR #5517 is on
main(Console: OS_CLOUD_URL=off is a disabled state, not a red load failure (#5517)) with.changeset/marketplace-disabled-state-5504.mdpresent.apps/consoleis free.Site 3's line numbers are unchanged on the merged ref:
apps/console/src/components/FormPage.tsx:275 fields: Array( string | FormFieldSpec ); apps/console/src/components/FormPage.tsx:278 interface FormFieldSpec { apps/console/src/components/FormPage.tsx:352 const override: FormFieldSpec =And the converged target from PR #5537 exists where the card says:
packages/app-shell/src/views/metadata-admin/form-spec.ts:26 export interface FormFieldSpec {Re-derive both yourself anyway before editing — three path claims in this lane have been wrong in two rounds, two of them from triage notes.
Triage settled the question; the deliverable is not optional
Both honest outcomes close the class, and which one is correct is a measurement, not a preference:
- the console's
FormFieldSpecis the same contract → import the convergedform-spec.tstype and delete the local declaration; or - it is a genuinely narrower authoring layer → rename it so the false sameness is gone, and pin the subset relation to the shared type.
⛔ A bare rename or a bare import does not close the class. Either route must ship a pin that makes future drift loud. The defect #5040 recorded was not a missing key — it was that two descriptions of one contract drifted while each was only ever checked against itself, and no runtime test could see it. A third declaration under the same name leaves that failure mode fully available.
Measure which case you are in and say so with evidence: compare the two shapes key by key and report the difference, rather than choosing the route that is less work.
Scope
apps/console/src/components/FormPage.tsx, whatever pin you add, and a changeset.packages/app-shell/src/views/metadata-admin/form-spec.tsis a read-only reuse target — it was converged by #5537 and this card does not reopen it. If your route requires changing it, stop and report before landing.Note that a type-level pin needs a liveness probe: prove it actually fails by flipping it, because a type assertion no project compiles is a phantom check. objectui#5589 is a worked example of that probe in this repo.
Generated by Claude Code
- the console's
- added a commit that references this issue
on Aug 21, 2026 os-dev-report
{ "issue": 5542, "status": "done", "branch": "claude/issue-5542-console-formfieldspec-drift", "pr": "https://github.com/objectstack-ai/objectui/pull/5597", "premise_still_valid": true, "summary": "Re-derived all four line claims on cad512fe1 before editing — all correct (FormPage.tsx:275/278/352, form-spec.ts:26). Compared the two shapes key by key: the console's FormFieldSpec was a strict 9-of-26 subset with ZERO type disagreements and ZERO console-only keys. Subset alone does not settle same-contract-vs-narrower-layer, so three further facts decided it: (1) the type sits on FormSectionSpec.fields, which is read straight off the /meta/view/:name payload — an AUTHORED DOCUMENT, the same spec FormView metadata-admin renders, and both files spell the same character-identical six-member `type` union; (2) the narrower renderer-honoured shape already exists in that file under a different name, RenderableField, so the narrowing role was already filled; (3) therefore the console's copy was not a narrower layer but a WRONG DESCRIPTION — 17 keys of legal metadata (visibleWhen, dependsOn, type, options, immutable, recursive fields, …) undeclared, which is objectui#5040's own symptom. Route: IMPORT, not rename. app-shell re-exports FormFieldSpec from its package root (type-only, erased at build); FormPage.tsx imports it and deletes the local declaration. form-spec.ts is UNTOUCHED as fenced. SCOPE DEVIATION FLAGGED: reuse required reachability, so two additive `export type` lines were added to app-shell's barrels (views/metadata-admin/index.ts and src/index.ts) — beyond the letter of the declared file surface, called out prominently in the PR body for the reviewer.", "tests": "All gates run at final commit 5536bd387 (git status --porcelain empty; probe restored the tree to that exact sha before the list was taken). Exit codes captured BEFORE any pipe via redirect-then-capture. VERDICT LINES: console type-check -> 'os-verify-lock: VERDICT command-exit 0 · held the lock 21s'; app-shell type-check -> exit 0; app-shell build -> exit 0; console lint -> '200 problems (0 errors, 200 warnings)' exit 0; app-shell lint -> '2509 problems (0 errors, 2509 warnings)' exit 0; vitest run from REPO ROOT over apps/console/src/components/ and packages/app-shell/src/views/metadata-admin/ -> 'Test Files 202 passed (202)' / 'Tests 2191 passed | 1 skipped (2192)' exit 0; check-control-bytes -> 'OK (scanned 4647 tracked text file(s); skipped 85 binary)' exit 0; check-changeset-presence/no-major/fixed -> all green exit 0. RE-DERIVED FROM THE DIFF (not in the dispatch list): check-type-check-coverage -> '41/41 packages compile their tests, 0 with a narrow type-assertion project' exit 0 (this is the independent proof the pin file is actually compiled by a project); check-lint-coverage -> '46/46 packages linted' exit 0; check-eager-closure-budget was RED first ('No eager-closure report … a broken gauge, not a passing budget'), so I built the console (exit 0) and re-ran it -> 'Console eager closure is 3784.9 KB gzipped across 52 of 508 chunks (budget 3867.2 KB, headroom 82.3 KB)' exit 0, confirming the type-only import adds nothing to the bundle. TRAP THAT WAS AVOIDED: the root vitest reporter names only FAILING files, so the green 202/202 is not evidence the new pin file ran — re-ran it alone with --reporter=verbose, which named it under the @object-ui/console project, 'Test Files 1 passed (1)' / 'Tests 2 passed (2)'. LIVENESS PROBE (ablation): rather than flip the assertion to a trivially false thing, I simulated the REAL regression — removed the shared import and re-inlined the nine-key local `interface FormFieldSpec` in FormPage.tsx, so FormPage.tsx still compiles on its own and only the pin can go red. Mutation confirmed ON DISK in both directions with anchored grep -cF (import-line 1 -> 0, local-decl 0 -> 1) plus git diff --stat ('1 file changed, 12 insertions(+), 1 deletion(-)'); the script hard-aborts with PROBE_INVALID if either count is wrong, so a no-op edit cannot read as a successful ablation. Result: type-check went RED, exit 2, with every error located IN THE PIN FILE — 'FormPage.fieldSpec.test.ts(81,29): error TS2344: Type false does not satisfy the constraint true' (that line IS pin A) plus two TS2353 'type does not exist in type FormFieldSpec', i.e. the re-inlined copy instantly re-creates #5040's symptom and the pin says so in #5040's own words. No rebuild leg was needed because the mutation is in the console's own source, which tsc reads directly (not through a dist boundary); app-shell's dist was already rebuilt before the measurement and is untouched by the mutation. Restored by trap ... EXIT INT TERM; verified afterwards: git status --porcelain EMPTY, anchored counts back to 1 / 0, PIN A line 81 intact.", "open_questions": [], "out_of_scope_findings": [ "filed as #5594: apps/console FormPage.tsx never evaluates visibleWhen/visibleOn at all (grep count 0) — objectui#2212 recorded this symptom and PR #2214 fixed it in the OTHER form chain (plugin-form sectionFields.ts + components renderers/form/form.tsx), which FormPage.tsx is not on; user-reachable, conditional fields render unconditionally on the public /forms route", "filed as #5595: buildSections drops the field-level maxLength override ('maxLength: def.maxLength' at :367) while its own docstring at :340 promises 'Field-level FormField overrides take precedence over object defaults'; the value is load-bearing (reaches the DOM at :875 and :983). Pre-existing and unchanged by this PR, but newly EXPRESSIBLE because the shared type declares the key — the new test records the current answer explicitly rather than assuming it", "filed as #5596 (finding label): the same drift class one level up — FormViewSpec and FormSectionSpec are each hand-declared twice (console + app-shell SchemaForm.tsx), and unlike the field spec those two have ALREADY drifted in BOTH directions (app-shell has description/visibleWhen/visibleOn, console has label/groups/sharing/submitBehavior and a string arm on columns), so neither is a subset of the other. Filed as an observation rather than queued because converging the containers is a genuine architectural call, not a mechanical one" ] }Two notes that contradict nothing but are worth the PM's eye
-
A scope deviation I chose to make rather than stop for. The card fenced
form-spec.tsread-only and it is untouched — but it also listed the file surface as
FormPage.tsx+ pin + changeset, and reuse of an unreachable type is impossible.
FormFieldSpecwas not exported from@object-ui/app-shell's root, and
FormPage.tsxalready documents that same unreachability for a sibling case
(urlParams), which is plausibly how the third copy came to exist in the first place.
So I added two additiveexport typelines to app-shell's barrels. The fence named
one file and I did not touch it; I am flagging the barrels because "additive and
obviously fine" is exactly how scope creep reads from the inside. -
The footer instruction and the platform disagreed, and the platform won. The
dispatch asked for the barehttps://claude.ai/codefooter. On PR creation the write
path rewrote it to the session-URL form; on the subsequent body edit the bare form was
accepted as written. Recording it because it is platform behaviour, not another actor
editing the PR, and because a bare footer is the form that gets stripped on later
edits.
Generated by Claude Code
-
- added a commit that references this issue
on Aug 21, 2026
Found while implementing #5040 (PR #5537). Filed unassigned, for PM triage — not
worked on here:
apps/consolewas outside that card's declared file surface andis held by in-flight #5504.
Mechanism
#5040 is written around two inline descriptions of one contract — the
metadata-admin form field authoring type — converging into one. Measured, there
are three:
packages/app-shell/src/views/metadata-admin/SchemaForm.tsx—FormFieldSpecviews/metadata-admin/form-spec.tspackages/app-shell/src/views/metadata-admin/widgets.tsx—WidgetProps.fieldSpecFormFieldSpecapps/console/src/components/FormPage.tsx:278—interface FormFieldSpecSite 3 declares its own
interface FormFieldSpec, used at:275(
fields: Array<string \| FormFieldSpec>) and:352(const override: FormFieldSpec = …). It is a separate, hand-written description of the sameauthoring shape, under the same name, in a different package.
Why this is worth a card rather than a shrug
The defect #5040 recorded was not that a key was missing — it was that two
descriptions of one contract drifted, and nothing could notice because each
was only ever checked against itself. PR #5537 removed one of the two seams. A
third declaration under the same name leaves the same failure mode available:
site 3 can drift from the app-shell surface exactly as site 2 did, and the
symptom would again be "the type rejects the configuration the runtime accepts",
which no runtime test can see.
Note site 3 was NOT the source of the
dependsOndefect and does not have toadopt the app-shell type as-is — whether these are one contract or two layers
(
apps/consolemay legitimately author a narrower shape) is exactly thequestion triage should settle, and the answer might be "declare them distinct
and rename one", not "merge".
Severity note
Filed as an observation (
finding, nopm:queue): there is no user-reachablefailure today. Recording it because the shape — one contract, several
independent descriptions — is the class this repo has repeatedly paid for, and
because a card written around "two copies" would read as complete while a third
stayed open. Severity is triage's call, not mine at filing time.
Boundary
FormFieldSpec/FormPage.tsx/console-duplicate phrasings) — no card covers site 3.
FormFieldSpec没有声明dependsOn,但两个 widget 都读它 —— 类型化的 form spec 写不出这两个 widget 的必备配置 #5040: that card's declared surface waspackages/app-shell/src/views/metadata-admin/**, and it is landing theapp-shell half only, which PR metadata-admin: FormFieldSpec declares
dependsOn, one declaration for both halves #5537 states in its scope section.OS_CLOUD_URL=offrenders the marketplace as a red load-failure with wrong guidance, and the home page still promotes the marketplace entry #5504 is untouched by this report; it is named only because it currently holdsapps/console.Generated by Claude Code
Generated by Claude Code