synartesis 0.3.0 → 0.3.2

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.
@@ -35,12 +35,75 @@ var SnapshotError = class extends SynartesisError {
35
35
  code = "SNAPSHOT_ERROR";
36
36
  absent;
37
37
  };
38
+ function longestString(value) {
39
+ const pending = [value];
40
+ let best = "";
41
+ let visited = 0;
42
+ while (pending.length > 0 && visited < MAX_SNAPSHOT_NODES) {
43
+ const item = pending.pop();
44
+ visited += 1;
45
+ if (typeof item === "string") {
46
+ if (item.length > best.length) {
47
+ best = item;
48
+ }
49
+ } else if (Array.isArray(item)) {
50
+ for (const child of item) {
51
+ pending.push(child);
52
+ }
53
+ } else if (typeof item === "object" && item !== null) {
54
+ for (const child of Object.values(item)) {
55
+ pending.push(child);
56
+ }
57
+ }
58
+ }
59
+ return best;
60
+ }
61
+ var MAX_SNAPSHOT_NODES = 5e4;
62
+ var DIFF_BUDGET = 8;
63
+ function lineDiff(before, after) {
64
+ const a = before.split("\n");
65
+ const b = after.split("\n");
66
+ let head = 0;
67
+ while (head < a.length && head < b.length && a[head] === b[head]) {
68
+ head += 1;
69
+ }
70
+ let tail = 0;
71
+ while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) {
72
+ tail += 1;
73
+ }
74
+ const removed = a.slice(head, a.length - tail);
75
+ const added = b.slice(head, b.length - tail);
76
+ const show = (lines, mark) => [
77
+ ...lines.slice(0, DIFF_BUDGET).map((line) => ` ${mark} ${line}`),
78
+ ...lines.length > DIFF_BUDGET ? [` ${mark} ... ${String(lines.length - DIFF_BUDGET)} more`] : []
79
+ ];
80
+ return [
81
+ ` at line ${String(head + 1)}:`,
82
+ ...show(removed, "-"),
83
+ ...show(added, "+"),
84
+ ` ${String(removed.length)} removed, ${String(added.length)} added.`
85
+ ].join("\n");
86
+ }
87
+ function brief(value) {
88
+ if (value === void 0) {
89
+ return "undefined";
90
+ }
91
+ try {
92
+ const text = JSON.stringify(value);
93
+ return text.length <= 160 ? text : `${text.slice(0, 157)}...`;
94
+ } catch {
95
+ return "(a value too deeply nested to print)";
96
+ }
97
+ }
38
98
  var DriftConflict = class extends SynartesisError {
39
99
  constructor(seq, expected, actual) {
100
+ const before = longestString(expected);
101
+ const after = longestString(actual);
102
+ const body = before !== "" && after !== "" && before !== after ? lineDiff(before, after) : ` expected: ${brief(expected)}
103
+ actual: ${brief(actual)}`;
40
104
  super(
41
105
  `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)}`
106
+ ${body}`
44
107
  );
45
108
  this.seq = seq;
46
109
  this.expected = expected;
@@ -82,4 +145,4 @@ export {
82
145
  JournalError,
83
146
  describe
84
147
  };
85
- //# sourceMappingURL=chunk-K3QIPVBY.js.map
148
+ //# sourceMappingURL=chunk-WNLRMSDB.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 longest string anywhere in a snapshot, which for anything file-shaped is\n * the contents. Found by looking rather than by field name: a snapshot is\n * whatever the server's read returned, and servers nest the payload\n * differently. Short strings are ignored so a path or an id is never mistaken\n * for the body.\n */\nfunction longestString(value: unknown): string {\n // Walked with an explicit stack rather than by recursion. A snapshot is\n // whatever an upstream server sent back, and a recursive walk overflowed on\n // one nested about five thousand deep -- which would turn a drift halt, the\n // moment a person most needs a clear message, into a stack trace. The visit\n // cap is the same argument applied to breadth.\n const pending: unknown[] = [value];\n let best = \"\";\n let visited = 0;\n\n while (pending.length > 0 && visited < MAX_SNAPSHOT_NODES) {\n const item = pending.pop();\n visited += 1;\n\n if (typeof item === \"string\") {\n if (item.length > best.length) {\n best = item;\n }\n } else if (Array.isArray(item)) {\n // One at a time: push(...huge) exceeds the argument limit and throws.\n for (const child of item) {\n pending.push(child);\n }\n } else if (typeof item === \"object\" && item !== null) {\n for (const child of Object.values(item)) {\n pending.push(child);\n }\n }\n }\n\n return best;\n}\n\n/**\n * A ceiling on how much of a snapshot is worth walking to find its text. Any\n * real payload is found long before this; anything past it is a server sending\n * something pathological, and a drift report is the wrong place to hang.\n */\nconst MAX_SNAPSHOT_NODES = 50_000;\n\n/** How many changed lines are worth printing before it stops being readable. */\nconst DIFF_BUDGET = 8;\n\n/**\n * What changed, as lines, rather than both documents in full. Trims the common\n * head and tail so only the region that actually differs is shown.\n */\nfunction lineDiff(before: string, after: string): string {\n const a = before.split(\"\\n\");\n const b = after.split(\"\\n\");\n\n let head = 0;\n while (head < a.length && head < b.length && a[head] === b[head]) {\n head += 1;\n }\n let tail = 0;\n while (\n tail < a.length - head &&\n tail < b.length - head &&\n a[a.length - 1 - tail] === b[b.length - 1 - tail]\n ) {\n tail += 1;\n }\n\n const removed = a.slice(head, a.length - tail);\n const added = b.slice(head, b.length - tail);\n const show = (lines: readonly string[], mark: string): string[] => [\n ...lines.slice(0, DIFF_BUDGET).map((line) => ` ${mark} ${line}`),\n ...(lines.length > DIFF_BUDGET\n ? [` ${mark} ... ${String(lines.length - DIFF_BUDGET)} more`]\n : []),\n ];\n\n return [\n ` at line ${String(head + 1)}:`,\n ...show(removed, \"-\"),\n ...show(added, \"+\"),\n ` ${String(removed.length)} removed, ${String(added.length)} added.`,\n ].join(\"\\n\");\n}\n\n/** A value with no text in it, kept short enough to read. */\nfunction brief(value: unknown): string {\n // JSON.stringify returns undefined rather than a string for a top-level\n // undefined, and .length on that throws. Journal values are parsed JSON, so\n // this is the only one of its cases that can reach here.\n if (value === undefined) {\n return \"undefined\";\n }\n try {\n const text = JSON.stringify(value);\n return text.length <= 160 ? text : `${text.slice(0, 157)}...`;\n } catch {\n // JSON.stringify recurses, so it overflows on a deeply nested value and\n // throws on a circular one. Saying less is better than a stack trace in\n // place of the drift report.\n return \"(a value too deeply nested to print)\";\n }\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 what differs is\n * carried here for a human to judge.\n *\n * What differs, not both documents in full: printing the whole expected and\n * actual contents of a 200-line file buried the one line that mattered in two\n * screens of escaped JSON. Both values are still on the row, and\n * `synartesis show <run>` prints them.\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 const before = longestString(expected);\n const after = longestString(actual);\n // Two texts to compare, and they are not the same text. Anything else --\n // a resource that is simply gone, a snapshot with no body in it -- has no\n // lines to diff, so it says what it has.\n const body =\n before !== \"\" && after !== \"\" && before !== after\n ? lineDiff(before, after)\n : ` expected: ${brief(expected)}\\n actual: ${brief(actual)}`;\n\n super(\n `drift at sequence ${String(seq)}: the resource is not in the state this run left it in.\\n${body}`,\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":"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","names":[]}
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  toPayload,
25
25
  verifyAgainstServers,
26
26
  wasRefused
27
- } from "./chunk-FIIKQECN.js";
27
+ } from "./chunk-LNR5GXPG.js";
28
28
  import {
29
29
  DriftConflict,
30
30
  ManifestError,
@@ -32,7 +32,7 @@ import {
32
32
  SynartesisError,
33
33
  UpstreamError,
34
34
  describe
35
- } from "./chunk-K3QIPVBY.js";
35
+ } from "./chunk-WNLRMSDB.js";
36
36
 
37
37
  // src/cli.ts
38
38
  import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
@@ -1491,7 +1491,7 @@ function runList(journal, asJson, journalPath) {
1491
1491
  );
1492
1492
  }
1493
1493
  out("");
1494
- if (bytesOf(journalPath) > PRUNE_NAG_BYTES) {
1494
+ if ((bytesOf(journalPath) ?? 0) > PRUNE_NAG_BYTES) {
1495
1495
  out(` ${style.quiet(`This journal is ${sizeOf(journalPath)}; synartesis prune reclaims what is old enough to lose.`)}`);
1496
1496
  out("");
1497
1497
  }
@@ -1606,26 +1606,28 @@ function bytesOf(path) {
1606
1606
  try {
1607
1607
  return statSync(path).size;
1608
1608
  } catch {
1609
- return 0;
1609
+ return void 0;
1610
1610
  }
1611
1611
  }
1612
1612
  function sizeOf(path) {
1613
- try {
1614
- const bytes = bytesOf(path);
1615
- if (bytes < 1024 * 1024) {
1616
- return `${String(Math.max(1, Math.round(bytes / 1024)))} kB`;
1617
- }
1618
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1619
- } catch {
1613
+ const bytes = bytesOf(path);
1614
+ if (bytes === void 0) {
1620
1615
  return "unknown";
1621
1616
  }
1617
+ if (bytes < 1024 * 1024) {
1618
+ return `${String(Math.max(1, Math.round(bytes / 1024)))} kB`;
1619
+ }
1620
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1622
1621
  }
1623
1622
  var PRUNE_DEFAULT_DAYS = 30;
1623
+ var PRUNE_MAX_DAYS = 36500;
1624
1624
  function runPrune(argv, journal, journalPath) {
1625
1625
  const given = flag(argv, "--older-than");
1626
1626
  const days = given === void 0 ? PRUNE_DEFAULT_DAYS : Number(given);
1627
- if (!Number.isFinite(days) || days < 0) {
1628
- throw new UsageError(`--older-than takes a number of days, not ${given ?? ""}`);
1627
+ if (!Number.isFinite(days) || days < 0 || days > PRUNE_MAX_DAYS) {
1628
+ throw new UsageError(
1629
+ `--older-than takes a number of days from 0 to ${String(PRUNE_MAX_DAYS)}, not ${given ?? ""}`
1630
+ );
1629
1631
  }
1630
1632
  const before = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
1631
1633
  const stale = journal.prunableRuns(before);