docs(agents): rule out depending on a useMemo/useCallback identity - #9257
Conversation
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
维护者速读(终稿)改了什么 — objectui 为什么改 — 八张独立立卡的同一缺陷形态(objectui#5976 · #6018 · #6591 · #6592 · #6697 · #6724 · #6813 · #6862),第八张还击穿了第七张的修法;分诊席裁定这是 React 自己的契约(说明书脱节/恢复不变量),不进决策箱,写成房规。八张实例卡都已关闭、不重开。 风险与代价(含回滚) — 受管面一段文字;objectui 的文档/changeset/受管守卫等门禁全绿, 席位意见 — 席内按契约复审档复核通过,建议合并。 你要做的 — 受管面:请人工合并 objectui PR #9257(本席不翻 ready、不入队、不挂 auto-merge)。 Generated by Claude Code |
Fixes #8640
Writes the rule triage ruled (comment 5620057836) once, as
AGENTS.mdsection 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
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, inpackages/permissions/src/:MePermissionsProvider.tsxkeys five module-levelWeakMapcaches (createDiscardProofCache,discardProofCache.ts) ondataKey = data ?? NO_DATA— the payload object itself. React cannot discard those, so a discard no longer churns the published context value.useCallbackdriver's identity. A forced discard re-ran it, andsetData(json)installs a fresh payload object even when the answer is byte-identical ⇒dataKeymoves ⇒ 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 current tree shows both halves landed: the effect now keys on the four fetch values plus the
initialPermissionsguard (endpoint, fetcher, maxRetries, retryBaseDelayMs, initialPermissions), never on a driver's identity, andloadPermissionsis 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 auseEffect/useMemodependency 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
useEffectwhose dependency array names auseMemo-bound identifier", cross-checked against a fetch-call-site heuristic and positive-controlled against the knownplugin-mapmember); 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 intoeslint.config.jsasobject-ui.Coverage over the eight, 4 caught / 4 missed:
dataConfigmemo and the fetch effect are the same componentdataConfig/mapConfigfamily — found by this sweepuseCallbackdriver and the effect, one provider filegetMapConfig(schema)in the render body named in a dep array. Nothing memo-bound to matchpackages/permissions, the dep array is inplugin-list. That card names the miss in its own words: the heuristic "readspermsas a plain value"⇒ 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:
react-hooks/exhaustive-deps, which is live here atwarn(spread fromreactHooks.configs.recommended.rules,eslint-plugin-react-hooks7.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.Premises re-derived (⛔ none inherited)
package.jsonpins"react": "19.2.8", andpnpm-lock.yamlresolvesreact@19.2.8. The card's reading still holds. (Unrelated near-miss worth naming:19.2.18also appears in the lockfile — that is@types/react.)Activity/ Offscreen subtree —grepfor anActivityimport fromreact(and forunstable_Activity) overpackages/+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.mdatbedd7344: 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).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.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'sretrystays auseCallback— "This one may stay auseCallback: 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 ofuseMemo/useCallbackfor 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:⇒ 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.check-control-bytescheck-doc-linkscheck-changeset-presencecheck-changeset-claimscheck-changeset-overwritecheck-changeset-no-majorcheck-changeset-fixedcheck-new-cross-file-line-citationscheck-doc-fence-languagescheck-shell-escape-residueAGENTS.mdfences examinedcheck-bash32-floorcheck-doc-expression-carriagecheck-governed-queue-guard --test AGENTS.mdcheck-doc-snippet-typesAGENTS.mdholds 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.pnpm lintAGENTS.mdnames it as CI's)Manual control-byte sweep beyond the gate:
grep -naPover 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)
warnhere —react-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, sincepreserve-manual-memoizationsits closest to [WIP] Enhance every detail of the designer #10's subject and would not block anything today. Successor: whoever writes that card.providerCtxIdentity.discarded.test.tsx's docblock ("179 files importinguseMemo", the positive control for the zero-Activityreading) 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