@redue/cli 0.1.0-alpha.16 → 0.1.0-alpha.18

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/CHANGELOG.md CHANGED
@@ -1,5 +1,102 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-alpha.18 — 2026-10-23
4
+
5
+ Typecheck in more project shapes, and pnpm test reuse on Linux. In the census
6
+ of 108 public TypeScript repos, the ones `init` makes ready went from 8 to 11
7
+ for typecheck (the census clones each repo's current head, so one or two
8
+ repos move between runs) and stayed at 22 for tests.
9
+
10
+ - pnpm test checks are reused on Linux. A recorded run accepts an installed
11
+ file hard-linked into the pnpm store the install recorded, when the store
12
+ holds it at its content address (pnpm names store files by their sha512).
13
+ Links anywhere else stay UNVERIFIED. Each file is hashed once; REDUE keeps
14
+ its identity and store path in its state and hashes it again only when the
15
+ identity changes. A 46 MB install takes about 160 ms to check the first
16
+ time. Limit: an in-place edit of an installed file in another project that
17
+ shares the store file is not seen until the next recorded run.
18
+
19
+ - Typecheck scripts in more shapes. A script of `tsc --noEmit` runs with
20
+ `-p`/`--project`, `--skipLibCheck` or `--pretty`, in any argument order,
21
+ qualifies for the direct npm recipe and the pnpm contract. A chain of such
22
+ runs joined by `&&` becomes one check per run (`typecheck.1`,
23
+ `typecheck.2`); the hook skips the script only when every run is CURRENT
24
+ and otherwise runs the stale ones through `redue run`. `check-types` joins
25
+ the recognized script names.
26
+ - The hook acts on a typecheck script's form only while the script in
27
+ package.json still has exactly the configured `tsc` runs. Before, a script
28
+ changed to `tsc --noEmit && eslint .` was rewritten to `redue run
29
+ typecheck`, which ran tsc alone.
30
+ - Without a typecheck script, `init` proposes a `tsc --noEmit` check for a
31
+ root `tsconfig.json` (direct npm recipe, pnpm) and for each pnpm workspace
32
+ package with one, unless a script runs a TypeScript checker in another form
33
+ (`tsc -b`, `vue-tsc`, ...). package.json is not changed; the check is
34
+ written only when the contract holds.
35
+ - The pnpm contract no longer needs `include: ["src"]`. Its source inputs are
36
+ the directories the config's `include` covers, as TypeScript computes them
37
+ for new files, minus `node_modules`. `exclude` entries stay inputs, since an
38
+ import can still reach an excluded file. A config extended from an installed
39
+ package is an installed input.
40
+ - Fixed: a config with three or more pnpm or test checks never got a
41
+ synchronized answer. The observer dropped control requests over 4 KB, and a
42
+ sync request carries about 1.5 KB of context digests per check; status
43
+ showed "Unexpected end of JSON input" for every check. The limit is 1 MiB.
44
+
45
+ - The direct npm TypeScript recipe takes any TypeScript 5.x or 6.0, not only
46
+ the five reviewed releases. Another release qualifies when package-lock.json
47
+ records the registry's `typescript` at that version with an integrity
48
+ (sha512, or sha1 from registries that record only sha1), npm's cache holds
49
+ the tarball under it, and the installed package is exactly the tarball's
50
+ files. Nothing is downloaded; a missing tarball
51
+ keeps the check recording-only with `npm-tarball-uncached`. The check takes
52
+ about 100 ms per full probe (TypeScript 5.x and 6.0 tarballs, 120-140 files).
53
+ CI runs the direct contract suite against 5.0.4, 5.1.6, 5.2.2, 5.3.3, 5.4.5,
54
+ 5.5.4, 5.7.3 and 5.8.3.
55
+
56
+ - Contributors: `npm test` runs up to four test files at a time, keeping two
57
+ cores free (a 3-core CI macOS runner runs one at a time), and takes about
58
+ 6 minutes on the owner's Mac (was 19.6). The two TypeScript 6 fixture
59
+ suites are separate files so they run side by side.
60
+
61
+ ## 0.1.0-alpha.17 — 2026-10-22
62
+
63
+ Workspaces and package managers beyond npm. In the census of 108 public
64
+ TypeScript repos, the ones `init` makes ready went from 5 to 8 for typecheck
65
+ and from 6 to 22 for tests.
66
+
67
+ - pnpm 9-12 for the pnpm TypeScript contract (it took only an exact pnpm 12
68
+ pin). The installed `.modules.yaml` names the version; a pin is optional,
69
+ with or without Corepack's `+sha512` hash (pins with a hash never matched
70
+ before). `.npmrc` settings that only change what gets installed are
71
+ accepted; settings that move or copy installed files, or run code during
72
+ install, are refused in either file: custom modules, virtual-store or
73
+ lockfile directories, the global virtual store, injected workspace
74
+ packages, separate lockfiles, pnpmfile hooks, `configDependencies`.
75
+ - The tests recipe takes single-package pnpm 9-12 and Yarn `node-modules`
76
+ installs, and gives each workspace package (npm or Yarn `workspaces`, pnpm
77
+ `packages`) whose test script runs Vitest directly its own `PACKAGE:test`
78
+ check, run in the package directory. Inputs stay the whole checkout; every
79
+ package's node_modules is an installed input. Jest and root test scripts in
80
+ workspaces stay recording-only. A package added later makes the
81
+ workspace's checks UNVERIFIED until `init` runs again.
82
+ - A recorded test run is UNVERIFIED when an installed file has another hard
83
+ link: a package store outside the checkout (pnpm's default on Linux, Yarn's
84
+ hardlinks modes) could change it unseen. pnpm on macOS clones files and is
85
+ not affected; on Linux, pnpm test checks qualify but are not reused unless
86
+ pnpm copies files.
87
+ - Observer: an installed link to a directory (pnpm links every package) is
88
+ observed by its link chain when the target is itself observed installed;
89
+ any other directory link still makes observation unavailable.
90
+ - Workspace patterns honour `!pattern` exclusions; before, a negated pattern
91
+ matched every directory.
92
+ - Fixed: a new workspace package manifest did not re-plan a test check, for
93
+ npm workspaces too.
94
+
95
+ Eval on alpha.17 code (CI 39828002753, 84 sessions, test tasks on twilio,
96
+ cheerio and SwitchHosts plus the pnpm project devops-daily): 0 critical
97
+ findings; 23 skipped test runs, each passing three plain reruns; nothing was
98
+ skipped on devops-daily, whose recorded runs were UNVERIFIED on the Linux
99
+ runner.
3
100
  ## 0.1.0-alpha.16 — 2026-10-22
4
101
 
5
102
  Changes to the Claude Code integration, and one fix for commands that meet
package/README.md CHANGED
@@ -37,7 +37,7 @@ it; relevant source edits make it stale. [Run the reproducible demo](docs/contri
37
37
 
38
38
  ## Install the alpha
39
39
 
40
- The opt-in pilot uses **0.1.0-alpha.16**. Follow the
40
+ The opt-in pilot uses **0.1.0-alpha.18**. Follow the
41
41
  [fixed-version pilot guide](docs/user/pilot.md) for supported projects, setup,
42
42
  updates and voluntary feedback. No external usage results are claimed yet.
43
43
 
package/bin/redue.mjs CHANGED
@@ -4,7 +4,8 @@ import {mark,traceBase,traceContext,traceEnvironment,traceId} from '../src/decis
4
4
  traceBase({...traceContext(),request:traceId(),work:'client'});
5
5
  mark('client.loaded');
6
6
  import {directCompiler,directQualification,directEnvironment,directInterpretation} from '../src/direct-typescript.mjs';
7
- import {testEnvironment,testRunnerEntry,testsInterpretation,testsQualification} from '../src/test-direct.mjs';
7
+ import {runArgv,tscRun} from '../src/typescript-invocation.mjs';
8
+ import {testEnvironment,testRunnerAt,testsInterpretation,testsQualification} from '../src/test-direct.mjs';
8
9
  import os from 'node:os';
9
10
  import path from 'node:path';
10
11
  import {createHash} from 'node:crypto';
@@ -194,7 +195,7 @@ function which(name){
194
195
  if(found)return found;
195
196
  error(`cannot resolve executable ${name} on PATH`);
196
197
  }
197
- function token(value){
198
+ function token(value,cwd='.'){
198
199
  if(value==='@node')return process.execPath;
199
200
  if(value==='@project')return root;
200
201
  if(value==='@typescript-compiler:.'){
@@ -202,8 +203,8 @@ function token(value){
202
203
  }
203
204
  if(value.startsWith('@test-runner:')){
204
205
  // Qualification reports a missing runner; status and stop still work.
205
- try{return testRunnerEntry(root,value.slice('@test-runner:'.length));}
206
- catch{return path.join(root,'node_modules',value.slice('@test-runner:'.length),'<unavailable>');}
206
+ try{return testRunnerAt(root,cwd,value.slice('@test-runner:'.length)).entry;}
207
+ catch{return path.join(root,cwd,'node_modules',value.slice('@test-runner:'.length),'<unavailable>');}
207
208
  }
208
209
  if(value==='@vstate/direct-typescript-probe')return path.join(productRoot,'src','direct-typescript-probe.mjs');
209
210
  if(value.startsWith('@typescript-bin:')){
@@ -226,10 +227,10 @@ function token(value){
226
227
  if(value.startsWith('@which:'))return which(value.slice(7));
227
228
  return value;
228
229
  }
229
- function argv(values,label){
230
+ function argv(values,label,cwd){
230
231
  if(!Array.isArray(values)||!values.length||values.some(v=>typeof v!=='string'||!v))
231
232
  error(`${label} must be nonempty exact argv`);
232
- return values.map(token);
233
+ return values.map(value=>token(value,cwd));
233
234
  }
234
235
  const categories=['source','generated','installedDependencies','environment','toolchain','runtime',
235
236
  'probeContinuity'];
@@ -256,11 +257,12 @@ const checks=publicConfig.checks.map(check=>{
256
257
  const yarnWorkspace=check.qualification==='yarn-workspace-tsc-v1';
257
258
  if(check.qualification&&!npmAutomatic&&!npmDirect&&!yarnWorkspace&&!pnpmTypecheck&&!testsDirect)
258
259
  error(`${check.name}: unknown qualification contract`);
259
- if(npmDirect&&(manager!=='npm'||!check.script||check.cwd&&check.cwd!=='.'||
260
- JSON.stringify(check.command)!==JSON.stringify(['@node','@typescript-compiler:.','--noEmit'])))
260
+ if(npmDirect&&(manager!=='npm'||check.cwd&&check.cwd!=='.'||!Array.isArray(check.command)||
261
+ check.command[0]!=='@node'||check.command[1]!=='@typescript-compiler:.'||
262
+ !tscRun({},{script:null,args:check.command.slice(2)}).run?.noEmit))
261
263
  error(`${check.name}: direct recipe requires its explicit local compiler command`);
262
264
  // The script's own arguments follow the runner; qualification compares them with the script.
263
- if(testsDirect&&(manager!=='npm'||!check.script||check.cwd&&check.cwd!=='.'||!Array.isArray(check.command)||
265
+ if(testsDirect&&(!['npm','pnpm','yarn'].includes(manager)||!check.script||check.cwd&&check.cwd!=='.'&&!check.workspace||!Array.isArray(check.command)||
264
266
  check.command[0]!=='@node'||!/^@test-runner:(?:vitest|jest)$/.test(check.command[1]||'')))
265
267
  error(`${check.name}: test recipe requires its explicit local test runner command`);
266
268
  if(npmAutomatic&&(manager!=='npm'||check.cwd&&check.cwd!=='.'))
@@ -270,22 +272,29 @@ const checks=publicConfig.checks.map(check=>{
270
272
  check.command[1]!=='workspace'||check.command[2]!==check.workspace||
271
273
  check.command[3]!=='run'))
272
274
  error(`${check.name}: Yarn workspace qualification needs its discovered workspace command`);
273
- if(pnpmTypecheck&&(manager!=='pnpm'||!check.script||!check.command||
275
+ if(pnpmTypecheck&&(manager!=='pnpm'||!Array.isArray(check.command)||
274
276
  check.command[0]!=='@node'||
275
277
  check.command[1]!==`@typescript-bin:${check.cwd||'.'}`||
276
278
  check.workspace&&check.cwd==='.'||
277
- !check.command.slice(2).every(value=>typeof value==='string'&&value)))
279
+ !tscRun({},{script:null,args:check.command.slice(2)}).run))
278
280
  error(`${check.name}: pnpm qualification needs its discovered standalone TypeScript command`);
279
281
  const reason=check.kind==='test'?'test runtime, transforms and cache inputs need review':
280
282
  check.kind==='lint'?'lint plugins, config and resolver inputs need review':
281
283
  check.kind==='build'?'build environment and generated inputs need review':
282
284
  'check inputs need review';
283
- const autoProbes=npmDirect?[['@node','@vstate/direct-typescript-probe','@project',check.script]]:npmAutomatic?[['@node','@vstate/typescript-contract-probe',
285
+ // A chained script has one check per tsc run: each check's run counts them,
286
+ // so a run added to or removed from the script invalidates them all.
287
+ const runCount=(npmDirect||pnpmTypecheck)&&check.script?publicConfig.checks.filter(other=>
288
+ other.qualification===check.qualification&&other.script===check.script&&
289
+ (other.cwd||'.')===(check.cwd||'.')&&(other.workspace||null)===(check.workspace||null)).length:1;
290
+ if(runCount>16)error(`${check.name}: more than 16 checks for script ${check.script}`);
291
+ const run=npmDirect||pnpmTypecheck?runArgv({script:check.script??null,args:check.command.slice(2),count:runCount}):null;
292
+ const autoProbes=npmDirect?[['@node','@vstate/direct-typescript-probe','@project',...run]]:npmAutomatic?[['@node','@vstate/typescript-contract-probe',
284
293
  '@project',check.script,'@which:npm']]:yarnWorkspace?
285
294
  [['@node','@vstate/yarn-workspace-typecheck-probe','@project',
286
295
  check.workspace,check.command[4],'@which:yarn']]:pnpmTypecheck?
287
296
  [['@node','@vstate/pnpm-typecheck-probe','@project',
288
- check.workspace||'.',check.script]]:[];
297
+ check.workspace||'.',...run]]:[];
289
298
  const windowsContext=process.platform==='win32'?
290
299
  ['ComSpec','COMSPEC','PATHEXT','USERPROFILE']:[];
291
300
  const autoEnvironment=npmDirect?directEnvironment:testsDirect?testEnvironment:npmAutomatic?{variables:['NODE_OPTIONS','NODE_PATH','CI','HOME',
@@ -297,7 +306,8 @@ const checks=publicConfig.checks.map(check=>{
297
306
  {variables:['NODE_OPTIONS','NODE_PATH','CI','HOME','NODE_ENV','BASH_ENV','ENV',...windowsContext],
298
307
  prefixes:['PNPM_','COREPACK_','npm_config_','DYLD_','TSGO_'],
299
308
  pathExecutables:['node'],executableIdentity:true}:{};
300
- return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`),
309
+ return {name:check.name,command:argv(scriptCommand||check.command,`${check.name}.command`,
310
+ typeof check.cwd==='string'&&check.cwd&&!path.isAbsolute(check.cwd)&&!check.cwd.split('/').includes('..')?check.cwd:'.'),
301
311
  cwd:check.cwd,inputs:check.inputs||[],generatedInputs:check.generatedInputs||[],
302
312
  installedInputs:check.installedInputs||(
303
313
  npmAutomatic||npmDirect||testsDirect?['node_modules/**']:[]),
@@ -309,6 +319,7 @@ const checks=publicConfig.checks.map(check=>{
309
319
  coverageReasons:check.coverageReasons||Object.fromEntries(
310
320
  categories.map(category=>[category,reason])),
311
321
  qualification:check.qualification||null,script:check.script||null,
322
+ ...(runCount>1?{runCount}:{}),
312
323
  ...(npmAutomatic?{qualificationInterpretation:'npm-launcher-withheld@1'}:{}),
313
324
  ...(npmDirect?{qualificationInterpretation:directInterpretation}:{}),
314
325
  ...(testsDirect?{qualificationInterpretation:testsInterpretation}:{}),
@@ -122,6 +122,17 @@ post-scripts. In every other case the command runs as typed: turbo caches its
122
122
  own results, so the hook never rewrites it. Filters (`--filter`) and nx are not
123
123
  matched.
124
124
 
125
+ For a typecheck check that runs the compiler, the script's forms act only when
126
+ the script in package.json still has exactly the configured runs: one `tsc`
127
+ run per check with the same arguments, and no pre- or post-script. Otherwise
128
+ the command runs as typed. A script that chains `tsc` runs with `&&` has one
129
+ check per run, `typecheck.1`, `typecheck.2` and so on (see
130
+ [direct TypeScript](direct-typescript.md#script-forms)); each run's own
131
+ `npx tsc ...` form matches its check. The hook skips the script when all its
132
+ runs are CURRENT, PASS and `reuse_eligible`, and otherwise rewrites it to
133
+ `redue run` of the runs that are not, in the script's order, joined by `&&`.
134
+ In a turbo or `pnpm -r` fan-out, a package's script counts only the same way.
135
+
125
136
  Each form may end with `2>&1` and then one `| tail -n N`, `| head -n N`,
126
137
  `| tail -N` or `| head -N`. It may also be the first command of a `;` or `&&`
127
138
  chain. Nothing else matches. Each form has its own `if` rule, so other Bash
@@ -56,6 +56,10 @@ Advanced explicit contracts may declare `generatedInputs`, `installedInputs`, na
56
56
  reviewed escape hatches, not onboarding requirements. A coverage rationale cannot
57
57
  override unsupported storage, unresolved links, missing inputs, or observation failure.
58
58
 
59
+ A chained typecheck script gets one generated check per `tsc` run, all with
60
+ the script's name (see [script forms](direct-typescript.md#script-forms)). A
61
+ proposed check for a project without a typecheck script has no `script`.
62
+
59
63
  Input patterns are relative to the project root. They cannot name a `.git`
60
64
  directory, because REDUE does not observe one; `!.git/**` is fine.
61
65
 
@@ -4,8 +4,10 @@ This check means: the reviewed local TypeScript compiler completed the configure
4
4
  check against declared, observed inputs and relevant execution context. It does
5
5
  not mean npm or npm lifecycle scripts completed.
6
6
 
7
- For a supported single-package npm project with an existing standalone
8
- `tsc --noEmit` script, installed dependencies, Node 22/24 and TypeScript 5.5.2/5.6.3/5.9.3/6.0.2/6.0.3:
7
+ For a supported single-package npm project with a typecheck script of `tsc`
8
+ runs (see [script forms](#script-forms)) or no typecheck script and a root
9
+ `tsconfig.json`, installed dependencies, Node 22/24 and TypeScript 5.x or 6.0
10
+ (see [compiler identity](#compiler-identity)):
9
11
 
10
12
  ```sh
11
13
  redue init --recipe typescript-direct --check typecheck --dry-run
@@ -33,9 +35,64 @@ A generated check contains:
33
35
 
34
36
  `script` identifies the reviewed source recipe; `command` is what actually runs:
35
37
  the resolved Node executable and local TypeScript implementation, with exact argv
36
- and no shell. TypeScript 5.5.2 and 5.6.3 use `lib/tsc.js`; 5.9.3, 6.0.2 and 6.0.3 use `lib/_tsc.js` directly,
37
- without its outer compile-cache-enabling shim. No compiler is fetched or bundled.
38
- The entire installed package must match reviewed official distribution bytes.
38
+ and no shell. The entry is `lib/_tsc.js` where the package has one (5.7 and
39
+ later), without its outer compile-cache-enabling shim, and `lib/tsc.js`
40
+ otherwise. No compiler is fetched or bundled.
41
+
42
+ ## Compiler identity
43
+
44
+ The entire installed `node_modules/typescript` must be an official package,
45
+ byte for byte, in one of two ways:
46
+
47
+ - TypeScript 5.5.2, 5.6.3, 5.9.3, 6.0.2 or 6.0.3: its files match the digest
48
+ REDUE ships for that release.
49
+ - Any other TypeScript 5.x or 6.0 release: package-lock.json records the
50
+ registry's `typescript` package at the installed version (no alias, an
51
+ `https` tarball URL, not a file or git source) with an integrity (sha512,
52
+ or sha1 where a registry records only sha1), npm's cache holds the tarball
53
+ under that integrity, and the installed files are exactly the tarball's
54
+ files. REDUE reads the cache only; it never downloads. The cache is the one
55
+ npm uses: `npm_config_cache`, else `cache` in the project's `.npmrc`, then
56
+ the user's, else `~/.npm` (`%LocalAppData%\npm-cache` on Windows); the
57
+ global npmrc is not read. `npm-shrinkwrap.json` is not read on this path.
58
+ `npm ci` fills the cache. When the tarball is missing the check stays
59
+ recording-only with the reason `npm-tarball-uncached`; `npm cache add
60
+ typescript@VERSION` adds it. The check's compiler identity is then the
61
+ lockfile integrity, so a lockfile change re-probes.
62
+
63
+ CI runs the same contract suite against the last release of each minor from
64
+ 5.0 to 5.8 that the reviewed releases do not cover. Later minors (6.1 and up)
65
+ wait until CI covers them. TypeScript 7 (the native compiler) is not
66
+ supported.
67
+
68
+ ## Script forms
69
+
70
+ A typecheck script qualifies when it is one or more `tsc` runs joined by `&&`,
71
+ each with `--noEmit` and only these arguments, in any order: `-p`/`--project`
72
+ with a config file or directory inside the project, `--skipLibCheck`, and
73
+ `--pretty` (optionally `true` or `false`). For example
74
+ `tsc -p tsconfig.app.json --noEmit` or
75
+ `tsc --noEmit && tsc --noEmit -p tsconfig.node.json`. `tsc -b`, `--watch`,
76
+ other options, quotes, variables and other commands keep the script
77
+ recording-only.
78
+
79
+ A chain gets one check per run, named `typecheck.1`, `typecheck.2` and so on,
80
+ each with the script's name and that run's arguments in its command. Each run
81
+ is checked as its own program against its own config, and is reused only while
82
+ the script still has as many runs as the config has checks for it, one of them
83
+ with exactly those arguments. Adding or removing a run, or a pre- or
84
+ post-script, makes every check of the script not reusable until `init` runs
85
+ again. The script as a whole
86
+ (`npm run typecheck`) is skipped only when its current runs and the configured
87
+ checks correspond one to one and all are CURRENT (see [agents](agents.md)).
88
+
89
+ With no typecheck script and a root `tsconfig.json`, init proposes a
90
+ `tsc --noEmit` check with no `script` field, unless a script already runs a
91
+ TypeScript checker in another form (`tsc -b`, `vue-tsc`, `svelte-check`,
92
+ `tsgo`, `astro check`, `nuxi typecheck`). package.json is not changed. The
93
+ check is written only when the contract holds; otherwise no typecheck check is
94
+ added. The pnpm contract does the same for each workspace package with a
95
+ `tsconfig.json` and no typecheck script.
39
96
 
40
97
  Existing configs are never overwritten. To compare choices, use a new project-local
41
98
  `--config redue.direct.config.json` with init; this explicitly selects separate
@@ -1,4 +1,4 @@
1
- # Opt-in development pilot: alpha.16
1
+ # Opt-in development pilot: alpha.18
2
2
 
3
3
  Use one fixed build for several ordinary development sessions. The owner will
4
4
  invite two or three willing testers and approve their projects; nothing is
@@ -9,8 +9,9 @@ controlled compiler/agent trials are acceptance evidence, not independent adopti
9
9
 
10
10
  Start with a trusted, single-package npm Git checkout, normally installed locked
11
11
  dependencies, and an existing standalone `tsc --noEmit` script without pre/post
12
- scripts. The direct recipe accepts only the official, unchanged TypeScript
13
- **5.5.2, 5.6.3, 5.9.3, 6.0.2 or 6.0.3** packages and Node **22 or 24**. No workspaces, named
12
+ scripts. The direct recipe accepts only an official, unchanged TypeScript **5.x or
13
+ 6.0** package (a reviewed release, or one matching the tarball package-lock.json
14
+ names in npm's cache) and Node **22 or 24**. No workspaces, named
14
15
  tsconfig selection, emitted/incremental/composite builds, custom loaders or
15
16
  outside-root inputs are added by this pilot. Do not downgrade or change your
16
17
  project to fit; record an unsupported setup as feedback.
@@ -35,7 +36,7 @@ are **NOT RUN**. The recording shortcut is macOS/direct-recipe only.
35
36
  For a new default npm installation:
36
37
 
37
38
  ```sh
38
- npm install --global @redue/cli@0.1.0-alpha.16
39
+ npm install --global @redue/cli@0.1.0-alpha.18
39
40
  redue --version
40
41
  cd my-project
41
42
  redue --config redue.config.json init --recipe typescript-direct --check typecheck --dry-run
@@ -89,7 +90,7 @@ using it; keep the same npm prefix, config and any explicit state selection.
89
90
 
90
91
  ```sh
91
92
  redue --config redue.config.json stop
92
- npm install --global @redue/cli@0.1.0-alpha.16
93
+ npm install --global @redue/cli@0.1.0-alpha.18
93
94
  redue --config redue.config.json agent setup codex --dry-run
94
95
  redue --config redue.config.json agent setup codex --apply
95
96
  redue --config redue.config.json start
@@ -18,8 +18,12 @@ matches are reported as ambiguous rather than guessed. It never creates a new
18
18
  project script. Automatic npm-launcher applicability was withdrawn in alpha.4;
19
19
  ordinary npm scripts remain recording-only. See the [maintainer notice](npm-launcher-notice.md).
20
20
  The direct recipe is opt-in, available since alpha.5. The opt-in `init --recipe typescript-direct` supports
21
- a standalone `tsc --noEmit` script in a single-package npm installation using
22
- reviewed TypeScript 5.5.2/5.6.3/5.9.3/6.0.2/6.0.3 and Node 22/24. It executes the local compiler,
21
+ a typecheck script of `tsc --noEmit` runs in any argument order, chained runs,
22
+ or no typecheck script at all (a proposed `tsc --noEmit`; see
23
+ [script forms](direct-typescript.md#script-forms)), in a single-package npm installation using
24
+ TypeScript 5.x or 6.0 (a reviewed release, or one whose installed files match the
25
+ tarball package-lock.json names in npm's cache; see
26
+ [compiler identity](direct-typescript.md#compiler-identity)) and Node 22/24. It executes the local compiler,
23
27
  not npm. Full installed compiler identity, config/membership, installed inputs
24
28
  and relevant Node environment are checked before reuse. See the
25
29
  [exact direct boundary](direct-typescript.md). Unsupported plugins, preloads,
@@ -45,13 +49,31 @@ project whose test script runs Vitest 4/5 or Jest 29 directly, the opt-in
45
49
  In particular, test caches and build-generated inputs are not excluded to
46
50
  make evidence green.
47
51
 
48
- Pinned pnpm 12 projects are read from `packageManager`, `pnpm-workspace.yaml`,
49
- the lockfile, and the installed `node_modules/.modules.yaml` metadata. The
52
+ pnpm 9-12 projects are read from `packageManager`, `pnpm-workspace.yaml`,
53
+ `.npmrc`, the lockfile, and the installed `node_modules/.modules.yaml`
54
+ metadata. The installed metadata names the pnpm version; a `packageManager`
55
+ pin is optional, and when present (with or without Corepack's `+sha512` hash)
56
+ it must name the installed version. Settings that only change what gets
57
+ installed are accepted, because the installed files are what REDUE observes.
58
+ Settings that move or copy installed files, or run code during install, are
59
+ not: custom `modulesDir`, `virtualStoreDir` or `lockfileDir`, the global
60
+ virtual store, injected workspace packages, separate workspace lockfiles,
61
+ pnpmfile hooks and `configDependencies`. They are refused when either
62
+ `pnpm-workspace.yaml` or `.npmrc` sets them. The linker is read from the file
63
+ the installed major reads (`.npmrc` for pnpm 9, `pnpm-workspace.yaml` then
64
+ `.npmrc` for pnpm 10, `pnpm-workspace.yaml` for pnpm 11 and 12) and must match
65
+ the installed one. Only the project's files are read: settings from a user or
66
+ global rc file, or `npm_config_*` variables during install, are not. The
50
67
  supported local layouts are pnpm's standard isolated and hoisted
51
68
  `node_modules` with a project-local `.pnpm` virtual store. `init` can discover
52
- root and workspace scripts. For a standalone `tsc --noEmit` or `tsc -p ...`
53
- script in the narrow TypeScript contract, it records the exact installed
54
- compiler invocation directly. This avoids pnpm 12's task-run bookkeeping
69
+ root and workspace scripts. For a typecheck script of `tsc` runs
70
+ ([script forms](direct-typescript.md#script-forms); `tsc -p .` without
71
+ `--noEmit` is also accepted), or a package with a `tsconfig.json` and no
72
+ typecheck script, it records the exact installed compiler invocation directly.
73
+ The source inputs are the directories the config's `include` covers, as
74
+ TypeScript itself computes them for new files, minus `node_modules` (`exclude`
75
+ entries stay inputs: an import can still reach an excluded file); a file the
76
+ program reads outside them keeps the check recording-only. This avoids pnpm's task-run bookkeeping
55
77
  writes during `pnpm run`; it does not alter the project's script. Test, lint,
56
78
  build, and ambiguous TypeScript scripts still execute through `pnpm run` and
57
79
  start recording-only. The generated `@typescript-bin:WORKSPACE` token resolves
@@ -2,8 +2,24 @@
2
2
 
3
3
  `redue init --recipe tests-direct --check test` records a project's unit test
4
4
  run and reports later whether that result still applies to the files you have.
5
- It supports a single-package npm project whose test script runs Vitest 4 or 5,
6
- or Jest 29, directly. It runs on macOS and Linux with Node 22 or 24.
5
+ It supports a single-package project installed by npm, pnpm 9-12 (isolated or
6
+ hoisted `node_modules`) or Yarn with the `node-modules` linker, whose test
7
+ script runs Vitest 4 or 5, or Jest 29, directly. It runs on macOS and Linux with
8
+ Node 22 or 24. The project has one lockfile kind, matching any
9
+ `packageManager` declaration; Yarn needs that declaration. With pnpm, the
10
+ runner is the package `node_modules/RUNNER` links to, inside the checkout.
11
+ Jest also needs jest-util 29 at `node_modules/jest-util`, which pnpm does not
12
+ place there unless it hoists.
13
+
14
+ In a workspace (npm or Yarn `workspaces`, or `packages` in
15
+ `pnpm-workspace.yaml`), `init` gives each package whose test script runs
16
+ Vitest directly its own check, `PACKAGE:test`, run in the package's directory
17
+ with the Vitest that Node finds from there. Its inputs are still the whole
18
+ checkout, so an edit in any package makes it STALE. The root's and every
19
+ package's `node_modules` are observed as installed files, without Vite's
20
+ caches in them. Jest is not supported in workspace packages, and neither is a
21
+ test script at the workspace root. A package added later needs `init` again:
22
+ its manifest appearing makes the workspace's checks UNVERIFIED until then.
7
23
 
8
24
  ## Setup
9
25
 
@@ -21,8 +37,8 @@ runner invocation: no `&&`, quotes, globs or variable assignments. Watch mode,
21
37
  Jest's `--onlyChanged` are refused. Vitest needs `run`, `--run` or
22
38
  `--watch=false`. Other scripts stay recording-only.
23
39
 
24
- REDUE runs the local runner with Node directly. npm, its lifecycle scripts and
25
- `package.json` are not changed or executed.
40
+ REDUE runs the local runner with Node directly. The package manager, its
41
+ lifecycle scripts and `package.json` are not changed or executed.
26
42
 
27
43
  ## What REDUE claims
28
44
 
@@ -63,6 +79,10 @@ kept but marked UNVERIFIED, with the reason, when the run:
63
79
  about a name other than localhost;
64
80
  - started a process that did not load the recorder: a shell, a binary, or a
65
81
  Node process with its own environment;
82
+ - had an installed file with another hard link, other than a link into the
83
+ pnpm store the install recorded in `node_modules/.modules.yaml` (pnpm's
84
+ default on Linux). A change through a link elsewhere, such as Yarn's
85
+ hardlinks modes, is not seen.
66
86
 
67
87
  Lookups that can be asked again become run facts, checked before every CURRENT
68
88
  answer: whether a path outside the checkout exists, where a link resolves, the
@@ -103,4 +123,8 @@ Tests that write files git does not ignore change the run's inputs.
103
123
  are not indexed at start), makes the result STALE.
104
124
  - A config written by an earlier build of this recipe is UNVERIFIED with "run
105
125
  redue init again"; move `redue.config.json` aside and run init.
126
+ - pnpm's store is trusted not to change a file in place, as pnpm never does.
127
+ Editing an installed file in another project that shares the same store
128
+ file changes it here too, unseen. Use `pnpm patch`, or a copying install
129
+ (`package-import-method=copy`), if you edit installed files.
106
130
  - 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.16",
4
+ "version": "0.1.0-alpha.18",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "tag": "alpha"
@@ -11,7 +11,7 @@
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
16
  "demo": "node test/acceptance/package-smoke.mjs --demo",
17
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"
@@ -134,7 +134,7 @@ function earlierBlock(host,rows=[]){
134
134
  }
135
135
  // One form per check: the package manager's script form when there is one,
136
136
  // since that is what the project's own instructions usually name.
137
- const blockForms=(host,config,relative)=>host!=='claude'?[]:hookForms(config,relative).map(row=>({kind:row.kind,
137
+ const blockForms=(host,config,relative)=>host!=='claude'?[]:hookForms(config,relative).filter(row=>!row.part).map(row=>({kind:row.kind,
138
138
  form:row.forms.find(form=>/^(?:npm run|pnpm|yarn) /.test(form))??row.forms.find(form=>!form.startsWith('redue '))??row.forms[0]}));
139
139
  const priorBlocks=(host,relative)=>[false,true].map(legacy=>`\n\n<!-- REDUE:${host}:begin -->\n`+
140
140
  `Before deciding whether configured verification needs repeating, use REDUE.\n`+
@@ -166,7 +166,7 @@ function knownBlock(host,relative,block){
166
166
  function skillFor(relative,host,config){
167
167
  // Double quotes work in POSIX shells, cmd and PowerShell for ordinary names.
168
168
  const redue=redueFor(relative)??`redue --config ${JSON.stringify(relative)}`,status=`${redue} status --sync --json`;
169
- const rows=host==='claude'?hookForms(config,relative):[];
169
+ const rows=host==='claude'?hookForms(config,relative).filter(row=>!row.part):[];
170
170
  const label=row=>row.kind==='test'?(row.check==='test'?'Unit tests':`Unit tests \`${row.check}\``):
171
171
  row.check==='typecheck'?'Typecheck':`Typecheck \`${row.check}\``;
172
172
  const commands=rows.map(row=>`- ${label(row)}: `+