Repository navigation
spec(data): an object declares which image field is the record's picture (imageField, beside nameField), refused at publish unless it names an image or avatar field — ruling A on hotcrm#1199 #21182
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3area:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchand removed
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01UtnxvdiN376GF3sgXwAw4d
Account:os-sales(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21182-object-image-field
Worktree:objectstack-issue-21182
Domain:domain:spec
Seat:domain:spec#1(seat post #6017)
File surface:packages/spec/src/data/object.zod.ts(ObjectSchema.imageField, a sibling ofnameField, refused at authoring/publish unless it names a declaredimageoravatarfield of the same object, with a prescription naming the two types), itspackages/spec/liveness/object.jsonrow (planneduntil objectui's record chrome reads it), the regenerated spec artefacts, one sentence incontent/docs/data-modeling/objects.mdx, tests, and one.changeset/21182-*.md. ⛔ Nopage:headerprop, ⛔ no consumer-side tolerance; the objectui record-chrome reader is the Seam's other end, ⛔ not this PR. Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(default judgment tier; the contract review runs atCONTRACT_REVIEW_TIERthrough an isolated subagent)
Clause-②: yes
Thread-read: none
Serial constraints cleared: census of every open PR's file list at 2026-10-01T17:00Z onorigin/mainb42e0346: none touchespackages/spec/src/data/object.zod.ts,packages/spec/liveness/object.jsonorcontent/docs/data-modeling/objects.mdx. #20234 (seat 5) holdsliveness/field.jsonandliveness/README.md, different files. #20928 and #21095 (this seat, in flight) regenerate theuishards; this card's shards aredata. Area axis: #20928 isarea:recordsin flight, with a disjoint file surface.objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21182,
"status": "done",
"branch": "claude/issue-21182-object-image-field",
"pr": "#21221",
"session": "session_01UtnxvdiN376GF3sgXwAw4d",
"premise_still_valid": true,
"summary": "Added ObjectSchema.imageField (optional string, declared after nameField and its two deprecated title companions; the describe and TSDoc name the record chrome as its reader, say that reader is pending, and say an empty field shows no picture). The key is refused at parse when it names a field the object does not declare, or a field of any type except image/avatar. The refusal is one custom issue at ['imageField'] whose prescription names both accepted types. It is called from ObjectSchema's existing superRefine beside refuseForeignTreeReference, so defineStack/create(), os validate (ObjectStackDefinitionSchema) and the metadata save door (getMetadataTypeSchema('object')) all refuse; each door is pinned. Zone 2 #2 measured: NO door refuses nameField for naming an undeclared field (objectTitleCompleteness classifies it 'synthesized'; validateRecordTitle only warns; validate-object-field-refs leaves the scalar pointers to a separate decision), so there was no nameField door to reuse and the tree-reference door was the existing fit. The liveness row object.imageField is planned, with carrier objectstack-ai/objectui#11383 (the consumer card the director filed, found through #21182's cross-reference timeline). Also landed: a form-reconciliation omit row (declared-not-enforced group), one Display-table row in objects.mdx, regenerated authorable-surface/reference docs/state-counts, and the changeset (@objectstack/spec minor, Clause-② yes (widening)). Fixture triage: one metadata-protocol count pin moved object 43 → 44 on the field/picklist precedent.",
"tests": "Tree 06cb584 (HEAD after merging origin/main 862f12c). spec:pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2→ 'Test Files 595 passed (595) / Tests 17422 passed | 1 todo', exit 0 (run at 56d2707; the merge brought no packages/spec change). spec typecheck (tsc + scripts + test layer) exit 0 ('52 file(s) / 246 error(s) ... held'). metadata-protocol full suite: '1 failed | 2972 passed' → the count pin 'expected 44 to be 43' (object gained a declared key) → triaged → file rerun 18/18; typecheck exit 0. lint: '118 passed / 5489 passed', exit 0 (direction: the packages that parse ObjectSchema; objectql not run, because 0 fixtures repo-wide set imageField, control nameField hits). New pins src/data/object-image-field.test.ts: 10/10. ABLATION 1 (committed state, scripts/ablation-replace.mjs wrap mode): removing the refuseNonPictureImageField call gave 'Tests 7 failed | 3 passed' (all 7 refusals red, 3 controls green); restore 'blob == HEAD (1d7f36b646ba) and git diff HEAD is empty'. ABLATION 2 (type half only, includes(type) → return): '4 failed | 6 passed' (text, file, create(), save door red; the undeclared/prototype/stack existence pins green, as predicted); restore proven the same way. Spec tests import src, so no dist was involved. Gates: dispatch-gates --commands derived 110 at 06cb584; --ran reconciles '110 derived, 109 run, 1 NOT-MEASURED, 0 UNRUN'. CI at 06cb584 when the report was written: in_progress (13 checks completed with 0 failures, 19 in progress).",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 relay writes (each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #21221; body read back byte-identical, 8827 chars); (2) label-write --assign os-sales → POST /repos//issues/21221/assignees (read back matches; zero label writes, because the dispatch named no labels and skip-changeset does not apply); (3) this os-dev-report comment → POST /repos//issues/21182/comments. Plus git push to the branch (not REST).",
"gates": "110 derived / 109 run, all exit 0 on the recorded run / 1 NOT-MEASURED. Spec and generated: check:generated, check:liveness, check:empty-state, check:authorable-surface, check:api-surface (stayed green after rebuild: no api-surface move), check:docs, check:export-origins, check:exported-any, check:dual-source-exports, check:entry-nameability, check:browser-reachable-entries, check:objectui-pin-citations, check:llms-txt, check:skill-refs, check:strictness-ledger, check:variant-docs, check:yaml-examples, check:duration-unit-keys all 0. Changeset: check-changeset-no-major, check-adr-0087-registration, check-empty-changeset, check:changeset-gate-self-tests all 0. Docs: check:doc-authoring, check:docs-spec-enumerations, check:doc-anchors, check:docs-redirects, check:docs-single-h1, check-doc-frontmatter, check-section-landing-index all 0. check-spec-docblock-symbol-anchors 0; check:nul-bytes 0; check:cross-package-test-inputs 0; check:test-source-alias 0; check:type-check-coverage 0. Reruns: check:skill-examples first exit 3 (PREREQUISITE NOT MET, client-react unbuilt) → after building @objectstack/client-react... exit 0 (259/259); check:type-check-debt first killed by my own 280s runner timeout (exit 124) → full rerun exit 0 (26 raw, none above record). NOT MEASURED: check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET (reads every package dist; 32 unbuilt here; a whole-repo build does not fit the foreground cap); CI Lint & Repo Gates owns it. CI on PR head 06cb584: in_progress at report time.",
"line_budget": "n/a — the diff touches no skills/** and no line-ratcheted ledger. Diff vs merge base: 12 files, +317 / -4 (under the 5000-line human-merge threshold).",
"files_changed": [
".changeset/21182-object-image-field.md",
"content/docs/data-modeling/objects.mdx",
"content/docs/references/api/metadata.mdx",
"content/docs/references/data/object.mdx",
"content/docs/references/system/migration.mdx",
"packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts",
"packages/spec/authorable-surface/data.json",
"packages/spec/liveness/object.json",
"packages/spec/liveness/state-counts/object.md",
"packages/spec/src/data/object-image-field.test.ts",
"packages/spec/src/data/object.zod.ts",
"packages/spec/src/system/metadata-form-zod-reconciliation.test.ts"
],
"deviations": [
"Zone 2 #2 falsified in part: nameField has no 'names a declared field' door at any tier, so imageField's refusal went into ObjectSchema's existing superRefine, the door the tree-reference rule (refuseForeignTreeReference) already uses. No new door, and nothing in @objectstack/lint changed.",
"Two files outside the claim's declared file surface, both forced by the change itself (the dispatch's contract-face fixture triage): packages/spec/src/system/metadata-form-zod-reconciliation.test.ts (one omit row; without it the top-level zod-only leg goes red, measured) and packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts (object served-property count 43 → 44).",
"Zone 2 #3 shape: the README's required fields for a planned row are status (verifiedAt/evidenceScope/evidence optional; any evidence is scanned). No authorWarn/authorHint, on README rule 1 and the app.branding.logo planned precedent (display metadata, takes effect when the carrier lands).",
"Zone 2 #4: read objectui at the .objectui-sha pin 31971ff1, not the card's 8001068b9c; the finding is the same (title only, no image read). Consumer card found: objectstack-ai/objectui#11383.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Per AGENTS.md (which takes precedence) commits carry the model-free pair 'Claude-Session' + 'Co-authored-by: Claude', and the PR body ends with the session-URL footer.",
"The base merge (origin/main 862f12c) touched no os-regen path and no packages/spec file, so it was a plain git merge, not os-regen-merge.sh; no os-regen-pending marker was raised."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — nameField existence is refused at no door (an undeclared name is classified 'synthesized' and auto-provisioned). This is an observation, not a defect: the design is deliberate. PR Acceptance notes.",
"carrier: 承接者:无 · noted, not filed — an ObjectExtension can override the type of a field that the owning object's imageField names; the object door cannot see a later merge. No producer writes this shape (no reach). PR Acceptance notes.",
"carrier: whoever flips the row when objectstack-ai/objectui#11383 lands · noted, not filed — Studio's object form does not offer imageField (omit row, declared-not-enforced). Whether to offer it is a ruling for when the key is enforced."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT — PR #21221 @
06cb584a8adomain:specseat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim5936366763· 2026-10-01T18:33Z- Shape (read on GitHub): draft against
main; first lineFixes #21182;Clause-②: yesat a line start; the card'sSeam:line in the body; PR assigneeos-sales. No other card number sits beside a closing keyword. - Scope: 12 files. Spec:
object.zod.ts, a new pin file, the liveness row and state counts, and the regeneratedauthorable-surface/ reference pages. Docs: oneobjects.mdxrow. One changeset. Two forced pin moves are declared: the form-reconciliationomitrow, and the metadata-protocol/meta/typescount forobject, 43 → 44. - Contract review (at tier, isolated): PASS
5937964141on this head. It re-measured the door premise: no door onmainrefusesnameFieldfor an undeclared name, so the refusal ridesObjectSchema's existingsuperRefineand reachesdefineStack/os validate/ the save door, each pinned. It also re-read the objectui pin for theplannedgrade. - Prose checked sentence by sentence:
- The changeset's door list (
defineStack,ObjectSchema.create(),os validate, the save door's422 INVALID_METADATA) matches the pins and the review. - "No renderer reads the key yet" / "nothing draws it" matches the liveness grade
plannedand objectuicontainers.tsxat the pin (0imageFieldreads). - "Nothing that parsed before is refused" holds: the key is new and
ObjectExtensionSchemadoes not take it. - The
objects.mdxrow and the.describe()say the same and keep the claim as narrow as the enforcement.
- The changeset's door list (
- Gates on this head: 35 check-runs, 33
successand 2 skipped.check-expected-skips: OK.check-governed-merges --pr 21221: NOT governed, 321 changed lines.mergeable_state: clean. - Tests and ablations (report, head
06cb584a): spec 595/595 files; lint 118/118; metadata-protocol green after the count pin; the new pins 10/10. Ablation of the refusal call went 7 red / 3 green; ablation of the type half went 4 red / 6 green, as predicted. - Out-of-scope, one line each:
nameFieldexistence refused at no door: noted. The deferral is written intovalidate-object-field-refs.ts's header as a separate decision.- An
ObjectExtensioncan redefine the field an object-level pointer names after the object door passed: noted, not filed. This is generic to every object-level field pointer and pre-dates this PR, and with no reader ofimageFieldit has no measuredreach:. - Studio does not offer
imageFielduntil it is enforced: theomitrow records it. - Review note:
schema-design.mdx's second object-property table gained no row. The card asked for one sentence andobjects.mdxcarries it, so this is not a delivery gap.
- Serial note: PR feat(spec,client)!: ActionSchema gains outcomeMessages (success copy per handler outcome, ${result.*}), and environments.delete stops guaranteeing message #21214 also moves a count in
protocol.meta-types-degenerate-derivation.test.ts(action, a different line). Whichever lands second is re-checked for the merge. - Landing: this PR is readied and enters the merge queue now.
- Shape (read on GitHub): draft against
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded: PR #21221 →
9969228626domain:specseat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim5936366763· 2026-10-01T18:56Z- Landed: PR feat(spec): ObjectSchema.imageField — an object declares its record picture, refused unless it names an image or avatar field #21221 went through the merge queue as
9969228626, with one parent (read withgit rev-list --parents).object.zod.ts,liveness/object.json, the new pin file, the changeset andobjects.mdxare blob-equal to the reviewed head06cb584a8a. - Card: closed
completedby the PR'sFixesline, and no other issue closed in the window. This act removespm:dispatchedand the assignee. - The Seam's other end: record chrome draws the record's picture from the object's
imageFieldin the record page header — the renderer half of objectstack#21182 (ruling A on hotcrm#1199) objectui#11383 (the record chrome reader) is the consumer. Theobject.imageFieldliveness row staysplanneduntil it lands, and flips in that landing. hotcrm#1199'scrm_account.logowaits on that reader, as the director's card recorded.
- Landed: PR feat(spec): ObjectSchema.imageField — an object declares its record picture, refused unless it names an image or avatar field #21221 went through the merge queue as
Filed by the director seat, summon #32,
session_016tKoy8NJa35Yih1FdzrVmn, as the platform half of the maintainer's ruling A on objectstack-ai/hotcrm#1199. The ruling is batch #261 item 3, maintainer 「其他四张同意」; the record is theRuling:comment on hotcrm#1199. ⛔ Not a claim.Seam:
spec:ObjectSchema.imageField→renderer:objectui record chrome (page:header, containers.tsx)What was ruled
An object declares which of its fields is the record's picture, and the record page header draws it. Two refinements of that:
page:headerprop.page:header'siconwas retired (objectui#3829 ruling 「全部接受」, route c) with the text "the header's own identity is drawn by the record chrome", so per-page header props are the wrong home.The first reader is hotcrm's
crm_account.logoin the account detail header. The maintainer's 2026-09-18 ruling 5731201698 (batch #163, 「163 同意」) asked for it by name.Readings (the refs as named)
maind34aa58a2a:PageHeaderProps(packages/spec/src/ui/component.zod.ts:508) declarestitleandsubtitle, plus retired keys; it has no image key.object.zod.ts,page.zod.tsandview.zod.tsdeclare no object-level image / avatar / logo key. The only image keys are the gallerycoverField(view.zod.ts:1429) and kanban'scoverImageField.nameField(ADR-0079) is atobject.zod.ts:2158.main8001068b9c: the record chrome inpackages/components/src/renderers/layout/containers.tsx(:2157–:2194) draws the title only and reads no image.mainfb408a7304:crm_account.logois already on the account form (src/sales/views/account.view.ts:228) and the cards cover (:106). Addingimagetopage:headerprops is refused by lint/validate withcomponent-props-unknown-key(measured on hotcrm#1199, dev report 5832835389).Scope
ObjectSchema.imageField: optional, a sibling ofnameField, naming a field of the same object whose type isimageoravatar(field.zod.ts:79). The describe and TSDoc state what draws it.planneduntil objectui's record chrome reads the key. That is the Seam's acceptance.gen:schema,gen:docs,gen:api-surface), and a sentence in the object docs.minor,Clause-②: yes (widening).Lane
domain:spec, level S–M,mode:subagent; the contract review runs atCONTRACT_REVIEW_TIER. The consumer half is filed in objectui,Blocked-by:this card plus an installable@objectstack/specrelease. hotcrm#1199 waits on that objectui card.Dedupe
mcp__github__search_issues, open and closed:private-OWD cross-owner row, so #5493's by-id widener deferral is inert on the posture it was filed for — and its unit test cannot see it (fake engine bypasses middleware) #7281), both closed and unrelated: title masking and width arbitration.Dedupe words:
object imageField·record header image·record chrome avatar·primary image fieldGenerated by Claude Code