Repository navigation
spec(liveness): object.imageField is live — the record chrome draws it at the objectui pin - #21824
Conversation
… the objectui pin The liveness row flips planned -> live, citing objectui's PageHeaderRenderer and recordPictureUrl at the .objectui-sha pin 9dfaca654311. The describe and TSDoc drop the pending-renderer sentence, objects.mdx moves with them, and the showcase field zoo declares imageField: 'f_image'. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…ject.imageField Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e6295c56b09850efb6b13fb566c8fb2ae9ce14d1 && git checkout e6295c56b09850efb6b13fb566c8fb2ae9ce14d1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dd58618c2838c01f343a6f859fd4bb28531097d2 80a7677773fbae19389adb607fb970e9b0f23b46 && git checkout -B drift-repro dd58618c2838c01f343a6f859fd4bb28531097d2 && git merge --no-ff 80a7677773fbae19389adb607fb970e9b0f23b46
node scripts/docs-audit/affected-docs.mjs --json dd58618c2838c01f343a6f859fd4bb28531097d2
|
…meField (objectstack-ai#21854) Fixes objectstack-ai#21765 Clause-②: no Item 3 of the card, under the director seat's ruling A (`5989738766`): Studio's object form now offers `imageField`, the record's picture, as a plain `text` row beside `nameField`. Items 1 and 2 landed in PR objectstack-ai#21824. With this PR, the card's last open item is done. ## What changed - **The form row** (`packages/spec/src/data/object.form.ts`): one `{ field: 'imageField', type: 'text', colSpan: 1, helpText }` row directly after `nameField`. It has the same face as `nameField`, for the same reason: the value names one of the object's own fields, and the registry has no own-field picker. The row adds no picker and no validator. `refuseNonPictureImageField` at parse stays the one judge, and its refusal at save is what an author sees. The helpText says what the parse accepts: a field of this object whose type is `image` or `avatar`. Left empty, there is no record picture and no placeholder. The metadata form's text input writes no key when cleared (objectui `SchemaForm.tsx` at the pin `0abd4f9f8769`: `onChange(e.target.value || undefined)`), so "empty" in the helpText means the key is unset, which the parse reads as no picture. - **The stale ledger row** (`metadata-form-zod-reconciliation.test.ts`): the `imageField` `omit` row in the declared-not-enforced group is deleted. No other row moves. - **The catalogs**: the four `packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts` files were regenerated with `node scripts/check-i18n-bundles.mjs --write`. The zh-CN / ja-JP / es-ES `label` and `helpText` leaves were then written in place, since translated-locale values are hand-written in those files (AGENTS.md, Documentation Guardrails). A second `--write` kept them and dropped the six provenance entries the first extract had added to the three `*.source-hashes.generated.ts` companions. Those companions are byte-unchanged in this diff. - **The pin** (`metadata-form-declared-rows.pin.test.ts`): three tests. A lit and dark control for the locator. The key is offered once, as a `text` row with no widget. It sits directly after `nameField`, in the same section, at the same `colSpan`. The reconciliation test proves only that the key is offered somewhere on the form; it reads neither the control nor the position, so this pin covers what it misses. No test title or string carries a tracker id. - **Two measured counts in `platform-objects` were re-taken**, both forced by the new row: - the object form's open-section leaf population: 114 → 116 (`object-collapsed-sections-echo-decisions.test.ts`); - the catalog-wide translated-label control: 659 → 660 per locale (`object-lifecycle-panel-echo-decisions.test.ts`). The `openEchoes` reading stays at zero. - **Changeset**: `@objectstack/spec` patch and `@objectstack/platform-objects` patch (below). ⛔ No Zod change, no liveness change, no new gate or ledger class. ## The reconciliation test: predicted, then measured Predicted before writing: red without the form row, green with it. Measured on the committed state, with `scripts/ablation-replace.mjs` in wrap mode and a `trap` restore proven by blob equal to HEAD: | state | reading | |---|---| | row deleted, form row present (this PR, `c730a8c598`) | 2 files, **87 passed (87)** | | row deleted, form row removed (anchor 1 → 0, blob `d867e358e5e5` → `a67cdaed2b22`) | **3 failed / 84 passed**: `object.(root): accepted by the Zod but unauthorable in the form … expected [ 'imageField' ] to deeply equal []`, plus the two new pin tests. Restored: blob `d867e358e5e5` == HEAD, `git diff HEAD` empty | | control: the stale row planted back beside the form row | **1 failed / 75 passed**: `object.(root).imageField: the form offers it now — drop the ledger entry`. Restored: blob `2a7c49dca958` == HEAD | The first attempt at the control leg was a no-op. The tool refused it before running anything, because the replacement text contained its own anchor. It was redone with a non-overlapping replacement, which is the reading above. ## What ships Each package was built and its `dist/` grepped, with a positive control: - `@objectstack/spec`: the new helpText is in 8 dist files; the control, `nameField`'s helpText, is in 8. - `@objectstack/platform-objects`: the new en helpText is in 6 dist files, the control in 6. The zh-CN, ja-JP and es-ES labels are in 6 each, matched on literal or unicode-escaped text. Both packages publish the change, so both get `patch`. `skip-changeset` does not apply. ## Verification (final head `9f6b177875`) - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: Test Files 614 passed | 1 skipped (615), Tests 18385 passed | 1 skipped | 1 todo. The skipped file is `scripts/root-entry-type-nameability.pin.test.ts`, which is gated by `OS_EXPECT_ROOT_NAMEABILITY`. - `pnpm --filter @objectstack/spec run typecheck`: exit 0. `tsc -p tsconfig.test.json --listFilesOnly` compiles `object.form.ts` and both changed spec tests. - `pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2`: 59 files, **949 passed**. Run before the two counts were re-taken, it gave exactly two failures: `expected 116 to be 114` and `zh-CN positive control: expected 660 to be 659`. Typecheck: exit 0. - `metadata-forms-vocabulary.test.ts`: 5 passed. `pnpm check:i18n`: exit 0, `platform-objects in sync (11 bundle(s))`. `pnpm check:i18n-coverage`: exit 0, `13 config(s), 621 baselined untranslated string(s), none new`. - `pnpm --filter @objectstack/spec run check:generated`: exit 0 (15 artifacts). - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 87 commands from 10 paths against the merge base `8832655af`. All 87 were run on `9f6b177875`. `--ran` reconciliation: `87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED`. Four spec gates first exited 3 (`PREREQUISITE NOT MET`: the dist predated a test-title edit). They exited 0 after `pnpm --filter @objectstack/spec build`, and the record keeps that last reading. - eslint `--no-inline-config --format json` on the 9 changed TS files: 9 files, 0 errors, 0 warnings. The population is `eslint.config.mjs:971` (`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`). Linting is not type-aware (no `parserOptions.project`), so untouched files' verdicts cannot move. ## Acceptance notes - **Surface, stated.** The claim named `object.form.ts`, the reconciliation test, the four catalogs and a changeset. Three more files changed: - `metadata-form-declared-rows.pin.test.ts` holds the pin the dispatch asked for (item 4). - The two `platform-objects` echo-decision tests carry the counts the row moves. Those count lines are the ones PR objectstack-ai#19673 moved when it added rows to the same section. A reviewer who reads them as outside the surface can say so. - No `platform-objects` source or object file moved, and no other catalog key changed. - The authored leaves live in the generated catalogs themselves, so no further file joins the surface for them. - `origin/main` gained one commit after the branch point (`2799155678`, test titles in `packages/spec/src/data/`). It touches none of this PR's paths. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #21765
Clause-②: no
Seam:
spec:ObjectSchema.imageField→renderer:objectui record chrome (page:header, containers.tsx)Why
Part ofand not a closing line. The card's item 3 stays open: whether Studio's object form offersimageField. Itsomitrow 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-shaonmainis9dfaca654311.git merge-base --is-ancestor c096f032793d 9dfaca654311exits 0 in the objectui clone, so the pin carries objectui PR 11619's merge. In objectui at9dfaca654311,packages/components/src/renderers/layout/containers.tsx::1474PageHeaderRenderer. On its record-context branch (objectSchemaanddatafrom the record context),:2392readsobjSchema?.imageField.:1458recordPictureUrl(value)resolves the served row's value of that field. It takes the expanded{ url }form, a baresys_fileid (served from/api/v1/storage/files/), a legacy URL string, or the first entry of amultiplelist that resolves.:2449icon={recordPicture}puts the picture in the record chip'siconslot beside the H1. Anavatarfield is drawn round and cropped, animagefield whole in a rounded square. An empty value draws nothing: no initials, no placeholder.What changed
packages/spec/liveness/object.jsonimageField:planned→live,verifiedAt2026-10-05,evidenceScope: cross-repo. The evidence has three parts: the authoring judgement (object.zod.ts#refuseNonPictureImageField), plusobjectui @9dfaca654311containers.tsx#PageHeaderRendererand#recordPictureUrlwith 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.packages/spec/src/data/object.zod.ts:.describe()drops "Pending renderer: the record chrome does not draw it yet." Its "is to draw" becomes "draws".PageHeaderRenderer, the record chip's icon slot). No shape, refusal or error text moves.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.pnpm --filter @objectstack/spec check:generatednamed two stale artifacts,check:docsandcheck:liveness.--fixregenerated exactly those two:content/docs/references/{api/metadata,data/object,system/migration}.mdx;liveness/state-counts/object.md, whereobjectmoves to live 51 / planned 1.examples/app-showcase/src/data/objects/field-zoo.object.ts:imageField: 'f_image', on the existingField.image(). This is the object and field the pin-bump smoke drew with a temporary edit.imagevalue is a managedsys_fileid. The seed's own note onshowcase_task.cover(src/data/seed/index.ts, ADR-0104) says why a seed cannot honestly mint one..changeset/21765-object-image-field-live.md:@objectstack/specpatch,Clause-②: no. It is text only.Item 3: Studio's object-form
omitrow (measured, not decided)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 ispackages/spec/src/data/object.form.ts.: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."scripts/ablation-replace.mjsin wrap mode:26f47a279c33→bb160bb1b747.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 []".26f47a279c33) andgit diff HEADempty. Baseline before the probe: 76/76.#19331made for 45 keys. It also takes the fourmetadata-forms.generated.tscatalogs inpackages/platform-objects, with authoredzh-CN/ja-JP/es-ESleaves. Those files are outside this claim's file surface.planned" while the ledger sayslive. 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 --noEmitclean, 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.objectreads 52 classified (live 51, planned 1); the planned one isexternalSharingModel. The repo total reads 1000 live · 1 experimental · 1 live-elsewhere · 108 dead · 8 planned = 1118. The repo-local evidence path and the#refuseNonPictureImageFieldanchor resolve. Theobjectui:paths are attributed and counted, never resolved: that is the gate's boundary.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 107 commands from 9 paths against merge base75ddcd1b4.--ranreconciles 107 derived, 107 run, 0 NOT-MEASURED, 0 UNRUN, and every one exited 0.eslint --no-inline-config --format jsonon the two changed TS files: 2 files, 0 errors, 0 warnings.eslint.config.mjs:971(**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}).eslint.config.mjs:328, noparserOptions.project), so this diff cannot move a verdict on an untouched file.GET /api/v1/meta/object/showcase_field_zooservesimageField: 'f_image'withfields.f_image.type: 'image'. That is theobjectSchemathe reader reads./api/v1/storage/upload/presigned+completeand setf_imageto its id. The record then servesf_imageas{ id, name, size, mimeType, url: '/api/v1/storage/files/…' }, the formrecordPictureUrltakes first. That URL answers 200image/pngafter the redirect, with bytes identical to the upload.os validateA/B on the declaration. Without it,f_imagecarries "declared but nothing in this stack reads or displays it" (71 warnings). With it, that warning goes and nothing else moves (70).Acceptance notes
showcase_field_zoo/f_imageat this same pin. This PR makes that temporary declaration permanent, and the served-object check above covers what the reader reads.packages/spec/CHANGELOG.mdstill saysimageField"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.omitrow'swhytext is stale from this PR's landing; see item 3 above.Generated by Claude Code