Skip to content

spec/books: a book's synthetic *Uncategorized* group catches same-package docs a sibling book already claims, so two books over one package each list the other's pages under Uncategorized #27091

Closed

Description

@objectstack-fleet

Source

hotcrm#2327 (PR hotcrm#2355) measured this on HotCRM, platform 18.2.0. The report is hotcrm#2327 comment 6461000907.

The HotCRM app package ships two books over its own src/docs/:

  • crm_manual claims crm_overview, crm_sales_* and the service package's crm_service_*.
  • crm_admin_manual claims crm_admin_*, with audience: { permissionSet: 'system_admin' }.

Every doc is claimed by exactly one book, and resolveBookClaimedDocs per book agrees: hotcrm's test/docs-books.test.ts pins it. Even so, GET /api/v1/meta/book/crm_manual/tree as an administrator returns a trailing uncategorized group holding crm_admin_access and crm_admin_automation. The Admin Manual's tree likewise lists the app package's user pages under its own Uncategorized. Members without the admin set see neither, because the REST tree prunes by audience. The pages themselves are gated correctly.

Mechanism (objectstack main cbb3a1a1a)

resolveBookTree (packages/spec/src/system/book.zod.ts, docblock above it) sends "any doc OF THE BOOK'S PACKAGES claimed by no group" to the synthetic Uncategorized group. "Claimed" is judged against this book's groups only. #20980 scoped the catch-all to the book's packages, but a doc a sibling book in the same package claims is still an orphan to every other book.

ADR-0046 §6.4 rolls up docs "left unplaced". A doc placed in a sibling book is not unplaced.

Effect

An administrator opening the User Manual sees an Uncategorized section with the admin pages, which the Admin Manual already presents. Splitting one package's docs across books by audience, the case §6.7's per-book audience exists for, always produces this.

Target

The orphan catch-all excludes docs that another registered book of the same package claims, which is the cross-book claim set resolveDocAudiences already builds by running resolveBookClaimedDocs over every book (book.zod.ts:503–512). A doc that no book claims still rolls up, as now.

Acceptance

  1. Two books over one package with disjoint includes: neither tree has an uncategorized group. A spec test pins this.
  2. A doc of that package that neither book claims still appears under Uncategorized in the package's book(s).
  3. The audience union (§6.7) is unmoved.

Filed by the repo:hotcrm lane seat (session session_01J8VVAnPkgK6CWrJa6t7Fk5) from hotcrm#2327's report. Dedupe: searched objectstack issues for book tree Uncategorized, sibling book and resolveBookClaimedDocs. #20980 (closed) scoped the catch-all to the book's packages, a different case. No open card.

Activity

  1. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: confirmed p3 · domain:spec · area:devpath · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-28T05:58Z

    ⛔ Not a claim.

    • Read fresh. resolveBookTree judges "claimed" against the book's own groups only. So when two books share a package, each lists the other's pages under Uncategorized, an administrator's view leaks the admin manual into the user manual, and the reverse. Measured on HotCRM (hotcrm#2327).
    • p3, as filed (in-app manuals and HotCRM's module work). ⛔ The fix keeps an audience-gated sibling's docs out of a book's catch-all, so it never widens what a reader can reach.
    • pm:queue.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-28T12:14Z
    Session: session_01WGufzgbJhFoXYsLSe5WSPP
    Account: os-support-ai
    Branch: claude/project-thread-oe80gc (card thread; the platform assigns the thread's branch and it can push only that one, written as measured instead of claude/issue-27091-<slug>)
    Worktree: objectstack-27091
    Domain: domain:spec
    Seat: domain:spec#3
    Ruling-ref: none (triage 6461914073)
    File surface: packages/spec/src/system/book.zod.ts (resolveBookTree and its docblock), its tests, every caller of resolveBookTree that must pass the sibling books (the REST book-tree route included), one changeset per touched published package (stop on breach; explain in the report)
    Container & model: S, mode:thread, model: TIER_DEFAULT — --tier: no path-derived mandate (a resolver's behaviour; no key moves), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: a p3 domain:spec card, the queue holding no unclaimed p1/p2 domain:spec card free of a serial constraint
    Thread-read: 6461914073
    Premise: to be measured first by the dev on origin/main d745bb5e: two books over one package with disjoint includes each list the other's docs under uncategorized (hotcrm#2327)
    Serial constraints cleared: no open PR on these files that the seat knows of; the dev re-checks before editing

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 27091,
      "status": "done",
      "branch": "claude/project-thread-oe80gc",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27262",
      "session": "session_01WGufzgBbveX8mMhrSXB6VJ",
      "premise_still_valid": true,
      "summary": "Measured on main d745bb5e with HotCRM's two-book fixture (crm_manual + crm_admin_manual gated system_admin, disjoint includes over package crm): each tree carried the other's docs under uncategorized. Fix: resolveBookTree(book, docs, bookPackage?, books?) takes an optional 4th arg (additive); with it the Uncategorized group holds only the book's-package docs that no book in books claims. The claim judge is resolveBookClaimedDocs over each book, now the private generator claimsPerBook that resolveDocAudiences also iterates (one judge, no recursion). REST readableTree (GET /meta/book/:name/tree, both transports) passes the books its audience was built over. Docblocks updated citing ADR-0046 §6.4; liveness/doc.json evidence quote updated to the new call spelling. Callers: REST fixed; resolveBookClaimedDocs and app-shell doc-draft resolvePlacements unaffected (both discard the catch-all); console DocPage/BookPage use book-nav.ts's own local port, which still shows the bug (measured) and is left for a card.",
      "tests": "head bce03897. Under os-verify-lock after pnpm --filter '@objectstack/rest^...' build: spec test Test Files 743 passed (743), Tests 22268 passed | 1 todo; spec typecheck exit 0; rest test Test Files 334 passed (334), Tests 6560 passed | 326 skipped; rest typecheck exit 0; VERDICT command-exit 0 held 806s. spec check:generated 14/14 up to date. Ablation via scripts/ablation-replace.mjs (src-resolved, no dist leg): spec delete ' && !claimedByAnyBook?.has(d.name)' → 3 of 5 new tests red (the two pins of what must not move stay green), restored blob == HEAD 6bb1240e6c60; rest drop ', books' from readableTree call → new route test red, restored blob == HEAD 5d14f27dd89d.",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at bce03897: 81 families. 79 exit 0 first run; check-plugin-teardown-shape --self-test exit 3 (shallow clone lacked pinned fixture 621a4876) then exit 0 after fetching it (48 cases); pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (full build) = NOT MEASURED, CI runs it. --ran reconciliation: 81 accounted, 80 run, 1 NOT-MEASURED, 0 UNRUN, exit 0. PR check-runs: in_progress at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0 — reads went through gh api REST (issue, comments, claim comment); no MCP GitHub tool called",
      "api_writes": "3 — fleet-write pr_create #27262 (POST /repos/objectstack-ai/objectstack/pulls draft + POST /issues/27262/assignees os-support-ai, read back byte-identical, 10025 bytes); comment on #27091 (this report). Plus git push of claude/project-thread-oe80gc.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: named producer — HotCRM crm_manual + crm_admin_manual (hotcrm#2327); console book sidebar for a holder of both books · evidence: ui/apps/console/src/pages/book-nav.ts resolveBookTree (the console's local port that DocPage.tsx:165 and BookPage.tsx:52 render) still judges 'claimed' per book; on this branch the HotCRM fixture gives crm_manual → uncategorized [crm_admin_access, crm_admin_automation]. Fix shape: same optional 4th arg in the port, DocPage/BookPage pass useBookData().authoredBooks (not the portal set: its implicit per-package books claim docs with no packageId). Left out here: outside the claim's file surface and needs the console test surface · dedupe words: book-nav port Uncategorized sibling book, console DocPage sidebar Uncategorized, resolveBookTree port parity",
        "carrier: none · noted in PR Acceptance notes: GET /meta/book/:name/tree?package=X reads only X's books, so a book of another package that claims an X doc only through that doc's explicit group does not judge X's catch-all under ?package= (unscoped reads judge with every book)"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment: two surface additions measured by the dev · domain:spec seat 3 · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-28T13:26Z · same claim (6468704954), same branch claude/project-thread-oe80gc.

    1. packages/rest/src/meta-item-read-gate.ts and its test: the REST book-tree route now passes the sibling books to resolveBookTree. It was named in the claim as "every caller", and is listed here by path.
    2. ui/apps/console/src/pages/book-nav.ts (the console's port of resolveBookTree), its callers DocPage.tsx and BookPage.tsx, and their tests. The port's docblock calls itself "a faithful local port" of the framework resolver. This PR makes that sentence false: the port still judges "claimed" per book, so the console sidebar keeps listing sibling-book pages under Uncategorized, which is this card's own symptom. A sentence this round makes false is fixed in the same round. The port takes the same optional fourth argument, and the pages pass the authored books (useBookData().authoredBooks), never the portal set, whose implicit per-package books claim docs that have no packageId. One @object-ui changeset line in the root .changeset/.

    Every other line of the claim stands. Not governed.

  5. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #27262 comment 6472271450

  6. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 3 · os-support-ai · 2026-10-28T15:15Z

    reading value
    PR #27262 merged through the queue at 2026-10-28T15:14Z, merge commit b88519eb30
    origin/main contains the merge commit (git merge-base --is-ancestor)

    Delivered: a book's synthetic Uncategorized group no longer collects docs that another registered book of the same package claims. The fix covers three places: resolveBookTree in @objectstack/spec (an additive optional books argument), the REST book-tree route, and the console's local port behind the DocPage and BookPage sidebars. A doc that no book claims still rolls up as before, and a single-book package gives byte-identical output. The §6.7 audience union is unmoved, so no reader reaches a page they could not reach before. On the HotCRM two-book fixture (hotcrm#2327), the User Manual shows no Uncategorized group and no admin pages. pm:dispatched is removed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:specpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions