@redue/cli 0.0.0-stage → 0.1.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +180 -0
  3. package/README.md +162 -2
  4. package/bin/redue.mjs +371 -0
  5. package/bin/vstate.mjs +3 -0
  6. package/docs/README.md +21 -0
  7. package/docs/user/agents.md +253 -0
  8. package/docs/user/configuration.md +60 -0
  9. package/docs/user/direct-typescript.md +114 -0
  10. package/docs/user/getting-started.md +148 -0
  11. package/docs/user/json.md +59 -0
  12. package/docs/user/npm-launcher-notice.md +42 -0
  13. package/docs/user/pilot.md +145 -0
  14. package/docs/user/receipt-selection-notice.md +60 -0
  15. package/docs/user/shell.md +25 -0
  16. package/docs/user/supported-projects.md +90 -0
  17. package/examples/redue.config.json +8 -0
  18. package/package.json +47 -4
  19. package/src/agent-integration.mjs +294 -0
  20. package/src/agent-policy.md +70 -0
  21. package/src/cached-state.mjs +42 -0
  22. package/src/claude-hook.mjs +115 -0
  23. package/src/cli.mjs +499 -0
  24. package/src/control-endpoint.mjs +11 -0
  25. package/src/daemon.mjs +1236 -0
  26. package/src/decision-profile.mjs +32 -0
  27. package/src/decision-validation.mjs +51 -0
  28. package/src/declared-plan.mjs +125 -0
  29. package/src/direct-run-certificate.mjs +185 -0
  30. package/src/direct-typescript-probe.mjs +22 -0
  31. package/src/direct-typescript.mjs +109 -0
  32. package/src/executable-lookup.mjs +18 -0
  33. package/src/glob.mjs +9 -0
  34. package/src/history-worker.mjs +26 -0
  35. package/src/index.mjs +306 -0
  36. package/src/installed-inputs.mjs +110 -0
  37. package/src/linux-inotify.py +228 -0
  38. package/src/linux-observer.mjs +79 -0
  39. package/src/node-onboarding.mjs +274 -0
  40. package/src/npm-typecheck-environment.mjs +36 -0
  41. package/src/npm-typecheck-probe.mjs +120 -0
  42. package/src/observation.mjs +59 -0
  43. package/src/owned-state.mjs +32 -0
  44. package/src/path-identity.mjs +43 -0
  45. package/src/plan-worker.mjs +140 -0
  46. package/src/plan.mjs +10 -0
  47. package/src/platform-observation.mjs +17 -0
  48. package/src/platform-state.mjs +14 -0
  49. package/src/pnpm-inputs.mjs +355 -0
  50. package/src/pnpm-typecheck-probe.mjs +47 -0
  51. package/src/presentation.mjs +53 -0
  52. package/src/probe-result.mjs +25 -0
  53. package/src/process-liveness.mjs +11 -0
  54. package/src/project-location.mjs +34 -0
  55. package/src/run-lock.mjs +77 -0
  56. package/src/run-recorder.cjs +78 -0
  57. package/src/state-store.mjs +73 -0
  58. package/src/toolchain-probe.mjs +21 -0
  59. package/src/typescript-contract-probe.mjs +5 -0
  60. package/src/typescript-input-facts.mjs +68 -0
  61. package/src/typescript-list.mjs +141 -0
  62. package/src/typescript-qualification.mjs +77 -0
  63. package/src/windows-command.mjs +16 -0
  64. package/src/windows-process.mjs +15 -0
  65. package/src/yarn-workspace-inputs.mjs +148 -0
  66. package/src/yarn-workspace-typecheck-probe.mjs +107 -0
@@ -0,0 +1,140 @@
1
+ import {parentPort,workerData} from 'node:worker_threads';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import {createHash} from 'node:crypto';
5
+ import {discover} from './plan.mjs';
6
+ import {InputIndex} from './index.mjs';
7
+ import {directProject,directQualification} from './direct-typescript.mjs';
8
+ import {queriesMatch} from './typescript-list.mjs';
9
+ import {planGuard,inputKey} from './decision-validation.mjs';
10
+ import {timing,traceBase} from './decision-profile.mjs';
11
+
12
+ traceBase(workerData.diagnostic);
13
+ try {
14
+ const totalStarted=performance.now();
15
+ const configStarted=performance.now();
16
+ const config=JSON.parse(fs.readFileSync(workerData.config));
17
+ timing('worker.config_load',configStarted);
18
+ let discoveryStarted=performance.now();
19
+ let bundle=workerData.reuse?.bundle;
20
+ let validatedIndex=null,guard=null,reuseReason='no_validated_plan',planReused=false;
21
+ if(bundle){
22
+ const started=performance.now();
23
+ try{
24
+ guard=planGuard(bundle);
25
+ const queryStarted=performance.now();
26
+ // Cached membership queries load the compiler API; establish its reviewed
27
+ // implementation before that optimization can execute project code.
28
+ const directRoots=new Set(config.checks.filter(check=>check.qualification===directQualification)
29
+ .map(check=>directProject(config.root,check.script).tsRoot));
30
+ if(directRoots.size&&(workerData.reuse.queries||[]).some(([kind,,,options])=>
31
+ kind==='readDirectory'&&!directRoots.has(options?.tsRoot)))throw Error('retained compiler implementation changed');
32
+ const queriesValid=guard===workerData.reuse.guard&&
33
+ (!workerData.reuse.queries?.length||queriesMatch(workerData.reuse.queries));
34
+ timing('worker.compiler_queries',queryStarted,{queries:workerData.reuse.queries?.length||0,matched:queriesValid?1:0});
35
+ if(guard!==workerData.reuse.guard||!queriesValid)reuseReason='configuration_or_resolution_changed';
36
+ else {
37
+ validatedIndex=new InputIndex(bundle);
38
+ validatedIndex.coldScan(progress=>parentPort.postMessage({kind:'progress',
39
+ phase:'validating',scanned:progress.scanned,total:progress.total}));
40
+ if(validatedIndex.unavailable)throw Error(validatedIndex.unavailable);
41
+ if(planGuard(bundle)!==guard)throw Error('plan dependencies changed during validation');
42
+ // Compiler imports can change effective membership. Until that content
43
+ // has been rediscovered, never reuse a compiler input contract.
44
+ const changed=[...validatedIndex.checks].some(([name,row])=>
45
+ row.plan.qualification&&inputKey(row)!==workerData.reuse.inputs[name]);
46
+ reuseReason=changed?'compiler_inputs_changed':'dependencies_unchanged';
47
+ planReused=!changed;
48
+ }
49
+ }catch{reuseReason='validation_unavailable';}
50
+ timing('worker.plan_validation',started,{reason:reuseReason,
51
+ files:validatedIndex?.files.size||0,
52
+ bytes:validatedIndex?.rehashedBytes||0,
53
+ hashingMs:Math.round(validatedIndex?.profile.hashingMs||0),
54
+ enumerationMs:Math.round(validatedIndex?.profile.enumerationMs||0)});
55
+ }
56
+ discoveryStarted=performance.now();
57
+ if(!planReused){bundle=discover(config,workerData.state);validatedIndex=null;
58
+ guard=planGuard(bundle);}
59
+ timing('worker.discovery_reason',performance.now(),{reason:reuseReason,reused:planReused?1:0});
60
+ const discoveryMs=performance.now()-discoveryStarted;
61
+ timing(planReused?'worker.discovery_reused':'worker.discovery',discoveryStarted,{checks:Object.keys(bundle.plans).length});
62
+ parentPort.postMessage({kind:'progress',phase:'indexing',discoveryMs,scanned:0,total:null});
63
+ const digest=file=>createHash('sha256').update(fs.readFileSync(file)).digest('hex');
64
+ let retained=null;
65
+ const retainedStarted=performance.now();
66
+ try {if(workerData.forceCold)throw Error('fresh reconciliation required');
67
+ retained=JSON.parse(fs.readFileSync(path.join(workerData.state,'index-v1.json')));
68
+ const identity=fs.statSync(bundle.root,{bigint:true});
69
+ if(retained.schema!==1||retained.root!==bundle.root||
70
+ JSON.stringify(retained.identity)!==JSON.stringify([String(identity.dev),String(identity.ino)])||
71
+ JSON.stringify(retained.planIds)!==JSON.stringify(Object.fromEntries(
72
+ Object.entries(bundle.plans).map(([name,plan])=>[name,plan.id])))||
73
+ retained.cursor!==digest(path.join(workerData.state,'fs-events.snapshot'))||
74
+ Object.keys(retained.external||{}).length!==new Set(Object.values(bundle.plans)
75
+ .flatMap(plan=>plan.externalObservationRoots||
76
+ Object.keys(plan.externalPatterns||{}))).size||
77
+ Object.entries(retained.external||{}).some(([root,value])=>
78
+ value.cursor!==digest(path.join(workerData.state,
79
+ `external-${createHash('sha256').update(root).digest('hex').slice(0,16)}.snapshot`))))
80
+ retained=null;
81
+ } catch {retained=null;}
82
+ timing('worker.retained_state_validation',retainedStarted,{reused:retained?1:0});
83
+ if(retained){
84
+ timing('worker.total',totalStarted,{mode:'retained'});
85
+ parentPort.postMessage({ok:true,bundle,discoveryMs,indexingMs:0,
86
+ restartMode:'retained-replay',index:retained.index,
87
+ external:Object.fromEntries(Object.entries(retained.external||{})
88
+ .map(([root,value])=>[root,value.index]))});
89
+ } else {
90
+ if(workerData.reconcileFaultFile&&fs.existsSync(workerData.reconcileFaultFile))
91
+ throw Error('injected reconciliation failure');
92
+ const indexingStarted=performance.now(), index=validatedIndex||new InputIndex(bundle);
93
+ if(!validatedIndex){const report=progress=>parentPort.postMessage({kind:'progress',phase:'indexing',discoveryMs,...progress});
94
+ index.coldScan(report);}
95
+ const indexingMs=performance.now()-indexingStarted;
96
+ timing(validatedIndex?'worker.index_reused':'worker.index',indexingStarted,{files:index.files.size,
97
+ entries:index.profile.enumeratedEntries,hashed:index.rehashedFiles,
98
+ bytes:index.rehashedBytes,enumerationMs:Math.round(index.profile.enumerationMs),
99
+ matchingMs:Math.round(index.profile.matchingMs),
100
+ hashingMs:Math.round(index.profile.hashingMs)});
101
+ if(index.unavailable)throw Error(index.unavailable);
102
+ const external={};
103
+ for(const boundary of [...new Set(Object.values(bundle.plans)
104
+ .flatMap(plan=>plan.externalObservationRoots||
105
+ Object.keys(plan.externalPatterns||{})))]) {
106
+ const plans=Object.fromEntries(Object.entries(bundle.plans)
107
+ .filter(([,plan])=>(plan.externalObservationRoots||
108
+ Object.keys(plan.externalPatterns||{})).includes(boundary))
109
+ .map(([name,plan])=>[name,{...plan,patterns:plan.externalPatterns[boundary],
110
+ sourcePatterns:[],
111
+ additivePatterns:plan.externalPatterns[boundary]||[],
112
+ installedPhysicalRoots:[]}]));
113
+ const outside=new InputIndex({root:boundary,plans},{trackGit:false,includeInstalled:true});
114
+ outside.coldScan();
115
+ if(outside.unavailable)throw Error(outside.unavailable);
116
+ external[boundary]={files:[...outside.files],
117
+ aliasDependencies:[...outside.aliasDependencies].map(([name,refs])=>[name,[...refs]]),
118
+ rows:[...outside.checks]
119
+ .map(([name,row])=>({name,files:[...row.files],sum:row.sum.toString('hex'),
120
+ revision:row.revision,lastReason:row.lastReason})),
121
+ eventCount:outside.eventCount,rehashedFiles:outside.rehashedFiles,
122
+ rehashedBytes:outside.rehashedBytes,profile:outside.profile};
123
+ }
124
+ timing('worker.total',totalStarted,{mode:'cold',files:index.files.size});
125
+ parentPort.postMessage({ok:true,bundle,discoveryMs,indexingMs,guard,planReused,
126
+ inputKeys:Object.fromEntries([...index.checks].map(([name,row])=>[name,inputKey(row)])),
127
+ restartMode:'cold-index',
128
+ external,
129
+ index:{files:[...index.files],
130
+ aliasDependencies:[...index.aliasDependencies].map(([name,refs])=>[name,[...refs]]),
131
+ rows:[...index.checks].map(([name,row])=>({
132
+ name,files:[...row.files],sum:row.sum.toString('hex'),revision:row.revision,
133
+ lastReason:row.lastReason})),
134
+ eventCount:index.eventCount,rehashedFiles:index.rehashedFiles,
135
+ rehashedBytes:index.rehashedBytes,profile:index.profile}});
136
+ }
137
+ } catch(error) {
138
+ parentPort.postMessage({ok:false,error:error?.message||String(error),
139
+ classification:error?.name||'Error'});
140
+ }
package/src/plan.mjs ADDED
@@ -0,0 +1,10 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {discoverDeclared} from './declared-plan.mjs';
3
+
4
+ export const sha = value => createHash('sha256').update(value).digest('hex');
5
+
6
+ export function discover(config) {
7
+ if (config.provider !== 'declared-project@1')
8
+ throw Error(`unsupported input-plan provider: ${config.provider}`);
9
+ return discoverDeclared(config);
10
+ }
@@ -0,0 +1,17 @@
1
+ import parcelWatcher from '@parcel/watcher';
2
+ import {subscribeLinux} from './linux-observer.mjs';
3
+
4
+ export const hasHistory=process.platform==='darwin';
5
+ export const backend=process.platform==='win32'?'windows':hasHistory?'fs-events':'inotify';
6
+ export function subscribe(root,callback){
7
+ if(process.platform!=='darwin'&&process.platform!=='linux'&&process.platform!=='win32')
8
+ throw Error(`filesystem observation is unavailable on ${process.platform}`);
9
+ if(process.platform==='linux')return subscribeLinux(root,callback);
10
+ if(process.platform==='win32')return parcelWatcher.subscribe(root,callback,{backend}).then(handle=>({
11
+ unsubscribe:()=>handle.unsubscribe(),
12
+ // This is not a continuity barrier. Decision-grade reads on Windows use
13
+ // deterministic reconciliation, including after a watcher restart.
14
+ synchronize:async()=>{},watchFiles:async()=>{}
15
+ }));
16
+ return parcelWatcher.subscribe(root,callback,{backend});
17
+ }
@@ -0,0 +1,14 @@
1
+ import path from 'node:path';
2
+
3
+ export function userStateBase(platform=process.platform,env=process.env,
4
+ home=process.env.HOME||'',paths=path) {
5
+ if(platform==='win32')return paths.isAbsolute(env.LOCALAPPDATA||'')?
6
+ env.LOCALAPPDATA:paths.join(home,'AppData','Local');
7
+ if(platform==='linux')return paths.isAbsolute(env.XDG_STATE_HOME||'')?
8
+ env.XDG_STATE_HOME:paths.join(home,'.local','state');
9
+ return paths.join(home,'Library','Application Support');
10
+ }
11
+
12
+ export function stateIdentity(value,platform=process.platform) {
13
+ return platform==='win32'?path.win32.normalize(value).toLowerCase():value;
14
+ }
@@ -0,0 +1,355 @@
1
+ // Pinned pnpm 12 node-modules installation provider. The engine sees only
2
+ // repository-relative input sets and resolution relationships, never pnpm
3
+ // virtual-store names as semantic identities.
4
+ import {compilerFiles} from './typescript-list.mjs';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import {createRequire} from 'node:module';
8
+ import {execFileSync,spawnSync} from 'node:child_process';
9
+ import micromatch from './glob.mjs';
10
+ import YAML from 'yaml';
11
+ import {resolveLinks} from './installed-inputs.mjs';
12
+ import {withinPath as within,realObservedPath,samePath} from './path-identity.mjs';
13
+ import {timing} from './decision-profile.mjs';
14
+
15
+ const rel=(root,file)=>path.relative(root,file).split(path.sep).join('/');
16
+ const read=file=>JSON.parse(fs.readFileSync(file,'utf8'));
17
+ const exists=file=>{try{fs.lstatSync(file);return true;}catch{return false;}};
18
+ function yaml(file){
19
+ const doc=YAML.parseDocument(fs.readFileSync(file,'utf8'),{uniqueKeys:true});
20
+ if(doc.errors.length)throw Error(`${path.basename(file)} cannot be interpreted safely`);
21
+ return doc.toJS()||{};
22
+ }
23
+ function safePatterns(values){
24
+ if(!Array.isArray(values)||values.some(v=>typeof v!=='string'||!v||
25
+ path.isAbsolute(v)||v.split('/').includes('..')))
26
+ throw Error('pnpm workspace patterns are unsupported');
27
+ return values;
28
+ }
29
+ export function pnpmProjectInfo(root){
30
+ root=realObservedPath(root);
31
+ const pkg=read(path.join(root,'package.json'));
32
+ const pin=/^pnpm@(12\.\d+\.\d+)(?:\+[^\s]+)?$/.exec(pkg.packageManager||'');
33
+ if(!pin)throw Error('an exact pnpm 12 packageManager pin is required');
34
+ const workspaceFile=path.join(root,'pnpm-workspace.yaml');
35
+ const settings=exists(workspaceFile)?yaml(workspaceFile):{};
36
+ const patterns=safePatterns(settings.packages||[]);
37
+ const linker=settings.nodeLinker||'isolated';
38
+ return {version:pin[1],linker,patterns,settings};
39
+ }
40
+ export function pnpmInstallInfo(root){
41
+ const started=performance.now();
42
+ root=realObservedPath(root);
43
+ const pkg=read(path.join(root,'package.json'));
44
+ const {version,linker,patterns,settings}=pnpmProjectInfo(root);
45
+ const workspaceFile=path.join(root,'pnpm-workspace.yaml');
46
+ if(!['isolated','hoisted'].includes(linker))throw Error(`pnpm ${linker} linker is recording-only`);
47
+ if(settings.modulesDir||settings.virtualStoreDir||settings.enableGlobalVirtualStore)
48
+ throw Error('custom pnpm modules or virtual-store location is recording-only');
49
+ if(settings.injectWorkspacePackages)
50
+ throw Error('injected workspace dependencies are recording-only');
51
+ if(settings.sharedWorkspaceLockfile===false)
52
+ throw Error('separate workspace lockfiles are recording-only');
53
+ if(exists(path.join(root,'.pnpmfile.cjs'))||exists(path.join(root,'.pnpmfile.mjs')))
54
+ throw Error('custom pnpm installation hooks need review');
55
+ const modulesFile=path.join(root,'node_modules','.modules.yaml');
56
+ if(!exists(modulesFile))throw Error('installed pnpm module metadata is missing');
57
+ const modules=yaml(modulesFile);
58
+ if(modules.nodeLinker!==linker||modules.packageManager!==pkg.packageManager)
59
+ throw Error('installed pnpm linker or version differs from project configuration');
60
+ const virtualStore=path.join(root,'node_modules','.pnpm');
61
+ // pnpm 12 on Windows can omit virtualStoreDir when using its project-local
62
+ // default. Accept only the observed, ordinary local directory in that case.
63
+ const declared=modules.virtualStoreDir;
64
+ const selected=declared===undefined?virtualStore:
65
+ path.resolve(path.dirname(modulesFile),declared);
66
+ let localStore=false;
67
+ try{localStore=fs.lstatSync(virtualStore).isDirectory()&&
68
+ samePath(realObservedPath(virtualStore),realObservedPath(selected));}
69
+ catch{}
70
+ if(!localStore||!exists(path.join(virtualStore,'lock.yaml')))
71
+ throw Error('nonstandard or incomplete project-local pnpm virtual store');
72
+ if(!exists(path.join(root,'pnpm-lock.yaml')))
73
+ throw Error('pnpm lockfile is missing');
74
+ if(exists(path.join(root,'.npmrc'))){
75
+ // pnpm 12 reads project settings from pnpm-workspace.yaml. Registry/auth
76
+ // entries affect acquisition, not a completed local TypeScript invocation.
77
+ for(const line of fs.readFileSync(path.join(root,'.npmrc'),'utf8').split(/\r?\n/)){
78
+ const trimmed=line.trim();if(!trimmed||/^[#;]/.test(trimmed))continue;
79
+ const key=trimmed.split('=')[0].trim();
80
+ if(key!=='registry'&&!/^\/\/[^\s]+\/:_authToken$/.test(key))
81
+ throw Error('project .npmrc has execution-relevant pnpm settings requiring review');
82
+ }
83
+ }
84
+ timing('pnpm.install_metadata',started);
85
+ return {version,linker,patterns,settings,modulesFile,
86
+ configurationInputs:['package.json','pnpm-lock.yaml',
87
+ ...(exists(workspaceFile)?['pnpm-workspace.yaml']:[]),
88
+ ...(exists(path.join(root,'.npmrc'))?['.npmrc']:[]),
89
+ 'node_modules/.modules.yaml','node_modules/.pnpm/lock.yaml']};
90
+ }
91
+ export function pnpmWorkspacePackages(root,patterns){
92
+ const started=performance.now();
93
+ root=realObservedPath(root);
94
+ const found=new Map();
95
+ if(!patterns.length)return found;
96
+ const files=execFileSync('git',['ls-files','--cached','--others','--exclude-standard',
97
+ '-z','--',':(glob)**/package.json'],{cwd:root,
98
+ env:{...process.env,GIT_OPTIONAL_LOCKS:'0'},maxBuffer:32*1024*1024,
99
+ timeout:5000}).toString().split('\0').filter(Boolean);
100
+ for(const file of files){
101
+ const dir=path.posix.dirname(file);
102
+ if(dir==='.'||dir.split('/').includes('node_modules')||
103
+ !micromatch.isMatch(dir,patterns,{dot:true}))continue;
104
+ const pkg=read(path.join(root,file));
105
+ if(!pkg.name||found.has(pkg.name))throw Error('pnpm workspace name missing or duplicated');
106
+ found.set(pkg.name,{name:pkg.name,dir,manifest:file,pkg});
107
+ }
108
+ if(found.size>100)throw Error('pnpm workspace count exceeds the narrow Node onboarding contract');
109
+ timing('pnpm.workspace_discovery',started,{packages:found.size});
110
+ return found;
111
+ }
112
+ const deps=(pkg,includeDev)=>({...pkg.dependencies,
113
+ ...(includeDev?pkg.devDependencies:{}),
114
+ ...pkg.optionalDependencies,...pkg.peerDependencies});
115
+ function typeOutputs(member){
116
+ const values=new Set();
117
+ if(typeof member.pkg.types==='string')values.add(member.pkg.types);
118
+ if(typeof member.pkg.typings==='string')values.add(member.pkg.typings);
119
+ const visit=(value,key)=>{
120
+ if(typeof value==='string'&&key==='types')values.add(value);
121
+ else if(value&&typeof value==='object')for(const [k,v] of Object.entries(value))visit(v,k);
122
+ };
123
+ visit(member.pkg.exports,'');
124
+ if(!values.size)throw Error(`${member.name} has no declared TypeScript output boundary`);
125
+ return [...values].map(value=>{
126
+ if(path.isAbsolute(value)||value.split('/').includes('..'))
127
+ throw Error(`${member.name} has an unsupported declaration path`);
128
+ const folder=path.posix.dirname(value.replace(/^\.\//,''));
129
+ if(folder==='.')throw Error(`${member.name} has a root declaration requiring review`);
130
+ return `${member.dir}/${folder}/**`;
131
+ });
132
+ }
133
+ function nodeCandidates(root,requester,name){
134
+ const result=[];let current=requester;
135
+ for(;;){
136
+ if(!within(current,root))break;
137
+ if(path.basename(current)!=='node_modules')
138
+ result.push(path.join(current,'node_modules',name));
139
+ if(current===root)break;
140
+ current=path.dirname(current);
141
+ }
142
+ return result;
143
+ }
144
+ function outsideNodeCandidates(root,name){
145
+ const result=[];let current=path.dirname(root);
146
+ for(;;){
147
+ if(path.basename(current)!=='node_modules')result.push(path.join(current,'node_modules',name));
148
+ const parent=path.dirname(current);if(parent===current)break;current=parent;
149
+ }
150
+ return result;
151
+ }
152
+ function packageAt(root,requester,name){
153
+ const candidates=nodeCandidates(root,requester,name);
154
+ for(const candidate of candidates){
155
+ let stat;try{stat=fs.statSync(candidate);}catch(e){
156
+ if(e.code==='ENOENT')continue;
157
+ throw Error(`${name} resolution cannot be inspected: ${e.code||e.name}`);
158
+ }
159
+ if(!stat.isDirectory())throw Error(`${name} resolves to an unsupported entry`);
160
+ const resolution=resolveLinks(candidate,root);
161
+ if(!resolution.physical)throw Error(`${name} resolution is unavailable: ${resolution.reason}`);
162
+ const physical=resolution.physical;
163
+ if(!within(physical,root))throw Error(`${name} resolves outside the observed checkout`);
164
+ const manifest=path.join(physical,'package.json');
165
+ if(!exists(manifest))throw Error(`${name} installed manifest is missing`);
166
+ return {physical,logical:candidate,candidates,pkg:read(manifest),
167
+ links:resolution.links};
168
+ }
169
+ for(const candidate of outsideNodeCandidates(root,name)){
170
+ try{fs.lstatSync(candidate);throw Error(`${name} may resolve outside the observed checkout`);}
171
+ catch(e){if(e.code!=='ENOENT')throw e;}
172
+ }
173
+ return {physical:null,candidates};
174
+ }
175
+ function parsedTypeScript(root,dir,ts){
176
+ const config=path.join(root,dir,'tsconfig.json'),reads=new Set();
177
+ const host={...ts.sys,readFile(file){reads.add(path.resolve(file));return ts.sys.readFile(file);},
178
+ onUnRecoverableConfigFileDiagnostic(){}};
179
+ const parsed=ts.getParsedCommandLineOfConfigFile(config,{},host);
180
+ if(!parsed||parsed.errors?.length)throw Error('TypeScript configuration unavailable');
181
+ const raw=ts.readConfigFile(config,ts.sys.readFile);
182
+ if(raw.error||!Array.isArray(raw.config?.include)||raw.config.include.length!==1||
183
+ !/^src(?:$|\/)/.test(raw.config.include[0]))
184
+ throw Error('TypeScript source file membership is outside the supported src include');
185
+ if(parsed.options.plugins?.length||parsed.options.incremental||parsed.options.composite||
186
+ parsed.projectReferences?.length||parsed.options.paths||parsed.options.baseUrl||
187
+ parsed.options.rootDirs?.length||parsed.options.typeRoots?.length||
188
+ parsed.options.customConditions?.length||parsed.options.moduleSuffixes?.length)
189
+ throw Error('custom TypeScript compiler or resolution options need review');
190
+ const supported=new Set([undefined,ts.ModuleResolutionKind.Node10,
191
+ ts.ModuleResolutionKind.Node16,ts.ModuleResolutionKind.NodeNext]);
192
+ if(!supported.has(parsed.options.moduleResolution))
193
+ throw Error('TypeScript module resolver is outside the supported Node contract');
194
+ for(const file of reads)if(!within(realObservedPath(file),root))
195
+ throw Error('TypeScript configuration escapes the checkout');
196
+ return {parsed,configs:[...reads].map(file=>rel(root,file)).sort()};
197
+ }
198
+ function listedTypeScriptFiles(root,selected,tsc){
199
+ // --listFilesOnly resolves the compiler's effective module/file set but
200
+ // performs no typecheck and emits no project outputs.
201
+ const out=spawnSync(process.execPath,[tsc,'-p','tsconfig.json','--listFilesOnly'],
202
+ {cwd:path.join(root,selected.dir),env:process.env,encoding:'utf8',
203
+ // The native NTFS public workspace takes 3-5 s to list ~770 inputs;
204
+ // 5 s sometimes expired despite bounded, progressing compiler work.
205
+ timeout:10000,maxBuffer:32*1024*1024,stdio:['ignore','pipe','pipe']});
206
+ if(out.error||out.signal||out.status!==0)
207
+ throw Error('TypeScript input listing is unavailable');
208
+ const files=out.stdout.trim().split(/\r?\n/).filter(Boolean)
209
+ .map(file=>realObservedPath(file));
210
+ if(!files.length)throw Error('TypeScript input listing is empty');
211
+ return files;
212
+ }
213
+ function installedPackageRoot(root,file){
214
+ const modules=path.join(root,'node_modules');
215
+ if(!within(file,modules))return null;
216
+ const parts=rel(root,file).split('/');
217
+ const at=parts.lastIndexOf('node_modules');
218
+ if(at<0||!parts[at+1])return null;
219
+ const count=parts[at+1].startsWith('@')?2:1;
220
+ const packageRoot=path.join(root,...parts.slice(0,at+1+count));
221
+ const manifest=path.join(packageRoot,'package.json');
222
+ return exists(manifest)&&read(manifest).name?packageRoot:null;
223
+ }
224
+ function sharedHardlinkIn(folder){
225
+ const stack=[folder];
226
+ while(stack.length){
227
+ const current=stack.pop();
228
+ for(const item of fs.readdirSync(current,{withFileTypes:true})){
229
+ if(item.name==='node_modules')continue;
230
+ const file=path.join(current,item.name);
231
+ if(item.isDirectory())stack.push(file);
232
+ else if(item.isFile()&&fs.statSync(file).nlink>1)return true;
233
+ else if(item.isSymbolicLink())throw Error('installed package contains an unmodeled link');
234
+ }
235
+ }
236
+ return false;
237
+ }
238
+ export function pnpmTypecheckInputs(root,workspace,script,{captureQueries=false}={}){
239
+ const totalStarted=performance.now();
240
+ root=realObservedPath(root);
241
+ const install=pnpmInstallInfo(root),members=pnpmWorkspacePackages(root,install.patterns);
242
+ const selected=workspace==='.'?{name:'.',dir:'.',manifest:'package.json',
243
+ pkg:read(path.join(root,'package.json'))}:members.get(workspace);
244
+ if(!selected)throw Error(`workspace ${workspace} is not declared`);
245
+ if(!/^(?:tsc --noEmit|tsc -p (?:\.|tsconfig\.json)(?: --noEmit)?)$/.test(
246
+ selected.pkg.scripts?.[script]?.trim()||'')||
247
+ selected.pkg.scripts?.['pre'+script]||selected.pkg.scripts?.['post'+script])
248
+ throw Error('typecheck must be one standalone TypeScript invocation');
249
+ const selectedDir=path.join(root,selected.dir),lookup=createRequire(path.join(selectedDir,'package.json'));
250
+ let tsPackage;try{tsPackage=realObservedPath(lookup.resolve('typescript/package.json'));}
251
+ catch{throw Error('installed TypeScript compiler cannot be resolved');}
252
+ if(!within(tsPackage,root))throw Error('TypeScript compiler is outside the observed checkout');
253
+ const tsRoot=path.dirname(tsPackage),ts=lookup('typescript');
254
+ const tsc=path.join(tsRoot,'bin','tsc');
255
+ if(!exists(tsc))throw Error('installed TypeScript executable is missing');
256
+ const configStarted=performance.now();
257
+ const {configs}=parsedTypeScript(root,selected.dir,ts);
258
+ timing('pnpm.ts_config',configStarted,{configs:configs.length});
259
+ const closure=new Map(),installed=new Set(),generated=new Set(),
260
+ links=new Set(),candidates=new Set(),absences=[],instances=new Map();
261
+ let resolutionMs=0,resolutionCount=0;
262
+ const closureStarted=performance.now();
263
+ const visit=(requester,owner,includeDev=false)=>{
264
+ for(const [name,spec] of Object.entries(deps(owner.pkg,includeDev))){
265
+ if(owner.pkg.dependenciesMeta?.[name]?.injected)
266
+ throw Error(`injected workspace dependency ${name} is recording-only`);
267
+ const resolutionStarted=performance.now();
268
+ const found=packageAt(root,path.join(root,requester.dir),name);
269
+ resolutionMs+=performance.now()-resolutionStarted;resolutionCount++;
270
+ const optional=Object.hasOwn(owner.pkg.optionalDependencies||{},name)||
271
+ owner.pkg.peerDependenciesMeta?.[name]?.optional===true;
272
+ if(!found.physical){
273
+ if(!optional)throw Error(`${owner.name} requires unresolved installed package ${name}`);
274
+ absences.push({requester:owner.name,name});
275
+ for(const candidate of found.candidates)if(within(candidate,root))
276
+ candidates.add(rel(root,candidate));
277
+ // An outside ancestor can shadow a supported absence without a root event.
278
+ // For this alpha, a missing optional is qualified only when the caller's
279
+ // Node lookup boundary is explicitly observed by the project checkout.
280
+ continue;
281
+ }
282
+ for(const candidate of found.candidates){
283
+ if(candidate===found.logical)break;
284
+ if(within(candidate,root))candidates.add(rel(root,candidate));
285
+ }
286
+ for(const link of found.links||[])if(within(link.path,root))
287
+ links.add(rel(root,link.path));
288
+ const member=[...members.values()].find(row=>
289
+ path.join(root,row.dir)===found.physical);
290
+ if(String(spec).startsWith('workspace:')&&member?.name!==name)
291
+ throw Error(`${owner.name} workspace dependency ${name} is not linked to its workspace`);
292
+ if(member){
293
+ if(owner.name===member.name||member.name===selected.name)continue;
294
+ if(!closure.has(member.name)){
295
+ closure.set(member.name,member);
296
+ for(const value of typeOutputs(member))generated.add(value);
297
+ visit(member,member,true);
298
+ }
299
+ }
300
+ }
301
+ };
302
+ visit(selected,selected,true);
303
+ timing('pnpm.dependency_closure',closureStarted,{relationships:resolutionCount,
304
+ resolutionMs:Math.round(resolutionMs),workspaceLinks:links.size,
305
+ candidates:candidates.size});
306
+ const listingStarted=performance.now();
307
+ const captured=captureQueries?compilerFiles(tsRoot,path.join(root,selected.dir)):null;
308
+ const files=captured?captured.files.map(realObservedPath):listedTypeScriptFiles(root,selected,tsc);
309
+ timing('pnpm.ts_input_listing',listingStarted,{files:files.length});
310
+ const mappingStarted=performance.now();
311
+ for(const file of files){
312
+ if(within(file,path.join(selectedDir,'src'))||
313
+ [...closure.values()].some(member=>within(file,path.join(root,member.dir))))continue;
314
+ const packageRoot=installedPackageRoot(root,file);
315
+ if(!packageRoot)throw Error(`TypeScript consumes a file outside the observed pnpm contract: ${rel(root,file)}`);
316
+ if(!instances.has(packageRoot))instances.set(packageRoot,{name:read(path.join(packageRoot,'package.json')).name,
317
+ physicalRoot:rel(root,packageRoot)});
318
+ installed.add(`${rel(root,packageRoot)}/**`);
319
+ installed.add(`!${rel(root,packageRoot)}/node_modules/**`);
320
+ }
321
+ timing('pnpm.input_mapping',mappingStarted,{instances:instances.size,
322
+ installedPatterns:installed.size});
323
+ installed.add(`${rel(root,tsRoot)}/**`);
324
+ installed.add(`!${rel(root,tsRoot)}/node_modules/**`);
325
+ if(!instances.has(tsRoot))instances.set(tsRoot,{name:'typescript',physicalRoot:rel(root,tsRoot)});
326
+ const sharedStarted=performance.now();
327
+ const shared=[...instances.keys()].filter(sharedHardlinkIn);
328
+ timing('pnpm.hardlink_inspection',sharedStarted,{instances:instances.size,
329
+ shared:shared.length});
330
+ const source=[...new Set([...install.configurationInputs,selected.manifest,
331
+ ...configs,`${selected.dir==='.'?'':selected.dir+'/'}src/**`,
332
+ ...closure.values().map(member=>member.manifest),...generated])];
333
+ timing('pnpm.total_discovery',totalStarted,{instances:instances.size,
334
+ inputFiles:files.length});
335
+ return {install,workspace:selected,closure:[...closure.values()],source,
336
+ installed:[...installed],generated:[...generated],
337
+ // The read-only state probe consumes this same resolved compiler file set.
338
+ // It is deliberately omitted from the persisted user-facing input plan.
339
+ listedFiles:files,discoveryQueries:captured?.queries,
340
+ configs,configurationInputs:[...new Set([...install.configurationInputs,
341
+ ...configs,...members.values().map(member=>member.manifest)])],
342
+ installation:{provider:'pnpm',version:install.version,linker:install.linker},
343
+ installedInstances:[...instances.values()],
344
+ linkTriggers:[...links],absences,
345
+ limitations:[...(absences.length?[
346
+ 'optional dependency absence has ancestor Node lookup locations outside the observed checkout']:[]),
347
+ ...(shared.length&&!['linux','win32'].includes(process.platform)?
348
+ ['installed package hardlinks have unobserved storage aliases']:[])],
349
+ // Installed hardlinks can change through a store alias without a project
350
+ // directory notification. A decision-grade read must rehash them.
351
+ synchronizedInstalledRead:['linux','win32'].includes(process.platform),
352
+ resolutionCandidates:[...candidates].map(value=>
353
+ ({path:value,observedPath:value})),workspacePatterns:install.patterns,
354
+ tsc,tsRoot};
355
+ }
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // Read-only pnpm/TypeScript applicability probe. Only a digest or fixed
3
+ // diagnostic leaves the process; no source, configuration, or environment
4
+ // value is emitted.
5
+ import {contextOnly,probeResult} from './probe-result.mjs';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import {createHash} from 'node:crypto';
9
+ import {pnpmTypecheckInputs} from './pnpm-inputs.mjs';
10
+ import {withinPath as within,realObservedPath} from './path-identity.mjs';
11
+
12
+ const [rootArg,workspace,script]=process.argv.slice(2);
13
+ const hash=value=>createHash('sha256').update(value).digest('hex');
14
+ const digest=file=>hash(fs.readFileSync(file));
15
+ const fail=code=>{throw Object.assign(Error(code),{code});};
16
+ try{
17
+ if(!rootArg||!workspace||!script)fail('probe-arguments');
18
+ const root=realObservedPath(rootArg);
19
+ if(process.env.NODE_OPTIONS||process.env.NODE_PATH||process.env.BASH_ENV||
20
+ process.env.ENV||Object.entries(process.env).some(([key,value])=>{
21
+ if(/^npm_config_prefix$/i.test(key)&&process.platform==='win32'){
22
+ return !path.isAbsolute(value);
23
+ }
24
+ return /^(PNPM_|COREPACK_|npm_config_|DYLD_|TSGO_)/i.test(key);
25
+ }))
26
+ fail('pnpm-execution-environment-unsupported');
27
+ const contextHash=hash(JSON.stringify([root,workspace,script,process.version]));
28
+ if(contextOnly(contextHash))process.exit(0);
29
+ const contract=pnpmTypecheckInputs(root,workspace,script,{captureQueries:process.argv.includes('--redue-checkpoint')});
30
+ if(contract.limitations.length)fail('pnpm-input-coverage-unavailable');
31
+ const selected=path.join(root,contract.workspace.dir),tsc=contract.tsc;
32
+ const inputs=contract.listedFiles;
33
+ if(!inputs.length)fail('typescript-input-list-unavailable');
34
+ const inputRoots=[path.join(selected,'src'),...contract.generated.map(pattern=>
35
+ path.join(root,pattern.slice(0,-3))),...contract.installed.filter(p=>!p.startsWith('!'))
36
+ .map(pattern=>path.join(root,pattern.slice(0,-3)))];
37
+ for(const file of inputs)if(!within(file,root)||
38
+ !inputRoots.some(boundary=>within(file,boundary)))
39
+ fail('typescript-input-outside-pnpm-contract');
40
+ const files=[...new Set(inputs)].sort().map(file=>
41
+ [hash(path.relative(root,file)),digest(file)]);
42
+ const configs=contract.configs.map(file=>[hash(file),digest(path.join(root,file))]);
43
+ probeResult(hash(JSON.stringify({schema:1,workspace,script,
44
+ files,configs,node:process.version,tsc:digest(tsc),
45
+ packageManager:contract.install.version,linker:contract.install.linker})),contextHash,contract.discoveryQueries);
46
+ }catch(e){console.error(`VSTATE_REASON:${e.code&&/^[a-z-]+$/.test(e.code)?e.code:
47
+ 'pnpm-typecheck-contract-unavailable'}`);process.exitCode=2;}
@@ -0,0 +1,53 @@
1
+ // Presentation consumes evidence; it never grants applicability.
2
+ export function checkLabel(row) {
3
+ return row.result==='FAIL'?'FAILED':row.freshness;
4
+ }
5
+ export function explanation(row) {
6
+ const reason=row.reason||'No verification evidence is available.';
7
+ if(reason==='no receipt')return 'No execution has been recorded. Run this check when verification is needed.';
8
+ if(reason==='declared inputs match')return 'Recorded evidence still applies to the declared inputs.';
9
+ if(reason==='execution environment context not supplied')return 'Cached status cannot confirm your execution context. Use --sync if worthwhile, or rerun the check.';
10
+ if(/requires? a synchronized content read/.test(reason))return 'Cached status cannot establish applicability here. Use --sync if worthwhile, or rerun the check.';
11
+ if(reason.startsWith('input coverage unresolved: '))return 'Recording-only: '+reason.slice(27);
12
+ if(/input plan (rebuilding|indexing)|observer initialization|input observation reconciliation pending/.test(reason))
13
+ return 'Checking inputs after startup or changes. Wait for observation to finish, then query again.';
14
+ if(/observer (heartbeat|process|stopping)/.test(reason))return 'Observer unavailable. Run redue start; previous outcomes remain recorded.';
15
+ return reason;
16
+ }
17
+ export function renderStatus(value,{short=false}={}) {
18
+ if(short)return `REDUE ${String(value.state||'unverified').toUpperCase()}`;
19
+ const lines=[];
20
+ for(const row of value.checks||[]) {
21
+ const label=checkLabel(row),symbol=label==='CURRENT'&&row.result==='PASS'?'✓':label==='FAILED'?'✗':'?';
22
+ const mark=label==='STALE'?'⚠':symbol;
23
+ const historical=row.result?` / ${row.result}`:'';
24
+ lines.push(`${mark} ${row.name} ${label}${historical}`);
25
+ if(label!=='CURRENT')lines.push(` ${row.result==='FAIL'?`Historical failure; applicability ${row.freshness}. `:''}${explanation(row)}`);
26
+ }
27
+ if(!lines.length)lines.push('UNVERIFIED — observation is initializing; no check state is available yet.');
28
+ if(!value.observation?.healthy)lines.push('Observation unavailable or pending. Previous results are not reusable.');
29
+ return lines.join('\n');
30
+ }
31
+ export function renderExplain(value,name) {
32
+ const rows=name?value.checks.filter(row=>row.name===name):value.checks;
33
+ return rows.map(row=>{
34
+ const lines=[`${row.name}: ${checkLabel(row)}${row.result?` / ${row.result}`:''}`];
35
+ if(row.result==='FAIL')lines.push(`Historical failure; applicability: ${row.freshness}.`);
36
+ if(row.verification_recipe==='npm-direct-typescript@1')lines.push('Recorded recipe: direct local TypeScript compiler; npm and lifecycle scripts were not executed.');
37
+ lines.push(explanation(row));
38
+ if(row.changed_inputs?.length)lines.push('Relevant inputs changed:',...row.changed_inputs.map(file=>` ${file}`));
39
+ if(row.freshness==='STALE')lines.push(`Due again when verification is needed: redue run ${JSON.stringify(row.name)}`);
40
+ if(row.invocation)lines.push(`Last invocation: ${row.invocation.status}`+
41
+ (Number.isFinite(row.invocation.durationMs)?` (${(row.invocation.durationMs/1000).toFixed(2)}s)`:'')+'.');
42
+ return lines.join('\n');
43
+ }).join('\n\n');
44
+ }
45
+ export function renderRun(value) {
46
+ const label=value.result==='FAIL'?'FAILED':value.result||String(value.invocation).toUpperCase();
47
+ const lines=[`${value.check}: ${label} recorded.`];
48
+ if(value.invocation!=='exited')lines.push(`Invocation ${value.invocation}; no reusable passing evidence.`);
49
+ else if(!value.coverage_qualified)lines.push('Applicability remains UNVERIFIED: input coverage is incomplete.');
50
+ else if(!value.stable)lines.push('Applicability remains UNVERIFIED: inputs changed or observation was unavailable during execution.');
51
+ else lines.push('Inspect redue status before reusing this evidence. Cached status may require --sync to establish applicability.');
52
+ return lines.join('\n');
53
+ }