- Shipped
- August 28, 2026 at 1:00 AM UTC
- Author
- Kamo
- Commit
- 30f2bf9
The composer has always offered an author three chips under "Visible to" — Members, Team Members, Public. They were written to the row and indexed into Qdrant so the assistant would respect them, and then no human-facing read ever looked at them again. Every article endpoint was gated on VIEW_KB_ARTICLES and nothing else, so a member holding only that right was served the organization's entire corpus: team-only material, unfinished drafts, articles waiting on review, and the archive. KbReaderScope is the whole gate, and the order of its rules is the design: org + still active never waived, by any right maintainer **************** see everything — a reviewer filtered by audience could be assigned an article to approve and then handed a 404 for it author CREATE is separate from EDIT, so an author who is not a maintainer must still reach their own draft PUBLISHED draft/in-review/archived are editorial workflow audience team member -> all three tiers; anyone else -> public and members. Cumulative, not exact-tier: an article opened to the public is not thereby hidden from staff aiEnabled is deliberately not consulted — it means "train the assistant on this", and requiring it would make the "Visible to" label false. Three chips off is left to mean what it says; the composer warns the author instead. Applied to /articles, /articles/tree, /articles/overview, /articles/{guid}, /versions, /media, /search and both pin surfaces. Notes: - The tree reparents rather than truncates. A team-only overview page with published children is an ordinary shape, and dropping the subtree with its parent would hide articles the author deliberately opened up. Kept pure and static so the rule is testable without a database; also guards a parent cycle, which the old walk would have recursed into. - Lists filter before paging. Filtering a page the database already cut leaves the caller reading the total as the corpus size. - overview counts come from the filtered list, and it now carries the reader's scope so the console's rail is drawn from the same answer. - A hidden article and a missing one both answer 404. A 403 on a guid is an oracle for exactly what the flags hide, and getByGuid's throw is caught so the missing case cannot escape as a 500 and become that oracle instead. - Pinning is gated like the read it keeps; unpinning deliberately is not, since a member most wants to clear a pin whose article was narrowed away from them. Also closes two cross-org write holes found in the same controller: /reorder had no organization check at all — EDIT_KB_ARTICLES in any org could reorder and, through parentId, reparent another org's article into its own tree — and /restore/{versionNumber} had none either. Every other write here checks it. 68 tests green, 25 of them new.