131 Commits
Author SHA1 Message Date
Mike Schwarz f6edb6c8a7 FLOOR: a finished item's Result shows its resolution and its closing note (#2)
A finished item's Result on the floor now shows its resolution, where most finished rows keep their outcome, and then a different closing note below it, labelled. Either one alone is the result, the same text isn't shown twice, and a closing note that couldn't be read no longer hides the resolution. Before, the floor showed the closing note and used the resolution only when there was no note.

Two files (+29/-2): the floor page and its test. Source-only entry; no tag, pin, release or deploy.
2026-10-23 03:09:29 -07:00
Mike Schwarz 652d2980b5 FLOOR: finished work, and find by id (#1)
A person looking at a rig's floor can now see its finished work, with each item's answer first in plain words, and find an item by id. Coverage is honest: a bounded or unreadable read says so instead of claiming there's nothing. An artifact reference wraps without changing its text.

Six files (+291/-8): the floor page and server, the runtime contract, and tests. Source-only entry; no tag, pin, release or deploy.
2026-10-23 01:45:42 -07:00
mvschwarz e7346bbdbd Strip internal QA seat-name attribution from the shipped design note
Leak gate on ce6476b..ba33aa1 flagged one thing: routing-data-boundary.md — a
shipped public design doc — named the internal "sdk-qa" seat five times in the
MF1 lineage prose. Naming an internal seat in shipped product documentation
violates the mechanism-only condition for public lineage (distinct from
commit-message and source-comment attribution, which the standing rule keeps).

Per orch's ruling: preserve the lineage, strip the attribution. The value of the
crossing progression in a routing-data-boundary doc is WHY the boundary is
shaped this way — the six crossing types — not which seat found each. The five
references now read as mechanism only (round numbers, "the trojans", "the
spellings tried"), no seat name. The WHO stays in the commit messages.

Doc-only, one file; no code, test, or contract change. 378/378 unchanged.
2026-08-08 17:58:38 -07:00
mvschwarz ba33aa1efa ctx.onClose returns nothing native, and its control fires at the real site
sdk-qa re-round 5 — MF1 request side (response side + ownCallback proven sound,
untouched). Two defects on ctx.onClose:

1. RETURN-VALUE CROSSING. ctx.onClose was `(cb) => req.on("close",
   ownCallback(cb))`. EventEmitter.on returns the emitter — the native request —
   so an ordinary handler got req straight back as the return value. ownCallback
   guarded the callback's this/args but not the RETURN direction.

2. CONTROL-CANNOT-FIRE. The prior request-side control tested ownCallback in
   isolation, so removing it from the real wiring left the suite green — it
   satisfied "its own control" and missed the point (the control-must-fire
   lesson, landing on exactly that ruling).

Both fixed. ctx-building is extracted to app/handler-context.mjs (makeCtx), so
the actual wiring is unit-testable; onClose now wraps the callback with
ownCallback AND returns undefined (never the emitter). test/handler-context.test.mjs
drives the ACTUAL makeCtx across the ctx crossing matrix — no native this, args,
or return — and carries negative controls proving each assertion can fail;
demonstrated by stripping both guards from the real makeCtx (suite RED, test 249
of the run fails) and restoring (green). The design note is corrected: the
request side now states the return crossing and the real-site control, not an
isolated-wrapper claim. The adapter's sanctioned `req` count drops 6->3 as
method/headers/body/close move into makeCtx; the confinement guard is updated
deliberately.

MF2 verified closed, unchanged. 374 -> 378 tests, 378/378 green.
2026-08-08 17:35:08 -07:00
mvschwarz 0fa8d86607 The facade owns every invocation, not just every reference
sdk-qa re-round 4 — MF1 again (MF2 verified closed), and an unsound-property
return rather than an enumeration gap. The facade's five methods were fully
enumerated, but the implementation rule ("forward only handler-owned arguments
into the native response") is false when an argument is a CALLBACK the native
response later invokes: Node runs res.end(chunk, cb) with the native response as
cb's `this`, from which this.req.url recovers the request (measured against a
real ServerResponse: end-callback this === res). The same class sat on the
request side — req.on("close", cb) invokes cb with this === req.

MF1 lineage is now five narrower rules, each right about the property and too
narrow in the mechanism: spelling guard -> res.req -> facade property -> facade
delegation -> callback this-binding. The fix states the rule as a property, not
a channel list: THE FACADE OWNS EVERY INVOCATION AND EVERY REFERENCE. Any
handler-supplied callback is replaced with a facade-owned wrapper (ownCallback,
exported) that the facade invokes with no `this` a handler can use (a lexical
arrow) and no forwarded arguments — native this/args discarded — so no native
object arrives as the receiver or an argument of handler code. Applied to every
facade method (write/end/writeHead/setHeader via an arg mapper) and to
ctx.onClose on the request side.

Controls: test/response-facade.test.mjs gains the crossing matrix, measured
against a REAL ServerResponse — a callback passed to any method runs with no
native `this` and no native argument (sdk-qa's end(cb) trojan as negative
control) — plus a direct ownCallback test for the onClose path. The design note
is corrected a fifth time and now states the property ("owns every invocation
and reference") rather than a list of covered channels, with the five-round
history.

372 -> 374 tests, 374/374 green.
2026-08-08 17:19:44 -07:00
mvschwarz f9e7acf6c0 The response facade hands nothing native to handler code, in any direction
sdk-qa re-round 3 — MF1 again (MF2 verified closed). The facade's pipeFrom
invoked a handler-supplied source's pipe(res), handing the raw response — and
through res.req the request — straight to handler-controlled code: an ordinary
handler supplied a trojan source and recovered the url (reproduced on 5d48468).
Four rounds, four crossings: spelling guard -> res.req -> facade property ->
facade delegation. Each moved the boundary and leaked through the next crossing.

The property, stated once for the whole class and enforced: no facade-held
native object (the response, or the request it back-references) may cross into
handler-controlled code in ANY direction — argument, this-binding, return value,
callback parameter, or event payload. The facade (now app/response-facade.mjs,
a unit-testable module) passes only the handler's own arguments into res,
returns only the facade / a primitive / undefined, uses no `this`, and inverts
pipeFrom: it PULLS chunks from the handler-supplied source with a facade-owned
pump and writes them to res itself, handing the source nothing. files/raw
streams the same way.

Controls: test/response-facade.test.mjs enumerates every method for the
all-crossings property — none returns/yields res, none reads this, and pipeFrom
iterates a source without handing it the response (sdk-qa's pipe(res) delegation
trojan is the negative control, plus a this-rebinding case and a mid-stream
throw). The existing runtime back-reference control (res.req unreachable from a
real handler) stays. The design note is corrected AGAIN — it claimed
unreachable scope while pipeFrom handed res out — and now states the
all-crossings property once, with MF1's four-round history, since it is review
surface.

368 -> 372 tests, 372/372 green.
2026-08-08 17:05:47 -07:00
mvschwarz 5d4846877a Handlers get a response facade, and an ack must be a recorded generation
sdk-qa re-round on cb6dfac — two MUST-FIX, both reproduced on that tip first.

MF1 — the request confinement leaked through the response object. Removing req
from ctx was real, but handlers still received Node's res, whose res.req
back-references the request: an ordinary handler read the url via res.req.url
(reproduced: it returned "/api/qa-leak?secret=xyz"). The design note overstated
"cannot reach in any spelling" — false while res.req was reachable. Handlers now
receive a RESPONSE FACADE exposing only writeHead/setHeader/write/end/pipeFrom,
closing over res in a scope they cannot reach; every method returns the facade,
a primitive, or undefined — never res. No object in handler scope retains the
request. files/raw streams via res.pipeFrom instead of piping to the raw res.
Control plants the back-reference read (res.req.url, the socket parser, the
connection) into a real handler, boots, and asserts every path yields nothing.
The note is corrected to state the facade is the mechanism and to stop
overstating. The req-confinement guard and adapter count are unchanged (the
facade uses res, not req).

MF2 — monotonic+bounded is not membership. Bounding the ack by the surface's
own latest gen still admitted a foreign one: with A=1, B=2, A=3 recorded, A's
latest is 3 and 2 <= 3, so acking 2 (recorded for B) on A passed. Each surface
now tracks the set of generations recorded FOR IT; an ack must be a MEMBER of
that set and forward of the watermark, so a future gen (never recorded) and a
foreign gen (recorded for another surface) are both refused. Generations at or
below the watermark are pruned. The interleaved discriminator (A=1,B=2,A=3, ack
2 on A) is added to the bounded-ack control and refused; A's own gen 3 accepted.

367 -> 368 tests, 368/368 green.
2026-08-08 12:29:13 -07:00
mvschwarz cb6dfacdd2 Design note: confinement is the boundary, the guard is defense-in-depth
The routing-data-boundary note still described the pre-MF1 state and had no
account of the ctx confinement — a load-bearing note describing an older shape
is the RS-M1 class again, and this note IS review surface for the sdk-qa re-round
and the PM gate. Updated to state the hierarchy explicitly (orch): CONFINEMENT
(handlers receive a ctx with no url and no req; the raw request is unavailable to
response-capable handler code in any spelling) is the MECHANISM and the boundary;
the req-identifier-count guard is DEFENSE-IN-DEPTH that catches a change
reintroducing the breach — nobody should read the counter AS the boundary. Also
notes that widening a handler back to (request, req, res) removes the boundary.
Documentation only; 367/367 unchanged.
2026-08-08 12:09:53 -07:00
mvschwarzandClaude Opus 4.8 f9230edc08 drive.md redline: mark the revision PM-endorsed, not pending
pm-studiobox endorsed the drive redline from the actual diff, including the
unaddressed-GET 400 and the settle-ack as advisory/exposed appliedGen (accepting
the divergence from their pre-build position as the better mechanism). The banner
said "PM review pending"; it now records the endorsement. Documentation only.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-08 12:05:10 -07:00
mvschwarz 8550db3579 Confine the raw request to the routing adapter; bound the settle-ack
sdk-qa RETURN on 5e02cf8 — two MUST-FIX and a contract-doc mismatch. Both
MUST-FIX reproduced on the current tip first.

MF1 — the raw-request boundary was syntax-sensitive. The req.url-once guard
counted one textual spelling, so an equivalent property access (`req["url"]`)
served an unenumerated 200 while the suite stayed 366/366 — the scanner-shaped
failure the structural change was meant to remove. The fix is data-flow, not
spelling: handlers now receive a `ctx` (method, headers, searchParams, verb,
rest, readBody, onClose) with NO url and NO req, so response-capable handler
code cannot reach the raw request in any spelling. `req` is referenced only in
the adapter, to route and to build ctx. The guard now asserts handlers and the
static flow are req-free and the adapter's `req` references are exactly the
sanctioned set — which every spelling must trip because each must name `req`.
Control plants literal, bracket, destructured, and aliased raw-url reads at the
adapter entry and asserts each adds a `req` reference (caught). Fail-first: on
the prior tip `req["url"]` served 200/unlisted; here it raises the count 6->7.

MF2 — an impossible settle-ack poisoned future state. The GET endpoint accepted
an arbitrary `applied` generation: a fresh slot took `999999` before any op
existed, so appliedGen reported 999999 with a recorded gen of 1 and the required
record-to-apply gap was false with no apply. Externally reachable: any caller
knowing a surface id could move the ack permanently ahead. The ack is now
bounded to a generation actually recorded for that surface (<= slot.op.gen) and
monotonic within it; a future or foreign gen (gen is global) is ignored.
Negative control BEFORE the positive record-to-apply test: a future ack on a
fresh slot, an ack above the recorded gen, and a foreign gen are all refused.

IMPORTANT — annotations.md still documented the removed cross-origin fallback.
The normative paragraph and conformance row said a cross-origin target falls
back to the surface document; the shipped behaviour (and I3) now degrades to
spatial-only with no element anchoring. Both corrected to state spatial-only /
no fallback, so the stable contract and the build agree.

366 -> 367 tests, 367/367 green.
2026-08-08 12:04:24 -07:00
mvschwarz 5e02cf8446 A self-served URL app composes, and a cross-origin target stays spatial-only
review.r2 contract audit — the last two Important findings.

I2 — the installable-app surface.url branch had no composition path. The app
contract defines surface.url for a self-served app, mutually exclusive with path
and with no entry to copy. Composition unconditionally joined m.surface.entry, so
a schema-valid URL app could not compose — it was reported missing or threw. It
now branches: a url app emits a URL row and copies nothing (it serves itself),
and may still declare a provider and calls. Control: a surface.url app composes
to a row carrying the url and no path.

I3 — a cross-origin annotation target produced a FALSE anchor. On an unreadable
cross-origin nested target the shell fell back to the outer surface document, and
the annotation layer resolved the record's selector against it — so a host and
its nested doc both carrying `#publish` would anchor to the wrong one, the exact
false anchor the contract says it prevents. The shell now returns a spatial-only
view for a cross-origin declared target (a rect, no readable doc) instead of
falling back to the surface document, and the layer's view() honours spatial-only
without falling back while elementAnchor refuses a doc-less view. A cross-origin
surface stays spatial-only, as the contract requires. Structural controls updated
(the fallback rename, the spatialOnly branch, the doc guard) with the positive
control proving they can fail; the pixel-level behaviour remains browser-verified,
as this repo has no browser harness.

365 -> 366 tests, 366/366 green. Audit round complete: all Criticals, Importants,
and Minors fixed (I4 excluded — PM's lane).
2026-08-08 11:16:51 -07:00
mvschwarz 15563c8909 Degraded reads, falsy defers, files/raw errors, and four stale doc lines
review.r2 contract audit — the Important and Minor findings that need no design
ruling (I4 excluded, PM's lane; drive C2 shipped separately).

I1 — a conforming degraded read looked healthy to watchSignal. The contract's
honest-degradation shape is HTTP 200 with { ok:false, degraded }; watchSignal
checked only HTTP status, so a degraded envelope did not throw and was delivered
as fresh data with degraded never reported. It now inspects ok:false at 200,
reports degraded, and does not deliver or advance the cursor on it. Control: a
200 ok:false with a moved marker reports degraded (not healthy) and its body is
not delivered.

M2 — deferWhileDirty dropped falsy deferred changes. null was both the sentinel
and a legal value, so deferring null/false/0/"" reported "deferred" and then the
!pending flush guard read the falsy value as nothing to do. A distinct hasPending
sentinel tracks whether a change is held separately from what it is. Control: all
four falsy values survive a defer-then-flush.

I6 — /api/files/raw violated the stable error contract. A bare empty 404 is not
4xx JSON { ok:false, error }, and a directory passed existsSync, got 200 headers,
then failed mid-stream — success committed before the read could fail. It now
statSync-and-isFile before writing any status, and errors are 4xx JSON. Control
boots the runtime, asserts JSON 404 for a missing path and refuses a directory
(discovered via files/tree) before any 200. The per-route discriminators for
read/raw are updated to stay mutually exclusive on raw's error naming "raw".

I5 — capture-target was live behaviour outside the closed shell protocol. The
shell accepts { t:"capture-target" } and replies capture-target-result, but the
protocol table (declared closed) had no row. Documented into the table per the
same-commit rule — my own recent work brought into the contract it lived outside.

M1 — two conformance rows contradicted their own normative sections and the
build: focus.md said `by` cannot be overridden while --identity-header ships
verified attribution; change-signal.md said no observe verb mints a marker while
focus and drive both honour ?since=. Both rows corrected to what ships.

362 -> 365 tests, 365/365 green.
2026-08-08 11:11:05 -07:00
mvschwarz c70a78d97f Drive ops are addressed to a surface and acknowledged on apply
review.r2 CRITICAL + PM plan-lock (qitem e2ba1531 / audit C2): one global
/api/drive slot delivered every op to every kept-alive surface — including the
play/export/delete side effects the contract itself names — because an op could
not say which surface it was for. PM ruled per-surface DELIVERY (structural, not
global-channel-plus-filter) plus a settle-ack, as one revision. Plan-lock GO on
all three questions; drive.md redline rides this candidate for PM review.

PROPERTY 1 — per-surface delivery, structural. Each surface has its own slot,
keyed by the id it supplies about itself; a surface polls GET /api/drive?surface=
<its own id> and reads only its own slot. A mis-addressed op lands in a slot the
wrong surface never polls, so it is undeliverable there — nothing to filter, no
adopter responsible for filtering correctly. Fail-first: the old global model
delivers A's op to kept-alive B and C; the new control (drive A, keep B/C alive,
assert delete/export/play never fire on B/C) catches exactly that.

BREAKING (Q3, PM-endorsed): an unaddressed POST or GET is refused 400 naming the
required surface field. A warned unaddressed default still fans out, so the
default that caused the defect is removed, not softened. Every in-repo consumer
(drive.test harness, the per-route positive control) migrated in this commit;
listed BREAKING in contract-meta with a changelog entry.

PROPERTY 2 — settle-ack (Q2, advisory/exposed per PM). POST returns on RECORD
(recorded: true) — record and apply kept explicitly apart, a near-synonym pair.
The surface acks the highest generation it has APPLIED via ?applied=; the runtime
exposes appliedGen, which advances only when apply() has RESOLVED (the harder
half, named for applied not received). An agent that sequences awaits appliedGen
>= its posted gen; one that does not stays fast. The gap is observable: a control
blocks apply() and asserts appliedGen < postedGen during the window, catch-up
after. driveSurface exposes applied (resolved) and accepted (offered) separately.

drive.md redline marks every changed (stable) section for PM; contract-meta
example gains drive.surfaces; the per-route parity guard caught that omission.

359 -> 362 tests, 362/362 green.
2026-08-08 11:02:12 -07:00
mvschwarz 37c20d9ded The raw request URL is a data boundary too, and null is not a manifest
review.r2 routing/startup audit, two landing-blocker Criticals reproduced at the
pinned SHA, plus a doc/test drift sweep.

RS-C1 — the raw req.url was readable one consumer earlier than the pathname.
matchApi put the pathname behind a data boundary (rounds 5-6), but req.url —
the raw string — was still read at handleRequest's entry before the match, and
a pre-lookup arm deriving the prefix from fragments ("/" + "api/") served a 200
the table never saw and the literal fence never named (reproduced on the current
tip: 200 / unlisted / zero fence violations). Fix, same standard one level up:
routeRequest is now the SINGLE consumer of the raw URL and returns DATA —
{ api } with no pathname, or { static } carrying the URL the file flow needs.
req.url is read exactly once (that call); the static flow takes its URL from the
decision, not a second req.url read. Guard: a req.url-once test asserts exactly
one code read and that it is the routeRequest call — a pre-lookup arm is a second
read and fails it. Discriminator: review.r2's exact fragment-derived arm is
planted where req.url is available (the entry, where neither the fence nor the
u/API_PREFIX unwritability controls plant) and shown to register as that second
read while the literal fence still returns clean — the point of RS-C1.

RS-C2 — valid JSON that is not a manifest object crashed boot. validateManifest
read doc.surfaces without requiring doc be a plain object; "null" parses, then
null.surfaces THREW at load, the process exited 1, and /api/contract never came
up — the exact crash warn-first exists to prevent. The committed invalid-at-boot
control used only SYNTACTICALLY invalid JSON, so it exercised the guarded parse
catch and stayed green. Fix: an object gate before the surfaces[] check refuses
null, primitives, and arrays the same visible, still-serving way. Control covers
the whole semantic-invalid family (null, number, string, array, object missing
surfaces[]), not one repro value.

RS-M1 — docs/design/routing-data-boundary.md and reserved-verbs.test.mjs still
named resolveApi (renamed to matchApi in 862bfd2); swept, and the note now names
routeRequest too.

356 -> 359 tests, 359/359 green.
2026-08-08 10:48:24 -07:00
mvschwarz 727bb77eba Annotations read "persisted" from the disclosure, not the presence of a store
review.r2 contract audit, CRITICAL (PM-flagged severe, queued high — a false
durability claim is the kind users discover by losing work). The default
in-memory runtime is write-capable, so the shell built an annotations store and
the layer reduced persistence to Boolean(store), rendering "· persisted" over
marks that vanish on restart. The contract (annotations.md:236-250) reports
persistence precisely so "your marks are saved" and "your marks vanish when you
close this tab" cannot look alike; the layer was showing the wrong one.

The label now follows the runtime's own disclosure. marksPersist(store,
persistence) — a pure exported function — returns true only when a store exists
AND persistence === "file" (the one mode that survives a restart today). It is
fail-safe: any unrecognised or absent disclosure reads "session only", because
a false "persisted" costs the user their work while a false "session only" costs
nothing. The shell forwards meta.annotations.persistence into attach().

Controls: the label decision is tested directly against the disclosure (the bug
pinned — store present + "memory" must be false), plus a guard that the shell
still forwards the disclosure. Fail-first: the old Boolean(store) logic returns
true for a memory runtime and fails the new control.

354 -> 356 tests, 356/356 green.
2026-08-08 10:38:28 -07:00
mvschwarz 81f934d851 Every declared route has a positive control a misbind cannot hide behind
orch accepted the round-6 misbind residue (a resolver returning a DIFFERENT
declared row's index) as stated residue rather than MUST-FIX, on two
conditions. This satisfies condition (1): every declared route keeps a positive
control, so a misbind is caught by a route no longer answering as itself.

The control is structural, not a checklist. The verb list comes from the
runtime's own runtime.routes, and a declared route with no discriminator FAILS
the test — so a new route cannot be added without giving it a positive control,
which makes the condition self-enforcing rather than promised. Each route is
driven live and asserted to answer with an own-key (or content-type) unique to
its response; a mutual-exclusivity pass asserts no other route's discriminator
matches, so a misbind toward any route is caught from both sides.

Fail-first paid its keep twice: the /api/contract body carries top-level focus,
drive, capture and annotations sub-blocks, so the first discriminators
collided — a planted focus->contract misbind PASSED. Corrected to key focus and
drive on the { marker, own-key } pair only their own response carries, and
re-verified the same planted misbind is now caught (marker absent in contract).

Also closes the coverage gap this exposed: the four /api/files/* verbs had only
classification-list mentions, no behavioral control — a misbind of files/read
would have hidden. They now answer as themselves under this test.

353 -> 354 tests, 354/354 green.
2026-08-08 10:21:22 -07:00
mvschwarz 862bfd245d 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.
2026-08-08 10:13:50 -07:00
mvschwarz 6fc145b608 The pathname does not exist downstream, and the TDZ judge is the evaluator
Round 5, built to PM's ratified criterion and approved proposal. Both findings
were the same disease — a guard trusting a region or a spelling — and both are
closed by removing the material the violation needs, not by a fifth guard.

PROPERTY 1 (MUST-FIX): the API flow holds no pathname AT ALL. Routing is now
resolveApi() in app/verbs.mjs — the grammar module — which takes the raw
request URL, matches the table, and returns an execute closure. Handlers
receive { searchParams, verb, rest }; verified first that no handler used
anything but searchParams. handleRequest's api flow has neither `u` nor
API_PREFIX as a binding, and the static flow's URL is constructed AFTER the
api branch returns — that ordering is load-bearing and is recorded, with the
whole invariant, in docs/design/routing-data-boundary.md (PM's addition: a
future maintainer must know they are breaking the property on purpose).
sdk-qa's derived-prefix arm — reproduced on the unfixed tree first: 200,
unreported — is now UNWRITABLE at the ordinary place: `u` is in TDZ and
API_PREFIX is not a binding, so the plant detonates as a ReferenceError on
first request and never serves. The unwritability control asserts exactly
that for both their round-4 literal and round-5 derived-prefix plants, kept
permanent. New property tests pin routing to its definition: routes=[] misses
everything, routes=[X] resolves exactly X, and the resolved request carries no
pathname field. 404 body byte-identical; handler signature change is internal
shape (PM confirmed no contract-doc change).

PROPERTY 2 (HIGH): the static TDZ analyzer is DELETED — the fourth deletion on
this branch, per the pattern that a thing that keeps needing guards should
stop existing. Four static versions each classified source text and each
missed the next ordinary spelling (misplaced walk, direct initializer, IIFE
initializer, standalone IIFE statement — reproduced on the unfixed tree first:
boot exit 1, analyzer 0 violations). The guard is now the evaluator: spawn the
module, assert it evaluates; the control plants all four historical spellings
and asserts Node refuses each naming the binding. Coverage of future spellings
is the evaluator's by construction — the check is not a model of evaluation,
it is evaluation. Stated trade: no early failure on merely-fragile
arrangements; the position discipline stays as the init-block comment.

350 -> 352 tests, 352/352 green.
2026-08-08 00:50:37 -07:00
mvschwarz ab936f810c No trusted interior: routing is one lookup, dispatch receives a row
Fourth round from sdk-qa, and PM named why the previous three failed
identically: each fix defined a trusted REGION — a spelling list, a fenced file
region, then the dispatcher body — and the next raw arm simply moved inside the
newest region. A trusted region is always one special-case away from wrong,
exactly as a denylist is always one spelling away. All three findings are that
shape; all three are closed by enforcing the property instead.

1) MUST-FIX: a raw arm inside dispatchApi was fence-exempt — sdk-qa planted one
after the namespace check and got 200/unreported/unnamed with 348 green,
recreating the shipped public-404/internal-200 split (reproduced here before
fixing). Per PM's ruling, ROUTING and DISPATCH are now separate: the single
API_ROUTES.find() in handleRequest is the only operation in the runtime that
matches a pathname to a handler, and dispatch receives the resolved ROW.
dispatchApi is deleted — there is no dispatcher body left to trust. The fence
now contains DATA only (API_PREFIX, CONTRACT_VERB, the table); nothing
executable with unbounded reach lives inside it, so "a route cannot serve
without a row" is structural: writing a special case anywhere in the dispatch
path requires typing an /api literal outside the fence, which fails the suite.
sdk-qa's plant is kept as the STANDING fifth form of the bypass control, at the
ordinary place — inside the routing block — booted live and required to be
named.

2) MUST-FIX: a dangling symlink was accepted at declaration and became an
out-of-root write target once its outside target appeared — realpath fails the
same way for nothing-there and for a broken link, so the ancestor walk stepped
OVER an existing unresolvable component and settled on the root. The walk now
distinguishes absent from broken: it may step over what does not exist, never
over what exists (lstat) and cannot be resolved (realpath) — dangling links and
link loops are unverifiable at declaration, and the contract assigns
containment to declaration. Tests cover the link-as-target, the child-below-it,
QA's full two-phase timeline (refused before AND after the outside target
exists), the live in-root link positive control, and multi-binding composition.

3) HIGH: an IIFE initializer crashed boot while the guard passed — the
exemption keyed on how the initializer's text BEGAN (a spelling), and the body
stripper removed bodies without asking whether they were invoked. The property
is "does this evaluate at initialization": only provably-deferred bare function
expressions are exempt (a bare arrow cannot be immediately invoked; a bare
function expression defers only when the statement ends at its closing brace),
and stripFunctionBodies keeps any body followed by an invocation paren.
Controls plant both IIFE spellings and both bare negatives; sdk-qa's exact
plant against the real source is named (statement -> handleRequest), and the
three legitimate IIFE initializers already in the file (OVERLAY_DIR, roots,
SDK_VERSION) scan clean because their references are all earlier.

348 -> 350 tests, 350/350 green.
2026-08-08 00:20:41 -07:00
mvschwarz 805b8b348e The fence exempts line SHAPES, never scanned-for comment markers
Self-audit after orch's warning that the fence is the next target: the
quote-aware trailing-comment stripper read a regex literal containing // as a
comment start — `const cut = /\/\//; if (u.pathname === "/api/x") …` was cut
before inspection and a live raw arm evaded the tripwire entirely. Verified
against the shipped fence before fixing: evasion caught = false.

The exemption is now pure-comment LINES only, decided by shape (^\s*//), with
no attempt to find a comment inside a code line. A code line mentioning /api in
a trailing comment gets flagged too — over-blocking is safe, a cut is a hole,
and the parser you almost wrote is the attack surface.

The evasion is the fourth planted form in the bypass control, booted live like
the other three: it serves 200, is absent from runtime.routes, and the fence
must name it or the suite fails.

Honest residue, stated rather than implied: the fence catches the literal every
innocently-typed arm carries. Deliberately obfuscated literals (concatenation,
unicode escapes) are past any textual tripwire; the table mechanism and review
are the layers for that class.

348/348 green.
2026-08-07 20:53:07 -07:00
mvschwarz e34d3eeaed The table IS the router: routes cannot exist without being enumerated
sdk-qa returned 93b5612 with three findings of one shape — each earlier fix
handled the instance, not the class. All three closed here, each with a control
seen failing on the broken version before the fix was written.

1) Capture targets: performability, not the final component. The isDirectory
check covered "the target is a file" and missed "the deepest existing ancestor
is a file" — declare file-parent/child and the 200 hands the consumer a
guaranteed ENOTDIR. resolveInRoot now requires every existing component of the
resolved path to be a directory: an existing target must be a dir, a missing
target's deepest existing ancestor must be a dir. Unperformable bindings lose
to performable ones exactly as existence decides between bindings generally,
and blocked-everywhere refuses naming the blocking file. Tests for both, plus
the multi-binding composition.

2) Route discovery: PM's ruling, opt-in IS the hole. serves() was a
registration wrapper a raw handler could skip — sdk-qa planted three spellings
that served 200, appeared nowhere in runtime.routes, and passed the suite,
reproducing the original product failure. The wrapper is DELETED, not patched:
API_ROUTES is now a declarative table that request dispatch walks AND that
runtime.routes/classification enumerate — one structure, two readers, so a
route cannot serve without a row. The boot-time enumeration walk is gone with
it (the set is complete at load by construction), which also removes the
top-level await that caused the TDZ boot failure two commits ago.

What a table cannot prevent is a raw arm typed above the dispatch — so the
routing surface is fenced by markers and routingSurfaceViolations() (verbs.mjs)
fails the suite on any /api mention outside the fence, in every spelling,
replacing discoverApiRoutes (a per-spelling regex, the approximation PM ruled
against). The bypass control plants sdk-qa's exact three raw forms, boots each,
and asserts orch's dichotomy: served implies reported-or-named, else 404.
Run against 93b5612 it fails with the finding's own words: serves 200, not in
runtime.routes, tripwire blind.

3) The TDZ guard now covers INITIALIZERS. `const probe = handleRequest;`
executes at module load exactly like a statement; the previous scan excluded
declarations wholesale, so the runtime crashed while the guard passed 2/2.
Initializers are scanned for references to later bindings — except pure
function expressions, whose bodies run at call time, with embedded function
bodies and string contents stripped so handler tables do not false-positive.
Controls: the planted initializer fires (statement N names handleRequest), the
arrow initializer does not, and sdk-qa's exact plant against the real file
reports the probe line naming handleRequest.

contract-meta.md's runtime.routes bullet now states the by-construction claim
and records why the registration wording was false.

344 -> 348 tests, 348/348 green.
2026-08-07 20:50:00 -07:00
mvschwarz 93b561204b Boot again, and make the ordering guard name the property that actually failed
The runtime died at load: the boot-time route-chain walk sat in the init block
above `const handleRequest` and called it in its temporal dead zone — the fifth
TDZ from this file, created while satisfying the position guard that exists to
prevent it. The guard asserted POSITION ("startup after every module binding")
and matched only two whitelisted call names, so it never saw the statement that
carried the defect, and being "at the bottom" of the known bindings read as
compliance while being before the one binding the statement touches.

The arrangement now: ALL module-level executable statements — init calls, the
two intervals that sat mid-file, the walk, .listen — live in one init block
after the last declaration, and the walk runs after `handleRequest` (which it
calls) and before .listen so the first request sees the complete route set.

The guard now checks two properties and can be seen to fire:
- REACHABILITY: no module-level statement references a binding declared later.
  This is the property whose violation IS the boot ReferenceError; its failure
  message names the binding and says that being last is not the same as being
  after everything you TOUCH. Run against the broken tree it reports exactly
  statement 1051 → handleRequest@1057.
- POSITION: every executable statement follows every binding — kept because it
  also covers what startup's callees may GROW to touch, which no reference scan
  of the call site can see.
- It recognises every top-level executable statement (bare calls, awaits,
  chains), not a name whitelist, and the analysis is a function a positive
  control runs against a planted violation.

Also: contract-response-parity's namespace-advertisement check keyed on a regex
over source (`u.pathname === "..."`) that found 0 routes once the arms went
through serves(). It now reads `runtime.routes` from /api/contract — the
router itself, the same one source PM ruled classification must use.

343 -> 344 tests, 344/344 green.
2026-08-07 20:24:38 -07:00
mvschwarz 4d231186e6 WIP — CURRENT BOUNDARY, TREE IS RED. Do not build on this without reading below.
Committed at operator instruction to preserve work at a context boundary. NOT
pushed. Main untouched at ce6476b.

⛔ THE RUNTIME DOES NOT BOOT. `node app/serve-studio.mjs` dies with:
     ReferenceError: Cannot access 'handleRequest' before initialization
     app/serve-studio.mjs:1051
Suite is 227/343 as a direct consequence — nearly every failure is "server never
came up", not independent defects.

THE CAUSE, exactly, and the one-line fix:
I added a boot-time walk of the routing chain so `runtime.routes` is COMPLETE
rather than "whatever has been hit". My own TDZ guard then failed, correctly,
because the walk sat before later module bindings — so I moved it into the bottom
init block beside loadAnnotations(). But `handleRequest` is a `const` declared
BELOW that block, so the walk now references it in its temporal dead zone.

FIX: the walk must run after `handleRequest` is defined and before `.listen()` —
that position satisfies both constraints. It is one move, not a redesign.

FIFTH TDZ FROM THIS FILE, and the sharpest: I created it while satisfying the
guard that exists to prevent it. Moving startup work "to the bottom" is only
correct relative to the bindings it actually touches, and the guard checks
position, not reachability.

WHAT IS GOOD IN THIS COMMIT (was 340/340 green before the move):
- classification now derives from the RUNTIME'S OWN ROUTE TABLE, not a regex.
  Every /api arm goes through serves(), which matches on the verb grammar and
  registers; /api/contract reports runtime.routes. This is PM's bar.
- the control boots a runtime with LIVE planted routes of BOTH forms (exact and
  prefix), verifies they answer 200, and asserts the runtime reports them as
  unclassified. The previous control fired on the wrong stage of the pipeline.
- prefix-aware reservation matching (isRuntimeOwned/verbMatches) — an exact
  Set.has() cannot express a parameterized runtime verb.
- an existing FILE refused as a capture target at declaration, with tests
- duplicate/symlinked bindings deduped by RESOLVED path, with tests
- contract: substituting an implementation inherits its WHOLE contract, including
  the parts your own consumer never exercises (studio-impl's html finding)

STILL NOT DONE: nothing beyond the boot fix is required for the mechanism itself.
2026-08-07 20:11:33 -07:00
mvschwarz 77c16e8561 Reserve verbs by construction, and catch the next omission
sdk-qa returned the branch with 3 MUST-FIX and 2 HIGH, all mine, all with
reproducers, and all written from the spec before the diff was read.

TWO OF THEM WERE THE SAME META-PATTERN AND PM RULED THE MECHANISM, NOT THE
INSTANCES: a guard that existed elsewhere did not reach new code.

1. /api/capture-target was missing from SDK_OWNED, so a one-provider studio routed
   it to a provider that never declared it — public 404 over a capability
   /api/contract advertised, while the runtime answered 200 internally. focus and
   drive were fixed this way and the fix DID NOT GENERALISE, because the guard
   filtered routes against a HARDCODED LIST: a verb nobody added was never checked.

   Now there is ONE declaration, app/verbs.mjs, and the compositor DERIVES
   SDK_OWNED from it rather than restating it. Every served route must be
   classified runtime-owned or substitutable, and an unclassified one FAILS — that
   is the check that catches the next verb rather than the last one, with a control
   that plants an unclassified route to prove it can fail.

5. Root kinds are an OPEN vocabulary, so `constructor` is a valid spelling — and on
   a plain object it is INHERITED, turning an honest 400 into a 500. Already
   guarded in the SDK's other root-binding code; the newer map reopened it. Now a
   shared openVocabMap/lookup primitive with its behaviour asserted, not just its
   presence.

2. FILE-VIEWER RACE. The modal is shared and the read is async: click a slow file
   then a fast one and the slow response lands last, writing its body under the
   newer title. For a review viewer that labels one artifact with another's
   filename — complete, consistent and wrong. A generation guard now discards a
   superseded response, including a superseded FAILURE, and closing invalidates
   in-flight reads.

3. MULTI-LOCATION ROOTS silently picked the first binding. Containment holds for a
   path that does not exist yet — which must stay allowed — so the first location
   swallowed everything and later bindings were unreachable even when the target
   existed there. Existence now decides; existing under two bindings REFUSES rather
   than choosing, because this contract's own line is that landing somewhere
   unexpected is worse than refusing.

4. CONCURRENT DECLARATIONS all resolved from the first response — one
   indistinguishable listener per call, and no parent-source check. Each call now
   carries an id the shell echoes, and only the parent is heard.

AND THE METHOD, recorded in runtime-api.md rather than left in a report: prove SDK
verbs on a SINGLE-PROVIDER composition with a public-versus-internal port check. A
multi-provider dev box masks this entire class — something always answers, so the
surface looks healthy. That is why sdk-qa found finding 1 and I did not.

Also: the runtime is no longer one file. A harness copying serve-studio.mjs alone
now fails with ERR_MODULE_NOT_FOUND, which is how I found the one that did.

339 tests.
2026-08-07 19:20:45 -07:00
mvschwarz b83a71359b Say that the file viewer is only as reachable as the bound roots
studio-impl adopted file-viewer and hit this on a real install: the component was
correct and had nothing reachable to open. Proof and feedback files lived in a
mission workspace no bound root covered, so every chain entry would have failed
through the root-pinned files verbs.

Invisible from the SDK side, which is why it needed saying. The dependency runs
from a component this repo ships to a binding that lives in the CONSUMER's
composition, and nothing in between reports it: the viewer looks broken and the
roots look irrelevant.

Named in runtime-api.md beside the verbs it depends on, and at the top of the
component itself — the two places someone debugging it will actually be. With the
instruction that matters: if the viewer fails on EVERY entry, check the roots
before checking the viewer.

Found by adopting it, not by reviewing it. The two defects that blocked item 12
were both in the consumer — escaped markdown links leaving fileLink nothing to
attach to, and this — and neither is visible from here.

Doc only; 329 tests.
2026-08-07 18:18:54 -07:00
mvschwarz 6e824339e9 Say how to rasterise a marked-up surface, and why not the obvious way
Measured while probing whether the founder's pause-and-capture is buildable. The
constraint is not where it looks: it is the SERIALISATION PATH, not the element.

An SVG containing <foreignObject> exports fine through a data: URI and TAINTS
through a blob: URL. Plain shapes are clean either way. A same-origin image or
video frame draws directly. An <iframe> element cannot be drawn at any origin.

I nearly wrote "foreignObject taints" instead, which is the obvious summary and is
FALSE. Text marks use foreignObject so they WRAP; a contract saying they taint
would have pushed the next person back to a single-line <text> that runs off its
box. A contract that forbids a working technique is worse than a missing note,
because nobody re-tests a documented prohibition.

The competing explanation is recorded as false rather than omitted: "it taints
when it pulls an external resource" sounds right and was offered with a real
measurement behind it. Falsified directly — no-external, same-origin img and
cross-origin img all taint through blob:, none through data:. A wrong mechanism
with a sound number attached is the kind that gets written into a contract,
because the number is checkable and the story rides along unexamined.

And the rule carries its reason, at studio-impl's ask: blob: is the CONVENTIONAL
optimisation for a large SVG, so someone will make that swap as a tidy-up and
silently break capture for any artifact carrying a text mark. Nothing throws at
the swap; the export just starts failing later. A rule without its reason reads as
arbitrary preference and gets optimised away.

Also states that a video must reach readyState >= 2 before it is drawn — drawImage
on an empty video does not throw, it draws nothing, which is a false positive I
reported to myself before catching it.

Doc only; 329 tests.
2026-08-06 17:10:44 -07:00
mvschwarz 88e7dfebad A surface may ask for markup, as it may already summon the agent sidebar
studio-impl built a Mark-up button into their artifact modal and deleted it: the
surface->shell set is closed and there was no verb for it. The only route was the
user knowing to reach for the header — which a surface that has just opened an
artifact has good reason to shortcut, because it knows the user is looking at
something markable and the header does not.

Added mirroring `open-agent`, which already lets a surface summon a studio-level
control. That precedent is the argument: if a surface may ask for the agent
sidebar, asking for markup is the same act on the same channel with the same
guard.

It is a REQUEST, not a seizure. Only the ACTIVE surface is heard, the shell keeps
the state, the header button stays the visible truth, and a surface can neither
read the state back nor hold it off for the user.

The click and the message now go through ONE setter. Two places flipping that flag
is two places computing one property, and the second one forgets a listener — the
test asserts `markupOn` is assigned exactly twice, its declaration and the setter.

Also documents the pattern studio-impl found, in annotations.md: an app that opens
a document in its own modal declares annotation-target and annotation-context per
navigation and hands both back on close, so a mark lands on the picture rather
than the panel around it. Recorded rather than left to be rediscovered — the seam
is only obvious once you have needed it, and they needed it before I documented it.

329 tests.
2026-08-06 17:02:49 -07:00
mvschwarz b024894daf Capture targets are declared as a root KIND plus a relative path
SDK ASK 1, in the shape PM ruled after the deviation. The ask arrived as
`declareCaptureTarget(dir)` taking a raw directory, which app-manifest.md:213
forbids in as many words — "roots are KINDS, never paths … works on exactly one
machine" — and which is independently an unbounded write surface: an app could
name any directory on the box.

Refused at DECLARATION, not at write time. A target that cannot be EXPRESSED
unsafely cannot be INHERITED unsafely by whatever eventually captures, and only
one of those two checks is impossible to forget.

TWO THINGS THIS NEEDED THAT THE ASK DID NOT READ LIKE. The runtime had never been
told about roots — the launcher resolved them and kept them, which was fine while
nothing declared a path at runtime — so studio.mjs now passes them. And the
containment walks to the deepest EXISTING ancestor, because a slice's feedback/
legitimately does not exist before anything is captured into it; refusing there
would invert the order an app actually works in.

Containment resolves BOTH sides with realpath, so a symlink inside the root
pointing out is refused — a lexical startsWith follows one straight out and
reports success. Pinned with a positive control beside the escapes, because an
over-restrictive boundary and a correct one are indistinguishable under an
attack-only test and this repo has shipped that mistake before.

An unbound kind is a refusal, never a fallback: landing captures in an unexpected
directory is worse than not capturing.

THIS VERB WRITES NOTHING and the contract still has no write verbs. It records
where a capture WOULD go. /api/contract reports capture.consumedBy: null because
no capture action exists in this runtime — the seam is observable rather than
inert, which is the honest interim PM accepted and the reason it is explicitly NOT
lockable until CAPTURE (studio-impl's, §5c) consumes it end to end.

328 tests.
2026-08-06 15:30:48 -07:00
mvschwarz 8061ff42d6 A shared file viewer, so two apps do not render "a file" two ways
SDK ASK 2 from the proof-app PRD (§5e). The review app renders a feedback chain
whose entries are files; the next app will render a different list of files. Two
apps rendering a file two ways is how a studio stops feeling like one application.

It runs in the APP's frame and draws no annotation UI. The shell's overlay already
sits above the stage, so a viewer opened here is beneath it and can be marked up
without this file knowing annotation exists — which is what makes the app's "ships
zero drawing UI" achievable rather than aspirational.

TWO DECISIONS THAT ARE SAFETY, NOT STYLE:

HTML renders in a sandboxed iframe via srcdoc, never innerHTML. An HTML proof
artifact is a document from somewhere else; injecting it would hand it this app's
origin and this app's verbs. Verified against a file carrying a <script>: not
injected into the app DOM, script did not run, app title unchanged, frame origin
opaque.

Markdown ESCAPES FIRST, then formats — the order is the whole property. A renderer
that formats first emits whatever HTML the file contained. Link hrefs are limited
to schemes that cannot execute. Verified against a hostile file: javascript: link
stripped to text, raw <img onerror> and <script> not rendered as elements, no XSS
global fired, markup shown as literal text, which is the right answer for a proof
artifact.

Kind comes from the runtime's own answer, not from the filename — the verb already
decided, and re-deciding by extension disagrees first on the files that matter: a
.txt holding a terminal capture, a .md that is really a log.

Chain entries are ICON + FILENAME, never thumbnails, per the PRD. A wall of
thumbnails turns an ordered story into a contact sheet.

An unreadable file says so and says it is NOT an empty file. Closing empties the
body so media stops.

TWO OF THESE TESTS FAILED ON THEIR OWN EXPLANATORY COMMENTS the first time they
ran — a keyword assertion cannot tell a comment describing a property from code
violating it. They now match comment-stripped source, and the one check that could
not be expressed that way was deleted rather than kept as a guard that trains
people to reword honest prose.

Ask 1 is NOT in this commit: its shape was under a PRD deviation and is now ruled
kind-bound; building next. 317 tests.
2026-08-06 15:24:23 -07:00
mvschwarz ce6476b85c runtime.version identifies the release, not the bytes
The field says it tells you which build is running. studio-impl's checkpoint
mechanism makes that not quite true: they patch their installed copy from a
committed patch re-applied on install, which is the disciplined form of a practice
this repo has otherwise paid for — declared, reproducible, and it fails the install
rather than half-applying.

But a patched package still reports the version in its manifest. So the field
narrows which RELEASE you are looking at and does not prove the files match it,
and it would be least accurate exactly when it is most used: a bug report from a
box whose shell is not the shell at that SHA.

Stated rather than fixed. Making the runtime hash its own files to detect a patch
would be inventing a guarantee nobody asked for, against a practice that is
legitimate; scoping the claim honestly is what keeps the field credible. Same
reason the consumer-seam guarantee excludes a modified runtime source.

Doc only; 309 tests.
2026-08-06 02:48:27 -07:00
mvschwarz ceb2b1afbc The rail says which studio this is and which build is running
Founder-directed, prototyped by studio-impl. Rail title becomes OPENRIG STUDIO;
the line under it becomes the live SDK version; the collapse control moves to the
top-right of the rail head with the conventional panel glyph; the foot carries the
wordmark, tagline and links.

runtime.version IS CONTRACT, and that was the open question. Accepting it as one:
this contract already tells consumers not to write a version in prose and to read
it from the package itself — a remote consumer had no way to do that, and now does.
Documented in contract-meta.md with the guard that matters: it is INFORMATIONAL and
must never carry a compatibility decision. That is what capabilities and
contractVersion are for, and they answer the question this field looks like it
answers. A consumer gating on a package version writes a check that passes on the
reference runtime and refuses every other implementation of the same contract.

Read, never typed — resolved from the package.json beside the runtime at boot, null
when it cannot be. Verified all three sources agree: package 0.8.0, /api/contract
0.8.0, rail SDK V0.8.0.

A DEFECT IN THE HANDOVER THAT MEASURING FOUND: setRail assigned
`railBtn.textContent` on every state change AND on boot, so it destroyed the inline
SVG before anyone saw it. The served HTML had no hamburger and the live DOM did —
reading either alone gives the wrong answer, which is exactly what happened to me
twice before I compared them. The glyph is markup and stays markup; JS sets only
the label, and direction is conveyed by mirroring the glyph in CSS. Verified across
expand -> collapse -> expand: SVG present in all three, hamburger in none.

TWO TEST FIXTURES MADE FAITHFUL RATHER THAN TWO CHECKS LOOSENED. The zero-config
and parity harnesses copied app/ and fixtures/ but not package.json, so the runtime
correctly answered "version unknown" and parity read that as divergence. A real
install always has it beside app/; the harness was testing a layout that never
ships. The zero-config runtime-block check NAMES its intended additions rather than
allowing any, which is deliberate, so `version` is declared there — a third
unannounced field still fails.

309 tests.
2026-08-06 02:29:05 -07:00
mvschwarz 85d8424d0e The rail runs the full height and the header starts beside it
Founder-directed, prototyped and measured by studio-impl, routed here rather than
landed because app/shell.html is this repo's. The header no longer spans the full
width; the rail goes to the top of the page; the collapse control moves onto the
rail.

#app becomes a row with the rail as its direct child, and a new #maincol holds the
header above #shellframes beside it. The launcher is now one continuous edge from
the top of the window, and the header belongs to the surface it sits above rather
than reading as a second band across everything.

The toggle moves for a reason beyond layout: among MARKUP and AGENTS it read as a
studio-level control when it is only ever about the launcher. A control belongs to
the thing it acts on.

ONE CHANGE TO WHAT WAS HANDED OVER, which studio-impl offered: their patch added a
second #rail-head block beside the original rather than restructuring, and the same
would have applied to body.rail-collapsed #rail-head. Two rules for one selector is
two places computing one property — the shape this repo has already paid for three
times — so they are merged. The `font-size: 0` that used to hide the head text goes
with it: a .headtext wrapper hides explicitly, and an inherited-zero trick sitting
beside a display:none that already does the job is one of the two that drifts.

MEASURED IN A LIVE SHELL, both states:
  expanded  : rail 212x720 (full viewport height), #topbar.left = 212
  collapsed : body.rail-collapsed, rail 52x720, #topbar.left = 52, glyphs intact
Every header control hit-tested at its own centre and reachable — box placement and
"is this the element a click reaches" are different claims, and an overlap in this
same file was found that way before.

AND THE THING THE RESTRUCTURE COULD MOST PLAUSIBLY HAVE BROKEN, checked rather than
assumed: an agent mark anchored to an element in the surface landed with a gap of
0x0 after #stage moved 212px right. The overlay mounts on #stage, not on #app or
#shellframes, so the outer restructure does not reach it — studio-impl said so and
it is now measured and pinned.

309 tests. The committed checks are structural and say so.
2026-08-06 02:10:15 -07:00
mvschwarz a7970dc4e0 Record the reproduced routing gap where the stale claim can be read
The commit that added test/runtime-ownership.test.mjs states the 404 symptom was
unproven. studio-impl then reproduced it against the unguarded tool — the only
place it can be shown, since the guard prevents it — and the evidence went into a
private working note.

That was the wrong home and studio-impl was right to say so. A reader of a public
commit will never open a private repo's notes, so the correction was technically
recorded and structurally unreachable: the same shape as a control left behind in
the repo its code moved out of. It is also my own banked rule turned around — a
retraction must be as WIDELY DISTRIBUTED as the claim it retracts, not merely as
durable — and I had applied it to durability alone.

A commit message cannot be corrected without moving a pushed SHA, so the durable
home is the file the claim is about. The table now sits beside the case it
qualifies, where anyone touching this test will meet it.

The measurement note travels with it, because it is the part that generalises: the
verb row shows NO DELTA — 404 before and 404 after — and the finding is the
COMBINATION of the three rows. B answering 200 for the same verb with the same
probe is the control that makes the after-404 a real negative rather than a probe
reading nothing. A reading that is identical before and after, where what changed
is what the reading MEANS, is not caught by diffing faithfully.

No behaviour change; 302 tests.
2026-08-06 01:46:37 -07:00
mvschwarz bd9debce8f A second studio refuses .runtime/ instead of repainting the first's rail
studio-impl found this and the founder found the symptom by opening it. Two
studios sharing a STUDIO_DIR share .runtime/, and composing is destructive: the
second to boot repainted the first's rail while the first kept serving the routing
table it built IN MEMORY at its own boot, and never reads that directory again.

The result is an app that is PRESENT, LOOKS INSTALLED, AND CANNOT WORK — a
PROVIDERS tab that loaded 200 while every verb behind it 404'd, because the process
showing the row had never heard of the provider. No check caught it and none of the
obvious ones could: the row is real, the page is real, the 200 is real. Same family
as the systemd unit hijack — a second instance quietly overwriting a first's state,
invisible because the running process keeps answering.

The second boot now refuses, by the rule already applied to ports and units:
REFUSE RATHER THAN ADOPT. A stale stamp — one whose port nothing is serving — is
taken over and SAID OUT LOUD, because a silent takeover is the same silence this
guard exists to remove.

LIVENESS IS THE STAMPED PORT, NOT THE PID. A pid can be reused by anything, so a
dead studio whose number came round again would refuse a legitimate boot forever.
The question that matters is whether something is still SERVING from this
.runtime, so that is the one asked — and it deliberately does not ask WHO answers,
because a stranger on that port is still a reason to refuse and asking the studio
would mean trusting a reply from the process we are unsure about.

REPRODUCED BEFORE FIXING, and the control is the point: with the stamp removed, a
studio composing ["files"] on one port silently changed a live studio's rail from
["workspace"] to ["floor","files"] while it went on serving. That is the defect,
measured.

SCOPE I DID NOT PROVE, stated because the commit would otherwise imply it: I
reproduced the REPAINT, not the 404. My fixture used an app whose verbs the
reference runtime answers itself, so it returned 200 and masked the routing gap;
the 404 needs a provider-backed app. The repaint is the cause and is what this
removes.

Also the first behavioural test tools/studio.mjs has ever had — it is a script with
no exports, so its boot has been hand-verified until now.

302 tests.
2026-08-06 01:31:06 -07:00
mvschwarz 02d7c9e470 The persistence check cannot be silent any more
orch named the shape in one line: silence from that check meant BOTH "verified"
and "could not ask", so the two outcomes were indistinguishable — a check that
cannot fail, which is the same defect class as the one it was added to catch.

The query lives behind `if systemctl ... 2>/dev/null; then`, so a failed query
skipped the whole block and printed nothing. On a host with no user manager — a
container, or systemctl-present-but-unreachable, both already handled elsewhere in
this script — the run would look exactly like a verified one.

Three outcomes, three distinct lines, none of them silence: systemd agrees;
systemd disagrees (a reboot would start the wrong studio); or the query failed and
persistence is NOT VERIFIED, with the reason quoted. The last one says explicitly
that the unit FILE was checked and what remains unconfirmed is whether the manager
picked it up — a daemon-reload that did not take looks identical to success from
every runtime check.

Driven against four fake systemctl behaviours including the worst one: a non-zero
exit with NO message on stderr, which now reports "non-zero, no message" rather
than falling through quietly.

I had asked cloud-impl to watch this line on a real box to settle whether it
degrades quietly. Removing the ambiguity is better than asking someone to observe
it — the observation would still have to distinguish two silences.
2026-08-05 13:55:28 -07:00
mvschwarz 62b904161b One systemd unit per studio, so a second provision cannot eat the first
cloud-impl found this on a live box with a reboot scheduled and held, and it would
have eaten that reboot. The unit was named `openrig-studio.service` for every
studio, so provisioning a SECOND studio on the same box overwrote the FIRST one's
unit and daemon-reloaded it. The old process kept running.

WHY NOBODY WOULD NOTICE, and this is the part worth keeping: everything anyone
would check said fine. `is-active` reported active. The original port answered 200.
Both were measuring the OLD PROCESS. Only `show ExecStart` disagreed, and nothing
was looking at it. The damage stays invisible until a restart, at which point the
box starts the second studio and the first never comes back — on that box the live
studio was serving happily while its unit already pointed at a test studio that had
FAILED its own verification.

The unit name is now derived from STUDIO_DIR: stable across reprovisions of one
studio, distinct between different ones, and printed by the run. One box with
several studios is a normal thing to want — testing a provision beside a live one
is exactly how this was found.

A unit belonging to a DIFFERENT studio is REFUSED rather than overwritten, which is
the rule the port guard already applies to a foreign studio: refuse rather than
adopt. Rewriting our own unit stays allowed; that is an ordinary reprovision. The
legacy shared-name unit is retired only when it names THIS studio — when it names
another one it is left strictly alone and reported, because that is precisely the
studio this defect used to steal.

AND THE CHECK THAT WOULD HAVE CAUGHT IT, added: verify the DEFINITION against
intent rather than the runtime against hope. The written unit must name this
studio's dir and this run's port, and after enabling, systemd's own loaded
definition is compared too — a daemon-reload that did not take leaves the manager
holding the old one while every runtime check still looks healthy.

Driven against a fake HOME with five cases including the negative control that
reproduces the original loss: under the old shared name the first studio's
persistence is gone, which is what a reboot would have booted. Linux target,
Darwin host, so the block was driven directly rather than the provision run end
to end.
2026-08-05 13:51:08 -07:00
mvschwarz 6e0da5037c Presence is not meaning: three provisioning defects from a real VPS
cloud-impl found all three provisioning a real VPS, named the shared shape,
and reported two of them against their own code. They are the same error at three
layers: something EXISTS, so it is treated as valid, and the check cannot tell
"declared and incomplete" from "declared absent" or "present and unusable".

D11 — install-app.mjs refused a deliberate "no provider". `apps/workspace` declares
`provider: { required: false }` with a note saying it needs none, and the checker
read the KEY'S PRESENCE as a declaration: three failures against a manifest that
was documenting its own correctness. WORKSPACE was uninstallable on any box. A
provider is declared by a PACKAGE, not by a key existing. One shared predicate
rather than two edits, because two sites deciding one question is the drift shape
this repo has already paid for three times — reverting either site alone still
brings failures back, which is how I know both mattered.

D9 — the provider branch tested whether a lockfile EXISTS, not whether it WORKS,
and committed to `npm ci` on that basis. A stub lockfile locking nothing sailed
past the file test and died inside npm. Present-but-unusable is worse than absent:
absent fails where the branch already handles it. Now it tries the exact path,
SAYS the lockfile was unusable, and falls through. Verified against real npm and a
real stub lockfile, with a control proving npm ci genuinely refuses it.

D10 — dependencies were installed for every provider on disk regardless of which
apps were enabled, so a provider nobody asked for could stop the whole provision.

D10 IS ALSO A CORRECTION TO MY OWN CLAIM, and that is the part worth recording.
I shipped the curated four-app default saying "two providers, no ffmpeg" — in the
README, in the commit message, and in my report to orch. It was FALSE: the app list
narrowed, the provider install did not, so all seven providers' dependencies landed
anyway. I asserted a benefit I never measured, in the same change where I was
careful to log what the cap dropped. The claim is true now because this makes it
true, not because it was true then.

Resolution walks JSON in node and is read line by line, never word-split. It
covers both ways an app needs a backend: naming one, and REQUIRING a verb somebody
else serves — `calls` is provider-agnostic by design, so an unserved required call
means reconciliation refuses the install. Optional calls pull nothing, mirroring
the ladder in app-manifest.md where only `required: true` may start a provider no
app names as its own.

298 tests. The shell paths were driven directly against fixtures and real npm;
this targets Linux and the host is Darwin, so the provision was not run end to end.
2026-08-05 13:34:59 -07:00
mvschwarz 9c824a7b06 The header says when it drops an action instead of dropping it in silence
studio-impl found this by building against it: the shell renders at most six app
actions and `decl.actions.slice(0, 6)` discarded the rest without a word. A surface
declaring seven rendered six and had no way to discover it — not in a DOM it cannot
see, not in any response, not anywhere. The seventh button simply never worked,
which reads as a bug in the app rather than a limit of the shell.

The bound itself is right and stays: the left half shares one fixed bar with the
title, the crumb and the shell's own half, so an unlimited list pushes into
controls the surface does not own. The silence was the defect.

Told on BOTH channels, because they reach different people. A console warning
naming the dropped ids reaches whoever is building the app right now. A
`{t:"header-overflow", limit, declared, dropped}` message reaches THE APP, which is
the only party that can adapt — and "an app cannot see the cap to respect it" was
the actual complaint, which a doc alone would not have answered.

The number is now a named constant rather than a literal inside a slice. That is
why it went undocumented: it existed in exactly one place with no name to look up
and nothing for a doc to point at. A committed test reads the shipped limit and the
documented one and compares them, so the two cannot drift.

WHILE DOCUMENTING IT I FALSIFIED A NEIGHBOURING SENTENCE AND FIXED IT:
shell-protocol.md said `markup` "is the one message that travels in that
direction". Adding a second made that false in the same commit that made it false.

Verified in a live shell rather than reasoned about — `tellSurface` is a const
declared after renderAppHeader's first caller, and this file has shipped four
TDZs. A surface declaring eight actions rendered six, received
{limit:6, declared:8, dropped:["a7","a8"]}, and the console carried the warning
naming them. The committed checks are structural and say so, each with a control.

290 tests.
2026-08-05 06:43:53 -07:00
mvschwarz f6f3c66d5d The front door names what the repo now ships
`0a104fe` bundled an app and a provider into this repository and the README's
"Where everything is" table still listed neither. That table is the front door —
someone reading it got an incomplete map of two top-level directories that ship.

Also states the thing the change could otherwise be read as: the SDK is still a
shell and a contract, not a framework you import. A bundled app registers a row
and calls verbs like any other, with no privileged access. VERIFIED rather than
asserted — nothing under `app/` references `apps/` or `providers/`, and SDK_OWNED
is unchanged, so the bundled app's verbs are as substitutable as anyone's.

CONFORMANCE, checked rather than assumed: I expected the drive and focus rows
saying "no shipped surface in this repository does this" to have gone stale.
Measured — ARTIFACTS uses the three annotation messages and does NOT use drive or
focus, so both rows are still accurate and are left alone. Understating the
product costs as much as overstating it, and so does correcting a row that was
right.

What DID change is that annotations now has the in-repo consumer its siblings
still lack, and that row is added — attributed to the QA seat that requested the
seams and verified the migration, since evidence produced by the author of a
feature is a different claim from evidence produced by someone else. No counts are
restated here; they belong to that seat's proof and would rot in this file.

285 tests.
2026-08-05 06:29:56 -07:00
mvschwarz 0a104fe038 Bundle Artifacts with the Studio SDK 2026-08-05 06:20:18 -07:00
mvschwarz 57efab56b5 A bare host gets four apps, not thirteen
Removing the hardcoded list made provisioning install whatever the apps repository
happened to ship — 13 apps and 7 providers on a fresh box, including a video
toolchain that wants ffmpeg and a canvas needing a licence the user has to go and
acquire. A first boot should show someone the studio works, not hand them every
dependency the ecosystem has ever accumulated.

The default is workspace, rig-designer, factory, files. Two providers, no ffmpeg,
nothing to go and get. Two of the four need no provider at all, and the other two
ride the host and factory backends — so the set demonstrates the actual product
(see your rig, drive it, browse the box, work) at the lowest possible install cost.

NOT a pinned list, which is the failure the hardcoded one already had: the apps
repository moves on its own schedule, so a provisioner asserting yesterday's names
breaks a box that did nothing wrong. The curated set is INTERSECTED with what the
clone actually ships, and a curated name that has been renamed or dropped is
reported and skipped rather than fatal.

NO SILENT CAP. The run prints what it did not install and how to ask for it.
A default that quietly installs four of thirteen looks identical to one that
installed everything, and the person who wanted canvas would have no idea it was a
decision rather than a bug. `APPS="..."` selects exactly; `APPS=all` restores
install-everything.

All five paths driven against a fake apps repository rather than reasoned about:
curated default (4 of 13), the skipped list naming what was left out, APPS=all
(13), an explicit list obeyed verbatim, and a curated app the repo dropped being
named as missing while the other three still install.

This targets Linux and this host is Darwin, so the selection block was driven
directly rather than the provision run end to end. Stated because it bounds the
claim.
2026-08-05 06:11:30 -07:00
mvschwarz 07fa5c0efd 0.8.0 — annotations
A human and an agent can now mark up the same screen, which is the gap the three
existing primitives left: focus says what the user is looking at, drive operates
the surface, and neither lets either party say 'this part, here, is wrong.'

Minor rather than patch: new verbs, new capabilities, a new contract document and
three new surface->shell messages. All additive, so contractVersion stays 0.1 — a
surface built on 0.1 stable items is unaffected and a surface that adopts nothing
behaves exactly as before.

README updated BEFORE the tag, deliberately. v0.6.0 was tagged at a tree that
still held every defect a cold build then found, and main-correct-with-the-tag-
wrong is the worse state because the tag is what gets pinned.
v0.8.0
2026-08-05 06:07:32 -07:00
mvschwarz a7d3785bec Marks can be told to re-measure when a surface moves something
studio-qa drove the nested seam and measured a repaint gap: an ARTIFACTS canvas
item moved (220,170) -> (760,430) and its mark stayed at the old rect until an
unrelated window resize snapped it. Anchoring resolved correctly and the stored
selector and offset were right — nothing was wrong except that nothing told the
layer to look again.

An element that MOVES changes no size, no scroll and no viewport, so every signal
the shell can observe for free stays silent. The mark keeps reporting `anchored`
and keeps looking correct, which is why this needed reporting rather than showing
up as an error.

Two ways to say it: `{t:"annotation-refresh"}` from the surface, and refresh() on
the handle for a caller that already holds it. Both re-measure WITHOUT reloading —
the board is not re-read and undo survives, which is the distinction from a surface
or context change where reloading is right.

Re-posting annotation-target with the same frame id stays a no-op, which is the
other half of studio-qa's finding. Overloading it as a nudge would make every
redundant declaration re-anchor and reset undo, so the nudge gets its own verb.
The layer also observes size and scroll on the anchored document; those are free
and explicitly NOT sufficient, which is why the explicit hook exists.

MEASURED BEFORE AND AFTER, both paths: element moved 300x180, mark stale by
exactly -300x-180, and both the message and the handle closed the gap to 0x0.

TWO PROBE DEFECTS, and the second is the instructive one:
- I posted the nudge via `.parent` from the top window, so e.source was the top
  window and the active-surface guard correctly rejected it. The guard working
  looked like the fix failing.
- I then measured `state().records[].anchor` — the anchor as WRITTEN, which
  correctly never changes. The DRAWN position is recomputed each render as
  (current element rect + stored offset). A correctly-following mark looked frozen
  because I was reading a field that does not decide what is on screen. Now
  hit-tested against the rendered SVG.

283 tests.
2026-08-05 06:04:13 -07:00
mvschwarz 29ebd8b01e Annotations can anchor inside a surface's nested document
studio-qa measured the gap while migrating ARTIFACTS: it renders the open artifact
in a same-origin nested iframe, so an agent naming `#publish` resolved against the
host surface's chrome instead. That loses the property the whole feature exists
for — an agent names an element and the human sees the mark on THAT element.

It fails in the worst available way. If the host carries the same id, the mark
renders, reports `anchored`, and sits on the wrong element: nothing errors and
nothing looks degraded. The browser proof is built around exactly that — a host
surface and its nested document BOTH carrying `#publish`, with the resulting mark
hit-tested against both boxes. It landed on the artifact's button (y=256, inside
the artifact box) and not the host decoy (y=56).

A surface names the frame: `{t:"annotation-target", frame:"artifactFrame"}`. It
does not hand over a document and does not compose the persistence scope, which is
studio-qa's own boundary and the right one. Orthogonal to `annotation-context`:
the context says WHICH BOARD, the target says WHERE TO LOOK, and a surface can
need either without the other.

The SHELL resolves the chain, because a nested frame's getBoundingClientRect is
relative to its PARENT's viewport rather than the top window — the offsets have to
be added or every anchor lands somewhere plausible and wrong. Same rule as
composing the scope key in one place: two features walking that chain
independently is two places computing one property.

Cross-origin falls back to the surface document rather than pretending. A
cross-origin frame cannot be read for element identity, and the alternative is
anchors that can never resolve.

FOUND WHILE PROVING IT: the nested frame was showing a 404 page, because serving
follows REGISTRATION and I had only registered the host row. That is failure mode
#11 in this repo's own skill, and the 404 body named the reason.

281 tests. The committed checks for the guard and the chain walk are structural
and say so, each with a control proving it can fail; the behaviour is the browser
proof above.
2026-08-05 05:59:30 -07:00
mvschwarz ab05910005 A failing provider install now says why
`npm ci --prefix ... --silent` suppresses npm's ERRORS as well as its progress.
The step runs under `set -euo pipefail`, so a failed provider install aborted the
whole provision with no text naming what went wrong: a registry 404, a bad engine
constraint and a network timeout all produced exactly the same nothing. The
operator is left at a dead end at the moment they most need the output, and the
obvious next move — run it again — reproduces the silence.

Output is captured rather than streamed, so a healthy run stays as quiet as it was
and the log is printed only when there is a failure to explain. The provider is
named, the exit status is preserved, and the command is echoed.

VERIFIED BOTH DIRECTIONS, with a negative control proving the old shape genuinely
produced a bare non-zero and zero text — otherwise the new test cannot tell that
anything changed.

TWO PROBE DEFECTS FOUND ON THE WAY, both of which would have had me "fix" working
code:
- My fake npm ended in `exit 7`. `exit` inside `eval` terminates the
  command-substitution subshell outright, so the function never reached its own
  error printing and the probe reported empty output. Real npm is an external
  command that RETURNS a status. The fake is now an external script.
- The dry-run assertion grepped for the command string, which the announcement
  line legitimately contains — a keyword check cannot tell "would: echo X" from
  having run it. Anchored to the line start instead.

This script targets Linux and the host here is Darwin, so the function was driven
directly rather than the provision being run end to end. Stated because it bounds
what the verification covers.
2026-08-05 05:53:06 -07:00
mvschwarz 69472e4479 Annotations: mark up any surface, including ones that never heard of it
studio-qa extracted a working annotation layer from ARTIFACTS and handed it over
as a patch rather than a description, so this is one implementation moving into
the SDK instead of a second one being written from a diff. Applied at the SHA
they published (verified before applying) and wired at the studioShell seam.

The shell draws marks on the overlay above the stage. That is the whole design:
a feature requiring surfaces to cooperate would work on none of the surfaces
that already exist, which is the population that matters. Verified against FLOOR,
which knows nothing about annotation.

THE SUB-CONTEXT. ARTIFACTS holds many documents behind one surface id, so a board
keyed on the id alone either hangs page one's marks over page two or collapses
every thread into one. The shell cannot work out what a surface's documents are,
so a surface MAY declare `{t:"annotation-context", id}` and the shell folds it
into the persistence key. A surface that says nothing gets one board keyed on its
id, exactly as before. The key is composed in the shell and nowhere else: two
places composing one key is two places computing one property, and they drift.

`/api/annotations` is scoped, and a read that FORGOT the scope answers empty
rather than everything — the failure to avoid is not an error, it is another
surface's marks rendered plausibly over this one. The verbs are deliberately NOT
reserved in SDK_OWNED: they answer stored marks, which is data on the box like
the files verbs, not the runtime describing itself.

Marks live in MEMORY unless `--annotations <file>` names one. Not under the
fixtures directory, because the change signal watches it recursively and a marks
file there would reload the surface being annotated on every stroke. Not inside
the package, because that makes node_modules state and a copied tree then carries
one instance's marks into another.

FOUND BY THE TESTS AND THE LOOK, not by review:
- I wrote loadAnnotations() and never called it. Writes worked, the board looked
  healthy, and the loss appeared only on the next restart. The test restarts the
  runtime rather than reading back in-process, because a write and a read in one
  process are both served by the in-memory map and agree either way.
- A missing anchor was drawn on the canvas exactly like an anchored one. The side
  panel said "anchor missing" while the arrow pointed confidently at whatever now
  occupied that space. Now dashed, desaturated and badged "?" — visible, because
  deleting it would lose what someone wrote, but never mistakable.
- The contract doc shipped as a BINARY file. A literal NUL in the scope-key
  sentence made it `data` rather than text: it rendered fine and broke every
  grep and diff silently.

The conformance table says which checks are structural. The active-surface guard
on the new message was verified by driving a live shell; the committed check
asserts the guard is present and carries a control proving it can fail.

278 tests, 278 passing (was 263).
2026-08-05 05:44:52 -07:00
mvschwarzandClaude Opus 5 4ef723294c A runtime with a real identity overrides by, and says which kind it produces
focus.md has said since it shipped that `by` is caller-declared here and that a
runtime with a real identity for its caller MUST override it. It said that while no
runtime could, which made it a promise about other people's implementations.

The case turned out to be real and immediate: a provider on the founder's box
derives the user from a proxy-set access header, and migrating it onto the platform
verb would have traded header-verified attribution for self-declared — a regression
on the one box where that header exists.

CONFIGURED, NOT SNIFFED. `--identity-header <name>` (or the env var) is the operator
asserting that something upstream strips that header from client requests and sets
it itself. Trusting an inbound header by default would be WORSE than
caller-declared, because anyone can set one. Unconfigured, this runtime behaves
exactly as documented before.

AND THE LIE I FOUND IN MY OWN VERIFICATION OUTPUT RATHER THAN BEING TOLD: the first
version fell back to the caller's claim when configured but no header arrived,
while /api/contract still reported attribution "verified". A consumer told
attribution is verified would then trust a value nobody checked. Configured now
means this runtime DERIVES identity: no identity available is `by: null`, which is
a different answer from "whatever you say".

/api/contract now reports focus: { attribution, identityHeader } — the header's
NAME, never a value. That closes a hole my own contract left open: it told
consumers not to build a trust decision on `by` without knowing which kind of
runtime produced it, and gave them no way to ask. The question had no answer until
it became possible for the answer to differ.

FOUR CONTROLS, AND ONE OF THEM IS NOT PLANT-VERIFIED — stated rather than implied.
The override and the null-not-fallback rule both fail against plants that restore
the old behaviour. The third — that an unconfigured runtime ignores an inbound
header — did NOT fail when I planted unconditional header trust, because
writeFocus independently checks the configured name and neutralised the plant. That
is defence in depth working, and it also means that control is currently unproven
by that plant rather than verified by it. Believing otherwise is how a control that
cannot fail gets counted as protection.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-05 05:17:11 -07:00
mvschwarzandClaude Opus 5 6981b8154d Reserve the runtime's own verbs so a provider cannot capture them
A provider declared /api/focus in its verbs and implemented POST only. The
compositor routes a declared verb to its provider BEFORE the runtime, so it
captured both methods and GET /api/focus answered 404 on the founder's box — while
GET /api/contract went on advertising focus.read, because the runtime declares the
capability and cannot see that its route was taken. An agent doing feature
detection was told the capability was there and then met a 404. /api/drive reached
the runtime only because no provider happened to claim it.

Fixing that provider's declaration treats the instance. Reserving the routes closes
the class: a provider should not be ABLE to shadow a verb that answers state living
in the runtime process.

THE MEMBERSHIP TEST IS WHOSE STATE THE VERB ANSWERS, and it is written into the
code because I got it wrong in the other direction first. Asked this morning
whether the FILES verbs belonged in this set I said yes, then retracted it: files
are DIRECTORIES ON THE BOX, so in a real studio they should be served by something
that knows about real directories, and reserving them would have forbidden the
correct thing. focus and drive are the opposite — focusRecord and driveOp are
module state in serve-studio.mjs, in memory, in this process. Nothing else can hold
them, so nothing else may answer for them.

That is the same property the three already reserved share: the runtime describing
itself, its fixture state, and its own change stream.

THE TEST ASSERTS BOTH DIRECTIONS, because reserving too much is as wrong as
reserving too little and only one of those gets noticed by the person it blocks. It
reads the reserved set out of the compositor and the routed verbs out of the
runtime rather than restating either, so a new in-process verb that nobody reserves
fails. Verified against two plants: un-reserving /api/focus reproduces the
founder's-box failure, and adding /api/files/tree fails as over-reservation.

KNOWN CONSEQUENCE, not a no-op: a provider's POST /api/focus that wrote its own
focus.json becomes unreachable. That is correct — the SDK owns focus now — but it
is a behaviour change for that app rather than a silent improvement, and it lands
with the refactor onto the SDK primitive rather than under it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-05 05:09:10 -07:00
mvschwarzandClaude Opus 5 f833f6c8ba Tell an agent when nothing is listening, instead of answering ok
Two QA seats found the same thing independently on the founder's box: an agent
POSTed a drive op to a hand-built surface that had never adopted the helper, got
back { ok: true, gen: 2 }, and nothing happened. The agent did everything right.

THAT IS THIS RUNTIME'S BUG, NOT THE APP'S. A hand-built app is allowed not to adopt
a primitive. A runtime is not allowed to report success for a delivery it cannot
make. I had already written the gap into the README when drive shipped — accepting
an op and honouring it are two different claims, and only the first was answered —
and this is that sentence happening to someone.

MEASURED, NOT DECLARED. The suggestion routed to me was a manifest or installer
warning for a surface that declares nothing. I did not build that: a declaration is
an INTENTION and is wrong in both directions — a surface can declare adoption with a
broken listener, or hand-roll a listener and declare nothing. Neither is what the
caller needs to know.

GET /api/drive is the poll an adopting surface makes, so a runtime that has served
ZERO drive reads since boot has nobody listening, and it is the only party that can
know. POST now answers `listening`, and /api/contract carries `drive: { listening,
reads, ops }` so an agent can ask before it drives rather than learn from a no-op.

It fires at the moment of the mistake, in the response the caller is already
reading. A doc line fires when somebody goes looking, which is after they have
concluded the platform is broken.

NOT CLAIMED, and drive.md still says so: listening is not HONOURED. A surface can
poll, receive, and do nothing. That is the refusal shape, still unspecified until a
real application needs one.

TWO GUARDS NEEDED THE SAME CORRECTION THIS FIELD EXPOSED, and it is the third time
today. zero-config asserted the top-level key set was EQUAL to the baseline, which
reads as strictness and is actually a rule that no additive field may ever ship —
the thing contract v0.1 explicitly permits. It blocked this documented additive
field the first time one arrived. Now MAY GAIN, NEVER LOSE, verified by planting a
removal of `capabilities` and watching it fail. Undocumented fields stay
contract-response-parity's job; owning that in two places is how the two drift.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-05 05:04:36 -07:00
mvschwarzandClaude Opus 5 fb17fa5ab7 A check for the fault that renders a live-looking page with a blank header
Four temporal dead zones have shipped from app/shell.html, three of them in one
afternoon. Same shape every time: a collection declared beside the code that fires
it, read by activate() during boot. The class earned a mechanism rather than more
care, because of how it presents — the rail renders, the tabs render, the page is
plainly alive, and the header is blank. Three of the four were found by something
ELSE being missing. None by looking at the page, and no assertion in the suite
could see any of them.

THE FIRST VERSION OF THIS TEST ASKED THE RIGHT QUESTION AND COULD NOT ANSWER IT.
"Is any module binding read above its declaration" needs to tell an identifier
from a property, and scanning text cannot: it reported three offenders on correct
code — an object property key, a substring inside a string, and a property access.
That is the same misfire this repository deleted a guard for earlier today, and a
guard that fires on correct code teaches people to reword correct code until it
stops.

So it asks a smaller question it can answer structurally: every module-level Map
and Set is declared in the state block at the top. All four faults were exactly
that — two Sets and a Map, each declared beside its consumer.

ON ITS FIRST RUN IT FOUND A REAL ONE I HAD NOT PUT THERE. `frames`, the surface
id -> iframe map, is pre-existing shell state that lived beside its first use.
Moved into the block, and the shell re-verified in a browser afterwards because
relocating live state is a real change and not a formatting one: it boots, the
header declaration still survives a surface round trip, and the frames still show.

What it does NOT do is written into both the test and the block it enforces: it is
not a TDZ prover, a scalar read early still slips past, and it does not run the
shell. The genuine check is a browser that fails when the page does not finish
booting — which needs a dependency this SDK does not have and will not grow for
it. Stating the limit beats implying coverage that is not there.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-05 04:47:09 -07:00