@venn-lang/contracts 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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +254 -0
  3. package/dist/index.d.ts +620 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +1342 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/node.d.mts +287 -0
  8. package/dist/node.d.mts.map +1 -0
  9. package/dist/node.mjs +387 -0
  10. package/dist/node.mjs.map +1 -0
  11. package/dist/signal-source.types-BI3TYaCi.d.ts +317 -0
  12. package/dist/signal-source.types-BI3TYaCi.d.ts.map +1 -0
  13. package/dist/testing.d.ts +84 -0
  14. package/dist/testing.d.ts.map +1 -0
  15. package/dist/testing.js +276 -0
  16. package/dist/testing.js.map +1 -0
  17. package/package.json +77 -0
  18. package/src/capabilities/host-capability.ts +18 -0
  19. package/src/capabilities/index.ts +2 -0
  20. package/src/conformance/expect-venn-error.ts +18 -0
  21. package/src/conformance/harness.types.ts +14 -0
  22. package/src/conformance/index.ts +2 -0
  23. package/src/dotenv/dotenv-files.ts +30 -0
  24. package/src/dotenv/index.ts +2 -0
  25. package/src/dotenv/parse-dotenv.ts +34 -0
  26. package/src/errors/contract-codes.ts +37 -0
  27. package/src/errors/index.ts +7 -0
  28. package/src/errors/venn-error.ts +20 -0
  29. package/src/host/create-host.ts +16 -0
  30. package/src/host/create-node-host.ts +29 -0
  31. package/src/host/create-test-host.ts +28 -0
  32. package/src/host/create-worker-host.ts +32 -0
  33. package/src/host/host.types.ts +26 -0
  34. package/src/host/index.ts +5 -0
  35. package/src/host/unavailable.ts +23 -0
  36. package/src/index.ts +10 -0
  37. package/src/logger/console-logger.ts +12 -0
  38. package/src/logger/index.ts +3 -0
  39. package/src/logger/logger.types.ts +18 -0
  40. package/src/logger/memory-logger.ts +12 -0
  41. package/src/node.ts +8 -0
  42. package/src/port/assert-capabilities.ts +18 -0
  43. package/src/port/assert-port-shape.ts +18 -0
  44. package/src/port/bind-port.ts +28 -0
  45. package/src/port/index.ts +5 -0
  46. package/src/port/missing-capabilities.ts +14 -0
  47. package/src/port/port.types.ts +36 -0
  48. package/src/ports/clock/clock.port.ts +15 -0
  49. package/src/ports/clock/clock.types.ts +17 -0
  50. package/src/ports/clock/index.ts +4 -0
  51. package/src/ports/clock/system-clock.ts +9 -0
  52. package/src/ports/clock/virtual-clock.ts +22 -0
  53. package/src/ports/console/console.port.ts +13 -0
  54. package/src/ports/console/console.types.ts +17 -0
  55. package/src/ports/console/index.ts +3 -0
  56. package/src/ports/console/memory-console.ts +36 -0
  57. package/src/ports/console/node-console.ts +80 -0
  58. package/src/ports/file-system/file-system.errors.ts +10 -0
  59. package/src/ports/file-system/file-system.port.ts +12 -0
  60. package/src/ports/file-system/file-system.types.ts +27 -0
  61. package/src/ports/file-system/index.ts +6 -0
  62. package/src/ports/file-system/memory-fs.ts +46 -0
  63. package/src/ports/file-system/node-fs.ts +93 -0
  64. package/src/ports/index.ts +9 -0
  65. package/src/ports/lock-provider/fake-lock.ts +6 -0
  66. package/src/ports/lock-provider/in-process-lock.ts +26 -0
  67. package/src/ports/lock-provider/index.ts +4 -0
  68. package/src/ports/lock-provider/lock-provider.port.ts +16 -0
  69. package/src/ports/lock-provider/lock-provider.types.ts +8 -0
  70. package/src/ports/manifest-provider/default-manifest.ts +31 -0
  71. package/src/ports/manifest-provider/edit/edit-dependency.ts +82 -0
  72. package/src/ports/manifest-provider/edit/index.ts +7 -0
  73. package/src/ports/manifest-provider/edit/table-span.ts +39 -0
  74. package/src/ports/manifest-provider/index.ts +31 -0
  75. package/src/ports/manifest-provider/manifest.port.ts +16 -0
  76. package/src/ports/manifest-provider/manifest.types.ts +54 -0
  77. package/src/ports/manifest-provider/memory-manifest.ts +14 -0
  78. package/src/ports/manifest-provider/project.types.ts +94 -0
  79. package/src/ports/manifest-provider/read/index.ts +15 -0
  80. package/src/ports/manifest-provider/read/read-dependencies.ts +27 -0
  81. package/src/ports/manifest-provider/read/read-package.ts +32 -0
  82. package/src/ports/manifest-provider/read/read-profiles.ts +32 -0
  83. package/src/ports/manifest-provider/read/read-targets.ts +37 -0
  84. package/src/ports/manifest-provider/read/read-tooling.ts +15 -0
  85. package/src/ports/manifest-provider/read/read-workspace.ts +22 -0
  86. package/src/ports/manifest-provider/read/scalars.ts +35 -0
  87. package/src/ports/manifest-provider/read-run-settings.ts +38 -0
  88. package/src/ports/manifest-provider/resolve-alias.ts +22 -0
  89. package/src/ports/manifest-provider/toml/index.ts +3 -0
  90. package/src/ports/manifest-provider/toml/parse-toml.ts +52 -0
  91. package/src/ports/manifest-provider/toml/read-value.ts +104 -0
  92. package/src/ports/manifest-provider/toml/sections.ts +39 -0
  93. package/src/ports/manifest-provider/toml-docs.ts +27 -0
  94. package/src/ports/manifest-provider/toml-manifest.ts +41 -0
  95. package/src/ports/process-provider/fake-process.ts +32 -0
  96. package/src/ports/process-provider/index.ts +10 -0
  97. package/src/ports/process-provider/node-spawn.ts +52 -0
  98. package/src/ports/process-provider/process-provider.port.ts +13 -0
  99. package/src/ports/process-provider/process-provider.types.ts +47 -0
  100. package/src/ports/random/fixed-random.ts +10 -0
  101. package/src/ports/random/index.ts +4 -0
  102. package/src/ports/random/random.port.ts +13 -0
  103. package/src/ports/random/random.types.ts +10 -0
  104. package/src/ports/random/seeded-random.ts +19 -0
  105. package/src/ports/secret-provider/env-secrets.ts +19 -0
  106. package/src/ports/secret-provider/index.ts +6 -0
  107. package/src/ports/secret-provider/memory-secrets.ts +15 -0
  108. package/src/ports/secret-provider/secret-provider.port.ts +13 -0
  109. package/src/ports/secret-provider/secret-provider.types.ts +17 -0
  110. package/src/ports/secret-provider/secret.ts +17 -0
  111. package/src/ports/signal-source/fake-signals.ts +44 -0
  112. package/src/ports/signal-source/index.ts +11 -0
  113. package/src/ports/signal-source/node-signals.ts +33 -0
  114. package/src/ports/signal-source/signal-source.port.ts +13 -0
  115. package/src/ports/signal-source/signal-source.types.ts +24 -0
  116. package/src/testing.ts +16 -0
@@ -0,0 +1,52 @@
1
+ import { cursor, readValue } from "./read-value.js";
2
+ import { enterSection, enterTableArray } from "./sections.js";
3
+
4
+ /**
5
+ * A TOML reader for `venn.toml`: sections, nested `[a.b]`, arrays of tables
6
+ * (`[[bin]]`), `key = value`, strings, numbers, bools, arrays and inline tables.
7
+ *
8
+ * Not a full TOML parser. No dates, no multi-line strings, no dotted keys.
9
+ * `venn.toml` is our own format, so the subset is a decision rather than a
10
+ * shortfall.
11
+ *
12
+ * @returns the root table. Malformed lines are skipped, never thrown on.
13
+ */
14
+ export function parseToml(content: string): Record<string, unknown> {
15
+ const root: Record<string, unknown> = {};
16
+ let section = root;
17
+ for (const raw of content.split(/\r?\n/)) {
18
+ const line = stripComment(raw).trim();
19
+ if (line === "") continue;
20
+ if (line.startsWith("[")) section = openSection(root, line);
21
+ else assign(section, line);
22
+ }
23
+ return root;
24
+ }
25
+
26
+ /** `[[bin]]` appends another table, `[bin]` opens the one and only. */
27
+ function openSection(root: Record<string, unknown>, line: string): Record<string, unknown> {
28
+ if (line.startsWith("[[")) {
29
+ return enterTableArray(root, line.slice(2, line.indexOf("]]")));
30
+ }
31
+ return enterSection(root, line.slice(1, line.indexOf("]")));
32
+ }
33
+
34
+ function stripComment(line: string): string {
35
+ let quote: string | undefined;
36
+ for (let i = 0; i < line.length; i++) {
37
+ const ch = line[i];
38
+ if (ch === '"' || ch === "'") quote = quote === ch ? undefined : (quote ?? ch);
39
+ else if (ch === "#" && !quote) return line.slice(0, i);
40
+ }
41
+ return line;
42
+ }
43
+
44
+ function assign(section: Record<string, unknown>, line: string): void {
45
+ const eq = line.indexOf("=");
46
+ if (eq < 0) return;
47
+ const key = line
48
+ .slice(0, eq)
49
+ .trim()
50
+ .replace(/^["']|["']$/g, "");
51
+ section[key] = readValue(cursor(line.slice(eq + 1)));
52
+ }
@@ -0,0 +1,104 @@
1
+ /** A position in the text being read. Mutable, because reading advances it. */
2
+ export interface Cursor {
3
+ readonly text: string;
4
+ index: number;
5
+ }
6
+
7
+ /** A cursor over `text`, positioned at the start. */
8
+ export function cursor(text: string): Cursor {
9
+ return { text, index: 0 };
10
+ }
11
+
12
+ /**
13
+ * One TOML value: a string, a number, a bool, an array, or an inline table.
14
+ *
15
+ * Reads by walking rather than by splitting, because a comma can sit inside a
16
+ * value: splitting turns `["a,b"]` into two items and leaves
17
+ * `{ version = "^4" }` as the text it was written in.
18
+ */
19
+ export function readValue(cur: Cursor): unknown {
20
+ skipSpace(cur);
21
+ const ch = cur.text[cur.index];
22
+ if (ch === '"' || ch === "'") return readString(cur);
23
+ if (ch === "[") return readArray(cur);
24
+ if (ch === "{") return readInlineTable(cur);
25
+ return readBare(cur);
26
+ }
27
+
28
+ export function skipSpace(cur: Cursor): void {
29
+ while (/\s/.test(cur.text[cur.index] ?? "")) cur.index++;
30
+ }
31
+
32
+ /** A basic string reads escapes; a literal one, in single quotes, does not. */
33
+ function readString(cur: Cursor): string {
34
+ const quote = cur.text[cur.index];
35
+ cur.index++;
36
+ let out = "";
37
+ while (cur.index < cur.text.length && cur.text[cur.index] !== quote) {
38
+ if (quote === '"' && cur.text[cur.index] === "\\") out += unescaped(cur);
39
+ else out += cur.text[cur.index++];
40
+ }
41
+ cur.index++;
42
+ return out;
43
+ }
44
+
45
+ const ESCAPES: Record<string, string> = { n: "\n", t: "\t", r: "\r", "\\": "\\", '"': '"' };
46
+
47
+ function unescaped(cur: Cursor): string {
48
+ const code = cur.text[cur.index + 1] ?? "";
49
+ cur.index += 2;
50
+ return ESCAPES[code] ?? code;
51
+ }
52
+
53
+ function readArray(cur: Cursor): unknown[] {
54
+ cur.index++;
55
+ const out: unknown[] = [];
56
+ while (!atClose(cur, "]")) {
57
+ out.push(readValue(cur));
58
+ skipSeparator(cur);
59
+ }
60
+ cur.index++;
61
+ return out;
62
+ }
63
+
64
+ function readInlineTable(cur: Cursor): Record<string, unknown> {
65
+ cur.index++;
66
+ const out: Record<string, unknown> = {};
67
+ while (!atClose(cur, "}")) {
68
+ const key = readKey(cur);
69
+ if (key === undefined) break;
70
+ out[key] = readValue(cur);
71
+ skipSeparator(cur);
72
+ }
73
+ cur.index++;
74
+ return out;
75
+ }
76
+
77
+ function readKey(cur: Cursor): string | undefined {
78
+ skipSpace(cur);
79
+ const equals = cur.text.indexOf("=", cur.index);
80
+ if (equals < 0) return undefined;
81
+ const raw = cur.text.slice(cur.index, equals).trim();
82
+ cur.index = equals + 1;
83
+ return raw.replace(/^["']|["']$/g, "");
84
+ }
85
+
86
+ function atClose(cur: Cursor, close: string): boolean {
87
+ skipSpace(cur);
88
+ return cur.index >= cur.text.length || cur.text[cur.index] === close;
89
+ }
90
+
91
+ function skipSeparator(cur: Cursor): void {
92
+ skipSpace(cur);
93
+ if (cur.text[cur.index] === ",") cur.index++;
94
+ }
95
+
96
+ /** Anything unquoted: `true`, `12`, `1.5`, or a word the manifest gives meaning. */
97
+ function readBare(cur: Cursor): unknown {
98
+ const start = cur.index;
99
+ while (cur.index < cur.text.length && !",]}".includes(cur.text[cur.index] ?? "")) cur.index++;
100
+ const raw = cur.text.slice(start, cur.index).trim();
101
+ if (raw === "true" || raw === "false") return raw === "true";
102
+ const num = Number(raw);
103
+ return raw !== "" && !Number.isNaN(num) ? num : raw;
104
+ }
@@ -0,0 +1,39 @@
1
+ // Opening `[a.b]` or `[[a.b]]`: working out where the keys that follow go.
2
+
3
+ /** The table `[path]` names, created along the way if it is not there yet. */
4
+ export function enterSection(root: Record<string, unknown>, path: string): Record<string, unknown> {
5
+ let node = root;
6
+ for (const key of keysOf(path)) {
7
+ node[key] ??= {};
8
+ node = node[key] as Record<string, unknown>;
9
+ }
10
+ return node;
11
+ }
12
+
13
+ /**
14
+ * `[[bin]]`: one more of something there may be several of.
15
+ *
16
+ * Returns the table just appended, so two `[[bin]]` sections give two bins
17
+ * rather than one bin written twice.
18
+ */
19
+ export function enterTableArray(
20
+ root: Record<string, unknown>,
21
+ path: string,
22
+ ): Record<string, unknown> {
23
+ const keys = keysOf(path);
24
+ const last = keys.pop();
25
+ if (!last) return root;
26
+ const parent = enterSection(root, keys.join("."));
27
+ const found = Array.isArray(parent[last]) ? (parent[last] as unknown[]) : [];
28
+ parent[last] = found;
29
+ const table: Record<string, unknown> = {};
30
+ found.push(table);
31
+ return table;
32
+ }
33
+
34
+ function keysOf(path: string): string[] {
35
+ return path
36
+ .split(".")
37
+ .map((part) => part.trim().replace(/^["']|["']$/g, ""))
38
+ .filter((part) => part !== "");
39
+ }
@@ -0,0 +1,27 @@
1
+ const COMMENT = /^\s*#\s?(.*)$/;
2
+ const KEY = /^\s*([A-Za-z_][\w-]*)\s*=/;
3
+
4
+ /**
5
+ * The comment block written directly above a key, read as that key's
6
+ * documentation. The same idea as `##` above a declaration in a `.vn`.
7
+ *
8
+ * A blank line breaks the block, so a comment separated from a key belongs to
9
+ * nobody and is never attributed to the next one down.
10
+ *
11
+ * @returns documentation keyed by the key it sits above.
12
+ */
13
+ export function tomlDocs(content: string): Record<string, string> {
14
+ const docs: Record<string, string> = {};
15
+ let block: string[] = [];
16
+ for (const line of content.split(/\r?\n/)) {
17
+ const comment = COMMENT.exec(line);
18
+ if (comment) {
19
+ block.push(comment[1] ?? "");
20
+ continue;
21
+ }
22
+ const key = KEY.exec(line)?.[1];
23
+ if (key && block.length > 0) docs[key] = block.join("\n").trim();
24
+ block = [];
25
+ }
26
+ return docs;
27
+ }
@@ -0,0 +1,41 @@
1
+ import type { Manifest, ManifestProvider } from "./manifest.types.js";
2
+ import {
3
+ readDependencies,
4
+ readPackage,
5
+ readProfiles,
6
+ readTargets,
7
+ readTooling,
8
+ readWorkspace,
9
+ } from "./read/index.js";
10
+ import { readRunSettings } from "./read-run-settings.js";
11
+ import { parseToml } from "./toml/index.js";
12
+
13
+ /**
14
+ * The real one: parse `venn.toml` into a {@link Manifest}.
15
+ *
16
+ * Parsing happens once, at construction, so repeated `load()` calls are free
17
+ * and always agree.
18
+ *
19
+ * @param args.content - the manifest as written.
20
+ */
21
+ export function createTomlManifest(args: { content: string }): ManifestProvider {
22
+ const manifest = toManifest(parseToml(args.content));
23
+ return { load: () => manifest };
24
+ }
25
+
26
+ function toManifest(data: Record<string, unknown>): Manifest {
27
+ const pkg = readPackage(data);
28
+ return {
29
+ name: pkg.name,
30
+ version: pkg.version ?? "0.0.0",
31
+ package: pkg,
32
+ targets: readTargets(data, pkg.name),
33
+ dependencies: readDependencies(data.dependencies),
34
+ devDependencies: readDependencies(data["dev-dependencies"]),
35
+ patch: readDependencies(data.patch),
36
+ profiles: readProfiles(data),
37
+ tooling: readTooling(data),
38
+ workspace: readWorkspace(data),
39
+ ...readRunSettings(data),
40
+ };
41
+ }
@@ -0,0 +1,32 @@
1
+ import type { ProcessHandle, ProcessProvider } from "./process-provider.types.js";
2
+
3
+ /**
4
+ * The double: a scripted process that never touches the OS.
5
+ *
6
+ * Output is streamed before the wait resolves, exactly as the real one does it,
7
+ * so a caller that shows progress is exercised here and not only against a real
8
+ * machine.
9
+ *
10
+ * @param args.exitCode - what every run reports. Defaults to 0.
11
+ * @param args.output - what every run writes. Defaults to nothing.
12
+ */
13
+ export function createFakeProcess(
14
+ args: { exitCode?: number; output?: string } = {},
15
+ ): ProcessProvider {
16
+ const code = args.exitCode ?? 0;
17
+ const output = args.output ?? "";
18
+ return {
19
+ spawn: (spawned) => {
20
+ if (output !== "") spawned.onOutput?.(output);
21
+ return handle(code, output);
22
+ },
23
+ };
24
+ }
25
+
26
+ function handle(code: number, output: string): ProcessHandle {
27
+ return {
28
+ pid: 0,
29
+ wait: async () => ({ code, output }),
30
+ kill: () => {},
31
+ };
32
+ }
@@ -0,0 +1,10 @@
1
+ export { createFakeProcess } from "./fake-process.js";
2
+ export { ProcessProviderPort } from "./process-provider.port.js";
3
+ export type {
4
+ ProcessHandle,
5
+ ProcessProvider,
6
+ ProcessResult,
7
+ SpawnArgs,
8
+ } from "./process-provider.types.js";
9
+ // node-spawn is deliberately absent: like node-fs it lives behind
10
+ // @venn-lang/contracts/node, so this barrel stays Worker-safe.
@@ -0,0 +1,52 @@
1
+ import { type ChildProcess, spawn } from "node:child_process";
2
+ import type {
3
+ ProcessHandle,
4
+ ProcessProvider,
5
+ ProcessResult,
6
+ SpawnArgs,
7
+ } from "./process-provider.types.js";
8
+
9
+ /**
10
+ * The real one, on `node:child_process`.
11
+ *
12
+ * Output is streamed to whoever asked and kept at the same time: a package
13
+ * manager needs watching while it works and reading once it is done, and only
14
+ * one of the two would leave either a command that looks hung or a result
15
+ * nobody can inspect.
16
+ */
17
+ export function createNodeSpawn(): ProcessProvider {
18
+ return { spawn: (args) => start(args) };
19
+ }
20
+
21
+ function start(args: SpawnArgs): ProcessHandle {
22
+ const child = spawn(args.command, [...(args.args ?? [])], {
23
+ cwd: args.cwd,
24
+ env: args.env ? { ...process.env, ...args.env } : process.env,
25
+ shell: args.shell ?? false,
26
+ });
27
+ const chunks: string[] = [];
28
+ listen(child, chunks, args.onOutput);
29
+ return { pid: child.pid ?? 0, wait: () => ended(child, chunks), kill: () => void child.kill() };
30
+ }
31
+
32
+ function listen(child: ChildProcess, into: string[], onOutput?: (chunk: string) => void): void {
33
+ for (const stream of [child.stdout, child.stderr]) {
34
+ stream?.on("data", (chunk: Buffer) => {
35
+ const text = chunk.toString("utf8");
36
+ into.push(text);
37
+ onOutput?.(text);
38
+ });
39
+ }
40
+ }
41
+
42
+ /**
43
+ * A command that could never start reads as one that ran and failed, with the
44
+ * reason as its output, because that is what a caller can act on. `127` is what
45
+ * a shell says for "no such command", and it means the same here.
46
+ */
47
+ function ended(child: ChildProcess, chunks: string[]): Promise<ProcessResult> {
48
+ return new Promise((resolve) => {
49
+ child.on("error", (err) => resolve({ code: 127, output: `${chunks.join("")}${err.message}` }));
50
+ child.on("close", (code) => resolve({ code: code ?? 0, output: chunks.join("") }));
51
+ });
52
+ }
@@ -0,0 +1,13 @@
1
+ import type { Port } from "../../port/index.js";
2
+ import type { ProcessProvider } from "./process-provider.types.js";
3
+
4
+ /**
5
+ * The {@link ProcessProvider} contract. Implementations: `node-spawn`,
6
+ * `fake-process`.
7
+ */
8
+ export const ProcessProviderPort: Port<ProcessProvider> = {
9
+ id: "venn.port.process",
10
+ version: 1,
11
+ requires: ["process"],
12
+ methods: ["spawn"],
13
+ };
@@ -0,0 +1,47 @@
1
+ /** What to run, where, and how to hear from it while it runs. */
2
+ export interface SpawnArgs {
3
+ command: string;
4
+ args?: readonly string[];
5
+ /** Where it runs. The current directory unless this says otherwise. */
6
+ cwd?: string;
7
+ /** Added to the environment the host was started with. */
8
+ env?: Record<string, string>;
9
+ /**
10
+ * Whether to go through the platform shell.
11
+ *
12
+ * Needed for a command that is a script rather than an executable: on Windows
13
+ * `pnpm` is a `.cmd`, and nothing runs it without one. Off by default,
14
+ * because a shell re-reads the arguments and a quote that meant one thing to
15
+ * the caller means another to `cmd.exe`. Only the caller knows which of the
16
+ * two it is holding.
17
+ */
18
+ shell?: boolean;
19
+ /**
20
+ * Called with each chunk the command writes, as it writes it.
21
+ *
22
+ * A package manager installing a hundred packages takes half a minute, and a
23
+ * command silent for half a minute reads as one that has hung. What is
24
+ * streamed here is kept as well, so a caller can show it and read it after.
25
+ */
26
+ onOutput?: (chunk: string) => void;
27
+ }
28
+
29
+ /** How a run ended. */
30
+ export interface ProcessResult {
31
+ code: number;
32
+ /** Everything it wrote, stdout and stderr together, in the order written. */
33
+ output: string;
34
+ }
35
+
36
+ /** A spawned subprocess. */
37
+ export interface ProcessHandle {
38
+ readonly pid: number;
39
+ /** Resolves when the command ends. Never rejects: a failure is a result. */
40
+ wait(): Promise<ProcessResult>;
41
+ kill(): void;
42
+ }
43
+
44
+ /** Spawn subprocesses: a package manager, a driver, anything the host allows. */
45
+ export interface ProcessProvider {
46
+ spawn(args: SpawnArgs): ProcessHandle;
47
+ }
@@ -0,0 +1,10 @@
1
+ import type { Random } from "./random.types.js";
2
+
3
+ /** The double: always the same value, and always `min` from `int`. */
4
+ export function createFixedRandom(args: { value?: number } = {}): Random {
5
+ const value = args.value ?? 0;
6
+ return {
7
+ next: () => value,
8
+ int: (min) => min,
9
+ };
10
+ }
@@ -0,0 +1,4 @@
1
+ export { createFixedRandom } from "./fixed-random.js";
2
+ export { RandomPort } from "./random.port.js";
3
+ export type { Random } from "./random.types.js";
4
+ export { createSeededRandom } from "./seeded-random.js";
@@ -0,0 +1,13 @@
1
+ import type { Port } from "../../port/index.js";
2
+ import type { Random } from "./random.types.js";
3
+
4
+ /**
5
+ * The {@link Random} contract. Implementations: `seeded-random`,
6
+ * `fixed-random`.
7
+ */
8
+ export const RandomPort: Port<Random> = {
9
+ id: "venn.port.random",
10
+ version: 1,
11
+ requires: ["random"],
12
+ methods: ["next", "int"],
13
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A source of randomness. Every implementation is reproducible, so a failing
3
+ * run can be replayed.
4
+ */
5
+ export interface Random {
6
+ /** Uniform float in [0, 1). */
7
+ next(): number;
8
+ /** Uniform integer in the inclusive range [min, max]. */
9
+ int(min: number, max: number): number;
10
+ }
@@ -0,0 +1,19 @@
1
+ import type { Random } from "./random.types.js";
2
+
3
+ /**
4
+ * The real one: a mulberry32 PRNG, seeded once per worker. Same seed, same
5
+ * sequence, so a run reproduces.
6
+ */
7
+ export function createSeededRandom(args: { seed: number }): Random {
8
+ let state = args.seed >>> 0;
9
+ const next = (): number => {
10
+ state = (state + 0x6d2b79f5) | 0;
11
+ let t = Math.imul(state ^ (state >>> 15), 1 | state);
12
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
13
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
14
+ };
15
+ return {
16
+ next,
17
+ int: (min, max) => min + Math.floor(next() * (max - min + 1)),
18
+ };
19
+ }
@@ -0,0 +1,19 @@
1
+ import { makeSecret } from "./secret.js";
2
+ import type { SecretProvider } from "./secret-provider.types.js";
3
+
4
+ /**
5
+ * The real one: secrets from `process.env`.
6
+ *
7
+ * The `process` global is probed rather than imported, so this file stays
8
+ * neutral and reports every name as absent in a Worker instead of throwing.
9
+ */
10
+ export function createEnvSecrets(): SecretProvider {
11
+ const env: Record<string, string | undefined> = typeof process === "undefined" ? {} : process.env;
12
+ return {
13
+ get: (name) => {
14
+ const value = env[name];
15
+ return value === undefined ? undefined : makeSecret({ reveal: value });
16
+ },
17
+ has: (name) => env[name] !== undefined,
18
+ };
19
+ }
@@ -0,0 +1,6 @@
1
+ export { createEnvSecrets } from "./env-secrets.js";
2
+ export { createMemorySecrets } from "./memory-secrets.js";
3
+ export { makeSecret, REDACTED } from "./secret.js";
4
+ export { SecretProviderPort } from "./secret-provider.port.js";
5
+ export type { KnownSecret } from "./secret-provider.suite.js";
6
+ export type { Secret, SecretProvider } from "./secret-provider.types.js";
@@ -0,0 +1,15 @@
1
+ import { makeSecret } from "./secret.js";
2
+ import type { SecretProvider } from "./secret-provider.types.js";
3
+
4
+ /**
5
+ * The double: secrets given by name, wrapped so they still redact.
6
+ *
7
+ * @param args.values - the raw values, keyed by secret name.
8
+ */
9
+ export function createMemorySecrets(args: { values: Record<string, string> }): SecretProvider {
10
+ const values = args.values;
11
+ return {
12
+ get: (name) => (name in values ? makeSecret({ reveal: values[name] as string }) : undefined),
13
+ has: (name) => name in values,
14
+ };
15
+ }
@@ -0,0 +1,13 @@
1
+ import type { Port } from "../../port/index.js";
2
+ import type { SecretProvider } from "./secret-provider.types.js";
3
+
4
+ /**
5
+ * The {@link SecretProvider} contract. Implementations: `env-secrets`,
6
+ * `memory-secrets`.
7
+ */
8
+ export const SecretProviderPort: Port<SecretProvider> = {
9
+ id: "venn.port.secrets",
10
+ version: 1,
11
+ requires: ["secrets"],
12
+ methods: ["get", "has"],
13
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A value that redacts itself. Serialising one, by any route, yields the
3
+ * redaction marker: redaction happens at the producer, because redacting in the
4
+ * UI is already too late.
5
+ */
6
+ export interface Secret {
7
+ /** The only way to the raw value, and deliberately something you must ask for. */
8
+ reveal(): string;
9
+ toString(): string;
10
+ toJSON(): string;
11
+ }
12
+
13
+ /** Resolves `secrets.*` names to redaction-marked values. */
14
+ export interface SecretProvider {
15
+ get(name: string): Secret | undefined;
16
+ has(name: string): boolean;
17
+ }
@@ -0,0 +1,17 @@
1
+ import type { Secret } from "./secret-provider.types.js";
2
+
3
+ /** What every serialised secret collapses to. */
4
+ export const REDACTED = "‹redigido›";
5
+
6
+ /**
7
+ * Wrap a raw value so it cannot leak through `toString` or `toJSON`.
8
+ *
9
+ * @param args.reveal - the raw value, reachable only via `Secret.reveal()`.
10
+ */
11
+ export function makeSecret(args: { reveal: string }): Secret {
12
+ return {
13
+ reveal: () => args.reveal,
14
+ toString: () => REDACTED,
15
+ toJSON: () => REDACTED,
16
+ };
17
+ }
@@ -0,0 +1,44 @@
1
+ import type {
2
+ SignalHandler,
3
+ SignalSource,
4
+ SystemSignal,
5
+ Unsubscribe,
6
+ } from "./signal-source.types.js";
7
+
8
+ /** A {@link SignalSource} with the bell a test rings. */
9
+ export interface FakeSignals extends SignalSource {
10
+ /** Deliver `signal` to everyone still listening for it. */
11
+ raise(signal: SystemSignal): void;
12
+ /** Which signals are currently being listened for, for assertions. */
13
+ readonly listening: readonly SystemSignal[];
14
+ }
15
+
16
+ /**
17
+ * The double: a signal arrives because a test said so. No process, no operating
18
+ * system, no risk of stopping the test runner along with the code under test.
19
+ */
20
+ export function createFakeSignals(): FakeSignals {
21
+ const handlers = new Map<SystemSignal, Set<SignalHandler>>();
22
+ return {
23
+ get listening() {
24
+ return [...handlers].filter(([, set]) => set.size > 0).map(([signal]) => signal);
25
+ },
26
+ on: (signal, handler) => subscribe({ handlers, signal, handler }),
27
+ raise: (signal) => {
28
+ for (const handler of [...(handlers.get(signal) ?? [])]) handler(signal);
29
+ },
30
+ };
31
+ }
32
+
33
+ function subscribe(args: {
34
+ handlers: Map<SystemSignal, Set<SignalHandler>>;
35
+ signal: SystemSignal;
36
+ handler: SignalHandler;
37
+ }): Unsubscribe {
38
+ const set = args.handlers.get(args.signal) ?? new Set<SignalHandler>();
39
+ set.add(args.handler);
40
+ args.handlers.set(args.signal, set);
41
+ return () => {
42
+ set.delete(args.handler);
43
+ };
44
+ }
@@ -0,0 +1,11 @@
1
+ export { createFakeSignals, type FakeSignals } from "./fake-signals.js";
2
+ export { SignalSourcePort } from "./signal-source.port.js";
3
+ export {
4
+ ALL_SIGNALS,
5
+ type SignalHandler,
6
+ type SignalSource,
7
+ type SystemSignal,
8
+ type Unsubscribe,
9
+ } from "./signal-source.types.js";
10
+ // node-signals is deliberately absent: it lives behind @venn-lang/contracts/node, so
11
+ // this barrel stays Worker-safe.