Skip to content

styling rules: carve out data-driven author-declared colour, so the written rule matches the practised one #5185

Description

@os-steve

Filed and routed by the repo:objectui execution seat (session session_01AMHbqfPiETJHA95r6WzZWS) under the maintainer's direct-dispatch channel, on an explicit ruling given in-session on 2026-08-18.

Authorising instruction (quoted verbatim, not paraphrased)

Asked how to resolve the conflict between the flat styling ban and the repo's practice, the maintainer chose option A:

「A:修订文档,明写数据驱动豁免(推荐)」 — 在 styling 文档里明确切出「作者声明的数据驱动颜色」一块:仅允许用 inline style 发布 CSS 自定义属性、由静态 utilities 消费;组件自写的静态颜色禁令照旧。

Why

AGENTS.md §2 and skills/objectui/rules/styling.md state a flat rule: no inline styles, no hard-coded colour values. The repo does not actually work that way, and the gap is not marginal — measured on origin/main during objectui#5141's implementation:

  • the rule has no mechanical enforcement (no ESLint rule backs it);
  • there are 254 style={{ occurrences under packages/*/src;
  • three established data-driven author-colour paths already publish an author's value through inline style: plugin-gantt bars, conditionalFormatting, and Studio's own option swatch — the last of which paints the same options[].color metadata key straight from the raw hex;
  • objectui#5141 (PR fix(fields): render the author's declared option hex instead of quantizing it to nine palette families #5184) adds a fourth, and was reviewed and sanctioned on exactly this ruling.

The cost of leaving it flat is not theoretical. It is what made #5141's triage stop-clause ambiguous: the implementing agent had to decide for itself whether a written, unenforced rule constrained how or whether, and correctly escalated rather than guessing. The next agent on any data-driven-colour card hits the same fork.

What to change

Both files, minimally:

  • AGENTS.md §2
  • skills/objectui/rules/styling.md

State the carve-out precisely enough that it decides the next case without a human:

  • Permitted: publishing author-declared, data-driven colour via inline style only as CSS custom properties that static Tailwind utilities consume — so dark: variants stay real variants and the design system keeps theme control. The worked reference is getBadgeHexAppearance / getDotHexAppearance in packages/fields/src/index.tsx (PR fix(fields): render the author's declared option hex instead of quantizing it to nine palette families #5184).
  • Still banned: component-authored static colour, hard-coded colour literals in class strings, and any inline style that hard-codes a colour such that dark mode renders identically. That last clause is the actual hazard the original rule exists to prevent, and it should be named as such.
  • Say why, since the rationale is what makes the boundary decidable: semantic tokens exist so theming and dark mode work automatically. A pattern that preserves that property satisfies the rule; one that defeats it does not — regardless of whether the word style appears.

Do not silently widen this into "inline styles are fine". The ban survives for everything except author-declared data-driven colour routed through custom properties.

Explicitly out of scope

Governance

AGENTS.md and skills/** are governed surfaces. The PR for this card gets drafted, pushed, and reviewed by the seat, then left for the maintainer to merge by hand. No AI seat may enqueue it, flip it ready for auto-merge, or merge it.

Activity

  1. self-assigned this
    on Aug 18, 2026
  2. os-steve commented on Aug 18, 2026

    @os-steve
    CollaboratorAuthor

    Claim: PM loop round 1 (maintainer direct-dispatch channel — ruling given in-session 2026-08-18, quoted verbatim in the card body)
    Session: session_01AMHbqfPiETJHA95r6WzZWS
    Branch: claude/issue-5185-styling-data-driven-carveout
    Worktree: objectui-issue-5185
    Domain: repo:objectui (execution seat)
    File surface: AGENTS.md (§2) and skills/objectui/rules/styling.md — nothing else (stop on breach; explain in the report)
    Container & model: S–M, mode:subagent, model: opus — dispatch-gates.mjs --tier gives no path-derived mandate for these paths, and the ⛔ mandatory fable clause ① binds only .claude/skills/pm-dispatch/**, which this is not. My judgment: the ruling is already fully specified, so what remains is precise rule-boundary drafting, not design — the default judgment tier. I considered raising it, since this text becomes the rule future agents apply, and settled on opus because the PR gets a seat review and a human merge before it can bind anyone.
    Clause-②: no — prose only; no code, no contract, no accept/reject behaviour, no public surface.
    Serial constraints cleared: no open PR and no in-flight claim touches AGENTS.md or skills/objectui/** in this repo. Adjacent but disjoint: PR5124, PR5080 and PR4930 all sit in the governed queue awaiting a human merge and all touch skills/** — none touches skills/objectui/rules/styling.md or AGENTS.md. Merge-order risk is therefore nil, but if any of them lands first, merge main before opening this PR.

    ⛔ Governed surface — landing rule, stated up front

    AGENTS.md and skills/** are governed. This PR gets drafted, pushed, and reviewed by me, then left visibly waiting for the maintainer to merge by hand. I will not flip it ready for auto-merge, will not enqueue it, and will not merge it. "Reviewed + green" is not an exception. Open it as a draft and leave it there.

    The ruling to implement (settled; do not re-litigate)

    Option A of three, chosen by the maintainer: amend the docs to carve out data-driven author-declared colour, keeping the ban on component-authored static colour. Options B (case-by-case exceptions) and C (ESLint-enforced flat ban) were explicitly not chosen — in particular, do not add or propose a lint rule, and do not audit the 254 existing style={{ sites.

    What makes this card easy to get wrong

    The failure mode is writing a carve-out so loose it reads as "inline styles are fine now". The boundary has to be sharp enough to decide the next case without a human:

    • the permitted shape is author-declared, data-driven colour published as CSS custom properties consumed by static utilities — the worked reference is getBadgeHexAppearance / getDotHexAppearance in packages/fields/src/index.tsx (PR fix(fields): render the author's declared option hex instead of quantizing it to nine palette families #5184, landing now);
    • the hazard the original rule exists to prevent should be named: an inline style that hard-codes a colour so dark mode renders identically. Write that down, because it is the property that actually distinguishes the two cases — not the presence of the word style;
    • carry the rationale, not just the rule: semantic tokens exist so theming and dark mode work automatically. A pattern preserving that satisfies the rule; one defeating it does not.

    Verify before you write

    The evidence in the card body was measured by me on origin/main and is a clue, not a specification — re-derive it. In particular re-check the 254 count and the three existing author-colour paths (plugin-gantt bars, conditionalFormatting, Studio's option swatch) before citing any number in the docs. Prefer not to cite a raw count in the doc text at all — a number in prose rots the moment someone adds a call site, and this repo already has cards open about exactly that failure mode. Describe the shape, not the census.

    Read the current text of both files first: if §2 or styling.md already carries a partial exception, amend it in place rather than appending a second, competing paragraph.

    Gates

    Derive from your actual diff. A docs-only change here implicates at least Internal Docs Link Check, Skill Guide Path Check, Control Byte Scan, and the changeset gates. That list is a clue, not a spec. Note Skill Guide Path Check specifically — you are editing under skills/, so it is live for this diff.

    Changeset: docs-only. This repo has no skip-changeset label and one must never be minted; if no released package changes, an empty-frontmatter changeset is the sanctioned declaration.

    Premise-first

    If the current text already says something equivalent, or the ruling turns out not to fit the file structure, report premise_still_valid: false with evidence rather than forcing an edit.

    Deliver

    Draft PR against main, first line Fixes #5185, and leave it draft. Push the branch early.

    Return the structured JSON report AND post it as an issue comment whose first line is <!-- os-dev-report -->.


    Generated by Claude Code

  3. os-steve commented on Aug 18, 2026

    @os-steve
    CollaboratorAuthor
    {
      "issue": 5185,
      "status": "done",
      "branch": "claude/issue-5185-styling-data-driven-carveout",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5188",
      "premise_still_valid": true,
      "summary": "Implemented ruling option A: amended AGENTS.md section 2 and skills/objectui/rules/styling.md in place to carve out author-declared, data-driven colour from the flat inline-style ban. Permitted shape is narrow - the declared colour published as CSS custom properties consumed by STATIC Tailwind utilities, so dark: stays a real variant and the design system keeps theme control; a new 'Rule: Author-Declared, Data-Driven Colour' section in styling.md carries the correct/forbidden pair, the still-banned list, the rationale, and an explicit colour-only scope note (runtime geometry is named as a separate question this does not answer). The hazard clause - any inline style that hard-codes a colour such that dark mode renders identically - is named explicitly in BOTH files as the property that decides a case. Three sections that would otherwise have disagreed now point at the owning section (top summary blockquote, 'Use Tailwind Utility Classes Only', 'Dark Mode with Semantic Tokens' - the last being the direct tension, since its 'no manual dark: variants' line is exactly backwards for the carrier pattern), and 'Use Semantic Color Tokens' was sharpened to ban the colour LITERAL rather than arbitrary-value syntax, because the sanctioned carrier bg-[color:var(--os-badge-bg)] is itself an arbitrary value. No lint rule added or proposed; no existing style={{ site audited or changed; packages/fields untouched. Draft PR left draft per the governed-surface rule.",
      "premise_evidence": "Neither file carried any partial exception before this change - both stated the flat ban, so there was nothing to amend in place and no competing paragraph risk from a pre-existing carve-out. Card evidence re-derived on origin/main@6ca910bdf: the style={{ census is 260 under packages/*/src, NOT the 254 the card measured the same day (which is why no raw count appears in the doc text - the shape is described instead). All three author-colour paths confirmed by location: packages/plugin-gantt/src/GanttView.tsx:3637, ruleToStyle in packages/core/src/evaluator/listConditional.ts:381, packages/app-shell/src/views/metadata-admin/SchemaForm.tsx:1701. Material finding for review: all three use the DIRECT form the new rule does not permit, so the doc cites them as the practice that made the flat rule untrue rather than as conforming examples.",
      "files_changed": [
        "AGENTS.md",
        "skills/objectui/rules/styling.md",
        ".changeset/styling-data-driven-colour-carveout-5185.md"
      ],
      "tests": "Docs-only diff; no build or test suite is implicated, so no heavy-verify lock was taken. Gate union re-derived from the actual diff and re-run at the final commit 3c146dd0b with a clean working tree (git status --porcelain = 0 lines): check-control-bytes exit=0 'OK (scanned 4595 tracked text file(s); skipped 85 binary)'; check-skills-paths exit=0 'OK (94/95 stated path(s) resolve across 18 guide file(s); 1 baselined)' - live for this diff since it edits under skills/, and its checked-assertion count rises with the change while staying green; check-doc-links exit=0 'Links are valid across 13 scan roots'; check-changeset-presence exit=0 'No source of a released package changed in this range, so no changeset is owed'; check-changeset-fixed exit=0; check-changeset-no-major exit=0. Beyond the gate, the three changed files were scanned directly for control bytes (grep -naP over the C0 range plus DEL): no hits. check-links.yml was inspected and does NOT run on pull_request - it is schedule + workflow_dispatch only, so external-link flake cannot gate this PR. No ablation applies to a prose change.",
      "gates_run": [
        "check-control-bytes: pass (Control Byte Scan)",
        "check-skills-paths: pass (Skill Guide Path Check)",
        "check-doc-links: pass (Internal Docs Link Check)",
        "check-changeset-presence: pass",
        "check-changeset-fixed: pass",
        "check-changeset-no-major: pass"
      ],
      "governed_surface": "PR #5188 is draft and left draft. Auto-merge not enabled, not enqueued, not merged. Awaiting the maintainer's hand merge.",
      "deviations": [
        "Amended FOUR sections of styling.md plus the top blockquote, not only the two files' primary ban lines. Reason: the dispatch forbids two rules in one document that disagree, and 'Rule: Dark Mode with Semantic Tokens' ('DO NOT manually add dark: variants') plus 'Rule: Use Semantic Color Tokens' ('bg-[#3b82f6] // Arbitrary value') both contradict the sanctioned carrier pattern on its own worked example. Each amendment is one to four lines and points at the single owning section; total diff is +102/-2 across three files.",
        "Did NOT cite the 254 (or the re-derived 260) count in the doc text, per the dispatch's strong preference - the same-day drift from 254 to 260 is itself the evidence, and it is recorded in the PR body and the commit message instead of the doc.",
        "The worked reference getBadgeHexAppearance / getDotHexAppearance is NOT on main - PR #5184 is still open (draft). The symbols were read from that PR's head 26e78af0f rather than assumed. The doc attributes them to PR #5184 and states the pattern in full in a fenced example, so the rule stands alone if that reference moves. The cited path packages/fields/src/index.tsx does exist on main, which is what Skill Guide Path Check asserts (fenced blocks are excluded from that gate; only inline prose spans are checked)."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  4. os-steve commented on Aug 18, 2026

    @os-steve
    CollaboratorAuthor

    ACCEPT — and held for a human merge, permanently. PR #5188 (repo:objectui seat PM, session session_01AMHbqfPiETJHA95r6WzZWS, reviewer of record).

    Landing path: governed surface, terminal three-piece. AGENTS.md and skills/** are agent-instruction files that only the maintainer may merge. PR #5188 stays draft; I have not flipped it ready, not enabled auto-merge, not enqueued it, and will not. Listed under awaiting a human merge in my round report. Green and reviewed is not an exception — the human merge is the review record for this surface.

    CI: 19 checks, all terminal — 17 success, 2 skipped, zero failures. (19 rather than 21 because a docs-only diff doesn't run Bundle Analysis or Live E2E.) Skill Guide Path Check is green and is genuinely live for this diff, which matters: it's the gate that would catch a broken path reference in a skills/ guide.

    Scope: exactly the declared surface — AGENTS.md, skills/objectui/rules/styling.md, and an empty-frontmatter changeset (the sanctioned docs-only declaration; no skip-changeset label minted, correctly). No lint rule added or proposed, no existing style={{}} site audited, packages/fields untouched. Options B and C stayed shut.

    The deviation is the best thing in this PR. The dispatch named two files; the dev amended four sections plus the top blockquote, and was right to. Two of them were live contradictions that a minimal edit would have left in place:

    • "Rule: Dark Mode with Semantic Tokens" says "DO NOT manually add dark: variants" — which is exactly backwards for the sanctioned carrier, where an explicit dark: variant is what keeps dark mode real. Left alone, the guide would have banned the pattern it had just permitted, two screens apart.
    • "Rule: Use Semantic Color Tokens" banned bg-[#3b82f6] as "Arbitrary value" — but the sanctioned carrier bg-[color:var(--os-badge-bg)] is an arbitrary value. Sharpening the ban to the colour literal rather than the syntax is the correct fix and is the difference between a rule that decides cases and one that contradicts itself.

    That is precisely the "two rules in one document that disagree is worse than the flat rule you are fixing" hazard, found and closed without being told which sections.

    The text is decidable, which was the whole point. It names the hazard in both files — an inline style that hard-codes a colour such that dark mode renders identically — and says outright that this, not the presence of the word style, is the property that decides a case. It carries the rationale as a test ("Read that rationale as the test. It decides cases this page does not list"), and it scopes hard: colour only, author-declared only, with runtime geometry explicitly named as a separate question this carve-out does not answer. That last sentence is what stops the next reader widening this into "inline styles are fine now".

    Two measurement notes I'm recording because they correct me:

    1. My census was already stale when I wrote it. I measured 254 style={{ under packages/*/src; the dev re-derived 260 the same day. Neither number is in the doc — which is exactly why I asked for the shape rather than a count, and the same-day drift is the argument, not a hypothetical.
    2. The carve-out does not retroactively bless the three paths I cited to the maintainer. Re-measured, all three — plugin-gantt/src/GanttView.tsx:3637, ruleToStyle in core/src/evaluator/listConditional.ts:381, app-shell/.../SchemaForm.tsx:1701 — use the direct form (style={{ backgroundColor: … }}), which the new rule forbids. The doc correctly cites them as the practice that made the flat rule untrue, not as conforming examples. So after this lands they are non-conforming-but-unenforced, exactly as they were before. Auditing them was explicitly out of scope here; whether they should be migrated to the carrier pattern is a real follow-up question and I am flagging it rather than filing unilaterally, since it is capability-shaped work across three packages.

    Worked reference now resolves. It was drafted while PR #5184 was still open, so the dev read the symbols from that PR's head and stated the pattern in full inline so the rule stands alone. #5184 has since merged (a09bc33ff), and I confirmed on main that getBadgeHexAppearance (:1409), getDotHexAppearance (:1427) and the exact quoted carrier string (:1393) are all live.


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions