Carry the signals declaration, and warn when it names a verb nobody answers

A provider now declares which of its verbs honour ?since= and whether it mints a
process identity. It is a declaration rather than something derived for the
reason everything else in this format is: a verb answering ?since= is a property
of the implementation, not of the route, so nothing can work it out by looking.

Carried explicitly by the composer alongside seeds and supplies, so adding a
field to the contract without carrying it fails the field-by-field test rather
than going quiet one layer down — which is how seeds and supplies were being
dropped before anyone noticed.

The warning is the part that matters. A signals.verbs entry naming a verb this
provider does not declare would have a consumer polling ?since= against a route
the backend never answers: a declaration that quietly does nothing, which is the
class this seam keeps removing and which arrives each time through a new field.
Warned rather than refused, because the provider still works and the rest of the
studio should not be held up by it. Prefix verbs satisfy a signals entry for
their children, using the trailing-slash rule byte routes and calls already use,
so a correctly declared prefix is not warned at — a false alarm is how a warning
becomes something people learn to ignore.

Both halves verified by planting: dropping the carry trips the field-by-field
test, and disabling the warning branch trips the warning test. The first attempt
at the second plant changed the warning's WORDING rather than its branch, so it
applied, produced no defect, and reported the control as passing — the defect was
measured directly before the suite result was believed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 5 committed 2026-08-04 13:44:13 -07:00
1 parent 2b444e954a
commit 911eb2def8
4 files changed
+85 -1

No files matched your search

+22
View File
@@ -54,6 +54,7 @@ about itself; an app may not override any of this.
},
"serves": ["/media/", "/cutprev/", "/cuts/"],
"verbs": ["/api/clips", "/api/cuts", "/api/export-status/"],
"signals": { "verbs": ["/api/clips"], "boot": true },
"supplies": ["ffmpeg", "ffprobe"]
}
```
@@ -67,9 +68,30 @@ about itself; an app may not override any of this.
| `run.companions[]` | stable | long-running processes started **with** the provider |
| `serves[]` | stable | **byte-route prefixes**, matched by prefix |
| `verbs[]` | stable | the `/api/` verbs this provider ANSWERS |
| `signals` | stable | which verbs answer `?since=<marker>`, and whether this provider mints a process identity. See `change-signal.md` |
| `supplies[]` | provisional | binaries this package ships, so the host need not have them |
| `seeds` | provisional | how to make an empty root usable on first run |
### `signals` — what a consumer may poll, declared by the thing that knows
```json
"signals": { "verbs": ["/api/clips"], "boot": true }
```
`signals.verbs[]` are the observe verbs that honour `?since=<marker>` and answer
`{ changed, marker, … }`. `signals.boot` says the provider mints a process
identity, which is what makes a CODE change detectable at all. The semantics of
both live in `change-signal.md`; this is only where a provider declares them.
**It is a declaration and not a derivation for the reason everything else here
is: only the provider knows.** A verb answering `?since=` is a property of the
implementation, not of the route, so nothing can work it out by looking.
A `signals.verbs` entry that names a verb this provider does not declare in
`verbs[]` is **warned** — a consumer polling it would be polling something this
backend never answers, which is a declaration that quietly does nothing rather
than an error the operator can see.
### `seeds` — first-run scaffolding, declared by the package that knows how
```json
+1 -1
View File
@@ -264,7 +264,7 @@ defect this repository has already paid for twice.
| `/signal.js` serving `watchSignal` / `preserveAcross` / `deferWhileDirty` | **yes** |
| latch-on-first-observation, marker-backwards-is-changed, degraded reporting | **yes**, with controls |
| `?since=` on an observe verb | **not yet** — the reference runtime's verbs are fixture-backed and mint no marker. The consumer half is shipped and tested against a scripted transport |
| `signals` in `provider.json`, carried through composition | **not yet** |
| `signals` in `provider.json`, carried through composition | **yes** — documented in `app-manifest.md`, carried by the composer, and a `signals.verbs` entry the provider does not answer is warned |
| `preserve` accepted as contract on a surface row and carried to the shell | **yes** — schema and runtime validation kept in step by a committed test |
| the helper reading `preserve` from the row and driving capture/restore from it | **yes** — `declaredKinds` + `preserveDeclared`, with controls |
| `standardAdapter` binding `scroll`, `form` and `playhead` to real DOM | **WRITTEN, NOT YET VERIFIED.** The code exists and its RULES are under control — which kinds it claims, and which fields it refuses. Its DOM behaviour has not been exercised in a browser, and a test runner cannot exercise it. **Do not cite the code as evidence it works; this row flips on a browser pass, not on the code landing.** |
+44
View File
@@ -112,6 +112,7 @@ test("EVERY contract field on a provider.json reaches the spec, not just the one
verbs: ["/api/clips"],
supplies: ["ffmpeg", "ffprobe"],
seeds: { root: "project", marker: "timeline.json", entry: "new.mjs", export: "scaffold" },
signals: { verbs: ["/api/clips"], boot: true },
},
},
});
@@ -124,6 +125,49 @@ test("EVERY contract field on a provider.json reaches the spec, not just the one
assert.deepEqual(spec.supplies, ["ffmpeg", "ffprobe"], "supplies was dropped — the install gate reads it");
assert.deepEqual(spec.seeds, { root: "project", marker: "timeline.json", entry: "new.mjs", export: "scaffold" },
"seeds was dropped, so a documented declaration could never reach the seeder");
assert.deepEqual(spec.signals, { verbs: ["/api/clips"], boot: true },
"signals was dropped, so a consumer could never learn which verbs answer ?since=");
});
test("a signals verb the provider does not answer is WARNED, not carried silently", () => {
// A declaration that quietly does nothing: a consumer would poll ?since=
// against a route this backend never serves. The same class the seam has
// removed repeatedly, arriving through a new field.
const t = tree({
apps: { alpha: { provider: { package: "@x/video" } } },
providers: {
video: {
package: "@x/video", run: { entry: "v.mjs" },
verbs: ["/api/clips"],
signals: { verbs: ["/api/clips", "/api/nothing-answers-this"], boot: true },
},
},
});
const r = t.run(["alpha"]);
assert.ok(r.warnings.some((w) => /nothing-answers-this/.test(w)),
`an unanswerable signal verb was carried without a word: ${JSON.stringify(r.warnings)}`);
// The positive half: the verb that IS answered must not be warned about, or
// the check is just noise that gets ignored.
assert.ok(!r.warnings.some((w) => /signals\.verbs names \/api\/clips\b/.test(w)),
"a correctly declared signal verb was warned about");
});
test("a prefix-declared verb satisfies a signals declaration for its children", () => {
// The trailing-slash rule already used by byte routes and calls. Without this
// a provider declaring /api/export-status/ would be warned for signalling on
// it, which is exactly the false alarm that teaches people to ignore warnings.
const t = tree({
apps: { alpha: { provider: { package: "@x/video" } } },
providers: {
video: {
package: "@x/video", run: { entry: "v.mjs" },
verbs: ["/api/export-status/"],
signals: { verbs: ["/api/export-status/job7"], boot: false },
},
},
});
assert.deepEqual(t.run(["alpha"]).warnings.filter((w) => /signals\.verbs/.test(w)), [],
"a prefix declaration did not satisfy a signals entry for its child");
});
// --------------------------------------------------- the unmatched-call ladder
+18
View File
@@ -218,7 +218,25 @@ export function composeRail({ appsRoot, enabled, doors = [], runtimeDir, studioR
// silently one layer down.
spec.seeds = decl.seeds ?? null;
spec.supplies = decl.supplies ?? [];
// Which verbs answer ?since=, and whether this provider mints a process
// identity (contract/change-signal.md). Carried explicitly like every other
// provider-owned field, so adding one to the contract without carrying it
// here fails the composer's field-by-field test rather than going quiet.
spec.signals = decl.signals ?? null;
spec.declaredBy = `${pkg} (provider.json)`;
// A signal verb this provider does not actually answer is a declaration that
// does nothing: a consumer would poll ?since= against a route this backend
// never serves. Warned rather than refused, because the provider still works
// and the rest of the studio should not be held up by it.
for (const v of decl.signals?.verbs ?? []) {
if (!(spec.verbs ?? []).some((d) => verbMatches(d, v))) {
warnings.push(
`${pkg}: signals.verbs names ${v}, which this provider does not declare in verbs[] — ` +
`a consumer polling it would be polling a route this backend does not answer`
);
}
}
}
// ---- the unmatched-call ladder ---------------------------------------------