Seams
One page per contract with one implementation on each side, a fake for tests, and a gate that names
its boundary (#168, under #167 / #162). A seam page is what a reviewer is handed: this contract,
architecture.md’s five control-plane invariants, and the question where does this boundary leak,
and what breaks on a pivot?
Every page has the same six sections — what the seam is · the contract, verbatim · who implements it
and who consumes it · what fences it · what crosses it that should not · pivot cost — and every
ts fence is the source: test/docs-gate.test.ts fails when a fence differs from the declaration its
marker names. When it does, bun scripts/docs/refresh-fences.ts <page> is the fix and the diff is
the review. One page is generated whole (ported-provenance.md, bun <script> > <page>) and diffed
the same way.
The table of seams — domain, gate, and each page’s own statement of what crosses it — lives in
architecture.md → Seams, generated from these pages by
scripts/docs/seams-table.ts so it cannot restate a leak a page has closed. A page states its leak;
it does not fix it.
Leak numbers
Section titled “Leak numbers”L1–L6 number the six places a vendor’s name or shape crossed the loop’s boundary, and the seam
pages, architecture.md and the glossary use the numbers as identifiers (“since L2”, “leak L5”).
The numbering started in a working note that is not in the repository; this is the committed
definition. Every row’s stated in column is where the claim is made and checked in this checkout,
so a row can be verified rather than taken on the number’s word.
| # | What crossed | Where it is now | Stated in |
|---|---|---|---|
| L1 | the vendor’s Pi* prefix on shapes of ours — PiSession, PiPromptRequest, PiRpcEvent, the module pi-rpc.ts |
renamed: ThreadRuntime, PromptRequest, RuntimeCommand in thread-runtime.ts, and PiRpcEvent → RuntimeEvent, which L2 then moved below the seam to host/runtime-event.ts |
glossary.md → Retired, applied to the source tree by test/glossary-gate.test.ts |
| L2 | the runtime’s event and stored-entry shapes, read above the host | below the seam: the event is read only under packages/web/src/host/ (runtime-event.ts, map-events.ts, transcript.ts), and a thread’s branch is a transcript of ours (packages/core/src/transcript.ts) |
the leak sections of thread-runtime.md (event side) and storage.md (persistence side) |
| L3 | the permission mode, read three layers away from the runtime | the wire shape is ours (packages/core/src/permission-mode.ts); the vendor’s persisted mode is read below the seam (packages/web/src/host/permission-mode-adapter.ts) |
permission-mode.md — its leak section reads Nothing., its pivot section states the three-step fix |
| L4 | the four dialog kinds are the runtime’s extension-UI method names | kept, by decision — the kinds are the vocabulary (glossary.md → dialog kind) |
dialog.md |
| L5 | the guard’s tool names are the runtime’s, and the guard is the runtime’s own extension hook | open, by decision, and stated wherever it shows: the guards page, the guarded-tool-call flow, and the composition roots on the web-providers page | guards.md, flows/guarded-tool-call.md, web-providers.md |
| L6 | first-party code outside the vendor roots naming the vendor’s model and credential types — the search provider vocabulary and auth | the provider contract names its own model and credential types; the vendor gate’s import allowlist is empty (VENDOR_IMPORT_ALLOWED = 0) |
web-providers.md, Ratchets |
L1, L2, L3 and L6 are fixed; L4 and L5 are decided and stay, which is why the seams table still
reads L5 for the guards. A row’s claim is the page’s, not this table’s: the page is where a
reopened leak is stated first.