Skip to content

docs(spec): related-list descriptions say which unknown names are refused; primary list and trackHistory describe the v3 record page - #27352

Draft
objectstack-fleet[bot] wants to merge 4 commits into
mainfrom
claude/project-thread-1zx7zi
Draft

objectstack-fleet[bot] wants to merge 4 commits into
mainfrom
claude/project-thread-1zx7zi

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 #27311
Fixes #27312

Clause-②: no
Metadata-protocol: none

Seat ruling on #27311: option A (by design). The boundary in packages/lint/src/validate-related-list-refs.ts's header is deliberate: under a package closure a child outside it may be a downstream package's object that points in, which the closure cannot see. Refusing there would refuse legitimate stacks, and option B needs a new declaration surface no card asks for. So the contract text changes to say what is enforced. No change to the lint rule, defineStack or the accept set.

Landing order. #27312's text describes the record page v3 that PR #27310 builds. #27310 has merged (eef83734), and this branch carries it through a merge of main at cf218eed1.

维护者速读

  • 改了什么:只改说明文字。relatedLists 和 field group 的 relatedList 说清楚哪些未知名称会被拒;relatedList: 'primary' 和 enable.trackHistory 改成描述 v3 记录页(主表在详情列里,字段变更进时间线,不再有独立页签)。
  • 为什么改:旧文字承诺了运行时并不做的拒绝(包内发布会存下未知子对象名),也还在描述 v2 页面的「独立页签」和「History 页签」。
  • 风险与回滚:零运行时风险,没有键、类型、默认值或导出变化;回滚即还原文字。r1 按契约复核补了两处精确度:平台前缀名(sys_ / cloud_ / ai_)在 os validate / os build 不判、留给运行时;trackHistory 改说「字段变更历史」。
  • 先看:packages/spec/src/data/object.zod.ts(relatedLists 的 describe 与 docblock),packages/spec/src/data/field.zod.ts(relatedList 的 describe),content/docs/ui/pages.mdx(合成记录页一段)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/record-page-describes.test.ts,应见 10 个测试全绿。
  • 需要你做的:无,feat(plugin-detail,app-shell,components): record page v3 layout #27310 已合并,席位落地。

What changes

Description text only. No key, type, default, accepted value or export moves; check:api-surface and check:authorable-surface are green with no artifact change.

#27311: ObjectSchema.relatedLists and fieldGroups[i].relatedList

Measured on origin/main 6d7634a: defineStack (collectRelatedListNameErrors, stack.zod.ts) refuses a name whose child is declared with no inbound FK, and a name whose child is nowhere (sibling-package and platform objects stay unjudged). The runtime door runs validateRelatedListRefs, which refuses a stack child with no inbound FK on every write, and a child missing from the collection only under wholeCollection (reference-integrity-suite.ts: a gated write with no packageScope). A write into a package stores the entry.

Before (relatedLists describe):

An entry that matches no inbound lookup / master_detail field of this object is refused.

After:

An entry whose child is in the stack but has no inbound lookup / master_detail field to this object is refused everywhere. An entry whose child exists nowhere is refused by os validate / os build (which leave a sys_ / cloud_ / ai_ name to the runtime) and by a metadata write that names no package; a write into a package stores it, since a child outside the package may be a downstream package's object that points here, and the record page places nothing for it.

Before (fieldGroups[i].relatedList describe):

A group with relatedList holds no fields: a field that names it as its group is refused, and so is a name that matches no inbound lookup / master_detail field.

After:

A group with relatedList holds no fields: a field that names it as its group is refused. The name is judged as a relatedLists entry is: refused when its child is in the stack with no inbound lookup / master_detail field, or exists nowhere (by os validate / os build, which leave a sys_ / cloud_ / ai_ name to the runtime, and by a metadata write that names no package); a write into a package stores it, and the record page places nothing for it.

Both docblocks say the same, and the relatedLists one points at the lint rule's header for the reasoning. The platform-prefix clause (r1) is measured at stack.zod.ts collectRelatedListNameErrors: if (!fieldsByObject.has(child) && (artifactScope.has(child) || isPlatformObjectName(child))) return;, where isPlatformObjectName is hasPlatformObjectPrefix (PLATFORM_OBJECT_PREFIXES = ['sys_', 'cloud_', 'ai_'], platform-object-names.ts). The runtime door under wholeCollection judges such a name against the registry (validate-related-list-refs.ts: if (!fieldsByObject.has(child) && !options?.wholeCollection) return;).

#27312: relatedList: 'primary' and enable.trackHistory

Measured on #27310's head 349cd5625c:

  • ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts buildDefaultTabs: a list with isBody ?? isPrimary is a record:related_list inside the Details tab, placed by bodyTablePlacement right after the FIRST field section; every other list is a side card in the one Related tab. No per-list tab is emitted. The column tiers (recordPageColumns.ts) draw the same Details content in the details column.
  • Field changes: RecordDetailView.tsx reads sys_audit_log only when enable.trackHistory === true (historyEnabled), and only for the synthesized page; auditEntriesToFeedItems turns them into field_change items of the one timeline stream (recordTimelineStream.ts, filters All / Activity / Discussion / Changes, streamChanges: 'Changes' in en.ts). No History tab is emitted. On an assigned page RecordFieldChangesContext is null, and a record:history block self-fetches the record's sys_activity rows without reading trackHistory.

Before (relatedList describe, the arms that move):

true/absent = shown as a supporting list, a side card beside the record (the shared "Related" tab on the tabbed page); 'primary' = part of the record's main content, drawn in the record's body (the details column, after the field sections) as a full-width table (its own tab on the tabbed page).

After:

true/absent = shown as a supporting list, a side card in the record's related column (the shared "Related" tab on the tabbed page); 'primary' = part of the record's main content, drawn in the record's body (the details column, after its first field section) as a full-width table (inside the Details tab on the tabbed page, with no tab of its own).

Before (enable.trackHistory describe):

Show the record History tab (audit-trail UI). Pair with per-field trackHistory to pick which field diffs are summarized; audit capture itself is always on for compliance

After:

Show the record's field changes (audit-trail UI): on the default record page they join the record's one timeline stream, under its Changes filter. An authored page shows a field-change history only where it places a record:history block. Pair with per-field trackHistory to pick which field diffs are summarized; audit capture itself is always on for compliance

The order asked for "and the History block on an authored page". Measured, record:history does not read trackHistory, so the text says an authored page shows a field-change history where it places that block, and does not say the flag feeds it. r1 narrows "changes" to "a field-change history": an authored record:activity block self-fetches sys_activity and still lists field edits as activity lines (record-activity.tsx), which the docblock now says.

Other sentences the same promises reached

  • content/docs/ui/pages.mdx, "The synthesized default": the tab strip is now Details / Timeline / Related (plus Approvals), a primary list is a table inside Details, attachments close the Related tab, and the timeline is one stream with the Changes filter. The three-column sentence drops "then Activity and History".
  • ui/packages/plugin-detail/README.md (r1), the wide-layout table: the 1280 px timeline column said "the discussion composer on top, then Activity and History"; on main after feat(plugin-detail,app-shell,components): record page v3 layout #27310 it is one stream (buildDefaultTimeline emits one record:activity), so it now reads "one stream: the composer on top, then the discussion, the activity and, with enable.trackHistory, the field changes, filtered by All / Activity / Discussion / Changes". The changeset gains '@object-ui/plugin-detail': patch because its files[] ships README.md.
  • packages/spec/liveness/object.json, row trackHistory: the evidence names the timeline stream instead of the History tab (status unchanged, live).
  • packages/spec/src/system/metadata-form-zod-reconciliation.test.ts: one why string said a 'primary' switch "promotes the list to its own tab"; it now says "places the list in the record's body".
  • Regenerated with pnpm --filter @objectstack/spec check:generated --fix: content/docs/references/{api/metadata,data/field,data/object,system/migration}.mdx. No translation bundle carries these describes (the Studio form's own helpText rows are separate text; see Acceptance notes).

Tests

  • New packages/spec/src/data/record-page-describes.test.ts (10 tests) pins each rewritten describe's load-bearing phrase in the JSON-schema projection, plus the absence of "its own tab" / "History tab" and of the old unbounded refusal sentence.
  • Reverse verification: with object.zod.ts and field.zod.ts restored from origin/main 6d7634a, the file goes red, 10 of 10 (Test Files 1 failed, Tests 10 failed); restored from HEAD, git diff HEAD empty.
  • r1 extends three pins (the platform-prefix clause in both related-list describes, and "a field-change history" in trackHistory). Reverse leg: with object.zod.ts restored from the r0 head e4e06533f, exactly those 3 go red (Tests 3 failed | 7 passed); restored from HEAD, the tree is clean.
  • @objectstack/spec on head 0fae4859d (after the main merge), under the verify lock: build, gen:schema, gen:docs and check:generated (all 14 artifacts up to date), then test (Test Files 750 passed, Tests 22306 passed, 1 todo) and typecheck: VERDICT command-exit 0. Only packages/spec source and one README move, and the public surface is byte-unchanged, so no importer owes a run.

Acceptance notes

Verification

Derived gates on head 0fae4859d (after the main merge and a full pnpm exec turbo run build --concurrency=2, 115/115 tasks): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 106 (the README adds check-sdui-manifest and its self-test); all 106 exit 0. dispatch-gates --ran reconciles: 106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN.

106 gate commands and exit codes
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:api-surface :: exit 0
pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0
pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0
pnpm --filter @objectstack/spec run check:export-origins :: exit 0
pnpm --filter @objectstack/spec run check:exported-any :: exit 0
pnpm --filter @objectstack/spec run check:generated :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:llms-txt :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:skill-refs :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-checklist :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:spec-docblock-symbol-anchors :: exit 0
pnpm check:spec-parsed-alias :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzgaGBuahtvHVCAa4ny

…and trackHistory describe the v3 record page

relatedLists and fieldGroups[i].relatedList now state the enforced
boundary: a stack child with no inbound FK is refused everywhere; a child
that exists nowhere is refused by defineStack and by a metadata write that
names no package; a write into a package stores it and the record page
places nothing for it.

relatedList: 'primary' is a body table in the details column (inside the
Details tab when tabbed, with no tab of its own), and enable.trackHistory
feeds the record's one timeline stream (its Changes filter) instead of a
History tab.

Claude-Session: https://claude.ai/code/session_01WGufzgaGBuahtvHVCAa4ny
Co-Authored-By: Claude <noreply@anthropic.com>
…es the timeline stream; changeset

Claude-Session: https://claude.ai/code/session_01WGufzgaGBuahtvHVCAa4ny
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 5 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/liveness/object.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json cf218eed1fdb22467875dc012e296f47342942e3.

⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/liveness/object.json) — pages documenting those are invisible to this run
  • 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 cf218eed1fdb22467875dc012e296f47342942e3 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8b84c77367c4dfa0f4c8a03b38fd71ff088c8ab0 — the merge of head 0fae4859d8cfb549e435c1ee4a8b9551e942001b into base cf218eed1fdb22467875dc012e296f47342942e3, 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 8b84c77367c4dfa0f4c8a03b38fd71ff088c8ab0 && git checkout 8b84c77367c4dfa0f4c8a03b38fd71ff088c8ab0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cf218eed1fdb22467875dc012e296f47342942e3 0fae4859d8cfb549e435c1ee4a8b9551e942001b && git checkout -B drift-repro cf218eed1fdb22467875dc012e296f47342942e3 && git merge --no-ff 0fae4859d8cfb549e435c1ee4a8b9551e942001b

node scripts/docs-audit/affected-docs.mjs --json cf218eed1fdb22467875dc012e296f47342942e3

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head e4e06533f1b596c975eaec3ae6287d4b26bfd934 · 2026-10-29T02:52Z

Triage seat, taking needs:contract-review on this head (#27062 (a)). The review runs at CONTRACT_REVIEW_TIER. If the lane claims or posts a record on this head first, triage withdraws.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e4e06533f1b596c975eaec3ae6287d4b26bfd934
Local-runs: none
Scope: packages/spec/src/data/field.zod.ts, packages/spec/src/data/object.zod.ts + packages/spec/src/stack.zod.ts (collectRelatedListNameErrors, isPlatformObjectName), packages/lint/src/validate-related-list-refs.ts, packages/lint/src/reference-integrity-suite.ts (validateRelatedListRefs member), packages/lint/src/runtime-gate.ts (runtimeWriteType: args.type), packages/metadata-protocol/src/protocol-authoring-gate.ts + protocol-write-package-scope.ts (resolveWritePackageScope), packages/spec/src/data/record-page-describes.test.ts (the pin), ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts (bodyTablePlacement, buildDefaultTabs, buildDefaultTimeline), ui/packages/plugin-detail/src/synth/recordPageColumns.ts, ui/packages/plugin-detail/src/RecordPageColumns.tsx, ui/packages/app-shell/src/views/RecordDetailView.tsx (historyEnabled, historyPage, timelineFieldChanges, RecordFieldChangesContext.Provider), ui/packages/plugin-detail/src/recordTimelineStream.ts, ui/packages/plugin-detail/src/renderers/record-history.tsx, ui/packages/plugin-detail/src/renderers/record-activity.tsx, ui/packages/i18n/src/locales/en.ts (streamChanges). UI code read on origin/main 8d66cbe0, which carries PR #27310's merge eef83734 (merged 2026-10-29T02:22:42Z, so the landing-order constraint is met).

Inputs read: card #27311 body and all 3 comments; card #27312 body and all 3 comments (gh api GET; the PR fixes both); PR #27352 body, file list (11 files), both PR comments, net diff vs main; 55 check-runs on the head: 42 success, 13 skipped, 0 failure, 0 pending (Test Core 1-8/8, Lint & Repo Gates, Lint · PM dispatch-gates, Check Changeset, Governed Surface Queue Guard, Spec property liveness, PR-body guards all success).

① Derived judgments

  • Accept set: unchanged — right. The two hits hunks touch only .describe() strings and docblocks on FieldSchema.relatedList, ObjectCapabilities.trackHistory, ObjectFieldGroupSchema.relatedList and ObjectSchemaBase.relatedLists. No key, type, default, regex (RELATED_LIST_ENTRY_PATTERN unchanged), union arm or .optional() moves. collectRelatedListNameErrors, validateRelatedListRefs and the suite wiring are byte-unchanged against origin/main. Seat ruling option A (no lint / defineStack / accept-set change) is honoured.
  • Public surface: JSON-schema description text only — right. The regenerated references (content/docs/references/{api/metadata,data/field,data/object,system/migration}.mdx) are the derived output of exactly those four describes; check:generated, check:api-surface, check:authorable-surface are inside the green Lint & Repo Gates / Lint · PM dispatch-gates runs, and no baseline file is in the diff, so the surface artifacts did not move. Right.
  • Liveness ledger: trackHistory row evidence re-cited, status live unchanged — right (Spec property liveness check-run success); the evidence names RecordDetailView historyEnabled + RecordFieldChangesContext, which is what main holds (RDV :2226-2231, :4142).
  • Reconciliation-test why string for relatedList reworded from "promotes the list to its own tab" to "places the list in the record's body" — right against buildDefaultTabs (a body table in Details, no tab emitted per list).
  • New pin test record-page-describes.test.ts (10 cases) pins the load-bearing phrases of the four describes through z.toJSONSchema and the absence of "its own tab" / "History tab" / the old unbounded refusal sentence. It reads the projection every consumer reads, follows $refs and steps into fieldGroups[] items. Right; Test Core green on the head.
  • No Metadata-protocol move (none), no migration entry, no ADR-0087 conversion owed (describe-only). Right.

② Semver level

.changeset/27311-27312-record-page-describes.md: '@objectstack/spec': patch. The diff publishes description text only in @objectstack/spec; no other published package is touched (content/docs/**, packages/spec/liveness/**, test files are unpublished). Patch is the correct level. Clause-②: no on the PR body and in the changeset agree with each other and with the diff: no accepted-value narrows, no refusal is added or removed, every stack that parsed / validated / published before still does. Check Changeset (both runs) and check-changeset-no-major are success. Clause-②: no — correct.

③ Boundary flags

open_questions: [] in the dev report. Dev flags, each judged against the tree:

  1. Deviation from the spec: the describe() of relatedList 'primary' and of enable.trackHistory still narrate the v2 record page (own tab, History tab) after the v3 layout #27312 order ("trackHistory feeds ... on an authored page, the History block"): the dev measured that record:history does not read trackHistory and wrote the text accordingly. Verified on main: ui/packages/plugin-detail/src/renderers/record-history.tsx :11-17 self-fetches sys_activity via useRecordContext and never reads enable.trackHistory (no trackHistory hit in any non-test plugin-detail file); RDV :2314 "an assigned page is rendered as authored, and its record:history reads its own source". The deviation is correct and the text is the honest one. Answered.
  2. Skills still narrate v2 / unbounded refusal (skills/objectstack-ui/SKILL.md :109, :119 "its own tab"; skills/objectstack-data/rules/relationships.md :353 "a name matching no inbound FK is refused") — confirmed present on the head. skills/** is Tier H and not in this PR's surface; the describes and the skills now disagree until the follow-up lands. Escalated: the lane / PM owes a governed Tier H follow-up card (dedupe words in the dev report); not a defect of the hits files.
  3. ui/content/docs/guide/slotted-pages.md :130-132, :138 — the dev left it to PR feat(plugin-detail,app-shell,components): record page v3 layout #27310. feat(plugin-detail,app-shell,components): record page v3 layout #27310 has merged, and on origin/main the file STILL says "→ History; so promoting a child table to its own tab is a one-word change" and "(the discussion composer, then Activity and History)". feat(plugin-detail,app-shell,components): record page v3 layout #27310 did not fix it. Escalated: a ui-docs follow-up card is owed (this file is outside packages/spec and outside this PR's hits; it is the carrier's gap, not this PR's).
  4. Landing order ("lands only after feat(plugin-detail,app-shell,components): record page v3 layout #27310 merges"): satisfied — feat(plugin-detail,app-shell,components): record page v3 layout #27310 merged at 02:22:42Z; the PR's UI claims hold on the post-merge main (read directly, see ④). No remaining serial constraint.
  5. Studio form help (object.form.ts relatedLists "saving or validating the object refuses it") kept because Studio's pre-write check (feat(app-shell,i18n): Studio orders an object's related lists, and a field group can place a related table #27303) refuses such a name for its reader — plausible and outside hits; the lane's accept.
  6. Lane-remainder observation (not reviewed here, for the lane's ACCEPT): content/docs/ui/pages.mdx now says the strip is "Details, Timeline and Related, plus Approvals"; buildDefaultTabs places Timeline second only for a worked object (recordTimelineDefaultReason(def) !== null) and otherwise LAST, after Approvals. The sentence names the tabs, not their order, so it is not false; the lane may want "in that order for a worked object" if it cares. packages/spec/liveness/field.json :504 relatedList note still carries "its own tab on the tabbed page" inside a dated 2026-10-28 (spec: an object declares the business order of its related lists — relatedLists: ['task', 'event', …], an ordered list of child relationships like highlightFields is of fields; unlisted ones follow the derived order; a name that matches no inbound relationship is refused #27069) re-cite — a historical note, not a claim about the tree; harmless.

④ Design judgment

packages/spec/src/data/object.zod.ts — relatedLists describe + docblock, fieldGroups[i].relatedList describe + docblock (against card #27311's ruling, option A, and their first consumers: collectRelatedListNameErrors at defineStack, validateRelatedListRefs at the runtime door).

Each clause of the new text, read against the live code:

  • "a child that IS in the stack but has no inbound lookup / master_detail field to this object is refused everywhere: at defineStack (os validate, os build) and at the runtime metadata write door" — stack.zod.ts :3917 if (!fieldsByObject.has(child) && (artifactScope.has(child) || isPlatformObjectName(child))) return; → a child in fieldsByObject (stack objects + every packages[].manifest.objects + extensions) with no matching relationship falls through to the error. validate-related-list-refs.ts :147 if (!fieldsByObject.has(child) && !options?.wholeCollection) return; → a child the snapshot holds is judged regardless of wholeCollection, i.e. under a package closure too. collectRelatedListNameErrors runs before the object-count early return (:4434) so package-body-only artifacts are judged. os validate / os build load the config through defineStack (validate.ts :258, compile.ts :288). Holds.
  • "a child that exists nowhere is refused at defineStack and by a metadata write that names no package (judged against the whole collection the host serves)" — defineStack: a child in neither fieldsByObject, artifactScope nor platform-prefixed is refused. Runtime: suite member :414 passes { wholeCollection: true } iff ctx.runtimeWriteType !== undefined && !ctx.packageScope; runtime-gate.ts :1126 sets runtimeWriteType: args.type on every gated write; protocol-authoring-gate.ts :390/:439 sets packageScope from resolveWritePackageScope(evt.packageId), which returns undefined when the write names no package (or sys_metadata, or a package the registry does not know). So "names no package" ⇔ whole-collection judgment. Holds.
  • "A write into a package judges only that package's closure, and a child outside it may be a downstream package's object that points in, so the entry is stored, and the record page places nothing for it. ... the validate-related-list-refs rule's header in @objectstack/lint carries the reasoning." — the header (:28-39) says exactly this; deriveRelatedLists builds lists from FKs, so an entry with no matching FK places nothing. Holds. This is the card's observed behaviour, now stated instead of contradicted.
  • fieldGroups[i].relatedList "is judged exactly as a relatedLists entry's is" — both judges walk fieldGroups[i].relatedList through the same judge closure (stack.zod.ts :3927-3932, lint :160-165). Holds.

Attempts to break it:

  • Platform-prefixed misspelling. An entry sys_tsak whose child exists nowhere: isPlatformObjectName is a PREFIX test (hasPlatformObjectPrefix, platform-object-names.ts :192), so defineStack leaves it unjudged, while the whole-collection runtime door refuses it. The describe's "exists nowhere is refused by os validate / os build" is therefore one edge wider than the gate; the stack.zod.ts docblock records the platform exemption but the new relatedLists describe / docblock do not. Pre-existing boundary, deliberate (hard throws vs third-party prefixed objects), narrow. should-fix (precision; a clause "a platform-prefixed name is left to the runtime" would close it). Not a FAIL: the old text promised an unbounded refusal; the new text is true on every surface for every non-prefixed name.
  • "In the stack" under a package write. The runtime snapshot's objects under a closure is that closure, so a child that lives in a sibling package of the same host with no FK is also stored. The docblock covers it ("judges only that package's closure"); the describe's "a child outside the package may be a downstream package's object that points here" reads as the same case. Acceptable; a nit, no action needed.
  • Does the gate run on a package publish at all? The card measured it does (the nope entry was stored, not skipped). The suite member is runtimeTypes: ['flow', 'object'], so object writes reach it. Consistent.

packages/spec/src/data/object.zod.ts — enable.trackHistory describe + docblock (against card #27312 and first consumer RecordDetailView + the timeline stream):

  • "on the default record page they join the record's one timeline stream, under its Changes filter" — RDV :2226-2231 historyEnabled requires enable.trackHistory === true; :2320 historyPage = historyEnabled && !assignedPage && ... (synthesized page only); :3535 auditEntriesToFeedItems(historyEntries) → field_change items; :4142 RecordFieldChangesContext.Provider value={assignedPage ? null : timelineFieldChanges}; recordTimelineStream.ts :105-120 filters all | activity | discussion | changes, changes keeps field_change; en.ts :1523 streamChanges: 'Changes'; buildDefaultTimeline emits one record:activity, no History tab anywhere in buildDefaultTabs. Holds.
  • "An authored page shows changes only where it places a record:history block" / docblock "which reads the record's own activity, not this flag" — record-history.tsx self-fetches sys_activity field-mutating rows, never reads trackHistory; on an assigned page the context is null, so a record:activity block shows no trackHistory field diffs there. Holds. One precision nit: a record:activity block on an authored page still self-fetches sys_activity and draws field-mutating rows as activity lines (and unifiedTimeline: false only filters field_change items out, adds none), so "only" is a shade strong for a reader who counts those lines as "changes". should-fix at most (e.g. "shows the audit-trail changes only where ..."); the docblock already says the block reads the record's own activity, so the contract is not misleading.
  • "Audit capture ... stays on" unchanged from main and still true (sys_audit_log write path unconditional per the liveness row and [spec/cli] 对象 enable.files/trackHistory/activities/feeds 为 dead property:schema 接受、liveness 仅告警不拦 #2707). Not moved by this PR.

packages/spec/src/data/field.zod.ts — relatedList describe + docblock (against card #27312 and first consumer buildDefaultTabs / bodyTablePlacement / RecordPageColumns):

Pin test record-page-describes.test.ts (the gate for the rewritten rule, read as one change): pins phrases, not sentences; projects through z.toJSONSchema with io: 'input', follows $ref into fieldGroups[].items; negative pins on the two v2 phrases and the old unbounded sentence. It would go red on a revert of either hits file (the dev's reverse leg), and green CI on the head confirms the forward leg. Sound.

Must-fix: none. Should-fix: (a) relatedLists / fieldGroups[i].relatedList text does not name the platform-prefixed exemption at defineStack; (b) trackHistory "only where it places a record:history block" is a shade strong for an authored record:activity block's own activity lines; (c) pre-existing, carried-unchanged field-group placement sentence runs ahead of the UI's #27069 fence. None reverses a stated behaviour or re-promises a refusal the gate does not make; each is a precision edge narrower than the text it replaces.

Implemented-by: claude/project-thread-1zx7zi
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

…ackHistory narrows to a field-change history; plugin-detail README timeline column is the one stream

Regenerates the references from the merged tree.

Claude-Session: https://claude.ai/code/session_01WGufzgaGBuahtvHVCAa4ny
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 0fae4859d8cfb549e435c1ee4a8b9551e942001b · 2026-10-29T06:53Z

Triage seat, taking needs:contract-review on this head (#27062 (a)); a delta re-review from triage's PASS on e4e06533. The review runs at CONTRACT_REVIEW_TIER. If the lane claims or posts a record on this head first, triage withdraws.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0fae4859d8cfb549e435c1ee4a8b9551e942001b
Local-runs: none
Scope: packages/spec/src/data/field.zod.ts, packages/spec/src/data/object.zod.ts + packages/spec/src/stack.zod.ts (collectRelatedListNameErrors, isPlatformObjectName, the artifactScope call site), packages/spec/src/system/constants/platform-object-names.ts (PLATFORM_OBJECT_PREFIXES, hasPlatformObjectPrefix), packages/lint/src/validate-related-list-refs.ts, packages/lint/src/reference-integrity-suite.ts (validateRelatedListRefs member, wholeCollection predicate), packages/lint/src/authoring-rules.ts (suite wiring), packages/cli/src/commands/{validate,compile}.ts (what os validate / os build import from @objectstack/lint), packages/spec/src/data/record-page-describes.test.ts (the pin), .changeset/27311-27312-record-page-describes.md, ui/packages/plugin-detail/README.md + package.json (files[]), ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts (bodyTablePlacement, buildDefaultTabs, buildDefaultTimeline), ui/packages/plugin-detail/src/synth/recordPageColumns.ts, ui/packages/plugin-detail/src/synth/relatedArrangement.ts + useRelatedArrangement.ts (new on main since the prior record), ui/packages/app-shell/src/views/RecordDetailView.tsx (historyEnabled, historyPage, RecordFieldChangesContext), ui/packages/app-shell/src/utils/deriveRelatedLists.ts (resolveRelatedListOrder), ui/packages/plugin-detail/src/renderers/record-history.tsx (HISTORY_TYPES), ui/packages/plugin-detail/src/renderers/record-activity.tsx, ui/packages/plugin-detail/src/renderers/recordActivityFeed.ts (ACTIVITY_TYPE_TO_FEED_TYPE, activityRowToFeedItem), ui/packages/plugin-detail/src/recordTimelineStream.ts (filterTimelineStream), ui/packages/i18n/src/locales/en.ts (streamChanges). UI code re-read on origin/main bbb49c9f (which carries #27310's merge and the post-e4e06533 moves to the cited consumer files); spec/lint/cli code read on the head.

Inputs read: card #27311 body and all 3 comments; card #27312 body and all 3 comments (gh api GET); card #27354 (the Tier H follow-up the PR body names) exists, open, pm:blocked; PR #27352 body, file list (12 files), all 4 PR comments, net diff vs main (verified against the merge base cf218eed1: the same 12 files); the prior PASS record on e4e06533; the delta (d859f3a649 merge of origin/main cf218eed1, then 0fae4859d8); 74 check-runs on the head: 60 success, 13 skipped, 1 cancelled, 0 failure, 0 pending. The cancelled run is one of three Check Changeset runs on this same head (06:12:08Z, superseded by the 06:13:37Z success run; the 05:28:56Z run is also success), so the gate verdict is green. Test Core 1-8/8, Test UI 1-16/16, Lint & Repo Gates, Lint · PM dispatch-gates, Governed Surface Queue Guard, Spec property liveness, Build Docs, PR-body guards all success.

Delta since the prior PASS, judged commit by commit.

  • d859f3a649 (merge of origin/main cf218eed1): recomputed with git merge-tree --write-tree e4e06533 cf218eed1 → tree 7d9f88a7…; the committed merge tree is f0a1cf34…. The gap is ONE file, content/docs/references/data/field.mdx: the committed merge kept a CurrencyValue section (+ its import lines and the frontmatter description) that an automatic merge drops. A hand-resolved GENERATED file. It is corrected in the same push: 0fae4859d8 regenerates field.mdx and removes exactly that block (the 14 deletions in its stat), so head-vs-main on that file is the one relatedList row, and check:generated is inside the green Lint & Repo Gates on this head. No hits file is touched by the merge; the merge brings main's moves to the cited UI consumers (buildDefaultPageSchema.ts, recordPageColumns.ts, new relatedArrangement.ts / useRelatedArrangement.ts), re-read below. Right, with the hand-resolution noted and neutralised.
  • 0fae4859d8: eight files. In the hits files: (i) both related-list describes and docblocks gain the platform-prefix clause ("which leave a sys_ / cloud_ / ai_ name to the runtime"; docblock names hasPlatformObjectPrefix); (ii) trackHistory describe narrows "shows changes only where" to "shows a field-change history only where", and its docblock gains "(a record:activity block there lists field edits only as activity lines)". Outside hits: the changeset gains '@object-ui/plugin-detail': patch and a README bullet; the plugin-detail README's 1280 px row becomes the one-stream sentence; the pin test's three affected assertions move with the text; the four references regenerate. The delta is exactly what its message says.

Prior should-fixes: (a) platform-prefix exemption unnamed — resolved exactly (both describes, both docblocks, and the pin). (b) trackHistory "only where it places a record:history block" a shade strong — not resolved: the sentence was narrowed to "a field-change history" but the "only" is still false on the tree, and the r1 docblock parenthetical is wrong on type (④). (c) field-group placement sentence runs ahead of the #27069 UI fence — unchanged (pre-existing, carried, out of these cards' scope; still should-fix for a follow-up, not of this PR). Prior ③-escalation 3 (ui/content/docs/guide/slotted-pages.md) — the file is absent from origin/main (retired by #27329), nothing owed.

① Derived judgments

  • Accept set: unchanged — right. The hits hunks touch only .describe() strings and docblocks on FieldSchema.relatedList, ObjectCapabilities.trackHistory, ObjectFieldGroupSchema.relatedList, ObjectSchemaBase.relatedLists. No key, type, default, regex (RELATED_LIST_ENTRY_PATTERN), union arm or .optional() moves. collectRelatedListNameErrors, isPlatformObjectName, validateRelatedListRefs and the suite member are not in the diff. Seat ruling option A honoured.
  • Public surface: JSON-schema description text only — right. The four regenerated references are the derived output of the four describes; check:generated, check:api-surface, check:authorable-surface sit inside the green gates; no baseline file is in the diff. The hand-resolved merge of field.mdx was regenerated away in the same head (above). Right.
  • Published-file claim for @object-ui/plugin-detail — right. package.json files[] is ['dist', 'README.md', 'CHANGELOG.md', 'LICENSE'], so the README row IS a published change and owes a release line; a root .changeset naming @object-ui/* is this repo's convention (142 existing changesets do). The 1280 px row reads one stream, "with enable.trackHistory, the field changes, filtered by All / Activity / Discussion / Changes" — buildDefaultTimeline emits one record:activity; TIMELINE_STREAM_FILTERS = ['all','activity','discussion','changes']; RecordDetailView historyEnabled requires enable.trackHistory === true. Right.
  • Liveness ledger trackHistory row evidence re-cited, status live — unchanged since the prior record; Spec property liveness success. Right.
  • Reconciliation-test why string — unchanged since the prior record. Right.
  • Pin test (10 cases, three assertions moved with the text): projects through z.toJSONSchema with io: 'input', follows $ref, steps into fieldGroups[].items; negative pins on "its own tab" / "History tab" / the two old unbounded sentences. It pins the r1 phrases exactly, including the trackHistory "only where it places a record:history block" phrase — so the should-fix in ④ must move the pin with the text. Right as a gate for the text it pins.
  • No Metadata-protocol move, no migration entry, no ADR-0087 conversion owed (describe-only). Right.

② Semver level

.changeset/27311-27312-record-page-describes.md: '@objectstack/spec': patch, '@object-ui/plugin-detail': patch. @objectstack/spec publishes description text only; @object-ui/plugin-detail publishes a README row (in its files[]). Two patches are the correct levels, and the second one is NEW in this delta and owed (the prior head did not touch a published UI file). Clause-②: no on the PR body and in the changeset agree with each other and with the diff: no accepted-value narrows, no refusal added or removed, every stack that parsed / validated / published before still does. Check Changeset success on the governing run; check-changeset-no-major inside green gates. Clause-②: no — correct.

③ Boundary flags

open_questions: [] in the dev report (unchanged; the r1 push posted no new report). Each flag against the tree on this head:

  1. Deviation from the spec: the describe() of relatedList 'primary' and of enable.trackHistory still narrate the v2 record page (own tab, History tab) after the v3 layout #27312 order (trackHistory "feeds … the History block on an authored page"): record-history.tsx self-fetches sys_activity via useRecordContext, maps HISTORY_TYPES = {created, updated, assigned, shared, deleted}, never reads trackHistory; RDV :2320 historyPage = historyEnabled && !assignedPage && …. Not saying the flag feeds that block is correct. Answered. What the r1 text says INSTEAD about authored pages is judged in ④.
  2. Skills still narrate v2 / unbounded refusal — confirmed present on the head (skills/objectstack-ui/SKILL.md :109, :119 "own tab"; skills/objectstack-data/rules/relationships.md :353 "a name matching no inbound FK is refused"). Tier H, outside this PR's surface; the follow-up is carded as skills: objectstack-ui and objectstack-data still teach a primary related list's "own tab" and an unbounded relatedLists refusal after the spec describes moved (#27352) #27354 (open, domain:skills, pm:blocked). Escalated and now carded; not a defect of the hits files.
  3. ui/content/docs/guide/slotted-pages.md — absent from origin/main (retired by docs(ui-merge): fold objectui.org into objectstack docs, K4 — retire the site, host 301s, links #27329). Closed, nothing owed.
  4. Landing order — feat(plugin-detail,app-shell,components): record page v3 layout #27310 merged; the head carries main through cf218eed1. Satisfied.
  5. Studio form help (object.form.ts) kept — outside hits; the lane's ACCEPT.
  6. Merge hand-resolution of a generated file (new this round, mine): the committed merge of field.mdx kept a CurrencyValue block an automatic merge drops; the same push regenerated it. Nothing remains on the head, and check:generated is green. Answered by the tree; noted so the next reviewer of this branch recomputes the merge as I did.
  7. Lane-remainder observations (not reviewed, for the lane's ACCEPT): content/docs/ui/pages.mdx, packages/spec/liveness/object.json, the reconciliation test, the generated references, the README row and the changeset body are byte-for-byte the lane's. buildDefaultTabs' own docblock on main still says "Emits Details / Related / Activity / History tabs" and relatedArrangement.ts's header says the authored relatedLists order "is not read here or anywhere on this page yet" while deriveRelatedLists.ts :427 reads it — stale UI comments, not this PR's.

④ Design judgment

packages/spec/src/data/object.zod.ts — relatedLists describe + docblock, fieldGroups[i].relatedList describe + docblock (against card #27311's ruling, option A, and first consumers collectRelatedListNameErrors at defineStack, validateRelatedListRefs at the runtime door).

  • "a child in the stack but with no inbound lookup / master_detail field to this object is refused everywhere" — stack.zod.ts judge: a child in fieldsByObject (stack objects + every packages[].manifest.objects + objectExtensions) with no matching relationship falls through to the error. validate-related-list-refs.ts judge: if (!fieldsByObject.has(child) && !options?.wholeCollection) return; → a child the snapshot holds is judged under a package closure too. Holds.
  • "exists nowhere is refused by os validate / os build (which leave a sys_ / cloud_ / ai_ name to the runtime)" — NEW this round. stack.zod.ts :3918 if (!fieldsByObject.has(child) && (artifactScope.has(child) || isPlatformObjectName(child))) return;; isPlatformObjectName = hasPlatformObjectPrefix, a startsWith test over PLATFORM_OBJECT_PREFIXES = ['sys_', 'cloud_', 'ai_'] (platform-object-names.ts :181-195). Is defineStack the ONLY judge on the CLI, so that the exemption is the whole CLI story? validate.ts / compile.ts import runAuthoringRules from @objectstack/lint, which carries the reference-integrity suite; the suite member passes { wholeCollection: true } iff ctx.runtimeWriteType !== undefined && !ctx.packageScope, and the CLI never sets runtimeWriteType (suite comment :331-333, confirmed by a grep of packages/cli for wholeCollection / runtimeWriteType: none). So on the CLI the lint member leaves every child outside fieldsByObject unjudged and defineStack's prefix exemption is the only verdict. Holds. The sibling-package case (artifactScope, DefineStackOptions.artifactObjects) is also left unjudged, but such a child exists in a sibling package, so "exists nowhere" correctly excludes it.
  • "and by a metadata write that names no package" / docblock "the runtime door, which sees the registry, judges it" — the runtime rule has NO prefix exemption: under wholeCollection a sys_tsak child missing from the collection is refused. Holds, and it closes prior should-fix (a) exactly.
  • "a write into a package stores it … the record page places nothing for it" — unchanged from the prior head; rule header :28-39 states the boundary; deriveRelatedLists builds lists from FKs. Holds.
  • fieldGroups[i].relatedList "judged as a relatedLists entry is" — both judges walk fieldGroups[i].relatedList through the same closure. Holds.

Attempts to break it: a prefixed misspelling (sys_tsak) — now said correctly on both surfaces. A prefixed name that a platform package DOES provide and that IS registered at the host — fieldsByObject.has(child) is true at the runtime door, so it is judged on its FK like any stack child; the text's "refused everywhere" for a stack child with no FK covers it. A prefixed name in a stack whose own objects declare it (e.g. a stack that ships its own sys_* object) — fieldsByObject.has(child) is true at defineStack too, so the exemption does not apply and the FK is judged; the text's "leave a sys_ name to the runtime" is read as the "exists nowhere" arm only, which is where the clause sits. Nothing breaks. No finding.

packages/spec/src/data/object.zod.ts — enable.trackHistory describe + docblock (against card #27312 and first consumer RecordDetailView + the timeline stream).

  • "on the default record page they join the record's one timeline stream, under its Changes filter" / docblock "that page has no separate History tab" — RDV :2226-2230 historyEnabled requires enable.trackHistory === true; :2320 synthesized page only; auditEntriesToFeedItems → field_change; filterTimelineStream changes keeps field_change; en.ts :1523 streamChanges: 'Changes'; buildDefaultTabs emits Details / Timeline / Related / Approvals, no History. Holds.
  • "An authored page shows a field-change history only where it places a record:history block" (describe) and "(a record:activity block there lists field edits only as activity lines)" (docblock, NEW this round) — read against record-activity.tsx + recordActivityFeed.ts on main: on an assigned page RecordFieldChangesContext is null (RDV :4142), so the flag's sys_audit_log diffs do not reach a record:activity block there — that part is right. But the block SELF-FETCHES sys_activity (record-activity.tsx :22-23, :223) and ACTIVITY_TYPE_TO_FEED_TYPE (recordActivityFeed.ts :138-143) maps created / updated / deleted / assigned / shared rows to field_change items carrying changedFields (:1004-1012); filterTimelineStream keeps them under changes and EXCLUDES them under activity (:120-122); applyFeedConfig drops them only when the author writes unifiedTimeline: false (:1059-1065). record-history.tsx builds its history from the SAME five sys_activity types (HISTORY_TYPES, :62). So on an authored page a record:activity block shows the same field-change rows a record:history block shows, typed as changes, not "as activity lines". The describe's "only where it places a record:history block" is false on the tree, and the r1 parenthetical is wrong on type. The flag's own contract (what trackHistory: true adds, and where) stays true; the error is a wrong prescription about a neighbouring block. should-fix (prior (b), not resolved): say "An authored page shows a field-change history where it places a record:history or record:activity block; both read the record's own activity, not this flag", drop the "activity lines" parenthetical, and move the pin phrase with it. Not a FAIL: it neither re-promises the v2 tab nor claims the flag feeds a block it does not feed; the old text ("Show the record History tab") was false on every surface, and the new text is false on one clause about another block.
  • "Audit capture … stays on regardless of this flag" — unchanged. Not moved.

packages/spec/src/data/field.zod.ts — relatedList describe + docblock (against card #27312 and first consumer buildDefaultTabs / bodyTablePlacement / the column tier — RE-READ on origin/main bbb49c9f, because the merge in this delta moved the consumer files).

  • "true/absent = a side card in the record's related column (the shared "Related" tab on the tabbed page)" — buildDefaultTabs :995-997, :1021: sides = related.filter(!isBody) → one Related tab; recordPageColumns.ts draws the Related tab's children in the related column. Holds.
  • "'primary' = drawn in the record's body (the details column, after its first field section) as a full-width table (inside the Details tab on the tabbed page, with no tab of its own)" — isBody = rel.isBody ?? rel.isPrimary === true (:995); bodyTablePlacement :926-940 splits the details node around the tables after resolved.slice(0, 1), and with one section or none the tables follow the single details node; the Details tab is the only place a body table is emitted; no per-list tab anywhere. The merge-introduced reader arrangement (relatedArrangement.ts, useRelatedArrangement.ts) reorders / collapses / hides related CARDS and has no isBody / isPrimary read, so a body table's placement is untouched by it. Holds on the current consumer.
  • "'primary' places a list and does not order it: the parent object's relatedLists orders …" — carried unchanged; deriveRelatedLists.ts :427 resolveRelatedListOrder([...owned, ...referenced], objectDef.relatedLists) reads the authored order (the relatedArrangement.ts header saying otherwise is a stale comment). Holds.
  • Carried-unchanged sentence "A field group's relatedList can place it among the field sections instead" — bodyTablePlacement's FENCE (:919-924) still says the group entry "is not read here", and no UI file reads fieldGroups[].relatedList for placement. Prior should-fix (c), pre-existing, not asked of these cards, unchanged by this hunk. should-fix, out of this card's scope, for the lane / PM to hold against the spec: an object declares the business order of its related lists — relatedLists: ['task', 'event', …], an ordered list of child relationships like highlightFields is of fields; unlisted ones follow the derived order; a name that matches no inbound relationship is refused #27069 UI read point.

Pin test (the gate for the rewritten rules, read as one change): pins phrases, not sentences; every r1 phrase is pinned, so the ④ should-fix on trackHistory must move one assertion with the text. Sound as a gate for what it pins; Test Core green on the head.

Must-fix: none. Should-fix: (b) trackHistory's authored-page clause ("only where it places a record:history block") and the r1 docblock parenthetical ("only as activity lines") do not hold against record-activity.tsx's self-fetch, which shows the same sys_activity field-change rows typed field_change; fix the two sentences and the pin together. (c) pre-existing, carried-unchanged field-group placement sentence runs ahead of the UI's #27069 fence — a follow-up, not this PR. Prior (a) is closed exactly. Neither remaining item reverses a stated behaviour, re-promises a refusal the gate does not make, or re-promises the v2 tabs.

Implemented-by: claude/project-thread-1zx7zi
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

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