Repository navigation
fix(spec): a book's Uncategorized group skips docs a sibling book claims - #27262
Conversation
resolveBookTree takes an optional fourth argument, every registered book, and sends to the synthetic Uncategorized group only the docs of the book's packages that no book claims (ADR-0046 §6.4: "left unplaced"). The claim judge is resolveBookClaimedDocs over each book, the pass resolveDocAudiences already runs, now shared through one generator. The REST book-tree route passes the books it reads. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgBbveX8mMhrSXB6VJ
📓 Docs Drift Check6 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 149 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b209e0326875aa4c1a63bc0e8cd14c34db03efff && git checkout b209e0326875aa4c1a63bc0e8cd14c34db03efff
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0d45c0e71f0a03bdcf2230901515c2a29716a92b e4a5898dee9d3f85b3756d01867501bcdcd50e5c && git checkout -B drift-repro 0d45c0e71f0a03bdcf2230901515c2a29716a92b && git merge --no-ff e4a5898dee9d3f85b3756d01867501bcdcd50e5c
node scripts/docs-audit/affected-docs.mjs --json 0d45c0e71f0a03bdcf2230901515c2a29716a92b |
…ling book claims The console's port of resolveBookTree (book-nav.ts) takes the same optional fourth argument as the framework resolver: every authored book judges the Uncategorized catch-all, so a doc a sibling book places is not listed there. DocPage and BookPage pass useBookData().authoredBooks, never the portal set. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgBbveX8mMhrSXB6VJ
Contract reviewServed-tier: Reviewed against GitHub (PR object, file list, net diff ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgBbveX8mMhrSXB6VJ
Contract reviewServed-tier: Round 2 on the same brief. Round 1 (record 6470694256) passed ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
维护者速读 (席位终稿)
席位意见:接受。契约复核两轮 PASS(6470694256、6472230364)。第一轮遗留的控制台移植在第二轮已修好,并钉了与框架答案一致的对拍测试。 席位核对 · ACCEPT ·
|
Requested by Yibo Wang · project thread
Fixes #27091
Claim: 6468704954
Clause-②: no
Metadata-protocol: none
维护者速读
resolveBookTree的合成 Uncategorized 组只收「没有任何已注册 book 认领」的本包文档;GET /meta/book/:name/tree把它已读到的全部 book 传进去。签名只加了一个可选的第四参数books。控制台文档侧栏用的本地移植版book-nav.ts同步改成同样的规则,页面传入已编写的 book(不含合成的隐式包 book)。permissionSet限定的管理员手册)时,两本书的树都把对方的页面列在 Uncategorized 下;ADR-0046 §6.4 只汇总「未被放置」的文档,被兄弟书放置的文档不算。HotCRM 实测(hotcrm#2327)。books的调用方结果逐字节不变。回滚 = revert 本 PR。packages/spec/src/system/book.zod.ts(resolveBookTree孤儿过滤与新的claimsPerBook)、packages/rest/src/meta-item-read-gate.ts的readableTree、ui/apps/console/src/pages/book-nav.ts(移植版同一处)。pnpm --filter @objectstack/spec exec vitest run src/system/book.test.ts -t "sibling book"跑出 5 条全绿;两本书互不相交的 include 下,两棵树都没有uncategorized组。What changes
resolveBookTree(book, docs, bookPackage?, books?)(@objectstack/spec, additive): an optional fourth argument, every registered book. With it, the synthetic Uncategorized group takes only the docs of the book's packages that NO book inbooksclaims. Without it, the answer is the same as before, byte for byte. The docblock states the new rule and cites ADR-0046 §6.4.resolveDocAudiencesalready ran (resolveBookClaimedDocsover each book with its ownpackageId) is now the private generatorclaimsPerBook.resolveDocAudiencesunions audiences over it, andresolveBookTree's orphan pass reads its union (claimedByBooks). No second judge was written. There is no recursion:resolveBookClaimedDocscallsresolveBookTreewithoutbooks.GET /meta/book/:name/tree(@objectstack/rest):DocsAudience.readableTreepasses the books the audience was built over (the route's own book read,?package=scope included). ThereadableTreedocblock says so.packages/spec/liveness/doc.jsonquotes the REST call site verbatim, so its quote now readsresolveBookTree(book, docs, book._packageId, books). No row changes grade.@object-ui/console):ui/apps/console/src/pages/book-nav.tsis the console's own port of the resolver, and itsresolveBookTreetakes the same optional fourth argument. Its privateclaimedByBooksmirrors the spec's (the authored groups' entries of each book, never the synthetic group).DocPage(the book sidebar) andBookPage(the book landing) passuseBookData().authoredBooks. They never pass the portal set, because its synthetic implicit per-package books are a reading fallback, not an authored claim.@objectstack/specpatch,@objectstack/restpatch,@object-ui/consolepatch, one root.changeset/file with both declaration lines.Why
hotcrm#2327 measured it on HotCRM 18.2.0. The app ships
crm_manualandcrm_admin_manual(gated{ permissionSet: 'system_admin' }) over one package. Every doc is claimed by exactly one book, yet an administrator'sGET /api/v1/meta/book/crm_manual/treeends with anuncategorizedgroup holding the admin pages, and the Admin Manual lists the user pages the same way. "Claimed" was judged against the book's own groups only. #20980 scoped the catch-all to the book's packages but did not cover a sibling book in the same package.Callers of
resolveBookTreebooks?packages/rest/src/meta-item-read-gate.tsreadableTree(the/meta/book/:name/treeanswer, both transports)packages/specresolveBookClaimedDocsui/packages/app-shell/.../doc-draft.tsresolvePlacements(Studio doc preview)packages/cli/src/utils/collect-docs.test.tsThe console's docs pages (
ui/apps/console/src/pages/DocPage.tsx,BookPage.tsx) do not call the spec function. They callbook-nav.ts's own local port of it. Onbce03897that port still judged "claimed" per book: with the HotCRM fixture below it answeredcrm_manual→{"start":["crm_overview"],"sales":["crm_sales_leads"],"uncategorized":["crm_admin_access","crm_admin_automation"]}. Its header calls it "a faithful local port", so the second commit (7f1a2b23, added to the claim by amendment 6470220479) gives it the same rule, and both pages pass the authored books.Tests
packages/spec/src/system/book.test.ts, a new describe block (5 tests) for a doc a sibling book claims:uncategorizedgroup.audience-gated sibling's docs never reach the other book's tree, and every entry withbookswas already an entry without it (the catch-all only shrinks).resolveDocAudiencesover both books plus a stray doc), and each book's claim set equals its tree's authored entries.JSON.stringify) to the tree withoutbooks.ui/apps/console/src/pages/book-nav.siblingBooks-27091.test.tsx(new, 4 tests), on the HotCRM fixture:uncategorizedgroup, and each equals the spec resolver's answer with the same books (what the tree route serves).booksthe port's tree is unchanged.DocPagerendered at/docs/crm_manual/crm_overviewshows the book's own pages, no Uncategorized heading, and no link to either admin page.packages/rest/src/rest.test.ts: a holder of every book opensorg_bookandgated_bookthrough the route. Each tree is{ g: [own], uncategorized: ['loose_note'] }. The comment on the existing anonymous-tree test now gives the new reason the sibling docs are absent.Acceptance notes
?package=scope. The tree route reads books in the same package scope as its docs, so under?package=Xonly X's books judge X's catch-all. Two shapes of a book of ANOTHER package can claim an X doc, and neither judges there: a group whoseincludematches the doc because the group carriespackage: X, and a group whosekeyan X doc names in its explicitgroup. Without?package=, every book judges.useBookDatareads every book and doc throughmeta.getItems, so in the console every authored book judges.Verification
Base
origin/maind745bb5e; heade4a5898d. The spec and REST readings below were taken onbce03897. The second and third commits touch only the console files, the changeset and one indent inrest.test.ts. The console readings and the derived gates were taken one4a5898d.Measure first (base d745bb5, HotCRM's two-book fixture)
The same script on this branch:
Suites (under os-verify-lock, after the rest^... closure build)
pnpm --filter @objectstack/spec test: Test Files 743 passed (743), Tests 22268 passed, 1 todo.pnpm --filter @objectstack/spec typecheck: exit 0.pnpm --filter @objectstack/rest test: Test Files 334 passed (334), Tests 6560 passed, 326 skipped.pnpm --filter @objectstack/rest typecheck: exit 0.VERDICT command-exit 0, held 806s.pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date.On
7f1a2b23(console commit), one lock run:pnpm --filter '@object-ui/console^...' build && pnpm --filter @object-ui/console test && pnpm --filter @object-ui/console type-check.@object-ui/consoletest: Test Files 259 passed (259), Tests 2575 passed (2575).@object-ui/consoletype-check: exit 0.VERDICT command-exit 0, held 1427s.vitest run --project @object-ui/console siblingBooks-27091): Test Files 1 passed, Tests 4 passed.Ablation (one each, via scripts/ablation-replace.mjs, restored to the HEAD blob)
&& !claimedByAnyBook?.has(d.name)turns 3 of the 5 new tests red (tests 1–3). Tests 4 and 5 stay green, as designed: they pin what must not move. Restored: blob == HEAD6bb1240e6c60., booksfrom thereadableTreecall turns the new route test red. Restored: blob == HEAD5d14f27dd89d.&& !claimedByAnyBook?.has(d.name)inbook-nav.tsturns 3 of the 4 new console tests red. The control stays green. Restored: blob == HEADd3a3071ca099.DocPagecallingresolveBookTree(book, allDocs)withoutauthoredBooksturns the rendered-sidebar test red (1 of 4). Restored: blob == HEADd41eb5b096ad. The first try of this leg matched no test file (a path filter the vitest project did not resolve), so it measured nothing. It was re-run with a name filter.Derived gates
Round 1, on
bce03897: 81 families. 79 exit 0 on the first run.check-plugin-teardown-shape --self-testexited 3 because the shallow clone lacked its pinned fixture621a4876; after fetching it, it exits 0 (48 cases).check:dual-build-cjs-loadsexited 3, PREREQUISITE NOT MET.Round 2, on
e4a5898d:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 84 families. The console diff addscheck-sdui-manifestand its self-test, andpnpm --dir ui exec vitest run scripts/(5020 passed, 2 skipped). Reconciled with--ran: 84 accounted for, 83 run, 1 NOT MEASURED, 0 UNRUN, exit 0.pnpm check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET (it needs every package'sdist, so a full build). NOT MEASURED locally; CI runs it.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzgBbveX8mMhrSXB6VJ