Files
jason.woltje a1b72432cd launchpad: row 217 C2-06 work in progress, UNTESTED, handed to Beakley (mos-1500)
Guard, limits, M11 fence, broker guard/page store/check event, BusError
reason, runtime wiring and the CLI replies verb. No runner work, no tests,
nothing run. BUILD.md lists the rulings, the row 225 contract and what is left.
2026-10-24 05:39:16 -05:00

8.2 KiB

Row 217 (Case 2 C2-06): build notes

Brief: docs/plans/2026-10-20_case2-rows.md, section C2-06, at base 45dca69f6. Builder: Launchpad until 2026-10-23 19:09Z. Then Mos moved coding to Codex seats (mos-1500, Q245) and the row went to Beakley on beakley/row217.

State at handoff

All code in this commit is UNTESTED. No node, no test and no mutant has run on it. No test file is written. packages/harness/src/runner.mjs is not started.

Done, untested:

  • packages/library/src/limits.mjs (new, pure): REPLY_LIMITS, resolveReplyLimits(read) gives {requireCitation, citationsPerReply, fallback, unreadable}, checkReplyLimits(value) gives a clone or null. An unreadable business file forces requireCitation true. A key falls back to its default when replyLimits is not an object or the key holds an invalid value.
  • packages/library/src/guard.mjs (new): GUARD_CODES in design 5.2's table order, FIXED_TEXTS, WEB_CITE_DEFINITIONS, REASON_MAX (300), GuardError, parseReply, checkReply, libraryReader, lastCommits, guardReply.
  • packages/library/src/index.mjs: exports for both.
  • packages/business/src/business.mjs: replyLimits is a known top key. The validated output doesn't carry it.
  • packages/business/src/organization.mjs: the M11 fence replyLimitsFence(old, document, actor) compares against old.human, refuses the tester and any <definition>.<instance> of old.roles, and is called for both document and written. setReplyLimits and readRaw are exported.
  • packages/bus/src/broker.mjs: the guard call, the page store, the check event, library.limits.set, library.status gains replyLimits, role.claim gains library, thread.messages gains check and last.
  • packages/bus/src/server.mjs, client.mjs: BusError.reason (sage-3599).
  • packages/bus/src/runtime.mjs: guard deps and the library.limits.set dispatch.
  • packages/cli/src/limits.mjs: limits replies --file LIMITS.

Left:

  • packages/harness/src/runner.mjs: the C2-07 runner work (see "Runner plan" below).
  • packages/cli/README.md: a line for limits replies.
  • Every test in the brief and the rulings below, then the mutants.
  • The rebases onto rows 283, 216 and 285, with the drift checks below.
  • This file's gate results and the manifest.

Rulings in force

sage-3595:

  1. Supporting scope: a stub classify test now. Before in-review, rebase onto row 283's landing (honker/row283-build bd2c1c4ea when ruled) and add two real-fixture tests: a supporting cite with as: "canon" refuses playground-cited-as-canon, and the stored check result names supporting.
  2. runtime.mjs is approved for the guard wiring and the one library.limits.set branch. List its hunks here, name it in the manifest, and drift-check it after the 216 and 285 rebases.
  3. The check result is an action.allowed event with action: "message.checked". The events kind CHECK at packages/bus/src/migrations/3b.sql:9 has no message.checked, so a new kind would be a schema step. The event and the message INSERT share one transaction: a mutant that splits them must fail.
  4. readLibrary at the cited commit stands in for the C2-05a index. On the 216 rebase, compare 216's index set with readLibrary's for the fixture and record the result here.
  5. The retry is for a library business only. Tests: a JSON body between 32,768 characters and 64 KiB gets the retry; outside a library business nothing changes.
  6. Old role.claim pins stay as they are.
  7. render.mjs:6 and render.test.mjs:58 belong to row 225. Leave them.
  8. thread.messages rows that carry check get last per path cite: the file's last commit at the served tip, one git call per distinct path per read. Test with a fixture file committed after the cite.

sage-3599: BusError gets an optional reason. Only the citation guard's refusals set it. The envelope carries reason only when it is set. It is the guard's own text, never a quote, file text, url or a path other than the cited one, at most 300 characters. Tests: a guard refusal reaches the runner with code and reason; a refusal without a reason has an envelope byte-identical to today's, checked against an existing pin; a mutant that drops the client.mjs pass-through fails.

sage-3602: store file per path cite from the same git log call as the trailer. Test with a cited commit that didn't touch the file, so file differs from the cited commit, and last equals file.

The check-result contract with row 225 (launchpad-0118, launchpad-0120, morgana-0200, sage-3596)

  • Stored on the message.checked event: {citations: [...]} in body order.
  • A path cite: {scope, agent, file}. scope is the classify scope. agent is the cite's file's last commit's Mosaic-Agent trailer at the cited commit, or null. file is that commit's 40-hex id, or null.
  • A url cite: {scope: "web", agent: null}.
  • On thread.messages, each path cite gains last: the file's last commit at the served tip (40-hex or null). The Console compares last with file.
  • Rows without check are unchanged. No changed boolean. The webui passes rows through unchanged.

Hunks outside the brief's file list

  • packages/bus/src/runtime.mjs: imports setReplyLimits; the library deps gain guard, replyLimits (organizationWriter readRaw, or {readable: false} with no organization), last and GuardError; the organization dispatch gains library.limits.set.
  • packages/bus/src/server.mjs: a refusal(e, other) helper above serve's doc comment builds every BusError envelope and adds reason only when set. The synchronous request path accepts a Promise result (the guard is async), with a 120 s timeout, and releases own when it settles.
  • packages/bus/src/client.mjs: new BusError(r.error, reason) with reason a string cut at 300.

Decisions and their tradeoffs

  • The guard runs async outside the SQL transaction, then re-enters request() through #guardPass, so message.send's insert and the check event stay one synchronous transaction.
  • The guard runs before message.send's own checks, so a guard code can come before an authority code.
  • The check event id is derived from the message id (a UUID-shaped sha256), so thread.messages finds it through the unique index on events.id.
  • With no organization configured, replyLimits reads as unreadable, so it fails closed.
  • With more than one library project, the guard uses the first in the business file's order.
  • A library business without guard deps refuses guarded sends with project-not-configured.
  • The page store is per cap: 20 pages of 200 KB, kept only in a library business, dropped at session delete or disconnect and at run end.
  • The CLI replies verb reads the revision itself from limits.
  • Guard reasons never carry a url or a quote (sage-3599).

Runner plan (not built)

role.claim's reply is discarded at runner.mjs:462; keep it and read library === true. In a library business, at the reply site (runner.mjs:582-587):

  • Fixed texts (the session gave no answer) and (the turn failed); the reason stays in the run log. A fixed text goes with no note, and unslop.sent() is not called, so the note waits.
  • A body that parses as a JSON object with a string text gets unslop.note() appended to text, is serialized again and is never clipped. A plain body keeps clip(body) + unslop.note().
  • On a refusal whose code is in GUARD_CODES, or request-too-large from the client, or the broker's invalid-request from string(a.body, 32768) for a body over 32,768 characters (ruling 5; the runner can tell it by the body length it sent), run one new turn in the same session naming the code and err.reason, or the 64 KiB limit. Bracket it with usage like other turns.
  • After the retry: a table code sends (no answer passed the citation check), too large sends (the answer was too long to send). One retry per answer, whatever the cause.
  • runTurn's output buffer holds up to 4 * BODY_MAX characters, so a reply over 64 KiB can reach the send.
  • Outside a library business nothing changes.

Mutants planned

  • The M11 writer check removed.
  • document.human compared instead of old.human.
  • The check event split from the insert transaction.
  • The client.mjs reason pass-through dropped.