The resolver returns data: no closure, no status, no handler off the table

sdk-qa round 6, one MUST-FIX, and it was the design note's own sentence turned
on the design: resolveApi held the pathname AND a respond callback AND
returned executable closures, so a resolver-internal special case could mint a
200 before the table lookup — reproduced on 6fc145b first: status 200,
minted:true, absent from runtime.routes, all committed controls green. The
trusted region had moved into the routing definition's wrapper.

The fix strips response authority from the path-bearing side. matchApi
(renamed from resolveApi — the name now says what it may do) returns ONLY
data: { index, rest, searchParams } for a hit, { index: -1, notFound } for a
miss where notFound is a 404 BODY object, never a status. The dispatch site
fetches the executable as API_ROUTES[m.index] — table membership is the only
door to a handler — and pins the miss status to 404 in serve-studio. A
special case inside the resolver therefore cannot mint a closure that runs
(unknown fields are inert downstream), cannot set a status, and cannot reach
a handler that is not a row.

The permanent RESOLVER-BOUNDARY DISCRIMINATOR plants special cases INSIDE
matchApi and asserts each channel dead: the round-6 closure-mint is inert
(non-200, canary never runs, nothing enumerated) and a status-mint loses to
the hardcoded 404. The property test additionally asserts the match result
contains no function-valued field.

STATED RESIDUE, pinned by the discriminator rather than hidden: the resolver's
remaining freedom is its charter — choosing which DECLARED row a path maps to.
A special case can mis-route to a different declared row's index; the
discriminator asserts that blast radius exactly (the served response is a
declared row's, nothing new is enumerated) and the design note records it. It
can never again produce a response that is not a declared row's.

docs/design/routing-data-boundary.md updated to the as-built shape: data-only
resolver, membership-only dispatch, the discriminator, and the residue.

352 -> 353 tests, 353/353 green.
This commit is contained in:
mvschwarz committed 2026-08-08 10:13:50 -07:00
1 parent 6fc145b608
commit 862bfd245d
4 files changed
+178 -70

No files matched your search

+18 -11
View File
@@ -20,7 +20,7 @@ import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { RUNTIME_OWNED_VERBS, SUBSTITUTABLE_VERBS, openVocabMap, lookup, verbMatches, resolveApi } from "./verbs.mjs";
import { RUNTIME_OWNED_VERBS, SUBSTITUTABLE_VERBS, openVocabMap, lookup, verbMatches, matchApi } from "./verbs.mjs";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, "..");
@@ -1042,9 +1042,9 @@ function filesSearch(q) {
// row matched, bounded by the row that registered it, and handlers receive
// { searchParams, verb, rest } — never a pathname.
//
// ROUTING lives in resolveApi (app/verbs.mjs), the one place a pathname meets
// this table. handleRequest hands over the raw URL and receives an execute
// closure; the pathname is OUT OF SCOPE downstream by construction, so "a
// ROUTING lives in matchApi (app/verbs.mjs), the one place a pathname meets
// this table. handleRequest hands over the raw URL and receives a table
// index; the pathname is OUT OF SCOPE downstream by construction, so "a
// route cannot serve without a row" is a fact about the data flow. See
// docs/design/routing-data-boundary.md for the load-bearing invariant.
//
@@ -1217,20 +1217,27 @@ const API_ROUTES = [
// <<< API ROUTING SURFACE
const handleRequest = async (req, res) => {
// THE API FLOW HOLDS NO PATHNAME. Routing happens inside resolveApi (the
// THE API FLOW HOLDS NO PATHNAME. Routing happens inside matchApi (the
// grammar module): this function hands over the raw request URL and receives
// either null (not the api namespace) or an execute closure that already
// carries its resolved row. Neither `u` nor the namespace prefix exists as a
// either null (not the api namespace) or a table INDEX plus request data —
// never a closure, never a status. Neither `u` nor the namespace prefix exists as a
// binding anywhere in this flow — a pathname special-case here is a
// ReferenceError, not a code-review question. THE ORDERING BELOW IS
// LOAD-BEARING: the static-flow URL is deliberately constructed AFTER this
// branch returns, so no code that could see an api request ever holds a
// pathname. See docs/design/routing-data-boundary.md before "simplifying"
// either fact away.
const api = resolveApi(req.url, `http://127.0.0.1:${PORT}`, API_ROUTES, sendJson);
if (api) {
try { await api.execute(req, res); }
catch (e) { return sendJson(res, 500, { ok: false, error: String(e.message || e) }); }
const m = matchApi(req.url, `http://127.0.0.1:${PORT}`, API_ROUTES);
if (m) {
try {
// TABLE MEMBERSHIP IS THE ONLY DOOR TO A HANDLER, and 404 is the only
// status the miss branch can have: the resolver returns an index and a
// body object — data — never a closure and never a status. See the
// resolver-boundary discriminator in test/reserved-verbs.test.mjs.
const row = API_ROUTES[m.index];
if (row) await row.handler({ searchParams: m.searchParams, verb: row.verb, rest: m.rest }, req, res);
else sendJson(res, 404, m.notFound);
} catch (e) { return sendJson(res, 500, { ok: false, error: String(e.message || e) }); }
return;
}
const u = new URL(req.url, `http://127.0.0.1:${PORT}`);
+26 -19
View File
@@ -81,31 +81,38 @@ export const isRuntimeOwned = (pathname) =>
//
// THE DATA BOUNDARY (PM's ratified criterion, round 5): after the table lookup
// resolves a row, the raw pathname and the namespace prefix are OUT OF SCOPE
// for everything downstream. The runtime's api flow never constructs a URL at
// all — it hands this function the raw request URL and receives back either
// null (not this namespace) or an `execute` closure. The row branch closes over
// a request object carrying ONLY { searchParams, verb, rest }; the miss branch
// closes over the pathname internally for its 404 message. So a pathname
// special-case cannot be WRITTEN downstream: there is no pathname there to
// branch on. That is a fact about the data flow, not a claim a guard checks —
// the guards that remain (the literal fence, the unwritability control) police
// the residue, which is this function itself: the definition of routing, where
// any special case must now be written in plain sight and where the
// routes=[] property test sees it.
// for everything downstream — the runtime's api flow never constructs a URL at
// all. AND (round 6, sdk-qa): the path-bearing side holds NO RESPONSE
// AUTHORITY. The previous shape returned an `execute` closure and took a
// respond callback, so a resolver-internal special case could mint a 200
// before the table lookup — the trusted region had moved in here. Now this
// function returns ONLY DATA:
//
// null — not the api namespace
// { index, rest, searchParams } — a TABLE INDEX, never a handler
// { index: -1, notFound } — a 404 BODY object, never a status
//
// The executable is fetched downstream as `routes[m.index]` — table membership
// is the only door to a handler — and the miss status is HARDCODED 404 at the
// single dispatch site. A special case written in here cannot mint a closure
// that runs (unknown fields are inert downstream), cannot mint a success
// status (there is no status channel), and cannot reach a handler that is not
// a row. What it CAN still do is the function's charter and its stated
// residue: return a different DECLARED row's index — a wrong routing decision
// among enumerated rows, written into the definition of routing where the
// resolver-boundary discriminator and review see it.
export const API_PREFIX = "/api/";
export function resolveApi(rawUrl, base, routes, respond) {
export function matchApi(rawUrl, base, routes) {
const u = new URL(rawUrl, base);
if (!u.pathname.startsWith(API_PREFIX)) return null;
const row = routes.find((r) => verbMatches(r.verb, u.pathname));
if (!row) {
const pathname = u.pathname;
return { execute: (req, res) => respond(res, 404, { ok: false,
error: `no such contract route: ${pathname} — see GET /api/contract for capabilities and contract/runtime-api.md for the verb set` }) };
const index = routes.findIndex((r) => verbMatches(r.verb, u.pathname));
if (index === -1) {
return { index: -1, notFound: { ok: false,
error: `no such contract route: ${u.pathname} — see GET /api/contract for capabilities and contract/runtime-api.md for the verb set` } };
}
const request = { searchParams: u.searchParams, verb: row.verb, rest: u.pathname.slice(row.verb.length) };
return { execute: (req, res) => row.handler(request, req, res) };
return { index, rest: u.pathname.slice(routes[index].verb.length), searchParams: u.searchParams };
}
// ---- the routing surface tripwire ------------------------------------------
+34 -11
View File
@@ -11,14 +11,22 @@ prefix are out of scope for everything downstream.** In this codebase that is
implemented more strongly: the runtime's API flow in `app/serve-studio.mjs`
never holds a pathname at all.
- Routing happens in exactly one place: `resolveApi()` in `app/verbs.mjs`. It
- Routing happens in exactly one place: `matchApi()` in `app/verbs.mjs`. It
receives the raw request URL, constructs the URL object, matches the table,
and returns an `execute` closure.
and returns **data only**: a table index plus `{ rest, searchParams }`, or
`{ index: -1, notFound }` where `notFound` is a 404 *body object*.
- **The path-bearing side holds no response authority** (round 6, sdk-qa). An
earlier shape returned an `execute` closure and took a respond callback — so
a resolver-internal special case could mint a 200 before the table lookup.
Now the resolver cannot mint a closure that runs (unknown fields are inert
downstream), cannot set a status (the miss branch is hardcoded 404 at the
dispatch site), and cannot reach a handler that is not a row (the executable
is fetched as `API_ROUTES[m.index]` — table membership is the only door).
- Handlers receive `{ searchParams, verb, rest }` — never a pathname, never a
URL object. A handler needing path data uses `rest`, which is already scoped
to its own matched verb.
- `handleRequest` calls `resolveApi` and executes the result. Neither `u` nor
`API_PREFIX` is a binding anywhere in that flow.
- `handleRequest` calls `matchApi` and dispatches the indexed row. Neither `u`
nor `API_PREFIX` is a binding anywhere in that flow.
- The static-file flow's `const u = new URL(...)` is constructed **after** the
API branch has returned. That ordering is load-bearing: it is what guarantees
no code that can see an API request ever holds a pathname.
@@ -60,17 +68,32 @@ to remove it:
round-4 literal arm and round-5 derived-prefix arm at the top of the API flow
and asserts each detonates as a `ReferenceError` naming the absent binding —
and above all, never serves.
- **Property tests on `resolveApi`**: with `routes=[]` every API path misses;
with one route, exactly its matches resolve; the resolved request carries no
pathname field.
- **Resolver-boundary discriminator** (`test/reserved-verbs.test.mjs`): plants
special cases *inside* `matchApi` and asserts every response channel is dead —
a minted `{ execute }` is inert and never runs, a minted status loses to the
hardcoded 404, and nothing a resolver special case does can be enumerated.
- **Property tests on `matchApi`**: with `routes=[]` every API path misses;
with one route, exactly its matches resolve; the match result carries no
pathname field and no function-valued field.
- **The literal fence** (`routingSurfaceViolations`): every textual `/api`
mention in `serve-studio.mjs` lives inside the marked data-only surface.
- **Bypass control**: raw arms planted in the static flow demonstrably cannot
serve API requests (404), in all the spellings sdk-qa used across rounds.
## The stated residue
The resolver's remaining freedom is exactly its charter: choosing which
declared row a path maps to. A special case written into `matchApi` can
mis-route a path to a *different declared row's* index — the discriminator pins
this blast radius (the served response is a declared row's; nothing new is
enumerated) — but it can never produce a response that is not a declared
row's. A wrong routing decision must be written into the eight-line definition
of routing, where review and the `routes=[]` property test see it.
## If you need to change this
If a future feature genuinely needs path data downstream, extend the resolved
`request` (e.g., a richer `rest`) inside `resolveApi` — do not reintroduce the
pathname. If you believe the boundary itself must move, that is a PM-level
design change: say so in the commit and update this note in the same change.
If a future feature genuinely needs path data downstream, extend the match
result (e.g., a richer `rest`) inside `matchApi` — do not reintroduce the
pathname, and never return a function or a status. If you believe the boundary
itself must move, that is a PM-level design change: say so in the commit and
update this note in the same change.
+100 -29
View File
@@ -29,7 +29,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process";
import { RUNTIME_OWNED_VERBS, SUBSTITUTABLE_VERBS, openVocabMap, lookup,
routingSurfaceViolations, verbMatches, isRuntimeOwned, resolveApi } from "../app/verbs.mjs";
routingSurfaceViolations, verbMatches, isRuntimeOwned, matchApi } from "../app/verbs.mjs";
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const read = (...p) => fs.readFileSync(path.join(REPO, ...p), "utf8");
@@ -134,11 +134,11 @@ test("the safe primitive actually resists prototype keys", () => {
// Boot a mutated copy of the runtime and probe it. Shared by the two controls
// below, which differ only in WHAT they plant.
async function bootMutated(plantedSrc, probes) {
async function bootMutated(plantedSrc, probes, file = "serve-studio.mjs") {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "routes-ctl-"));
fs.cpSync(path.join(REPO, "app"), path.join(dir, "app"), { recursive: true });
fs.cpSync(path.join(REPO, "fixtures"), path.join(dir, "fixtures"), { recursive: true });
fs.writeFileSync(path.join(dir, "app", "serve-studio.mjs"), plantedSrc);
fs.writeFileSync(path.join(dir, "app", file), plantedSrc);
const port = 9960 + Math.floor(Math.random() * 30);
const proc = spawn(process.execPath, [path.join(dir, "app", "serve-studio.mjs"), "--port", String(port)],
{ cwd: dir, stdio: ["ignore", "pipe", "pipe"] });
@@ -147,10 +147,14 @@ async function bootMutated(plantedSrc, probes) {
try { await fetch(`http://127.0.0.1:${port}/api/contract`, { cache: "no-store" }); break; }
catch { await new Promise((r) => setTimeout(r, 50)); }
}
const statuses = {};
for (const p of probes) statuses[p] = (await fetch(`http://127.0.0.1:${port}${p}`, { cache: "no-store" })).status;
const statuses = {}, bodies = {};
for (const p of probes) {
const r = await fetch(`http://127.0.0.1:${port}${p}`, { cache: "no-store" });
statuses[p] = r.status;
bodies[p] = await r.text();
}
const c = await (await fetch(`http://127.0.0.1:${port}/api/contract`, { cache: "no-store" })).json();
return { statuses, routes: c.runtime.routes };
return { statuses, bodies, routes: c.runtime.routes };
} finally { proc.kill(); fs.rmSync(dir, { recursive: true, force: true }); }
}
@@ -241,7 +245,7 @@ test("UNWRITABILITY CONTROL: sdk-qa's derived-prefix arm at the ordinary place i
// naming the missing binding. If either ever boots again, a pathname has
// been reintroduced into the api flow and the data boundary is gone.
const src = read("app", "serve-studio.mjs");
const ordinary = /const api = resolveApi\(req\.url[^\n]*\n/;
const ordinary = /const m = matchApi\(req\.url[^\n]*\n/;
assert.match(src, ordinary, "the api flow entry moved — re-anchor this control before trusting it");
const plants = [
@@ -289,35 +293,102 @@ test("UNWRITABILITY CONTROL: sdk-qa's derived-prefix arm at the ordinary place i
}
});
test("routing is exactly the table: routes=[] misses everything, routes=[X] resolves exactly X", async () => {
test("routing is exactly the table: routes=[] misses everything, routes=[X] resolves exactly X", () => {
// The property test the pure function enables — routing asserted against its
// DEFINITION rather than against source text. A special case smuggled into
// resolveApi that does not consult the table would let some path resolve
// with an EMPTY table, which is the first assertion here.
// matchApi that does not consult the table would let some path resolve with
// an EMPTY table, which is the first assertion here.
const BASE = "http://127.0.0.1:9999";
const sent = [];
const respond = (res, code, obj) => sent.push({ code, obj });
for (const p of ["/api/focus", "/api/contract", "/api/anything/else", "/api/qa-derived"]) {
const decision = resolveApi(p, BASE, [], respond);
assert.ok(decision, `routes=[]: '${p}' left the api namespace unclaimed`);
decision.execute(null, null);
assert.equal(sent.at(-1).code, 404, `routes=[]: '${p}' resolved despite an empty table`);
const m = matchApi(p, BASE, []);
assert.ok(m, `routes=[]: '${p}' left the api namespace unclaimed`);
assert.equal(m.index, -1, `routes=[]: '${p}' resolved despite an empty table`);
assert.match(m.notFound.error, /no such contract route/);
}
assert.equal(resolveApi("/not-api", BASE, [], respond), null, "a non-api path was claimed");
assert.equal(matchApi("/not-api", BASE, []), null, "a non-api path was claimed");
let got = null;
const X = [{ verb: "/api/export-status/", handler: (request) => { got = request; } }];
const hit = resolveApi("/api/export-status/42?a=b", BASE, X, respond);
await hit.execute(null, null);
assert.equal(got.verb, "/api/export-status/");
assert.equal(got.rest, "42", "the prefix remainder is not handed to the handler as rest");
assert.equal(got.searchParams.get("a"), "b");
assert.equal(got.pathname, undefined,
"the resolved request carries a pathname — the data boundary leaks the thing it exists to remove");
const miss = resolveApi("/api/export-status", BASE, X, respond);
miss.execute(null, null);
assert.equal(sent.at(-1).code, 404, "an exact path matched a prefix verb without the trailing slash");
const X = [{ verb: "/api/export-status/", handler: () => {} }];
const hit = matchApi("/api/export-status/42?a=b", BASE, X);
assert.equal(hit.index, 0);
assert.equal(hit.rest, "42", "the prefix remainder is not returned as rest");
assert.equal(hit.searchParams.get("a"), "b");
assert.equal(hit.pathname, undefined,
"the match result carries a pathname — the data boundary leaks the thing it exists to remove");
assert.equal(matchApi("/api/export-status", BASE, X).index, -1,
"an exact path matched a prefix verb without the trailing slash");
// AND THE RESULT IS DATA, NOT AUTHORITY (round 6): no closure to run, no
// status to set. A resolver special case has neither channel.
for (const m of [matchApi("/api/focus", BASE, X), matchApi("/api/export-status/1", BASE, X)]) {
for (const v of Object.values(m)) {
assert.notEqual(typeof v, "function",
"matchApi returned a function — the path-bearing side has response authority again");
}
}
});
test("RESOLVER-BOUNDARY DISCRIMINATOR: a special case inside matchApi cannot mint a public response", async () => {
// sdk-qa round 6: the trusted region had moved into the resolver — it held
// the pathname AND a respond callback AND returned executable closures, so a
// pre-lookup special case could mint a 200 that appeared in no enumeration
// (reproduced on 6fc145b: status 200, minted:true, absent from routes).
// Now the resolver returns DATA and this control plants special cases INSIDE
// it — at the precise boundary — and asserts each channel is dead:
// closure-mint: an { execute } return is INERT — downstream never calls it;
// status-mint: there is no status channel — the miss branch is hardcoded
// 404 at the dispatch site, whatever the resolver returns.
// The genuine registered routes stay the positive control (every other test
// in this file). THE STATED RESIDUE, asserted for exactly what it is: the
// resolver can still return a DIFFERENT DECLARED row's index — a wrong
// routing decision confined to enumerated rows, which is the routing
// definition's charter; what it can never do again is produce a response
// that is not a declared row's.
const src = read("app", "verbs.mjs");
const anchor = " const index = routes.findIndex((r) => verbMatches(r.verb, u.pathname));";
assert.ok(src.includes(anchor), "matchApi's lookup moved — re-anchor this control before trusting it");
// closure-mint: the round-6 plant, verbatim shape. Must be inert: not 200,
// the canary body never served, the path absent from routes.
{
const planted = src.replace(anchor,
' if (u.pathname === API_PREFIX + "qa-resolver-mint") return { execute: (req, res) => { throw new Error("canary"); }, minted: true };\n' + anchor);
assert.notEqual(planted, src, "closure-mint plant did not apply");
const { statuses, bodies, routes } = await bootMutated(planted, ["/api/qa-resolver-mint"], "verbs.mjs");
assert.notEqual(statuses["/api/qa-resolver-mint"], 200,
"a resolver-minted closure SERVED — the path-bearing side has response authority again");
assert.ok(!bodies["/api/qa-resolver-mint"].includes("canary"), "the minted closure ran");
assert.ok(!routes.some((v) => v.includes("qa-resolver-mint")), "the minted route was enumerated");
}
// status-mint: a crafted miss with a success-shaped body and a code field.
// The dispatch site's hardcoded 404 must win.
{
const planted = src.replace(anchor,
' if (u.pathname === API_PREFIX + "qa-status-mint") return { index: -1, notFound: { ok: true, minted: true }, code: 200 };\n' + anchor);
assert.notEqual(planted, src, "status-mint plant did not apply");
const { statuses } = await bootMutated(planted, ["/api/qa-status-mint"], "verbs.mjs");
assert.equal(statuses["/api/qa-status-mint"], 404,
"the resolver set a response status — the miss branch is no longer pinned to 404");
}
// misbind: the stated residue, pinned to its exact blast radius — the
// response served is a DECLARED row's (the contract handler's), reported
// routes are unchanged, and the misbound path itself is not enumerated.
{
const src2 = read("app", "serve-studio.mjs");
const rows = [...src2.matchAll(/\{ verb: (?:"[^"]+"|CONTRACT_VERB)/g)];
const contractIndex = rows.findIndex((m2) => m2[0].includes("CONTRACT_VERB"));
assert.ok(contractIndex >= 0, "could not locate the contract row's index in the table");
const planted = src.replace(anchor,
` if (u.pathname === API_PREFIX + "qa-misbind") return { index: ${contractIndex}, rest: "", searchParams: u.searchParams };\n` + anchor);
assert.notEqual(planted, src, "misbind plant did not apply");
const { statuses, bodies, routes } = await bootMutated(planted, ["/api/qa-misbind"], "verbs.mjs");
assert.equal(statuses["/api/qa-misbind"], 200, "the misbind residue changed shape — re-derive this control");
assert.ok(bodies["/api/qa-misbind"].includes("contractVersion"),
"misbind served something that is NOT a declared row's response — a new channel exists");
assert.ok(!routes.some((v) => v.includes("qa-misbind")), "the misbound path was enumerated");
}
});
test("the runtime source keeps every /api mention inside the routing surface", () => {