@descryy/runtime-contracts 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Runtime Event vocabulary.
3
+ *
4
+ * The exact 19 values below are the canonical list from
5
+ * `documents/descry-runtime-plan.md` §14 — reproduced verbatim, not
6
+ * reconstructed. An earlier version of this file was written before that
7
+ * document was available in this repo and guessed at a vocabulary that did
8
+ * not match it; this replaces that guess.
9
+ *
10
+ * This vocabulary is DELIBERATELY INDEPENDENT of the Graph Engine's 15 node
11
+ * types / 15 edge types (descry-core CLAUDE.md, DEC-061) — plan §14 states
12
+ * this explicitly. A runtime event is an observation, not a graph fact, and
13
+ * this list may grow or change on its own schedule. If a runtime need ever
14
+ * looks like it wants a new *graph* node or edge type, that is out of scope
15
+ * here — stop and raise it (plan principle 12). The graph vocabulary is
16
+ * frozen; this one is not.
17
+ */
18
+ export const RUNTIME_EVENT_TYPES = [
19
+ "PROCESS_STARTED",
20
+ "PROCESS_READY",
21
+ "PROCESS_EXITED",
22
+ "PAGE_CREATED",
23
+ "NAVIGATION",
24
+ "CLICK",
25
+ "INPUT",
26
+ /**
27
+ * Beyond plan §14's printed 19 — same standing as `COLLECTOR_ERROR` below,
28
+ * which that list also predates. §5 had no honest type for "an image was
29
+ * captured": not `INPUT`, not `CLICK`, not nothing. Landed together with
30
+ * its one emitter (`BrowserActionCollector.screenshot()`, RT-061) in the
31
+ * same change, per RT-053's own lesson — a declared-but-dead type with no
32
+ * annotation is the mistake, not a type existing at all, but the safest
33
+ * way not to repeat it is to not create one ahead of its wiring.
34
+ */
35
+ "SCREENSHOT",
36
+ /**
37
+ * Video evidence (competitive research, `documents/decisions-inbox/`:
38
+ * `PROPOSAL-video-evidence-capture.md`, this repo, and
39
+ * `descry-desktop`'s companion `DEC-D-NEXT-video-evidence-kind.md` --
40
+ * Greptile's TREX: a captured video of a UI change playing out, attached
41
+ * to a finding alongside screenshots/logs). Same standing as `SCREENSHOT`
42
+ * immediately above and the same discipline: landed together with its one
43
+ * emitter (`BrowserActionCollector.captureVideo()`, `browser-session.ts`'s
44
+ * new `recordVideo` option), not ahead of it.
45
+ *
46
+ * `source: "browser-dom"`, the same source `SCREENSHOT` already uses --
47
+ * see `evidence.ts`'s `EVIDENCE_SOURCES` list for why this did NOT get a
48
+ * new source of its own. Every entry in that list names a domain being
49
+ * *watched*; a kind of captured artifact (an image, a video) is not a
50
+ * domain, which is exactly `SCREENSHOT`'s own precedent applied here
51
+ * rather than re-litigated.
52
+ */
53
+ "VIDEO",
54
+ "CONSOLE_MESSAGE",
55
+ "NETWORK_REQUEST",
56
+ "NETWORK_RESPONSE",
57
+ "HTTP_ERROR",
58
+ "BACKEND_LOG",
59
+ "EXCEPTION",
60
+ "STACK_TRACE",
61
+ /**
62
+ * §17 (Test Runner Support). **All four are emitted**, by
63
+ * `packages/test-runner`, for `node:test` (RT-073), Jest and Vitest
64
+ * (RT-123) — each read from that runner's own structured reporter API,
65
+ * never from printed prose.
66
+ *
67
+ * The annotation this comment replaces said *"Nothing emits these three,
68
+ * on purpose, not yet"* and asked to be removed "when §17 lands and one
69
+ * of these three actually gets emitted somewhere." §17 landed in RT-073
70
+ * and the comment outlived it by four entries — noted because the
71
+ * mechanism this file exists to guard against is a claim that stops
72
+ * being true and nothing forcing it to be re-read.
73
+ *
74
+ * ## `TEST_SKIPPED` is the fourth, and it exists because the other three
75
+ * made a skipped test unsayable
76
+ *
77
+ * Measured across three runners (RT-124):
78
+ *
79
+ * | runner | `test.skip` | `test.todo` |
80
+ * | --- | --- | --- |
81
+ * | node:test | `test:pass` carrying `skip: true` | `test:pass` carrying `todo: true` |
82
+ * | Vitest 4 | `state: "skipped"` | `state: "skipped"` |
83
+ * | Jest 30 | omitted from `onTestCaseResult` entirely | `status: "todo"` |
84
+ *
85
+ * node:test's is why this is a contract change rather than a bug fix:
86
+ * **a skipped test arrives as a pass event**, and with no fourth member
87
+ * the translator mapped it to `TEST_PASSED`. A skipped test is not a
88
+ * pass (nothing was asserted), not a failure (nothing failed), and not
89
+ * absent (the developer wrote it and the runner knows about it).
90
+ *
91
+ * Counting them was tried first and is what shipped in RT-123. It is
92
+ * honest and insufficient: a counter carries no `sourceLocation`, no
93
+ * `qualifiedName` and no graph node, so **"the function you changed has
94
+ * a test and it is disabled" cannot be expressed at all** — a real
95
+ * finding, needing something resolvable to a `TEST_CASE` node to exist.
96
+ * Expressible-at-all, not accuracy, is what decided this.
97
+ *
98
+ * **Landed with its producers in the same change**, deliberately. §3's
99
+ * warning is about `DATABASE_QUERY`/`EXTERNAL_REQUEST` sitting declared
100
+ * and dead below; a type merged "ready for the producers next week" is
101
+ * that same shape regardless of intent.
102
+ *
103
+ * Consumers were checked before this was added rather than after: the
104
+ * only `eventType` branching outside `test-runner` is
105
+ * `evidence-package`'s two **allowlists** (`NETWORK_EVENTS`,
106
+ * `BACKEND_LOG_EVENTS`), and `composeFinding` does not read `eventType`
107
+ * at all. So nothing treats "not passed" as evidence of a problem, and a
108
+ * disabled test cannot be read as a failing one. A future
109
+ * `!== "TEST_PASSED"` would reintroduce exactly that.
110
+ *
111
+ * ## Why there is no `TEST_ABORTED` (RT-129)
112
+ *
113
+ * JUnit reports **four** outcomes, not three: `aborted` (a failed
114
+ * assumption — ran real code, bailed on a precondition) is distinct from
115
+ * `skipped` (`@Disabled` — never ran). Measured by Lane A (RT-088),
116
+ * along with the fact that JUnit's own XML collapses them into one bare
117
+ * `<skipped>` element while its `TestExecutionListener` keeps them apart.
118
+ *
119
+ * The distinction is real and did **not** get a type. The test applied:
120
+ * **does collapsing it change the answer to "was this code covered?"**
121
+ * Skipped-vs-passed does, oppositely, which is why the fourth member
122
+ * above exists. Skipped-vs-aborted does not — every one of them answers
123
+ * `notRun`, carries a location and a qualified name, and is not a
124
+ * failure, so no consumer routes differently. What differs is the
125
+ * *explanation*, and an explanation belongs in the payload.
126
+ *
127
+ * So `TEST_SKIPPED`'s payload carries `skipReason`
128
+ * (`test-runner`'s `SkipReason`): a closed, runner-agnostic vocabulary
129
+ * of `disabled` / `not-implemented` / `precondition-unmet` /
130
+ * `unspecified`. `unspecified` is a real answer, not a fallback — Vitest
131
+ * cannot tell `.skip` from `.todo`, and saying so beats guessing.
132
+ *
133
+ * This also repaired RT-124's own silent collapse: node:test **does**
134
+ * distinguish `skip` from `todo`, and the first version of this type
135
+ * threw that away.
136
+ */
137
+ "TEST_STARTED",
138
+ "TEST_PASSED",
139
+ "TEST_FAILED",
140
+ "TEST_SKIPPED",
141
+ /**
142
+ * `DATABASE_QUERY` / `EXTERNAL_REQUEST`: §18 (Database / External Service
143
+ * Observation). Both are now real. `DATABASE_QUERY` is emitted by
144
+ * `@descryhq-wq/runtime-database-observation`'s `DatabaseQueryCollector`, via
145
+ * real `node:sqlite` driver instrumentation (RT-141). `EXTERNAL_REQUEST`
146
+ * is emitted by `@descryhq-wq/runtime-external-service-observation`'s
147
+ * `ExternalRequestCollector`, via real global-`fetch` instrumentation
148
+ * (RT-146) — same `--import`-preload mechanism, a different global.
149
+ * Neither is derived from logs. What §18 still does not have: a
150
+ * transaction-boundary concept for queries, graph correlation for either
151
+ * (no resolver from a table name or a hostname to a graph node exists),
152
+ * and provider classification for external requests (a captured
153
+ * hostname, never a guessed provider name) — real, disclosed gaps, not
154
+ * silent ones.
155
+ */
156
+ "DATABASE_QUERY",
157
+ "EXTERNAL_REQUEST",
158
+ /**
159
+ * The observer failed, not the observed system. Added beyond plan §14's
160
+ * printed list, which §14 explicitly permits ("the exact event vocabulary
161
+ * can evolve") and which §17 in fact requires: `collector failure` is one
162
+ * of the failures the runtime model must represent, under the rule **"a
163
+ * collector failure must not be mistaken for an application success or
164
+ * absence of failure."**
165
+ *
166
+ * Without this type there was no way to state that at all. A collector
167
+ * whose consumption loop threw mid-stream logged to the console and
168
+ * resolved `stop()` cleanly, so a run that lost half its backend output
169
+ * was indistinguishable from a run where the backend printed half as
170
+ * much. That is precisely the confusion between *found nothing* and
171
+ * *stopped looking* that the five report categories exist to prevent.
172
+ */
173
+ "COLLECTOR_ERROR",
174
+ /**
175
+ * `DEC-NEXT-fault-layer-empirical-confirmation.md`'s `dependency`
176
+ * mechanism, landed together with its one emitter
177
+ * (`packages/orchestrator/src/instrumented-execution.ts`, via
178
+ * `@descryy/runtime-controller`'s `checkDependencyVersions`) in the same
179
+ * change -- same "not declared ahead of its wiring" discipline
180
+ * `SCREENSHOT`'s own comment states above. Not `COLLECTOR_ERROR`: this is
181
+ * not the observer failing, it is a real structural fact the runtime
182
+ * established about the project it is executing (the lockfile-declared
183
+ * version of a dependency does not match what is actually installed),
184
+ * the same "a genuinely new kind of fact gets its own type" precedent
185
+ * `DATABASE_QUERY`/`EXTERNAL_REQUEST` already set for §18 above. Payload
186
+ * carries `name`/`declaredVersion`/`resolvedVersion`, mirroring
187
+ * `DependencyVersionMismatch`'s own shape.
188
+ */
189
+ "DEPENDENCY_VERSION_MISMATCH",
190
+ /**
191
+ * The same ruling's `environment` mechanism, same emitter, same standing
192
+ * as `DEPENDENCY_VERSION_MISMATCH` above -- landed together with its one
193
+ * emitter, not a `COLLECTOR_ERROR` because nothing failed to observe.
194
+ * Payload carries `declaredVersion`/`declaredSource`/`observedVersion`,
195
+ * mirroring `EnvironmentVersionCheckResult`'s own shape.
196
+ */
197
+ "ENVIRONMENT_VERSION_MISMATCH",
198
+ /**
199
+ * RT-225: `BrowserNetworkCollector`'s WebSocket lifecycle listener
200
+ * (`page.on("websocket")`, plus the per-socket `close`/`socketerror`
201
+ * events Playwright's `WebSocket` object exposes). Closes
202
+ * `documents/decisions-inbox/RT-PENDING-websocket-network-observation-gap.md` --
203
+ * a WebSocket connection, successful or refused, fired none of
204
+ * `NETWORK_REQUEST`/`NETWORK_RESPONSE`/`HTTP_ERROR` before this, because
205
+ * those three are wired to `request`/`requestfinished`/`requestfailed`
206
+ * only, and Playwright surfaces WebSocket traffic through a wholly
207
+ * separate event stream.
208
+ *
209
+ * A connection **attempt** reuses `NETWORK_REQUEST` (a real outbound call
210
+ * was made, to a URL -- the same fact that type already exists to
211
+ * record) and a connection **failure** (`socketerror`) reuses
212
+ * `HTTP_ERROR` with `status: null` -- the exact "no usable response ever
213
+ * came back" convention this vocabulary already applies to a refused
214
+ * `fetch()` (`requestfailed`) and to this collector's own timeout. Both
215
+ * reuses are deliberate: forcing WS's *fields* into the HTTP shape would
216
+ * fabricate a `method`/`status` nothing observed, but the *event*
217
+ * (request went out / no usable response arrived) is the identical fact
218
+ * under a different wire protocol, and reusing the type keeps this
219
+ * evidence inside the existing `network` signal channel
220
+ * (`packages/finding/src/signal-channel.ts`) and the existing
221
+ * `NETWORK_EVENTS` package view with no governance-table change.
222
+ *
223
+ * `WEBSOCKET_CLOSED` is the one genuinely new fact, because a **close**
224
+ * has no honest HTTP-shaped answer: `NETWORK_RESPONSE` means "a response
225
+ * with a status arrived," which forcing a close into it would fabricate.
226
+ * Measured, not assumed (`websocket-intercept-gap.test.ts`): Playwright's
227
+ * public `WebSocket.on("close")` carries no code or reason at all --
228
+ * identical for a clean server-initiated 1000 close and a raw abrupt TCP
229
+ * termination. So this type does not claim "normal" vs "abnormal"
230
+ * either; it states only that the socket closed, and says so in its own
231
+ * payload rather than guessing. Emitted only when no `socketerror` was
232
+ * already reported for the same socket, so a failed connection is never
233
+ * double-reported once as `HTTP_ERROR` and again as `WEBSOCKET_CLOSED`.
234
+ *
235
+ * Deliberately outside `signal-channel.ts`'s `CHANNEL_BY_EVENT_TYPE`
236
+ * table (§5.2 of the ratified `ai-governance-spec.md`, a separate,
237
+ * already-settled ruling this change does not reopen) -- falls through to
238
+ * `null`/no-signal there, which is that table's own documented "fail
239
+ * closed" default for a type it has not ruled on, and is the honest
240
+ * answer for a fact this thin: "the socket closed" carries far less on
241
+ * its own than a real request/response/error triple does.
242
+ *
243
+ * Per-message frame traffic (`framesent`/`framereceived`) is not
244
+ * captured as evidence at all -- out of scope here, disclosed rather than
245
+ * silently dropped, in `browser-network-collector.ts`'s own module
246
+ * comment. Neither is `EventSource`/SSE, which shares the same
247
+ * uncovered shape (the RT-PENDING note above flagged it as unmeasured;
248
+ * still unmeasured after this change).
249
+ */
250
+ "WEBSOCKET_CLOSED",
251
+ ];
252
+ //# sourceMappingURL=runtime-event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-event.js","sourceRoot":"","sources":["../src/runtime-event.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,iBAAiB;IACjB,eAAe;IACf,gBAAgB;IAChB,cAAc;IACd,YAAY;IACZ,OAAO;IACP,OAAO;IACP;;;;;;;;OAQG;IACH,YAAY;IACZ;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO;IACP,iBAAiB;IACjB,iBAAiB;IACjB,kBAAkB;IAClB,YAAY;IACZ,aAAa;IACb,WAAW;IACX,aAAa;IACb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2EG;IACH,cAAc;IACd,aAAa;IACb,aAAa;IACb,cAAc;IACd;;;;;;;;;;;;;;OAcG;IACH,gBAAgB;IAChB,kBAAkB;IAClB;;;;;;;;;;;;;;OAcG;IACH,iBAAiB;IACjB;;;;;;;;;;;;;;OAcG;IACH,6BAA6B;IAC7B;;;;;;OAMG;IACH,8BAA8B;IAC9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmDG;IACH,kBAAkB;CACV,CAAC"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Origin -> service root (RT-023, built on RT-028's `ProcessHandle.serviceName`/
3
+ * `.port` and `ExecutionConfiguration.services`). Answers "which service's
4
+ * declared `cwd` served this browser origin," the missing link
5
+ * `BrowserConsoleCollector` needs to turn `http://localhost:4000/app.js`
6
+ * into a candidate on-disk root before the existing `file://`-based source
7
+ * resolution logic can run.
8
+ *
9
+ * Deliberately stops here, at a configuration-layer fact, not a collector.
10
+ * `packages/browser`'s runtime code depends on `@descryy/runtime-contracts`
11
+ * only (RT-021's precedent) -- this lives in contracts so that boundary
12
+ * holds for this resolver too.
13
+ *
14
+ * Two distinct, RT-024-shaped honesty gaps live here, not one:
15
+ *
16
+ * 1. **The port join proves an address, not a correspondent.** Matching
17
+ * `origin.port` to the `ProcessHandle` we recorded on that port proves
18
+ * the browser's origin matches a port this execution assigned -- not
19
+ * that the browser actually talked to that specific process. Under
20
+ * ephemeral allocation (RT-024's own default) this is true almost all
21
+ * of the time, which is exactly the phrase RT-024 says not to write
22
+ * down as identity. Nothing here re-verifies the process is still the
23
+ * one that was live when the browser made the request.
24
+ * 2. **A resolved root is configured, not verified** -- see
25
+ * `ConfiguredServiceRoot`.
26
+ *
27
+ * Same class of gap, two layers: a port match confirms an address: a
28
+ * `cwd` confirms a declared root. Neither confirms who was actually there.
29
+ */
30
+ import type { Execution } from "./execution.ts";
31
+ /**
32
+ * A service's declared working directory, recovered by joining a running
33
+ * process's assigned port back to `ExecutionConfiguration.services`.
34
+ *
35
+ * **Configured, not verified.** This proves the origin maps to a directory
36
+ * the orchestrator was told to run the service from -- it does not prove
37
+ * any specific file a browser frame names actually lives under it. A
38
+ * static file server rooted at a subdirectory (`public/`, `dist/`), a
39
+ * symlink, a proxy, or a `cwd` that changed after spawn would all break
40
+ * the stronger claim without breaking this weaker one. Same shape as
41
+ * RT-024's "readiness proves liveness, not identity": this proves *a*
42
+ * root, not that a given file came from the checkout in it.
43
+ *
44
+ * Not a refusal, because the weaker claim is still real and useful, and
45
+ * because this project already has a pattern for exactly this gap: the
46
+ * caller joins the frame's path onto `declaredCwd` and confirms the file
47
+ * exists there before trusting a source location built from it -- the same
48
+ * `existsSync` discipline `createNodeSourceLocationResolver` already
49
+ * applies to `file://` paths rather than trusting "the path looks right."
50
+ * A failed existence check downstream is the honest refusal; this function
51
+ * returning a root is not, by itself, a claim that the check will pass.
52
+ */
53
+ export interface ConfiguredServiceRoot {
54
+ readonly serviceName: string;
55
+ readonly declaredCwd: string;
56
+ }
57
+ /**
58
+ * Closed, so a caller can branch on *why* rather than pattern-match prose --
59
+ * same reasoning as `descry-core`'s `attrs.refusalClass`: free-text refusal
60
+ * reasons can't be counted by a census or acted on differently by a caller.
61
+ * The four cases split into three different kinds of problem a caller
62
+ * would want to tell apart: `originNoPort`/`originUnparseable` mean the
63
+ * input itself wasn't a resolvable origin; `noProcessOnPort` means nothing
64
+ * in this execution claims that port at all; `processUnnamed` is a wiring
65
+ * gap (a real process, spawned without a `serviceName`) that's closable by
66
+ * whoever spawned it; `serviceNotConfigured` is a configuration error (a
67
+ * `serviceName` with no matching `services` entry) -- different from a
68
+ * wiring gap because the name exists but the config doesn't back it.
69
+ */
70
+ export declare const SERVICE_ROOT_REFUSALS: readonly ["originNoPort", "originUnparseable", "noProcessOnPort", "processUnnamed", "serviceNotConfigured"];
71
+ export type ServiceRootRefusal = (typeof SERVICE_ROOT_REFUSALS)[number];
72
+ export type ServiceRootLookup = {
73
+ readonly found: true;
74
+ readonly root: ConfiguredServiceRoot;
75
+ } | {
76
+ readonly found: false;
77
+ readonly refusal: ServiceRootRefusal;
78
+ readonly message: string;
79
+ };
80
+ /**
81
+ * Resolves a browser origin (e.g. `"http://localhost:4000"`) to the
82
+ * service that served it, by port: `execution.processes[].port` (the real,
83
+ * OS-assigned port -- RT-028) to `.serviceName`, then `.serviceName` to
84
+ * `execution.configuration.services[name].cwd`.
85
+ *
86
+ * `origin` is a clean origin, not a full frame location -- stripping a
87
+ * `file:37:12`-shaped V8 frame down to its origin is the caller's job
88
+ * (mirrors `createNodeSourceLocationResolver`'s own division of labor:
89
+ * frame parsing is a separate concern from root resolution).
90
+ *
91
+ * Two services sharing the same `declaredCwd` in configuration is not
92
+ * ambiguous here: resolution runs process -> serviceName -> a direct keyed
93
+ * lookup in `services`, never a reverse search by directory, so a
94
+ * coincidentally-shared path on the output side has nothing to disambiguate.
95
+ */
96
+ export declare function resolveServiceRootForOrigin(origin: string, execution: Pick<Execution, "processes" | "configuration">): ServiceRootLookup;
97
+ //# sourceMappingURL=source-root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-root.d.ts","sourceRoot":"","sources":["../src/source-root.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,6GAMxB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAA;CAAE,GAC9D;IAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9F;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,WAAW,GAAG,eAAe,CAAC,GACxD,iBAAiB,CA2CnB"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Origin -> service root (RT-023, built on RT-028's `ProcessHandle.serviceName`/
3
+ * `.port` and `ExecutionConfiguration.services`). Answers "which service's
4
+ * declared `cwd` served this browser origin," the missing link
5
+ * `BrowserConsoleCollector` needs to turn `http://localhost:4000/app.js`
6
+ * into a candidate on-disk root before the existing `file://`-based source
7
+ * resolution logic can run.
8
+ *
9
+ * Deliberately stops here, at a configuration-layer fact, not a collector.
10
+ * `packages/browser`'s runtime code depends on `@descryy/runtime-contracts`
11
+ * only (RT-021's precedent) -- this lives in contracts so that boundary
12
+ * holds for this resolver too.
13
+ *
14
+ * Two distinct, RT-024-shaped honesty gaps live here, not one:
15
+ *
16
+ * 1. **The port join proves an address, not a correspondent.** Matching
17
+ * `origin.port` to the `ProcessHandle` we recorded on that port proves
18
+ * the browser's origin matches a port this execution assigned -- not
19
+ * that the browser actually talked to that specific process. Under
20
+ * ephemeral allocation (RT-024's own default) this is true almost all
21
+ * of the time, which is exactly the phrase RT-024 says not to write
22
+ * down as identity. Nothing here re-verifies the process is still the
23
+ * one that was live when the browser made the request.
24
+ * 2. **A resolved root is configured, not verified** -- see
25
+ * `ConfiguredServiceRoot`.
26
+ *
27
+ * Same class of gap, two layers: a port match confirms an address: a
28
+ * `cwd` confirms a declared root. Neither confirms who was actually there.
29
+ */
30
+ /**
31
+ * Closed, so a caller can branch on *why* rather than pattern-match prose --
32
+ * same reasoning as `descry-core`'s `attrs.refusalClass`: free-text refusal
33
+ * reasons can't be counted by a census or acted on differently by a caller.
34
+ * The four cases split into three different kinds of problem a caller
35
+ * would want to tell apart: `originNoPort`/`originUnparseable` mean the
36
+ * input itself wasn't a resolvable origin; `noProcessOnPort` means nothing
37
+ * in this execution claims that port at all; `processUnnamed` is a wiring
38
+ * gap (a real process, spawned without a `serviceName`) that's closable by
39
+ * whoever spawned it; `serviceNotConfigured` is a configuration error (a
40
+ * `serviceName` with no matching `services` entry) -- different from a
41
+ * wiring gap because the name exists but the config doesn't back it.
42
+ */
43
+ export const SERVICE_ROOT_REFUSALS = [
44
+ "originNoPort",
45
+ "originUnparseable",
46
+ "noProcessOnPort",
47
+ "processUnnamed",
48
+ "serviceNotConfigured",
49
+ ];
50
+ /**
51
+ * Resolves a browser origin (e.g. `"http://localhost:4000"`) to the
52
+ * service that served it, by port: `execution.processes[].port` (the real,
53
+ * OS-assigned port -- RT-028) to `.serviceName`, then `.serviceName` to
54
+ * `execution.configuration.services[name].cwd`.
55
+ *
56
+ * `origin` is a clean origin, not a full frame location -- stripping a
57
+ * `file:37:12`-shaped V8 frame down to its origin is the caller's job
58
+ * (mirrors `createNodeSourceLocationResolver`'s own division of labor:
59
+ * frame parsing is a separate concern from root resolution).
60
+ *
61
+ * Two services sharing the same `declaredCwd` in configuration is not
62
+ * ambiguous here: resolution runs process -> serviceName -> a direct keyed
63
+ * lookup in `services`, never a reverse search by directory, so a
64
+ * coincidentally-shared path on the output side has nothing to disambiguate.
65
+ */
66
+ export function resolveServiceRootForOrigin(origin, execution) {
67
+ let port;
68
+ try {
69
+ const url = new URL(origin);
70
+ if (url.port === "") {
71
+ return {
72
+ found: false,
73
+ refusal: "originNoPort",
74
+ message: `origin "${origin}" has no explicit port; cannot match against a process's assigned port`,
75
+ };
76
+ }
77
+ port = Number(url.port);
78
+ }
79
+ catch {
80
+ return { found: false, refusal: "originUnparseable", message: `"${origin}" is not a parseable origin` };
81
+ }
82
+ const process = execution.processes.find((candidate) => candidate.port === port);
83
+ if (process === undefined) {
84
+ return {
85
+ found: false,
86
+ refusal: "noProcessOnPort",
87
+ message: `no process in this execution is bound to port ${port}`,
88
+ };
89
+ }
90
+ if (process.serviceName === null) {
91
+ return {
92
+ found: false,
93
+ refusal: "processUnnamed",
94
+ message: `the process bound to port ${port} has no serviceName -- it was not spawned as a named service, so its declared root cannot be recovered from configuration.services`,
95
+ };
96
+ }
97
+ const service = execution.configuration.services[process.serviceName];
98
+ if (service === undefined) {
99
+ return {
100
+ found: false,
101
+ refusal: "serviceNotConfigured",
102
+ message: `serviceName "${process.serviceName}" (from the process bound to port ${port}) has no matching entry in configuration.services`,
103
+ };
104
+ }
105
+ return { found: true, root: { serviceName: process.serviceName, declaredCwd: service.cwd } };
106
+ }
107
+ //# sourceMappingURL=source-root.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-root.js","sourceRoot":"","sources":["../src/source-root.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA+BH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,cAAc;IACd,mBAAmB;IACnB,iBAAiB;IACjB,gBAAgB;IAChB,sBAAsB;CACd,CAAC;AAOX;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,2BAA2B,CACzC,MAAc,EACd,SAAyD;IAEzD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,GAAG,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC;YACpB,OAAO;gBACL,KAAK,EAAE,KAAK;gBACZ,OAAO,EAAE,cAAc;gBACvB,OAAO,EAAE,WAAW,MAAM,wEAAwE;aACnG,CAAC;QACJ,CAAC;QACD,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,IAAI,MAAM,6BAA6B,EAAE,CAAC;IAC1G,CAAC;IAED,MAAM,OAAO,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IACjF,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,OAAO,EAAE,iBAAiB;YAC1B,OAAO,EAAE,iDAAiD,IAAI,EAAE;SACjE,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;QACjC,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,OAAO,EAAE,gBAAgB;YACzB,OAAO,EAAE,6BAA6B,IAAI,oIAAoI;SAC/K,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,SAAS,CAAC,aAAa,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACtE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,OAAO,EAAE,sBAAsB;YAC/B,OAAO,EAAE,gBAAgB,OAAO,CAAC,WAAW,qCAAqC,IAAI,mDAAmD;SACzI,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;AAC/F,CAAC"}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@descryy/runtime-contracts",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "description": "The six runtime contracts: Execution, Evidence, Runtime Event, Collector, Capability, Correlation. Agents 2-4 build against this; it does not depend on them.",
6
+ "license": "UNLICENSED",
7
+ "engines": {
8
+ "node": ">=22.5"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "registry": "https://npm.pkg.github.com"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc -b"
24
+ },
25
+ "dependencies": {}
26
+ }