Commit 07bf21f
spec(liveness): object.imageField is live — the record chrome draws it at the objectui pin (#21824)
Part of #21765
Clause-②: no
Seam: `spec:ObjectSchema.imageField` → `renderer:objectui record chrome
(page:header, containers.tsx)`
**Why `Part of` and not a closing line.** The card's item 3 stays open:
whether Studio's object form offers `imageField`. Its `omit` row lives
in this repo, and a written rule says the row is stale once the key is
enforced. The same rule hands the offer itself to a decision, and the
gate refuses the row's deletion unless that decision is made in the same
change (measured below). So item 3 goes back to the seat as an open
question. This PR does items 1 and 2, and adds the declaring example the
director seat's unblock asked for.
## The reader, measured at the pin
`.objectui-sha` on `main` is `9dfaca654311`. `git merge-base
--is-ancestor c096f032793d 9dfaca654311` exits 0 in the objectui clone,
so the pin carries objectui PR 11619's merge. In objectui at
`9dfaca654311`,
`packages/components/src/renderers/layout/containers.tsx`:
- `:1474` `PageHeaderRenderer`. On its record-context branch
(`objectSchema` and `data` from the record context), `:2392` reads
`objSchema?.imageField`.
- `:1458` `recordPictureUrl(value)` resolves the served row's value of
that field. It takes the expanded `{ url }` form, a bare `sys_file` id
(served from `/api/v1/storage/files/`), a legacy URL string, or the
first entry of a `multiple` list that resolves.
- `:2449` `icon={recordPicture}` puts the picture in the record chip's
`icon` slot beside the H1. An `avatar` field is drawn round and cropped,
an `image` field whole in a rounded square. An empty value draws
nothing: no initials, no placeholder.
## What changed
1. **Liveness row** `packages/spec/liveness/object.json` `imageField`:
`planned` → `live`, `verifiedAt` 2026-10-05, `evidenceScope:
cross-repo`. The evidence has three parts: the authoring judgement
(`object.zod.ts#refuseNonPictureImageField`), plus `objectui
9dfaca654311` `containers.tsx#PageHeaderRenderer` and
`#recordPictureUrl` with the lines above. The note is rewritten to what
is true now. Both halves are live, the carrier is gone, and the note
names the reference-app declaration.
2. **Describe and TSDoc** `packages/spec/src/data/object.zod.ts`:
- The `.describe()` drops "Pending renderer: the record chrome does not
draw it yet." Its "is to draw" becomes "draws".
- The TSDoc paragraph that said the reader "does not read the key yet"
now names the reader (`PageHeaderRenderer`, the record chip's icon
slot). No shape, refusal or error text moves.
3. **Hand-written doc** `content/docs/data-modeling/objects.mdx` →
Display: "is to draw ... once the renderer reads it; no renderer draws
it yet" becomes "draws beside the title". The row also gains the
describe's own sentence: an empty field shows no picture.
4. **Regenerated by the repo's tooling, not by hand.** `pnpm --filter
@objectstack/spec check:generated` named two stale artifacts,
`check:docs` and `check:liveness`. `--fix` regenerated exactly those
two:
-
`content/docs/references/{api/metadata,data/object,system/migration}.mdx`;
- `liveness/state-counts/object.md`, where `object` moves to live 51 /
planned 1.
5. **Declaring example**
`examples/app-showcase/src/data/objects/field-zoo.object.ts`:
`imageField: 'f_image'`, on the existing `Field.image()`. This is the
object and field the pin-bump smoke drew with a temporary edit.
- Not seeded. The showcase seeds the field zoo, but a stored `image`
value is a managed `sys_file` id. The seed's own note on
`showcase_task.cover` (`src/data/seed/index.ts`, ADR-0104) says why a
seed cannot honestly mint one.
6. **Changeset** `.changeset/21765-object-image-field-live.md`:
`@objectstack/spec` `patch`, `Clause-②: no`. It is text only.
## Item 3: Studio's object-form `omit` row (measured, not decided)
- **Where it lives:** in this repo. The row is
`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts:394-400`,
in the group "Declared, not enforced yet — no offer until it is
enforced" (`:386`). The form it excuses is
`packages/spec/src/data/object.form.ts`.
- **The written rule** is at `:307-309`: "The not-enforced-yet rows hold
only while the verdict does. Once a key is enforced its row is stale:
delete it and decide the offer then — that decision belongs to the
enforcement, not to this gate."
- **Deleting the row alone, measured** on the committed state with
`scripts/ablation-replace.mjs` in wrap mode:
- The anchor hit 1 → 0, blob `26f47a279c33` → `bb160bb1b747`.
- The run went **1 failed / 75 passed**: `object: every top-level key
the author may write is offered, or its omission is recorded`, which
printed "object.(root): accepted by the Zod but unauthorable in the form
— offer it, or add a root ledger entry that records why it is not
offered: expected [ 'imageField' ] to deeply equal []".
- Restore: blob == HEAD (`26f47a279c33`) and `git diff HEAD` empty.
Baseline before the probe: 76/76.
- **Why it is left unchanged.** The rule's first half cannot land
without its second half, and the rule calls the second half a decision.
Neither way out is mechanical:
- A form row repeats the call `#19331` made for 45 keys. It also takes
the four `metadata-forms.generated.ts` catalogs in
`packages/platform-objects`, with authored `zh-CN` / `ja-JP` / `es-ES`
leaves. Those files are outside this claim's file surface.
- A recorded reason fits no existing class. The ruled classes admit only
their ruled key lists.
- **Consequence while it is open:** from this PR's landing, the row's
reason says "liveness verdict `planned`" while the ledger says `live`.
The test reads liveness for no class except the three ruled ones, so it
stays green. The stale reason is what the open question is about.
## Tests and gates (tree `80a7677773`, the final commit)
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: **615 files passed, 18360 passed / 1 todo**, `VERDICT
command-exit 0`.
- `pnpm --filter @objectstack/spec run typecheck`: exit 0, "52 file(s) /
246 error(s) / 135 pinned signature(s) held".
- `pnpm --filter @objectstack/example-showcase verify`: `os validate`
"Validation passed", `tsc --noEmit` clean, **32 files / 399 tests
passed**, `VERDICT command-exit 0`. The dependency closure was built
first (`turbo run build --filter=@objectstack/example-showcase^...`,
60/60).
- `check:liveness`: exit 0. `object` reads 52 classified (live 51,
planned 1); the planned one is `externalSharingModel`. The repo total
reads 1000 live · 1 experimental · 1 live-elsewhere · 108 dead · 8
planned = 1118. The repo-local evidence path and the
`#refuseNonPictureImageField` anchor resolve. The `objectui:` paths are
attributed and counted, never resolved: that is the gate's boundary.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived **107** commands from 9 paths
against merge base `75ddcd1b4`. `--ran` reconciles **107 derived, 107
run, 0 NOT-MEASURED, 0 UNRUN**, and every one exited 0.
- Lint, a measured narrowing:
- `eslint --no-inline-config --format json` on the two changed TS files:
**2 files, 0 errors, 0 warnings**.
- The population is `eslint.config.mjs:971`
(`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`).
- No type-aware linting (`eslint.config.mjs:328`, no
`parserOptions.project`), so this diff cannot move a verdict on an
untouched file.
- Served-object check on a fresh showcase backend (my own port, torn
down after):
- `GET /api/v1/meta/object/showcase_field_zoo` serves `imageField:
'f_image'` with `fields.f_image.type: 'image'`. That is the
`objectSchema` the reader reads.
- I uploaded a 1×1 PNG through `/api/v1/storage/upload/presigned` +
`complete` and set `f_image` to its id. The record then serves `f_image`
as `{ id, name, size, mimeType, url: '/api/v1/storage/files/…' }`, the
form `recordPictureUrl` takes first. That URL answers 200 `image/png`
after the redirect, with bytes identical to the upload.
- Field-consumer warning, an `os validate` A/B on the declaration.
Without it, `f_image` carries "declared but nothing in this stack reads
or displays it" (71 warnings). With it, that warning goes and nothing
else moves (70).
- No ablation or reverse verification applies: this PR authors no
behaviour. It moves a ledger row and text, and adds one declaration the
parse already accepts.
## Acceptance notes
- **Browser check: NOT MEASURED.** It needs a console build at the pin,
about 9 minutes of the shared box. The pin-bump smoke on PR #21800
already drew this picture on `showcase_field_zoo` / `f_image` at this
same pin. This PR makes that temporary declaration permanent, and the
served-object check above covers what the reader reads.
- `packages/spec/CHANGELOG.md` still says `imageField` "is accepted,
stored and served but nothing draws it". That entry is release-owned and
was true when it shipped, so it is not touched.
- The `omit` row's `why` text is stale from this PR's landing; see item
3 above.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a0176ef commit 07bf21f
9 files changed
Lines changed: 35 additions & 15 deletions
File tree
- .changeset
- content/docs
- data-modeling
- references
- api
- data
- system
- examples/app-showcase/src/data/objects
- packages/spec
- liveness
- state-counts
- src/data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
71 | | - | |
| 71 | + | |
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
960 | 960 | | |
961 | 961 | | |
962 | 962 | | |
963 | | - | |
| 963 | + | |
964 | 964 | | |
965 | 965 | | |
966 | 966 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
167 | | - | |
| 167 | + | |
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
341 | 341 | | |
342 | 342 | | |
343 | 343 | | |
344 | | - | |
| 344 | + | |
345 | 345 | | |
346 | 346 | | |
347 | 347 | | |
| |||
628 | 628 | | |
629 | 629 | | |
630 | 630 | | |
631 | | - | |
| 631 | + | |
632 | 632 | | |
633 | 633 | | |
634 | 634 | | |
| |||
Lines changed: 7 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
32 | 39 | | |
33 | 40 | | |
34 | 41 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
36 | | - | |
| 35 | + | |
| 36 | + | |
37 | 37 | | |
38 | | - | |
39 | | - | |
| 38 | + | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2253 | 2253 | | |
2254 | 2254 | | |
2255 | 2255 | | |
2256 | | - | |
2257 | | - | |
2258 | | - | |
2259 | | - | |
| 2256 | + | |
| 2257 | + | |
| 2258 | + | |
| 2259 | + | |
| 2260 | + | |
| 2261 | + | |
2260 | 2262 | | |
2261 | | - | |
| 2263 | + | |
2262 | 2264 | | |
2263 | 2265 | | |
2264 | 2266 | | |
| |||
0 commit comments