@venn-lang/artifacts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vinicius Borges
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @venn-lang/artifacts
2
+
3
+ > The `artifacts` namespace: record the traces, videos, HARs and screenshots a run produces.
4
+
5
+ A flow that fails is only useful if you can see what it left behind. This plugin gives a run three
6
+ verbs for filing artifacts and hands every one of them to the `ArtifactStore` port, so where the
7
+ bytes actually live is a decision the host makes at startup and not something baked into the
8
+ language.
9
+
10
+ ## Install
11
+
12
+ Nothing to install. `@venn-lang/artifacts` is part of the standard library, and the CLI and the language
13
+ server both load [`@venn-lang/stdlib`](../stdlib), which lists every stdlib plugin. A file reaches the
14
+ namespace with one line.
15
+
16
+ ```ruby
17
+ use "venn/artifacts"
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```ruby
23
+ module demo.checkout
24
+
25
+ use "venn/artifacts"
26
+ use "venn/assert"
27
+
28
+ afterEach { artifacts.flush }
29
+
30
+ flow "Checkout" {
31
+ step "place the order" {
32
+ expect true
33
+ }
34
+
35
+ on failure {
36
+ artifacts.save "trace" "video" "har"
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## Verbs
42
+
43
+ | Verb | Call | Result |
44
+ | --- | --- | --- |
45
+ | `artifacts.save` | `artifacts.save "trace" "video" "har"` | `list<artifacts.ArtifactRef>`, one ref per kind |
46
+ | `artifacts.flush` | `artifacts.flush` | the refs stored since the last flush, now drained |
47
+ | `artifacts.attach` | `artifacts.attach "report.html" { kind: "report", size: 2048 }` | one `artifacts.ArtifactRef` |
48
+
49
+ `artifacts.save` takes as many kinds as you hand it. The vocabulary has no variadic, so the verb is
50
+ declared with `restArg`: the first argument says what a kind is and the ones past it are left
51
+ unchecked, which is better than describing them wrongly. Both call spellings work, with spaces in
52
+ the bareword form and commas inside brackets:
53
+
54
+ ```ruby
55
+ artifacts.save "trace" "video" "har"
56
+ artifacts.save("trace", "video", "har")
57
+ ```
58
+
59
+ `artifacts.attach` names the artifact positionally; `kind` (default `"attachment"`) and `size` are
60
+ options. `flush` drains the pending buffer and leaves the stored refs in place, so calling it twice
61
+ in a row returns an empty list the second time.
62
+
63
+ ## The type it publishes
64
+
65
+ `artifacts.ArtifactRef` is a record of `name: string`, `kind: string` and an optional
66
+ `size: number`. It is what every verb hands back, and what hover and completion in the editor read.
67
+
68
+ ## The ArtifactStore port
69
+
70
+ | | |
71
+ | --- | --- |
72
+ | id | `venn.port.artifact-store` |
73
+ | version | 1 |
74
+ | requires | `fs` |
75
+ | methods | `put`, `get`, `list`, `flush` |
76
+
77
+ Two implementations ship together, which is what makes this a port rather than a module with a good
78
+ interface:
79
+
80
+ - `createMemoryArtifactStore()` keeps a `Map` of stored refs plus a pending buffer. This is the one
81
+ [`@venn-lang/stdlib`](../stdlib) binds, so the verbs work offline.
82
+ - `createRealArtifactStore()` is a stub. Every call throws a `VennError` with code `VN8090`, because
83
+ this repository is the language and ships no real storage backend.
84
+
85
+ Both run the same conformance suite, `artifactStoreConformance` in
86
+ `src/store/artifact-store.suite.ts`. A third implementation is one file, one line in the local
87
+ `index.ts` and one line in the test.
88
+
89
+ The port lists `flush` among its methods on purpose. A method the descriptor leaves out is a method
90
+ nobody checks at load time, and it would surface as a `TypeError` mid-run instead of a legible
91
+ `VN2011` before anything starts.
92
+
93
+ ## API
94
+
95
+ | Export | What it is |
96
+ | --- | --- |
97
+ | `artifactsPlugin` | The plugin definition: namespace `artifacts`, `requires: ["fs"]`. Also the default export. |
98
+ | `ArtifactStorePort` | The port descriptor. |
99
+ | `ArtifactStore` | The port interface: `put`, `get`, `list`, `flush`. |
100
+ | `createMemoryArtifactStore()` | The in-memory double. |
101
+ | `createRealArtifactStore()` | The real store, stubbed to throw `VN8090`. |
102
+ | `ArtifactRef` | The TypeScript type: `{ name, kind, size? }`. |
103
+ | `ArtifactRefSchema` | The Zod schema registered as the plugin's nominal `ArtifactRef` type. |
104
+ | `artifactsTypeDefs` | The `TypeSpec` for `artifacts.ArtifactRef`, which the checker and the LSP read. |
105
+
106
+ Binding a different store means one entry in the runner's port list:
107
+
108
+ ```ts
109
+ import { ArtifactStorePort, createMemoryArtifactStore } from "@venn-lang/artifacts";
110
+ import { createRunner } from "@venn-lang/runtime";
111
+
112
+ const runner = createRunner({
113
+ host,
114
+ plugins,
115
+ sink,
116
+ uri,
117
+ ports: [{ port: ArtifactStorePort, impl: createMemoryArtifactStore() }],
118
+ });
119
+ ```
120
+
121
+ ## See also
122
+
123
+ - [`@venn-lang/sdk`](../sdk) for `definePlugin` and `defineAction`.
124
+ - [`@venn-lang/contracts`](../contracts) for `Port`, `assertPortShape` and `VennError`.
125
+ - [`@venn-lang/notify`](../std-notify) for telling someone about a run that failed.
@@ -0,0 +1,77 @@
1
+ import { PluginDefinition, ZodType } from "@venn-lang/sdk";
2
+ import { TypeSpec } from "@venn-lang/types";
3
+ import { Port } from "@venn-lang/contracts";
4
+ //#region src/plugin.d.ts
5
+ /**
6
+ * The `@venn-lang/artifacts` plugin. Registers the `artifacts` namespace: the
7
+ * `save`, `flush` and `attach` verbs and the nominal `artifacts.ArtifactRef`
8
+ * type. Requires the `fs` capability, since the store writes to disk.
9
+ */
10
+ declare const artifactsPlugin: PluginDefinition;
11
+ //#endregion
12
+ //#region src/types/artifact-ref.types.d.ts
13
+ /**
14
+ * The receipt for a stored artifact: a trace, a video, a HAR, a screenshot.
15
+ * `kind` names the category. `size` is the byte count, absent until known.
16
+ */
17
+ interface ArtifactRef {
18
+ name: string;
19
+ kind: string;
20
+ size?: number;
21
+ }
22
+ //#endregion
23
+ //#region src/types/artifact-ref.schema.d.ts
24
+ /** Runtime validator for the nominal `artifacts.ArtifactRef` type. */
25
+ declare const ArtifactRefSchema: ZodType<ArtifactRef>;
26
+ //#endregion
27
+ //#region src/types/type-defs.d.ts
28
+ /**
29
+ * The types `@venn-lang/artifacts` publishes to the checker, under the `artifacts`
30
+ * namespace. Kept as data, and mirroring `artifact-ref.types.ts` by hand, so a
31
+ * generator reading the emitted `.d.ts` can replace this file unnoticed.
32
+ */
33
+ declare const artifactsTypeDefs: Readonly<Record<string, TypeSpec>>;
34
+ //#endregion
35
+ //#region src/store/artifact-store.types.d.ts
36
+ /**
37
+ * Keeps references to what a run produced.
38
+ *
39
+ * `put`, `get` and `list` address the stored set. `flush` drains the pending
40
+ * buffer accumulated since the last flush, leaving the stored set untouched.
41
+ */
42
+ interface ArtifactStore {
43
+ /** Stores a ref and queues it for the next flush. Returns the ref as stored. */
44
+ put(ref: ArtifactRef): Promise<ArtifactRef>;
45
+ get(name: string): Promise<ArtifactRef | undefined>;
46
+ list(): Promise<readonly ArtifactRef[]>;
47
+ flush(): Promise<readonly ArtifactRef[]>;
48
+ }
49
+ //#endregion
50
+ //#region src/store/artifact-store.port.d.ts
51
+ /**
52
+ * The port the `artifacts` verbs reach through to keep what a run produced.
53
+ * Requires the `fs` capability, so a host without it fails to load the plugin
54
+ * rather than failing mid-run.
55
+ */
56
+ declare const ArtifactStorePort: Port<ArtifactStore>;
57
+ //#endregion
58
+ //#region src/store/memory-store.d.ts
59
+ /**
60
+ * The in-memory `ArtifactStore` used by tests and by hosts without a disk.
61
+ *
62
+ * @returns a fresh store; nothing is shared between calls.
63
+ */
64
+ declare function createMemoryArtifactStore(): ArtifactStore;
65
+ //#endregion
66
+ //#region src/store/real-store.d.ts
67
+ /**
68
+ * The disk-backed `ArtifactStore`. Not wired up in this build.
69
+ *
70
+ * @returns a store whose every method throws, so the gap is a legible failure
71
+ * rather than a silent no-op.
72
+ * @throws {VennError} `VN8090` on any call.
73
+ */
74
+ declare function createRealArtifactStore(): ArtifactStore;
75
+ //#endregion
76
+ export { type ArtifactRef, ArtifactRefSchema, type ArtifactStore, ArtifactStorePort, artifactsPlugin, artifactsPlugin as default, artifactsTypeDefs, createMemoryArtifactStore, createRealArtifactStore };
77
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugin.ts","../src/types/artifact-ref.types.ts","../src/types/artifact-ref.schema.ts","../src/types/type-defs.ts","../src/store/artifact-store.types.ts","../src/store/artifact-store.port.ts","../src/store/memory-store.ts","../src/store/real-store.ts"],"mappings":";;;;;;;;;cASa,iBAAiB;;;;;;;UCLb;EACf;EACA;EACA;;;;;cCHW,mBAAmB,QAAQ;;;;;;;;cCG3B,mBAAmB,SAAS,eAAe;;;;;;;;;UCCvC;;EAEf,IAAI,KAAK,cAAc,QAAQ;EAC/B,IAAI,eAAe,QAAQ;EAC3B,QAAQ,iBAAiB;EACzB,SAAS,iBAAiB;;;;;;;;;cCLf,mBAAmB,KAAK;;;;;;;;iBCArB,6BAA6B;;;;;;;;;;iBCE7B,2BAA2B"}
package/dist/index.js ADDED
@@ -0,0 +1,195 @@
1
+ import { arg, defineAction, definePlugin, restArg, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { VennError } from "@venn-lang/contracts";
4
+ //#region src/store/artifact-store.port.ts
5
+ /**
6
+ * The port the `artifacts` verbs reach through to keep what a run produced.
7
+ * Requires the `fs` capability, so a host without it fails to load the plugin
8
+ * rather than failing mid-run.
9
+ */
10
+ const ArtifactStorePort = {
11
+ id: "venn.port.artifact-store",
12
+ version: 1,
13
+ requires: ["fs"],
14
+ methods: [
15
+ "put",
16
+ "get",
17
+ "list",
18
+ "flush"
19
+ ]
20
+ };
21
+ //#endregion
22
+ //#region src/store/memory-store.ts
23
+ /**
24
+ * The in-memory `ArtifactStore` used by tests and by hosts without a disk.
25
+ *
26
+ * @returns a fresh store; nothing is shared between calls.
27
+ */
28
+ function createMemoryArtifactStore() {
29
+ const stored = /* @__PURE__ */ new Map();
30
+ let pending = [];
31
+ return {
32
+ async put(ref) {
33
+ stored.set(ref.name, ref);
34
+ pending.push(ref);
35
+ return ref;
36
+ },
37
+ async get(name) {
38
+ return stored.get(name);
39
+ },
40
+ async list() {
41
+ return [...stored.values()];
42
+ },
43
+ async flush() {
44
+ const drained = pending;
45
+ pending = [];
46
+ return drained;
47
+ }
48
+ };
49
+ }
50
+ //#endregion
51
+ //#region src/store/real-store.ts
52
+ /**
53
+ * The disk-backed `ArtifactStore`. Not wired up in this build.
54
+ *
55
+ * @returns a store whose every method throws, so the gap is a legible failure
56
+ * rather than a silent no-op.
57
+ * @throws {VennError} `VN8090` on any call.
58
+ */
59
+ function createRealArtifactStore() {
60
+ return {
61
+ put: async () => notImplemented(),
62
+ get: async () => notImplemented(),
63
+ list: async () => notImplemented(),
64
+ flush: async () => notImplemented()
65
+ };
66
+ }
67
+ function notImplemented() {
68
+ throw new VennError({
69
+ code: "VN8090",
70
+ message: "The artifact store is not implemented in this build."
71
+ });
72
+ }
73
+ /**
74
+ * `artifacts.attach "screenshot.png" { kind: "image", size: 4096 }`.
75
+ *
76
+ * Files one already existing artifact under a name so the report can show it.
77
+ * `kind` defaults to `attachment` and `size` stays absent when not given.
78
+ *
79
+ * @returns the stored `artifacts.ArtifactRef`.
80
+ */
81
+ const attachAction = defineAction({
82
+ name: "attach",
83
+ doc: "Attach an artifact by name, with an optional kind and size.",
84
+ params: z.object({
85
+ kind: z.string().optional(),
86
+ size: z.number().optional()
87
+ }).optional(),
88
+ args: [arg("name", t.string, "What to file it under.")],
89
+ result: t.ref("artifacts.ArtifactRef"),
90
+ run: (ctx, input) => ctx.port(ArtifactStorePort).put(buildRef(input))
91
+ });
92
+ function buildRef(input) {
93
+ const params = input.params ?? {};
94
+ return {
95
+ name: String(input.args[0] ?? ""),
96
+ kind: params.kind ?? "attachment",
97
+ size: params.size
98
+ };
99
+ }
100
+ //#endregion
101
+ //#region src/actions/flush.ts
102
+ /**
103
+ * `artifacts.flush`.
104
+ *
105
+ * Persists everything buffered since the last flush and empties the buffer.
106
+ * Stored refs survive; only the pending queue is drained.
107
+ *
108
+ * @returns the list of `artifacts.ArtifactRef` that were written out.
109
+ */
110
+ const flushAction = defineAction({
111
+ name: "flush",
112
+ doc: "Flush pending artifacts, returning the refs that were drained.",
113
+ result: t.list(t.ref("artifacts.ArtifactRef")),
114
+ run: (ctx) => ctx.port(ArtifactStorePort).flush()
115
+ });
116
+ //#endregion
117
+ //#region src/actions/save.ts
118
+ /**
119
+ * `artifacts.save trace video har`.
120
+ *
121
+ * Each positional argument names a kind of artifact to keep from this run. One
122
+ * ref is stored per kind, in the order given.
123
+ *
124
+ * @returns the list of stored `artifacts.ArtifactRef`, one per kind.
125
+ */
126
+ const saveAction = defineAction({
127
+ name: "save",
128
+ doc: "Store one artifact per positional kind ref and return the stored refs.",
129
+ args: [restArg("kinds", t.string, "One or more kinds to store, as positional arguments.")],
130
+ result: t.list(t.ref("artifacts.ArtifactRef")),
131
+ run: async (ctx, input) => {
132
+ const store = ctx.port(ArtifactStorePort);
133
+ return Promise.all(input.args.map((kind) => store.put(toRef(kind))));
134
+ }
135
+ });
136
+ function toRef(kind) {
137
+ const name = String(kind);
138
+ return {
139
+ name,
140
+ kind: name
141
+ };
142
+ }
143
+ //#endregion
144
+ //#region src/actions/index.ts
145
+ /** Every verb in the `artifacts` namespace, in the order the plugin registers them. */
146
+ const artifactsActions = [
147
+ saveAction,
148
+ flushAction,
149
+ attachAction
150
+ ];
151
+ //#endregion
152
+ //#region src/types/artifact-ref.schema.ts
153
+ /** Runtime validator for the nominal `artifacts.ArtifactRef` type. */
154
+ const ArtifactRefSchema = z.object({
155
+ name: z.string(),
156
+ kind: z.string(),
157
+ size: z.number().optional()
158
+ });
159
+ //#endregion
160
+ //#region src/types/type-defs.ts
161
+ /**
162
+ * The types `@venn-lang/artifacts` publishes to the checker, under the `artifacts`
163
+ * namespace. Kept as data, and mirroring `artifact-ref.types.ts` by hand, so a
164
+ * generator reading the emitted `.d.ts` can replace this file unnoticed.
165
+ */
166
+ const artifactsTypeDefs = {
167
+ /**
168
+ * The receipt for a stored artifact: a trace, a video, a HAR, a screenshot.
169
+ * `size` is absent until the store knows how many bytes it kept.
170
+ */
171
+ ArtifactRef: t.record({
172
+ name: t.string,
173
+ kind: t.string,
174
+ size: t.number
175
+ }, { optional: ["size"] }) };
176
+ //#endregion
177
+ //#region src/plugin.ts
178
+ /**
179
+ * The `@venn-lang/artifacts` plugin. Registers the `artifacts` namespace: the
180
+ * `save`, `flush` and `attach` verbs and the nominal `artifacts.ArtifactRef`
181
+ * type. Requires the `fs` capability, since the store writes to disk.
182
+ */
183
+ const artifactsPlugin = definePlugin({
184
+ name: "venn/artifacts",
185
+ version: "0.0.0",
186
+ namespace: "artifacts",
187
+ requires: ["fs"],
188
+ actions: artifactsActions,
189
+ types: { ArtifactRef: ArtifactRefSchema },
190
+ typeDefs: artifactsTypeDefs
191
+ });
192
+ //#endregion
193
+ export { ArtifactRefSchema, ArtifactStorePort, artifactsPlugin, artifactsPlugin as default, artifactsTypeDefs, createMemoryArtifactStore, createRealArtifactStore };
194
+
195
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/store/artifact-store.port.ts","../src/store/memory-store.ts","../src/store/real-store.ts","../src/actions/attach.ts","../src/actions/flush.ts","../src/actions/save.ts","../src/actions/index.ts","../src/types/artifact-ref.schema.ts","../src/types/type-defs.ts","../src/plugin.ts"],"sourcesContent":["import type { Port } from \"@venn-lang/contracts\";\nimport type { ArtifactStore } from \"./artifact-store.types.js\";\n\n/**\n * The port the `artifacts` verbs reach through to keep what a run produced.\n * Requires the `fs` capability, so a host without it fails to load the plugin\n * rather than failing mid-run.\n */\nexport const ArtifactStorePort: Port<ArtifactStore> = {\n id: \"venn.port.artifact-store\",\n version: 1,\n requires: [\"fs\"],\n // List every method an action reaches for. An omission is not checked at load\n // time, so it surfaces as a TypeError mid-run instead of a legible VN2011.\n methods: [\"put\", \"get\", \"list\", \"flush\"],\n};\n","import type { ArtifactRef } from \"../types/index.js\";\nimport type { ArtifactStore } from \"./artifact-store.types.js\";\n\n/**\n * The in-memory `ArtifactStore` used by tests and by hosts without a disk.\n *\n * @returns a fresh store; nothing is shared between calls.\n */\nexport function createMemoryArtifactStore(): ArtifactStore {\n const stored = new Map<string, ArtifactRef>();\n let pending: ArtifactRef[] = [];\n return {\n async put(ref) {\n stored.set(ref.name, ref);\n pending.push(ref);\n return ref;\n },\n async get(name) {\n return stored.get(name);\n },\n async list() {\n return [...stored.values()];\n },\n async flush() {\n const drained = pending;\n pending = [];\n return drained;\n },\n };\n}\n","import { VennError } from \"@venn-lang/contracts\";\nimport type { ArtifactStore } from \"./artifact-store.types.js\";\n\n/**\n * The disk-backed `ArtifactStore`. Not wired up in this build.\n *\n * @returns a store whose every method throws, so the gap is a legible failure\n * rather than a silent no-op.\n * @throws {VennError} `VN8090` on any call.\n */\nexport function createRealArtifactStore(): ArtifactStore {\n return {\n put: async () => notImplemented(),\n get: async () => notImplemented(),\n list: async () => notImplemented(),\n flush: async () => notImplemented(),\n };\n}\n\nfunction notImplemented(): never {\n throw new VennError({\n code: \"VN8090\",\n message: \"The artifact store is not implemented in this build.\",\n });\n}\n","import { type ActionDefinition, type ActionInput, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { ArtifactStorePort } from \"../store/index.js\";\nimport type { ArtifactRef } from \"../types/index.js\";\n\nconst attachParams = z.object({ kind: z.string().optional(), size: z.number().optional() });\n\n/**\n * `artifacts.attach \"screenshot.png\" { kind: \"image\", size: 4096 }`.\n *\n * Files one already existing artifact under a name so the report can show it.\n * `kind` defaults to `attachment` and `size` stays absent when not given.\n *\n * @returns the stored `artifacts.ArtifactRef`.\n */\nexport const attachAction: ActionDefinition = defineAction({\n name: \"attach\",\n doc: \"Attach an artifact by name, with an optional kind and size.\",\n params: attachParams.optional(),\n args: [arg(\"name\", t.string, \"What to file it under.\")],\n result: t.ref(\"artifacts.ArtifactRef\"),\n run: (ctx, input) => ctx.port(ArtifactStorePort).put(buildRef(input)),\n});\n\nfunction buildRef(input: ActionInput<unknown>): ArtifactRef {\n const params = (input.params ?? {}) as { kind?: string; size?: number };\n return {\n name: String(input.args[0] ?? \"\"),\n kind: params.kind ?? \"attachment\",\n size: params.size,\n };\n}\n","import { type ActionDefinition, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { ArtifactStorePort } from \"../store/index.js\";\n\n/**\n * `artifacts.flush`.\n *\n * Persists everything buffered since the last flush and empties the buffer.\n * Stored refs survive; only the pending queue is drained.\n *\n * @returns the list of `artifacts.ArtifactRef` that were written out.\n */\nexport const flushAction: ActionDefinition = defineAction({\n name: \"flush\",\n doc: \"Flush pending artifacts, returning the refs that were drained.\",\n result: t.list(t.ref(\"artifacts.ArtifactRef\")),\n run: (ctx) => ctx.port(ArtifactStorePort).flush(),\n});\n","import { type ActionDefinition, defineAction, restArg } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { ArtifactStorePort } from \"../store/index.js\";\nimport type { ArtifactRef } from \"../types/index.js\";\n\n/**\n * `artifacts.save trace video har`.\n *\n * Each positional argument names a kind of artifact to keep from this run. One\n * ref is stored per kind, in the order given.\n *\n * @returns the list of stored `artifacts.ArtifactRef`, one per kind.\n */\nexport const saveAction: ActionDefinition = defineAction({\n name: \"save\",\n doc: \"Store one artifact per positional kind ref and return the stored refs.\",\n // The vocabulary has no variadic, so `restArg` types the first kind and\n // leaves the rest unchecked. Better an unchecked argument than a wrong type.\n args: [restArg(\"kinds\", t.string, \"One or more kinds to store, as positional arguments.\")],\n result: t.list(t.ref(\"artifacts.ArtifactRef\")),\n run: async (ctx, input) => {\n const store = ctx.port(ArtifactStorePort);\n return Promise.all(input.args.map((kind) => store.put(toRef(kind))));\n },\n});\n\nfunction toRef(kind: unknown): ArtifactRef {\n const name = String(kind);\n return { name, kind: name };\n}\n","import type { ActionDefinition } from \"@venn-lang/sdk\";\nimport { attachAction } from \"./attach.js\";\nimport { flushAction } from \"./flush.js\";\nimport { saveAction } from \"./save.js\";\n\n/** Every verb in the `artifacts` namespace, in the order the plugin registers them. */\nexport const artifactsActions: ActionDefinition[] = [saveAction, flushAction, attachAction];\n","import { type ZodType, z } from \"@venn-lang/sdk\";\nimport type { ArtifactRef } from \"./artifact-ref.types.js\";\n\n/** Runtime validator for the nominal `artifacts.ArtifactRef` type. */\nexport const ArtifactRefSchema: ZodType<ArtifactRef> = z.object({\n name: z.string(),\n kind: z.string(),\n size: z.number().optional(),\n});\n","import { type TypeSpec, t } from \"@venn-lang/types\";\n\n/**\n * The types `@venn-lang/artifacts` publishes to the checker, under the `artifacts`\n * namespace. Kept as data, and mirroring `artifact-ref.types.ts` by hand, so a\n * generator reading the emitted `.d.ts` can replace this file unnoticed.\n */\nexport const artifactsTypeDefs: Readonly<Record<string, TypeSpec>> = {\n /**\n * The receipt for a stored artifact: a trace, a video, a HAR, a screenshot.\n * `size` is absent until the store knows how many bytes it kept.\n */\n ArtifactRef: t.record({ name: t.string, kind: t.string, size: t.number }, { optional: [\"size\"] }),\n};\n","import { definePlugin, type PluginDefinition } from \"@venn-lang/sdk\";\nimport { artifactsActions } from \"./actions/index.js\";\nimport { ArtifactRefSchema, artifactsTypeDefs } from \"./types/index.js\";\n\n/**\n * The `@venn-lang/artifacts` plugin. Registers the `artifacts` namespace: the\n * `save`, `flush` and `attach` verbs and the nominal `artifacts.ArtifactRef`\n * type. Requires the `fs` capability, since the store writes to disk.\n */\nexport const artifactsPlugin: PluginDefinition = definePlugin({\n name: \"venn/artifacts\",\n version: \"0.0.0\",\n namespace: \"artifacts\",\n requires: [\"fs\"],\n actions: artifactsActions,\n types: { ArtifactRef: ArtifactRefSchema },\n typeDefs: artifactsTypeDefs,\n});\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@venn-lang/artifacts",
3
+ "version": "0.1.0",
4
+ "description": "The artifacts namespace: record the traces, videos, HARs and screenshots a run produces.",
5
+ "keywords": [
6
+ "venn",
7
+ "testing",
8
+ "e2e",
9
+ "artifacts"
10
+ ],
11
+ "homepage": "https://github.com/venn-lang/venn/tree/main/packages/std-artifacts#readme",
12
+ "bugs": "https://github.com/venn-lang/venn/issues",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/venn-lang/venn.git",
16
+ "directory": "packages/std-artifacts"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Vinicius Borges",
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "development": "./src/index.ts",
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src",
33
+ "!src/**/*.test.ts",
34
+ "!src/**/*.suite.ts"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@venn-lang/contracts": "0.1.0",
41
+ "@venn-lang/sdk": "0.1.0",
42
+ "@venn-lang/types": "0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "tsdown": "^0.22.14",
46
+ "typescript": "^7.0.2",
47
+ "vitest": "^4.1.10"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "test": "vitest run",
52
+ "typecheck": "tsc --noEmit"
53
+ }
54
+ }
@@ -0,0 +1,32 @@
1
+ import { type ActionDefinition, type ActionInput, arg, defineAction, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { ArtifactStorePort } from "../store/index.js";
4
+ import type { ArtifactRef } from "../types/index.js";
5
+
6
+ const attachParams = z.object({ kind: z.string().optional(), size: z.number().optional() });
7
+
8
+ /**
9
+ * `artifacts.attach "screenshot.png" { kind: "image", size: 4096 }`.
10
+ *
11
+ * Files one already existing artifact under a name so the report can show it.
12
+ * `kind` defaults to `attachment` and `size` stays absent when not given.
13
+ *
14
+ * @returns the stored `artifacts.ArtifactRef`.
15
+ */
16
+ export const attachAction: ActionDefinition = defineAction({
17
+ name: "attach",
18
+ doc: "Attach an artifact by name, with an optional kind and size.",
19
+ params: attachParams.optional(),
20
+ args: [arg("name", t.string, "What to file it under.")],
21
+ result: t.ref("artifacts.ArtifactRef"),
22
+ run: (ctx, input) => ctx.port(ArtifactStorePort).put(buildRef(input)),
23
+ });
24
+
25
+ function buildRef(input: ActionInput<unknown>): ArtifactRef {
26
+ const params = (input.params ?? {}) as { kind?: string; size?: number };
27
+ return {
28
+ name: String(input.args[0] ?? ""),
29
+ kind: params.kind ?? "attachment",
30
+ size: params.size,
31
+ };
32
+ }
@@ -0,0 +1,18 @@
1
+ import { type ActionDefinition, defineAction } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { ArtifactStorePort } from "../store/index.js";
4
+
5
+ /**
6
+ * `artifacts.flush`.
7
+ *
8
+ * Persists everything buffered since the last flush and empties the buffer.
9
+ * Stored refs survive; only the pending queue is drained.
10
+ *
11
+ * @returns the list of `artifacts.ArtifactRef` that were written out.
12
+ */
13
+ export const flushAction: ActionDefinition = defineAction({
14
+ name: "flush",
15
+ doc: "Flush pending artifacts, returning the refs that were drained.",
16
+ result: t.list(t.ref("artifacts.ArtifactRef")),
17
+ run: (ctx) => ctx.port(ArtifactStorePort).flush(),
18
+ });
@@ -0,0 +1,7 @@
1
+ import type { ActionDefinition } from "@venn-lang/sdk";
2
+ import { attachAction } from "./attach.js";
3
+ import { flushAction } from "./flush.js";
4
+ import { saveAction } from "./save.js";
5
+
6
+ /** Every verb in the `artifacts` namespace, in the order the plugin registers them. */
7
+ export const artifactsActions: ActionDefinition[] = [saveAction, flushAction, attachAction];
@@ -0,0 +1,30 @@
1
+ import { type ActionDefinition, defineAction, restArg } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { ArtifactStorePort } from "../store/index.js";
4
+ import type { ArtifactRef } from "../types/index.js";
5
+
6
+ /**
7
+ * `artifacts.save trace video har`.
8
+ *
9
+ * Each positional argument names a kind of artifact to keep from this run. One
10
+ * ref is stored per kind, in the order given.
11
+ *
12
+ * @returns the list of stored `artifacts.ArtifactRef`, one per kind.
13
+ */
14
+ export const saveAction: ActionDefinition = defineAction({
15
+ name: "save",
16
+ doc: "Store one artifact per positional kind ref and return the stored refs.",
17
+ // The vocabulary has no variadic, so `restArg` types the first kind and
18
+ // leaves the rest unchecked. Better an unchecked argument than a wrong type.
19
+ args: [restArg("kinds", t.string, "One or more kinds to store, as positional arguments.")],
20
+ result: t.list(t.ref("artifacts.ArtifactRef")),
21
+ run: async (ctx, input) => {
22
+ const store = ctx.port(ArtifactStorePort);
23
+ return Promise.all(input.args.map((kind) => store.put(toRef(kind))));
24
+ },
25
+ });
26
+
27
+ function toRef(kind: unknown): ArtifactRef {
28
+ const name = String(kind);
29
+ return { name, kind: name };
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `@venn-lang/artifacts`: the verbs that record what a run left behind (traces,
3
+ * videos, HARs, screenshots) and the `ArtifactStore` port that keeps them.
4
+ */
5
+
6
+ export { artifactsPlugin, artifactsPlugin as default } from "./plugin.js";
7
+ export * from "./store/index.js";
8
+ export * from "./types/index.js";
package/src/plugin.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { definePlugin, type PluginDefinition } from "@venn-lang/sdk";
2
+ import { artifactsActions } from "./actions/index.js";
3
+ import { ArtifactRefSchema, artifactsTypeDefs } from "./types/index.js";
4
+
5
+ /**
6
+ * The `@venn-lang/artifacts` plugin. Registers the `artifacts` namespace: the
7
+ * `save`, `flush` and `attach` verbs and the nominal `artifacts.ArtifactRef`
8
+ * type. Requires the `fs` capability, since the store writes to disk.
9
+ */
10
+ export const artifactsPlugin: PluginDefinition = definePlugin({
11
+ name: "venn/artifacts",
12
+ version: "0.0.0",
13
+ namespace: "artifacts",
14
+ requires: ["fs"],
15
+ actions: artifactsActions,
16
+ types: { ArtifactRef: ArtifactRefSchema },
17
+ typeDefs: artifactsTypeDefs,
18
+ });
@@ -0,0 +1,16 @@
1
+ import type { Port } from "@venn-lang/contracts";
2
+ import type { ArtifactStore } from "./artifact-store.types.js";
3
+
4
+ /**
5
+ * The port the `artifacts` verbs reach through to keep what a run produced.
6
+ * Requires the `fs` capability, so a host without it fails to load the plugin
7
+ * rather than failing mid-run.
8
+ */
9
+ export const ArtifactStorePort: Port<ArtifactStore> = {
10
+ id: "venn.port.artifact-store",
11
+ version: 1,
12
+ requires: ["fs"],
13
+ // List every method an action reaches for. An omission is not checked at load
14
+ // time, so it surfaces as a TypeError mid-run instead of a legible VN2011.
15
+ methods: ["put", "get", "list", "flush"],
16
+ };
@@ -0,0 +1,15 @@
1
+ import type { ArtifactRef } from "../types/index.js";
2
+
3
+ /**
4
+ * Keeps references to what a run produced.
5
+ *
6
+ * `put`, `get` and `list` address the stored set. `flush` drains the pending
7
+ * buffer accumulated since the last flush, leaving the stored set untouched.
8
+ */
9
+ export interface ArtifactStore {
10
+ /** Stores a ref and queues it for the next flush. Returns the ref as stored. */
11
+ put(ref: ArtifactRef): Promise<ArtifactRef>;
12
+ get(name: string): Promise<ArtifactRef | undefined>;
13
+ list(): Promise<readonly ArtifactRef[]>;
14
+ flush(): Promise<readonly ArtifactRef[]>;
15
+ }
@@ -0,0 +1,4 @@
1
+ export { ArtifactStorePort } from "./artifact-store.port.js";
2
+ export type { ArtifactStore } from "./artifact-store.types.js";
3
+ export { createMemoryArtifactStore } from "./memory-store.js";
4
+ export { createRealArtifactStore } from "./real-store.js";
@@ -0,0 +1,30 @@
1
+ import type { ArtifactRef } from "../types/index.js";
2
+ import type { ArtifactStore } from "./artifact-store.types.js";
3
+
4
+ /**
5
+ * The in-memory `ArtifactStore` used by tests and by hosts without a disk.
6
+ *
7
+ * @returns a fresh store; nothing is shared between calls.
8
+ */
9
+ export function createMemoryArtifactStore(): ArtifactStore {
10
+ const stored = new Map<string, ArtifactRef>();
11
+ let pending: ArtifactRef[] = [];
12
+ return {
13
+ async put(ref) {
14
+ stored.set(ref.name, ref);
15
+ pending.push(ref);
16
+ return ref;
17
+ },
18
+ async get(name) {
19
+ return stored.get(name);
20
+ },
21
+ async list() {
22
+ return [...stored.values()];
23
+ },
24
+ async flush() {
25
+ const drained = pending;
26
+ pending = [];
27
+ return drained;
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,25 @@
1
+ import { VennError } from "@venn-lang/contracts";
2
+ import type { ArtifactStore } from "./artifact-store.types.js";
3
+
4
+ /**
5
+ * The disk-backed `ArtifactStore`. Not wired up in this build.
6
+ *
7
+ * @returns a store whose every method throws, so the gap is a legible failure
8
+ * rather than a silent no-op.
9
+ * @throws {VennError} `VN8090` on any call.
10
+ */
11
+ export function createRealArtifactStore(): ArtifactStore {
12
+ return {
13
+ put: async () => notImplemented(),
14
+ get: async () => notImplemented(),
15
+ list: async () => notImplemented(),
16
+ flush: async () => notImplemented(),
17
+ };
18
+ }
19
+
20
+ function notImplemented(): never {
21
+ throw new VennError({
22
+ code: "VN8090",
23
+ message: "The artifact store is not implemented in this build.",
24
+ });
25
+ }
@@ -0,0 +1,9 @@
1
+ import { type ZodType, z } from "@venn-lang/sdk";
2
+ import type { ArtifactRef } from "./artifact-ref.types.js";
3
+
4
+ /** Runtime validator for the nominal `artifacts.ArtifactRef` type. */
5
+ export const ArtifactRefSchema: ZodType<ArtifactRef> = z.object({
6
+ name: z.string(),
7
+ kind: z.string(),
8
+ size: z.number().optional(),
9
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The receipt for a stored artifact: a trace, a video, a HAR, a screenshot.
3
+ * `kind` names the category. `size` is the byte count, absent until known.
4
+ */
5
+ export interface ArtifactRef {
6
+ name: string;
7
+ kind: string;
8
+ size?: number;
9
+ }
@@ -0,0 +1,3 @@
1
+ export { ArtifactRefSchema } from "./artifact-ref.schema.js";
2
+ export type { ArtifactRef } from "./artifact-ref.types.js";
3
+ export { artifactsTypeDefs } from "./type-defs.js";
@@ -0,0 +1,14 @@
1
+ import { type TypeSpec, t } from "@venn-lang/types";
2
+
3
+ /**
4
+ * The types `@venn-lang/artifacts` publishes to the checker, under the `artifacts`
5
+ * namespace. Kept as data, and mirroring `artifact-ref.types.ts` by hand, so a
6
+ * generator reading the emitted `.d.ts` can replace this file unnoticed.
7
+ */
8
+ export const artifactsTypeDefs: Readonly<Record<string, TypeSpec>> = {
9
+ /**
10
+ * The receipt for a stored artifact: a trace, a video, a HAR, a screenshot.
11
+ * `size` is absent until the store knows how many bytes it kept.
12
+ */
13
+ ArtifactRef: t.record({ name: t.string, kind: t.string, size: t.number }, { optional: ["size"] }),
14
+ };