Skip to content

feat(spec,lint,app-shell)!: a field group's relatedList names body tables only - #27592

Draft
objectstack-fleet[bot] wants to merge 11 commits into
mainfrom
claude/project-thread-xkumfw
Draft

objectstack-fleet[bot] wants to merge 11 commits into
mainfrom
claude/project-thread-xkumfw

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

Requested by Yibo Wang · project thread

Fixes #27478

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no file under skills/** teaches fieldGroups[].relatedList; skills/objectstack-data/rules/relationships.md and skills/objectstack-ui/SKILL.md teach only the FK's own relatedList, whose meaning does not change.

Seat ruling on #27478: option B (narrowing). Whether a related list is a body table is decided once, by the relationship's own keys on the child's FK (relatedList: 'primary', or a master_detail that sets inlineEdit and declares no relatedList), per ruling B on #27055. A field group's relatedList only positions a body table; it never promotes a side card. Option A would give body-ness two sources that can disagree (the FK saying side card, the group saying body). So a group naming a relationship that is not a body table is refused, with a prescription, and Studio offers only body tables.

Landing order. PR #27352 rewrites the same fieldGroups[i].relatedList describe and docblock. Per the seat, #27352 landed first and this PR writes its text on top of #27352's wording, replacing only its "relatedList: false still hides the table" sentence. No other #27352 line is touched.

维护者速读

  • 改了什么:新增 @objectstack/spec 的唯一判定 isBodyTableRelationship(这个子关系是不是记录页的主表);field group 的 relatedList 指向侧栏卡片或被隐藏的关系时,defineStack 和对象写入口(lint 规则 related-list-group-not-body)都会拒绝并给出改法;Studio 只列主表;记录页、Studio 和拒绝读同一个判定。
  • 为什么改:以前这种分组能保存、能发布,但记录页在分组位置什么都不画,静默失效;Studio 还会主动给出这种选项。
  • 风险与回滚:BREAKING 只针对一种写法(分组指向非主表),仓库内示例、平台对象、dogfood 和 HotCRM 都没有这种写法;已存行照常加载,下次保存会 422。另有一格行为变化:master_detail + inlineEdit + relatedList: true 现在是侧栏卡片(与该键的说明一致)。回滚即还原本 PR。
  • 先看:packages/spec/src/data/related-list-body-table.ts(判定与拒绝句),packages/spec/src/stack.zod.ts(collectRelatedListNameErrors 内的调用),ui/packages/app-shell/src/views/studio-design/ObjectGroupInspector.tsx。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/related-list-body-table.test.ts src/stack-related-lists-order.test.ts,应全绿。
  • 需要你做的:无,席位负责落地。

What changes

Measured first, on origin/main 273137d

An object whose field group names a relationship that is not a body table parsed, passed defineStack and the object write door, and drew nothing at the group. A probe over six child shapes (a plain lookup, lookup with relatedList: true, lookup with relatedList: false, a master_detail with no inlineEdit, and two body-table controls) answered the same for all six: ObjectSchema.safeParse: true, defineStack: accepted, door errors: []. buildDefaultTabs (plugin-detail) drew details(basic,terms) with record:related_list(task) left in Related, so a group naming task placed nothing.

The record page also had two copies of the rule. app-shell isMainContentList (behind deriveRelatedLists' isPrimary and Studio's relationship inspector) counted 'primary', or a master_detail with inlineEdit and no relatedList. RecordDetailView's isBody was isPrimary OR master_detail + inlineEdit, which ignored relatedList: true.

One predicate

isBodyTableRelationship(field) in packages/spec/src/data/related-list-body-table.ts, exported from @objectstack/spec/data. Its readers:

  • the record page: deriveRelatedLists sets isPrimary from it (isMainContentList is deleted), and RecordDetailView's isBody is now isPrimary, so plugin-detail's bodyTablePlacement receives the spec verdict;
  • Studio: RelatedTablePlacement (ObjectGroupInspector.tsx) offers only relationships with isPrimary, and the relationship inspector reads an absent relatedList through it;
  • the refusal, at defineStack and at the door.

fieldGroupBodyTableRefusal writes the one refusal sentence both surfaces print.

The refusal

  • defineStack (os validate, os build): collectRelatedListNameErrors pushes the sentence for fieldGroups[i].relatedList, as a STACK_CROSS_REFERENCE_INVALID issue.
  • The object write door (PUT /api/v1/meta/object, Studio, MCP): validate-related-list-refs reports related-list-group-not-body (error) at objects.<name>.fieldGroups[i].relatedList, so the write answers 422 INVALID_METADATA. It runs under the closure boundary the rule already documents: a child outside the judged collection is left unjudged, as the name check leaves it. os explain related-list-group-not-body has an entry.
  • A bare child name names every FK of that child, so it is refused when any of them is not a body table; the remedy then points at object.field for the one that is.
  • relatedLists entries are not judged this way: they order body tables and side cards alike.

Example sentence (from the spec test):

Object 'lead' fieldGroups[2].relatedList 'task' names a related list that is not a body table: 'task.lead_id' is a side card. A field group's relatedList only positions a body table (a FK declaring relatedList: 'primary', or a master_detail FK that sets inlineEdit and declares no relatedList), so the record page draws nothing at the group for any other list. Set relatedList: 'primary' on that FK, or remove the group's relatedList.

Describe and docblock

Written on top of #27352's wording (merged as ad3a640). Only the last sentence of the describe and of the docblock change.

Before (fieldGroups[i].relatedList describe, last sentence):

relatedList: false on the relationship still hides the table.

After:

It names a body table only (a FK declaring relatedList: 'primary', or a master_detail FK that sets inlineEdit and declares no relatedList): a group naming a side card or a list hidden by relatedList: false draws nothing, so it is refused where the child's fields are known (os validate / os build and the metadata write door), naming the FK and the remedy (set relatedList: 'primary' on it, or remove the group's relatedList).

The docblock says the same, names isBodyTableRelationship and the rule related-list-group-not-body, and keeps "Body tables no group places keep the record page's default position." content/docs/references/data/object.mdx is regenerated. The phrases are pinned in related-list-body-table.test.ts, beside #27352's pins in record-page-describes.test.ts, which stay green.

field.zod.ts's "A field group's relatedList can place it among the field sections" (on relatedList: 'primary') is true as written once this lands, so it is not edited (and it is #27352's line).

One record page cell moves

A master_detail FK that sets inlineEdit and declares relatedList: true is a side card, as relatedList's description says ("write true to keep it a side card") and as Studio's relationship inspector writes it. RecordDetailView used to draw it as a body table. It now moves to the related column. The seat confirmed this reading. Pinned by RecordDetailView.bodyTablePredicate-27478.test.tsx.

ADR-0087 and stored rows

  • Disposition: registered field-group-related-list-side-card-refused, a D3 semantic entry (packages/spec/src/migrations/entries/semantic/18.field-group-related-list-side-card-refused.ts) with no D2 conversion, because which remedy the author meant (promote the FK, or drop the placement) is not recorded.
  • Stored rows: the object parse does not judge this, so a stored sys_metadata object row carrying such a group is not rewritten and still loads and registers unchanged. Its record page draws what it drew before, except the one cell above. Its next save through the metadata write door is refused 422 at fieldGroups[i].relatedList until fixed, and so is a write of the child that turns a body table a stored group places into a side card (pinned in runtime-gate.object-writes.test.ts).
  • Census at the change: no example, platform object, dogfood fixture or HotCRM object (hotcrm 47a0666) declares a field group relatedList at all. Deployed metadata was not measured.
  • Metadata-protocol: narrowed: a value an author could write is now refused (AGENTS.md: "a value refused ... is narrowed"), so every package this diff moves takes minor (@objectstack/spec, @objectstack/lint, @object-ui/app-shell, @object-ui/plugin-detail, @object-ui/i18n), and the BREAKING banner stays.

Liveness

  • object.json fieldGroups.relatedList: planned → live (readers bodyTablePlacement, collectRelatedListNameErrors, validateRelatedListRefs).
  • field.json relatedList: evidence re-cited to deriveRelatedLists and isBodyTableRelationship.

Why

A group naming a side card saved, published and silently drew nothing, and Studio offered it. Under ruling B the FK is the only source of body-ness, so the group is refused rather than honoured.

Verification

All on head 78c79004 unless marked (head 31b2326c changes only the changeset's levels and declaration line), under os-verify-lock.

  • New or changed tests:
    • packages/spec/src/data/related-list-body-table.test.ts: the predicate's verdict table (14 shapes plus null and undefined), the exact refusal sentence, and the describe pins.
    • packages/spec/src/stack-related-lists-order.test.ts: defineStack refuses a group naming a side card, an inline-edited master_detail with relatedList: true, a plain master_detail and a hidden list, and a mixed bare name; body-table controls pass.
    • packages/lint/src/validate-related-list-refs.test.ts and runtime-gate.object-writes.test.ts: the door refuses a group naming a side card and publishes it once the FK is primary; a child write that makes a placed body table a side card is refused; the lint sentence equals the defineStack sentence word for word.
    • packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts: PUT answers 422 with rule related-list-group-not-body at objects.rl_quote.fieldGroups[0].relatedList; the control saves.
    • ui/packages/app-shell/src/views/RecordDetailView.bodyTablePredicate-27478.test.tsx (mounted page): an inline-edited master_detail with relatedList: true is a side card; with no relatedList it is drawn at its group in the details column.
    • ui/packages/app-shell/src/views/studio-design/ObjectGroupInspector.relatedListPlacement-27054.test.tsx: Studio offers None plus body tables only, and a stored side-card value shows the not-a-body-table line.
    • ui/packages/plugin-detail/src/synth/__tests__/buildDefaultPageSchema.bodyTables-27055.test.ts: a group naming a side card promotes nothing.
  • Reverse legs (scripts/ablation-replace.mjs, each restored to HEAD with git diff HEAD empty, on 55a063ec):
    • RecordDetailView isBody put back to the old inline copy: the mounted test went 1 failed, 1 passed (the control stayed green);
    • the defineStack push deleted: stack-related-lists-order went 4 failed, 19 passed;
    • the lint finding never pushed: lint went 4 failed, 33 passed.
  • Suites: @objectstack/spec 839 files, 23799 tests passed; @objectstack/lint 177 files, 7607 passed; @objectstack/metadata-protocol 279 files passed, 4 skipped; touched ui files 3 files, 22 tests passed; ui scripts/ 169 files passed, 2 skipped. check:generated 14 of 14 up to date.
  • Build: turbo run build --concurrency=2, 115 of 115 tasks.
  • Gates: dispatch-gates --commands derives 113; 113 run, all exit 0; --ran reconciles 113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN.
  • Dogfood (title has ! and the narrowing arm): full @objectstack/dogfood test exit 0, 350 files passed, 3 skipped (2817 tests passed, 20 skipped).
  • check-changeset-no-major with this body as the event: exit 0 on 31b2326c (level axis: no (narrowing) and narrowed, no moved package graded patch); check-adr-0087-registration exit 0

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzgpxjnsDPHLokR644U

…bles only

One predicate, isBodyTableRelationship in @objectstack/spec/data, decides
whether an inbound relationship is a body table; defineStack and the lint
door member refuse a field group naming a side card or a hidden list; the
record page and Studio read the same predicate.

Claude-Session: https://claude.ai/code/session_01WGufzgpxjnsDPHLokR644U
Co-authored-by: Claude <noreply@anthropic.com>
… predicate verdicts and refusals

Claude-Session: https://claude.ai/code/session_01WGufzgpxjnsDPHLokR644U
Co-authored-by: Claude <noreply@anthropic.com>
…0087 D3), changeset, liveness rows

Claude-Session: https://claude.ai/code/session_01WGufzgpxjnsDPHLokR644U
Co-authored-by: Claude <noreply@anthropic.com>
…names body tables only

Written on top of the #27352 wording: the sentence saying relatedList: false
on the FK still hides the table is replaced by the body-table rule, the
refusal at defineStack and the metadata write door, and the remedy. A
describe pin in related-list-body-table.test.ts; object.mdx regenerated.

Claude-Session: https://claude.ai/code/session_01WGufzgpxjnsDPHLokR644U
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 16 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/flows.mdx (via inlineEdit (symbol, a field of interface RelatedListRelationshipFields))
  • content/docs/data-modeling/field-types.mdx (via inlineEdit (symbol, a field of interface RelatedListRelationshipFields))
  • content/docs/data-modeling/fields.mdx (via inlineEdit (symbol, a field of interface RelatedListRelationshipFields))
  • content/docs/data-modeling/object-extensions.mdx (via fieldGroups (literal, a string literal in validateRelatedListRefs))
  • content/docs/data-modeling/schema-design.mdx (via fieldGroups (literal, a string literal in validateRelatedListRefs))
  • content/docs/frontend/plugins/plugin-view.mdx (via inlineEdit (symbol, a field of interface RelatedListRelationshipFields))
  • content/docs/getting-started/common-patterns.mdx (via inlineEdit (symbol, a field of interface RelatedListRelationshipFields))
  • content/docs/protocol/kernel/i18n-standard.mdx (via fieldGroups (literal, a string literal in validateRelatedListRefs))
  • content/docs/protocol/objectui/layout-dsl.mdx (via inlineEdit (symbol, a field of interface RelatedListRelationshipFields))
  • content/docs/ui/create-vs-edit-form.mdx (via fieldGroups (literal, a string literal in validateRelatedListRefs))
  • content/docs/ui/field-grouping-and-order.mdx (via fieldGroups (literal, a string literal in validateRelatedListRefs))
  • content/docs/ui/forms.mdx (via fieldGroups (literal, a string literal in validateRelatedListRefs))
  • content/docs/ui/pages.mdx (via relatedList (symbol, a field of interface RelatedListRelationshipFields))
  • content/docs/ui/views.mdx (via inlineEdit (symbol, a field of interface RelatedListRelationshipFields))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via fieldGroups (literal, a string literal in validateRelatedListRefs))
  • content/docs/releases/v17/17-1.mdx (via relatedList (symbol, a field of interface RelatedListRelationshipFields))
  • content/docs/releases/v17/17-3.mdx (via fieldGroups (literal, a string literal in validateRelatedListRefs))
  • content/docs/releases/v17/17-5.mdx (via fieldGroups (literal, a string literal in validateRelatedListRefs))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, …) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: master_detail (literal, 39 pages)
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 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 — 166 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 4a00b2b7c159685c8931d4fa84695a8d7bc04a7c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 29f7bc1e86112a772e9c4cffab7d84cea1290b0c — the merge of head 31b2326cd10779fea6b58c373c4eca1d4d476298 into base 4a00b2b7c159685c8931d4fa84695a8d7bc04a7c, 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 29f7bc1e86112a772e9c4cffab7d84cea1290b0c && git checkout 29f7bc1e86112a772e9c4cffab7d84cea1290b0c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4a00b2b7c159685c8931d4fa84695a8d7bc04a7c 31b2326cd10779fea6b58c373c4eca1d4d476298 && git checkout -B drift-repro 4a00b2b7c159685c8931d4fa84695a8d7bc04a7c && git merge --no-ff 31b2326cd10779fea6b58c373c4eca1d4d476298

node scripts/docs-audit/affected-docs.mjs --json 4a00b2b7c159685c8931d4fa84695a8d7bc04a7c

⚠️ 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 4a00b2b7c159685c8931d4fa84695a8d7bc04a7c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…narrowed and ships minor

A value an author could write is refused, which AGENTS.md reads as narrowed,
so each package the diff moves takes minor.

Claude-Session: https://claude.ai/code/session_01WGufzgpxjnsDPHLokR644U
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 31b2326 · 2026-10-29T15:19Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 31b2326cd10779fea6b58c373c4eca1d4d476298
Local-runs: none
Scope: packages/spec/src/data/index.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/data/related-list-body-table.ts, packages/spec/src/migrations/entries/semantic/18.field-group-related-list-side-card-refused.ts, packages/spec/src/stack.zod.ts + narrowing floor: .changeset/27478-field-group-related-list-body-tables.md, packages/lint/src/index.ts, packages/lint/src/reference-integrity-suite.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/runtime-gate.object-writes.test.ts, packages/lint/src/validate-related-list-refs.test.ts, packages/lint/src/validate-related-list-refs.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/liveness/field.json, packages/spec/liveness/object.json, packages/spec/liveness/state-counts/object.md, packages/spec/src/data/related-list-body-table.test.ts, packages/spec/src/stack-related-lists-order.test.ts, ui/packages/app-shell/src/utils/deriveRelatedLists.ts, ui/packages/app-shell/src/views/RecordDetailView.bodyTablePredicate-27478.test.tsx, ui/packages/app-shell/src/views/RecordDetailView.tsx, ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx, ui/packages/app-shell/src/views/studio-design/ObjectGroupInspector.relatedListPlacement-27054.test.tsx, ui/packages/app-shell/src/views/studio-design/ObjectGroupInspector.tsx, ui/packages/i18n/src/namespaces/studio/en.ts, ui/packages/i18n/src/namespaces/studio/zh.ts, ui/packages/plugin-detail/src/synth/tests/buildDefaultPageSchema.bodyTables-27055.test.ts, ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts; cited and read, not reviewed: packages/spec/src/data/field.zod.ts (the relatedList / inlineEdit describes on main), packages/lint/src/runtime-gate.ts (baseline / candidate / stored-universe set difference), ui/packages/app-shell/src/views/studio-design/relatedListVocabulary.ts (Studio's catalog is deriveRelatedLists with includeHidden), ui/packages/app-shell/src/index.ts (public entry), docs/adr/0087-metadata-protocol-upgrade-contract.md (D2 / D3), docs/adr/0035-config-driven-master-detail.md, scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs, AGENTS.md § changeset declarations. Prose faces (content/docs/references/data/object.mdx, regenerated) are the lane seat's at TIER_DEFAULT.

① Derived judgments

Read off the diff at 31b2326c and the head's check-runs, not the PR body.

  • Accept set, fieldGroups[i].relatedList (object.zod.ts, stack.zod.ts, validate-related-list-refs.ts) — narrowed, RIGHT. The Zod schema itself is unchanged (z.string().regex(RELATED_LIST_ENTRY_PATTERN).optional()); the parse still accepts every spelling. The narrowing is a cross-object judgement added where the child's FK keys are known: collectRelatedListNameErrors pushes fieldGroupBodyTableRefusal's sentence when any relationship the entry names fails isBodyTableRelationship, and validateRelatedListRefs pushes the same sentence as related-list-group-not-body at error. Both run the unknown-name check first and the body check only when namedBy(entry) is non-empty, so a name that matches nothing earns the unknown-name refusal alone, and a child outside the stack / closure (artifactScope, platform name, package closure at the door) stays unjudged on both surfaces exactly as the name check leaves it. The two surfaces build inbound identically (child then field order, audit FKs excluded, extensions merged by name) and loop relatedLists then fieldGroups in the same order, so the lint pin "issues equal messages, word for word, 4 of 4" holds by construction. relatedLists entries are deliberately not judged (they order side cards too; relatedLists's describe on main says so). Right.
  • isBodyTableRelationship as the one predicate — RIGHT. Verdict table: 'primary' on a lookup or master_detail is a body table; any other declared relatedList (true / false) is not; absent relatedList on a master_detail with inlineEdit true / 'grid' / 'form' is; a plain master_detail, any lookup without 'primary', and a non-FK type are not. This is field.zod.ts's own relatedList describe on main ("Absent on a master_detail field that sets inlineEdit, the list is 'primary' by default; write true to keep it a side card"). The head's tree holds no other copy of the rule outside tests: isMainContentList is deleted, RecordDetailView.isBody is isPrimary, ObjectFieldInspector.readRelatedListMode and deriveRelatedLists.isPrimary call the spec, Studio's RelatedTablePlacement reads isPrimary off useRelatedListCatalog, which is deriveRelatedLists(..., { includeHidden: true }). Triage's "one rule across the seam, not three copies" is met.
  • Public surface, @objectstack/spec/data — added, RIGHT. isBodyTableRelationship, fieldGroupBodyTableRefusal, NamedInboundRelationship, RelatedListRelationshipFields are exported through data/index.ts; api-surface/data.json and export-origins/data.json carry the four rows, Spec property liveness and Lint & Repo Gates are green on the head. related-list-body-table.ts is a pure predicate and a message writer, the same shape as controlled-by-parent-master.ts and externalBindingDatasourceRefusal beside it, so Human Minimum 1 (no runtime logic in packages/spec) holds.
  • Public surface, @objectstack/lint — added, RIGHT. RELATED_LIST_GROUP_NOT_BODY is exported from index.ts; RULE_EXPLANATIONS has the entry; reference-integrity-suite.ts keeps validateRelatedListRefs as the one member (the rule rides the existing suite row, census 0 on the shipped corpus, as the comment says).
  • Public surface, @object-ui/app-shell — RIGHT, no removal published. isMainContentList was exported from utils/deriveRelatedLists.ts but never from src/index.ts (the package's only entry); its one importer on main was ObjectFieldInspector.tsx, rewritten here. The changeset's "internal helper" is accurate.
  • Record page behaviour, one cell — RIGHT, and it is a reconciliation, not a semantic change. A master_detail + inlineEdit + relatedList: true list was drawn as a body table by RecordDetailView's inline copy and as a side card by deriveRelatedLists; the spec text on main says side card. The spec's meaning does not move, so Metadata-protocol is not semantic on this account; the cell is correctly carried as a BREAKING behaviour note in the changeset and the D3 entry.
  • bodyTablePlacement — RIGHT, read-only. The renderer places only nodes in bodies (which isBody ?? isPrimary selects); a group naming a side card finds no body with that objectName/relationshipField, so it places nothing and the group (fieldless) collapses out of the sections, as it did before. The plugin-detail diff is comments and one test; no logic moved.
  • Liveness — RIGHT. object.json fieldGroups.relatedList planned → live with three readers cited (bodyTablePlacement, collectRelatedListNameErrors, validateRelatedListRefs); state-counts/object.md 76/1 → 77/0; field.json relatedList re-cited. Spec property liveness is green.
  • Check-runs on 31b2326c. Every gate family is green: Test Core 8/8, Test UI 16/16, Type Check (workspace, source, consumer, debt ledger), Lint (ESLint, repo & tooling, docs & code guards, CI/release/engine, PM dispatch-gates, scoped), Build Core / UI / Docs, Live E2E UI, Dogfood Regression Gate, Governed Surface Queue Guard, PR-body guards, Check PR Size, Spec property liveness, Check Documentation Links, Flag docs. Check Changeset ran three times on this same head: 14:21:54 failure (the one red step was "Guard against accidental major bumps (launch window)", check-changeset-no-major, whose input is the PR body via the edited trigger), 14:23:16 cancelled, 14:24:38 success. The newest run on the head is the verdict and it is green; the earlier red is the body-as-input race the workflow header documents (Check Changeset concludes success when it measured nothing, and no edited trigger ever re-reads a Clause-②: line added after the last push — a correct seat action leaves the level axis unjudged behind a green check #16776), not a tree defect — the changeset file is identical across the three runs.

② Semver level

  • Changeset .changeset/27478-field-group-related-list-body-tables.md: @objectstack/spec minor, @objectstack/lint minor, @object-ui/app-shell minor, @object-ui/plugin-detail minor, @object-ui/i18n minor; title feat(spec,lint,app-shell)!:; BREAKING banner; both declaration lines; exactly one ADR-0087 marker registered field-group-related-list-side-card-refused.
  • Clause-②: line: no (narrowing) — RIGHT, on the PR body and in the changeset, identical. The diff refuses a value the published accept set admitted (a group naming a side card or a hidden list) and promotes nothing: no published accept set widens. (narrowing) is BREAKING per AGENTS.md § 3, and the title's ! and the banner agree.
  • Metadata-protocol: narrowed — RIGHT. AGENTS.md: "a value refused ... is narrowed". The claim's original none was corrected by the seat's amendment (comment 6499241991) and the head carries narrowed in both places.
  • Level — RIGHT. narrowed takes at least minor on every package the diff moves; all five moved published packages are minor, none patch, no major (the launch-window guard's newest run is green). @objectstack/metadata-protocol is touched only in a test file and publishes nothing, so it correctly has no entry. Note, not a finding: @object-ui/plugin-detail's own diff is docblock text and a test, so minor over-grades what that package ships; the rule as written ("a package the diff moves") admits it and the gate accepts it.
  • ADR-0087 disposition — RIGHT. D3 semantic entry 18.field-group-related-list-side-card-refused.ts (id unique at head, step 18 beside the other 18.* entries), no D2 conversion. ADR-0087 D2: "a change whose old shape has no lossless mapping cannot be converted; it goes to D3" — which remedy the author meant (promote the FK, name object.field, or drop the placement) is not recorded, so no conversion is writable. surface carries no backticks or pipes; replacement, reason, acceptanceCriteria are filled and name the 422 and the STACK_CROSS_REFERENCE_INVALID text a consumer verifies against. The "Require an ADR-0087 disposition" step is green in all three Check Changeset runs.

③ Boundary flags

  • open_questions[0] (os-dev-report 6499180965): "Metadata-protocol: none copied from the claim; AGENTS.md reads a refused value as narrowed; changeset entries are patch with the BREAKING banner." Answered on this head. The seat's amendment (6499241991) set narrowed; head 31b2326c differs from the reported 78c79004 exactly in the changeset's levels (all minor) and declaration line, and the PR body carries narrowed. The level axis is consistent and the gate is green. Closed.
  • out_of_scope_findings[0]: governed files not edited. Verified in the tree: no file under skills/ or .claude/skills/ mentions fieldGroups together with relatedList, so Skills-docs: none is accurate; docs/adr/0035-config-driven-master-detail.md's read-side note describes relatedList placement by the FK's keys and says nothing about field groups, so it needs no amendment for ruling B. Closed.
  • Dev flag, "deployed metadata was not measured." Named in the PR body, the changeset and the D3 entry's reason. The code makes the consequence bounded (see ④: a stored row loads, registers and renders as before; only its own next save, or a child write that demotes a placed body table, is refused with the prescription), and the in-repo / HotCRM census is zero. Not a blocker; recorded for the seat as the one residual to carry into the release note.
  • Serial constraint with docs(spec): related-list descriptions say which unknown names are refused; primary list and trackHistory describe the v3 record page #27352: the describe and docblock are written on top of docs(spec): related-list descriptions say which unknown names are refused; primary list and trackHistory describe the v3 record page #27352's merged text, replacing only the "relatedList: false still hides the table" sentence; the pins in related-list-body-table.test.ts assert the new phrases and the absence of the old one, and docs(spec): related-list descriptions say which unknown names are refused; primary list and trackHistory describe the v3 record page #27352's record-page-describes.test.ts pins are in the green Test Core shards. Honoured.
  • No other dev flags in the body or the report.

④ Design judgment

Narrowing face, against the published spec on main: does the change take a declared capability away at the consumer, or make the spec's own refusal real? It makes the spec's own text real. On main, fieldGroups[i].relatedList's describe already said "Places a related list's body table at this position", and field.zod.ts's relatedList docblock already said a field group can place a 'primary' list among the body. Placing a side card was never declared; it was accepted silently and drew nothing, which is the card's observed bug (class c). Ruling B (body-ness by the FK's keys, ruling B on #27055) is implemented as written.

Per hits file:

  • packages/spec/src/data/related-list-body-table.ts — holds. The predicate reads only type, relatedList, inlineEdit, none of which carries a .default() in field.zod.ts, so the parsed field and a stored body agree (the module header's claim, verified). Break attempts: inlineEdit on a lookup → false (correct: inlineEdit is a master-detail key; the renderer never drew it as a body); master_detail + inlineEdit: false → false; 'primary' + inlineEdit absent on a lookup → true (the record page already drew it so). fieldGroupBodyTableRefusal returns undefined for an empty named (so the unknown-name refusal stands alone) and for an all-body set; for a mixed bare child name it refuses and points at the body FK as object.field, which RELATED_LIST_ENTRY_PATTERN accepts and defineStack passes (pinned). Hidden lists are named as "hidden (relatedList: false)" rather than "a side card", so the remedy sentence is truthful for both.
  • packages/spec/src/stack.zod.ts — holds. The new push sits inside the existing per-parent loop after judge, uses the same inbound map (now carrying def), and is skipped for a non-string entry. The closure boundary is inherited: a child outside the stack yields namedBy empty, so neither refusal fires (pinned: rlo_sibling under artifactObjects). STACK_CROSS_REFERENCE_INVALID is the right envelope: this is a cross-object judgement, not a parse, which is also why the object parse and stored rows are untouched. First consumer (os validate / os build) gets one sentence per offending group, one issue each.
  • packages/spec/src/data/object.zod.ts — holds, one should-fix in prose. The docblock names isBodyTableRelationship and related-list-group-not-body, and keeps "Body tables no group places keep the record page's default position." The .describe() states the body-table rule and the refusal sites. Should-fix (prose, not blocking): the describe's remedy clause lists two remedies ("set relatedList: 'primary' on it, or remove the group's relatedList") while the docblock and the refusal sentence list three for a mixed bare name (also "name only the body table as object.field"); the published JSON-schema description is the one an author reads in Studio and docs, so it should carry the third remedy too. No consumer is misled into an invalid state by the omission, so should-fix.
  • packages/spec/src/data/index.ts — holds. export * from './related-list-body-table' with a comment naming the readers; the artifacts are regenerated and the gates green.
  • packages/spec/src/migrations/entries/semantic/18.field-group-related-list-side-card-refused.ts — holds against ADR-0087 D3 and its first consumer (migrate meta / the generated upgrade guide). The surface names the exact shapes refused, the replacement gives the author's two edits (and the object.field form), the acceptanceCriteria names the verifiable signals (no STACK_CROSS_REFERENCE_INVALID issue containing the sentence; 200 instead of 422 related-list-group-not-body; the record page's two outcomes). The one-cell behaviour change is disclosed in reason. check:upgrade-guide and the migration tests are in the green Lint & Repo Gates / Test Core.

Narrowing floor, the door and the UI (read in the cited code):

  • Door semantics for stored rows — holds, and the PR's account is true in the code. runRuntimeAuthoringRules refuses candidate findings minus baseline findings minus stored-universe findings located on ANOTHER entry (runtime-gate.ts, fingerprint rule+where+path+message). So: a stored parent whose group names a side card still loads and registers (the object parse does not judge it); its own re-save is refused (the finding is on the written item's slot, which the stored universe never cancels); an unchanged re-save of the side-card child is NOT refused (the stored universe holds the same finding on the parent, another entry, and cancels it); an unrelated write is not refused (baseline holds the finding); a child write that demotes a placed body table IS refused with the finding at the parent's path (pinned at the lint level and at PUT 422 in metadata-protocol). The 422 lands on the child write with a path on the parent, which is the same shape related-list-ref-unknown already uses for a child write that drops the FK; the message names the FK being written, so the author is pointed at their own edit.
  • Studio — holds. The picker offers only isPrimary relationships, spelled object.field when the child has several (so the mixed-child case writes the accepted form), refuses to write any other pick (namesBodyTablesOnly), and keeps a stored side-card or hidden value visible with a destructive-styled explanation (en and zh). readRelatedListMode resolves an absent relatedList through the spec predicate. One rule, no copy.
  • Record page — holds. deriveRelatedLists.isPrimary is the predicate; RecordDetailView.isBody = isPrimary; the mounted test pins the moved cell and its control. Side cards keep their related-column cards; body tables keep their group positions through bodyTablePlacement.
  • i18n — both locales carry the hint and the new not-body line; {entry} is the only placeholder, interpolated by tFormat.

Findings: no must-fix. Should-fix: (1) the .describe() remedy clause should also name the object.field remedy the sentence and docblock give for a mixed bare child name. Residual for the seat: deployed sys_metadata was not censused; the code bounds the consequence to the affected row's own next save.

Implemented-by: claude/project-thread-xkumfw
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

…ld remedy too

The refusal sentence and the docblock offer three remedies; the describe,
which Studio and the reference docs show, now offers the same three.
Pin updated; object.mdx regenerated.

Claude-Session: https://claude.ai/code/session_01WGufzgpxjnsDPHLokR644U
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head e236829 · 2026-10-29T15:40Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e2368298162fd519f986c13743f7bdf8a9bb71f6
Local-runs: none
Scope: packages/spec/src/data/index.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/data/related-list-body-table.ts, packages/spec/src/migrations/entries/semantic/18.field-group-related-list-side-card-refused.ts, packages/spec/src/stack.zod.ts + narrowing floor: .changeset/27478-field-group-related-list-body-tables.md, packages/lint/src/index.ts, packages/lint/src/reference-integrity-suite.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/runtime-gate.object-writes.test.ts, packages/lint/src/validate-related-list-refs.test.ts, packages/lint/src/validate-related-list-refs.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/liveness/field.json, packages/spec/liveness/object.json, packages/spec/liveness/state-counts/object.md, packages/spec/src/data/related-list-body-table.test.ts, packages/spec/src/stack-related-lists-order.test.ts, ui/packages/app-shell/src/utils/deriveRelatedLists.ts, ui/packages/app-shell/src/views/RecordDetailView.bodyTablePredicate-27478.test.tsx, ui/packages/app-shell/src/views/RecordDetailView.tsx, ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx, ui/packages/app-shell/src/views/studio-design/ObjectGroupInspector.relatedListPlacement-27054.test.tsx, ui/packages/app-shell/src/views/studio-design/ObjectGroupInspector.tsx, ui/packages/i18n/src/namespaces/studio/en.ts, ui/packages/i18n/src/namespaces/studio/zh.ts, ui/packages/plugin-detail/src/synth/tests/buildDefaultPageSchema.bodyTables-27055.test.ts, ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts; cited and read, not reviewed: packages/spec/src/data/related-list-body-table.ts fieldGroupBodyTableRefusal (the three-remedy sentence the describe now mirrors), packages/lint/src/validate-related-list-refs.ts (the os explain remedy text), content/docs/references/data/object.mdx (regenerated from the describe; prose face, the lane seat's at TIER_DEFAULT, read here only to confirm the regen is faithful).

Delta review. This record is for head e2368298, one commit on top of 31b2326c, which record 6500998234 (this seat, PASS) judged in full. The compare 31b2326c...e2368298 is one commit, docs(spec): the field group relatedList describe names the object.field remedy too, touching three files, four lines: packages/spec/src/data/object.zod.ts (+1 −1, the fieldGroups[i].relatedList .describe() string), packages/spec/src/data/related-list-body-table.test.ts (+1 −1, the pin on that string), content/docs/references/data/object.mdx (+2 −2, the two generated rows carrying that description). The local clone's git diff --stat between the two heads agrees (3 files, 4 insertions, 4 deletions). The changeset, the D3 entry, api-surface/data.json, export-origins/data.json, the liveness files, stack.zod.ts, related-list-body-table.ts, index.ts, every packages/lint file and every ui/ file are byte-identical to 31b2326c. Everything the delta does not touch stands as judged in record 6500998234; every item it touches is judged afresh below.

① Derived judgments

Read off the compare and the head's tree, not the commit message.

  • Accept set — unchanged, RIGHT. The Zod node is still z.string().regex(RELATED_LIST_ENTRY_PATTERN).optional(); only the .describe() argument moved. No parse result, no defineStack issue, no lint finding and no door verdict changes: fieldGroupBodyTableRefusal, collectRelatedListNameErrors and validateRelatedListRefs are untouched. The narrowing judged in 6500998234 ① stands as judged there.
  • Public surface — one JSON-schema description string changed, RIGHT. z.toJSONSchema(ObjectFieldGroupSchema).properties.relatedList.description now ends "naming the FK and the remedy (set relatedList: 'primary' on it, name only the body table as object.field when a bare child name also names a side card, or remove the group's relatedList)". A description is documentation on the published schema, not an accept-set change; api-surface/data.json and export-origins/data.json carry no description text and are unchanged in the delta, correctly. The four @objectstack/spec/data exports, @objectstack/lint's RELATED_LIST_GROUP_NOT_BODY and the @object-ui/app-shell surface stand as judged in 6500998234.
  • Generated prose — faithful, RIGHT. Read, not reviewed: the two | **relatedList** | string | optional | ... | rows in content/docs/references/data/object.mdx (lines 318 and 704, the two tables that embed ObjectFieldGroupSchema) are, after \| unescaping, byte-equal to the describe string at the head (1232 characters each). The commit's "object.mdx regenerated" is true in the tree; @objectstack/spec check:generated (Lint & Repo Gates) will confirm it mechanically once the head's runs exist.
  • Pin — moved with the text, RIGHT. related-list-body-table.test.ts line 94 asserts the new three-remedy clause verbatim against the JSON-schema description; the other three pins in that describe block (body-table-only sentence, "draws nothing, so it is refused", the absence of "still hides the table") are unchanged and still hold against the head's string. stack-related-lists-order.test.ts line 204 and related-list-body-table.test.ts line 69 pin the refusal SENTENCE ("Set relatedList: 'primary' on that FK, or remove the group's relatedList."), which the delta does not touch, so they are unaffected; the sentence and the describe are two different texts and both pins are consistent with the head.
  • Check-runs on e2368298 — PENDING, not green. At 15:44Z (the commit is dated 15:37:08Z) the head has 0 check-runs; its five check-suites (objectstack-fleet, vercel, fly-io, cloudflare-workers-and-pages, claude) are queued with no conclusion, the commit status is pending with 0 contexts, and actions/runs?head_sha= lists 0 workflow runs. No gate verdict exists on this head yet, and none is claimed here. The gate families the delta can move are Test Core (the moved pin, packages/spec), Lint & Repo Gates (check:generated on object.mdx), Build Docs, Spec property liveness (the describe is cited evidence) and Check PR Size; Check Changeset reads a body and a changeset the delta does not touch. The tree evidence above (pin equals describe; mdx equals describe; nothing else moved) is why no red is expected, but the record names them pending: the landing check (full-green PR checks) is the seat's to read off the API, not off this record.

② Semver level

Unchanged by the delta: .changeset/27478-field-group-related-list-body-tables.md is byte-identical to 31b2326c (five minor entries, feat(...)!: title, BREAKING banner, Clause-②: no (narrowing), Metadata-protocol: narrowed, one ADR-0087 marker registered field-group-related-list-side-card-refused), and the PR body carries the same two declaration lines. A description-only change publishes no new accept set and takes no level of its own; the levels, the Clause-②: line, the narrowed reading and the D3 disposition stand as judged in record 6500998234 ②. Clause-②: no (narrowing) — RIGHT.

③ Boundary flags

④ Design judgment

Narrowing face, against the published spec on main: the delta takes nothing further away and makes nothing further real; it corrects what the published description SAYS about a refusal that was already judged to hold. The question for a description change is whether the text now matches the enforcement an author meets.

  • packages/spec/src/data/object.zod.ts — holds; the should-fix is closed. Judged against its first consumers, the JSON-schema description Studio and the reference docs show: an author whose bare child name (say event) names a side card and a body table now reads the same three ways out that the 422 / STACK_CROSS_REFERENCE_INVALID sentence gives them, in the same order (promote the FK, name only the body table as object.field, drop the group's relatedList). Break attempts on the wording: (a) "when a bare child name also names a side card" is the one case the sentence actually offers the object.field remedy in (pick is non-empty only when at least one named FK is a body table (bodies non-empty), which an object.field entry, naming one FK, can never satisfy), so the condition is truthful; (b) the describe says "side card" where the sentence may also say "hidden (relatedList: false)" for the other FK, but the preceding clause of the same describe already names "a list hidden by relatedList: false" among what is refused, and the remedy applies identically, so no author is led into an invalid state. Note, not a finding. (c) The string is a single-quoted TS literal with \' escapes; the pin asserts the unescaped text through z.toJSONSchema, which is the exact surface consumers read, so the escaping cannot drift from the pin unseen.
  • packages/spec/src/data/related-list-body-table.ts, packages/spec/src/stack.zod.ts, packages/spec/src/data/index.ts, packages/spec/src/migrations/entries/semantic/18.field-group-related-list-side-card-refused.ts — byte-identical to 31b2326c; they hold as judged in record 6500998234 ④. The D3 entry's replacement already named the object.field form, so the describe now agrees with the upgrade guide too.
  • Narrowing floor (door semantics, Studio, record page, i18n, lint rule explanation) — no file in it moved; stands as judged in record 6500998234 ④. validate-related-list-refs.ts line 104 (the os explain remedy text) already carried "name only a body table as object.field" on the previous head, so the three surfaces an author can read (describe, os explain, the sentence) now agree.
  • Nothing outside the finding. The compare shows no other hunk; the commit adds no code, no test case beyond the moved pin, no artifact beyond the regenerated rows.

Findings: no must-fix; no should-fix. Note: the describe's remedy condition says "also names a side card" where the sentence may also name a hidden list; harmless, recorded for the next prose pass. Pending: every check-run on e2368298 (0 runs, 5 suites queued at 15:44Z); landing reads them, not this record.

Implemented-by: claude/project-thread-xkumfw
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

…-xkumfw

# Conflicts:
#	ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx

This branch has not been deployed

No deployments
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/l tests tooling

Projects

None yet

2 participants