Skip to content

docs(react-pages,skills,showcase): teach the data-invalidation nonce in a react page's live-data effect, opt the two showcase react pages in (#20395) - #21045

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-20395-react-page-invalidation-nonce
Oct 1, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-20395-react-page-invalidation-nonce

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20395
Clause-②: no — docs, skill prose, two strings in a generator and two example pages; no protocol key moves.

The objectstack half of ruling A on objectui#10887 member 3 (ruling 5864668610, maintainer 「同意」 2026-09-28). The objectui half landed in PR objectui#10973 (merge deca847a8): ReactKindPage injects useDataInvalidation into the kind: 'react' author scope. This repo's pinned console (.objectui-sha = db11afd4967cd9d39381c5e21dc2deec9d706204, 78 commits ahead of that merge, behind 0 — the seat's REST compare at unlock) carries it, so the six sites that still taught or ran the pre-hook pattern are brought to it.

The hook, read at the pinned sha

Read with git show db11afd49:PATH in the objectui checkout after git fetch --depth=1 origin db11afd49…, not at objectui HEAD:

  • packages/components/src/renderers/layout/react-page.tsx — the scope memo spreads buildComponentScope(adapter) and adds useAdapter, useDataInvalidation, data, variables, page; the memo still depends on [schema, adapter] only, so the scope identity, and with it the page's own state, is unchanged by the new member.
  • packages/react/src/data-invalidation.ts:106 — export function useDataInvalidation(objectName?: string, recordId?: string): number. The nonce is a NUMBER: useState(0), incremented once per bus change that dataChangeMatches (same objectName, or a change reported on '*'; a record-scoped change also stales a list reader that passed no recordId). With no objectName the hook subscribes to nothing and returns 0 forever. Its docblock prescribes the use this PR teaches: put the nonce in the fetch effect's dependency array; the refetch happens in place, no remount.
  • The bus is fed by useMutationInvalidationBridge(dataSource), mounted once in packages/app-shell/src/console/AppContent.tsx:449, so every create/update/delete through the console's data source reaches the readers. ListView (renderers/basic/data-list.tsx:137), ObjectView, elements.tsx and record-picker.tsx read it at that sha.

Every sentence the six files now carry about the hook is true of that sha.

What changed (7 files, +33 / -15)

  1. content/docs/ui/react-pages.mdx — the scope table gains the useDataInvalidation row; the Live data example reads const changed = useDataInvalidation('showcase_invoice') and names changed in the effect's dependencies; one paragraph after the block says why: a page action elsewhere writes the object, the bus reports it, the nonce moves, the effect re-runs in place — no remount, the page's state survives. The in-page link #live-data resolves (check:doc-anchors green; github-slugger).
  2. skills/objectstack-ui/rules/pages.md (Tier H) — one bullet in the react scope list, in the file's voice. Paid for by deleting the blockquote's two sentences that restated the bullet above them (the contract covers the curated core set; runtime-injected blocks read props from the registry; record:* rejected) — the bullet still says all three. No other line re-wrapped.
  3. packages/spec/scripts/build-react-blocks-contract.ts — the ## Injected scope line gains useDataInvalidation; the Live-data note gains the nonce sentence. skills/objectstack-ui/references/react-blocks.md regenerated through pnpm --filter @objectstack/spec gen:react-blocks, never hand-edited: exactly those two lines moved (the note paragraph and the scope line), 115 → 115 lines; check:react-blocks green. references/_index.md untouched (not this generator's output).
  4. examples/app-showcase/src/ui/pages/crm-workbench.page.ts — const projectChanged = useDataInvalidation('showcase_project'); the KPI effect's dependencies [refreshStats, reloadKey] → [refreshStats, reloadKey, projectChanged], plus a two-line comment.
  5. examples/app-showcase/src/ui/pages/renewals-pipeline.page.ts — two nonces, showcase_project and showcase_invoice (the two objects the rollup reads; the hook takes one object, and a reader on '*' matches only wildcard changes); dependencies [adapter, sel, reload] → [adapter, sel, reload, projectChanged, invoiceChanged]; the comment's "reload bumps re-run it" now reads "reload bumps and bus writes re-run it".
  6. examples/app-showcase/test/react-page-adapter-query-contract.test.ts — NOT in the claim's file surface; a bounded in-place fix, see Deviations.

What a page action now does on the two showcase pages

The example app is rendered by the pinned console, so the new dependency is a runtime behaviour change there:

  • CRM Workbench: any write to showcase_project the bus reports — a save in the page's own ObjectForm, a row action on its ListView, a record action from another block or page — moves projectChanged, and the KPI strip (Total / Active) re-reads in place. Before, it re-read only when the page's own afterSave bumped reloadKey, so a write from anywhere else left the strip stale until PageView's remount (which PR objectui#10777 removes).
  • Renewals Pipeline: any reported write to showcase_project or showcase_invoice (an invoice marked paid from the Invoices ListView beside it, say) re-runs the Account 360 rollup — Projects / Invoices / Open AR — in place, with sel and the drawer state kept. Before, only the drawer's own save (reload) did.
  • Kept on purpose, outside this card: reloadKey / reload stay in the dependency lists because they also key the ListView and ObjectForm remounts those pages do (key={reloadKey}). After this change a save in the page's own form runs the effect twice — the key bump and the bus event — one redundant read, same answer. Retiring the key-remount pattern is a separate decision (Acceptance notes).

skills/** readings

Lines and tokens; tokens are the ratchet's ceil(utf8 bytes / 4), read from check-skills-token-ratchet at 2f2fa11d7 (before) and c1770a387 (after).

surface lines before → after tokens before → after ceiling
skills/objectstack-ui/rules/pages.md (touched, authored) 453 → 453 5681 → 5676 (headroom 11 → 16) 5692, not moved
skills/objectstack-ui/references/react-blocks.md (touched, generator-owned: measured, not ratcheted) 115 → 115 3340 → 3409 —
whole package skills/objectstack-ui/** (11 files) 2231 → 2231 30770 → 30834 —
whole catalog skills/**, ratcheted (authored) total — 143505 → 143500 of 154938 —
whole catalog skills/**, bundle total (shipped tree) — 154058 → 154122 —
all skills/*/SKILL.md summed 4395 → 4395 — —

Net: the authored skill prose shrinks by 5 tokens; the +69 in the bundle is the generator's output. No ceiling moved, no re-wrap bought a line.

Changeset

None; skip-changeset is the seat's to apply. skills/** and content/docs/** appear in no package's files[] (every package.json under packages/ and examples/ walked: zero name skills, content/docs or a parent path); @objectstack/example-showcase is private: true; packages/spec/scripts/ is outside @objectstack/spec's files[] (dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json). No private: false package is touched.

Tests and gates (head c1770a387)

Heavy runs went through scripts/pm/os-verify-lock.sh (slot issue-20395; one 540 s queue-timeout behind an objectui full build, read as NOT MEASURED and resumed on the same slot).

  • pnpm --filter @objectstack/spec build — exit 0 (held 72 s).
  • pnpm turbo run build --filter="@objectstack/example-showcase^..." --concurrency=2 — 60/60 tasks, exit 0 (held 318 s); then pnpm turbo run build --filter="!@objectstack/docs" --concurrency=2 — 72/72, 60 cached, exit 0 (held 42 s), so the whole-workspace gates could measure.
  • Showcase, under the lock: typecheck (tsc --noEmit) exit 0; validate (objectstack validate) exit 0 with advisory warnings only, all pre-existing and none on the two pages; vitest run --maxWorkers=2 at 2ecc5d7ff, before the harness fix: 28 files passed, 1 failed — react-page-adapter-query-contract.test.ts × 4, ReferenceError: projectChanged is not defined; at c1770a387, clean tree: Test Files 29 passed (29), Tests 385 passed (385) (held 46 s).
  • Dispatch gates: the 89 commands of dispatch-gates --commands at 2f2fa11d7 re-derived on this branch with --repo objectstack-ai/objectstack — identical list. --ran at c1770a387 derives 94 families (the test file adds check:engine-double-contract, check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:where-matcher, all run green): 94 derived famil(ies) accounted for — 94 run, 0 NOT-MEASURED (a DERIVED zero). Also run, outside the derivation: pnpm --filter @objectstack/spec run check:generated exit 0 and check:skill-docs exit 0. Eight gates first answered exit 3 (prerequisite: no dist/) and were re-run green after the builds — docs-transcript-drift, dts-closure, dual-build-cjs-loads, lean-entry-closure, sourcemap-no-sources-content, spec check:skill-examples (259 prose examples type-check), lint check:doc-formula-expressions and check:doc-security-posture; spec check:docs was re-run after the spec build and that run is the one cited.
  • By name, each exit 0: check-skills-token-ratchet and its --self-test, check:skill-refs, check:skill-docs, check:react-blocks, check:doc-authoring, check-doc-frontmatter, check-docs-section-name, check-doc-route-spelling --advisory, check:doc-anchors, check:skill-frame-sync, check:skill-identifier-liveness, check:role-word, check:pm-governed-merges, check-adr-0087-registration --base origin/main, check:react-page-adapter-contract, check:page-declaration-shape, check:examples-live-imports, check:nul-bytes.
  • Governed: check-governed-merges --branch: GOVERNED, landing tier H via skills/** × 2; 48 changed lines, under the 5000 threshold. Draft, four-piece; no seat readies, queues or arms auto-merge on it.
  • Not run locally: pnpm lint (repo-wide eslint, CI's run); the showcase Playwright smoke (test:smoke, a browser lane); and no browser run of the two pages — the runtime behaviour above is derived from the pinned-sha readings, and objectui's pin (react-page-invalidation.test.tsx in PR objectui#10973) is the one that drives the real renderer with a page in the taught shape.

Deviations

  • File surface +1: examples/app-showcase/test/react-page-adapter-query-contract.test.ts (6 insertions, 3 deletions). The test lifts the renewals page's React.useEffect(...) call verbatim out of source and evaluates it through new Function('React', 'adapter', 'sel', 'reload', 'setRelated', …); the dependency list now names projectChanged and invoiceChanged, so the lifted call threw four times. The harness gains the two parameters, bound to 0, and a three-line comment. Bounded in-place fix, four conditions: ① the same change this card makes — the effect's dependency list is what the card edits, and the harness enumerates exactly that list; ② mechanical, the shape pinned by the existing new Function signature; ③ the file's last touches on origin/main are 2026-08-24 (eb10fcfd4) and 2026-08-21 (6cca75c3a) and no claim on it is visible from this worktree — the seat's open-PR file-list read at unlock covered the six named files, not this one, so the seat confirms; ④ inside the showcase pnpm test family already in this PR's verification set, no new verification surface. The claim's file surface wants this path appended in the same round (the seat's write).
  • The first commit's co-author trailer carried a model name and the pre-push hook (check:commit-card-trailers) refused it before anything left the worktree; amended to the model-free pair and pushed. Nothing else deviated.

Acceptance notes (observed, not filed)

  • skills/objectstack-ui/rules/pages.md's Master/detail example and crm-workbench.page.ts key the ListView on a counter (key={reload} / key={reloadKey}) to refresh it after a save. At the pinned sha ListView reads the bus itself (data-list.tsx:137), so that remount is redundant for the refresh; it still works and the runtime honours it, so this is a boundary note and not a card — it is also the pattern data-invalidation.ts's header says the bus "deliberately replaces". Retiring it changes what the example teaches. carrier: this lane's seat, when it chooses; noted, not filed.
  • Same file, the kind:'react' paragraph: "os build does NOT lint react source" stands a few lines under "os validate rejects them here (react-block-needs-record-context)". Two different commands, and os validate does read react source (that rule and page-source-className-tailwind), so the sentence holds for os build only as far as it goes; not re-worded here (not this card, and the file has 16 tokens of headroom). carrier: none; noted, not filed.

Generated by Claude Code

维护者速读(草稿)

改了什么。 objectui 已把 useDataInvalidation(数据失效总线的读端,返回一个数字 nonce)注入 kind: react 页面的作者作用域(objectui PR 10973,已在本仓钉住的控制台 db11afd49 内)。本 PR 是 objectstack 这一半:react-pages 文档的作用域表与 Live data 示例、objectstack-ui skill 的 react 作用域规则(一条)、生成器 build-react-blocks-contract.ts 的两处字符串及其产物 react-blocks.md(经脚本重新生成)、以及 showcase 的两个 react 页面(crm-workbench、renewals-pipeline)在 useAdapter 读数据的 effect 依赖里加入 nonce。附带一处测试 harness 的机械修正(见 Deviations)。

为什么改。 裁决 A(批次 #233 第 2 项,2026-09-28「同意」):PageView 在页面动作后的整页 remount 将被移除(objectui 10777),所有读数据的块都改走失效总线;react 页面自己的 useAdapter 读取若不接总线,会成为唯一在页面动作后留下陈旧数据的面。文档与 skill 教的示例若不带 nonce,AI 照抄出来的页面默认就是陈旧的。

风险与代价(含回滚)。 纯文档 / skill / 示例改动,无协议键变动(Clause-②: no),不发布任何 npm 包(skills、content/docs 不在任何包的 files[],showcase 为 private)。skill 棘轮:pages.md 5681 → 5676 tokens(上限 5692 不动),新增那条规则靠删掉同文件里重复的两句付账;生成文件 +69 tokens 不受棘轮约束。运行时影响仅限 showcase 示例应用的两个页面:页面内自己保存后 effect 会跑两次(key 自增 + 总线事件),多一次读、结果不变。回滚:revert 本 PR 即可,无迁移。

席位意见。 (留空,席位定稿)

你要做的。 这是 Tier H(skills/**)四件套草稿 PR:需要 os-zhuang 或 hotlong 的 APPROVED review,然后由 domain:skills 席位落地。

(本节由席位在 PR 创建后补贴:dev 报告 5924107460 申报的遗漏。)

claude added 2 commits October 1, 2026 02:32
…in a react page's live-data effect

The kind:'react' author scope injects useDataInvalidation (objectui
deca847a8, in the pinned console db11afd49). The react-pages guide's scope
table and Live data example, the objectstack-ui skill's react scope list,
the generator's injected-scope line and Live-data note (react-blocks.md
regenerated through gen:react-blocks), and the two showcase react pages
that read through useAdapter in effects now carry the nonce, so a page
action elsewhere re-runs the read in place with no remount.

Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg
Co-authored-by: Claude <noreply@anthropic.com>
…wo bus nonces

The rollup effect's dependency list now names projectChanged and
invoiceChanged (the injected useDataInvalidation nonces), so the harness
that evaluates the lifted call verbatim supplies both as numbers that never
move; the four executed-contract assertions read the same body once, as before.

Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2a4254a608cc290214633883b8858969db791c21 → packageMentionDocs.

@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 1, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c1770a3875778c2a07f91dcff1941586215bb884
Local-runs: none

Inputs: card #20395 (body; comments 5865123040, 5870067887, 5923248286, 5923384068, 5924107460 as stored after its one edit); PR #21045 (body read twice — 13228 bytes at creation, 15213 bytes after the seat's append; file list, 7 files; the API diff, byte-identical to git diff 2f2fa11d7..c1770a387 modulo index lines); the head's check-runs (two reads); objectstack at the merge-base 2f2fa11d7, at origin/main d67b94280 and at the head; objectui at the pinned sha db11afd4967cd9d39381c5e21dc2deec9d706204 (.objectui-sha reads the same at the head and at origin/main); ruling 5864668610 and PR objectui#10973 (merged deca847a8). main's advance since the base touches none of the seven files, so the diff against current main is the same 7 files, +33/-15.

① Derived judgments

(a) Every sentence about the hook, read at db11afd49. packages/components/src/renderers/layout/react-page.tsx imports useAdapter, useDataInvalidation from @object-ui/react (:47); the scope memo (:186-206) spreads buildComponentScope(adapter) and adds useAdapter, useDataInvalidation, data, variables, page, with dependencies [schema, adapter] only, so the scope identity, and with it the page's own state, is unchanged by the new member. packages/react/src/data-invalidation.ts:106 declares export function useDataInvalidation(objectName?: string, recordId?: string): number — a useState(0) counter incremented once per bus change for which dataChangeMatches holds ('*', or the same objectName; a record-scoped change also stales a reader that passed no recordId); with no objectName it subscribes to nothing and stays 0. Its docblock (:96-104) prescribes exactly the use taught here: the nonce in the fetch effect's dependency array, "the refetch happens in place, no remount". The bus is fed by useMutationInvalidationBridge(dataSource), mounted once at packages/app-shell/src/console/AppContent.tsx:449; the ListView renderer reads the same hook at packages/components/src/renderers/basic/data-list.tsx:137. PageView at the pin keys on identity and no longer remounts after a page action (its header :34-37; no-refresh-key-remount.ratchet.test.ts holds it), and objectui's own pin react-page-invalidation.test.tsx asserts "one bus event on the page object re-runs the page read once, in place, with no remount" plus the lit control. Against that reading: the scope-table row in react-pages.mdx (returns a number that moves when a write to that object is reported on the bus; name it in the dependencies of the effect that reads through useAdapter) — true; the Live data paragraph (reported on the bus, the same bus the ListView block refreshes from, the nonce moves, the effect runs again in place, the page is not remounted so its state survives) — true; the pages.md bullet (the data-invalidation bus nonce, a number; name it in the deps of the effect that reads through useAdapter, so a page action elsewhere that writes obj re-runs that read in place, with no remount) — true; the generator's two strings (the scope line gains useDataInvalidation; the Live-data note: a number that moves when a write to obj is reported, name it in the deps of the effect that reads through useAdapter, re-runs in place with no remount) — true. No sentence claims more than the pin delivers.

(b) The Live data example. const changed = useDataInvalidation('showcase_invoice') and }, [adapter, changed]); — the nonce is in the dependency array; the read is on showcase_invoice, the same object the nonce names. The one-paragraph why is the mechanism in (a) and nothing else. The [Live data](#live-data) link targets the ## Live data heading at :165; Check Documentation Links is success. The page's other sample ("A complete page", :422-494) has no useAdapter effect, so no untaught site remains on the page.

(c) pages.md's paid line. Read at the base: the deleted text is one sentence over four blockquote lines — the contract covers the curated core set; runtime-injected blocks outside it (ObjectMetric, ObjectKanban, …) read their props from the block registry at render time; except the record:* family, rejected on this surface. Each teaching has a home in the bullet directly above it at the head (:176-190): "The scope is built at runtime from the public block registry (every non-container public block gets a PascalCase wrapper), so blocks like ObjectMetric / ObjectKanban exist even though the written contract below documents only the curated core set"; "Exception — the record:* family is NOT usable here … os validate rejects them here (react-block-needs-record-context)". The one phrase with no surviving spelling, "read their props from the block registry at render time", names the runtime mechanism, not an author action; its author-facing consequence (the written contract covers only the core set; the data props come from the spec schemas — the blockquote sentence that stays) survives. Not a lost rule. Counts, ceil(utf8 bytes / 4) (the convention at check-skills-token-ratchet.mjs:184): 453 → 453 lines, 22724 → 22701 bytes, 5681 → 5676 tokens; ceiling row ['skills/objectstack-ui/rules/pages.md', 5692] at :536, untouched (the diff touches nothing under scripts/). git diff -U0 shows exactly two hunks, -171,0 +172,3 and -203,4 +206,1: no untouched line re-wrapped. The ratchet and its --self-test run in Type Check · source gates (success). The dev's own numbers match. (The PR body says "two sentences", the report "a restated blockquote sentence"; it is one sentence — immaterial.)

(d) The generator. The diff to packages/spec/scripts/build-react-blocks-contract.ts is two string literals (the note at :139, the scope line at :172). The script's only emit target is OUT_MD = skills/objectstack-ui/references/react-blocks.md (:29, :174); react-blocks.md moved by exactly those two lines (:10 and :115), 115 → 115 lines, 3340 → 3409 tokens — the +69 is the note sentence. check:react-blocks (tsx scripts/build-react-blocks-contract.ts --check) runs in Type Check · source gates, success: the committed file is the script's output. skills/objectstack-ui/references/_index.md is byte-identical between base and head; both it (skills/*/references/_index.md) and react-blocks.md are merge=os-regen rows (.gitattributes:165-166), neither hand-edited.

(e) The two showcase pages. A .page.ts is definePage({ kind: 'react', source: \…` })— the react source is a template-literal string;useDataInvalidationresolves at render from the injected closure scope, so no import exists to add andtscnever sees the identifier.crm-workbench.page.ts: one nonce, projectChanged = useDataInvalidation('showcase_project'); the KPI effect reads showcase_projectand its dependencies are[refreshStats, reloadKey, projectChanged]— right.renewals-pipeline.page.ts: two nonces, showcase_projectandshowcase_invoice, the two objects the rollup reads (the hook takes one object; a reader on '*'matches only wildcard changes, so two readers is the correct spelling); dependencies[adapter, sel, reload, projectChanged, invoiceChanged]— right; the firstReact.useEffect(in the source (:86) is still the rollup, so the harness's anchor in (f) still lifts it.objectstack validateridesDogfood Verify CLI(success); the showcasetsc --noEmitridesType Check · workspaceand itsvitest runridesTest Core`, both still in progress at my second read (recorded in ③).

The dev's double-run note, judged: for CRM Workbench it is a correct upper bound — a save in the page's own ObjectForm on showcase_project bumps reloadKey in afterSave and the same write reaches the bus through the bridge and moves projectChanged; whether that is two effect runs or one depends on whether React batches the two state updates into one commit, and the result is the same rows either way. For Renewals Pipeline it is not a correct reading: the page's own form is the drawer on showcase_account (:177-181), whose write is reported on showcase_account — matched by neither nonce — so only reload moves and the rollup runs once. The PR body's rationale for keeping the counters ("they also key the ListView and ObjectForm remounts those pages do") holds for CRM Workbench (key={reloadKey} on both blocks) and not for Renewals, where reload keys nothing and is solely the rollup's dependency (:64, :107, :180); keeping it there is still right, because an account save is a write the nonces do not cover. Should the page text say so? No: the pages' comments teach the pattern correctly, the redundancy belongs to the retained key-remount idiom the acceptance note already names as a separate decision, and an example page should not teach a transient double read. The seat's 速读 sentence inherits the over-generalisation — see ③.

(f) The test harness, outside the claim's file surface. examples/app-showcase/test/react-page-adapter-query-contract.test.ts lifts the first React.useEffect(…) out of the renewals source verbatim and evaluates it through new Function('React', 'adapter', 'sel', 'reload', …, 'setRelated', effectSource); the dependency array is an expression that call must evaluate, so the two identifiers this card adds threw ReferenceError four times at 2ecc5d7ff. The four bounded-in-place conditions, judged at the head: ① same defect class — the harness enumerates exactly the dependency list the card edits, and it broke only because of this card's own edit; ② mechanical — two parameters and two 0 arguments in the shape the existing signature pins, plus a three-line comment; ③ no one else's claim — the file's last touches on origin/main are eb10fcfd4 (2026-08-24) and 6cca75c3a (2026-08-21), and No other open PR may claim the same single-writer path is success on the head; ④ same gate family — it runs under the showcase vitest run already in the verification set. All four hold. The edit does not weaken the test: the stub React.useEffect runs the body once and ignores the dependency array; the nonces are bound to 0 and never enter the body; the extraction control still asserts both adapter.find calls are in the lifted text, and the four executed-contract assertions ($top reaches the adapter, rows under data, total under a cap, capped) measure the body's run exactly as before. Nothing about re-runs was asserted before and nothing is now.

(g) Scope. Seven files; under packages/spec only scripts/build-react-blocks-contract.ts; nothing under packages/lint; no objectui file. The ⛔ lines of the claim hold.

(h) The PR body. At creation (13228 bytes): line 1 Fixes #20395; Clause-②: no at a line start; ## The hook, read at the pinned sha; the ## skills/** readings table with the two touched files (lines and tokens, the ratcheted one with its ceiling) and the whole-package / catalog rows; the generator diff summary (item 3 of ## What changed); the session-URL footer once; no model identifier. ## 维护者速读(草稿) absent at that read — the dev's deviation is accurate. At my second read (updated 03:34:10Z, 15213 bytes) the seat had appended that section (five parts and a one-line provenance note); line 1, the Clause-② line and the single footer are intact.

② Semver level

Nothing published changes. @objectstack/spec files[] is dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json — scripts/ is outside it. @objectstack/example-showcase is private: true. Of the 80 package.json files under packages/ and examples/ at the head, none has a files[] entry naming skills or content. No Zod schema, no protocol key, no export moves; the scope widening was objectui's half (Clause-②: yes there, PR objectui#10973). Clause-②: no is right, as the card body states. skip-changeset is correct; the seat applied it at about 03:34Z, and Check Changeset re-ran in the original check suite to success (03:34:36Z), superseding the 03:26:56Z failure that predated the label.

③ Boundary flags

  • Missing 速读 draft — declared by the dev (5924107460, the stored text); the seat has since appended it to the PR body (verified in the stored body). One correction for the seat's finalisation: its sentence「页面内自己保存后 effect 会跑两次(key 自增 + 总线事件),多一次读、结果不变」holds for crm-workbench only; on renewals-pipeline the page's own form writes showcase_account, which neither nonce reads, so the rollup runs once (①(e)). Prose, not diff; no code change needed.
  • File surface +1 — the test harness; the four conditions hold at the head (①(f)). The claim comment 5923384068 still lists six files at my read; the append of the seventh is the seat's pending write, as the dev asked.
  • Trailers — the two published commits 2ecc5d7ff and c1770a387 both end with Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg and Co-authored-by: Claude at the anthropic address — the model-free pair. The refused first attempt never left the worktree (the pre-push hook runs check-commit-card-trailers.mjs --pre-push); nothing model-named is on the remote. Accepted.
  • api_writes: 3, one over the budget of 2 — the third was a comment_edit of the dev's own report to add the 速读 deviation the first copy omitted. The right call: the report comment is the dev's identity record and its GitHub copy must equal the terminal report; a second comment would have left two reports (the stale one un-marked) at the same write cost, and editing one's own comment is not the "state you did not set" class. Recorded with its reason; accepted, not escalated.
  • mcp_calls: 0 — consistent with the record: every comment on the card and the PR is authored by objectstack-fleet[bot], and the dev names the with-fleet.sh --via dispatch relay for all three writes.
  • Gate claims against the head's check-runs. The dev reports 94 derived families all exit 0 locally, plus check:generated, check:skill-docs, the token ratchet and its self-test, check:react-blocks, and check-governed-merges --branch = GOVERNED tier H via skills/** ×2 (48 changed lines). CI is the verdict. First read (runs completed up to 03:33:22Z): 32 runs — 21 success, 8 in_progress, 2 skipped, 1 failure (Check Changeset, pre-label). Second read, 03:38:33Z: 44 runs — 28 success, 9 skipped, 7 in_progress, 0 failure. Not success or skipped at the second read, all in_progress: Lint & Repo Gates (required); Test Core (1/6), (3/6), (4/6), (5/6), (6/6) (shards; (2/6) success; the required Test Core rollup not yet present); Type Check · workspace (· source gates, · consumer gates, · debt ledger success; the required TypeScript Type Check rollup not yet present). Skipped: Console Pin Gate (no .objectui-sha move), Packed-tarball smoke (opt-in) ×2, and in the two label-event suites Auto Label, Check PR Size and Check Changeset once each. Required contexts concluded success: Build Core, Dogfood Regression Gate (rollup and three shards), Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. An in-progress required job is not a pass: three of the seven required contexts had not concluded at my read.
  • Landing. The diff hits skills/** twice, so the PR is Tier H whole: this record is the contract review of record for the head and does not by itself lift the landing — that waits for an authorized APPROVED review by an account in GOVERNED_APPROVERS and for the three open required contexts to conclude success, after which the owning seat lands it. No seat readies, queues or arms auto-merge before both.
  • Observed, not a defect. The PR body's "What a page action now does" bullet over-generalises the double run and the key-remount rationale to both pages (①(e)); the dev's three acceptance notes (the redundant key-remount on the ListView block in pages.md's Master/detail example and crm-workbench; the os build sentence in pages.md; the double read) are correctly left unfiled as boundary notes.

Implemented-by: claude/issue-20395-react-page-invalidation-nonce
Reviewed-by: session_01KTZmMfzVzjNvyaLyQ8mHvg

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

改了什么:objectui 那一半(PR objectui#10973,已在本仓钉住的控制台 db11afd49 内)把 useDataInvalidation 注入了 react 页面的作者作用域;本 PR 是 objectstack 这一半——react-pages 文档的作用域表与 Live data 示例、objectstack-ui 技能的一条规则、生成器 build-react-blocks-contract.ts 的两处字符串及其产物 react-blocks.md(经脚本重生成),以及 showcase 两个 react 页面在 useAdapter 读数据的 effect 依赖里加入 nonce。附带一处测试 harness 的机械修正(它枚举的正是本卡改的依赖列表)。

为什么改:裁决 A(objectui#10887 member 3,批次 #233 第 2 项,你 9 月 28 日「同意」)。PageView 页面动作后的整页 remount 已移除,读数据的块都走失效总线;react 页面自己的 useAdapter 读取若不接总线,就是唯一会留陈旧数据的面;技能与文档不教 nonce,AI 照抄出来的页面默认就是陈旧的。

风险与代价(含回滚):只改文档、技能、示例与一个示例测试,不碰协议键、不发包(skip-changeset);技能棘轮 pages.md 5681 → 5676 tokens(上限不动,新规则靠删同文件里重复的一句付账)。运行时影响仅限 showcase 两页:crm-workbench 在页内自己保存后 effect 会跑两次(key 自增 + 总线事件),多一次读、结果不变;renewals-pipeline 自己的表单写的是 showcase_account,两个 nonce 都不读它,只跑一次。复审(契约复审档,记录 5924298890)逐句在钉住的 sha 上核过。回滚 = revert。

席位意见:建议批准。三条观察留给后续(技能示例里对 ListView 的 key-remount 已属冗余;os build 那句只对 build 成立;上述双读),均不是本 PR 的缺陷。

你要做的:在本 PR 上留一条 APPROVED review(os-zhuang 或 hotlong)。本席本班收班,落地(清 needs-user-decision、翻 ready、挂 auto-merge)由下一任 skills 席按座位贴 #7623 的留守项执行;CI 尚有三项必查在跑,落地前需全绿。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 1, 2026 05:19
@os-zhuang
os-zhuang enabled auto-merge October 1, 2026 05:19
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 63d1a7c Oct 1, 2026
54 of 55 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-20395-react-page-invalidation-nonce branch October 1, 2026 05:54
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 size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

3 participants