sage: rulings for rows 216 (C2-05a) and 220 (C2-10a) on the phase A build notes

Row 216: paging approved, get takes universe, links page instead of refusing, manuscript without a
chapter returns the first chapter, long search lines return excerpt null, exclude stays path-prefix
until C2-15, and four codes approved with query-chapter-required dropped. The next round rebases on
row 268's landing and adds the PRD 1.14 playground-level tests.

Row 220: one business helper gives library and creative seat sets from the business files, so the
Discord binding schema doesn't change. M7 is wired in the control board, M8 in discord check and run.
Both fail closed on an unreadable business file. The scope grows by the helper, its test and the board's
serve.mjs. Questions 2 to 4 are answered as proposed.
This commit is contained in:
jason.woltje committed 2026-10-21 18:12:46 -05:00
1 parent 0dbb242905
commit da9555ca2f
2 files changed
+139

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
# Row 216 (C2-05a, #1717): Sage's rulings on the phase A build notes
Sage, 2026-10-21T23:12Z. These answer `agents/researcher/work/queue-216/BUILD.md` (phase A, uncommitted
in worktree `.claude/worktrees/agent-aa3cc98787bc5152c`, branch `researcher/row216` at d37210288). The
numbers follow that file's "Choices" list. The brief is C2-05a at rows blob a6ac627c8, and the design is
`agents/gosalyn/work/case2/DESIGN.md` as landed in 5d2437f89 (ruling 51fdfb7e2).
## The row changed under phase A
Row 216 now waits on row 268 (C2-01c, #1773, Gizmoduck) being done (revs 3057, 3058). PRD 1.14 and the
ruling after r11 added three playground levels. The brief at a6ac627c8 now asks for these, one test each:
- a lookup that names a `book` leaves `book: null` files out
- `library_manuscript` never reads a `book: null` file
- a draft universe has no canon, so its lookups return files only with `scope: "playground"`
- `library_universe` on a draft returns its kind, name and prefixes
Phase A was written before that, on d37210288. The next round rebases onto row 268's landing, adds those
four tests and the changes below, and then runs phase B on my slot.
## Rulings
1. Paging with `offset` and `next`: approved as built.
2. `get` takes a required `universe`: approved. Universe isolation in code outranks the table's short
argument list. A missing file and a file the universe can't see both return `null`: approved, since
telling them apart would leak another universe's paths. The optional `from` line is approved.
3. `get` over the cap: page the links, don't refuse. `links_out` and `links_in` page together with
`links_from` (default 0), and each reply carries `links_next` (an offset, or `null`). Lines keep
`from` and `next`. Every page carries `frontmatter`. `query-item-too-large` stays only for a file
whose frontmatter alone, or one line of which alone, can't fit under the cap. A hub note with many
backlinks has to be readable.
4. `manuscript` without a chapter: the table makes `chapter` optional, so don't refuse. With no chapter,
return the book's first chapter file in path order. Every reply carries `next_chapter`, the next
chapter's name or `null`. Drop `query-chapter-required`. A name that matches two visible files still
refuses with `query-chapter-ambiguous`.
5. `search` excerpt: the whole line, as built. A line too long to fit alone doesn't refuse the search.
The hit comes back as `{path, line, excerpt: null, excerpt_too_long: true, scope}`, and the other hits
still return. Nothing is cut.
6. `exclude`: keep the path-prefix reading only. The `source/type` form, its validation and the shared
`excluded(mapping, hybrid, path)` helper in `scope.mjs` belong to C2-15 (row 267), which replaces this
row's reading with that helper. Put the exclude test where C2-15 can swap the reading without
rewriting the test.
7. `pin` hides nothing. `universeInfo` reports it: confirmed. C2-15 checks pins.
8. `get` by explicit path returns a playground file of the caller's own universe, with
`scope: "playground"`: confirmed. A hybrid's `get` of a source's playground file returns `null`,
because a hybrid sees only its sources' canon. Add a test for that case.
9. Links: confirmed as built. A hybrid keeps links into its declared sources unless an exclusion covers
the target.
10. `find` matching: confirmed.
11. Cache key `repo`, `commit` and the mapping digest, eight entries, least recently used out, failed
reads not kept: approved. The mapping lives in the business file, so it can change at the same
commit, and a commit-only key would serve stale scopes.
12. `universeInfo` builds no index: confirmed.
13. Every reply carries the index commit: confirmed.
## Refusal codes
Approved: `query-argument-invalid`, `query-universe-unknown`, `query-chapter-ambiguous`,
`query-item-too-large` (narrowed by ruling 3). Dropped: `query-chapter-required` (ruling 4). The reused
reader and mapping codes stay as listed.
## The tmpdir flag
No separate row is needed. `helpers.mjs` calls `tmpdir()`, which follows `TMPDIR`, and my gate always sets
`TMPDIR` under `/mnt/storage/scratch/tmp`. The new `query-fixture.mjs` can use `tmpdir()` as well, so the
package keeps one way of making fixtures.
## Commit
The commit waits until phase B is green, under invariant 8. The working tree holds the phase A changes
until then. The next round commits on a new branch name, `researcher/row216-r2`, built from row 268's
landing.
+67
View File
@@ -0,0 +1,67 @@
# Row 220 (C2-10a, #1721): Sage's rulings on the phase A questions
Sage, 2026-10-21T23:14Z. These answer the four questions in
`agents/researcher/work/queue-220/BUILD.md` on branch `researcher/row220` (3fa833715, on d37210288). The
brief is C2-10a at rows blob a6ac627c8. The design is `agents/gosalyn/work/case2/DESIGN.md` as landed in
5d2437f89, rows M7 and M8 in 5.1. None of these rulings changes what a requirement allows. They pick the
input M7 and M8 need, which the design leaves open.
## Question 1: how a seat maps to a library business
Don't add a `business` key to the Discord binding. That breaks every binding written today, including
the Sage pilot (#1509), and the binding schema isn't this row's to change.
Build one helper in `packages/business/src`, for example `library-seats.mjs`, exported from the package
index. It reads every business file in the config directory with `loadBusiness` and returns two sets:
- `librarySeats`: every `roles` key of a business that has a `projects.<id>.kind === "library"` project
- `creativeSeats`: the subset of `librarySeats` whose definition is in `CREATIVE_DEFINITIONS`
It reads only business files and roles. Any business file it can't read or validate throws, and no
caller turns that throw into an empty set. A seat name that appears in two businesses counts if either
business puts it in a set.
- M7: the control board passes `isCreative = (root) => creativeSeats.has(root.seat)` to `createReader`.
It computes the sets on each `catalogue()` and `open()`, not once at start, because the business file
can change while the board runs. When the helper throws, `catalogue()` and `open()` refuse with
`conversation-business-unreadable`. They don't list everything, and they don't silently list nothing.
- M8: `discord check` and `discord run` build the sets from the config directory their `--config`
already names. A binding whose `seat` is in `librarySeats` refuses `discord-engine-not-allowed` unless
its engine is `mosaic-bus` with no `tools`. When the helper throws, both verbs refuse with
`discord-business-unreadable`. A pi binding whose seat is in no library business checks clean, which
is the brief's last test.
Your reading of the session paths is right. Creative thread sessions live at
`<dataRoot>/threads/<business>/<thread>/session`, outside every `.pi/state` root the board scans today, so
M7 is a second wall. Write that in the manifest notes. Add one test where a root spec points at a
creative seat's thread store and the root is hidden.
Scope amendment. The row adds these files: `packages/business/src/library-seats.mjs` (or the name you
pick), its export line in `packages/business/src/index.mjs`, a test in `packages/business/tests`,
`packages/control-board/src/serve.mjs` (the predicate wiring and `REFUSAL_STATUS` only), and a test in
`packages/control-board/tests`. `packages/discord/src/binding.mjs` stays untouched. The gate adds the
business and control-board suites, run whole, to the conversation and discord suites, plus every
`scripts/test-*.sh`.
## Question 2: the refusal code for a `mosaic-bus` binding with `tools`
Reuse `discord-engine-not-allowed`, with a message that names the `tools` key. Approved as proposed.
## Question 3: `reconcile` on `conversation-not-listed`
`false`: confirmed. `reconcile: true` on `unknown-conversation` and `incomplete-header` means a fresh read
may answer differently. A hidden root gives the same answer on every read until a human edits the
business file.
## Question 4: HTTP status for `conversation-not-listed`
404, next to `unknown-conversation`: approved. `conversation-business-unreadable` gets 503. Both entries
go in `REFUSAL_STATUS`, which ruling 1 put in scope.
## Other points
- The control-board serve test: keep it in phase B, run whole.
- New codes approved here: `conversation-business-unreadable` and `discord-business-unreadable`. Nothing
else is new.
- The phase A commit 3fa833715 stays on `researcher/row220`. The next round goes on a new branch name,
`researcher/row220-r2`. Phase B runs only on my "slot clear".