synartesis 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arhaan Khan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,501 @@
1
+ # Synartesis
2
+
3
+ An undo layer for AI agents.
4
+
5
+ [![check](https://github.com/ArhaanDev24/Synartesis/actions/workflows/check.yml/badge.svg)](https://github.com/ArhaanDev24/Synartesis/actions/workflows/check.yml)
6
+ [![MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE)
7
+
8
+ An agent with write access to a real system runs twenty steps, misreads step
9
+ seven, and applies the rest to the wrong records. Today your options are to
10
+ reverse it by hand from the transcript, restore a backup and lose every
11
+ legitimate change made in the same window, or accept the damage.
12
+
13
+ Synartesis sits between your MCP client and the servers it talks to. It records
14
+ every tool call with the state that call replaced, and it can put that state
15
+ back. What cannot be put back, it refuses to let an agent do unsupervised.
16
+
17
+ It is not a sandbox: the container your agent runs in is disposable, but the
18
+ CRM row it updated over the network is not. It is not a tracing tool: a trace
19
+ tells you `update_customer` ran forty times, not what the values were before.
20
+
21
+ ## What it can and cannot do
22
+
23
+ Every tool gets one of four classifications, which you write down in a manifest:
24
+
25
+ | Class | Meaning | Example | What happens |
26
+ |---|---|---|---|
27
+ | `readonly` | Changes nothing | `get_customer` | Recorded, forwarded |
28
+ | `reversible` | Prior state can be restored exactly | `update_customer` | State captured before the write; written back on undo |
29
+ | `compensable` | Cannot be reversed, but can be offset | `create_charge` | A different call neutralises it |
30
+ | `irreversible` | Neither | `send_email` | **Suspended until a human approves it** |
31
+
32
+ A tool your manifest does not mention is treated as `irreversible`. That is
33
+ deliberate: silently forwarding an unknown destructive call is the one failure
34
+ worth avoiding most.
35
+
36
+ ## Requirements
37
+
38
+ | Tool | Version | Check with |
39
+ |---|---|---|
40
+ | Node | 22 or newer | `node --version` |
41
+ | pnpm | 9 or newer | `pnpm --version` |
42
+ | A C toolchain | any | `cc --version` |
43
+
44
+ `pnpm` comes with Node via corepack:
45
+
46
+ ```bash
47
+ corepack enable pnpm
48
+ ```
49
+
50
+ The C toolchain is needed once, to compile SQLite's native bindings. On macOS
51
+ run `xcode-select --install`; on Debian or Ubuntu, `apt install build-essential`.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ curl -fsSL https://raw.githubusercontent.com/ArhaanDev24/Synartesis/main/install.sh | bash
57
+ ```
58
+
59
+ Or from a clone, if you would rather read it first:
60
+
61
+ ```bash
62
+ git clone https://github.com/ArhaanDev24/Synartesis.git && cd Synartesis && ./install.sh
63
+ ```
64
+
65
+ The script checks your Node version, builds, and links `synartesis` and
66
+ `synartesis-proxy` into the first writable directory already on your PATH. It
67
+ edits no shell profile and needs no sudo. Pass `--no-link` to build only.
68
+
69
+ ```bash
70
+ synartesis --help
71
+ ```
72
+
73
+ If nothing could be linked, nothing breaks: every command Synartesis prints
74
+ spells itself out in whichever form actually runs on your machine.
75
+
76
+ ## Walkthrough
77
+
78
+ This uses a toy CRM that ships with the repo, so you can see the whole loop
79
+ without pointing anything at real data. Run it from a scratch directory.
80
+
81
+ ```bash
82
+ mkdir -p /tmp/synartesis-demo && cd /tmp/synartesis-demo
83
+ ```
84
+
85
+ ### 1. Write a policy
86
+
87
+ `init` starts a server, asks it what tools it has, and writes a manifest.
88
+ Replace `SYNARTESIS` with the path you cloned into.
89
+
90
+ ```bash
91
+ node SYNARTESIS/dist/cli.js init crm -- node SYNARTESIS/dist/toy-crm.js --state ./crm.json
92
+ ```
93
+
94
+ `init` prints the path it wrote to. Unless a policy already sits above the
95
+ directory you are standing in, that is `~/.synartesis/synartesis.yaml`. Open it:
96
+ every tool that isn't a self-declared read starts as `irreversible` with a
97
+ `TODO`. **Working through those TODOs is the job.** A finished policy for this
98
+ fixture ships in the repo, so copy it into this directory rather than typing it
99
+ out — a policy here takes precedence over the one in your home:
100
+
101
+ ```bash
102
+ cp SYNARTESIS/manifests/toy-crm.yaml ./synartesis.yaml
103
+ ```
104
+
105
+ Then edit the `args` line so it points at your clone and keeps its data in
106
+ this directory. The copy ships with `args: ["dist/toy-crm.js"]`; it needs both
107
+ the path to your clone and the `--state` file:
108
+
109
+ ```yaml
110
+ servers:
111
+ crm:
112
+ command: node
113
+ args: ["SYNARTESIS/dist/toy-crm.js", "--state", "./crm.json"]
114
+ ```
115
+
116
+ ### 2. Point your agent at the proxy
117
+
118
+ Wherever your MCP client lists servers, replace the entry for the server you
119
+ want covered with the proxy. For Claude Desktop or Claude Code that is a
120
+ `mcpServers` block:
121
+
122
+ ```json
123
+ {
124
+ "mcpServers": {
125
+ "crm": {
126
+ "command": "node",
127
+ "args": ["SYNARTESIS/dist/proxy.js", "--manifest", "/tmp/synartesis-demo/synartesis.yaml"]
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ The agent sees the same tools with the same names and the same results. That is
134
+ the point: nothing about your agent changes.
135
+
136
+ For this walkthrough you do not need a real agent. This does the same thing:
137
+
138
+ ```bash
139
+ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"demo-agent","version":"0"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"update_customer","arguments":{"id":"c_001","plan":"free","notes":"wrong edit"}}}' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"delete_customer","arguments":{"id":"c_002"}}}' | node SYNARTESIS/dist/proxy.js --manifest ./synartesis.yaml --journal ./journal.db > /dev/null
140
+ ```
141
+
142
+ Look at the damage:
143
+
144
+ ```bash
145
+ cat crm.json
146
+ ```
147
+
148
+ Ada is on the wrong plan with the wrong notes, and Grace is gone.
149
+
150
+ ### 3. See what it did
151
+
152
+ ```bash
153
+ node SYNARTESIS/dist/cli.js list --journal ./journal.db
154
+ ```
155
+
156
+ ```bash
157
+ node SYNARTESIS/dist/cli.js show RUN_ID --journal ./journal.db
158
+ ```
159
+
160
+ `show` prints each call with its class, its status, and the exact call that
161
+ would undo it, already resolved to literal values.
162
+
163
+ ### 4. Undo it
164
+
165
+ Look before you leap:
166
+
167
+ ```bash
168
+ node SYNARTESIS/dist/cli.js undo RUN_ID --dry-run --journal ./journal.db
169
+ ```
170
+
171
+ Then do it:
172
+
173
+ ```bash
174
+ node SYNARTESIS/dist/cli.js undo RUN_ID --journal ./journal.db
175
+ ```
176
+
177
+ ```bash
178
+ cat crm.json
179
+ ```
180
+
181
+ Grace is back and Ada is on her original plan, with her original notes.
182
+
183
+ ### 5. Watch it refuse
184
+
185
+ Undo is not a blunt instrument. If something else changed a record after the
186
+ agent touched it, writing the old value back would destroy that work, so
187
+ Synartesis stops and shows you both values.
188
+
189
+ Run the damage command from step 2 again. That creates a second run, so take
190
+ the run id from the top of `list`, which is ordered most recent first. Then
191
+ edit the record by hand:
192
+
193
+ ```bash
194
+ node -e 'const f="./crm.json",s=JSON.parse(require("fs").readFileSync(f));s.customers.c_001.notes="a human wrote this";require("fs").writeFileSync(f,JSON.stringify(s,null,2))'
195
+ ```
196
+
197
+ ```bash
198
+ node SYNARTESIS/dist/cli.js undo RUN_ID --journal ./journal.db
199
+ ```
200
+
201
+ It halts at the record that moved, prints the expected and actual state, and
202
+ exits non-zero. It is not all-or-nothing: undo works newest first, so anything
203
+ it had already put back before reaching the drifted record stays put back —
204
+ here `delete_customer` is reversed and `c_002` comes back, and then it stops.
205
+ `show` afterwards tells you which actions are still outstanding. Once you have
206
+ resolved the conflict yourself, `undo --replan` rebuilds the plan against the
207
+ world as it now is and carries on.
208
+
209
+ ## Approving what cannot be undone
210
+
211
+ `send_email` is classified `irreversible`, so the agent cannot send one on its
212
+ own. The call is **refused immediately** with an action id and the command that
213
+ would approve it. The agent tells you, you decide, and it tries again.
214
+
215
+ It does not hold the call open while waiting. That was the first design and it
216
+ does not survive contact with a real client: every useful window for a person
217
+ to notice, open a terminal and decide is longer than a client will wait for a
218
+ tool, so the two cannot be reconciled by picking a better timeout.
219
+
220
+ Approval also does not happen on the terminal the agent is using: the proxy
221
+ talks MCP over stdin and stdout, so there is nothing there to prompt on, and a
222
+ desktop client has no terminal at all. The request goes to the journal, and you
223
+ answer it from anywhere:
224
+
225
+ ```bash
226
+ node SYNARTESIS/dist/cli.js gates --journal ./journal.db
227
+ ```
228
+
229
+ ```bash
230
+ node SYNARTESIS/dist/cli.js approve ACTION_ID --by your-name --journal ./journal.db
231
+ ```
232
+
233
+ ```bash
234
+ node SYNARTESIS/dist/cli.js deny ACTION_ID --by your-name --reason "not this one" --journal ./journal.db
235
+ ```
236
+
237
+ An approval is **single use** and expires after an hour, so it covers the retry
238
+ it was granted for and cannot quietly authorise the same call tomorrow. It is
239
+ not tied to one session, because people restart their client and an approval
240
+ stranded in a dead session would be no approval at all.
241
+
242
+ Nothing is ever approved by silence. An unanswered request simply stays
243
+ unanswered, visible in `synartesis gates` until someone decides.
244
+
245
+ The agent is told all of this when it connects, so it can explain itself rather
246
+ than reporting an opaque failure.
247
+
248
+ ## Real servers
249
+
250
+ If you would rather follow steps than read about it, there is a
251
+ [guide to running this against your own files](TESTING-ON-A-REAL-SYSTEM.md),
252
+ with the gate and the drift check as the two things worth testing on purpose.
253
+
254
+ Synartesis has nothing to do with email in particular. It sits on the MCP
255
+ protocol, so its subject is whatever the servers you have connected can do:
256
+ your files, your repositories, your database, your tickets, your agent's own
257
+ memory. What it can undo depends entirely on what those servers expose, and
258
+ each manifest below says plainly where that runs out.
259
+
260
+ | Manifest | Server | State it governs |
261
+ | --- | --- | --- |
262
+ | [`filesystem.yaml`](manifests/filesystem.yaml) | `@modelcontextprotocol/server-filesystem` | real files on disk |
263
+ | [`memory.yaml`](manifests/memory.yaml) | `@modelcontextprotocol/server-memory` | the knowledge graph an agent keeps about you |
264
+ | [`git.yaml`](manifests/git.yaml) | `mcp-server-git` | a real repository's index and history |
265
+ | [`github.yaml`](manifests/github.yaml) | `github/github-mcp-server` | issues, pull requests, file contents |
266
+ | [`toy-crm.yaml`](manifests/toy-crm.yaml) | the fixture in this repo | the worked example of every class |
267
+
268
+ Every one of those but `github.yaml` was checked against the server actually
269
+ running. Two demos run the whole loop for real:
270
+
271
+ ```bash
272
+ ./demo/filesystem-demo.sh
273
+ ./demo/memory-demo.sh
274
+ ```
275
+
276
+ The filesystem demo overwrites a file and moves another, restores both, then
277
+ shows undo refusing when a human edited the file in between, and the gate
278
+ refusing to create a directory this server has no way to remove.
279
+
280
+ The memory demo is the sharper one. The agent adds two people to the graph, one
281
+ of whom was already there, and the server quietly ignores the duplicate. Undo
282
+ therefore has to remove exactly one of them: the inverse is built from what the
283
+ server said it created, not from what the agent asked for, so the person who
284
+ was there first survives being undone. The same session then tries to delete an
285
+ entity and is held, because deleting an entity also deletes every relation
286
+ touching it and one inverse call cannot put back both.
287
+
288
+ ### Where each one runs out
289
+
290
+ The limits are the interesting part, and they are properties of the servers
291
+ rather than of Synartesis.
292
+
293
+ - **filesystem**: `move_file` is reversible from its arguments alone, so no
294
+ pre-read is declared and drift cannot be checked for it. `create_directory`
295
+ is `irreversible` not because directories are precious but because this
296
+ server exposes no way to remove one.
297
+ - **memory**: `add_observations` and `delete_observations` are exact opposites
298
+ that disagree about what to call the same field. A path can read a field and
299
+ cannot rename one, so that inverse cannot be written at all and the call is
300
+ gated instead.
301
+ - **git**: nearly every read this server offers answers in prose meant for a
302
+ person, so almost nothing can be inverted from a captured state however
303
+ reversible the underlying git operation is. Commits are gated because this
304
+ server exposes no reset, no revert, and no way to move a branch.
305
+
306
+ Two things worth knowing if you write your own, both found by running these
307
+ against live servers rather than by reading documentation:
308
+
309
+ `$result` is the structured block, and it need not match the text one. The
310
+ memory server answers `create_entities` with a bare list in its text block and
311
+ `{"entities": [...]}` in `structuredContent`. Synartesis walks the structured
312
+ one, because that is the machine-readable contract.
313
+
314
+ And `synartesis check` proves a tool exists, not that a path resolves. It
315
+ cannot: no call has been made, so there is no result to walk. Run the thing
316
+ once and read `synartesis show` before you rely on an inverse.
317
+
318
+ ## Writing a manifest
319
+
320
+ The manifest is the whole product. It should take fifteen minutes for an API
321
+ you know.
322
+
323
+ ```yaml
324
+ version: 1
325
+
326
+ servers:
327
+ crm:
328
+ command: node
329
+ args: ["./crm-server.js"]
330
+
331
+ tools:
332
+ - match: "crm.get_customer"
333
+ class: readonly
334
+
335
+ # Read the record before overwriting it, then write that record back.
336
+ - match: "crm.update_customer"
337
+ class: reversible
338
+ snapshot:
339
+ tool: "crm.get_customer"
340
+ args:
341
+ id: "$.id"
342
+ inverse:
343
+ tool: "crm.update_customer"
344
+ args:
345
+ id: "$.id"
346
+ name: "$snapshot.name"
347
+ plan: "$snapshot.plan"
348
+
349
+ # Nothing to read beforehand; the id only exists once the call returns.
350
+ - match: "crm.create_customer"
351
+ class: compensable
352
+ inverse:
353
+ tool: "crm.delete_customer"
354
+ args:
355
+ id: "$result.id"
356
+
357
+ - match: "crm.send_*"
358
+ class: irreversible
359
+ gate: always
360
+ ```
361
+
362
+ There are exactly three things a value can refer to:
363
+
364
+ | Prefix | Refers to | Available in |
365
+ |---|---|---|
366
+ | `$.` | the arguments the agent sent | `snapshot` and `inverse` |
367
+ | `$snapshot.` | what the pre-read captured | `inverse` |
368
+ | `$result.` | what the forward call returned | `inverse` |
369
+
370
+ Anything else is a literal. A reference can stand alone, in which case the
371
+ value keeps its type, or sit inside a sentence, in which case it is substituted
372
+ as text:
373
+
374
+ ```yaml
375
+ sha: "$result.content.sha" # the value itself
376
+ message: "Revert agent change to $.path" # text with the path substituted
377
+ ```
378
+
379
+ Write `$$` for a literal dollar sign. There are no expressions, conditionals or
380
+ functions, and there will not be: the moment this becomes a language it stops
381
+ being something you can write in fifteen minutes.
382
+
383
+ Paths can index a list with `[0]` and read one field from every element with
384
+ `[]`:
385
+
386
+ ```yaml
387
+ labels: "$snapshot.labels[].name" # [{name: "bug"}, ...] becomes ["bug", ...]
388
+ ```
389
+
390
+ That covers the common case where an API hands a field back richer than it
391
+ takes it, which is what GitHub does with issue labels. `[]` reads the same key
392
+ from each element and nothing else: it is still a path, not a transform. A
393
+ reference copies values, it cannot compute them, so an API needing a genuinely
394
+ different shape is one the inverse should leave that field out of, and say so.
395
+
396
+ Other things to know:
397
+
398
+ - `match` supports `*`, which matches within one segment: `crm.send_*` matches
399
+ `crm.send_email` but not `crm.a.b`. The most specific pattern wins regardless
400
+ of the order rules are written in.
401
+ - The inverse of a patch should restore **every** field, not re-apply a patch.
402
+ If the same record is edited twice in one run, a partial inverse leaves the
403
+ fields the second edit touched behind.
404
+ - `gate: on_write` is a heuristic for tools like a raw SQL runner, where
405
+ destructiveness cannot be read from the tool name. Anything it cannot
406
+ confidently read as a single read statement is gated. Use `gate: always`
407
+ wherever certainty matters.
408
+ - A malformed manifest stops the proxy from starting, with the file and line to
409
+ fix. It will never run with a policy it could not understand.
410
+
411
+ ## Commands
412
+
413
+ | Command | Does |
414
+ |---|---|
415
+ | `init <server> -- <cmd>` | Introspect a server and draft a manifest |
416
+ | `list` | Every recorded run |
417
+ | `show <runId>` | One run's timeline, with the undo for each step |
418
+ | `gates` | What is waiting for a decision |
419
+ | `approve <actionId>` | Allow a suspended call |
420
+ | `deny <actionId>` | Refuse one |
421
+ | `undo <runId>` | Reverse a run, newest action first |
422
+ | `undo <runId> --replan` | Same, but rebuild each undo from the current manifest |
423
+ | `check` | Load a manifest and verify it against the servers it names |
424
+
425
+ `--manifest` and `--journal` are found rather than typed. Both are looked for
426
+ from the current directory upwards, the way a version control tool finds its
427
+ root, so inside a project that has a `synartesis.yaml` every command works with
428
+ no flags at all. When there is nothing above you either, both come from
429
+ `~/.synartesis` — most of what anyone guards is not part of a project, and
430
+ should not need a directory of its own. `SYNARTESIS_HOME` moves that. A journal
431
+ that does not exist yet is placed beside the policy, so the proxy that creates
432
+ it and the CLI that reads it agree without either being told.
433
+
434
+ Other flags: `--dry-run`, `--to <seq>` and `--replan` on `undo`, `--all` on
435
+ `approve` and `deny`, `--json` on `list`, `show` and `gates`.
436
+
437
+ Exit codes: `0` succeeded, `1` halted or refused, `2` bad usage or
438
+ configuration.
439
+
440
+ The proxy takes `--manifest`, `--journal`, `--gate-timeout <seconds>` and
441
+ `--log-level`. It logs structured JSON to stderr; stdout is reserved for
442
+ protocol traffic.
443
+
444
+ ## What it does not do
445
+
446
+ - **It cannot un-send what has been seen.** An email that has been read, a
447
+ posted message, a file deleted with no backup. This is why the gate exists.
448
+ - **Compensable actions cannot be checked for drift.** They declare no pre-read,
449
+ so undo compensates them and marks them `[unverified]` in its report.
450
+ - **Undo halts on uncertainty, and steps over the merely permanent.** Drift, an
451
+ unknown outcome, or a failed reversing call stop it, because continuing past
452
+ those could destroy something. An action that simply cannot be undone, like a
453
+ sent email, is reported and left in place while everything else is reverted:
454
+ no amount of stopping un-sends it, and stopping would only leave the rest
455
+ wrong too. Either way the run is marked `partial`.
456
+ - **A call interrupted mid-flight is recorded as unknown**, not as failed. Undo
457
+ refuses to walk past it, because whether it applied cannot be determined.
458
+ - **An undo is only as good as the policy that recorded it.** Inverses are
459
+ resolved when the call happens, not when you undo, so a mistake in a manifest
460
+ is baked into every run made under it. `undo --replan` rebuilds them from a
461
+ corrected manifest using the state already captured, which is the way out.
462
+
463
+ ## Watching it work
464
+
465
+ Synartesis is not a daemon and cannot be one. An MCP client spawns a stdio
466
+ server itself and owns its lifetime, so nothing long-running could sit in
467
+ between and see those calls. What a person wants from a daemon is usually the
468
+ reassurance that it is there and doing something, and that needs somewhere to
469
+ look rather than a background process:
470
+
471
+ ```bash
472
+ synartesis watch
473
+ ```
474
+
475
+ It redraws as the agent works: what has been called, what class each call was,
476
+ and anything waiting on a decision, with the command to approve it. Ctrl-C
477
+ stops it. Piped rather than run in a terminal, it prints the state once and
478
+ exits.
479
+
480
+ ## Trust
481
+
482
+ A manifest names commands and Synartesis runs them. Treat one you did not write
483
+ the way you would treat a shell script from the same source: read it first.
484
+ There is no sandbox here, and there is not meant to be.
485
+
486
+ ## Development
487
+
488
+ ```bash
489
+ pnpm test
490
+ ```
491
+
492
+ ```bash
493
+ pnpm typecheck && pnpm lint
494
+ ```
495
+
496
+ Every push runs those on Linux and macOS across Node 22 and 24, plus the demo
497
+ and the installer.
498
+
499
+ ## Licence
500
+
501
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,85 @@
1
+ // src/errors.ts
2
+ var SynartesisError = class extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = new.target.name;
6
+ }
7
+ };
8
+ var ManifestError = class extends SynartesisError {
9
+ constructor(message, location) {
10
+ super(
11
+ location === void 0 ? message : `${location.file}:${String(location.line)}:${String(location.column)}: ${message}`
12
+ );
13
+ this.location = location;
14
+ }
15
+ location;
16
+ code = "MANIFEST_ERROR";
17
+ };
18
+ var UpstreamError = class extends SynartesisError {
19
+ constructor(server, operation, cause) {
20
+ super(`upstream ${server} failed during ${operation}: ${describe(cause)}`, { cause });
21
+ this.server = server;
22
+ this.operation = operation;
23
+ }
24
+ server;
25
+ operation;
26
+ code = "UPSTREAM_ERROR";
27
+ };
28
+ var SnapshotError = class extends SynartesisError {
29
+ constructor(tool, reason, options) {
30
+ super(`snapshot via ${tool} failed: ${reason}`, options);
31
+ this.tool = tool;
32
+ this.absent = options?.absent ?? false;
33
+ }
34
+ tool;
35
+ code = "SNAPSHOT_ERROR";
36
+ absent;
37
+ };
38
+ var DriftConflict = class extends SynartesisError {
39
+ constructor(seq, expected, actual) {
40
+ super(
41
+ `drift at sequence ${String(seq)}: the resource is not in the state this run left it in.
42
+ expected: ${JSON.stringify(expected)}
43
+ actual: ${JSON.stringify(actual)}`
44
+ );
45
+ this.seq = seq;
46
+ this.expected = expected;
47
+ this.actual = actual;
48
+ }
49
+ seq;
50
+ expected;
51
+ actual;
52
+ code = "DRIFT_CONFLICT";
53
+ };
54
+ var RollbackHalted = class extends SynartesisError {
55
+ constructor(seq, reason, options) {
56
+ super(`rollback halted at sequence ${String(seq)}: ${reason}`, options);
57
+ this.seq = seq;
58
+ }
59
+ seq;
60
+ code = "ROLLBACK_HALTED";
61
+ };
62
+ var JournalError = class extends SynartesisError {
63
+ code = "JOURNAL_ERROR";
64
+ constructor(operation, cause) {
65
+ super(`journal ${operation} failed: ${describe(cause)}`, { cause });
66
+ }
67
+ };
68
+ function describe(cause) {
69
+ if (cause instanceof Error) {
70
+ return cause.message;
71
+ }
72
+ return typeof cause === "string" ? cause : JSON.stringify(cause);
73
+ }
74
+
75
+ export {
76
+ SynartesisError,
77
+ ManifestError,
78
+ UpstreamError,
79
+ SnapshotError,
80
+ DriftConflict,
81
+ RollbackHalted,
82
+ JournalError,
83
+ describe
84
+ };
85
+ //# sourceMappingURL=chunk-K3QIPVBY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts"],"sourcesContent":["/**\n * The taxonomy from spec 3.5. Classes are added as the phase that raises them\n * lands, so every class here has a live throw site.\n */\nexport abstract class SynartesisError extends Error {\n abstract readonly code: string;\n\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = new.target.name;\n }\n}\n\nexport interface SourceLocation {\n readonly file: string;\n readonly line: number;\n readonly column: number;\n}\n\n/**\n * An invalid or unmatched policy. Carries a source location wherever the\n * manifest is at fault, because \"never start with a broken policy\" is only\n * useful if the operator is told which line to fix.\n */\nexport class ManifestError extends SynartesisError {\n readonly code = \"MANIFEST_ERROR\";\n\n constructor(\n message: string,\n readonly location?: SourceLocation,\n ) {\n super(\n location === undefined\n ? message\n : `${location.file}:${String(location.line)}:${String(location.column)}: ${message}`,\n );\n }\n}\n\n/** The wrapped server failed, or could not be reached at all. */\nexport class UpstreamError extends SynartesisError {\n readonly code = \"UPSTREAM_ERROR\";\n\n constructor(\n readonly server: string,\n readonly operation: string,\n cause: unknown,\n ) {\n super(`upstream ${server} failed during ${operation}: ${describe(cause)}`, { cause });\n }\n}\n\n/**\n * The pre-read failed, so the action must not proceed. A reversible action\n * without a snapshot is silently irreversible, which is the one outcome this\n * product exists to prevent.\n */\nexport class SnapshotError extends SynartesisError {\n readonly code = \"SNAPSHOT_ERROR\";\n\n constructor(\n readonly tool: string,\n reason: string,\n options?: { cause?: unknown; absent?: boolean },\n ) {\n super(`snapshot via ${tool} failed: ${reason}`, options);\n /**\n * The read reached the server and the server said no such resource, as\n * opposed to the read not completing at all. Only the former tells us\n * anything about the resource itself.\n */\n this.absent = options?.absent ?? false;\n }\n\n readonly absent: boolean;\n}\n\n/**\n * The resource changed after the agent touched it. Writing the old value back\n * would silently destroy whatever happened in between, so both values are\n * carried here for a human to judge.\n */\nexport class DriftConflict extends SynartesisError {\n readonly code = \"DRIFT_CONFLICT\";\n\n constructor(\n readonly seq: number,\n readonly expected: unknown,\n readonly actual: unknown,\n ) {\n super(\n `drift at sequence ${String(seq)}: the resource is not in the state this run left it in.\\n` +\n ` expected: ${JSON.stringify(expected)}\\n` +\n ` actual: ${JSON.stringify(actual)}`,\n );\n }\n}\n\n/**\n * An inverse failed, so the run is partially reverted. Continuing past it would\n * produce a state that is neither the before nor the after (D6).\n */\nexport class RollbackHalted extends SynartesisError {\n readonly code = \"ROLLBACK_HALTED\";\n\n constructor(\n readonly seq: number,\n reason: string,\n options?: { cause?: unknown },\n ) {\n super(`rollback halted at sequence ${String(seq)}: ${reason}`, options);\n }\n}\n\n/**\n * Not in spec 3.5, which covers failures on the proxy's forward path. A journal\n * write failing is different in kind: it means the record of what the agent did\n * is incomplete, so the call must not proceed. Always fatal, never swallowed.\n */\nexport class JournalError extends SynartesisError {\n readonly code = \"JOURNAL_ERROR\";\n\n constructor(operation: string, cause: unknown) {\n super(`journal ${operation} failed: ${describe(cause)}`, { cause });\n }\n}\n\nexport function describe(cause: unknown): string {\n if (cause instanceof Error) {\n return cause.message;\n }\n return typeof cause === \"string\" ? cause : JSON.stringify(cause);\n}\n"],"mappings":";AAIO,IAAe,kBAAf,cAAuC,MAAM;AAAA,EAGlD,YAAY,SAAiB,SAA+B;AAC1D,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO,WAAW;AAAA,EACzB;AACF;AAaO,IAAM,gBAAN,cAA4B,gBAAgB;AAAA,EAGjD,YACE,SACS,UACT;AACA;AAAA,MACE,aAAa,SACT,UACA,GAAG,SAAS,IAAI,IAAI,OAAO,SAAS,IAAI,CAAC,IAAI,OAAO,SAAS,MAAM,CAAC,KAAK,OAAO;AAAA,IACtF;AANS;AAAA,EAOX;AAAA,EAPW;AAAA,EAJF,OAAO;AAYlB;AAGO,IAAM,gBAAN,cAA4B,gBAAgB;AAAA,EAGjD,YACW,QACA,WACT,OACA;AACA,UAAM,YAAY,MAAM,kBAAkB,SAAS,KAAK,SAAS,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC;AAJ3E;AACA;AAAA,EAIX;AAAA,EALW;AAAA,EACA;AAAA,EAJF,OAAO;AASlB;AAOO,IAAM,gBAAN,cAA4B,gBAAgB;AAAA,EAGjD,YACW,MACT,QACA,SACA;AACA,UAAM,gBAAgB,IAAI,YAAY,MAAM,IAAI,OAAO;AAJ9C;AAUT,SAAK,SAAS,SAAS,UAAU;AAAA,EACnC;AAAA,EAXW;AAAA,EAHF,OAAO;AAAA,EAgBP;AACX;AAOO,IAAM,gBAAN,cAA4B,gBAAgB;AAAA,EAGjD,YACW,KACA,UACA,QACT;AACA;AAAA,MACE,qBAAqB,OAAO,GAAG,CAAC;AAAA,cACf,KAAK,UAAU,QAAQ,CAAC;AAAA,cACxB,KAAK,UAAU,MAAM,CAAC;AAAA,IACzC;AARS;AACA;AACA;AAAA,EAOX;AAAA,EATW;AAAA,EACA;AAAA,EACA;AAAA,EALF,OAAO;AAalB;AAMO,IAAM,iBAAN,cAA6B,gBAAgB;AAAA,EAGlD,YACW,KACT,QACA,SACA;AACA,UAAM,+BAA+B,OAAO,GAAG,CAAC,KAAK,MAAM,IAAI,OAAO;AAJ7D;AAAA,EAKX;AAAA,EALW;AAAA,EAHF,OAAO;AASlB;AAOO,IAAM,eAAN,cAA2B,gBAAgB;AAAA,EACvC,OAAO;AAAA,EAEhB,YAAY,WAAmB,OAAgB;AAC7C,UAAM,WAAW,SAAS,YAAY,SAAS,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC;AAAA,EACpE;AACF;AAEO,SAAS,SAAS,OAAwB;AAC/C,MAAI,iBAAiB,OAAO;AAC1B,WAAO,MAAM;AAAA,EACf;AACA,SAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AACjE;","names":[]}