@redue/cli 0.1.0-alpha.3 → 0.1.0-alpha.30

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 (67) hide show
  1. package/CHANGELOG.md +806 -0
  2. package/README.md +157 -99
  3. package/bin/redue.mjs +267 -61
  4. package/docs/README.md +3 -1
  5. package/docs/user/agents.md +333 -15
  6. package/docs/user/configuration.md +28 -2
  7. package/docs/user/direct-typescript.md +219 -0
  8. package/docs/user/getting-started.md +49 -7
  9. package/docs/user/json.md +4 -2
  10. package/docs/user/node-script.md +177 -0
  11. package/docs/user/npm-launcher-notice.md +42 -0
  12. package/docs/user/pilot.md +146 -0
  13. package/docs/user/sandbox.md +312 -0
  14. package/docs/user/shell.md +3 -3
  15. package/docs/user/supported-projects.md +69 -23
  16. package/docs/user/tests-direct.md +143 -0
  17. package/package.json +4 -3
  18. package/src/agent-integration.mjs +467 -0
  19. package/src/agent-policy.md +28 -0
  20. package/src/cached-state.mjs +24 -1
  21. package/src/claude-hook.mjs +616 -0
  22. package/src/cli.mjs +449 -54
  23. package/src/daemon.mjs +442 -93
  24. package/src/decision-profile.mjs +15 -1
  25. package/src/decision-validation.mjs +12 -1
  26. package/src/declared-plan.mjs +38 -6
  27. package/src/direct-run-certificate.mjs +254 -0
  28. package/src/direct-typescript-probe.mjs +27 -0
  29. package/src/direct-typescript.mjs +205 -0
  30. package/src/glob.mjs +8 -0
  31. package/src/history-worker.mjs +21 -13
  32. package/src/index.mjs +108 -21
  33. package/src/installed-inputs.mjs +7 -1
  34. package/src/installed-observation.mjs +23 -0
  35. package/src/landlock.py +527 -0
  36. package/src/linux-inotify.py +39 -10
  37. package/src/node-onboarding.mjs +203 -54
  38. package/src/node-script.mjs +406 -0
  39. package/src/npm-integrity.mjs +124 -0
  40. package/src/npm-typecheck-environment.mjs +36 -0
  41. package/src/npmrc.mjs +16 -0
  42. package/src/observation.mjs +34 -12
  43. package/src/path-identity.mjs +8 -0
  44. package/src/plan-worker.mjs +17 -17
  45. package/src/pnpm-inputs.mjs +122 -57
  46. package/src/pnpm-shim-templates.json +180 -0
  47. package/src/pnpm-shims.mjs +54 -0
  48. package/src/pnpm-typecheck-probe.mjs +11 -6
  49. package/src/presentation.mjs +13 -4
  50. package/src/probe-deadline.mjs +37 -0
  51. package/src/probe-result.mjs +7 -3
  52. package/src/project-location.mjs +34 -0
  53. package/src/python-onboarding.mjs +89 -0
  54. package/src/run-recorder.cjs +91 -0
  55. package/src/sandbox.mjs +653 -0
  56. package/src/setup.mjs +205 -0
  57. package/src/stats.mjs +72 -0
  58. package/src/test-direct.mjs +608 -0
  59. package/src/test-recorder.cjs +435 -0
  60. package/src/typescript-contract-probe.mjs +4 -187
  61. package/src/typescript-input-facts.mjs +92 -0
  62. package/src/typescript-invocation.mjs +92 -0
  63. package/src/typescript-list.mjs +118 -13
  64. package/src/typescript-qualification.mjs +29 -49
  65. package/src/workspace-manifests.mjs +24 -0
  66. package/src/yarn-workspace-inputs.mjs +3 -0
  67. package/src/yarn-workspace-typecheck-probe.mjs +2 -1
@@ -0,0 +1,312 @@
1
+ # Sandboxed recipe (typecheck, lint and tests, any language)
2
+
3
+ The sandboxed recipe runs a typecheck, lint or test command inside an operating
4
+ system sandbox and reports later whether that result still applies to the
5
+ files you have. The sandbox holds for every process the command starts, so it
6
+ covers programs REDUE cannot record: TypeScript 7's native compiler, esbuild,
7
+ oxlint and tsgolint, biome, turbo, and Python tools such as mypy, pyright, ty
8
+ and ruff, and test runners such as pytest.
9
+
10
+ **The claim.** A sandboxed result holds for a run that sees only the project's
11
+ files that git doesn't ignore, except its documentation, its installed
12
+ packages and the toolchain, the way CI does. It does not say that a run with
13
+ your own configuration files, a service on your machine or the network would
14
+ give the same result.
15
+
16
+ A check that reads the documentation (a doctest over `README.md`) sees it
17
+ too. A check that needs files git ignores (generated code, another package's
18
+ built declarations) gets the whole checkout. See
19
+ [hidden files](#files-git-ignores-and-documentation).
20
+
21
+ ## Where it applies
22
+
23
+ - **Tests:** a root `test` script, and pytest in the project's `.venv`. See
24
+ [tests](#tests). For a Node test script, `init` tries the sandbox before the
25
+ [direct test recipe](tests-direct.md), which becomes the fallback, when
26
+ both would run the same script. In the
27
+ census, on the repos where both qualified, the direct recipe's results went
28
+ STALE when the project's build rewrote its output, and the sandboxed ones,
29
+ with ignored files hidden, stayed CURRENT.
30
+ - **Node projects:** a root `typecheck` or `lint` script (npm, pnpm, or Yarn
31
+ with the `node-modules` linker). `init` tries the
32
+ [direct TypeScript recipe](direct-typescript.md) and the pnpm and Yarn
33
+ TypeScript recipes first; they stay first where they apply. Then the
34
+ sandboxed recipe, before the [general recipe](node-script.md), since it
35
+ also covers the native programs a script starts. Where init's trials show
36
+ a difference, or the sandbox cannot run, `init` writes the general recipe
37
+ as before. Where the sandbox cannot start at run time (inside Claude Code's
38
+ macOS sandbox), a script the general recipe takes is recorded as it records
39
+ it.
40
+ - **Python projects:** mypy, pyright, ty and ruff installed in the project's
41
+ `.venv` (`uv sync` makes one; Poetry does with `virtualenvs.in-project`).
42
+ `init` uses the arguments your CI workflow, Makefile, tox, nox, justfile or
43
+ shell scripts in `scripts/` run the tool with, through `uv run`, `poetry run`,
44
+ `python -m` or `.venv/bin`. Without them it uses the tool's default:
45
+ `ruff check`, `ty check`, `pyright`, and `mypy` (with `.` when mypy's
46
+ configuration names no `files`).
47
+ - **Platforms:** macOS (Seatbelt, `sandbox-exec`) and Linux 6.7 or later
48
+ (Landlock; the launcher needs Python 3).
49
+
50
+ ```sh
51
+ redue init
52
+ redue start
53
+ redue run typecheck
54
+ redue status --sync
55
+ ```
56
+
57
+ A Python check in the config:
58
+
59
+ ```json
60
+ {"name": "typecheck", "kind": "typecheck", "qualification": "sandbox-v3",
61
+ "command": [".venv/bin/mypy", "src"], "inputs": ["**", "..."], "installedInputs": [".venv/**", "..."]}
62
+ ```
63
+
64
+ A Node check names its script, as the general recipe's do:
65
+ `{"name": "lint", "script": "lint", "kind": "lint", "qualification": "sandbox-v3", ...}`.
66
+ `sandbox-v3` hides the files git ignores and the documentation from the run,
67
+ `sandbox-v2` only the files git ignores; `sandbox-v1` runs it on the whole
68
+ checkout.
69
+
70
+ With the Claude Code hook, `uv run mypy src`, `poetry run mypy src`,
71
+ `python -m mypy src`, `.venv/bin/mypy src` and `mypy src` are recognized as
72
+ that check. The last two count only when PATH finds the tool or Python in the
73
+ project's `.venv/bin` first. When the hook runs the check through REDUE, it
74
+ keeps `uv run` or `poetry run` in front, so the environment still syncs first.
75
+
76
+ ## Files git ignores and documentation
77
+
78
+ `init` runs each check plainly, then sandboxed with the files git ignores and
79
+ the documentation hidden. When both give the same result, it writes
80
+ `sandbox-v3`. When the plain run passed and the sandboxed one failed only
81
+ because it was refused a file it hid (no other refusal line, and not a line
82
+ the plain run printed too), it writes `sandbox-v1` (the whole checkout)
83
+ without running it again: without the hiding, a run can then only fail
84
+ where a plain one passes, never pass where it fails. When the runs differ
85
+ in another way, `init` runs the check once more with the whole checkout,
86
+ reusing the plain run, and writes `sandbox-v1` only if that agrees. So a
87
+ check runs two or three times at init, not four. `init` no longer writes
88
+ `sandbox-v2`; a check already set up with it keeps it.
89
+
90
+ With `sandbox-v3`, each run also hides the documentation: the files git
91
+ lists (and does not ignore) named `*.md`, `*.mdx`, `*.rst`, `LICENSE*` or
92
+ `CHANGELOG*` anywhere, and text, image and PDF files (`.txt`, `.adoc`,
93
+ `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.webp`, `.pdf`) in the root `docs/`
94
+ folder. Code in `docs/` (examples a typecheck includes) is not hidden, and
95
+ folders stay listable. The receipt names the files the run hid. A later edit
96
+ to one of them, or its deletion, leaves the result CURRENT: editing the
97
+ README does not make the tests STALE. A new documentation file, a link named
98
+ like one, or a change made while the run ran still counts. On macOS the
99
+ files with those extensions are refused by name pattern, in any case and in
100
+ `node_modules` too: `README.MD` or `Docs/logo.svg` is refused but not
101
+ listed, so an edit to it counts, and a check that reads one gets
102
+ `sandbox-v1` on macOS. `LICENSE*` and `CHANGELOG*` files are refused by the
103
+ exact paths git lists, so `license-check.ts` stays readable. On Linux each
104
+ listed file is hidden like a git-ignored one. In a checkout with more than
105
+ 100 `LICENSE*` and `CHANGELOG*` files (vendored packages), those files are
106
+ not hidden on either platform and count as inputs.
107
+
108
+ With `sandbox-v2`, each run hides what `git ls-files --others --ignored
109
+ --exclude-standard --directory` lists, except `node_modules`, `.venv` and
110
+ runner caches: build output (`dist/`, `.next/`), coverage files, `.env` files.
111
+ Only the project's rules count: its `.gitignore` files and
112
+ `.git/info/exclude`, not your global excludes file. Those files are not
113
+ inputs, so a build or test run that writes them leaves the result CURRENT.
114
+
115
+ When a `sandbox-v2` or `sandbox-v3` run fails with a refusal that names a
116
+ hidden file (a missing file counts only when it names one, since Linux tools
117
+ report an unreadable file as missing), REDUE runs the check again in the
118
+ sandbox without hiding anything. That result is kept and compared on every
119
+ file, git-ignored ones and documentation included. Run `redue init` again to
120
+ write another recipe for a check that keeps needing them.
121
+
122
+ ## What the run sees
123
+
124
+ It may read and execute only beneath:
125
+
126
+ - the checkout, without the files git ignores for `sandbox-v2`, and without
127
+ its documentation too for `sandbox-v3`;
128
+ - the toolchain: Node's installation and the package managers' (with
129
+ Corepack's downloads), or the Python installation the `.venv` was made from;
130
+ - system directories (`/usr`, `/bin`, `/etc` and the like on Linux; `/usr`,
131
+ `/System`, `/Library`, `/opt/homebrew` and the like on macOS);
132
+ - a private home directory and a fresh temporary directory, in REDUE's state.
133
+
134
+ A typecheck or lint has no network: no TCP connection, loopback included
135
+ (a test run on Linux gets loopback; see [tests](#tests)). On macOS its only
136
+ Unix sockets are in its own temporary directory, which is new for each run.
137
+ On Linux 6.12 and later it cannot reach an abstract Unix socket outside the
138
+ run.
139
+
140
+ What the check leaves running is stopped when it exits, as at the end of a
141
+ CI job: its process group gets SIGTERM, and SIGKILL a second later. On Linux
142
+ the launcher also stops daemons that started their own session. On macOS
143
+ such a daemon outlives the run, but no later run can reach it: its socket
144
+ was in a temporary directory that is gone. The run gets no terminal input.
145
+
146
+ The checkout's `.git` is not an input. Tools still see it, as they do in a CI
147
+ checkout (a linter that applies `.gitignore` only inside a repository does),
148
+ but git commands find no repository: `GIT_DIR` points nowhere. A script that
149
+ runs git (`git diff --exit-code`) gives a different result inside the
150
+ sandbox, and `init` leaves it to the other recipes.
151
+
152
+ It gets a controlled environment, not yours:
153
+
154
+ - the variables the check declares (`TZ`, `LANG`, `CI`, `USER`, `LC_*`,
155
+ `DYLD_*`, `LD_*`, and `NODE_*`, `npm_config_*`, `PNPM_*`, `YARN_*`,
156
+ `COREPACK_*` for Node, or `PYTHON*`, `MYPY*`, `RUFF_*`, `PYRIGHT*`, `TY_*`
157
+ for Python), with your values;
158
+ - your PATH, without the project's `.venv/bin`, which a Python check gets
159
+ first instead, so `uv run` and an activated environment give the same PATH;
160
+ - `HOME`, `TMPDIR` and the XDG directories in its private directories;
161
+ `VIRTUAL_ENV` for a Python check; Corepack's home where Corepack keeps it.
162
+
163
+ ## Tests
164
+
165
+ A test run gets what a typecheck or lint gets, and also:
166
+
167
+ - **On Linux, loopback, and no remote host.** It may listen on any address
168
+ and accept connections, and connect only to the local host (`127.0.0.0/8`,
169
+ `::1`, `0.0.0.0`, `::`) and Unix sockets.
170
+ - Landlock cannot tell a loopback address from a remote one, so a
171
+ seccomp filter in the test's processes does instead. It refuses sockets
172
+ other than Unix, netlink and TCP when they are created (UDP included),
173
+ TCP Fast Open (`MSG_FASTOPEN`) and `io_uring`. It hands each `connect` to
174
+ REDUE's launcher. The launcher allows a local port no process listens on
175
+ (the test gets "connection refused") or one the run's own processes
176
+ listen on, and a Unix socket file in the checkout, the run's home or its
177
+ temporary directory. It refuses a service started outside the run (your
178
+ local database) and every remote host. Nothing else goes through the
179
+ launcher, so worker processes talk to each other as usual. If the
180
+ launcher's supervisor cannot start, or stops before the run ends, the
181
+ result is UNVERIFIED, never CURRENT.
182
+ - **On macOS, no network.** No TCP or UDP, loopback included: only Unix
183
+ sockets in its own temporary directory (its home is beneath it), as a
184
+ typecheck or lint gets. Seatbelt cannot refuse a service started outside
185
+ the run while allowing the run's own servers: a rule that refuses one
186
+ loopback port under a loopback allow is not applied, and an allow list of
187
+ every other port does not compile. A suite that starts a server on
188
+ loopback, or reads a local database, fails init's trial: init leaves the
189
+ test check out, and the tests run without REDUE, as before. A test check
190
+ set up by alpha.28 on macOS is not CURRENT after the upgrade; if its runs
191
+ now fail inside the sandbox, run `redue init` again.
192
+ - **A fresh home.** Its home directory is new and empty for each run, as on
193
+ CI: what a test leaves there is not observed, so it must not reach a later
194
+ run.
195
+ - **A private `/tmp`.** Its `TMPDIR` is its own, new for each run, and it may
196
+ not write in `/tmp` or `/var/tmp` outside it. A test that writes to a fixed
197
+ path there is refused.
198
+
199
+ `init` compares a test run's exit status and the counts its summary prints
200
+ (passed, failed, skipped), not every line: test runners print in the order
201
+ their workers finish. Git-ignored files are hidden from a test run when the
202
+ trial agrees, as for typecheck and lint. A suite that reads back git-ignored
203
+ files it writes in the same run (a coverage database, a build it starts
204
+ itself) gets the whole checkout. Files a hidden run writes beneath hidden
205
+ folders, or new files git ignores, are not changes to its inputs.
206
+
207
+ ## What REDUE claims
208
+
209
+ CURRENT means the last sandboxed run's inputs are unchanged:
210
+
211
+ - every file in the checkout git doesn't ignore but its documentation
212
+ (`sandbox-v3`), every file git doesn't ignore (`sandbox-v2`), or every file
213
+ in it, git-ignored files and git-ignored links to files in it included
214
+ (`sandbox-v1`), except `.git` and runner caches (the general recipe's, plus
215
+ `__pycache__`, `.mypy_cache`, `.ruff_cache` and `.pytest_cache`);
216
+ - `node_modules` and `.venv`, observed as installed files, by identity;
217
+ - the environment the run gets (without display variables such as `TERM`
218
+ and `COLUMNS`), and the places it may read;
219
+ - Node's and the package managers' executables, or the interpreter the
220
+ `.venv` was made from, by file identity;
221
+ - every directory on PATH, by identity: installing, removing or upgrading a
222
+ program in one makes the result STALE;
223
+ - `.git/info/exclude` and `.git/config`, by content: ruff and biome read the
224
+ ignore rules there, and they decide what `sandbox-v2` hides. For a project
225
+ in a subdirectory of its repository, also the `.gitignore` files above it.
226
+
227
+ ## Telling a refusal from a real failure
228
+
229
+ A run the sandbox refused something fails like any other. REDUE does not run
230
+ every failure twice to find out:
231
+
232
+ - `init` runs each proposed check plainly and sandboxed, and proposes it
233
+ when both give the same exit status and the same output, and a failure
234
+ shows no sign of a refusal. The output is compared without colors,
235
+ timings, the temporary and home directories, and lines about caches. The
236
+ one exception is a check that passed plainly and failed only because it
237
+ was refused a hidden file: it gets the whole checkout without another run
238
+ (see [files git ignores](#files-git-ignores-and-documentation)).
239
+ Otherwise `init` writes what it would have without this recipe and says why.
240
+ These are the only runs `init` makes.
241
+ - When a `sandbox-v2` or `sandbox-v3` run fails with a refusal that names a hidden file, it
242
+ runs again in the sandbox without hiding (see
243
+ [files git ignores](#files-git-ignores-and-documentation)).
244
+ - When a sandboxed run fails and its output has a line that looks like a
245
+ refusal ("Operation not permitted", "Permission denied", "Read-only file
246
+ system", a name lookup or a connection that failed), REDUE runs the check
247
+ again outside the sandbox. If that exits differently, the result is the
248
+ plain run's and UNVERIFIED: "the sandbox changed the result", with the
249
+ line. A failure without such a line is not run again.
250
+
251
+ ## The private home
252
+
253
+ A typecheck or lint's home directory is kept between runs, per check, so caches tools
254
+ build there (font caches, a type checker's cache) are not rebuilt every run.
255
+ It starts empty: your configuration files are not in it. What a run leaves
256
+ there can change a later run, and REDUE trusts it like a runner cache. Remove
257
+ it with `redue remove-state`.
258
+
259
+ ## Limits
260
+
261
+ - Inside Claude Code's own sandbox on macOS, `sandbox-exec` cannot start: the
262
+ run goes ahead unsandboxed and is UNVERIFIED ("the run was not sandboxed").
263
+ On Linux, Landlock works inside it.
264
+ - Windows is not supported. The check stays recording-only there.
265
+ - Linux before 6.7 (Landlock ABI below 4) cannot refuse TCP connections, so a
266
+ run there is UNVERIFIED. UDP is not refused on Linux.
267
+ - System files (time zones, certificates, `/etc`) are not inputs: a change to
268
+ them does not make a result STALE.
269
+ - A program that reads files in `.git` itself, without git (a commit hash for
270
+ a version string), reads something REDUE does not observe: a commit does
271
+ not make the result STALE.
272
+ - Runner caches in the checkout are trusted as the general recipe trusts them.
273
+ - On Linux, a run can still reach a daemon started outside it through a Unix
274
+ socket file (Landlock does not cover them), and what that daemon reads is
275
+ not seen. On macOS it cannot.
276
+ - On Linux, uv hard-links `.venv` files to its cache. The cache is
277
+ toolchain: a file rewritten through the cache, not through the `.venv`, is
278
+ noticed only when the observer next reads the `.venv` (when it starts).
279
+ - A `.venv` link that leads out of the checkout, other than the interpreter's,
280
+ keeps the check recording-only and names the link, and so does a `.venv`
281
+ that uses the system site-packages.
282
+ - A git-ignored link to a directory, or out of the checkout, is not observed,
283
+ and neither is a `node_modules` directory outside the installed ones (a
284
+ nested example project's).
285
+ - A script that changes files (`--fix`, `--write`) does not get this recipe:
286
+ `init` would run it twice.
287
+ - With `sandbox-v2` or `sandbox-v3`, a run cannot read a hidden file, but it can still see
288
+ that one exists, its size and its times, and write beneath it. On Linux it
289
+ can also list a hidden folder; on macOS it cannot. A tool that acts on
290
+ whether a git-ignored file exists, without reading it, is not covered: a
291
+ new or removed one does not make the result STALE. In a census of 136
292
+ test suites with ignored files after their build, none did.
293
+ - On Linux, a test run's UDP is refused, loopback included: the filter
294
+ decides when the socket is made, before its address is known. A test that
295
+ needs it fails its trial and keeps another recipe.
296
+ - On Linux, the launcher decides a `connect` from the address it reads in
297
+ the process's memory, and the kernel reads it again after. A process that
298
+ changes the address in between, from another thread, could reach another
299
+ host. Honest test code does not.
300
+ - On Linux, a typecheck or lint can still connect to a Unix socket file
301
+ outside it (a local database's).
302
+ - On Linux, a file a `sandbox-v2` or `sandbox-v3` run creates in a folder that holds hidden
303
+ files (the checkout's root, when anything in it is hidden; with `sandbox-v3`,
304
+ any folder with a README) cannot be read
305
+ back in the same run. `init`'s trial and the rerun without hiding catch a
306
+ check that needs to.
307
+ - A file whose git status changes without a change to its content (`git add
308
+ -f` of an ignored file, `git rm --cached` of a tracked one) is noticed only
309
+ when its content next changes. A change to `.gitignore` or
310
+ `.git/info/exclude` makes the result STALE.
311
+ - A run that moves a hidden folder into a visible place can read it there.
312
+ That changes the checkout during the run, so the run is not reused.
@@ -11,9 +11,9 @@ REDUE UNVERIFIED
11
11
  ```
12
12
 
13
13
  It reads the cached snapshot, checks its expiry and observer liveness, and never
14
- synchronizes, traverses installed packages, or runs probes/checks. Like ordinary
15
- status, it can remain conservative when caller context or installed hardlinks
16
- need a synchronized read. Do not turn that uncertainty into a green badge.
14
+ synchronizes, traverses installed packages, or runs probes/checks. Like
15
+ `status --cached`, it can remain conservative when caller context or installed
16
+ hardlinks need a synchronized read. Do not turn that uncertainty into a green badge.
17
17
 
18
18
  For an existing async/bounded prompt framework, use `redue status --short` from
19
19
  the project root as the segment command and display unavailable on errors or
@@ -12,18 +12,37 @@ is known. When npm is not pinned in `package.json`,
12
12
  leaves the version unknown rather than guessing if that inspection fails.
13
13
 
14
14
  At the repository root, `init` looks for existing typecheck, test, lint, and
15
- build scripts. It prefers familiar script names, then unique recognizable
15
+ build scripts. It prefers familiar script names (for typecheck: `typecheck`,
16
+ `type-check`, `check-types`, `check:types`, `types:check`, `tsc`, `types`,
17
+ `lint:types`), then for typecheck one script named under such a name
18
+ (`typecheck:test`, `tsc:build`; not a watch script), then unique recognizable
16
19
  `tsc`, Vitest/Jest, ESLint, or Vite commands under other names. Multiple
17
20
  matches are reported as ambiguous rather than guessed. It never creates a new
18
- project script. It recognizes canonical
19
- `tsc --noEmit` on local npm installations as a candidate for automatic
20
- qualification. Before CURRENT is possible, a read-only probe checks the
21
- TypeScript configuration and effective file list, local compiler, installed
22
- contents, npm/Node identity, and relevant caller context. Unsupported plugins,
23
- preloads, config graphs, or outside-root inputs leave the check UNVERIFIED with
24
- an explanation. A discovered script can still be run and recorded in that
25
- state. Root and package-level npm/Yarn workspace scripts are discovered from
26
- `package.json` workspace declarations. Pinned Yarn 4 `node-modules`/classic
21
+ project script. Without `--recipe`, `init` tries the strict recipes below first, then,
22
+ for a root typecheck, lint or test script, the [sandboxed recipe](sandbox.md) on
23
+ macOS and Linux (the script run inside an OS sandbox, with the files git
24
+ ignores and the documentation hidden when it gives the same result that way,
25
+ else with only the files git ignores hidden, else on the whole checkout; a
26
+ test run also gets a private `/tmp`, and loopback-only network on Linux or no
27
+ network on macOS),
28
+ then, for typecheck and lint, the [general recipe](node-script.md): the script run
29
+ through the package manager with every Node process recorded and the whole
30
+ checkout as its input. Python projects get the sandboxed recipe for mypy,
31
+ pyright, ty, ruff and pytest in their `.venv`. `--recipe typescript-direct`,
32
+ `tests-direct`, `node-script` or `sandbox` selects one recipe alone. The direct TypeScript recipe supports
33
+ a typecheck script of `tsc --noEmit` runs in any argument order, chained runs,
34
+ or no typecheck script at all (a proposed `tsc --noEmit`; see
35
+ [script forms](direct-typescript.md#script-forms)), in a single-package npm installation using
36
+ TypeScript 5.x or 6.0 (a reviewed release, or one whose installed files match the
37
+ tarball package-lock.json names in npm's cache; see
38
+ [compiler identity](direct-typescript.md#compiler-identity)) and Node 22/24. It executes the local compiler,
39
+ not npm. Full installed compiler identity, config/membership, installed inputs
40
+ and relevant Node environment are checked before reuse. See the
41
+ [exact direct boundary](direct-typescript.md). Unsupported plugins, preloads,
42
+ configuration or outside-root inputs remain UNVERIFIED. Root and package-level npm/Yarn workspace scripts are discovered from
43
+ `package.json` workspace declarations. In a workspace of more than 50
44
+ packages, `init` proposes the root's checks only and names what it skipped;
45
+ add package checks to the config yourself. Up to 1000 packages are read. Pinned Yarn 4 `node-modules`/classic
27
46
  workspaces with a standalone `tsc -p .` script, local TypeScript, a conventional
28
47
  `src` include, and Node module resolution can qualify automatically. For one
29
48
  workspace, use `redue init --workspace NAME --check typecheck`; it writes only
@@ -36,21 +55,48 @@ When an internal dependency is consumed through built declarations, changing
36
55
  only its source does not stale the selected check until the consumed output
37
56
  changes. Unsupported custom resolution, project references, outside-root
38
57
  source links, and installation modes remain recording-only. Other Yarn
39
- typechecks, Vitest/Jest, ESLint, and builds start recording-only unless a
40
- separately justified explicit contract is provided.
58
+ typechecks, Vitest/Jest, ESLint, and builds in workspace packages start
59
+ recording-only unless a separately justified explicit contract is provided.
60
+ A root test script gets the sandboxed recipe when its trial gives the same
61
+ exit status and test counts as a plain run. Otherwise, for a single-package
62
+ project whose test script runs Vitest 4/5 or Jest 29 directly, `init` uses the
63
+ direct test recipe, which records the run and guards its inputs; see
64
+ [direct tests](tests-direct.md).
41
65
  In particular, test caches and build-generated inputs are not excluded to
42
66
  make evidence green.
43
67
 
44
- Pinned pnpm 12 projects are read from `packageManager`, `pnpm-workspace.yaml`,
45
- the lockfile, and the installed `node_modules/.modules.yaml` metadata. The
68
+ pnpm 9-12 projects are read from `packageManager`, `pnpm-workspace.yaml`,
69
+ `.npmrc`, the lockfile, and the installed `node_modules/.modules.yaml`
70
+ metadata. The installed metadata names the pnpm version; a `packageManager`
71
+ pin is optional, and when present (with or without Corepack's `+sha512` hash)
72
+ it must name the installed version. Settings that only change what gets
73
+ installed are accepted, because the installed files are what REDUE observes.
74
+ Settings that move or copy installed files, or run code during install, are
75
+ not: custom `modulesDir`, `virtualStoreDir` or `lockfileDir`, the global
76
+ virtual store, injected workspace packages, separate workspace lockfiles,
77
+ pnpmfile hooks and `configDependencies`. They are refused when either
78
+ `pnpm-workspace.yaml` or `.npmrc` sets them. The linker is read from the file
79
+ the installed major reads (`.npmrc` for pnpm 9, `pnpm-workspace.yaml` then
80
+ `.npmrc` for pnpm 10, `pnpm-workspace.yaml` for pnpm 11 and 12) and must match
81
+ the installed one. Only the project's files are read: settings from a user or
82
+ global rc file, or `npm_config_*` variables during install, are not. The
46
83
  supported local layouts are pnpm's standard isolated and hoisted
47
84
  `node_modules` with a project-local `.pnpm` virtual store. `init` can discover
48
- root and workspace scripts. For a standalone `tsc --noEmit` or `tsc -p ...`
49
- script in the narrow TypeScript contract, it records the exact installed
50
- compiler invocation directly. This avoids pnpm 12's task-run bookkeeping
85
+ root and workspace scripts. For a typecheck script of `tsc` runs
86
+ ([script forms](direct-typescript.md#script-forms); `tsc -p .` without
87
+ `--noEmit` is also accepted), or a package with a `tsconfig.json` and no
88
+ typecheck script, it records the exact installed compiler invocation directly.
89
+ The source inputs are the directories the config's `include` covers, as
90
+ TypeScript itself computes them for new files, minus `node_modules` (`exclude`
91
+ entries stay inputs: an import can still reach an excluded file); a file the
92
+ program reads outside them keeps the check recording-only. This avoids pnpm's task-run bookkeeping
51
93
  writes during `pnpm run`; it does not alter the project's script. Test, lint,
52
- build, and ambiguous TypeScript scripts still execute through `pnpm run` and
53
- start recording-only. The generated `@typescript-bin:WORKSPACE` token resolves
94
+ build, and ambiguous TypeScript scripts still execute through `pnpm run`; root
95
+ typecheck and lint ones can use the [general recipe](node-script.md). The pnpm
96
+ and Yarn TypeScript recipes read TypeScript's compiler API, which TypeScript 7
97
+ does not ship: with TypeScript 7 installed they refuse and say so. The
98
+ general recipe runs such a script, but TypeScript 7's native compiler is a
99
+ program REDUE does not record, so the result is UNVERIFIED. The generated `@typescript-bin:WORKSPACE` token resolves
54
100
  the local compiler when the check runs; it is not an absolute machine path.
55
101
 
56
102
  The pnpm contract indexes consumed installed package contents inside the
@@ -61,10 +107,10 @@ global virtual stores, injected workspace dependencies, and external links
61
107
  without an observed boundary remain recording-only with a reason. Installed
62
108
  files with hard-linked aliases outside the checkout remain recording-only on
63
109
  macOS: writes through an unseen alias cannot support CURRENT there. On Linux,
64
- redue watches project-installed file inodes and rehashes the selected
65
- installed inputs on every synchronized read. A cached read remains UNVERIFIED
66
- for these pnpm checks; a synchronized read can establish CURRENT after the
67
- rehash. If an inode watch cannot be installed, observation becomes unavailable
110
+ redue watches project-installed file inodes and re-reads the identity of the
111
+ selected installed inputs on every synchronized read. A cached read remains UNVERIFIED
112
+ for these pnpm checks; a synchronized read can establish CURRENT after that
113
+ re-read. If an inode watch cannot be installed, observation becomes unavailable
68
114
  instead of dropping that file. The global store is not watched. An absent
69
115
  optional package remains unqualified when Node could later
70
116
  resolve it from an unobserved ancestor `node_modules`; project-local candidate
@@ -0,0 +1,143 @@
1
+ # Direct test recipe (Vitest and Jest)
2
+
3
+ `redue init --recipe tests-direct --check test` (or `redue init`, where the
4
+ [sandboxed recipe](sandbox.md) cannot take the test script) records a project's unit test run and reports later whether that result
5
+ still applies to the files you have.
6
+ It supports a single-package project installed by npm, pnpm 9-12 (isolated or
7
+ hoisted `node_modules`) or Yarn with the `node-modules` linker, whose test
8
+ script runs Vitest 4 or 5, or Jest 29, directly. It runs on macOS and Linux with
9
+ Node 22 or 24. The project has one lockfile kind, matching any
10
+ `packageManager` declaration; Yarn needs that declaration. With pnpm, the
11
+ runner is the package `node_modules/RUNNER` links to, inside the checkout.
12
+ Jest also needs jest-util 29 at `node_modules/jest-util`, which pnpm does not
13
+ place there unless it hoists.
14
+
15
+ In a workspace (npm or Yarn `workspaces`, or `packages` in
16
+ `pnpm-workspace.yaml`), `init` gives each package whose test script runs
17
+ Vitest directly its own check, `PACKAGE:test`, run in the package's directory
18
+ with the Vitest that Node finds from there. Its inputs are still the whole
19
+ checkout, so an edit in any package makes it STALE. The root's and every
20
+ package's `node_modules` are observed as installed files, without Vite's
21
+ caches in them. Jest is not supported in workspace packages, and neither is a
22
+ test script at the workspace root. A package added later needs `init` again:
23
+ its manifest appearing makes the workspace's checks UNVERIFIED until then.
24
+
25
+ ## Setup
26
+
27
+ ```sh
28
+ redue init --recipe tests-direct --check test
29
+ redue start
30
+ redue run test
31
+ redue status --sync
32
+ ```
33
+
34
+ `init` picks the `test`, `test:unit` or `test:check` script when it runs a
35
+ runner directly, otherwise the only script that does. The script must be one
36
+ runner invocation: no `&&`, quotes, globs or variable assignments. Watch mode,
37
+ `--changed`, `related`, snapshot updates (`-u`), shuffling, sharding and
38
+ Jest's `--onlyChanged` are refused. Vitest needs `run`, `--run` or
39
+ `--watch=false`. Other test scripts stay recording-only: the
40
+ [general recipe](node-script.md) covers typecheck and lint scripts, not tests.
41
+
42
+ REDUE runs the local runner with Node directly. The package manager, its
43
+ lifecycle scripts and `package.json` are not changed or executed.
44
+
45
+ ## What REDUE claims
46
+
47
+ CURRENT means the last run's inputs are unchanged: every file in the checkout
48
+ outside `node_modules`, the installed files in `node_modules`, the Node binary,
49
+ the exact command, and the environment. Every environment variable the run's
50
+ JavaScript read directly must have the same value, or still be unset. Node and
51
+ libc read some variables natively, and Vite lists the environment for its own,
52
+ so these are always compared: `TZ`, `LANG` and every `NODE_*`, `LC_*`,
53
+ `VITE_*`, `DYLD_*` and `LD_*` variable. Display-only variables are not
54
+ compared: `_`, `TERM`, `TERM_PROGRAM`, `COLORTERM`, `FORCE_COLOR`, `NO_COLOR`,
55
+ `FORCE_TTY`, `COLUMNS`, `LINES` and `AI_AGENT`.
56
+ It does not mean a rerun would give the same result. A test that depends on the
57
+ time of day or on randomness can pass now and fail later on the same inputs.
58
+
59
+ The default config excludes `**/*.md`, `**/*.mdx` and, with `--coverage`,
60
+ `coverage/**`. Editing those files keeps a result CURRENT unless the run read
61
+ them. A run may create git-ignored files (for example under `test/tmp`) and
62
+ read back what it wrote, in the same process or inside a directory it created:
63
+ when every change during the run is such an output, the result keeps the files
64
+ as the run left them.
65
+
66
+ ## The guard
67
+
68
+ Every Node process and worker thread of the run loads a recorder
69
+ (`src/test-recorder.cjs`). After the run REDUE checks the record. The result is
70
+ kept but marked UNVERIFIED, with the reason, when the run:
71
+
72
+ - read a file that the config excludes, or a git-ignored file REDUE does not
73
+ observe, that the run did not create first;
74
+ - changed an input during the run, or read a git-ignored output an earlier
75
+ run left before writing it again: the result depends on that leftover;
76
+ - listed a directory, or read a file over 1 MB, outside the checkout, other
77
+ than files and directories the run created, Jest's cache directory and the
78
+ runners' data directories (Vitest's `vitest` folder in the user data
79
+ directory);
80
+ - connected to anything other than loopback or a local socket, or asked DNS
81
+ about a name other than localhost;
82
+ - started a process that did not load the recorder: a shell, a binary, or a
83
+ Node process with its own environment. A process that replaces itself with
84
+ another program (`process.execve`) counts as starting it, and it must
85
+ become a recorded Node process. A shell is read by its command, as the
86
+ [general recipe](node-script.md#programs) reads it: one that runs another
87
+ program before it becomes Node counts too;
88
+ - used one of Node's bindings with file, network or program access
89
+ (`process.binding('fs')`, `'spawn_sync'` and others), or ran Node with
90
+ `--expose-internals`: the recorder cannot follow those;
91
+ - had an installed file with another hard link, other than a link into the
92
+ pnpm store the install recorded in `node_modules/.modules.yaml` (pnpm's
93
+ default on Linux) or, without a pnpm store, links that are all installed
94
+ files the check observes (esbuild's install links its binary into its
95
+ platform package). The link count of each such file is checked again
96
+ before every answer, so a link added later is noticed. A change through a
97
+ link elsewhere, such as Yarn's hardlinks modes, is not seen. This applies
98
+ to the [general recipe](node-script.md) too.
99
+
100
+ Lookups that can be asked again become run facts, checked before every CURRENT
101
+ answer: whether a path outside the checkout exists, where a link resolves, the
102
+ content of each smaller file read outside it (Linux native bindings read
103
+ `/proc/version` and `/usr/bin/ldd` to pick their C library), and
104
+ the existence and listings of excluded or ignored files in the checkout.
105
+
106
+ When two runs on the same inputs and environment give different results, the
107
+ check is UNVERIFIED until the inputs change.
108
+
109
+ A `--coverage` run may read back the default `coverage/` report: Jest reads
110
+ every JavaScript file under its roots, an earlier report's included, only to
111
+ map dependencies, and that does not decide pass or fail. This is not allowed
112
+ when the configuration names coverage thresholds, which make the report decide
113
+ the result, or another report directory. Configuration can be code, so REDUE
114
+ looks for `coverageThreshold`, `thresholds`, `coverageDirectory` and
115
+ `reportsDirectory` in `package.json`'s `jest` field and the runner's config
116
+ files; when one appears, reading an old report is UNVERIFIED.
117
+
118
+ ## Known causes of UNVERIFIED
119
+
120
+ Tests that write files git does not ignore change the run's inputs.
121
+
122
+ ## Limits
123
+
124
+ - Runner caches and data are trusted: `node_modules/.vite`,
125
+ `node_modules/.vite-temp`, `node_modules/.vitest`, Vitest's typecheck build
126
+ info in `node_modules/vitest/dist`, Jest's cache directory and Vitest's user
127
+ data directory are not inputs.
128
+ - Code that copies the environment and reads the copy, and native code that
129
+ reads files, environment variables or the network, is not seen.
130
+ - Metadata of excluded files (size, times) is not compared, only their
131
+ existence.
132
+ - A file the run creates and another process reads is the run's output only
133
+ when it is inside a directory the run created.
134
+ - Outputs a run keeps are part of the result's inputs afterwards. Deleting
135
+ them, or an observer restart that reindexes without them (git-ignored files
136
+ are not indexed at start), makes the result STALE.
137
+ - A config written by an earlier build of this recipe is UNVERIFIED with "run
138
+ redue init again"; move `redue.config.json` aside and run init.
139
+ - pnpm's store is trusted not to change a file in place, as pnpm never does.
140
+ Editing an installed file in another project that shares the same store
141
+ file changes it here too, unseen. Use `pnpm patch`, or a copying install
142
+ (`package-import-method=copy`), if you edit installed files.
143
+ - Windows is not supported; the check stays recording-only there.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@redue/cli",
3
3
  "private": false,
4
- "version": "0.1.0-alpha.3",
4
+ "version": "0.1.0-alpha.30",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "tag": "alpha"
@@ -11,9 +11,10 @@
11
11
  "redue": "./bin/redue.mjs"
12
12
  },
13
13
  "scripts": {
14
- "test": "node --test --test-concurrency=1 test/*.test.mjs",
14
+ "test": "node test/run-suite.mjs",
15
15
  "test:package": "node test/acceptance/package-smoke.mjs",
16
- "demo": "node test/acceptance/package-smoke.mjs --demo"
16
+ "demo": "node test/acceptance/package-smoke.mjs --demo",
17
+ "pretest": "npm --prefix test/fixtures/typescript-5-5-2 ci --no-audit --no-fund && npm --prefix test/fixtures/typescript-6-0-2 ci --no-audit --no-fund && npm --prefix test/fixtures/typescript-6-0-3 ci --no-audit --no-fund"
17
18
  },
18
19
  "dependencies": {
19
20
  "@parcel/watcher": "2.5.6",