Skip to content

feat(types): declare dueLike on the DetailViewField twin (objectui#9738) - #9993

Merged
os-tesla merged 1 commit into
mainfrom
claude/issue-9738-duelike-declared-on-twin
Sep 20, 2026
Merged

os-tesla merged 1 commit into
mainfrom
claude/issue-9738-duelike-declared-on-twin

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes #9738

Clause-②: yes

Maintainer ruling, director seat summon 24, 2026-09-18 — letter A, quoted:

dueLike is declared on the published TypeScript twin, so the validator, the type checker, the renderer and the docs say the same thing; the key's meaning — a date / datetime field rendered with due-date semantics — is written once in the declaration

Base for every reading below: edbcf1e7aa17ea85bcc61eb8c3fa657c0a3bdc59, which was origin/main when this branch was cut. Head: 0457692462.

What landed

  • packages/types/src/views.ts — DetailViewField gains dueLike?: boolean. The docblock states the rendering effect: both halves of the overdue affordance (the Overdue Nd wording inside the relative face, and text-red-600 on the cell's span), their two different day thresholds, the field-NAME fallback, and that writing false does not suppress that fallback. Every sentence is checked against the tree — see the section below.
  • packages/types/src/zod/views.zod.ts — untouched, and that is a verified reading, not an assumption. DetailViewFieldSchema already declares dueLike: z.boolean().optional().describe(...); the whole defect was that the mirror declared it and the twin did not. Nothing in this branch edits that file.
  • packages/types/src/__tests__/mirror-only-published-keys-9729.test.ts — the pin moves with the measurement; it is ⛔ not "a test that was fixed". See below.
  • packages/types/src/__tests__/zod-mirror-parity.test.ts — the MirroredUndeclared row for views.zod.ts#DetailViewFieldSchema is deleted, and the ledger docstring's two figures are corrected to the values its own sizing pin derives.
  • .changeset/duelike-declared-on-detail-view-field-9738.md — @object-ui/types minor.

The widening, measured

1. Member-declaration delta on DetailViewField, comments stripped, base vs head

Instrument: the TypeScript parser, so comments are stripped by construction rather than by a regex — ts.createSourceFile, find the DetailViewField interface declaration, list members.

base edbcf1e7aa  MEMBER_COUNT=13
                 MEMBERS=name,label,type,format,render,value,readonly,visible,span,
                         options,reference_to,reference_field,currency
head 0457692462  MEMBER_COUNT=14
                 MEMBERS=name,label,type,format,render,value,readonly,visible,span,
                         options,reference_to,reference_field,currency,dueLike

Lit control, same instrument, same corpus, both legs: currency is read on base and on head. So the base zero for dueLike is a measured absence, not a dead instrument. Delta: exactly one key.

2. Ablation — a document the twin REFUSED at base and ACCEPTS at head

A throwaway probe file was added to packages/types/src/__tests__/ and the package's own type-check leg run over it. It exists in neither commit; it was deleted before the first git add, and the tree is clean of it.

import type { DetailViewField } from '../views.js';
export const probe: DetailViewField = { name: 'end_date', dueLike: true };
base edbcf1e7aa  tsc -p tsconfig.test.json  -> exit 2
  src/__tests__/zz-ablation-probe-9738.test.ts(3,59): error TS2353: Object literal may
  only specify known properties, and 'dueLike' does not exist in type 'DetailViewField'.

head 0457692462  tsc -p tsconfig.test.json  -> exit 0   (probe removed; the whole leg is green)

Exact code at base: TS2353. That is the same code the card's four-axis analysis named, re-derived here rather than copied.

3. The pin moved WITH the declaration — it was not repaired

mirror-only-published-keys-9729.test.ts carried a @ts-expect-error whose own docblock said it "fails in both directions — if the key stops being refused the directive becomes unused and TypeScript reports it". Declaring the key is exactly that direction, and it fired:

head, before the pin was moved:
  src/__tests__/mirror-only-published-keys-9729.test.ts(190,1): error TS2578: Unused '@ts-expect-error' directive.
  src/__tests__/zod-mirror-parity.test.ts(3756,14): error TS2322: Type '"views.zod.ts#DetailViewFieldSchema"' is not assignable to type 'never'.
  src/__tests__/zod-mirror-parity.test.ts(3777,14): error TS2322: Type '"dueLike"' is not assignable to type 'never'.

Three reds, all of them the instruments reporting the ruling correctly. What replaced the directive keeps the pin discriminating in its new direction:

  • the assignment it guarded now stands on its own, so the twin losing the member again is TS2353 on the same leg;
  • plus an exact Expect< Equal< DetailViewField['dueLike'], boolean OR undefined > > (spelled with a real union in the source; the word is used here because this body eats short angle-bracket-shaped fragments). ⛔ Not weakened to extends: the bare assignment alone would still compile if someone gave DetailViewField an index signature while removing the member — which is precisely the shape ObjectGridSchema has one section up in the same file, where an authored key is absorbed as any. The exact equality separates DECLARED from ABSORBED, and also refuses a widening to any.

⛔ No pin was deleted to get green, and no exact assertion was relaxed.

4. The ledger row, and why the neighbouring row stayed

MirroredUndeclared measures "the zod mirror STATES a key the TypeScript twin does not". Once the twin states it, that pair measures never, so the row is deleted — the ledger's own docblock calls that "the ratchet doing its job, and the only direction this seed may move in". Its sizing pin then reported the docstring figures, derived at test time:

expected { entries: 8, keys: 102 } to deeply equal { entries: 7, keys: 101 }

Corrected to the derived values, per that assertion's own "WHICH SIDE TO CHANGE" instruction (an entry removed ⇒ correct the docstring). ⛔ No key set was edited to satisfy a sentence.

Note the asymmetry with the row directly above it: objectql.zod.ts#ObjectGridSchema stayed after objectui#9739's letter C, because a retirement tombstone is still a stated member. Two rulings, the same day, moving this ledger in opposite directions. A comment left where the deleted row stood says so.

The docblock's factual claims, checked against the tree

PR objectui#9360 was failed for a docblock asserting what the code does not do. Each claim here was verified:

claim where the tree says it
the authored key on a detail-view field reaches the cell DetailSection calls enrichDetailField(field, objectDefField), whose body opens const enriched = { ...viewField };, then renders CellRenderer value=… field=enrichedField
the cell renderer reads the key first, name convention second resolveDueLike in packages/fields/src/index.tsx: const declared = (field as DateFieldMetadata OR DateTimeFieldMetadata OR undefined)?.dueLike; if (declared === true) return true; then the DUE_LIKE_FIELD_NAME regex
false does not suppress the fallback same function — only === true short-circuits; any other value falls through to the name test. content/docs/fields/datetime.mdx says the same ("authoring a neutral name" is the opt-out)
the wording half formatRelativeDate in @object-ui/core: if (diffDays < -1 && options?.dueLike) returns the Overdue Nd phrase
the styling half, on both cells, whatever face isOverdueInstant(date, dueLike) gates text-red-600 in DateCellRenderer's span and in DateTimeCellRenderer's cellClass, outside the per-face branches
the two thresholds differ by a day isOverdueInstant is date < startOfToday; formatRelativeDate gates on diffDays < -1 — so the red starts a day before the wording does
beyond a week the wording stops, the styling does not formatRelativeDate returns the absolute face for diffDays < -7, before the overdue branch; cellClass is computed before any branch

Behaviour is not merely read, it is run: packages/plugin-detail/src/__tests__/DetailSection.dueLikeReachesTheCell-9729.test.tsx draws end_date (a name that deliberately does not match the convention) with and without the key and asserts the drawn wording changes, with a by-value control leg.

What was run

All at head 0457692462, in a dedicated worktree, heavy runs serialised through the shared verify lock.

command result
pnpm --filter @object-ui/types type-check exit 0 — all three legs echoed: tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json. The third is the leg that reads @ts-expect-error.
pnpm exec vitest run packages/types/ (repo root) exit 0 — Test Files 209 passed (209), Tests 4834 passed (4834)
pnpm exec vitest run over the three renderer suites that read the key exit 0 — Test Files 3 passed (3), Tests 21 passed (21)
pnpm --filter @object-ui/types lint exit 0 — 298 problems (0 errors, 298 warnings), every warning a pre-existing no-explicit-any; none falls inside the added range
node scripts/check-changeset-presence.mjs exit 0 — "3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
node scripts/check-changeset-no-major.mjs exit 0 — "No changeset declares a major bump"
pnpm check:control-bytes exit 0 — 8061 tracked text files scanned
pnpm check:new-line-citations exit 0 — "0 new citation(s)"
node scripts/check-governed-queue-guard.mjs --test (the four changed paths) NOT GOVERNED — 4 paths checked against 5 governed surfaces, none matched

Lint narrowing, declared. The repo's lint is turbo run lint, i.e. each package's own eslint .; the narrowing run here is that same script for the one package this diff touches. (1) The population is read from eslint's own config resolution, not guessed. (2) --format json over that leg counts 280 files, 0 errors, 298 warnings. (3) Invariance for untouched files: eslint.config.js configures no project / projectService, so type-aware linting is not enabled and this diff cannot move a verdict on a file it does not contain. The full farm is CI's run.

Acceptance notes

Out-of-scope observations, recorded here and in the report, ⛔ not filed and ⛔ not fixed:

  • The in-repo documentation pages content/docs/fields/date.mdx and content/docs/fields/datetime.mdx teach dueLike on object field metadata (DateFieldMetadata / DateTimeFieldMetadata), which was already declared. Neither page documents the detail-view field key this PR declares. That is a gap, not a contradiction, and no page needed editing for this ruling. Noted, not filed — no PR or person is queued to touch those pages. Dedupe words if it is ever wanted: dueLike · detail-view field · docs fields date mdx.
  • resolveDueLike gives dueLike: false no suppressing power — the name convention still wins. That is deliberate and documented on both sides; recorded because the new docblock has to state it, and a reader could otherwise take false for an opt-out.

⛔ This PR does not open the wider question of whether a UI hint like this belongs in @objectstack/spec; the key stays objectui-owned until the protocol claims it. Nothing upstream is touched or proposed.

⛔ This PR does not add, remove or otherwise touch the needs:contract-review label the seat hung on the card, does not leave draft, and does not enqueue. The seat lands it.

This branch was produced in the Claude Code session https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L.


Generated by Claude Code

…i#9738)

Maintainer ruling, letter A: the key is declared on the published
TypeScript twin, so the validator, the type checker, the renderer and the
docs say the same thing, and the key's meaning is written once in the
declaration.

- `packages/types/src/views.ts` — `DetailViewField` gains
  `dueLike?: boolean`, with a docblock stating the rendering effect it
  actually has (both halves of the overdue affordance, their different
  day thresholds, and the field-name fallback that `false` does not
  suppress). Every sentence is checked against the tree it describes.
- `packages/types/src/zod/views.zod.ts` — untouched. The mirror already
  declared the key; that was the whole defect.
- `mirror-only-published-keys-9729.test.ts` — the pin MOVES with the
  declaration rather than being repaired. The `@ts-expect-error` that
  recorded the refusal turns unused the moment the twin declares the key
  (`TS2578`), so it is replaced by the assignment it guarded plus an
  exact `Equal` that separates a DECLARED member from one absorbed by an
  index signature. Losing the member again is `TS2353` on the same leg.
- `zod-mirror-parity.test.ts` — the `MirroredUndeclared` row for
  `views.zod.ts#DetailViewFieldSchema` is deleted, which is the only
  direction that ratchet may move in, and the ledger docstring's two
  figures are corrected to the values the sizing pin derives.

Clause-②: yes — the published type's accept set widens by exactly one
key. Measured, not asserted: the interface parses to 13 members at
edbcf1e and 14 at head, the difference being `dueLike`, with
`currency` read by the same instrument on both legs as the lit control;
and a `DetailViewField` literal carrying the key is `TS2353` at
edbcf1e and compiles at head.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 15 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/5903-objectgantt-declared-keys.md

  • names packages/types/src/__tests__/zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⚠️ The JUSTIFICATION for that parity clause was retired (objectui#9743); the clause itself stands and did not move. As first written it credited the zod-mirror-parity ratchet with a zero-drift reading for this pair — but at that time the ratchet measured three directions and was structurally blind to the MIRRORED-but-undeclared one (objectui#9711), so a zero from it recorded that it had not looked in that direction, not that nothing was there. objectui#9725 landed the fourth direction, and it covers this pair BY NAME: packages/types/src/__tests__/zod-mirror-parity.test.ts registers objectql.zod.ts#ObjectGanttSchema in both its mirror map and its declaration map; assertionMirroredUndeclaredMatchesLedger requires every registered pair's mirrored-but-undeclared key set to equal that pair's MirroredUndeclared ledger entry — never for a pair the ledger does not name — and assertionNoVacuousMirroredUndeclaredMeasurement refuses a measurement that has degenerated to any. ⛔ Read this pair's verdict off that reconciliation, which re-derives it on every run, rather than off any figure written here; when this paragraph was authored, on 2026-09-18, it required no MirroredUndeclared entry for the pair.

.changeset/5928-classname-style-props-rename.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Where the non-pair is recorded now. zod-mirror-parity.test.ts keys its existing EXCLUSIONS entry — the mechanism that accounts for every exported const with no TypeScript declaration to mirror, each with its stated reason — to ClassNameStylePropsSchema. Named for its own two keys, the const leaves no like-named declaration for a name-derived pairing to reach for.

.changeset/6051-gantt-flat-config-declared-keys.md

  • names packages/types/src/__tests__/zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⚠️ The JUSTIFICATION for that parity clause was retired (objectui#9743) — the same retirement objectui#5903's entry carries, for the same clause and the same reason; the clause itself stands and did not move. As first written it credited the zod-mirror-parity ratchet with a zero-drift reading for this pair, at a time when that ratchet measured three directions and was structurally blind to the MIRRORED-but-undeclared one (objectui#9711), so a zero from it recorded that it had not looked in that direction. objectui#9725 landed the fourth direction, and it covers this pair BY NAME: packages/types/src/__tests__/zod-mirror-parity.test.ts registers objectql.zod.ts#ObjectGanttSchema in both its mirror map and its declaration map; assertionMirroredUndeclaredMatchesLedger reconciles every registered pair's mirrored-but-undeclared key set against that pair's MirroredUndeclared ledger entry — never for a pair the ledger does not name — and assertionNoVacuousMirroredUndeclaredMeasurement refuses a measurement that has degenerated to any. ⛔ Read this pair's verdict off that reconciliation, which re-derives it on every run, rather than off any figure written here; when this paragraph was authored, on 2026-09-18, it required no MirroredUndeclared entry for the pair.

.changeset/6150-undeclared-but-consumed-keys.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⭐ AMENDED, and the amendment ships in this same release. objectui#7804's TreeViewSchema slice gave the key a zod arm after all — a NAMED REFUSAL (handlerKeyRefusal(key, 'runtime-slot', label)), never a shape — because "no mirror entry" is not neutral under BaseSchema.passthrough(): it meant an authored { "type": "tree-view", "onNodeClick": { "action": "toast" } } parsed GREEN, survived the parse, and reached a call site that expects a function. ⇒ the three clauses this bullet used to carry are no longer true of the code shipping beside it. The key is now a MEMBER of TreeViewSchema.shape and an authored value is refused BY NAME at path onNodeClick; it has LEFT zod-mirror-parity.test.ts's RuntimeOnlyDeclared for that file's KnownDrift; and it is no longer "the first pair to sit there without also sitting in UnmirroredDeclared" — draining it emptied that difference, so RuntimeOnlyDeclared is now a SUBSET of UnmirroredDeclared and the union of the two equals UnmirroredDeclared itself. ⛔ objectui#6152's ruling is untouched by any of this: what the key still does not have, and never will, is a z.function() shape — no serialized document could satisfy one.

.changeset/6175-column-state-persistence.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Nothing is retired. Both spellings remain declared on DataTableSchema; onColumnReorder stays declared and stays unwired, exactly as the RuntimeOnlyDeclared ledger in zod-mirror-parity.test.ts records it. Which of the two survives is a declared-surface ruling that stays open and is deliberately not settled here.

.changeset/6594-headercolor-mirror-enum.md

  • names packages/types/src/views.ts → packages/types/src/views.ts — edited by this change

    The nearest precedent is objectui#5853 (.changeset/5853-tablecolumn-type-canonical-union.md), which narrowed TableColumn.type on the same three-ends pattern and exported a TABLE_COLUMN_TYPES tuple for the zod mirror to build its enum from. That shape is not available here and the difference is structural, not a preference: packages/types/src/views.ts is a type-only module, so a tuple there would add a runtime export to the package barrel (a value export cannot ride the barrel's export type block) and a runtime import edge from the zod entry into views.js. finding(types): TableColumn.type disagrees three ways — interface declares 8 literals, zod mirror accepts any string, renderer's live read set handles values outside the union via an as any cast #5853 had a second reason to export — producers needed its normalizeTableColumnType() at their emit seam — and headerColor has no producer that needs a runtime value. The literals are therefore written on each half and the anti-drift guarantee is carried by the pin above, which also covers the third end a shared tuple could not reach: the renderer, in a package @object-ui/types must not depend on.

.changeset/6639-objectgrid-title-mirrored.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The gain is the typed refusal: the mirror's .passthrough() base was already admitting any title unexamined, and it now enforces the declared string. zod-mirror-parity.test.ts's UnmirroredDeclared ledger records the key as worked off — the ledger's first shrink by repair (97 + 1 mirrored + 23 reclassified is what the seeded "121" now means).

.changeset/7287-resolve-title-field-shared-ladder.md

  • names views.ts → packages/types/src/views.ts — edited by this change

    def.primaryField — a DetailViewSchema key (@object-ui/types views.ts), read off an OBJECT def and ranked ABOVE the canonical nameField ADR-0079 Phase 2 made the pointer (AGENTS.md Commandment #0.1). No producer can put it there: @objectstack/spec's object schema is a strictObject that answers unrecognized_keys: ['primaryField'], and ObjectSchema.create() throws — which is why objectstack#6326 deleted the identical read from two lint rules. A census across both repos found zero object payloads carrying it (the only writers are three test fixtures), and primaryField appears in zero files of the shipped @objectstack/spec@17.2.0 dist against 68 for nameField. Same shape as the undeclared objectDef.titleField read objectui#6531 measured and Five more consumer-side reads of the undeclared object-level titleField survive #6531 — in ObjectView view-config fallbacks and a useRecordSearch memo key #6557 removed. DetailViewSchema.primaryField is untouched and still honoured by DetailView's own header — it is a view key, and on a view it is legitimate.

.changeset/7344-handler-string-any-mirrors.md

.changeset/7586-primary-field-consumer-reads.md

  • names views.ts → packages/types/src/views.ts — edited by this change

    primaryField is a DetailViewSchema key (@object-ui/types views.ts) — a view key, which DetailView.resolveDisplayTitle reads off schema and is welcome to. Read off an object def it is undeclared: @objectstack/spec's object schema is a strictObject answering unrecognized_keys: ['primaryField'], and ObjectSchema.create() throws. primaryField appears in zero files of the shipped @objectstack/spec@17.2.0 dist, against 68 for the canonical nameField. objectstack#6326 removed the identical read from two lint rules; objectui#7287 / PR fix(plugin-detail): resolveTitleField delegates to the shared ADR-0079 ladder #7585 removed it from resolveTitleField. These two survived it — and three of this repo's own changelogs already called the probe "not a spec property — always undefined" while the code kept honouring it.

.changeset/7654-floating-chatbot-trigger-icon-tombstone.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Every other tombstone in this package pairs ?: never with a retirementTombstone() refusal on the Zod twin. There is no twin here to carry one: FloatingChatbotConfig has no Zod mirror at all, and floatingConfig sits in the UnmirroredDeclared ledger (zod-mirror-parity.test.ts, complex.zod.ts#ChatbotSchema). BaseSchema is .passthrough(), so the whole floatingConfig object rides through unvalidated — before this change and after it. Minting a mirror to host a refusal would be the declared-but-UNMIRRORED axis (objectui#6152), a different defect: a key can be mirrored and inert, or unmirrored and live, and fixing one says nothing about the other. This change does not widen into it.

.changeset/7804-tree-view-handler-slot.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The pair moves from zod-mirror-parity.test.ts's RuntimeOnlyDeclared to its KnownDrift, which empties the former of the one entry the latter did not also hold — so the two unmirrored ledgers are now in a containment relation, and the cross-ledger figure that recorded their difference states the containment instead.

.changeset/8338-retire-toast-action.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The parity ledgers drain with it, every figure re-derived by zod-mirror-parity.test.ts's own AST and mirror instruments rather than stepped by hand: KnownDrift 42 entries / 64 keys → 41 / 63 (the entry's whole content, so the entry went too — the ledger's first loss by RETIRING a key rather than by moving either face toward the other), and WiderThanDeclared 23 / 36 / 47 arms, split 6 / 30 / 0 / 11 → 22 / 35 / 45, split 6 / 29 / 0 / 10. The pair itself stays registered, so EXPECTED_MIRROR_PAIRS does not move.

.changeset/8572-chatbot-body-retired.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Why this key and not another. It was the ONE place in this vocabulary where body did not mean "what goes inside this component": zod-mirror-parity.test.ts carried the pair under KnownDrift as "two different meanings of one key", and the same collision was the whole reason chatbot was the single arm of the component union whose output was not assignable to SchemaNode. Both ledger rows move with this change, and the two pins that recorded the old state are INVERTED rather than deleted (see below).

.changeset/object-view-unmirrored-keys-7779.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Who is NOT affected: every correctly typed document, and every document that never wrote these keys — absent stays valid on all nine. No renderer changed. The parity ledger (zod-mirror-parity.test.ts) records the move: UnmirroredDeclared 14 entries / 96 keys to 14 / 87, the ObjectViewSchema entry re-derived into the SPEC-DERIVED half because the mirror now references the spec in code.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with edbcf1e7a (merge-base with origin/main): 3 file(s) changed outside .changeset/, read against 1203 pending declaration(s) that publish a body (1762 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3054.3 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-CuDuhvNV.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.91KB 130.72KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 216.90KB 60.15KB
fields (index.js) 249.62KB 63.02KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.73KB 20.08KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 132.96KB 35.17KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 254.30KB 66.21KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 137.49KB 34.64KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 213.44KB 58.21KB
plugin-kanban (index.js) 48.10KB 14.94KB
plugin-list (index.js) 113.42KB 27.95KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.05KB 21.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 109.04KB 36.08KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.38KB 1.98KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.74KB 2.54KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 15.71KB 5.30KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 04576924623cce8ccf97be32524fa209fb9046db

Read in full before judging: AGENTS.md at the checkout (593 lines, commandments #0, #0.1, #9, #11 and the operational sections), the PR body, the card objectui#9738 body and all 5 comments (single page, comments: 5 on the card, 5 returned), the 2 PR comments, all 36 check runs (page 1 returned 30, page 2 the remaining 6). Head sha equals the remote tip of claude/issue-9738-duelike-declared-on-twin (git ls-remote reading). Diff against merge-base edbcf1e7aa is 4 files, +168 / -44, matching the brief.

① Derived judgments

  • ACCEPTED SET widened — DetailViewField (published: packages/types/src/index.ts exports it in its export type block) gains dueLike?: boolean. Members read off the interface at head: name, label, type, format, render, value, readonly, visible, span, options, reference_to, reference_field, currency, dueLike — 14, one more than base. Judged the RIGHT widening and EXACTLY as wide as the mirror. The objectui mirror packages/types/src/zod/views.zod.ts#DetailViewFieldSchema (a plain z.object, strip mode, untouched by this diff) declares, quoted from head: dueLike: z.boolean().optional().describe('Marks a date/datetime field as due/deadline-semantic, gating the relative "Overdue Nd" wording (vs. neutral "Nd ago" for start/end/created dates)'). The twin's dueLike?: boolean resolves to boolean OR undefined: the root tsconfig.json sets "strict": true and the same grep finds no exactOptionalPropertyTypes anywhere in the packages/types extends chain (the strict hit is the control). Not any, not an index signature — the new exact-equality pin refuses both.
  • @objectstack/spec carries NOTHING on this shape — so the widening aligns the twin to objectui's own mirror, not to the protocol, exactly as the ruling says. Reading source, not the PR body: the checkout has no node_modules, so the "installed" leg was a dead instrument (key and control both zero; that reading is void and not relied on). pnpm-lock.yaml resolves @objectstack/spec@17.4.0; the sibling /home/user/objectstack/packages/spec/package.json is "version": "17.4.0". In packages/spec/src: dueLike (case-sensitive) 0 files against control reference_to 8 files in the same command. Case-insensitive duelike|overdue|due_like hits 6 files, every one read: automation/time-relative-trigger.zod.ts and its tests (a trigger's withinDays lookback), ui/view.zod.ts borderColorField (a Gantt stroke colour), and ai/solution-blueprint.zod.ts prose — none is a field-level due-semantic flag. No detail-view field shape exists in the spec: export const DetailView* 0 in packages/spec/src while export const ListViewSchema resolves in ui/view.zod.ts (control). Commandment #0 is therefore engaged by the KEY, not by this diff: the key is not a spec property, and the maintainer's ruling names that explicitly ("objectui-owned until the protocol claims it"). Commandment #0.1 is not engaged: the diff touches no renderer and adds no fallback; it moves the contract, deliberately, which is the direction that rule permits.
  • PUBLISHED SURFACE — the dueLike docblock (ships in the .d.ts). Every code-behaviour claim checked against head: resolveDueLike in packages/fields/src/index.tsx short-circuits only on declared === true, then tests DUE_LIKE_FIELD_NAME; isOverdueInstant returns date earlier than startOfToday; formatRelativeDate in packages/core/src/utils/date-display.ts returns the absolute face when diffDays is below minus 7 BEFORE the branch gated on diffDays below minus 1 AND options?.dueLike, and options.t is called only inside that branch, so "Overdue 2d" is indeed the shortest phrase and the red (from isOverdueInstant, applied to className on the date span and to cellClass on the datetime span, both computed before the face branches) starts a day before the wording and outlives it; enrichDetailField in packages/plugin-detail/src/fieldEnrichment.ts opens const enriched: Record<string, any> = { ...viewField }; the probe packages/plugin-detail/src/__tests__/DetailSection.dueLikeReachesTheCell-9729.test.tsx exists at head. One SOFT over-statement, not a false claim about code: the docblock's "two documentation pages" and the changeset's "the date and datetime documentation pages teach it" — content/docs/fields/date.mdx and datetime.mdx teach dueLike on DateFieldMetadata / DateTimeFieldMetadata literals; neither mentions a detail view or DetailViewField (0 hits, control DateTimeFieldMetadata 3 hits in the same file). The docblock itself states "It is the same key, with the same meaning", which is what makes the sentence defensible; a tighter spelling ("teach the key on object field metadata") would be better, and I record it rather than fail on it.
  • ACCEPTED-SET INSTRUMENT — mirror-only-published-keys-9729.test.ts. The @ts-expect-error that recorded the refusal is removed; the assignment authoredDueLike: DetailViewField = { name: 'end_date', dueLike: true } now stands on its own, the lit control authoredCurrency is retained, and an exact equality of DetailViewField['dueLike'] against boolean OR undefined is added, separating DECLARED from ABSORBED. The mirror-side pins above it are untouched and still assert the mirror KEEPS the key (with an unknown-key control that is stripped) and REFUSES dueLike: 'yes' by path name. Judged: the pin moved with the ruling and discriminates in its new direction. Zero @ts-expect-error mentioning dueLike remain under packages/types/src/__tests__ against 475 ts-expect-error mentions in that directory (control, same command).
  • LEDGER — zod-mirror-parity.test.ts MirroredUndeclared row views.zod.ts#DetailViewFieldSchema deleted; docstring figures 8/102 to 7/101. Independently re-derived here by stripping comments from the interface body and counting entry keys and union arms: base entries=8 arms=102, head entries=7 arms=101. The sizing pin (describe('the fourth direction is enumerated and sized at test time (objectui#9711)')) reads the figures off the docstring's own regex and compares them to the AST-derived count, so the corrected prose is re-verified on every run (commandment 完善设计器的每一个细节 #9 satisfied). Test shards 1 to 4 and Type Check are success at this head. The neighbouring objectql.zod.ts#ObjectGridSchema row stays, correctly: a tombstone is still a stated mirror member. No key set was edited to satisfy a sentence.
  • Nothing else moves. views.zod.ts untouched (verified reading, 4-file diffstat), no renderer file, no spec import, no other published type. No cross-file path:line citation is introduced in the diff (commandment [WIP] Update documentation for project #11; Line Citation Gate success at head).

② Semver level

Changeset .changeset/duelike-declared-on-detail-view-field-9738.md: frontmatter '@object-ui/types': minor, body carries Clause-②: yes — the published type's accept set widens by exactly one key. A widening needs at least minor; repo policy forbids major in the fixed group and Changeset Bump Policy is success. Level and declaration match ①, and match the ruling's own Execution line ("Clause-②: yes ... changeset @object-ui/types minor").

③ Boundary flags

  • Ruling vs diff. hotlong's ruling comment (letter A, maintainer 「同意」 2026-09-18T14:45Z) authorises: dueLike?: boolean declared on the field-config type with a describe stating its rendering effect; mirror and twin re-pinned as agreeing; the upstream-spec question NOT opened; changeset minor. The diff does exactly those things and nothing beyond them. The card body's line "裁 A ⇒ 在本车道立一张执行卡" was superseded by the ruling's "needs-user-decision → pm:queue", so Fixes #9738 on the decision card is consistent with how the ruling routed it. Answered: no unauthorised implementation.
  • Flag: docs teach dueLike on object field metadata only; the detail-view key remains undocumented — "noted, not filed". Measured true (see ①). The card body's A-execution line included 「把仓内文档与新声明对齐」, but the maintainer's ruling did not restate it and the dispatch's declared file surface excluded content/docs, so it is outside this PR's authorised scope and pre-existing, not created by this diff. It does not affect the widening's correctness. However commandment Add automated testing infrastructure and CI/CD workflows #2 makes docs part of a feature's completion, and the implementer's "Successor: none" leaves it owned by nobody — this must be ESCALATED to a follow-up card (document DetailViewField.dueLike, dedupe words dueLike · detail-view field · docs fields date mdx). The reviewer cannot file; the adopting seat should.
  • Flag: dueLike: false does not suppress the field-NAME fallback. Answered: matches resolveDueLike (only === true short-circuits) and datetime.mdx ("set dueLike: false is not required to opt out, but authoring a neutral name is"); the docblock states it; no accepted-set change is involved and none is implied. No action.
  • Flag: the PR does not propose the key to @objectstack/spec. Answered: matches the ruling verbatim, and the protocol has no counterpart surface (measured in ①). No action.
  • Flag: label / draft / queue untouched. Procedural, seat-owned: PR is draft, labels package: types and tests; needs:contract-review sits on the card. Not a contract matter.
  • Changeset Claim Re-read bot (15 pending changesets naming touched files). Each quoted paragraph read against this diff: they concern other pairs (ObjectGanttSchema, ClassNameStylePropsSchema, TreeViewSchema, ChatbotSchema, DataTableSchema) or other views.ts keys (headerColor, primaryField), and the ones mentioning MirroredUndeclared route the reader to the reconciliation rather than to a figure. None is falsified by the row deletion.

Implemented-by: claude/issue-9738-duelike-declared-on-twin
Reviewed-by: session_018HrVaotisyhgmot9o2MLRq

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Clause-② gate CLEARED on a PASS — provenance, and one machine reading that needs stating rather than hiding. domain:ui#2 execution seat (PM session session_018HrVaotisyhgmot9o2MLRq), 2026-09-20T09:22Z, under the maintainer's direct instruction 「#9926 / #9993 你可以执行契约复审」.

The record this clear cites

  • Record: comment 5748920034 on this PR — a ## Contract review naming head 04576924623cce8ccf97be32524fa209fb9046db, VERDICT: PASS.
  • Carrier cleared: needs:contract-review removed from card objectui#9738 at 2026-09-20T09:21:41Z (four-step write, read back).
  • Tier: ⛔ this seat did not self-review. CONTRACT_REVIEW_TIER is claude-fable-5-1 and this seat is served Opus, so the fuse 「未达档 ⛔ 不自审,走保险丝路」 applies: the review was rendered by an isolated at-tier subagent, briefed adversarially and fed only this card, its rulings and the PR itself — ⛔ never this seat's conclusions. The record was adopted verbatim; ⛔ nothing in it was rewritten or polished.

⚠️ --pair 9993 reads exit 4, and here is exactly why

PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9993 returns 4, on row C3: the gate 「was bound on the CARD carrier only … with no hang ever recorded on the other」, so one removal appears where a legitimate clear leaves two — the strip signature.

⇒ The missing half is ⛔ not this clear. The protocol requires 「PR 与卡双载体同笔挂:PR 一存在即挂」, and the PR-side carrier was never hung by the dispatching seat. That is a pre-existing half-state on this pair, and it is what makes the pair illegible to that row after any honest clear.

⛔ This seat did not manufacture the missing signature. Re-hanging the label in order to remove it again would fabricate the very evidence the row exists to read. The protocol's own reading rule covers this state instead: 「PASS + 无标 + head 未动 = 已清标不是被剥」 — a PASS record on an unmoved head is what separates a cleared gate from a stripped one — together with 「清标缺引记录即半态」, which is why this comment exists and cites the record id and the head.

The same script's C6-RECORD row confirms the other half independently: it reads comment 5748920034 on this head, sees the Reviewed-by: line, and notes that its Served-tier: names the tier constant, so it 「reads at tier on C7 as well」.

What follows

  • Flipping ready and enqueuing now. ⛔ No approval review is submitted by this seat, and ⛔ nothing is merged by hand.
  • The review's escalated flag is being filed as its own card, as the record asks: the detail-view dueLike key is taught nowhere in content/docs (the two field pages teach it on object field metadata), and the implementer left it with 「Successor: none」.

Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 20, 2026 09:22
@os-tesla
os-tesla added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 8b446f5 Sep 20, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-9738-duelike-declared-on-twin branch September 20, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants