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
Activity
⭐ 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:
observed correct /me/permissionsrequests3 1 distinct context identities published 3 1 consumer effect runs downstream 3 1 ⭐ 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:
- These instances are not independent. They compose — a remaining one can silently undo a landed one, which means the class is not being retired by fixing members one at a time.
- ⭐ It sharpens axis 2 (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 is a value-side fix 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 an effect-side one, and the measurement shows the effect-side defect reaching through the value-side guard. Any rule that treats the two halves separately has to say what happens at that seam.
- It sharpens axis 3 (is a forced-discard pin required on every new provider). census(finding): both permission providers build their context value in a useMemo, so a discard churns ctx identity for every usePermissions() consumer #6813 had one and was still defeated — because its pin covers its own cache, not an upstream refetch that changes the cache key. A rule that mandates the pin without saying what the pin must cover would not have caught this.
⛔ What this does NOT change
- ⛔ Not a re-grade. census(finding): MePermissionsProvider's fetch driver is a useCallback the effect depends on, so a discard costs a redundant /me/permissions round trip #6862 remains
p3: still latent, still not wrong data. The composition is real but React does not discard on its own in this tree today. - ⛔ Not a defect in census(finding): both permission providers build their context value in a useMemo, so a discard churns ctx identity for every usePermissions() consumer #6813. Its guard does what it claims; it is simply upstream of a key it cannot control.
- ⛔ Still no priority claimed for this card, and still no proposed answer.
Provenance
Measured 2026-09-08 on PR #8643's branch, on the defective code, using objectui#6813's own forced-discard proxy (
providerCtxIdentity.discarded.test.tsx, which patches bothuseMemoanduseCallback).⚠️ Not independently re-measured by me — a ruling should re-derive it, as with everything else on this card.
Generated by Claude Code
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
- ⛔ Do not invent exceptions. If the eight instances show a case where identity-dependence is genuinely unavoidable, propose it in the PR with the instance that forces it — do not write a rule with a carve-out you have not measured.
⚠️ A rule with no instrument is objectui#8953 all over again (a rule landed inAGENTS.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.- ⛔ This card writes the rule, not the ninth fix. The eight instances keep their own cards.
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
Claim: PM loop round 1
Session:session_01MCLBsUgfykL74aU716rzVK(GitHubos-sales, skills seat), claimed at 2026-09-12T06:26Z
Branch:claude/issue-8640-memo-identity-rule
Worktree:objectui-issue-8640
Domain: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): objectuiAGENTS.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 reopened
Container & 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 #8640
Clause-②: no
Thread-read: 5620057836
Serial constraints cleared: objectuiAGENTS.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
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
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
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).- Implemented-by: os-dev subagent on
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. - The rule triage ruled (5620057836) lands once, as section 5 commandment [WIP] Enhance every detail of the designer #10 (+1 bullet, 577 → 578 lines; no line ratchet exists in objectui): never depend on the IDENTITY of a
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. - Seat's own readings: the diff is that one bullet + an empty-frontmatter changeset (objectui's correct form); the line follows the section's numbered-English voice and cites the instances by number as neighbouring commandments do; body first line
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. - Evidence: control-bytes, doc-links, changeset presence/claims/overwrite/no-major/fixed, cross-file line citations, fence languages, shell-escape residue, bash32 floor — all exit 0; governed guard exit 3 GOVERNED (AGENTS.md ×1);
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. - Feasibility reading carried, filed by this seat as a follow-up (a partial net, not the class's retirement): a same-file AST rule would have caught
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). - Landing plan: draft kept,
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
- Implemented-by: os-dev subagent on
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
Raised by the
domain:uiexecution-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/useCallbackresult keeping its identity, when React reserves the right to discard the cache.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:
Activity/ Offscreen subtree — the documented scenario where a memo cache genuinely is discarded.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:
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./me/permissionsround trip.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.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.⚠️ Neither has been re-measured by me — a ruling should re-derive both, since the React version in particular can move under a lockfile refresh.