lawspec 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 brain-fuel
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,240 @@
1
+ # LawSpec
2
+
3
+ **State the law once. Check it everywhere.**
4
+
5
+ LawSpec 0.1 compiles reusable laws into native property tests, executable examples,
6
+ and implementation adapters. The compiler is Haskell, distributed as prebuilt
7
+ WebAssembly with a Node CLI and an asynchronous, typed JavaScript API.
8
+
9
+ ## Install and try it
10
+
11
+ The checkout includes the npm package in `npm/`. Build an installable archive:
12
+
13
+ ```sh
14
+ npm pack ./npm
15
+ npm install --save-dev ./lawspec-0.1.0.tgz
16
+ ```
17
+
18
+ No Haskell toolchain is needed to install or run the npm package. Node 22+ and the
19
+ selected target's build tools are required. The reference platforms are macOS
20
+ and Linux. This checkout has not been published to the npm registry.
21
+
22
+ From an empty application directory, use the installed `lawspec` command:
23
+
24
+ ```sh
25
+ lawspec init --target javascript
26
+ npm install
27
+ lawspec check
28
+ lawspec explain 'example.atoi_codec::itoa and then atoi yields a'
29
+ lawspec doctor
30
+ lawspec generate
31
+ ```
32
+
33
+ Implement `src/example/atoi_codec.mjs`, then run `npm test`:
34
+
35
+ ```javascript
36
+ export function itoa(value) { return String(value); }
37
+ export function atoi(value) { return Number(value); }
38
+ ```
39
+
40
+ The starter adapters intentionally throw until implemented. Generated tests
41
+ include the supplied examples, signed 32-bit boundary cases, and randomized
42
+ properties with the selected framework's shrinking and failure reporting.
43
+
44
+ ## Targets
45
+
46
+ | Target | Build setup | Test libraries | Test command |
47
+ | --- | --- | --- | --- |
48
+ | `java` | Maven, JDK 25, release 25 | JetCheck 0.3.0, JUnit Jupiter 5.14.x | `mvn test` |
49
+ | `python` | Python 3.13 or 3.14, pyproject | pytest 8.4.x, Hypothesis 6.135.26+ (6.x) | `python -m pytest` |
50
+ | `javascript` | Node 22+, npm, ESM | fast-check 4.x, node:test | `npm test` |
51
+ | `typescript` | Node 22+, npm, TypeScript 5.9.x, ESM | fast-check 4.x, node:test | `npm test` |
52
+ | `go` | Go modules, Go 1.22–1.26 | Rapid 1.2.0, testing | `go test ./...` |
53
+ | `haskell` | Stack, GHC 9.10, LTS 24.58 | Hspec 2.11, Hedgehog 1.5, hspec-hedgehog 0.3 | `stack test` |
54
+ | `kotlin` | JDK/JVM 25, Gradle 9.1–9.3, Kotlin 2.3.21 | Kotest 5.9.1 | `gradle test` |
55
+
56
+ Java 25 and Python 3.13 are the minimum baselines. New JVM releases are admitted
57
+ through compatibility profiles after testing; v0.1's current JVM profile certifies
58
+ 25. Python templates declare `requires-python = ">=3.13"` and runtime checks
59
+ currently recognize 3.13 and 3.14. Kotlin templates pin Gradle's supported build
60
+ configuration to Kotlin 2.3.21 and target JVM 25.
61
+
62
+ `npm/compatibility.json` records inclusive minimum/exclusive maximum dependency
63
+ bounds. Unknown, prerelease, missing, and incompatible versions fail preflight.
64
+ Build-tool probes inspect resolved dependencies, compiler settings, source roots,
65
+ and runner configuration. Unverifiable custom filtering/configuration is rejected
66
+ with setup instructions. Build tools may populate their normal caches while
67
+ resolving dependencies; LawSpec does not run dependency installers during generation.
68
+
69
+ ## Existing projects and configuration
70
+
71
+ `init` creates `lawspec.json` and a starter specification. It creates build
72
+ files only when no existing build setup is detected. Existing build files are
73
+ preserved, and setup instructions describe the changes you need to make yourself.
74
+
75
+ ```json
76
+ {
77
+ "version": 1,
78
+ "sources": ["laws"],
79
+ "targets": [
80
+ {"language": "java", "root": "java"},
81
+ {"language": "python", "root": "python", "python": ".venv/bin/python"},
82
+ {"language": "haskell", "root": "haskell"}
83
+ ]
84
+ }
85
+ ```
86
+
87
+ Source entries are files or directories, relative to the configuration file.
88
+ Directories are scanned for `.lawspec` files. Every target has its own project
89
+ root. Use `init --target python --project python` to add a target; `--config`
90
+ selects a different configuration file. Targets can override `sourceDir` and
91
+ `testDir` with relative paths. Configure the native build to include those paths
92
+ before generation. Go's source and test directories must be the same.
93
+
94
+ Defaults are `src`/`test` for JS, TS and Haskell; `src`/`tests` for Python;
95
+ `src/main/java`/`src/test/java` for Java; the corresponding Kotlin directories;
96
+ and unit-based package directories at the Go project root. Python accepts a
97
+ `python` interpreter override; Java accepts `maven`; Kotlin accepts `gradle` and
98
+ otherwise uses a local `gradlew` or `gradle` on PATH.
99
+
100
+ For Python, create and select a 3.13+ virtual environment and install the printed
101
+ test dependencies. For Stack, run `stack build --test --no-run-tests` once before
102
+ `doctor`; this resolves the snapshot and generates the Cabal description through
103
+ Hpack. Doctor uses that existing description without rewriting it. Test discovery
104
+ uses `test/Spec.hs` with `hspec-discover`.
105
+
106
+ Commands:
107
+
108
+ - `check`: parse, resolve, type-check and expand laws without target dependencies.
109
+ - `explain [unit::law]`: display expansion steps and inherited example inputs.
110
+ - `doctor`: inspect selected native environments and print corrective instructions.
111
+ - `generate`: check environments, validate every output, then write artifacts.
112
+ - `generate --dry-run`: show proposed file operations without applying them.
113
+ - `generate --check`: fail when generated files need updating, without writing.
114
+
115
+ Use `--target <language>` to select a configured language, and `--json` for
116
+ machine-readable check, explanation, doctor and generation output. Generation
117
+ fails before writing if any selected target is incompatible or any output
118
+ conflicts with file ownership.
119
+
120
+ ## Language slice
121
+
122
+ ```lawspec
123
+ unit example.atoi_codec
124
+
125
+ itoa :: Int32 -> Text
126
+ atoi :: Text -> Int32
127
+
128
+ law `round trip` is
129
+ definition is
130
+ `left inverse` atoi itoa
131
+ end
132
+ description is
133
+ "applying {itoa} and then {atoi} recovers the original integer"
134
+ end
135
+ example `negative` is
136
+ x = -42
137
+ end
138
+ end
139
+ ```
140
+
141
+ The implicit prelude defines `left inverse` and `round trip identity is preserved`.
142
+ A law may reference a local reusable law or a prelude law. The compiler performs
143
+ capture-avoiding expansion and specializes types; it does not recognize codec
144
+ function names specially. `explain` shows the final property:
145
+
146
+ ```text
147
+ for all (x :: Int32) . atoi (itoa (x)) = x
148
+ ```
149
+
150
+ Reusable laws can declare typed unary function parameters and `requires Eq a`.
151
+ Definitions support law application, function application/composition, universal
152
+ quantification, integer literals and equality. Function signatures use `Int32`
153
+ and `Text`; generic variables are supported in reusable laws. v0.1 generates
154
+ quantified `Int32` inputs, including multiple inputs. `Text` can be an intermediate
155
+ or compared result. Functions are synchronous and unary.
156
+
157
+ Examples refer to the expanded input names, including names inherited from the
158
+ prelude. Bind every input exactly once. Ambiguous names and out-of-range values
159
+ are errors. Descriptions and rationales use `{function}` references; `{{` and `}}`
160
+ produce literal braces. Metadata blocks follow the order shown in `scratch.md`:
161
+ definition, optional description, optional rationale, examples, optional references.
162
+ `--` starts a line comment. Names that cannot be emitted portably are diagnosed.
163
+
164
+ Additional primitives, external law packages, cross-unit imports beyond the
165
+ prelude, async functions, direct existing-symbol binding and browser hosting are
166
+ outside this release.
167
+
168
+ ## Ownership
169
+
170
+ Implementation adapters are created once and belong to you. Implement them or
171
+ have them delegate to existing application functions. Regeneration never rewrites
172
+ them. If the required adapter contract changes, generation prints the new stub
173
+ shape for you to apply manually.
174
+
175
+ Generated tests are tracked in `.lawspec/generated.json` with content hashes.
176
+ Commit that manifest alongside generated tests. LawSpec refuses to overwrite
177
+ unowned files or edited generated files, even if an unowned file has matching
178
+ contents. Obsolete tests are deleted only when they still match their recorded
179
+ hash. User adapters remain. Output paths cannot traverse outside a target root
180
+ or pass through symlinks. Writes use temporary files and atomic replacement;
181
+ concurrent edits detected during preflight abort generation.
182
+
183
+ ## JavaScript API
184
+
185
+ ```javascript
186
+ import { createCompiler } from 'lawspec';
187
+
188
+ const compiler = await createCompiler();
189
+ const result = await compiler.planGeneration({
190
+ sources: [{ path: 'codec.lawspec', content: sourceText }],
191
+ target: 'python'
192
+ });
193
+ if (result.diagnostics.length) console.error(result.diagnostics);
194
+ else console.log(result.files);
195
+ ```
196
+
197
+ `check`, `expand`, and `planGeneration` are asynchronous and share structured
198
+ source/diagnostic types. `planGeneration` returns proposed paths, contents and
199
+ ownership; it does not inspect a host environment or write files. The CLI applies
200
+ compatibility and ownership checks. TypeScript declarations ship in the package.
201
+ The same compiler instance supports repeated and concurrent requests, serialized
202
+ by the JS shim.
203
+
204
+ ## Build and verify
205
+
206
+ ```sh
207
+ stack test
208
+ # With wasm32-wasi-cabal and wasm32-wasi-ghc installed:
209
+ tools/wasm.sh
210
+ node --test npm/test/*.test.mjs
211
+ node tools/parity.mjs
212
+ node tools/build-integrity.mjs
213
+ node tools/package-smoke.mjs
214
+ ```
215
+
216
+ Stack is the native development tool. The WASM build uses GHC's wasm32-wasi backend
217
+ and Cabal, following Rice's Tax. `tools/wasm.sh` reads `~/.ghc-wasm/env` when present.
218
+ The current artifact was built using GHC WASM 9.14.1.20260731; WASM dependencies
219
+ are frozen in `wasm/cabal.project.freeze`. Install the cross compiler through
220
+ [ghc-wasm-meta](https://gitlab.haskell.org/haskell-wasm/ghc-wasm-meta).
221
+
222
+ Haskell's export table generates the JS API and `.d.ts` files. The build records
223
+ compiler-source and artifact hashes in `npm/build.json`; CI rejects stale WASM
224
+ or hand-edited generated wrappers. The npm archive is a self-contained consumer
225
+ artifact, with no install-time compilation or download hook.
226
+
227
+ For all seven native integrations, install their build tools, then:
228
+
229
+ ```sh
230
+ # Set LAWSPEC_GRADLE to a Gradle 9.3.0 executable if it is not on PATH.
231
+ node tools/bootstrap-integration.mjs
232
+ node tools/integration.mjs
233
+ ```
234
+
235
+ These scripts use isolated `.integration/` projects. Bootstrap installs the pinned
236
+ test dependencies; integration verifies that correct adapters pass, broken adapters
237
+ fail, regeneration preserves implementations, and generation leaves build files
238
+ unchanged. Arguments select individual targets. `LAWSPEC_PYTHON=3.14` selects the
239
+ additional Python reference environment. CI also exercises Node 22/24/26 and packs
240
+ and installs the npm archive. Registry publication is a separate release action.
package/api.mjs ADDED
@@ -0,0 +1,6 @@
1
+ // Generated from LawSpec.Gen. Do not edit.
2
+ import { loadCore } from './launcher.mjs';
3
+ export async function createCompiler() {
4
+ const call = await loadCore();
5
+ return { check: (input) => call({ ...input, method: 'check' }), expand: (input) => call({ ...input, method: 'expand' }), planGeneration: (input) => call({ ...input, method: 'planGeneration' }) };
6
+ }
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, mkdir, readdir, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { createCompiler } from "../api.mjs";
5
+ import { targets, templates, commands, setup } from "../templates.mjs";
6
+ import { doctor } from "../doctor.mjs";
7
+ import {
8
+ readOptional,
9
+ planWrites,
10
+ applyWrites,
11
+ atomicWrite,
12
+ safePath,
13
+ } from "../files.mjs";
14
+ const args = process.argv.slice(2);
15
+ const verb = args.shift();
16
+ const options = {};
17
+ const positional = [];
18
+ for (let i = 0; i < args.length; i++) {
19
+ const arg = args[i];
20
+ if (["--target", "--project", "--config"].includes(arg)) {
21
+ if (!args[i + 1] || args[i + 1].startsWith("--"))
22
+ throw new Error(`Missing value for ${arg}`);
23
+ options[arg.slice(2)] = args[++i];
24
+ } else if (["--dry-run", "--check", "--json"].includes(arg))
25
+ options[arg.slice(2)] = true;
26
+ else if (arg.startsWith("--")) throw new Error(`Unknown option: ${arg}`);
27
+ else positional.push(arg);
28
+ }
29
+ const configFile = path.resolve(options.config || "lawspec.json");
30
+ const configRoot = path.dirname(configFile);
31
+ const output = (value) =>
32
+ console.log(
33
+ typeof value === "string" ? value : JSON.stringify(value, null, 2),
34
+ );
35
+ const shellQuote = (value) =>
36
+ "'" + String(value).replaceAll("'", "'\\''") + "'";
37
+ function testCommand(target, root) {
38
+ const testDir = target.testDir || "test";
39
+ let command = commands[target.language];
40
+ if (target.language === "python")
41
+ command = `${shellQuote(target.python || "python3")} -m pytest`;
42
+ if (target.language === "java")
43
+ command = `${shellQuote(target.maven || "mvn")} test`;
44
+ if (target.language === "kotlin")
45
+ command = `${shellQuote(target.gradle || "gradle")} test`;
46
+ if (target.language === "javascript")
47
+ command = `node --test ${shellQuote(testDir)}/*.test.mjs`;
48
+ if (target.language === "typescript")
49
+ command = `npm exec -- tsc -p tsconfig.json && node --test ${shellQuote("dist/" + testDir)}/*.test.js`;
50
+ return `(cd ${shellQuote(root)} && ${command})`;
51
+ }
52
+ function diagnostics(result) {
53
+ if (result.diagnostics?.length)
54
+ throw Object.assign(
55
+ new Error(
56
+ result.diagnostics
57
+ .map(
58
+ (d) =>
59
+ `${d.at ? `${d.at.file}:${d.at.line}:${d.at.column}: ` : ""}${d.code}: ${d.message}`,
60
+ )
61
+ .join("\n"),
62
+ ),
63
+ { diagnostics: result.diagnostics },
64
+ );
65
+ return result;
66
+ }
67
+ async function sources(config) {
68
+ const files = [];
69
+ async function visit(file) {
70
+ const info = await stat(file);
71
+ if (info.isDirectory())
72
+ for (const entry of (await readdir(file)).sort())
73
+ await visit(path.join(file, entry));
74
+ else if (file.endsWith(".lawspec")) files.push(file);
75
+ }
76
+ for (const source of config.sources)
77
+ await visit(path.resolve(configRoot, source));
78
+ if (!files.length) throw new Error("No .lawspec source files found");
79
+ return Promise.all(
80
+ [...new Set(files)].sort().map(async (file) => ({
81
+ path: path.relative(configRoot, file).split(path.sep).join("/"),
82
+ content: await readFile(file, "utf8"),
83
+ })),
84
+ );
85
+ }
86
+ async function init() {
87
+ const language = options.target;
88
+ if (!targets.includes(language))
89
+ throw new Error(`Choose --target ${targets.join("|")}`);
90
+ const root = path.resolve(configRoot, options.project || ".");
91
+ const current = await readOptional(configFile);
92
+ const config =
93
+ current === null
94
+ ? { version: 1, sources: ["laws"], targets: [] }
95
+ : JSON.parse(current);
96
+ if (
97
+ config.targets.some(
98
+ (t) =>
99
+ t.language === language && path.resolve(configRoot, t.root) === root,
100
+ )
101
+ )
102
+ throw new Error("Target is already configured");
103
+ await mkdir(root, { recursive: true });
104
+ const buildFiles = [
105
+ "pom.xml",
106
+ "pyproject.toml",
107
+ "package.json",
108
+ "go.mod",
109
+ "stack.yaml",
110
+ "package.yaml",
111
+ "build.gradle",
112
+ "build.gradle.kts",
113
+ "settings.gradle.kts",
114
+ ];
115
+ const hasBuild = (await readdir(root)).some(
116
+ (f) => buildFiles.includes(f) || f.endsWith(".cabal"),
117
+ );
118
+ const additions = hasBuild ? {} : templates(language);
119
+ for (const name of Object.keys(additions)) {
120
+ const file = await safePath(root, name);
121
+ if ((await readOptional(file)) !== null)
122
+ throw new Error(`Will not overwrite ${file}`);
123
+ }
124
+ const starter = await safePath(configRoot, "laws/atoi_codec.lawspec");
125
+ await safePath(configRoot, path.basename(configFile));
126
+ if (current === null && (await readOptional(starter)) !== null)
127
+ throw new Error(
128
+ "Starter specification already exists; create lawspec.json manually to use it",
129
+ );
130
+ if (!hasBuild && language !== "go")
131
+ await mkdir(path.join(root, "src"), { recursive: true });
132
+ for (const [name, content] of Object.entries(additions))
133
+ await atomicWrite(path.join(root, name), content, true);
134
+ if (current === null)
135
+ await atomicWrite(
136
+ starter,
137
+ await readFile(new URL("../starter.lawspec", import.meta.url), "utf8"),
138
+ true,
139
+ );
140
+ config.targets.push({
141
+ language,
142
+ root: path.relative(configRoot, root) || ".",
143
+ });
144
+ await atomicWrite(
145
+ configFile,
146
+ JSON.stringify(config, null, 2) + "\n",
147
+ current === null,
148
+ );
149
+ output(
150
+ `Configured ${language}. ${hasBuild ? "Existing build files preserved." : "Created missing project build files."}\n${setup[language]}\nNext: lawspec doctor, then lawspec generate.`,
151
+ );
152
+ }
153
+ async function main() {
154
+ if (!verb || ["help", "--help", "-h"].includes(verb)) {
155
+ output(
156
+ "LawSpec 0.1.0\nUsage: lawspec init --target <language> [--project <directory>]\n lawspec check | doctor | explain <unit>::<law> | generate\nOptions: --config <path>, --target <language>, --json\nGeneration: --dry-run, --check\nTargets: " +
157
+ targets.join(", "),
158
+ );
159
+ return;
160
+ }
161
+ if (verb === "--version") {
162
+ output("0.1.0");
163
+ return;
164
+ }
165
+ if (positional.length > (verb === "explain" ? 1 : 0))
166
+ throw new Error(`Unexpected argument: ${positional.join(" ")}`);
167
+ if (verb === "init") return init();
168
+ if (!["check", "doctor", "explain", "generate"].includes(verb))
169
+ throw new Error(`Unknown command: ${verb}`);
170
+ const config = JSON.parse(await readFile(configFile, "utf8"));
171
+ if (
172
+ config.version !== 1 ||
173
+ !Array.isArray(config.sources) ||
174
+ !Array.isArray(config.targets)
175
+ )
176
+ throw new Error(
177
+ "Expected lawspec.json version 1 with sources and targets arrays",
178
+ );
179
+ const selected = config.targets.filter(
180
+ (t) => !options.target || t.language === options.target,
181
+ );
182
+ if (["doctor", "generate"].includes(verb) && !selected.length)
183
+ throw new Error("No matching configured target");
184
+ const roots = selected.map((t) => path.resolve(configRoot, t.root));
185
+ if (new Set(roots).size !== roots.length)
186
+ throw new Error("Each configured target needs its own project root");
187
+ if (verb === "doctor") {
188
+ const reports = await Promise.all(
189
+ selected.map((t, i) => doctor(t, roots[i])),
190
+ );
191
+ output(
192
+ options.json
193
+ ? reports
194
+ : reports
195
+ .map(
196
+ (r) =>
197
+ `${r.target}: ${r.ok ? "ready" : `${r.message}\n${r.instructions}`}`,
198
+ )
199
+ .join("\n"),
200
+ );
201
+ if (reports.some((r) => !r.ok)) process.exitCode = 1;
202
+ return;
203
+ }
204
+ const input = { sources: await sources(config) };
205
+ const compiler = await createCompiler();
206
+ if (verb === "check") {
207
+ const result = diagnostics(await compiler.check(input));
208
+ output(
209
+ options.json
210
+ ? result
211
+ : `Checked ${result.laws.length} executable law(s).`,
212
+ );
213
+ return;
214
+ }
215
+ if (verb === "explain") {
216
+ const result = diagnostics(await compiler.expand(input));
217
+ const indices = result.laws
218
+ .map((e, i) => ({ e, i }))
219
+ .filter(
220
+ ({ e }) => !positional[0] || `${e.owner}::${e.name}` === positional[0],
221
+ );
222
+ if (!indices.length) throw new Error("No matching executable law");
223
+ output(
224
+ options.json
225
+ ? indices.map(({ e, i }) => ({ ...e, expansion: result.expansions[i] }))
226
+ : indices
227
+ .map(
228
+ ({ e, i }) =>
229
+ `${e.owner}::${e.name}\n${e.trace.join("\n=> ")}\n=> ${result.expansions[i]}`,
230
+ )
231
+ .join("\n\n"),
232
+ );
233
+ return;
234
+ }
235
+ if (options["dry-run"] && options.check)
236
+ throw new Error("--dry-run and --check are mutually exclusive");
237
+ const artifacts = [];
238
+ for (const target of selected)
239
+ artifacts.push(
240
+ diagnostics(
241
+ await compiler.planGeneration({
242
+ ...input,
243
+ target: target.language,
244
+ sourceDir: target.sourceDir,
245
+ testDir: target.testDir,
246
+ }),
247
+ ).files,
248
+ );
249
+ const reports = await Promise.all(
250
+ selected.map((t, i) => doctor(t, roots[i])),
251
+ );
252
+ const failed = reports.filter((r) => !r.ok);
253
+ if (failed.length)
254
+ throw new Error(
255
+ failed
256
+ .map((r) => `${r.target}: ${r.message}\n${r.instructions}`)
257
+ .join("\n"),
258
+ );
259
+ const plans = await Promise.all(
260
+ artifacts.map((files, i) => planWrites(roots[i], files)),
261
+ );
262
+ if (!options["dry-run"] && !options.check) await applyWrites(plans);
263
+ const summary = plans.map((plan, i) => ({
264
+ target: selected[i].language,
265
+ changes: plan.changes.map((c) => ({ action: c.action, path: c.relative })),
266
+ preservedAdapters: plan.preserved,
267
+ adapterUpdates: plan.adapterUpdates,
268
+ test: testCommand(selected[i], roots[i]),
269
+ }));
270
+ output(
271
+ options.json
272
+ ? summary
273
+ : summary
274
+ .map(
275
+ (r) =>
276
+ `${r.target}: ${r.changes.length} ${options["dry-run"] || options.check ? "planned" : "applied"} change(s), ${r.preservedAdapters.length} user adapter(s) preserved.${r.adapterUpdates.length ? "\nReview required adapter signatures:\n" + r.adapterUpdates.map((a) => a.path + "\n" + a.requiredAdapter).join("\n") : ""}\nTest: ${r.test}`,
277
+ )
278
+ .join("\n"),
279
+ );
280
+ if (options.check && plans.some((p) => p.changes.length))
281
+ process.exitCode = 1;
282
+ }
283
+ main().catch((error) => {
284
+ if (options.json)
285
+ output({
286
+ diagnostics: error.diagnostics || [
287
+ { code: "cli", message: error.message, at: null },
288
+ ],
289
+ });
290
+ else console.error(`lawspec: ${error.message}`);
291
+ process.exitCode = 1;
292
+ });
package/build.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "version": 1,
3
+ "compilerSources": [
4
+ "package.yaml",
5
+ "src/LawSpec/Api.hs",
6
+ "src/LawSpec/Compile.hs",
7
+ "src/LawSpec/Emit.hs",
8
+ "src/LawSpec/Gen.hs",
9
+ "src/LawSpec/Model.hs",
10
+ "src/LawSpec/Parser.hs",
11
+ "src/LawSpec/Prelude.hs",
12
+ "stack.yaml",
13
+ "stack.yaml.lock",
14
+ "wasm/app/Exports.hs",
15
+ "wasm/cabal.project",
16
+ "wasm/cabal.project.freeze",
17
+ "wasm/lawspec-wasm.cabal"
18
+ ],
19
+ "digests": {
20
+ "package.yaml": "cb68fcf21def8f7a880bbab7088208a86fcf313a83fe2c02e18957872cf65c67",
21
+ "src/LawSpec/Api.hs": "00b1e4bc81231ebd387f10652463904084ebf9c3c4486fd2c2217a6d4dbfc157",
22
+ "src/LawSpec/Compile.hs": "5ea31f4a66df87ddba2ff0b498d12eea5cbbefb0e10e3bf1c1d2bf4a88f5792e",
23
+ "src/LawSpec/Emit.hs": "27ba0cafbea8d5a82aac439f4e72f98e003cfdc177f99a902f8d11f033684b8e",
24
+ "src/LawSpec/Gen.hs": "b283001bc18971ddb2159f2ec7e51ac58aa7e191b52577006140694b09a06987",
25
+ "src/LawSpec/Model.hs": "8f501e134cd1480261ae8bb9e3df8255efd38a02eb883468c6bc2c342e200970",
26
+ "src/LawSpec/Parser.hs": "f546502c6eedb0a4ccd30d11e3b017aedf5a7145080ea58cfd34753fd9b509c6",
27
+ "src/LawSpec/Prelude.hs": "d4b0ae08eab0f30b4b8c819ce360f7cccb1f77d5ea38ded3b88dbb07936b10b6",
28
+ "stack.yaml": "20ccf4d599e355e60b7aa4f814a7cd4299fe2048616cc2e6dbdc22a7bd8cec73",
29
+ "stack.yaml.lock": "ae222b9c81af920c56e50fa4596fa57786e7fa5a7b461390362b2b2ff63818c7",
30
+ "wasm/app/Exports.hs": "4ecbdac8faa2449e6fc61b93c82e6278fa14374154f29b4dacf0eef433f06a47",
31
+ "wasm/cabal.project": "021e560afdc5eb4cb7169e7119ecb8f92c9ee170245af909c94b612941bff5cc",
32
+ "wasm/cabal.project.freeze": "733dbed3d2ecccb26e874fd58f136296dad772184deb1658c196b5d54a0814dc",
33
+ "wasm/lawspec-wasm.cabal": "d11ccf7991e406f0fd26f124c54d6552613b892328f8e22a964e38d03c7d6de4",
34
+ "npm/core.wasm": "14d88c08f92b10ac2939d6b92bc428b0ee255d56e88890fd46abb1e959a26b2b",
35
+ "npm/core_jsffi.js": "88d136efe92f7cff5758c8fec8d9b6bbc9707fe37741cebeeb415fe34ce3d72b",
36
+ "npm/api.mjs": "d6df654600172131ac66a55b86876fc29cde2a76a0a63bbefff3183488a4837b",
37
+ "npm/index.d.ts": "8003304d88dbb01a07bd3dbdc4d680a193db1ec323c5712b9860e166ca9cc94b"
38
+ }
39
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "version": 1,
3
+ "targets": {
4
+ "java": {
5
+ "java": ["25", "26"],
6
+ "jetCheck": ["0.3.0", "0.3.1"],
7
+ "junit-jupiter": ["5.14.0", "5.15.0"]
8
+ },
9
+ "python": {
10
+ "python": ["3.13", "3.15"],
11
+ "pytest": ["8.4.0", "8.5.0"],
12
+ "hypothesis": ["6.135.26", "7"]
13
+ },
14
+ "javascript": {
15
+ "fast-check": ["4", "5"]
16
+ },
17
+ "typescript": {
18
+ "fast-check": ["4", "5"],
19
+ "typescript": ["5.9", "5.10"],
20
+ "@types/node": ["22", "23"]
21
+ },
22
+ "go": {
23
+ "go": ["1.22", "1.27"],
24
+ "rapid": ["1.2.0", "1.2.1"]
25
+ },
26
+ "haskell": {
27
+ "hspec": ["2.11", "2.12"],
28
+ "hedgehog": ["1.5", "1.6"],
29
+ "hspec-hedgehog": ["0.3.0.0", "0.3.0.1"],
30
+ "ghc": ["9.10", "9.11"]
31
+ },
32
+ "kotlin": {
33
+ "java": ["25", "26"],
34
+ "gradle": ["9.1", "9.4"],
35
+ "kotlin": ["2.3.21", "2.3.22"]
36
+ }
37
+ }
38
+ }
package/core.wasm ADDED
Binary file