Skip to content

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

@objectstack-fleet

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 the Ruling: 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:

  • It is one object-level declaration that every detail page reads, ⛔ not a page:header prop. page:header's icon was 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.
  • No initials/placeholder avatar when the field is empty (zero pull, ruled out).

The first reader is hotcrm's crm_account.logo in 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)

  • spec main d34aa58a2a:
    • PageHeaderProps (packages/spec/src/ui/component.zod.ts:508) declares title and subtitle, plus retired keys; it has no image key.
    • object.zod.ts, page.zod.ts and view.zod.ts declare no object-level image / avatar / logo key. The only image keys are the gallery coverField (view.zod.ts:1429) and kanban's coverImageField.
    • nameField (ADR-0079) is at object.zod.ts:2158.
  • objectui main 8001068b9c: the record chrome in packages/components/src/renderers/layout/containers.tsx (:2157–:2194) draws the title only and reads no image.
  • hotcrm main fb408a7304: crm_account.logo is already on the account form (src/sales/views/account.view.ts:228) and the cards cover (:106). Adding image to page:header props is refused by lint/validate with component-props-unknown-key (measured on hotcrm#1199, dev report 5832835389).
  • Mainstream modelling: Dynamics 365 tables carry a primary image column drawn in the form header; HubSpot company records show the company logo in the record header.

Scope

  1. ObjectSchema.imageField: optional, a sibling of nameField, naming a field of the same object whose type is image or avatar (field.zod.ts:79). The describe and TSDoc state what draws it.
  2. Refused loudly at authoring/publish when it names a field the object does not declare, or a field of any other type, with a prescription naming the two accepted types. ⛔ No consumer-side tolerance (Prime Directive Add comprehensive test suite for Zod schema validation #12).
  3. A liveness-ledger row that stays planned until objectui's record chrome reads the key. That is the Seam's acceptance.
  4. Generated artifacts (gen:schema, gen:docs, gen:api-surface), and a sentence in the object docs.
  5. Changeset minor, Clause-②: yes (widening).

Lane

domain:spec, level S–M, mode:subagent; the contract review runs at CONTRACT_REVIEW_TIER. The consumer half is filed in objectui, Blocked-by: this card plus an installable @objectstack/spec release. hotcrm#1199 waits on that objectui card.

Dedupe

mcp__github__search_issues, open and closed:

Dedupe words: object imageField · record header image · record chrome avatar · primary image field


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01UtnxvdiN376GF3sgXwAw4d
    Account: os-sales (the seat's linked user as GET /user answers 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 of nameField, refused at authoring/publish unless it names a declared image or avatar field of the same object, with a prescription naming the two types), its packages/spec/liveness/object.json row (planned until objectui's record chrome reads it), the regenerated spec artefacts, one sentence in content/docs/data-modeling/objects.mdx, tests, and one .changeset/21182-*.md. ⛔ No page:header prop, ⛔ 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 at CONTRACT_REVIEW_TIER through an isolated subagent)
    Clause-②: yes
    Thread-read: none
    Serial constraints cleared: census of every open PR's file list at 2026-10-01T17:00Z on origin/main b42e0346: none touches packages/spec/src/data/object.zod.ts, packages/spec/liveness/object.json or content/docs/data-modeling/objects.mdx. #20234 (seat 5) holds liveness/field.json and liveness/README.md, different files. #20928 and #21095 (this seat, in flight) regenerate the ui shards; this card's shards are data. Area axis: #20928 is area:records in flight, with a disjoint file surface.

  2. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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

  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21221 @ 06cb584a8a

    domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim 5936366763 · 2026-10-01T18:33Z

    • Shape (read on GitHub): draft against main; first line Fixes #21182; Clause-②: yes at a line start; the card's Seam: line in the body; PR assignee os-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 regenerated authorable-surface / reference pages. Docs: one objects.mdx row. One changeset. Two forced pin moves are declared: the form-reconciliation omit row, and the metadata-protocol /meta/types count for object, 43 → 44.
    • Contract review (at tier, isolated): PASS 5937964141 on this head. It re-measured the door premise: no door on main refuses nameField for an undeclared name, so the refusal rides ObjectSchema's existing superRefine and reaches defineStack / os validate / the save door, each pinned. It also re-read the objectui pin for the planned grade.
    • Prose checked sentence by sentence:
      • The changeset's door list (defineStack, ObjectSchema.create(), os validate, the save door's 422 INVALID_METADATA) matches the pins and the review.
      • "No renderer reads the key yet" / "nothing draws it" matches the liveness grade planned and objectui containers.tsx at the pin (0 imageField reads).
      • "Nothing that parsed before is refused" holds: the key is new and ObjectExtensionSchema does not take it.
      • The objects.mdx row and the .describe() say the same and keep the claim as narrow as the enforcement.
    • Gates on this head: 35 check-runs, 33 success and 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:
      1. nameField existence refused at no door: noted. The deferral is written into validate-object-field-refs.ts's header as a separate decision.
      2. An ObjectExtension can 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 of imageField it has no measured reach:.
      3. Studio does not offer imageField until it is enforced: the omit row records it.
      4. Review note: schema-design.mdx's second object-property table gained no row. The card asked for one sentence and objects.mdx carries 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.
  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21221 → 9969228626

    domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim 5936366763 · 2026-10-01T18:56Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions