tslaws 0.6.3 → 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.
- package/README.md +8 -8
- package/dist/api.d.ts +10 -0
- package/dist/api.js +7 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -6
- package/dist/check.d.ts +2 -0
- package/dist/check.js +133 -41
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +149 -54
- package/dist/compile.d.ts +12 -0
- package/dist/compile.js +207 -6
- package/dist/coverage.d.ts +13 -1
- package/dist/coverage.js +16 -23
- package/dist/derived.d.ts +8 -0
- package/dist/derived.js +52 -0
- package/dist/e2e.d.ts +0 -9
- package/dist/e2e.js +0 -20
- package/dist/generators.d.ts +8 -8
- package/dist/generators.js +59 -99
- package/dist/globs.d.ts +3 -0
- package/dist/globs.js +24 -0
- package/dist/init.js +5 -5
- package/dist/lawFiles.d.ts +21 -0
- package/dist/lawFiles.js +95 -0
- package/dist/lawKinds.d.ts +38 -0
- package/dist/lawKinds.js +210 -0
- package/dist/lawNames.d.ts +6 -0
- package/dist/lawNames.js +22 -0
- package/dist/list.d.ts +22 -0
- package/dist/list.js +36 -0
- package/dist/lock.d.ts +16 -3
- package/dist/lock.js +23 -4
- package/dist/lower.d.ts +10 -0
- package/dist/lower.js +178 -0
- package/dist/mcp.d.ts +2 -8
- package/dist/mcp.js +40 -61
- package/dist/migrate/arbitraries.d.ts +23 -0
- package/dist/migrate/arbitraries.js +202 -0
- package/dist/migrate/convert.d.ts +20 -0
- package/dist/migrate/convert.js +915 -0
- package/dist/migrate/framework.d.ts +48 -0
- package/dist/migrate/framework.js +249 -0
- package/dist/migrate/index.d.ts +16 -0
- package/dist/migrate/index.js +155 -0
- package/dist/migrate/report.d.ts +35 -0
- package/dist/migrate/report.js +40 -0
- package/dist/migrate/source.d.ts +42 -0
- package/dist/migrate/source.js +163 -0
- package/dist/migrate/tests.d.ts +44 -0
- package/dist/migrate/tests.js +184 -0
- package/dist/plan.d.ts +50 -0
- package/dist/plan.js +118 -0
- package/dist/pool.d.ts +21 -0
- package/dist/pool.js +54 -0
- package/dist/promote.d.ts +30 -2
- package/dist/promote.js +133 -7
- package/dist/property.d.ts +12 -26
- package/dist/property.js +33 -74
- package/dist/propertyRun.d.ts +24 -0
- package/dist/propertyRun.js +56 -0
- package/dist/propertyWorker.js +7 -9
- package/dist/proposals.d.ts +43 -0
- package/dist/proposals.js +192 -0
- package/dist/regressions.js +23 -2
- package/dist/smt.js +9 -8
- package/dist/types.d.ts +2 -0
- package/hooks/AGENTS.snippet.md +2 -2
- 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
|
|
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
|
|
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
|
|
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
|
|
204
|
-
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
+
return;
|
|
98
170
|
if (blocked.has(lawKey(law.file, law.name)))
|
|
99
|
-
|
|
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
|
-
|
|
108
|
-
}
|
|
109
|
-
let source = '';
|
|
110
|
-
try {
|
|
111
|
-
source = readFileSync(join(options.cwd, law.file), 'utf8');
|
|
179
|
+
return;
|
|
112
180
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
123
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|