An independent cold build of v0.7.0 — fresh clone, documentation only, no contact with me — found three things. The first is the one that matters and it is the worst class this repository deals in. FOCUS AND DRIVE ANSWERED WITHOUT BEING ADVERTISED. Both verbs worked, both were documented, and neither namespace appeared in /api/contract's capabilities. But contract-meta.md tells every consumer that each namespace it uses must appear there and to CHECK BEFORE USE — so an agent doing exactly what the contract instructs concluded the features were absent. A verb that answers without advertising is worse than a missing verb: the documented path leads to the wrong answer, and the consumer has no reason to doubt it. This was mine, and it was pattern-matching rather than reasoning. When drive shipped I checked whether focus had declared a capability, found none, and made drive match it for consistency. Consistency with an omission. Read and write are separate entries because they are separately true, which is what files.read has always meant, and it lets a runtime offer read-only focus honestly instead of claiming a namespace it half-implements. TWO GUARDS HAD TO BE LOOSENED, AND THAT IS THE INTERESTING PART. Both asserted the capability set was EXACTLY equal to a baseline. True when written, wrong as a rule: capabilities is the feature-detection list, so it must grow when a namespace ships or it can never advertise anything. Equality there would have blocked this very fix — a guard that reads as strictness and is actually a rule that no feature may ever be declared. Both now assert the additive property that was meant: capabilities MAY GAIN, NEVER LOSE, since a consumer gates on them. Verified by planting a removal and watching both fail. One of those loops was written backwards first — iterating the current runtime and requiring the baseline to contain each, which reads identically and asserts the opposite. Caught because the suite failed, not because it looked wrong. A guard now compares the documented capability list against the served one, and a second asserts every namespace with a served route is advertised. Neither existed: measured first, the parity suite passed at 4/4 with a capability deleted from the documented example, because the comparator descends objects and capabilities is an array. Capabilities are shape, not data — the rest of the arrays in that response are counts and errors and differ by design — so they get a named check rather than a general array descent. Also: POST /api/focus's response shape is now documented rather than inferred from a readback, and the generated project's run snippet no longer omits --surfaces, without which the new surface never reaches the rail and looks like a broken row. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
9.1 KiB
Focus — what the user is looking at
contractVersion: 0.1
Everything here is opt-in and additive. A surface that reports no focus
behaves exactly as it did before this document existed, and contractVersion
does not move.
Why this is a contract
An agent that cannot see what the user is looking at is working blind. The measured state before this document is worse than missing: a focus record already exists, three of four surveyed applications already write it, and nothing off the box can read it — the write verb has no matching read, so agents open the file directly.
That single gap is what this specifies away. Three measured defects come with it,
and each one is a rule below rather than a note: a second writer blanking the
record, a selection that means four different things, and a view change that
reports nothing because the reporter compares selections only.
The record (stable)
{
"surface": "canvas",
"selection": ["<whatever this surface's selection IS>"],
"view": { "page": 2 },
"note": "one line a human or agent may set",
"at": "<ISO timestamp of the last change>",
"by": "<who last wrote it>"
}
| field | stability | meaning |
|---|---|---|
surface |
stable | the id of the surface this record describes |
selection |
stable | typed by that surface — see below |
view |
stable | where the surface is looking: page, board, range, tab. Shape is the surface's |
note |
stable | one line, free text |
at |
stable | ISO timestamp of the last change |
by |
stable | the actor that last wrote it |
selection is typed by its surface, never universally (stable)
Across the surveyed applications selection is asset names, opaque canvas
shape ids, absolute file paths, and timeline slot ids — four types
sharing one word. A universal schema would be correct for one application and
silently wrong for the rest.
So the record identifies its surface and the selection is read in that
context. A consumer that does not know how to interpret a given surface's
selection must be able to tell that it does not, rather than receive something
plausible.
This is the same decision, for the same measured reason, as the standard preserve
adapter declining selection in change-signal.md.
Reading focus (stable)
GET /api/focus — and its absence is the defect this document exists for.
It answers the change-signal shape, so an agent polls it with the primitive already specified rather than a second mechanism:
{ "ok": true, "changed": true, "marker": "<opaque>", "focus": { "...": "the record" } }
?since=<marker> is honoured exactly as change-signal.md describes: the marker
is server-minted, monotonic and opaque, and a consumer compares it only for
equality. A provider serving focus declares the verb in its signals.verbs.
Focus is readable over HTTP and not only from the filesystem. Reading the record off disk works only for a consumer on that machine, which excludes every remote agent and every consumer in another process boundary. The verb is the contract; where a provider keeps the record is its own business.
The marker moves on ANY field change (stable)
Including a view change with an unchanged selection. This is stated because
the surveyed implementation compares selections only, so moving to another page
or board reports nothing — the user has plainly changed what they are looking at
and the agent is told nothing at all.
Writing focus (stable)
POST /api/focus — a write updates the fields it names and leaves the rest.
The body is a JSON object naming the fields to change. A write answers with the record as it now stands, and with the marker that write produced:
{ "ok": true, "marker": "<opaque>", "focus": { "...": "the record after the write" } }
Returning the record means a writer does not need a follow-up read to know
what it produced, and the marker it gets back is the one a subsequent ?since=
compares against. A write that names no known field is refused with
{ "ok": false, "error": "<what> — <what to do>" } and HTTP 400 rather than
silently doing nothing.
That is the whole rule, and it exists because whole-record replacement is a
measured defect: a second verb overwrote the record with view blanked and asset
paths reduced to basenames, so pinning something destroyed the view context the
focus reporter had just written. The two writers were both behaving reasonably;
the format made them collide.
So a write carrying only selection leaves view exactly as it was. A field is
cleared by naming it with an explicit null, which is a statement rather than an
accident.
at is server-set on every write. A timestamp the caller supplies cannot be
trusted to say when the record last genuinely changed, and it is the field a
consumer uses to decide whether focus is stale.
by is caller-declared, and a runtime that has a real identity for the caller
MUST override it. That distinction is drawn rather than glossed: attribution is
only as good as the identity behind it, and a contract that called this
server-set would be describing a guarantee the reference runtime cannot make. It
is single-process, loopback, and knows nothing about who is calling — so it
records what it was told and this table says so. Do not build a trust decision
on by without knowing which kind of runtime produced it.
Addressing state WITHIN a surface (stable)
The same problem as focus, arriving from the other end. Focus lets an agent ask what the user is looking at; an address lets anyone hand that to someone else — a link a human pastes, a state a reload restores, a reference an agent is given rather than described.
Across the surveyed applications there is not one use of location.hash,
URLSearchParams or pushState. The shell's ?s=<id> picks which surface is
open and nothing addresses state inside one.
The shell owns the QUERY; the surface owns the HASH.
That split is the rule rather than a convention: ?s= belongs to the shell, and a
surface writing there would fight the thing hosting it. So a surface's own state
goes in the hash, and the runtime helper's readAddress / writeAddress do
exactly that and leave the query untouched.
import { readAddress, writeAddress } from "/signal.js";
const state = readAddress(location.href); // {} when nothing is addressed
history.replaceState(null, "", writeAddress(location.href, { page: "3" }));
The address is a flat map of strings, because a URL is something a human is
expected to read and paste. A surface with structure to address flattens it; a
blank value is dropped rather than written as &k=, and an empty state clears
the hash rather than leaving a bare #.
Restoring on load is the half that matters. An address that is produced but never read is decoration: it makes a URL that looks meaningful and restores nothing. A surface adopting this reads the address before its first render.
Failure modes this design answers
- A consumer is not on the box. It reads over HTTP. This is the whole point.
- Two writers, one record. Field-scoped writes; neither blanks the other.
- The user changed page but not selection. The marker moves; it is a change.
- A consumer meets a surface it does not understand. The record names its surface, so the consumer can say so rather than misread the selection.
- Nothing reports focus. The verb answers an empty record and the marker does not move. A surface that never reports is not an error.
Conformance — what ships TODAY
| specified | shipped |
|---|---|
the record, its fields, and surface-typed selection |
yes |
GET /api/focus answering {changed, marker, focus} under ?since= |
yes, in the reference runtime |
| field-scoped writes that cannot blank each other | yes, with controls |
at server-set on every write |
yes |
POST answering {ok, marker, focus} so a writer needs no follow-up read |
yes |
the namespace advertised at GET /api/contract -> capabilities (focus.read, focus.write) |
yes — and it was NOT, until an independent cold build followed the documented feature-detection path and concluded the verb was absent |
by overridden from a real caller identity |
no, and it cannot be here. The reference runtime is single-process and loopback and has no identity for its caller, so by is what the caller declared. A runtime that HAS one must override it |
a provider declaring the verb in signals.verbs |
specified; the reference runtime is single-process and declares nothing |
readAddress / writeAddress, hash-owned and query-untouched |
yes, with controls |
| a SHIPPED surface that actually addresses its state | no. None of the surfaces in this repository has view state worth addressing — the shell hosts, and the starter renders a fixture. The helper and its rules are contract and tested; the first application with a view is what will exercise them, and adding UI here purely to demonstrate it would be inventing a feature to satisfy a document |
Rows are measured against the shipped runtime rather than recalled.