mirror of
https://github.com/mvschwarz/openrig-studio.git
synced 2026-11-07 05:56:21 -10:00
main
131
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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
|
||
|
|
5d4846877a |
Handlers get a response facade, and an ack must be a recorded generation
sdk-qa re-round on
|
||
|
|
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. |
||
|
|
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]> |
||
|
|
8550db3579 |
Confine the raw request to the routing adapter; bound the settle-ack
sdk-qa RETURN on
|
||
|
|
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). |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
e34d3eeaed |
The table IS the router: routes cannot exist without being enumerated
sdk-qa returned |
||
|
|
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.
|
||
|
|
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
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
0a104fe038 | Bundle Artifacts with the Studio SDK | ||
|
|
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. |
||
|
|
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 |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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).
|
||
|
|
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]>
|
||
|
|
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]> |
||
|
|
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]>
|
||
|
|
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]> |