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

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 (52) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +180 -0
  3. package/README.md +123 -2
  4. package/bin/redue.mjs +329 -0
  5. package/bin/vstate.mjs +3 -0
  6. package/docs/README.md +20 -0
  7. package/docs/user/agents.md +32 -0
  8. package/docs/user/configuration.md +57 -0
  9. package/docs/user/getting-started.md +127 -0
  10. package/docs/user/json.md +59 -0
  11. package/docs/user/shell.md +25 -0
  12. package/docs/user/supported-projects.md +89 -0
  13. package/examples/redue.config.json +8 -0
  14. package/package.json +46 -4
  15. package/src/cached-state.mjs +27 -0
  16. package/src/cli.mjs +390 -0
  17. package/src/control-endpoint.mjs +11 -0
  18. package/src/daemon.mjs +989 -0
  19. package/src/decision-profile.mjs +18 -0
  20. package/src/decision-validation.mjs +41 -0
  21. package/src/declared-plan.mjs +125 -0
  22. package/src/executable-lookup.mjs +18 -0
  23. package/src/glob.mjs +9 -0
  24. package/src/history-worker.mjs +18 -0
  25. package/src/index.mjs +306 -0
  26. package/src/installed-inputs.mjs +110 -0
  27. package/src/linux-inotify.py +228 -0
  28. package/src/linux-observer.mjs +79 -0
  29. package/src/node-onboarding.mjs +263 -0
  30. package/src/npm-typecheck-probe.mjs +120 -0
  31. package/src/observation.mjs +37 -0
  32. package/src/owned-state.mjs +32 -0
  33. package/src/path-identity.mjs +43 -0
  34. package/src/plan-worker.mjs +131 -0
  35. package/src/plan.mjs +10 -0
  36. package/src/platform-observation.mjs +17 -0
  37. package/src/platform-state.mjs +14 -0
  38. package/src/pnpm-inputs.mjs +355 -0
  39. package/src/pnpm-typecheck-probe.mjs +47 -0
  40. package/src/presentation.mjs +52 -0
  41. package/src/probe-result.mjs +21 -0
  42. package/src/process-liveness.mjs +11 -0
  43. package/src/run-lock.mjs +77 -0
  44. package/src/state-store.mjs +37 -0
  45. package/src/toolchain-probe.mjs +21 -0
  46. package/src/typescript-contract-probe.mjs +188 -0
  47. package/src/typescript-list.mjs +68 -0
  48. package/src/typescript-qualification.mjs +96 -0
  49. package/src/windows-command.mjs +16 -0
  50. package/src/windows-process.mjs +15 -0
  51. package/src/yarn-workspace-inputs.mjs +148 -0
  52. package/src/yarn-workspace-typecheck-probe.mjs +107 -0
@@ -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,52 @@
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
+ lines.push(explanation(row));
37
+ if(row.changed_inputs?.length)lines.push('Relevant inputs changed:',...row.changed_inputs.map(file=>` ${file}`));
38
+ if(row.freshness==='STALE')lines.push(`Due again when verification is needed: redue run ${JSON.stringify(row.name)}`);
39
+ if(row.invocation)lines.push(`Last invocation: ${row.invocation.status}`+
40
+ (Number.isFinite(row.invocation.durationMs)?` (${(row.invocation.durationMs/1000).toFixed(2)}s)`:'')+'.');
41
+ return lines.join('\n');
42
+ }).join('\n\n');
43
+ }
44
+ export function renderRun(value) {
45
+ const label=value.result==='FAIL'?'FAILED':value.result||String(value.invocation).toUpperCase();
46
+ const lines=[`${value.check}: ${label} recorded.`];
47
+ if(value.invocation!=='exited')lines.push(`Invocation ${value.invocation}; no reusable passing evidence.`);
48
+ else if(!value.coverage_qualified)lines.push('Applicability remains UNVERIFIED: input coverage is incomplete.');
49
+ else if(!value.stable)lines.push('Applicability remains UNVERIFIED: inputs changed or observation was unavailable during execution.');
50
+ else lines.push('Inspect redue status before reusing this evidence. Cached status may require --sync to establish applicability.');
51
+ return lines.join('\n');
52
+ }
@@ -0,0 +1,21 @@
1
+ // Internal supported-provider protocol. Default stdout is unchanged, so
2
+ // ordinary run checkpoints keep the same probe/result identity. The daemon
3
+ // may reuse a complete result only after revalidating its input key AND this
4
+ // independently checked execution/toolchain context. No TTL grants trust.
5
+ import {deflateSync} from 'node:zlib';
6
+ export function contextOnly(context) {
7
+ if(process.argv.includes('--redue-context')){
8
+ process.stdout.write(context);return true;
9
+ }
10
+ return false;
11
+ }
12
+ export function probeResult(output,context,queries=null) {
13
+ if(!process.argv.includes('--redue-checkpoint')){process.stdout.write(output);return;}
14
+ const raw=queries?JSON.stringify(queries):null;
15
+ let value=JSON.stringify({output,context,queryData:raw&&Buffer.byteLength(raw)<=8*1024*1024?
16
+ deflateSync(raw).toString('base64'):null});
17
+ // Optimization metadata must not overflow the ordinary probe channel.
18
+ // Larger contracts keep the normal complete probe, without result reuse.
19
+ if(Buffer.byteLength(value)>512*1024)value=JSON.stringify({output,context,queries:null});
20
+ process.stdout.write(value);
21
+ }
@@ -0,0 +1,11 @@
1
+ import fs from 'node:fs';
2
+
3
+ export function processAlive(pid) {
4
+ if(!Number.isSafeInteger(pid)||pid<1)return true;
5
+ if(process.platform==='linux')try{
6
+ const stat=fs.readFileSync(`/proc/${pid}/stat`,'utf8');
7
+ if(['Z','X'].includes(stat.slice(stat.lastIndexOf(')')+2)[0]))return false;
8
+ }catch(e){if(e.code!=='ENOENT'&&e.code!=='ESRCH')return true;}
9
+ try{process.kill(pid,0);return true;}
10
+ catch(e){return e.code!=='ESRCH';}
11
+ }
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {randomUUID} from 'node:crypto';
4
+ import {processAlive} from './process-liveness.mjs';
5
+
6
+ const alive=processAlive;
7
+ const write=(file,value)=>{
8
+ const temp=file+`.${process.pid}.tmp`;
9
+ fs.writeFileSync(temp,JSON.stringify(value),{mode:0o600});
10
+ fs.renameSync(temp,file);
11
+ };
12
+
13
+ export function inspectRunLock(state) {
14
+ const lock=path.join(state,'run.lock');
15
+ if(!fs.existsSync(lock))return {state:'absent'};
16
+ let owner;
17
+ try{owner=JSON.parse(fs.readFileSync(path.join(lock,'owner.json'),'utf8'));}
18
+ catch{return {state:'uncertain',reason:'lock owner record missing or unreadable'};}
19
+ if(owner.schema!==1||!owner.token||!Number.isSafeInteger(owner.pid))
20
+ return {state:'uncertain',reason:'lock owner record invalid'};
21
+ if(alive(owner.pid))return {state:'live',reason:'wrapper process exists',owner};
22
+ if(owner.phase==='running'&&Number.isSafeInteger(owner.childPid)&&alive(owner.childPid))
23
+ return {state:'uncertain',reason:'recorded check process is still running',owner};
24
+ if(['launching','running','finished'].includes(owner.phase))
25
+ return {state:'uncertain',reason:'a descendant check process may survive; operator review required',owner};
26
+ if(owner.phase!=='prepared')
27
+ return {state:'uncertain',reason:'unknown wrapper phase',owner};
28
+ return {state:'abandoned',reason:'wrapper died before child launch',owner};
29
+ }
30
+
31
+ function reclaim(state,confirmed=false){
32
+ const lock=path.join(state,'run.lock'),guard=path.join(state,'run-recovery.lock');
33
+ fs.mkdirSync(guard);
34
+ try{
35
+ const status=inspectRunLock(state);
36
+ if(status.state!=='abandoned'){
37
+ const owner=status.owner;
38
+ const candidate=confirmed&&status.state==='uncertain'&&owner&&
39
+ !alive(owner.pid)&&(!Number.isSafeInteger(owner.childPid)||!alive(owner.childPid));
40
+ if(!candidate)throw Error(`run lock ${status.state}: ${status.reason}; inspect ${lock}`);
41
+ }
42
+ const entries=fs.readdirSync(lock);
43
+ if(entries.length!==1||entries[0]!=='owner.json')
44
+ throw Error('run lock has unexpected files; operator inspection required');
45
+ const quarantined=path.join(state,`abandoned-run-${randomUUID()}`);
46
+ fs.renameSync(lock,quarantined);
47
+ fs.unlinkSync(path.join(quarantined,'owner.json'));fs.rmdirSync(quarantined);
48
+ }finally{fs.rmdirSync(guard);}
49
+ }
50
+
51
+ export function recoverRunLock(state,confirmed=false){
52
+ if(!confirmed)throw Error('explicit --confirm-check-stopped required');
53
+ reclaim(state,true);
54
+ return {recovered:true};
55
+ }
56
+
57
+ export function acquireRunLock(state) {
58
+ const lock=path.join(state,'run.lock');
59
+ const token=randomUUID();
60
+ try{fs.mkdirSync(lock);}
61
+ catch(e){
62
+ if(e.code!=='EEXIST')throw e;
63
+ reclaim(state);
64
+ fs.mkdirSync(lock);
65
+ }
66
+ const owner={schema:1,token,pid:process.pid,phase:'prepared',childPid:null,
67
+ startedAt:Date.now()};
68
+ const file=path.join(lock,'owner.json');write(file,owner);
69
+ return {
70
+ phase(value,childPid=null){owner.phase=value;owner.childPid=childPid;write(file,owner);},
71
+ release(){
72
+ const current=JSON.parse(fs.readFileSync(file,'utf8'));
73
+ if(current.token!==token)throw Error('run lock ownership changed; refusing removal');
74
+ fs.unlinkSync(file);fs.rmdirSync(lock);
75
+ }
76
+ };
77
+ }
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export const STATE_SCHEMA=1;
5
+
6
+ export function atomicJson(file,value){
7
+ const temp=file+`.`+process.pid+`.tmp`;
8
+ fs.writeFileSync(temp,JSON.stringify(value),{mode:0o600});
9
+ // Windows readers and AV can briefly hold the replaced name open. Keep the
10
+ // old complete file until replacement succeeds; never delete it first.
11
+ for(let attempt=0;;attempt++)try{fs.renameSync(temp,file);break;}catch(e){
12
+ if(process.platform!=='win32'||!['EPERM','EACCES','EBUSY'].includes(e.code)||attempt===9)
13
+ throw e;
14
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,10*(attempt+1));
15
+ }
16
+ }
17
+
18
+ export function readReceipts(state){
19
+ let value;
20
+ try{value=JSON.parse(fs.readFileSync(path.join(state,'receipts-v1.json'),'utf8'));}
21
+ catch(e){if(e.code==='ENOENT')return {schema:STATE_SCHEMA,checks:{}};throw e;}
22
+ if(value.schema!==STATE_SCHEMA||!value.checks||typeof value.checks!=='object')
23
+ throw Error(`incompatible receipt state in ${state}; inspect or remove only this vstate state directory`);
24
+ return value;
25
+ }
26
+
27
+ export function commitReceipt(state,name,receipt){
28
+ const prior=readReceipts(state),runId=receipt.invocation?.runId;
29
+ if(!/^[0-9a-f-]{36}$/.test(runId||''))throw Error('receipt lacks a valid run ID');
30
+ const runs=path.join(state,'runs-v1');
31
+ fs.mkdirSync(runs,{recursive:true,mode:0o700});
32
+ // One immutable outcome per invocation. Never overwrite an existing run.
33
+ fs.writeFileSync(path.join(runs,runId+'.json'),
34
+ JSON.stringify({schema:STATE_SCHEMA,check:name,receipt}),{flag:'wx',mode:0o600});
35
+ prior.checks[name]=receipt;
36
+ atomicJson(path.join(state,'receipts-v1.json'),prior);
37
+ }
@@ -0,0 +1,21 @@
1
+ // Read-only execution identity for explicitly named local tools. Output contains
2
+ // hashes and metadata only; the verifier hashes the output again for its receipt.
3
+ import fs from 'node:fs';
4
+ import {createHash} from 'node:crypto';
5
+ import {fileURLToPath} from 'node:url';
6
+
7
+ const sha=value=>createHash('sha256').update(value).digest('hex');
8
+ function identity(file) {
9
+ const real=fs.realpathSync(file),stat=fs.statSync(real,{bigint:true});
10
+ if(!stat.isFile())throw Error('toolchain identity requires a regular file');
11
+ const facts=[sha(real),...['dev','ino','mode','size','mtimeNs','ctimeNs']
12
+ .map(key=>String(stat[key]))];
13
+ // Hash small scripts/bundles on each probe. For large native executables,
14
+ // version plus identity/metadata keeps synchronized reads inexpensive.
15
+ if(stat.size<=16n*1024n*1024n)facts.push(sha(fs.readFileSync(real)));
16
+ return facts;
17
+ }
18
+ const own=fileURLToPath(import.meta.url);
19
+ process.stdout.write(JSON.stringify({schema:1,nodeVersion:process.version,
20
+ node:identity(process.execPath),probe:identity(own),
21
+ tools:process.argv.slice(2).map(identity)}));