agentfootprint 9.40.0 → 9.41.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 (87) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +2 -1
  3. package/ai-instructions/claude-code/SKILL.md +1 -1
  4. package/dist/adapters/memory/agentcore.js +3 -1
  5. package/dist/adapters/memory/agentcore.js.map +1 -1
  6. package/dist/artifacts/conformance/cases.js +840 -0
  7. package/dist/artifacts/conformance/cases.js.map +1 -0
  8. package/dist/artifacts/conformance/index.js +18 -0
  9. package/dist/artifacts/conformance/index.js.map +1 -0
  10. package/dist/artifacts/conformance/run.js +223 -0
  11. package/dist/artifacts/conformance/run.js.map +1 -0
  12. package/dist/artifacts/conformance/types.js +11 -0
  13. package/dist/artifacts/conformance/types.js.map +1 -0
  14. package/dist/artifacts/index.js +10 -1
  15. package/dist/artifacts/index.js.map +1 -1
  16. package/dist/core/Agent.js +86 -0
  17. package/dist/core/Agent.js.map +1 -1
  18. package/dist/core/agent/runManifest.js +102 -0
  19. package/dist/core/agent/runManifest.js.map +1 -0
  20. package/dist/esm/adapters/memory/agentcore.js +3 -1
  21. package/dist/esm/adapters/memory/agentcore.js.map +1 -1
  22. package/dist/esm/artifacts/conformance/cases.d.ts +23 -0
  23. package/dist/esm/artifacts/conformance/cases.js +837 -0
  24. package/dist/esm/artifacts/conformance/cases.js.map +1 -0
  25. package/dist/esm/artifacts/conformance/index.d.ts +11 -0
  26. package/dist/esm/artifacts/conformance/index.js +11 -0
  27. package/dist/esm/artifacts/conformance/index.js.map +1 -0
  28. package/dist/esm/artifacts/conformance/run.d.ts +47 -0
  29. package/dist/esm/artifacts/conformance/run.js +217 -0
  30. package/dist/esm/artifacts/conformance/run.js.map +1 -0
  31. package/dist/esm/artifacts/conformance/types.d.ts +197 -0
  32. package/dist/esm/artifacts/conformance/types.js +10 -0
  33. package/dist/esm/artifacts/conformance/types.js.map +1 -0
  34. package/dist/esm/artifacts/index.d.ts +1 -0
  35. package/dist/esm/artifacts/index.js +5 -0
  36. package/dist/esm/artifacts/index.js.map +1 -1
  37. package/dist/esm/core/Agent.d.ts +26 -0
  38. package/dist/esm/core/Agent.js +86 -0
  39. package/dist/esm/core/Agent.js.map +1 -1
  40. package/dist/esm/core/agent/runManifest.d.ts +80 -0
  41. package/dist/esm/core/agent/runManifest.js +98 -0
  42. package/dist/esm/core/agent/runManifest.js.map +1 -0
  43. package/dist/esm/events/payloads.d.ts +159 -0
  44. package/dist/esm/events/registry.d.ts +3 -1
  45. package/dist/esm/events/registry.js +2 -0
  46. package/dist/esm/events/registry.js.map +1 -1
  47. package/dist/esm/index.d.ts +1 -0
  48. package/dist/esm/index.js +7 -0
  49. package/dist/esm/index.js.map +1 -1
  50. package/dist/esm/memory/define.js +12 -0
  51. package/dist/esm/memory/define.js.map +1 -1
  52. package/dist/esm/memory/define.types.d.ts +27 -0
  53. package/dist/esm/memory/define.types.js.map +1 -1
  54. package/dist/esm/strategies/attach.js.map +1 -1
  55. package/dist/events/registry.js +2 -0
  56. package/dist/events/registry.js.map +1 -1
  57. package/dist/index.js +23 -12
  58. package/dist/index.js.map +1 -1
  59. package/dist/memory/define.js +12 -0
  60. package/dist/memory/define.js.map +1 -1
  61. package/dist/memory/define.types.js.map +1 -1
  62. package/dist/strategies/attach.js.map +1 -1
  63. package/dist/types/adapters/memory/agentcore.d.ts.map +1 -1
  64. package/dist/types/artifacts/conformance/cases.d.ts +24 -0
  65. package/dist/types/artifacts/conformance/cases.d.ts.map +1 -0
  66. package/dist/types/artifacts/conformance/index.d.ts +12 -0
  67. package/dist/types/artifacts/conformance/index.d.ts.map +1 -0
  68. package/dist/types/artifacts/conformance/run.d.ts +48 -0
  69. package/dist/types/artifacts/conformance/run.d.ts.map +1 -0
  70. package/dist/types/artifacts/conformance/types.d.ts +198 -0
  71. package/dist/types/artifacts/conformance/types.d.ts.map +1 -0
  72. package/dist/types/artifacts/index.d.ts +1 -0
  73. package/dist/types/artifacts/index.d.ts.map +1 -1
  74. package/dist/types/core/Agent.d.ts +26 -0
  75. package/dist/types/core/Agent.d.ts.map +1 -1
  76. package/dist/types/core/agent/runManifest.d.ts +81 -0
  77. package/dist/types/core/agent/runManifest.d.ts.map +1 -0
  78. package/dist/types/events/payloads.d.ts +159 -0
  79. package/dist/types/events/payloads.d.ts.map +1 -1
  80. package/dist/types/events/registry.d.ts +3 -1
  81. package/dist/types/events/registry.d.ts.map +1 -1
  82. package/dist/types/index.d.ts +1 -0
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/memory/define.d.ts.map +1 -1
  85. package/dist/types/memory/define.types.d.ts +27 -0
  86. package/dist/types/memory/define.types.d.ts.map +1 -1
  87. package/package.json +1 -1
@@ -0,0 +1,840 @@
1
+ "use strict";
2
+ /**
3
+ * artifacts/conformance/cases — the battery itself.
4
+ *
5
+ * Each case holds ONE law of the port and says which one in its `law` field,
6
+ * so a failure reads as a broken promise rather than as a broken assertion.
7
+ * Nothing here imports a test framework: a case throws to fail, which is the
8
+ * one convention every runner in every language already agrees on.
9
+ *
10
+ * The laws come from the port's own constitution (`artifacts/types.ts`): scope
11
+ * is always the first argument and a ref alone opens nothing; `get`/`head`
12
+ * answer `null` for missing OR expired; reads page; `head` describes without
13
+ * the payload; refs are MINTED, never content-addressed; `parentRefs` are
14
+ * proven at mint; `get` verifies a digest and `getStream` deliberately does
15
+ * not.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.artifactStoreConformance = void 0;
19
+ const naming_js_1 = require("../naming.js");
20
+ const types_js_1 = require("../types.js");
21
+ // ─── The little assertion kit ────────────────────────────────────────
22
+ /** Fail this case, naming what was expected. */
23
+ function check(condition, message) {
24
+ if (!condition)
25
+ throw new Error(message);
26
+ }
27
+ /**
28
+ * The error a call failed with, or `undefined` when it succeeded.
29
+ *
30
+ * Takes a THUNK, so a store that throws synchronously where the port promises
31
+ * a promise is still measured on its semantics rather than blowing the case up
32
+ * on its calling convention.
33
+ */
34
+ async function attempt(work) {
35
+ try {
36
+ await work();
37
+ return undefined;
38
+ }
39
+ catch (err) {
40
+ return err;
41
+ }
42
+ }
43
+ /** Both halves of a call that may answer OR throw, for the cases where the
44
+ * distinction is itself the law under test. */
45
+ async function settle(work) {
46
+ try {
47
+ return { value: await work() };
48
+ }
49
+ catch (err) {
50
+ return { error: err };
51
+ }
52
+ }
53
+ /** `name: message` for an error, for the refusal checks. */
54
+ function textOf(err) {
55
+ return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
56
+ }
57
+ /** Is this the refusal the port names for this problem? */
58
+ function isRefusal(err, kind) {
59
+ // Instance checks first, then the `code` — an adapter may carry its own
60
+ // subclass, and a store in another package that re-created the class
61
+ // (bundlers duplicate modules) is judged on the code it publishes rather
62
+ // than on identity it cannot control.
63
+ const code = err?.code;
64
+ if (kind === 'invalid')
65
+ return err instanceof types_js_1.InvalidArtifactError || code === 'ERR_INVALID_ARTIFACT';
66
+ if (kind === 'unknown-parent') {
67
+ return err instanceof types_js_1.UnknownParentRefError || code === 'ERR_UNKNOWN_PARENT_REF';
68
+ }
69
+ return err instanceof types_js_1.ArtifactIntegrityError || code === 'ERR_ARTIFACT_INTEGRITY';
70
+ }
71
+ /** Read every page of a listing, so a paging bug shows up as a wrong SET. */
72
+ async function allPages(store, scope, limit) {
73
+ const refs = [];
74
+ const pages = [];
75
+ let cursor;
76
+ // A bound, not a `while (true)`: a store whose cursor never terminates
77
+ // should fail this case, not hang the suite that was checking it.
78
+ for (let page = 0; page < 50; page++) {
79
+ const result = await store.list(scope, { limit, ...(cursor !== undefined && { cursor }) });
80
+ pages.push(result);
81
+ for (const row of result.artifacts)
82
+ refs.push(row.ref);
83
+ cursor = result.cursor;
84
+ if (cursor === undefined)
85
+ return { refs, pages };
86
+ }
87
+ throw new Error(`list() never stopped: 50 pages of ${limit} and the cursor is still present. A cursor is ` +
88
+ `promised only when MORE rows exist, so a listing that keeps handing one back is a pane ` +
89
+ `that scrolls forever.`);
90
+ }
91
+ /** Everything a stream handed over, as text. */
92
+ async function drain(body) {
93
+ const reader = body.getReader();
94
+ const chunks = [];
95
+ for (;;) {
96
+ const { done, value } = await reader.read();
97
+ if (done)
98
+ break;
99
+ if (value !== undefined)
100
+ chunks.push(value);
101
+ }
102
+ const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
103
+ const whole = new Uint8Array(total);
104
+ let at = 0;
105
+ for (const chunk of chunks) {
106
+ whole.set(chunk, at);
107
+ at += chunk.byteLength;
108
+ }
109
+ return new TextDecoder().decode(whole);
110
+ }
111
+ /** One chunked payload, as the port's stream shape. */
112
+ function streamOf(chunks) {
113
+ const encoder = new TextEncoder();
114
+ return new ReadableStream({
115
+ start(controller) {
116
+ for (const chunk of chunks)
117
+ controller.enqueue(encoder.encode(chunk));
118
+ controller.close();
119
+ },
120
+ });
121
+ }
122
+ /** The payload as a comparable value — binary compared byte for byte. */
123
+ function sameValue(left, right) {
124
+ if (left instanceof Uint8Array || right instanceof Uint8Array) {
125
+ if (!(left instanceof Uint8Array) || !(right instanceof Uint8Array))
126
+ return false;
127
+ return left.byteLength === right.byteLength && left.every((byte, at) => byte === right[at]);
128
+ }
129
+ return JSON.stringify(left) === JSON.stringify(right);
130
+ }
131
+ // ─── The battery ─────────────────────────────────────────────────────
132
+ const putHeadGet = {
133
+ name: 'put-mints-a-ticket-head-describes-get-redeems',
134
+ law: 'put mints a ticket and reports what it swept; head describes the artifact WITHOUT its payload; get redeems both.',
135
+ async run(store, kit) {
136
+ const scope = kit.scope('round-trip');
137
+ const data = { rows: [1, 2, 3] };
138
+ const { meta, swept } = await store.put(scope, {
139
+ kind: 'dataset/rows',
140
+ mediaType: 'application/json',
141
+ data,
142
+ label: 'Q3 rows',
143
+ });
144
+ check(Array.isArray(swept) && swept.length === 0, `put into an EMPTY scope reported ${JSON.stringify(swept)} swept. A sweep is a fact the ` +
145
+ `caller puts on the record, so inventing one describes an eviction that never happened.`);
146
+ check((0, naming_js_1.isArtifactRef)(meta.ref), `put minted ${JSON.stringify(meta.ref)}, which is not a ref of this port's grammar ` +
147
+ `('art_' + 22 base62). The model speaks these strings; a store that mints its own ` +
148
+ `spelling makes every consumer parse two.`);
149
+ check(meta.kind === 'dataset/rows' && meta.label === 'Q3 rows', `the ticket came back as kind=${JSON.stringify(meta.kind)} label=${JSON.stringify(meta.label)}. ` +
150
+ `kind is the consumer vocabulary a redeemer decides from — a store may not rewrite it.`);
151
+ check(meta.bytes === JSON.stringify(data).length, `meta.bytes = ${String(meta.bytes)} for a payload of ${JSON.stringify(data).length} bytes. ` +
152
+ `bytes is what a consumer decides from without paying for the payload; a wrong count is ` +
153
+ `a wrong decision every time.`);
154
+ check(typeof meta.createdAt === 'number' && Number.isFinite(meta.createdAt), `meta.createdAt = ${String(meta.createdAt)} is not a unix-ms timestamp.`);
155
+ const described = await store.head(scope, meta.ref);
156
+ check(described !== null, 'head() answered null for an artifact that was just stored.');
157
+ check(described.ref === meta.ref && described.kind === 'dataset/rows', `head() described a different artifact (${JSON.stringify(described.ref)}).`);
158
+ check(!('data' in described) && !('payload' in described), `head() carried the payload. head IS the render-by-ref decision — a consumer picks what ` +
159
+ `to do from kind and bytes WITHOUT paying for the bytes, and a head that ships them ` +
160
+ `costs exactly what the claim check was built to avoid.`);
161
+ const got = await store.get(scope, meta.ref);
162
+ check(got !== null, 'get() answered null for an artifact that was just stored.');
163
+ check(got.meta.ref === meta.ref, 'get() answered a record whose ticket is a different ref.');
164
+ check(sameValue(got.data, data), `get() answered ${JSON.stringify(got.data)} for a payload of ${JSON.stringify(data)}. ` +
165
+ `The one thing a claim check must never do is honor a ticket with somebody else's parcel.`);
166
+ },
167
+ };
168
+ const payloadShapes = {
169
+ name: 'payloads-round-trip-as-the-value-they-were-given',
170
+ law: 'JSON, text and binary payloads come back as the values they went in as — never an approximation.',
171
+ async run(store, kit) {
172
+ const scope = kit.scope('shapes');
173
+ const shapes = [
174
+ ['json', { a: [1, 2], b: 'x' }, 'application/json'],
175
+ ['text', 'plain, with an em-dash — and a ünicode name', 'text/plain'],
176
+ // 0 and 255 included on purpose: a store that round-trips bytes through
177
+ // a string encoding loses exactly these.
178
+ ['binary', new Uint8Array([0, 1, 2, 250, 255]), 'application/octet-stream'],
179
+ ];
180
+ for (const [kind, data, mediaType] of shapes) {
181
+ const { meta } = await store.put(scope, { kind, mediaType, data });
182
+ const got = await store.get(scope, meta.ref);
183
+ check(got !== null && sameValue(got.data, data), `a ${kind} payload came back as ${JSON.stringify(got?.data)}. A store may re-encode a ` +
184
+ `payload for its own storage; it may not hand back a different value.`);
185
+ check(got.meta.mediaType === mediaType, `the mediaType came back as ${JSON.stringify(got.meta.mediaType)} and ` +
186
+ `${JSON.stringify(mediaType)} went in — a consumer decodes from that field.`);
187
+ }
188
+ },
189
+ };
190
+ const refsAreMinted = {
191
+ name: 'refs-are-minted-never-derived-from-the-payload',
192
+ law: 'A ref is MINTED and opaque: identical bytes stored twice are two artifacts with two refs, and neither ref describes its payload.',
193
+ async run(store, kit) {
194
+ const scope = kit.scope('minted');
195
+ const data = { the: 'same', bytes: [1, 2, 3] };
196
+ const first = await store.put(scope, { kind: 'k', mediaType: 'application/json', data });
197
+ const second = await store.put(scope, { kind: 'k', mediaType: 'application/json', data });
198
+ check(first.meta.ref !== second.meta.ref, `storing identical bytes twice returned ONE ref (${first.meta.ref}). Content addressing ` +
199
+ `looks like a saving and is a defect here: it folds two tenants' identical payloads ` +
200
+ `into one object, and it can never name two generations of "the current dataset".`);
201
+ // Both are real, and deleting one leaves the other — the practical half of
202
+ // the same law, and the half a de-duplicating store silently breaks.
203
+ await store.delete(scope, first.meta.ref);
204
+ check((await store.get(scope, first.meta.ref)) === null, 'delete() left the first artifact readable.');
205
+ const survivor = await store.get(scope, second.meta.ref);
206
+ check(survivor !== null && sameValue(survivor.data, data), 'deleting one artifact took its identical twin with it — which is what a store that ' +
207
+ 'keyed on the payload would do, and it means one caller can delete another’s data by ' +
208
+ 'storing the same bytes.');
209
+ // Opaque, and cheap to check: the ref is not the digest wearing a prefix,
210
+ // which is the shape a content-addressed store arrives in.
211
+ const digested = await store.put(scope, {
212
+ kind: 'k',
213
+ mediaType: 'application/json',
214
+ data,
215
+ digest: 'sha-256',
216
+ });
217
+ check(!(digested.meta.digest ?? '').includes(digested.meta.ref.slice(4, 14)), `the ref ${JSON.stringify(digested.meta.ref)} is derived from the payload's digest. The ` +
218
+ `digest is METADATA, never the key: content as key collides two tenants' identical ` +
219
+ `bytes into one object and can never name two generations of "the current dataset".`);
220
+ },
221
+ };
222
+ const refAloneOpensNothing = {
223
+ name: 'a-ref-alone-opens-nothing',
224
+ law: 'A ref is not an authorization: under any other scope it heads, gets, lists and deletes as if it never existed.',
225
+ async run(store, kit) {
226
+ const owner = kit.scope('owner');
227
+ const { meta } = await store.put(owner, {
228
+ kind: 'dataset/rows',
229
+ mediaType: 'text/plain',
230
+ data: 'confidential to the owning scope',
231
+ });
232
+ // Every neighbour a scope tuple has: another conversation, another tenant,
233
+ // another principal. Each one holds the ref and must get nothing.
234
+ const neighbours = [
235
+ ['another conversation', kit.scope('neighbour')],
236
+ ['another tenant', { ...owner, tenant: `${kit.token}-t2` }],
237
+ ['another principal', { ...owner, principal: `${kit.token}-p2` }],
238
+ ];
239
+ for (const [what, neighbour] of neighbours) {
240
+ check((await store.get(neighbour, meta.ref)) === null, `get() from ${what} answered the artifact. Scope is the first argument of every verb ` +
241
+ `precisely so a ref that leaks — into a log, a transcript, a screenshot — is not a ` +
242
+ `key to somebody else's data.`);
243
+ check((await store.head(neighbour, meta.ref)) === null, `head() from ${what} described the artifact. A description is a disclosure: kind, ` +
244
+ `label and bytes are exactly what an attacker holding a ref wants.`);
245
+ check((await store.list(neighbour)).artifacts.length === 0, `list() from ${what} carried the owner's rows.`);
246
+ // And a neighbour must not be able to DELETE it either — the verb that
247
+ // leaves no trace to notice afterwards.
248
+ await store.delete(neighbour, meta.ref);
249
+ }
250
+ const still = await store.get(owner, meta.ref);
251
+ check(still !== null && still.data === 'confidential to the owning scope', 'the owning scope lost the artifact — either a neighbour deleted it, or the store ' +
252
+ 'answers the wrong scope. Both are the same defect from the other side.');
253
+ },
254
+ };
255
+ const confusableScopes = {
256
+ name: 'confusable-scopes-are-not-one-scope',
257
+ law: 'Two DIFFERENT scope tuples that a naive encoder spells identically stay two scopes.',
258
+ async run(store, kit) {
259
+ // The scope tuple IS the isolation boundary, so the way it becomes an
260
+ // address has to be injective. Each pair below is two DIFFERENT tuples a
261
+ // naive encoder folds into one: the separator donated by a value (a JWT
262
+ // `sub` is routinely a URI), the absence marker worn by a real name, and a
263
+ // value that already looks escaped.
264
+ //
265
+ // The unique token goes in the field the pair holds EQUAL — putting it in
266
+ // a field the pair varies would pull the two spellings apart and quietly
267
+ // make the case unfalsifiable.
268
+ const t = kit.token;
269
+ const pairs = [
270
+ [
271
+ 'a slash inside a tenant vs. the same slash inside a principal',
272
+ { tenant: 'acme/hr', principal: 'alice', conversationId: t },
273
+ { tenant: 'acme', principal: 'hr/alice', conversationId: t },
274
+ ],
275
+ [
276
+ 'a slash inside a principal vs. the same slash inside a conversation',
277
+ { tenant: t, principal: 'alice/c1', conversationId: 'x' },
278
+ { tenant: t, principal: 'alice', conversationId: 'c1/x' },
279
+ ],
280
+ [
281
+ 'no tenant vs. a tenant literally named _',
282
+ { conversationId: t },
283
+ { tenant: '_', principal: '_', conversationId: t },
284
+ ],
285
+ [
286
+ 'a value that already looks escaped vs. the value it would decode to',
287
+ { tenant: 'a%2Fb', conversationId: t },
288
+ { tenant: 'a/b', conversationId: t },
289
+ ],
290
+ ];
291
+ for (const [what, left, right] of pairs) {
292
+ const { meta } = await store.put(left, {
293
+ kind: 'note',
294
+ mediaType: 'text/plain',
295
+ data: 'confidential to the left scope',
296
+ });
297
+ check((await store.get(right, meta.ref)) === null &&
298
+ (await store.head(right, meta.ref)) === null &&
299
+ (await store.list(right)).artifacts.length === 0, `two scopes collided when ${what}. Whatever mapping a store uses to make a tuple legal ` +
300
+ `for its backend must be INJECTIVE — a leak closed in one store and left open in ` +
301
+ `another is still a leak, and this one is reachable by choosing a value.`);
302
+ await store.delete(right, meta.ref);
303
+ const owner = await store.get(left, meta.ref);
304
+ check(owner !== null && owner.data === 'confidential to the left scope', `the neighbour's delete removed the left scope's artifact when ${what}.`);
305
+ }
306
+ },
307
+ };
308
+ const oneAbsence = {
309
+ name: 'missing-expired-and-foreign-scope-are-one-absence',
310
+ law: 'Missing, expired and foreign-scope are ONE indistinguishable answer — `null`, never an error and never a different shape.',
311
+ harnessNeeds: ['advanceTime'],
312
+ async run(store, kit) {
313
+ const scope = kit.scope('absence');
314
+ const elsewhere = kit.scope('elsewhere');
315
+ // Three ways to have nothing, which a caller must not be able to tell
316
+ // apart: distinguishing them is an oracle for another scope's contents.
317
+ const never = 'art_' + 'a'.repeat(22);
318
+ const { meta: expiring } = await store.put(scope, {
319
+ kind: 'k',
320
+ mediaType: 'text/plain',
321
+ data: 'briefly here',
322
+ // Relative to the STORE's clock, not this process's: a store on an
323
+ // injected clock and a store on the wall clock are both entitled to
324
+ // their own idea of now, and the battery reads it the one way the port
325
+ // exposes it (the `createdAt` a store stamps).
326
+ expiresAt: (await kit.now(store)) + 60_000,
327
+ });
328
+ const { meta: foreign } = await store.put(elsewhere, {
329
+ kind: 'k',
330
+ mediaType: 'text/plain',
331
+ data: "somebody else's",
332
+ });
333
+ check((await store.get(scope, expiring.ref)) !== null, 'the artifact was gone before its stated expiry.');
334
+ await kit.advance(store, 60_001);
335
+ for (const [what, ref] of [
336
+ ['a ref that was never minted here', never],
337
+ ['an EXPIRED artifact', expiring.ref],
338
+ ["another scope's artifact", foreign.ref],
339
+ ]) {
340
+ for (const verb of ['get', 'head']) {
341
+ const { value, error } = await settle(() => verb === 'get' ? store.get(scope, ref) : store.head(scope, ref));
342
+ check(error === undefined, `${verb}() of ${what} THREW ${textOf(error)}. An absence is answered, not thrown — a ` +
343
+ `caller that must catch to find out whether data exists has an error channel that ` +
344
+ `means two different things.`);
345
+ check(value === null, `${verb}() of ${what} answered ${JSON.stringify(value)} instead of null. "No data" is ` +
346
+ `the only actionable fact, and telling the three apart lets a caller reason about a ` +
347
+ `scope it is not allowed to read.`);
348
+ }
349
+ }
350
+ // A malformed ref is the same absence, not a validation error: the model
351
+ // speaks these strings, and a hallucinated one must read as "no data".
352
+ check((await store.get(scope, 'not-a-ref')) === null &&
353
+ (await store.head(scope, 'not-a-ref')) === null, 'a ref that is not of this grammar was not answered as absent. A model can say anything; ' +
354
+ 'the honest answer to a ref that resolves to nothing is always the same one.');
355
+ },
356
+ };
357
+ const expiryIsStated = {
358
+ name: 'expiry-is-stated-at-mint-never-sprung',
359
+ law: 'Expiry is STATED on the ticket at mint — the store may only TIGHTEN a caller’s time — and an artifact born expired is refused.',
360
+ async run(store, kit) {
361
+ const scope = kit.scope('expiry');
362
+ // The store's own clock (see `missing-expired-…` for why it is read this
363
+ // way and not from this process).
364
+ const storeNow = await kit.now(store);
365
+ const stated = storeNow + 60_000;
366
+ const { meta } = await store.put(scope, {
367
+ kind: 'k',
368
+ mediaType: 'text/plain',
369
+ data: 'x',
370
+ expiresAt: stated,
371
+ });
372
+ check(meta.expiresAt !== undefined, 'a put that STATED an expiry came back with no expiresAt on the ticket. Expiry a ' +
373
+ 'consumer cannot read is expiry sprung on them — the ref simply stops resolving one ' +
374
+ 'day and nothing in the ticket ever said it would.');
375
+ check(meta.expiresAt <= stated, `the ticket says the artifact expires at ${String(meta.expiresAt)}, LATER than the ` +
376
+ `${String(stated)} the caller stated. A store's own retention may only tighten a ` +
377
+ `caller's promise; extending it keeps data somebody asked to have dropped.`);
378
+ // The same fact from head, because head is what a later consumer reads.
379
+ const described = await store.head(scope, meta.ref);
380
+ check(described?.expiresAt === meta.expiresAt, `head() reports expiresAt=${String(described?.expiresAt)} and the mint said ` +
381
+ `${String(meta.expiresAt)}. One artifact, one expiry.`);
382
+ const born = await attempt(() => store.put(scope, {
383
+ kind: 'k',
384
+ mediaType: 'text/plain',
385
+ data: 'x',
386
+ expiresAt: storeNow - 60_000,
387
+ }));
388
+ check(isRefusal(born, 'invalid'), `a put whose expiry is already in the past was answered with ${textOf(born)}. An artifact ` +
389
+ `born expired resolves for nobody; storing one hands back a ticket that was never ` +
390
+ `redeemable.`);
391
+ },
392
+ };
393
+ const deleteIsAgreement = {
394
+ name: 'delete-removes-and-deleting-an-absence-is-agreement',
395
+ law: 'delete removes the artifact, and deleting what is not there is agreement rather than an error.',
396
+ async run(store, kit) {
397
+ const scope = kit.scope('delete');
398
+ const { meta } = await store.put(scope, { kind: 'k', mediaType: 'text/plain', data: 'x' });
399
+ await store.delete(scope, meta.ref);
400
+ check((await store.get(scope, meta.ref)) === null && (await store.head(scope, meta.ref)) === null, 'delete() left the artifact readable. A deletion that does not delete is the kind of ' +
401
+ 'thing a person finds out about from a regulator.');
402
+ check((await store.list(scope)).artifacts.length === 0, 'delete() removed the payload and left the ticket in the listing — a pane then offers a ' +
403
+ 'row nobody can open.');
404
+ // A retry, a double-click and a cleanup job all do this. So does a caller
405
+ // holding a ref the model invented.
406
+ for (const [what, ref] of [
407
+ ['the same ref twice', meta.ref],
408
+ ['a ref that never existed', 'art_' + 'b'.repeat(22)],
409
+ ['a string that is not a ref at all', 'not-a-ref'],
410
+ ]) {
411
+ const err = await attempt(() => store.delete(scope, ref));
412
+ check(err === undefined, `delete() of ${what} threw ${textOf(err)}. Deleting an absence is not an error, it is ` +
413
+ `agreement — and a cleanup path that throws is a cleanup path somebody wraps in an ` +
414
+ `empty catch.`);
415
+ }
416
+ },
417
+ };
418
+ const listPages = {
419
+ name: 'list-pages-newest-first-and-carries-no-payload',
420
+ law: 'list pages this scope only, newest first, every row exactly once, with a cursor ONLY when more rows exist — and never a payload.',
421
+ harnessNeeds: ['advanceTime'],
422
+ async run(store, kit) {
423
+ const scope = kit.scope('listing');
424
+ const neighbour = kit.scope('not-listed');
425
+ const order = [];
426
+ for (let n = 0; n < 5; n++) {
427
+ const { meta } = await store.put(scope, {
428
+ kind: `k${n}`,
429
+ mediaType: 'text/plain',
430
+ data: `v${n}`,
431
+ });
432
+ order.unshift(meta.ref); // newest first, as the listing must answer
433
+ // A store whose order is a timestamp needs the timestamps to differ; one
434
+ // whose order is insertion does not care. Both are held to the same
435
+ // answer, which is the point of asking through the port.
436
+ await kit.advance(store, 10);
437
+ }
438
+ await store.put(neighbour, { kind: 'theirs', mediaType: 'text/plain', data: 'x' });
439
+ const { refs, pages } = await allPages(store, scope, 2);
440
+ check(new Set(refs).size === refs.length, `paging repeated a row (${refs.join(', ')}). A cursor over equal timestamps needs a ` +
441
+ `second, total ordering or a row lands on both sides of a page boundary.`);
442
+ check(refs.length === 5, `paging saw ${refs.length} of 5 rows. A listing that loses a row is a pane missing an ` +
443
+ `artifact nobody can explain.`);
444
+ check(refs.join(',') === order.join(','), `the listing was not newest-first. Got ${refs.join(', ')}; expected ${order.join(', ')}. ` +
445
+ `Newest-first is the order every listing in this package promises.`);
446
+ const last = pages[pages.length - 1];
447
+ check(last?.cursor === undefined, 'the final page still carried a cursor. A cursor promises MORE, so one on the last page ' +
448
+ 'is a "load more" that loads nothing.');
449
+ for (const page of pages.slice(0, -1)) {
450
+ check(page.cursor !== undefined, 'a non-final page carried no cursor, so the rows after it are unreachable.');
451
+ }
452
+ for (const row of pages[0].artifacts) {
453
+ check(!('data' in row), `a listing row carried its payload. A listing is tickets — bytes never ride one, or ` +
454
+ `drawing a pane costs what the claim check was built to avoid.`);
455
+ }
456
+ const theirs = await store.list(neighbour);
457
+ check(theirs.artifacts.length === 1, `the neighbouring scope listed ${theirs.artifacts.length} rows instead of its own one. ` +
458
+ `A listing IS the permission check for everything downstream of it.`);
459
+ },
460
+ };
461
+ const awkwardScopes = {
462
+ name: 'awkward-scope-values-are-names-not-paths',
463
+ law: 'A scope value is opaque DATA: slashes, dots, unicode and long values address one scope and never a place the store did not intend.',
464
+ async run(store, kit) {
465
+ // Every one of these is a value a real identity system hands over: a `sub`
466
+ // that is a URI, a tenant named after a path, a display name with an
467
+ // emoji. They arrive as data and must land as literals.
468
+ //
469
+ // Each entry takes the run token and decides WHERE to put it, because the
470
+ // namespacing must not land on the field whose spelling is under test —
471
+ // prefixing an "empty conversation id" would quietly make it non-empty and
472
+ // the case would prove nothing.
473
+ const awkward = [
474
+ [
475
+ 'slashes in every field',
476
+ (t) => ({ tenant: 'a/b', principal: 'c/d', conversationId: `${t}/e/f` }),
477
+ ],
478
+ [
479
+ 'parent-directory hops',
480
+ (t) => ({ tenant: '..', principal: '.', conversationId: `${t}/../..` }),
481
+ ],
482
+ ['unicode and an emoji', (t) => ({ tenant: 'ünïcødé-😀', conversationId: `${t}-😀` })],
483
+ ['a backslash and a quote', (t) => ({ tenant: 'a\\b', principal: `q'"`, conversationId: t })],
484
+ [
485
+ 'a value that reads as SQL',
486
+ (t) => ({ conversationId: `${t}'; DROP TABLE af_artifacts; --` }),
487
+ ],
488
+ ['an empty conversation id', (t) => ({ tenant: t, conversationId: '' })],
489
+ // 200 rather than 1000: a store whose scope becomes a DIRECTORY has a
490
+ // 255-byte component ceiling it cannot argue with, and a battery that
491
+ // demanded more would be testing the filesystem rather than the port.
492
+ // The law is "long values are not truncated INTO EACH OTHER", checked
493
+ // below, and 200 shows that on every column.
494
+ ['a very long tenant', (t) => ({ tenant: 'x'.repeat(200), conversationId: t })],
495
+ ];
496
+ for (const [what, build] of awkward) {
497
+ const scope = build(kit.token);
498
+ const { meta } = await store.put(scope, {
499
+ kind: 'k',
500
+ mediaType: 'text/plain',
501
+ data: `stored under ${what}`,
502
+ });
503
+ const got = await store.get(scope, meta.ref);
504
+ check(got !== null && got.data === `stored under ${what}`, `a scope with ${what} did not round-trip: get() answered ${JSON.stringify(got?.data)}. ` +
505
+ `A scope value is somebody's opaque string; a store that mangles it hands them a ` +
506
+ `different scope's data or none of their own.`);
507
+ check((await store.list(scope)).artifacts.length === 1, `a scope with ${what} stored an artifact its own listing cannot see.`);
508
+ }
509
+ // Two long values that a truncating store folds together — the failure
510
+ // mode a length ceiling introduces if it is met by cutting rather than by
511
+ // refusing.
512
+ const long = 'y'.repeat(190);
513
+ const left = { tenant: `${long}-left`, conversationId: kit.token };
514
+ const right = { tenant: `${long}-right`, conversationId: kit.token };
515
+ const { meta } = await store.put(left, { kind: 'k', mediaType: 'text/plain', data: 'left' });
516
+ check((await store.get(right, meta.ref)) === null, 'two long tenants that differ only in their last characters became ONE scope. A store ' +
517
+ 'that truncates a name to fit its backend has turned a ceiling into a leak.');
518
+ },
519
+ };
520
+ const oversizedRefused = {
521
+ name: 'oversized-payload-is-refused-before-the-write',
522
+ law: 'A payload larger than the whole scope budget is refused BEFORE the write, and nothing partial lands.',
523
+ harnessNeeds: ['boundedStore'],
524
+ async run(_store, kit) {
525
+ const bounded = await kit.bounded(64);
526
+ const scope = kit.scope('ceiling');
527
+ const { meta: kept } = await bounded.put(scope, {
528
+ kind: 'keeper',
529
+ mediaType: 'text/plain',
530
+ data: 'small',
531
+ });
532
+ const err = await attempt(() => bounded.put(scope, { kind: 'oversized', mediaType: 'text/plain', data: 'x'.repeat(500) }));
533
+ check(isRefusal(err, 'invalid'), `a payload larger than the entire scope budget was answered with ${textOf(err)}. Evicting ` +
534
+ `a whole scope to admit one object that may not fit either trades everything for ` +
535
+ `nothing — the honest answer is a refusal at the door.`);
536
+ // "Nothing partial lands" is the half that matters, and the half a store
537
+ // that checks the budget AFTER writing gets wrong.
538
+ const survivor = await bounded.get(scope, kept.ref);
539
+ check(survivor !== null && survivor.data === 'small', 'the refused put evicted what was already there. A put that cannot be honored must not ' +
540
+ 'be able to empty a scope on its way out.');
541
+ const listing = await bounded.list(scope);
542
+ check(listing.artifacts.length === 1 && listing.artifacts[0].ref === kept.ref, `after the refusal the scope holds ${listing.artifacts.length} artifacts. A refused put ` +
543
+ `leaves no ticket behind — a half-written artifact is a ref that resolves to bytes ` +
544
+ `nobody vouched for.`);
545
+ },
546
+ };
547
+ const parentRefsProven = {
548
+ name: 'parent-refs-are-proven-at-mint',
549
+ law: 'parentRefs are derivation FACTS proven at mint: a parent that does not resolve IN THE SAME SCOPE refuses the put and stores nothing.',
550
+ async run(store, kit) {
551
+ const scope = kit.scope('lineage');
552
+ const elsewhere = kit.scope('other-lineage');
553
+ const { meta: parent } = await store.put(scope, {
554
+ kind: 'dataset/rows',
555
+ mediaType: 'text/plain',
556
+ data: 'the source',
557
+ });
558
+ const { meta: child } = await store.put(scope, {
559
+ kind: 'chart/spec',
560
+ mediaType: 'application/json',
561
+ data: {},
562
+ parentRefs: [parent.ref],
563
+ });
564
+ check(child.parentRefs?.length === 1 && child.parentRefs[0] === parent.ref, `the child's parentRefs came back as ${JSON.stringify(child.parentRefs)}. Derivation ` +
565
+ `facts are the join a consumer folds over head() — a store that drops them drops the ` +
566
+ `only record of where a number came from.`);
567
+ check((await store.head(scope, child.ref))?.parentRefs?.[0] === parent.ref, 'head() described the child without the parents its own mint accepted.');
568
+ const unknown = await attempt(() => store.put(scope, {
569
+ kind: 'k',
570
+ mediaType: 'text/plain',
571
+ data: 'x',
572
+ parentRefs: ['art_' + 'c'.repeat(22)],
573
+ }));
574
+ check(isRefusal(unknown, 'unknown-parent'), `a put naming a parent that does not exist was answered with ${textOf(unknown)}. A ` +
575
+ `foreign key that dangles at birth is worse than no fact: every later consumer ` +
576
+ `inherits the lie.`);
577
+ // A parent in ANOTHER scope does not resolve HERE, and must not — a store
578
+ // that proves parents globally has made the ref a cross-scope oracle.
579
+ const foreign = await attempt(() => store.put(elsewhere, {
580
+ kind: 'k',
581
+ mediaType: 'text/plain',
582
+ data: 'x',
583
+ parentRefs: [parent.ref],
584
+ }));
585
+ check(isRefusal(foreign, 'unknown-parent'), `a put naming a parent from ANOTHER scope was answered with ${textOf(foreign)}. Proving ` +
586
+ `parents outside the scope tells the caller a ref exists somewhere, which is the one ` +
587
+ `fact scope isolation exists to withhold.`);
588
+ check((await store.list(elsewhere)).artifacts.length === 0, 'a put refused for a dangling parent stored the artifact anyway.');
589
+ },
590
+ };
591
+ const malformedRefused = {
592
+ name: 'malformed-puts-are-refused-by-name',
593
+ law: 'A put this store cannot honor AS STATED is refused by name, and stores nothing.',
594
+ async run(store, kit) {
595
+ const scope = kit.scope('malformed');
596
+ const cyclic = {};
597
+ cyclic['self'] = cyclic;
598
+ const malformed = [
599
+ ['a blank kind', { kind: '', mediaType: 'text/plain', data: 'x' }],
600
+ ['a blank mediaType', { kind: 'k', mediaType: ' ', data: 'x' }],
601
+ [
602
+ 'an unknown digest algorithm',
603
+ { kind: 'k', mediaType: 'text/plain', data: 'x', digest: 'md5' },
604
+ ],
605
+ // A payload the durable columns cannot carry. Refused everywhere on
606
+ // purpose: a store that accepts what only IT can hold breaks the port's
607
+ // one promise — swap the adapter and nothing else changes.
608
+ ['a payload JSON cannot carry', { kind: 'k', mediaType: 'application/json', data: cyclic }],
609
+ ['a function as a payload', { kind: 'k', mediaType: 'application/json', data: () => 1 }],
610
+ ];
611
+ for (const [what, input] of malformed) {
612
+ const err = await attempt(() => store.put(scope, input));
613
+ check(isRefusal(err, 'invalid'), `a put with ${what} was answered with ${textOf(err)}. Storing an approximation of what ` +
614
+ `a caller asked for is the accepted-and-silently-wrong failure — the one a caller ` +
615
+ `finds out about from their own users.`);
616
+ }
617
+ check((await store.list(scope)).artifacts.length === 0, 'a refused put left a ticket in the scope. A refusal that stores half an artifact is ' +
618
+ 'not a refusal.');
619
+ },
620
+ };
621
+ const refusalsKeepSecrets = {
622
+ name: 'refusals-carry-no-payload-and-no-scope',
623
+ law: 'A refusal teaches what to do without quoting the payload, the tenant or the principal.',
624
+ async run(store, kit) {
625
+ // A refusal is read by whoever provoked it, and often lands in a log the
626
+ // caller does not own. It must not be a way to find out who is signed in,
627
+ // which tenant a store holds, or what was inside somebody's dataset.
628
+ const secrets = {
629
+ tenant: `${kit.token}-acme-payroll`,
630
+ principal: `${kit.token}-alice@example.com`,
631
+ conversationId: `${kit.token}-c9`,
632
+ };
633
+ const payload = 'SALARY-BAND-7-CONFIDENTIAL';
634
+ const storeNow = await kit.now(store);
635
+ const provoked = [];
636
+ provoked.push(await attempt(() => store.put(secrets, {
637
+ kind: 'k',
638
+ mediaType: 'text/plain',
639
+ data: payload,
640
+ parentRefs: ['art_' + 'd'.repeat(22)],
641
+ })));
642
+ provoked.push(await attempt(() => store.put(secrets, { kind: '', mediaType: 'text/plain', data: payload })));
643
+ provoked.push(await attempt(() => store.put(secrets, {
644
+ kind: 'k',
645
+ mediaType: 'text/plain',
646
+ data: payload,
647
+ expiresAt: storeNow - 60_000,
648
+ })));
649
+ for (const err of provoked) {
650
+ check(err !== undefined, 'a put this battery expected to be refused was accepted.');
651
+ const text = textOf(err);
652
+ for (const [what, secret] of [
653
+ ['the payload', payload],
654
+ ['the tenant', secrets.tenant],
655
+ ['the principal', secrets.principal],
656
+ ]) {
657
+ check(!text.includes(secret), `a refusal quotes ${what}. Provoking an error must not be a way to read one — the ` +
658
+ `message travels into logs, transcripts and screenshots that the data's owner ` +
659
+ `never sees.\n message: ${text}`);
660
+ }
661
+ }
662
+ },
663
+ };
664
+ const digestRidesTheTicket = {
665
+ name: 'digest-is-minted-over-the-payload-and-rides-the-ticket',
666
+ law: 'A requested digest is computed over the payload at put, rides every ticket, and is the SAME for the same bytes.',
667
+ async run(store, kit) {
668
+ const scope = kit.scope('digest');
669
+ const { meta } = await store.put(scope, {
670
+ kind: 'k',
671
+ mediaType: 'application/json',
672
+ data: { n: 7 },
673
+ digest: 'sha-256',
674
+ });
675
+ check(/^sha-256:[0-9a-f]{64}$/.test(meta.digest ?? ''), `the minted digest is ${JSON.stringify(meta.digest)}. One algorithm, one spelling — a ` +
676
+ `consumer comparing two stores' digests must not have to normalise them first.`);
677
+ const twin = await store.put(scope, {
678
+ kind: 'k',
679
+ mediaType: 'application/json',
680
+ data: { n: 7 },
681
+ digest: 'sha-256',
682
+ });
683
+ check(twin.meta.digest === meta.digest, `the same bytes digested to two values (${String(meta.digest)} / ` +
684
+ `${String(twin.meta.digest)}). The digest is what makes an idempotent re-put ` +
685
+ `detectable; a digest that is per-artifact is a random number with a hex format.`);
686
+ const other = await store.put(scope, {
687
+ kind: 'k',
688
+ mediaType: 'application/json',
689
+ data: { n: 8 },
690
+ digest: 'sha-256',
691
+ });
692
+ check(other.meta.digest !== meta.digest, 'two different payloads digested to the same value.');
693
+ const got = await store.get(scope, meta.ref);
694
+ check(got !== null && sameValue(got.data, { n: 7 }), 'a whole, digested artifact did not read back. get() verifies the digest silently when ' +
695
+ 'the payload is intact; a refusal here means the store disagrees with its own mint.');
696
+ check((await store.head(scope, meta.ref))?.digest === meta.digest, 'head() described the artifact without the digest its own mint stamped — which is the ' +
697
+ 'value a streaming caller is told to check for itself.');
698
+ // Absent when not asked for: a digest nobody requested is a cost nobody
699
+ // agreed to, and an absent field is a fact.
700
+ const plain = await store.put(scope, { kind: 'k', mediaType: 'text/plain', data: 'x' });
701
+ check(plain.meta.digest === undefined, `a put that did not ask for a digest came back with ${JSON.stringify(plain.meta.digest)}.`);
702
+ },
703
+ };
704
+ const getVerifiesDigest = {
705
+ name: 'get-refuses-a-payload-that-no-longer-matches-its-digest',
706
+ law: 'get is the VERIFYING read: bytes that no longer match their minted digest are refused by name, never returned as if whole.',
707
+ harnessNeeds: ['corrupt'],
708
+ async run(store, kit) {
709
+ const scope = kit.scope('integrity');
710
+ const { meta } = await store.put(scope, {
711
+ kind: 'report/csv',
712
+ mediaType: 'text/plain',
713
+ data: 'true bytes',
714
+ digest: 'sha-256',
715
+ });
716
+ await kit.corrupt(store, scope, meta.ref);
717
+ const err = await attempt(() => store.get(scope, meta.ref));
718
+ check(isRefusal(err, 'integrity'), `get() of a payload that no longer matches its minted digest answered ${textOf(err)}. ` +
719
+ `Corrupt data delivered as whole data is the one thing a claim check must never do: ` +
720
+ `honor a ticket with somebody else's parcel.`);
721
+ // The ticket survives its parcel — a caller can still see WHAT it was, and
722
+ // re-create it from its source.
723
+ const described = await store.head(scope, meta.ref);
724
+ check(described !== null && described.digest === meta.digest, 'the integrity refusal took the ticket with it. head() is how a caller learns what was ' +
725
+ 'lost and where it came from; a store that hides the meta leaves them guessing.');
726
+ },
727
+ };
728
+ const getStreamDoesNotVerify = {
729
+ name: 'get-stream-does-not-verify-the-digest',
730
+ law: 'getStream bounds memory, NOT integrity: it hands over the bytes it holds, unverified and unhidden, with the digest still on the ticket.',
731
+ members: ['getStream'],
732
+ harnessNeeds: ['corrupt'],
733
+ async run(store, kit) {
734
+ // The asymmetry is a PROMISE, pinned here so a store cannot quietly close
735
+ // it either way. Verifying needs the whole payload — the exact cost this
736
+ // member exists to avoid — so a store that "fixed" it would have bought
737
+ // the guarantee with the memory a caller chose this member to save, while
738
+ // a store that also dropped the digest from the ticket would leave the
739
+ // caller no way to check for themselves.
740
+ const scope = kit.scope('stream-integrity');
741
+ const { meta } = await store.put(scope, {
742
+ kind: 'report/csv',
743
+ mediaType: 'text/plain',
744
+ data: 'true bytes',
745
+ digest: 'sha-256',
746
+ });
747
+ await kit.corrupt(store, scope, meta.ref);
748
+ const streamed = await store.getStream(scope, meta.ref);
749
+ check(streamed !== null, 'getStream() answered null for an artifact that is present. Its `null` means ' +
750
+ 'missing-or-expired, exactly like get() — a damaged payload is neither.');
751
+ const bytes = await drain(streamed.body);
752
+ check(bytes !== 'true bytes', 'the harness did not actually change the stored payload, so this case proved nothing. ' +
753
+ 'corrupt() must replace the bytes behind the store’s back.');
754
+ check(streamed.meta.digest === meta.digest, `the streamed ticket carries digest ${JSON.stringify(streamed.meta.digest)} and the mint ` +
755
+ `stamped ${JSON.stringify(meta.digest)}. The digest rides ANYWAY so a caller who needs ` +
756
+ `the guarantee can hash what it collected and compare — drop it and the loss stops ` +
757
+ `being named and starts being hidden.`);
758
+ // And the verifying read still refuses, so the two members really are the
759
+ // two different promises the port says they are.
760
+ check(isRefusal(await attempt(() => store.get(scope, meta.ref)), 'integrity'), 'getStream handed the bytes over unverified AND get accepted them. Then there is no ' +
761
+ 'verifying read at all, and the trade this case exists to pin was never made.');
762
+ },
763
+ };
764
+ const streamedPut = {
765
+ name: 'streamed-put-round-trips-and-declares-its-bytes',
766
+ law: 'A streamed put stores what it streamed under a ticket the other five verbs answer for, and a payload that contradicts its DECLARED bytes is refused.',
767
+ members: ['putStream'],
768
+ async run(store, kit) {
769
+ const scope = kit.scope('streamed');
770
+ const chunks = ['col_a,col_b\n', '1,2\n', '3,4\n'];
771
+ const whole = chunks.join('');
772
+ const { meta, swept } = await store.putStream(scope, { kind: 'report/csv', mediaType: 'text/csv', bytes: whole.length, label: 'Q3 export' }, streamOf(chunks));
773
+ check((0, naming_js_1.isArtifactRef)(meta.ref) && meta.bytes === whole.length && meta.label === 'Q3 export', `a streamed put minted ${JSON.stringify(meta.ref)} with bytes=${String(meta.bytes)} ` +
774
+ `(declared ${whole.length}). bytes is STAMPED from the declaration here, because ` +
775
+ `retention has to plan before the payload arrives.`);
776
+ check(Array.isArray(swept), 'a streamed put did not report what retention swept.');
777
+ // The other five verbs do not know how the payload arrived.
778
+ check((await store.head(scope, meta.ref))?.kind === 'report/csv', 'head() did not describe a streamed artifact.');
779
+ const got = await store.get(scope, meta.ref);
780
+ const read = got?.data instanceof Uint8Array ? new TextDecoder().decode(got.data) : got?.data;
781
+ check(read === whole, `get() of a streamed artifact answered ${JSON.stringify(read)}. A payload that arrived in ` +
782
+ `chunks is one payload; how it got there is the adapter's business.`);
783
+ check((await store.list(scope)).artifacts.some((row) => row.ref === meta.ref), 'a streamed artifact is missing from its own scope’s listing.');
784
+ // A declaration the payload contradicts is refused — a meta that lies
785
+ // about its own payload is worse than no artifact, because retention
786
+ // planned against it and every consumer reads it.
787
+ const lying = await attempt(() => store.putStream(scope, { kind: 'report/csv', mediaType: 'text/csv', bytes: 9_999 }, streamOf(chunks)));
788
+ check(lying !== undefined, 'a streamed put whose payload did not match its declared bytes was accepted. The ticket ' +
789
+ 'then misdescribes the artifact for every later consumer, and retention planned ' +
790
+ 'against a number that was never true.');
791
+ check((await store.list(scope)).artifacts.length === 1, 'the refused streamed put left an artifact behind.');
792
+ },
793
+ };
794
+ const streamingFeatureDetected = {
795
+ name: 'streaming-members-are-feature-detected',
796
+ law: 'The optional members are present as functions or ABSENT — never something a caller has to guess about, and never faked over bytes already held whole.',
797
+ async run(store, kit) {
798
+ for (const member of ['putStream', 'getStream']) {
799
+ const value = store[member];
800
+ check(value === undefined || typeof value === 'function', `'${member}' is present as ${typeof value}. Feature detection reads ` +
801
+ `\`typeof store.${member} === 'function'\`, so a member that is present-but-not-a-` +
802
+ `function is detected as available and then fails at the door.`);
803
+ }
804
+ if (store.getStream !== undefined) {
805
+ // `null` means missing-or-expired here too, so a caller can branch on one
806
+ // rule for both readers.
807
+ const scope = kit.scope('stream-absence');
808
+ check((await store.getStream(scope, 'art_' + 'e'.repeat(22))) === null &&
809
+ (await store.getStream(scope, 'not-a-ref')) === null, 'getStream() did not answer null for a ref that resolves to nothing. It answers the ' +
810
+ 'same absence get() does, or a caller needs two rules for one fact.');
811
+ }
812
+ },
813
+ };
814
+ /**
815
+ * The battery, in the order a store fails it most usefully: the round trip
816
+ * before the isolation built on it, isolation before the refusals, refusals
817
+ * before integrity, and the optional streaming leg last.
818
+ */
819
+ exports.artifactStoreConformance = [
820
+ putHeadGet,
821
+ payloadShapes,
822
+ refsAreMinted,
823
+ refAloneOpensNothing,
824
+ confusableScopes,
825
+ oneAbsence,
826
+ expiryIsStated,
827
+ deleteIsAgreement,
828
+ listPages,
829
+ awkwardScopes,
830
+ oversizedRefused,
831
+ parentRefsProven,
832
+ malformedRefused,
833
+ refusalsKeepSecrets,
834
+ digestRidesTheTicket,
835
+ getVerifiesDigest,
836
+ getStreamDoesNotVerify,
837
+ streamedPut,
838
+ streamingFeatureDetected,
839
+ ];
840
+ //# sourceMappingURL=cases.js.map