@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
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,254 @@
1
+ # @venn-lang/contracts
2
+
3
+ > The ports the Venn core runs on, their implementations, and the host that carries them.
4
+
5
+ The core knows nothing about files, clocks, subprocesses or secrets. It knows **ports**: small
6
+ typed interfaces whose implementations arrive at startup inside a `Host` object. This package
7
+ defines every port, ships two implementations of each (a real one and a test double), and provides
8
+ the conformance suites both must pass. There is no DI container and no runtime resolution: the
9
+ entry point assembles a `Host` and passes it inward.
10
+
11
+ ## Usage
12
+
13
+ The CLI assembles the Node host and hands it to the runtime:
14
+
15
+ ```ts
16
+ import { createNodeHost } from "@venn-lang/contracts/node";
17
+
18
+ const host = createNodeHost({ root: process.cwd() });
19
+ await host.fs.write("out/report.json", new TextEncoder().encode("{}"));
20
+ ```
21
+
22
+ Tests take the all-doubles host, overriding only what the test cares about:
23
+
24
+ ```ts
25
+ import { createTestHost, createVirtualClock } from "@venn-lang/contracts";
26
+
27
+ const host = createTestHost({ clock: createVirtualClock({ start: 1_700_000_000_000 }) });
28
+ await host.clock.sleep(5_000); // resolves immediately; now() moved on by 5000 ms
29
+ ```
30
+
31
+ Binding an implementation to a port goes through `bindPort`, which negotiates capabilities and
32
+ then checks the shape:
33
+
34
+ ```ts
35
+ import { bindPort, createMemoryFs, FileSystemPort } from "@venn-lang/contracts";
36
+
37
+ const fs = bindPort({ port: FileSystemPort, impl: createMemoryFs(), caps: ["fs"] });
38
+ // caps: [] throws VN2010. An impl without `list` throws VN2011.
39
+ ```
40
+
41
+ ## Entry points
42
+
43
+ | Specifier | Platform | Holds |
44
+ | --- | --- | --- |
45
+ | `@venn-lang/contracts` | neutral (Node and Web Worker) | ports, doubles, hosts, errors, capability negotiation |
46
+ | `@venn-lang/contracts/node` | Node | the `node:*`-backed implementations and `createNodeHost` |
47
+ | `@venn-lang/contracts/testing` | test | the conformance suites; imports vitest and fast-check |
48
+
49
+ The split is enforced by the build, not by convention: `index` and `testing` are built with tsdown
50
+ `platform: "neutral"`, so a stray `node:*` import fails the build. That is what lets
51
+ [`@venn-lang/core`](../core) run inside the editor's Web Worker.
52
+
53
+ ## Two implementations or it is not a port
54
+
55
+ Every port lands in a single commit with three things: the real implementation, the test double,
56
+ and a conformance suite (TCK) that both of them run. If you cannot name the second implementation
57
+ straight away (`memory`, `fake`, `seeded`, `fixed`, `virtual`), it is not a port. It is a module
58
+ with a good interface, and it should stay one.
59
+
60
+ `Port<T>` is a TypeScript descriptor, not a Zod schema. In Zod 4 `z.function()` is a factory rather
61
+ than a validatable type, so a port's function shape cannot be expressed as data. Zod stays on data.
62
+
63
+ ```ts
64
+ export interface Port<T> {
65
+ readonly id: string; // "venn.port.filesystem"
66
+ readonly version: number; // the contract version, not the package version
67
+ readonly requires: readonly HostCapability[]; // negotiated against Host.caps
68
+ readonly methods: readonly (keyof T & string)[]; // checked against the interface by the compiler
69
+ }
70
+ ```
71
+
72
+ Three checks, in order:
73
+
74
+ 1. **Compile time.** `methods` cannot name anything that is not a key of `T`.
75
+ 2. **Load time.** `bindPort` compares `requires` against `Host.caps` (VN2010), then checks
76
+ `typeof impl[m] === "function"` for each declared method (VN2011).
77
+ 3. **Behaviour.** The conformance suite. The suite is the specification; the prose is commentary.
78
+
79
+ `AnyPort` is `Port<T>` with the element type erased, so heterogeneous collections of ports can be
80
+ typed without variance friction.
81
+
82
+ ## Ports
83
+
84
+ | Port | `id` | Requires | Methods | Real | Double |
85
+ | --- | --- | --- | --- | --- | --- |
86
+ | `FileSystem` | `venn.port.filesystem` | `fs` | `read` `write` `exists` `remove` `list` | `createNodeFs` (`/node`) | `createMemoryFs` |
87
+ | `Clock` | `venn.port.clock` | `clock` | `now` `sleep` | `createSystemClock` | `createVirtualClock` |
88
+ | `Random` | `venn.port.random` | `random` | `next` `int` | `createSeededRandom` | `createFixedRandom` |
89
+ | `SecretProvider` | `venn.port.secrets` | `secrets` | `get` `has` | `createEnvSecrets` | `createMemorySecrets` |
90
+ | `ProcessProvider` | `venn.port.process` | `process` | `spawn` | `createNodeSpawn` (`/node`) | `createFakeProcess` |
91
+ | `Console` | `venn.port.console` | `io` | `write` `writeError` `readLine` `args` | `createNodeConsole` (`/node`) | `createMemoryConsole` |
92
+ | `SignalSource` | `venn.port.signals` | `process` | `on` | `createNodeSignals` (`/node`) | `createFakeSignals` |
93
+ | `LockProvider` | `venn.port.lock` | none | `acquire` | `createInProcessLock` | `createFakeLock` |
94
+ | `ManifestProvider` | `venn.port.manifest` | none | `load` | `createTomlManifest` | `createMemoryManifest` |
95
+
96
+ Implementations marked `/node` are reachable only through `@venn-lang/contracts/node`, and are
97
+ deliberately absent from the folder barrels so the main entry stays neutral. Each port also
98
+ exports its descriptor: `FileSystemPort`, `ClockPort`, `RandomPort`, `SecretProviderPort`,
99
+ `ProcessProviderPort`, `ConsolePort`, `SignalSourcePort`, `LockProviderPort`,
100
+ `ManifestProviderPort`.
101
+
102
+ `LockProvider` and `ManifestProvider` require no capability: one is promise chaining, the other is
103
+ parsing. Neither touches the outside world.
104
+
105
+ ### Notes on individual ports
106
+
107
+ - **FileSystem.** `read` and `remove` on a missing path throw VN8010, never a raw `ENOENT`.
108
+ `list` is one level deep and reads a non-directory as empty. `createNodeFs({ root })` resolves
109
+ relative paths against `root` and leaves absolute paths alone.
110
+ - **Clock.** `createVirtualClock` makes `sleep` advance internal time and resolve at once. Its
111
+ `advance` and `setTime` sit outside `ClockPort.methods` on purpose: they are test controls, not
112
+ part of the negotiated contract.
113
+ - **SecretProvider.** `makeSecret({ reveal })` wraps a raw value so that `toString()` and
114
+ `toJSON()` both collapse to `REDACTED`. `reveal()` is the only way out, and it is deliberately
115
+ explicit. Redaction happens at the producer, so anything reaching a reporter is already marked.
116
+ - **ProcessProvider.** `spawn` streams each chunk to `onOutput` as it arrives and keeps the same
117
+ text in `ProcessResult.output`. `SpawnArgs.env` adds to the host environment rather than
118
+ replacing it, and `shell` is off unless the caller asks for it.
119
+ - **SignalSource.** The fake is what tests use: raising a real SIGINT would stop the test runner
120
+ rather than the code under test. `createNodeSignals` subscribes silently to a signal the platform
121
+ does not have, which `isKnownSignal` reports on directly.
122
+ - **ManifestProvider.** `createTomlManifest({ content })` parses a `venn.toml` into a `Manifest`:
123
+ `[package]`, `[lib]` and `[[bin]]` targets, dependencies, `[patch]`, profiles, `[tooling]`,
124
+ `[workspace]`, `[env.*]`, `[paths]` and `[format]`.
125
+
126
+ ### Manifest helpers
127
+
128
+ Reading and rewriting `venn.toml` without going through a provider:
129
+
130
+ | Export | What it does |
131
+ | --- | --- |
132
+ | `parseToml(content)` | The `venn.toml` subset: sections, nested `[a.b]`, `[[bin]]`, scalars, arrays, inline tables |
133
+ | `tomlDocs(content)` | The comment block written directly above each key, as that key's documentation |
134
+ | `defaultManifest(overrides?)` | A `Manifest` with nothing declared, which is what a project with no `venn.toml` gets |
135
+ | `resolveAlias({ spec, paths })` | Splits a `#alias/rest` specifier against `[paths]`, or `undefined` when it names no alias |
136
+ | `addDependency({ text, name, version, table? })` | The manifest text with one dependency written in, kept in name order |
137
+ | `removeDependency({ text, name, table? })` | The manifest text without that dependency |
138
+ | `readInheritable(data)` | The subset of `[package]` a workspace member may inherit. A name is never inherited |
139
+ | `DEPENDENCIES` `DEFAULT_PROFILES` `LIB_ROOT` `MAIN_ROOT` `BIN_DIR` | The conventional table name, profiles and target roots |
140
+
141
+ Types: `Manifest`, `FormatSettings`, `PackageInfo`, `BuildTarget`, `TargetKind`, `Dependency`,
142
+ `Profile`, `ToolingSettings`, `WorkspaceSettings`, `PackageManagerName`, `DependencyEdit`,
143
+ `AliasTarget`.
144
+
145
+ ## Capabilities and the host
146
+
147
+ `ALL_CAPABILITIES` is the full set: `fs`, `process`, `net`, `clock`, `random`, `secrets`, `log`,
148
+ `io`. A host advertises the subset it can actually serve in `Host.caps`, and a port declares what
149
+ it needs in `requires`. Negotiation compares the two before anything runs.
150
+
151
+ ```ts
152
+ export interface Host {
153
+ readonly fs: FileSystem;
154
+ readonly proc: ProcessProvider;
155
+ readonly clock: Clock;
156
+ readonly random: Random;
157
+ readonly secrets: SecretProvider;
158
+ readonly log: Logger;
159
+ readonly lock: LockProvider;
160
+ readonly caps: readonly HostCapability[];
161
+ }
162
+ ```
163
+
164
+ | Assembler | Capabilities | Built from |
165
+ | --- | --- | --- |
166
+ | `createHost.worker()` | `fs` `clock` `random` `secrets` `log` | memory fs, system clock, seeded random, memory secrets and logger |
167
+ | `createHost.test(overrides?)` | all eight | doubles throughout, with any field overridable |
168
+ | `createNodeHost({ root? })` (`/node`) | all eight | node fs, real spawn, system clock, env secrets, console logger |
169
+
170
+ `createWorkerHost` and `createTestHost` are also exported directly; `createHost` is the object that
171
+ groups the two neutral ones. The Node assembler is deliberately not on it, because it pulls in
172
+ `node:*`.
173
+
174
+ A worker has no `process`, so its `proc` field is built by `unavailable()`. Every declared method
175
+ on it throws VN2012 with a readable message, rather than a `TypeError` halfway through a test.
176
+
177
+ ```ts
178
+ import { ProcessProviderPort, unavailable } from "@venn-lang/contracts";
179
+ import type { ProcessProvider } from "@venn-lang/contracts";
180
+
181
+ const proc = unavailable<ProcessProvider>({
182
+ capability: "process",
183
+ methods: ProcessProviderPort.methods,
184
+ });
185
+ ```
186
+
187
+ ## Conformance
188
+
189
+ A suite takes a `ConformanceSpec<T>`: a display name and a `factory` that returns a fresh
190
+ implementation, synchronously or not. Running it is one call per implementation.
191
+
192
+ ```ts
193
+ // file-system.test.ts
194
+ import { fileSystemConformance } from "@venn-lang/contracts/testing";
195
+
196
+ fileSystemConformance({ name: "memory", factory: () => createMemoryFs() });
197
+ fileSystemConformance({
198
+ name: "node-fs",
199
+ factory: async () => createNodeFs({ root: await mkdtemp(join(tmpdir(), "venn-fs-")) }),
200
+ });
201
+ ```
202
+
203
+ Some suites ask for more than the spec: `secretProviderConformance` takes a `known` secret,
204
+ `manifestProviderConformance` takes an `expectedName`, `processProviderConformance` takes `runs`
205
+ and `expected`, and `signalSourceConformance` takes a whole `SignalSpec` saying how a signal is
206
+ delivered to that implementation.
207
+
208
+ Exported from `@venn-lang/contracts/testing`: `clockConformance`, `fileSystemConformance`,
209
+ `lockProviderConformance`, `manifestProviderConformance`, `processProviderConformance`,
210
+ `randomConformance`, `secretProviderConformance`, `signalSourceConformance`, plus `expectVennError`
211
+ and the types `ConformanceSpec`, `PortFactory` and `SignalSpec`.
212
+
213
+ `expectVennError({ op, code })` asserts that an async operation rejects with a `VennError` whose
214
+ `.code` matches a regex. Suites assert on codes, never on prose, so error wording stays free to
215
+ improve.
216
+
217
+ Adding an implementation to an existing port is one new file, one line in the folder barrel and one
218
+ line in the test. The suite itself is never touched.
219
+
220
+ ## Errors
221
+
222
+ Every failure crossing a contracts boundary is a `VennError`: an `Error` with a stable `code` and
223
+ an optional structured `detail`.
224
+
225
+ | Code | Raised by | When |
226
+ | --- | --- | --- |
227
+ | `VN2010` | `hostMissingCapability` | a port requires a capability the host does not advertise |
228
+ | `VN2011` | `portShapeMismatch` | an implementation is missing a method the port declares |
229
+ | `VN2012` | `capabilityUnavailable` | code reached a capability the host marked unavailable |
230
+ | `VN8010` | `fsNotFound` | a read or remove targeted a path that does not exist |
231
+
232
+ ## Environment files
233
+
234
+ Both the runner and the editor resolve dotenv files through the same three exports, so they cannot
235
+ disagree about where a value lives.
236
+
237
+ | Export | What it does |
238
+ | --- | --- |
239
+ | `DOTENV_CONVENTION` | `.env`, `.env.${name}`, `.env.local`, `.env.${name}.local`, lowest precedence first |
240
+ | `dotenvFiles({ configured?, name })` | Which files to read, in order, for one environment |
241
+ | `parseDotenv(content)` | `NAME=value` per line, with comments, blanks, `export ` and quotes. No variable expansion |
242
+
243
+ ## Logging
244
+
245
+ `Logger` has a single method, `log(entry)`, where an entry is a `LogLevel` and a message.
246
+ `createConsoleLogger()` writes through the global console and is safe in both Node and a worker.
247
+ `createMemoryLogger()` returns a `MemoryLogger`, which keeps every entry in `entries` for
248
+ assertions.
249
+
250
+ ## See also
251
+
252
+ - [`@venn-lang/core`](../core) receives a `Host` and imports nothing else for I/O.
253
+ - [`@venn-lang/runtime`](../runtime) executes flows against that host.
254
+ - [`@venn-lang/cli`](../cli) assembles the Node host and is the only package free to use `node:*`.