@redue/cli 0.1.0-alpha.26 → 0.1.0-alpha.28
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 +80 -0
- package/README.md +39 -11
- package/bin/redue.mjs +35 -16
- package/docs/user/node-script.md +2 -2
- package/docs/user/sandbox.md +121 -20
- package/docs/user/supported-projects.md +9 -5
- package/package.json +1 -1
- package/src/claude-hook.mjs +9 -1
- package/src/cli.mjs +82 -10
- package/src/daemon.mjs +18 -6
- package/src/declared-plan.mjs +2 -2
- package/src/index.mjs +2 -1
- package/src/landlock.py +351 -7
- package/src/presentation.mjs +2 -1
- package/src/python-onboarding.mjs +9 -7
- package/src/sandbox.mjs +197 -27
- package/src/stats.mjs +72 -0
package/src/cli.mjs
CHANGED
|
@@ -8,8 +8,8 @@ import {patternMatcher} from './glob.mjs';
|
|
|
8
8
|
import {observedBy} from './installed-observation.mjs';
|
|
9
9
|
import {changedFiles,coverageReadable,gitIgnored,recordedEnvironment,testEnvironmentIssue,testGuardConfig,testRunGuard,testRunFacts,sharedInstalledFile,testsInterpretation,testsQualification} from './test-direct.mjs';
|
|
10
10
|
import {childProgram,generalIssue,lookup,pnpmConfigFiles,scriptCaches,scriptInstalledPaths,scriptInterpretation,scriptQualification,scriptRunFacts,pnpmNameless,toolOutput,toolchainDirs} from './node-script.mjs';
|
|
11
|
-
import {denialLine,endWithChild,runToEnd,sandboxArgv,sandboxContext,sandboxDirs,sandboxEnv,
|
|
12
|
-
sandboxQualification,sandboxSupport,systemPlaces} from './sandbox.mjs';
|
|
11
|
+
import {denialLine,endWithChild,hiddenEntries,hiddenQualification,hiddenRefusal,isSandboxed,runToEnd,supervisorIssue,visibleChange,testRun as sandboxTestRun,sandboxArgv,sandboxContext,sandboxDirs,sandboxEnv,
|
|
12
|
+
sandboxInterpretation,sandboxInterpretationOf,sandboxPlaces,sandboxQualification,sandboxSupport,systemPlaces} from './sandbox.mjs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import net from 'node:net';
|
|
15
15
|
import os from 'node:os';
|
|
@@ -29,6 +29,7 @@ import {atomicJson,commitReceipt,readReceipts,receiptRevision} from './state-sto
|
|
|
29
29
|
import {timing,profiling,mark,traceContext} from './decision-profile.mjs';
|
|
30
30
|
import {readCachedStatus,unavailableCached} from './cached-state.mjs';
|
|
31
31
|
import {renderRun} from './presentation.mjs';
|
|
32
|
+
import {note} from './stats.mjs';
|
|
32
33
|
|
|
33
34
|
mark('cli.loaded');
|
|
34
35
|
const here=path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -86,7 +87,7 @@ function environmentReads() {
|
|
|
86
87
|
}
|
|
87
88
|
const recordedRecipes=[testsQualification,scriptQualification];
|
|
88
89
|
// A sandboxed check's run is recorded where its sandbox cannot start (execute).
|
|
89
|
-
function contextHashes() {const reads=config.checks.some(check=>
|
|
90
|
+
function contextHashes() {const reads=config.checks.some(check=>recordedRecipes.includes(check.qualification)||isSandboxed(check.qualification))?environmentReads():{};
|
|
90
91
|
return Object.fromEntries(config.checks.map(check=>[
|
|
91
92
|
check.name,Object.fromEntries([...(check.environment?.variables||[])
|
|
92
93
|
.map(key=>[key,digest(process.env[key]??'<unset>')]),
|
|
@@ -99,7 +100,7 @@ function contextHashes() {const reads=config.checks.some(check=>[...recordedReci
|
|
|
99
100
|
`__vstate_path_executable_${name}__`,pathExecutableIdentity(name)]),
|
|
100
101
|
...(check.environment?.executableIdentity?[['__vstate_node_identity__',nodeIdentity()]]:[]),
|
|
101
102
|
// A sandboxed run's environment, toolchain and places (sandbox.mjs).
|
|
102
|
-
...(check.qualification
|
|
103
|
+
...(isSandboxed(check.qualification)?Object.entries(sandboxContext(check,process.env,root)):[]),
|
|
103
104
|
// The variables the last test run read directly, compared here: which
|
|
104
105
|
// list was compared, and the first variable whose value now differs.
|
|
105
106
|
// Control requests are small, so the values stay out of them.
|
|
@@ -299,7 +300,7 @@ async function execute() {
|
|
|
299
300
|
// A sandboxed check runs inside the OS sandbox. Where the sandbox cannot
|
|
300
301
|
// start, a script the general recipe takes is recorded as that recipe
|
|
301
302
|
// records it (`fallback`); any other runs as it is, and is not reused.
|
|
302
|
-
const sandboxed=selected.qualification
|
|
303
|
+
const sandboxed=isSandboxed(selected.qualification),support=sandboxed?sandboxSupport():null;
|
|
303
304
|
const fallback=Boolean(support?.issue&&typeof selected.script==='string'&&!generalIssue(root,selected.script));
|
|
304
305
|
// A test run's processes record what the guard checks (test-recorder.cjs),
|
|
305
306
|
// and so do a general recipe run's, through the package manager.
|
|
@@ -330,8 +331,21 @@ async function execute() {
|
|
|
330
331
|
if(support.issue)captureIssues.push(`the run was not sandboxed: ${support.issue}`);
|
|
331
332
|
else{
|
|
332
333
|
const dirs=sandboxDirs(state,name);
|
|
334
|
+
// A test run: loopback networking, supervised on Linux (the launcher
|
|
335
|
+
// reports in REDUE's state, outside what the test may touch), a
|
|
336
|
+
// private /tmp, and a fresh home each run: what a test leaves in its
|
|
337
|
+
// home is not observed, so it must not reach a later run.
|
|
338
|
+
const tests=sandboxTestRun(selected),stats=tests&&support.mode==='landlock'?path.join(state,`launcher-${runId}.json`):null;
|
|
339
|
+
if(tests){dirs.home=path.join(dirs.tmp,'home');fs.mkdirSync(dirs.home,{mode:0o700});}
|
|
333
340
|
const reads=[root,dirs.home,dirs.tmp,...sandboxPlaces(selected,process.env,root),...systemPlaces];
|
|
334
|
-
sandbox
|
|
341
|
+
// sandbox-v2 hides the checkout's git-ignored entries. Where git
|
|
342
|
+
// cannot list them, nothing is hidden, and the receipt compares every file.
|
|
343
|
+
const hidden=selected.qualification===hiddenQualification?hiddenEntries(root):null;
|
|
344
|
+
const options={reads,tmp:dirs.tmp,python:support.python,stats,
|
|
345
|
+
...tests?{network:'loopback',writes:[root,dirs.home,dirs.tmp],
|
|
346
|
+
fault:process.env.VSTATE_TEST_FAULTS==='1'?process.env.VSTATE_TEST_SUPERVISOR||null:null}:{}};
|
|
347
|
+
sandbox={mode:support.mode,dirs,reads,hidden,options,stats,
|
|
348
|
+
argv:sandboxArgv(support.mode,command,{...options,hidden:(hidden||[]).map(rel=>path.join(root,rel.replace(/\/$/,'')))}),
|
|
335
349
|
env:sandboxEnv(selected,process.env,{root,...dirs})};
|
|
336
350
|
}
|
|
337
351
|
}
|
|
@@ -476,6 +490,17 @@ async function execute() {
|
|
|
476
490
|
unexplained=touched.filter(file=>!ignored.has(file));
|
|
477
491
|
if(!unexplained.length){eventsExplained=true;if(changed.length)outputs=changed;}
|
|
478
492
|
}
|
|
493
|
+
// A run that hid git-ignored files: what it wrote beneath them (test
|
|
494
|
+
// output, coverage data) is not a change to its inputs (visibleChange);
|
|
495
|
+
// anything else is named.
|
|
496
|
+
let hiddenOnly=false;
|
|
497
|
+
if(sandbox?.hidden&&snap&&end&&snap.planId===end.planId&&snap.observationHealthy&&end.observationHealthy&&
|
|
498
|
+
(snap.inputEventSerial===end.inputEventSerial||end.inputEventPathsComplete===true)){
|
|
499
|
+
const before=snap.files||{},after=end.files||{},events=end.inputEventPaths||[];
|
|
500
|
+
// During the run a new file outside the hidden entries was readable: it counts.
|
|
501
|
+
const found=visibleChange(root,sandbox.hidden,before,after,[...Object.keys(before),...Object.keys(after),...events],{during:true});
|
|
502
|
+
if(found===null){eventsExplained=true;hiddenOnly=true;}else unexplained=[found];
|
|
503
|
+
}
|
|
479
504
|
// The end certificate, recorded or probed, must cover what this compile received.
|
|
480
505
|
// With no input event, gap or content change in between, it also holds at the start.
|
|
481
506
|
// A run checked by full probes is compared with it too, for its run facts.
|
|
@@ -514,8 +539,47 @@ async function execute() {
|
|
|
514
539
|
`: ${named.slice(0,5).join(', ')}${more>0?` and ${more} more`:''}`:'')+unrecorded);
|
|
515
540
|
}
|
|
516
541
|
// A recorded certificate changes only the probe part of the fingerprint.
|
|
517
|
-
if(!outputs&&(!snap||!end||snap.planId!==end.planId||(recording?snap.inputKey!==end.inputKey:snap.fingerprint!==end.fingerprint)||
|
|
542
|
+
if(!outputs&&!hiddenOnly&&(!snap||!end||snap.planId!==end.planId||(recording?snap.inputKey!==end.inputKey:snap.fingerprint!==end.fingerprint)||
|
|
518
543
|
snap.revision!==end.revision))captureIssues.push('declared inputs changed or were not observed');
|
|
544
|
+
// A run that hid git-ignored files and failed with a refusal that names
|
|
545
|
+
// one runs again in the sandbox without hiding them: the check needs
|
|
546
|
+
// them. That run's result is kept, compared on every file, when the
|
|
547
|
+
// inputs held through it too. Its own refusal is handled as below.
|
|
548
|
+
let hiddenRerun=null;
|
|
549
|
+
if(sandbox?.hidden?.length&&result==='FAIL'&&!requestedSignal){
|
|
550
|
+
const line=hiddenRefusal(output.head+'\n'+output.tail,sandbox.hidden);
|
|
551
|
+
if(line){
|
|
552
|
+
console.error(`redue: the sandboxed run failed with what looks like a refusal of a file git ignores (${line}); running it again in the sandbox without hiding them`);
|
|
553
|
+
const again=await runToEnd(sandboxArgv(sandbox.mode,command,sandbox.options),
|
|
554
|
+
{cwd:plan.cwd,env:sandbox.env,echo:true,started:child=>{activeChild=child;}});
|
|
555
|
+
activeChild=null;
|
|
556
|
+
hiddenRerun={line,exitCode:again.status,signal:again.signal||null,...again.error?{errorCode:again.error.code||again.error.name}:{}};
|
|
557
|
+
if(requestedSignal){
|
|
558
|
+
captureIssues.push('interrupted during the run without hidden files');
|
|
559
|
+
Object.assign(invocation,{status:'interrupted',exitCode:null,signal:requestedSignal});
|
|
560
|
+
}
|
|
561
|
+
else if(again.error||again.status===null)
|
|
562
|
+
captureIssues.push(`the run without hidden files did not finish (${again.error?.code||again.signal}), so a refusal of a file git ignores is not ruled out (${line})`);
|
|
563
|
+
else{
|
|
564
|
+
const wait=decisionWait(process.env.VSTATE_CAPTURE_TIMEOUT_MS);
|
|
565
|
+
// Inputs, plan, observer and environment held through it as through the first run.
|
|
566
|
+
let confirmed=null;const environmentNow=contextHashes()[name];
|
|
567
|
+
try{confirmed=await call({action:'snapshot',name,contextHashes:contextHashes(),
|
|
568
|
+
...Number.isSafeInteger(snap?.inputEventSerial)?{sinceSerial:snap.inputEventSerial}:{}},wait.timeout,wait.deadline);}catch{}
|
|
569
|
+
const last=confirmed?.snapshots?.[name];
|
|
570
|
+
if(!last?.observationHealthy||!snap||last.planId!==snap.planId||last.fingerprint!==snap.fingerprint||last.revision!==snap.revision||
|
|
571
|
+
last.inputEventSerial!==end?.inputEventSerial||last.planGeneration!==snap.planGeneration||
|
|
572
|
+
confirmed.observation?.generation!==before?.observation?.generation)
|
|
573
|
+
captureIssues.push('declared inputs changed or were not observed during the run without hidden files');
|
|
574
|
+
if(JSON.stringify(environmentNow)!==JSON.stringify(environmentBefore))
|
|
575
|
+
captureIssues.push('declared environment changed during the run without hidden files');
|
|
576
|
+
invocation.hiddenExitCode=invocation.exitCode;invocation.exitCode=again.status;
|
|
577
|
+
result=again.status===0?'PASS':'FAIL';
|
|
578
|
+
output.head=`${again.stdout}\n${again.stderr}`.slice(0,65536);output.tail='';
|
|
579
|
+
sandbox.hidden=null;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
519
583
|
// A sandboxed run that failed with what looks like a refusal by the
|
|
520
584
|
// sandbox runs again outside it. When that run exits differently, the
|
|
521
585
|
// sandbox changed the result: the plain run's result is kept, not reused.
|
|
@@ -542,6 +606,8 @@ async function execute() {
|
|
|
542
606
|
}
|
|
543
607
|
}
|
|
544
608
|
}
|
|
609
|
+
// A test run whose network was not supervised throughout is not reused.
|
|
610
|
+
if(sandbox?.stats){const issue=supervisorIssue(sandbox.stats);if(issue)captureIssues.push(issue);fs.rmSync(sandbox.stats,{force:true});}
|
|
545
611
|
if(sandbox)fs.rmSync(sandbox.dirs.tmp,{recursive:true,force:true});
|
|
546
612
|
const stable=captureIssues.length===0;
|
|
547
613
|
// Two runs on the same inputs, environment and plan that disagree make the
|
|
@@ -562,7 +628,9 @@ async function execute() {
|
|
|
562
628
|
// A recorded run's receipt carries its run facts, even none; observers that
|
|
563
629
|
// predate run facts do not reuse version 2.
|
|
564
630
|
// The recipe that made the receipt, which status and the hook name.
|
|
565
|
-
|
|
631
|
+
// A sandboxed receipt names what the run did: hid the git-ignored files or not.
|
|
632
|
+
const receipt={result,invocation,target,stable,fingerprintVersion:recorded||testRun?2:1,
|
|
633
|
+
recipe:(fallback?scriptQualification:own?(sandbox?.hidden?hiddenQualification:sandboxQualification):selected.qualification)||null,
|
|
566
634
|
...(recorded||testRun?{runFacts}:{}),
|
|
567
635
|
...(testRun?{verificationRecipe:general?scriptInterpretation:testsInterpretation,guardIssues:guard?.issues||['the run record is unavailable'],
|
|
568
636
|
environmentReads:guard?.environmentReads||{},...(nondeterministic?{nondeterministic}:{})}:{}),
|
|
@@ -576,12 +644,15 @@ async function execute() {
|
|
|
576
644
|
planId:snap?.planId??plan.id,fingerprint:(recording||outputs?end:snap)?.fingerprint??null,
|
|
577
645
|
provider:plan.provider,environmentHashes:environmentBefore,
|
|
578
646
|
...(observedContext?{observedContext,verificationRecipe:directInterpretation}:{}),
|
|
579
|
-
|
|
647
|
+
// `hidden`: the git-ignored entries the run did not see, which the observer does not compare.
|
|
648
|
+
...(own?{verificationRecipe:sandbox?.hidden?sandboxInterpretationOf(hiddenQualification):sandboxInterpretation,
|
|
649
|
+
sandbox:{mode:sandbox?.mode??null,hidden:sandbox?.hidden??null,...hiddenRerun?{hiddenRerun}:{},...sandboxRerun?{rerun:sandboxRerun}:{}}}:{}),
|
|
580
650
|
...(fallback?{sandbox:{mode:null,fallback:'node-script',issue:support.issue}}:{}),
|
|
581
651
|
files:(outputs?end:snap)?.files??null,observerGeneration:before?.observation?.generation??null,
|
|
582
652
|
startRevision:snap?.revision??null,endRevision:end?.revision??null};
|
|
583
653
|
const persistenceStarted=performance.now();
|
|
584
654
|
commitReceipt(state,name,receipt);
|
|
655
|
+
note(state,{kind:'run',check:name,ms:invocation.durationMs??null,result,stable});
|
|
585
656
|
if(testRun){const reads=environmentReads();reads[name]=receipt.environmentReads;atomicJson(readsFile,{schema:1,checks:reads});}
|
|
586
657
|
timing('cli.receipt_persistence',persistenceStarted);
|
|
587
658
|
const reloadStarted=performance.now();
|
|
@@ -671,7 +742,8 @@ async function main(){
|
|
|
671
742
|
await new Promise(r=>setTimeout(r,100));
|
|
672
743
|
if(fs.existsSync(path.join(state,'observer.lock')))throw Error('observer did not stop');}
|
|
673
744
|
}
|
|
674
|
-
|
|
745
|
+
// A stopping observer can still be closing its files (Windows: ENOTEMPTY, EBUSY).
|
|
746
|
+
fs.rmSync(state,{recursive:true,force:true,maxRetries:10,retryDelay:200});
|
|
675
747
|
console.log(JSON.stringify({removed:state}));return;
|
|
676
748
|
}
|
|
677
749
|
if(action==='status'){console.log(JSON.stringify(cached()));return;}
|
package/src/daemon.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import {npmCacheDir} from './npm-integrity.mjs';
|
|
|
25
25
|
import {runFromArgv} from './typescript-invocation.mjs';
|
|
26
26
|
import {buildInfoPath,recordedCertificate} from './direct-run-certificate.mjs';
|
|
27
27
|
import {timing,mark,traceBase,traceContext,traceScope,traceEnvironment,traceId} from './decision-profile.mjs';
|
|
28
|
-
import {sandboxChange} from './sandbox.mjs';
|
|
28
|
+
import {isSandboxed,sandboxChange,visibleChange as sandboxVisibleChange} from './sandbox.mjs';
|
|
29
29
|
|
|
30
30
|
const configFile=path.resolve(process.argv[2]);
|
|
31
31
|
const config=JSON.parse(fs.readFileSync(configFile));
|
|
@@ -348,6 +348,16 @@ function describeChangedInput(plan,file) {
|
|
|
348
348
|
.map(mapping=>mapping.logical).slice(0,2);
|
|
349
349
|
return file+' changed'+(matches.length?` (installed via ${matches.join(', ')})`:'');
|
|
350
350
|
}
|
|
351
|
+
// A run that hid the checkout's git-ignored entries (sandbox@2): the first
|
|
352
|
+
// changed input it could see (sandbox.mjs's visibleChange), or null; once
|
|
353
|
+
// per receipt and pair of fingerprints.
|
|
354
|
+
const hiddenRun=r=>Array.isArray(r?.sandbox?.hidden)&&Boolean(r.files);
|
|
355
|
+
function visibleChange(row,r,name) {
|
|
356
|
+
const key=(r.invocation?.runId||'')+'\0'+r.fingerprint+'\0'+combinedSummary(name).fingerprint;
|
|
357
|
+
if(row.visibleKey!==key){const current=combinedFiles(name);
|
|
358
|
+
row.visibleKey=key;row.visibleChange=sandboxVisibleChange(root,r.sandbox.hidden,r.files,current,[...Object.keys(r.files),...Object.keys(current)]);}
|
|
359
|
+
return row.visibleChange;
|
|
360
|
+
}
|
|
351
361
|
function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=false) {
|
|
352
362
|
if(!index)return [];
|
|
353
363
|
return [...index.checks].map(([name,row])=>{
|
|
@@ -368,7 +378,7 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
368
378
|
return item;}
|
|
369
379
|
if(pendingChecks.has(name)){item.reason='filesystem change pending content inspection';return item;}
|
|
370
380
|
if(r&&r.planId) item.declared_inputs_match=r.planId===row.plan.id&&
|
|
371
|
-
r.fingerprint===combinedSummary(name).fingerprint;
|
|
381
|
+
(r.fingerprint===combinedSummary(name).fingerprint||hiddenRun(r)&&visibleChange(row,r,name)===null);
|
|
372
382
|
if(r&&!(r.fingerprintVersion===1||r.fingerprintVersion===2&&Array.isArray(r.runFacts))){
|
|
373
383
|
item.reason='receipt uses earlier input-resolution format';return item;}
|
|
374
384
|
if(row.plan.unresolved.length){
|
|
@@ -400,7 +410,7 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
400
410
|
item.reason='target failure conflicts with outer invocation outcome';return item;}
|
|
401
411
|
if(!r.stable){const issues=r.checkpoint?.issues?.length?` (${r.checkpoint.issues.join('; ')})`:'';
|
|
402
412
|
// A sandboxed run that could not start, whose result the sandbox changed, or whose rerun outside it did not finish, says so alone.
|
|
403
|
-
const sandbox=r.checkpoint?.issues?.length&&r.checkpoint.issues.every(issue=>/^the (?:sandbox changed|run was not sandboxed|run outside the sandbox)/.test(issue));
|
|
413
|
+
const sandbox=r.checkpoint?.issues?.length&&r.checkpoint.issues.every(issue=>/^the (?:sandbox changed|run was not sandboxed|run outside the sandbox|network supervisor)/.test(issue));
|
|
404
414
|
item.reason=sandbox?r.checkpoint.issues.join('; '):`inputs changed during execution or observation gap${issues}`;return item;}
|
|
405
415
|
const names=[...(row.plan.environment.variables||[]),
|
|
406
416
|
...(row.plan.environment.prefixes||[]).map(prefix=>
|
|
@@ -410,7 +420,7 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
410
420
|
...(row.plan.environment.executableIdentity?['__vstate_node_identity__']:[])];
|
|
411
421
|
// A sandboxed run's variables, toolchain, PATH directories and places
|
|
412
422
|
// (sandbox.mjs): every entry either side has, and the first change named.
|
|
413
|
-
const sandboxChanged=row.plan.qualification
|
|
423
|
+
const sandboxChanged=isSandboxed(row.plan.qualification)&&contextHashes?.[name]?
|
|
414
424
|
sandboxChange(r?.environmentHashes,contextHashes[name]):null;
|
|
415
425
|
if(names.length&&!contextHashes?.[name])unconfirmed??='execution environment context not supplied';
|
|
416
426
|
item.freshness='STALE';
|
|
@@ -431,8 +441,10 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
431
441
|
item.freshness='UNVERIFIED';item.reason='environment variables the last run read were not compared';}
|
|
432
442
|
else item.reason=`environment variable ${contextHashes[name].__redue_env_changed__} differs from the last run`;
|
|
433
443
|
}
|
|
434
|
-
else if(r.fingerprint!==combinedSummary(name).fingerprint) {
|
|
444
|
+
else if(r.fingerprint!==combinedSummary(name).fingerprint&&!(hiddenRun(r)&&visibleChange(row,r,name)===null)) {
|
|
435
445
|
const key=r.fingerprint+'\0'+combinedSummary(name).fingerprint;
|
|
446
|
+
if(row.staleReasonKey!==key&&hiddenRun(r)){
|
|
447
|
+
row.staleInputs=[visibleChange(row,r,name)];row.staleReason=describeChangedInput(row.plan,row.staleInputs[0]);row.staleReasonKey=key;}
|
|
436
448
|
if(row.staleReasonKey!==key){
|
|
437
449
|
let changed=row.lastReason;row.staleInputs=[];
|
|
438
450
|
const candidate=changed?.endsWith(' changed')?changed.slice(0,-8):null;
|
|
@@ -807,7 +819,7 @@ function markInputEvent(name,rel,type,replay) {
|
|
|
807
819
|
const serial=(inputEventSerial.get(name)||0)+1;
|
|
808
820
|
inputEventSerial.set(name,serial);
|
|
809
821
|
// Kept only for recorded test and script checks and sandboxed ones; trimmed in batches.
|
|
810
|
-
if(['npm-tests-direct-v1','node-script-v1'
|
|
822
|
+
if(['npm-tests-direct-v1','node-script-v1'].includes(index?.checks.get(name)?.plan.qualification)||isSandboxed(index?.checks.get(name)?.plan.qualification)){
|
|
811
823
|
const list=inputEventPaths.get(name)||[];list.push([serial,rel]);
|
|
812
824
|
if(list.length>2*eventPathLimit)list.splice(0,list.length-eventPathLimit);inputEventPaths.set(name,list);
|
|
813
825
|
}
|
package/src/declared-plan.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {qualifyTypeScript} from './typescript-qualification.mjs';
|
|
|
8
8
|
import {directBuildInfo} from './direct-typescript.mjs';
|
|
9
9
|
import {qualifyTests,testWorkspacePatterns,testsQualification} from './test-direct.mjs';
|
|
10
10
|
import {qualifyNodeScript,scriptQualification} from './node-script.mjs';
|
|
11
|
-
import {qualifySandbox,sandboxInstalledExclusions
|
|
11
|
+
import {isSandboxed,qualifySandbox,sandboxInstalledExclusions} from './sandbox.mjs';
|
|
12
12
|
import {yarnWorkspaceTypecheckInputs} from './yarn-workspace-inputs.mjs';
|
|
13
13
|
import {pnpmTypecheckInputs} from './pnpm-inputs.mjs';
|
|
14
14
|
import {checkRun} from './typescript-invocation.mjs';
|
|
@@ -81,7 +81,7 @@ export function discoverDeclared(config) {
|
|
|
81
81
|
const excluded=buildInfo?['!'+buildInfo]:[];
|
|
82
82
|
// A sandboxed check leaves out the .venv's links that lead out of the
|
|
83
83
|
// checkout; its qualification names them.
|
|
84
|
-
const sandboxed=selected.qualification
|
|
84
|
+
const sandboxed=isSandboxed(selected.qualification);
|
|
85
85
|
if(sandboxed)excluded.push(...sandboxInstalledExclusions(root));
|
|
86
86
|
const installedStarted=performance.now();
|
|
87
87
|
const installed=resolveDeclaredInstalled(root,
|
package/src/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import micromatch from './glob.mjs';
|
|
|
6
6
|
import {sha} from './plan.mjs';
|
|
7
7
|
import {resolveLinks} from './installed-inputs.mjs';
|
|
8
8
|
import {validInputRelative,identityFields,racyUntil} from './path-identity.mjs';
|
|
9
|
+
import {isSandboxed} from './sandbox.mjs';
|
|
9
10
|
|
|
10
11
|
const tracked = root => [...new Set(execFileSync('git',
|
|
11
12
|
['ls-files','--cached','--others','--exclude-standard','-z'],
|
|
@@ -204,7 +205,7 @@ export class InputIndex {
|
|
|
204
205
|
// (its package.json stands for them).
|
|
205
206
|
// The second form is for recorded checks only: their recorder
|
|
206
207
|
// classifies a read through the link where it leads.
|
|
207
|
-
const recorded=name=>['node-script-v1','npm-tests-direct-v1'
|
|
208
|
+
const recorded=name=>['node-script-v1','npm-tests-direct-v1'].includes(this.checks.get(name).plan.qualification)||isSandboxed(this.checks.get(name).plan.qualification);
|
|
208
209
|
const observedDir=installed?physical=>!this.includeInstalled&&
|
|
209
210
|
([...this.installedRootSet].some(root=>physical.startsWith(root+'/'))||
|
|
210
211
|
!physical.split('/').includes('node_modules')&&!this.installedPath(physical+'/package.json')&&matched.every(recorded))&&
|