Skip to content

spec(ui): field-level conditionalFormatting — a field declares [{ condition, style }] cell rules (CEL over value and record), the row block's own grammar, for presentational rules that carry no semantic (maintainer-directed) #22228

Description

@objectstack-fleet

Filing gate: ③ a maintainer-directed task. Filed by the director seat (seat post #12708, summon #35, session_01VYToj6PQehTEKNrjGM9akg) after the decision discussion on objectui#11815. The maintainer asked whether a date field should be configured with conditionalFormatting directly for cases like an overdue date (「对于类似场景需求,是否建议日期字段直接配置 conditionalFormatting」); the seat answered that a deadline is a semantic and stays a declaration (objectui#11815, ruled D), while a field-level conditional-formatting block is the right home for presentational rules that carry no semantic, and proposed this card; the maintainer answered 「同意」. ⛔ Not a claim.

Reader: the domain:spec seat. Clause-②: yes — the published accept set of a field definition widens by one key; the spec-lane review at tier applies. priority:p3 (not on the North Star path; a capability the row-level block already half-serves).

What exists today

  • List views, the object-grid and object-kanban page blocks declare conditionalFormatting: [{ condition, style }]: "the first rule whose CEL condition holds applies its CSS style map to the row" (packages/spec/src/ui/component.zod.ts, the describe on the grid's and the kanban card's member; ListViewSchema.shape.conditionalFormatting is the single definition). examples/app-showcase/src/ui/views/field-zoo.view.ts authors one.
  • A field definition carries per-record CEL predicates for behaviour — visibleWhen, readonlyWhen, requiredWhen (packages/spec/src/data/field.zod.ts) — and no presentational rule. A cell's colour today comes either from a row rule (the whole row, once per view) or from a renderer's own hard-coded judgement (the date cell's overdue red, objectui#11815).

What this card adds, and only this

One optional key on the field definition, in the row block's own grammar:

  • conditionalFormatting: [{ condition, style }] on a field, ordered; the first rule whose CEL condition holds applies its CSS style map to that field's cell, wherever the field renders (grid, kanban card, record page, related list, report cell).
  • CEL scope: value (the field's value on this record) and record (the row), the same record scope the row rules and visibleWhen already evaluate in; today() and the engine's existing date functions as they are.
  • Precedence: a field rule styles its cell; a row rule styles the row; both may hold. ⛔ No new style vocabulary: the style map is the row block's.
  • The authoring gate rejects a condition that does not parse, as it does for visibleWhen (ADR-0049: declared means enforced).

Examples the card is for: an amount below zero in red; stock under its safety level in amber; an SLA field in three tiers by remaining time; a priority cell coloured by its value. ⛔ Not for a deadline's overdue state: that is a semantic declaration (objectui#11815 D: dueLike with settledWhen), from which wording, colour, and later filters and metrics derive; a style rule cannot produce the wording and would be re-authored per view.

Landing shape

  • packages/spec schema + describe, the ADR-0087 conversion table entry, the generated baselines and forms, docs (content/docs/... where visibleWhen is taught), a pin test for the authoring gate and for the CEL scope names.
  • Changeset: @objectstack/spec minor.
  • objectui consumes it as its own card after the spec publishes: the cell renderers read the resolved style through one helper (the same host-to-cell channel objectui#11815 opens), ⛔ not this card.

Dedupe: the 128 open objectstack issues listed through the repository endpoint and scanned (title and body) for conditionalFormatting / field style / cell style / 条件格式: 0. Prior rulings read: objectui#9738 (dueLike is objectui-owned until the protocol claims it), objectui#11676 and #11815 (no guessing a record's state; a deadline is declared) → 3 hits; ADR none on conditional formatting.

Related: packages/spec/src/ui/component.zod.ts · packages/spec/src/data/field.zod.ts · examples/app-showcase/src/ui/views/field-zoo.view.ts · objectui#11815 · objectui packages/fields/src/index.tsx.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-09T19:43Z
    Session: session_01KNKBCRDJCu5tGy3TEbvtrF
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-22228-field-conditional-formatting
    Worktree: objectstack-issue-22228
    Domain: domain:spec
    Seat: domain:spec#3 (seat post #18883)
    File surface (at origin/main ee8751d41e; stop on breach and explain in the report):

  2. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22228,
    "status": "done",
    "branch": "claude/issue-22228-field-conditional-formatting",
    "pr": "#22546",
    "session": "session_01KNKBCRDJCu5tGy3TEbvtrF",
    "premise_still_valid": true,
    "summary": "FieldSchema gains optional conditionalFormatting: an ordered list of { condition, style } rules. The first rule whose CEL condition holds styles that field's cell. The rule element moved out of ListViewSchema into packages/spec/src/shared/conditional-formatting.zod.ts (ConditionalFormattingRuleSchema), because data/field.zod.ts cannot import ui/view.zod.ts. Both members mount it, pinned by identity, so there is one grammar and no second style vocabulary. The accept set of list views and the grid/kanban blocks is unchanged. @objectstack/lint's field walk judges each condition at os build, os validate and the object save door, with scope value + record only: value is passed as ExprSchemaHint.roots for this one call, SCOPE_ROOTS is not widened, and a new fieldFormattingRootIssue verdict refuses previous, parent, the user roots and every other judged root. Liveness row: planned, drilled, no authorWarn, with the flip condition written in. No ADR-0087 entry: check:adr-0087-registration exits 0, and the key was refused by strict FieldSchema on the base. Docs: one table row and a Cell formatting subsection in content/docs/data-modeling/fields.mdx with the amount-below-zero example. Changesets: spec minor, lint minor, metadata-core patch, driver-sql patch.",
    "tests": "All locked runs (os-verify-lock) on the merged tree 14b8535. The one later commit, ffce73a, touches only packages/spec/llms.txt; check:llms-txt and the two spec files that relate to it re-ran green there (2 files, 32 passed). spec --project local: 632 files, 18894 passed, 1 todo. spec --project repo: 54 files, 915 passed. lint: 132 files, 6047 passed. metadata-core: 17 files, 388 passed. metadata-protocol (whole): 223 files passed, 3 skipped; 28347 tests passed, 19 skipped. metadata: 58 files, 871 passed. driver-sql: 218 files passed, 11 skipped; 3635 passed, 206 skipped. dogfood test/expression-conformance.test.ts: 7 passed. Examples showcase / crm / todo: 33/408, 5/45 and 7/238 passed, and objectstack validate passed on all three. typecheck exit 0 for spec (incl. check:test-typecheck), lint (incl. check:test-typecheck), metadata-core, driver-sql, metadata-protocol and dogfood. New pins: spec field-conditional-formatting.test.ts (19), lint validate-expressions.field-conditional-formatting.test.ts (41), and 1 FLS case. cli integration tier: declared to CI, since no spawn entry was touched. Filter direction: each package with --filter on itself, over the dispatch's consumer radius; no downstream sweep. ABLATION: 4 legs, each committed first and mutated via scripts/ablation-replace.mjs WRAP (anchor 1 -> 0, blob changed), each restored to blob == HEAD with git diff HEAD empty. The subjects resolve to src through relative imports, so no dist leg applies. Expected direction red, observed red: (1) value root dropped from the cell-rule check call: 4 red / 37 green. (2) fieldFormattingRootIssue always null: 29 red / 12 green. (3) the field mounts a loose z.object instead of the shared element: 9 red / 10 green. (4) the FLS scrub drops only the condition, not the whole rule: 1 red / 51 green.",
    "mcp_calls": "0",
    "api_writes": "3 REST writes, each a POST /repos/objectstack-ai/objectstack/dispatches through the fleet-write relay (scripts/pm/fleet-write/dispatch.mjs, transport auto -> dispatch). The relay executed them as objectstack-fleet[bot]: (1) pr_create, which opened draft #22546; the read-back matched byte for byte, 12761 bytes. (2) label-write --assign zhuangjianguo on #22546, with no label written; the read-back matches. (3) this os-dev-report comment on #22228. Plus git push to the branch: the empty probe push, then each WIP commit; not REST.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: none filed. main-red note: pnpm check:platform-checklist exits 1 on a pristine origin/main faf6348 worktree, the same as on this branch. areas/attachments-storage.json reports ABSENT SYMBOL packages/services/service-storage/src/attachment-access-hooks.ts#canEdit and anchors 27 below a floor of 28; the last edit to the hooks file is ce3d0ad. This diff touches neither file. The gate is kept out of per-PR CI by design, and its standing caller platform-checklist-watchdog.yml owns the anchor issue ("check:platform-checklist is red on main"). carrier: that watchdog. Noted, not filed. dedupe words: platform-checklist canEdit attachment-access-hooks anchors floor",
    "carrier: objectui's cell-renderer card (after this spec publishes). Note, not filed: nothing refuses a cell rule that reads through a reference field (record.REF.FIELD), which is the same as visibleWhen. Whether a cell host hydrates the related record is for that card to declare. Noted in Acceptance notes.",
    "carrier: none (承接者:无). Note, not filed: check:doc-formula-expressions admits only the field-level When slots, so a docs example of conditionalFormatting[].condition is judged by no corpus gate. Recorded in Acceptance notes only."
    ],
    "gates": "At ffce73a. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 116 families, the same set as before the last commit. 115 were run with their exit codes recorded; check:dual-build-cjs-loads is NOT MEASURED, reason: no whole-workspace build per dispatch, left to CI. --ran verdict: '116 derived, 115 run, 0 NOT-MEASURED, 1 UNRUN' (that UNRUN is dual-build-cjs-loads). Exit codes: 114 x 0, and pnpm check:platform-checklist x 1, which is red identically on pristine origin/main faf6348 (see out_of_scope_findings). Artifact-roster block: 51 commands run, check:error-code-provenance included, 48 exit 0. The 3 PR-context roster guards exit 2 without PR context and were re-run wired to PR #22546 (PR_NUMBER / PR_HEAD_REF / PR_BODY / GITHUB_REPOSITORY / GITHUB_TOKEN). check-closing-target-claim exit 0: 'PR #22546 closes #22228, and each carries a Claim: whose Branch: line names claude/issue-22228-field-conditional-formatting'. check-partof-closing-keyword exit 0. check-single-claim-paths exit 0. Spec artifacts: build, then check:generated, then --fix only for what it proved stale. That was skill-refs, api-surface (prereq), docs and strictness-ledger, then docs once more after the merge. Final check:generated: all 15 current. Governed predicate: check-governed-merges.mjs --test over the 3-dot file list says '0 of 28 path(s) hit the register after 3 generated-artifact lift(s)', NOT governed. Two caveats: the merge-group guard job cannot recompute the lifts, and the size and fork checks are NOT MEASURED by --test. CI at report time on ffce73a: 32 check runs, 11 success, 2 skipped, 19 in_progress.",
    "line_budget": "PR #22546: 699 additions + 36 deletions = 735 changed lines (generated included), under the 3,000-line threshold.",
    "deviations": [
    "Files beyond the claim's file surface, each demanded by a gate or by the shared element, all listed in the PR body: shared/conditional-formatting.zod.ts and ui/view.zod.ts for the shared element. The re-keyed declaring position in the dogfood expression-conformance ledger and evaluated-slot-population.test.ts. metadata-form-zod-reconciliation.test.ts: a root omit row, the page.print precedent, instead of a form control. metadata-core FLS positions plus a test. driver-sql FIELD_KEY_STORAGE_CLASS. metadata-protocol CARD_PROPERTY_COUNTS field 77 -> 78. packages/spec/llms.txt, whose counts check:llms-txt checks. Generated: content/docs/references, strictness counts, skills/
    /references/_index.md (generator-owned).",
    "Changeset entries beyond the two the dispatch named: @objectstack/metadata-core patch and @objectstack/driver-sql patch. Both packages ship changed code (an FLS scrub for the new key, and a storage-class row). This follows the dispatch's 'EVERY package that publishes something new' rule; the #22227 precedent added none for the same two files.",
    "The list view's unknown-rule-key refusal text changed, because the element is shared: the history sentence is now true for both members, and the color guidance leads with 'put it in style'. The accept set is unchanged; no pin or doc carried the old text.",
    "main moved 3 commits after the merge 5b81e00 (to faf6348). Not re-merged: the file sets are disjoint and a local merge-tree is clean. The PM decides whether a base sync is owed before enqueue.",
    "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. AGENTS.md was followed instead: the model-free trailer pair, and the session-URL footer on the PR body.",
    "MCP: none. git push was used once as the write probe and after each WIP commit (one category in the budget)."
    ],
    "files_changed": [
    ".changeset/22228-field-conditional-formatting.md",
    "content/docs/data-modeling/fields.mdx",
    "content/docs/references/data/field.mdx (generated)",
    "content/docs/references/data/object.mdx (generated)",
    "content/docs/references/system/migration.mdx (generated)",
    "docs/audits/2026-07-unknown-key-strictness-ledger.counts/shared.md (generated)",
    "docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md (generated)",
    "packages/drivers/driver-sql/src/builtin-column-collision.ts",
    "packages/lint/src/validate-expressions.ts",
    "packages/lint/src/validate-expressions.test.ts",
    "packages/lint/src/validate-expressions.field-conditional-formatting.test.ts",
    "packages/metadata-core/src/object-schema-fls-references.ts",
    "packages/metadata-core/src/object-schema-fls-references.test.ts",
    "packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts",
    "packages/qa/dogfood/test/expression-conformance.ledger.ts",
    "packages/spec/authorable-surface/data.json (generated)",
    "packages/spec/liveness/field.json",
    "packages/spec/liveness/state-counts/field.md (generated)",
    "packages/spec/llms.txt",
    "packages/spec/src/data/field.zod.ts",
    "packages/spec/src/data/field-conditional-formatting.test.ts",
    "packages/spec/src/shared/conditional-formatting.zod.ts",
    "packages/spec/src/shared/evaluated-slot-population.test.ts",
    "packages/spec/src/system/metadata-form-zod-reconciliation.test.ts",
    "packages/spec/src/ui/view.zod.ts",
    "skills/objectstack-ai/references/_index.md (generated)",
    "skills/objectstack-data/references/_index.md (generated)",
    "skills/objectstack-ui/references/_index.md (generated)"
    ]
    }

  3. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #22546 at ffce73a2ac. The contract review at CONTRACT_REVIEW_TIER is owed before it lands

    domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-09T22:11Z · holder of claim on this card (round 2). Report: os-dev-report 6090072652. Thread-read: 6090072652.

    Checked in the diff, not taken from the report (28 files, +699 / −36 = 735 changed lines, under the 3,000-line threshold):

    • The key: FieldSchema.conditionalFormatting is optional, z.array(ConditionalFormattingRuleSchema), ordered, with the card's value / record scope and "display only" in its describe.
    • One element: shared/conditional-formatting.zod.ts declares the { condition, style } rule once, and ListViewSchema.conditionalFormatting mounts it too. The list view's accept set is unchanged. Its unknown-key refusal text now holds for both members.
    • The authoring gate: the field walk in validate-expressions.ts judges each condition through the record-scoped check. value reaches only this call as ExprSchemaHint.roots, and SCOPE_ROOTS is not widened. fieldFormattingRootIssue refuses every other judged root (previous, parent, the user roots).
    • The readers beside it:
      • field-level security drops a whole rule whose condition names a denied field (metadata-core);
      • the SQL driver classes the key as presentation (driver-sql);
      • the liveness row is planned, with no reader in either repo (measured on objectui main and the pin), and it names its flip;
      • no ADR-0087 entry is owed: the key was refused by the strict FieldSchema before, and check:adr-0087-registration exits 0.
    • The changeset: @objectstack/spec minor, @objectstack/lint minor, @objectstack/metadata-core patch, @objectstack/driver-sql patch, Clause-②: yes (widening). The PR's first line is Fixes #22228.
    • Out of the card's sequencing, as ruled: objectui's cell renderers, its own card after the spec publishes.

    Evidence, as reported, with its shape checked:

    • the full spec, lint, metadata-core, metadata-protocol, metadata and driver-sql suites;
    • the dogfood expression-conformance file, the three examples, and typecheck across six packages;
    • four ablation legs, each red in the predicted direction, with the restores blob-equal;
    • --ran 116 derived, 115 run, and check:dual-build-cjs-loads NOT MEASURED as dispatched;
    • the roster block, and the three PR-context guards wired to this PR.

    Declared deviations, accepted, and named for the contract review to judge:

    • the files beyond the claim's surface, each demanded by a gate or by the shared element (listed in the PR body);
    • the two extra changeset entries (metadata-core, driver-sql), each for shipped code;
    • the list view's refusal text.

    Main drift: main moved three commits after the branch's merge 5b81e00352, and none touches a path of this PR. Six os-regen artifacts moved on main; none is one this PR regenerates. No base sync is owed for the generated files. The pnpm check:platform-checklist red is identical on pristine main and is the watchdog's.

    Nit, for the next push that changes this PR's files (none is ordered for it): the changeset's example is one inline code span holding backticks (P\value < 0``). A backslash does not escape inside a code span, so the published CHANGELOG renders it broken. A double-backtick span fixes it.

    Next: the PR stays a draft, marked needs:contract-review. The review runs once the head's check-runs settle. Then it lands through the queue.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22546 → 3d886eec0b (Fixes #22228). The card is closed as completed

    domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-09T23:14Z.

    • Landed: through the merge queue at 2026-10-09T23:12Z as 3d886eec0b. Its first parent is 0e00d3611d. The queue did not eject it.
    • Content check against the reviewed head ffce73a2ac:
      • All 28 PR paths on 3d886eec0b are blob-equal to that head.
      • main moved none of them between the branch's merge base 40a6ee50ac and the queue base, so the equality hides no dropped change from main.
    • The review chain: ACCEPT 6090113020; contract review PASS 6090502579 at ffce73a2ac; the seat's pre-queue record 6090533824. That record names the governed-surface lift of the three generated skills/*/references/_index.md paths by the queue guard.
    • What now holds:
      • A field may declare conditionalFormatting: [{ condition, style }], the list view's own rule element, which now lives in shared/conditional-formatting.zod.ts and is mounted by both members.
      • Each condition is judged at os build, os validate and the object save door: it must parse, read record.FIELD and name declared fields, and it may read only value and record.
      • Field-level security drops a whole rule that names a denied field.
      • The SQL driver keeps the key out of column DDL.
    • Planned, as the card sequences it: no renderer reads the key yet; the liveness row is planned and names its flip. The contract review escalated, non-blocking, that this flip has no carrier card yet. Carrier: this seat, at the publish of this spec version. It files objectui's cell-renderer card then and names it in the liveness row.
    • Carried nit: the changeset example's inline code span holds backslash-escaped backticks, which CommonMark renders broken in the published CHANGELOG. No push carried it.

    This act removes pm:dispatched; the domain, type, priority and area labels stay.


    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

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specenhancementNew feature or requestpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions