@venn-lang/notify 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,131 @@
1
+ # @venn-lang/notify
2
+
3
+ > The `notify` namespace: send a Slack message, a webhook or an email from a flow.
4
+
5
+ A run that fails at 3am is only useful if somebody hears about it. This plugin gives a flow three
6
+ dispatch verbs and routes all of them through the `Notifier` port, so the transport is chosen by the
7
+ host at startup. Under test that means a recorder you can assert against, with nothing leaving the
8
+ machine.
9
+
10
+ ## Install
11
+
12
+ Nothing to install. `@venn-lang/notify` 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/notify"
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```ruby
23
+ module demo.checkout
24
+
25
+ use "venn/notify"
26
+ use "venn/assert"
27
+
28
+ flow "Checkout" {
29
+ step "place the order" {
30
+ expect true
31
+ }
32
+
33
+ on failure {
34
+ notify.slack "#qa" { mention: "@oncall" }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## Verbs
40
+
41
+ | Verb | Call | Options |
42
+ | --- | --- | --- |
43
+ | `notify.slack` | `notify.slack "#qa"` | `mention` |
44
+ | `notify.webhook` | `notify.webhook "https://hook.test/build"` | `json` |
45
+ | `notify.email` | `notify.email "qa@example.com"` | `subject`, `body` |
46
+
47
+ Each verb takes its destination as the one positional argument, the channel for Slack, the URL for a
48
+ webhook, the recipient for an email, and everything else as options:
49
+
50
+ ```ruby
51
+ const posted = notify.slack "#builds" { mention: "@oncall" }
52
+ const hooked = notify.webhook "https://hook.test/build" { json: { status: "green" } }
53
+ const mailed = notify.email "qa@example.com" { subject: "Nightly", body: "All green." }
54
+
55
+ expect posted.delivered == true
56
+ ```
57
+
58
+ All three return `notify.Receipt`.
59
+
60
+ ## The type it publishes
61
+
62
+ `notify.Receipt` is a record of `delivered: bool` and `id: string`. `delivered` is the dispatch, not
63
+ the reading: nobody here knows whether a human saw it.
64
+
65
+ ## The Notifier port
66
+
67
+ | | |
68
+ | --- | --- |
69
+ | id | `venn.port.notifier` |
70
+ | version | 1 |
71
+ | requires | `net` |
72
+ | methods | `send` |
73
+
74
+ Two implementations ship together, which is what makes this a port rather than a module with a good
75
+ interface:
76
+
77
+ - `createFakeNotifier()` records every notification in memory and returns
78
+ `{ delivered: true, id: "fake-<n>" }`. It is a `FakeNotifier`, so the recorded messages are
79
+ readable through `sent`. This is the one [`@venn-lang/stdlib`](../stdlib) binds.
80
+ - `createRealNotifier()` is a stub. Every call throws a `VennError` with code `VN8090`, because this
81
+ repository is the language and ships no live Slack, webhook or SMTP wiring.
82
+
83
+ Both run the same conformance suite, `notifierConformance` in `src/clients/notifier.suite.ts`: a
84
+ `send` resolves a receipt with a boolean `delivered` and a string `id`.
85
+
86
+ Asserting on what a flow sent, from TypeScript:
87
+
88
+ ```ts
89
+ import { createFakeNotifier } from "@venn-lang/notify";
90
+
91
+ const notifier = createFakeNotifier();
92
+ await notifier.send({ kind: "slack", channel: "#qa", mention: "@vini" });
93
+
94
+ expect(notifier.sent[0]).toMatchObject({ kind: "slack", channel: "#qa" });
95
+ ```
96
+
97
+ ## API
98
+
99
+ | Export | What it is |
100
+ | --- | --- |
101
+ | `notifyPlugin` | The plugin definition: namespace `notify`, `requires: ["net"]`. Also the default export. |
102
+ | `notifyActions` | The three action definitions, in order: `slack`, `webhook`, `email`. |
103
+ | `NotifierPort` | The port descriptor. |
104
+ | `Notifier` | The port interface: `send(message)`. |
105
+ | `FakeNotifier` | A `Notifier` that also exposes `sent`, the notifications it recorded. |
106
+ | `Notification` | What is handed to the port: `kind`, `channel`, and the optional `subject`, `body`, `mention`, `json`. |
107
+ | `NotificationKind` | `"slack" \| "webhook" \| "email"`. |
108
+ | `NotifyReceipt` | What the port returns: `{ delivered, id }`. |
109
+ | `createFakeNotifier()` | The recording double. |
110
+ | `createRealNotifier()` | The real notifier, stubbed to throw `VN8090`. |
111
+
112
+ Binding a different notifier means one entry in the runner's port list:
113
+
114
+ ```ts
115
+ import { createRunner } from "@venn-lang/runtime";
116
+ import { createFakeNotifier, NotifierPort } from "@venn-lang/notify";
117
+
118
+ const runner = createRunner({
119
+ host,
120
+ plugins,
121
+ sink,
122
+ uri,
123
+ ports: [{ port: NotifierPort, impl: createFakeNotifier() }],
124
+ });
125
+ ```
126
+
127
+ ## See also
128
+
129
+ - [`@venn-lang/sdk`](../sdk) for `definePlugin` and `defineAction`.
130
+ - [`@venn-lang/contracts`](../contracts) for `Port`, `assertPortShape` and `VennError`.
131
+ - [`@venn-lang/artifacts`](../std-artifacts) for the traces and videos a failure leaves behind.
@@ -0,0 +1,82 @@
1
+ import { ActionDefinition, PluginDefinition } from "@venn-lang/sdk";
2
+ import { Port } from "@venn-lang/contracts";
3
+ //#region src/actions/index.d.ts
4
+ /** Every verb in the `notify` namespace, in the order the plugin registers them. */
5
+ declare const notifyActions: ActionDefinition[];
6
+ //#endregion
7
+ //#region src/port/notifier.types.d.ts
8
+ /** The kind of channel a notification targets. */
9
+ type NotificationKind = "slack" | "webhook" | "email";
10
+ /**
11
+ * One message handed to the `Notifier`. `kind` picks the channel type and
12
+ * decides which of the optional fields carry meaning: `subject` and `body` for
13
+ * email, `mention` for Slack, `json` for a webhook.
14
+ */
15
+ interface Notification {
16
+ kind: NotificationKind;
17
+ /** Where it goes, read in the terms of its `kind`: a Slack channel, a URL, an address. */
18
+ channel: string;
19
+ subject?: string;
20
+ body?: string;
21
+ mention?: string;
22
+ json?: unknown;
23
+ }
24
+ /** What a `Notifier` answers with once a message is on its way. */
25
+ interface NotifyReceipt {
26
+ /** The dispatch succeeded. Says nothing about whether anyone read it. */
27
+ delivered: boolean;
28
+ /** The notifier's own handle for the message. */
29
+ id: string;
30
+ }
31
+ /** The contract every notification goes through, whatever the channel. */
32
+ interface Notifier {
33
+ /**
34
+ * Dispatches one message.
35
+ *
36
+ * @returns the receipt for the dispatch.
37
+ * @throws {VennError} `VN8090` from an implementation that cannot deliver.
38
+ */
39
+ send(message: Notification): Promise<NotifyReceipt>;
40
+ }
41
+ /** A `Notifier` that keeps every sent notification in memory, for assertions. */
42
+ interface FakeNotifier extends Notifier {
43
+ readonly sent: readonly Notification[];
44
+ }
45
+ //#endregion
46
+ //#region src/port/notifier.port.d.ts
47
+ /**
48
+ * The port every `notify` verb dispatches through. Requires the `net`
49
+ * capability, so a host without it fails to load the plugin rather than
50
+ * failing mid-run.
51
+ */
52
+ declare const NotifierPort: Port<Notifier>;
53
+ //#endregion
54
+ //#region src/clients/fake-notifier.d.ts
55
+ /**
56
+ * The in-memory `Notifier`. Records what it is handed and touches no network.
57
+ *
58
+ * @returns a fresh notifier whose `sent` array starts empty and grows in
59
+ * dispatch order.
60
+ */
61
+ declare function createFakeNotifier(): FakeNotifier;
62
+ //#endregion
63
+ //#region src/clients/real-notifier.d.ts
64
+ /**
65
+ * The live `Notifier`. No Slack, webhook or email wiring in this build.
66
+ *
67
+ * @returns a notifier whose `send` throws, so the gap is a legible failure
68
+ * rather than a silent no-op.
69
+ * @throws {VennError} `VN8090` on every `send`.
70
+ */
71
+ declare function createRealNotifier(): Notifier;
72
+ //#endregion
73
+ //#region src/plugin.d.ts
74
+ /**
75
+ * The `@venn-lang/notify` plugin. Registers the `notify` namespace: the `slack`,
76
+ * `webhook` and `email` verbs and the nominal `notify.Receipt` type. Requires
77
+ * the `net` capability, since every verb dispatches over the network.
78
+ */
79
+ declare const notifyPlugin: PluginDefinition;
80
+ //#endregion
81
+ export { type FakeNotifier, type Notification, type NotificationKind, type Notifier, NotifierPort, type NotifyReceipt, createFakeNotifier, createRealNotifier, notifyPlugin as default, notifyPlugin, notifyActions };
82
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/actions/index.ts","../src/port/notifier.types.ts","../src/port/notifier.port.ts","../src/clients/fake-notifier.ts","../src/clients/real-notifier.ts","../src/plugin.ts"],"mappings":";;;;cAMa,eAAe;;;;KCLhB;;;;;;UAOK;EACf,MAAM;;EAEN;EACA;EACA;EACA;EACA;;;UAIe;;EAEf;;EAEA;;;UAIe;;;;;;;EAOf,KAAK,SAAS,eAAe,QAAQ;;;UAItB,qBAAqB;WAC3B,eAAe;;;;;;;;;cC/Bb,cAAc,KAAK;;;;;;;;;iBCAhB,sBAAsB;;;;;;;;;;iBCEtB,sBAAsB;;;;;;;;cCDzB,cAAc"}
package/dist/index.js ADDED
@@ -0,0 +1,134 @@
1
+ import { arg, defineAction, definePlugin, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { VennError } from "@venn-lang/contracts";
4
+ //#region src/port/notifier.port.ts
5
+ /**
6
+ * The port every `notify` verb dispatches through. Requires the `net`
7
+ * capability, so a host without it fails to load the plugin rather than
8
+ * failing mid-run.
9
+ */
10
+ const NotifierPort = {
11
+ id: "venn.port.notifier",
12
+ version: 1,
13
+ requires: ["net"],
14
+ methods: ["send"]
15
+ };
16
+ /**
17
+ * `notify.email "qa@example.com" { subject: "Nightly failed", body: report }`.
18
+ *
19
+ * Sends one email. Both `subject` and `body` are optional; an empty recipient
20
+ * is passed straight through to the notifier, which decides what to do with it.
21
+ *
22
+ * @returns a `notify.Receipt`.
23
+ */
24
+ const email = defineAction({
25
+ name: "email",
26
+ doc: "Send an email notification.",
27
+ params: z.object({
28
+ subject: z.string().optional(),
29
+ body: z.string().optional()
30
+ }).optional(),
31
+ args: [arg("to", t.string, "Who receives it.")],
32
+ result: t.ref("notify.Receipt"),
33
+ run: (ctx, input) => ctx.port(NotifierPort).send({
34
+ kind: "email",
35
+ channel: String(input.args[0] ?? ""),
36
+ subject: input.params?.subject,
37
+ body: input.params?.body
38
+ })
39
+ });
40
+ //#endregion
41
+ //#region src/actions/index.ts
42
+ /** Every verb in the `notify` namespace, in the order the plugin registers them. */
43
+ const notifyActions = [
44
+ defineAction({
45
+ name: "slack",
46
+ doc: "Send a Slack notification to a channel.",
47
+ params: z.object({ mention: z.string().optional() }).optional(),
48
+ args: [arg("channel", t.string, "Where to post: `#builds`, or a user.")],
49
+ result: t.ref("notify.Receipt"),
50
+ run: (ctx, input) => ctx.port(NotifierPort).send({
51
+ kind: "slack",
52
+ channel: String(input.args[0] ?? ""),
53
+ mention: input.params?.mention
54
+ })
55
+ }),
56
+ defineAction({
57
+ name: "webhook",
58
+ doc: "Send a webhook notification with a JSON body.",
59
+ params: z.object({ json: z.unknown().optional() }).optional(),
60
+ args: [arg("url", t.string, "Where to post the JSON body.")],
61
+ result: t.ref("notify.Receipt"),
62
+ run: (ctx, input) => ctx.port(NotifierPort).send({
63
+ kind: "webhook",
64
+ channel: String(input.args[0] ?? ""),
65
+ json: input.params?.json
66
+ })
67
+ }),
68
+ email
69
+ ];
70
+ //#endregion
71
+ //#region src/clients/fake-notifier.ts
72
+ /**
73
+ * The in-memory `Notifier`. Records what it is handed and touches no network.
74
+ *
75
+ * @returns a fresh notifier whose `sent` array starts empty and grows in
76
+ * dispatch order.
77
+ */
78
+ function createFakeNotifier() {
79
+ const sent = [];
80
+ return {
81
+ sent,
82
+ send: async (message) => {
83
+ sent.push(message);
84
+ return {
85
+ delivered: true,
86
+ id: `fake-${sent.length}`
87
+ };
88
+ }
89
+ };
90
+ }
91
+ //#endregion
92
+ //#region src/clients/real-notifier.ts
93
+ /**
94
+ * The live `Notifier`. No Slack, webhook or email wiring in this build.
95
+ *
96
+ * @returns a notifier whose `send` throws, so the gap is a legible failure
97
+ * rather than a silent no-op.
98
+ * @throws {VennError} `VN8090` on every `send`.
99
+ */
100
+ function createRealNotifier() {
101
+ return { send: async () => {
102
+ throw new VennError({
103
+ code: "VN8090",
104
+ message: "Notifier real client not implemented in this build"
105
+ });
106
+ } };
107
+ }
108
+ //#endregion
109
+ //#region src/plugin.ts
110
+ /**
111
+ * The `@venn-lang/notify` plugin. Registers the `notify` namespace: the `slack`,
112
+ * `webhook` and `email` verbs and the nominal `notify.Receipt` type. Requires
113
+ * the `net` capability, since every verb dispatches over the network.
114
+ */
115
+ const notifyPlugin = definePlugin({
116
+ name: "venn/notify",
117
+ version: "0.0.0",
118
+ namespace: "notify",
119
+ requires: ["net"],
120
+ actions: notifyActions,
121
+ typeDefs: {
122
+ /**
123
+ * What the notifier answers with once a message is on its way. `delivered`
124
+ * reports the dispatch, not the reading: nobody here knows whether it was seen.
125
+ */
126
+ Receipt: t.record({
127
+ delivered: t.bool,
128
+ id: t.string
129
+ }) }
130
+ });
131
+ //#endregion
132
+ export { NotifierPort, createFakeNotifier, createRealNotifier, notifyPlugin as default, notifyPlugin, notifyActions };
133
+
134
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["params","params"],"sources":["../src/port/notifier.port.ts","../src/actions/email.ts","../src/actions/slack.ts","../src/actions/webhook.ts","../src/actions/index.ts","../src/clients/fake-notifier.ts","../src/clients/real-notifier.ts","../src/types.ts","../src/plugin.ts"],"sourcesContent":["import type { Port } from \"@venn-lang/contracts\";\nimport type { Notifier } from \"./notifier.types.js\";\n\n/**\n * The port every `notify` verb dispatches through. Requires the `net`\n * capability, so a host without it fails to load the plugin rather than\n * failing mid-run.\n */\nexport const NotifierPort: Port<Notifier> = {\n id: \"venn.port.notifier\",\n version: 1,\n requires: [\"net\"],\n methods: [\"send\"],\n};\n","import { type ActionDefinition, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { NotifierPort } from \"../port/index.js\";\n\nconst params = z\n .object({\n subject: z.string().optional(),\n body: z.string().optional(),\n })\n .optional();\n\n/**\n * `notify.email \"qa@example.com\" { subject: \"Nightly failed\", body: report }`.\n *\n * Sends one email. Both `subject` and `body` are optional; an empty recipient\n * is passed straight through to the notifier, which decides what to do with it.\n *\n * @returns a `notify.Receipt`.\n */\nexport const email: ActionDefinition = defineAction({\n name: \"email\",\n doc: \"Send an email notification.\",\n params,\n args: [arg(\"to\", t.string, \"Who receives it.\")],\n result: t.ref(\"notify.Receipt\"),\n run: (ctx, input) =>\n ctx.port(NotifierPort).send({\n kind: \"email\",\n channel: String(input.args[0] ?? \"\"),\n subject: input.params?.subject,\n body: input.params?.body,\n }),\n});\n","import { type ActionDefinition, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { NotifierPort } from \"../port/index.js\";\n\nconst params = z.object({ mention: z.string().optional() }).optional();\n\n/**\n * `notify.slack \"#qa\" { mention: \"@oncall\" }`.\n *\n * Posts to a Slack channel or a user. `mention` pings someone alongside the post.\n *\n * @returns a `notify.Receipt`.\n */\nexport const slack: ActionDefinition = defineAction({\n name: \"slack\",\n doc: \"Send a Slack notification to a channel.\",\n params,\n args: [arg(\"channel\", t.string, \"Where to post: `#builds`, or a user.\")],\n result: t.ref(\"notify.Receipt\"),\n run: (ctx, input) =>\n ctx.port(NotifierPort).send({\n kind: \"slack\",\n channel: String(input.args[0] ?? \"\"),\n mention: input.params?.mention,\n }),\n});\n","import { type ActionDefinition, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { NotifierPort } from \"../port/index.js\";\n\nconst params = z.object({ json: z.unknown().optional() }).optional();\n\n/**\n * `notify.webhook \"https://hooks.example.com/build\" { json: { status: \"red\" } }`.\n *\n * Posts a JSON payload to a URL. The payload rides in `json` and is sent as is.\n *\n * @returns a `notify.Receipt`.\n */\nexport const webhook: ActionDefinition = defineAction({\n name: \"webhook\",\n doc: \"Send a webhook notification with a JSON body.\",\n params,\n args: [arg(\"url\", t.string, \"Where to post the JSON body.\")],\n result: t.ref(\"notify.Receipt\"),\n run: (ctx, input) =>\n ctx.port(NotifierPort).send({\n kind: \"webhook\",\n channel: String(input.args[0] ?? \"\"),\n json: input.params?.json,\n }),\n});\n","import type { ActionDefinition } from \"@venn-lang/sdk\";\nimport { email } from \"./email.js\";\nimport { slack } from \"./slack.js\";\nimport { webhook } from \"./webhook.js\";\n\n/** Every verb in the `notify` namespace, in the order the plugin registers them. */\nexport const notifyActions: ActionDefinition[] = [slack, webhook, email];\n","import type { FakeNotifier, Notification } from \"../port/index.js\";\n\n/**\n * The in-memory `Notifier`. Records what it is handed and touches no network.\n *\n * @returns a fresh notifier whose `sent` array starts empty and grows in\n * dispatch order.\n */\nexport function createFakeNotifier(): FakeNotifier {\n const sent: Notification[] = [];\n return {\n sent,\n send: async (message) => {\n sent.push(message);\n return { delivered: true, id: `fake-${sent.length}` };\n },\n };\n}\n","import { VennError } from \"@venn-lang/contracts\";\nimport type { Notifier } from \"../port/index.js\";\n\n/**\n * The live `Notifier`. No Slack, webhook or email wiring in this build.\n *\n * @returns a notifier whose `send` throws, so the gap is a legible failure\n * rather than a silent no-op.\n * @throws {VennError} `VN8090` on every `send`.\n */\nexport function createRealNotifier(): Notifier {\n return {\n send: async () => {\n throw new VennError({\n code: \"VN8090\",\n message: \"Notifier real client not implemented in this build\",\n });\n },\n };\n}\n","import { type TypeSpec, t } from \"@venn-lang/types\";\n\n/**\n * The types `@venn-lang/notify` publishes to the checker, under the `notify`\n * namespace. Kept as data, and mirroring `NotifyReceipt` in\n * `port/notifier.types.ts` by hand, so a generator reading the emitted `.d.ts`\n * can replace this file unnoticed. The name drops its prefix on the way in\n * because the namespace already says `notify`.\n */\nexport const notifyTypeDefs: Readonly<Record<string, TypeSpec>> = {\n /**\n * What the notifier answers with once a message is on its way. `delivered`\n * reports the dispatch, not the reading: nobody here knows whether it was seen.\n */\n Receipt: t.record({ delivered: t.bool, id: t.string }),\n};\n","import { definePlugin, type PluginDefinition } from \"@venn-lang/sdk\";\nimport { notifyActions } from \"./actions/index.js\";\nimport { notifyTypeDefs } from \"./types.js\";\n\n/**\n * The `@venn-lang/notify` plugin. Registers the `notify` namespace: the `slack`,\n * `webhook` and `email` verbs and the nominal `notify.Receipt` type. Requires\n * the `net` capability, since every verb dispatches over the network.\n */\nexport const notifyPlugin: PluginDefinition = definePlugin({\n name: \"venn/notify\",\n version: \"0.0.0\",\n namespace: \"notify\",\n requires: [\"net\"],\n actions: notifyActions,\n typeDefs: notifyTypeDefs,\n});\n\nexport { notifyPlugin as default };\n"],"mappings":";;;;;;;;;AAQA,MAAa,eAA+B;CAC1C,IAAI;CACJ,SAAS;CACT,UAAU,CAAC,KAAK;CAChB,SAAS,CAAC,MAAM;AAClB;;;;;;;;;ACMA,MAAa,QAA0B,aAAa;CAClD,MAAM;CACN,KAAK;CACL,QAlBa,EACZ,OAAO;EACN,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;EAC7B,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,CAAC,CAAC,CACD,SAaD;CACA,MAAM,CAAC,IAAI,MAAM,EAAE,QAAQ,kBAAkB,CAAC;CAC9C,QAAQ,EAAE,IAAI,gBAAgB;CAC9B,MAAM,KAAK,UACT,IAAI,KAAK,YAAY,CAAC,CAAC,KAAK;EAC1B,MAAM;EACN,SAAS,OAAO,MAAM,KAAK,MAAM,EAAE;EACnC,SAAS,MAAM,QAAQ;EACvB,MAAM,MAAM,QAAQ;CACtB,CAAC;AACL,CAAC;;;;AG1BD,MAAa,gBAAoC;CFOV,aAAa;EAClD,MAAM;EACN,KAAK;EACL,QAZa,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,SAY1D;EACA,MAAM,CAAC,IAAI,WAAW,EAAE,QAAQ,sCAAsC,CAAC;EACvE,QAAQ,EAAE,IAAI,gBAAgB;EAC9B,MAAM,KAAK,UACT,IAAI,KAAK,YAAY,CAAC,CAAC,KAAK;GAC1B,MAAM;GACN,SAAS,OAAO,MAAM,KAAK,MAAM,EAAE;GACnC,SAAS,MAAM,QAAQ;EACzB,CAAC;CACL,CEnBkD;CDOT,aAAa;EACpD,MAAM;EACN,KAAK;EACL,QAZa,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,SAYxD;EACA,MAAM,CAAC,IAAI,OAAO,EAAE,QAAQ,8BAA8B,CAAC;EAC3D,QAAQ,EAAE,IAAI,gBAAgB;EAC9B,MAAM,KAAK,UACT,IAAI,KAAK,YAAY,CAAC,CAAC,KAAK;GAC1B,MAAM;GACN,SAAS,OAAO,MAAM,KAAK,MAAM,EAAE;GACnC,MAAM,MAAM,QAAQ;EACtB,CAAC;CACL,CCnByD;CAAS;AAAK;;;;;;;;;ACEvE,SAAgB,qBAAmC;CACjD,MAAM,OAAuB,CAAC;CAC9B,OAAO;EACL;EACA,MAAM,OAAO,YAAY;GACvB,KAAK,KAAK,OAAO;GACjB,OAAO;IAAE,WAAW;IAAM,IAAI,QAAQ,KAAK;GAAS;EACtD;CACF;AACF;;;;;;;;;;ACPA,SAAgB,qBAA+B;CAC7C,OAAO,EACL,MAAM,YAAY;EAChB,MAAM,IAAI,UAAU;GAClB,MAAM;GACN,SAAS;EACX,CAAC;CACH,EACF;AACF;;;;;;;;AEVA,MAAa,eAAiC,aAAa;CACzD,MAAM;CACN,SAAS;CACT,WAAW;CACX,UAAU,CAAC,KAAK;CAChB,SAAS;CACT,UAAU;;;;;ADDV,SAAS,EAAE,OAAO;EAAE,WAAW,EAAE;EAAM,IAAI,EAAE;CAAO,CAAC,ECC3C;AACZ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@venn-lang/notify",
3
+ "version": "0.1.0",
4
+ "description": "The notify namespace: send a Slack message, a webhook or an email from a flow.",
5
+ "keywords": [
6
+ "venn",
7
+ "testing",
8
+ "e2e",
9
+ "notify"
10
+ ],
11
+ "homepage": "https://github.com/venn-lang/venn/tree/main/packages/std-notify#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-notify"
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,33 @@
1
+ import { type ActionDefinition, arg, defineAction, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { NotifierPort } from "../port/index.js";
4
+
5
+ const params = z
6
+ .object({
7
+ subject: z.string().optional(),
8
+ body: z.string().optional(),
9
+ })
10
+ .optional();
11
+
12
+ /**
13
+ * `notify.email "qa@example.com" { subject: "Nightly failed", body: report }`.
14
+ *
15
+ * Sends one email. Both `subject` and `body` are optional; an empty recipient
16
+ * is passed straight through to the notifier, which decides what to do with it.
17
+ *
18
+ * @returns a `notify.Receipt`.
19
+ */
20
+ export const email: ActionDefinition = defineAction({
21
+ name: "email",
22
+ doc: "Send an email notification.",
23
+ params,
24
+ args: [arg("to", t.string, "Who receives it.")],
25
+ result: t.ref("notify.Receipt"),
26
+ run: (ctx, input) =>
27
+ ctx.port(NotifierPort).send({
28
+ kind: "email",
29
+ channel: String(input.args[0] ?? ""),
30
+ subject: input.params?.subject,
31
+ body: input.params?.body,
32
+ }),
33
+ });
@@ -0,0 +1,7 @@
1
+ import type { ActionDefinition } from "@venn-lang/sdk";
2
+ import { email } from "./email.js";
3
+ import { slack } from "./slack.js";
4
+ import { webhook } from "./webhook.js";
5
+
6
+ /** Every verb in the `notify` namespace, in the order the plugin registers them. */
7
+ export const notifyActions: ActionDefinition[] = [slack, webhook, email];
@@ -0,0 +1,26 @@
1
+ import { type ActionDefinition, arg, defineAction, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { NotifierPort } from "../port/index.js";
4
+
5
+ const params = z.object({ mention: z.string().optional() }).optional();
6
+
7
+ /**
8
+ * `notify.slack "#qa" { mention: "@oncall" }`.
9
+ *
10
+ * Posts to a Slack channel or a user. `mention` pings someone alongside the post.
11
+ *
12
+ * @returns a `notify.Receipt`.
13
+ */
14
+ export const slack: ActionDefinition = defineAction({
15
+ name: "slack",
16
+ doc: "Send a Slack notification to a channel.",
17
+ params,
18
+ args: [arg("channel", t.string, "Where to post: `#builds`, or a user.")],
19
+ result: t.ref("notify.Receipt"),
20
+ run: (ctx, input) =>
21
+ ctx.port(NotifierPort).send({
22
+ kind: "slack",
23
+ channel: String(input.args[0] ?? ""),
24
+ mention: input.params?.mention,
25
+ }),
26
+ });
@@ -0,0 +1,26 @@
1
+ import { type ActionDefinition, arg, defineAction, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { NotifierPort } from "../port/index.js";
4
+
5
+ const params = z.object({ json: z.unknown().optional() }).optional();
6
+
7
+ /**
8
+ * `notify.webhook "https://hooks.example.com/build" { json: { status: "red" } }`.
9
+ *
10
+ * Posts a JSON payload to a URL. The payload rides in `json` and is sent as is.
11
+ *
12
+ * @returns a `notify.Receipt`.
13
+ */
14
+ export const webhook: ActionDefinition = defineAction({
15
+ name: "webhook",
16
+ doc: "Send a webhook notification with a JSON body.",
17
+ params,
18
+ args: [arg("url", t.string, "Where to post the JSON body.")],
19
+ result: t.ref("notify.Receipt"),
20
+ run: (ctx, input) =>
21
+ ctx.port(NotifierPort).send({
22
+ kind: "webhook",
23
+ channel: String(input.args[0] ?? ""),
24
+ json: input.params?.json,
25
+ }),
26
+ });
@@ -0,0 +1,18 @@
1
+ import type { FakeNotifier, Notification } from "../port/index.js";
2
+
3
+ /**
4
+ * The in-memory `Notifier`. Records what it is handed and touches no network.
5
+ *
6
+ * @returns a fresh notifier whose `sent` array starts empty and grows in
7
+ * dispatch order.
8
+ */
9
+ export function createFakeNotifier(): FakeNotifier {
10
+ const sent: Notification[] = [];
11
+ return {
12
+ sent,
13
+ send: async (message) => {
14
+ sent.push(message);
15
+ return { delivered: true, id: `fake-${sent.length}` };
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,2 @@
1
+ export { createFakeNotifier } from "./fake-notifier.js";
2
+ export { createRealNotifier } from "./real-notifier.js";
@@ -0,0 +1,20 @@
1
+ import { VennError } from "@venn-lang/contracts";
2
+ import type { Notifier } from "../port/index.js";
3
+
4
+ /**
5
+ * The live `Notifier`. No Slack, webhook or email wiring in this build.
6
+ *
7
+ * @returns a notifier whose `send` throws, so the gap is a legible failure
8
+ * rather than a silent no-op.
9
+ * @throws {VennError} `VN8090` on every `send`.
10
+ */
11
+ export function createRealNotifier(): Notifier {
12
+ return {
13
+ send: async () => {
14
+ throw new VennError({
15
+ code: "VN8090",
16
+ message: "Notifier real client not implemented in this build",
17
+ });
18
+ },
19
+ };
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `@venn-lang/notify`: the verbs that tell someone what a run did (`slack`,
3
+ * `webhook`, `email`) and the `Notifier` port they dispatch through.
4
+ */
5
+
6
+ export * from "./actions/index.js";
7
+ export * from "./clients/index.js";
8
+ export { notifyPlugin, notifyPlugin as default } from "./plugin.js";
9
+ export * from "./port/index.js";
package/src/plugin.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { definePlugin, type PluginDefinition } from "@venn-lang/sdk";
2
+ import { notifyActions } from "./actions/index.js";
3
+ import { notifyTypeDefs } from "./types.js";
4
+
5
+ /**
6
+ * The `@venn-lang/notify` plugin. Registers the `notify` namespace: the `slack`,
7
+ * `webhook` and `email` verbs and the nominal `notify.Receipt` type. Requires
8
+ * the `net` capability, since every verb dispatches over the network.
9
+ */
10
+ export const notifyPlugin: PluginDefinition = definePlugin({
11
+ name: "venn/notify",
12
+ version: "0.0.0",
13
+ namespace: "notify",
14
+ requires: ["net"],
15
+ actions: notifyActions,
16
+ typeDefs: notifyTypeDefs,
17
+ });
18
+
19
+ export { notifyPlugin as default };
@@ -0,0 +1,8 @@
1
+ export { NotifierPort } from "./notifier.port.js";
2
+ export type {
3
+ FakeNotifier,
4
+ Notification,
5
+ NotificationKind,
6
+ Notifier,
7
+ NotifyReceipt,
8
+ } from "./notifier.types.js";
@@ -0,0 +1,14 @@
1
+ import type { Port } from "@venn-lang/contracts";
2
+ import type { Notifier } from "./notifier.types.js";
3
+
4
+ /**
5
+ * The port every `notify` verb dispatches through. Requires the `net`
6
+ * capability, so a host without it fails to load the plugin rather than
7
+ * failing mid-run.
8
+ */
9
+ export const NotifierPort: Port<Notifier> = {
10
+ id: "venn.port.notifier",
11
+ version: 1,
12
+ requires: ["net"],
13
+ methods: ["send"],
14
+ };
@@ -0,0 +1,41 @@
1
+ /** The kind of channel a notification targets. */
2
+ export type NotificationKind = "slack" | "webhook" | "email";
3
+
4
+ /**
5
+ * One message handed to the `Notifier`. `kind` picks the channel type and
6
+ * decides which of the optional fields carry meaning: `subject` and `body` for
7
+ * email, `mention` for Slack, `json` for a webhook.
8
+ */
9
+ export interface Notification {
10
+ kind: NotificationKind;
11
+ /** Where it goes, read in the terms of its `kind`: a Slack channel, a URL, an address. */
12
+ channel: string;
13
+ subject?: string;
14
+ body?: string;
15
+ mention?: string;
16
+ json?: unknown;
17
+ }
18
+
19
+ /** What a `Notifier` answers with once a message is on its way. */
20
+ export interface NotifyReceipt {
21
+ /** The dispatch succeeded. Says nothing about whether anyone read it. */
22
+ delivered: boolean;
23
+ /** The notifier's own handle for the message. */
24
+ id: string;
25
+ }
26
+
27
+ /** The contract every notification goes through, whatever the channel. */
28
+ export interface Notifier {
29
+ /**
30
+ * Dispatches one message.
31
+ *
32
+ * @returns the receipt for the dispatch.
33
+ * @throws {VennError} `VN8090` from an implementation that cannot deliver.
34
+ */
35
+ send(message: Notification): Promise<NotifyReceipt>;
36
+ }
37
+
38
+ /** A `Notifier` that keeps every sent notification in memory, for assertions. */
39
+ export interface FakeNotifier extends Notifier {
40
+ readonly sent: readonly Notification[];
41
+ }
package/src/types.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type TypeSpec, t } from "@venn-lang/types";
2
+
3
+ /**
4
+ * The types `@venn-lang/notify` publishes to the checker, under the `notify`
5
+ * namespace. Kept as data, and mirroring `NotifyReceipt` in
6
+ * `port/notifier.types.ts` by hand, so a generator reading the emitted `.d.ts`
7
+ * can replace this file unnoticed. The name drops its prefix on the way in
8
+ * because the namespace already says `notify`.
9
+ */
10
+ export const notifyTypeDefs: Readonly<Record<string, TypeSpec>> = {
11
+ /**
12
+ * What the notifier answers with once a message is on its way. `delivered`
13
+ * reports the dispatch, not the reading: nobody here knows whether it was seen.
14
+ */
15
+ Receipt: t.record({ delivered: t.bool, id: t.string }),
16
+ };