Repository navigation
docs(governance): objectui is a product-neutral renderer — AGENTS.md rule, ADR-0060, and a ratchet over registry keys, files and locale keys - #12253
Conversation
…rule, ADR-0060 and its ratchet A product composes its pages from generic blocks; a page that cannot be composed is fixed by a generic block here or a contract door in @objectstack/spec, never by a widget, dialog, hook or i18n namespace named for the product. What exists stays until its page migrates and may only shrink. - AGENTS.md: a "Product-neutral renderer" section in §1. - docs/adr/0060-product-neutral-renderer.md: the ruling, the readings taken, and the frozen-baseline block the ratchet reads as its ceiling. - app-shell: product-neutral-renderer.ratchet.test.ts and its live list product-specific.allowlist.json (delete-only; must stay inside the ADR's ceiling). - scripts/markdown-test-inputs.mjs: the test reads the ADR. - .github/pull_request_template.md: one checkbox. - An empty-frontmatter changeset: nothing publishes. Claude-Session: https://claude.ai/code/session_012dGTuJen61ECNHXYXKChoC Co-authored-by: Claude <noreply@anthropic.com>
…ew pull-request template check-installed-spec-pin-claims scans every tracked file, so the new .github/pull_request_template.md is its input; the ledger is declared total over tracked markdown and goes red until that read is recorded. Claude-Session: https://claude.ai/code/session_012dGTuJen61ECNHXYXKChoC Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card #12224 (body and all five comments), PR #12253 (body, its seven-file list, the net diff against the merge-base Check-runs on the head, re-read at 2026-10-12T23:12Z before posting: 33 total, all ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
② Semver level
Clause-②: no — no accept-set and no public API of any released package widens or narrows. ③ Boundary flagsThe dev's flags (os-dev report 6125187793,
What the maintainer must look at (Tier H: the approval is the landing act, and these ride with it):
Implemented-by: VERDICT: PASS Generated by Claude Code |
…is not a gate GitHub prefills .github/pull_request_template.md only for pull requests opened through the web form, so a PR opened through the API carries no checkbox and nothing checks for one. The Consequences bullet claimed every pull request carries it; it now says which ones do and that it is a prompt. The bounds list's "the checkbox and review catch the rest" is narrowed the same way: the rest is left to review. The frozen-baseline block is unchanged. Claude-Session: https://claude.ai/code/session_012dGTuJen61ECNHXYXKChoC Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card #12224 (body and all eight comments, the rework scope This head is the previously reviewed Check-runs on the head, re-read at 2026-10-12T23:27Z before posting: 33 total (a ① Derived judgmentsEvery accept-set and public-surface change the net diff implies, each judged:
② Semver level
Clause-②: no — no accept-set and no public API of any released package widens or narrows; the PR body's ③ Boundary flagsThe dev's rework report ( The earlier dev report's flags ( What the maintainer must look at (Tier H: the approval is the landing act, and these ride with it). Items 1–4 and 6 are carried forward from
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读(终稿)PR #12253 · card #12224 · head 改了什么
为什么改
风险与代价(含回滚)
席位意见
你要做的
Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Fixes #12224
Clause-②: no
objectui is now a product-neutral renderer, as the maintainer ruled on 2026-10-12. This pull request writes the rule into
AGENTS.md, records it as ADR-0060, and holds it with a ratchet test.AGENTS.mdanddocs/adr/**are governed (Tier H), so the whole pull request stays a draft until an authorized approval. Drafted in sessionsession_012dGTuJen61ECNHXYXKChoC, branchclaude/issue-12224-product-neutral-renderer.The ruling, verbatim and untranslated:
维护者速读(草稿)
改了什么
@objectstack/spec的通用块拼自己的页面,文案留在产品自己的包里。页面拼不出来时,有两条路:在 objectui 加一个按「画什么」命名的通用块,或者在@objectstack/spec开一个契约门。不再为某个产品加专用的组件、对话框、hook 或文案命名空间。AGENTS.md(§1 新增一节,共 +10 行)和新的 ADR-0060。27c30c10)。这张清单只能删,不能加。出现以下任何一种,测试都会变红:新的产品命名注册、冻结目录里的新文件、冻结命名空间下的新文案键。为什么改
风险与代价(含回滚)
cloud、hotcrm)打头的命名空间或键,拒收;冻结命名空间下,不许加键;起中性名字的新命名空间,放行。详见下文「H3」。cloud-connection:panel这个注册;console/cloud-connection/和console/diagnostics/CloudAiModelStatus.tsx;@object-ui/types的cloud.ts与zod/cloud.zod.ts;planTier与aiModelStatus两个命名空间;environment命名空间(卡上只写了environment.entitlement)。席位意见
(留空,由席位定稿)
你要做的
pnpm test都会在unit项目里跑它。What changes
AGENTS.mddocs/adr/0060-product-neutral-renderer.mdorigin/mainbefore opening and is still free; 0059 is the last ADR there.packages/app-shell/src/__tests__/product-neutral-renderer.ratchet.test.tsunitproject.packages/app-shell/src/__tests__/product-specific.allowlist.jsonscripts/markdown-test-inputs.mjscheck-installed-spec-pin-claimsreads the new template. That gate scans every tracked file, and the ledger is pinned total over tracked markdown..github/pull_request_template.mdcross-repo-issue-closer.yml,check-changeset-claims.mjs) finds anything in it..changeset/12224-product-neutral-renderer.md__tests__is excluded from app-shell's build.How the ratchet measures (ADR-0060 has the full text)
deriveRegistryKeys(scripts/check-doc-component-types.mjs), the one registration reader thatobjectui check's known-type list already comes from. A key counts when any colon segment leads with a product word. It does not scan forregister('cloud:itself.packages/,apps/orexamples/and either sits in a frozen directory (app-shell/src/environment/orapp-shell/src/console/organizations/, recursively) or has a path segment led by a product word.cloudandhotcrm, matched only when they lead the name at a word boundary, after skipping a hook's leadinguse. SouseCloudThingcounts, whileword-cloudandcloudydo not.H1: both registration spellings
At
923504f4a, a literalregister('cloud:grep finds two cloud widgets:cloud:ai-model-statusandcloud:onboarding-next.cloud:plan-statusandcloud:workspace-timezone-noticeregister asregister('plan-status', …, { namespace: 'cloud', skipFallback: true, … })and similar, so a literal scan misses them. The derivation reads both spellings, and it found a fifth product registration that the card's inventory did not list:cloud-connection:panel, withapp-shell:twins. The live list holds 8 keys. A fixture control in the test checks both spellings against a synthetic tree, so the namespace half is still tested after the last product widget has migrated. M1 and M2 below show the real tree going red on each spelling.H3: the locale rule's reading
Read literally, the card says two things: a new top-level namespace fails, and the list may only lose entries. Together they forbid every new namespace forever, including the generic-block copy that rule 2 asks for. The reading taken here follows rule 2's own words, "never an i18n namespace named for the product":
ADR-0060 records this, and the 速读 above asks the maintainer to confirm it.
The frozen namespaces fall into three groups:
cloudConnection,cloudOnboarding,cloudPlanStatus,cloudWorkspaceTimezoneNotice.organizations,workspace, and the whole ofenvironment. Every source consumer ofenvironmentsits in the frozenenvironment/directory.planTierandaiModelStatus. Each has one consumer, and that consumer is a frozen or product-named file.Some namespaces were considered and not frozen; the ADR gives the reasons:
organization(singular), because@object-ui/authand the layout also read it;chatbotQuota, a generic quota state;marketplaceandconnectAgent, features of the open stack.The locale bundles are the ten in
packages/i18n/src/locales/*.ts: en, zh, ja, ko, de, fr, es, pt, ru, ar. The test reads them through@object-ui/i18n/locales.Mutation proofs (acceptance 2)
All five proofs follow the same method:
5298f952).ablation-replace.mjs(objectstack'sscripts/). It needs its anchor to hit exactly once and checks the write on disk, and its WRAP mode runs the test and then restores the file. The new-file leg (M3) was instead a bash script with atrapthat removes the file.git diff HEADempty andgit status --porcelainempty.Two first attempts did not count:
the anchor count moved 1 -> 1, a drop of 0), and its restore proved the files unchanged.M1: a literal
register('cloud:mutation-probe', …)added toconsole/marketplace/InstalledListWidget.tsx.M2: the namespace spelling,
register('mutation-probe', …, { namespace: 'cloud', skipFallback: true }), in the same file.M3: a new
packages/app-shell/src/environment/Foo.tsx.M4: a new key under
cloudOnboarding, inzh.tsonly.M5: M1 plus the move an agent reaches for, appending
cloud:mutation-probeto the live list.The registration assertion now passes, because the key is listed. The ceiling assertion turns red instead.
Gates (objectui, at
27c30c10, local)objectstack's
dispatch-gates.mjsdoes not live in objectui. These gates were therefore derived by hand from objectui'spackage.json,.github/workflows/lint.yml,repo-gates.yml,ci.yml,changeset-presence.ymlandline-citation-gate.yml. Each exit code was captured before any pipe.pnpm exec vitest run --maxWorkers=2 scripts/__tests__/ packages/app-shell/src/__tests__/product-neutral-renderer.ratchet.test.ts(repository-root form)Test Files 182 passed | 2 skipped (184)·Tests 5573 passed | 2 skipped (5575)pnpm exec vitest run --project unit --maxWorkers=2 packages/app-shell/Test Files 277 passed | 1 skipped (278)·Tests 3839 passed | 9 skipped (3848)pnpm --filter @object-ui/app-shell type-check(echoed> @object-ui/app-shell@17.7.0 type-check, afterturbo run build --filter='@object-ui/app-shell^...', 28/28 tasks)tsc --noEmit && tsc -p tsconfig.test.jsonclean;--listFilesOnlyshows the new test among the project's 5366 fileseslinton the two lintable changed files (--format json: 1 + 1 files)parserOptions.project/projectServiceineslint.config.js), so this diff cannot change a verdict on an untouched file.node scripts/check-governed-queue-guard.mjs --test(the 7 paths)⛔ GOVERNED — 2 of 7 path(s) are on a governed surface(docs/adr/**,AGENTS.md), which is the expected verdictnode scripts/check-changeset-presence.mjs✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)(empty frontmatter)node scripts/check-changeset-no-major.mjs✅ No changeset declares a major bump.node scripts/check-changeset-claims.mjs✅ No pending changeset names a file this change touches.node scripts/check-new-cross-file-line-citations.mjsVERDICT new-cross-file-line-citations: 0 new citation(s)node scripts/check-control-bytes.mjs✅ check-control-bytes: OK (scanned 8511 tracked text file(s); skipped 85 binary).node scripts/check-test-path-roots.mjs✅ check-test-path-roots: OKnode scripts/check-doc-links.mjsLinks are valid across 17 scan roots.node scripts/check-lint-coverage.mjs✅ lint coverage: 46/46 packages lintednode scripts/markdown-test-inputs.mjs --audit123 candidate test files, all adjudicated; 87 declared entries, all present.node scripts/check-unreferenced-sources.mjsOK Every shipped source file in every covered package is reachable.node scripts/check-published-tsconfig-tooling-exclude.mjsOK -- all 34 enforced package(s) carry the directory form.node scripts/check-doc-component-types.mjs✅ Every documented component type is registered.Declared to CI, not run locally:
domanddom-heavyprojects of app-shell. This diff adds oneunit-project test and changes no source those projects import.pnpm lint(repository-wide).pnpm check(the CLI self-check).Test (shard N/12)matrix.Selector check:
select-affected-tests.mjskeeps the ratchet in every pull request's fast set. A change confined topackages/plugin-maporpackages/vscode-extensionstill selects it, with the reason "reads the disk or spawns a process". A change to ADR-0060 alone selects it through the markdown ledger.Acceptance notes
dist, and a regression test on frozen code has to stay writable until that code migrates. This is the H2 decision, and it is recorded in ADR-0060.organizations/manage/is inside the frozen directory, since the freeze is recursive.scripts/__tests__/turbo-test-inputs.test.tsguards the Vitest configuration program, not the files a test reads, so the ratchet needs no row there. It ran green above.scripts/markdown-test-inputs.mjsdid need rows, both listed under "What changes".923504f4a, the base, which still matchedorigin/mainwhen this pull request was opened. The baseline is read at this pull request's head. If feat: Setup catalog pages read the registry (editable undersingle, read-only under a wall); assignment pages stay data pages; pickers list the registry (objectstack ADR-0131 D3/D7) #7611 lands first with keys under a frozen namespace, the live list and ceiling are re-read at the new head before landing; no exemption is reserved for it..claude/skills/pm-dispatch/references/filing-gate.md. Measured onobjectstack@9f4b652e3, that file is 58 lines against acheck-skill-line-ratchetceiling of 58, so its headroom is 0. The drafted line (- UI 卡写明所需通用块或 spec 契约门;要产品命名 widget/dialog/hook/命名空间的卡退回重立。, 116 bytes, green oncheck:pm-skill-id-lint) takes the file to 59. The ratchet then answers✗ … filing-gate.md is 59 lines; the ratchet ceiling is 58. The dispatch allowed one line and said to stop on a breach, so the rider waits for the PM to choose how the line is paid for. Its branch,claude/issue-12224-objectui-filing-gate-rider, is pushed with no commits.Generated by Claude Code