Skip to content

[Decision] should the server-side app-nav filter also prune a doc menu entry by the docs audience, so a member who cannot read the doc never receives the entry — or is renderer-side pruning (objectui#10188) the whole answer? #19790

Description

@objectstack-fleet

Ruled: 5793362670 · letter A · 2026-09-23T10:42Z
Blocked-by: #19482

Filing gate: ② (a decision only the maintainer can make — a permission-boundary change). Reader: the director seat's decision batch; after the ruling, the domain:cli seat (packages/rest) if A. ⛔ Not a claim. Split from #19482 (PR #19789): this card carries the server-side half of the audience rule; #19482 keeps the schema, whose describe states the rule for the renderer.

维护者速读

事情:#19482 给应用菜单加了「文档」项(指向一本书或一篇文档),裁决的追加条款写的是:看不了这篇文档 / 这本书里一页都看不了的成员,菜单上就不出现这个条目。今天的实现里,文档内容在服务端是有门的(/meta/doc、/meta/book 读取按受众拦),但服务端下发菜单时(filterAppForUserWithReason)只按 requiredPermissions、服务是否存在、对象是否可用来剪,没有按文档受众剪。所以一个看不了文档的成员,拿到的 /meta/app 里仍有这个条目(能看到条目名、书名/文档名,点进去看不到内容);「不显示」全靠前端渲染器(objectui#10188,尚未上线)去剪。

选项:

  • A 服务端菜单过滤加一条「文档受众」判断:看不了的成员,下发的菜单里就没有这个条目(与 requiredPermissions 同一处、同一种做法)。另开一张执行卡,落 packages/rest。
  • B 保持现状:服务端只挡内容,条目剪不剪交给渲染器;接受「条目名会泄露给非读者」。

荐 A。请回一个字母:A / B。

os-decision-facets

  • ① 项目长远合理性:菜单过滤函数自己的注释写明「秘密属于服务端」,requiredPermissions 已经在服务端剪;文档条目走另一条路(只靠前端)会让「谁看得到菜单项」有两套答案,每个消费方(控制台、MCP、第三方前端)各自再实现一遍。A 一处兑现,B 让契约的一半只在一个渲染器里成立。⇒ A。
  • ② 实际业务拉动:来自维护者点名的需求([Decision] admins author Markdown docs in the console and put them on the app menu — runtime doc/book authoring surface and a doc navigation item (maintainer's stated need) #19482:管理员写文档并放进菜单);受众剪枝是裁决追加条款里明写的行为。泄露的只是条目名/书名/文档名,不是内容 ⇒ 真实但不紧急。
  • ③ 防 AI 犯错:describe 已声明「读不到的文档条目不渲染」;若只有前端兑现,一个 AI 读 /meta/app 或写另一个前端时会拿到它「不该看到」的条目 —— 声明而服务端不兑现,正是「声明即强制」要避免的形状。⇒ A。
  • ④ 创业阶段不扩散:A 是在一个既有过滤函数里加一个分支(复用已有的文档受众解析),不新增键、不新增门禁;B 零成本。⛔ 不因此翻字母:A 不扩能力面,只是让已声明的行为在服务端成立。

Governing text: #19482 ruling addendum 5754645447 (「a doc entry the member may not read is not rendered」; 「a member with no readable page in it does not see the entry at all」); ADR-0046 §6.7 (docs content gated server-side at the doc/book reads); the filterAppForUserWithReason docblock in packages/rest/src/rest-server.ts.

Prior rulings read: audience,navigation,doc,book,nav,filter → 37 hits; ADR-0057 D10, ADR-0021 D2, ADR-0021 D3, ADR-0029 D7, ADR-0030 Decision §2, ADR-0030 Decision §6 — each read, none rules on nav-entry pruning by doc audience (notification / dataset / ownership scopes); thread: this card is new; repo: objectstack-ai/objectstack

推荐:A。自检:只看①选 A;②③④ 是否翻转:否。回退 B。置信缺口:⛔ 未量 resolveDocAudiences 在菜单过滤时拿到调用者权限集的成本(每次 /meta/app 都要算);⛔ 未量 book 条目「一页都看不了」的剪法在服务端要遍历多少页。

Evidence (measured by the #19482 round and its at-tier contract review on PR #19789, head f9baf5cb3f)

  • filterAppForUserWithReason (packages/rest/src/rest-server.ts) prunes on requiredPermissions, requiresService, object servability and empty groups — no audience arm.
  • Content gating exists at /meta/doc, /meta/book, /meta/book/:name/tree (ADR-0046 §6.7).
  • The objectui renderer (objectui#10188) has not shipped, so today no doc item renders at all — the leak is latent until it does, and then only entry names leak.

Dedupe

REST list of the 435 most recently updated issues (open + closed) grepped locally: filterAppForUserWithReason 0 · nav … audience 1 (#19482, the parent) · lit control requiredPermissions 3 (#18159, #19186, #19503 — none this question).

Activity

  1. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #217 item 2 · letter A · maintainer 「217 同意」 2026-09-23T10:40Z

    Director seat, summon #28 (session_01GLdRPcbaCBQCTvVmU6YEUY). Presented in this seat's chat with recommendation A; the maintainer approved the batch as presented. B ⛔ (a rule the server declares but only one renderer honours; entry names leak to non-readers through /meta/app, MCP and any other front end).

    Governing text: #19482 ruling addendum 5754645447 (「a doc entry the member may not read is not rendered」; 「a member with no readable page in it does not see the entry at all」); ADR-0046 §6.7 (docs content gated server-side at the doc / book reads); the filterAppForUserWithReason docblock (packages/rest/src/rest-server.ts:3231) — secrets belong to the server. Readings this summon: the filter's arms are requiresService (:3263 / :3276) and requiredPermissions / servability; ⛔ no audience arm; PR #19789 (the doc variant) is Tier H awaiting the maintainer's merge. Prior rulings read: server-side,app-nav,filter,prune,menu,entry,docs,audience,member,read,receives,renderer-side (+20 more) → 308 hits; ADR-0076 D11, ADR-0029 D9.7, ADR-0056 D2, ADR-0076 D9, ADR-0127 D6, ADR-0005 Decision §5, ADR-0030 Decision §2, ADR-0035 Decision §4, ADR-0035 Decision §6, ADR-0036 Decision §1; thread: none — none rules on nav-entry pruning by doc audience.

    Ruled — A: the server-side nav filter prunes doc entries by the docs audience

    1. filterAppForUserWithReason gains an audience arm beside requiredPermissions: a doc entry naming a doc the caller may not read is pruned; a book entry whose readable subset for the caller is empty is pruned; the reason spelling follows the existing ones. It reuses the audience resolution the /meta/doc / /meta/book reads already apply — ⛔ no second resolver.
    2. The renderer's pruning (Studio: a Markdown editor for doc items (create / edit / preview, assign to a book) and rendering of the doc navigation item on the app menu — objectui half of objectstack#19482 objectui#10188) stays — the server is the authority, the renderer is defence in depth.
    3. Pins: a non-reader's /meta/app carries no such entry; a reader's does; a book entry with one readable page stays.
    4. First readings for the dev, as the card's gaps: the per-request cost of resolving audiences during /meta/app, and the traversal a book entry needs — report them; ⛔ no caching layer is invented in this card.

    Execution

    needs-user-decision → pm:blocked in this stroke, Blocked-by: objectstack-ai/objectstack#19482 on the body (the doc variant lands first; the unlock scan returns this card when #19482 closes). Lane by landing site: domain:spec → domain:cli (packages/rest), priority:p2, Clause-②: no (a permission-boundary tightening, no spec key).

  2. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblock re-derivation: pm:blocked → pm:queue · 2026-09-24T23:54Z

    Acting on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「帮我排查一下 blocked 卡片,哪些需要更新状态帮我更新。」; where — the maintainer's chat with session session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-24. ⛔ Not a claim and not a dispatch: the card returns to its lane's take order. Candidate surfaced by check-half-states.mjs H19 on this sweep.

    State: pm:blocked → pm:queue; domain:cli, priority:p2 unchanged; no assignee.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 9 (serial)
    Session: session_01TnPAC1UsTGfHPXVUCL6iLn
    Account: os-steve
    Branch: claude/issue-19790-nav-doc-audience-prune
    Worktree: objectstack-issue-19790
    Domain: domain:cli
    Seat: domain:cli#1
    File surface: packages/rest/src/rest-server.ts (filterAppForUserWithReason / filterAppForUser, the gate their two callers hand them, and the audience-resolution plumbing those callers need), its tests under packages/rest/src/, any hand-written content/docs/** line the change makes false, and one .changeset/19790-*.md. packages/spec/** (the DocNavItemSchema describe included) and every other package are read-only. packages/rest/CHANGELOG.md and package.json belong to the Version Packages PR. Stop on breach and explain in the report
    Container & model: M, mode:subagent, model: default judgment tier (this act's node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/rest/src/rest-server.ts answered: no path-derived mandate). A maintainer-ruled permission-boundary arm in the server's nav filter. It must reuse the docs audience resolution, with no second resolver, fail closed, and report its per-request cost, so it gets the default judgment tier.
    Clause-②: no
    Thread-read: 5824249598
    Serial constraints cleared: the maintainer's order in this seat's session (「继续派发」) and serial dispatch: R8 (#19922) reached its end state in this seat's record 5827511003 (released with a reason, pm:blocked on #20112), so no dev is in flight in this lane. The file lists of all 10 open PRs were read in this act. None touches packages/rest/**; the Version Packages PR #17076 owns the CHANGELOG and package.json versions, outside this surface. The seat post's queue head #19328 stays fenced behind the open draft PR #19637. The NORTH-STAR 优先级 3 pre-pick read at fa00ebf4: the domain:cli pm:queue holds no open P0/P1

    Taken 2026-09-25T05:52Z, origin/main fa00ebf4. Clause-②: no, per the ruling's execution line (5793362670): a permission-boundary tightening with no spec key. The server comes to honour the audience rule DocNavItemSchema already declares, so no accepted input moves and no published surface grows. The dev posts no second claim; this comment is its identity.

  4. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 19790,
    "status": "done",
    "branch": "claude/issue-19790-nav-doc-audience-prune",
    "pr": "#20128",
    "session": "session_01TnPAC1UsTGfHPXVUCL6iLn — subagent run inside the PM seat session (the parent's harness-stamped id); identity = the branch",
    "premise_still_valid": true,
    "summary": "filterAppForUserWithReason gains the ruled docs-audience arm: a type:'doc' entry is pruned when its doc is unreadable, when its book's own audience denies the caller, or (book alone) when none of the book's CLAIMED pages is readable; both callers (list + by-name) build one NavDocAudienceGate per request and hand it in, and the arm fails CLOSED (no gate, or a thrown book/doc read, drops every doc entry + warn). All verdicts come from one new private resolveDocsAudience (spec helpers audienceAllows / resolveDocAudiences / docAudienceAllows / resolveBookTree / resolveBookClaimedDocs / deriveImplicitPackageBook), which the five docs reads (/meta/book, /meta/book/:name, /meta/doc, /meta/doc/:name, /meta/book/:name/tree) now also use — no second resolver; their responses are byte-identical (1584-request BASE-vs-HEAD probe, 0 diffs). Cost: zero added reads when no app carries a doc entry; otherwise once per request (1 book read, 1 holdings resolution only if a set-gated book exists, 1 doc read only off the fast path or for a book-alone entry), per book-alone entry one resolveBookClaimedDocs traversal; no cache. Anonymous GET /meta/app is 401 before any read (pinned), so the arm has no anonymous path to cover.",
    "tests": "New packages/rest/src/meta-app-nav-doc-audience.test.ts, 19 cases driving the real list / by-name / tree routes: non-reader has no gated doc/book/all-gated-book entry and the wire carries none of the names or labels; reader has them; a book with exactly one readable page stays (and the tree read agrees: [crm_intro]); an org book whose every CLAIMED page is gated is pruned although its tree serves an Uncategorized orphan; book+doc pruned when the book is gated; group/area collapse; list/by-name parity for non-holder and holder; requiredPermissions control; four fail-closed rows (book read throws, doc read throws, holdings unresolvable, no gate); fast path + existence (unwritten doc served, unknown book pruned, no holdings resolved); cost (no doc entry = 0 book/doc reads; two apps = 1 book read, 1 doc read, 1 holdings call); anonymous 401. Runs: file 19/19 green; @objectstack/rest local project Test Files 195 passed, Tests 3284 passed | 1 skipped; typecheck exit 0 (test layer 0 errors). ABLATION (fix committed first, cb800e2): scripts/ablation-replace.mjs --delete of the arm line, wrap mode, anchor x1 -> x0, blob 5c7dbf8f1015 -> f0b3c3cdd872 on disk; result Tests 12 failed | 7 passed — red: every non-reader row, the four fail-closed rows, non-holder parity, fast-path existence, two-app cost; green: holder, one-readable-page, requiredPermissions control, no-book-entry, no-doc-entry cost, anonymous (the expected direction). Restore proven: blob after restore == HEAD blob 5c7dbf8f1015, git diff HEAD empty; re-run 19/19 green. Subject is src-imported (./rest-server.js via vitest, no dist in the resolution path), so no rebuild/dist preflight applies. BYTE-IDENTITY of the docs reads: one-off probe (deleted, not committed) of BASE fa00ebf rest-server.ts vs this branch over 4 callers x 3 book sets x 3 failure modes x book/doc list+item+tree routes (both spellings, ?package=, ?include=content): compared=1584 diffs=0, statuses 200:752 401:168 403:28 404:240 500:396, 55 distinct responses, control non-holder 403 vs holder 200. Gates: 90 derived at f2bc219, all exit 0, --ran reconciliation a derived zero NOT-MEASURED; pnpm lint full union exit 0 at f2bc219.",
    "mcp_calls": "0 — no MCP tool of any kind was called",
    "api_writes": "3 — each through the fleet-write relay (repository_dispatch, executed as objectstack-fleet[bot]): (1) pr_create POST /repos/objectstack-ai/objectstack/pulls, draft forced, #20128, relay run 36104733901 success; (2) label-write POST /repos//issues/20128/assignees {os-steve}, relay run 36104793574 success, read-back MATCHES (assignee os-steve; labels documentation/size/l/tests/tooling are the path labeler's, untouched); (3) post-stamped POST /repos//issues/19790/comments (this report). Not REST: git push x4 of the branch. Everything else was reads.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · the docs READS fail OPEN when the book list read rejects: fetchAudienceBooks maps the fault to [] (read as "no set-gated book anywhere", so the fast path serves everything to an authenticated caller), and /meta/doc/:name maps a rejected corpus read to [] (the gated doc then reads as unclaimed, so org). Failing probe (one-off, stub protocol whose getMetaItems({type:book}) rejects, authenticated non-holder of crm_admin): GET /meta/doc/crm_admin_runbook 403 -> 200 WITH the body; GET /meta/doc lists it. Reachable with the shipped protocol: metadata-protocol getMetaItems rethrows every sys_metadata read failure except "unprovisioned" (#5532), so a store fault on the book read after the doc read succeeded serves the gated doc. Pre-existing at fa00ebf (byte probe: BASE and HEAD answer identically); this card kept the reads byte-identical by ruling, and the nav gate does NOT inherit it (readAudienceBooks / readDocCorpus report the fault; the reads still map it to []). Site: packages/rest/src/rest-server.ts fetchAudienceBooks + the /meta/doc/:name corpus read. · dedupe: fetchAudienceBooks fail open · docs audience book read failure · meta doc permissionSet leak · catch(() => []) audience gate",
    "class: b · GET /meta/:type/:name/layers (and the deprecated ?layers=true, same helper serveMetaItemLayered) serves the app document with NO filterAppForUserWithReason pass — every arm bypassed, requiredPermissions included, not only the new audience arm. Probe (one-off, authenticated member holding neither finance.access nor crm_admin): /meta/app/crm/layers 200 carrying both the audience-gated doc entry and the requiredPermissions entry in code/overlay/effective; list and by-name routes prune both. Contract: content/docs/ui/apps.mdx requiredPermissions row 「The entry is never served: it is absent from the /meta body」 and the by-name caller comment 「the by-name route must not serve a nav entry the list route prunes, or reading the single-app JSON defeats the filter」. Seam: spec:AppSchema.navigation[].requiredPermissions + DocNavItemSchema audience -> runtime:packages/rest/src/rest-server.ts serveMetaItemLayered · dedupe: meta app layers requiredPermissions · layered view nav filter bypass · filterAppForUser layers",
    "class: b · GET /meta/:type/:name/published serves the stored app document unfiltered (probe as above: 200 with both gated entries). Same contract text. Seam: spec:AppSchema.navigation[].requiredPermissions + DocNavItemSchema audience -> runtime:packages/rest/src/rest-server.ts GET /meta/:type/:name/published · dedupe: published app nav filter · meta published requiredPermissions leak",
    "class: b · GET /meta/:type/:name/history and /diff return stored app versions with no nav filter — CODE-READ only, not driven (no probe was run for these two). Same contract text. Seam: spec:AppSchema.navigation[].requiredPermissions -> runtime:packages/rest/src/rest-server.ts GET /meta/:type/:name/history, /diff · dedupe: meta history app nav · metadata diff requiredPermissions",
    "class: b · packages/runtime/src/domains/meta.ts handleMetadataRequest (HttpDispatcher /meta catch-all: list via protocol.getMetaItems, item via getMetaItem) applies no app-nav filter at all where that door answers — CODE-READ; which compositions route /meta/app to it rather than to RestServer is NOT MEASURED (the REST routes shadow it where RestServer is mounted). Seam: spec:AppSchema.navigation -> runtime:packages/runtime/src/domains/meta.ts handleMetadataRequest · dedupe: http-dispatcher meta app filter · runtime meta domain requiredPermissions",
    "NOT MEASURED · MCP list_metadata / describe_metadata: only the names live in this tree (packages/spec/src/system/constants/platform-tool-names.ts); the implementations are elsewhere, so whether they serve app nav past the filter cannot be read here · carrier: 承接者:无 · noted, not filed",
    "carrier: 承接者:无 · noted in the PR Acceptance notes, not filed — without ?package=, /meta/book/:name/tree resolves over the env-wide corpus, so every book tree lists every doc it does not claim under Uncategorized (the resolver's documented "nothing is ever dropped" rule); it is why the nav arm counts claimed pages, not tree entries"
    ],
    "gates": {
    "node scripts/check-adr-0087-registration.mjs --base origin/main": "exit 0 · ✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).",
    "node scripts/check-adr-0087-registration.mjs --self-test": "exit 0 · ✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)",
    "node scripts/check-changeset-no-major.mjs --base origin/main": "exit 0 · ✓ This diff introduces no major bump.",
    "node scripts/check-changeset-no-major.mjs --self-test": "exit 0 · ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #",
    "node scripts/check-ci-filter-parity.mjs": "exit 0 · OK: all 184 declared cross-package glob(s) (131 unique) are covered by core or crosspkg, every crosspkg entry still covers one, and the test job's if: still nam",
    "node scripts/check-closing-keyword-parity.mjs": "exit 0 · check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 9546 tracked file(s), ",
    "node scripts/check-closing-keyword-parity.mjs --self-test": "exit 0 · ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.",
    "node scripts/check-comment-mask-adoption.mjs": "exit 0 · OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/, all 14 recorded and every recorded row still reached (13 unconverted, 1 s",
    "node scripts/check-comment-mask-adoption.mjs --self-test": "exit 0 · PASS check-comment-mask-adoption --self-test (0 failure(s))",
    "node scripts/check-comment-mask-corpus.mjs": "exit 0 · ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 7159 files, 0 disagree, 0 unparseable, 78.6s (comparator self-test: 26 cases pass).",
    "node scripts/check-doc-frontmatter.mjs": "exit 0 · ✓ check-doc-frontmatter: 2 content root(s) verified, each against its own floor — content/docs 406, content/blog 3.",
    "node scripts/check-doc-frontmatter.mjs --self-test": "exit 0 · ✓ check-doc-frontmatter --self-test: 99 assertions — the card's own description observed failing with the parser's message and the FILE line, every other violation kind o",
    "node scripts/check-doc-route-spelling.mjs --advisory": "exit 0 · ✓ route-spelling guard (advisory): population clean — every shape-matched literal spells its ledger row.",
    "node scripts/check-doc-route-spelling.mjs --self-test": "exit 0 · ✓ check-doc-route-spelling self-test: extraction tidy-up, the variant relation (plural + pinned lexicon, no prefix heuristic), walk wiring (releases/ and node_modules/ ou",
    "node scripts/check-docs-section-name.mjs": "exit 0 · so it is carried by --self-test rather than by this corpus.",
    "node scripts/check-docs-section-name.mjs --self-test": "exit 0 · ✓ check-docs-section-name self-test: 85 cases pass (real temp trees on disk; both historical misses reproduced as RED, both arms driven RED, the duplicate-key and syntax-",
    "node scripts/check-empty-changeset.mjs --base origin/main": "exit 0 · ✓ No changeset from the merge base modified or deleted by this diff (#17712).",
    "node scripts/check-empty-changeset.mjs --self-test": "exit 0 · ✓ check-empty-changeset --self-test: 159 assertions over real temp git repos (real scan() path)",
    "node scripts/check-keyed-text-bounds.mjs": "exit 0 · ✓ check:keyed-text-bounds: 112 .object.ts files under packages/
    + apps/
    * + examples/** (walk is repo-wide; 0 outside), 117 object declarations, 250 declared index ent",
    "node scripts/check-keyed-text-bounds.mjs --self-test": "exit 0 · PASS check-keyed-text-bounds --self-test (0 failure(s))",
    "node scripts/check-platform-object-tenancy-census.mjs": "exit 0 · ✓ platform-object tenancy census matches the tree: 84 platform-namespace objects, 58 in the machinery's reach, 26 outside it, every exclusion explained by a declaration o",
    "node scripts/check-platform-object-tenancy-census.mjs --self-test": "exit 0 · ✓ check-platform-object-tenancy-census self-test: all checks pass (84 objects, 26 outside the machinery)",
    "node scripts/check-plugin-teardown-shape.mjs": "exit 0 · ✓ check:plugin-teardown-shape: 69 Plugin implementation(s) across 6590 source(s) under packages/; every teardown-shaped method (stop / shutdown / close / dispose) sits ",
    "node scripts/check-plugin-teardown-shape.mjs --self-test": "exit 0 · ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay green, every roster name",
    "node scripts/check-registry-log-declared.mjs": "exit 0 · OK: 74 vitest-running package(s) walked, 9 selected as engine-booting, every one declares a recognised registry log level (debug/info/warn/error/silent).",
    "node scripts/check-registry-log-declared.mjs --self-test": "exit 0 · self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor.",
    "node scripts/check-rest-log-spy-declared.mjs": "exit 0 · OK: 30 of 197 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.",
    "node scripts/check-rest-log-spy-declared.mjs --self-test": "exit 0 · check-rest-log-spy-declared self-test reached its verdict: 23 case(s), 0 failure(s).",
    "node scripts/check-section-landing-index.mjs": "exit 0 · ✓ check-section-landing-index: 8 section index block(s) enumerate their meta.json pages, in order, both directions (ai, api, automation, data-modeling, kernel, permission",
    "node scripts/check-section-landing-index.mjs --self-test": "exit 0 · ✓ check-section-landing-index --self-test: 31 assertions over synthetic inputs and a temp fixture (real judge()/run() path); every limb -- both shapes in sync, missing pa",
    "node scripts/check-system-context-census.mjs": "exit 0 · check-system-context-census: OK — 112 elevation read sites in 20 packages across 45 files, living in 93 symbol(s); the page cites 106 symbol(s) against 106 required, over",
    "node scripts/check-system-context-census.mjs --self-test": "exit 0 · check-system-context-census --self-test: all cases passed",
    "node scripts/check-undeclared-dep-imports.mjs": "exit 0 · ✓ check:undeclared-dep-imports: 81 workspace packages under packages/
    + apps/** + examples/, 2592 non-test src files, 2107 @objectstack/* specifiers (0 assembled, not",
    "node scripts/check-undeclared-dep-imports.mjs --self-test": "exit 0 · PASS check-undeclared-dep-imports --self-test (0 failure(s))",
    "node scripts/docs-audit/check-affected-docs.mjs": "exit 0 · ✓ affected-docs self-test: 605 cases pass.",
    "node scripts/docs-audit/check-drift-comment.mjs": "exit 0 · ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).",
    "node scripts/pm/release-rehearsal-clone.mjs --self-test": "exit 0 · ✓ self-test passed",
    "pnpm --filter @objectstack/lint run check:doc-formula-expressions": "exit 0 · ✓ check:doc-formula-expressions (field-level *When, #11407): 14 predicate(s) on a statically determinable field layer judged clean; 6 skipped as undeterminable.",
    "pnpm --filter @objectstack/lint run check:doc-security-posture": "exit 0 · ✅ 27 ObjectSchema.create example(s) in 227 marked block(s) across 248 prose file(s) in 2 root(s) carry an os validate-clean security posture",
    "pnpm --filter @objectstack/spec run check:docs": "exit 0 · ✅ 226 generated files in sync with packages/spec",
    "pnpm --filter @objectstack/spec run check:duration-unit-keys": "exit 0 · ✓ check:duration-unit-keys — 204 unit-declaring numeric key(s) across 2589 source file(s) all carry their unit in the key name (or in a sibling unit, or under a declare",
    "pnpm --filter @objectstack/spec run check:empty-state": "exit 0 · ✓ all classified (2 closed, 2 open, 4 output, 9 scope)",
    "pnpm --filter @objectstack/spec run check:liveness": "exit 0 · ✓ packages/spec/liveness/state-counts.md is current — the same 40 row(s), no count column left in the README.",
    "pnpm --filter @objectstack/spec run check:skill-examples": "exit 0 · ✅ 259 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them",
    "pnpm --filter @objectstack/spec run check:strictness-ledger": "exit 0 · ✓ docs/audits/2026-07-unknown-key-strictness-ledger.counts.md is current — 453 site(s) measured, 1 authorable strip site(s) left.",
    "pnpm --filter @objectstack/spec run check:variant-docs": "exit 0 · ✓ variant/doc gate: 18 discriminated union(s) — 8 governed (every variant mentioned in a bound doc), 10 exempt.",
    "pnpm --filter @objectstack/spec run check:yaml-examples": "exit 0 · ✅ 18 tagged YAML example(s) across 1 file(s) validate against their declared live spec schemas",
    "pnpm check:authz-resolver": "exit 0 · ✓ check:authz-resolver: single shared authorization resolver intact; both entry points delegate.",
    "pnpm check:changeset-gate-self-tests": "exit 0 · ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #",
    "pnpm check:corpus-claim-drift": "exit 0 · check-corpus-claim-drift: OK, no new claim sites beside a pinned spelling.",
    "pnpm check:cross-package-test-inputs": "exit 0 · OK: 29 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split "test:repo" task); 13 walked root(s) judged, 3 on",
    "pnpm check:dispatcher-error-vocabulary": "exit 0 · [#15723] the ARGUMENT POSITION of new APIError( … ) and APIError.from( … ) IS now in this gate's population — the apierrorarg shape. APIError.from copies the record's",
    "pnpm check:doc-anchors": "exit 0 · ✅ check-doc-anchors: 376 internal #fragment link(s) across 411 source file(s) all resolve to a real heading",
    "pnpm check:doc-authoring": "exit 0 · ✓ doc authoring guard: sibling-package prose ids hold the baseline — 815 pinned site(s) across 231 file(s), 91983 string(s) read in 1253 parsed source(s), no growth, no b",
    "pnpm check:docs-audit-scope": "exit 0 · ✓ scope injection is live: the workflow audits the list handed in as args.handwritten, and refuses an invocation that hands in no scope at all.",
    "pnpm check:docs-redirects": "exit 0 · check-docs-redirects: OK (apps/docs/redirects.mjs: 98 entries -- 95 page destination(s) resolved against content/docs, 3 wildcard destination(s) resolved to a directory, ",
    "pnpm check:docs-single-h1": "exit 0 · ✓ check-docs-single-h1: 406 page(s) under content/docs/ carry no body-level # heading (0 subtree(s) excluded, see --list).",
    "pnpm check:docs-spec-enumerations": "exit 0 · OK the hand-written spec enumerations agree with packages/spec/package.json -- 17 subpath(s) held ORDERED in content/docs/deployment/troubleshooting.mdx; 15 protocol name",
    "pnpm check:docs-transcript-drift": "exit 0 · ✓ check-docs-transcript-drift: 4 declared transcript value(s) across 406 page(s) under content/docs/ equal what the registry derives today, and no undeclared block quotes",
    "pnpm check:driver-memory-census": "exit 0 · check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states "#6664 census: 2 ruled consumers". This gate polic",
    "pnpm check:dts-closure": "exit 0 · check-dts-closure: 73 built package(s) swept - 167/167 declared declaration file(s) present across 73 package(s); 0 built package(s) declare no declaration entry point an",
    "pnpm check:dual-build-cjs-loads": "exit 0 · ✓ check:dual-build-cjs-loads — 105 published require entry point(s) across 67 package(s) load; 660 emitted CommonJS file(s) parse; 1 cross-format behaviour probe(s) agree",
    "pnpm check:engine-double-contract": "exit 0 · check-engine-double-contract: OK — 902 pinned, 133 in the DEBT ledger, 3 exempt.",
    "pnpm check:gitlink-declared": "exit 0 · check-gitlink-declared: OK (9546 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare).",
    "pnpm check:issue-citations": "exit 0 · ✅ check-issue-citations --self-test: grammar narrowed, four 404 causes kept apart, both board strategies agree, diff scope red AND green, scope contract pinned (73 cases,",
    "pnpm check:lean-entry-closure": "exit 0 · ✓ check-lean-entry-closure: 2 published condition(s) measured from a real load.",
    "pnpm check:logger-receiver-detach": "exit 0 · OK every log channel keeps its receiver: 2860 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s).",
    "pnpm check:nul-bytes": "exit 0 · check-nul-bytes: OK (scanned 9539 text file(s) -- 9539 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).",
    "pnpm check:objectql-double-limit": "exit 0 · OK ObjectQL double limit conformance holds: 410 double(s) graded, 212 apply the caller's bound or refuse it loudly.",
    "pnpm check:objectui-changeset": "exit 0 · ✓ objectui-range --self-test: all checks passed",
    "pnpm check:org-identifier": "exit 0 · check-org-identifier: OK (2882 author-facing source file(s), 17 session binding(s) resolved, no removed session.tenantId alias).",
    "pnpm check:page-declaration-shape": "exit 0 · check-page-declaration-shape: OK — 34 page entries across 2872 sources under packages/
    , examples/, apps/ all reach the kernel through a discoverable declaration (:", "pnpm check:pm-changeset-deadline-census": "exit 0 · ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, the verdict truth ta", "pnpm check:published-files": "exit 0 · ✓ check:published-files — 70 publishable package(s) of 81 workspace member(s) declare a fileswhitelist that covers every entry point plus CHANGELOG.md and admits no te", "pnpm check:published-readme-links": "exit 0 · ✓ check:published-readme-links — 178 outbound link(s) across 61 published markdown file(s): 0 root-relative, 0 non-canonical origin(s), 27 docs-site page(s) resolved (0 v", "pnpm check:query-options-erasure": "exit 0 · ✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new, and every file measured parsed. Every other non-test file under packages/ is c", "pnpm check:react-page-adapter-contract": "exit 0 · ✓ check-react-page-adapter-contract: 21 app-showcase page module(s) + 1 content/docs react-page sample(s) (from 392 doc file(s), 1995 fenced block(s)) — every adapter que", "pnpm check:refd-timer-probe": "exit 0 · OK check-refd-timer-probe: 7154 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and nowhere else.", "pnpm check:role-word": "exit 0 · check-role-word: OK, no new occurrences of the reserved word.", "pnpm check:route-envelope": "exit 0 · ✓ Express-style response modules — 4 module(s) discovered and audited (walked, not enumerated — #9937), 12 hand-built body/bodies (count reported, NOT pinned): 2 conforma", "pnpm check:skill-identifier-liveness": "exit 0 · check-skill-identifier-liveness OK — Leg 1: 457 citation(s) over 53 published file(s) checked against 107876 implementation word tokens (0 ledgered exemption(s)); Leg 2: ", "pnpm check:slot-lookup": "exit 0 · ✓ slot-lookup ratchet holds: 106 unswept site(s) in 25 file(s), none new, and every file in the population parsed. Every other file under packages/ is covered bypnpm li",
    "pnpm check:sourcemap-no-sources-content": "exit 0 · check-sourcemap-no-sources-content: 69 built package(s) swept - 508 map(s), none embed source text.",
    "pnpm check:test-source-alias": "exit 0 · check-test-source-alias OK — 74 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/; 50 published subpath(s) resolved through ev",
    "pnpm check:tier-file-adoption": "exit 0 · OK: 81 workspace package(s) walked, 70 nightly-tier test file(s) on disk (70 e2e, 0 live), owned by 1 package(s); every one reads OS_TEST_TIERS.",
    "pnpm check:type-check-coverage": "exit 0 · check-type-check-coverage: OK — 77/81 workspace packages type-checked (plus the root), 4 in the DEBT ledger (53 frozen raw errors, https://github.com/objectstack-ai/objec",
    "pnpm check:type-check-debt": "exit 0 · check-type-check-coverage --re-measure: OK — 4 ledger entr(ies) re-measured in 21.3s, 53 raw tsc error(s) total, none above its recorded number.",
    "pnpm check:vendor-version-stamps": "exit 0 · check:vendor-version-stamps: OK — 7543 file(s) scanned.",
    "pnpm check:watch-hint-literal": "exit 0 · ✓ check-watch-hint-literal: 71 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 47, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 8 -",
    "pnpm check:where-matcher": "exit 0 · ✓ where-matcher conformance holds: 433 matcher(s) discovered, 433 answer the combinator battery correctly or refuse it loudly (282 refuse).",
    "pnpm lint (full union: eslint . --no-inline-config) @ f2bc219": "exit 0 · no problems printed, 107s — the whole union, not a narrowing",
    "pnpm --filter @objectstack/rest run typecheck @ cb800e2": "exit 0 · check:test-typecheck: OK — 0 file(s) / 0 error(s) / 0 pinned signature(s) (tsc --noEmit green first)",
    "pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 @ cb800e2": "exit 0 · Test Files 195 passed (195) · Tests 3284 passed | 1 skipped (3285)",
    "pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 src/meta-app-nav-doc-audience.test.ts @ cb800e2 (post-ablation restore)": "exit 0 · Tests 19 passed (19)",
    "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran-exit.txt @ f2bc219": "exit 0 · ✓ dispatch-gates --ran: 90 derived famil(ies) accounted for — 90 run, 0 NOT-MEASURED (a DERIVED zero — all 90 recorded an exit code and none of them is 3)"
    },
    "line_budget": "n/a",
    "deviations": [
    "PM mechanism assumption 3 refined by measurement: a book entry's "readable subset" is its readable CLAIMED pages (resolveBookClaimedDocs, the membership resolveDocAudiences uses), not the entries of its tree — resolveBookTree appends every unclaimed doc as a synthetic Uncategorized group, so counting tree entries keeps nearly every book whenever any doc is readable (pinned: the ops tree serves crm_intro under Uncategorized while nav_ops_book is pruned). book+doc: pruned when either the book's own audience denies (tree read 401/403, rest-server.ts book-tree admitsBook check) or the doc is unreadable; a readable doc in an unreadable book is possible via the docAudienceAllows union (book.zod.ts) and is still pruned. Existence: a doc absent from the corpus is served (the resolver's own org default for a doc it has no entry for); a book naming nothing resolves to an implicit book with no page and is pruned.",
    "Conflict in the order, stated not silently chosen: SUGGESTED ROUTE says the gate "degrades exactly as the reads do", and also "must not fail open". Measured: the reads fail OPEN on a rejected book read (probe, finding 1). The gate follows "must not fail open": a thrown book or doc read drops every doc entry of that response and logs a warn; the reads keep their answers.",
    "Reuse went one step wider than the doc-list + book-tree pair: the /meta/book list and /meta/book/:name gates also moved onto resolveDocsAudience.admitsBook, so all five docs reads and the nav arm share one resolution; byte identity proven by the 1584-request probe and the existing read suites.",
    "Test file is meta-app-nav-doc-audience.test.ts (sibling naming, beside meta-app-nav-servability-gate.test.ts) rather than the gates-from-pm expected path nav-doc-audience.test.ts; re-derivation on the real diff yields the same 90 commands.",
    "Two gates first answered PREREQUISITE NOT MET (exit 3, nothing measured): check:skill-examples (client-react had no .d.ts) and check:dual-build-cjs-loads (workspace unbuilt). Re-run exit 0 after building what they read (client + client-react; then turbo build 73/73 tasks, 51 cached). Three dist-reading gates that had run before the full build (dts-closure, lean-entry-closure, published-files) were re-run after it, exit 0.",
    "Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer; AGENTS.md and the order win — commits carry the model-free pair, the PR body ends with the session-URL footer.",
    "Commit 3b67a44 has a wip(rest) subject (the WIP push AGENTS.md asks for before a minutes-long build); squash landing folds it.",
    "Anonymous: measured, GET /meta/app answers 401 before any read, so the arm deliberately has no anonymous branch (order asked to measure and say).",
    "Other doors (order point 5) are listed as findings rather than fixed, per the order; three of them driven by a one-off probe, two code-read, MCP NOT MEASURED."
    ],
    "files_changed": [
    "packages/rest/src/rest-server.ts — DocsAudience + NavDocAudienceGate types; resolveDocsAudience; readMetaList / readAudienceBooks / readDocCorpus / audienceBooksOf / docCorpusOf (fetchAudienceBooks keeps its [] degradation); resolveNavDocAudience + docNavEntries + navTarget; the arm in filterNav; a 5th gate parameter on filterAppForUser / filterAppForUserWithReason, passed by both callers; the five docs read gates rebuilt on the shared helper",
    "packages/rest/src/meta-app-nav-doc-audience.test.ts — new, 19 cases",
    "content/docs/ui/apps.mdx — Audience bullet: the server prunes the entry itself; book+doc and claimed-pages wording",
    ".changeset/19790-nav-doc-audience-prune.md — @objectstack/rest patch",
    "diff vs merge base fa00ebf: 4 files, +853 / -95 (dispatch-gates reading)"
    ],
    "ci": "in_progress at head f2bc219 — one snapshot read after pr_create: 32 check runs, 12 success, 2 skipped, 18 in_progress, 0 failing. Not waited on (os-dev: CI convergence is the PM's).",
    "cleanup": "worktree ../objectstack-issue-19790 removed after node_modules removal (git worktree remove, no --force, exit 0; the tree was clean and at origin f2bc219). One-off probe files were deleted before each commit and never committed. No server, watcher or background job was started."
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    In-seat review: ACCEPT, PR #20128 at head f2bc219be0

    domain:cli execution PM seat #6024 · session session_01TnPAC1UsTGfHPXVUCL6iLn · 2026-09-25T07:00Z · reviewed on GitHub and origin/main fa00ebf4, against the ruling 5793362670

    Review checklist

    • PR form: draft, base main, assignee os-steve. The first line is Fixes #19790, the only closing binding, and the second is the claim's Clause-②: no. One footer, no model identifier.
    • Scope: 4 files (+853/−95), all inside the claim's surface: rest-server.ts, one new test file, content/docs/ui/apps.mdx and one changeset. packages/spec/** is untouched.
    • The change, read in the diff:
      • filterAppForUserWithReason gains the ruled arm beside requiredPermissions as a bare entry-level continue: if (e.type === 'doc' && (!docAudienceGate || !docAudienceGate(e))) continue;. It fails closed: no gate, no doc entry.
      • Both callers, list :6347 and by-name :7367, build ONE gate per request with resolveNavDocAudience and pass it in. This is the Nav gating cannot express "prune when the destination cannot serve" — enable.apiEnabled is never consulted by filterAppForUser #7912 parity.
      • The verdicts come from one private resolveDocsAudience, spelled only in the spec's helpers (audienceAllows, resolveDocAudiences, docAudienceAllows, resolveBookTree, resolveBookClaimedDocs, deriveImplicitPackageBook). The five docs reads now answer from it too, so there is no second resolver (ruling point 1).
      • The seat checked the one semantic seam: the new docReader passes an absent audience to docAudienceAllows, which itself defaults to org (book.zod.ts). That is the doc list's former explicit org fallback.
    • Entry semantics, falsified and refined by the dev:
      • A book's readable subset is its readable CLAIMED pages (resolveBookClaimedDocs), not tree entries. resolveBookTree appends every unclaimed doc as a synthetic Uncategorized group, so counting tree entries would keep nearly every book.
      • book + doc is pruned when either the book's own audience or the doc denies.
      • Existence: an unknown book resolves to an empty implicit book and is pruned; a doc absent from the corpus takes the resolver's own org default.
    • Cost (ruling point 4, measured, no cache):
      • no doc entry in the response: 0 added reads;
      • otherwise, once per request: 1 book read, 1 holdings resolution only when a set-gated book exists, and 1 doc read off the fast path or for a book-alone entry;
      • one claimed-pages traversal per book-alone entry.
    • Anonymous: GET /meta/app answers 401 before any read (pinned), so the arm needs no anonymous branch.
    • Tests:
      • meta-app-nav-doc-audience.test.ts has 19 cases on the real list, by-name and tree routes: non-reader, reader, one readable page, claimed versus Uncategorized, book+doc, group and area collapse, list/by-name parity, the requiredPermissions control, four fail-closed rows, the fast path, cost and 401.
      • Ablation of the arm: 12 red / 7 green in the expected direction; restored, 19/19.
      • The package suite passes (195 files, 3284 tests).
      • Byte-identity of the five docs reads, BASE versus HEAD: 1584 requests, 0 diffs.
    • Gates: 90/90 --ran at f2bc219b (0 NOT-MEASURED), plus pnpm lint (the full union).
    • Changeset: @objectstack/rest patch. The server honours what DocNavItemSchema already declares, per the ruling's Clause-②: no. The prose matches the code sentence by sentence, including fail-closed and cost.
    • Docs: the apps.mdx Audience bullet now says the server prunes the entry itself, and adds the book+doc and claimed-pages rules.
    • Governed / size: check-governed-merges --pr 20128: NOT governed, +853/−95.
    • Clause-② gate: neither limb hits. There is no packages/spec/src/** in the diff, and the claim declares no.

    A PM order conflict the dev resolved correctly, recorded in public: the order said the gate "degrades exactly as the reads do" and also "must not fail open". The dev measured that the reads fail OPEN on a thrown book read, and followed "must not fail open". That was the seat's inconsistency, and the dev's choice is the ruling's intent.

    Out-of-scope findings, one row each

  6. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    LANDED — bc80e16260597d52b062b7e1ee810c7b9a99aed2 (PR #20128), verified by CONTENT on origin/main

    domain:cli execution PM seat #6024 · session session_01TnPAC1UsTGfHPXVUCL6iLn · 2026-09-25T07:24Z

    probe before (6780e34a) origin/main
    the arm if (e.type === 'doc' && (!docAudienceGate || !docAudienceGate(e))) continue; in packages/rest/src/rest-server.ts 0 1
    private async resolveDocsAudience( (the one resolution the reads and the arm share) 0 1
    private async resolveNavDocAudience( (the per-request gate both callers build) 0 1
    untouched control: const servabilityGate = await this.resolveNavServability(p, environmentId) ?? undefined; 2 2
    meta-app-nav-doc-audience.test.ts + .changeset/19790-nav-doc-audience-prune.md in the tree (git ls-tree origin/main) 0 2
    content/docs/ui/apps.mdx: "The server applies this rule to the entry itself" 0 1
  7. added a commit that references this issue on Sep 28, 2026
    bc80e16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions