@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,14 @@
1
+ import type { HostCapability } from "../capabilities/index.js";
2
+
3
+ /**
4
+ * The gap between what is required and what the host offers.
5
+ *
6
+ * @returns the required capabilities absent from `caps`, in the order required.
7
+ */
8
+ export function missingCapabilities(args: {
9
+ requires: readonly HostCapability[];
10
+ caps: readonly HostCapability[];
11
+ }): readonly HostCapability[] {
12
+ const present = new Set(args.caps);
13
+ return args.requires.filter((cap) => !present.has(cap));
14
+ }
@@ -0,0 +1,36 @@
1
+ import type { HostCapability } from "../capabilities/index.js";
2
+
3
+ /**
4
+ * A typed contract carrying at least two implementations, one real and one
5
+ * double.
6
+ *
7
+ * Deliberately not a Zod schema: in Zod 4 `z.function` is a factory rather than
8
+ * a validatable type, so a port's function shape cannot be expressed as data.
9
+ * `methods` buys a compile-time check (each entry must be a key of `T`) and a
10
+ * load-time one (`typeof` per method); the behavioural guarantee is the
11
+ * conformance suite.
12
+ *
13
+ * @typeParam T - the interface the implementation must satisfy.
14
+ */
15
+ export interface Port<T> {
16
+ /** Stable identity, e.g. "venn.port.filesystem". */
17
+ readonly id: string;
18
+ /** Version of the contract, not of the package. Removing a method bumps it. */
19
+ readonly version: number;
20
+ /** Capabilities negotiated against `Host.caps` before binding. */
21
+ readonly requires: readonly HostCapability[];
22
+ /** Methods an implementation must provide. Each must be a key of `T`. */
23
+ readonly methods: readonly (keyof T & string)[];
24
+ }
25
+
26
+ /**
27
+ * A `Port<T>` with its element type erased. Every `Port<T>` is assignable to it
28
+ * (its `methods` widen to `string[]`), so a heterogeneous collection of ports,
29
+ * such as the runtime's binding table, types without variance friction.
30
+ */
31
+ export type AnyPort = {
32
+ readonly id: string;
33
+ readonly version: number;
34
+ readonly requires: readonly HostCapability[];
35
+ readonly methods: readonly string[];
36
+ };
@@ -0,0 +1,15 @@
1
+ import type { Port } from "../../port/index.js";
2
+ import type { Clock } from "./clock.types.js";
3
+
4
+ /**
5
+ * The {@link Clock} contract. Implementations: `system-clock`, `virtual-clock`.
6
+ *
7
+ * `advance` and `setTime` are absent on purpose: they are how a test drives a
8
+ * virtual clock, not something a caller of the port may reach for.
9
+ */
10
+ export const ClockPort: Port<Clock> = {
11
+ id: "venn.port.clock",
12
+ version: 1,
13
+ requires: ["clock"],
14
+ methods: ["now", "sleep"],
15
+ };
@@ -0,0 +1,17 @@
1
+ /** Now, sleeping, deadlines. */
2
+ export interface Clock {
3
+ /** Milliseconds since the epoch, or since the virtual clock's start. */
4
+ now(): number;
5
+ sleep(ms: number): Promise<void>;
6
+ }
7
+
8
+ /**
9
+ * A {@link Clock} whose flow of time a test controls.
10
+ *
11
+ * `advance` and `setTime` sit outside the negotiated contract, so code holding
12
+ * a `Clock` can never reach them.
13
+ */
14
+ export interface VirtualClock extends Clock {
15
+ advance(ms: number): void;
16
+ setTime(epochMs: number): void;
17
+ }
@@ -0,0 +1,4 @@
1
+ export { ClockPort } from "./clock.port.js";
2
+ export type { Clock, VirtualClock } from "./clock.types.js";
3
+ export { createSystemClock } from "./system-clock.js";
4
+ export { createVirtualClock } from "./virtual-clock.js";
@@ -0,0 +1,9 @@
1
+ import type { Clock } from "./clock.types.js";
2
+
3
+ /** Wall-clock time, on the `Date` and `setTimeout` globals. */
4
+ export function createSystemClock(): Clock {
5
+ return {
6
+ now: () => Date.now(),
7
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
8
+ };
9
+ }
@@ -0,0 +1,22 @@
1
+ import type { VirtualClock } from "./clock.types.js";
2
+
3
+ /**
4
+ * The double. `sleep` advances internal time and resolves at once, so a test
5
+ * about a timeout costs nothing to run.
6
+ *
7
+ * @param args.start - the epoch this clock begins at. Defaults to 0.
8
+ */
9
+ export function createVirtualClock(args: { start?: number } = {}): VirtualClock {
10
+ let time = args.start ?? 0;
11
+ const advance = (ms: number): void => {
12
+ time += Math.max(0, ms);
13
+ };
14
+ return {
15
+ now: () => time,
16
+ sleep: async (ms) => advance(ms),
17
+ advance,
18
+ setTime: (epochMs) => {
19
+ time = epochMs;
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,13 @@
1
+ import type { Port } from "../../port/index.js";
2
+ import type { Console } from "./console.types.js";
3
+
4
+ /**
5
+ * The {@link Console} contract. Implementations: `node-console`,
6
+ * `memory-console`.
7
+ */
8
+ export const ConsolePort: Port<Console> = {
9
+ id: "venn.port.console",
10
+ version: 1,
11
+ requires: ["io"],
12
+ methods: ["write", "writeError", "readLine", "args"],
13
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The program's console: standard streams and process arguments.
3
+ *
4
+ * Everything a script needs to talk to the outside world without knowing
5
+ * whether it runs on Node, in a test, or in a browser worker. The host supplies
6
+ * the wiring.
7
+ */
8
+ export interface Console {
9
+ /** Write to standard output, exactly as given (no trailing newline). */
10
+ write(text: string): void;
11
+ /** Write to standard error. */
12
+ writeError(text: string): void;
13
+ /** Read the next line from standard input, or null at end of input. */
14
+ readLine(): Promise<string | null>;
15
+ /** The command-line arguments passed after the script path. */
16
+ args(): readonly string[];
17
+ }
@@ -0,0 +1,3 @@
1
+ export { ConsolePort } from "./console.port.js";
2
+ export type { Console } from "./console.types.js";
3
+ export { createMemoryConsole, type MemoryConsole } from "./memory-console.js";
@@ -0,0 +1,36 @@
1
+ import type { Console } from "./console.types.js";
2
+
3
+ /** A {@link Console} that keeps its transcript instead of printing it. */
4
+ export interface MemoryConsole extends Console {
5
+ readonly out: string;
6
+ readonly err: string;
7
+ }
8
+
9
+ /**
10
+ * The double: records what was written, reads from a scripted input.
11
+ *
12
+ * @param args.input - the lines `readLine` hands back, in order.
13
+ * @param args.argv - what `args()` reports.
14
+ */
15
+ export function createMemoryConsole(
16
+ args: { input?: readonly string[]; argv?: readonly string[] } = {},
17
+ ): MemoryConsole {
18
+ const state = { out: "", err: "" };
19
+ const input = [...(args.input ?? [])];
20
+ return {
21
+ write: (text) => {
22
+ state.out += text;
23
+ },
24
+ writeError: (text) => {
25
+ state.err += text;
26
+ },
27
+ readLine: () => Promise.resolve(input.length > 0 ? (input.shift() ?? null) : null),
28
+ args: () => args.argv ?? [],
29
+ get out() {
30
+ return state.out;
31
+ },
32
+ get err() {
33
+ return state.err;
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,80 @@
1
+ import { createInterface, type Interface } from "node:readline";
2
+ import type { Console } from "./console.types.js";
3
+
4
+ /**
5
+ * Where a node console reads and writes. Injectable, so a test can drive the
6
+ * real implementation and read back what a script printed.
7
+ */
8
+ export interface ConsoleStreams {
9
+ argv?: readonly string[];
10
+ stdout?: { write(text: string): unknown };
11
+ stderr?: { write(text: string): unknown };
12
+ /** Where lines are read from. Defaults to the process's own standard input. */
13
+ stdin?: NodeJS.ReadableStream & { isTTY?: boolean };
14
+ }
15
+
16
+ /**
17
+ * The real console: Node's own streams.
18
+ *
19
+ * stdin is opened lazily, on the first `readLine`. A script that never reads
20
+ * input must not hold the process open waiting for a line that never comes.
21
+ *
22
+ * @param streams - overrides for stdin, stdout, stderr and argv. Each defaults
23
+ * to the process's own.
24
+ */
25
+ export function createNodeConsole(streams: ConsoleStreams = {}): Console {
26
+ const lines = createLineReader(streams.stdin ?? process.stdin);
27
+ const out = streams.stdout ?? process.stdout;
28
+ const err = streams.stderr ?? process.stderr;
29
+ return {
30
+ write: (text) => void out.write(text),
31
+ writeError: (text) => void err.write(text),
32
+ readLine: () => lines.next(),
33
+ args: () => streams.argv ?? [],
34
+ };
35
+ }
36
+
37
+ interface LineReaderState {
38
+ buffered: string[];
39
+ waiting: ((line: string | null) => void)[];
40
+ done: boolean;
41
+ input: NodeJS.ReadableStream & { isTTY?: boolean };
42
+ reader?: Interface;
43
+ }
44
+
45
+ function createLineReader(input: NodeJS.ReadableStream & { isTTY?: boolean }): {
46
+ next(): Promise<string | null>;
47
+ } {
48
+ const state: LineReaderState = { buffered: [], waiting: [], done: false, input };
49
+ return { next: () => next(state) };
50
+ }
51
+
52
+ function next(state: LineReaderState): Promise<string | null> {
53
+ open(state);
54
+ if (state.buffered.length > 0) return Promise.resolve(state.buffered.shift() ?? null);
55
+ if (state.done) return Promise.resolve(null);
56
+ return new Promise((resolve) => state.waiting.push(resolve));
57
+ }
58
+
59
+ /** Attach to stdin on the first read; a TTY with no piped input ends at once. */
60
+ function open(state: LineReaderState): void {
61
+ if (state.reader || state.done) return;
62
+ if (state.input.isTTY) {
63
+ state.done = true;
64
+ return;
65
+ }
66
+ state.reader = createInterface({ input: state.input, crlfDelay: Number.POSITIVE_INFINITY });
67
+ state.reader.on("line", (line) => deliver(state, line));
68
+ state.reader.on("close", () => finish(state));
69
+ }
70
+
71
+ function deliver(state: LineReaderState, line: string): void {
72
+ const resolve = state.waiting.shift();
73
+ if (resolve) resolve(line);
74
+ else state.buffered.push(line);
75
+ }
76
+
77
+ function finish(state: LineReaderState): void {
78
+ state.done = true;
79
+ for (const resolve of state.waiting.splice(0)) resolve(null);
80
+ }
@@ -0,0 +1,10 @@
1
+ import { VennError } from "../../errors/index.js";
2
+
3
+ /** VN8010: a read or a remove targeted a path that does not exist. */
4
+ export function fsNotFound(args: { path: string }): VennError {
5
+ return new VennError({
6
+ code: "VN8010",
7
+ message: `File not found: "${args.path}".`,
8
+ detail: { path: args.path },
9
+ });
10
+ }
@@ -0,0 +1,12 @@
1
+ import type { Port } from "../../port/index.js";
2
+ import type { FileSystem } from "./file-system.types.js";
3
+
4
+ /**
5
+ * The {@link FileSystem} contract. Implementations: `node-fs`, `memory-fs`.
6
+ */
7
+ export const FileSystemPort: Port<FileSystem> = {
8
+ id: "venn.port.filesystem",
9
+ version: 1,
10
+ requires: ["fs"],
11
+ methods: ["read", "write", "exists", "remove", "list"],
12
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Read and write bytes.
3
+ *
4
+ * This is the port that lets `@venn-lang/core` run both in Node (the CLI) and in a
5
+ * Web Worker (the LSP). A leaked `import fs from "node:fs"` in core would break
6
+ * the editor, so all byte I/O goes through here.
7
+ */
8
+ export interface FileSystem {
9
+ /** @throws VennError VN8010 when the path does not exist. */
10
+ read(path: string): Promise<Uint8Array>;
11
+ write(path: string, bytes: Uint8Array): Promise<void>;
12
+ exists(path: string): Promise<boolean>;
13
+ /** @throws VennError VN8010 when the path does not exist. */
14
+ remove(path: string): Promise<void>;
15
+ /**
16
+ * What a directory holds, one level deep, in no promised order. A path that
17
+ * is not a directory reads as empty rather than raising: asking what is
18
+ * inside something that holds nothing has an answer.
19
+ */
20
+ list(path: string): Promise<readonly DirEntry[]>;
21
+ }
22
+
23
+ /** One name inside a directory, and whether it holds more. */
24
+ export interface DirEntry {
25
+ name: string;
26
+ directory: boolean;
27
+ }
@@ -0,0 +1,6 @@
1
+ export { fsNotFound } from "./file-system.errors.js";
2
+ export { FileSystemPort } from "./file-system.port.js";
3
+ export type { DirEntry, FileSystem } from "./file-system.types.js";
4
+ export { createMemoryFs } from "./memory-fs.js";
5
+ // node-fs is deliberately absent: it lives behind @venn-lang/contracts/node, so this
6
+ // barrel stays Worker-safe.
@@ -0,0 +1,46 @@
1
+ import { fsNotFound } from "./file-system.errors.js";
2
+ import type { DirEntry, FileSystem } from "./file-system.types.js";
3
+
4
+ /** The double: byte-exact, isolated per instance, no disk. */
5
+ export function createMemoryFs(): FileSystem {
6
+ const files = new Map<string, Uint8Array>();
7
+ return {
8
+ async read(path) {
9
+ const bytes = files.get(path);
10
+ if (!bytes) throw fsNotFound({ path });
11
+ return bytes.slice();
12
+ },
13
+ async write(path, bytes) {
14
+ files.set(path, bytes.slice());
15
+ },
16
+ async exists(path) {
17
+ return files.has(path);
18
+ },
19
+ async remove(path) {
20
+ if (!files.delete(path)) throw fsNotFound({ path });
21
+ },
22
+ async list(path) {
23
+ return listUnder([...files.keys()], path);
24
+ },
25
+ };
26
+ }
27
+
28
+ /**
29
+ * What a directory holds, worked out from the paths written.
30
+ *
31
+ * There are no directories here, only paths, so a directory is whatever a path
32
+ * implies one to be. For everything the language does with a directory that is
33
+ * the same answer the real file system gives, which is what lets the TCK ask
34
+ * both the same questions.
35
+ */
36
+ function listUnder(paths: readonly string[], directory: string): DirEntry[] {
37
+ const prefix = directory === "" || directory === "." ? "" : `${directory.replace(/\/+$/, "")}/`;
38
+ const seen = new Map<string, boolean>();
39
+ for (const path of paths) {
40
+ if (!path.startsWith(prefix)) continue;
41
+ const rest = path.slice(prefix.length);
42
+ const slash = rest.indexOf("/");
43
+ if (rest !== "") seen.set(slash < 0 ? rest : rest.slice(0, slash), slash >= 0);
44
+ }
45
+ return [...seen].map(([name, isDirectory]) => ({ name, directory: isDirectory }));
46
+ }
@@ -0,0 +1,93 @@
1
+ import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join } from "node:path";
3
+ import { VennError } from "../../errors/index.js";
4
+ import { fsNotFound } from "./file-system.errors.js";
5
+ import type { DirEntry, FileSystem } from "./file-system.types.js";
6
+
7
+ /**
8
+ * The real file system, rooted at `root`.
9
+ *
10
+ * @param args.root - where relative paths resolve. Defaults to ".".
11
+ * @returns a {@link FileSystem} that reports a missing path as VN8010 and any
12
+ * other failure as VN8019.
13
+ */
14
+ export function createNodeFs(args: { root?: string } = {}): FileSystem {
15
+ const root = args.root ?? ".";
16
+ // A root is where *relative* paths resolve. Joining it onto an absolute path
17
+ // corrupts the path rather than relocating it: `join(".", "C:/x")` is
18
+ // `.\C:\x`, which cannot exist, and the failure surfaces far from here.
19
+ const at = (path: string): string => (isAbsolute(path) ? path : join(root, path));
20
+ return {
21
+ read: (path) => readBytes(at(path)),
22
+ write: (path, bytes) => writeBytes(at(path), bytes),
23
+ exists: (path) => pathExists(at(path)),
24
+ remove: (path) => removePath(at(path)),
25
+ list: (path) => listPath(at(path)),
26
+ };
27
+ }
28
+
29
+ async function listPath(directory: string): Promise<DirEntry[]> {
30
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
31
+ const found: DirEntry[] = [];
32
+ for (const entry of entries) {
33
+ found.push({ name: entry.name, directory: await holdsMore(directory, entry) });
34
+ }
35
+ return found;
36
+ }
37
+
38
+ type Entry = { name: string; isDirectory(): boolean; isSymbolicLink(): boolean };
39
+
40
+ /**
41
+ * Whether an entry holds more, following a link if it is one.
42
+ *
43
+ * `readdir` reports a symlink as a symlink and not as what it points at, and
44
+ * whole directory trees are built out of them: pnpm links every installed
45
+ * package into place, so `node_modules/zod` is a link. Trusting the first
46
+ * answer finds no packages at all in a full `node_modules`.
47
+ */
48
+ async function holdsMore(directory: string, entry: Entry): Promise<boolean> {
49
+ if (entry.isDirectory()) return true;
50
+ if (!entry.isSymbolicLink()) return false;
51
+ const target = await stat(join(directory, entry.name)).catch(() => undefined);
52
+ return target?.isDirectory() ?? false;
53
+ }
54
+
55
+ async function readBytes(file: string): Promise<Uint8Array> {
56
+ try {
57
+ return new Uint8Array(await readFile(file));
58
+ } catch (err) {
59
+ throw mapMissing(err, file);
60
+ }
61
+ }
62
+
63
+ async function writeBytes(file: string, bytes: Uint8Array): Promise<void> {
64
+ await mkdir(dirname(file), { recursive: true });
65
+ await writeFile(file, bytes);
66
+ }
67
+
68
+ async function pathExists(file: string): Promise<boolean> {
69
+ try {
70
+ await stat(file);
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ async function removePath(file: string): Promise<void> {
78
+ try {
79
+ await rm(file);
80
+ } catch (err) {
81
+ throw mapMissing(err, file);
82
+ }
83
+ }
84
+
85
+ function mapMissing(err: unknown, file: string): VennError {
86
+ if (isEnoent(err)) return fsNotFound({ path: file });
87
+ const message = err instanceof Error ? err.message : String(err);
88
+ return new VennError({ code: "VN8019", message });
89
+ }
90
+
91
+ function isEnoent(err: unknown): boolean {
92
+ return typeof err === "object" && err !== null && (err as { code?: string }).code === "ENOENT";
93
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./clock/index.js";
2
+ export * from "./console/index.js";
3
+ export * from "./file-system/index.js";
4
+ export * from "./lock-provider/index.js";
5
+ export * from "./manifest-provider/index.js";
6
+ export * from "./process-provider/index.js";
7
+ export * from "./random/index.js";
8
+ export * from "./secret-provider/index.js";
9
+ export * from "./signal-source/index.js";
@@ -0,0 +1,6 @@
1
+ import type { LockProvider } from "./lock-provider.types.js";
2
+
3
+ /** The double: grants immediately and serialises nothing. */
4
+ export function createFakeLock(): LockProvider {
5
+ return { acquire: async () => () => {} };
6
+ }
@@ -0,0 +1,26 @@
1
+ import type { LockProvider, Release } from "./lock-provider.types.js";
2
+
3
+ /**
4
+ * The real one: a chained-promise mutex per name. Waiters are served in the
5
+ * order they asked, and the chain lives only in this process.
6
+ */
7
+ export function createInProcessLock(): LockProvider {
8
+ const tails = new Map<string, Promise<void>>();
9
+ return {
10
+ acquire: (name) => acquire(tails, name),
11
+ };
12
+ }
13
+
14
+ async function acquire(tails: Map<string, Promise<void>>, name: string): Promise<Release> {
15
+ const previous = tails.get(name) ?? Promise.resolve();
16
+ let release: Release = () => {};
17
+ const held = new Promise<void>((resolve) => {
18
+ release = resolve;
19
+ });
20
+ tails.set(
21
+ name,
22
+ previous.then(() => held),
23
+ );
24
+ await previous;
25
+ return release;
26
+ }
@@ -0,0 +1,4 @@
1
+ export { createFakeLock } from "./fake-lock.js";
2
+ export { createInProcessLock } from "./in-process-lock.js";
3
+ export { LockProviderPort } from "./lock-provider.port.js";
4
+ export type { LockProvider, Release } from "./lock-provider.types.js";
@@ -0,0 +1,16 @@
1
+ import type { Port } from "../../port/index.js";
2
+ import type { LockProvider } from "./lock-provider.types.js";
3
+
4
+ /**
5
+ * The {@link LockProvider} contract. Implementations: `in-process-lock`,
6
+ * `fake-lock`.
7
+ *
8
+ * Requires nothing: a mutex is bookkeeping, so it binds even on a host with no
9
+ * capabilities at all.
10
+ */
11
+ export const LockProviderPort: Port<LockProvider> = {
12
+ id: "venn.port.lock",
13
+ version: 1,
14
+ requires: [],
15
+ methods: ["acquire"],
16
+ };
@@ -0,0 +1,8 @@
1
+ /** Releases a held lock. Calling it more than once is a no-op. */
2
+ export type Release = () => void;
3
+
4
+ /** Named mutexes: the semantics behind `@lock` and `@serial`. */
5
+ export interface LockProvider {
6
+ /** Resolves once the lock named `name` is held by this caller alone. */
7
+ acquire(name: string): Promise<Release>;
8
+ }
@@ -0,0 +1,31 @@
1
+ import type { Manifest } from "./manifest.types.js";
2
+ import { DEFAULT_PROFILES } from "./read/index.js";
3
+
4
+ /**
5
+ * A manifest with nothing declared: what a project without a `venn.toml` is.
6
+ *
7
+ * The one place stating what a project gets for saying nothing at all, so a
8
+ * caller who wants one field need not spell the other dozen.
9
+ *
10
+ * @param overrides - fields to state explicitly. `name` and `version` fall back
11
+ * to `[package]` before falling back to the defaults.
12
+ */
13
+ export function defaultManifest(overrides: Partial<Manifest> = {}): Manifest {
14
+ const pkg = { name: "", authors: [], ...overrides.package };
15
+ return {
16
+ name: overrides.name ?? pkg.name,
17
+ version: overrides.version ?? pkg.version ?? "0.0.0",
18
+ package: pkg,
19
+ targets: [],
20
+ dependencies: [],
21
+ devDependencies: [],
22
+ patch: [],
23
+ profiles: { ...DEFAULT_PROFILES },
24
+ tooling: { manager: "pnpm" },
25
+ env: {},
26
+ envFiles: [],
27
+ paths: {},
28
+ format: {},
29
+ ...overrides,
30
+ };
31
+ }