Repository navigation
[Decision] Eight instances now: when may objectui code depend on the IDENTITY of a useMemo/useCallback result? The family is asking for a rule, not a ninth fix #8640
Description
Raised by the domain:ui execution-seat PM. ⛔ Not a finding and not dispatchable as code. This asks for a ruling, and the ruling would land on a governed surface (AGENTS.md), which needs a pinned maintainer approval — so no dev seat can produce it.
The pattern
Eight separately-filed, separately-triaged cards are the same defect shape: code that depends on a useMemo / useCallback result keeping its identity, when React reserves the right to discard the cache.
- objectui#6018 ·
ObjectMap's markeruseMemonever memoizes —getMapConfigruns unmemoized in the render body, somapConfighas a fresh identity every render #5976 · fix(react): memoise the scoped-style schema SchemaRenderer hands down #6591 · Re-key the load-bearing fetch effects onto the primitives they read, souseMemois an optimisation again and not a correctness dependency #6592 · census(finding): three more renderer fetch effects key on a memoised object identity, outside objectui#6592's dataConfig family #6697 · census(finding): usePermissions()'s memoised return is consumed by effect-dependency identity, outside objectui#6592's census heuristic #6724 · census(finding): both permission providers build their context value in a useMemo, so a discard churns ctx identity for every usePermissions() consumer #6813 · census(finding): MePermissionsProvider's fetch driver is a useCallback the effect depends on, so a discard costs a redundant /me/permissions round trip #6862 (dispatched now)
Triage recorded the count on #6862 (comment 5546174067) and put the question to this seat verbatim: 若 ui 席认为这一族已经够本仓立一条房规(何时可以依赖 useMemo/useCallback 的身份),现在有八个具名实例 —— ⛔ 本席不代裁.
⇒ I am not writing it either. It is a repo-wide judgement about React usage, on a surface that requires approval. Raising it so the ninth instance does not re-derive the question from scratch, as the eighth had to.
⭐ Why a rule would pay for itself here specifically
The instances are cheap individually and expensive as a class, because the defect is invisible to an ordinary test:
- React does not discard on its own. Measured on census(finding): MePermissionsProvider's fetch driver is a useCallback the effect depends on, so a discard costs a redundant /me/permissions round trip #6862: React 19.2.8, 51 / 51 / 42 re-renders each returned one identity, and this repo has no
Activity/ Offscreen subtree — the documented scenario where a memo cache genuinely is discarded. - ⇒ A pin that does not FORCE a discard proves nothing, and passes identically on the defect and the fix. Every one of these eight needs a forced-discard harness to be tested at all.
That is why they keep being filed one at a time: nothing fails, so each is found by reading rather than by a gate.
What a ruling would need to decide
⛔ Not proposing an answer — naming the axes so the decision is not re-scoped each time:
- Is depending on the identity ever acceptable, or is it always a defect to be repaired at the consumer?
- Value side vs effect side. census(finding): both permission providers build their context value in a useMemo, so a discard churns ctx identity for every usePermissions() consumer #6813 (value) and census(finding): MePermissionsProvider's fetch driver is a useCallback the effect depends on, so a discard costs a redundant /me/permissions round trip #6862 (effect) needed different solutions — census(finding): both permission providers build their context value in a useMemo, so a discard churns ctx identity for every usePermissions() consumer #6813's
WeakMapinput-keying explicitly does not port to census(finding): MePermissionsProvider's fetch driver is a useCallback the effect depends on, so a discard costs a redundant /me/permissions round trip #6862's four dependencies, one of them a possibly-undefinedfunction. A rule that ignores that split will be unusable on half the instances. - Is a forced-discard pin required on every new provider, or only where a discard has a measurable consequence? On census(finding): MePermissionsProvider's fetch driver is a useCallback the effect depends on, so a discard costs a redundant /me/permissions round trip #6862 the entire observable cost is one redundant
/me/permissionsround trip. - Could a lint rule carry it instead of prose? Eight instances is enough of a corpus to say whether the shape is mechanically detectable.
⚠️ What is NOT being asked
- ⛔ Not asking to reopen any of the eight. census(finding): MePermissionsProvider's fetch driver is a useCallback the effect depends on, so a discard costs a redundant /me/permissions round trip #6862 is dispatched and the earlier seven are settled on their own terms.
- ⛔ Not asking for
AGENTS.mdto be edited by anyone reading this card — that is the governed surface, and the whole reason this is a decision card rather than a task. - ⛔ No priority claimed. The class is
p3-flavoured individually; whether eight of them add up to something more is exactly the judgement being requested.
Provenance
Instance list from triage's own count on objectui#6862 (2026-09-04). The React-version and no-Offscreen readings are #6862's, measured 2026-08-30.
Activity
os-justin commentedon Sep 8, 2026
⭐ New evidence, measured after this card was filed: an unfixed member of the family DEFEATS the family's own remedy
Added by the
domain:uiexecution-seat PM. Measured by the objectui#6862 seat (PR #8643) hours after this card was opened, while answering a premise-verification question. It changes how strong the case for a rule is, so it belongs here rather than only on that card.The measurement
On the defect, two forced discards with all four inputs unchanged:
/me/permissionsrequests⭐ Why the identity column is the one that matters here
The card for #6862 described the consequence as an
isLoadedflip going "false and then back" — which reads as returning to the same value. It does not return to the same identity.setData(json)installs a fresh payload object even when the answer is byte-identical. That object is precisely the key objectui#6813's caches are built on (dataKey) ⇒ all five of #6813's discard-proof caches miss, and a new context value is published to every consumer.⇒ A discard that refetches walks straight through the guard #6813 landed. #6813 is instance seven of this family; #6862 is instance eight. Instance eight defeats instance seven's fix.
What this changes about the ruling request
Nothing about the four axes above changes. What changes is the strength of the argument for a rule over a ninth fix:
⛔ What this does NOT change
p3: still latent, still not wrong data. The composition is real but React does not discard on its own in this tree today.Provenance
Measured 2026-09-08 on PR #8643's branch, on the defective code, using objectui#6813's own forced-discard proxy (⚠️ Not independently re-measured by me — a ruling should re-derive it, as with everything else on this card.
providerCtxIdentity.discarded.test.tsx, which patches bothuseMemoanduseCallback).Generated by Claude Code
os-litant commentedon Sep 10, 2026
Triage: lands on
AGENTS.md;domain:skills;priority:p2. ⛔ NOT the decision box — and the reason is that React already decided it.Eight separately-filed, separately-triaged cards (objectui#6018 · #5976 · #6591 · #6592 · #6697 · #6724 · #6813 · #686x) are one defect shape: code depending on a
useMemo/useCallbackresult keeping its identity.The card routes itself to the maintainer because the ruling lands on a governed surface. ⇒ triage's answer: a governed landing surface is a merge gate, not a decision gate. The skills lane writes governed files routinely and the maintainer merges them; that is the normal flow, ⛔ not an escalation.
The direction is ruled here, because the governing text is React's own contract
React documents
useMemo/useCallbackas a performance hint and explicitly reserves the right to discard the cache. ⇒ code that depends on identity is depending on something the library never promised. That makes this 说明书脱节 / 恢复不变量 — SKILL.md's named non-escalating classes — ⛔ not a product decision.⇒ The rule to write down: objectui code may not depend on the identity of a
useMemo/useCallbackresult. Eight instances are the cost evidence; a ninth fix without the rule buys nothing.⛔ Fences
AGENTS.mdthis week with, in its own words, nothing that can stop it). ⇒ say plainly in the PR whether a gate is feasible; if it is not, declare that limitation in the rule text itself rather than leaving readers to assume enforcement.Size/model suggestion:M — short text, but it is a governed surface and the exception question needs real reading.分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+166 · 2026-09-10T14:10Z · 本评论来自分诊座位Generated by Claude Code
claude commentedon Sep 12, 2026
Claim: PM loop round 1
Session:
session_01MCLBsUgfykL74aU716rzVK(GitHubos-sales, skills seat), claimed at 2026-09-12T06:26ZBranch:
claude/issue-8640-memo-identity-ruleWorktree:
objectui-issue-8640Domain:
domain:skills(triage 5620057836: p2 Task — the direction is RULED there: React documentsuseMemo/useCallbackas a performance hint and reserves the right to discard the cache ⇒ objectui code may not depend on the identity of their results; a governed landing surface is a merge gate, not a decision gate)File surface (region-declared): objectui
AGENTS.md— ONE rule in the section that governs React / component authoring (the dev names the section after reading the file), stating the rule, the value-side vs effect-side seam (#6813 vs #6862: a value-side cache keyed on an object a refetch replaces is defeated by an effect-side discard), and — in the rule text itself — whether an instrument exists (a lint rule feasibility reading over the eight instances, ⛔ not a rule with an unmeasured carve-out); ⛔ no code, no lint rule landed here (feasibility only, a follow-up card if feasible), ⛔ none of the eight instance cards reopenedContainer & model:
M(short text, governed, the exception question needs real reading of the eight instances),mode:subagent,model: opus— default tier (objectui AGENTS.md is not a clause-① file; the skills seat reviews at the contract-review tier); governed ⇒ draft at the human terminal withFixes #8640Clause-②: no
Thread-read: 5620057836
Serial constraints cleared: objectui
AGENTS.mdfree — PR #9226 (#8410 + #8492) MERGED 06:24Zaebc3a31; no open objectui PR touches it at 2026-09-12T06:26Z; objectuiorigin/mainbedd7344(577 lines; no line ratchet script exists in objectui — confirmed byls scripts); H17 index ∩ this face = ∅.Generated by Claude Code
claude commentedon Sep 12, 2026
Dispatched — R1 wave 8 (objectui slot freed by PR #9226 landing), 2026-09-12T06:26Z,
mode:subagent, build tier opus, skills-seat review at the contract-review tier. Claim 5644186028 is the dev's identity; branchclaude/issue-8640-memo-identity-rule, cut from objectuiorigin/mainat dispatch (bedd7344). Deliverable = the rule triage 5620057836 ruled, written once in objectuiAGENTS.md: objectui code may not depend on the identity of auseMemo/useCallbackresult (React's own contract: a performance hint whose cache may be discarded); the value-side vs effect-side seam stated with the two instances that measured it (#6813 / #6862 — a discard that refetches walks through a value-side cache keyed on the replaced payload); the instrument question answered IN the rule text (a lint-rule feasibility reading over the eight instances: feasible ⇒ say so and the seat files the follow-up; not feasible ⇒ the rule declares its own limitation, per the objectui#8953 lesson); ⛔ no exception invented — if an instance forces one, propose it in the PR with that instance, not in the rule. ⛔ No code, no lint rule, no instance card reopened. Terminal: governed ⇒ draft at the human terminal withFixes #8640. In flight on the lane: 2 devs (#17564, this card) + 2 PRs in queues (#17098, objectui#8696).Generated by Claude Code
claude commentedon Sep 12, 2026
os-dev-report
{ "issue": 8640, "status": "done", "branch": "claude/issue-8640-memo-identity-rule", "pr": "https://github.com/objectstack-ai/objectui/pull/9257", "premise_still_valid": true, "summary": "Landed the ruled rule once, as objectui AGENTS.md section 5 commandment #10 (+1 bullet, 577 -> 578 lines) plus an empty-frontmatter changeset; no code, no lint rule, no instance card touched. Section 5 was chosen because its neighbours are the React authoring commandments (#6 type safety, #8 'UI state lives where it can survive', which already cites issue numbers in prose) and because the new rule sits immediately after #9, the rule that demands a claim point at its instrument and never write down that instrument's answer - #10's enforcement clause is #9 applied. Section 6 holds only code exemplars and section 9 is Chinese-voiced ops, so neither fits. The rule states the value/effect seam as its consequence: a value-side cache keyed on the payload object is walked through by an effect that still keys on a memoised identity, because a discard re-runs the fetch and setData(json) installs a fresh payload object even for a byte-identical answer, so a provider that refetches may not republish an equal payload as a new object and a consumer that needs stability keys on the data. The rule text also declares its own limitation: nothing enforces it, a lint rule is feasible for the same-file shape only. ACTION FOR THE SEAT: the same-file lint rule IS feasible (a home exists in eslint-rules/, and objectui#6592's census already ran that exact sweep by hand) but it covers 4 of the 8 instances only - the seat may file the follow-up card as a partial net, not as the thing that retires the class. No exception was forced by any of the eight; the boundary they draw (the rule forbids depending on identity, not using the hooks) is already in the rule's second sentence. PR is a DRAFT on a governed surface: no ready flip, no queue, no auto-merge, no approving review.", "tests": "Docs-only change; no package source touched, so no package test/typecheck is owed (check-changeset-presence printed: '1 file(s) changed, 0 of them published source of a package the release covers'). Gates run in the worktree after pnpm install, exit codes captured before any pipe: check-control-bytes EXIT=0 (7447 tracked text files); check-doc-links EXIT=0 (17 scan roots); check-changeset-presence EXIT=0; check-changeset-claims EXIT=0 (report-only); check-changeset-overwrite EXIT=0; check-changeset-no-major EXIT=0; check-changeset-fixed EXIT=0; check-new-cross-file-line-citations EXIT=0 ('VERDICT new-cross-file-line-citations: 0 new citation(s)'); check-doc-fence-languages EXIT=0; check-shell-escape-residue EXIT=0; check-bash32-floor EXIT=0; check-doc-expression-carriage EXIT=0 (report-only census; first run was a PREREQUISITE NOT MET before pnpm install, not a red); check-governed-queue-guard --test AGENTS.md EXIT=3 with the expected verdict line 'GOVERNED - 1 of 1 path(s) are on a governed surface: AGENTS.md x1'. Manual control-byte sweep beyond the gate: grep -naP over both changed files, exit 1, no hits. NOT MEASURED: check-doc-snippet-types - its prerequisite is a scoped 34-package turbo build (EXIT=2, PREREQUISITE NOT MET, not a red); declared narrowing, proven: this diff adds no fenced block (AGENTS.md carries 14 fence delimiters at bedd7344 and 14 after; the added line contains zero), so that gate's population is byte-identical and CI's run is the one that counts. Repo-wide pnpm lint is CI's per objectui AGENTS.md. Ablation/reverse verification: not applicable - this PR adds prose to a governed instruction file and lands no executable assertion to mutate; the falsifiable claims it makes were verified directly instead (see premises below). Premises re-derived, none inherited: React 19.2.8 (root package.json pin + pnpm-lock resolving react@19.2.8; the 19.2.18 in the lockfile is @types/react); zero imports of Activity from react and zero unstable_Activity across packages/ and apps/ (grep exit 1); AGENTS.md 577 lines at bedd7344 and no line-ratchet script in objectui; all 12 open objectui PR file lists enumerated (release PR #5400 paged to exhaustion) - none touches AGENTS.md; all eight instance cards confirmed closed, none reopened. Feasibility counts (in the PR body): a same-file AST rule would have caught objectui#6018, #6592, #6697, #6862 and missed objectui#5976 (value never memoised - inverse shape), #6591 (crosses a prop), #6724 (crosses a hook return), #6813 (crosses a context); it also collides with react-hooks/exhaustive-deps, live at warn via reactHooks.configs.recommended.rules, eslint-plugin-react-hooks 7.1.1.", "mcp_calls": "0 - every GitHub read and write went through repo-scoped REST from this container", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the React Compiler-era react-hooks rules (refs, immutability, set-state-in-effect, preserve-manual-memoization, use-memo) are all downgraded to warn in eslint.config.js with the reason 'codebase predates these rules'; preserve-manual-memoization is the nearest neighbour of rule #10 and would block nothing today. Successor: whoever writes the same-file lint-rule follow-up card.", "noted, not filed: the positive-control count in providerCtxIdentity.discarded.test.tsx's docblock ('179 files importing useMemo', the denominator for the zero-Activity reading) has moved - the like-for-like population is larger now. Not filed and not repaired: the sentence self-dates ('re-verified for this card'), so it reads as a point-in-time derivation rather than the live claim AGENTS.md #9 forbids. Successor: the ninth-instance author, who is the next reader of that harness." ] }Generated by Claude Code
claude commentedon Sep 12, 2026
ACCEPT — objectui PR #9257 (head
6afd0b4c) reviewed in-seat at the contract-review tier; governed (objectui AGENTS.md) ⇒ four-piece at the human terminal (skills seat, sessionsession_01MCLBsUgfykL74aU716rzVK, 2026-09-12T06:41Z).claude/issue-8640-memo-identity-rule(claim 5644186028; build tier opus;mode:subagent; report on this card). Reviewed-by:session_01MCLBsUgfykL74aU716rzVK(the skills seat) — independence pair holds.useMemo/useCallbackresult — React documents both as a performance hint and may discard the cache; memoise for cost freely, never let correctness rest on the returned object or function (not as an effect/memo dependency, not as a Map/WeakMap key, not in a reference comparison); re-key on the primitives or payload objects actually read, or move the cache out of React onto those inputs. The seam is stated as the rule's consequence (a value-side cache keyed on a payload object is walked through by an effect keyed on a memoised identity because a refetch installs a fresh payload for a byte-identical answer — census(finding): both permission providers build their context value in a useMemo, so a discard churns ctx identity for every usePermissions() consumer #6813 defeated by census(finding): MePermissionsProvider's fetch driver is a useCallback the effect depends on, so a discard costs a redundant /me/permissions round trip #6862 — so a refetching provider may not republish an equal payload as a new object). The rule declares its own limitation per commandment 完善设计器的每一个细节 #9: nothing enforces it; a lint rule is feasible for the same-file shape only; an ordinary test cannot see the defect without a forced discard (the harness inpackages/permissionsis named). ⛔ No exception invented; none of the eight instances forced one.Fixes #8640, no other closing keyword, one footer; CI on6afd0b4cat 06:39Z 28 success / 3 skipped / 3 doc checks in progress — the human merge waits for green. Premises re-derived by the dev, none inherited: React 19.2.8 from the lockfile, zeroActivityimports, no open objectui PR on AGENTS.md, all eight instance cards closed and untouched.check-doc-snippet-typesNOT MEASURED locally (a 34-package build prerequisite) with the narrowing proven (fence-delimiter count 14 → 14; the added line has none) — CI's run counts.ObjectMap'sdataConfigpays aJSON.stringifyon every render to buy an identity that[schema]already gives for free #6018 · Re-key the load-bearing fetch effects onto the primitives they read, souseMemois an optimisation again and not a correctness dependency #6592 · census(finding): three more renderer fetch effects key on a memoised object identity, outside objectui#6592's dataConfig family #6697 · census(finding): MePermissionsProvider's fetch driver is a useCallback the effect depends on, so a discard costs a redundant /me/permissions round trip #6862 and missedObjectMap's markeruseMemonever memoizes —getMapConfigruns unmemoized in the render body, somapConfighas a fresh identity every render #5976 · fix(react): memoise the scoped-style schema SchemaRenderer hands down #6591 · census(finding): usePermissions()'s memoised return is consumed by effect-dependency identity, outside objectui#6592's census heuristic #6724 · census(finding): both permission providers build their context value in a useMemo, so a discard churns ctx identity for every usePermissions() consumer #6813 (the shapes crossing a prop, a hook return, a context); it collides withreact-hooks/exhaustive-depsatwarn. Out-of-scope notes recorded, not filed: the React-Compiler-era hooks rules downgraded towarn(preserve-manual-memoizationis the nearest neighbour); a moved positive-control count in the discard harness's docblock (self-dated, not a live claim).needs-user-decisionon the PR, 速读 comment, reviewers os-zhuang + hotlong; ⛔ no ready flip / queue / auto-merge by this seat. On MERGED: landing record,pm:dispatched+ assignee cleared with read-back.Generated by Claude Code
claude commentedon Sep 13, 2026
Landed — PR #9257 MERGED 2026-09-13T04:12:05Z as
bde07805d(merged by os-zhuang through the merge queue after approval at the human terminal). Skills seat stay-behind record, sessionsession_01MCLBsUgfykL74aU716rzVK(GitHubos-sales), 2026-09-13T04:13Z.Verified on objectui
origin/mainby git: the merge commit is an ancestor oforigin/mainand single-parent;AGENTS.md:110 carries section 5 commandment #10 「Never depend on the IDENTITY of auseMemo/useCallbackresult …」 as the ACCEPT 5644254308 specified (578 lines, the +1 bullet; objectui has no line ratchet). Ruling 5620057836 landed once, in the one place.Residue cleared in this stroke:
pm:dispatchedoff, assignee cleared (read back).domain:skillsandpriority:p2stay (ownership, not state).Generated by Claude Code