Skip to content

docs(agents): rule out depending on a useMemo/useCallback identity - #9257

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-8640-memo-identity-rule
Sep 13, 2026
Merged

docs(agents): rule out depending on a useMemo/useCallback identity#9257
os-zhuang merged 1 commit into
mainfrom
claude/issue-8640-memo-identity-rule

Conversation

@claude

@claude claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #8640

Writes the rule triage ruled (comment 5620057836) once, as AGENTS.md section 5, commandment #10. ⛔ No code change, ⛔ no lint rule landed, ⛔ none of the eight instance cards touched — all eight are closed and stay closed.

Why section 5, and not 6 or 9

section what lives there verdict
5. Coding Standards (the Commandments) numbered authoring rules, English, one rule per bullet — including the two React-authoring ones: #6 type safety over magic and #8 "UI state lives where it can survive", which already cites objectui#2269 / #2257 / #4001 in prose chosen
6. Implementation Patterns two code exemplars (the registry, the renderer loop). No rules at all — a rule here would be the only one
9. Operational Rules process and ops, written in Chinese: housekeeping, how to run tests, multi-agent discipline, the governed surface, local dev ports

Two further reasons for section 5 specifically:

The seam the rule has to state (measurement 5590671486, re-derived here)

Read off the merged tree at bedd7344, in packages/permissions/src/:

  • Value side — objectui#6813. MePermissionsProvider.tsx keys five module-level WeakMap caches (createDiscardProofCache, discardProofCache.ts) on dataKey = data ?? NO_DATA — the payload object itself. React cannot discard those, so a discard no longer churns the published context value.
  • Effect side — objectui#6862. The fetch effect used to depend on a useCallback driver's identity. A forced discard re-ran it, and setData(json) installs a fresh payload object even when the answer is byte-identicaldataKey moves ⇒ every one of those five discard-proof caches misses and a new context value reaches every consumer. Instance eight walked straight through instance seven's guard.
  • ⇒ the rule's consequence, stated in the rule text: a provider that refetches may not republish an equal payload as a new object, and a consumer that needs stability keys on the data, not on a cache. Neither half is sufficient alone.

The current tree shows both halves landed: the effect now keys on the four fetch values plus the initialPermissions guard (endpoint, fetcher, maxRetries, retryBaseDelayMs, initialPermissions), never on a driver's identity, and loadPermissions is a module-level function. The seam is stated as a rule, not as a ninth repair.

Instrument feasibility — a READING with counts, ⛔ no rule written here

Shape tested: a single-file AST rule — a useMemo / useCallback-bound identifier named in a useEffect / useMemo dependency array of the same component.

Feasible, and this repo has already run it by hand once. objectui#6592's census was exactly that sweep (its own words: "every useEffect whose dependency array names a useMemo-bound identifier", cross-checked against a fetch-call-site heuristic and positive-controlled against the known plugin-map member); it is what produced objectui#6697's three sites. A home exists too: eslint-rules/ is a local ESLint plugin (ten rules, each with a unit test beside it), wired into eslint.config.js as object-ui.

Coverage over the eight, 4 caught / 4 missed:

instance would a same-file rule see it? why
objectui#6018 dataConfig memo and the fetch effect are the same component
objectui#6592 the dataConfig/mapConfig family — found by this sweep
objectui#6697 three renderer fetch effects — also found by this sweep
objectui#6862 useCallback driver and the effect, one provider file
objectui#5976 inverse shape: the value was never memoised — a bare getMapConfig(schema) in the render body named in a dep array. Nothing memo-bound to match
objectui#6591 the fresh identity crosses a prop into the child
objectui#6724 crosses a hook return — the memo is in packages/permissions, the dep array is in plugin-list. That card names the miss in its own words: the heuristic "reads perms as a plain value"
objectui#6813 crosses a React context into every consumer package

⇒ the reading: feasible for the same-file half only, and the half it cannot reach is where the two most expensive instances lived. So the rule keeps its own limitation clause either way; the seat may file a follow-up for the same-file rule as a partial net, ⛔ not as the thing that closes the class.

Two constraints that follow-up card will need, reported so it is not re-derived:

  • It collides with react-hooks/exhaustive-deps, which is live here at warn (spread from reactHooks.configs.recommended.rules, eslint-plugin-react-hooks 7.1.1). That rule requires listing the memoised identifier; a naive "don't list a memo in deps" rule would set the two against each other and invite a suppression comment. The repair the family actually used is to change what the consumer reads — re-key on primitives (objectui#6592) or cache outside React (objectui#6813) — so the lint rule must point at re-keying, not at deletion.
  • A pin proves nothing unless it forces a discard — re-derived below — so the follow-up cannot be validated by ordinary render tests.

Premises re-derived (⛔ none inherited)

  • React version: 19.2.8 — root package.json pins "react": "19.2.8", and pnpm-lock.yaml resolves react@19.2.8. The card's reading still holds. (Unrelated near-miss worth naming: 19.2.18 also appears in the lockfile — that is @types/react.)
  • No Activity / Offscreen subtreegrep for an Activity import from react (and for unstable_Activity) over packages/ + apps/: zero hits, exit 1. So React still has no documented occasion to discard here, which is why the class is latent and invisible to ordinary tests.
  • AGENTS.md at bedd7344: 577 lines, 578 after this change (+1 bullet). objectui has no line-ratchet script (ls scripts — none of the 80-odd gates ratchets this file's length).
  • No open PR touches AGENTS.md — all 12 open objectui PRs' file lists enumerated at 07:0xZ (the release PR chore: release packages #5400 paged to exhaustion, 7 pages): zero hits.
  • All eight instance cards are closed, verified individually; ⛔ none reopened, ⛔ none commented on by this PR.

No exception is forced — but the instances do draw a boundary

⛔ No carve-out is written into the rule. The eight were all repairable at the consumer (re-key on primitives) or by moving the cache out of React onto the same inputs, so none of them forces an exception.

What they do draw is the rule's scope, and the tree states it in a comment already worth pointing at: MePermissionsProvider's retry stays a useCallback"This one may stay a useCallback: nothing keys on its identity." That is not an exception; it is the rule read correctly. The rule forbids depending on the identity, never the use of useMemo / useCallback for cost. The rule text says so in its own second sentence ("Memoise for cost freely"), so no reader needs an exception list to get that case right.

Governed surface

node scripts/check-governed-queue-guard.mjs --test AGENTS.md → exit 3, verbatim verdict line:

⛔ GOVERNED — 1 of 1 path(s) are on a governed surface: AGENTS.md x1 — the repo-root agent instruction file

⇒ this PR stays a draft for the maintainer to hand-merge. ⛔ Not flipped ready, ⛔ not enqueued, ⛔ no auto-merge, ⛔ no approving review from any seat.

Gates

Run in the worktree after pnpm install; exit codes captured before any pipe.

gate exit note
check-control-bytes 0 7447 tracked text files scanned
check-doc-links 0 17 scan roots
check-changeset-presence 0 0 published source files changed; 1 changeset added
check-changeset-claims 0 report-only
check-changeset-overwrite 0 no pre-existing changeset modified
check-changeset-no-major 0
check-changeset-fixed 0
check-new-cross-file-line-citations 0 0 new citations (the rule cites files by path, never by line)
check-doc-fence-languages 0
check-shell-escape-residue 0 AGENTS.md fences examined
check-bash32-floor 0
check-doc-expression-carriage 0 report-only census
check-governed-queue-guard --test AGENTS.md 3 the expected GOVERNED verdict, above
check-doc-snippet-types NOT MEASURED its prerequisite is a scoped 34-package turbo build. Declared narrowing: this diff adds no fenced blockAGENTS.md holds 14 fence delimiters before and after, and the added line contains zero. The gate's population is byte-identical, so CI's run is the one that counts.
repo-wide pnpm lint not owed CI's run (objectui AGENTS.md names it as CI's)

Manual control-byte sweep beyond the gate: grep -naP over both changed files for C0/C1 control bytes — exit 1, no hits.

维护者速读(草稿)

改了什么 — objectui AGENTS.md 第 5 节(编码戒律)新增一条 #10:objectui 代码不得依赖 useMemo / useCallback 返回值的身份(identity)。同时补一个空 frontmatter 的 changeset(纯文档,不发版)。共 +1 行规则 + 1 个 changeset 文件,⛔ 不动任何代码。

为什么改 — 同一个缺陷形态已经被分八次单独发现、单独分诊、单独修(objectui#5976 · #6018 · #6591 · #6592 · #6697 · #6724 · #6813 · #6862)。React 官方把这两个 hook 写成性能提示,并明确保留丢弃缓存的权利 ⇒ 依赖它的身份等于依赖库从未承诺过的东西,这是「说明书脱节」,不是产品决策。更要紧的是这八个互相不独立:第八个(#6862)的 effect 侧缺陷,会直接穿透第七个(#6813)已经落地的 value 侧防护 —— 一个一个修不会让这一类退休,所以写规则,不写第九个修复。

风险与代价(含回滚) — 风险极低:纯文档、无运行时影响、无发版。代价是这条规则没有任何东西强制执行,这一点已经写进规则正文本身(不让读者误以为有门禁),原因是:lint 规则只能覆盖「同文件」那一半(八个里 4 个),跨 prop / 跨 hook 返回 / 跨 context 的另一半(4 个,恰好是最贵的两个所在)任何单文件规则都看不见。回滚 = 还原这一行,无任何依赖。

席位意见 — (留空,待 skills 席位定稿)

你要做的 — 读一遍新增的 #10(AGENTS.md 第 5 节的最后一条),确认这条规则的措辞是你要的房规;认可就手动合并(受管面:PR 停在 draft,人类那次合并动作本身就是审核记录)。如果你希望把「同文件」那一半做成 lint 规则,请说一声,由 skills 席位另立卡 —— 本 PR ⛔ 不落地任何 lint 规则。

Acceptance notes (observations, ⛔ nothing filed)

  • The React Compiler-era hook rules are all downgraded to warn herereact-hooks/refs, immutability, set-state-in-effect, preserve-manual-memoization, use-memo, with the config's own reason ("codebase predates these rules"). Not a defect and ⛔ not filed; it is context the follow-up lint-rule card will want, since preserve-manual-memoization sits closest to [WIP] Enhance every detail of the designer #10's subject and would not block anything today. Successor: whoever writes that card.
  • The count inside providerCtxIdentity.discarded.test.tsx's docblock ("179 files importing useMemo", the positive control for the zero-Activity reading) has moved — the like-for-like population is larger now. ⛔ Not filed and ⛔ not repaired here: the sentence self-dates itself ("re-verified for this card"), so it reads as a point-in-time derivation rather than the live claim commandment 完善设计器的每一个细节 #9 forbids. Recorded only because the next reader of that harness is exactly the ninth-instance author.

Seat session, as prose so an edit cannot strip it: session_01MCLBsUgfykL74aU716rzVK.


Generated by Claude Code

Eight separately-filed instances were one defect shape: code resting on the
identity of a memoised value or callback, which React documents as a
performance hint whose cache it may discard. Write the rule once in section 5
next to the other React authoring commandments, state the value-side /
effect-side seam the family measured, and declare in the rule text that
nothing enforces it — a lint rule reaches the same-file shape only.

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

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)

改了什么 — objectui AGENTS.md 第 5 节「戒律」加第 #10 条:永不依赖 useMemo / useCallback 返回值的身份。React 把这两个 hook 定义为性能提示、保留丢弃缓存的权利,所以拿返回的对象/函数当 effect 依赖、当 Map/WeakMap 键、做引用比较,都是在依赖库从未承诺的东西;要稳定就按真正读到的原始值或 payload 对象重新做键,或把缓存挪到 React 之外。条文同时写明「值侧/效果侧」这道缝(#6813 的值侧缓存被 #6862 的效果侧丢弃击穿:重取会为逐字节相同的答案装一个新对象),以及按 #9 戒律自报的局限:目前没有任何门禁执行这条,同文件形态可做 lint,跨 prop / hook 返回 / context 的形态单文件规则够不到;普通测试也看不到这个缺陷,必须强制丢弃才能钉住。

为什么改 — 八张独立立卡的同一缺陷形态(objectui#5976 · #6018 · #6591 · #6592 · #6697 · #6724 · #6813 · #6862),第八张还击穿了第七张的修法;分诊席裁定这是 React 自己的契约(说明书脱节/恢复不变量),不进决策箱,写成房规。八张实例卡都已关闭、不重开。

风险与代价(含回滚) — 受管面一段文字;objectui 的文档/changeset/受管守卫等门禁全绿,check-doc-snippet-types 本地未测(需 34 包构建)但本 diff 不含代码块、由 CI 跑。回滚 = revert 本 PR。后续:本席另立一张「同文件 lint 规则」卡(实测只能网住 8 例中的 4 例,是局部网不是收尾)。

席位意见 — 席内按契约复审档复核通过,建议合并。

你要做的 — 受管面:请人工合并 objectui PR #9257(本席不翻 ready、不入队、不挂 auto-merge)。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 13, 2026 03:54
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit bde0780 Sep 13, 2026
36 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8640-memo-identity-rule branch September 13, 2026 04:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs-user-decision

Projects

None yet

2 participants