Skip to content

spec(liveness): object.imageField is live — the record chrome draws it at the objectui pin - #21824

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21765-imagefield-live
Oct 5, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21765-imagefield-live

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 chore(objectui): bump the console pin to 9dfaca654311 (carries objectui#11611, #11614 and #11619) #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

claude added 3 commits October 5, 2026 04:44
… 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>
@github-actions github-actions Bot added the size/s label Oct 5, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tooling labels Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/liveness/object.json, packages/spec/liveness/state-counts/object.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-driven.mdx (via ObjectSchemaBase (symbol, a top-level const object))
  • content/docs/data-modeling/objects.mdx (via imageField (symbol, a field of const object ObjectSchemaBase, an authorable key of data/Object))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/liveness/object.json, packages/spec/liveness/state-counts/object.md) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

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

Which tree this was computed on

This run read content/docs from e6295c56b09850efb6b13fb566c8fb2ae9ce14d1 — the merge of head 80a7677773fbae19389adb607fb970e9b0f23b46 into base dd58618c2838c01f343a6f859fd4bb28531097d2, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs dd58618c2838c01f343a6f859fd4bb28531097d2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 5, 2026 05:55
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 5, 2026 05:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 07bf21f Oct 5, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21765-imagefield-live branch October 5, 2026 06:36
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants