diff --git a/.changeset/issue-8875-cite-by-content-convention.md b/.changeset/issue-8875-cite-by-content-convention.md new file mode 100644 index 0000000000..2a1265edba --- /dev/null +++ b/.changeset/issue-8875-cite-by-content-convention.md @@ -0,0 +1,14 @@ +--- +--- + +`AGENTS.md` only — no released package source is touched, so this declares **no release** with +an empty frontmatter rather than a bump. A `patch` here would version-bump the whole 39-package +fixed group for a change no consumer can observe. + +objectui#8875 clause 1: adds Coding Standard **#11 — cite by CONTENT, not by line address**, the +human-facing convention behind the differential gate that landed in objectui#8974. objectui#7853 +ruled the class; objectui#8047 mechanized it for test names only, exempting comments and failure +messages because "a human reads them beside the code they annotate". #11 records the point that +justification is positional: it holds for a same-file citation and does not survive the citation +crossing a file boundary, where nothing puts the cited line in front of the reader and nothing +tells them it moved. diff --git a/AGENTS.md b/AGENTS.md index 30f47ec4b5..8f8ceba5c3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,6 +108,13 @@ interface BaseSchema { - **#8 — UI state lives where it can survive (objectui#2269, ADR-0054 C3).** Classify every piece of UI state before writing it: **addressable** (user would share it / expect it back after refresh / Back should respect it) → the **URL** (`?recordId=`, `?form=`, `?tab=` — constants in `app-shell/src/urlParams.ts`, never string literals); **preference** (stable across records/sessions) → localStorage or server prefs; **truly ephemeral** (hover, open dropdown — nobody cares if it's lost) → component state. The rule: **state that must survive a data refresh may never live only in an uncontrolled component** (that's how the detail-tab reset happened — objectui#2257). Corollary: **refresh data, don't rebuild UI** — after a save/action, invalidate the affected data (`notifyDataChanged` from `@object-ui/react`) so consumers refetch in place; never bump a `key=` to remount a subtree (it destroys scroll, collapsed sections, tab state, in-progress inline edits, and triggers a refetch storm). - **#9 — A "verified" claim is only as good as the check that re-verifies it (objectui#7833).** Prose that declares something checked, counted, aligned or covered — in a doc, a code comment, a script or workflow header, or this file — states a fact that was derived once, when it was written, and is derived never again; no later reading can tell such an assertion from a live one. So when you declare something verified, **point at the instrument that re-derives it — the script, the gate, the test — and never write down its answer**: "the count this gate prints", not the count; "the list this script enumerates", not the list. A figure that is still correct is the dangerous case, not the safe one — a reader who spot-checks it confirms it and is still wrong once the population it described has moved out from under it. Where nothing re-derives the claim, say that in the text rather than let it read as live. This file is not exempt: it has carried such a claim and needed a repair (objectui#7800). The measured instances behind this rule sit on objectui#7833 — pointed at, not copied here, which is this rule applied to itself. - **#10 — Never depend on the IDENTITY of a `useMemo` / `useCallback` result (objectui#8640, ruled over eight instances: objectui#5976 · #6018 · #6591 · #6592 · #6697 · #6724 · #6813 · #6862).** React documents both hooks as a **performance hint** and reserves the right to throw the cache away and recompute even when the dependency list compares equal — the identity they hand back is something the library never promised. Memoise for cost freely; just never let correctness rest on the object or function that comes back — ⛔ not as a `useEffect` / `useMemo` dependency, ⛔ not as a `Map` / `WeakMap` key, ⛔ not in a reference comparison. Re-key the consumer on the **primitives or the payload objects it actually reads** (objectui#6592), or move the cache out of React onto those same inputs (`packages/permissions/src/discardProofCache.ts`, objectui#6813). **Both sides of the seam, or neither holds:** a value-side cache keyed on a payload object is walked straight through by an effect that still keys on a memoised identity — a discard re-runs the fetch and `setData(json)` installs a **fresh payload object even for a byte-identical answer**, so every such cache misses and a new value reaches every consumer (objectui#6813 defeated by #6862, measured on objectui#8640). ⇒ 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. ⚠️ **Nothing enforces this rule** — said here per #9 rather than left to be assumed: a lint rule is feasible for the same-file shape only (`eslint-rules/` would host it), while the instances that crossed a module boundary — a prop, a hook return, a context — are out of reach of any single-file rule; the feasibility reading and its counts are on objectui#8640's PR. ⚠️ Nor does an ordinary test see the defect: React does not discard on its own in this tree, so a pin that does not **force** a discard passes identically on defect and fix — `providerCtxIdentity.discarded.test.tsx` in `packages/permissions` forces one. +- **#11 — Cite by CONTENT, not by line address; a CROSS-FILE `path:line` is banned outright (objectui#7853, objectui#8875).** When a comment, an assertion or failure message, a doc, a changeset or a script header cites an assertion, a call, or a line of code, cite it by something that travels with the thing itself — a quoted distinctive string, a symbol name, a test name, a heading anchor — and ⛔ never by a `path:line` address. A line address is read by nothing: no gate parses it and the file it points at is never opened, so it cannot fail — it rots the first time a line is inserted above the thing it cites, and stays wrong until a reader believes it. objectui#7853 ruled the class and landed as `fa7d66c45`; objectui#6548, #6998, #7289, #7913 and #8045 are the one-at-a-time repairs that followed it, and the class kept recurring — which is why it is now mechanized rather than remembered. + - **The same-file carve-out stands; crossing a file boundary is what makes the citation indefensible.** objectui#8047 mechanized the ruling as the `no-line-address-in-test-name` ESLint rule, over test names only, deliberately exempting comments and failure messages on the ground stated in that rule's own header: *"A human reads them beside the code they annotate, and the next reader of that code corrects a wrong one."* ⭐ That justification is **positional** — it holds for a SAME-FILE citation and it does not survive the citation crossing a file boundary. The reader of `packages/types/src/crud.ts` is not looking at `packages/core/src/actions/ActionRunner.ts`: nothing puts the cited line in front of them, nothing tells them it moved, and the person who moves it never sees the citation. Measured once, inside a single pull request's blast radius (objectui#8875 — a historical reading, ⛔ not re-derived here and ⛔ not a live count): 73 cross-file hits across four syntaxes, 14 of them moved by that one branch's line shifts, and **10 of those 14 were already false before the branch existed**. ⇒ same-file line addresses keep the objectui#8047 exemption; cross-file ones are the banned form, in every carrier the ruling names. + - Maintainer's ruling, 2026-09-10 — **原文照录、不翻译**: + > 跨文件的「某文件第几行」引用, 这种完全没必要吧,是否应该避免 + - **The gate is DIFFERENTIAL — it reads what your branch ADDS, never a tree-wide total.** `pnpm check:new-line-citations` (`scripts/check-new-cross-file-line-citations.mjs`, workflow `Line Citation Gate`, landed by objectui#8974) compares your branch against its base and reports the cross-file line-address citations the branch introduced; read that script's `ENFORCEMENT` constant for whether findings block today, and its file header for the syntaxes it scans. ⛔ An absolute count was explicitly refused, and not on taste: a tree-wide number also moves when unrelated line shifts flip a stale citation into a true one by accident, scoring as progress an unearned green that belongs to nobody. The existing population is therefore ⛔ not the gate's denominator; `pnpm census:cross-file-line-citations` prints the tree-wide picture and is report-only by design. ⛔ Never copy either instrument's answer into prose — that is #9 applied to this rule. + - **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. ⛔ 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. A citation into a regenerated artifact (`dist/*.d.ts`) is *unresolvable*, not a finding. + - **⛔ Don't store a line number as a key, either.** The dividing line is **stored vs computed**: a `path:line` string a script *computes at runtime to print a diagnostic* cannot expire, while one that is *stored and compared* — a ledger key, a baseline entry — is this same defect with a gate wired to it, so it goes red instead of going quiet. Key those by symbol name or anchor text. ---