Repository navigation
feat(spec): ObjectSchema.imageField — an object declares its record picture, refused unless it names an image or avatar field - #21221
Conversation
… image/avatar field Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…ngeset Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…ss counts for imageField Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…t declares imageField Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d 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 — 137 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 d4099f859ca4ec1d1e3646ee333906d868b0a17a && git checkout d4099f859ca4ec1d1e3646ee333906d868b0a17a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ce4e205e2b6abd7a49f98a27cd8297a1e52f828b 06cb584a8a9c279870b50dfa7a8b2ba56e1fa3a6 && git checkout -B drift-repro ce4e205e2b6abd7a49f98a27cd8297a1e52f828b && git merge --no-ff 06cb584a8a9c279870b50dfa7a8b2ba56e1fa3a6
node scripts/docs-audit/affected-docs.mjs --json ce4e205e2b6abd7a49f98a27cd8297a1e52f828b
|
Contract reviewServed-tier: Inputs: card #21182 (body and both comments, the claim and the ① Derived judgmentsAccept-set changes the diff implies, each judged against
Public-surface changes beyond the schema: the exported types ② Semver level
③ Boundary flags
Note for the dispatching seat, prose face, not a contract face: Check-runs on the head (read 2026-10-01T18:32Z, after waiting for the last to conclude): 35 check-runs, 33 Implemented-by: VERDICT: PASS |
Fixes #21182
Clause-②: yes
Seam:
spec:ObjectSchema.imageField→renderer:objectui record chrome (page:header, containers.tsx)The platform half of ruling A on objectstack-ai/hotcrm#1199 (batch 261 item 3, 「其他四张同意」): an object declares which of its fields is the record's picture, and the record page header draws it. The liveness row is
planned: no renderer reads the key yet. Its reader is objectstack-ai/objectui#11383 (the record chrome draws the record's picture fromimageField), which is the seam's acceptance. This PR does not touch the renderer.What changed
ObjectSchema.imageField(packages/spec/src/data/object.zod.ts): optionalstring, declared afternameFieldand its two deprecated title companions. One object-level declaration, not apage:headerprop. The describe and TSDoc say which reader draws it, say that reader is not live yet, and say an empty field shows no picture (no initials or placeholder).refuseNonPictureImageField, is called fromObjectSchema's existingsuperRefinebesiderefuseForeignTreeReference. A name the object does not declare, or a declared field of any type exceptimage/avatar, is ONEcustomissue at['imageField']. Its prescription names both accepted types (Field.image()/Field.avatar()). Nothing on the consumer side tolerates a bad pointer.object.imageField:planned,evidenceScope: cross-repo, evidence anchored to the refusal and to the objectui record chrome at the pin. The note names the carrier record chrome draws the record's picture from the object'simageFieldin the record page header — the renderer half of objectstack#21182 (ruling A on hotcrm#1199) objectui#11383. It is notauthorWarn'd, on theapp.branding.logoprecedent and README rule 1: this is display metadata, and an authored value takes effect when the reader lands without being authored again.omitrow forobject.(root).imageFieldin the "declared, not enforced yet — no offer until it is enforced" group, besideexternalSharingModel. Without it the top-level zod-only leg goes red. This PR does not decide whether Studio should offer the key: that is a ruling for when the key is enforced.content/docs/data-modeling/objects.mdx→ Display. It namesimageField, its two accepted types, and says the refusal happens at validate/publish. It also says the record page header "is to draw" the picture once the renderer reads it, and that no renderer draws it yet. Regenerated:authorable-surface/data.json,content/docs/references/**(3 pages),liveness/state-counts/object.md.api-surfacedid not move:check:api-surfacestayed green after rebuilding..changeset/21182-object-image-field.md:@objectstack/specminor,Clause-②: yes (widening).@objectstack/metadata-protocol:protocol.meta-types-degenerate-derivation.test.tspins the number of top-level properties/meta/typesserves forobject. That count goes 43 → 44 because one key was declared, not because the derivation changed. This follows thefield74 → 75 (picklist) precedent already written in that comment.Doors measured: where
nameFieldis judged, and whyimageFieldis not judged thereThe dispatch asked for
imageField's check to go wherevernameField's existence check lives. Measured atorigin/maine18fea6d: no door refusesnameFieldfor naming an undeclared field.objectTitleCompletenessclassifies that case assynthesized, because the registry auto-provisions the primary.validateRecordTitleonly warns, and only onnone.validate-object-field-refs.tsexplicitly leaves the scalar pointers (nameField/stageField) to a separate decision. So there was nonameFielddoor to reuse.The existing door that matches is the one
refuseForeignTreeReferenceuses:ObjectSchema's ownsuperRefine. Only the object holds the field map the pointer resolves against, and every authoring door parses through it. Each door is pinned inobject-image-field.test.ts:ObjectSchema.safeParse/.create()customat['imageField'];create()throwsdefineStack/os validate(ObjectStackDefinitionSchema)customat['objects', 0, 'imageField']saveMetaItem→getMetadataTypeSchema('object'))objectto this schema; refused,customat['imageField']No new door was added and nothing in
@objectstack/lintchanged.Tests (tree
06cb584a, HEAD after mergingorigin/main862f12c0)pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 595 files, 17422 passed | 1 todo, exit 0. Run at56d27077; the merge brought nopackages/specchange.pnpm --filter @objectstack/spec run typecheck(tsc + scripts + test-layer): exit 0. The new test file compiles undertsconfig.test.jsonand adds no debt.@objectstack/metadata-protocol: full suite, 1 failed / 2972 passed before the triage, the count pin above. After the triage the file reran at 18/18.typecheckexit 0.@objectstack/lint: 118 files, 5489 passed, exit 0. The door is not in lint; I ran it because lint parsesObjectSchemafixtures.@objectstack/objectql: not run. No fixture anywhere setsimageField: 0 hits repo-wide before this change, withnameFieldhitting as the control. The refusal only fires when the key is set, so nothing in objectql can move.src/data/object-image-field.test.ts, 10 tests. Controls:imageaccepted,avataraccepted, key absent accepted. Refusals: undeclared,text,file, a prototype name (toString),create(), the save-door schema, and the stack door. Every refusal asserts issuecode+path, plus the named subjects (the field name and both accepted types).Ablation (committed state,
scripts/ablation-replace.mjs, wrap mode, restore proven)Spec's own tests import
src/, so nodistwas involved.refuseNonPictureImageField(...)call from thesuperRefine: 7 failed | 3 passed. All seven refusal pins went red and the three controls stayed green. Restore: blob1d7f36b646ba== HEAD,git diff HEADempty.includes(type)→return): 4 failed | 6 passed. The red ones weretext,file,create()(atextpointer) and save-door (atextpointer). The undeclared, prototype-name and stack-door pins stayed green, which is correct because they are existence refusals. Restore proven the same way.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 110 commands at06cb584a.--ranreconciles 110 derived, 109 run, 1 NOT-MEASURED, 0 UNRUN. Every run gate exited 0, includingcheck:generated,check:liveness,check:empty-state,check:authorable-surface,check:api-surface,check:docs,check:docs-spec-enumerations,check:doc-authoring,check:spec-docblock-symbol-anchors,check:changeset-no-major,check:adr-0087-registrationandcheck:nul-bytes.check:skill-examplesfirst answered exit 3 (PREREQUISITE NOT MET:client-reactwas unbuilt). After building@objectstack/client-react...it answered 259/259, exit 0.check:type-check-debtwas first killed by my own 280 s per-gate timeout. A full rerun gave exit 0: 26 raw errors, none above the record.check:dual-build-cjs-loads. Exit 3, PREREQUISITE NOT MET: it reads every package'sdist, 32 are unbuilt in this worktree, and a whole-repo build does not fit the foreground cap. CI'sLint & Repo Gatesruns it.Acceptance notes
nameFieldexistence is refused nowhere. This is an observation, not a defect report: the auto-provision design makessynthesizeddeliberate. It is recorded here because it is whyimageFielddid not inherit a "nameField door".ObjectExtensioncan override the type of a field that the owning object'simageFieldnames.ObjectExtensionSchemacannot declareimageField(its strict shape refuses the key), but itsfieldsmay override an existing field's type. The merge happens after the object door, so the object parse cannot see such an override. No producer writes that shape today. Recorded here, not filed (no reach)..objectui-shapin31971ff1, not at the card's8001068b9c. The finding is the same: the title comes fromnameFieldand no image key is read. objectui's ownimageFieldhits at the pin are the gallery view's legacy option (galleryViewOptions), a different key on a different schema.Generated by Claude Code