@supernovae-st/nika 0.71.0 → 0.120.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.
- package/LICENSE +202 -0
- package/README.md +993 -30
- package/dist/bin/nika.js +407 -0
- package/dist/index.cjs +3554 -0
- package/dist/index.d.cts +889 -0
- package/dist/index.d.ts +889 -0
- package/dist/index.js +3502 -0
- package/docs/architecture.md +146 -0
- package/docs/http-api.md +160 -0
- package/docs/migrating-to-0.116.md +162 -0
- package/docs/testing.md +173 -0
- package/openapi.json +1797 -0
- package/package.json +77 -24
- package/bin.js +0 -49
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# SDK architecture
|
|
2
|
+
|
|
3
|
+
The package has one public facade and two execution adapters:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
application
|
|
7
|
+
|
|
|
8
|
+
v
|
|
9
|
+
Nika facade
|
|
10
|
+
|
|
|
11
|
+
v
|
|
12
|
+
Transport interface <--- lifecycle and authority seam
|
|
13
|
+
| |
|
|
14
|
+
v v
|
|
15
|
+
NativeProcessTransport HttpTransport
|
|
16
|
+
| |
|
|
17
|
+
v v
|
|
18
|
+
local nika process authenticated nika serve
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Modules and responsibilities
|
|
22
|
+
|
|
23
|
+
- `src/index.ts` is the public Module. It validates caller-owned values, owns
|
|
24
|
+
run handles, and exposes one stable vocabulary.
|
|
25
|
+
- `src/lib/transport.ts` is the Interface. It describes the operations both
|
|
26
|
+
Adapters must implement and makes unsupported authority explicit.
|
|
27
|
+
- `src/lib/native-process-transport.ts` is the local Adapter. It spawns the
|
|
28
|
+
selected engine without a shell and consumes newline-delimited machine
|
|
29
|
+
events. It reads the first machine frame before it returns a run, so
|
|
30
|
+
`run()` resolves on the engine's admission and rejects on its refusal.
|
|
31
|
+
- `src/lib/run-refusal.ts` reads the engine's one pre-run refusal object (its
|
|
32
|
+
check report carrying `clean: false`, or its `{ error }` envelope) and turns
|
|
33
|
+
it into the typed `NikaOperationError`. It judges nothing: a shape the
|
|
34
|
+
engine was not measured to write is a protocol fault, never a refusal.
|
|
35
|
+
- `src/lib/legacy-run-refusals.ts` is temporary. It recovers the three refusal
|
|
36
|
+
dialects of engines up to 0.119.0, which predate the one `run --json`
|
|
37
|
+
grammar, and is deleted with its call sites once no supported engine writes
|
|
38
|
+
them.
|
|
39
|
+
- `src/lib/http-transport.ts` is the remote Adapter. It verifies the remote
|
|
40
|
+
server identity once per client, resolves and verifies a local engine only
|
|
41
|
+
for caller-owned snapshot capture, then uses the authenticated HTTP
|
|
42
|
+
contract.
|
|
43
|
+
- `src/lib/run-session.ts` is the lifecycle Seam. It owns the eager event
|
|
44
|
+
pump, bounded independent observers, cancellation memoization, and the sole
|
|
45
|
+
terminal settlement. It builds the `NikaRun` handle as closures over itself,
|
|
46
|
+
so the Run owns `events()`, `result()`, `status()` and `cancel()`.
|
|
47
|
+
- `src/lib/run-events.ts` is the semantic Adapter: a pure projection of one
|
|
48
|
+
protocol frame onto the SDK's lifecycle vocabulary, with the frame kept by
|
|
49
|
+
identity on `raw`. It is applied at the edge of an observer view. There is
|
|
50
|
+
one history and one bounded queue per observer, and no second event bus.
|
|
51
|
+
- `openapi.json` and `src/generated/openapi.d.ts` pin the HTTP contract judged
|
|
52
|
+
by CI. `scripts/check-sdk-coverage.js` fails if a live runtime path is
|
|
53
|
+
missing or if the SDK names a path outside that contract.
|
|
54
|
+
|
|
55
|
+
## Authority rules
|
|
56
|
+
|
|
57
|
+
The SDK transports engine facts; it does not reproduce engine decisions.
|
|
58
|
+
Parsing, admission, scheduling, cancellation settlement, receipts, trace
|
|
59
|
+
verification, permits, and cost remain engine-owned.
|
|
60
|
+
|
|
61
|
+
Some operations deliberately have one authority:
|
|
62
|
+
|
|
63
|
+
- resident workflow discovery, durable status, and schedules require HTTP;
|
|
64
|
+
- a direct native process refuses those operations with
|
|
65
|
+
`NikaCompatibilityError`;
|
|
66
|
+
- remote execution by contained workflow name uses the resident registry;
|
|
67
|
+
explicit local paths need a compatible local engine to capture a snapshot;
|
|
68
|
+
- when that capture is red the HTTP adapter returns the local engine's plain
|
|
69
|
+
`nika check --json` report, so `findings[]` stays canonical and no workflow
|
|
70
|
+
bytes are sent;
|
|
71
|
+
- HTTP observation (attach, durable status, events, cancel, workflow catalog,
|
|
72
|
+
schedule status, trace verdicts) needs no local engine;
|
|
73
|
+
- remote trace verification currently returns the engine's typed unavailable
|
|
74
|
+
verdict because the server has no path-free journal authority.
|
|
75
|
+
|
|
76
|
+
## Lifecycle invariants
|
|
77
|
+
|
|
78
|
+
1. `run()` resolves only after stable admission and returns an immutable
|
|
79
|
+
`NikaRun` handle: `id`, `events()`, `result()`, `status()`, `cancel()`, and
|
|
80
|
+
`done`, the compatibility alias of `result()`. Its members are closures
|
|
81
|
+
over the run's one session, so an extracted method still works. The handle
|
|
82
|
+
owns the lifecycle and nothing else: checking, proof, catalogs and
|
|
83
|
+
authoring stay on the facade. A run handle never means "maybe a run": a refusal
|
|
84
|
+
before admission rejects `run()` with `NikaOperationError` and no handle
|
|
85
|
+
exists. Over HTTP a red local snapshot refuses before any request is sent;
|
|
86
|
+
on the native transport the first machine frame decides. A run event is the
|
|
87
|
+
engine's admission evidence and is replayed as the first event; a refusal
|
|
88
|
+
object is the whole stream. The one spawn that executes the workflow is the
|
|
89
|
+
one that judges it: the SDK adds no preflight check and never spawns twice.
|
|
90
|
+
2. `run.result()` is the only terminal promise (`run.done` is the same one).
|
|
91
|
+
Workflow failure is result data; configuration, transport, protocol, and
|
|
92
|
+
compatibility failures throw. A `paused` result is a human gate: neither a
|
|
93
|
+
failure nor a completed execution.
|
|
94
|
+
3. A session's history retains at most `eventBufferSize` frames (default 4096,
|
|
95
|
+
never unbounded; each frame is bounded by `machineBufferBytes`). That
|
|
96
|
+
product bounds the retained history only, not the session or the process:
|
|
97
|
+
frames already handed to a consumer, other views and other runs are not
|
|
98
|
+
counted in it. Two different
|
|
99
|
+
bounds can be exceeded and they are never confused: a live view that falls
|
|
100
|
+
behind fails with `reason: 'live_backpressure'`, and a view opened after
|
|
101
|
+
more frames than it can be given is refused with
|
|
102
|
+
`reason: 'replay_truncated'` plus what was `observed` and `retained`.
|
|
103
|
+
Neither skips a frame, neither touches another view, and neither touches
|
|
104
|
+
the result: a run longer than the bound still settles normally. The
|
|
105
|
+
session is memory over what this process saw. It is not a control plane,
|
|
106
|
+
keeps nothing on disk, and reads no engine journal to extend a replay.
|
|
107
|
+
`run.events()` creates an independent bounded observer in the lifecycle
|
|
108
|
+
vocabulary. Aborting an observer never cancels the run. The vocabulary is a
|
|
109
|
+
stateless per-frame projection: it names a fact only for a (kind, status)
|
|
110
|
+
pair a producer defines, never defaults a state word, never deduplicates,
|
|
111
|
+
and never synthesizes a frame a transport did not emit. Every other frame
|
|
112
|
+
keeps flowing as `engine.event`, so transports differ in cardinality but
|
|
113
|
+
never in names, and `event.raw` is always the untouched protocol frame.
|
|
114
|
+
4. `run.cancel()` is idempotent per run: every call returns the one request.
|
|
115
|
+
5. `run.status()` reads the durable HTTP projection and refuses a native-only
|
|
116
|
+
run instead of guessing from local process state.
|
|
117
|
+
6. Schedule creation uses `If-None-Match: *`; updates require the exact opaque
|
|
118
|
+
revision in `If-Match`.
|
|
119
|
+
7. `attachRun()` is the one recovery door. It creates a fresh owned session
|
|
120
|
+
for an existing HTTP job; its initial sequence is caller-owned durable
|
|
121
|
+
checkpoint state, never inferred from in-memory SDK history. A native run
|
|
122
|
+
is process-bound: its `run.id` is an ephemeral correlation id, and the SDK
|
|
123
|
+
keeps no local job store, starts no hidden `nika serve`, and reads no
|
|
124
|
+
journal to fake durability.
|
|
125
|
+
8. Ownership has no registry. The facade maps each handle it created to its
|
|
126
|
+
session in a `WeakMap`. The deprecated `nika.events(run)`,
|
|
127
|
+
`nika.cancel(run)` and `nika.status(run)` wrappers resolve through it, so a
|
|
128
|
+
foreign, reconstructed, serialized, or other-client handle still throws
|
|
129
|
+
`NikaRunOwnershipError`. `nika.events(run)` keeps yielding the protocol
|
|
130
|
+
vocabulary unchanged. The wrappers stay for one release train counted from
|
|
131
|
+
publication: they ship unchanged in the first published train that carries
|
|
132
|
+
the Run-owned lifecycle, the earliest train that may remove them is the one
|
|
133
|
+
after it, and the removal is the One SDK baseline owner's decision (#114),
|
|
134
|
+
announced in that train's release notes. No version or date is fixed.
|
|
135
|
+
9. An interrupted observation is a typed transport error carrying its cursor
|
|
136
|
+
(`NikaObservationInterrupted`), never an engine failure: the run may still
|
|
137
|
+
be running. The engine's own `interrupted` evidence state is data instead:
|
|
138
|
+
a `run.interrupted` event and `result.status === 'interrupted'`.
|
|
139
|
+
|
|
140
|
+
## Deletion test
|
|
141
|
+
|
|
142
|
+
If one Adapter is removed, the facade and lifecycle Seam remain coherent and
|
|
143
|
+
the other Adapter still compiles. If the Transport Interface is removed,
|
|
144
|
+
authority differences leak into every public method. If `run-session.ts` is
|
|
145
|
+
removed, each consumer must reimplement buffering, ownership, cancellation,
|
|
146
|
+
and settlement. Those boundaries therefore carry real architectural load.
|
package/docs/http-api.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# HTTP contract
|
|
2
|
+
|
|
3
|
+
`openapi.json` is the checked-in contract pin. The SDK authenticates every
|
|
4
|
+
route except public `GET /health`; bearer tokens are redacted from failures.
|
|
5
|
+
A non-2xx answer typed as `{ error: { code, message } }` becomes a
|
|
6
|
+
`NikaOperationError` carrying `status`, `code`, and the refused `operation`;
|
|
7
|
+
For a check by served name, a typed 404 or 422 instead returns
|
|
8
|
+
`{ clean: false, error }`; authentication and transport failures still throw.
|
|
9
|
+
Any other non-2xx body is discarded and reported as a redacted
|
|
10
|
+
`NikaTransportError`.
|
|
11
|
+
|
|
12
|
+
| HTTP route | SDK surface | Contract |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `GET /health` | internal identity handshake | public liveness and protocol versions |
|
|
15
|
+
| `GET /v1/openapi.json` | generation only | authenticated OpenAPI 3.1 document |
|
|
16
|
+
| `GET /v1/workflows` | `listWorkflows()` | contained relative workflow names |
|
|
17
|
+
| `GET /v1/workflows/{name}` | `workflow(name)` | path-free metadata, never source bytes |
|
|
18
|
+
| `POST /v1/check` | `check()` | validates a served name or immutable snapshot bytes without a job |
|
|
19
|
+
| `POST /v1/jobs` | `run()` | admits a served name or exact snapshot bytes with an idempotency key |
|
|
20
|
+
| `GET /v1/jobs/{id}` | internal settlement | durable job identity, outputs, receipt, settlement, or redacted error |
|
|
21
|
+
| `GET /v1/jobs/{id}/status` | `run.status()` | current status only |
|
|
22
|
+
| `GET /v1/jobs/{id}/events` | `run.events()` / `attachRun()` | bounded, sequenced SSE with replay |
|
|
23
|
+
| `POST /v1/jobs/{id}/cancel` | `run.cancel()` | 200 a settled job or its terminal replay; 202 the request accepted on a running job, settled later by observation |
|
|
24
|
+
| `GET /v1/jobs/{id}/trace/verify` | `traceVerify(receipt)` | engine-owned typed trace verdict; `reason` only on a verdict that does not hold |
|
|
25
|
+
| `GET/PUT /v1/schedules/{id}` | `scheduleStatus()` / `schedule()` | resident schedule projection and CAS mutation |
|
|
26
|
+
|
|
27
|
+
## Connection rules
|
|
28
|
+
|
|
29
|
+
- HTTPS is required for every host except loopback. Plain HTTP is accepted
|
|
30
|
+
only for `localhost`, `127.0.0.0/8`, or `[::1]`, and only with an explicit
|
|
31
|
+
`allowInsecureHttp: true`; that opt-in never admits a routable host.
|
|
32
|
+
- URLs containing credentials, a query, or a fragment are rejected.
|
|
33
|
+
- Tokens must contain 32–512 visible ASCII bytes and are never sent to
|
|
34
|
+
`/health`.
|
|
35
|
+
- Each request has a bounded timeout and each JSON/SSE machine frame has a
|
|
36
|
+
byte ceiling.
|
|
37
|
+
- Remote `check()` refuses `model` and `nativeStrict`; remote `run()` refuses
|
|
38
|
+
`model`, `maxCostUsd` and the deprecated `vars` until a request envelope
|
|
39
|
+
owns them.
|
|
40
|
+
- Remote `run()` sends `inputs` as `JobByName.inputs` only for a served name
|
|
41
|
+
and only when `GET /health` advertises `jobInputs`. That capability, not a
|
|
42
|
+
202, is the negotiation: a resident from before the envelope accepts the
|
|
43
|
+
extra field and ignores its values, so the SDK refuses it after `/health`
|
|
44
|
+
alone with `NikaCompatibilityError` (`capability: 'jobInputs'`). The
|
|
45
|
+
`inputs` envelope is engine-owned (nika#1642) and lands in the pinned
|
|
46
|
+
`openapi.json` when the engine pin reaches a release that serves it.
|
|
47
|
+
- A snapshot body takes no `inputs` overlay, an empty map included: `run()` of
|
|
48
|
+
a local path with `inputs` is refused before any capture or request.
|
|
49
|
+
- The serialized `inputs` map is bounded at 1 MiB by the SDK, the bound the
|
|
50
|
+
native channel reads. The resident's whole-request ceiling is its own and may
|
|
51
|
+
be lower.
|
|
52
|
+
- Caller-provided workflow catalog names must be contained slash-separated
|
|
53
|
+
paths. Absolute paths, backslashes, empty segments, `.` and `..` are
|
|
54
|
+
rejected before network I/O.
|
|
55
|
+
|
|
56
|
+
A contained `.nika` name uses the resident registry without a local
|
|
57
|
+
engine. Prefix a local file with `./` to capture and submit its snapshot.
|
|
58
|
+
A successful by-name check returns `clean: true` and the compact resident
|
|
59
|
+
acknowledgement; no local check report or exit code is fabricated.
|
|
60
|
+
|
|
61
|
+
## Settlement
|
|
62
|
+
|
|
63
|
+
The terminal `execution.settled` frame and the durable job nest the run's
|
|
64
|
+
`settlement` whole (engine 0.118, ADR-128): its `status` and `cause`, the
|
|
65
|
+
elapsed time, the task tally, the spend with its qualifier, and the failure
|
|
66
|
+
named with its task. The SDK types every known field, refuses a settlement
|
|
67
|
+
whose `status` contradicts the record carrying it, keeps fields it does not
|
|
68
|
+
know, and never derives a settlement from an exit code; a job the resident
|
|
69
|
+
lost (`interrupted`) carries none.
|
|
70
|
+
|
|
71
|
+
## Frame time and journal evidence (engine main)
|
|
72
|
+
|
|
73
|
+
Both resident projections are closed, and the SDK refuses any field it does
|
|
74
|
+
not know. Engine main adds two optional fields that are
|
|
75
|
+
ahead of the pinned `openapi.json`: no released engine writes them yet, and a
|
|
76
|
+
resident that predates them never sends them, so nothing changes against a
|
|
77
|
+
released resident. They are read so that a resident built from engine main can
|
|
78
|
+
be observed at all; the pin itself moves only with a release.
|
|
79
|
+
|
|
80
|
+
- `JobEvent.at` is when the resident admitted the event: an RFC 3339 timestamp
|
|
81
|
+
in UTC, outside the event's hash chain. It rides `event.raw.at` untouched.
|
|
82
|
+
Anything that is not such a timestamp is a `NikaProtocolError`. The durable
|
|
83
|
+
`Job` declares no `at`, so one there is still an unknown field.
|
|
84
|
+
- `evidence`, on the terminal frame and on the durable `Job`, reports that the
|
|
85
|
+
run's journal mirror stopped recording. It is exactly a `status` and a
|
|
86
|
+
`reason`. The one status is `mirror_lost`. The reason is `write_failed`
|
|
87
|
+
(opening, writing or syncing the journal failed) or `record_refused` (a
|
|
88
|
+
record could not be admitted within the writer's bounds): a coarse class,
|
|
89
|
+
never OS text and never a path. Any other shape or word is a
|
|
90
|
+
`NikaProtocolError`, as the engine itself refuses one, and its value is
|
|
91
|
+
never quoted in the error.
|
|
92
|
+
|
|
93
|
+
`evidence` is independent of the execution and is never a verdict: a run can
|
|
94
|
+
settle `succeeded` while its mirror is lost. It never changes `result.status`,
|
|
95
|
+
the settlement, or the receipt's identity checks. `run.result()` copies it to
|
|
96
|
+
`result.evidence` from the frame or record that settled the run, so a caller
|
|
97
|
+
who never iterates events still learns the trace may be incomplete before
|
|
98
|
+
trusting `traceVerify`. Its absence claims nothing: not that a journal exists,
|
|
99
|
+
only that no loss was reported. A native run never carries it.
|
|
100
|
+
|
|
101
|
+
## Lifecycle vocabulary over the resident's frames
|
|
102
|
+
|
|
103
|
+
`run.events()` names the resident's closed `JobEvent` frames in the SDK's
|
|
104
|
+
lifecycle vocabulary and keeps each frame on `event.raw`:
|
|
105
|
+
|
|
106
|
+
| `JobEvent.kind` | `JobEvent.status` | `event.kind` |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `execution.started` | any | `run.started` |
|
|
109
|
+
| `execution.settled` | `paused` | `run.waiting` |
|
|
110
|
+
| `execution.settled` | `succeeded` · `failed` · `cancelled` | `run.settled` |
|
|
111
|
+
| `execution.cancelled` | `cancelled` only | `run.settled` |
|
|
112
|
+
| `execution.refused` | `failed` only | `run.settled` |
|
|
113
|
+
| `execution.interrupted` · `interrupted` | `interrupted` | `run.interrupted` |
|
|
114
|
+
| anything else: a `null` or future kind; an end kind whose status is absent, `null`, future, `queued` or `running`; or a pair that contradicts itself (`execution.refused` carrying `succeeded` or `cancelled`, `execution.cancelled` carrying `succeeded` or `failed`, an end kind carrying `interrupted`) | | `engine.event` |
|
|
115
|
+
|
|
116
|
+
The pairs above are exhaustive and listed, never computed as a product of
|
|
117
|
+
kinds and words. An unnamed frame is still delivered with `event.raw` intact,
|
|
118
|
+
and `run.result()` still reads the state word the engine wrote on it.
|
|
119
|
+
|
|
120
|
+
The resident streams no per-task frame, so an HTTP run yields no `task.*`
|
|
121
|
+
event: the SDK never synthesizes one. `event.sequence` is the validated SSE
|
|
122
|
+
id, the cursor to persist. The engine's `interrupted` (execution ownership
|
|
123
|
+
was lost, settlement unknown) is data: a `run.interrupted` event and
|
|
124
|
+
`result.status`. It is unrelated to `NikaObservationInterrupted` below, which
|
|
125
|
+
is this client losing its view of a run that may still be running.
|
|
126
|
+
|
|
127
|
+
## SSE recovery
|
|
128
|
+
|
|
129
|
+
The client checks that SSE ids are canonical positive integers and equal
|
|
130
|
+
`data.sequence`. An identical duplicate is ignored. A conflicting duplicate,
|
|
131
|
+
gap, or out-of-order frame is a protocol failure. After a reset the client
|
|
132
|
+
asks durable job state before reconnecting with `Last-Event-ID`; retry delays
|
|
133
|
+
and attempts are bounded.
|
|
134
|
+
|
|
135
|
+
A replacement Node process can call `attachRun(jobId, { lastEventId })`. The
|
|
136
|
+
SDK proves that the durable job exists before returning an owned run handle,
|
|
137
|
+
then sends the cursor as `Last-Event-ID`. Persist the job id and last event
|
|
138
|
+
sequence in the same application transaction that records each consumed event.
|
|
139
|
+
A cursor means “fully processed”, not merely “received”.
|
|
140
|
+
|
|
141
|
+
## Idempotency and schedules
|
|
142
|
+
|
|
143
|
+
HTTP `run()` requires a caller-owned `idempotencyKey` of 1–255 bytes. Omitting
|
|
144
|
+
it throws `NikaConfigurationError` before network I/O or local snapshot capture.
|
|
145
|
+
Persist the key before admission and retry the exact request with the same key
|
|
146
|
+
if the response is lost or times out. The SDK never generates a hidden key or
|
|
147
|
+
retries admission automatically. Reusing a key with different request bytes
|
|
148
|
+
is an engine conflict, not a retry success. Direct native runs still omit the
|
|
149
|
+
key and reject it if supplied.
|
|
150
|
+
|
|
151
|
+
The namespace is the whole durable job store under the server's configured
|
|
152
|
+
`state-root`, across workflows, clients, schedules, and server restarts. The
|
|
153
|
+
current engine has no time-based eviction: keys remain bound while that state
|
|
154
|
+
root exists and still count toward its configured job capacity. Use globally
|
|
155
|
+
unique, business-stable keys; do not recycle daily counters or workflow-local
|
|
156
|
+
names.
|
|
157
|
+
|
|
158
|
+
Schedules use compare-and-swap semantics. Create omits `revision`; update must
|
|
159
|
+
carry the exact previous `sha256:...` revision. The SDK never fabricates or
|
|
160
|
+
normalizes schedule facts.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Migrating to 0.116
|
|
2
|
+
|
|
3
|
+
Version 0.116 is an intentional breaking consolidation of the two published
|
|
4
|
+
0.115 clients into one `Nika` facade. This is a 0.x minor release, but existing
|
|
5
|
+
0.115 imports and method calls do not all remain source-compatible. Migrate in
|
|
6
|
+
a branch and run the packed-package gauntlets before upgrading production.
|
|
7
|
+
|
|
8
|
+
## Removed 0.115 surfaces
|
|
9
|
+
|
|
10
|
+
- The `@supernovae-st/nika-client/local` export and `LocalNika` class are
|
|
11
|
+
removed. Use `new Nika({ bin, cwd })`; call `check()`, `run()`, `events()`,
|
|
12
|
+
and `traceVerify()` on that instance.
|
|
13
|
+
- The root `nika.jobs` and `nika.workflows` namespaces are removed. Use the
|
|
14
|
+
facade methods shown below.
|
|
15
|
+
- `Nika.fromEnv()`, `nika.health()`, `Nika.verifyWebhook()`, and the exported
|
|
16
|
+
webhook helper are removed. Construct the client explicitly; health is now
|
|
17
|
+
an internal compatibility preflight. Keep webhook verification in the
|
|
18
|
+
application boundary that owns its signing format.
|
|
19
|
+
- Preview artifact, workflow-source/reload, and `runAndCollect` helpers are
|
|
20
|
+
removed instead of continuing as methods that always refuse.
|
|
21
|
+
- Node 18 and 20 are no longer supported; the package now requires Node 22 or
|
|
22
|
+
newer.
|
|
23
|
+
|
|
24
|
+
## Constructor migration
|
|
25
|
+
|
|
26
|
+
The 0.115 root constructor was HTTP-only. In 0.116, no URL means the native
|
|
27
|
+
process transport; supplying `url` and `token` selects HTTP. Remote `check()`
|
|
28
|
+
and `run()` also need a local Nika binary (`bin`, `NIKA_BIN`, or the exact
|
|
29
|
+
optional host payload package) because
|
|
30
|
+
the SDK captures and validates immutable snapshot bytes before admission.
|
|
31
|
+
The current by-name HTTP path also accepts contained workflow names without a
|
|
32
|
+
local engine; prefix a local file with `./` to retain snapshot capture.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// 0.115
|
|
36
|
+
const oldClient = new Nika({ url, token, timeout: 30_000 });
|
|
37
|
+
|
|
38
|
+
// 0.116
|
|
39
|
+
const nika = new Nika({
|
|
40
|
+
url,
|
|
41
|
+
token,
|
|
42
|
+
bin: process.env.NIKA_BIN,
|
|
43
|
+
requestTimeout: 30_000,
|
|
44
|
+
allowInsecureHttp: url.startsWith('http://127.0.0.1'),
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`timeout`, retries, polling, concurrency, logger, and per-run `signal` options
|
|
49
|
+
from the old HTTP client are gone. Request and frame bounds are client
|
|
50
|
+
invariants; observe or cancel a returned run through its owned lifecycle.
|
|
51
|
+
|
|
52
|
+
## Method mapping
|
|
53
|
+
|
|
54
|
+
| 0.115 | 0.116 |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `local.check(file)` | `nika.check(file)` |
|
|
57
|
+
| `local.run(file)` | `await nika.run(file)`, then `nika.events(run)` and `run.done` |
|
|
58
|
+
| `local.runToEnd(file)` | `const run = await nika.run(file); await run.done` |
|
|
59
|
+
| `local.traceVerify(path)` | No path-based replacement; retain the engine-issued receipt and call `nika.traceVerify(receipt)` |
|
|
60
|
+
| `nika.jobs.submit(workflow)` | `nika.run(workflow)` |
|
|
61
|
+
| `nika.jobs.status(id)` | retain the owned run; `nika.status(run)` |
|
|
62
|
+
| `nika.jobs.stream(id)` | `attachRun(id)`, then `events(run)` |
|
|
63
|
+
| `nika.jobs.cancel(id)` | `nika.cancel(run)` |
|
|
64
|
+
| `nika.workflows.list()` | `nika.listWorkflows()` |
|
|
65
|
+
| `nika.workflows.metadata(name)` | `nika.workflow(name)` |
|
|
66
|
+
|
|
67
|
+
In 0.116 the run handle was only `{ id, done }`. The Run now owns its
|
|
68
|
+
lifecycle: `run.events()`, `run.result()`, `run.status()` and `run.cancel()`,
|
|
69
|
+
with `run.done` kept as the alias of `run.result()`. The `nika.events(run)`,
|
|
70
|
+
`nika.cancel(run)` and `nika.status(run)` forms this guide shows still work
|
|
71
|
+
unchanged as deprecated wrappers, and `nika.events(run)` still yields the
|
|
72
|
+
protocol vocabulary; `run.events()` yields one lifecycle vocabulary on both
|
|
73
|
+
transports with that frame on `event.raw`. The wrappers stay for one release
|
|
74
|
+
train counted from the first published train that carries the new API, with
|
|
75
|
+
no version or date fixed; "Migrating to the Run-owned lifecycle" in the
|
|
76
|
+
README defines that window.
|
|
77
|
+
|
|
78
|
+
The client-level methods reject a look-alike object and a run from another
|
|
79
|
+
client, so persist the job id and reattach after a process restart instead of
|
|
80
|
+
rebuilding a handle by hand.
|
|
81
|
+
|
|
82
|
+
`LocalNika.version()`, `dryRunPlan()`, and `test()` have no One SDK method in
|
|
83
|
+
0.116. Keep those CLI-facing probes in deployment/CI (`nika --version`,
|
|
84
|
+
`nika run --dry-run --json`, and `nika test`) until a future typed authority is
|
|
85
|
+
explicitly admitted. This release does not silently emulate them.
|
|
86
|
+
|
|
87
|
+
## The check report changed shape
|
|
88
|
+
|
|
89
|
+
`check()` still returns `clean` and `exitCode`, but the object around them is
|
|
90
|
+
no longer the 0.115 `LocalCheckReport` (eleven curated fields plus a `raw`
|
|
91
|
+
escape hatch). It is the engine's own machine report, passed through: the
|
|
92
|
+
former `raw` contents are now the top level, `reportVersion` is
|
|
93
|
+
`report_version`, and `parseFatal` and `warnings` are gone. The engine emits
|
|
94
|
+
its identity under both casings (`engineVersion` and `engine_version`,
|
|
95
|
+
`buildSha` and `build_sha`, `specSha` and `spec_sha`, `checkReportVersion`
|
|
96
|
+
and `report_version`); read either, do not diff them. In TypeScript only
|
|
97
|
+
`report_version`, `clean`, and `exitCode` are typed; every other field,
|
|
98
|
+
including `cost`, `findings`, and `hints`, is `unknown` behind an index
|
|
99
|
+
signature, so strict callers narrow it themselves. Two problems that look like
|
|
100
|
+
findings (a missing `permits:` block, a missing `max_tokens:`) are reported
|
|
101
|
+
under `hints[]` (`{ kind, task, advice }`, no `code`), not `findings[]`, in
|
|
102
|
+
both versions.
|
|
103
|
+
|
|
104
|
+
`runToEnd()` returned `{ ok, exitCode, events[] }` with every event buffered.
|
|
105
|
+
`run.done` returns the terminal result only; the events ride `events(run)`
|
|
106
|
+
as a bounded iterator. In 0.116 a session retained 256 frames, below the 273 a
|
|
107
|
+
measured clean native run of 90 `mock/echo` tasks writes, so such a run had to
|
|
108
|
+
be observed concurrently or its prefix was gone. The default is now 4096 and a view opened
|
|
109
|
+
after the result replays every retained frame; past the bound it is refused
|
|
110
|
+
with `reason: 'replay_truncated'`, never shortened, and the result is
|
|
111
|
+
unaffected. An explicit `eventBufferSize` keeps its cap. The removed methods (`version()`, `dryRunPlan()`, path-based
|
|
112
|
+
`traceVerify()`) are absent, not stubbed: calling them throws a plain
|
|
113
|
+
`TypeError: … is not a function`, and a trace that only exists as a file
|
|
114
|
+
path in a later process has no SDK verification door in 0.116.
|
|
115
|
+
|
|
116
|
+
## New resident discovery
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const names = await nika.listWorkflows();
|
|
120
|
+
const metadata = await nika.workflow(names[0]);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
These methods require an HTTP client. A native-process client returns a typed
|
|
124
|
+
`NikaCompatibilityError` with capability `workflowCatalog`.
|
|
125
|
+
|
|
126
|
+
## Durable status
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const run = await nika.run('flow.nika');
|
|
130
|
+
console.log(await nika.status(run));
|
|
131
|
+
console.log(await run.done);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`status(run)` is an observation, not terminal settlement. Keep `run.done` as
|
|
135
|
+
the sole terminal promise. Native-process runs refuse `status()` because a
|
|
136
|
+
short-lived process has no independent durable status authority.
|
|
137
|
+
|
|
138
|
+
## Durable run recovery
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const recovered = await nika.attachRun(saved.jobId, {
|
|
142
|
+
lastEventId: saved.lastEventSequence,
|
|
143
|
+
});
|
|
144
|
+
for await (const event of nika.events(recovered)) {
|
|
145
|
+
await saveApplicationCheckpoint(recovered.id, event.sequence);
|
|
146
|
+
}
|
|
147
|
+
console.log(await recovered.done);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`attachRun()` is HTTP-only. It proves the job exists, returns a normal owned
|
|
151
|
+
run handle, and resumes the stream with `Last-Event-ID`. Save the job id and
|
|
152
|
+
event cursor in application durable state before the original process exits.
|
|
153
|
+
If more events arrive before the application subscribes than its configured
|
|
154
|
+
buffer can retain, `events()` refuses with `NikaEventBufferOverflowError`
|
|
155
|
+
instead of silently skipping a replay prefix.
|
|
156
|
+
|
|
157
|
+
## Contract and release alignment
|
|
158
|
+
|
|
159
|
+
Version 0.116 targets the engine train whose OpenAPI contract contains check,
|
|
160
|
+
jobs, status, events, cancellation, typed trace verification, resident
|
|
161
|
+
workflow discovery, and schedule CAS. Do not publish the SDK before the
|
|
162
|
+
matching engine release and native payload assets exist.
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Testing and release evidence
|
|
2
|
+
|
|
3
|
+
The release judge is the packed package consumed from an isolated Node
|
|
4
|
+
project, not a source import. Local development still starts with the fast
|
|
5
|
+
gates:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm ci
|
|
9
|
+
npm test
|
|
10
|
+
npm run build
|
|
11
|
+
npm run check:coverage
|
|
12
|
+
npm run check:release-evidence
|
|
13
|
+
NIKA_BIN=/path/to/nika npm run gauntlet:check
|
|
14
|
+
NIKA_BIN=/path/to/nika npm run gauntlet:run
|
|
15
|
+
NIKA_BIN=/path/to/nika npm run gauntlet:projects
|
|
16
|
+
NIKA_BIN=/path/to/nika npm run gauntlet:depth
|
|
17
|
+
NIKA_BIN=/path/to/nika npm run gauntlet:hostile
|
|
18
|
+
NIKA_BIN=/path/to/nika npm run gauntlet:recovery
|
|
19
|
+
NIKA_BIN=/path/to/nika npm run gauntlet:one-door
|
|
20
|
+
npm audit
|
|
21
|
+
npm pack --dry-run
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
All engine-backed gauntlets use `NIKA_BIN` as the canonical explicit binary.
|
|
25
|
+
`NIKA_GAUNTLET_BIN` remains a compatibility fallback for the corpus-only
|
|
26
|
+
scripts. Evidence is invalid when the recorded engine identity does not match
|
|
27
|
+
the intended release candidate. `npm run check:release-evidence` binds every
|
|
28
|
+
current committed gauntlet result and packed tarball identity to the root
|
|
29
|
+
package version. Historical ledgers are limited to an explicit allowlist and
|
|
30
|
+
must remain labelled as non-gating evidence.
|
|
31
|
+
|
|
32
|
+
CI adds a behavioral provenance replay. It downloads the Linux x64 asset for
|
|
33
|
+
the exact root package version, verifies its GitHub attestation and published
|
|
34
|
+
`SHA256SUMS` entry, then reruns all 100 deterministic workflows, the hostile suite, all five mini-SaaS projects, all five depth projects,
|
|
35
|
+
the two-process recovery scenario, and five scenarios through six execution
|
|
36
|
+
doors from a freshly packed SDK. The runner
|
|
37
|
+
mints an ephemeral run-signing key. Its
|
|
38
|
+
cancellation fixtures retain the in-process `nika:wait` cases and add an
|
|
39
|
+
owned loopback rendezvous for controlled task-boundary cancellation. They
|
|
40
|
+
need no shell command, platform sandbox, or sandbox waiver. Cancellation
|
|
41
|
+
and sealed-trace claims are exercised against the public binary. The
|
|
42
|
+
cancellation fixtures cancel an execution that is observably inside its 10 s
|
|
43
|
+
`nika:wait` (the durable status reads `running`, no longer `queued`, and a
|
|
44
|
+
further delay has passed) and record both the cancel reply and the terminal it
|
|
45
|
+
leads to. On engine 0.118 the resident answers 202 `cancellation_requested`;
|
|
46
|
+
its execution owner then records `execution.interrupted` with
|
|
47
|
+
`status=interrupted` once the grace expires inside a task, or, when the request
|
|
48
|
+
lands at a task boundary, `execution.cancelled` (the `cancel_job` writer) or
|
|
49
|
+
`execution.settled` (the racing settlement writer) with `status=cancelled` and a
|
|
50
|
+
settlement whose cause is `operator`. A cancel that lands before the execution
|
|
51
|
+
starts is a 200 `cancelled` whose terminal is one of those two writer kinds,
|
|
52
|
+
only with `status=cancelled`. The verifiers bind each cancel reply to the
|
|
53
|
+
terminals it may lead to, demand the run status of that terminal, and refuse any
|
|
54
|
+
other pairing. The parsed deterministic and packed-project results must match
|
|
55
|
+
exactly except for the recovery job UUID. The hostile comparison excludes
|
|
56
|
+
`generated_at` and per-scenario duration and canonicalizes only the two ratified
|
|
57
|
+
writer kinds of a cancelled terminal after checking the exact pairing. This proves that the
|
|
58
|
+
attested public release currently reproduces the committed behavioral claims.
|
|
59
|
+
It does not claim cryptographic proof of when the committed JSON file itself
|
|
60
|
+
was originally written.
|
|
61
|
+
|
|
62
|
+
## Test layers
|
|
63
|
+
|
|
64
|
+
1. Unit tests cover configuration, local process framing, HTTP protocol
|
|
65
|
+
validation, SSE recovery, independent observer backpressure, scheduling,
|
|
66
|
+
receipts, and typed errors.
|
|
67
|
+
2. The generated corpus holds 100 distinct use cases and 100 valid workflows.
|
|
68
|
+
3. The deterministic runner executes every workflow with `mock/echo` and
|
|
69
|
+
seals trace evidence without paid-provider dependence.
|
|
70
|
+
4. Project gauntlets install the tarball into fresh applications and exercise
|
|
71
|
+
realistic multi-step use cases.
|
|
72
|
+
5. Hostile tests mutate transport frames, timing, status codes, identities,
|
|
73
|
+
revisions, and replay order.
|
|
74
|
+
6. Public Personas use only the README, exported types, packed package, public
|
|
75
|
+
binary/help, loopback HTTP, and public documentation. They are synthetic
|
|
76
|
+
users, never substitutes for human usability evidence.
|
|
77
|
+
|
|
78
|
+
The latest public-only first-contact wave and its convergent debt are recorded
|
|
79
|
+
in [`gauntlet/personas/REPORT.md`](../gauntlet/personas/REPORT.md).
|
|
80
|
+
|
|
81
|
+
## Socratic risk matrix
|
|
82
|
+
|
|
83
|
+
Every release wave must ask and demonstrate an answer to these questions:
|
|
84
|
+
|
|
85
|
+
- Can a first-time Node user succeed from the README without repository
|
|
86
|
+
knowledge?
|
|
87
|
+
- Do ESM and CommonJS load from the packed tarball on every supported Node
|
|
88
|
+
major?
|
|
89
|
+
- Does a workflow the engine refuses reject native `run()` with the engine's
|
|
90
|
+
code and findings, on every refusal dialect a supported engine writes, with
|
|
91
|
+
no run handle, no second spawn and no preflight check? Does output that
|
|
92
|
+
proves neither admission nor refusal stay a protocol fault? The answer is
|
|
93
|
+
`test/native-run-admission.test.ts`. Its `wire-*` cases replay stdout and
|
|
94
|
+
stderr captured byte-for-byte from real engines
|
|
95
|
+
(`test/fixtures/run-wire/README.md` records which, and how); its
|
|
96
|
+
`SYNTHETIC` cases are invented hostile shapes. A replay proves the SDK
|
|
97
|
+
decodes those bytes, never that an engine still writes them: a new engine
|
|
98
|
+
release needs a new capture.
|
|
99
|
+
- What happens if the server dies after admission but before the first SSE
|
|
100
|
+
frame?
|
|
101
|
+
- What happens if SSE reconnects after a duplicate, gap, conflicting replay,
|
|
102
|
+
or terminal race?
|
|
103
|
+
- Can one slow observer overflow without damaging another observer or
|
|
104
|
+
`run.result()`?
|
|
105
|
+
- Does one application read the same lifecycle words over the native process
|
|
106
|
+
and over HTTP, with `event.raw` still the protocol frame, and without a
|
|
107
|
+
per-task event the resident never streamed?
|
|
108
|
+
- Does a frame whose state word is absent, null, future, or still running
|
|
109
|
+
stay an `engine.event` instead of being named settled?
|
|
110
|
+
- Is a human gate (`paused`) kept apart from both failure and completion, and
|
|
111
|
+
the engine's `interrupted` evidence apart from a broken observation?
|
|
112
|
+
- Can two clients race the same idempotency key with equal and unequal
|
|
113
|
+
snapshots?
|
|
114
|
+
- Does cancellation win or replay honestly when settlement races it?
|
|
115
|
+
- Does a stale schedule writer receive the current revision without mutating
|
|
116
|
+
durable state?
|
|
117
|
+
- Do process and server restarts preserve the facts the API claims are
|
|
118
|
+
durable?
|
|
119
|
+
- Can a replacement client reattach with its last committed SSE cursor without
|
|
120
|
+
replaying an application side effect?
|
|
121
|
+
- Are auth failures, token rotation, malformed content types, compressed
|
|
122
|
+
bodies, oversized frames, invalid UTF-8, and timeouts typed and redacted?
|
|
123
|
+
- Are receipt job, execution, and trace identities consistent across SSE,
|
|
124
|
+
durable state, and verification?
|
|
125
|
+
- Is the run-signing private key still confined to engine custody, with only
|
|
126
|
+
public trust material entering application infrastructure?
|
|
127
|
+
- Does every live OpenAPI route have a deliberate SDK treatment?
|
|
128
|
+
- Does every documented example compile and run from the tarball?
|
|
129
|
+
- Does the version agree across package metadata, lockfile, optional native
|
|
130
|
+
packages, OpenAPI identity, engine release, npm, and GitHub?
|
|
131
|
+
- Can a claimed capability be deleted without a gate becoming red? If yes,
|
|
132
|
+
the capability is not yet wired.
|
|
133
|
+
|
|
134
|
+
## Release evidence
|
|
135
|
+
|
|
136
|
+
Record exact commands, versions, commit SHAs, platform, run counts, cost, and
|
|
137
|
+
the path to machine-readable results. A green unit suite alone is never release
|
|
138
|
+
evidence. A failed or skipped lane stays named; it is not rounded into a pass.
|
|
139
|
+
|
|
140
|
+
The release ceremony is deliberately two-step. `release.yml` validates the
|
|
141
|
+
tagged engine assets, starts the released Linux binary, proves the live
|
|
142
|
+
OpenAPI/types pin, embeds the exact prepared commit and release version in all
|
|
143
|
+
five package manifests before packing, and publishes four payloads plus the SDK
|
|
144
|
+
through npm trusted publishing with GitHub OIDC and Sigstore provenance bound
|
|
145
|
+
to the workflow identity. Every package registers organization `supernovae-st`,
|
|
146
|
+
repository `nika-client`, workflow filename `release.yml`, and environment
|
|
147
|
+
`npm-publish`, with direct `npm publish` enabled. The GitHub-hosted publish job
|
|
148
|
+
uses Node 24, npm 11.19.1 and `id-token: write`; it receives no npm write token.
|
|
149
|
+
`release-heal.yml` dispatches that same file on `main`; it does not publish or
|
|
150
|
+
exchange an OIDC token itself. All five package manifests identify the SDK
|
|
151
|
+
repository; native `SOURCE.json` still identifies the separate engine source.
|
|
152
|
+
See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
|
|
153
|
+
An occupied version is accepted only after its exact
|
|
154
|
+
prepared tarball integrity and fetched registry bytes match; errors other than
|
|
155
|
+
an explicit registry 404 refuse publication. `release-finalize.yml` refuses to create the SDK tag and GitHub
|
|
156
|
+
Release until all five exact versions are publicly observable on npm and every
|
|
157
|
+
published manifest carries the same prepared commit and version.
|
|
158
|
+
|
|
159
|
+
## One-door parity and process supervision
|
|
160
|
+
|
|
161
|
+
`gauntlet:one-door` compares CLI, raw HTTP by name and snapshot, and packed SDK
|
|
162
|
+
native, by-name and snapshot execution. It checks success, failure, recovery,
|
|
163
|
+
paused observation and controlled cancellation. `NIKA_ONE_DOOR_REPORT` names
|
|
164
|
+
its output file; CI retains it alongside the replay results. Development mode
|
|
165
|
+
uses an offline installation of the freshly packed SDK with the explicit
|
|
166
|
+
`NIKA_BIN`. Public npm parity requires `NIKA_PUBLIC_SDK_VERSION` and an outer
|
|
167
|
+
artifact-provenance gate; runtime agreement alone is not an attestation.
|
|
168
|
+
|
|
169
|
+
All harnesses own their child processes, impose finite deadlines and await
|
|
170
|
+
cleanup before emitting green evidence. The corpus runs in a fresh project and
|
|
171
|
+
HOME. Changed fixtures require new measured results: old committed ledgers
|
|
172
|
+
remain historical observations until a successful exact-version replay replaces
|
|
173
|
+
them. Never relabel an old binary or weaken the replay comparison.
|