Repository navigation
Console FormPage still drops the other three conditional-rule surfaces after #5594: section-level visibleWhen/visibleOn, and the object-level field rules visibleWhen/readonlyWhen/requiredWhen #5627
Description
Found while implementing #5594 (wiring the view-level field visibility predicate into
apps/console/src/components/FormPage.tsx). Filed unassigned for PM triage — outside that
card's scope: its triage fence declared it "the predicate wiring plus a regression pin",
and the two surfaces below are different keys with different user-visible consequences, so
folding them in would have been unreviewable scope creep rather than a bounded fix.
Verified on 8c87f0583 (the merge-base of the #5594 branch), before and after that change.
Mechanism
FormPage.tsx is the second, independent form renderer — its own buildSections, its own
JSX, serving both /f/:slug (public, anonymous) and /forms/:name (internal). After
#5594 it honours one of the four conditional-rule surfaces the sibling chain honours.
The other three are still dropped, and both drops happen in the same place: the two local
types that describe the payloads, which admit none of these keys, so nothing downstream can
read them.
1. Section-level visibleWhen / visibleOn
The local section type declares four keys and no visibility at all:
interface FormSectionSpec {
label?: string;
collapsible?: boolean;
collapsed?: boolean;
columns?: 1 | 2 | 3 | 4 | '1' | '2' | '3' | '4';
fields: Array[string | FormFieldSpec]; // angle brackets spelled out; see note below
}and the render maps every section unconditionally (FormPage.tsx:1398):
{sections.map((sec, i) => (
The app-shell counterpart does declare and honour it — FormSectionSpec in
packages/app-shell/src/views/metadata-admin/SchemaForm.tsx carries
visibleWhen/visibleOn (documented there as "Canonical section visibility predicate
(ADR-0089)"), and SchemaFormBody filters sections through readVisibility before
rendering. So a section an author conditioned away renders in full on the two console form
routes, including the public one.
2. Object-level field rules visibleWhen / readonlyWhen / requiredWhen
The local object-schema field type admits eight keys and none of the ADR-0036 rules:
interface ObjectFieldDef {
type: string;
label?: string;
required?: boolean;
defaultValue?: unknown;
maxLength?: number;
options?: ...;
placeholder?: string;
helpText?: string;
}buildSections reads only those, so RenderableField.readonly is override.readonly ?? false
and required is override.required ?? def.required ?? false — static, never conditional.
The sibling chain carries all three out of the object schema and resolves them through
resolveFieldRuleState — packages/plugin-form/src/sectionFields.ts, fromObjectSchema:
// Field-level conditional rules (ADR-0036) — the form renderer resolves
// them via `resolveFieldRuleState`; without this copy a sectioned form
// silently dropped rules that the flat (schema-order) form honors.
visibleWhen: field.visibleWhen,
readonlyWhen: field.readonlyWhen,
requiredWhen: field.requiredWhen,Note that #5594's fix reads the view-level predicate off the FormField override only.
An object-level visibleWhen — authored on the object's field, not on the form view — is a
separate slot the sibling keeps separate on purpose (sectionFields.ts routes the view
predicate into visibleOn precisely so the object rule is never clobbered), and this
renderer reads neither of those two.
Why this is not a duplicate
- Form-view FormField.visibleOn (CEL) is never evaluated — conditional fields always render #2212 and PR fix(form): evaluate view-level FormField.visibleOn with the canonical CEL engine #2214: the view-level
FormField.visibleOnon the plugin-form chain. - DrawerForm's no-sections field builder drops
visibleWhen/readonlyWhen/requiredWhen(andgroup) — ModalForm's identical builder carries them #4755:DrawerForm's no-sections field builder dropping the same three object-level
rules — same class, a third renderer, closed. - Console FormPage (the standalone /forms and internal FormView renderer) never evaluates visibleWhen/visibleOn — objectui#2212 was fixed in the OTHER form renderer #5594: the view-level field predicate in this renderer. Landed; this issue is what it
deliberately left. - [finding] The SAME drift class one level up: FormViewSpec and FormSectionSpec are each hand-declared twice (console + app-shell), and #5542 only converged the leaf #5596: the type-drift class one level up (
FormViewSpec/FormSectionSpeceach
hand-declared twice). Adjacent and relevant — convergingFormSectionSpecwould make the
section keys declarable here — but declarable is not evaluated. That is exactly the
[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 / Console FormPage (the standalone /forms and internal FormView renderer) never evaluates visibleWhen/visibleOn — objectui#2212 was fixed in the OTHER form renderer #5594 relationship repeating one level up: [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 madevisibleWhendeclarable on
the console's field type, and the predicate still rendered unconditionally until Console FormPage (the standalone /forms and internal FormView renderer) never evaluates visibleWhen/visibleOn — objectui#2212 was fixed in the OTHER form renderer #5594.
Searched open issues before filing (section visibility / FormSectionSpec / readonlyWhen
/ requiredWhen / console form renderer / conditional field phrasings). #5596 is the only
open match and it is about the type declarations, not the evaluation.
Severity note
User-reachable on the public /f/:slug route, same as #5594 — and the object-level half is
worse than fail-open hiding: a readonlyWhen that should lock a field leaves it editable,
which pairs badly with the server's fail-CLOSED behaviour for the unbound-scope case
(objectstack#4889, described in packages/core/src/evaluator/fieldRules.ts) — the user
edits, the save reports success, and the value never lands.
Two questions a fix has to answer rather than assume, which is part of why this is filed
instead of folded in:
- Whether a section hidden by its predicate should also suppress its fields' values on
submit — Console FormPage (the standalone /forms and internal FormView renderer) never evaluates visibleWhen/visibleOn — objectui#2212 was fixed in the OTHER form renderer #5594 deliberately kept conditional visibility a rendering rule for fields
(a hidden field still submits its value, matching the plugin-form chain), and the same
question repeats at section granularity. - Whether
requiredWhenhere needs theserverOwnedValuecarve-outresolveFieldRuleState
applies (A required field whosedefaultValueis a runtime token (NOW()/current_user) cannot be submitted from a create form #4069 /requiredWhen+ a runtimedefaultValuestill deadlocks a create form (the conditional half of #4069) #4085), given this route serves both create and edit (type: 'form'action fired from a record opens an empty CREATE form —/forms/:nameignores the?recordId=ActionRunner forwards #4278).
Note on the code blocks above
The two type excerpts have their generic angle brackets spelled out as Array[...] —
GitHub's body sanitizer eats short angle-bracket fragments, fenced code included (this was
measured on the #5594 PR body, which had Record of string to unknown silently reduced
to Record). Read the real declarations at apps/console/src/components/FormPage.tsx.
Generated by Claude Code
Activity
huangyiirene commentedon Aug 22, 2026
Concentration-round first-touch (triage seat): promoted
finding→pm:queue, Bug (labelbug), size M.Bug because it is user-reachable on the public
/f/:slugroute and the object-level half is worse than fail-open hiding: an un-honouredreadonlyWhenpairs with the server's fail-closed unbound-scope behaviour into "user edits, save reports success, value never lands".The card's two open questions, ruled at triage by precedent rather than escalated — both have settled answers one level down the sibling chain:
requiredWhentakes theserverOwnedValuecarve-out (resolveFieldRuleStatealready applies it, A required field whosedefaultValueis a runtime token (NOW()/current_user) cannot be submitted from a create form #4069/requiredWhen+ a runtimedefaultValuestill deadlocks a create form (the conditional half of #4069) #4085) since the route serves both create and edit — reuse the resolver, don't re-derive.Fix shape: carry the keys through the two local types and evaluate via the existing shared machinery (
readVisibilityfor sections,resolveFieldRuleStatefor the three field rules) — the consumer-side re-implementation is the thing to end, not extend. Note the sanitizer warning in the card: read the real declarations from the file, not the escaped excerpts. Adjacent: #5596 (type-drift, declarable ≠ evaluated) — coordinate, don't fold uninvited.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Claim: PM loop round 18
Session:
session_01EuPCi56cnGyykygi3z9w4mBranch:
claude/issue-5627-formpage-conditional-rule-surfacesWorktree:
objectui-issue-5627Domain:
domain:uiFile surface:
apps/console/src/components/FormPage.tsxand its tests (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opusClause-②: no
Serial constraints cleared:
apps/consoleis free — #5619/PR #5718 merged 22:42Z and released it. Round-18 siblings fenced disjoint: #5424 (packages/auth) and #5344 (packages/app-shell/src/views/metadata-admin/inspectors/).Tier basis. No path-derived mandate. Content judgement: clause ② does not trip — these keys are already declared on the sibling chain and in the spec; making a renderer that ignores them honour them is
declared = enforcedrestoration, not a widening of any authoring surface. Size follows triage'sM.useIsWorkspaceAdminno longer exists. It is replaced byuseWorkspaceAdminStatus, which returns a resolved/unresolved verdict rather than a bare boolean. IfFormPageor anything it renders touches admin gating, re-derive againstc7cd2b60b, not against anything the card describes.RULING — triage's first-touch, 2026-08-22T13:10Z, binding. Both of the card's open questions were ruled by precedent rather than escalated:
requiredWhentakes theserverOwnedValuecarve-out (resolveFieldRuleStatealready applies it, A required field whosedefaultValueis a runtime token (NOW()/current_user) cannot be submitted from a create form #4069/requiredWhen+ a runtimedefaultValuestill deadlocks a create form (the conditional half of #4069) #4085), since the route serves both create and edit. Reuse the resolver, do not re-derive it.Fix shape, ruled: carry the keys through the two local types and evaluate via the existing shared machinery —
readVisibilityfor sections,resolveFieldRuleStatefor the three field rules. ⛔ "The consumer-side re-implementation is the thing to end, not extend."origin/mainc7cd2b60b:interface ObjectFieldDefis at:95✓ (the card quotes it without a line).sections.maprenders unconditionally at:1403(the card says:1398— drift of 5). A secondsections.mapsits at:393; establish which one is the render and which is the builder.FormSectionSpecreturns ZERO hits inFormPage.tsx. The card quotesinterface FormSectionSpec { … }as living in this file. It does not — at least not under that name. The card was verified on8c87f0583and main has moved a long way since. Re-derive the section type's real name and shape from the file before writing anything, and say in your report what it actually is. The card's behavioural claim (sections render unconditionally, no visibility key is admitted) is independently supported by:1403; it is the type citation that is stale. Note the app-shell counterpart genuinely is calledFormSectionSpec(packages/app-shell/src/views/metadata-admin/SchemaForm.tsx) — do not let the two blur.The severity anchor, so scope stays honest: the object-level half is worse than fail-open hiding. An un-honoured
readonlyWhenleaves a field editable that should be locked, which pairs with the server's fail-closed unbound-scope behaviour into "the user edits, the save reports success, and the value never lands." User-reachable on the public/f/:slugroute.Sanitizer warning, carried from the card and confirmed by this lane's own measurement: GitHub's body sanitizer eats bare angle-bracket fragments, fenced code included (angle brackets inside backticks survive). The card's type excerpts are written as
Array[...]for that reason. ⛔ Read the real declarations from the file, never from the card's escaped excerpts — and when you write your PR body and report, keep generics inside backticks.Adjacent, do not fold uninvited: #5596 (the type-drift class one level up — declarable ≠ evaluated). Coordinate if it matters; do not absorb it.
Generated by Claude Code
fix(console): honour the other three conditional-rule surfaces in For…
os-zhuang commentedon Aug 23, 2026
os-dev-report
{ "issue": 5627, "status": "done", "branch": "claude/issue-5627-formpage-conditional-rule-surfaces", "pr": "https://github.com/objectstack-ai/objectui/pull/5728", "premise_still_valid": true, "premise_note": "The BEHAVIOURAL premise is intact and re-measured on c7cd2b60b: `ObjectFieldDef` at :95 admits none of the ADR-0036 rules, and the render maps every section unconditionally at :1403 (the card said :1398). The stale half is the TYPE citation, exactly as the dispatch measured: `FormSectionSpec` has zero hits in `FormPage.tsx`.", "section_type_actually_called": "There is no local section INPUT type in this file any more. #5596 already landed and replaced the hand copy with the shared `FormSectionSpec` from `packages/app-shell/src/views/metadata-admin/form-spec.ts`, reached here via the imported `FormViewSpec` (`form.sections` / `form.groups`). That shared type has DECLARED `visibleWhen`/`visibleOn` all along - its own docstring names this card while saying so. The only local section type is the renderer's own normalised row, `interface RenderableSection` at :351 (label/columns/collapsible/collapsed/fields), which is what I widened with a `visibleWhen` slot. So the section half of this card was EVALUATION-only; the local type work was the object-field half alone.", "summary": "FormPage now honours the other three conditional-rule surfaces. Sections: `buildSections` carries `sec.visibleWhen ?? sec.visibleOn` onto `RenderableSection` and the render calls the new `isSectionVisible`, which routes the predicate through `evalFieldPredicate` (same engine, same `record.*`/`previous.*` scope, same fail-open-but-loud behaviour as the field predicate since #5594). Object-level rules: `ObjectFieldDef` admits `visibleWhen`/`readonlyWhen`/`requiredWhen`, `buildSections` carries them onto the row as a group (`RenderableField.rules`), and the new `resolveRowState` hands that group WHOLE to `@object-ui/core`'s `resolveFieldRuleState` - no rule semantics re-derived here, which is what carries the static-flag precedence rules and the `serverOwnedValue` carve-out in unchanged. `isCreateForm` is a REQUIRED parameter read off the URL switch `readFormRecordTarget` already owns (#4085 forbids inferring it). The two visibility LAYERS are ANDed and never collapsed, and both are evaluated even when the first has decided, so a typo in either layer still warns. `FieldInput` now takes the resolved `{ readonly, required }` as a prop instead of the static flags, so the control and its asterisk read one verdict.", "ruling_clause_1": "Pinned as ruled, and the stop-and-report escape did NOT fire. A hidden section's fields still submit - asserted positively on the POST body. I looked for the concrete contradiction the ruling warned about (a validation firing on a value the user could never see) and there is none in this renderer: the only client-side enforcement is the DOM `required` attribute, which is only ever emitted on a control that is rendered, so a field inside a hidden section carries no constraint. A server-side required check on such a value is the same exposure a statically hidden field has had since before #5594, i.e. not a new fork.", "ruling_clause_2": "`requiredWhen` takes the `serverOwnedValue` carve-out via `resolveFieldRuleState`, not re-derived - `isServerOwnedValue` is handed the declared default the row already carries. Consequence worth flagging to PM: because the resolver applies `serverOwnedValue` to BOTH spellings of required, a create form here also stops marking a STATIC `required: true` field required when its `defaultValue` is a runtime instruction (#4069). That is the resolver's ruled behaviour and every sibling renderer already has it; narrowing the carve-out to the conditional half would have meant a consumer-side re-derivation, which the ruling forbids.", "readVisibility_note": "The ruling named `readVisibility` for sections. It is module-private to app-shell's `SchemaForm.tsx` and is NOT part of `@object-ui/app-shell`'s published surface, so it cannot be imported - and what it does is a two-key `??`, which this file already spells once at build time for fields. The half that decides anything, the ENGINE, is the shared `evalFieldPredicate`. No fourth consumer-side re-implementation of the evaluation was written.", "how_each_surface_was_asserted": { "mechanism": "React Testing Library render of the real `FormPage` over stubbed `fetch`, on BOTH routes (`/forms/:name` internal and `/f/:slug` public). Every surface asserts RENDERED OUTPUT, never an intermediate object - static checks cannot see any of these failures. New file `apps/console/src/components/FormPage.conditionalRules.test.tsx`, 24 cases.", "section_visibleWhen_visibleOn": "Element presence: the section HEADING and its fields are both absent when the predicate is false and present when true (`queryByText('Escalation')` + `queryByLabelText('Notes')`) - heading AND fields, which is what distinguishes a section rule from a field rule applied to each member. Covered: canonical key, deprecated alias, canonical-wins-over-alias, the `{ dialect, source }` wire shape, live re-evaluation as the user types (both directions), `previous.*` binding on an edit form, and the public route.", "object_visibleWhen": "Element presence via `queryByLabelText`, plus a dedicated layering case proving a view-level predicate cannot re-show what the object rule hid and vice versa (three rows in one form: view-true-over-object-false, view-false-over-object-true, both-true).", "object_readonlyWhen": "`expect(control).toBeDisabled()` on the real input, resolved by id (`#f_<name>`), plus the false-predicate twin left editable in the same render, plus a live re-evaluation case that types into a sibling and waits for the control to become disabled.", "object_requiredWhen": "`expect(control).toBeRequired()` AND the required MARKER read out of the rendered `label` element (`label[for=f_<name>]`.textContent contains `*`), asserted together so the control and the marker cannot pass separately. `getByLabelText` deliberately not used for these - the marker is inside the label, so a required field's accessible name becomes `Notes*` and an exact-string query would pass or fail for the wrong reason.", "submit_behaviour_clause_1": "Positive assertion on the POST body: a field inside a section hidden by its predicate, seeded via `?prefill_notes=carried`, is read back off the captured request as `carried`.", "carve_out": "Create form vs edit form, same object schema, same `requiredWhen`: not required on create (`defaultValue: 'NOW()'`), required on edit under `?recordId=`. The edit case is the control that stops the create case from being satisfied by a renderer that never reads `requiredWhen` at all." }, "tests": "All runs under the shared verify lock (`scripts/pm/os-verify-lock.sh`), all VERDICT lines `command-exit 0`, all on final commit `2eed0fe2a` (working tree clean at that sha; `git rev-parse --short HEAD` = 2eed0fe2a). (1) New + existing pins: `pnpm exec vitest run apps/console/src/components/FormPage.conditionalRules.test.tsx apps/console/src/components/FormPage.visibleWhen.test.tsx --maxWorkers=2` -> `Test Files 2 passed (2) / Tests 36 passed (36)`. (2) Whole console suite: `pnpm exec vitest run apps/console --maxWorkers=2` -> `Test Files 72 passed (72) / Tests 782 passed (782)`, TEST_EXIT=0. (3) `pnpm --filter @object-ui/console type-check` (script echoed: `tsc --noEmit && tsc -b tsconfig.node.json --force`) -> TYPECHECK_EXIT=0, after `pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build` -> BUILD_EXIT=0 (142s). (4) `pnpm --filter @object-ui/console lint` (script echoed: `eslint .`) -> LINT_EXIT=0, `202 problems (0 errors, 202 warnings)`; the two exports I added contribute two more `react-refresh/only-export-components` warnings of a category this file already carries a dozen times (it already exports `buildSections`, `isFieldVisible`, `readPrefill`, ...). REVERSE VERIFICATION: `FormPage.tsx` reverted to `c7cd2b60b` with the new pin file in place -> `Tests 20 failed | 4 passed (24)`. Mutation confirmed ON DISK before the run, anchored on the text it had to remove and restore (`resolveRowState` 0 hits, `isSectionVisible` 0 hits, the pre-#5627 render line `sec.fields.filter((f) => isFieldVisible` back at 1 hit) - not on an exit code and not on a bare diff; restored by a `trap ... EXIT INT TERM`, and the tree verified clean afterwards. Direction observed = the predicted one (red), with two sub-shapes worth naming: the two unit cases fail as `TypeError: isSectionVisible is not a function` / `resolveRowState is not a function` (the exports are new), and `CONTROL: a broken section predicate fails OPEN, loudly` fails only on its LOUD half (`expected \"warn\" to be called at least once`) - fail-open is green either way, because a renderer that never evaluates cannot hide anything. The 4 green pre-fix are exactly the controls that must be green on both sides: the two `no predicate at all` cases, `a static readonly is not weakened by a readonlyWhen that resolves false`, and the CREATE half of the carve-out (green pre-fix for the wrong reason, which is why its EDIT twin exists and is red).", "gates_run": [ "pnpm --filter @object-ui/console type-check -> PASS (TYPECHECK_EXIT=0)", "pnpm --filter @object-ui/console lint -> PASS (LINT_EXIT=0; 202 problems, 0 errors)", "pnpm exec vitest run apps/console -> PASS (72 files / 782 tests, TEST_EXIT=0)", "node scripts/check-control-bytes.mjs -> PASS ('check-control-bytes: OK (scanned 4776 tracked text file(s); skipped 85 binary)')", "node scripts/check-changeset-presence.mjs -> PASS ('1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)')", "node scripts/check-changeset-no-major.mjs -> PASS ('No changeset declares a major bump')", "node scripts/check-changeset-fixed.mjs -> PASS ('All workspace packages are in the changeset fixed group')", "pnpm check:phantom-deps -> PASS ('Every in-scope import is declared by the package that publishes it')", "pnpm check:self-import -> PASS ('No package names itself inside its own src/')", "pnpm check:spec-symbols -> PASS ('spec symbol derivation: 1295 files scanned against 4966 spec export names')", "pnpm check:esm-specifiers -> PASS ('no un-ledgered package emits an extensionless relative specifier')", "pnpm check:i18n-keys -> PASS ('Every in-scope call-site key resolves against the en pack')", "pnpm check:i18n-drift -> PASS ('No en value changed in this range')", "pnpm check:published-dist -> PASS ('No published package's build output carries tooling material')", "pnpm check:eager-closure -> PASS (exit 0)", "pnpm check:action-forward-parity -> PASS (exit 0)", "pnpm type-check:coverage -> PASS ('45/46 via type-check, 0 known-broken')", "pnpm lint:coverage -> PASS ('46/46 packages linted, 0 with outstanding errors')" ], "gate_derivation_note": "Derived from objectui's OWN `.github/workflows/` (ci.yml, lint.yml, control-bytes.yml, changeset-*.yml, performance-budget.yml, published-dist-gate.yml, node-esm-load-gate.yml) and the root `package.json`, NOT from `scripts/pm/dispatch-gates.mjs` - that script lives in objectstack and objectui has no `scripts/pm/`. Not run, and why: `check:doc-types` / `check:doc-snippets` / `docs-links` / `check-links` (no docs touched), `check:skills-paths` (no skills touched), `shadcn-check` (no components touched), `check:node-esm-load` full leg and `live-e2e` (CI's - they build/boot; the specifier leg passed), `pnpm check` (the CLI's metadata check - no metadata touched). NARROWING DECLARED for the three big ones: type-check, lint and test were run for the ONLY package this diff touches, in full - `eslint .` over `apps/console` is that package's own gate command, not a file subset, so the narrowing excludes only packages whose files this diff does not contain and whose lint config is not type-aware across package boundaries. Repo-wide `pnpm lint` / `pnpm type-check` / `pnpm test` remain CI's run.", "files_changed": [ "apps/console/src/components/FormPage.tsx (M) - header docblock; `ObjectFieldDef` +3 rule keys; `RenderableField` +`rules` group and a re-worded `visibleWhen` docstring naming its LAYER; `RenderableSection` +`visibleWhen`; `buildSections` carries both; new exported `isSectionVisible` and `resolveRowState`; `FieldInput` takes a resolved `state` prop; the render skips a hidden section in place (returns null, keeps the positional key so a re-evaluation mid-typing cannot remount later sections and steal the caret) and resolves each row once", "apps/console/src/components/FormPage.conditionalRules.test.tsx (A) - 24 cases, all on rendered output", ".changeset/formpage-conditional-rule-surfaces-5627.md (A) - patch, @object-ui/console" ], "open_questions": [], "out_of_scope_findings": [ "filed as #5727: `readPrefill` in this same file seeds a RUNTIME-default `defaultValue` (`NOW()`, `current_user`, a CEL envelope) literally into the control, suppressing the server-side resolution the declaration asks for - `isRuntimeDefault` has zero hits in `FormPage.tsx` while `plugin-form`'s `schemaDefaults.ts` routes its seeding through it. Different defect class (seeding, not conditional-rule evaluation), filed unassigned with no label. Searched open issues first; no match." ], "fence_note": "Nothing outside the fence was edited. `packages/app-shell/**` and `packages/plugin-form/**` were READ as reference implementations only; #5596 was not folded in (it is already landed - this PR consumes its result); `content/docs/**` and `apps/site/**` untouched.", "zone_0_note": "Checked: `FormPage.tsx` and everything it renders touch NO admin gating. `useIsWorkspaceAdmin` / `useWorkspaceAdminStatus` have zero hits in this file and in its subtree, so #5619 / PR #5718 does not bear on this change. Branch cut from `origin/main` at `c7cd2b60b`, which is the sha the dispatch measured." }The issue body is not truncated - it ends with the
## Note on the code blocks abovesection explaining theArray[...]spelling of the two type excerpts, and I read the real declarations from the file rather than from those excerpts, as instructed.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Read-back correction on the report above, plus a sanitizer measurement that refines this lane's rule.
Measurement. The dispatch says angle brackets inside backticks survive. On this comment they did not: two backticked placeholders written as
#f_+ angle-bracketednameandlabel[for=f_+ angle-bracketedname+]came back as#f_andlabel[for=f_]— the angle-bracketed token was eaten inside backticks. Everything else survived, including{ dialect, source },?recordId=, and an escaped fat arrow. So the safe rule is narrower than "backticks protect you": an angle-bracketed placeholder is eaten wherever it appears, and a placeholder should be spelled with square brackets. Theos-dev-reportmarker itself survived as literal text.What those two fields should read:
object_readonlyWhen: the control is resolved by id,#f_[fieldname].object_requiredWhen: the marker is read out oflabel[for="f_[fieldname]"].Nothing else in the report is affected, and no claim changes.
Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
ACCEPT (on merits) — PR #5728 · reviewer of record,
domain:uiseat, round 18success+ 3skippedis verified at head2eed0fe2a.The stale type citation resolved into a better fact than "the card was wrong"
I measured that
FormSectionSpechad zero hits inFormPage.tsxand asked for the real name. The answer: there is no local section input type any more. #5596 already landed and replaced the hand copy with the sharedFormSectionSpecinpackages/app-shell/src/views/metadata-admin/form-spec.ts, reached here through the importedFormViewSpec— and that shared type has declaredvisibleWhen/visibleOnall along, with its own docstring naming this card while saying so.So the section half of this card was evaluation-only; the local type work was the object-field half alone. That is precisely the relationship the card predicted would repeat one level up — "#5542 made
visibleWhendeclarable on the console's field type, and the predicate still rendered unconditionally until #5594" — and it did: #5596 made the section keys declarable, and nothing evaluated them. Declarable is not evaluated, demonstrated twice on the same file.The ruling named a helper that cannot be imported — third instance of this pattern
Triage's fix shape named
readVisibilityfor sections. It is module-private to app-shell'sSchemaForm.tsxand not part of@object-ui/app-shell's published surface, so it cannot be reached fromapps/console. What it actually does is a two-key??, which this file already spells once at build time for fields; the half that decides anything is the sharedevalFieldPredicateengine — and that is what the change routes through. The ruling's observable requirement (⛔ "the consumer-side re-implementation is the thing to end, not extend") is met: no fourth re-implementation of the evaluation was written.That is the third time this lane has hit "a ruling's named home can be unbuildable — honour the observable requirement, not the literal placement", and the third time a dev measured the dependency/visibility direction before deciding rather than after.
Both ruled clauses, and one consequence beyond the card's literal scope
Clause 1 — the stop-and-report escape did not fire, and it was actually looked for. A hidden section's fields still submit, asserted positively on the POST body (a field inside a hidden section, seeded via
?prefill_notes=carried, read back off the captured request). The ruling warned about a concrete contradiction — a validation firing on a value the user could never see — and the dev established there is none with a reason: the only client-side enforcement is the DOMrequiredattribute, which is only ever emitted on a rendered control, so a field inside a hidden section carries no constraint. A server-side check on such a value is the exposure a statically hidden field has had since before #5594 — not a new fork.Clause 2 — carve-out reused, not re-derived, and its side effect surfaced rather than buried. Because
resolveFieldRuleStateappliesserverOwnedValueto both spellings of required, a create form here now also stops marking a staticrequired: truefield required when itsdefaultValueis a runtime instruction (#4069). That is the resolver's ruled behaviour and every sibling renderer already has it — and narrowing it to the conditional half would have meant exactly the consumer-side re-derivation the ruling forbids. Correct call, and correctly reported as a behaviour change rather than left for someone to discover.Reverse verification: 20 red / 4 green, and the four greens are the point
The four green-before are the controls that must be green on both sides — the two "no predicate at all" cases, "a static readonly is not weakened by a
readonlyWhenthat resolves false", and the CREATE half of the carve-out, which is green pre-fix for the wrong reason — which is why its EDIT twin exists and is red. That pairing is what stops the carve-out pin from being satisfied by a renderer that never readsrequiredWhenat all.Two red sub-shapes named honestly rather than counted: two cases fail as
TypeError: isSectionVisible is not a function(new exports), and the fail-open control fails only on its LOUD half — "fail-open is green either way, because a renderer that never evaluates cannot hide anything." Reporting which reds are evidence and which are artefacts is the difference between a reverse leg and a number.A detail static analysis would never surface: the render skips a hidden section in place, returning null while keeping the positional key, so a re-evaluation mid-typing cannot remount later sections and steal the caret. Assertion mechanism throughout is rendered DOM on both routes (internal
/forms/:nameand public/f/:slug), withrequiredWhenasserting the control and the label's asterisk together so neither can pass alone.I wrote, in three round-18 dispatches, that bare angle brackets are eaten while angle brackets inside backticks survive — generalised from #5661's read-back, where
`<ul>`and`<li>`came through intact. This dev's read-back contradicts the general form: two angle-bracketed placeholders were eaten even inside backticks.So the honest state is: two measurements, partly incompatible, and the rule is not "backticks protect." I am not averaging them into a confident wrong rule — what survives both readings is the operational instruction: ⛔ avoid angle-bracketed placeholders anywhere in a body written through this tooling, backticked or not, and always read the comment back. The distinguishing variable (real tag name vs placeholder? nesting? position?) is unmeasured, and saying so is better than a tidy rule that fails silently. Carried to the seat post in that form.
Out-of-scope finding verified: #5727 —
readPrefillin this same file seeds a runtime-defaultdefaultValue(NOW(),current_user, a CEL envelope) literally into the control, suppressing the server-side resolution the declaration asks for;isRuntimeDefaulthas zero hits here while plugin-form'sschemaDefaults.tsroutes its seeding through it. Different defect class, correctly filed rather than folded in — and note it is the sameserverOwnedValueconcept arriving from the other direction, which makes it a natural next card.Zone 0 checked and answered:
useIsWorkspaceAdmin/useWorkspaceAdminStatushave zero hits in this file and its subtree, so #5619's removal does not bear on this change. Branch cut fromc7cd2b60b, the sha the dispatch measured.Generated by Claude Code
fix(console): honour the other three conditional-rule surfaces in For…