@acquiredintelligence/attend 0.0.0-stage → 0.2.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.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -2
  3. package/dist/attend.d.ts +30 -0
  4. package/dist/attend.d.ts.map +1 -0
  5. package/dist/attend.js +464 -0
  6. package/dist/attend.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +4 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/core/ids.d.ts +12 -0
  12. package/dist/core/ids.d.ts.map +1 -0
  13. package/dist/core/ids.js +97 -0
  14. package/dist/core/ids.js.map +1 -0
  15. package/dist/core/reducer.d.ts +26 -0
  16. package/dist/core/reducer.d.ts.map +1 -0
  17. package/dist/core/reducer.js +437 -0
  18. package/dist/core/reducer.js.map +1 -0
  19. package/dist/core/seen.d.ts +22 -0
  20. package/dist/core/seen.d.ts.map +1 -0
  21. package/dist/core/seen.js +61 -0
  22. package/dist/core/seen.js.map +1 -0
  23. package/dist/core/state.d.ts +12 -0
  24. package/dist/core/state.d.ts.map +1 -0
  25. package/dist/core/state.js +90 -0
  26. package/dist/core/state.js.map +1 -0
  27. package/dist/core/step.d.ts +14 -0
  28. package/dist/core/step.d.ts.map +1 -0
  29. package/dist/core/step.js +17 -0
  30. package/dist/core/step.js.map +1 -0
  31. package/dist/core/threshold.d.ts +40 -0
  32. package/dist/core/threshold.d.ts.map +1 -0
  33. package/dist/core/threshold.js +72 -0
  34. package/dist/core/threshold.js.map +1 -0
  35. package/dist/core/types.d.ts +704 -0
  36. package/dist/core/types.d.ts.map +1 -0
  37. package/dist/core/types.js +24 -0
  38. package/dist/core/types.js.map +1 -0
  39. package/dist/core/window.d.ts +38 -0
  40. package/dist/core/window.d.ts.map +1 -0
  41. package/dist/core/window.js +322 -0
  42. package/dist/core/window.js.map +1 -0
  43. package/dist/decisions.d.ts +53 -0
  44. package/dist/decisions.d.ts.map +1 -0
  45. package/dist/decisions.js +115 -0
  46. package/dist/decisions.js.map +1 -0
  47. package/dist/errors.d.ts +36 -0
  48. package/dist/errors.d.ts.map +1 -0
  49. package/dist/errors.js +53 -0
  50. package/dist/errors.js.map +1 -0
  51. package/dist/index.d.ts +10 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +4 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/internal/adapter.d.ts +21 -0
  56. package/dist/internal/adapter.d.ts.map +1 -0
  57. package/dist/internal/adapter.js +62 -0
  58. package/dist/internal/adapter.js.map +1 -0
  59. package/dist/internal/assert.d.ts +5 -0
  60. package/dist/internal/assert.d.ts.map +1 -0
  61. package/dist/internal/assert.js +14 -0
  62. package/dist/internal/assert.js.map +1 -0
  63. package/dist/internal/clock.d.ts +8 -0
  64. package/dist/internal/clock.d.ts.map +1 -0
  65. package/dist/internal/clock.js +38 -0
  66. package/dist/internal/clock.js.map +1 -0
  67. package/dist/internal/http.d.ts +42 -0
  68. package/dist/internal/http.d.ts.map +1 -0
  69. package/dist/internal/http.js +122 -0
  70. package/dist/internal/http.js.map +1 -0
  71. package/dist/internal/images.d.ts +18 -0
  72. package/dist/internal/images.d.ts.map +1 -0
  73. package/dist/internal/images.js +75 -0
  74. package/dist/internal/images.js.map +1 -0
  75. package/dist/internal/json.d.ts +3 -0
  76. package/dist/internal/json.d.ts.map +1 -0
  77. package/dist/internal/json.js +5 -0
  78. package/dist/internal/json.js.map +1 -0
  79. package/dist/internal/prompt.d.ts +31 -0
  80. package/dist/internal/prompt.d.ts.map +1 -0
  81. package/dist/internal/prompt.js +149 -0
  82. package/dist/internal/prompt.js.map +1 -0
  83. package/dist/internal/shared.d.ts +12 -0
  84. package/dist/internal/shared.d.ts.map +1 -0
  85. package/dist/internal/shared.js +138 -0
  86. package/dist/internal/shared.js.map +1 -0
  87. package/dist/jev.d.ts +74 -0
  88. package/dist/jev.d.ts.map +1 -0
  89. package/dist/jev.js +156 -0
  90. package/dist/jev.js.map +1 -0
  91. package/dist/llm.d.ts +164 -0
  92. package/dist/llm.d.ts.map +1 -0
  93. package/dist/llm.js +79 -0
  94. package/dist/llm.js.map +1 -0
  95. package/dist/mcp.d.ts +130 -0
  96. package/dist/mcp.d.ts.map +1 -0
  97. package/dist/mcp.js +550 -0
  98. package/dist/mcp.js.map +1 -0
  99. package/dist/memory.d.ts +10 -0
  100. package/dist/memory.d.ts.map +1 -0
  101. package/dist/memory.js +50 -0
  102. package/dist/memory.js.map +1 -0
  103. package/dist/postgres.d.ts +25 -0
  104. package/dist/postgres.d.ts.map +1 -0
  105. package/dist/postgres.js +56 -0
  106. package/dist/postgres.js.map +1 -0
  107. package/dist/redis.d.ts +20 -0
  108. package/dist/redis.d.ts.map +1 -0
  109. package/dist/redis.js +83 -0
  110. package/dist/redis.js.map +1 -0
  111. package/dist/server/compat.d.ts +13 -0
  112. package/dist/server/compat.d.ts.map +1 -0
  113. package/dist/server/compat.js +5 -0
  114. package/dist/server/compat.js.map +1 -0
  115. package/dist/server/cursor.d.ts +15 -0
  116. package/dist/server/cursor.d.ts.map +1 -0
  117. package/dist/server/cursor.js +50 -0
  118. package/dist/server/cursor.js.map +1 -0
  119. package/dist/server/events.d.ts +42 -0
  120. package/dist/server/events.d.ts.map +1 -0
  121. package/dist/server/events.js +189 -0
  122. package/dist/server/events.js.map +1 -0
  123. package/dist/server/fanout.d.ts +28 -0
  124. package/dist/server/fanout.d.ts.map +1 -0
  125. package/dist/server/fanout.js +184 -0
  126. package/dist/server/fanout.js.map +1 -0
  127. package/dist/server/guard.d.ts +19 -0
  128. package/dist/server/guard.d.ts.map +1 -0
  129. package/dist/server/guard.js +27 -0
  130. package/dist/server/guard.js.map +1 -0
  131. package/dist/server/http.d.ts +4 -0
  132. package/dist/server/http.d.ts.map +1 -0
  133. package/dist/server/http.js +323 -0
  134. package/dist/server/http.js.map +1 -0
  135. package/dist/server/ingest.d.ts +4 -0
  136. package/dist/server/ingest.d.ts.map +1 -0
  137. package/dist/server/ingest.js +400 -0
  138. package/dist/server/ingest.js.map +1 -0
  139. package/dist/server/install.d.ts +5 -0
  140. package/dist/server/install.d.ts.map +1 -0
  141. package/dist/server/install.js +91 -0
  142. package/dist/server/install.js.map +1 -0
  143. package/dist/server/poll.d.ts +4 -0
  144. package/dist/server/poll.d.ts.map +1 -0
  145. package/dist/server/poll.js +45 -0
  146. package/dist/server/poll.js.map +1 -0
  147. package/dist/server/serve.d.ts +19 -0
  148. package/dist/server/serve.d.ts.map +1 -0
  149. package/dist/server/serve.js +238 -0
  150. package/dist/server/serve.js.map +1 -0
  151. package/dist/server/sessions.d.ts +14 -0
  152. package/dist/server/sessions.d.ts.map +1 -0
  153. package/dist/server/sessions.js +54 -0
  154. package/dist/server/sessions.js.map +1 -0
  155. package/dist/server/stream.d.ts +11 -0
  156. package/dist/server/stream.d.ts.map +1 -0
  157. package/dist/server/stream.js +216 -0
  158. package/dist/server/stream.js.map +1 -0
  159. package/dist/server/tools.d.ts +22 -0
  160. package/dist/server/tools.d.ts.map +1 -0
  161. package/dist/server/tools.js +55 -0
  162. package/dist/server/tools.js.map +1 -0
  163. package/dist/server/types.d.ts +351 -0
  164. package/dist/server/types.d.ts.map +1 -0
  165. package/dist/server/types.js +5 -0
  166. package/dist/server/types.js.map +1 -0
  167. package/dist/server.d.ts +11 -0
  168. package/dist/server.d.ts.map +1 -0
  169. package/dist/server.js +10 -0
  170. package/dist/server.js.map +1 -0
  171. package/dist/shell/budget.d.ts +4 -0
  172. package/dist/shell/budget.d.ts.map +1 -0
  173. package/dist/shell/budget.js +5 -0
  174. package/dist/shell/budget.js.map +1 -0
  175. package/dist/shell/copy.d.ts +20 -0
  176. package/dist/shell/copy.d.ts.map +1 -0
  177. package/dist/shell/copy.js +73 -0
  178. package/dist/shell/copy.js.map +1 -0
  179. package/dist/shell/decisions.d.ts +6 -0
  180. package/dist/shell/decisions.d.ts.map +1 -0
  181. package/dist/shell/decisions.js +42 -0
  182. package/dist/shell/decisions.js.map +1 -0
  183. package/dist/shell/read.d.ts +7 -0
  184. package/dist/shell/read.d.ts.map +1 -0
  185. package/dist/shell/read.js +21 -0
  186. package/dist/shell/read.js.map +1 -0
  187. package/dist/shell/scheduler.d.ts +4 -0
  188. package/dist/shell/scheduler.d.ts.map +1 -0
  189. package/dist/shell/scheduler.js +90 -0
  190. package/dist/shell/scheduler.js.map +1 -0
  191. package/dist/shell/scope.d.ts +27 -0
  192. package/dist/shell/scope.d.ts.map +1 -0
  193. package/dist/shell/scope.js +343 -0
  194. package/dist/shell/scope.js.map +1 -0
  195. package/dist/shell/types.d.ts +168 -0
  196. package/dist/shell/types.d.ts.map +1 -0
  197. package/dist/shell/types.js +2 -0
  198. package/dist/shell/types.js.map +1 -0
  199. package/dist/sqlite.d.ts +14 -0
  200. package/dist/sqlite.d.ts.map +1 -0
  201. package/dist/sqlite.js +39 -0
  202. package/dist/sqlite.js.map +1 -0
  203. package/dist/step.d.ts +18 -0
  204. package/dist/step.d.ts.map +1 -0
  205. package/dist/step.js +14 -0
  206. package/dist/step.js.map +1 -0
  207. package/dist/store/memory.d.ts +12 -0
  208. package/dist/store/memory.d.ts.map +1 -0
  209. package/dist/store/memory.js +264 -0
  210. package/dist/store/memory.js.map +1 -0
  211. package/dist/store/postgres.d.ts +24 -0
  212. package/dist/store/postgres.d.ts.map +1 -0
  213. package/dist/store/postgres.js +364 -0
  214. package/dist/store/postgres.js.map +1 -0
  215. package/dist/store/redis.d.ts +12 -0
  216. package/dist/store/redis.d.ts.map +1 -0
  217. package/dist/store/redis.js +540 -0
  218. package/dist/store/redis.js.map +1 -0
  219. package/dist/store/sqlite-worker.d.ts +91 -0
  220. package/dist/store/sqlite-worker.d.ts.map +1 -0
  221. package/dist/store/sqlite-worker.js +322 -0
  222. package/dist/store/sqlite-worker.js.map +1 -0
  223. package/dist/store/sqlite.d.ts +34 -0
  224. package/dist/store/sqlite.d.ts.map +1 -0
  225. package/dist/store/sqlite.js +238 -0
  226. package/dist/store/sqlite.js.map +1 -0
  227. package/dist/store/types.d.ts +137 -0
  228. package/dist/store/types.d.ts.map +1 -0
  229. package/dist/store/types.js +2 -0
  230. package/dist/store/types.js.map +1 -0
  231. package/dist/testing/conformance.d.ts +13 -0
  232. package/dist/testing/conformance.d.ts.map +1 -0
  233. package/dist/testing/conformance.js +542 -0
  234. package/dist/testing/conformance.js.map +1 -0
  235. package/dist/testing/wire.d.ts +16 -0
  236. package/dist/testing/wire.d.ts.map +1 -0
  237. package/dist/testing/wire.js +304 -0
  238. package/dist/testing/wire.js.map +1 -0
  239. package/dist/testing.d.ts +113 -0
  240. package/dist/testing.d.ts.map +1 -0
  241. package/dist/testing.js +655 -0
  242. package/dist/testing.js.map +1 -0
  243. package/dist/types.d.ts +5 -0
  244. package/dist/types.d.ts.map +1 -0
  245. package/dist/types.js +5 -0
  246. package/dist/types.js.map +1 -0
  247. package/package.json +130 -4
  248. package/skills/attend/SKILL.md +231 -0
  249. package/skills/attend/references/judges.md +283 -0
  250. package/skills/attend/references/kinds.md +62 -0
  251. package/skills/attend/references/mcp.md +132 -0
  252. package/skills/attend/references/sinks.md +73 -0
  253. package/skills/attend/references/stores.md +90 -0
  254. package/skills/attend/references/testing.md +120 -0
@@ -0,0 +1,542 @@
1
+ import { assertEqual, assertOk } from '../internal/assert.js';
2
+ const policy = {
3
+ closingGraceMs: 500, streamGapMs: 1000, passiveQuietMs: 4000, waitHorizonMs: 10_000,
4
+ maxWaits: 2, maxEvents: 250, maxWindowMs: 60_000, retryBackoffMs: [1000, 4000, 16_000],
5
+ judgeTimeoutMs: 15_000, deliveryTimeoutMs: 10_000, recentCount: 8, seenCount: 1000,
6
+ maxOutbox: 1000, maxDeadLetters: 1000, deadLetterBackoffMaxMs: 60_000,
7
+ };
8
+ function logged(id, now = 1000) {
9
+ return { input: { kind: 'events', events: [{ id, at: now, name: 'changed', data: { id }, kind: 'closing' }] }, now };
10
+ }
11
+ function window(id) {
12
+ return {
13
+ id, events: [{ id: `event:${id}`, at: 1, name: 'changed', data: { nested: [null, true, 'héllo 🌍\u0000'] }, text: 'héllo 🌍\u0000', kind: 'closing', seenAt: 2 }],
14
+ reason: 'closing', waits: 0, dropped: 0, gap: false, settledAt: 3,
15
+ };
16
+ }
17
+ function decision(id, scope, route = 'interrupt', at = 1000) {
18
+ return {
19
+ id, scope, route, at, window: window(id), confidence: 0.8, predicates: { relevant: 0.7 },
20
+ probabilities: { interrupt: 0.4, queue: 0.2, note: 0.1, ask: 0.1, skip: 0.1, wait: 0.1 },
21
+ };
22
+ }
23
+ function append(id, scope = 's', route = 'interrupt', at = 1000) {
24
+ return { kind: 'decision', id, scope, route, at, body: decision(id, scope, route, at) };
25
+ }
26
+ function letter(id, scope = 's', stage = 'judge', at = 1000) {
27
+ const failed = stage === 'sink' ? decision(id, scope, 'interrupt', at) : null;
28
+ return {
29
+ kind: 'deadLetter', id, scope, route: failed?.route ?? null, at,
30
+ body: { id, scope, stage, window: window(id), decision: failed, message: 'could not deliver', permanent: false, attempts: 3, at },
31
+ };
32
+ }
33
+ function record(scope, version) {
34
+ return {
35
+ scope, version, cursors: {}, seen: [], buffer: { entries: [], dropped: 0, gap: false, forcedAt: null },
36
+ batch: null, outbox: { decisions: [], deadLetters: [] }, recent: [], dueAt: null,
37
+ };
38
+ }
39
+ function write(scope, expectedVersion = 0, options = {}) {
40
+ const result = {
41
+ scope, expectedVersion, inputs: [logged(`${scope}:${expectedVersion}`)], appends: [], snapshot: null, ...options,
42
+ };
43
+ result.serialized = {
44
+ inputs: result.inputs.map((input) => JSON.stringify(input)),
45
+ snapshot: result.snapshot === null ? null : JSON.stringify(result.snapshot.record),
46
+ appends: result.appends.map((row) => JSON.stringify(row.body)),
47
+ };
48
+ return result;
49
+ }
50
+ async function rejects(run) {
51
+ try {
52
+ await run();
53
+ }
54
+ catch {
55
+ return;
56
+ }
57
+ throw new Error('expected rejection');
58
+ }
59
+ /**
60
+ * The store under test, with every value its reads return, and every write it was given, kept beside a
61
+ * structuredClone. After a case closes the store and yields one macrotask, none of them may have changed,
62
+ * so each read shape any case uses also checks aliasing. A store must return plain, cloneable data.
63
+ */
64
+ function watched(inner) {
65
+ const kept = [];
66
+ const keep = async (pending) => {
67
+ const value = await pending;
68
+ kept.push([value, structuredClone(value)]);
69
+ return value;
70
+ };
71
+ const log = inner.log;
72
+ const store = {
73
+ durable: inner.durable,
74
+ load: (scope) => keep(inner.load(scope)),
75
+ commit: (writes) => {
76
+ kept.push([writes, structuredClone(writes)]);
77
+ return keep(inner.commit(writes));
78
+ },
79
+ due: (now, limit, after) => keep(inner.due(now, limit, after)),
80
+ remove: (scope) => inner.remove(scope),
81
+ log: {
82
+ epoch: () => log.epoch(),
83
+ bounds: () => keep(log.bounds()),
84
+ read: (query) => keep(log.read(query)),
85
+ find: (kind, id) => keep(log.find(kind, id)),
86
+ wait: (after, blockMs, signal) => log.wait(after, blockMs, signal),
87
+ trim: (retention) => log.trim(retention),
88
+ compare: (a, b) => log.compare(a, b),
89
+ },
90
+ ...(inner.close ? { close: () => inner.close?.() ?? Promise.resolve() } : {}),
91
+ };
92
+ return { store, check: () => kept.forEach(([value, clone], i) => assertEqual(value, clone, `returned value ${i} unchanged by later calls`)) };
93
+ }
94
+ function casesFor(make, cases) {
95
+ return cases.map(([name, run]) => ({
96
+ name,
97
+ async run() {
98
+ const { store, check } = watched(await make());
99
+ let closed = false;
100
+ try {
101
+ await run(store);
102
+ closed = true;
103
+ await store.close?.();
104
+ await new Promise((resolve) => setTimeout(resolve, 0));
105
+ check();
106
+ }
107
+ finally {
108
+ if (!closed)
109
+ await store.close?.();
110
+ }
111
+ },
112
+ }));
113
+ }
114
+ /** Every guarantee in ScopeStore's doc comments, as cases. `make` returns a fresh, empty store per case. */
115
+ export function scopeStoreConformance(make) {
116
+ return casesFor(make, [
117
+ ['load of a missing scope is null', async (store) => {
118
+ assertEqual(await store.load('missing'), null, 'load');
119
+ }],
120
+ ['a commit at version 0 creates the scope and load returns its inputs in order', async (store) => {
121
+ const inputs = [logged('a'), logged('b'), logged('c'), { input: { kind: 'flush' }, now: 1001 }];
122
+ assertEqual(await store.commit([write('s', 0, { inputs })]), ['ok'], 'created');
123
+ assertEqual(await store.load('s'), { version: 4, snapshot: null, tail: inputs }, 'ordered inputs');
124
+ }],
125
+ ['the stored version counts committed inputs', async (store) => {
126
+ const first = [logged('a'), logged('b'), logged('c')];
127
+ const second = [logged('d'), logged('e')];
128
+ assertEqual(await store.commit([write('s', 0, { inputs: first })]), ['ok'], 'first commit');
129
+ assertEqual(await store.commit([write('s', 3, { inputs: second })]), ['ok'], 'second commit');
130
+ assertEqual(await store.load('s'), { version: 5, snapshot: null, tail: [...first, ...second] }, 'version');
131
+ }],
132
+ ['a write at a stale version conflicts and changes nothing', async (store) => {
133
+ await store.commit([write('s')]);
134
+ await store.commit([write('s', 1)]);
135
+ const before = await store.load('s');
136
+ assertEqual(await store.commit([write('s', 1, { appends: [append('stale')], due: 1000 })]), ['conflict'], 'stale');
137
+ assertEqual(await store.load('s'), before, 'unchanged');
138
+ assertEqual(await store.log.find('decision', 'stale'), null, 'no stale append');
139
+ assertEqual(await store.due(1000, 10), [], 'no stale due');
140
+ }],
141
+ ['a write at version 0 on an existing scope conflicts', async (store) => {
142
+ await store.commit([write('s')]);
143
+ const before = await store.load('s');
144
+ assertEqual(await store.commit([write('s')]), ['conflict'], 'create twice');
145
+ assertEqual(await store.load('s'), before, 'unchanged');
146
+ }],
147
+ ['N concurrent commits at one version give exactly one ok', async (store) => {
148
+ const first = logged('first');
149
+ await store.commit([write('s', 0, { inputs: [first] })]);
150
+ const inputs = Array.from({ length: 20 }, (_, i) => logged(`contender:${i}`));
151
+ const outcomes = await Promise.all(inputs.map((input) => store.commit([write('s', 1, { inputs: [input] })])));
152
+ assertEqual(outcomes.filter(([result]) => result === 'ok').length, 1, 'exactly one winner');
153
+ const winner = inputs[outcomes.findIndex(([result]) => result === 'ok')];
154
+ assertOk(winner, 'winner exists');
155
+ assertEqual(await store.load('s'), { version: 2, snapshot: null, tail: [first, winner] }, 'winner persisted');
156
+ }],
157
+ ['a conflicting write in a group leaves the other writes committed, with outcomes in order', async (store) => {
158
+ await store.commit([write('a'), write('b')]);
159
+ const before = await store.load('a');
160
+ const b = write('b', 1);
161
+ const c = write('c');
162
+ assertEqual(await store.commit([b, write('a', 0, { appends: [append('conflict', 'a')] }), c]), ['ok', 'conflict', 'ok'], 'outcomes');
163
+ assertEqual((await store.load('b'))?.version, 2, 'b committed');
164
+ assertEqual(await store.load('a'), before, 'a unchanged');
165
+ assertEqual(await store.load('c'), { version: 1, snapshot: null, tail: c.inputs }, 'c committed');
166
+ assertEqual(await store.log.find('decision', 'conflict'), null, 'no conflicting append');
167
+ }],
168
+ ['a group naming one scope twice rejects and applies nothing', async (store) => {
169
+ await rejects(() => store.commit([
170
+ write('a', 0, { appends: [append('a', 'a')], due: 1000 }),
171
+ write('b', 0, { appends: [append('b', 'b')], due: 2000 }), write('b'),
172
+ ]));
173
+ assertEqual(await store.load('a'), null, 'a absent');
174
+ assertEqual(await store.load('b'), null, 'b absent');
175
+ assertEqual(await store.log.read({ after: null, limit: 10 }), { rows: [], checked: null, more: false }, 'no rows');
176
+ assertEqual(await store.due(3000, 10), [], 'no due values');
177
+ }],
178
+ ['a snapshot replaces the stored one and trims the tail', async (store) => {
179
+ await store.commit([write('s', 0, { inputs: [logged('a'), logged('b'), logged('c')] })]);
180
+ const snapshot = { version: 5, record: { ...record('s', 5), seen: ['d', 'e'] }, policy, reducer: 2 };
181
+ await store.commit([write('s', 3, { inputs: [logged('d'), logged('e')], snapshot })]);
182
+ assertEqual(await store.load('s'), { version: 5, snapshot, tail: [] }, 'snapshot replaced');
183
+ const input = logged('f');
184
+ await store.commit([write('s', 5, { inputs: [input] })]);
185
+ assertEqual(await store.load('s'), { version: 6, snapshot, tail: [input] }, 'tail after snapshot');
186
+ const replacement = { version: 7, record: record('s', 7), policy, reducer: 3 };
187
+ await store.commit([write('s', 6, { snapshot: replacement })]);
188
+ assertEqual(await store.load('s'), { version: 7, snapshot: replacement, tail: [] }, 'older snapshot replaced');
189
+ }],
190
+ ['a snapshot and its tail round-trip with unicode, a NUL, a 250-entry window and a 1,000-id ring', async (store) => {
191
+ const entries = Array.from({ length: 250 }, (_, i) => ({ id: `e${i}`, at: i, seenAt: i + 1, name: 'n', text: 'héllo 🌍\u0000', data: { i, nested: [null, { yes: true }] }, kind: 'passive' }));
192
+ const d = decision('delivered', 's');
193
+ const l = letter('failed', 's');
194
+ if (l.kind !== 'deadLetter')
195
+ throw new Error('expected dead letter');
196
+ const snapshot = {
197
+ version: 1, policy, reducer: 1,
198
+ record: {
199
+ ...record('s', 1), cursors: { 'log:a': '🌍\u0000', 'mcp:b': null }, seen: Array.from({ length: 1000 }, (_, i) => `seen:${i}`),
200
+ buffer: { entries, dropped: 4, gap: true, forcedAt: 12, endedAt: 13 },
201
+ batch: { window: window('held'), phase: { phase: 'judging', attempt: 2, failures: 1, leaseUntil: 5000, offerWait: true } },
202
+ outbox: {
203
+ decisions: [{ item: { id: d.id, kind: 'decision', decision: d }, phase: { phase: 'inFlight', attempt: 1, failures: 0, leaseUntil: 4000 } }],
204
+ deadLetters: [{ item: { id: l.id, kind: 'deadLetter', letter: l.body }, phase: { phase: 'retrying', attempt: 2, failures: 1, at: 3000 } }],
205
+ },
206
+ recent: [{ id: 'prior', route: 'note', at: 1 }], dueAt: 3000,
207
+ },
208
+ };
209
+ await store.commit([write('s', 0, { snapshot })]);
210
+ const tail = [logged('tail:🌍'), logged('tail:\u0000')];
211
+ await store.commit([write('s', 1, { inputs: tail })]);
212
+ assertEqual(await store.load('s'), { version: 3, snapshot, tail }, 'large round trip');
213
+ }],
214
+ ['due returns scopes due by now, ascending by due then scope, paged after a key', async (store) => {
215
+ await store.commit([
216
+ write('a', 0, { due: 3000 }), write('b', 0, { due: 1000 }), write('c', 0, { due: 2000 }),
217
+ write('d', 0, { due: 9000 }), write('e', 0, { due: 1000 }), write('f', 0, { due: null }),
218
+ ]);
219
+ const expected = [{ due: 1000, scope: 'b' }, { due: 1000, scope: 'e' }, { due: 2000, scope: 'c' }, { due: 3000, scope: 'a' }];
220
+ assertEqual(await store.due(3000, 10), expected, 'due order');
221
+ assertEqual(await store.due(3000, 10), expected, 'read does not claim');
222
+ assertEqual(await store.due(3000, 2), expected.slice(0, 2), 'limit');
223
+ assertEqual(await store.due(3000, 10, { due: 1000, scope: 'b' }), expected.slice(1), 'inside tie');
224
+ assertEqual(await store.due(3000, 10, { due: 1000, scope: 'e' }), expected.slice(2), 'after tie');
225
+ assertEqual(await store.due(999, 10), [], 'none yet');
226
+ }],
227
+ ['an omitted due keeps the stored value and a null due clears it', async (store) => {
228
+ await store.commit([write('s', 0, { due: 2000 })]);
229
+ await store.commit([write('s', 1)]);
230
+ assertEqual(await store.due(2000, 10), [{ due: 2000, scope: 's' }], 'omitted keeps');
231
+ await store.commit([write('s', 2, { due: null })]);
232
+ assertEqual(await store.due(2000, 10), [], 'null clears');
233
+ }],
234
+ ['remove forgets the scope, its due value and its version', async (store) => {
235
+ await store.commit([write('s', 0, { due: 1000 })]);
236
+ await store.remove('s');
237
+ assertEqual(await store.load('s'), null, 'removed');
238
+ assertEqual(await store.due(2000, 10), [], 'not due');
239
+ assertEqual(await store.commit([write('s')]), ['ok'], 'recreated');
240
+ }],
241
+ ['scopes that are prefixes of each other or hold punctuation stay separate', async (store) => {
242
+ const scopes = ['a', 'a/b', 'a:b', 'a%b', 'a_b', 'ünï', 'a/b/c'];
243
+ for (const [i, scope] of scopes.entries())
244
+ await store.commit([write(scope, 0, { due: (i + 1) * 1000 })]);
245
+ for (const scope of scopes)
246
+ assertEqual(await store.load(scope), { version: 1, snapshot: null, tail: [logged(`${scope}:0`)] }, scope);
247
+ assertEqual((await store.due(10_000, 10)).map((key) => key.scope), scopes, 'separate due values');
248
+ }],
249
+ ['what a read returned is unchanged by later commits, trims and removes', async (store) => {
250
+ const kept = [];
251
+ const keep = async (pending) => {
252
+ const value = await pending;
253
+ kept.push([value, structuredClone(value)]);
254
+ return value;
255
+ };
256
+ const first = { version: 2, record: record('s', 2), policy, reducer: 1 };
257
+ await store.commit([write('s', 0, { inputs: [logged('a'), logged('b')], snapshot: first, appends: [append('one')], due: 1000 }), write('t', 0), write('u', 0, { due: 1000 })]);
258
+ await keep(store.load('s'));
259
+ await keep(store.load('t'));
260
+ await store.commit([write('s', 2, { appends: [letter('two')] })]);
261
+ await keep(store.load('s'));
262
+ const bounds = await keep(store.log.bounds());
263
+ for (const query of [{}, { scope: 's' }, { scopePrefix: 's' }, { kinds: ['deadLetter'] }])
264
+ await keep(store.log.read({ after: null, limit: 10, ...query }));
265
+ await keep(store.log.read({ after: bounds.first, limit: 10 }));
266
+ await keep(store.log.read({ after: bounds.last, limit: 10 }));
267
+ await keep(store.log.read({ after: null, limit: 10, routes: ['interrupt'] }));
268
+ await keep(store.log.read({ after: null, limit: 10, sinceAt: 0 }));
269
+ await keep(store.log.read({ after: bounds.first, limit: 10, scope: 's' }));
270
+ const page = await keep(store.log.read({ after: null, limit: 1 }));
271
+ const [firstDue] = await keep(store.due(1000, 1));
272
+ const [secondDue] = await keep(store.due(1000, 1, firstDue));
273
+ await keep(store.due(1000, 1, secondDue));
274
+ await keep(store.log.find('decision', 'one'));
275
+ await keep(store.due(1000, 10));
276
+ assertEqual(await store.commit([write('s', 3, { inputs: [logged('c')], appends: [append('one', 's', 'queue', 2000), letter('two', 's', 'sink', 2000)], due: 1500 }), write('t', 1)]), ['ok', 'ok'], 'duplicate appends commit');
277
+ assertEqual(await store.commit([write('s', 4, { inputs: [logged('d')], appends: [append('three')], due: 2000 })]), ['ok'], 'later commit');
278
+ await keep(store.log.find('decision', 'three'));
279
+ await keep(store.log.read({ after: page.checked, limit: 1 }));
280
+ await keep(store.log.find('deadLetter', 'two'));
281
+ await keep(store.log.bounds());
282
+ await keep(store.due(2000, 10));
283
+ await store.log.trim({ maxRows: 1 });
284
+ assertEqual(await store.commit([write('s', 5, { snapshot: { version: 6, record: record('s', 6), policy, reducer: 1 }, appends: [append('four'), letter('five'), append('one', 's', 'note', 3000)], due: null })]), ['ok'], 'snapshot commit');
285
+ await keep(store.log.read({ after: null, limit: 10 }));
286
+ await keep(store.log.find('decision', 'one'));
287
+ await keep(store.log.bounds());
288
+ await keep(store.due(3000, 10));
289
+ await keep(store.load('s'));
290
+ await store.log.trim({ olderThanMs: 1 });
291
+ await store.remove('s');
292
+ assertEqual(await store.commit([write('s', 0, { snapshot: { version: 1, record: record('s', 1), policy, reducer: 2 }, appends: [append('six')] })]), ['ok'], 're-created');
293
+ await store.remove('t');
294
+ for (const [value, clone] of kept)
295
+ assertEqual(value, clone, 'returned values unchanged');
296
+ }],
297
+ ]);
298
+ }
299
+ async function seed(store, rows) {
300
+ for (const row of rows) {
301
+ const version = (await store.load(row.scope))?.version ?? 0;
302
+ assertEqual(await store.commit([write(row.scope, version, { appends: [row] })]), ['ok'], 'seed');
303
+ }
304
+ }
305
+ function ids(rows) {
306
+ return rows.map((row) => row.id);
307
+ }
308
+ function position(rows, index) {
309
+ const row = rows[index];
310
+ if (!row)
311
+ throw new Error(`missing row ${index}`);
312
+ return row.position;
313
+ }
314
+ async function within(pending, ms) {
315
+ let timer;
316
+ try {
317
+ await Promise.race([pending, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`wait exceeded ${ms}ms`)), ms); })]);
318
+ }
319
+ finally {
320
+ clearTimeout(timer);
321
+ }
322
+ }
323
+ /**
324
+ * Every guarantee in DecisionLog's doc comments. With `concurrent`, which returns two stores over
325
+ * one database, it also checks that a reader never skips a row while two writers commit at once,
326
+ * and that durable stores share an epoch.
327
+ */
328
+ export function decisionLogConformance(make, options) {
329
+ const cases = [
330
+ ['an empty log has no bounds and reads nothing', async (store) => {
331
+ assertEqual(await store.log.bounds(), { first: null, last: null }, 'empty bounds');
332
+ assertEqual(await store.log.read({ after: null, limit: 10 }), { rows: [], checked: null, more: false }, 'empty page');
333
+ }],
334
+ ['rows appear only when their write commits', async (store) => {
335
+ await store.commit([write('s', 0, { appends: [append('committed')] })]);
336
+ assertEqual(ids((await store.log.read({ after: null, limit: 10 })).rows), ['committed'], 'committed row');
337
+ assertEqual(await store.commit([write('s', 0, { appends: [append('conflict')] })]), ['conflict'], 'conflict');
338
+ await rejects(() => store.commit([write('a', 0, { appends: [append('rejected', 'a')] }), write('a')]));
339
+ assertEqual(await store.log.find('decision', 'conflict'), null, 'no conflict row');
340
+ assertEqual(await store.log.find('decision', 'rejected'), null, 'no rejected row');
341
+ assertEqual(ids((await store.log.read({ after: null, limit: 10 })).rows), ['committed'], 'only committed');
342
+ }],
343
+ ['rows read back in commit order with strictly increasing positions', async (store) => {
344
+ const appends = [append('one', 'a'), append('two', 'b'), letter('three', 'b'), append('four', 'a')];
345
+ await store.commit([write('a', 0, { appends: appends.slice(0, 1) })]);
346
+ await store.commit([write('b', 0, { appends: appends.slice(1, 3) })]);
347
+ await store.commit([write('a', 1, { appends: appends.slice(3) })]);
348
+ const rows = (await store.log.read({ after: null, limit: 10 })).rows;
349
+ assertEqual(rows.map(({ position: _, ...row }) => row), appends, 'append and commit order');
350
+ for (let i = 1; i < rows.length; i += 1)
351
+ assertOk(store.log.compare(position(rows, i - 1), position(rows, i)) < 0, 'strictly increasing');
352
+ assertEqual(await store.log.bounds(), { first: position(rows, 0), last: position(rows, 3) }, 'bounds');
353
+ }],
354
+ ['read returns rows strictly after a position and pages by limit', async (store) => {
355
+ await seed(store, Array.from({ length: 5 }, (_, i) => append(`${i + 1}`)));
356
+ const all = (await store.log.read({ after: null, limit: 10 })).rows;
357
+ const page = await store.log.read({ after: position(all, 0), limit: 2 });
358
+ assertEqual(page, { rows: all.slice(1, 3), checked: position(all, 2), more: true }, 'middle page');
359
+ assertEqual(await store.log.read({ after: page.checked, limit: 2 }), { rows: all.slice(3), checked: position(all, 4), more: false }, 'last page');
360
+ assertEqual(await store.log.read({ after: position(all, 4), limit: 2 }), { rows: [], checked: position(all, 4), more: false }, 'head never goes backwards');
361
+ }],
362
+ ['checked reaches the head when a read is exhausted and the last row examined when it stops early', async (store) => {
363
+ await seed(store, [...Array.from({ length: 6 }, (_, i) => letter(`letter:${i}`)), append('decision')]);
364
+ const all = (await store.log.read({ after: null, limit: 10 })).rows;
365
+ assertEqual(await store.log.read({ after: null, routes: ['queue'], limit: 10 }), { rows: [], checked: position(all, 6), more: false }, 'exhausted without matches');
366
+ const first = await store.log.read({ after: null, kinds: ['decision'], limit: 1, scanLimit: 4 });
367
+ assertEqual(first, { rows: [], checked: position(all, 3), more: true }, 'scan limit');
368
+ assertEqual(await store.log.read({ after: first.checked, kinds: ['decision'], limit: 2, scanLimit: 4 }), { rows: all.slice(6), checked: position(all, 6), more: false }, 'continued scan');
369
+ }],
370
+ ['a scoped read examines only that scope', async (store) => {
371
+ await seed(store, [...Array.from({ length: 50 }, (_, i) => append(`before:${i}`, 'b')), append('a', 'a'), ...Array.from({ length: 50 }, (_, i) => append(`after:${i}`, 'b'))]);
372
+ const row = await store.log.find('decision', 'a');
373
+ const head = (await store.log.bounds()).last;
374
+ assertEqual(await store.log.read({ after: null, scope: 'a', limit: 2, scanLimit: 4 }), { rows: [row], checked: head, more: false }, 'scope index');
375
+ }],
376
+ ['a prefix read counts every row it examines toward scanLimit, like kinds and routes', async (store) => {
377
+ await seed(store, [...Array.from({ length: 6 }, (_, i) => append(`other:${i}`, 'other')), append('mine', 'p/1')]);
378
+ const all = (await store.log.read({ after: null, limit: 10 })).rows;
379
+ const first = await store.log.read({ after: null, scopePrefix: 'p/', limit: 1, scanLimit: 4 });
380
+ assertEqual(first, { rows: [], checked: position(all, 3), more: true }, 'stops at the scan limit');
381
+ assertEqual(await store.log.read({ after: first.checked, scopePrefix: 'p/', limit: 2, scanLimit: 4 }), { rows: all.slice(6), checked: position(all, 6), more: false }, 'continued scan');
382
+ }],
383
+ ['a prefix read matches only scopes that start with the prefix', async (store) => {
384
+ const scopes = ['p/x', 'p/y', 'px', 'p%', 'p_', 'q'];
385
+ await seed(store, scopes.map((scope) => append(scope, scope)));
386
+ for (const [prefix, expected] of [['p/', ['p/x', 'p/y']], ['p%', ['p%']], ['p_', ['p_']], ['', scopes]]) {
387
+ assertEqual(ids((await store.log.read({ after: null, scopePrefix: prefix, limit: 10 })).rows), expected, `prefix ${prefix}`);
388
+ }
389
+ }],
390
+ ['kinds, routes and sinceAt filter rows', async (store) => {
391
+ await seed(store, [append('interrupt', 's', 'interrupt', 1000), append('queue', 's', 'queue', 2000), append('note', 's', 'note', 3000), letter('judge', 's', 'judge', 4000), letter('sink', 's', 'sink', 5000)]);
392
+ const query = async (filter) => ids((await store.log.read({ after: null, limit: 10, ...filter })).rows);
393
+ assertEqual(await query({ kinds: ['decision'] }), ['interrupt', 'queue', 'note'], 'decisions');
394
+ assertEqual(await query({ kinds: ['deadLetter'] }), ['judge', 'sink'], 'letters');
395
+ assertEqual(await query({ routes: ['interrupt'] }), ['interrupt', 'sink'], 'null route excluded');
396
+ assertEqual(await query({ routes: ['queue', 'note'] }), ['queue', 'note'], 'route list');
397
+ assertEqual(await query({ sinceAt: 3000 }), ['note', 'judge', 'sink'], 'inclusive since');
398
+ assertEqual(await query({ kinds: ['deadLetter'], routes: ['interrupt'], sinceAt: 5000 }), ['sink'], 'combined');
399
+ }],
400
+ ['find returns a row by kind and id', async (store) => {
401
+ const rows = [append('same'), letter('same', 's', 'sink')];
402
+ await seed(store, rows);
403
+ const all = (await store.log.read({ after: null, limit: 10 })).rows;
404
+ assertEqual(await store.log.find('decision', 'same'), all[0], 'decision');
405
+ assertEqual(await store.log.find('deadLetter', 'same'), all[1], 'dead letter');
406
+ assertEqual(await store.log.find('decision', 'unknown'), null, 'missing');
407
+ }],
408
+ ['an append whose kind and id are already logged is skipped and its write still commits', async (store) => {
409
+ await seed(store, [append('same')]);
410
+ const before = await store.log.read({ after: null, limit: 10 });
411
+ assertEqual(await store.commit([write('s', 1, { appends: [append('same', 's', 'queue', 2000)] })]), ['ok'], 'duplicate commits');
412
+ assertEqual((await store.load('s'))?.version, 2, 'version advanced');
413
+ assertEqual(await store.log.read({ after: null, limit: 10 }), before, 'first row retained');
414
+ }],
415
+ ['wait resolves on an append, at the timeout and on abort', async (store) => {
416
+ await seed(store, [append('first')]);
417
+ const first = (await store.log.bounds()).last;
418
+ await store.log.read({ after: null, limit: 10 });
419
+ await store.log.find('decision', 'first');
420
+ const abort = new AbortController();
421
+ try {
422
+ const pending = store.log.wait(first, 5000, abort.signal);
423
+ const start = Date.now();
424
+ await seed(store, [append('second')]);
425
+ await within(pending, 3900);
426
+ assertOk(Date.now() - start < 4000, 'append wakes waiter');
427
+ const head = (await store.log.bounds()).last;
428
+ const timeoutAt = Date.now();
429
+ await within(store.log.wait(head, 30, abort.signal), 1900);
430
+ assertOk(Date.now() - timeoutAt >= 20, 'wait did not return immediately');
431
+ assertOk(Date.now() - timeoutAt < 2000, 'timeout resolves');
432
+ const cancelled = new AbortController();
433
+ const waiting = store.log.wait(head, 60_000, cancelled.signal);
434
+ cancelled.abort();
435
+ await within(waiting, 900);
436
+ await within(store.log.wait(first, 60_000, abort.signal), 900);
437
+ }
438
+ finally {
439
+ abort.abort();
440
+ }
441
+ }],
442
+ ['trim keeps the newest rows by count or age and never drops the head', async (store) => {
443
+ const old = Date.now() - 10 * 86_400_000;
444
+ await seed(store, Array.from({ length: 5 }, (_, i) => append(`old:${i}`, 's', 'interrupt', old + i)));
445
+ const all = (await store.log.read({ after: null, limit: 10 })).rows;
446
+ await store.log.trim({ maxRows: 2 });
447
+ assertEqual((await store.log.read({ after: null, limit: 10 })).rows, all.slice(3), 'newest two');
448
+ assertEqual(await store.log.bounds(), { first: position(all, 3), last: position(all, 4) }, 'trimmed bounds');
449
+ await store.log.trim({ maxRows: 0 });
450
+ assertEqual((await store.log.read({ after: null, limit: 10 })).rows, all.slice(4), 'head survives zero');
451
+ await seed(store, [append('old:5', 's', 'interrupt', old + 5), append('now', 's', 'interrupt', Date.now())]);
452
+ await store.log.trim({ olderThanMs: 86_400_000 });
453
+ assertEqual(ids((await store.log.read({ after: null, limit: 10 })).rows), ['now'], 'age');
454
+ await store.log.trim({ olderThanMs: 0, maxRows: 0 });
455
+ assertEqual(ids((await store.log.read({ after: null, limit: 10 })).rows), ['now'], 'head always survives');
456
+ }],
457
+ ['the epoch is stable on one instance and new for each non-durable instance', async (store) => {
458
+ const epoch = await store.log.epoch();
459
+ assertOk(typeof epoch === 'string' && epoch.length > 0, 'nonempty epoch');
460
+ assertEqual(await store.log.epoch(), epoch, 'stable');
461
+ if (!store.durable) {
462
+ const other = await make();
463
+ try {
464
+ assertOk(await other.log.epoch() !== epoch, 'distinct incarnations');
465
+ }
466
+ finally {
467
+ await other.close?.();
468
+ }
469
+ }
470
+ }],
471
+ ];
472
+ const result = casesFor(make, cases);
473
+ const concurrent = options?.concurrent;
474
+ if (concurrent) {
475
+ const together = (name, run) => ({
476
+ name,
477
+ async run() {
478
+ const [rawA, rawB] = await concurrent();
479
+ const [a, b] = [watched(rawA), watched(rawB)];
480
+ let closed = false;
481
+ try {
482
+ await run(a.store, b.store);
483
+ closed = true;
484
+ await Promise.all([a.store.close?.(), b.store.close?.()]);
485
+ await new Promise((resolve) => setTimeout(resolve, 0));
486
+ a.check();
487
+ b.check();
488
+ }
489
+ finally {
490
+ if (!closed)
491
+ await Promise.all([a.store.close?.(), b.store.close?.()]);
492
+ }
493
+ },
494
+ });
495
+ result.push(together('two stores on one database share a durable epoch', async (a, b) => {
496
+ assertOk(a.durable && b.durable, 'both durable');
497
+ const epoch = await a.log.epoch();
498
+ assertOk(epoch.length > 0, 'nonempty epoch');
499
+ assertEqual(await b.log.epoch(), epoch, 'shared epoch');
500
+ }));
501
+ result.push(together('a reader never skips a row while two writers commit concurrently', async (a, b) => {
502
+ const pending = [a, b].flatMap((store, writer) => Array.from({ length: 100 }, (_, i) => {
503
+ const scope = `writer:${writer}:${i}`;
504
+ return store.commit([write(scope, 0, { appends: [append(scope, scope)] })]);
505
+ }));
506
+ const commits = Promise.allSettled(pending);
507
+ const seen = new Set();
508
+ let after = null;
509
+ let previous = null;
510
+ const end = Date.now() + 15_000;
511
+ try {
512
+ while (seen.size < 200 && Date.now() < end) {
513
+ const page = await a.log.read({ after, limit: 7 });
514
+ for (const row of page.rows) {
515
+ assertOk(!seen.has(row.id), 'no duplicate rows');
516
+ if (previous !== null)
517
+ assertOk(a.log.compare(previous, row.position) < 0, 'positions strictly increase');
518
+ previous = row.position;
519
+ seen.add(row.id);
520
+ }
521
+ if (after !== null && page.checked !== null)
522
+ assertOk(a.log.compare(after, page.checked) <= 0, 'cursor never retreats');
523
+ after = page.checked;
524
+ if (!page.more)
525
+ await new Promise((resolve) => setTimeout(resolve, 1));
526
+ }
527
+ const expected = [0, 1].flatMap((writer) => Array.from({ length: 100 }, (_, i) => `writer:${writer}:${i}`));
528
+ assertEqual([...seen].sort(), expected.sort(), 'every committed id exactly once');
529
+ }
530
+ finally {
531
+ const outcomes = await commits;
532
+ for (const outcome of outcomes) {
533
+ if (outcome.status === 'rejected')
534
+ throw outcome.reason;
535
+ assertEqual(outcome.value, ['ok'], 'concurrent write committed');
536
+ }
537
+ }
538
+ }));
539
+ }
540
+ return result;
541
+ }
542
+ //# sourceMappingURL=conformance.js.map