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

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 (53) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +180 -0
  3. package/README.md +126 -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/receipt-selection-notice.md +60 -0
  12. package/docs/user/shell.md +25 -0
  13. package/docs/user/supported-projects.md +89 -0
  14. package/examples/redue.config.json +8 -0
  15. package/package.json +46 -4
  16. package/src/cached-state.mjs +42 -0
  17. package/src/cli.mjs +401 -0
  18. package/src/control-endpoint.mjs +11 -0
  19. package/src/daemon.mjs +1004 -0
  20. package/src/decision-profile.mjs +18 -0
  21. package/src/decision-validation.mjs +41 -0
  22. package/src/declared-plan.mjs +125 -0
  23. package/src/executable-lookup.mjs +18 -0
  24. package/src/glob.mjs +9 -0
  25. package/src/history-worker.mjs +18 -0
  26. package/src/index.mjs +306 -0
  27. package/src/installed-inputs.mjs +110 -0
  28. package/src/linux-inotify.py +228 -0
  29. package/src/linux-observer.mjs +79 -0
  30. package/src/node-onboarding.mjs +263 -0
  31. package/src/npm-typecheck-probe.mjs +120 -0
  32. package/src/observation.mjs +37 -0
  33. package/src/owned-state.mjs +32 -0
  34. package/src/path-identity.mjs +43 -0
  35. package/src/plan-worker.mjs +131 -0
  36. package/src/plan.mjs +10 -0
  37. package/src/platform-observation.mjs +17 -0
  38. package/src/platform-state.mjs +14 -0
  39. package/src/pnpm-inputs.mjs +355 -0
  40. package/src/pnpm-typecheck-probe.mjs +47 -0
  41. package/src/presentation.mjs +52 -0
  42. package/src/probe-result.mjs +21 -0
  43. package/src/process-liveness.mjs +11 -0
  44. package/src/run-lock.mjs +77 -0
  45. package/src/state-store.mjs +73 -0
  46. package/src/toolchain-probe.mjs +21 -0
  47. package/src/typescript-contract-probe.mjs +188 -0
  48. package/src/typescript-list.mjs +68 -0
  49. package/src/typescript-qualification.mjs +96 -0
  50. package/src/windows-command.mjs +16 -0
  51. package/src/windows-process.mjs +15 -0
  52. package/src/yarn-workspace-inputs.mjs +148 -0
  53. package/src/yarn-workspace-typecheck-probe.mjs +107 -0
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ // Read-only reviewed boundary for an npm script invoking local TypeScript.
3
+ // A nonzero exit withdraws applicability. Output is a digest, never config values.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import {createHash} from 'node:crypto';
7
+ import {spawnSync} from 'node:child_process';
8
+ import {fileURLToPath} from 'node:url';
9
+
10
+ const [rootArg,nodeArg,npmArg,reviewedTsconfigHash,npmVersion,typescriptVersion]=process.argv.slice(2);
11
+ const sha=value=>createHash('sha256').update(value).digest('hex');
12
+ const inside=(file,dir)=>file===dir||file.startsWith(dir+path.sep);
13
+ const fail=reason=>{throw Error(`typecheck boundary unavailable: ${reason}`);};
14
+ const real=file=>fs.realpathSync(file);
15
+ function fileHash(file){
16
+ const digest=createHash('sha256'),fd=fs.openSync(file,'r'),buffer=Buffer.alloc(1024*1024);
17
+ try{for(;;){const count=fs.readSync(fd,buffer,0,buffer.length,null);
18
+ if(!count)break;digest.update(buffer.subarray(0,count));}}
19
+ finally{fs.closeSync(fd);}
20
+ return digest.digest('hex');
21
+ }
22
+ function treeHash(dir){
23
+ const digest=createHash('sha256');
24
+ function visit(folder,relative=''){
25
+ for(const entry of fs.readdirSync(folder,{withFileTypes:true}).sort((a,b)=>
26
+ a.name.localeCompare(b.name))){
27
+ const rel=relative?relative+'/'+entry.name:entry.name,full=path.join(folder,entry.name);
28
+ if(entry.isDirectory()){digest.update(`d\0${rel}\0`);visit(full,rel);}
29
+ else if(entry.isFile())digest.update(`f\0${rel}\0${fileHash(full)}\0`);
30
+ else fail('npm installation contains an unsupported entry');
31
+ }
32
+ }
33
+ visit(dir);return digest.digest('hex');
34
+ }
35
+ function namesAt(dir){
36
+ if(!fs.existsSync(dir))return null;
37
+ if(!fs.statSync(dir).isDirectory())fail('ancestor resolution boundary is not a directory');
38
+ const names=[];
39
+ for(const entry of fs.readdirSync(dir,{withFileTypes:true}).sort((a,b)=>
40
+ a.name.localeCompare(b.name))){
41
+ names.push(entry.name);
42
+ if(entry.name.startsWith('@')&&entry.isDirectory())
43
+ names.push(...fs.readdirSync(path.join(dir,entry.name)).map(name=>entry.name+'/'+name).sort());
44
+ }
45
+ return names;
46
+ }
47
+ function rcKeys(file){
48
+ if(!fs.existsSync(file))return null;
49
+ const keys=[];
50
+ for(const line of fs.readFileSync(file,'utf8').split(/\r?\n/)){
51
+ const trimmed=line.trim();
52
+ if(!trimmed||trimmed.startsWith('#')||trimmed.startsWith(';'))continue;
53
+ const at=trimmed.indexOf('=');
54
+ if(at<0)fail('npm configuration has an unsupported entry');
55
+ const key=trimmed.slice(0,at).trim();
56
+ // These affect package acquisition, not this offline `npm run typecheck`.
57
+ if(key!=='registry'&&!/^\/\/[^\s]+\/:_authToken$/.test(key))
58
+ fail('npm configuration contains an unreviewed key');
59
+ keys.push(key);
60
+ }
61
+ return keys.sort();
62
+ }
63
+ function pathExecutable(name){
64
+ for(const folder of (process.env.PATH||'').split(path.delimiter)){
65
+ if(!folder)continue;
66
+ const candidate=path.join(folder,name);
67
+ try{fs.accessSync(candidate,fs.constants.X_OK);return real(candidate);}catch{}
68
+ }
69
+ return null;
70
+ }
71
+ try{
72
+ if(!rootArg||!nodeArg||!npmArg||!reviewedTsconfigHash||!npmVersion||!typescriptVersion)
73
+ fail('arguments missing');
74
+ const root=real(rootArg),node=real(nodeArg),npm=real(npmArg),home=process.env.HOME;
75
+ if(!home||!path.isAbsolute(home))fail('HOME is unavailable');
76
+ const homeTarget=real(home);
77
+ if(real(process.execPath)!==node||pathExecutable('node')!==node)
78
+ fail('npm shebang would use a different Node executable');
79
+ if(process.env.NODE_OPTIONS||process.env.NODE_PATH||process.env.BASH_ENV||process.env.ENV)
80
+ fail('unreviewed Node or shell preload environment');
81
+ if(Object.keys(process.env).some(key=>key.toLowerCase().startsWith('npm_config_')))
82
+ fail('npm configuration environment override');
83
+ if(Object.keys(process.env).some(key=>key.startsWith('DYLD_')||key.startsWith('TSGO_')))
84
+ fail('unreviewed native runtime environment');
85
+ const npmRoot=path.dirname(path.dirname(npm)),prefix=path.dirname(path.dirname(path.dirname(npmRoot)));
86
+ if(path.basename(npmRoot)!=='npm'||path.basename(path.dirname(npmRoot))!=='node_modules')
87
+ fail('npm installation layout changed');
88
+ if(JSON.parse(fs.readFileSync(path.join(npmRoot,'package.json'),'utf8')).version!==npmVersion||
89
+ JSON.parse(fs.readFileSync(path.join(root,'node_modules/typescript/package.json'),'utf8'))
90
+ .version!==typescriptVersion)fail('reviewed npm or TypeScript version changed');
91
+ const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));
92
+ if(pkg.scripts?.typecheck!=='tsc --noEmit'||pkg.scripts?.pretypecheck||pkg.scripts?.posttypecheck)
93
+ fail('typecheck script or lifecycle changed');
94
+ const tsconfig=path.join(root,'tsconfig.json');
95
+ if(fileHash(tsconfig)!==reviewedTsconfigHash)fail('TypeScript configuration needs review');
96
+ const tsc=real(path.join(root,'node_modules','.bin','tsc'));
97
+ if(tsc!==path.join(root,'node_modules','typescript','bin','tsc'))
98
+ fail('typecheck executable does not resolve to local TypeScript');
99
+ const listed=spawnSync(tsc,['--noEmit','--listFilesOnly'],{cwd:root,env:process.env,
100
+ encoding:'utf8',timeout:10000,maxBuffer:4*1024*1024,stdio:['ignore','pipe','pipe']});
101
+ if(listed.error||listed.signal||listed.status!==0)fail('TypeScript input listing unavailable');
102
+ const files=listed.stdout.trim().split(/\r?\n/).filter(Boolean).map(file=>path.resolve(file));
103
+ if(!files.length||files.some(file=>!inside(file,root)||
104
+ !(inside(file,path.join(root,'src'))||file===path.join(root,'vite.config.ts')||
105
+ inside(file,path.join(root,'node_modules')))))
106
+ fail('TypeScript consumes an input outside the observed file sets');
107
+ const ancestors=[];
108
+ for(let dir=path.dirname(root);;dir=path.dirname(dir)){
109
+ ancestors.push([dir,namesAt(path.join(dir,'node_modules'))]);
110
+ if(dir===path.dirname(dir))break;
111
+ }
112
+ const configFiles=[path.join(root,'.npmrc'),path.join(home,'.npmrc'),
113
+ path.join(prefix,'etc','npmrc')];
114
+ const npmConfig=configFiles.map(file=>[file,rcKeys(file)]);
115
+ // Metadata alone is insufficient for the installed npm CLI and native tools.
116
+ const fingerprint=sha(JSON.stringify({schema:1,probe:fileHash(fileURLToPath(import.meta.url)),
117
+ homeTarget,files:files.sort(),ancestors,
118
+ npmConfig,npmTree:treeHash(npmRoot),node:fileHash(node),shell:fileHash('/bin/sh')}));
119
+ process.stdout.write(fingerprint);
120
+ }catch(e){console.error(e.message);process.exitCode=2;}
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {randomUUID} from 'node:crypto';
4
+ import {fork} from 'node:child_process';
5
+ import {fileURLToPath} from 'node:url';
6
+
7
+ // Observation contract: live notifications establish continuity only while the
8
+ // subscription is healthy; history may accelerate catch-up; a fresh scan is
9
+ // the deterministic fallback. A failed history operation never updates cursor.
10
+ const workerFile=path.join(path.dirname(fileURLToPath(import.meta.url)),'history-worker.mjs');
11
+ export function history(root,cursor,backend,deadlineMs,faultFile) {
12
+ const next=cursor+'.'+randomUUID()+'.next';
13
+ return new Promise((resolve,reject)=>{
14
+ const worker=fork(workerFile,[root,fs.existsSync(cursor)?cursor:'',next,backend,
15
+ faultFile||''],{stdio:['ignore','ignore','ignore','ipc']});
16
+ let settled=false,abandoned=false;
17
+ const finish=(error,value)=>{
18
+ if(settled)return;settled=true;clearTimeout(timer);
19
+ worker.kill('SIGKILL');
20
+ if(error){abandoned=true;try{fs.unlinkSync(next);}catch{}
21
+ reject(error);return;}
22
+ try{fs.renameSync(next,cursor);resolve(value);}
23
+ catch(e){try{fs.unlinkSync(next);}catch{}reject(e);}
24
+ };
25
+ const timer=setTimeout(()=>{
26
+ const e=Error(`history query did not settle within ${deadlineMs} ms`);
27
+ e.code='ETIMEDOUT';finish(e);
28
+ },deadlineMs);
29
+ worker.on('message',message=>message.ok?finish(null,message.changes):
30
+ finish(Object.assign(Error(message.error),{code:message.code})));
31
+ worker.once('error',e=>finish(e));
32
+ worker.once('exit',(code,signal)=>{
33
+ if(abandoned)try{fs.unlinkSync(next);}catch{}
34
+ if(!settled)finish(Error(`history helper exited ${code??signal}`));
35
+ });
36
+ });
37
+ }
@@ -0,0 +1,32 @@
1
+ // Resolve the destination, including linked parent directories, before any
2
+ // state creation or removal. A watched root is readable, never state-owned.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import {withinPath as within,realObservedPath} from './path-identity.mjs';
6
+
7
+ export function resolvedDestination(file) {
8
+ let current=path.resolve(file),rest=[];
9
+ for(;;) {
10
+ try {
11
+ const canonical=realObservedPath(current);
12
+ return path.join(canonical,...rest);
13
+ } catch(e) {
14
+ if(e.code!=='ENOENT')throw e;
15
+ const parent=path.dirname(current);
16
+ if(parent===current)throw Error('state destination cannot be resolved');
17
+ rest.unshift(path.basename(current));current=parent;
18
+ }
19
+ }
20
+ }
21
+
22
+ export function assertOwnedStatePlacement(config) {
23
+ const state=path.resolve(config.state),resolved=resolvedDestination(state);
24
+ const observed=[config.root,...(config.checks||[]).flatMap(check=>
25
+ check.allowedExternalRoots||[])].map(resolvedDestination);
26
+ for(const boundary of observed)if(within(resolved,boundary)||within(boundary,resolved))
27
+ throw Error('REDUE state overlaps an observed checkout or external root');
28
+ try{if(fs.lstatSync(state).isSymbolicLink())
29
+ throw Error('REDUE state directory must not be a symlink');}
30
+ catch(e){if(e.code!=='ENOENT')throw e;}
31
+ return {state,resolved};
32
+ }
@@ -0,0 +1,43 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+
4
+ // Windows callers may supply an 8.3 spelling while filesystem APIs and
5
+ // TypeScript report the long name. Canonicalize existing paths at observation
6
+ // boundaries; keep logical link paths separately for invalidation.
7
+ export function realObservedPath(value) {
8
+ return process.platform==='win32'?fs.realpathSync.native(value):fs.realpathSync(value);
9
+ }
10
+
11
+ // Use the host path implementation in production. Passing path.win32 makes
12
+ // the identity rules testable elsewhere; those tests are not NTFS acceptance.
13
+ export function normalizedPath(value,paths=path) {
14
+ const resolved=paths.resolve(value);
15
+ if(paths!==path.win32)return resolved;
16
+ if(/^\\\\\?\\UNC\\/i.test(resolved))return '\\\\'+resolved.slice(8);
17
+ if(/^\\\\\?\\[A-Za-z]:\\/.test(resolved))return resolved.slice(4);
18
+ return resolved;
19
+ }
20
+
21
+ export function samePath(left,right,paths=path) {
22
+ return paths.relative(normalizedPath(left,paths),normalizedPath(right,paths))==='';
23
+ }
24
+
25
+ export function withinPath(file,root,paths=path) {
26
+ const relative=paths.relative(normalizedPath(root,paths),normalizedPath(file,paths));
27
+ return relative===''||(!paths.isAbsolute(relative)&&relative!=='..'&&
28
+ !relative.startsWith('..'+paths.sep));
29
+ }
30
+
31
+ export function observedRelative(root,file,paths=path) {
32
+ if(!withinPath(file,root,paths))return null;
33
+ return paths.relative(normalizedPath(root,paths),normalizedPath(file,paths))
34
+ .split(paths.sep).join('/');
35
+ }
36
+
37
+ export function validInputRelative(rel,platform=process.platform) {
38
+ if(typeof rel!=='string'||!rel||path.posix.isAbsolute(rel)||
39
+ rel.split('/').includes('..'))return false;
40
+ if(platform==='win32'&&(path.win32.isAbsolute(rel)||/^[A-Za-z]:/.test(rel)||
41
+ rel.includes('\\')))return false;
42
+ return true;
43
+ }
@@ -0,0 +1,131 @@
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 {queriesMatch} from './typescript-list.mjs';
8
+ import {planGuard,inputKey} from './decision-validation.mjs';
9
+ import {timing} from './decision-profile.mjs';
10
+
11
+ try {
12
+ const totalStarted=performance.now();
13
+ const configStarted=performance.now();
14
+ const config=JSON.parse(fs.readFileSync(workerData.config));
15
+ timing('worker.config_load',configStarted);
16
+ let discoveryStarted=performance.now();
17
+ let bundle=workerData.reuse?.bundle;
18
+ let validatedIndex=null,guard=null,reuseReason='no_validated_plan',planReused=false;
19
+ if(bundle){
20
+ const started=performance.now();
21
+ try{
22
+ guard=planGuard(bundle);
23
+ const queryStarted=performance.now();
24
+ const queriesValid=!workerData.reuse.queries?.length||queriesMatch(workerData.reuse.queries);
25
+ timing('worker.compiler_queries',queryStarted,{queries:workerData.reuse.queries?.length||0,matched:queriesValid?1:0});
26
+ if(guard!==workerData.reuse.guard||!queriesValid)reuseReason='configuration_or_resolution_changed';
27
+ else {
28
+ validatedIndex=new InputIndex(bundle);
29
+ validatedIndex.coldScan(progress=>parentPort.postMessage({kind:'progress',
30
+ phase:'validating',scanned:progress.scanned,total:progress.total}));
31
+ if(validatedIndex.unavailable)throw Error(validatedIndex.unavailable);
32
+ if(planGuard(bundle)!==guard)throw Error('plan dependencies changed during validation');
33
+ // Compiler imports can change effective membership. Until that content
34
+ // has been rediscovered, never reuse a compiler input contract.
35
+ const changed=[...validatedIndex.checks].some(([name,row])=>
36
+ row.plan.qualification&&inputKey(row)!==workerData.reuse.inputs[name]);
37
+ reuseReason=changed?'compiler_inputs_changed':'dependencies_unchanged';
38
+ planReused=!changed;
39
+ }
40
+ }catch{reuseReason='validation_unavailable';}
41
+ timing('worker.plan_validation',started,{reason:reuseReason,
42
+ files:validatedIndex?.files.size||0,
43
+ bytes:validatedIndex?.rehashedBytes||0,
44
+ hashingMs:Math.round(validatedIndex?.profile.hashingMs||0),
45
+ enumerationMs:Math.round(validatedIndex?.profile.enumerationMs||0)});
46
+ }
47
+ discoveryStarted=performance.now();
48
+ if(!planReused){bundle=discover(config,workerData.state);validatedIndex=null;
49
+ guard=planGuard(bundle);}
50
+ timing('worker.discovery_reason',performance.now(),{reason:reuseReason,reused:planReused?1:0});
51
+ const discoveryMs=performance.now()-discoveryStarted;
52
+ timing(planReused?'worker.discovery_reused':'worker.discovery',discoveryStarted,{checks:Object.keys(bundle.plans).length});
53
+ parentPort.postMessage({kind:'progress',phase:'indexing',discoveryMs,scanned:0,total:null});
54
+ const digest=file=>createHash('sha256').update(fs.readFileSync(file)).digest('hex');
55
+ let retained=null;
56
+ const retainedStarted=performance.now();
57
+ try {if(workerData.forceCold)throw Error('fresh reconciliation required');
58
+ retained=JSON.parse(fs.readFileSync(path.join(workerData.state,'index-v1.json')));
59
+ const identity=fs.statSync(bundle.root,{bigint:true});
60
+ if(retained.schema!==1||retained.root!==bundle.root||
61
+ JSON.stringify(retained.identity)!==JSON.stringify([String(identity.dev),String(identity.ino)])||
62
+ JSON.stringify(retained.planIds)!==JSON.stringify(Object.fromEntries(
63
+ Object.entries(bundle.plans).map(([name,plan])=>[name,plan.id])))||
64
+ retained.cursor!==digest(path.join(workerData.state,'fs-events.snapshot'))||
65
+ Object.keys(retained.external||{}).length!==new Set(Object.values(bundle.plans)
66
+ .flatMap(plan=>plan.externalObservationRoots||
67
+ Object.keys(plan.externalPatterns||{}))).size||
68
+ Object.entries(retained.external||{}).some(([root,value])=>
69
+ value.cursor!==digest(path.join(workerData.state,
70
+ `external-${createHash('sha256').update(root).digest('hex').slice(0,16)}.snapshot`))))
71
+ retained=null;
72
+ } catch {retained=null;}
73
+ timing('worker.retained_state_validation',retainedStarted,{reused:retained?1:0});
74
+ if(retained){
75
+ timing('worker.total',totalStarted,{mode:'retained'});
76
+ parentPort.postMessage({ok:true,bundle,discoveryMs,indexingMs:0,
77
+ restartMode:'retained-replay',index:retained.index,
78
+ external:Object.fromEntries(Object.entries(retained.external||{})
79
+ .map(([root,value])=>[root,value.index]))});
80
+ } else {
81
+ if(workerData.reconcileFaultFile&&fs.existsSync(workerData.reconcileFaultFile))
82
+ throw Error('injected reconciliation failure');
83
+ const indexingStarted=performance.now(), index=validatedIndex||new InputIndex(bundle);
84
+ if(!validatedIndex){const report=progress=>parentPort.postMessage({kind:'progress',phase:'indexing',discoveryMs,...progress});
85
+ index.coldScan(report);}
86
+ const indexingMs=performance.now()-indexingStarted;
87
+ timing(validatedIndex?'worker.index_reused':'worker.index',indexingStarted,{files:index.files.size,
88
+ entries:index.profile.enumeratedEntries,hashed:index.rehashedFiles,
89
+ bytes:index.rehashedBytes,enumerationMs:Math.round(index.profile.enumerationMs),
90
+ matchingMs:Math.round(index.profile.matchingMs),
91
+ hashingMs:Math.round(index.profile.hashingMs)});
92
+ if(index.unavailable)throw Error(index.unavailable);
93
+ const external={};
94
+ for(const boundary of [...new Set(Object.values(bundle.plans)
95
+ .flatMap(plan=>plan.externalObservationRoots||
96
+ Object.keys(plan.externalPatterns||{})))]) {
97
+ const plans=Object.fromEntries(Object.entries(bundle.plans)
98
+ .filter(([,plan])=>(plan.externalObservationRoots||
99
+ Object.keys(plan.externalPatterns||{})).includes(boundary))
100
+ .map(([name,plan])=>[name,{...plan,patterns:plan.externalPatterns[boundary],
101
+ sourcePatterns:[],
102
+ additivePatterns:plan.externalPatterns[boundary]||[],
103
+ installedPhysicalRoots:[]}]));
104
+ const outside=new InputIndex({root:boundary,plans},{trackGit:false,includeInstalled:true});
105
+ outside.coldScan();
106
+ if(outside.unavailable)throw Error(outside.unavailable);
107
+ external[boundary]={files:[...outside.files],
108
+ aliasDependencies:[...outside.aliasDependencies].map(([name,refs])=>[name,[...refs]]),
109
+ rows:[...outside.checks]
110
+ .map(([name,row])=>({name,files:[...row.files],sum:row.sum.toString('hex'),
111
+ revision:row.revision,lastReason:row.lastReason})),
112
+ eventCount:outside.eventCount,rehashedFiles:outside.rehashedFiles,
113
+ rehashedBytes:outside.rehashedBytes,profile:outside.profile};
114
+ }
115
+ timing('worker.total',totalStarted,{mode:'cold',files:index.files.size});
116
+ parentPort.postMessage({ok:true,bundle,discoveryMs,indexingMs,guard,planReused,
117
+ inputKeys:Object.fromEntries([...index.checks].map(([name,row])=>[name,inputKey(row)])),
118
+ restartMode:'cold-index',
119
+ external,
120
+ index:{files:[...index.files],
121
+ aliasDependencies:[...index.aliasDependencies].map(([name,refs])=>[name,[...refs]]),
122
+ rows:[...index.checks].map(([name,row])=>({
123
+ name,files:[...row.files],sum:row.sum.toString('hex'),revision:row.revision,
124
+ lastReason:row.lastReason})),
125
+ eventCount:index.eventCount,rehashedFiles:index.rehashedFiles,
126
+ rehashedBytes:index.rehashedBytes,profile:index.profile}});
127
+ }
128
+ } catch(error) {
129
+ parentPort.postMessage({ok:false,error:error?.message||String(error),
130
+ classification:error?.name||'Error'});
131
+ }
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
+ }