lawspec 0.8.0 → 0.10.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 (61) hide show
  1. package/API-MIGRATION.md +180 -3
  2. package/GO.md +144 -0
  3. package/HASKELL.md +171 -0
  4. package/JAVA.md +113 -0
  5. package/KOTLIN.md +214 -0
  6. package/LANGUAGE.md +240 -12
  7. package/NATIVE-BINDINGS.md +1046 -0
  8. package/PRIMITIVES.md +1 -1
  9. package/PYTHON.md +152 -0
  10. package/README.md +86 -23
  11. package/REFINEMENTS.md +289 -3
  12. package/RELEASE-0.10.md +60 -0
  13. package/RELEASE-0.9.md +61 -0
  14. package/RUST.md +115 -1
  15. package/WEB.md +144 -0
  16. package/api.mjs +28 -2
  17. package/bin/lawspec.mjs +23 -7
  18. package/build.json +199 -37
  19. package/core.wasm +0 -0
  20. package/examples/native-payments/go/example/payments/domain.go +38 -0
  21. package/examples/native-payments/go/example/payments/native_generators_test.go +13 -0
  22. package/examples/native-payments/go/lawspec.json +136 -0
  23. package/examples/native-payments/haskell/lawspec.json +149 -0
  24. package/examples/native-payments/haskell/src/PaymentsDomain.hs +21 -0
  25. package/examples/native-payments/haskell/test/PaymentGenerators.hs +12 -0
  26. package/examples/native-payments/java/lawspec.json +165 -0
  27. package/examples/native-payments/java/src/main/java/domain/PaymentsDomain.java +32 -0
  28. package/examples/native-payments/java/src/test/java/domain/PaymentGenerators.java +16 -0
  29. package/examples/native-payments/javascript/lawspec.json +149 -0
  30. package/examples/native-payments/javascript/src/payments_domain.mjs +44 -0
  31. package/examples/native-payments/javascript/test/lawspec_generators.mjs +6 -0
  32. package/examples/native-payments/kotlin/lawspec.json +165 -0
  33. package/examples/native-payments/kotlin/src/main/kotlin/domain/PaymentsDomain.kt +17 -0
  34. package/examples/native-payments/kotlin/src/test/kotlin/domain/PaymentGenerators.kt +12 -0
  35. package/examples/native-payments/python/lawspec.json +149 -0
  36. package/examples/native-payments/python/src/payments_domain.py +60 -0
  37. package/examples/native-payments/python/tests/lawspec_generators.py +15 -0
  38. package/examples/native-payments/rust/lawspec.json +167 -0
  39. package/examples/native-payments/rust/src/domain.rs +37 -0
  40. package/examples/native-payments/rust/src/lib.rs +2 -0
  41. package/examples/native-payments/rust/tests/support/lawspec_generators.rs +11 -0
  42. package/examples/native-payments/typescript/lawspec.json +149 -0
  43. package/examples/native-payments/typescript/src/payments_domain.ts +42 -0
  44. package/examples/native-payments/typescript/test/lawspec_generators.ts +8 -0
  45. package/examples/specs/collections.lawspec +67 -0
  46. package/examples/specs/data_types.lawspec +66 -0
  47. package/examples/specs/finite_data.lawspec +37 -0
  48. package/examples/specs/list_contracts.lawspec +136 -0
  49. package/examples/specs/list_refinements.lawspec +51 -0
  50. package/examples/specs/matching.lawspec +49 -0
  51. package/examples/specs/payments.lawspec +76 -0
  52. package/examples/specs/recursive_refinements.lawspec +41 -0
  53. package/examples/specs/refined_definitions.lawspec +44 -0
  54. package/examples/specs/sum_refinements.lawspec +49 -0
  55. package/examples/specs/total_functions.lawspec +101 -0
  56. package/examples-command.mjs +11 -1
  57. package/files.mjs +16 -7
  58. package/index.d.ts +297 -27
  59. package/native-examples.mjs +81 -0
  60. package/package.json +2 -2
  61. package/templates.mjs +107 -17
@@ -0,0 +1,81 @@
1
+ import {readFile, readdir, mkdir} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {templates, commands, setup} from './templates.mjs';
4
+ import {safePath, planWrites, applyWrites} from './files.mjs';
5
+
6
+ async function projectFiles(directory, prefix = '') {
7
+ const files = {};
8
+ for (const entry of await readdir(directory, {withFileTypes: true})) {
9
+ const name = prefix + entry.name;
10
+ const location = new URL(entry.name + (entry.isDirectory() ? '/' : ''), directory);
11
+ if (entry.isDirectory()) Object.assign(files, await projectFiles(location, name + '/'));
12
+ else if (entry.isFile()) files[name] = await readFile(location, 'utf8');
13
+ else throw new Error(`Unsupported bundled example entry: ${name}`);
14
+ }
15
+ return files;
16
+ }
17
+
18
+ export async function exportNativePayments(options, selected) {
19
+ const root = await safePath(process.cwd(), options.output || 'native_payments');
20
+ await mkdir(root, {recursive: true});
21
+ const law = await readFile(new URL('./examples/specs/payments.lawspec', import.meta.url), 'utf8');
22
+ const plans = [];
23
+ const results = [];
24
+ for (const target of selected) {
25
+ const directory = await safePath(root, target);
26
+ await mkdir(directory, {recursive: true});
27
+ const files = {
28
+ ...templates(target, {minify: options.minify === true}),
29
+ ...await projectFiles(new URL(`./examples/native-payments/${target}/`, import.meta.url)),
30
+ 'laws/payments.lawspec': law,
31
+ };
32
+ const config = JSON.parse(files['lawspec.json']);
33
+ config.machineBits = options.machineBits ?? 64;
34
+ files['lawspec.json'] = JSON.stringify(config, null, options.minify ? undefined : 2) + '\n';
35
+ files['README.md'] = `# Native payments: ${target}
36
+
37
+ This project checks application-owned payment types against the shared LawSpec
38
+ model. Money maps to Price, currency variants have application names, and the
39
+ archive preserves distinct present and absent payments.
40
+
41
+ ## Run
42
+
43
+ Install LawSpec and the native dependencies described below, then run:
44
+
45
+ \`\`\`sh
46
+ lawspec check
47
+ lawspec generate
48
+ ${commands[target]}
49
+ \`\`\`
50
+
51
+ ${setup[target]}
52
+
53
+ JavaScript and TypeScript projects require \`npm install\` before testing.
54
+ The specification includes exact decimal arithmetic and explicit USD/GBP examples.
55
+ The native price generator deliberately samples only EUR values between 1.00 and
56
+ 2.00; examples and deterministic boundaries still check the other cases. Its
57
+ framework's mapping operation preserves the native shrinker.
58
+
59
+ The application types use native exact decimals where available and LawSpec's
60
+ framework-independent numeric runtime otherwise. Run generation before compiling
61
+ the application so that runtime support is present.
62
+
63
+ ## Change the application
64
+
65
+ Edit the domain implementation or generator, then run the native tests again.
66
+ For example, change the fee from 0.2 to 0.3: the exact-decimal law must fail.
67
+ Edit \`lawspec.json\` to change native bindings and rerun \`lawspec generate\`.
68
+
69
+ All exported project files are user-owned. Re-exporting preserves your edits;
70
+ it reports changed bundled versions for review. Compiler-generated files use a
71
+ separate ownership manifest and remain protected against accidental overwrites.
72
+ `;
73
+ const artifacts = Object.entries(files).map(([path, content]) => ({path, content, ownership: 'user'}));
74
+ const plan = await planWrites(directory, artifacts, {manifest: '.lawspec/example.json'});
75
+ plans.push(plan);
76
+ results.push({target, directory, example: 'payments', files: artifacts.map(({path, ownership}) => ({path, ownership})),
77
+ changes: plan.changes.length, preservedAdapters: plan.preserved, adapterUpdates: plan.adapterUpdates});
78
+ }
79
+ await applyWrites(plans);
80
+ return results;
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lawspec",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "State the law once. Check it everywhere.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,6 +8,6 @@
8
8
  "bin": { "lawspec": "bin/lawspec.mjs" },
9
9
  "exports": { ".": { "types": "./index.d.ts", "import": "./api.mjs" } },
10
10
  "types": "index.d.ts",
11
- "files": ["*.mjs", "*.json", "index.d.ts", "bin", "core.wasm", "core_jsffi.js", "README.md", "PRIMITIVES.md", "REFINEMENTS.md", "RUST.md", "LANGUAGE.md", "API-MIGRATION.md", "LICENSE", "starter.lawspec", "examples"],
11
+ "files": ["*.mjs", "*.json", "index.d.ts", "bin", "core.wasm", "core_jsffi.js", "README.md", "NATIVE-BINDINGS.md", "PRIMITIVES.md", "REFINEMENTS.md", "RUST.md", "JAVA.md", "PYTHON.md", "GO.md", "HASKELL.md", "KOTLIN.md", "WEB.md", "LANGUAGE.md", "API-MIGRATION.md", "RELEASE-0.9.md", "RELEASE-0.10.md", "LICENSE", "starter.lawspec", "examples"],
12
12
  "scripts": { "test": "node --test test/*.test.mjs" }
13
13
  }
package/templates.mjs CHANGED
@@ -34,8 +34,97 @@ export const setup = {
34
34
  kotlin:
35
35
  "Use JDK 25, Gradle 9.3.0, Kotlin plugin 2.3.21, JVM target 25, Kotest 5.9.1 (runner, assertions, property), and useJUnitPlatform(). Run gradle testClasses.",
36
36
  };
37
- const json = (value) => JSON.stringify(value, null, 2) + "\n";
38
- export function templates(target) {
37
+ // These render known scaffold structure; they never reformat user source.
38
+ const element = (name, value, attributes = {}) => ({ name, value, attributes });
39
+ const xmlEscape = (text) =>
40
+ String(text)
41
+ .replaceAll("&", "&")
42
+ .replaceAll('"', """)
43
+ .replaceAll("<", "&lt;")
44
+ .replaceAll(">", "&gt;");
45
+ function renderXml(node, minify, depth = 0) {
46
+ const indent = minify ? "" : " ".repeat(depth);
47
+ const attributes = Object.entries(node.attributes).map(
48
+ ([key, value]) => `${key}="${xmlEscape(value)}"`,
49
+ );
50
+ const open = `<${node.name}${
51
+ attributes.length === 0
52
+ ? ""
53
+ : minify
54
+ ? " " + attributes.join(" ")
55
+ : "\n" + attributes.map((a) => indent + " " + a).join("\n")
56
+ }>`;
57
+ if (!Array.isArray(node.value)) {
58
+ return `${indent}${open}${xmlEscape(node.value)}</${node.name}>`;
59
+ }
60
+ const children = node.value.map((child) =>
61
+ renderXml(child, minify, depth + 1),
62
+ );
63
+ return minify
64
+ ? `${open}${children.join("")}</${node.name}>`
65
+ : `${indent}${open}\n${children.join("\n")}\n${indent}</${node.name}>`;
66
+ }
67
+ function mavenProject(minify) {
68
+ const fields = (values) =>
69
+ Object.entries(values).map(([name, value]) => element(name, value));
70
+ const dependency = (groupId, artifactId, version) =>
71
+ element(
72
+ "dependency",
73
+ fields({ groupId, artifactId, version, scope: "test" }),
74
+ );
75
+ const plugin = (artifactId, version) =>
76
+ element(
77
+ "plugin",
78
+ fields({ groupId: "org.apache.maven.plugins", artifactId, version }),
79
+ );
80
+ return (
81
+ renderXml(
82
+ element(
83
+ "project",
84
+ [
85
+ ...fields({
86
+ modelVersion: "4.0.0",
87
+ groupId: "example",
88
+ artifactId: "lawspec-example",
89
+ version: "0.1.0",
90
+ }),
91
+ element(
92
+ "properties",
93
+ fields({
94
+ "maven.compiler.release": "25",
95
+ "project.build.sourceEncoding": "UTF-8",
96
+ }),
97
+ ),
98
+ element("dependencies", [
99
+ dependency("org.jetbrains", "jetCheck", "0.3.0"),
100
+ dependency("org.junit.jupiter", "junit-jupiter", "5.14.0"),
101
+ ]),
102
+ element("build", [
103
+ element("plugins", [
104
+ plugin("maven-compiler-plugin", "3.14.1"),
105
+ plugin("maven-surefire-plugin", "3.5.4"),
106
+ ]),
107
+ ]),
108
+ ],
109
+ {
110
+ xmlns: "http://maven.apache.org/POM/4.0.0",
111
+ "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
112
+ "xsi:schemaLocation":
113
+ "http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd",
114
+ },
115
+ ),
116
+ minify,
117
+ ) + "\n"
118
+ );
119
+ }
120
+ export function templates(target, { minify = false } = {}) {
121
+ if (typeof minify !== "boolean") throw new Error("minify must be a boolean");
122
+ const json = (value) =>
123
+ JSON.stringify(value, null, minify ? undefined : 2) + "\n";
124
+ const kotlinBlock = (name, statements) =>
125
+ minify
126
+ ? `${name} { ${statements.join("; ")} }`
127
+ : `${name} {\n${statements.map((line) => " " + line).join("\n")}\n}`;
39
128
  if (!targets.includes(target)) throw new Error(`Unknown target: ${target}`);
40
129
  const commonPackage = {
41
130
  name: "lawspec-example",
@@ -46,7 +135,8 @@ export function templates(target) {
46
135
  switch (target) {
47
136
  case "rust":
48
137
  return {
49
- "src/lib.rs": '// Application library. LawSpec maintains the included module declarations.\ninclude!("lawspec_modules.rs");\n',
138
+ "src/lib.rs":
139
+ '// Application library. LawSpec maintains the included module declarations.\ninclude!("lawspec_modules.rs");\n',
50
140
  "Cargo.toml": `[package]
51
141
  name = "lawspec-example"
52
142
  version = "0.1.0"
@@ -103,18 +193,7 @@ proptest = "=1.11.0"
103
193
  };
104
194
  case "java":
105
195
  return {
106
- "pom.xml": `<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
107
- <modelVersion>4.0.0</modelVersion><groupId>example</groupId><artifactId>lawspec-example</artifactId><version>0.1.0</version>
108
- <properties><maven.compiler.release>25</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
109
- <dependencies>
110
- <dependency><groupId>org.jetbrains</groupId><artifactId>jetCheck</artifactId><version>0.3.0</version><scope>test</scope></dependency>
111
- <dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.14.0</version><scope>test</scope></dependency>
112
- </dependencies>
113
- <build><plugins>
114
- <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.1</version></plugin>
115
- <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.4</version></plugin>
116
- </plugins></build>
117
- </project>\n`,
196
+ "pom.xml": mavenProject(minify),
118
197
  };
119
198
  case "go":
120
199
  return {
@@ -124,13 +203,24 @@ proptest = "=1.11.0"
124
203
  case "haskell":
125
204
  return {
126
205
  "stack.yaml": "snapshot: lts-24.58\npackages: [.]\n",
127
- "package.yaml": `name: lawspec-example\nversion: 0.1.0\ndependencies: [base, text, bytestring]\nlibrary:\n source-dirs: src\ntests:\n laws:\n main: Spec.hs\n source-dirs: test\n dependencies: [lawspec-example, hspec, hedgehog, hspec-hedgehog]\n build-tools: [hspec-discover]\n`,
206
+ "package.yaml": `name: lawspec-example\nversion: 0.1.0\ndependencies: [base, text, bytestring]\nlibrary:\n source-dirs: src\ntests:\n laws:\n main: Spec.hs\n source-dirs: test\n dependencies: [lawspec-example, hspec, hedgehog, hspec-hedgehog, containers, mtl]\n build-tools: [hspec-discover]\n`,
128
207
  "test/Spec.hs": "{-# OPTIONS_GHC -F -pgmF hspec-discover #-}\n",
129
208
  };
130
209
  case "kotlin":
131
210
  return {
132
211
  "settings.gradle.kts": 'rootProject.name = "lawspec-example"\n',
133
- "build.gradle.kts": `plugins { kotlin("jvm") version "2.3.21" }\nrepositories { mavenCentral() }\nkotlin { jvmToolchain(25) }\ndependencies {\n testImplementation("io.kotest:kotest-runner-junit5:5.9.1")\n testImplementation("io.kotest:kotest-assertions-core:5.9.1")\n testImplementation("io.kotest:kotest-property:5.9.1")\n}\ntasks.test { useJUnitPlatform() }\n`,
212
+ "build.gradle.kts":
213
+ [
214
+ kotlinBlock("plugins", ['kotlin("jvm") version "2.3.21"']),
215
+ kotlinBlock("repositories", ["mavenCentral()"]),
216
+ kotlinBlock("kotlin", ["jvmToolchain(25)"]),
217
+ kotlinBlock("dependencies", [
218
+ 'testImplementation("io.kotest:kotest-runner-junit5:5.9.1")',
219
+ 'testImplementation("io.kotest:kotest-assertions-core:5.9.1")',
220
+ 'testImplementation("io.kotest:kotest-property:5.9.1")',
221
+ ]),
222
+ kotlinBlock("tasks.test", ["useJUnitPlatform()"]),
223
+ ].join(minify ? "\n" : "\n\n") + "\n",
134
224
  };
135
225
  }
136
226
  }