Skip to content

Object fields: repeater / composite cannot declare typed sub-fields, so they are untyped JSON in forms and flows #25681

Closed

Description

@objectstack-fleet

Ruled: 6386744473 · batch #12 item 4 · letter b · 2026-10-24T16:30:45Z (object-level repeater / composite stay untyped JSON; docs + FieldType description + lint; a typed list is a child object; no sub-field member)

Category: ② maintainer decision / feature gap.

Need

repeater (and composite) are listed in FieldType as "embedded sub-object array with declared sub-fields", but an object field has no member to declare those sub-fields: FieldSchema refuses fields / subFields, and objectui's structured cell says so explicitly ("the spec's FieldSchema has no sub-field member for either type"). So a repeater on an object is an untyped JSON array: the form maps it to field:grid with no columns, sub-values get humanized keys instead of labels, there are no per-column types, options, required flags or translations, and nothing can validate or address one row.

Who benefits / scenario: business apps that want a short typed list on a record without a separate object. hotsrm (objectstack-ai/hotsrm#38, PR objectstack-ai/hotsrm#46) designed a supplier audit with checklist (item, score, note) and findings (finding, severity, requirement, linked SCAR) repeaters, and plans an 8D steps repeater (date, owner, attachment per D1–D8) for the supplier portal in v0.6. Without declared sub-fields none of these is usable in the form, and a flow cannot write back to one row (e.g. link the SCAR raised from a finding).

What hotsrm does regardless

Findings are a child object (srm_supplier_audit_finding, master_detail) and per-item checklist scores stay in an attached scoresheet; that stays even if repeaters gain sub-fields, because findings need their own record identity for SCAR links and counts. The 8D steps repeater is the remaining consumer that would use the feature.

Decision asked

Either (a) add a declared sub-field member (label, type, options, required, i18n keys) to repeater / composite object fields, rendered by the form grid (not chosen, ruling 6386744473); or (b) state in the FieldType description and docs that object-level repeaters are untyped JSON and that a typed list should be a child object, and refuse or lint a repeater that expects columns. ← ruled b.

Who acts: maintainer ruling, then the spec seat (packages/spec data/field, docs, one lint rule); objectui form grid: no work.

Dedupe: repo:objectstack-ai/objectstack repeater in:title,body → 0 hits (open + closed); repo:objectstack-ai/objectstack subFields composite → 0 hits.

Session: session_01RSBzVuyvrxknGC9yW432s4

Activity

  1. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first touch — enhancement · priority:p2 · domain:spec · area:records · needs-user-decision · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-24T15:58Z

    ⛔ Not a claim, ⛔ not a dispatch.

    • For the maintainer: (a) declared sub-fields on repeater / composite object fields, which is a protocol addition, or (b) say plainly that object-level repeaters are untyped JSON, so a typed list is a child object, and lint a repeater that expects columns.
    • Triage leaning: (b) now. hotsrm already models findings as a child object, and they need record identity for SCAR links. The one remaining consumer, the 8D steps list, is planned for hotsrm v0.6. (a) adds a permanent protocol surface plus objectui grid work (M) for that one future consumer. Revisit when v0.6 is scheduled. If (a) is chosen, it is graded p1 under 「协议新增 p1」.

    Generated by Claude Code

  2. os-elon-musk commented on Oct 24, 2026

    @os-elon-musk
    Collaborator

    Ruling: batch #12 item 4 · letter b · maintainer 「25681 b」 2026-10-24T16:30:45Z

    b = 不给对象字段的 repeater / composite 加子字段声明;明确写下「对象级 repeater / composite 是无类型 JSON,要带列的列表请建子对象(master_detail)」,并对期望列的 repeater 做 lint/拒收。协议不新增面。

    裁决含义(给执行席):

    • packages/spec/src/data/field.zod.ts 的 FieldType 描述改口:composite / repeater 是无类型 JSON 容器,CMS 血统(≈ Strapi component / ACF repeater);不承诺子字段。
    • content/docs 字段类型页同句,并指向子对象 + 内联表格的做法(Salesforce / Dynamics / ServiceNow / Odoo 的主流形态)。
    • lint:对象字段用 repeater / composite 且声明了 fields / subFields / columns 之类的键 ⇒ 报错;表单视图 view.zod.ts 的 fields 子行与 Studio 元数据表单(field.form.ts)不在此列,照旧。
    • objectui 表单网格不做新工作。
    • 选项 (a) 不立:唯一消费者是 hotsrm v0.6 的 8D steps,届时按子对象建模。hotsrm 现有的子对象做法(srm_supplier_audit_finding)即是推荐形态。

    工作量:S(spec 描述 + 文档一句 + 一条 lint 规则)。domain:spec。裁不派:spec 席按队列派发。


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3
    Session: session_01PC4NPXPgPmpDRG9DdDnyXT
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25681-repeater-untyped-json
    Worktree: objectstack-issue-25681
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: ruling b, as its executing notes list it.

    • packages/spec/src/data/field.zod.ts: the FieldType comments and descriptions for composite / repeater (about :96–:97, which promise "declared sub-fields") say they are untyped JSON containers of CMS lineage and promise no sub-fields.
    • The field-types docs page (content/docs/data-modeling/field-types.mdx) says the same and points a typed list to a child object with an inline grid.
    • One lint arm: an object field of type repeater / composite that declares fields / subFields / columns or similar is an error. The ruling excludes the form view's fields sub-rows (view.zod.ts) and the Studio metadata form (field.form.ts).
    • Also its tests, regenerated data shards and references, and one changeset .changeset/25681-repeater-untyped-json.md.
      No new protocol surface, and no objectui work. (stop on breach; explain in the report)
      Container & model: S, mode:cloud (packages/spec/src/** plus packages/lint), model: default (TIER_DEFAULT; dispatch-gates --tier for this surface: no path-derived mandate), effort: medium. The path limb hits (packages/spec/src/data/**), so a CONTRACT_REVIEW_TIER contract review is owed before the queue.
      Clause-②: no
      Metadata-protocol: semantic
      Responsibility: n/a — not a defect card
      Ruling-ref: 6386744473 (batch Add comprehensive test suite for Zod schema validation #12 item 4, letter b, maintainer 「25681 b」)
      Thread-read: 6386744473
      Serial constraints cleared: none. No open PR touches these files (all 18 open PRs' file lists read 2026-10-24T22:24Z), and no open pm:dispatched claim declares them.
      Premise: holds on origin/main 47af3dde42. FieldType describes composite / repeater as embedded sub-objects "with declared sub-fields" (field.zod.ts:96–:97), while FieldSchema (about :1098) declares no sub-field member. Mechanism to measure: FieldSchema is strict, so an object field carrying fields may already be refused at parse. If so, the lint arm reaches only unparsed doors, the posture security-owd-alias documents. Say which.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatched · round 3 · domain:spec seat 2 · 2026-10-24T22:34Z

    • Back end: mode:cloud, cloud session session_01T2U7eGg8aRTpJ8kKXxDiog (created through create_session), on branch claude/issue-25681-repeater-untyped-json. The claim 6390296997 stays the identity.
    • Order given: ruling b 6386744473 with its four execution notes verbatim, the lead to measure whether strict FieldSchema already refuses sub-field keys (which decides what the lint arm reaches), the view sub-row exclusion, the producer measurement, the Writes: line.
    • Round-2 lessons carried in the order: the cli integration project for migration entries; no tracker ids in printed guidance; an ablation on every new refusal branch; downstream producers (objectui, HotCRM) measured before a refusal or a removal; governed paths in their own PR.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 25681,
    "status": "done",
    "branch": "claude/issue-25681-repeater-untyped-json",
    "pr": "#25755",
    "session": "session_01T2U7eGg8aRTpJ8kKXxDiog",
    "premise_still_valid": true,
    "summary": "Ruling b executed with no new protocol surface. (1) FieldType comments (field.zod.ts:95-104) now say object-level composite / repeater are untyped JSON containers of CMS lineage with no declared sub-fields, and a typed list is a child object (master_detail, inlineEdit: 'grid'). (2) MEASURED: FieldSchema is strict and already refused fields / subFields / columns (and schema, items, properties, itemFields, of, children) at parse, but the message named only the key. Three FIELD_KEY_GUIDANCE rows (authoring-key-lint.ts) now make that refusal name the child-object route; the columns row also names lookupColumns / relatedListColumns / inlineColumns. (3) New lint rule field/embedded-sub-fields (error) in lintDataModel; its docblock and os explain state it reaches only the unparsed doors (os lint on a raw config, defineStack strict:false, which measured ACCEPTS and keeps the key, and the metadata-generation rubric) - the security-owd-alias posture. (4) Docs: field-types.mdx, validation-rules.mdx (removed a false sentence), layout-dsl.mdx (a form field's fields list is the form's). The published skill line is governed and rides its own draft PR #25741 (Refs #25681). Producers: this repo only examples/app-showcase/src/data/objects/field-zoo.object.ts:166-167, no sub-field key; HotCRM a4b3560: 0 object fields of either type; objectui a3c0da9 packages/fields/src/index.tsx:3625 already says the spec has no sub-field member. Assignee left as the PM set it (sunhaolin), mirrored on both PRs.",
    "tests": "Head 87ff1c7 (branch + origin/main 5cf1b9d merged, frozen install, cli... closure rebuilt): spec test exit 1 = 720/721 files, 21818/21842 tests, all 23 failures in scripts/liveness/check-liveness.test.ts (main-red, see out_of_scope_findings); spec test:repo exit 0 79/79 files 1269 tests; spec typecheck 0; lint test 0 162/162 files 7301 tests; lint typecheck 0; cli test (unit + integration) 0 501 passed + 1 skipped files 6297 tests; cli typecheck 0. Pre-merge head 22414cf, same battery all exit 0: spec 721/721 (21829), spec test:repo 79/79, lint 162/162 (7296), cli 500 + 1 skipped (6291). New pins: spec authoring-key-lint.test.ts +9 cases; lint data-model-rules.embedded-sub-fields.test.ts 16 cases; fix-lines census extended to 7 ids. Ablation A (commit 22414cf, data-model-rules.ts if (declared.length > 0) { -> if (false && ...), via scripts/ablation-replace.mjs): predicted 11 red / 13 green, observed 11 red / 13 green, restored blob 678d7d766605 == HEAD, git diff HEAD empty. Ablation B (authoring-key-lint.ts, delete the fields: row): predicted 2 red / 19 green, observed 2 red / 19 green, restored blob a2d3a01b6821 == HEAD. Both suites resolve the mutated module from src (relative import / in-package), so no dist leg.",
    "gates": "Main branch, head 87ff1c7: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 111; every one run with exit captured to a file; --ran answers '111 derived famil(ies) accounted for - 111 run, 0 NOT-MEASURED'; 110 exit 0, pnpm --filter @objectstack/spec run check:liveness exit 1 (main-red). check:generated exit 1 on 87ff1c7 (13/14, liveness/state-counts the main-red) and exit 0 (14/14) on 22414cf. check-dev-prereqs (local-only) exit 0. Governed PR #25741, head 14a58a3: 44 derived, --ran '39 run, 5 NOT-MEASURED (5 DERIVED from a recorded exit 3)' - check:doc-formula-expressions, check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content refused on an unbuilt worktree; CI builds them. check-governed-merges: #25755 0 of 12 paths governed; #25741 GOVERNED Tier H (skills/). CI check-runs on both PRs: in_progress at report time, not waited on.",
    "dogfood_battery": "n/a - no ! in the PR title and no (narrowing) arm (Clause-② no)",
    "mcp_calls": "4 - mcp__github__issue_read x2 (get, get_comments), mcp__github__search_pull_requests x2; zero GitHub MCP writes. Plus claude-code-remote add_repo x2 (hotcrm denied by the session classifier; objectui answered read_available), not GitHub writes",
    "api_writes": "3 - relay pr_create #25741 (POST /repos/objectstack-ai/objectstack/pulls + POST /issues/25741/assignees), relay pr_create #25755 (POST /pulls + POST /issues/25755/assignees), this os-dev-report comment via post-stamped.mjs (POST /issues/25681/comments). Relay transport: POST /repos/objectstack-ai/objectstack/dispatches x2. git push not counted",
    "deviations": [
    "Worktree: the cloud harness had checked the designated branch out in the primary checkout; I detached the primary and added ../objectstack-issue-25681 on the same branch (a branch cannot be checked out twice). The branch is platform-designated, so no empty-branch probe push.",
    "Commit trailers: the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude) instead of the harness reminder's model-named Co-Authored-By; AGENTS.md / the role file win, and the pre-push hook refuses a model id.",
    "File surface: the claim lists field.zod.ts, the field-types page, one lint arm, tests and a changeset. Added within the claimed container (packages/spec/src/
    + packages/lint): packages/spec/src/data/authoring-key-lint.ts (the three guidance rows), packages/lint/src/index.ts and rule-explanations.ts (export + os explain). Added docs: validation-rules.mdx and layout-dsl.mdx, whose sentences this ruling makes false or ambiguous. The changeset adds @objectstack/lint minor beside @objectstack/spec minor (a new rule).",
    "HotCRM producer read: add_repo was denied by the session classifier; read through the anonymous git lane (shallow clone, a4b3560), the same lane add_repo named for objectui.",
    "A worker restart killed the first CLI run mid-suite; the battery was re-run in full on the same head and again on the merge head.",
    "main moved 122 files (including packages/lint/src/index.ts and rule-explanations.ts) before the PR opened; merged origin/main 5cf1b9d (merge commit 87ff1c7, clean, no regen debt) and re-ran the full battery per AGENTS.md section 10."
    ],
    "open_questions": [
    { "question": "The ruling says lint/拒收. The parse already refused the keys; I also made that refusal name the child-object route (three FIELD_KEY_GUIDANCE rows) - keep?", "options": ["A keep: every parsed door (defineStack, os validate/build, saveMetaItem) names the route; message-only, no key added", "B drop: lint only, parse message stays 'unrecognized key'"], "recommendation": "A. Actual business need: defineStack is the door AI authors use, and it never reaches the lint. Long-term: one channel, one table (FIELD_KEY_GUIDANCE) already feeding FieldSchema. AI-proofing: a refusal that names where the list goes beats one that names only the key. Startup scope: no new key, no new gate (the lint arm is the ruled one)." },
    { "question": "Lint-arm home: lintDataModel (os lint + rubric) rather than the shared authoring registry (os validate/build/runtime gate).", "options": ["A lintDataModel", "B authoring registry"], "recommendation": "A. Every registry door parses first and refuses the field, so a registry entry would be a dead second voice; the unparsed doors are exactly where lintDataModel runs." },
    { "question": "record (ADR-0007) is also untyped JSON on an object field; the ruling names only composite / repeater, so the lint leaves it unjudged (pinned silent). Extend?", "options": ["A leave as ruled", "B extend to record"], "recommendation": "A - outside the ruling's words; a seat can raise it if a producer appears." }
    ],
    "out_of_scope_findings": [
    "class: a · reach: pnpm --filter @objectstack/spec check:liveness exits 1 on origin/main 5cf1b9d (key-mention anchoring: 1 UNANCHORED, translation/flows.screens -> packages/services/service-automation/src/engine.ts), and scripts/liveness/check-liveness.test.ts fails 23 tests; check:generated reports liveness/state-counts/ stale. Lint & Repo Gates / TypeScript Type Check on any PR merged onto that main inherit it · evidence: refactors f3fea0a and 0189524 moved renderFlowTextSlot out of engine.ts into node-text-rendering.ts; engine.ts now has 0 screens hits (its one hit at 47af3dd was a coincidental comment, now in run-resume.ts). Proved patch: delete the clause '; packages/services/service-automation/src/engine.ts#renderFlowTextSlot (...)' from that row's evidence in packages/spec/liveness/translation.json -> check:liveness green (run through ablation-replace, restored). A repoint to node-text-rendering.ts alone stays red (no screens there either). No open PR carries it · dedupe words: liveness flows.screens, renderFlowTextSlot engine.ts, key-mention UNANCHORED, translation.json screens",
    "carrier: PR #25741 · governed half (skills/objectstack-data/rules/field-types.md), Tier H, awaits the maintainer's approval; noted, not filed",
    "carrier: none · docs/audits/2026-06-handwritten-docs-accuracy-followups.md:66 asked whether composite/repeater sub-shapes are declared elsewhere; this PR answers it (they are not, by ruling); the audit record is left as written - noted, not filed"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed, governed half · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-25T06:02Z

    reading value
    PR #25741 (Tier H, skills/**) MERGED through the queue at 2026-10-25T05:57:37Z; merge commit 8f35ffcae1. Approved by os-zhuang (GOVERNED_APPROVERS) on 14a58a3ab0, who also marked it ready and enabled auto-merge, then the queue took it. The seat neither approved nor landed it
    queue branch gh-readonly-queue/main/pr-25741-* gone (REST matching-refs: 0 refs)
    origin/main carries 8f35ffcae1; skills/objectstack-data/rules/field-types.md rows: composite "Single embedded JSON object, untyped", repeater "Embedded JSON array, untyped … a typed list is a child object"

    State: this card stays open with pm:dispatched (Refs #25681 here). The code and docs half is PR #25755 (seat ACCEPT 6394076839, needs:contract-review). It lands after #25751 (the main-red fix) is in main, then a base merge and a CONTRACT_REVIEW_TIER PASS on its landing head. Fixes #25681 closes the card then.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 25681,
    "status": "done",
    "round": "patch round 1 (contract review 6395830582, FAIL on clause 2 only)",
    "branch": "claude/issue-25681-repeater-untyped-json",
    "pr": "#25755",
    "session": "session_01T2U7eGg8aRTpJ8kKXxDiog",
    "head": "3ea98b65 (parent 87ff1c7; plain commit, no rebase, no force, origin/main NOT merged this round)",
    "changed_lines": ".changeset/25681-repeater-untyped-json.md line 8: Clause-②: no -> Clause-②: yes (widening). Metadata-protocol: semantic, Ruling-ref: 6386744473 and both minor levels unchanged. Nothing else changed.",
    "premise_check": "Lane rule confirmed at .claude/skills/pm-dispatch/references/lanes/spec.md:20 (a card that widens the accept set or enlarges the public face is clause 2, however small). The diff adds FIELD_EMBEDDED_SUB_FIELDS and a RULE_EXPLANATIONS key to @objectstack/lint and three FIELD_KEY_GUIDANCE keys to @objectstack/spec.",
    "gates": "14 run on head 3ea98b6, each exit captured to a file before any pipe, all exit 0: check-adr-0087-registration --base origin/main ('this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)', so a widening needs no ADR-0087 marker) and --self-test; check-changeset-no-major --base origin/main and --self-test; check-empty-changeset --base origin/main and --self-test; pm/release-rehearsal-clone --self-test; release-pending-publish --self-test; check:changeset-gate-self-tests; check:closed-ceremony-text; check:objectui-changeset; check:pm-changeset-deadline-census; check:published-files; check:nul-bytes. No build and no suites: the delta is one declaration line.",
    "deviations": [
    "Step 2 (PATCH the PR #25755 body) NOT done: the role file forbids a dev PATCH of the PR body (os-dev.md:64, the body is written once at pr_create; a later change is named in the report for the seat to write), and the dispatch order says the role file wins over the order. Edit for the seat: in the PR #25755 body, replace the line-initial Clause-②: no (line 3) with Clause-②: yes (widening) and change nothing else. Until it lands, Check Changeset reads the body's no."
    ],
    "mcp_calls": "0 this round",
    "api_writes": "1 this round - this addendum comment via post-stamped.mjs (POST /issues/25681/comments, relay transport POST /dispatches). git push not counted. No pr or body write."
    }


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-25T15:22Z

    reading value
    PR #25755 MERGED through the queue at 2026-10-25T15:08:46Z; merge commit 4034f47bd0. ACCEPT 6394076839, corrected by addendum 6395867217 to Clause-②: yes (widening). The CONTRACT_REVIEW_TIER records were a FAIL 6395830582 on ② (the declaration), then PASS 6396098582 on 3ea98b655a after the one-line reset, confirmed by PASS 6398816114 on the landing head afd3a14250 (main merged in, a clean automatic merge). ci-failure read GREEN, 48/48, and check-governed-merges read NOT governed
    queue branch gh-readonly-queue/main/pr-25755-* gone (REST matching-refs: 0 refs)
    origin/main carries 4034f47bd0; authoring-key-lint.ts holds EMBEDDED_JSON_TYPED_LIST; packages/lint/src/data-model-rules.ts names field/embedded-sub-fields

    State: the card closed completed on the merge (Fixes #25681). Both halves are now in main: the governed skills half #25741 (8f35ffcae1, record 6394191601) and this code and docs half. pm:dispatched is removed in this stroke, and the dev's cloud session session_01T2U7eGg8aRTpJ8kKXxDiog is archived.

    • Accepted, no carrier: record (ADR-0007) is left unjudged by the lint, as ruled (open question 3, A).

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specenhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions