@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/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,sandboxInterpretation,sandboxPlaces,
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=>[...recordedRecipes,sandboxQualification].includes(check.qualification))?environmentReads():{};
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===sandboxQualification?Object.entries(sandboxContext(check,process.env,root)):[]),
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===sandboxQualification,support=sandboxed?sandboxSupport():null;
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={mode:support.mode,dirs,argv:sandboxArgv(support.mode,command,{reads,tmp:dirs.tmp,python:support.python}),
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
- const receipt={result,invocation,target,stable,fingerprintVersion:recorded||testRun?2:1,recipe:(fallback?scriptQualification:selected.qualification)||null,
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
- ...(own?{verificationRecipe:sandboxInterpretation,sandbox:{mode:sandbox?.mode??null,...sandboxRerun?{rerun:sandboxRerun}:{}}}:{}),
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
- fs.rmSync(state,{recursive:true,force:true});
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==='sandbox-v1'&&contextHashes?.[name]?
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','sandbox-v1'].includes(index?.checks.get(name)?.plan.qualification)){
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
  }
@@ -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,sandboxQualification} from './sandbox.mjs';
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===sandboxQualification;
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','sandbox-v1'].includes(this.checks.get(name).plan.qualification);
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))&&