Skip to content

fix(spec): record:activity's props row names items / loading as the host feed slot - #21422

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21279-record-activity-host-feed-guidance
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21279-record-activity-host-feed-guidance

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #21279
Clause-②: no

What changes

RecordActivityProps, the ComponentPropsMap['record:activity'] row, gains a guidance table that names items and loading. It uses the shape RecordHistoryProps already uses for entries / loading. Both keys stay undeclared, so the accept set does not change. Only the unrecognized-key refusal changes: it now names them as the host's feed slot and gives the remedy, which is to omit them. No new vocabulary.

  • packages/spec/src/ui/component.zod.ts: the two guidance entries, plus a docblock that records the renderer read points and the record:chatter feed mount.
  • packages/spec/src/ui/component-record-blocks.test.ts: pins through the row (below).
  • .changeset/21279-record-activity-host-feed-guidance.md: an @objectstack/spec patch, message text only.

Premise check (base 4b20c84748)

  1. Before. RecordActivityProps was strictObject({ surface, history, guidanceSets }, …) with no guidance map. Measured through the row: { items: [ … ] } gave one unrecognized_keys issue at path [] with keys ["items"], and the message was only the generic line: "Unrecognized key(s) on this record:activity: items. Until this shape was closed, …". loading got the same, and so did feed.items on record:chatter.
  2. The renderer reads both keys. objectui at the .objectui-sha pin 89cad75d5570, packages/plugin-detail/src/renderers/record-activity.tsx:
    • :161-166 reads items as an array, off the node or out of the properties bag, and loading as schema.loading ?? bag.loading.
    • :265-266 the host loading wins over the discussion context's flag and over the self-fetch state.
  3. Generated reference page. check:docs (inside check:generated) is green with nothing regenerated. The reference page does not lift guidance text, so no generated file is in this diff.

After

Through ComponentPropsMap['record:activity'], the same single unrecognized_keys issue (keys ["items"]) now carries:

Unrecognized key(s) on this `record:activity`: `items`.
  • `items` is not authorable surface. On a standalone `record:activity` it is the HOST's data channel: a host composing that block in code passes the feed it already owns, and the renderer presents it in place of its own sources; hand-authored items would ship a static snapshot of the feed that never updates. On a `record:chatter` / `record:discussion` `feed` nothing reads it. Omit it: the block presents the record page's discussion feed, and a standalone `record:activity` with no discussion context self-fetches the record's own `sys_activity` rows. Until this shape was closed, …

loading: "loading is not authorable surface. On a standalone record:activity it is the host's fetch state for its items feed and wins over the block's other loading sources, so authored true pins the loading state on forever. On a record:chatter / record:discussion feed nothing reads it. Omit it with items: each block takes its loading state from its own source."

(Round 2, head ab58157585: both bullets now state who reads the key on each mount, because the table is one flat map shared by the standalone row and the record:chatter / record:discussion feed. See Review round 1.)

Control: the record:history entries / loading messages are byte-identical before and after. I captured both probe outputs and the diff of the history section exits 0.

Pins (component-record-blocks.test.ts, through the row)

  • items (a feed and []) and loading (true and false) are each refused with exactly one issue: code unrecognized_keys, path [], keys the one key. The refusal is about the key, not a value domain.
  • The items message carries the named first sentence, the omit remedy and sys_activity. The loading message carries its named first sentence and "Omit it with items".
  • CONTROL: an unrelated unknown key on the row keeps the generic refusal, with no host-channel line.
  • The record:chatter and record:discussion feed, which is the same object, still refuses both keys at path ["feed"], and (round 2) the message carries the mount-true clause "On a record:chatter / record:discussion feed nothing reads it." with "Omit it".
  • The existing record:history pins are the unchanged control.

Reverse verification (ablation)

The ablation ran from the committed fix, with node scripts/ablation-replace.mjs wrapping the run. It changed the literal anchor guidance: { + items: 'items is the HOST to guidanceAblated: { …, which disables the whole table.

  • Mutation landed: anchor 1 to 0, blob a301b1717b3a to a2e40a0d5943.
  • vitest run src/ui/component-record-blocks.test.ts: 2 failed / 30 passed. The two named-message pins went red. The other three pins (refusal, control, chatter feed) stayed green, as they should: the ablation removes the message and leaves the accept set alone.
  • Restored: blob equals HEAD (a301b1717b3a), git diff HEAD is empty, and git status is clean.
  • Direction observed: red, the ordinary direction.

Verification (head b6224772eb)

  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 600 files, 17611 passed, 1 todo, exit 0. Before the merge, on 29f3682ea5: 599 files, 17578 passed.
  • pnpm --filter @objectstack/spec typecheck: exit 0. That covers tsc --noEmit, check:scripts-typecheck and check:test-typecheck. The edited test file is in tsconfig.test.json's program (--listFilesOnly: 1 hit) and has no debt-ledger entry.
  • pnpm --filter @objectstack/spec build && … check:generated: all 15 generated artifacts up to date. Nothing was regenerated.
  • node scripts/pm/check-widening-tells.mjs --declaration no --diff: exit 0. component.zod.ts was judged against a declared surface and no widening tell fired. The changeset and the test file are NOT MEASURED by construction. Clause-②: no holds.
  • Gate union from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (85 commands), each run on this head with its exit code captured before any pipe. --ran reconciliation: 83 exit 0, 1 NOT MEASURED, 1 unrun.
    • NOT MEASURED: check:dual-build-cjs-loads. It exited 3, PREREQUISITE NOT MET: it needs a whole-workspace build, and 63 of 77 build tasks are turbo cache misses here.
    • Unrun on this head: check:type-check-debt. Its --re-measure builds the whole workspace closure and then type-checks every package. On 29f3682ea5 it passed its self-test and coverage leg, then hit my 300 s per-command cap during the closure build. That run left partial dist/ trees in this worktree, which I removed before the rerun. It does not fit the foreground cap on this shared box, so it is declared to CI.
    • The diff adds no file and edits no tsconfig.
  • Lint, narrowed and proven: eslint --no-inline-config --format json on the two edited TS files gives 2 files, 0 errors, 0 warnings.
    • Population: both are in eslint's own population (ESLint.isPathIgnored is false for both).
    • Invariance: eslint.config.mjs never enables type-aware linting (0 projectService, no parserOptions.project; its own comment at :327-328 says so). So this diff cannot move the verdict on any untouched file. Repo-wide pnpm lint is CI's.

Acceptance notes

  • The record:chatter / record:discussion feed mount (superseded in round 2). Round 1 left the host-channel sentence describing the standalone block only, and declined chatter-mount wording as splitting one shape. The at-tier record 5954789199 (FAIL) overruled that: the guidance table is one flat map that cannot be scoped per mount, so the fix is wording true on both. Round 2 (ab58157585) states each mount.
  • PROPS_HISTORY's shared tail ("…was not read there…") is pre-existing-false for host-channel keys the renderer does read, on record:history and here. Not owed by this PR (record 5954789199 ③); the seat carries it.
  • main merged once. Merged fa7b565212 (merge b6224772eb). No conflict, none of this diff's files, no os-regen deferral. After the merge I ran a frozen install, a spec rebuild and check:generated, then the full spec suite and the gate union on the merge head. main has moved since, to 41a3c8df15: a comment-only edit in view.zod.ts with no overlap, so I did not merge again.

Review round 1

  • At-tier contract review FAIL on b6224772eb (5954789199), on one point: the new bullets were false on the record:chatter / record:discussion feed mount. Everything else (accept set unchanged, record:history byte-identical, patch / Clause-②: no) was judged right.
  • Patch round 2 (ab58157585, +47 / -29 in the same 3 files): the bullets are reworded for both mounts, the chatter pin asserts the mount-true clause, and the changeset sentence is corrected. Two ablations each turn the expected pins red, and the restore is proven (dev report 5955605217).

Generated by Claude Code

claude added 3 commits October 2, 2026 12:49
… host feed slot

`RecordActivityProps` gains a `guidance` table in the shape
`RecordHistoryProps` already uses for `entries` / `loading`: the
renderer takes both keys as a host-supplied feed, so an authored one
is a static snapshot (or a loading state pinned on). Both keys stay
refused; only the unrecognized-key message now names them and the
omit remedy.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see

Coarse fallback — 138 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 39a912ea73ddff7fc85ebb3379a8ce74ff1343f5 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 39a912ea73ddff7fc85ebb3379a8ce74ff1343f5

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b6224772ebaf0bf3366ab4c5b885949906a3c51c
Local-runs: none

Inputs: card #21279 (body; comments 5944174224, 5952583094, 5954557256), PR #21422 (body, file list, net diff origin/main...b6224772eb, 3 files, +100/-0), the head's check-runs, and objectui at the .objectui-sha pin 89cad75d5570 (record-activity.tsx, record-chatter.tsx, record-history.tsx, record-feed-host-slots-11321.test.ts), read-only. Head repo equals base repo; the PR is a draft; no governed path is in the file list.

① Derived judgments

  1. The accept set does not change — RIGHT. strictObject(options, shape) is closedObject(z.object(shape, { error: strictObjectError(options, shape) }).strict()) (packages/spec/src/shared/strict-object.ts:541-543). guidance reaches only the error map: strictUnknownKeyError consults guidance[key] to choose the prescription bullet for a key that is ALREADY refused (shared/suggestions.zod.ts:354, :380-384), and the candidate list is Object.keys(shape) plus extraKeys (strict-object.ts:353-356). The diff adds guidance.items / guidance.loading to the options object (component.zod.ts:1631-1640) and touches no key of the shape (:1641-1681 are context). So items and loading stay refused with one unrecognized_keys issue at [], which the new pins measure (component-record-blocks.test.ts, one issue, keys equal to the one key, for a feed, [], true and false). Through record:chatter / record:discussion (ComponentPropsMap :6389, :6403 both RecordChatterProps; feed: RecordActivityProps.optional() :1755) both keys are still refused at ['feed'], pinned.
  2. record:history's messages are byte-identical — RIGHT. The diff has no hunk in RecordHistoryProps (:2067-2085 on the head) or in PROPS_HISTORY (:277-280); its guidance.entries / guidance.loading text is untouched. The row the card named as the control is unchanged by the only file the diff edits in src.
  3. The renderer reads both keys on the standalone block — RIGHT. At the pin, record-activity.tsx:128-129 sets bag = schema.properties ?? {}; :161-166 reads hostItems from schema.items or bag.items and hostLoading = schema.loading ?? bag.loading; :259 takes hostItems ?? discussionItems ?? fetched ?? []; :265-266 takes hostLoading ?? discussion?.loading ?? (self-fetch state). The dev's citations hold. The sibling reads the same way (record-history.tsx:139-144, :207-208).
  4. The flat spelling is not covered, and the claim did not require it — RIGHT. The card scopes the properties spelling to the spec row ("the properties spelling is the spec row's to name"); the claim 5952583094 names guidance.items / guidance.loading on RecordActivityProps and nothing on the node. Node-level items / loading stay PageComponentSchema's generic refusal here and objectui PR 11420's named refusal at the pin (record-feed-host-slots-11321.test.ts, which takes the properties message from the row by reference, :153-162, so the next spec bump carries the new text there with no objectui edit).
  5. Each new sentence is true on EVERY row that reaches it — WRONG on the chatter mount. This is the FAIL. The table lives on one schema object, and RecordChatterProps.feed IS that object, so the two new bullets now answer record:chatter / record:discussion feed.items / feed.loading as well (the PR's own chatter pin shows the reach; it asserts the refusal only). At the pin, record-chatter.tsx reads feed?.limit, feed?.filterMode, feed?.types, feed?.showCompleted, feed?.unifiedTimeline, feed?.enableMentions (:190, :200, :229, :239-241, :264); its rows are discussion?.items (:233), its loading is discussion?.loading (:282), it reads neither feed.items nor feed.loading, and it never self-fetches (header :8-11: with no DiscussionContext it renders an empty feed). Read on that mount, these clauses are false: for items, "a host that composes this block in code passes the feed it already owns through it, and the renderer presents that feed in place of its own sources" (no host passes feed.items; the chatter renderer presents nothing from it) and "or self-fetches the record's own sys_activity rows" (no self-fetch exists there); for loading, "the host flag wins over the block's own, so authored true pins the loading state on forever" (nothing reads feed.loading; nothing is pinned). Only "Omit it" and "with no host items the block presents the record page's discussion feed" hold on both mounts. The PR's acceptance note concedes that the host-passes-it sentence "describes the standalone block" and declines the fix as out of scope because a second text would "split one shape". That premise does not hold: text true on both mounts needs no second schema, and strictObject offers no mount-scoped table either (guidance is a flat read-only record of string to string, keyed by key name, suggestions.zod.ts:271; the error map reads issue.code and issue.keys only, :368-369), so the text is the fix. The defect class is the card's own: an author, often an AI, reads the refusal to learn what the key does, and on the chatter mount it is told of a host channel that does not exist (Prime Directive chore: version packages #10's corollary, the claim no wider than the enforcement; Add comprehensive test suite for Zod schema validation #12, the message is contract text). Text this PR makes false is corrected in this PR.
    What the next head must say. Each bullet states, for both mounts, who reads the key: on a standalone record:activity it is the HOST's channel (host-composed in code, renderer presents it, hand-authored items are a static snapshot, authored loading: true pins the loading state); on a record:chatter / record:discussion feed nothing reads it. The remedy stays "Omit it"; the fallback sentence names the discussion feed for both mounts and the sys_activity self-fetch for the standalone block only. One acceptable form for items: "items is not authorable surface. On a standalone record:activity it is the HOST's data channel: a host composing that block in code passes the feed it already owns, and the renderer presents it in place of its own sources; hand-authored items would ship a static snapshot of the feed that never updates. On a record:chatter / record:discussion feed nothing reads it. Omit it: the block presents the record page's discussion feed, and a standalone record:activity with no discussion context self-fetches the record's own sys_activity rows." For loading: "loading is not authorable surface. On a standalone record:activity it is the host's fetch state for its items feed and wins over the block's own, so authored true pins the loading state on forever. On a record:chatter / record:discussion feed nothing reads it. Omit it with items: each block takes its loading state from its own source." The chatter pin then asserts the mount-true clause (that nothing reads the key there), not the refusal alone; the two standalone pins keep their first-sentence anchors adjusted to the new text; the docblock already states the chatter facts and needs no change beyond consistency.

② Semver level

@objectstack/spec patch, Clause-②: no — RIGHT for this diff. The shape behind z.object(shape).strict() is unchanged, so no key becomes accepted (no widening) and none is removed (no narrowing); the published input type z.input of the row is unchanged; only an error-map table and tests are added. A message-only fix in a released package takes a patch, never none. Check Changeset is green on the head. The dev's check-widening-tells --declaration no --diff exit 0 is a local reading consistent with the diff, not relied on here. Changeset sentences against the diff: "refused ... with only the generic ... line" is true at base (the PROPS_HISTORY tail also rode it; immaterial); "Both are keys the objectui renderer reads, as a feed a host that composes the block in code already owns" is true at the pin for the standalone block (① 3); "names items as the host's data channel and loading as the host's fetch state ... remedy: omit them" matches :1632-1639; "With no host items the block presents the record page's discussion feed, or fetches the record's own sys_activity rows" is true of record:activity; "the same shape record:history's row already uses" is true; "Both keys stay refused, through the row and through record:chatter / record:discussion's feed, which is the same object. Only the message text changes; record:history is unchanged" is true. The level and the clause stand; the ① 5 reword does not move either, and the next head keeps patch / Clause-②: no.

③ Boundary flags

  • One merge of main — verified. Branch commits past the merge-base fa7b565212: eefd7857e8 (fix), 29f3682ea5 (changeset), then b6224772eb, the one merge (parents 29f3682ea5, fa7b565212). origin/main at ceb4a939b4 is three commits past the merge-base (41a3c8df15, 5e58193338, ceb4a939b4); none touches the three files (empty diff on them), so this head needs no re-merge. Answered.
  • Gate reconciliation, 85 derived / 83 run / 1 NOT MEASURED / 1 UNRUN — both gaps are measured by CI on this head. check:dual-build-cjs-loads (NOT MEASURED locally) runs in ci.yml's build-core job (ci.yml:2265): check-run Build Core success. check:type-check-debt (UNRUN locally, declared to CI) runs in lint.yml's typecheck-debt job (lint.yml:6398): check-run Type Check · debt ledger success. The "nothing regenerated" claim is held by Type Check · source gates (check:generated --reconcile-only, check:authorable-surface, check:docs; lint.yml:5542, :5633, :5653) and Type Check · consumer gates (check:api-surface, :6619), both success. Lint & Repo Gates, Type Check · workspace, TypeScript Type Check, Spec property liveness, Governed Surface Queue Guard and the Dogfood gates are success; Console Pin Gate, Build Docs and the packed-tarball smoke are skipped by path. Check-runs read at 2026-10-02T14:36Z: 35 on the head, all concluded, 32 success and 3 skipped, 0 failed, 0 running (the six Test Core shards and their aggregate are success). Answered.
  • Partial-dist cleanup — worktree-local, gitignored, not in the diff (3 files, all additions). Not a finding. Answered.
  • Finding 1, the PROPS_HISTORY tail "was not read there" — pre-existing, not newly made false by this PR. RecordActivityProps carried history: PROPS_HISTORY before this diff (:1611, an unchanged context line), so the tail already rode record:activity's items refusal while record-activity.tsx:161-166 read the key at the pin, and record:history's entries / loading carry the same tail against record-history.tsx:139-144. This PR puts the new "the renderer presents that feed" bullet and the old "was not read there" tail into one message, as record:history already does; the contradiction is older than this diff. Escalated to the seat as a finding on PROPS_HISTORY (one history sentence for host-channel keys that ARE read); not owed on this head; nothing filed by this review.
  • Finding 2, the chatter-feed reach — answered in ① 5. It is the FAIL; "out of this card's surface" is not accepted, because the false text is this PR's own and the fix is wording, not a second schema.
  • open_questions: none declared; none raised beyond the above.
  • Check-runs still in progress at the final read: none.

Implemented-by: claude/issue-21279-record-activity-host-feed-guidance
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: FAIL

…ey on both mounts

`RecordChatterProps.feed` IS `RecordActivityProps`, so the `items` /
`loading` guidance also answers `record:chatter` / `record:discussion`
`feed.items` / `feed.loading`, where the chatter renderer reads neither
key and never self-fetches. Each bullet now states the standalone
`record:activity` facts (host data channel / host fetch state) and that
nothing reads the key on a chatter or discussion `feed`; the remedy stays
"Omit it", with the `sys_activity` self-fetch scoped to the standalone
block. The chatter pin asserts that mount-true clause on both names.
Shape and accept set unchanged; the changeset sentence is corrected.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ab581575851de1a92859746a04c1721f27b878e1
Local-runs: none

Inputs: card #21279 (body; comments 5944174224, 5952583094, 5954557256, 5955605217), the previous record 5954789199 (FAIL on head b622477, on its ① 5 alone), PR #21422 (body as updated for round 2, file list, net diff origin/main...ab58157585, 3 files, +118/-0, and the round-2 delta from b622477 to this head, 3 files, +47/-29), the head's check-runs, and objectui at the .objectui-sha pin 89cad75d5570 (record-activity.tsx, record-chatter.tsx, RecordChatterPanel.tsx, RecordActivityTimeline.tsx, plugin-detail/src/index.tsx, record-feed-host-slots-11321.test.ts), read-only. Head repo equals base repo; the PR is a draft; no governed path is in the file list; the pin is the same on origin/main and on this head. This is a narrow re-review: ① 1 to 4 and ② of the previous record are re-affirmed through the delta, ① 5 is re-judged in full, and ③ only where the delta moved it.

① Derived judgments

  1. The delta is exactly the round-2 claim, so ① 1 to 4 of the previous record stand — RIGHT. git diff b6224772eb ab58157585 touches the same three files the PR lists and nothing else: the changeset (2 lines: the intro sentence now names "the objectui record:activity renderer", and the first bullet is rewritten), the test (the describe comment; the two standalone pins' anchors re-cut to the new first sentences, with the sys_activity anchor becoming the mount-scoped fallback sentence; the chatter pin extended to record:discussion, asserting one issue, the mount-true clause and Omit it), and component.zod.ts (the docblock's second paragraph and the two guidance strings, :1625-1645 on the head). No hunk touches a shape: the keys of RecordActivityProps' shape are the same twelve at base and head (types through aria); shared/strict-object.ts and shared/suggestions.zod.ts are unchanged between origin/main and the head, so guidance still reaches only the error map (strictObject is closedObject(z.object(shape, { error }).strict()), strict-object.ts:541-543; guidance[key] is read only under issue.code === 'unrecognized_keys'). The accept set is unchanged (① 1 stands). The RecordHistoryProps section and PROPS_HISTORY were extracted from origin/main and from the head and diffed: byte-identical (① 2 stands). ① 3 is a fact about the pin, which did not move (stands). The delta adds nothing at node level (① 4 stands). origin/main is six commits past the merge-base fa7b565, at bdd3654; the diff of those commits on the three files is empty, so no re-merge is owed, and the PR reports mergeable.
  2. Each reworded sentence is true on BOTH mounts — RIGHT. The previous ① 5 is answered. The row reaches exactly two mounts: ComponentPropsMap['record:activity'] (component.zod.ts:6394) and RecordChatterProps.feed: RecordActivityProps.optional() (:1761), the latter bound to 'record:chatter' (:6395) and 'record:discussion' (:6409); packages/spec/src has no other non-test reference to either schema. At the pin, objectui mounts record-activity.tsx for the first and record-chatter.tsx for both names of the second (plugin-detail/src/index.tsx:795-830). Clause by clause:
    • items. "not authorable surface": refused on both mounts, pinned as one unrecognized_keys issue at [] and at ['feed']. "On a standalone record:activity it is the HOST's data channel: a host composing that block in code passes the feed it already owns, and the renderer presents it in place of its own sources": record-activity.tsx:161-165 reads schema.items or bag.items (bag is the properties bag, :128), and :259 takes hostItems ?? discussionItems ?? fetched ?? []. True. "hand-authored items would ship a static snapshot of the feed that never updates": the same read takes an authored array as-is. True. "On a record:chatter / record:discussion feed nothing reads it": record-chatter.tsx reads feed?.limit (:190, :200), feed?.filterMode (:229), feed?.types / feed?.showCompleted / feed?.unifiedTimeline (:239-241) and feed?.enableMentions (:264); its rows are discussion?.items (:233, :237). I followed the feed object past that file, since it travels on as config.feed: RecordChatterPanel.tsx reads config?.feed?.aria (:117) and forwards config?.feed to RecordActivityTimeline as its config (:179, :240); the timeline reads config?.showFilterToggle, showCommentInput, enableReactions, enableThreading and showSubscriptionToggle only (RecordActivityTimeline.tsx:285-289), and its items and loading are the props the chatter renderer set from the discussion context. The whole chain reads neither feed.items nor feed.loading. True. "Omit it: the block presents the record page's discussion feed": :259 on the activity block, :237 on the chatter block. True on both. "and a standalone record:activity with no discussion context self-fetches the record's own sys_activity rows": canSelfFetch requires hostItems === undefined && !discussion (plus a bound record and a data source, :175-180) and the fetch is dataSource.find('sys_activity', …) (:211); scoped to the standalone block, it claims nothing about the chatter mount, where no self-fetch exists (no data-source read in record-chatter.tsx; useRecordContext()'s result is discarded, :129). True.
    • loading. "not authorable surface": refused on both mounts. "On a standalone record:activity it is the host's fetch state for its items feed": :166 hostLoading = schema.loading ?? bag.loading; the renderer's interface documents it as the host flag paired with items (:92-96). True. "and wins over the block's other loading sources": :265-266 hostLoading ?? discussion?.loading ?? (canSelfFetch && fetched === null ? true : selfLoading). The block's other loading sources are exactly those two, the discussion flag and the self-fetch state, both behind ??, and the timeline receives loading from this one expression (:296). True; the dev's citation holds. "so authored true pins the loading state on forever": a non-nullish true wins on every render. True. "On a record:chatter / record:discussion feed nothing reads it": the chatter renderer passes loading={discussion?.loading} (:282), and the forwarded config chain above reads no loading. True. "Omit it with items: each block takes its loading state from its own source": the activity block takes the discussion flag, else its self-fetch state (:266); the chatter block takes the discussion flag (:282). True on both. It is loose on the chatter mount only in that "its own source" is the discussion context, which the items bullet names in the sentence before it; nothing false.
    • The docblock (component.zod.ts:1613-1631, a comment, not contract text): "the host flag wins over every other loading source" (the two others, as above), "reads its rows and its loading flag off the host's discussion context, reads neither feed.items nor feed.loading, and never self-fetches" (as above), and "guidance is one flat table keyed by key name" (guidance is typed as an optional read-only record of string to string, suggestions.zod.ts:271) are consistent with the bullets. The pin citations record-activity.tsx:161-166 and :265-266 are the ranges read above.
    • The pins assert substrings of the head's strings (checked against :1634-1645; the loading string's concatenation yields "On a record:chatter / record:discussion feed nothing reads it." as one sentence), and Test Core is success on the head. The chatter pin asserts the mount-true clause on both names, as the previous record required; the control pin (an invented key keeps the generic refusal) is unchanged. The dev's two ablations are self-evaluation and are not relied on; the CI reading suffices.
  3. objectui needs nothing beyond the next spec bump — still RIGHT. At the pin, packages/types/src/__tests__/record-feed-host-slots-11321.test.ts asserts the properties-spelling message equals the row's own issue message by reference (:154-160), with no literal anchor on the round-1 text, so the next @objectstack/spec bump carries the new text there with no objectui edit.

② Semver level

@objectstack/spec patch, Clause-②: no — still RIGHT. The shape keys are identical at base and head, so no key becomes accepted and none is removed; the row's input type is unchanged; only error-map text, a docblock and tests move. A message-only fix in a released package takes a patch, never none. Check Changeset is success on the head (both runs). The corrected changeset sentence, against ① 2: "The refusal now says who reads each key on each mount the row reaches" (two mounts, both named); "On a standalone record:activity, items is the host's data channel and loading the host's fetch state for that feed" (true); "On a record:chatter / record:discussion feed, which is the same object, nothing reads either" (true, chain included); "The remedy is the same on both: omit them. The block then presents the record page's discussion feed, and a standalone record:activity with no discussion context fetches the record's own sys_activity rows" (true); "This is the same guidance shape record:history's row already uses for entries / loading" (true: RecordHistoryProps.guidance carries entries and loading, byte-identical to origin/main). The reworded intro, "Both are keys the objectui record:activity renderer reads, as a feed a host that composes the block in code already owns", is true at the pin and now scoped to the renderer that does read them. The second bullet is unchanged from round 1 and was judged true there. The PR body and the changeset both carry Clause-②: no.

③ Boundary flags

  • The previous FAIL (5954789199 ① 5) — answered. Both bullets are reworded for both mounts in the forms that record named, the chatter pin asserts the mount-true clause on both names, and the changeset sentence is corrected. Nothing else moved (① 1). The round-1 acceptance note that declined the chatter-mount wording is superseded in the PR body, which now states the ruling.
  • One os-verify-lock queue-timeout retry — a local process event in the dev's worktree (exit 99, nothing acquired, nothing mutated, then a retry). It is not in the diff, and the diff is what this record judges; the ablation it gated is dev self-evaluation. Answered; not a finding.
  • check:type-check-debt declared to CI again; check:dual-build-cjs-loads NOT MEASURED locally — both are measured on this head. Type Check · debt ledger is lint.yml's typecheck-debt job (lint.yml:6172-6173, run: pnpm check:type-check-debt at :6398): check-run 110891936258, success. Build Core is ci.yml's build-core job (ci.yml:2065-2066, run: pnpm check:dual-build-cjs-loads at :2265): check-run 110892105924, success. Answered.
  • No merge of main this round — origin/main is six commits past the merge-base, none touching the three files (empty diff), and the PR is mergeable. Answered. The PR body's "main has moved since, to 41a3c8d" is stale prose (it has moved further); not a contract face, note only.
  • Pin citations — Lint & Repo Gates, which runs check:objectui-pin-citations (lint.yml:5930), is success on the head, and the cited ranges hold on a direct read (① 2).
  • PROPS_HISTORY tail — out of scope, as the previous record ruled; byte-identical to origin/main; the seat carries it. Not re-judged.
  • open_questions: none declared; none raised.
  • Check-runs on ab58157585 at the final read, 2026-10-02T15:35Z: 42, all completed; 37 success, 5 skipped (Build Docs, Console Pin Gate and Packed-tarball smoke (opt-in) by path; the second Auto Label and Check PR Size runs, triggered by the PR-body edit), 0 failure, 0 in progress. The six Test Core shards and their aggregate, Type Check · workspace, Type Check · source gates, Type Check · consumer gates, TypeScript Type Check, Spec property liveness, Governed Surface Queue Guard, Lint & Repo Gates, Build Core, Type Check · debt ledger, Check Changeset and the Dogfood gates are success. Check-runs still in progress at the final read: none.

Implemented-by: claude/issue-21279-record-activity-host-feed-guidance
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 15:48
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 15:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 3a6d92f Oct 2, 2026
43 of 44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21279-record-activity-host-feed-guidance branch October 2, 2026 16:14
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:ui size/m tests tooling

Projects

None yet

2 participants