Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/duelike-declared-on-detail-view-field-9738.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@object-ui/types': minor
---

`DetailViewField` declares `dueLike?: boolean` — the published TypeScript twin now accepts the key its own validator already judged and its own renderer already honours (objectui#9738).

Three of the four faces already agreed. `views.zod.ts#DetailViewFieldSchema` validated `dueLike` and kept it; `DetailSection` spreads the authored field into the bag `enrichDetailField` returns and hands it to the resolved cell renderer, whose `resolveDueLike` reads the key; the `date` and `datetime` documentation pages teach it. Only the twin disagreed — and because `DetailViewField` carries no index signature, it disagreed loudly: the same document the validator accepted was refused by the compiler.

base (edbcf1e7aa) const f: DetailViewField = { name: 'end_date', dueLike: true }
-> error TS2353: 'dueLike' does not exist in type 'DetailViewField'
head -> compiles

So an author following the docs and the validator got a red build, and an author following the type had no way to reach a shipped capability. Maintainer ruling, letter A: declare it, so the meaning of the key is written once, in the declaration.

`Clause-②: yes` — the published type's accept set widens by exactly one key. Measured by parsing the interface, comments stripped by construction: 13 members at base, 14 at head, the difference being `dueLike`; the same instrument reads `currency` on both legs, so the base zero is a measured absence rather than a silent instrument.

Nothing else moves. The zod mirror is untouched — it already declared the key, with the `describe` text this declaration's docblock restates. The two pins move WITH the measurement rather than being repaired: the `@ts-expect-error` that recorded the refusal is replaced by the assignment it guarded plus an exact `Equal` that separates a declared member from one absorbed by an index signature, and the `MirroredUndeclared` ledger row for this pair is deleted, which is the only direction that ratchet may move in.
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,42 @@
* assertion below is a statement about `origin/main` as it stands; whichever
* direction the review rules, the entries here move with it.
*
* ⭐ ONE OF THE TWO HAS SINCE BEEN RULED, and this file moved with it exactly as
* the paragraph above promised. objectui#9739 (maintainer, 2026-09-18) took
* letter C on `operators`: removed from the published mirror, a tombstone left
* in its place that refuses the key by name and names the spelling the upstream
* protocol declares. So the `operators` section below no longer pins a SILENT
* ACCEPTANCE — it pins the LOUD REFUSAL that replaced it, and the lit control
* beside it is what keeps the assertion honest in its new direction. The full
* per-half measurement, including the upstream re-derivation of the spelling the
* message names, lives in `object-grid-operators-tombstone-9739.test.ts`; what
* stays here is the BEFORE/AFTER continuity, so the defect this card measured
* cannot quietly return under the same name. `dueLike` is ⛔ untouched and still
* awaits its own ruling.
* ⭐ BOTH KEYS HAVE NOW BEEN RULED, and this file moved with each of them exactly
* as the paragraph above promised — in OPPOSITE directions, which is the thing to
* read here before assuming one ruling generalises to the other.
*
* - `operators` — objectui#9739 (maintainer, 2026-09-18) took letter C:
* removed from the published mirror, a tombstone left in its place that
* refuses the key by name and names the spelling the upstream protocol
* declares. So the `operators` section below no longer pins a SILENT
* ACCEPTANCE — it pins the LOUD REFUSAL that replaced it, and the lit
* control beside it is what keeps the assertion honest in its new
* direction. The full per-half measurement, including the upstream
* re-derivation of the spelling the message names, lives in
* `object-grid-operators-tombstone-9739.test.ts`.
* - `dueLike` — objectui#9738 (maintainer, 2026-09-18) took letter A: the
* key is DECLARED on the published TypeScript twin, so the validator, the
* type checker, the renderer and the docs now say the same thing. The
* mirror is ⛔ untouched — it already stated the key — so the section below
* that measures the mirror is unchanged, and it is the TypeScript half at
* the bottom of this file that inverted.
*
* What stays, in both cases, is the BEFORE/AFTER continuity, so neither defect
* this card measured can quietly return under the same name.
*
* ## The two keys do NOT behave the same way, and the difference is the ruling
*
* Both twins are hand-written interfaces, but only one of them can REFUSE:
*
* - `DetailViewField` carries no index signature, so TypeScript rejects an
* - `DetailViewField` carried no index signature, so TypeScript rejected an
* authored `dueLike` outright (`TS2353`). The two published faces therefore
* actively CONTRADICT each other: the validator judges the key and keeps
* it, the compiler refuses the same document.
* actively CONTRADICTED each other: the validator judged the key and kept
* it, the compiler refused the same document. ⭐ That was the loud defect,
* and it was ruled letter A (objectui#9738): the twin DECLARES the key now,
* so the accept set of the published TypeScript face widened by exactly one
* key and the contradiction is closed in the direction that keeps the
* renderer's behaviour. It still carries no index signature — which is what
* keeps the assignment at the bottom of this file a real assertion.
* - `ObjectGridSchema` extends `BaseSchema`, whose `[key: string]: any` index
* signature absorbs any unstated key. TypeScript neither declares nor
* refuses `operators`; it types it `any`. The faces did not contradict —
Expand Down Expand Up @@ -181,18 +196,55 @@ describe('complex.zod.ts#DashboardConfigSchema — `aria` is the CONTRAST, not t
/* ── The TypeScript side, where the two keys stop behaving alike ───────────── */

/**
* `DetailViewField` REFUSES `dueLike`. The `@ts-expect-error` below IS the
* assertion: it fails the package's `tsc -p tsconfig.test.json` leg in both
* directions — if the key stops being refused (someone declares it on the twin,
* which is one of the two remedies under review) the directive becomes unused
* and TypeScript reports it.
* `DetailViewField` ACCEPTS `dueLike`. This binding IS the assertion, and it is
* read by the package's `tsc -p tsconfig.test.json` leg.
*
* ⭐ THE DIRECTION OF THIS ASSERTION IS INVERTED FROM WHAT THIS CARD MEASURED,
* deliberately and by ruling — ⛔ not relaxed, and ⛔ not deleted, to keep a leg
* green. What stood here was a `@ts-expect-error` recording the REFUSAL, written
* so that it would fail in BOTH directions; objectui#9738 letter A declared the
* key on the twin, which is precisely the direction that turns such a directive
* unused (`TS2578`, measured on this file). So the measurement moves WITH the
* declaration, onto the same literal, and stays discriminating in its new
* direction: the twin losing the member again makes this line `TS2353` on the
* same leg.
*
* ⚠️ The assignment alone would not be enough, and the second assertion below is
* why. An index signature added to `DetailViewField` would let this literal
* compile while `dueLike` is undeclared — exactly the shape `ObjectGridSchema`
* has further up this file, where an authored key is absorbed as `any` and
* nothing contradicts it. The exact `Equal` separates DECLARED from ABSORBED.
*/
// @ts-expect-error objectui#9729 — `dueLike` is not a member of `DetailViewField`.
export const authoredDueLike: DetailViewField = { name: 'end_date', dueLike: true };

/** LIT CONTROL: a key the twin DOES declare is accepted on the same literal. */
export const authoredCurrency: DetailViewField = { name: 'end_date', currency: 'USD' };

/**
* Invariant equality — `extends` both ways would accept a narrowing. Spelled
* here rather than imported from `zod-mirror-parity.test.ts`, which exports the
* same two lines: no test file in this package imports another, and a two-line
* helper beside its single use is cheaper than establishing that it may.
*/
type Equal< A, B > =
(< T >() => T extends A ? 1 : 2) extends (< T >() => T extends B ? 1 : 2) ? true : false;
type Expect< T extends true > = T;

/**
* The member is DECLARED with a real type, not absorbed by an index signature
* and not widened to `any` — `boolean | undefined`, exactly.
*
* ⛔ `extends` is not an acceptable weakening here: `any` and a `boolean`-typed
* index signature both satisfy an `extends` written in either direction, and
* those are the two ways this declaration could be present in a listing while
* being absent as a contract. The mirror's own judgement of the same key — that
* `dueLike: 'yes'` is refused BY NAME — is pinned at the top of this file, so
* the two published faces are asserted to agree on the TYPE as well as on the
* name.
*/
export type assertionDueLikeIsDeclaredOnTheTwin =
Expect< Equal< DetailViewField['dueLike'], boolean | undefined > >;

/**
* The TWIN still does NOT refuse `operators` — and the absence of a
* `@ts-expect-error` here is the measurement, not an omission: adding one
Expand Down
47 changes: 24 additions & 23 deletions packages/types/src/__tests__/zod-mirror-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3464,11 +3464,17 @@ export const assertionWiderLedgerRecordsEveryKey: never = 0 as unknown as WiderL
* Exact MIRRORED-BUT-UNDECLARED key set per pair — the seed of the direction
* objectui#9711 opened, measured on the tree this ledger landed on.
*
* `this ledger seeds **8 entries** carrying **102 keys**` — and ⛔ read that off the
* `this ledger seeds **7 entries** carrying **101 keys**` — and ⛔ read that off the
* census at the bottom of this file, not off this sentence: both figures are pinned
* to this ledger's own AST by 'the fourth direction is enumerated and sized at test
* time', so an entry or an arm added here moves them or the file reddens.
*
* ⭐ Both figures moved DOWN by one when objectui#9738 retired the
* `views.zod.ts#DetailViewFieldSchema` row — the ratchet's only permitted
* direction, and the first time this seed has shrunk. The comment left where that
* row stood says which ruling took it and why the neighbouring `operators` row
* stayed after a ruling of its own.
*
* ## ⛔ What this ledger is NOT
*
* ⛔ It is NOT a worklist to be drained by DECLARING the keys it names. Declaring a
Expand Down Expand Up @@ -3699,28 +3705,23 @@ interface MirroredUndeclared {
"objectql.zod.ts#ObjectGridSchema":
| "operators";

/**
* LOCAL, one key. An author writing `dueLike` on a detail-view field gets a green
* `safeParse` from a published validator for a key the published `DetailViewField`
* never invited — and, because that twin carries no index signature, the SAME
* document is refused by the compiler (`TS2353`). The two published faces
* contradict each other rather than one of them being merely quiet.
*
* ⛔ This entry once justified itself with "a `git grep` finds it in the mirror and
* nowhere in this package's TypeScript declarations", which is FALSE as written and
* is corrected here rather than repeated: `field-types.ts` declares `dueLike` on
* `DateFieldMetadata` and on `DateTimeFieldMetadata`, both in this package. ⭐ A
* NAME is not a KEY — a key is (interface, name) — and a package-wide grep for the
* name answers a question this ledger is not asking. What the operator measures,
* and what stays true, is that the key is absent from THIS PAIR'S twin.
*
* The per-key consequences — that the mirror judges the key rather than merely
* passing it, and that objectui's own detail-view path READS it at runtime — are
* measured by `mirror-only-published-keys-9729.test.ts` and by the two probes
* objectui#9729 landed beside the renderers that read it. ⛔ No remedy is implied.
*/
"views.zod.ts#DetailViewFieldSchema":
| "dueLike";
// `views.zod.ts#DetailViewFieldSchema` HAD a row here, for `dueLike`, and its
// DELETION is the ratchet moving in the only direction this seed may move in
// — ⛔ not an entry lost in an edit. objectui#9738 (maintainer, 2026-09-18,
// letter A) declared `dueLike?: boolean` on the `DetailViewField` twin, so the
// mirror no longer states a key the twin does not and the pair measures
// `never`. Re-adding the row without re-adding the defect reddens the
// reconciliation below, which is the guard against exactly that.
//
// ⚠️ Read this beside the `objectql.zod.ts#ObjectGridSchema` row above, which
// STAYED after its own ruling: a retirement tombstone is still a stated
// member, a declaration on the twin is not. The two rulings landed the same
// day and moved this ledger in opposite directions.
//
// The per-key consequences — that the mirror judges the key rather than merely
// passing it, and that objectui's own detail-view path READS it at runtime —
// are measured by `mirror-only-published-keys-9729.test.ts` and by the two
// probes objectui#9729 landed beside the renderers that read it.
}

/**
Expand Down
54 changes: 54 additions & 0 deletions packages/types/src/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,60 @@ export interface DetailViewField {
* Currency code for currency fields (e.g. 'USD', 'EUR')
*/
currency?: string;
/**
* Marks a `date` / `datetime` field as due/deadline-semantic (vs. a plain
* start/end/created date). It is the same key, with the same meaning, as
* `DateFieldMetadata.dueLike` and `DateTimeFieldMetadata.dueLike` in
* `./field-types.ts` — those two are OBJECT metadata; this one is the
* AUTHORED detail-view field, which is a different key of the same name.
*
* ## What it does to the rendered cell
*
* Both halves of the overdue affordance, on the `date` and the `datetime`
* cell alike:
*
* - **wording** — inside the relative face, a past due date reads
* `Overdue Nd` instead of the neutral `N days ago`. `formatRelativeDate`
* (`@object-ui/core`) gates that phrase on this key; the phrase has no
* `Intl` equivalent, so it is also the only route by which that function
* reaches the translate fn.
* - **styling** — the cell's span gains `text-red-600` once the deadline
* day has passed, whichever display face it is painting
* (`isOverdueInstant` in `@object-ui/fields`).
*
* ⚠️ The two halves have different thresholds, inherited from the shared
* relative-time path and ⛔ not re-decided here: the red styling starts the
* day after the deadline, while the `Overdue Nd` wording starts the day after
* that (`formatRelativeDate` gates its phrase on a difference of more than
* one calendar day, so `Overdue 2d` is the shortest phrase this codebase
* produces). Beyond a week the relative face falls back to an absolute date
* and the wording stops; the styling does not.
*
* ## Why it is declared HERE, on the detail-view field
*
* Because the renderer already honours it here, on the AUTHORED field of a
* detail view — not only on object metadata. `DetailSection`
* (`@object-ui/plugin-detail`) spreads the authored field into the bag
* `enrichDetailField` returns and hands that bag to the resolved cell
* renderer, whose `resolveDueLike` reads this key first and falls back to the
* due/deadline field-NAME convention only when it is not `true`. That read is
* measured by rendering rather than by grep, in that package's
* `DetailSection.dueLikeReachesTheCell-9729.test.tsx`, which draws the same
* `end_date` field with and without the key and watches the drawn wording
* change.
*
* Until objectui#9738 this interface was the one published face that refused
* the key (`TS2353` — it carries no index signature) while
* `DetailViewFieldSchema` in `./zod/views.zod.ts` validated it and the
* renderer honoured it. The declaration is that contradiction's remedy
* (maintainer ruling, letter A), ⛔ not a new capability: the obligation was
* already on the books, with a reader and two documentation pages.
*
* ⚠️ Omitting the key is NOT the same as writing `false`. Absent, the
* field-NAME convention can still turn the affordance on; `false` does not
* suppress that fallback either — only a neutral field name does.
*/
dueLike?: boolean;
}

/**
Expand Down
Loading