tslaws 0.6.4 → 0.7.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 (68) hide show
  1. package/README.md +8 -8
  2. package/dist/api.d.ts +10 -0
  3. package/dist/api.js +7 -0
  4. package/dist/browser.d.ts +2 -2
  5. package/dist/browser.js +2 -6
  6. package/dist/check.d.ts +2 -0
  7. package/dist/check.js +133 -41
  8. package/dist/cli.d.ts +18 -0
  9. package/dist/cli.js +149 -54
  10. package/dist/compile.d.ts +12 -0
  11. package/dist/compile.js +207 -6
  12. package/dist/coverage.d.ts +13 -1
  13. package/dist/coverage.js +16 -23
  14. package/dist/derived.d.ts +8 -0
  15. package/dist/derived.js +52 -0
  16. package/dist/e2e.d.ts +0 -9
  17. package/dist/e2e.js +0 -20
  18. package/dist/generators.d.ts +8 -8
  19. package/dist/generators.js +59 -99
  20. package/dist/globs.d.ts +3 -0
  21. package/dist/globs.js +24 -0
  22. package/dist/init.js +5 -5
  23. package/dist/lawFiles.d.ts +21 -0
  24. package/dist/lawFiles.js +95 -0
  25. package/dist/lawKinds.d.ts +38 -0
  26. package/dist/lawKinds.js +210 -0
  27. package/dist/lawNames.d.ts +6 -0
  28. package/dist/lawNames.js +22 -0
  29. package/dist/list.d.ts +22 -0
  30. package/dist/list.js +36 -0
  31. package/dist/lock.d.ts +16 -3
  32. package/dist/lock.js +23 -4
  33. package/dist/lower.d.ts +10 -0
  34. package/dist/lower.js +178 -0
  35. package/dist/mcp.d.ts +2 -8
  36. package/dist/mcp.js +40 -61
  37. package/dist/migrate/arbitraries.d.ts +23 -0
  38. package/dist/migrate/arbitraries.js +202 -0
  39. package/dist/migrate/convert.d.ts +20 -0
  40. package/dist/migrate/convert.js +915 -0
  41. package/dist/migrate/framework.d.ts +48 -0
  42. package/dist/migrate/framework.js +249 -0
  43. package/dist/migrate/index.d.ts +16 -0
  44. package/dist/migrate/index.js +155 -0
  45. package/dist/migrate/report.d.ts +35 -0
  46. package/dist/migrate/report.js +40 -0
  47. package/dist/migrate/source.d.ts +42 -0
  48. package/dist/migrate/source.js +163 -0
  49. package/dist/migrate/tests.d.ts +44 -0
  50. package/dist/migrate/tests.js +184 -0
  51. package/dist/plan.d.ts +50 -0
  52. package/dist/plan.js +118 -0
  53. package/dist/pool.d.ts +21 -0
  54. package/dist/pool.js +54 -0
  55. package/dist/promote.d.ts +30 -2
  56. package/dist/promote.js +133 -7
  57. package/dist/property.d.ts +12 -26
  58. package/dist/property.js +33 -74
  59. package/dist/propertyRun.d.ts +24 -0
  60. package/dist/propertyRun.js +56 -0
  61. package/dist/propertyWorker.js +7 -9
  62. package/dist/proposals.d.ts +43 -0
  63. package/dist/proposals.js +192 -0
  64. package/dist/regressions.js +23 -2
  65. package/dist/smt.js +9 -8
  66. package/dist/types.d.ts +2 -0
  67. package/hooks/AGENTS.snippet.md +2 -2
  68. package/package.json +8 -6
package/README.md CHANGED
@@ -133,7 +133,7 @@ Anything else leaves the law `tested`, or a `law proved` `open`, with the reason
133
133
 
134
134
  ## Browser laws
135
135
 
136
- A law file named `*.browser.laws.ts` runs in a real browser (Chrome, Edge or Chromium; `TSLAWS_BROWSER` names another
136
+ A laws file that imports `tslaws/browser` (directly or through a helper module of yours) runs in a real browser (Chrome, Edge or Chromium; `TSLAWS_BROWSER` names another
137
137
  executable). Node generates and shrinks the inputs; each one is sent to a page where the compiled law and your code run
138
138
  against a live DOM. The law may be `async`. `tslaws/browser` has `mount`, `settle`, `click`, `type` and `fire`, and works with
139
139
  any framework or none: `mount(el => render(el))` accepts whatever renders into an element and may return its unmount.
@@ -141,7 +141,7 @@ any framework or none: `mount(el => render(el))` accepts whatever renders into a
141
141
  `mountComponent` with the React, Angular, DOM and custom-element adapters tests one component in isolation with props generated from its props type (function-typed props included), plus `spy`, `rendersSame` and Testing Library style queries.
142
142
  Browser laws are `tested`, never `proved`. See [docs/laws.md](../../docs/laws.md#browser-laws).
143
143
 
144
- A `*.browser.laws.ts` file that imports `tslaws/e2e` instead is an end-to-end law: it runs in Node and drives your whole running app through a real browser (`page()`, `open`, `newPage`, `route`, `settle`, `accessible`). Configure `e2e.server` in `tslaws.json` and the runner starts and stops your dev server. See [End-to-end laws](../../docs/laws.md#end-to-end-laws).
144
+ A laws file that imports `tslaws/e2e` instead is an end-to-end law (a file that reaches both is an error; by default the level comes from the name, `.browser.laws.ts` or `.e2e.laws.ts`, and `"levels": "imports"` in `tslaws.json` makes the imports decide instead): it runs in Node and drives your whole running app through a real browser (`page()`, `open`, `newPage`, `route`, `settle`, `accessible`). Configure `e2e.server` in `tslaws.json` and the runner starts and stops your dev server. See [End-to-end laws](../../docs/laws.md#end-to-end-laws).
145
145
 
146
146
  - **Frameworks:** `tslaws.json` `compilerOptions` (`jsx`, `jsxFactory`, `jsxFragmentFactory`, `jsxImportSource`, `experimentalDecorators`,
147
147
  `emitDecoratorMetadata`, `useDefineForClassFields`, `lib`, `types`, `strict`, ...) and `.tsx` sources let React, Angular and any DOM
@@ -166,8 +166,8 @@ See [docs/laws.md](../../docs/laws.md#the-dashboard).
166
166
 
167
167
  `tsc-laws.lock` holds a hash of every law declaration and the list of coverage exemptions. A change, an added law, a removed
168
168
  law or a new `@tslaws-exempt` fails the check until a person runs `tslaws accept`, which refuses to run without a terminal,
169
- under `CI`, or with `TSLAWS_AGENT` set, unless a person has lent it for a limited time with `tslaws delegate grant`. Agents propose laws in `*.proposed.laws.ts` and try them with `tslaws check
170
- --proposed` (no lock, no coverage gate); a person moves them into a `*.laws.ts` file and accepts.
169
+ under `CI`, or with `TSLAWS_AGENT` set, unless a person has lent it for a limited time with `tslaws delegate grant`. Agents propose laws in `*.proposed.laws.ts` files (or optionally in `.tslaws/proposals/`, outside the tree and git-ignored; `tslaws propose` or the MCP tool) and try them with `tslaws check
170
+ --proposed` (no lock, no coverage gate); a person moves one into the tree with `tslaws promote` and accepts. Every laws file in the tree must be in the lock.
171
171
 
172
172
  `hooks/claude-hook.mjs` is a Claude Code hook that blocks `git commit` and sends an agent back to work when the check fails;
173
173
  `hooks/claude-settings.example.json` wires it up and `hooks/AGENTS.snippet.md` tells the agent the rules.
@@ -199,13 +199,13 @@ tslaws check [files...] [--json] [--summary] [--watch] [--runs N] [--seed N] [--
199
199
  - `check --reporter text|json|junit|github` picks the output: `junit` is JUnit XML for CI test reports, `github` prints workflow
200
200
  annotations and appends a table to `GITHUB_STEP_SUMMARY`. See "CI reporters" in `docs/laws.md`.
201
201
 
202
- - `tslaws mcp` serves `tslaws_check`, `tslaws_check_proposed`, `tslaws_explain` and `tslaws_propose` over MCP (stdio) for
203
- agents; it cannot accept laws or touch the lock. `tslaws promote` is the person's step that moves proposed laws into the
204
- accepted file. See [docs/laws.md](../../docs/laws.md#agent-integration-mcp).
202
+ - `tslaws mcp` serves `tslaws_check`, `tslaws_check_proposed`, `tslaws_explain`, `tslaws_list` and `tslaws_propose` over MCP (stdio) for
203
+ agents; it cannot accept laws, promote a proposal or touch the lock. `tslaws promote <proposal> [--to PATH]` is the person's step that moves a proposal into the
204
+ tree; `tslaws propose <file.ts>` writes one; `tslaws list [--json]` says which files are laws files, at what level, and whether each is accepted. See [docs/laws.md](../../docs/laws.md#agent-integration-mcp).
205
205
  - Counterexamples are kept in `.tslaws/regressions` (committable) and replayed before the random search, so a fixed bug cannot
206
206
  return unnoticed. `check --no-regressions` skips the corpus; `tslaws regressions [list|clear]` shows or deletes it.
207
207
 
208
- Without files, every `*.laws.ts` under the current directory is used. The compiler is `--compiler`, `TSC_LAWS_BIN`, or
208
+ Without files, every `*.laws.ts` under the current directory is used, and every file the `"laws"` globs of `tslaws.json` pick that declares a law. The compiler is `--compiler`, `TSC_LAWS_BIN`, or
209
209
  `tsc-laws` on `PATH`; it finds its lib files through `TSC_TS_LIB_DIR`, or the TypeScript 7 libs scriptc installs in
210
210
  `node_modules/@scriptc/*`.
211
211
 
package/dist/api.d.ts CHANGED
@@ -57,3 +57,13 @@ type Registry = Map<string, fc.Arbitrary<unknown>>;
57
57
  export declare function arbitraryRegistry(): Registry;
58
58
  /** Registers the generator used for every parameter whose type is named `typeName`. */
59
59
  export declare function arbitrary<T>(typeName: string, generator: fc.Arbitrary<T>): void;
60
+ /** What a law may return: `false` fails it, anything else holds. A law may be async (browser laws wait for the page). */
61
+ export type LawBody = (...args: any[]) => boolean | void | Promise<boolean | void>;
62
+ export interface LawDeclare {
63
+ /** Declares a law: `law('name', (x: int) => holds(x))`. Parameters need type annotations, inputs are generated from them. */
64
+ (name: string, body: LawBody): void;
65
+ /** A law the prover must prove for every input; `open` when it cannot. */
66
+ proved(name: string, body: LawBody): void;
67
+ }
68
+ /** Declares a law. `tslaws` lowers these calls to checked functions before it runs them; they never run as written. */
69
+ export declare const law: LawDeclare;
package/dist/api.js CHANGED
@@ -101,3 +101,10 @@ export function arbitraryRegistry() {
101
101
  export function arbitrary(typeName, generator) {
102
102
  arbitraryRegistry().set(typeName, generator);
103
103
  }
104
+ function notRun() {
105
+ throw new Error('law() declares a specification and is run by `tslaws check`, not called at runtime');
106
+ }
107
+ /** Declares a law. `tslaws` lowers these calls to checked functions before it runs them; they never run as written. */
108
+ export const law = Object.assign((_name, _body) => notRun(), {
109
+ proved: (_name, _body) => notRun(),
110
+ });
package/dist/browser.d.ts CHANGED
@@ -2,8 +2,6 @@ import { type Browser } from 'playwright-core';
2
2
  import type * as fc from 'fast-check';
3
3
  import { type PropertyOutcome } from './property.ts';
4
4
  import type { CheckOptions } from './types.ts';
5
- /** `a.browser.laws.ts` runs in a browser. */
6
- export declare function isBrowserLawFile(file: string): boolean;
7
5
  export type BrowserEngine = 'chromium' | 'firefox' | 'webkit';
8
6
  /** The engine to use: the requested one, else `TSLAWS_BROWSER_ENGINE`, else chromium. Throws on an unknown name. */
9
7
  export declare function resolveEngine(requested?: string, env?: NodeJS.ProcessEnv): BrowserEngine;
@@ -17,6 +15,8 @@ export declare function closeBrowser(): Promise<void>;
17
15
  export interface BrowserLaw {
18
16
  file: string;
19
17
  name: string;
18
+ /** The exported function the law lowers to; defaults to `name`. */
19
+ symbol?: string;
20
20
  jsFile: string;
21
21
  generators: fc.Arbitrary<unknown>[];
22
22
  }
package/dist/browser.js CHANGED
@@ -9,10 +9,6 @@ import { readConfig } from "./coverage.js";
9
9
  import { chromium, firefox, webkit } from 'playwright-core';
10
10
  import { toWire } from "./generators.js";
11
11
  import { runProperty, summarise } from "./property.js";
12
- /** `a.browser.laws.ts` runs in a browser. */
13
- export function isBrowserLawFile(file) {
14
- return /\.browser\.laws\.[cm]?ts$/.test(file);
15
- }
16
12
  function findBrowser() {
17
13
  const fromEnv = process.env.TSLAWS_BROWSER;
18
14
  if (fromEnv !== undefined && fromEnv !== '')
@@ -226,7 +222,7 @@ async function bundleOf(jsFile, cwd) {
226
222
  /** Runs the counterexample once more, leaves the DOM as it is, and photographs the page. Best effort. */
227
223
  async function capture(page, law, args, options) {
228
224
  try {
229
- await page.evaluate(([name, input]) => window.__runLaw(name, input, true), [law.name, toWire(args)]);
225
+ await page.evaluate(([name, input]) => window.__runLaw(name, input, true), [law.symbol ?? law.name, toWire(args)]);
230
226
  const relative = `.tslaws/screenshots/${law.name.replace(/[^\w-]/g, '_')}.png`;
231
227
  mkdirSync(join(options.cwd, '.tslaws/screenshots'), { recursive: true });
232
228
  await page.screenshot({ path: join(options.cwd, relative) });
@@ -273,7 +269,7 @@ export async function runBrowserLaw(law, options) {
273
269
  await page.addScriptTag({ content: code });
274
270
  await page.addScriptTag({ content: harness });
275
271
  const call = async (...args) => {
276
- const outcome = (await page.evaluate(([name, input, pause]) => window.__runLaw(name, input, false, pause), [law.name, toWire(args), calls++ < 12 ? (options.browserPauseMs ?? 0) : 0]));
272
+ const outcome = (await page.evaluate(([name, input, pause]) => window.__runLaw(name, input, false, pause), [law.symbol ?? law.name, toWire(args), calls++ < 12 ? (options.browserPauseMs ?? 0) : 0]));
277
273
  if (outcome.error !== undefined) {
278
274
  const error = new Error(outcome.error.message);
279
275
  error.name = outcome.error.name ?? 'Error';
package/dist/check.d.ts CHANGED
@@ -4,6 +4,8 @@ export interface CheckReport {
4
4
  compileDiagnostics: string;
5
5
  compileOk: boolean;
6
6
  lockMissing: boolean;
7
+ /** Things worth a person's attention that do not fail the check: a file name that its imports do not back. */
8
+ notices: string[];
7
9
  /** Whether every law holds and nothing is open, failed or tampered with. */
8
10
  ok: boolean;
9
11
  }
package/dist/check.js CHANGED
@@ -1,16 +1,44 @@
1
- import { readFileSync } from 'node:fs';
2
1
  import { createRequire } from 'node:module';
3
- import { join } from 'node:path';
2
+ import { resolve } from 'node:path';
4
3
  import ts from 'typescript';
5
4
  import { build, readManifest } from "./compile.js";
6
- import { compilerOptions, declarationFiles, isDiscard, jsPath, runLawGuarded, typesOf, } from "./property.js";
5
+ import { compilerOptions, declarationFiles, isDiscard, jsPath, runLawGuarded, warmWorkers, typesOf, } from "./property.js";
7
6
  import { analyzeCoverage, coverFiles, readConfig } from "./coverage.js";
7
+ import { derivedStore } from "./derived.js";
8
+ import { jobs } from "./pool.js";
8
9
  import { compareLock, lawKey, readLock } from "./lock.js";
9
10
  import { proveLaw } from "./smt.js";
10
11
  import { decodeExample, loadExamples, regressionMessage, saveExample } from "./regressions.js";
11
12
  import { closeBrowser, runBrowserLaw } from "./browser.js";
12
- import { ensureServer, importsBoth, lawMode, runE2eLaw, stopServer } from "./e2e.js";
13
+ import { ensureServer, runE2eLaw, stopServer } from "./e2e.js";
14
+ import { inProposals } from "./lawFiles.js";
15
+ import { classifyLawFile } from "./lawKinds.js";
16
+ import { normaliseProposal } from "./proposals.js";
13
17
  import { declarationOf, exercisesOf, markFailing, Sources } from "./exercises.js";
18
+ /** Runs at most `limit` tasks at once. */
19
+ class Semaphore {
20
+ free;
21
+ queue = [];
22
+ constructor(limit) {
23
+ this.free = limit;
24
+ }
25
+ async run(task) {
26
+ if (this.free > 0)
27
+ this.free -= 1;
28
+ else
29
+ await new Promise((resolve) => this.queue.push(resolve));
30
+ try {
31
+ return await task();
32
+ }
33
+ finally {
34
+ const next = this.queue.shift();
35
+ if (next === undefined)
36
+ this.free += 1;
37
+ else
38
+ next();
39
+ }
40
+ }
41
+ }
14
42
  /** The `@tslaws-exempt` functions of the project, for `tslaws accept` to record. */
15
43
  export function exemptionsOf(options) {
16
44
  const covered = coverFiles(options.cwd, readConfig(options.cwd).cover ?? []);
@@ -26,9 +54,27 @@ export function exemptionsOf(options) {
26
54
  }).exempt;
27
55
  }
28
56
  export async function check(options, enforceLock) {
57
+ // A proposal placed in .tslaws/proposals by hand may write its imports from the project root; store them where they resolve.
58
+ for (const file of options.files)
59
+ if (inProposals(file))
60
+ normaliseProposal(options.cwd, file);
29
61
  const laws = readManifest(options.compiler, options.files, options.cwd);
30
62
  const results = [];
31
63
  const blocked = new Set();
64
+ const notices = [];
65
+ // The level of a laws file comes from its name (the default) or its imports (`levels: "imports"`); looked at once per file.
66
+ const levelsMode = readConfig(options.cwd).levels ?? 'names';
67
+ const verdicts = new Map();
68
+ const verdictOf = (file) => {
69
+ let verdict = verdicts.get(file);
70
+ if (verdict === undefined) {
71
+ verdict = classifyLawFile(options.cwd, file, levelsMode);
72
+ verdicts.set(file, verdict);
73
+ for (const notice of verdict.notices)
74
+ notices.push(notice);
75
+ }
76
+ return verdict;
77
+ };
32
78
  const sources = new Sources(options.cwd);
33
79
  // What a law reaches is syntactic and cheap; a project that cannot be read this way just has no locations.
34
80
  const exercisesFor = (law) => {
@@ -61,7 +107,14 @@ export async function check(options, enforceLock) {
61
107
  lock = readLock(options.lockFile);
62
108
  lockMissing = lock === undefined;
63
109
  const byKey = new Map(laws.map((l) => [lawKey(l.file, l.name), l]));
64
- for (const violation of compareLock(lock?.laws, laws)) {
110
+ // With levels from imports (or a lock that recorded levels), the resolved level of each laws file is part of the lock.
111
+ const levels = levelsMode === 'imports' || lock?.levels !== undefined
112
+ ? {
113
+ locked: lock?.levels,
114
+ now: Object.fromEntries([...new Set(laws.map((l) => l.file.replaceAll('\\', '/')))].map((f) => [f, verdictOf(f).kind])),
115
+ }
116
+ : undefined;
117
+ for (const violation of compareLock(lock?.laws, laws, levels)) {
65
118
  const law = byKey.get(violation.key);
66
119
  if (law === undefined) {
67
120
  const [file, name] = violation.key.split('::');
@@ -78,7 +131,9 @@ export async function check(options, enforceLock) {
78
131
  add(law, 'tampered', 'lock', {
79
132
  message: violation.reason === 'added'
80
133
  ? 'law is not in the lock; a person must run `tslaws accept`'
81
- : 'law changed since the lock was accepted',
134
+ : violation.reason === 'level'
135
+ ? 'the level this laws file runs at (node, browser or e2e) changed since the lock was accepted; a person must run `tslaws accept`'
136
+ : 'law changed since the lock was accepted',
82
137
  });
83
138
  }
84
139
  }
@@ -89,53 +144,61 @@ export async function check(options, enforceLock) {
89
144
  }
90
145
  const covered = coverFiles(options.cwd, readConfig(options.cwd).cover ?? []);
91
146
  const compiled = [...new Set([...options.files, ...covered])];
147
+ warmWorkers(laws.filter((law) => law.problems.length === 0 && verdictOf(law.file).kind === 'node').length);
92
148
  const built = build(options.compiler, compiled, options.cwd);
93
- const program = ts.createProgram(declarationFiles(built.outDir), compilerOptions);
149
+ // What the type checker works out is kept with the build it came from, so an unchanged project skips the checker.
150
+ const derived = derivedStore(options.cwd, built.key);
151
+ let programOf;
152
+ const program = () => (programOf ??= built.program ?? ts.createProgram(declarationFiles(built.outDir), compilerOptions));
153
+ const sourceCwd = built.program === undefined ? undefined : options.cwd;
94
154
  const require = createRequire(import.meta.url);
95
- for (const law of laws) {
155
+ const runOne = async (law, index) => {
156
+ const buffered = [];
157
+ try {
158
+ await runBody(law, (l, status, method, extra = {}) => {
159
+ buffered.push(() => add(l, status, method, extra));
160
+ });
161
+ }
162
+ finally {
163
+ finished.set(index, buffered);
164
+ flush();
165
+ }
166
+ };
167
+ const runBody = async (law, add) => {
96
168
  if (options.only !== undefined && !options.only.includes(law.name))
97
- continue;
169
+ return;
98
170
  if (blocked.has(lawKey(law.file, law.name)))
99
- continue;
171
+ return;
100
172
  options.onEvent?.({ type: 'start', file: law.file, name: law.name });
101
173
  let types;
102
174
  try {
103
- types = typesOf(program, built.outDir, law);
175
+ types = derived.law(law, () => typesOf(program(), built.outDir, law, sourceCwd));
104
176
  }
105
177
  catch (error) {
106
178
  add(law, 'error', 'generators', { message: error.message });
107
- continue;
108
- }
109
- let source = '';
110
- try {
111
- source = readFileSync(join(options.cwd, law.file), 'utf8');
179
+ return;
112
180
  }
113
- catch {
114
- // The compiler already reported an unreadable file.
115
- }
116
- if (importsBoth(source)) {
117
- add(law, 'error', 'manifest', {
118
- message: `${law.file}: a browser laws file is either component level (tslaws/browser) or end-to-end (tslaws/e2e), not both`,
119
- });
120
- continue;
181
+ const verdict = verdictOf(law.file);
182
+ if (verdict.error !== undefined) {
183
+ add(law, 'error', 'manifest', { message: verdict.error });
184
+ return;
121
185
  }
122
- const mode = lawMode(law.file, source);
123
- const browser = mode === 'component';
124
- const e2e = mode === 'e2e';
186
+ const browser = verdict.kind === 'browser';
187
+ const e2e = verdict.kind === 'e2e';
125
188
  const method = browser ? 'browser' : e2e ? 'e2e' : 'property';
126
189
  let fn;
127
190
  // A browser law touches the DOM when it loads, so only the browser loads it.
128
191
  if (!browser) {
129
192
  try {
130
- fn = require(jsPath(built.outDir, law.file))[law.name];
193
+ fn = require(jsPath(built.outDir, law.file))[law.symbol ?? law.name];
131
194
  }
132
195
  catch (error) {
133
196
  add(law, 'error', 'load', { message: `cannot load compiled law: ${error.message}` });
134
- continue;
197
+ return;
135
198
  }
136
199
  if (typeof fn !== 'function') {
137
200
  add(law, 'error', 'load', { message: 'compiled module does not export the law' });
138
- continue;
201
+ return;
139
202
  }
140
203
  }
141
204
  let details;
@@ -156,18 +219,24 @@ export async function check(options, enforceLock) {
156
219
  details = e2e
157
220
  ? await runE2eLaw({ file: law.file, name: law.name, fn: fn, generators: types.generators }, lawOptions, await ensureServer(e2eConfig ?? {}, options.cwd), limitMs)
158
221
  : browser
159
- ? await runBrowserLaw({ file: law.file, name: law.name, jsFile: jsPath(built.outDir, law.file), generators: types.generators }, lawOptions)
160
- : await runLawGuarded(built.outDir, law, lawOptions);
222
+ ? await runBrowserLaw({
223
+ file: law.file,
224
+ name: law.name,
225
+ symbol: law.symbol,
226
+ jsFile: jsPath(built.outDir, law.file),
227
+ generators: types.generators,
228
+ }, lawOptions)
229
+ : await runLawGuarded(built.outDir, law, lawOptions, types.plans);
161
230
  }
162
231
  catch (error) {
163
232
  add(law, 'error', method, { message: error.message });
164
- continue;
233
+ return;
165
234
  }
166
235
  if (details === 'timeout') {
167
236
  add(law, 'error', method, {
168
237
  message: `property test ran longer than ${limitMs / 1000}s; the law or the code it calls may not terminate`,
169
238
  });
170
- continue;
239
+ return;
171
240
  }
172
241
  if (details.failed) {
173
242
  if (details.counterexample === undefined) {
@@ -192,7 +261,7 @@ export async function check(options, enforceLock) {
192
261
  message: details.regression ? `${regressionMessage} (${details.counterexample}): ${reason}` : reason,
193
262
  });
194
263
  }
195
- continue;
264
+ return;
196
265
  }
197
266
  // Browser and async laws are tested, never proved: the prover cannot model a DOM or an await.
198
267
  const unprovable = browser || e2e ? 'runs in a browser' : fn.constructor.name === 'AsyncFunction' ? 'is async' : undefined;
@@ -206,7 +275,7 @@ export async function check(options, enforceLock) {
206
275
  : 'marked proved but proving is disabled'
207
276
  : undefined,
208
277
  });
209
- continue;
278
+ return;
210
279
  }
211
280
  const holds = (inputs) => {
212
281
  try {
@@ -248,15 +317,38 @@ export async function check(options, enforceLock) {
248
317
  });
249
318
  else
250
319
  add(law, 'tested', 'property', { runs: details.numRuns, seed: details.seed, message: proof.reason });
251
- }
320
+ };
321
+ const finished = new Map();
322
+ let nextToFlush = 0;
323
+ const flush = () => {
324
+ for (let ready = finished.get(nextToFlush); ready !== undefined; ready = finished.get(nextToFlush)) {
325
+ for (const emit of ready)
326
+ emit();
327
+ finished.delete(nextToFlush);
328
+ nextToFlush += 1;
329
+ }
330
+ };
331
+ // Property laws and proofs share the cores; browser and end-to-end laws share one page and one server, so they queue.
332
+ const lanes = { cpu: new Semaphore(jobs()), page: new Semaphore(1) };
333
+ await Promise.all(laws.map(async (law, index) => {
334
+ const skipped = (options.only !== undefined && !options.only.includes(law.name)) || blocked.has(lawKey(law.file, law.name));
335
+ if (skipped) {
336
+ finished.set(index, []);
337
+ flush();
338
+ return;
339
+ }
340
+ const lane = verdictOf(law.file).kind !== 'node' ? lanes.page : lanes.cpu;
341
+ await lane.run(() => runOne(law, index));
342
+ }));
252
343
  if (enforceLock) {
253
- const coverage = analyzeCoverage({
344
+ const coverage = derived.coverage(() => analyzeCoverage({
254
345
  cwd: options.cwd,
255
346
  outDir: built.outDir,
256
347
  lawFiles: options.files,
257
348
  coverFiles: covered,
258
- program,
259
- });
349
+ program: program(),
350
+ moduleOf: sourceCwd === undefined ? undefined : (file) => resolve(sourceCwd, file),
351
+ }));
260
352
  for (const { file, name } of coverage.uncovered) {
261
353
  results.push({
262
354
  file,
@@ -286,5 +378,5 @@ export async function check(options, enforceLock) {
286
378
  const ok = built.ok &&
287
379
  !lockMissing &&
288
380
  results.every((r) => r.status === 'proved' || r.status === 'bounded' || r.status === 'tested');
289
- return { results, compileDiagnostics: built.diagnostics, compileOk: built.ok, lockMissing, ok };
381
+ return { results, compileDiagnostics: built.diagnostics, compileOk: built.ok, lockMissing, notices, ok };
290
382
  }
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare function watched(filename: string | null): boolean;
1
2
  /** What a command needs from its caller. `home` is for unit tests; the CLI never sets it. */
2
3
  export interface CommandContext {
3
4
  isTTY: boolean;
@@ -10,4 +11,21 @@ export interface CommandContext {
10
11
  export declare function acceptCommand(cwd: string, flags: Map<string, string | true>, files: string[], ctx: CommandContext): Promise<number>;
11
12
  /** `tslaws delegate <action> [--for D] [--root DIR]`; `argv` is everything after `delegate`. */
12
13
  export declare function runDelegateCommand(argv: string[], cwd: string, ctx: CommandContext): number;
14
+ /** What `promote` and `propose` ask of their surroundings, beyond the context every command has. `ask`, `stdin` and `checkFile` are for unit tests; the CLI never sets them. */
15
+ export interface ProposalContext extends CommandContext {
16
+ ask?: (question: string) => Promise<string>;
17
+ stdin?: () => Promise<string>;
18
+ checkFile?: (file: string) => Promise<Map<string, string>>;
19
+ }
20
+ /**
21
+ * `tslaws propose <file> [--from PATH]`: writes a proposal into `.tslaws/proposals/` from `--from PATH` or from stdin.
22
+ * Anyone may run it, agents included: it writes only that directory and never touches the lock or the tree.
23
+ */
24
+ export declare function proposeCommand(cwd: string, flags: Map<string, string | true>, files: string[], ctx: ProposalContext): Promise<number>;
25
+ /**
26
+ * `tslaws promote <proposal> [--to PATH]`: a person moves a proposal out of `.tslaws/proposals/` into the tree. Refused
27
+ * without a terminal, under CI, as an agent and with TSLAWS_AGENT set; a delegation does not cover it. Never writes the
28
+ * lock. In-tree `*.proposed.laws.ts` files promote, per law, into the file of the same kind.
29
+ */
30
+ export declare function promoteCommand(cwd: string, flags: Map<string, string | true>, files: string[], ctx: ProposalContext): Promise<number>;
13
31
  export declare function main(args: string[]): Promise<number>;