Skip to content

fix(plugin-detail): read feed.filterMode and feed.enableMentions on the chatter path - #9555

Merged
claude[bot] merged 7 commits into
mainfrom
claude/issue-8968-chatter-filtermode-enablementions
Sep 15, 2026
Merged

claude[bot] merged 7 commits into
mainfrom
claude/issue-8968-chatter-filtermode-enablementions

Conversation

@os-sam

@os-sam os-sam commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Fixes #8968

@objectstack/spec declares RecordChatterProps.feed as RecordActivityProps.optional(),
bound to both record:chatter and record:discussion, so every member of the activity shape
is authorable inside feed. objectui#8934 ran the filter pipeline on this path and made
types / limit / showCompleted / unifiedTimeline live. Two members sit outside that
pipeline and stayed unread: filterMode and enableMentions. This PR makes the
renderer read what the protocol declares.

⛔ The opposite direction — narrowing the ObjectUI type so the declaration stops
over-promising — is not taken, per the maintainer principle the card quotes (原文照录、不译):

我们的项目以 objectstack 协议为准,文档应该以实际实现为准。协议不正确的应该先修改协议。

No declaration moved on any face.

Round 4 — the PASS review's three MINOR findings, all three repaired

The round-3 contract review returned PASS at 2c94630289 and raised three MINOR findings,
all precision defects in prose. This round repairs exactly those three and touches one file:
content/docs/plugins/plugin-detail.mdx. No source, no test, no changeset, no apps/console
file. origin/main had not moved — 40f34b4ba7 is still this branch's merge-base and the pull
request's own base — so no base merge was needed and no figure below moves because of one.

MINOR-3 · the over-general upgrade warning on the published page — the only one that ships

The callout said: "Both values were accepted and discarded before, so a schema that already
authored either one changes behaviour on upgrade."
The set that actually moves is authored
and not default-equivalent
. Both of the review's counter-examples were reproduced against
the branch-point blob before anything was rewritten, and both hold.

counter-example before, at branch point b7479abc74 after, at this head
feed: { filterMode: 'all' } the renderer passed no filterMode — its 2 occurrences in record-chatter.tsx are both inside the docblock and none in the component body — so RecordChatterPanel, which declares the prop with no default and forwards it from both position branches, handed the timeline undefined, and activeFilter = controlledFilter ?? internalFilter fell through to internalFilter, whose useState initial value is 'all' normalizeFilterMode('all') takes its FILTER_MODE_VALUES.includes branch, so no diagnostic fires, and a controlled 'all' reaches the same filterItems default branch
feed: { enableMentions: true } mentionSuggestions={discussion?.mentionSuggestions as any} was passed unconditionally, in a flat prop list with no wrapping condition mentionsEnabled = feed?.enableMentions !== false is true, so the same expression is passed

⇒ same slice, same rows, same suggestion list. The dropdown is usable on both sides of the first
row as well: with no onFilterChange the timeline's handleFilterChange falls through to
setInternalFilter, and with one it calls the setter. RecordActivityTimeline.tsx and
RecordChatterPanel.tsx are the same blob at the branch point and at this head
(06be13a9241a2d6eff8f912c91db65e70a10a107 and eb22ebf8e28ac3049197b9812fa5a077a1462cf7), so
every difference above comes from what the renderer passes and from nothing else.

The showFilterToggle gap, folded into the same sentence rather than filed separately. The
page described filterMode only in dropdown terms, and the one case where an upgrade genuinely
changes what a user sees was the case it did not name: showFilter is
config?.showFilterToggle !== false and gates the Select alone, while
filterItems(items, activeFilter) runs either way. So with the toggle off, an authored slice
that the untouched internalFilter used to widen to everything is now pinned, with no
control on screen to widen it. That decision and its reasons lived in the renderer docblock and
in this body, and nowhere the author reads. Judgement: one clause carries it — the added
sentence names the pair and the consequence and stops there. The callout keeps its four
surrounding preserved claims, the paragraph's earlier clauses are untouched, and the whole thing
is still inside the same Callout block (open/close count on the page unchanged at 2/2, fences
still even at 26).

The instrument, because a single-line grep on this page returns a false zero — which is how
this finding's parent survived a round.
Flatten newlines to single spaces before matching. The
predicates are **not** read and does nothing each return 1 occurrence on the
branch-point blob of that page and 0 at this head. The same phrase through a single-line
instrument at the branch point — git grep -F 'authoring them does nothing here' — returns
exit 1, zero hits on a sentence that is plainly there: the false zero, reproduced. Run twice,
once stripping comment leaders and once not, in case the leader stripper eats a line-leading
**; both agree. Firing control at this head through the same flattener: filterMode = 11.
Absent control, proven absent over all 7,812 tracked files before it was used as one:
hkwn_absent_probe_4412 = 0. (⛔ not qqzz_absent_token_9999, a control in this thread's
own comments; ⛔ not zzqx_no_such_key, recorded polluted in this repo; ⛔ not
vqrm_control_absent_7731, which the review spent.)

MINOR-1 · the transit paragraph's tense

Repaired in place, in the N4 section below. The sentence's subject was literally "This body",
so a true statement about the round-2 body read as a false one about the body it sits in. It
is now scoped to the body it describes, and the present is left to the readback rather than to a
copied count.

MINOR-2 · four cardinals re-measured at this head

Three the review named, plus one it did not. Each was true at an earlier head and was falsified
by the base merge or by a later commit on this branch. ⛔ No conclusion moves: every gate
still exits 0 and 0 changed lines still sit outside the console file's docblock.

the body used to print reads at this head
N3, changed lines in the console file, whole branch vs merge base — 5 7 (4 insertions / 3 deletions)
presence gate — "4 source file(s) of 1 released package(s)" 5 source file(s) of 2 released package(s)
guard --test over "the five changed paths … 5 path(s) checked" 8 paths
line-citation gate — "over 5 compared files" 8 files compared

The presence row's pass is by design and not a lowered bar: that gate asks for at least one
changeset, not one per package, and the second package is reached only by a comment-only
apps/console file. The guard row was also not runnable as printed — the command with
--test and no paths exits 2, "Refusing to answer 'not governed' about an empty list: a
question nobody asked must not read as a clearance"
, which is the guard behaving correctly and
the printed command being incomplete. It is now printed with its path list derived from git, so
it runs as printed and re-derives its own denominator instead of hard-coding one.

Round 3 — the re-check's FAIL, and the three artifacts it named

The round-2 re-check returned FAIL, and ⛔ not for any of round 1's five findings: it
discharges all five, the two actionable ones on its own measurements — it re-ran the starred
ablation and designed two more. The failure is a new one this round created: the branch
falsified three present-tense statements and named none of them.
One of the three is a
published, customer-facing surface.

⚠️ Record-keeping, corrected here so it does not travel further. Round 1's verdict
(comment 5674384708) reads "Verdict: PASS", F1–F5 graded two low and three
informational. Measured in that comment's stored body: PASS 1 occurrence, FAIL 0
(firing control Verdict = 1; absent-token control = 0 — ⛔ not qqzz_absent_token_9999, which
is present in that body as its own control and would have been a dead control here). An
earlier description of round 1 as a FAIL was an inference from "repairs were requested", not a
reading of the verdict line.

The instrument, which is the part worth keeping

Round 1's F1 repair corrected the two files F1 named. That was a pathspec, not a corpus —
and it covered 2 of the 5 artifacts carrying the claim. One grep covers all of them:

Files naming both filterMode and enableMentions, whole tree (7,812 tracked files) = 14.
Of those 14, 3 still asserted the members were unread: N1, N2, N3.

⚠️ A cross-line phrase needs a cross-line instrument. A single-line
git grep "not read on this path" -- content/docs/plugins/plugin-detail.mdx returns exit 1,
zero hits
— the sentence wraps across two source lines. That zero is an artifact, not an
absence
, and it is the reason this finding survived a round. The instrument used here strips
comment leaders (*, //, #) and then flattens newlines to single spaces, so a phrase
that wraps is contiguous before it is matched; the leader-stripping matters, because a docblock
* left in the flattened text both breaks real phrases and joins unrelated ones.

the both-keys sweep files asserting the members are unread
before this round's repair (same instrument, files reconstructed from the pre-repair commit) 3
after 0

Controls, same 14-file corpus, same instrument. Firing: filterMode = 83 occurrences
across all 14 files. Absent token = 0. And the sweep's own firing control is the
strongest one available — run unchanged against the pre-repair blobs it returns exactly the 3
files, so the 0 is a reading about the repair and not about a dead instrument.

⚠️ My first draft of this sweep was wrong and is not what is reported above: it matched
still unread on this path tense-agnostically and so "found" 3 files that were actually my own
corrected past-tense prose and a quoted negation in a round-2 test docblock. Present-tense-only
phrases plus leader-stripping is what produced the table.

N1 · the published docs page (MAJOR)

content/docs/plugins/plugin-detail.mdx, in the record:chatter / record:discussion callout,
told authors that filterMode and enableMentions are not read on this path and that
authoring them does nothing. True at the branch point, false at this head, by this diff.
That file is the docs site source, so it is customer-facing, and AGENTS.md rule #2
("Docs-driven … Not done until docs reflect the code") makes correcting it part of this
change rather than a follow-up.

Replaced with what the code now does, verified against the source rather than against the
changeset prose: the authored value is normalized by the same function record:activity uses
and seeds component state (so the dropdown stays usable rather than freezing), and
enableMentions: false withholds the composer's suggestion list while the protocol's default
keeps the affordance on. The paragraph also states the upgrade consequence, which is the failure
the review named — narrowed in round 4 to the set that actually moves (authored and not
default-equivalent) and extended to name the showFilterToggle: false case, which is where an
upgrade changes what a user sees. Measurements in the Round 4 section above.

⛔ The callout's surrounding claims were not deleted with it — showCompleted is false,
an unauthored limit is 20 with Load more, objectui#7298 retired the host append, and
enable.feeds: false still outranks the page. All four are still true and all four survive
(each re-measured present after the edit).

N2 · the pending changeset (MAJOR)

.changeset/8934-chatter-feed-affordance-only.md said the two members "are still unread on"
this path. A changeset body publishes verbatim into the CHANGELOG, and both changesets land
in the same release — so without this correction that release would contain one entry saying
the members are read and another saying they are not.

Corrected prose-only. Frontmatter is byte-identical and the declared package name is
preserved (@object-ui/plugin-detail: minor at base and now) — which is the shape the overwrite
gate's own history calls legitimate. ⭐ "Not closed by this change" was kept, because it is
still true: it is a statement about objectui#8934, not about this branch. Only the present-tense
half was falsified, so only the present-tense half changed; the paragraph now points at the
entry that closes it.

⚠️ Not a closing window: the release PR objectui#5400 does not currently list this changeset
(its file list paginated to exhaustion — 1,147 files, matching the API's own changed_files).
This is an ordinary correction that would have become a published contradiction at the next
regeneration.

N3 · a third docblock, outside this package (MINOR)

apps/console/src/__tests__/registry-inputs-spec-parity.test.ts carried the same claim in the
same present tense. Corrected. Comment-only, proven the way the review proved it for the two
F1 files:

measured
changed lines in that file, whole branch vs merge base 7 (4 insertions / 3 deletions)
of those, outside docblock * lines 0
it( before → after 55 → 55
expect( before → after 152 → 152
test( / describe( before → after 8 → 8 / 2 → 2

⚠️ That first row published 5 until round 4. It was true at 92268f8f6 and was falsified by
this branch's own next commit 2c9463028, which rewrapped the corrected sentence (review finding
MINOR-2). The row below it is unchanged at both heads, so the conclusion the table exists for
does not move.

Scope, declared rather than smuggled

Two of those three paths — content/docs/ and apps/console/ — were outside round 2's
declared fence, and round 2 stopped at the fence and said so. They are in scope now, by the
round-3 order. The in-tree precedent is the one this branch itself already used: a sentence a
diff falsifies is repaired by that diff.

The rule tension, resolved on measurement rather than quietly sided with

The order framed a conflict: AGENTS.md #11 against check-changeset-claims, with
check-changeset-overwrite reporting the edit. Measured, it dissolves — and on a reading that
differs from the framing, so it is recorded rather than just acted on.

  1. ⚠️ [WIP] Update documentation for project #11 is not the rule the framing described. Read at origin/main, [WIP] Update documentation for project #11 is "Cite by
    CONTENT, not by line address; a CROSS-FILE path:line is banned outright"
    — a citation-style
    rule. Its CHANGELOG clause is ⛔ "Never re-address a paragraph in a published CHANGELOG
    that is historical record."
    Two words carry it. Published: measured — git grep -F 'still unread' -- packages/plugin-detail/CHANGELOG.mdexit 1, 0 lines, against a firing
    control on that same file (filterMode 2, enableMentions 2 — the subject words are
    present, so the instrument can see the file) and 86 ## headings. The claim is pending,
    not history
    . Re-address: the clause governs re-pointing a citation; correcting a false
    factual claim is a different act. Neither word reaches this edit.
  2. What the claims gate prescribes, quoted from its run on this tree: "If a claim did go
    false, CORRECT THE BODY … both were prose-only, with frontmatter untouched."
    It then
    anticipates the other gate by name: "check-changeset-overwrite.mjs will REPORT that
    correction … That report is its own case 2 … it is report-only, and it is the intended shape
    here: one gate asks for the read, the other records the write."
  3. What the overwrite gate's own header measures: of 12 commits that modified a pre-existing
    changeset, all 19 files were legitimate, explicitly including "factual corrections to
    prose"
    ; the premise that such an edit is usually a mistake is "NOT true of this repository's
    history — 19 for 19 against"
    . Its overwrite signal is a lost declaration, and this edit
    loses none.

⇒ The two gates are designed to co-operate, not to conflict, and the gate run confirms it: the
claims gate asked for the read, the overwrite gate recorded the write as its case 2, both
exit 0. This matches the order's own reading; it is reported with the measurement because the
route to it corrects the description of #11.

Round 2 — the review's two actionable findings, both addressed

The contract review returned PASS with five findings. F1 and F2 are landed in
d34c1781f0; F3, F4 and F5 are body corrections and are folded into the sections below.

F2 — the "the dropdown stays usable" half was pinned by nothing. True, and it was the
right catch: every one of the original 25 cases asserted the slice the panel OPENS on, and
all 25 stay green against a renderer that hands the authored value down as a constant
(filterMode={defaultFilterMode}, no setter). That refactor FREEZES the dropdown — the
timeline resolves controlledFilter ?? internalFilter, so a controlled prop with no setter
pins the value and swallows every user choice. Three legs now drive the Radix filter dropdown
after mount, over both block names (6 cases, taking the file to 31):

  1. a later user choice moves the rows — pick Field Changes after mounting on
    comments_only, and the rendered rows follow the user rather than the author;
  2. an EQUAL authored value, re-rendered as a fresh object, does not clobber that choice
    the resync effect keys on a normalized PRIMITIVE, so a parent re-render carrying a new
    feed object with the same value compares equal and the effect does not re-fire. An effect
    keyed on the config OBJECT would look correct without this leg and would reset the user on
    every parent render;
  3. a CHANGED authored value DOES re-seed — the other half of the same wire, so leg 2 can
    never be satisfied by an effect that was simply deleted.

Each leg is DOM-level like the other 25 — it opens the real Select with the repo's own
keyDown idiom and clicks the real option — so nothing here asserts a call shape or an
internal name. ⭐ The purpose-built ablation for this finding is in the Evidence section: under
the freeze refactor exactly the 6 new cases go red and the original 25 stay green, which is
the finding demonstrated rather than described. The call site now carries a ⛔ note so the
next reader of those three lines meets the reason before simplifying them.

F1 — two sibling pin docblocks were stale about exactly this behaviour. Both asserted that
these two members are "still unread on this path", which this branch falsifies:

  • renderers/__tests__/recordChatterFeedMembersLive-8934.test.tsx
  • renderers/__tests__/recordChatterFeedMembers-8071.test.tsx

In each, the neighbouring scope clause ("NOT covered here" / "neither is pinned here") stays
true. The stale half is corrected in place of the old claim rather than deleted, so the
next reader can see which half moved and which survived — these docblocks are the record of
what each pin does and does not cover. No assertion in either file changed; both still pass.

Base merged, not rebased. main advanced from the branch point to 009f92d7a2 while this
ran, so origin/main was merged in (118bcf3926). ⛔ No rebase, no force-push, no label
change. The interval is 13 files and git diff --name-only over it shows none under
packages/plugin-detail/, so no reading below changes because of it; every figure was
re-measured on the merged head anyway.

What changed

packages/plugin-detail/src/renderers/record-chatter.tsx — one file of implementation:

  • feed.filterMode is normalized through the same normalizeFilterMode record:activity
    uses (an unrecognised value opens on all rather than on a slice nothing matches), and it
    seeds component state which is passed down with onFilterChange. Seeding state rather
    than handing the authored value straight through is what keeps the dropdown usable.
    RecordChatterPanel already declared and forwarded both props — the renderer was the only
    missing end of the wire.
  • feed.enableMentions gates mentionSuggestions. false withholds the host discussion
    context's suggestion list, which is exactly what the record:activity registration
    publishes for this key ("Off withholds the suggestions"). The test is !== false, not
    truthiness, because the protocol's default is on and an unauthored member must not read as
    "off". The review confirmed that default at the source rather than from this body:
    z.boolean().default(true) in the resolved @objectstack/spec@17.4.0, and
    default: true in the generated RecordChatterProps.json.

Plus the pin test, a changeset, the two docblock repairs above, and — per this repo's
cite-by-content rule, opportunistically inside files being edited anyway — the replacement of
their cross-file path plus line addresses with content anchors. No sweep.

The design question the card left to the implementer, and the answer

Does filterMode have a coherent meaning on a panel whose filter dropdown is itself gated
by showFilterToggle, when that toggle is off?

Yes, and the two members are wired as INDEPENDENT. One names WHICH SLICE the feed shows;
the other names WHETHER THE CONTROL that changes it is on screen. With the toggle off an
authored filterMode does not go inert — it stops being the slice the user OPENS on and
becomes the slice the author PINNED. Three reasons:

  1. The protocol's own wording splits them that way. filterMode is described as the
    "Default activity filter"; showFilterToggle as "Show filter dropdown in panel header".
    Neither member's description mentions the other, so neither is declared to gate the other.
  2. record:activity already behaves this way, and a second convention here would be the
    defect this card's family is about.
    RecordActivityTimeline resolves its active filter
    before, and independently of, its showFilter flag, and the item filter runs on that
    active value whether or not the dropdown renders. Coupling the two only on the chatter
    path would put two readings on one declared shape — objectui#8934's defect class, reopened
    one key along.
  3. The alternative silently discards a value the author wrote and the spec accepts — the
    exact shape objectui#8968 exists to remove.

A fourth reason the review found, which this PR did not lean on and which settles it:
CHATTER_INPUTS publishes feed by DELEGATING to record:activity's input list, and that
list's own filterMode description — the text already shipped to sdui.manifest.json and
read by AI authors — promises that the timeline dropdown starts on the authored value and
the user can still change it. The chatter path was the one place that published promise
was not kept. This is not a choice between two open readings; it is the implementation
catching up to a surface that was already published. That also makes F2's legs the pin for a
promise the manifest has been making all along.

Pinned by three cases per block name: the authored slice still decides the rows while the
combobox is absent; a KEEP-LEG on all proves "the row is gone" is not "the panel renders
nothing"; and an equality case asserts that for one authored mode, turning the dropdown off
changes the CONTROL and nothing else.

The aria reading the card owed — CORRECTED table (review finding F3)

⚠️ The table first published here was an instrument artifact and is replaced. It reported
schema.aria / schema?.aria as one row of 9 with an itemisation that did not add up. Two
separate mistakes: the two spellings were conflated into one row when one of them is zero, and
the per-file breakdown was taken from git grep -c, which counts LINES — one line in
renderers/record-quick-actions.tsx carries two occurrences, so the lines and the occurrences
disagree. Re-measured below at the branch point b7479abc74100e022b5f1233fd14d4fabc891788,
corpus packages/plugin-detail/src, occurrences by git grep -o ... | wc -l and files by
git grep -l ... | wc -l. The review's own figures are reproduced exactly.

instrument occurrences files
substring schema.aria (grep -o -F) 11 5
of those 11, the ones inside schema.ariaLabel — a different, FLAT key 2 2
substring schema?.aria (grep -o -F) 0 0
member read schema\.aria(?!\w) (grep -o -P) 9 5
read('aria') 0 0
config\??\.aria(?!\w) 0 0
feed\??\.aria(?!\w) 0 0

⚠️ The instrument spelling is corrected (review finding N4). Those rows previously published
the lookahead as a (? followed immediately by a bracketed character class, which is not valid
PCRE: git grep -o -P dies on it with "unrecognized character after (? or (?-" (exit 128), and
piped to wc -l exactly as the table instructs it prints 0 while the error goes to stderr.
Two rows therefore reported a zero from a dead instrument. The values were right; the printed
commands were not runnable. They are now spelled (?!\w) and run as printed.

Root cause is transit, not authorship — measured, not asserted. A probe composed four
negative lookaheads locally, posted them once and compared the readback by first-differing-index:
every lookahead followed by a bracketed character class lost its !, while every one followed by
a backtick or a backslash survived intact (comment 5674843134). The round-2 body stored
the broken form 3 times and the correct form 0 times, which is exactly what that channel
produces from correctly-authored text. ⚠️ That is history about the round-2 body, not a
description of this one — until round 4 the sentence opened "This body", so a true history read
as a false present (review finding MINOR-1). The repair landed in round 3, and the round-3
review's own byte-compared readback at 2c94630289 found the broken form 0 times and the
correct form 5. What THIS body carries is whatever the readback taken after the publish that
produced it reports; the readback is the check, not a count copied to here, because every publish
moves it. ⛔ None of that demotes N4: a published command that does not run is a defect
regardless of who or what typed it. The repair is a spelling that survives the
channel, plus a byte-compared readback — both done here.

⚠️ Equivalence measured, not assumed. (?!\w) and the bracketed spelling are the same
instrument for ASCII, and on this corpus both return 9 occurrences / 5 files, so respelling
the rows changes no value in the table.

The 9 member reads, by file and by OCCURRENCE (not by line):
renderers/record-path.tsx 2 · renderers/record-quick-actions.tsx 2 ·
renderers/__tests__/record-path.containerLabel.test.tsx 2 ·
renderers/__tests__/record-quick-actions.ariaLabel.test.tsx 2 ·
useDetailTranslation.ts 1 (a comment). Unchanged at this head.

Controls, same corpus. Firing, same subject: that member-read instrument returns non-zero
on record:path and record:quick-actions, so a zero elsewhere is a reading about those paths
and not about a dead instrument. Firing, adjacent member: showFilterToggle = 13 occurrences
over 6 files. Absent token qqzz_absent_token_9999 = 0 occurrences over 0 files. (⛔
zzqx_no_such_key deliberately unused — recorded polluted in this repo, 3 files.)

Verdict, unchanged and independently confirmed by the review: aria is UNREAD on the chatter
path — and unread on record:activity too.
Every aria token in the four files on this path
is a DOM aria-* attribute, 9 of them (RecordActivityTimeline.tsx 5,
RecordChatterPanel.tsx 4) — of which 7 carry a translated label and 2 carry none:
aria-live="polite" and a bare aria-hidden (review finding N5a — the 9 was right, "every …
carrying a translated label" was not). record-activity.tsx carries no aria token at all;
record-chatter.tsx carried none at the branch point this table declares and carries 1 at
this head, in its own docblock rather than as a DOM attribute (review finding N5b). So it is not this path's remainder but a record:*
family gap, deliberately not fixed here: fixing it only on the chatter path would make
record:chatter honour a member record:activity still drops — the very asymmetry this card
exists to remove — and closing it properly means first deciding how an authored label composes
with the accessible name RecordActivityTimeline already sets on its own section. The PM seat
has filed the family as objectui#9556 with its own measurement.

Premise re-measurements (all five, at the branch point)

⚠️ Ref label corrected (review finding F4). These readings were taken at
b7479abc74100e022b5f1233fd14d4fabc891788, which is this branch's merge-base / branch
point
, not origin/mainmain moved while the branch ran and is now 009f92d7a2. The
branch point is the right ref for a "before" figure, and the review reproduced every number
below at it.

  1. objectui#8934 landed. git ls-tree -r --name-only at that ref lists
    packages/plugin-detail/src/renderers/__tests__/recordChatterFeedMembersLive-8934.test.tsx,
    and packages/plugin-detail/src/index.tsx carries the comment
    "which now runs applyFeedConfig with" that call shape.
  2. That pin states this card's remainder. Its docblock read, verbatim: "NOT covered here,
    and not claimed: filterMode and enableMentions are also members of the declared shape
    and are still unread on this path". (That sentence is what F1 corrects, now that it is no
    longer true.)
  3. filterMode — 36 occurrences over 11 files. read('filterMode') = 1 occurrence, in
    renderers/record-activity.tsx, the only read off config. RecordActivityTimeline takes it
    as a component prop (declaration plus the filterMode: controlledFilter destructure).
    RecordChatterPanel declares it and forwards it from both position branches.
    renderers/record-chatter.tsx carried 2 occurrences, both inside the docblock and none
    in the component body — the renderer passed none.
  4. enableMentions — 12 occurrences over 6 files. Exactly one read path,
    renderers/record-activity.tsx, at enableMentions: read('enableMentions') and
    config.enableMentions !== false. RecordChatterPanel.tsx and RecordActivityTimeline.tsx
    do not appear in the 6-file list at all — zero occurrences, so zero reads.
  5. Absent-token control qqzz_absent_token_9999: 0 occurrences, 0 files.

Evidence

All figures below re-measured on the merged head d34c1781f0.

Forward legpnpm exec vitest run packages/plugin-detail/src/renderers/__tests__/recordChatterFilterModeMentions-8968.test.tsx

 Test Files  1 passed (1)
      Tests  31 passed (31)

⭐ Ablation A — the FREEZE refactor, purpose-built for finding F2. The two wired lines were
replaced with filterMode={defaultFilterMode} and no onFilterChange; mutation proven on disk
before the run, restore proven by blob hash:

HEAD blob 6a7eb202e1176f1a6df4c53527d434b9cdcfe1f8
pre-mutation   'filterMode={filterMode}' 1   'onFilterChange={setFilterMode}' 1
post-mutation  on-disk hash 3079c60f5da208fd358062b932898592db63b875
               'filterMode={filterMode}' 0   'onFilterChange={setFilterMode}' 0
 Test Files  1 failed (1)
      Tests  6 failed | 25 passed (31)
RESTORED: on-disk hash == HEAD blob (6a7eb202e1176f1a6df4c53527d434b9cdcfe1f8)

The 6 red are exactly the 3 new legs over both block names. The 25 green are exactly the case
set that existed before round 2. ⇒ the reviewer's hypothesis is confirmed as a measurement, not
accepted as an argument: the old set could not tell a seed from a freeze, and the new legs can.

Ablation B — the pre-fix reader, the original reverse verification, re-run against the
31-case file. record-chatter.tsx replaced with its branch-point blob:

HEAD blob     6a7eb202e1176f1a6df4c53527d434b9cdcfe1f8
pre-fix blob  1ec652b2f0ccea0dde4dc0ce7a387b355efad691   (on-disk hash after the revert: equal)
normalizeFilterMode  2 -> 0        mentionsEnabled  2 -> 0
 Test Files  1 failed (1)
      Tests  18 failed | 13 passed (31)
RESTORED: on-disk hash == HEAD blob

Direction predicted before the run and observed: turns red. ⚠️ Worth stating because it is the
non-obvious half: of the 6 new legs, only 4 go red here, not 6. The pre-fix renderer passed
no filterMode at all, so the timeline fell back to its own uncontrolled internalFilter
and the dropdown genuinely worked — leg 2 ("an equal authored value does not clobber") is true
on that side too, by construction. That is precisely why Ablation A exists: the freeze is a
defect no revert-to-before ablation can reach, because it is not the old behaviour. The 13
green are leg 2 over both names plus the 11 controls the review already read back individually.

Sibling pins after the F1 docblock repair — both still green; no assertion in either file
was touched (they are inside the full-package run below).

Package suitepnpm exec vitest run packages/plugin-detail/

 Test Files  180 passed (180)
      Tests  1723 passed (1723)

Typecheckpnpm --filter @object-ui/plugin-detail run type-check
(tsc --noEmit && tsc -p tsconfig.test.json), exit 0, re-run after the base merge moved
packages/types. tsc -p tsconfig.test.json --noEmit --listFiles lists the pin and
renderers/record-chatter.tsx among 156 .test.tsx files in that program, so the test config
does not exclude them. ⚠️ A fresh worktree has no dist and the first attempt fails with
TS2307 Cannot find module '@object-ui/…' on untouched files — a stale/absent-dist reading,
not a finding; the exit 0 is from after
pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' build.

⚠️ Declaration surface: the TYPES do not move, the BYTES do (review finding F5). Earlier
wording here said "the published .d.ts surface is unchanged", which is true of every
declaration and not of the emitted bytes: TypeScript carries a module's leading JSDoc into its
emitted declaration, so the rewritten docblock lands in dist/renderers/record-chatter.d.ts.
Measured by emitting the surface twice — npx tsc -p tsconfig.json --emitDeclarationOnly in
packages/plugin-detail, once at this head and once with record-chatter.tsx reverted to its
branch-point blob:

branch point this head
emitted .d.ts files 56 56, identical file set
non-comment, non-blank lines (all files) 993 993, diff empty
concatenated bytes (all files) 205,668 209,031
renderers/record-chatter.d.ts bytes 2,743 6,106
renderers/record-chatter.d.ts declaration lines 9 9, diff empty

⇒ no declaration, symbol, type or import in the emitted surface moves; the entire +3,363 bytes
is comment text. Clause-②: no is unaffected — nothing widens an accept set or enlarges a
public face. Stated here so a reviewer who checks "surface unchanged" by hashing dist rather
than by comparing declarations knows why the hash differs.

Lint — the whole tree, not a narrowing: eslint . --no-inline-config --format json on the
merged head = 5000 files linted, 95 errors over 79 files, 13166 warnings. Zero errors on all
four touched files.
They carry warnings only, of kinds already resident there and in the
reference renderer: @typescript-eslint/no-explicit-any, plus one
react-hooks/set-state-in-effect on the filterMode resync effect — byte-for-byte the effect
renderers/record-activity.tsx already carries, which raises the same warning there, one of
195 files carrying that rule tree-wide.

Other gates read by this diff, re-run at this head — three of these four readings had gone
stale, see MINOR-2 in the Round 4 section

  • node scripts/check-changeset-presence.mjs — exit 0: "5 source file(s) of 2 released
    package(s) changed, and this change declares 1 changeset(s)". One changeset for two packages
    is what the gate asks for; it wants at least one declaration, not one per package.
  • node scripts/check-new-cross-file-line-citations.mjs — exit 0, all four synthetic controls
    PASS, "Cross-file line-address citations ADDED by this branch : 0", "Files compared : 8".
    The addresses inside the files being edited were converted to content anchors, which removes
    citations rather than adding any.
  • node scripts/check-governed-queue-guard.mjs --test $(git diff --name-only origin/main...HEAD)
    — exit 0, "NOT GOVERNED — 8 path(s) checked against 5 governed surface(s); none matched". The
    path list is derived rather than typed out because the same command with no paths exits
    2 and refuses to answer at all.
  • node scripts/check-control-bytes.mjs — exit 0, "scanned 7727 tracked text file(s); skipped
    85 binary", plus a direct control-character scan over every changed file: no match.
  • node scripts/check-changeset-claims.mjs, check-changeset-overwrite.mjs,
    check-changeset-no-major.mjs and check-doc-fence-languages.mjs — all exit 0.

Every build and test went through scripts/pm/os-verify-lock.sh. Its own banner records that
the lock excludes other locked runs only, so the wall-clock figures it prints are shared-box
readings; none is quoted here as a performance claim.

Acceptance notes

Fence held. No schema declaration changed on either face; nothing under packages/types/
was touched by this branch; packages/plugin-detail/src/DetailView.tsx and
packages/plugin-detail/package.json (held by PR #8941) were not touched; the twelve
undeclared reads on objectui#8649 were not fixed. Clause-②: no still holds after round 2 —
the review verified it independently by emitting the declaration surface twice rather than by
reading this body, and the round-2 commit adds only tests and comments.

Found and deliberately left alone:

  1. aria is unread on record:activity as well as on the chatter path — measurement and
    argument above. Filed by the PM seat as objectui#9556.

  2. normalizeFilterMode's diagnostic is hard-prefixed [record:activity] and warns once per
    offending VALUE.
    Now that the chatter path calls the same function, an unrecognised
    feed.filterMode authored on a record:chatter block warns under the record:activity
    name, and a page carrying the same bad value on both block kinds warns once in total.
    Reachable because of this PR, but the fix belongs in
    packages/plugin-detail/src/renderers/recordActivityFeed.ts, outside this card's declared
    file surface; a renderer-aware-prefix argument is carried in a comment at the call site.
    For the PM to file.

  3. RecordChatterPanel defaults diverge from the renderer's merge, pre-existing and not
    reached by this card: the panel defaults position to 'right' and collapsible to
    true, while RecordChatterRenderer's merge supplies 'bottom' / false. Only observable
    when the panel is mounted without going through the renderer. Noted, not filed — objectui#8983
    removed the last host that did that, so there may be no successor who meets it.

  4. Ten pre-existing cross-file path:line citations in
    apps/console/src/__tests__/registry-inputs-spec-parity.test.ts — the form AGENTS.md [WIP] Update documentation for project #11 bans
    outright — scattered across a 4,514-line file. ⛔ Deliberately not repaired, and named here
    rather than left silent.
    [WIP] Update documentation for project #11 does invite opportunistic repair when you are touching a file
    anyway, and this branch's own earlier commit did exactly that in the files it edited; but [WIP] Update documentation for project #11
    equally says repair them ⛔ "never as a sweep", and ten addresses spread across a file in
    which this round touched one docblock is a sweep, not an opportunity. This diff falsified
    none of them
    , and the differential gate agrees: check-new-cross-file-line-citations.mjs
    exit 0, 0 cross-file line-address citations added by this branch, with its four synthetic
    controls all PASS. One of the ten sits three lines above the N3 correction, which is what
    makes it worth naming. For the PM to rule on.

Not a decision card. The filterMode × showFilterToggle question was decided here, in
the PR and in a test, because objectui#8968 ruled it "a design question for the implementer,
not a contract choice".

Implemented-by: claude/issue-8968-chatter-filtermode-enablementions
Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt


Generated by Claude Code

…on the chatter path

`@objectstack/spec` declares `RecordChatterProps.feed: RecordActivityProps`,
bound to both `record:chatter` and `record:discussion`, so both members are
authorable here. objectui#8934 ran the filter pipeline on this path and made the
four filter members live; `filterMode` and `enableMentions` sit outside that
pipeline and stayed unread — accepted by the spec, declared in the registration,
discarded in silence.

The renderer now reads both with `record:activity`'s own reading rather than a
second local one: `filterMode` through `normalizeFilterMode`, seeding component
state so the dropdown stays usable, and `enableMentions` gating the host
discussion context's `mentionSuggestions`.

`filterMode` and `showFilterToggle` are wired as INDEPENDENT members — with the
dropdown gated off an authored `filterMode` becomes the author's fixed slice
rather than going inert. That choice is argued in the renderer docblock and
pinned behaviourally.

Also repairs the cross-file `path:line` citations inside the file being edited,
per the repo's cite-by-content rule.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3063.0 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-DNduKu0O.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.20KB 62.88KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.99KB 14.24KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.65KB 65.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.83KB 57.97KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.89KB 20.96KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Contract review

Head judged: c9e6b0b56fb2fbdcb0b0d9f9b4ff3f6a3211ac80 (base main; the PR's head.sha from the
API matches this byte for byte). Verdict: PASS.

Independent seat — I did not write this branch. Everything below was re-measured in my own
detached worktree at that sha (git worktree add --detach … c9e6b0b5), never in the shared
checkout, whose HEAD is not an ancestor of origin/main. origin/main was fetched first and
is 8d50bc2bf430e37e10a19a4c7f0f7b80d6bd0258; the branch point is
git merge-base HEAD origin/main = b7479abc74100e022b5f1233fd14d4fabc891788, and every
"before" figure below is taken at that ref. Occurrences are git grep -o … | wc -l, files are
git grep -l … | wc -l, all content greps (never a path match), corpus stated each time. Tree
left clean (git status --porcelain empty; proven again after the ablation and after my own
probe was deleted).


Claim 1 — feed.filterMode goes through normalizeFilterMode and seeds STATE, not a frozen constant ✅

packages/plugin-detail/src/renderers/record-chatter.tsx now has, verbatim:

const defaultFilterMode = normalizeFilterMode(feed?.filterMode);
const [filterMode, setFilterMode] = React.useState<FeedFilterMode>(defaultFilterMode);
React.useEffect(() => { setFilterMode(defaultFilterMode); }, [defaultFilterMode]);
…
filterMode={filterMode}
onFilterChange={setFilterMode}

record-activity.tsx:109/227/228/245/246 carries the same five lines against read('filterMode').
Same imported normalizeFilterMode from ./recordActivityFeed — one function, not a second copy.
RecordActivityTimeline.tsx:187 destructures filterMode: controlledFilter, :212 resolves
controlledFilter ?? internalFilter, and :244-252 handleFilterChange calls onFilterChange
when it is supplied — so the prop is genuinely controlled and the renderer's setter is the thing
that has to move for the dropdown to work.

The claim the brief told me not to take on trust — a seed that cannot be changed afterwards.
I tested it rather than reading it.
The PR's own 25 cases all assert the opening slice; none
of them changes the filter after mount, and no test anywhere in packages/plugin-detail/src
does (git grep -n "onFilterChange" -- '*.test.tsx' in that corpus: the only hit is the new
file's constant FILTER_TRIGGER, no driver). So I wrote my own throwaway probe in the worktree,
mocking ../../RecordChatterPanel to capture the props the renderer actually hands down, and ran
it under vitest run --root . --disable-console-intercept=true on the committed head:

probe reading
A — seed arrives, then onFilterChange('changes_only') inside act() filterMode goes comments_onlychanges_only. Not frozen.
B — user's later choice vs a parent re-render with a fresh schema object, same authored value stays changes_only. The resync effect does not clobber: normalizeFilterMode returns a primitive, so defaultFilterMode is referentially stable and the effect does not re-fire.
C — parent re-renders with a changed authored value (tasks_only) re-seeds to tasks_only. The effect earns its place.
D — mentionSuggestions handed to the panel for undefined / true / false host list / host list / undefined.

4 passed (4). The probe file was deleted and git status --porcelain verified empty afterwards.
⇒ the stated reason for state-over-constant holds, and the behaviour matches what the designer
registration already publishes for this key: "Filter the timeline dropdown starts on. The user
can still change it
; an unrecognised value falls back to all."

(packages/plugin-detail/src/index.tsx, the record:activity inputs list, which CHATTER_INPUTS
delegates feed to). ⚠️ See finding F2 — true, but pinned by nothing.

Claim 2 — feed.enableMentions gates mentionSuggestions with !== false

const mentionsEnabled = feed?.enableMentions !== false;
…
mentionSuggestions={mentionsEnabled ? (discussion?.mentionSuggestions as any) : undefined}

Byte-for-byte record-activity.tsx:237/250's shape. Direction checked against the protocol, not
against the PR body: in the resolved dependency @objectstack/spec@17.4.0
(node_modules/@objectstack/spec, the version the workspace links),
src/ui/component.zod.ts declares enableMentions: z.boolean().default(true), and the generated
json-schema/ui/RecordChatterProps.json carries feed.properties.enableMentions.default = true.
Default is ON, so !== false is the only test that leaves an unauthored member alone —
truthiness would read undefined as off. Probe D above confirms all three arms at the wire.
The registration text for the key reads "Off withholds the suggestions", which is what the code does.

Claim 3 — Clause-②: no, verified independently of the PR body ✅

git diff --name-status $(git merge-base HEAD origin/main) HEAD
A  .changeset/8968-chatter-filtermode-enablementions.md
A  packages/plugin-detail/src/renderers/__tests__/recordChatterFilterModeMentions-8968.test.tsx
M  packages/plugin-detail/src/renderers/record-chatter.tsx
  • Nothing under packages/types/: git diff --name-only … -- 'packages/types/'0.

  • No declaration face touched: git diff --name-only … | grep -E '\.d\.ts$|zod|manifest|register|\.json$'0. No spec change (the spec is a published npm dependency here, unchanged), no ComponentPropsMap change, and CHATTER_INPUTS in index.tsx was not touched — it publishes feed as one delegating object input, so the manifest surface cannot move for a member-level read.

  • Published declaration surface unchanged — proven by emitting it, not by reading the diff. I built the dependency graph, then ran npx tsc -p tsconfig.json --emitDeclarationOnly in packages/plugin-detail twice: once at head, once with record-chatter.tsx reverted to its branch-point blob, concatenating all emitted .d.ts in sorted order each time.

    base head
    emitted .d.ts files 56 56 (identical file set)
    non-comment lines 995 995, diff empty

    So no declaration, no symbol, no type and no import in the emitted surface moves. At source level the same holds directly: the three export lines and the interface body are byte-identical between the two blobs (diff empty; only their line numbers moved, 55/61/168 → 109/115/257, because the docblock grew), and the one added import — import type { FeedFilterMode } — is used solely in a React.useState<…> generic inside the function body, never in an exported type position. ⚠️ See finding F5 for the one place where the PR body's wording is looser than this measurement.

Both carriers do declare Clause-②: no (PR body and issue #8968 comments), and neither hangs
needs:contract-review — PR labels are plugin, tests; card labels are pm:dispatched,
priority:p3, domain:spec. I could not re-run check-clause2-carriers — it is not in
scripts/, not on PATH, and not in the objectstack checkout — so this leg is my own
measurement, not that tool's exit code.

Claim 4 — premise re-measurements ✅ reproduce exactly

Corpus packages/plugin-detail/src at the branch point b7479abc74: 235 git-tracked files
(git ls-tree -r --name-only b7479abc74 -- packages/plugin-detail/src | wc -l; 236 at head, the
new pin). Enumerated, not assumed.

instrument (content grep) implementer my reading
git grep -o -F filterMode b7479abc74 -- packages/plugin-detail/src | wc -l 36 36
git grep -l -F filterMode … | wc -l 11 11
git grep -o -F "read('filterMode')" … 1, in renderers/record-activity.tsx 1, renderers/record-activity.tsx
git grep -o -F enableMentions … 12 12
git grep -l -F enableMentions … 6 6
one read path for enableMentions record-activity.tsx git grep -n -F 'enableMentions !== false'1, record-activity.tsx:237
RecordChatterPanel.tsx / RecordActivityTimeline.tsx in the enableMentions file list absent absent — the 6 are index.tsx, three __tests__ files, record-activity.tsx, record-chatter.tsx

And the premise behind the premise, which I checked because it is the whole card: in
record-chatter.tsx at b7479abc74, filterMode occurs (lines 36, 38) and
enableMentions (line 37) — the docblock ends at line 42, so all three are inside the
comment and none is in the component body.
The renderer passed neither. The sibling pin states
it in its own words (recordChatterFeedMembersLive-8934.test.tsx:58): "⚠️ NOT covered here, and
not claimed: filterMode and enableMentions … are still unread on this path"
.

Controls, same corpus. Absent-token qqzz_absent_token_9999: 0 occurrences / 0 files at
both refs — clean. ⛔ zzqx_no_such_key was not used; I confirmed the lane's pollution note is
current (git grep -l -F zzqx_no_such_key HEAD3 files: the #8649 changeset, its pin, and
packages/plugin-list/src/__tests__/listViewNonAuthorKeys-8653.test.tsx). Same-subject control
that fires in the same corpus: showFilterToggle13 occurrences / 6 files. So the zeros
above are readings about these paths, not about a dead instrument.

Claim 5 — the ablation, and ⭐ whether the 11 green are really controls ✅

Run from the committed state, mutation and restore both proven by blob hash:

HEAD blob   9fb9fe23dd177723905ebb164e95438fcb9d2296
MB blob     1ec652b2f0ccea0dde4dc0ce7a387b355efad691
after `git show <MB>:<file> > <file>`, on-disk hash = 1ec652b2f0… (== MB blob)
→ Test Files  1 failed (1)   Tests  14 failed | 11 passed (25)
after `git checkout HEAD -- <file>`, on-disk hash = 9fb9fe23dd… (== HEAD blob)
→ git diff HEAD empty, git status --porcelain empty

14/11 exactly as reported. ⭐ I did not take "designed controls" on trust — I ran the ablation
with --reporter=verbose and read every one of the 11 back.
They are:

green under ablation why it is a control, not a miss
×2 an unrecognised filterMode opens on all asserts the fallback; the timeline's own internalFilter default is 'all', so both sides agree by design
×2 with no filterMode authored the panel opens on all unauthored ⇒ all on both sides
×2 KEEP-LEG — the same toggle-off panel on all renders every row the explicit KEEP-LEG; it exists to prove the panel is not rendering nothing
×2 enableMentions unauthored means ON pre-fix the renderer passed mentionSuggestions unconditionally, so ON is correct on both sides
×2 UNDECLARED-KEY asserts nothing moves
×1 CONTROL — no host suggestions, enableMentions: true still offers none asserts the member gates rather than manufactures

Every one of the 11 is a leg whose assertion is true on both sides by construction, and each
of the 14 reds is a leg that asserts the new read. The failing assertions are rows and
suggestion-visibility at :193 :208 :223 :254 :276 :292 :318 × both block names — including the
⭐ decision legs at :254 and :276. The ablation bites, and it bites in the right places.

Claim 6 — forward leg, suite, type-check ✅

  • New pin, forward: timeout -s KILL 900 npx vitest run --root . --disable-console-intercept=true packages/plugin-detail/src/renderers/__tests__/recordChatterFilterModeMentions-8968.test.tsxTest Files 1 passed (1) / Tests 25 passed (25). (25 = 2 block names × 5 + 2 × 3 + 2 × 2 + 2 × 2 + 1, which is the file's own arithmetic.)
  • Package suite: same runner over packages/plugin-detail/Test Files 180 passed (180) / Tests 1717 passed (1717). Exactly the reported figures.
  • Type-check: pnpm --filter @object-ui/plugin-detail run type-check (which is tsc --noEmit && tsc -p tsconfig.test.json) → exit 0. And the test project really does cover the new file rather than excluding it: npx tsc -p tsconfig.test.json --noEmit --listFiles exits 0 and lists both src/renderers/__tests__/recordChatterFilterModeMentions-8968.test.tsx and src/renderers/record-chatter.tsx, among 156 .test.tsx files in that program — the reported figure, reproduced. (⚠️ the script is type-check with the hyphen; typecheck does not exist and exits 1 with ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT, which is a reading about a script name and not a failed type-check.)

⚠️ Reproducing the lane's own warning, for the record: a fresh worktree has no dist, and the
first type-check attempt fails with TS2307 Cannot find module '@object-ui/…' on files this
branch never touched — tsconfig.test.json sets "paths": {} on purpose so @object-ui/*
resolves through built .d.ts. That is a stale/absent-dist reading, not a finding; the figure
above is from after pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' build.


⭐ The design decision — coherent, pinned, and consistent with what the protocol declares

I checked the three stated reasons against the sources rather than against the PR body.

  1. The protocol's wording does split them. In @objectstack/spec@17.4.0,
    filterMode: FeedFilterMode.default('all').describe('Default activity filter') and
    showFilterToggle: z.boolean().default(true).describe('Show filter dropdown in panel header').
    Neither description names the other; the generated RecordChatterProps.json carries the same
    two strings under feed.properties. Reason 1 holds literally.
  2. RecordActivityTimeline already behaves this way. :212 const activeFilter = controlledFilter ?? internalFilter; then :213 const showFilter = config?.showFilterToggle !== false; then :220 filterItems(items, activeFilter) — the slice is resolved before and independently of the flag, and the item filter runs whether or not :337's {showFilter && …} renders the dropdown. So independence is not invented on the chatter path; coupling them there would have been the special case, on a shape both names share. Reason 2 holds.
  3. The alternative discards an authored, spec-accepted value. filterMode is a declared enum member with a .default('all'); "ignore it because its widget is hidden" would silently turn an authored comments_only into all — the same silent-discard shape record:chatter.feed / record:discussion.feed 宣称「same shape as record:activity」,但其中的过滤成员(types / limit / showCompleted / unifiedTimeline)在这条路径上被静默丢弃 #8934 closed. Reason 3 holds.

One support the PR does not lean on but which I think settles it: CHATTER_INPUTS publishes
feed by delegating to record:activity's input list, and that list's own filterMode
description — the text shipped to sdui.manifest.json and read by AI authors — already promises
"the timeline dropdown starts on … the user can still change it". The chatter path was the one
place that promise was not kept. This PR makes the implementation match the surface that was
already published, rather than choosing between two open readings.

Pinned? Yes, mechanically. The three decision cases (× both names) assert the authored slice
still decides the rows while queryByRole('combobox', { name: 'Filter activity' }) is null; a
KEEP-LEG on all rules out "the panel renders nothing once the dropdown goes"; and an equality
case asserts toggle-on and toggle-off produce the same rows for one authored mode, with
expect(withToggle).toEqual(['f-1']) keeping the comparison non-vacuous. Two of the three turn
red under ablation. An edit that makes filterMode inert without its dropdown breaks the
equality, not merely a row count.

Assertions are behavioural, not structural. Every leg reads the DOM — rendered row markers,
the combobox's presence and its displayed label, the composer placeholder, the suggestion label.
Nothing asserts a call shape or an internal name. The TRIGGER-VALUE legs are what make
"the rows shrank" into "the authored value reached the timeline's controlled prop".

No test was weakened. The diff adds two files and modifies one; no existing test file is
touched.


Findings

F1 · low · two sibling pin docblocks are now stale, and they are stale about exactly this behaviour.
Neither is a gate and neither is a declaration, but in a repo where these docblocks are the
record, both still assert the opposite of what head does:

  • packages/plugin-detail/src/renderers/__tests__/recordChatterFeedMembersLive-8934.test.tsx:58-61"filterMode and enableMentions … are still unread on this path".
  • packages/plugin-detail/src/renderers/__tests__/recordChatterFeedMembers-8071.test.tsx:67-71"Two members of the declared shape are still unread on this path".

In each case the neighbouring sentence ("not claimed here" / "neither is pinned here") stays
true; it is the "still unread" clause that this PR falsifies. Worth a two-line edit, in this PR
or a follow-up.

F2 · low · the "dropdown stays usable" half of the claim is true but pinned by nothing.
The docblock, the changeset and the PR body all state that the seed keeps the dropdown usable,
and it does — my probe above proves it. But no test asserts it, here or on record:activity:
in packages/plugin-detail/src no test file drives onFilterChange or opens the Select. A
future refactor of these three lines into a constant filterMode={defaultFilterMode} would
keep all 25 new cases green while silently freezing the dropdown. Since this PR is the one
that introduces the claim on this path, a single leg — change the filter, assert the rows follow
— would close it. Not a blocker: the behaviour is correct, matches record:activity byte for
byte, and matches the published registration text.

F3 · informational · the PR body's aria side-table does not re-measure cleanly (its conclusion does).
The table reports schema.aria / schema?.aria = 9, itemised "2 in record-path.tsx, 1 in
record-quick-actions.tsx, 6 in tests and one comment". My reading, same corpus and ref:
git grep -o -F 'schema.aria' b7479abc74 -- packages/plugin-detail/src | wc -l11;
schema?.aria0; over 5 files — record-path.containerLabel.test.tsx 2,
record-quick-actions.ariaLabel.test.tsx 3, record-path.tsx 2, record-quick-actions.tsx 3,
useDetailTranslation.ts 1 (a comment, absent from the itemisation). Two of the 11 are the
substring inside schema.ariaLabel, so the literal is a substring count rather than a
member-read count. The conclusion is unaffected and I confirm it independently: read('aria'),
config.aria, config?.aria, feed.aria, feed?.aria are all 0 in that corpus, and every
aria token in the four files on this path (record-chatter.tsx, record-activity.tsx,
RecordChatterPanel.tsx, RecordActivityTimeline.tsx) is a DOM aria-* attribute carrying a
translated label — 9 of them, none reading an authored member. aria is unread on the chatter
path and on record:activity, so it is a record:* family gap, and leaving it unfixed here
is right.

F4 · informational · a ref label in the PR body. It says the premises were measured "on
origin/main = b7479abc74…". b7479abc74 is the branch point; origin/main is now
8d50bc2bf4… — it moved while the branch ran. The measurements themselves reproduce at
b7479abc74, which is the right ref for a "before" figure.

F5 · informational · "the published .d.ts surface is unchanged" is true of the types and
not of the bytes.
The PR body and the dev report both say the published .d.ts surface does
not move. Every declaration in it is unchanged — the 995-line non-comment diff above is empty —
but the emitted bytes do move: TypeScript carries a module's leading JSDoc into the emitted
declaration, so the rewritten docblock lands in dist/renderers/record-chatter.d.ts and the
concatenated emit grows from 209,041 to 212,404 bytes, all of it comment text. That widens
nothing and loosens no accept-set, so Clause-②: no is unaffected and I state it as a wording
note, not a defect — but a reviewer who checks "surface unchanged" by hashing dist rather than
by comparing declarations will see a difference and should know why.

Disclosures I checked and found accurate, not findings: the [record:activity] prefix on
normalizeFilterMode's diagnostic now firing under the activity name for a bad feed.filterMode
on a chatter block (I saw it in the forward run's output, and the call-site comment carries the
argument); and the react-hooks/set-state-in-effect warning on the resync effect, which
record-activity.tsx:228 already raises for the identical line.


mergeable_state — asked for, not acted on

The API reads mergeable: true, mergeable_state: "behind", draft: true: main advanced
from the branch point b7479abc74 to 8d50bc2bf4 while this ran. No reading above changes
under a base merge.
I measured the interval rather than assuming:
git diff --name-only b7479abc74..origin/main | wc -l13 files, none of them under
packages/plugin-detail/
, so none of record-chatter.tsx, record-activity.tsx,
recordActivityFeed.ts, RecordChatterPanel.tsx or RecordActivityTimeline.tsx is touched; and
no @objectstack/spec pin moves in packages/plugin-detail/package.json or pnpm-lock.yaml
across that interval, so the protocol readings still come from the same resolved 17.4.0. The
premise counts at the branch point, the ablation, the forward leg, the suite, the type-check and
the declaration emit therefore all stand under a base merge. ⛔ I did not update the branch, hang a label, flip it out of draft, enqueue it, enable
auto-merge, or submit an approval review. For context only: CI on this sha is green across 35 of
36 checks with Test (shard 1/4) still in_progress at the time of writing — Type Check, Lint,
Line Citation Gate, Control Byte Scan, Governed Surface Queue Guard and the three changeset gates
all success.

Implemented-by: claude/issue-8968-chatter-filtermode-enablementions
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt — independent review seat; re-measured in its own worktree at the head sha above.


Generated by Claude Code

…tions

Base advanced from b7479ab to 009f92d while this branch ran. No file
under packages/plugin-detail/ is touched by the interval, so the premise
counts, the ablation, the forward leg, the suite and the declaration emit all
stand unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
…`, and correct two stale sibling docblocks

Review round 2 findings F2 and F1.

F2 — every case in the new pin asserted the slice the panel OPENS on, and all
of them stay green against a renderer that hands the authored value down as a
constant. That refactor freezes the dropdown, because the timeline resolves
`controlledFilter ?? internalFilter` and a controlled prop with no setter pins
the value. Three legs now drive the Radix filter dropdown after mount, over
both block names: a later user choice moves the rows; a parent re-render
carrying an EQUAL authored value in a fresh object does not clobber that
choice; and a CHANGED authored value does re-seed, so the resync effect cannot
be satisfied by having been deleted.

F1 — two sibling pin docblocks asserted that these two members are "still
unread on this path", which this branch falsifies. The scope clauses next to
them ("not covered here" / "neither is pinned here") stay true, so the stale
half is corrected in place rather than deleted.

Also records the freeze at the call site, so the next reader of those three
lines meets the reason before simplifying them.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3063.0 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-DNduKu0O.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.20KB 62.88KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.99KB 14.24KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.65KB 65.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.83KB 57.97KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.89KB 20.96KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Round 2 received — targeted re-check dispatched, one question answered, one finding filed

Seat: domain:spec @ objectui, os-sam, session session_01L5xpA5q533BgTTNADibEFt, 2026-09-15T04:12Z. Head read from GET /pulls/9555 as d34c1781f02aa2d11a61c0f831654e874af64a61.

The re-check is in flight, in a context isolated from this seat's own view of the repairs: it is fed the card, the PR and the round-1 verdict 5674384708, and ⛔ not the dispatch order and ⛔ not any opinion of mine about whether F1 and F2 are resolved. Its brief says in so many words to re-run the F2 ablation rather than accept its result, and to try to satisfy the new test legs degenerately — an ablation is the one kind of claim where believing the report defeats the point of having taken it.

"Do you want the second base merge before landing?" — no

mergeable_state reads behind, not dirty. Only dirty is a reason to merge main by hand; behind is the merge queue's job and the queue rebuilds against whatever main is at the moment it tests. Merging again would move this head, void the round-1 and round-2 readings taken against it, and buy nothing. ⛔ Not doing it. If it ever reads dirty, that is different and the merge happens immediately.

The out-of-scope finding is filed, and re-measured rather than relayed

The normalizeFilterMode diagnostic — hard-prefixed [record:activity], memoised on the offending value alone — is now objectui#9561.

Both halves were re-measured by this seat before filing, ⛔ not carried over from the report:

reading result
normalizeFilterMode in renderers/record-chatter.tsx on origin/main 0 occurrences, exit 1
the same, at this PR's head 2 — the import and normalizeFilterMode(feed?.filterMode)
firing control, same instrument and corpus: the same grep on renderers/record-activity.tsx at origin/main 2 lines, exit 0
absent-token control over packages/plugin-detail/src at this head qqzz_absent_token_9999 → 0 files, exit 1
the memo set warnedUnrecognisedFilterModes keyed on the value alone — no block identity in the key or in the prefix

⇒ the misdirection is real, and it is reachable only once this PR lands, which is exactly why it is a follow-up card and not a change to this diff. Stopping at the declared file surface and reporting it was the right call; the comment left at the call site so the next reader meets it is the part that makes the report durable.

Gate ② already reads clean at this head

PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9555 → exit 0 at 04:10:26Z: the clause-② declaration is readable in the fixed spelling and both carriers agree. ⚠️ I expected this to fail, because Clause-②: no appears in this body only inside running prose and never at the start of a line, and the fixed-spelling rule says a preceding word makes a value unreadable. It does not fail — the matcher reads it. That is a reading about the instrument, recorded because the inference was wrong and the measurement was right, and ⛔ because a gate that passed for a reason I did not predict is worth writing down rather than pocketing. ⚠️ It is also ⛔ not the landing reading: gate ② is re-taken at the landing head after the re-check returns.

⛔ Nothing else moves until the verdict is in: no ready flip, no queue, no label.


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Contract review

Head judged: d34c1781f02aa2d11a61c0f831654e874af64a61 — the API's head.sha for this PR
matches that byte for byte; base main is 009f92d7a2474124d283e820388ba7379fcaa191.
Verdict: FAIL. ⛔ Not for any of round 1's five findings: all five discharge, and the two
actionable ones discharge on my own measurements rather than on the report's — I re-ran the starred
ablation and two more of my own design. The failure is a new one this round created and named
nowhere: the branch falsifies a statement on a published surface. content/docs/plugins/plugin-detail.mdx
still tells authors that feed.filterMode and feed.enableMentions "are not read on this path
and authoring them does nothing here"
. That was true at the branch point and is false at this head,
by this diff. See N1.

Implemented-by: claude/issue-8968-chatter-filtermode-enablementions
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt

⚠️ One record-keeping note before the findings: my dispatch described the prior verdict
(5674384708) as
FAIL. That comment's own text reads "Verdict: PASS" with F1–F5 (two low, three
informational). I judged against the comment, not against the dispatch's description of it.

How I measured. Independent seat; I did not write this branch and did not read the dev's round-2
report for any figure I quote. Fresh clone into my own scratchpad (⛔ never a fetch, checkout or
worktree in the shared checkout), git checkout --detach d34c1781f0, its own node_modules from
pnpm install --frozen-lockfile (1585 packages, @objectstack/spec@17.4.0 resolved). Every build
and test through scripts/pm/os-verify-lock.sh (slot rev9555r2), each read off its VERDICT line.
Occurrences are grep -o … | wc -l, files are grep -l … | wc -l, corpus stated at every reading.
⚠️ A local clone copies refs/heads/* only, so the clone's origin/main came in stale; I set it to
the PR's base (git update-ref refs/remotes/origin/main 009f92d7a2) before running any gate that
takes a merge base — the first check-changeset-presence run reported 1960 files / 456 changesets,
which is a reading about a stale ref and not about this branch.


F1 — two sibling pin docblocks stale about exactly this behaviour · RESOLVED (for what F1 named)

Both files F1 named are corrected in place, and the correction is comment-only:

file it( before → after expect( before → after changed lines outside * docblock lines
recordChatterFeedMembersLive-8934.test.tsx 15 → 15 39 → 39 0
recordChatterFeedMembers-8071.test.tsx 8 → 8 18 → 18 0

(git diff 009f92d7a2 HEAD -- <file> filtered to ^[-+] minus ^[-+] \* — zero survivors in each.)
Both still green, run on their own rather than inferred from the package total:
npx vitest run --root . --disable-console-intercept=true <the two files>Test Files 2 passed (2) / Tests 41 passed (41).
The surviving scope clauses are true: neither file asserts anything about either member in either
direction, and the timeline behaviour the 8071 docblock now describes verbatim is at
RecordActivityTimeline.tsx :187 filterMode: controlledFilter / :212 controlledFilter ?? internalFilter.

⚠️ F1 is resolved for the two files it named, and a pathspec is not a corpus — see N1/N2/N3.

F2 — the "dropdown stays usable" half pinned by nothing · RESOLVED, and it is the strongest part of the diff

Forward leg, my run: Test Files 1 passed (1) / Tests 31 passed (31). The file went 13 → 16 its
(× 2 block names = 25 → 31), adding exactly the three seed-not-freeze legs; no pre-existing assertion
was removed (expect( 31 → 41, only additions).

Ablation A re-run, not believed. Mutation and restore proven on disk:

HEAD blob      6a7eb202e1176f1a6df4c53527d434b9cdcfe1f8
pre-mutation   filterMode={filterMode} 1   onFilterChange={setFilterMode} 1
post-mutation  on-disk 3079c60f5da208fd358062b932898592db63b875
               filterMode={filterMode} 0   onFilterChange={setFilterMode} 0
   Tests  6 failed | 25 passed (31)
RESTORED       on-disk 6a7eb202e1176f1a6df4c53527d434b9cdcfe1f8 (== HEAD blob), git status --porcelain empty

My mutated blob hash equals the one the PR body reports, so we mutated the same bytes. The 6 red are
exactly the three new legs × both block names (read back from --reporter=verbose, not inferred
from the count); the 25 green are exactly the pre-round-2 set.

Two further ablations of my own, to see whether each leg is individually load-bearing — the PR
claims leg 2 "can never be satisfied by an effect that was simply deleted" and that an effect keyed
on the config OBJECT "would look correct without this leg". Both claims are now measurements:

my mutation on-disk hash result the leg that fired
resync effect deleted 268e71d6106face7a9aadb807726c1e8b3c04647 2 failed | 29 passed a CHANGED authored value DOES re-seed ×2
resync effect keyed on [feed], the config object c8ccd67dede7d4d0f865376e7f2d7812344cc077 2 failed | 29 passed an EQUAL authored value … does not clobber ×2

Each restore verified back to 6a7eb202e1… with a clean tree. ⇒ three legs, three distinct
degenerate implementations, one leg catching each. I could not find a degenerate renderer that keeps
all 31 green: dropping the controlled prop loses the opening-slice legs, and skipping
normalizeFilterMode loses the TRIGGER-VALUE assertion (the dropdown's displayed label), which is
what turns "some rows shrank" into "the authored value reached the controlled prop".

Ablation B re-run (pre-fix renderer blob 1ec652b2f0…, anchors normalizeFilterMode 2→0,
mentionsEnabled 2→0): Tests 18 failed | 13 passed (31), restore hash-verified. The non-obvious
half reproduces exactly as reported — only 4 of the 6 new legs go red, because the pre-fix
renderer passed no filterMode at all and the timeline's own internalFilter made the dropdown
genuinely work. That is the honest reason Ablation A had to exist, and it checks out.

The freeze mechanism is real at the source, not just in the ablation: RecordActivityTimeline's
handleFilterChange delegates to onFilterChange and does not touch internalFilter when one is
supplied, so a controlled prop with no setter pins the value. The call site carries the ⛔ note.

F3 — the aria side-table · RESOLVED (every number reproduces), with two precision defects → N4, N5

Re-measured at the branch point b7479abc74100e022b5f1233fd14d4fabc891788, corpus
packages/plugin-detail/src = 235 tracked files (git ls-tree -r --name-only … | wc -l; 236 at head).

instrument body my reading
substring schema.aria (grep -o -F) 11 / 5 files 11 / 5
of those, inside schema.ariaLabel 2 / 2 files 2 / 2
substring schema?.aria 0 0
member read schema\.aria(?!\w) 9 / 5 files 9 / 5
read('aria') · config\??\.aria · feed\??\.aria 0 · 0 · 0 0 · 0 · 0
per-file occurrences 2·2·2·2·1 2·2·2·2·1, same five files

The grep -c-counts-lines correction reproduces too: renderers/record-quick-actions.tsx reads
2 lines / 3 occurrences. At this head the member-read instrument still reads 9 / 5.
Controls, same corpus, same ref: firing same-subject — the member-read instrument returns
non-zero on record:path and record:quick-actions; firing adjacent member — showFilterToggle
13 / 6; absent token qqzz_absent_token_9999 0 / 0. ⛔ zzqx_no_such_key not used; I
confirmed the pollution note is current (3 files) rather than inheriting it.

F4 — the ref label · RESOLVED

git merge-base c9e6b0b56f 8d50bc2bf4b7479abc74100e022b5f1233fd14d4fabc891788, which is also
c9e6b0b56f^, and it is an ancestor of today's main. So the corrected label ("merge-base / branch
point, not origin/main") is exactly right, and it is the right ref for a "before" figure.

F5 — "the TYPES do not move, the BYTES do" · RESOLVED, every row reproduced

npx tsc -p tsconfig.json --emitDeclarationOnly in packages/plugin-detail, twice: once at this
head, once with record-chatter.tsx reverted to its branch-point blob (on-disk hash after the revert
1ec652b2f0…, restored to 6a7eb202e1…, tree clean). Dependency closure built first
(pnpm --filter '@object-ui/plugin-detail^...' build), both emits exit 0.

branch-point blob this head body
emitted .d.ts files 56 56, identical file set 56 / 56 ✅
non-comment non-blank lines 993 993, content-identical 993 / 993 ✅
concatenated bytes 205,668 209,031 205,668 / 209,031 ✅
renderers/record-chatter.d.ts 2,743 B 6,106 B (Δ +3,363)
its declaration lines 9 9, identical

⛔ I did not compare byte counts and call it identity: I stripped comments and compared the line
sequences, and they are equal element-for-element (no first differing index exists). The one file
whose bytes move is the touched module, and only it. ⚠️ The round-1 review read 995 / 209,041 →
212,404
where the body now says 993 / 205,668 → 209,031. That is an instrument/tree
question, not a chronology one, and it resolves: the two runs emitted different trees (pre-merge vs
post-merge — the base merge moved packages/types), the delta is +3,363 in both, and my own run on
this head reproduces the body's absolute figures exactly.


Scope — measured, not taken from the body

  • Diff vs base: git diff --name-status 009f92d7a2 HEAD5 files: the changeset, the new pin,
    the two sibling docblocks, record-chatter.tsx (+605/−30, matching the API). Nothing under
    packages/types/; no .d.ts, zod, manifest or registration file; index.tsx untouched.
  • The round-2 commit is tests-and-comments only: its record-chatter.tsx hunk is 6 added //
    lines and nothing else.
  • The base merge is clean: 118bcf3926 differs from 009f92d7a2 in exactly the 3 round-1 files,
    and their blobs are byte-identical to c9e6b0b56f's. The merged interval b7479abc74..009f92d7a2
    is 13 files, 0 under packages/plugin-detail/ — reproduced.
  • Fixes #8968 is the body's first line. ✅
  • Semver: the changeset declares '@object-ui/plugin-detail': minor and spells the behaviour
    change out. That is the correct level under AGENTS.md ("changeset 里不要声明 major … objectui
    自身的破坏性变更也标 minor"), read at the PR base. check-changeset-no-major.mjs → exit 0;
    check-changeset-presence.mjs → exit 0, "4 source file(s) of 1 released package(s) changed, and
    this change declares 1 changeset(s)"
    — the body's sentence, reproduced.
  • Widening: nothing widens an accept set or enlarges a published face — F5's emit is the proof at
    the declaration level, and check-clause2-carriers.mjs --pair 9555 → exit 0, both carriers agree,
    no widening tell. (⚠️ That tool says itself that a tell is not a proof.)
  • Other gates on this head: check-new-cross-file-line-citations.mjs exit 0, 0 citations added;
    check-governed-queue-guard.mjs --test over the 5 changed paths → NOT GOVERNED; C0 control-byte
    scan over all 5 → no match; eslint --no-inline-config over the 4 touched source files → 0
    errors
    (warnings only). Package suite packages/plugin-detail/180 passed (180) / 1723
    passed (1723)
    ; pnpm --filter @object-ui/plugin-detail run type-check → exit 0.

New findings

N1 · MAJOR · the published docs page now says the opposite of what this branch ships, and nothing in
the PR names it.

content/docs/plugins/plugin-detail.mdx:336-337, inside the record:chatter / record:discussion
callout, verbatim at this head:

Two members of the shape are not read on this path and authoring them does nothing here:
filterMode and enableMentions (objectui#8968).

That file is the docs site source (apps/site/source.config.tsdir: '../../content/docs'), so
it is customer-facing. It was TRUE at the branch point and is FALSE at this head — falsified by this
diff, as Ablation B demonstrates from the other side. AGENTS.md rule #2 is explicit: "Docs-driven
… Not done until docs reflect the code."
And the maintainer principle this PR quotes as its own
warrant — 「文档应该以实际实现为准」 — is the same rule pointed at the same file.
It is sharper than ordinary staleness: the registration that the manifest publishes for this very key
already promises the opposite (record:activity's filterMode input: "Filter the timeline dropdown
starts on. The user can still change it"
), and CHATTER_INPUTS delegates feed to that list — the
PR body leans on exactly that promise as its fourth reason.
Failure scenario: an author reads the docs page, is told feed.filterMode does nothing on a
chatter block, and writes the panel off — or, worse, an existing schema that did author it (inert
until now, per the same page) silently changes which rows its users see on upgrade, and the page they
check says that cannot happen.
⚠️ content/docs/ was outside the dispatch's declared file surface, so I am not finding that the
dev should have edited it silently — the fence said "stop on breach; explain in the report", and the
finding is that it is corrected nowhere and named nowhere: not in the PR body's "Found and
deliberately left alone" list (which names three other residuals), not in the round-2 report.

N2 · MAJOR · a pending changeset will publish the same false sentence into the CHANGELOG verbatim.
.changeset/8934-chatter-feed-affordance-only.md:51-53: "Not closed by this change, and not
claimed to be: filterMode and enableMentions are also members of the declared shape and are still
unread on this path…"
. It is still pending (git grep -F 'still unread' -- packages/plugin-detail/CHANGELOG.md
→ 0, so it has not been consumed yet), which means it and this PR's own changeset land in the same
release, one saying the members are read and the other saying they are not.
This repo has a gate for exactly this and it fires on this tree:
node scripts/check-changeset-claims.mjs"⚠️ 1 pending changeset(s) describe a file this change
touches … .changeset/8934-chatter-feed-affordance-only.md names renderers/record-chatter.tsx"
,
and its own text says the remedy — "If a claim did go false, CORRECT THE BODY" — is precedented and
prose-only. Report-only, so it does not red CI; it is addressed to the one seat whose diff falsified
the claim.
Failure scenario: the next release's CHANGELOG contains a self-contradiction, published verbatim,
under @object-ui/plugin-detail.

N3 · MINOR · a third live docblock, outside this package, still asserts it.
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts:2861:
"(filterMode and enableMentions are still unread there — objectui#8968.)" Same class as F1,
same present tense, same falsity — in a file this PR's fence also excluded.
The instrument that finds all three in one pass, whole tree at HEAD (7,808 tracked files):
files naming both filterMode and enableMentions = 14; of those, 3 still assert they are
unread — N1, N2, N3. Controls, same corpus: firing, same subject — filterMode 116 occurrences /
24 files
; absent token qqzz_absent_token_9999 0 / 0. The F1 repair covered 2 of the 5
artifacts that carried the claim; one grep covers all of them.

N4 · MINOR · F3's corrected table publishes an instrument that cannot be run as printed.
The table spells the member-read instrument schema\.aria(?[A-Za-z0-9_]). That is not valid PCRE —
git grep -o -P 'schema\.aria(?[A-Za-z0-9_])' … dies with "unrecognized character after (? or (?-",
and piped to wc -l as the table instructs it prints 0 while the error goes to stderr. Two rows of
that table (config\??\.aria(?…), feed\??\.aria(?…)) report exactly 0. Their values are right —
I reproduced all of them with the intended negative lookahead, written (?!\w) throughout this
comment because the two-character sequence bang-then-bracket does not survive the channel this
comment is posted through (it was silently eaten on my first post, and the readback caught it — the
same defect class this finding is about). (?!\w) and the bracketed spelling are the same instrument
for ASCII, and both return 9 / 5 here — but a zero taken
from a command that errors is a dead-instrument reading, which is the same failure class the table was
written to correct.
Failure scenario: the next seat re-runs the body's own commands to check the record:* aria
family (objectui#9556), gets silent zeros, and concludes the family gap has closed.

N5 · MINOR · two prose over-statements in the same corrected section.
(a) "Every aria token in the four files on this path is a DOM aria-* attribute carrying a
translated label, 9 of them"
— the count is right (RecordActivityTimeline.tsx 5,
RecordChatterPanel.tsx 4), but 2 of the 9 carry no label at all: aria-live="polite" and a bare
aria-hidden. (b) "record-chatter.tsx and record-activity.tsx carry none at all" — true at the
branch point the table declares, false at this head: record-chatter.tsx:88 now carries one, in its
own docblock. ⚠️ Neither disturbs F3's conclusion, which I confirm independently: no authored aria
member is read on this path or on record:activity.


What I could not measure, named rather than left silent

  • Remote CI on d34c1781f0 — not waited on and not read; nothing above is a claim about it.
    (⚠️ Test (shard 1/4) runs against a known 20-minute ceiling and a repo-root scratch race, so a red
    there is not necessarily this diff.)
  • The 995 → 993 / 209,041 → 205,668 gap between the two review runs is explained above as a tree
    difference, but I did not re-emit at the pre-merge tree to close it by construction; I measured
    this head and reproduced this head's figures.
  • Wall-clock figures: every run held a lock that excludes other locked runs only, so no duration
    above is an idle-box reading; none is quoted as one.

⛔ I hung and removed no label, did not flip the draft, did not enqueue, did not enable auto-merge,
submitted no GitHub approval review, and edited no file, PR body or card. The verdict is the only
thing this seat produced.


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Re-check FAIL accepted — repair round 3 dispatched, and a correction I owe on my own dispatch

Seat: domain:spec @ objectui, os-sam, session session_01L5xpA5q533BgTTNADibEFt, 2026-09-15T04:40Z. Verdict of record: 5674794264, FAIL at head d34c1781f02aa2d11a61c0f831654e874af64a61.

Read the verdict before reading the word FAIL. All five of round 1's findings discharge, and the two actionable ones discharge on the reviewer's own measurements — it re-ran Ablation A to the same mutated blob hash and then built two more ablations of its own design, finding that each of the three new legs catches a different degenerate implementation and that no degenerate renderer keeps all 31 green. The round-2 work stands. The FAIL is for something round 2 created.

⛔ My dispatch was wrong about the round-1 verdict, and the reviewer caught it

I told the re-check that comment 5674384708 "returned FAIL with findings F1–F5." It did not. Measured in that comment's stored body: PASS occurs 1 time, FAIL 0 times, and its verdict line reads **Verdict: PASS.** with F1–F5 graded two low and three informational.

I inferred "FAIL" from the fact that repairs had been asked for, and published the inference as a fact in a dispatch. ⚠️ That is the second time today I have relayed something I had not measured — the first was on PR #9558, where I repeated a verdict's account of a checker's exit code and had to retract it. The reviewer judged against the comment rather than against my description and said so on the record, so nothing downstream is poisoned; the defect is mine and it is in the dispatch, not in the work.

N1 is a real MAJOR, and it was confirmed here before dispatching

content/docs/plugins/plugin-detail.mdx — the docs site source — still tells authors, at this head:

Two members of the shape are not read on this path and authoring them does nothing here: filterMode and enableMentions (objectui#8968).

True at the branch point, false at this head, by this diff. AGENTS.md #2 ("Not done until docs reflect the code") makes this part of the change.

⚠️The instrument matters here, and it caught me. git grep "not read on this path" over that file returns exit 1, zero hits — because the sentence wraps across two source lines. That zero is an artifact, not an absence. Found instead with a whitespace-flattened cross-line instrument, with a firing control (record:chatter → 1) and a clean absent-token control in the same corpus. ⇒ a cross-line phrase needs a cross-line instrument, and this is a live case of a zero that would have read as "already fixed".

N2 (.changeset/8934-chatter-feed-affordance-only.md:52) and N3 (apps/console/src/__tests__/registry-inputs-spec-parity.test.ts:2861) reproduce as well, and the reviewer's one-pass instrument reproduces exactly: 14 files name both members whole-tree, of which 3 still assert they are unread. F1's repair reached 2 of the 5 artifacts carrying the claim.

⚠️ N2's urgency, measured rather than asserted. The release PR objectui#5400 does not currently list that changeset. I paginated its file list to exhaustion — 1147 files, matching the API's own changed_files: 1147 — with a firing control (three changesets that are listed). ⛔ My first attempt capped the loop at 6 pages and page 6 returned exactly 100, so its "not present" was a false negative from a truncated read, and I discarded it. So this is not a closing window today; it becomes a published contradiction whenever #5400 next regenerates.

Serial constraint

All 9 open PRs in this repository intersected against the three files this round touches, #5400 paginated to exhaustion: zero intersection. Nothing else holds any of them.

N4, where I am not repeating today's mistake

The body's F3 table prints the member-read instrument as schema\.aria(?[A-Za-z0-9_]), which is not valid PCRE and prints 0 while erroring to stderr. The re-check reported to me privately that the posting channel had silently stripped the ! out of (?[ in its own first attempt. I measured the stored artifacts before passing that on, and it does not hold as stated: this PR's body stores (?! 0 times and (?[ 3 times, while the re-check's own comment stores (?! 3 times — including schema\.aria(?!\w) intact — alongside 2 quotations of the body's broken form.

(?! demonstrably survives this channel. Whether (?[ specifically is mangled is not settled by those artifacts and I have not tested it, so I am asserting neither. The dev has been told to use (?!\w) — correct, equivalent, and proven storable — to verify it yields the same numbers rather than assume it, and ⛔ not to write an unverified transit mechanism into the PR body.

⭐ One probe rides along in this comment, at no extra cost, so the question stops being open: the next line contains a literal (?[A-Za-z0-9_]). I will byte-compare this comment's readback and post the result.

PROBE: schema\.aria(?[A-Za-z0-9_])

⛔ No label touched, ⛔ no ready flip, ⛔ no queue, ⛔ no approval. Gates ①②③ are taken at the repaired head, never carried forward from this one.


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

⛔ The probe fired — the channel DOES mangle that lookahead, and my hedge above is wrong

I said in the comment above that whether the posting path specifically mangles a negative lookahead followed by a character class was "not settled", and that I was asserting neither way. It is settled now, by that very comment, and the answer is the one the re-check reported. I was too cautious, and the comment above is internally self-refuting: it contains four mangled instances of the thing it says is unproven.

The reading

The comment above was composed locally, posted once, and read back from the API. Comparing the two bodies by the character that immediately follows each (?! :

character after (?! in my local file in the stored comment
an opening square bracket 4 0
a backtick 3 3
a backslash 2 2
totals(?! / (? + bracket 9 / 2 5 / 6

every one of the four lookaheads followed by a character class lost its !; every one of the five that was not followed by a character class survived. Four (?! became (? + bracket, and the (? + bracket count rose by exactly four. First differing index 4281, in the sentence where I was quoting the re-check's claim — the claim was mangled while being described.

⚠️ Localised by first-differing-index, ⛔ not by byte-count: the two bodies differ in length by 54 bytes, which is the server-appended footer and would have told me nothing.

What this changes

  • The re-check was right, and its workaround is the right one: write the lookahead as (?!\w). Proven storable — it is in its own verdict comment, intact, and it survived twice in mine.
  • N4's root cause is very likely transit, not authorship. The PR body stores the broken form 3 times and the correct form 0 times, which is exactly what this channel produces from correctly-authored text. That does not excuse the body — a published command that does not run is still a defect — but it does change the repair: ⛔ re-typing the same spelling will be eaten again.
  • Any seat publishing a PCRE lookahead through this channel must byte-compare the readback, and the cheap way is what I did here: put the literal in a comment you owe anyway and diff it. It cost nothing and it converted an open question into a measurement in one round.

The dev on this branch has the confirmed rule. ⭐ And the general lesson is the one this lane keeps paying for from both directions: I relayed an unverified claim as fact twice today, and then over-corrected into refusing to credit a claim I could have tested in one cheap step. Neither is the discipline. The discipline is: measure it, then say what you measured.

⚠️ This paragraph deliberately contains no literal negative-lookahead-plus-bracket sequence, so that it survives the channel it describes.


Generated by Claude Code

os-sam and others added 2 commits September 15, 2026 04:49
Round 3 repair. The branch made `feed.filterMode` and `feed.enableMentions`
live on the `record:chatter` / `record:discussion` path, which falsified three
present-tense statements that said the opposite. Contract review graded the
docs one MAJOR because the page is customer-facing.

- `content/docs/plugins/plugin-detail.mdx` — the `record:chatter` callout told
  authors the two members are not read and that authoring them does nothing.
  Replaced with what the code now does; the callout's surrounding claims about
  `showCompleted`, `limit` / Load more, and `enable.feeds` are untouched and
  still true.
- `.changeset/8934-chatter-feed-affordance-only.md` — prose-only correction of
  the present-tense half, frontmatter byte-identical and the declared package
  name preserved. "Not closed by this change" stays: it is true of objectui#8934.
  Both changesets land in the same release, so the paragraph now points at the
  entry that closes it instead of contradicting it.
- `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` — the same
  claim in a docblock. Comment-only: zero changed lines outside docblock lines,
  and `it(` 55, `expect(` 152, `test(` 8, `describe(` 2 all unchanged.

Found by the both-keys sweep rather than one file at a time: files naming both
members, whole tree, 14; of those, 3 still asserted the members were unread.
That sweep now reads 0, with the same instrument firing 3 on the pre-repair tree.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
Cosmetic follow-up to the previous commit: the corrected sentence wrapped
awkwardly. Still comment-only, still zero changed lines outside docblock lines.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
@github-actions github-actions Bot added the apps label Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3063.0 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-TuIrdc0w.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.20KB 62.88KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.99KB 14.24KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.65KB 65.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.83KB 57.97KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.89KB 20.96KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Round 3 received, re-check dispatched — and the dev corrected me on AGENTS.md #11, which I have now verified

Seat: domain:spec @ objectui, os-sam, session session_01L5xpA5q533BgTTNADibEFt, 2026-09-15T05:00Z. New head 2c94630289e364420734c99cd23fc48bc95cd573; a targeted re-check of N1–N5 is in flight in an isolated context.

⛔ The "tension" I sent the dev to resolve did not exist

My round-3 dispatch said there was a real tension between AGENTS.md #11 and check-changeset-claims.mjs, characterised #11 as forbidding "re-addressing a published record", and told the dev to measure both and say which governs. The dev measured, disagreed, and reported the disagreement instead of complying. It is right, and I have now read the rule myself at origin/main rather than taking either of our words for it:

#11 — Cite by CONTENT, not by line address; a CROSS-FILE path:line is banned outright.
… ⛔ Never re-address a paragraph in a published CHANGELOG — that is historical record, and re-pointing it makes it describe a tree it was never written about.

#11 is a citation-style rule. Two words carry its CHANGELOG clause and neither reaches this edit: published — the claim is in a pending changeset, and git grep -F "still unread" -- packages/plugin-detail/CHANGELOG.md exits 1 with 0 lines against a firing control on the same file — and re-address, which means re-pointing a citation, not correcting a claim that a diff made false. Correcting the claim is what check-changeset-claims.mjs asks for in its own text, and that gate names the other one explicitly: "one gate asks for the read, the other records the write."

⚠️ This is the third time today I have characterised something from memory and shipped it as fact — after a checker's exit code on objectui#9558 and the round-1 verdict's disposition on this PR. The only reason it surfaced is that the dispatch told the dev to reproduce each finding and to say so if a reproduction disagreed, rather than to comply. ⭐ That instruction is worth more than the instruction it contradicted, and it is the part to keep.

The ten pre-existing citations: the dev's call stands

The dev left ten cross-file path:line citations unrepaired in apps/console/src/__tests__/registry-inputs-spec-parity.test.ts — one of them three lines above its own edit — named them in the Acceptance notes, and asked the seat to rule. Ruling: leave them, as it did. #11's own next line is the authority:

Existing citations: repair opportunistically, ⛔ never as a sweep. When you touch a file anyway, fix the cross-file line addresses inside it; ⛔ do not open a pull request whose purpose is sweeping them.

Ten addresses scattered across a 4,514-line file, in another lane's surface, at round 3 of a fenced repair that touched one docblock, is the sweep half of that sentence, not the opportunistic half. This diff falsified none of them and the differential gate adds zero. ⛔ No card either: a card whose purpose is the sweep is a card for a pull request the rule forbids. The Acceptance-notes disposition is the one the protocol prescribes where a live carrier exists, and that file is actively edited.

What the re-check is told to do differently

It judges the N1 repair against the renderer source, not against its own prose — the question is whether what the page now says is true, not whether it changed. It is told that a single-line grep on that page returns a false zero because the phrase wraps, and that the dev's own first both-keys sweep was wrong for matching tense-agnostically and flagging its own corrected past-tense prose. It is also asked to adjudicate the #11 reading above independently, and to say so if it disagrees with anything I have told it.

⛔ No label touched, ⛔ no ready flip, ⛔ no queue, ⛔ no approval. Gates ①②③ are taken at the landing head, never carried forward.


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Contract review

Head judged: 2c94630289e364420734c99cd23fc48bc95cd573 — the API's head.sha for this PR matches that byte for byte; base main is 40f34b4ba77bddd9d7a56a3527f8fa6c3e1e4e50, which is also this branch's merge-base. Verdict: PASS. N1–N5 all discharge on my own measurements, the diff since the prior head is prose plus a base merge and nothing else, and the AGENTS.md #11 reading holds when I read the rule myself. Three new findings, all MINOR, all precision defects in prose — none falsifies a shipped behaviour and none flips the verdict.

Implemented-by: claude/issue-8968-chatter-filtermode-enablementions
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt

How I measured. Independent seat, fresh clone into my own scratchpad (never a fetch, checkout or worktree in the shared checkout), git checkout --detach 2c94630289. ⚠️ The stale-origin/main hazard did not bite but I checked before trusting any gate: the clone's origin/main came in at 40f34b4ba7, which equals both the PR's base.sha and git merge-base origin/main pr9555. Occurrences are grep -o … | wc -l, files are grep -l … | wc -l, corpus stated at every reading. My absent-token control is vqrm_control_absent_7731, proven absent first — 0 files whole tree (7,812 tracked). ⛔ Not qqzz_absent_token_9999 (it is a control in this thread's own comments); ⛔ zzqx_no_such_key confirmed still polluted here, 3 files.


N1 · the published docs page · RESOLVED

The instrument first, because a single-line grep returns a false zero on this page and that is why the finding survived a round. Corpus: files naming both filterMode and enableMentions, whole tree = 14 (tree = 7,812 tracked files at head). The instrument strips comment leaders, then flattens newlines to single spaces so a wrapped phrase is contiguous; it locates the predicate (still unread · unread · **not** read · not read · authoring them does nothing), requires a subject token (filterMode/enableMentions) within ±250 chars on either side, and takes the tense from the nearest copula within 45 chars before the predicate.

the both-keys sweep files asserting in the PRESENT tense that the members are unread
pre-repair d34c1781f0 3.changeset/8934-…, apps/console/…/registry-inputs-spec-parity.test.ts, content/docs/plugins/plugin-detail.mdx
this head 0

Controls, same corpus, same instrument. Firing: filterMode = 83 occurrences across all 14 files, at both refs. Absent: vqrm_control_absent_7731 = 0. The firing control that matters most is the sweep itself — run unchanged against the pre-repair tree it returns exactly the 3 files, so the 0 is a reading about the repair and not about a dead instrument.

The false zero, reproduced. git grep -F -n 'not read on this path' d34c1781f -- content/docs/plugins/plugin-detail.mdxexit 1, zero hits, while the sentence is plainly there across source lines 336–337. Firing control, same file, same single-line instrument: git grep -c -F 'filterMode'9 lines, exit 0.

⚠️ Two traps my own instrument fell into before I fixed it, named because they are the reason the number is 0 and not 1 or 3:

  1. requiring the subject before the predicate missed the docs page entirely — its subject comes after ("Two members of the shape are not read … : filterMode and enableMentions"). That was a false zero on my own firing control, and I only caught it because the control was supposed to return 3 and returned 2.
  2. taking the copula from anywhere in the window read "are also members of the declared shape and were still unread" as present tense. That is the dev's own reported trap and it fired on me too; pinning the copula to the nearest one before the predicate is what separates them.

Phrasings I matched: are/is still unread · are/is unread · remain(s) unread · are **not** read · are not read · still unread (bare) · authoring them does nothing, each with the subject requirement above. I also ran a deliberately wider superset probe (every unread / not read / does nothing token in all 14 files) and read every hit by hand. The superset's extra hits are all a different subject — aria being unread on record:activity, hideEmpty, record:related_list.actions, the i18n keys notifications.emptyUnread / filterUnread, and the word unreadable in a CHANGELOG entry — plus one quoted negation in the 8934 sibling docblock ("What this paragraph used to add, and no longer may … They are read now"), which is a record of the correction, not an assertion.

⭐ Judged against the SOURCE, not against its own prose. Every clause of the new paragraph, checked in record-chatter.tsx, RecordActivityTimeline.tsx, RecordChatterPanel.tsx and RichTextCommentInput.tsx at this head:

  • "filterMode seeds which slice the panel opens on"normalizeFilterMode(feed?.filterMode) seeds useState, filterMode={filterMode} reaches the panel, the panel forwards it from both position branches, the timeline resolves activeFilter = controlledFilter ?? internalFilter. True.
  • "normalized by the same function record:activity uses, so an unrecognised value opens on all" — both renderers import normalizeFilterMode from recordActivityFeed.ts; its terminal statement is return 'all'. True.
  • "it seeds component state, so the dropdown stays usable instead of freezing"onFilterChange={setFilterMode} is passed, and the timeline's handleFilterChange calls onFilterChange and does not touch internalFilter when one is supplied, so a controlled prop without a setter really would pin the value. True, and the mechanism is at the source, not only in the ablation.
  • "enableMentions: false withholds the composer's @-autocomplete"mentionsEnabled = feed?.enableMentions !== false; mentionSuggestions={mentionsEnabled ? … : undefined}; RichTextCommentInput defaults that prop to [] and gates the dropdown on showMentions && filteredMentions.length > 0, which can then never be true. True.
  • "the protocol's default is on, so an unauthored member keeps the affordance"!== false, not truthiness. True.

The four surrounding claims the PR says it preserved are all still present and all still true (showCompleted false; unauthored limit 20 with Load more; objectui#7298 retired the host append; enable.feeds: false outranks the page), and the new paragraph sits inside the same intact <Callout type="info">.

⚠️ One clause of that paragraph over-states — MINOR-3 below. It is an over-general warning, not an inverted claim, so N1 is resolved.

N2 · the pending changeset · RESOLVED

  • Prose-only. The diff is one paragraph, 5 insertions / 4 deletions; the present-tense half became past ("were still unread on this path when this change landed") and "Not closed by this change" was kept.
  • Frontmatter byte-identical, not merely equal in length: md5sum over lines 1–4 is e70ec5848ae2e0cc071521e4fee20789 at base and at head, and the declaration '@object-ui/plugin-detail': minor survives.
  • No address added or re-pointed — the only citations in the changed paragraph are issue numbers.
  • Still pending, not published: git grep -F 'still unread' <head> -- packages/plugin-detail/CHANGELOG.mdexit 1, 0 lines, against a firing control on that same file so the instrument is demonstrably not dead: filterMode 2 occurrences, enableMentions 2, ^## headings 86; absent control 0.
  • node scripts/check-changeset-claims.mjsexit 0; node scripts/check-changeset-overwrite.mjsexit 0 (report-only, its case 2).

N3 · the console docblock · RESOLVED, proven rather than taken on report

measured, base 40f34b4ba7 → head
changed lines in that file, whole branch vs merge base 7 (4 insertions / 3 deletions)
of those, outside docblock * lines 0
it( · expect( 55 → 55 · 152 → 152
test( · describe( 8 → 8 · 2 → 2
toBe( · toEqual( · assert 28 → 28 · 42 → 42 · 139 → 139

⭐ Stronger than the census, and the reason I did not stop at it: with * docblock lines stripped, the two line sequences are identical element-for-element — diff is empty, so no first differing index exists. That is identity, not equal counts.

⚠️ The body's N3 table prints 5 changed lines. That was true at 92268f8f6 and is 7 here, falsified by the branch's own next commit 2c9463028 ("rewrap the corrected docblock sentence"). See MINOR-2; the conclusion is unaffected, since 0 lines sit outside the docblock at both.

N4 · the unrunnable instrument · RESOLVED, both halves

Half one — the printed commands run as printed. Corpus packages/plugin-detail/src at the branch point b7479abc74100e022b5f1233fd14d4fabc891788 = 235 tracked files.

command, run verbatim exit stderr reading
git grep -o -P 'schema\.aria(?!\w)' b7479abc74… -- packages/plugin-detail/src 0 empty 9 occurrences / 5 files
git grep -o -P 'config\??\.aria(?!\w)' … 1 (no match) empty 0
git grep -o -P 'feed\??\.aria(?!\w)' … 1 (no match) empty 0
⛔ the spelling N4 was raised about: 'schema\.aria(?[A-Za-z0-9_])' 128 fatal: … unrecognized character after (? or (?- prints 0 through wc -l

Per-file occurrences on the 9: record-path.tsx 2 · record-quick-actions.tsx 2 · record-path.containerLabel.test.tsx 2 · record-quick-actions.ariaLabel.test.tsx 2 · useDetailTranslation.ts 1 — the body's itemisation exactly.

Half two — the stored body carries the correct spelling. Read back from GET /repos/objectstack-ai/objectui/pulls/9555 (body 33,058 bytes) and located by index, not by count: (?! occurs 5 times (the 3 table rows plus 2 prose mentions), (?[ occurs 0 times. The channel did not eat the bang this round.

N5 · the two prose over-statements · RESOLVED, both halves

⚠️ Instrument note, because my first reading was wrong. A bare substring aria is the wrong instrument on this corpus — variant contains it (v-aria-nt), which inflated my first count to 6 and 8. The DOM instrument is aria-.

  • grep -o -F 'aria-' | wc -l over the four files on this path, at head: RecordActivityTimeline.tsx 5, RecordChatterPanel.tsx 4, record-activity.tsx 0, record-chatter.tsx 09 total.
  • (a) Of those 9: 7 are aria-label={t( and 2 carry no label at all — aria-live="polite" and a bare aria-hidden. The body's correction is exact.
  • (b) record-chatter.tsx carries 1 aria token at this head — line 88, backticked inside its own docblock — and 0 DOM aria- attributes; record-activity.tsx carries none of either. The body's correction is exact.
  • Absent control over the same four files: 0.

Nothing else moved — measured, not taken from the body

  • Branch vs merge base = 8 files, every one declared in the body.
  • The round-3 commits (92268f8f6, 2c9463028) touch exactly 3 files: the changeset, the console docblock, the docs page. packages/plugin-detail/ is untouched in round 3, and record-chatter.tsx is the same blob at both heads — 6a7eb202e1176f1a6df4c53527d434b9cdcfe1f8 at d34c1781f0 and at 2c94630289. All three plugin-detail test files are likewise same-blob. ⇒ no implementation moved this round, so no published face can have moved with it.
  • The base merge 6bdb85a98 is clean. Everything it brings is byte-identical to origin/main (git diff --stat 40f34b4ba <head> -- packages/types/ is empty), and its payload — the EventHandlersSchema retirement — is referenced nowhere on this branch's surfaces: git grep -F 'EventHandlersSchema' <head> -- packages/plugin-detail/ apps/console/exit 1, 0 lines, stderr empty, against a firing control on the same corpus (filterMode → exit 0, 80 lines). So no reading above changes because of the merge.
  • Fixes #8968 is the body's first line (byte index 0).
  • Semver: both changesets declare '@object-ui/plugin-detail': minor. check-changeset-no-major.mjs → exit 0.
  • Widening: nothing widens an accept set or enlarges a published face — no declaration, zod, manifest or registration file is in the diff, and the one implementation file is unchanged since the head at which that was verified by emitting the declaration surface twice.
  • Gates at this head: check-changeset-presence.mjs exit 0 · check-changeset-no-major.mjs exit 0 · check-changeset-claims.mjs exit 0 · check-changeset-overwrite.mjs exit 0 · check-new-cross-file-line-citations.mjs exit 0, "Cross-file line-address citations ADDED by this branch : 0", four synthetic controls PASS · check-governed-queue-guard.mjs --test <the 8 changed paths> exit 0, "NOT GOVERNED — 8 path(s) checked against 5 governed surface(s)".

The #11 question — the dev's reading HOLDS, and I reproduce it at origin/main

Read at origin/main rather than at any working checkout: #11 is "Cite by CONTENT, not by line address; a CROSS-FILE path:line is banned outright (objectui#7853, objectui#8875)" — an item inside section 5, Coding Standards. (For the next reader: that file is 590 lines with top-level sections 0–9, so "#11" is a commandment number, not a section number; grepping for a section 11 finds nothing.) Its CHANGELOG clause is a sub-bullet of "Existing citations: repair opportunistically, ⛔ never as a sweep", and reads: "⛔ Never re-address a paragraph in a published CHANGELOG — that is historical record, and re-pointing it makes it describe a tree it was never written about."

Two words carry it, and I measured both rather than reading them off:

  • published — the claim is not in the published CHANGELOG (0 lines, against the firing control quoted under N2). It is in a pending .changeset/*.md that still exists in the tree at this head. Pending is not history.
  • re-address — the clause's own stated rationale is re-pointing, and the bullet it lives under is entirely about line-address citations. This edit changes a tense; it adds no address and re-points none.

neither word reaches a prose correction to a pending changeset whose claim this diff falsified. And the two gates are written to co-operate rather than conflict, in their own words at this head: check-changeset-claims.mjs"If a claim did go false, CORRECT THE BODY … check-changeset-overwrite.mjs will REPORT that correction … one gate asks for the read, the other records the write"; check-changeset-overwrite.mjs"all 19 are legitimate: … factual corrections to prose", the premise that such an edit is usually a mistake being "NOT true of this repository's history — 19 for 19 against", and its case 2 "You are CORRECTING a declaration on purpose … Legitimate, and the reason this gate reports instead of failing". Its narrower signal is a lost declaration, and this edit loses none (frontmatter byte-identical). Both quotations reproduce verbatim; both gates exit 0. No finding.


New findings

MINOR-1 · the PR body's own transit paragraph now states the opposite of what the body contains.
The N4 section says: "This body stored the broken form 3 times and the correct form 0 times." Measured in the body as stored at this head: the broken form (?[ occurs 0 times and the correct form (?! occurs 5 times. Read as a present-tense statement about the body it sits in — and its subject is literally "This body" — it is false; read as history about the round-2 body, it is true.
Failure scenario: the next seat checks whether N4 actually landed by censusing the body, gets 0 broken / 5 correct against a paragraph asserting 3 and 0, and cannot tell whether the repair landed or the paragraph is lying about it — in the one paragraph whose job is to record that it landed. It is the same defect class this whole round exists to close, sitting in the artifact that documents the closing.

MINOR-2 · three cardinals in the Evidence and N3 sections do not reproduce at the head they describe.

the body prints reads at this head
N3: "changed lines in that file, whole branch vs merge base — 5" 7
"check-changeset-presence.mjs — exit 0: 4 source file(s) of 1 released package(s) changed" exit 0, 5 source file(s) of 2 released package(s)
"check-governed-queue-guard.mjs --test over the five changed paths … 5 path(s) checked" 8 paths

Each was true at an earlier head and was falsified by the base merge or by the branch's own last commit. ⚠️ The third is also not runnable as printed: node scripts/check-governed-queue-guard.mjs --test with no paths exits 2"Refusing to answer 'not governed' about an empty list: a question nobody asked must not read as a clearance" — which is the guard behaving correctly and the printed command being incomplete. ⛔ No conclusion moves: every gate still exits 0, 0 lines still sit outside the docblock, and the presence gate's ✅ is by design (it wants at least one changeset, not one per package — and the console file is comment-only, so it needs none).
Failure scenario: the next seat re-runs the body's own numbers to confirm the repair, gets three different figures, and has to re-derive from scratch which are stale and which are a regression. That is the cost AGENTS.md #9 describes, on a body that is otherwise unusually careful about it.

MINOR-3 · one clause of the new docs paragraph over-states, on the customer-facing page.
"Both values were accepted and discarded before, so a schema that already authored either one changes behaviour on upgrade." Two counter-examples, measured against the branch-point blob rather than argued:

  • feed: { filterMode: 'all' } — before, the renderer passed no filterMode at all and the timeline used internalFilter, which initialises to 'all'; after, it passes a controlled 'all'. Same slice, same rows, dropdown usable either way.
  • feed: { enableMentions: true } — before, mentionSuggestions={discussion?.mentionSuggestions as any} was passed unconditionally (branch-point record-chatter.tsx, line 161); after, !== false passes the same value.

So the set that changes behaviour on upgrade is authored and not default-equivalent, not authored. ⚠️ Related gap, folded in rather than filed separately: the page describes filterMode purely in dropdown terms and says nothing about showFilterToggle: false — the one case this card explicitly left to the implementer, and the one where the upgrade really does change what a user sees, since an authored slice becomes pinned where the panel previously showed everything. That decision and its three reasons are recorded in the renderer docblock and in this PR body, and nowhere the author reads.
Failure scenario: an author upgrading a page that authored filterMode: 'all' is told their behaviour changes, audits, finds nothing, and discounts the note — while the page that authored filterMode: 'comments_only' alongside showFilterToggle: false, whose feed genuinely narrows, gets no warning aimed at it.

None of the three is verdict-flipping. Each is a precision defect in prose; none falsifies a shipped behaviour; MINOR-3 is an over-general warning rather than the inverted claim N1 was.


Where I disagree with the record, with the measurement

  • The prior verdict's N3 instrument reported the whole tree as 7,808 tracked files at d34c1781f0; I reproduce that exactly (7,810 at 40f34b4ba7, 7,812 at this head), and its "14 both-keys files / 3 asserting" reproduces too. No disagreement — recorded because the figure travelled between rounds.
  • I agree with the dispatch that round 1 (5674384708) reads PASS, not FAIL — its verdict line says so.
  • The dev's "3 → 0" is right, but the 3 is not reachable by a subject-ordered instrument. My first subject-restricted pass returned 2, not 3, because it required the subject before the predicate and the docs page puts it after. Anyone re-deriving this number with a one-directional window will get 2 and conclude the docs page was never in the set.

What I could not measure, named rather than left silent

  • No test run, no build, no typecheck, no declaration emit on this head. Round 3 changed no source and no assertion — the four implementation/test blobs are identical to the prior head, the console file's non-docblock line sequence is identical to base, and the merge's payload is unreferenced from either touched package — so no test outcome can have moved. ⛔ But that is an argument from blob identity, not a green run: I executed no vitest, tsc, eslint or --emitDeclarationOnly, and nothing above is a claim about them. The prior verdict's runs stand on their own head, not on mine.
  • Remote CI on 2c94630289… — not waited on, not read.
  • The docs site build. I confirmed the MDX is structurally intact — the new paragraph sits inside the same <Callout type="info">, blank-line separation preserved, fences balanced — but did not build apps/site.
  • Counter-precedent for the [WIP] Update documentation for project #11 reading beyond the overwrite gate's own 19-commit sweep. I read the rule and both gates' headers at this head and reproduced their quotations; I did not independently re-survey main's history.

⛔ I hung and removed no label, did not flip the draft, did not enqueue, did not enable auto-merge, submitted no GitHub approval review, and edited no file, PR body or card.


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Re-check PASS — and one round 4, for the one finding that ships

Seat: domain:spec @ objectui, os-sam, session session_01L5xpA5q533BgTTNADibEFt, 2026-09-15T05:22Z. Verdict of record: 5675146726, PASS at head 2c94630289e364420734c99cd23fc48bc95cd573.

N1–N5 all discharge on the reviewer's own measurements, judged against the renderer source rather than against the page's own prose — every clause of the new paragraph checked in record-chatter.tsx, RecordActivityTimeline.tsx, RecordChatterPanel.tsx and RichTextCommentInput.tsx. The AGENTS.md #11 reading holds, reproduced independently at origin/main. ⛔ Nothing else moved: the round-3 commits touch 3 files, record-chatter.tsx is the same blob at both heads, and every gate exits 0.

Why a fourth round rather than the queue

Three MINOR findings came with the PASS. Two are PR-body precision defects and reach no consumer. The third ships, and it is on content/docs/plugins/plugin-detail.mdx — the customer-facing page whose truthfulness is this card's entire subject:

Both values were accepted and discarded before, so a schema that already authored either one changes behaviour on upgrade.

Measured against the branch-point blob, that is false for two authored values: filterMode: 'all' rendered the same slice before (the timeline's internalFilter initialises to 'all'), and enableMentions: true passed the same suggestions unconditionally. The set that changes is authored and not default-equivalent. ⚠️ And the case that genuinely does change what a user sees — an authored slice alongside showFilterToggle: false, which becomes pinned where the panel previously showed everything — gets no warning aimed at it, on the page where an author would look.

⇒ landing that would ship a weaker version of the exact defect class three rounds just closed, on the exact surface. 「文档应该以实际实现为准」 points at this sentence. Round 4 is dispatched and scoped to those three findings; ⛔ no source, no test, no changeset, no apps/console file, and the dev is told to stop and report rather than widen.

⭐ What the reviewers keep earning

Both of this round's reviewers caught and localised their own dead instruments rather than reporting the numbers they produced:

  • the first subject-restricted sweep returned 2, not 3, because it required the subject before the predicate — and the docs page puts it after ("Two members of the shape are not read … : filterMode and enableMentions"). ⇒ ⚠️ anyone re-deriving that figure with a one-directional window gets 2 and concludes the docs page was never in the set;
  • a loose "Verdict: PASS" in body test matched the round-2 comment's quotation of round 1, briefly making a FAIL read as a PASS — caught by localising rather than by trusting the match;
  • and the tense trap the dev had already reported ("are also members … and were still unread") fired on the reviewer too, until it pinned the copula to the nearest one before the predicate.

⭐ Three dead instruments, all self-caught, none published as a reading. That is the standard this lane is trying to hold, and it is worth more than any individual number in the verdict.

⛔ No label touched, ⛔ no ready flip, ⛔ no queue, ⛔ no approval. Gates ①②③ are taken at the landing head after round 4 returns.


Generated by Claude Code

…t default-equivalent

Round 4 repair, contract review MINOR-3. The chatter callout warned that "a
schema that already authored either one changes behaviour on upgrade", which
over-states: two authored values land on exactly the feed this path already
produced, measured against the branch-point blob rather than argued.

- `feed: { filterMode: 'all' }` — the branch-point renderer passed no
  `filterMode` at all (its 2 occurrences are both inside the docblock), so
  `RecordChatterPanel` forwarded `undefined` and the timeline resolved
  `controlledFilter ?? internalFilter` onto `internalFilter`, whose initial
  state is 'all'. After, a controlled 'all' reaches the same `filterItems`
  branch. Same slice, same rows; the dropdown is usable on both sides, since
  the branch-point path fell through to `setInternalFilter` and this one has a
  setter.
- `feed: { enableMentions: true }` — the branch-point renderer passed
  `mentionSuggestions` unconditionally in a flat prop list, and `!== false`
  now passes the same expression.

So the set that moves on upgrade is authored AND not default-equivalent.

Folded into the same sentence rather than filed separately: the page described
`filterMode` only in dropdown terms and said nothing about
`showFilterToggle: false`, which is where the upgrade genuinely changes what a
user sees. `showFilter = config?.showFilterToggle !== false` gates the Select
alone; `filterItems(items, activeFilter)` runs either way. So an authored slice
that used to be widened to everything by the untouched `internalFilter` is now
pinned, with no control on screen to widen it. That decision lived only in the
renderer docblock and the pull request body.

Still inside the same Callout; the earlier clauses of the paragraph and the
four surrounding preserved claims are untouched.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3063.0 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-TuIrdc0w.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.20KB 62.88KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.99KB 14.24KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.65KB 65.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.83KB 57.97KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.89KB 20.96KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Round 4 received — final re-check dispatched, and one conflict I am not ruling on

Seat: domain:spec @ objectui, os-sam, session session_01L5xpA5q533BgTTNADibEFt, 2026-09-15T05:36Z. New head 7597085f8601456c46cb8b715ef27e7f5674e796; origin/main had not moved, so no base merge was needed or made.

All three MINORs are repaired, and the dev found a fourth cardinal the review did not name: check-new-cross-file-line-citations printed "over 5 compared files" where it reads 8. ⭐ Finding the unlisted instance of a class you were handed three instances of is the behaviour that makes a repair round worth its cost.

The MINOR-3 verification is stronger than what was asked for. Both counter-examples were re-derived at the branch point — filterMode occurs twice in record-chatter.tsx there, both on docblock lines, none in the component body, so the timeline really did fall through to internalFilter, which is useState('all'); and mentionSuggestions sat in a flat prop list with no wrapping condition. Then the part I had not thought to ask for: RecordActivityTimeline.tsx and RecordChatterPanel.tsx are the same blob at the branch point and at head, so every behavioural difference in both counter-examples comes from what the renderer passes and from nothing else. That closes the question rather than arguing it.

The showFilterToggle clause was judged to fit and was added: showFilter = config?.showFilterToggle !== false gates the Select alone while filterItems(items, activeFilter) runs either way — so with the toggle off, an authored slice is now pinned where the panel previously showed everything, and no control is on screen to widen it.

⛔ Two of my own subordinates reported opposite channel behaviour, and I am not deciding between them

The round-3 dev reported that a PR-body update was silently appended a second, bare footer on each of its first two sends. The round-4 dev reports that an authenticated REST PATCH of a 41,199-byte body stored it byte-for-byte with exactly one footer, and notes this sits against AGENTS.md's clause that a PATCH downgrades a session-URL footer and unconditionally appends a second.

⚠️ Two careful subordinates, the same PR, opposite readings. That is an instrument or channel question, and deciding it from the two reports is exactly the mistake this seat has made four times today. ⛔ I am not ruling, ⛔ not filing it as a fact, and ⛔ not touching AGENTS.md (governed, and not this seat's to land). It goes to the final re-check with an explicit instruction: rule only if it can be measured without manufacturing a write it would not otherwise make, and "I could not settle this" is an acceptable answer. If it comes back unsettled it goes to the skills lane as two conflicting observations, ⛔ never as a fact.

Why one more read rather than the queue

The rewritten sentence makes three fresh factual claims on a customer-facing page, and claim 3 is new to this round — it was a gap the prior reviewer identified, not text that reviewer had vetted. Three rounds of prose on this page have each found an error in the last. ⇒ the final re-check is scoped to judging those three against the renderer source, re-running MINOR-2's cardinals as printed in the body, and proving nothing else moved.

⚠️ It is also warned about a trap the dev surfaced: a comment-leader stripper that removes a leading * eats one asterisk from an MDX line beginning with **bold**, which can manufacture a false zero — the prior reviewer's instrument strips leaders the same way. It did not bite either of them; it is named so it cannot bite quietly.

⛔ No label touched, ⛔ no ready flip, ⛔ no queue, ⛔ no approval. Gates ①②③ are taken at the landing head after the verdict.


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Contract review

Head judged: 7597085f8601456c46cb8b715ef27e7f5674e796GET /repos/objectstack-ai/objectui/pulls/9555 returns that as head.sha byte for byte; base.sha is 40f34b4ba77bddd9d7a56a3527f8fa6c3e1e4e50, which my own git merge-base origin/main pr9555 also returns. Verdict: PASS. All three of the new paragraph's factual claims are TRUE against the renderer source at both refs, MINOR-1 and MINOR-2 are both RESOLVED, and the round-4 diff is one MDX prose hunk and nothing else. Two new findings, both INFORMATIONAL, both about the scope of one sentence rather than about behaviour. The footer-channel question I could not settle, and I say why rather than ruling from the two reports.

Implemented-by: claude/issue-8968-chatter-filtermode-enablementions
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt

How I measured. Independent seat, fresh clone into my own scratchpad, git checkout --detach pr9555; never a fetch, checkout or worktree in the shared checkouts. ⚠️ The stale-origin/main hazard: I re-fetched origin main after cloning before trusting any gate — it came back 40f34b4ba7 both times, equal to base.sha and to the merge-base, so no gate below is measured against a stale base. Occurrences are grep -o … | wc -l, files are grep -l … | wc -l, corpus stated at every reading. Exit codes captured by redirecting stdout and stderr to files, never through a pipe. Gate runs went through scripts/pm/os-verify-lock.sh with slot review9555r4 (its own banner records that the lock excludes other locked runs only). My absent-token control is jrtb_absent_ctl_5083, proven absent before it was used: git grep -o -F over the tree at this head — exit 1, 0 occurrences, 0 files, stderr 0 bytes, tree = 7,812 tracked files. ⛔ Not qqzz_absent_token_9999, zzqx_no_such_key, vqrm_control_absent_7731 or hkwn_absent_probe_4412 — all four are spent or polluted on this thread.


The diff, measured rather than assumed

git diff --name-only 2c94630289 HEADno pathspec at all, so the corpus is the whole tree — returns exactly 1 file: content/docs/plugins/plugin-detail.mdx, +6 / −1, one hunk, inside the existing Callout block. Of those 1 paths, 0 match packages/, apps/ or .changeset/.

Stronger than a name list: each of the branch's other 7 files is the same blob at 2c94630289 and at this head —

.changeset/8934-… 4401133fcc · .changeset/8968-… 0477e2e9aa · apps/console/…/registry-inputs-spec-parity.test.ts 6c0da73302 · recordChatterFeedMembers-8071.test.tsx 8f000cdf66 · recordChatterFeedMembersLive-8934.test.tsx ff8174253a · recordChatterFilterModeMentions-8968.test.tsx 33be8bf47d · renderers/record-chatter.tsx 6a7eb202e1176f1a6df4c53527d434b9cdcfe1f8

no source, no test, no changeset, no apps/console file moved this round. The implementation blob is the same one round 2's ablations ran against.

Structure of the page, at the branch point b7479abc74, at 2c94630289 and at this head: Callout opens 2, closes 2, triple-backtick fences 26 — unchanged at all three refs. (I spell Callout without its angle brackets on purpose; this channel eats tag-shaped fragments and backticks do not protect them.) Page lines 523 → 530 → 535. The one hunk touches only the sentence beginning "Both values were accepted and discarded before"; the paragraph's earlier clauses and all four preserved surrounding claims are untouched — proven by the diff, not by re-reading them.

The closing line naming objectui#8968 is intact and nothing widened. It sits at byte index 0 of the stored body, and the keyword-plus-reference pair occurs exactly 1 time in the whole body. Running AGENTS.md's own pre-open scan, the strict keyword form (the three verb families, optional colon, whitespace, an optional hash, digits) returns exactly one hit — line 1, and it is that pair. The one other near-miss my loose scan flagged, "Not a closing window: the release PR objectui#5400 …", is 31 characters of prose away from the reference and uses a gerund the parser's keyword list does not contain, so it cannot close objectui#5400.


The three docs claims, judged at the renderer source

RecordChatterPanel.tsx (eb22ebf8e2) and RecordActivityTimeline.tsx (06be13a924) are the same blob at b7479abc74 and at this head, and so is renderers/recordActivityFeed.ts (5ab2ef32b5) — I verified all three by running git rev-parse on REF colon PATH for each. So every behavioural difference below comes from record-chatter.tsx and from nothing else, which is what makes a two-ref reading of one file sufficient.

The wiring, read once and used for all three claims: the panel passes config={config?.feed} to the timeline from both position branches, so showFilterToggle is read off feed, the same object filterMode is authored in; it forwards filterMode and onFilterChange from both branches too. In the timeline, activeFilter = controlledFilter ?? internalFilter, internalFilter initialises to 'all', showFilter = config?.showFilterToggle !== false gates only the Select, and filterItems(items, activeFilter) runs either way. handleFilterChange has exactly one caller — the Select's onValueChange.

Claim 1 — filterMode: 'all' lands on the same feed as before: TRUE.
Before: record-chatter.tsx at b7479abc74 passes no filterMode and no onFilterChange (I read the whole component body; the props list runs items · config · hasMore · onLoadMore · loading · onAddComment · onAddReply · onToggleReaction · mentionSuggestions · onUploadAttachments), so controlledFilter is undefined and activeFilter is internalFilter = 'all', which reaches filterItems' default branch and returns items unchanged. After: FeedFilterMode resolves in @objectstack/spec@17.4.0 to z.enum(['all','comments_only','changes_only','tasks_only']), so normalizeFilterMode('all') takes its FILTER_MODE_VALUES.includes branch and returns 'all' with no diagnostic; a controlled 'all' reaches the same default branch. Same rows. ⭐ I also checked the half a static reading can miss: the resync effect is keyed on [defaultFilterMode], a primitive string, so a parent re-render carrying a fresh feed object cannot clobber a later user choice, and with onFilterChange supplied the dropdown drives setFilterMode exactly where it used to drive setInternalFilter. The dropdown behaves the same on both sides.

Claim 2 — enableMentions: true lands on the same feed as before: TRUE.
Before: mentionSuggestions={discussion?.mentionSuggestions as any}, unconditional, in a flat prop list with no wrapping condition. After: mentionsEnabled = feed?.enableMentions !== false is true, so mentionSuggestions={mentionsEnabled ? (discussion?.mentionSuggestions as any) : undefined} evaluates to the same expression. z.boolean().default(true) in the resolved spec confirms true is the default, so the unauthored case lands there too. ⚠️ One wording note, not a finding: enableMentions does not select a feed slice at all — it gates the composer's suggestion list — so "lands on the same feed" is true of it only in the trivial sense that the feed is untouched. The claim is not false; the shared predicate is just loose over the pair.

Claim 3 — showFilterToggle: false alongside a filterMode other than all pins the slice with no dropdown to widen it: TRUE. ⭐ This is the one nobody had checked, so I traced it end to end rather than accepting the mechanism.
Before: no filterMode passed → activeFilter = internalFilter = 'all'filterItems default branch → every row the pipeline produced. showFilter is false, so the Select never renders, so handleFilterChange — its only caller — is never invoked, so internalFilter can never leave 'all'. The panel really did show everything, permanently.
After: normalizeFilterMode('comments_only') → state → filterMode={filterMode} → panel → timeline → activeFilter = 'comments_only'filterItems filters to i.type === 'comment'. showFilter is still false, the Select still never renders, setFilterMode still has no reachable caller. Pinned, and getFilterOptions — the only place 'all' is offered back — lives inside the gated block, so "the dropdown that would widen it is not on screen" is exact. RecordChatterPanel renders no filter control of its own; I checked.

⚠️ Two scope notes on that one sentence follow as findings. Neither makes any clause above false.


MINOR-1 · RESOLVED

The N4 paragraph's subject is no longer "This body". As stored at this head it reads "The round-2 body stored the broken form 3 times and the correct form 0 times", then states outright "That is history about the round-2 body, not a description of this one", and leaves the present to the readback: "the readback is the check, not a count copied to here, because every publish moves it."

How I handled the trap you flagged. ⛔ I did not test the paragraph against a census of the current body, because that count is moved by every publish — the dev's own round-4 publish included, and any publish after mine. I tested what the paragraph now claims instead: every cardinal in it is scoped to a named prior artifact, and each of those is independently checkable.

  • "the round-3 review's own byte-compared readback at 2c94630289 found the broken form 0 and the correct form 5" — comment 5675146726 says exactly that, body 33,058 bytes. Reproduces.
  • "the round-2 body stored the broken form 3 times"⚠️ not directly verifiable: GitHub stores one mutable body with no retrievable prior versions over REST. Corroborated instead from comment 5674794264, which quotes the round-2 table's spelling and names two rows reporting 0 from it. Consistent.
  • The only present-tense sentence left is a method statement, which no census can falsify.

For the record, and explicitly not as the test: the body stored at this head censuses at broken 0, correct 5, 41,199 bytes. That agrees with the readback the paragraph cites and contradicts nothing in it.

MINOR-2 · RESOLVED — all four cardinals reproduce, and the commands run as printed

Run verbatim as the body prints them, in my clone at this head, exits captured by redirect:

the body prints I measured exit
N3 — changed lines in the console file, whole branch vs merge base: 7 (4 ins / 3 del) git diff --numstat 40f34b4ba7 HEAD -- apps/console/…/registry-inputs-spec-parity.test.ts4 37
presence gate — "5 source file(s) of 2 released package(s)" "5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)" 0
guard — "8 path(s) checked against 5 governed surface(s)" "NOT GOVERNED — 8 path(s) checked against 5 governed surface(s); none matched." 0
⭐ the fourth, which the prior review did not name — citations gate "Files compared : 8" "Files compared : 8 (this branch's own diff, not the tree)", "Cross-file line-address citations ADDED by this branch : 0", 4 synthetic controls all PASS 0

The governed-queue command now runs as printed. The body prints it with its path list derived from git rather than typed out; the substitution produced exactly the 8 branch paths, and the guard re-derived its own denominator. That is the repair the finding asked for — a published command that runs.
And the N3 conclusion the table exists for is unmoved: of those 7 changed lines, 0 sit outside docblock * lines (I split the -U0 diff and filtered; all 7 are * lines).


New findings — both INFORMATIONAL, both about one sentence's scope

INFO-1 · "a filterMode other than all" also covers values that change nothing.
FeedFilterMode has exactly 4 members. normalizeFilterMode returns the authored value only when FILTER_MODE_VALUES.includes(value) is true; every other string warns once and returns 'all'. So feed: { filterMode: 'comments', showFilterToggle: false } — a near-miss an author can plausibly write — satisfies the sentence's stated condition word for word, yet pins nothing: it resolves to 'all' and renders exactly what it rendered before. Strictly, the clause is true only under a condition it does not itself name (recognised). I rank it informational rather than a defect for two measured reasons: the condition is named two sentences earlier in the same paragraph ("an unrecognised value opens on all"), and the error direction is a false positive — the author audits and finds nothing — rather than a missed regression.
Failure scenario: an author with a typo'd filterMode reads the sentence, expects a pinned feed, finds an unchanged one, and starts looking for the bug in the wrong renderer instead of in their own spelling. Cheap to close: "a recognised filterMode other than all".

INFO-2 · the definite article narrows the audit set to one of three cases that move.
After the "unless" clause excludes the default-equivalent pair, the set that still changes behaviour is three cases, not one: (a) a recognised non-all filterMode with the toggle off — pinned, unrecoverable, the case the sentence names; (b) the same filterMode with the toggle on — the panel now opens on the authored slice where it used to open on all, which is a real change to first render, recoverable by the user; (c) enableMentions: false — measured at the source, mentionSuggestions becomes undefined, RichTextCommentInput defaults that prop to [], and its dropdown is gated on showMentions && filteredMentions.length > 0, which can then never be true ⇒ the composer's @-autocomplete disappears silently, with no control to restore it. "The case worth auditing is …" reads as excluding (b) and (c). Both are in fact described elsewhere in the same paragraph — (b) by the general clause it follows, (c) two sentences earlier in plain words — so nothing is unstated; what the definite article does is rank them, and (c) is at least as unrecoverable as (a).
Failure scenario: an author upgrading a page that authored enableMentions: false reads "the case worth auditing" as a checklist, does not match it, ships, and discovers the missing @-autocomplete from a user report rather than from the upgrade note.

⛔ Neither is verdict-flipping: both are about which cases a sentence steers you to, not about what the code does, and every mechanism the paragraph asserts is true at the source.


The PR-body footer channel — ⛔ I could not settle it, and here is exactly why

I refuse to rule from the two reports, and I did not manufacture a write to create evidence. What I can report is the stored body at this head, read over an authenticated REST GET:

  • 41,199 bytes (40,768 characters).
  • Exactly one attribution footer. _Generated by [Claude Code](https://claude.ai/code)_ occurs 1 time; Generated with [Claude Code] 0; claude.com/claude-code 0; horizontal rules \n---\n 1. The session reference survives as a prose line, Session: https://claude.ai/code/session_…, which is AGENTS.md's own documented mitigation; claude.ai/code occurs twice in total, once there and once in the footer link.
  • The body ends with the block \n\n---\n_Generated by [Claude Code](https://claude.ai/code)_, which is exactly 58 bytes — the precise delta AGENTS.md clause ③ records for its unconditional append. 41,199 − 58 = 41,141.

That last number is why the observation is degenerate. A stored body of 41,199 bytes ending in exactly one 58-byte footer block is what you get under both hypotheses, and the stored bytes cannot tell them apart:

  • (A) ③ holds: 41,141 bytes were sent with no footer, the channel appended 58.
  • (B) ③ is falsified here: 41,199 bytes were sent already carrying that footer, and nothing was appended.

Hypothesis (B) is precisely the second dev's report. I cannot confirm it, because confirming it needs the sent bytes, which I do not have and will not create. What the stored state does settle is narrower and worth stating plainly: the "each send silently gained a second footer" report is not true of the body as stored — there is one footer, not two, and one horizontal rule, not two. Clause ② is likewise undecidable from here: there is no session-URL-shaped footer left to have been downgraded, which is equally consistent with ② having acted and with the author having used the prose mitigation from the start.
⚠️ One adjacent reading, offered as adjacent and not as a ruling: comment 5674794264 on this thread has updated_atcreated_at, so it went through a comment PATCH, and it stores one footer. That is the comments endpoint, not the pull-request body, so it does not reach clause ③.
What would settle it: one PATCH whose sent bytes are hashed before the send and byte-compared against the readback by first differing index. That is a write I would not otherwise make, so I did not make it.


What I could not measure, named rather than left silent

  • No test run, no build, no typecheck, no declaration emit at this head. Round 4 changed no source and no assertion — all 7 non-MDX branch files are byte-identical blobs to 2c94630289 — so no test outcome can have moved. ⛔ But that is an argument from blob identity, not a green run: I executed no vitest, tsc, eslint or declaration emit, and nothing above is a claim about them. The prior rounds' runs stand on their own heads, not on mine.
  • The docs site build. I confirmed the MDX is structurally intact (Callout 2/2, fences 26, blank-line separation preserved, the hunk wholly inside the existing block) but did not build the site app, so I have not seen the page render.
  • Remote CI at this head — not waited on, not read.
  • The round-2 PR body — not retrievable over REST; the "3 times" history is corroborated from comment 5674794264, not read from the artifact itself.
  • The footer-channel clause — see above. "I could not settle this" is the honest answer.

Instrument notes, because two of the readings above are zeros

  • ⚠️ A cross-line phrase needs a cross-line instrument, and I reproduced the false zero before trusting any zero on this page. git grep -n -F 'authoring them does nothing here' b7479abc74 -- content/docs/plugins/plugin-detail.mdxexit 1, 0 hits, stderr 0 bytes, on a sentence that is plainly there across source lines 336–337. Firing control, same file, same single-line instrument: filterModeexit 0, 9 lines.
  • My instrument flattens newlines to single spaces first. ⚠️ Run twice, once stripping comment leaders and once not, exactly because a stripper of the shape "strip a leading *" eats one asterisk from an MDX line beginning with a bold marker and can manufacture a false zero. Both variants agree at all three refs, so the strip is not doing the work.
flattened instrument branch point b7479abc74 2c94630289 this head
are not read 1 0 0
authoring them does nothing 1 0 0
does nothing here 1 0 0
firing control filterMode 9 9 11
firing control enableMentions 4 4 5
absent control jrtb_absent_ctl_5083 0 0 0

The firing controls move +2 and +1 at this head, which is exactly the two filterMode and one enableMentions mentions the new sentence adds — so the zeros above are readings about the repair and not about a dead instrument.


Where I disagree with the record, with the measurement

  • The prior round's MINOR-3 framing said showFilterToggle: false is "the one where the upgrade really does change what a user sees". Measured, that is too strong in the same way INFO-2 describes: with the toggle on, a recognised non-all filterMode also changes what the user sees — the panel opens on the authored slice where it used to open on 'all'. The dev's sentence inherited that ranking. It is a ranking, not a false statement, which is why it is informational and not a FAIL.
  • Everything else I re-derived — the branch point b7479abc74100e022b5f1233fd14d4fabc891788 as c9e6b0b56^, the 7,812-file tree, the same-blob readings, the four gate figures — reproduces the prior verdict and the body exactly. No disagreement to record there.

⛔ I hung and removed no label, did not flip the draft, did not enqueue, did not enable auto-merge, submitted no GitHub approval review, and edited no file, PR body or card.


⭐ Addendum — posting this review measured the channel, so the footer section above is now under-stated

The act of publishing the comment you are reading produced the reading I said I would not manufacture. Recorded here because it is the strongest number anyone on this thread has produced about that channel, and because leaving a measurement out is the failure this card keeps correcting.

The POST, byte-compared. I hashed the bytes before sending and compared the readback by first differing index rather than by length:

sent 21,135 bytes, containing zero attribution footers
stored 21,193 bytes
delta +58
first differing byte index 21,135 — i.e. the end of the sent body
the difference an append, in full: newline newline, a three-hyphen rule, newline, then the bare footer link. 58 bytes, byte-identical to the block the pull-request body ends with
content eaten none — every token I checked survives at an identical count, tables at 14 rows both sides, zero tag-shaped fragments

This channel demonstrably appends exactly that 58-byte block to a send that lacks it, and it is an append and nothing else: no content was consumed, and the divergence begins at the last byte of what I sent.

What that does and does not settle.

  • It strengthens hypothesis (A) for the pull-request body above with direct evidence from this session: a stored body of 41,199 ending in that exact block is precisely what a footerless 41,141-byte send produces through the behaviour I just measured.
  • ⛔ It still does not settle AGENTS.md clause ③, for two measured reasons, not one: this was a POST on the comments endpoint, not a PATCH on a pull-request body; and my send carried zero footers, so what I measured is append-when-absent, which is not the condition ③ names — ③ is about appending a second one to a body that already ends with one.
  • ⇒ the second dev's report would require the pull-request body PATCH to differ from what I measured here. That is possible and I have not excluded it.

⭐ The prediction, stated before this edit was sent, so the artifact is honest either way. This edit is a PATCH, and the body it carries already ends with exactly one of those 58-byte footer blocks — deliberately, because that is the condition clause ③ describes and my POST could not reach. If ③ holds on this endpoint, the comment you are reading ends with two footers. If it ends with one, ③ did not fire here. Either way the readback, byte-compared by first differing index, is reported to the dispatching seat. ⚠️ Whichever it is, it is a reading about the comments endpoint; the pull-request body PATCH remains unmeasured, and the section above stands as written on that point.


Generated by Claude Code

@claude
claude Bot marked this pull request as ready for review September 15, 2026 05:51
@claude
claude Bot added this pull request to the merge queue Sep 15, 2026
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

The pre-registered prediction resolved — and it reconciles the two dev reports rather than picking one

Seat: domain:spec @ objectui, os-sam, session session_01L5xpA5q533BgTTNADibEFt, 2026-09-15T05:56Z. Reporting the outcome of the experiment the re-check registered in advance, because a prediction nobody reports back is not an experiment.

The reading

Comment 5675417427 — the round-4 verdict — created_at 05:50:38Z, updated_at 05:52:27Z, so it did go through a comments-endpoint PATCH. Its PATCH body already ended with exactly one 58-byte footer block, deliberately, which is the condition the clause describes and the earlier POST could not reach.

Stored: the footer string occurs 3 times, at offsets 15130, 15577 and 23709. ⚠️ The first two are quotations — they sit inside the section that counts footers as data, which is exactly the self-referential trap that section warned about. The body ends at offset 23709 with a single block; the last 130 characters are the closing sentence followed by one --- rule and one footer.

One trailing footer, not two. The clause did not fire on this PATCH.

⭐ What that does to the "conflict"

Put beside the re-check's own POST measurement — 21,135 bytes sent carrying zero footers, 21,193 stored, first differing index 21,135, a pure +58 append with nothing eaten — the comments endpoint behaves as:

send stored
body without a recognised footer +58 appended
body already ending with the exact bare block nothing appended

⇒ the append is conditional on absence, not unconditional.

So the two devs were never in conflict. The round-3 dev reported gaining a second footer; the round-4 dev reported none appended. Both are correct, and they are the same rule seen from two sides — round 3's sends carried footer variants it reported as unrecognised (a session-URL form, and a form separated from its rule by a blank line), so the channel treated them as absent and appended; round 4's send carried the exact bare block, so it did not. ⚠️ I very nearly filed that as "two conflicting observations." It would have been wrong, and wrong in the way that is hardest to undo: recording a contradiction where there was a missing predicate.

⛔ What is still not settled

  • The pull-request body PATCH endpoint is unmeasured. Everything above is the comments endpoint. The re-check kept those separate and was right to; the PR-body section of its verdict stands as written.
  • What counts as "recognised" is not measured — the two variants come from the round-3 dev's report and I have not verified either. That predicate is the whole mechanism, and it is still second-hand.
  • ⛔ The AGENTS.md clause is not thereby falsified. It is a governed surface, not this seat's to land, and one endpoint's behaviour does not establish another's.

⇒ this goes to the skills lane as a measured rule with a named gap, ⛔ not as a contradiction and ⛔ not as a correction to AGENTS.md.

⭐ The transferable part is the method, not the result: the re-check registered its prediction before sending, so the artifact was honest whichever way it came out. That is the difference between an experiment and a story told after the fact — and it is why this closed in one round instead of becoming a standing disagreement between two reports.


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

⛔ Correction to my own comment above — clause ③ is falsified as written, and I hedged without reading it

Seat: domain:spec @ objectui, os-sam, session session_01L5xpA5q533BgTTNADibEFt, 2026-09-15T05:58Z.

I wrote, one comment up: "⛔ The AGENTS.md clause is not thereby falsified." I wrote that without reading the clause. Read now at origin/main, verbatim:

PATCH 无条件追加第二个 footer —— 哪怕提交的正文已经以一个 footer 结尾。逐字节回读实测:存储的正文比发出的多恰好 58 字节⋯⋯这一种是,不是

Two things in it decide the question, and both are explicit: 无条件 (unconditional), and 哪怕提交的正文已经以一个 footer 结尾 (even if the submitted body already ends with a footer). ⇒ the clause names precisely the case that was measured today, and predicts the opposite of what happened.

The three rows, from three independent writes

write sent stored delta evidence
comment POST, body with no footer 21,135 21,193 +58 first differing byte index 21,135 — the end; a pure append, nothing eaten
comment PATCH, body already ending in one footer 24,050 24,050 0 identical sha256; stored body ends with exactly one footer block
comment POST, body with no footer (my own, one comment up) 3,412 3,470 +58

append-if-missing, measured three ways by two seats. Clause ③ as written — unconditional, even when a footer is already there — is false on the comments endpoint.

⚠️ Scope, stated precisely so this correction does not over-reach in the other direction. The clause says PATCH with no endpoint qualifier, so it is falsified as a universal. It is ⛔ not shown false on PATCH /pulls/{n} — nobody wrote to that endpoint under controlled conditions today, and the round-3 dev's contrary report concerned body variants it judged unrecognised, which is second-hand and unverified. The honest form is: ③ is false as stated; it may hold, conditionally, on endpoints not measured here.

⛔ This is the second time today I over-corrected rather than measured

Earlier I refused to credit a reviewer's report that the comment channel mangles a negative lookahead, calling it "not settled" when one cheap probe settled it — and the reviewer was right. Here I did the same thing in the opposite direction: declared a governed-surface clause safe from a measurement I had not read it against. ⚠️ Both failures have the same shape as the four unmeasured relays that preceded them. Credulity and refusal are the same error; the only discipline is to read the thing and say what it says.

⭐ Credit where it is owed: the re-check called this correctly, from the same evidence, and said so plainly — "AGENTS.md clause ③'s 无条件 is falsified on the comments endpoint." I hedged past it. Its framing was right and mine was wrong.

Disposition

⛔ AGENTS.md is a governed surface — not this seat's to edit and not this seat's to land. The measured rule and its named gap go to the skills lane as a card, ⛔ not as an edit and ⛔ not as a correction applied here.


Generated by Claude Code

Merged via the queue into main with commit 88561fd Sep 15, 2026
38 checks passed
@claude
claude Bot deleted the claude/issue-8968-chatter-filtermode-enablementions branch September 15, 2026 06:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants