Skip to content

fix(spec): the rowColor prescription stops handing authors the one spelling the renderer drops - #18849

Merged
os-litant merged 7 commits into
mainfrom
claude/issue-18791-row-color-vocabulary-honesty
Sep 18, 2026
Merged

os-litant merged 7 commits into
mainfrom
claude/issue-18791-row-color-vocabulary-honesty

Conversation

@os-litant

@os-litant os-litant commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #18791

Clause-②: yes

RowColorConfigSchema.colors advertised Map of field value to color (hex/token), and
the view/row-color-without-colors diagnostic checked PRESENCE only. The sole renderer —
objectui plugin-grid's useRowColor — resolves far less than that. So the chain ran:
the gate fires, the gate's own fix string hands the author a hex, the hex parses,
publishes, clears the !config.colors guard, turns the gate GREEN, and colours nothing.
A control whose own prescription switches it off.

What the renderer actually does

Read at the pinned .objectui-sha 53ded82bf7a494f54e344e19099dbf00854b8694, not at
objectui's local HEAD (a different tree this repo does not consume):

  • COLOR_TO_CLASS has 23 entries, every key a bare lower-case word (red, slate,
    grey, …), each mapping to bg-NAME-100.
  • colorToClass returns a bg--prefixed value untouched; otherwise it looks up
    color.toLowerCase().trim() with hasOwnProperty and returns undefined for
    everything else. Tailwind v4 has no runtime, so no class can be fabricated from a hex.

Three landing points

  1. The describe now names the two spellings that reach a class and names a hex only
    as the thing that does not.
  2. The fix string (highest priority — the only half that ACTIVELY pushed authors
    into the trap) now prescribes a resolvable colour name. token went with the hex: it
    named nothing an author could look up and stood beside hex as an equal alternative.
  3. A new author-time warning, view/row-color-unresolvable-value — the half
    presence-only structurally cannot see, because a hex map CLEARS the guard that
    silences the older rule.

The new rule judges the shape a value has and deliberately does not transcribe
objectui's 23-entry map. Two structural facts carry it, and neither depends on what the
map contains: the bg- branch tests the raw value, and every key is a bare lower-case
word matched after toLowerCase() and trim(). That makes it sound — it never
accuses a value the renderer would have resolved, including 'RED' and ' red ' — and
deliberately incomplete: an unknown name such as chartreuse is shaped like a key and
is passed, pinned as a NON-rule. A hand-copy of another repo's vocabulary is a second
opinion that drifts silently in both directions.

Item 3 was gated on blast radius — measured, and it clears

The gate: if the rule would refuse anything currently authored, stop and report.

  • Nothing is refused at all, and nothing fails on a DEFAULT run. The finding is
    warning, and @objectstack/lint's splitBySeverity sorts everything that is not
    error into advisories, so os build / os validate / os lint still exit 0 on
    their default paths. The registration-time twin in @objectstack/objectql calls
    checkFieldCompleteness and never the view predicate, and warns without ever throwing.
    ⚠️ Corrected after the at-tier review (record 5723359135): under
    os lint --strict and os validate --strict a warning IS a failure — lint.ts:922
    computes failing = errors.length + (strict ? warnings.length : 0) and
    validate.ts:715 exits 1 on flags.strict && a non-zero warning count — so a stack
    carrying an unresolvable rowColor.colors value starts failing those strict runs.
    That is what the flag is for (its own docblock: so an app can rely on the
    warning-level rules this registry ships as its gate), which is why the seat ruled
    this additive rather than breaking; the changeset now states the consequence and the
    fix. ⛔ os build is NOT in that pair: build.ts is an alias for Compile, which
    carries --strict-body and no --strict.
  • This repo and the five example apps: the only shipped rowColor.colors map is
    examples/app-showcase's task grid — { low: 'slate', medium: 'blue', high: 'amber', urgent: 'red' } — four colour names, all resolving. Every other rowColor in the tree
    is field-only and belongs to the older rule. Grep controls run both ways: a lit
    control hitting 11 lines under examples/, a fabricated dark control returning exit 1.
  • objectui at the pinned sha does hold three hex colors literals, named here so the
    zero is checkable rather than asserted: all three are objectui's own React test
    fixtures (ObjectView.rowColorRelay-7218.test.tsx, in app-shell and plugin-view).
    They assert a relay by toEqual and never traverse checkViewCompleteness, so this
    rule does not judge them and does not change their verdict.

Verification

Round resumed after a container restart killed the previous session mid-flight; nothing
it implied was taken on trust, and re-measuring found two real gaps, both fixed here.

Run Verdict
pnpm --filter @objectstack/spec build exit 0 — check-dts-emitted: 34/34
pnpm --filter @objectstack/spec test (project local) exit 0 — 487 files / 14051 tests
pnpm --filter @objectstack/spec test:repo (project repo) exit 0 — 31 files / 536 tests
pnpm --filter @objectstack/spec typecheck exit 0
pnpm --filter @objectstack/spec check:generated exit 0 — all 15 artifacts current
check-adr-0087-registration / check-changeset-no-major / check-empty-changeset exit 0
pnpm check:nul-bytes, check-spec-docblock-symbol-anchors exit 0

Counts read against c5e927f9506. Heavy runs went through
scripts/pm/os-verify-lock.sh; every exit code was captured after a redirect, never
through a pipe.

Gap 1 — the changeset named a symbol that does not exist. Its "not breaking"
paragraph rested on partitionFindings; git grep found exactly one occurrence in the
repository, the changeset's own sentence. The mechanism was real, the name was not — the
router is splitBySeverity (packages/lint/src/authoring-rules.ts). Corrected, because
this text ships to consumers as CHANGELOG.md and an unresolvable symbol there is a dead
end for the reader who greps it — the same defect class as the card itself.

Gap 2 — two generated artifacts were stale. VIEW_ROW_COLOR_UNRESOLVABLE_VALUE is a
new public const on ./kernel, so check:api-surface and check:export-origins were
both red. The tree carried no .d.ts at all (a prior OS_SKIP_DTS=1 build), under which
gen:api-surface cannot run — so this was rebuilt for real first, then only the two the
aggregate proved stale were regenerated. The diff is two added lines and nothing else;
check:api-surface reads it as 0 breaking (removed/narrowed), 1 added, which is the
accept-set reading the minor bump and the clause-② widening declaration already claimed.

Test-fixture triage went by the rule's consumer radius, not by the edited package: the
RowColorConfigSchema fixture that pins "a hex does parse" is deliberately KEPT (the
assertion is correct — what was wrong is believing a parse means a colour), while the
corpus fixtures that were merely demonstrating a hex were respelled, because a fixture
is read as an example.

Acceptance notes

Out of scope for this PR, noted rather than fixed:

  • objectui's three hex rowColor.colors test fixtures at the pinned sha model the
    exact trap this card is about, as an example an AI or a human would copy. They are
    correct as relay assertions, so this is a readability trap and not a broken test, and
    objectui is read-only from here. Reported to the seat with dedupe words rather than
    filed by me.
  • content/docs/references/api/protocol.mdx and content/docs/references/data/object.mdx
    render rowColor as an inline type and so never expand the colors describe; only
    view.mdx carries the nested-shape tables that received the new sentence. Generated
    output, correct as generated — noted, not filed.

⛔ Not addressed here and deliberately untouched: the view.exportOptions format enum
region of view.zod.ts, which on-hold card #8346 declares as its trigger region. This
change lives at the RowColorConfigSchema describe and does not enter it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho


Generated by Claude Code


Generated by Claude Code

…elling the renderer drops

`RowColorConfigSchema.colors` advertised "hex/token" and the
`view/row-color-without-colors` diagnostic checks presence only, while the sole
renderer (objectui `plugin-grid`'s `useRowColor`) resolves a `bg-` literal or a
lower-cased colour NAME and returns `undefined` for everything else. The
diagnostic's own `fix` string handed the author `'<hex_or_token>'`: the gate
fired, prescribed a hex, the hex parsed, published, turned the gate green and
coloured nothing.

- the `colors` describe now names the two spellings that reach a class
- the `fix` string prescribes a resolvable colour name, pinned by feeding it
  back through `checkViewCompleteness`
- new `view/row-color-unresolvable-value` warning catches the hex copy the
  presence rule structurally cannot see, judging SHAPE rather than
  transcribing the renderer's vocabulary

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
…ence of a word

A bare "never says hex" pin is passed by deleting the word, which leaves the
reader who came to ask "can I paste the option colours in?" with no answer —
the same silence that let the original sentence be written. Pin instead that
every sentence naming a hex also names the consequence.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
…p, and add the changeset

Three of the four hex `colors` fixtures in `packages/spec` assert only that the
shape parses, which a hex does; but a fixture is read as an example, and this
corpus was demonstrating the one spelling `colorToClass` resolves to
`undefined`. The deliberate "a hex parses" pin is kept once, on
`RowColorConfigSchema`, annotated with where the enforcement actually lives.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
The "not breaking" paragraph rested on `partitionFindings`, a symbol this
repository does not contain — `git grep` found exactly one occurrence, the
changeset's own sentence. The mechanism it described is real; the name was
not. The router is `@objectstack/lint`'s `splitBySeverity`
(`packages/lint/src/authoring-rules.ts`), whose body is literally
`errors: findings.filter((f) => f.severity === 'error')` and
`advisories: findings.filter((f) => f.severity !== 'error')`, and every
`os build` / `os validate` / scaffold path reaches advisories through it.

This text ships to consumers as `CHANGELOG.md`, so an unresolvable symbol in
it is a dead end for the upgrading reader who greps it — the same class of
defect the card itself is about: a prescription naming something that is not
there.

Two other claims in the same paragraph are now stated so they can be checked
rather than taken:

- the objectql twin is field-only because it calls `checkFieldCompleteness`
  and never the view predicate (`registry.ts` imports exactly that one);
- the blast-radius zero names the three hex `colors` literals that DO exist
  at the pinned `.objectui-sha` — all three objectui's own React test
  fixtures in `ObjectView.rowColorRelay-7218.test.tsx`, asserting a relay by
  `toEqual` and never traversing `checkViewCompleteness`. A zero that does
  not name its near misses is not checkable.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
…le id

`VIEW_ROW_COLOR_UNRESOLVABLE_VALUE` is a new public const on the `./kernel`
entry, so the two generated export snapshots were stale the moment it landed
and `check:api-surface` / `check:export-origins` were both red — the sibling
`VIEW_ROW_COLOR_WITHOUT_COLORS` sits in both files, which is what made the
absence readable as a gap rather than as a surface nobody tracks.

Regenerated from a REAL build, not a fast one: the tree carried no `.d.ts` at
all (a prior `OS_SKIP_DTS=1` build), and `gen:api-surface` reads the built
declarations, so under that tree it could not have run. Rebuilt first
(`check-dts-emitted: 34/34 declared declaration file(s) present`), then
regenerated only the two the aggregate proved stale.

The diff is two added lines and nothing else — no phantom removals, which is
the signature a stale `dist` would have produced here:

    +    "VIEW_ROW_COLOR_UNRESOLVABLE_VALUE (const)",
    +    "VIEW_ROW_COLOR_UNRESOLVABLE_VALUE": "src/kernel/...

`check:api-surface` reads it as `0 breaking (removed/narrowed), 1 added`,
which is the accept-set reading the changeset's `minor` and its
`Clause-②: yes` declaration already claimed.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 18, 2026
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/kernel.json, packages/spec/export-origins/kernel.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/kernel.json, packages/spec/export-origins/kernel.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 88aa326deb8c0599803643be885708d391ef356e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ca1b4b2d8ba5e9db833795ad74738225b87629d7 — the merge of head 46f153569f47c97f2986f194510b067d458bed5e into base 88aa326deb8c0599803643be885708d391ef356e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ca1b4b2d8ba5e9db833795ad74738225b87629d7 && git checkout ca1b4b2d8ba5e9db833795ad74738225b87629d7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 88aa326deb8c0599803643be885708d391ef356e 46f153569f47c97f2986f194510b067d458bed5e && git checkout -B drift-repro 88aa326deb8c0599803643be885708d391ef356e && git merge --no-ff 46f153569f47c97f2986f194510b067d458bed5e

node scripts/docs-audit/affected-docs.mjs --json 88aa326deb8c0599803643be885708d391ef356e

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c5e927f95060553836a577ae0bee49ceebf3a320

① Derived judgments

All readings are against head c5e927f9506 (fetched into an owned ref refs/review/pr18849; the --deepen fetch overwrote FETCH_HEAD, so that first merge-base reading was discarded), merge base with origin/main = 9846f2763c2 (main 23 commits ahead), reviewed in a dedicated detached worktree. 8 files, +384/−15, no governed path in the file list, no ADR citation added.

  1. Card premise at the pin — HOLDS. Read the pinned object 53ded82bf7a494f54e344e19099dbf00854b8694:packages/plugin-grid/src/useRowColor.ts (blob 5c0cfcfd98d6), not local objectui HEAD dda8f3815 (a different tree; its copy of this one file happens to be the same blob, which is luck, not a reading). COLOR_TO_CLASS = 23 keys, 23/23 match ^[a-z]+$; colorToClass tests startsWith('bg-') on the RAW value, else toLowerCase().trim() then hasOwnProperty. The stale docblock at pinned lines 13–14 ("a CSS custom property approach is used") is still there — objectui-side, see ③.

  2. Soundness of isUnresolvableRowColor — no false positive found. The rule applies the same toLowerCase().trim() and the same raw-value bg- test the resolver does, then asks ^[a-z]+$; since every pinned key has that shape, "renderer resolves" implies "rule passes" structurally. Measured: the real checkViewCompleteness from the head source, fuzzed against a verbatim transcription of the pinned resolver, over 3848 distinct values (23 keys × 14 case/whitespace/unicode transforms incl. NBSP, BOM, CRLF; 39 hand-picked specials incl. Kelvin sign, dotted capital I, long s, fullwidth letters, constructor/__proto__; random strings): 0 values the renderer resolves and the rule accuses; 1610 resolved-and-passed, 1808 dead-and-accused, 430 dead-but-passed. The seven claimed pins hold: RED, red, bg-emerald-50/50, chartreuse pass; bg-red-100, the empty string, #94A3B8 accused. Incompleteness is pinned as a NON-rule test (chartreuse expects []) and written into the docblock; {} reaches only the presence rule (no double report); non-grid view types never fire.

  3. The prescription the gate hands out is one the renderer honours. Both fix strings now prescribe the literal red in place of the old hex_or_token placeholder; red resolves at the pin to bg-red-100; every colour name the two messages cite (red, blue, slate) resolves at the pin. The trap literal is gone from every prescription: 1 hit at the merge base (the old fix), 2 at head, both historical comments (functional-completeness.ts:576, functional-completeness.test.ts:336). Caveat: the test that pins the prescription feeds it back through this module's own (deliberately incomplete) rule; the cross-repo half — does the pinned renderer accept red — is my transcription reading, not a test, and cannot be one from this repo.

  4. Instrument liveness. Ablation in my scratch worktree (rule forced to false): 4 of 60 tests fail (3 in the new block plus the registry hygiene count); restore is blob-identical to HEAD (5b89b1e1bd6), tree clean. Baseline 60/60. The describe pin in view.test.ts passes 4/4 in default (lazy) mode and under OS_EAGER_SCHEMAS=1 — mode-invariant.

  5. Item-3 gate CLEARS — nothing currently authored is refused. This repo at head: the only authored rowColor.colors map is examples/app-showcase/src/ui/views/task.view.ts:249–256 = slate/blue/amber/red, all pinned keys; every other rowColor is field-only (lint fixtures showcase-shape.fixtures.ts:204, runtime-gate.view-writes.test.ts:169); no hand-written doc or skill carries a rowColor map. Pinned objectui: exactly three hex colors literals, all relay-test fixtures (app-shell ObjectView.rowColorRelay-7218.test.tsx:172,174, plugin-view same-named test :93); objectui imports checkViewCompleteness 0 times (control: 357 files import from @objectstack/spec). Severity is warning; splitBySeverity (packages/lint/src/authoring-rules.ts:1710) routes everything non-error to advisories; the objectql twin (registry.ts:1127) calls checkFieldCompleteness only and warns. Exception the changeset does not state: os lint --strict (lint.ts:910–920) and os validate --strict (validate.ts:643) count warnings as failing — see ③.

  6. Gap (a) fixed. partitionFindings: 0 hits at head (control: splitBySeverity 46 hits); the name entered in commit 6c91d0cb6d5's changeset and left in f9cd4ab722a.

  7. Gap (b) fixed, footprint exact. VIEW_ROW_COLOR_UNRESOLVABLE_VALUE appears once in api-surface/kernel.json and once in export-origins/kernel.json, mirroring the sibling VIEW_ROW_COLOR_WITHOUT_COLORS; neither const appears in api-surface-signatures.json, declaration-map/, json-schema.manifest/ or authorable-surface/, so nothing else is owed. The describe text is embedded only in content/docs/references (3 old occurrences at the merge base → 0 old / 3 new at head). The generator's own "0 breaking, 1 added" line: NOT MEASURED locally (item 10); CI's TypeScript Type Check job, which runs every spec gate sequentially on a fresh checkout, is green at this head.

  8. Routed-path drift: none that matters. Main moved 62 files since the merge base, three of them routed (content/docs/references/api/automation-api.mdx, api-surface/security.json, export-origins/security.json) — disjoint from the PR's routed paths; the full file-list intersection is empty. The driver's silent-drop case cannot arise.

  9. Changeset gates, by script path after git fetch --deepen 300 origin main: check-changeset-no-major exit 0 (level axis NOT APPLICABLE locally — no PR payload; CI Check Changeset carries it, success); check-empty-changeset exit 0; check-adr-0087-registration exit 0. check-clause2-carriers --pair 18849 exit 0: declaration yes readable in the fixed spelling on claim 5721579140 and in the PR body; both carriers agree. check-widening-tells --declaration no names exactly one tell (T3, api-surface/kernel.json:439, a new published export); --declaration yes clears.

  10. Suite per touched package (@objectstack/spec; scripts confirmed present: test = --project local, test:repo = --project repo, typecheck separate). Locally through os-verify-lock.sh: build exit 0 (check-dts-emitted 34/34; held 169s after a 363s wait). test, test:repo, typecheck, check:generated: NOT MEASURED — still queued behind two live devs when this record was forced out; my driver was stopped so the box is not left owing lock time. CI readings on a fresh checkout at this head: Test Core 6/6 shards + rollup success, Type Check · workspace success, TypeScript Type Check (all spec gates) success, Lint & Repo Gates success (concluded 00:41:40Z). What I did run: the two touched test files (60/60 and 4/4, both schema modes).

  11. The dev's NOT MEASURED declaration on packages/lint — the narrowing is legitimate and its reasoning is true where checkable. (i) True: no colors key under any rowColor in packages/lint (0; lit control: 2 field-only fixtures), so the new rule has no input there; lint's consumer types rule: string and no map is keyed on rule ids (0 hits for an index into FUNCTIONAL_COMPLETENESS_RULES), so no type-level break either. (ii) NOT VERIFIED: that @objectstack/formula / @objectstack/sdui-parser dist/ were empty in the dev's tree — that worktree no longer exists after the restart. (iii) My own lint leg: NOT MEASURED (same lock starvation). packages/lint has no test:repo script.

  12. CI re-read, not inherited: 39 check runs — 34 success / 5 skipped / 0 failed / 0 in progress. PR is draft, needs:contract-review on both carriers, Governed Surface Queue Guard success.

② Semver level

  • Accept set: unchanged. RowColorConfigSchema.colors is still z.record(z.string(), z.string()); only .describe() moved. A hex still parses.
  • Published surface: enlarged. New export VIEW_ROW_COLOR_UNRESOLVABLE_VALUE on ./kernel; one new member in the closed FUNCTIONAL_COMPLETENESS_RULES set; one new finding shape checkViewCompleteness can emit. That is the T3 tell the widening gate names, so Clause-②: yes is the honest declaration (a bare yes, no arm — legal; the PR body's prose "widening declaration" is not the machine spelling and decides nothing).
  • Level minor on @objectstack/spec: correct and honest. It satisfies the level axis for a yes; no major. No BREAKING banner and no ADR-0087 disposition — consistent with an additive reading and with precedent: layout-without-binding and tree-without-parent-field shipped as Minor in 17.3.0 (row-color-without-colors as Patch), none with a banner.
  • One wording gap in text that ships to CHANGELOG: "os build / os validate / os lint still exit 0" is true on the default path only; under --strict a warning fails the run by that flag's documented contract. Not a level error; a sentence for the seat (③).

③ Boundary flags

  • --strict consumers (seat decides). A third-party stack carrying a hex rowColor.colors map goes red under os lint --strict / os validate --strict once this ships. Repo precedent reads new warning rules as additive. Either keep that reading and have the changeset say "without --strict", or rule it a narrowing — then a BREAKING banner plus an ADR-0087 disposition marker become due (level stays minor in the launch window; check-adr-0087-registration would then read it).
  • ADR-0049 enforce-or-remove / ADR-0078. The change warns; it does not narrow. Whether colors VALUES should be refused at parse (a regex on the value space — breaking, and coupled to a renderer in another repo) is the card's own open question "where is the vocabulary declared so the two sides cannot drift", which triage said needs its own carrier and this PR deliberately does not answer. Nothing in the diff pre-empts that ruling.
  • ADR-0087. Nothing removed, renamed or converted; no ledger entry due; contingent only on the --strict ruling above.
  • ADR-0070: not engaged. ADR-0118 D2/D3: not engaged (no actor or system-context surface). Prime Directive 13: no cloud ADR cited, no anchor touched.
  • objectui residue needing its own carrier (read-only from here; verified at the pin): the false docblock at useRowColor.ts:13–14; the three hex relay fixtures; and the bg- passthrough being neither case- nor whitespace-tolerant ( bg-red-100 is dropped), which the new rule correctly mirrors.
  • On-hold export: PDF / print document generation — re-planned for v18, superseding the #1301 NOT_PLANNED closure #8346 region (view.zod.ts:1239–1264): untouched; the diff enters view.zod.ts at line 1189 only.

NOT MEASURED, plainly: spec test / test:repo / typecheck / check:generated locally (CI-green at this head on a fresh checkout is the reading I have, including the api-surface "0 breaking, 1 added" line); packages/lint test + typecheck locally; the dev's empty-dist observation about its own tree. Everything else above was measured against the head sha and the pinned objectui object.

Implemented-by: claude/issue-18791-row-color-vocabulary-honesty
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

席位处置 —— PASS 收下;--strict 一条本席裁为加法不破坏,但 changeset 那句话欠一个限定词,补丁轮只改它

domain:spec 席位,session_01LvwGppdonww4zGLWZo5rho,2026-09-18T00:56Z。达档复核记录逐字落档于 5723359135,VERDICT: PASS。档位核验:本席自 grep 复核轮转录的 harness 盖戳 message.model,105/105 在档,0 脱档。

⛔ 载体暂不摘 —— 下面的补丁轮会移动 head,摘了要重挂,而「摘掉又重挂」在事件流上与剥标无法区分(本班次已在 #15646 上栽过一次,见 5722016855)。补丁轮落定后一次摘。

⭐ 这一轮最值得记的一条:复核去找了假阳性,而不只是假阴性

本卡的规则声称「判形状不判成员」,因而声称永不冤枉渲染器认得的值。复核没有采信,而是把 head 源码里真实的 checkViewCompleteness 拿去对着钉住 sha 上渲染器的逐字转写跑了 3848 个不同的值(23 个词 × 14 种大小写/空白/unicode 变形,含 NBSP、BOM、CRLF;39 个手挑特例,含开尔文符号、带点大写 I、长 s、全角字母、constructor / __proto__;随机串):

渲染器认得而规则冤枉的 0
认得且放行 1610
认不得且指出 1808
认不得但放行(声明性的不完整) 430

⇒ 假阳性零 —— 而假阳性才是这条规则更坏的失效方向(冤枉一个能用的值,等于把作者从对的路上赶走)。那 430 个「认不得但放行」是故意的,并且以一条 NON-rule 测试钉住(chartreuse 期望 [])。

⭐ 药方本身也被验了,而这正是本卡的主题

本卡的起因是一条门禁开出的药方会把它自己关掉。复核逐条查了新药方:两处 fix 串现在给的是字面量 red,而 red 在钉住的 sha 上解析成 bg-red-100;药方里提到的每个颜色名(red / blue / slate)在钉住的树上都解析得出。陷阱字面量 hex_or_token 在 head 上只剩两处历史注释。

⚠️ 复核自己标了这条读数的边界,本席照录:钉住药方的那条测试是把药方喂回本模块自己那条(故意不完整的)规则;跨仓的另一半——钉住的渲染器是否接受 red——是复核的转写读数,不是测试,而且在本仓内做不成测试。

⚖️ --strict 分级:本席裁 加法,不破坏

复核实测:os lint --strict(lint.ts:910-920)与 os validate --strict(validate.ts:643)把 warning 计入失败。本席现读源码复证:

const failing = errors.length + (strict ? warnings.length : 0);

⇒ 一个带 hex rowColor.colors 的三方 stack,在本 PR 发运后跑 --strict 会红。那是真的。

但本席裁它不是收窄,依据有二:

  1. 接受集没有动 —— RowColorConfigSchema.colors 仍是 z.record(z.string(), z.string()),一个 hex 照样解析通过。按 SKILL.md:476 的判据(放宽接受集或扩大公开面),本 PR 命中的是扩大公开面(一个新导出、一个新规则成员)⇒ Clause-②: yes 正确,而不是 (narrowing)。

  2. ⭐ --strict 这个旗标自己的 docblock 写明了它的用途(逐字):

    --strict (cli: os lint --strict — a flag that makes warning-severity findings fail the run, so an app can rely on the 119 warning-level rules instead of re-implementing them locally (epic hotcrm#1579, step 1) #15935) makes a warning fail it too — so an app can rely on the warning-level rules this registry ships as its gate instead of re-implementing them locally at error level

    ⇒ 「注册表新增一条 warning 级规则会让 --strict 变红」正是这个旗标被设计出来要做的事,不是它被破坏。选择 --strict 的 app 已经选择了「registry 新增的 warning 会成为我的闸门」。

仓内先例同向:layout-without-binding 与 tree-without-parent-field 都以 Minor 发运且无横幅。

⇒ 不挂 BREAKING 横幅、不加 ADR-0087 处置标记;minor 维持。

🔴 但那句话要改 —— 它会变成永久的 CHANGELOG

changeset 现在写「os build / os validate / os lint still exit 0」。默认路径为真,--strict 下为假。 一个在 CI 里跑 os lint --strict 的三方 app 升级后会红,而 CHANGELOG 里没有任何一句提醒它。

⚠️ CHANGELOG 一旦发版就是不可变的历史。本班次刚在 #18704 上为同一类问题跑过一轮(那句是直接的假,这句是不完整),⇒ 同样处理,才叫一致。

⇒ 补丁轮已派发,范围锁死为那一句加一个限定词 + 一句 --strict 说明。⛔ 不改代码、⛔ 不改分级、⛔ 不加横幅。

本席另立卡的两处(⛔ 不在本轮)

  1. objectui 侧残留(读取面只读,派发令禁止编辑):useRowColor.ts:13-14 那段说「用的是 CSS custom property」的 docblock 在钉住的 sha 上仍在且为假;三处 hex relay 夹具;以及 bg- 直通既不宽容大小写也不宽容空白( bg-red-100 会被丢掉)—— 新规则正确地镜像了这个行为。
  2. 词表声明在哪 —— 本卡自己的未决问题:让两侧不能漂移的那个声明点在哪里。分诊说它要自己的载体,本 PR 故意不回答,且复核确认 diff 里没有任何东西预判它。

复核声明的 NOT MEASURED,本席照录不打折

spec 的 test / test:repo / typecheck / check:generated 本地未测 —— 锁被两个在飞 dev 占着,复核把记录逼出来时仍在排队,并且主动停了自己的驱动,不让盒子欠锁时间。本席拿到的读数是 CI 在该 head 上全新 checkout 的绿(Test Core 6/6 分片 + rollup、Type Check · workspace、TypeScript Type Check、Lint & Repo Gates 于 00:41:40Z 结束)。⚠️ 那是 CI 的读数,⛔ 不是本地复证。


Generated by Claude Code

…quence

The exit-0 sentence held only on the default path. `os lint --strict`
(`failing = errors + (strict ? warnings : 0)`) and `os validate --strict`
(`flags.strict && warnings.length > 0 ? 1 : 0`) both promote a warning to a
failure, and the new `view/row-color-unresolvable-value` finding reaches both
through `runAuthoringRules` -> `splitBySeverity` -> advisories. A third-party
stack carrying a hex `rowColor.colors` map and gating CI on either strict run
therefore goes red on upgrade, with no word about it in the CHANGELOG this
changeset compiles into.

Prose only: the accept set, the rule, its severity and the declaration are
unchanged.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

📌 两条记录性更正 —— 复核记录 5723359135 与本 PR 的 head 都已被后续事件超过

domain:spec 席位,session_01LvwGppdonww4zGLWZo5rho,2026-09-18T01:17Z。⛔ 本评论不修改任何复核记录 —— 记录要么逐字采纳、要么整份作废,⛔ 席位不得编辑。下面两条是席位的话,不是记录的话。

① 已落档的复核记录低报了自己的覆盖面(⛔ 不是高报)

5723359135 的 ① 第 10 条写:spec 的 test / test:repo / typecheck / check:generated 四项本地 NOT MEASURED,因为锁被占。

复核轮在本席落档之后自行恢复并补完了一项:pnpm --filter @objectstack/spec test(project local)EXIT=0,持锁 178s。⇒ 现在 NOT MEASURED 的是三项(test:repo / typecheck / check:generated),不是四项。

⚠️ 本席不改那份记录,理由有二:

  1. 记录的纪律是「逐字采纳或整份作废」,⛔ 没有第三条;
  2. ⭐ 这个偏差的方向是低报 —— 记录声称测得比实际少。复核自己的标准是「一份高报覆盖面的记录比 FAIL 更糟」,而低报是安全方向。⇒ 不足以整份作废。

裁决、分级、③ 边界旗逐条未变,VERDICT 仍是 PASS。

② 更要紧的一条:那份记录名的 head 已经不是现在的 head

记录名的是 c5e927f9506。补丁轮(把 changeset 里那句 --strict 不成立的话改掉)把 head 推到了 46f153569f4。

⇒ 按 references/contract-review.md:40-42 的落地前检三条,①「席内条款②复核 PASS 在案」要求记录名的就是要落地的那个 head。现在不是。⛔ 所以本 PR 不能凭 5723359135 入队,无论它是 PASS。

已派新的达档 delta 复核,范围锁死在那一个 prose 文件的增量上。载体 needs:contract-review 继续挂在两侧(卡 #18791 与本 PR)—— ⛔ 不先摘再挂:那会在事件流上留下与剥标无法区分的单次移除,本班次已在 #15646 上栽过一次(见 5722016855)。

顺带记下补丁轮比派发令做得更对的两处

  1. 本席只点名了一句话要加限定词;施工席测出两句 —— 紧接着的「Nothing that builds today starts failing」是同一个只对默认路径成立的假设的复述。⇒ 只改第一句,会在永久的 CHANGELOG 历史里,让一句新写的「--strict 会红」和两行之下一句「什么都不会开始失败」正面打架。两句都改了,第二句作为「围栏内但超出派发令」点名上报,⛔ 不是默默做掉。
  2. ⭐ os build 根本不在那对 strict 命令里,这是本席派发令里的一个错:build.ts 是 Compile 的 7 行别名,Compile 带的是 --strict-body(针对缺 body 的 callable),没有 --strict。⇒ 照本席的话把 os build 和 lint / validate 并列写进去,本身就会是一句新的假话。施工席查了哪些命令真有这个旗标,而不是照抄。

Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 46f153569f47c97f2986f194510b067d458bed5e

① Derived judgments

Narrow delta review extending record 5723359135 (PASS at c5e927f9506). It is exactly as narrow as briefed: git diff --numstat c5e927f9506 46f153569f4 is one file, .changeset/18791-row-color-vocabulary-honesty.md, +9/−4, one commit (docs(changeset): qualify the exit-0 claim and name the --strict consequence); no .ts, .json, .mdx or test file, no rule id, severity, level or declaration change. Every code, rule, level and declaration judgment in 5723359135 is carried forward unchanged; nothing measured here overturns any of them. Merge base with origin/main after git fetch --deepen 300 origin main is still 9846f2763c2 (main moved 88aa326 to d099764 during this review and is now 29 commits ahead; the branch is 7 ahead).

  1. Hunk footprint. The single hunk is @@ -52,10 +52,15 @@; lines 1–51 are byte-identical between the two heads (diff of the two slices exits 0), so frontmatter '@objectstack/spec': minor (line 2) and Clause-②: yes (line 7) are untouched. Mechanical check: zero changed lines match Clause-② or @objectstack/spec'; lit control on the same diff: 2 changed lines match strict. Claim 4 holds.

  2. Exit-0 clause now scoped (claim 1). Lines 55–56 read "still exit 0 on their DEFAULT paths". Lines 59–63 name the strict consequence and the fix (red, or bg-red-200). The fix is one the new rule itself accepts: isUnresolvableRowColor at head passes any bare lower-case word and any raw bg--prefixed value, so both prescriptions silence the finding. That red resolves at the pinned objectui to bg-red-100 is carried forward from the prior record's item 3, not re-read.

  3. Second qualification (claim 2). Line 58, "Nothing that builds today on a default run starts failing", is the very next sentence after the exit-0 clause in the same paragraph, now qualified. Looked for a third: a grep over exit / fail / refuse / break / throw / green / default at head hits lines 18 and 22 (the historical gate-goes-green narrative, not an exit-code claim), 53 (the seat's settled "Not breaking" ruling) and 55–61 (the two qualified sentences plus the new strict sentence). The only remaining candidate is the second half of lines 58–59, "nothing authored today is refused": that is the item-3 gate statement (accept set unchanged; zero authored maps in the measured corpus carry an unresolvable value), scoped by the blast-radius paragraph immediately following, and true on both readings — not a restatement of the default-path assumption. No third unqualified sentence.

  4. os build correctly absent from the strict pair (claim 3). packages/cli/src/commands/build.ts at head is 7 lines: class Build extends Compile, overriding only description, so it inherits Compile's flags. compile.ts defines 'strict-body' (line 72) and no strict flag; zero reads of flags.strict (lit control: 1 read of flags['strict-body']); no static strict override; its two ruleAdvisories.length sites (lines 369, 915) only print, never exit. Across every file under packages/cli/src/commands/, a strict-named flag definition exists only in lint.ts:702, validate.ts:69 and i18n/check.ts:112. plugin/build.ts is os plugin build, a different command. Naming os build beside lint and validate would indeed have been a new false claim; the changeset does not.

  5. The strict path reaches this finding. functional-completeness.ts:593 severity: 'warning' to validate-functional-completeness.ts:73 severity: f.severity, passed through unchanged — and that walker calls checkViewCompleteness on views[].list, views[].listViews.*, objects[].list and objects[].listViews.* (lines 131, 139, 170 and the loop at 175) to runAuthoringRules (authoring-rules.ts:1699; rule-table entry at 508/521) to splitBySeverity (line 1710; everything non-error becomes an advisory). Lint: lint.ts:679 maps only info to suggestion, so a warning stays a warning; :920 filters them; :922 failing = errors.length + (strict ? warnings.length : 0). Validate: runAuthoringRules('validate', …) at :348 to splitBySeverity :356 to ruleAdvisories :357, pushed into warnings at :647–648, JSON exit at :715 (status 1 when flags.strict is set and the warnings list is non-empty) and text exit at :744. The flag's purpose is as the changeset states it: lint.ts:910–917 docblock ("so an app can rely on the warning-level rules this registry ships as its gate"); validate.ts:69 "Treat warnings as errors". Not run: os lint --strict against a hex fixture — a built CLI is not owed for a prose delta; the chain above is a static reading.

  6. Objectql twin sentence ("warns without ever throwing", line 57–58) is a runtime path with no strict flag; carried forward from prior item 5 (registry.ts:1127).

  7. Gates at this head, by script path in a fresh detached worktree at 46f153569f4 (pnpm check:changeset-no-major is not a root script — confirmed against package.json, which carries only check:empty-changeset, check:adr-0087-registration and the combined self-test script): check-changeset-no-major self-test exit 0 (299 assertions), check exit 0 ("This diff introduces no major bump"; level axis NOT APPLICABLE locally — no PR payload; CI Check Changeset success carries it); check-empty-changeset self-test exit 0 (159), check exit 0 (1 declaring changeset added, none from the merge base modified or deleted); check-adr-0087-registration self-test exit 0 (384), check exit 0 ("adds no declared-breaking changeset (1 non-breaking changeset(s) seen)"). check-clause2-carriers --pair 18849 exit 0: pair head-sha 46f153569f4, declaration yes on claim 5721579140 and in the PR body, both carriers agree. Every exit code captured after a redirect, never through a pipe. The worktree was removed afterwards; the primary checkout is untouched.

  8. CI re-read at 46f153569f4, 01:25Z, not inherited: 42 check runs — 38 success / 4 skipped / 0 failed / 0 in progress. At my first read Lint & Repo Gates was still in progress; it concluded success at 01:24:39Z. All seven required contexts are success. Remote branch head unchanged at 46f153569f4 (git ls-remote). PR still draft; needs:contract-review present on the PR.

  9. Coverage correction to the record being extended. 5723359135 ① item 10 lists spec test, test:repo, typecheck and check:generated as NOT MEASURED locally; the reviewing round later completed pnpm --filter @objectstack/spec test (project local) at exit 0, so three remain unmeasured locally (test:repo, typecheck, check:generated). The posted record understates its own coverage and stands verbatim.

② Semver level

Unchanged from 5723359135 and not re-litigated: accept set unchanged (colors is still z.record(z.string(), z.string())), published surface enlarged (the T3 tell), Clause-②: yes and @objectstack/spec: minor byte-untouched by this delta (item 1). The seat's ruling that a new warning-level rule reddening --strict is additive stands; no BREAKING banner and no ADR-0087 disposition are due, and check-adr-0087-registration reads the changeset as non-breaking (item 7). The prior record's one wording gap (② last bullet, ③ first bullet) is closed by this delta: the changeset now states what exits 0 by default, what fails under --strict, and the fix.

③ Boundary flags

  • The --strict flag from the prior record is resolved: prose corrected, ruling settled by the seat, nothing further owed here.
  • A prose delta raises nothing new. The prior record's remaining flags (objectui-side residue at the pin, the cross-repo vocabulary-declaration question, the ADR-0049 / ADR-0078 accept-set question, on-hold export: PDF / print document generation — re-planned for v18, superseding the #1301 NOT_PLANNED closure #8346 region untouched) are unchanged and carried forward.
  • One reading note, not a finding: "nothing authored today is refused" (lines 58–59) is true as scoped by the blast-radius paragraph that follows it; a reader who takes "refused" as "fails a strict run" is answered by the next sentence, so no contradiction lands in CHANGELOG. The seat may leave it.

NOT MEASURED, plainly: no package suite (none is owed for a changeset-only diff; nothing was queued on the verify lock); check-widening-tells not re-run (no code delta — the prior reading is carried forward); the pinned objectui resolver not re-read (prior items 1–3 carried forward); dispatch-gates not run (its STALE TREE derivation would describe this tree's 8-path set, not this 1-path delta); os lint --strict not executed against a fixture (the static chain in item 5 stands in for it).

Implemented-by: claude/issue-18791-row-color-vocabulary-honesty
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

2 participants