Repository navigation
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
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-gridandobject-kanbanpage blocks declareconditionalFormatting: [{ condition, style }]: "the first rule whose CELconditionholds applies its CSSstylemap to the row" (packages/spec/src/ui/component.zod.ts, the describe on the grid's and the kanban card's member;ListViewSchema.shape.conditionalFormattingis the single definition).examples/app-showcase/src/ui/views/field-zoo.view.tsauthors 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 CELconditionholds applies its CSSstylemap 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) andrecord(the row), the same record scope the row rules andvisibleWhenalready 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
stylemap 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/specschema + describe, the ADR-0087 conversion table entry, the generated baselines and forms, docs (content/docs/...wherevisibleWhenis taught), a pin test for the authoring gate and for the CEL scope names.- Changeset:
@objectstack/specminor. - 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
objectstack-fleet commentedon Oct 9, 2026
Claim: PM loop round 2 · 2026-10-09T19:43Z
Session:
session_01KNKBCRDJCu5tGy3TEbvtrFAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-22228-field-conditional-formattingWorktree:
objectstack-issue-22228Domain:
domain:specSeat:
domain:spec#3(seat post #18883)File surface (at
origin/mainee8751d41e; stop on breach and explain in the report):packages/spec/src/data/field.zod.tsand its tests: one optional keyconditionalFormatting: [{ condition, style }]in the row block's own grammar, reusing the list view's rule schema rather than a second copy.packages/lint/src/validate-expressions.tsjudges the condition, the way it judgesvisibleWhenand, since215e66204a,settledWhen, with the scope namesvalueandrecord.visibleWhen;.changeset/22228-*.md(@objectstack/specminor, and@objectstack/lintif its gate moves).Container & model:
M,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate; clause-② suspect surfacepackages/spec/src/**). A widening: built at the default tier, and the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.Clause-②: yes (widening)
Responsibility: n/a — not a defect card
Thread-read: none
Serial constraints cleared:
field.zod.ts.validate-expressions.ts: PR fix(lint): a declared read attachment resolves only where record is a served row, so os validate refuses a flow condition that reads one #22509 (lint: a declaredattachedOnReadblock is accepted in a flow condition on its object, where the bound row is the stored row and never carries it (os validatepasses, the flow fails at run time) #22481, this seat, in the merge queue) edits this file'scheckclosure, and makes a new call site fields-only by construction. Ordinary concurrency: this PR mergesmainafter it lands.dueLikewithsettledWhen(per-record CEL), so overdue wording and colour derive from one declaration and the name-pattern guess retires (objectui#11815 ruled D) #22227 (215e66204a), the blocker named on this file in the seat post, has landed.objectstack-fleet commentedon Oct 9, 2026
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)"
]
}
objectstack-fleet commentedon Oct 9, 2026
✅ ACCEPT: PR #22546 at
ffce73a2ac. The contract review atCONTRACT_REVIEW_TIERis owed before it landsdomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-09T22:11Z · holder of claim on this card (round 2). Report:os-dev-report6090072652. 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):
FieldSchema.conditionalFormattingis optional,z.array(ConditionalFormattingRuleSchema), ordered, with the card'svalue/recordscope and "display only" in its describe.shared/conditional-formatting.zod.tsdeclares the{ condition, style }rule once, andListViewSchema.conditionalFormattingmounts it too. The list view's accept set is unchanged. Its unknown-key refusal text now holds for both members.validate-expressions.tsjudges each condition through therecord-scopedcheck.valuereaches only this call asExprSchemaHint.roots, andSCOPE_ROOTSis not widened.fieldFormattingRootIssuerefuses every other judged root (previous,parent, the user roots).metadata-core);driver-sql);planned, with no reader in either repo (measured on objectuimainand the pin), and it names its flip;FieldSchemabefore, andcheck:adr-0087-registrationexits 0.@objectstack/specminor,@objectstack/lintminor,@objectstack/metadata-corepatch,@objectstack/driver-sqlpatch,Clause-②: yes (widening). The PR's first line isFixes #22228.Evidence, as reported, with its shape checked:
--ran116 derived, 115 run, andcheck:dual-build-cjs-loadsNOT MEASURED as dispatched;Declared deviations, accepted, and named for the contract review to judge:
metadata-core,driver-sql), each for shipped code;Main drift:
mainmoved three commits after the branch's merge5b81e00352, and none touches a path of this PR. Sixos-regenartifacts moved onmain; none is one this PR regenerates. No base sync is owed for the generated files. Thepnpm check:platform-checklistred is identical on pristinemainand 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
objectstack-fleet commentedon Oct 9, 2026
Landed: PR #22546 →
3d886eec0b(Fixes #22228). The card is closed as completeddomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-09T23:14Z.3d886eec0b. Its first parent is0e00d3611d. The queue did not eject it.ffce73a2ac:3d886eec0bare blob-equal to that head.mainmoved none of them between the branch's merge base40a6ee50acand the queue base, so the equality hides no dropped change frommain.6090113020; contract review PASS6090502579atffce73a2ac; the seat's pre-queue record6090533824. That record names the governed-surface lift of the three generatedskills/*/references/_index.mdpaths by the queue guard.conditionalFormatting: [{ condition, style }], the list view's own rule element, which now lives inshared/conditional-formatting.zod.tsand is mounted by both members.os build,os validateand the object save door: it must parse, readrecord.FIELDand name declared fields, and it may read onlyvalueandrecord.plannedand 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.This act removes
pm:dispatched; the domain, type, priority and area labels stay.Generated by Claude Code