@redue/cli 0.1.0-alpha.29 → 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.
- package/CHANGELOG.md +27 -0
- package/bin/redue.mjs +19 -18
- package/docs/user/getting-started.md +4 -3
- package/docs/user/sandbox.md +26 -12
- package/docs/user/supported-projects.md +5 -5
- package/docs/user/tests-direct.md +2 -2
- package/package.json +1 -1
- package/src/node-onboarding.mjs +10 -3
- package/src/sandbox.mjs +16 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.30 — 2026-11-03
|
|
4
|
+
|
|
5
|
+
`init` runs a check fewer times, and tests try the sandbox first.
|
|
6
|
+
|
|
7
|
+
- **Fewer trial runs at init.** A check proposed for the sandboxed recipe
|
|
8
|
+
now runs once plainly and once in the sandbox with ignored files and
|
|
9
|
+
documentation hidden (`sandbox-v3`). If they differ, it runs once more
|
|
10
|
+
with the whole checkout (`sandbox-v1`), compared with the same plain run.
|
|
11
|
+
When the plain run passed and the sandboxed one failed only on reading a
|
|
12
|
+
hidden file, the check gets `sandbox-v1` without that run. `init` no
|
|
13
|
+
longer tries `sandbox-v2`; existing `sandbox-v2` checks keep working.
|
|
14
|
+
On the 13 census repos with the slowest checks, `init --dry-run` took
|
|
15
|
+
4740 s in all before and 3870 s after on Linux, 6929 s and 5784 s on
|
|
16
|
+
macOS. OneUptime went from 1204 s to 903 s (Linux) and 1206 s to 605 s
|
|
17
|
+
(macOS). Nomi did not get faster (485 s to 383 s on Linux, 836 s to
|
|
18
|
+
893 s on macOS): its typecheck agrees on the first trial, and each of its
|
|
19
|
+
two runs stops at init's 5-minute limit.
|
|
20
|
+
- **Tests try the sandbox first.** For a Node test script, `init` tries the
|
|
21
|
+
sandboxed recipe before the direct test recipe, which becomes its
|
|
22
|
+
fallback, when both would run the same script. In the census, on the repos where both qualified (2 on each OS),
|
|
23
|
+
the direct recipe's results went STALE when the project's build rewrote
|
|
24
|
+
its output, and the sandboxed results stayed CURRENT. A test script now
|
|
25
|
+
costs `init` one plain run and one or two sandboxed runs.
|
|
26
|
+
- Known limit: artsy/eigen's typecheck, which fails in the census, no
|
|
27
|
+
longer agrees with the whole checkout at init and falls back to the
|
|
28
|
+
general recipe (Linux) or recording (macOS).
|
|
29
|
+
|
|
3
30
|
## 0.1.0-alpha.29 — 2026-11-03
|
|
4
31
|
|
|
5
32
|
Editing documentation no longer makes sandboxed checks STALE, a fast check
|
package/bin/redue.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import {directCompiler,directQualification,directEnvironment,directInstalledInpu
|
|
|
7
7
|
import {runArgv,tscRun} from '../src/typescript-invocation.mjs';
|
|
8
8
|
import {testEnvironment,testRunnerAt,testsInterpretation,testsQualification} from '../src/test-direct.mjs';
|
|
9
9
|
import {scriptEnvironment,scriptInterpretation,scriptKinds,scriptQualification} from '../src/node-script.mjs';
|
|
10
|
-
import {docsQualification,hiddenDocsIssue,hiddenListIssue,
|
|
10
|
+
import {docsQualification,hiddenDocsIssue,hiddenListIssue,isSandboxed,pythonTool,sandboxEnvironment,sandboxInterpretationOf,sandboxKinds,sandboxQualification} from '../src/sandbox.mjs';
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import {createHash} from 'node:crypto';
|
|
@@ -156,11 +156,15 @@ if(command==='init'){
|
|
|
156
156
|
discovery.checks=chosen;discovery.config.checks=chosen.map(row=>row.config);
|
|
157
157
|
}
|
|
158
158
|
// A sandboxed check is proposed only when it gives the same exit status and
|
|
159
|
-
// output inside the sandbox as outside it
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// (
|
|
163
|
-
//
|
|
159
|
+
// output inside the sandbox as outside it, with the checkout's git-ignored
|
|
160
|
+
// files and documentation hidden (sandbox-v3). When the plain run passed
|
|
161
|
+
// and the sandboxed one failed only because it was refused a file it hid
|
|
162
|
+
// (sandboxTrial's refusedHidden), the check gets the whole checkout
|
|
163
|
+
// (sandbox-v1) without another run: without the hiding it can only fail
|
|
164
|
+
// where the plain run passes. Any other difference is tried once with the
|
|
165
|
+
// whole checkout, reusing the plain run. Otherwise init writes what it
|
|
166
|
+
// would have without the sandboxed recipe, with the reason. Each check
|
|
167
|
+
// costs one plain run and one or two sandboxed runs.
|
|
164
168
|
const trialIssue=trial=>trial.issue?`the sandbox cannot run here (${trial.issue})`:
|
|
165
169
|
// A failure both ways that shows a refusal cannot be told from a real one.
|
|
166
170
|
trial.same?null:trial.plain!==trial.sandboxed?`exit ${trial.sandboxed} inside the sandbox, ${trial.plain} outside it${trial.line?` (${trial.line})`:''}`:
|
|
@@ -175,18 +179,15 @@ if(command==='init'){
|
|
|
175
179
|
if(!docs.issue&&docs.same){
|
|
176
180
|
row.config={...row.config,qualification:docsQualification};row.qualification=docsQualification;
|
|
177
181
|
row.reason=row.reason.replace('the whole checkout is its input',"the checkout's files git doesn't ignore, except its documentation, are its input")+`; ran both ways at init, exit ${docs.plain}`;continue;}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
`; ran both ways at init, exit ${hidden.plain}; it needs the documentation (${trialIssue(docs)})`;continue;}
|
|
185
|
-
// Without git, nothing is hidden: the whole checkout is tried on its own.
|
|
186
|
-
const trial=hidden.plainRun?await sandboxTrial(discovery.root,row.config,{plain:hidden.plainRun}):
|
|
187
|
-
hidden.issue===hiddenListIssue?await sandboxTrial(discovery.root,row.config):hidden;
|
|
182
|
+
if(!docs.issue&&docs.refusedHidden){
|
|
183
|
+
row.reason+=`; at init it passed, then failed reading a file that was hidden (${docs.refusedHidden}), so the whole checkout is its input, not run that way at init`;continue;}
|
|
184
|
+
// Without a list of the ignored files or the documentation, the whole checkout is tried on its own.
|
|
185
|
+
const trial=docs.plainRun?await sandboxTrial(discovery.root,row.config,{plain:docs.plainRun}):
|
|
186
|
+
[hiddenListIssue,hiddenDocsIssue].includes(docs.issue)?await sandboxTrial(discovery.root,row.config):docs;
|
|
187
|
+
if(trial!==docs&&!trial.issue)trials++;
|
|
188
188
|
const issue=trialIssue(trial);
|
|
189
|
-
if(!issue){row.reason+=`; ran both ways at init, exit ${trial.plain};
|
|
189
|
+
if(!issue){row.reason+=`; ran both ways at init, exit ${trial.plain}; the whole checkout is its input: `+
|
|
190
|
+
(docs.issue?docs.issue:`with the files git ignores and the documentation hidden, ${trialIssue(docs)}`);continue;}
|
|
190
191
|
if(row.fallback)discovery.checks[at]={...row.fallback,reason:`Sandboxed recipe unavailable: ${issue}; ${row.fallback.reason}`};
|
|
191
192
|
else{discovery.checks[at]=null;discovery.skipped.push({kind:row.config.name,reason:`${row.config.name}: sandboxed recipe unavailable: ${issue}`});}
|
|
192
193
|
}
|
|
@@ -230,7 +231,7 @@ if(command==='init'){
|
|
|
230
231
|
if(!samePath(realObservedPath(path.dirname(configFile)),discovery.root))
|
|
231
232
|
error('init writes only at the repository root; use --config there or --dry-run');
|
|
232
233
|
fs.writeFileSync(configFile,JSON.stringify(discovery.config,null,2)+'\n',{flag:'wx'});
|
|
233
|
-
if(!json){console.log(`Created ${configFile}. ${trials?'
|
|
234
|
+
if(!json){console.log(`Created ${configFile}. ${trials?'Checks ran inside and outside the sandbox to choose their recipe; no results were recorded.':'No checks were executed.'}`);
|
|
234
235
|
console.log('Ready to qualify means the supported contract will be validated; it is not verification evidence.');
|
|
235
236
|
console.log(`Next: review the config, then redue start and redue run ${JSON.stringify(discovery.checks[0].config.name)}.`);
|
|
236
237
|
console.log('Connect your agent: redue agent setup codex (or claude). Preview with --dry-run.');
|
|
@@ -100,11 +100,12 @@ redue explain typecheck
|
|
|
100
100
|
Use a check actually printed by `init`. For a workspace:
|
|
101
101
|
`redue init --workspace @example/package --check typecheck`.
|
|
102
102
|
Initialization previews detected commands and qualification confidence before writing
|
|
103
|
-
`redue.config.json`. An existing config is never overwritten. No
|
|
104
|
-
|
|
103
|
+
`redue.config.json`. An existing config is never overwritten. No dependencies are
|
|
104
|
+
installed; a check proposed for the sandboxed recipe runs once plainly and once or
|
|
105
|
+
twice in the sandbox, and no results are recorded. A missing/unsupported boundary is explained. `init` tries
|
|
105
106
|
the strict recipes first and uses the [general recipe](node-script.md) for a root
|
|
106
107
|
typecheck or lint script none of them takes. Test scripts use the
|
|
107
|
-
[direct test recipe](tests-direct.md).
|
|
108
|
+
[sandboxed recipe](sandbox.md) first, then the [direct test recipe](tests-direct.md).
|
|
108
109
|
|
|
109
110
|
“Ready to qualify” means REDUE has a supported recipe whose prerequisites will be
|
|
110
111
|
validated. It is not a receipt. “Recording-only” can record PASS/FAIL without claiming
|
package/docs/user/sandbox.md
CHANGED
|
@@ -20,8 +20,13 @@ built declarations) gets the whole checkout. See
|
|
|
20
20
|
|
|
21
21
|
## Where it applies
|
|
22
22
|
|
|
23
|
-
- **Tests:** a root `test` script the
|
|
24
|
-
|
|
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.
|
|
25
30
|
- **Node projects:** a root `typecheck` or `lint` script (npm, pnpm, or Yarn
|
|
26
31
|
with the `node-modules` linker). `init` tries the
|
|
27
32
|
[direct TypeScript recipe](direct-typescript.md) and the pnpm and Yarn
|
|
@@ -70,11 +75,17 @@ keeps `uv run` or `poetry run` in front, so the environment still syncs first.
|
|
|
70
75
|
|
|
71
76
|
## Files git ignores and documentation
|
|
72
77
|
|
|
73
|
-
`init` runs each check
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`sandbox-
|
|
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.
|
|
78
89
|
|
|
79
90
|
With `sandbox-v3`, each run also hides the documentation: the files git
|
|
80
91
|
lists (and does not ignore) named `*.md`, `*.mdx`, `*.rst`, `LICENSE*` or
|
|
@@ -88,7 +99,7 @@ like one, or a change made while the run ran still counts. On macOS the
|
|
|
88
99
|
files with those extensions are refused by name pattern, in any case and in
|
|
89
100
|
`node_modules` too: `README.MD` or `Docs/logo.svg` is refused but not
|
|
90
101
|
listed, so an edit to it counts, and a check that reads one gets
|
|
91
|
-
`sandbox-
|
|
102
|
+
`sandbox-v1` on macOS. `LICENSE*` and `CHANGELOG*` files are refused by the
|
|
92
103
|
exact paths git lists, so `license-check.ts` stays readable. On Linux each
|
|
93
104
|
listed file is hidden like a git-ignored one. In a checkout with more than
|
|
94
105
|
100 `LICENSE*` and `CHANGELOG*` files (vendored packages), those files are
|
|
@@ -218,10 +229,13 @@ CURRENT means the last sandboxed run's inputs are unchanged:
|
|
|
218
229
|
A run the sandbox refused something fails like any other. REDUE does not run
|
|
219
230
|
every failure twice to find out:
|
|
220
231
|
|
|
221
|
-
- `init` runs each proposed check
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
timings, the temporary and home directories, and lines about caches.
|
|
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)).
|
|
225
239
|
Otherwise `init` writes what it would have without this recipe and says why.
|
|
226
240
|
These are the only runs `init` makes.
|
|
227
241
|
- When a `sandbox-v2` or `sandbox-v3` run fails with a refusal that names a hidden file, it
|
|
@@ -57,11 +57,11 @@ changes. Unsupported custom resolution, project references, outside-root
|
|
|
57
57
|
source links, and installation modes remain recording-only. Other Yarn
|
|
58
58
|
typechecks, Vitest/Jest, ESLint, and builds in workspace packages start
|
|
59
59
|
recording-only unless a separately justified explicit contract is provided.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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).
|
|
65
65
|
In particular, test caches and build-generated inputs are not excluded to
|
|
66
66
|
make evidence green.
|
|
67
67
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Direct test recipe (Vitest and Jest)
|
|
2
2
|
|
|
3
|
-
`redue init
|
|
4
|
-
|
|
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
5
|
still applies to the files you have.
|
|
6
6
|
It supports a single-package project installed by npm, pnpm 9-12 (isolated or
|
|
7
7
|
hoisted `node_modules`) or Yarn with the `node-modules` linker, whose test
|
package/package.json
CHANGED
package/src/node-onboarding.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import {testProject,testRecipeScript,testCheckInputs,testsQualification} from '.
|
|
|
3
3
|
import {generalIssue,scriptCheckInputs,scriptProject,scriptQualification} from './node-script.mjs';
|
|
4
4
|
import {sandboxCheckInputs,sandboxKinds,sandboxQualification} from './sandbox.mjs';
|
|
5
5
|
// Init proposes the general recipe for typecheck and lint scripts; test
|
|
6
|
-
// scripts use the direct test recipe
|
|
6
|
+
// scripts use the sandboxed recipe first, then the direct test recipe
|
|
7
|
+
// (Vitest, Jest).
|
|
7
8
|
const generalKinds=['typecheck','lint'];
|
|
8
9
|
const perPackageLimit=50;
|
|
9
10
|
import fs from 'node:fs';
|
|
@@ -302,7 +303,8 @@ export function discoverNodeProject(root,{recipe=null}={}) {
|
|
|
302
303
|
// sandboxed recipe, which also covers programs the general recipe cannot
|
|
303
304
|
// record. It is ready only once init has run it both ways (bin/redue.mjs);
|
|
304
305
|
// `fallback` is the row init writes when that shows a difference or the
|
|
305
|
-
// sandbox cannot run here: the general recipe's, when it takes the script
|
|
306
|
+
// sandbox cannot run here: the general recipe's, when it takes the script,
|
|
307
|
+
// or the direct test recipe's.
|
|
306
308
|
// Where the sandbox cannot start at run time, a script the general recipe
|
|
307
309
|
// takes is recorded as it would record it (cli.mjs).
|
|
308
310
|
// A script whose program is not installed gets none, and neither does one
|
|
@@ -310,7 +312,12 @@ export function discoverNodeProject(root,{recipe=null}={}) {
|
|
|
310
312
|
const writes=script&&['pre'+script,script,'post'+script].map(name=>scripts[name]).find(text=>typeof text==='string'&&
|
|
311
313
|
/(?:^|\s)--(?:fix|write|unsafe-fixes)(?=[\s=]|$)/.test(text));
|
|
312
314
|
if(writes)classification={...classification,reason:`Sandboxed recipe unavailable (the script changes files: ${writes.match(/--(?:fix|write|unsafe-fixes)/)[0]}); ${classification.reason}`};
|
|
313
|
-
|
|
315
|
+
// A test script gets it before the direct test recipe, which is then its
|
|
316
|
+
// fallback, when both run the same script: in the census the direct
|
|
317
|
+
// recipe's results went STALE when the build changed its output, and the
|
|
318
|
+
// sandboxed ones stayed CURRENT.
|
|
319
|
+
if((auto||recipe==='sandbox')&&script&&sandboxKinds.includes(kind)&&!observationIssue&&
|
|
320
|
+
(classification.level!=='ready'||auto&&kind==='test'&&check.script===script)&&
|
|
314
321
|
!/REDUE cannot find/.test(generalIssue||'')&&!writes){
|
|
315
322
|
try{
|
|
316
323
|
const project=scriptProject(root,script);
|
package/src/sandbox.mjs
CHANGED
|
@@ -583,7 +583,19 @@ export function runToEnd(argv,{cwd,env,timeout=0,echo=false,started=null}){
|
|
|
583
583
|
child.on('close',(status,signal)=>finish({status,signal}));
|
|
584
584
|
});
|
|
585
585
|
}
|
|
586
|
-
//
|
|
586
|
+
// The line that shows a run that failed in the sandbox, and passed outside
|
|
587
|
+
// it, was refused a file it hid, when nothing else differs that a refusal
|
|
588
|
+
// could explain: no other refusal line, no supervisor issue, and not a line
|
|
589
|
+
// the plain run printed too. A run without the hiding then could only fail
|
|
590
|
+
// where the plain one passes, never pass where it fails.
|
|
591
|
+
function refusedHidden(plain,sandboxed,entries,supervisor){
|
|
592
|
+
if(plain.status!==0||supervisor||!entries.length)return null;
|
|
593
|
+
const line=hiddenRefusal(sandboxed.output,entries);
|
|
594
|
+
if(!line||plain.output.includes(line))return null;
|
|
595
|
+
const others=sandboxed.output.split(/\r?\n/).filter(text=>!hiddenRefusal(text,entries)).join('\n');
|
|
596
|
+
return denialLine(others)?null:line;
|
|
597
|
+
}
|
|
598
|
+
// {same, plain, sandboxed, line, refusedHidden} or {issue} when the sandbox cannot run here.
|
|
587
599
|
// With `hide` 'docs', the documentation (hiddenDocs) is hidden too.
|
|
588
600
|
// With `hide`, the git-ignored entries hiddenEntries lists are kept from the
|
|
589
601
|
// sandboxed run, and it also returns them, their count, the run times and, on
|
|
@@ -632,8 +644,10 @@ export async function sandboxTrial(root,check,{timeout=300000,hide=false,plain:e
|
|
|
632
644
|
const counts=tests?[plain,sandboxed].map(r=>testCounts(r.output)):null;
|
|
633
645
|
const read=counts?.some(c=>Object.values(c).some(value=>value!==null));
|
|
634
646
|
const differs=tests&&read?(JSON.stringify(counts[0])===JSON.stringify(counts[1])?null:{plain:JSON.stringify(counts[0]),sandboxed:JSON.stringify(counts[1])}):output;
|
|
635
|
-
|
|
647
|
+
const same=plain.status===sandboxed.status&&!denied&&!differs&&!supervisor;
|
|
648
|
+
return {same,plain:plain.status,sandboxed:sandboxed.status,output:differs,plainRun:plain,
|
|
636
649
|
line:supervisor||denied||(plain.status===sandboxed.status?null:sandboxed.output.trim().split('\n').at(-1)?.slice(0,200)||null),
|
|
650
|
+
refusedHidden:same?null:refusedHidden(plain,sandboxed,entries,supervisor),
|
|
637
651
|
...hide?{hidden:entries.length,entries,docs:docEntries.length,plainMs:plain.ms,sandboxedMs:sandboxed.ms,...launcher?{rules:launcher.rules,setupMs:launcher.ms}:{}}:{}};
|
|
638
652
|
}finally{fs.rmSync(scratch,{recursive:true,force:true});}
|
|
639
653
|
}
|