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
Activity
- addedenhancementNew feature or requestNew feature or requestarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchand removed
on Oct 8, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 · 2026-10-09T19:43Z
Session:session_01KNKBCRDJCu5tGy3TEbvtrF
Account:zhuangjianguo(the seat's linked user asGET /useranswers 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 (atorigin/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.- The authoring gate: the field-rule CEL judge in
packages/lint/src/validate-expressions.tsjudges the condition, the way it judgesvisibleWhenand, since215e66204a,settledWhen, with the scope namesvalueandrecord. - The rest: the generated spec artifacts; the liveness ledger row; the docs page that teaches
visibleWhen;.changeset/22228-*.md(@objectstack/specminor, and@objectstack/lintif its gate moves). - An ADR-0087 conversion entry only if the measurement shows a new optional key owes one.
- ⛔ Not objectui's renderer: that is its own card after the spec publishes.
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: - Open PRs: the 7 open PRs' file lists, read at 2026-10-09T19:43Z. None touches
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.- The hot-file queue: spec(data): a date or datetime field declares its deadline semantic —
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 commented
on Oct 9, 2026 ContributorAuthorMore actionsos-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 commented
on Oct 9, 2026 ContributorAuthorMore actions✅ 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):
- The key:
FieldSchema.conditionalFormattingis optional,z.array(ConditionalFormattingRuleSchema), ordered, with the card'svalue/recordscope and "display only" in its describe. - One element:
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. - The authoring gate: the field walk in
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). - 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 objectuimainand the pin), and it names its flip; - no ADR-0087 entry is owed: the key was refused by the strict
FieldSchemabefore, andcheck:adr-0087-registrationexits 0.
- field-level security drops a whole rule whose condition names a denied field (
- The changeset:
@objectstack/specminor,@objectstack/lintminor,@objectstack/metadata-corepatch,@objectstack/driver-sqlpatch,Clause-②: yes (widening). The PR's first line isFixes #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;
--ran116 derived, 115 run, andcheck:dual-build-cjs-loadsNOT 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:
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
- The key:
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsLanded: PR #22546 →
3d886eec0b(Fixes #22228). The card is closed as completeddomain:specseat 3 (#18883) ·zhuangjianguo· sessionsession_01KNKBCRDJCu5tGy3TEbvtrF· 2026-10-09T23:14Z.- Landed: through the merge queue at 2026-10-09T23:12Z as
3d886eec0b. Its first parent is0e00d3611d. The queue did not eject it. - Content check against the reviewed head
ffce73a2ac:- All 28 PR paths on
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.
- All 28 PR paths on
- The review chain: ACCEPT
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. - What now holds:
- A field may declare
conditionalFormatting: [{ condition, style }], the list view's own rule element, which now lives inshared/conditional-formatting.zod.tsand is mounted by both members. - Each condition is judged at
os build,os validateand the object save door: it must parse, readrecord.FIELDand name declared fields, and it may read onlyvalueandrecord. - Field-level security drops a whole rule that names a denied field.
- The SQL driver keeps the key out of column DDL.
- A field may declare
- Planned, as the card sequences it: no renderer reads the key yet; the liveness row is
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. - 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
- Landed: through the merge queue at 2026-10-09T23:12Z as
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 withconditionalFormattingdirectly 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:specseat.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
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.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).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.stylemap is the row block's.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:
dueLikewithsettledWhen), 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.@objectstack/specminor.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 (
dueLikeis 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 · objectuipackages/fields/src/index.tsx.Generated by Claude Code