@redue/cli 0.1.0-alpha.4 → 0.1.0-alpha.6

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.
@@ -0,0 +1,72 @@
1
+ // Versioned, explicit offline compiler recipe. No package manager is executed.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import {createHash} from 'node:crypto';
5
+ import {createRequire} from 'node:module';
6
+ import {realObservedPath,withinPath} from './path-identity.mjs';
7
+ export const directInterpretation='npm-direct-typescript@1';
8
+ export const directQualification='npm-typescript-direct-v1';
9
+ const reviewed={
10
+ '5.6.3':{entry:'lib/tsc.js',digest:'e674b5dc4dac50f6e91be30094aeea983a1f340231717e62b0b6199ae3049eb2'},
11
+ '5.9.3':{entry:'lib/_tsc.js',digest:'d1b32e75475710fdc85102b1365254d84ceb192d3639d962c53fe1355ce274ca'},
12
+ };
13
+ const fail=code=>{throw Object.assign(Error(code),{code});};
14
+ const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
15
+ export const directEnvironment={variables:['NODE_ENV','NODE_OPTIONS','NODE_PATH','NODE_COMPILE_CACHE','VSCODE_INSPECTOR_OPTIONS'],
16
+ prefixes:['NODE_','DYLD_','LD_','TSGO_','TSC_'],executableIdentity:true};
17
+ export function directEnvironmentIssue(env=process.env){
18
+ if(![22,24].includes(Number(process.versions.node.split('.')[0])))return 'direct-node-version-unsupported';
19
+ if(env.VSCODE_INSPECTOR_OPTIONS)return 'direct-execution-environment-unsupported';
20
+ for(const [key,value] of Object.entries(env)){
21
+ if(/^NODE_/i.test(key)){
22
+ // Node 22.13.0 predates env-proxy initialization. This injected flag is
23
+ // inert there, but remains strictly fingerprinted as NODE_ context.
24
+ if(key==='NODE_USE_ENV_PROXY'&&value==='1'&&process.version==='v22.13.0'&&
25
+ !process.allowedNodeEnvironmentFlags.has('--use-env-proxy'))continue;
26
+ if(key==='NODE_ENV'&&['','production','test'].includes(value))continue;
27
+ if(value!=='')return 'direct-execution-environment-unsupported';
28
+ }
29
+ if(/^(DYLD_|LD_|TSGO_|TSC_)/i.test(key)&&value!=='')return 'direct-execution-environment-unsupported';
30
+ }
31
+ return null;
32
+ }
33
+ export function directCompiler(root,{verify=true}={}){
34
+ root=realObservedPath(root);
35
+ const manifest=createRequire(path.join(root,'package.json')).resolve('typescript/package.json');
36
+ const tsRoot=path.dirname(realObservedPath(manifest));
37
+ if(!withinPath(tsRoot,root)||tsRoot!==realObservedPath(path.join(root,'node_modules/typescript')))
38
+ fail('direct-typescript-layout-unsupported');
39
+ const version=JSON.parse(fs.readFileSync(manifest)).version,spec=reviewed[version];
40
+ if(!spec)fail('direct-typescript-version-unsupported');
41
+ if(verify){
42
+ const files=[];
43
+ const walk=(dir,relative='')=>{
44
+ for(const entry of fs.readdirSync(dir,{withFileTypes:true})){
45
+ const name=relative?relative+'/'+entry.name:entry.name,full=path.join(dir,entry.name);
46
+ if(entry.isDirectory())walk(full,name);
47
+ else if(entry.isFile())files.push([name,full]);
48
+ else fail('direct-typescript-implementation-unreviewed');
49
+ }
50
+ };walk(tsRoot);
51
+ const digest=createHash('sha256');
52
+ for(const [name,file] of files.sort(([a],[b])=>a<b?-1:a>b?1:0))digest.update(name+'\0'+hash(fs.readFileSync(file))+'\0');
53
+ if(digest.digest('hex')!==spec.digest)fail('direct-typescript-implementation-unreviewed');
54
+ }
55
+ return {tsRoot,version,entry:path.join(tsRoot,spec.entry),digest:spec.digest};
56
+ }
57
+ export function directProject(root,script,{verify=true}={}){
58
+ const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json')));
59
+ if(pkg.workspaces||pkg.packageManager&&!/^npm@/.test(pkg.packageManager)||
60
+ !['package-lock.json','npm-shrinkwrap.json'].some(file=>fs.existsSync(path.join(root,file)))||
61
+ ['yarn.lock','pnpm-lock.yaml','bun.lock','bun.lockb'].some(file=>fs.existsSync(path.join(root,file))))
62
+ fail('direct-npm-project-unsupported');
63
+ if(pkg.scripts?.[script]?.trim()!=='tsc --noEmit'||pkg.scripts?.['pre'+script]||pkg.scripts?.['post'+script])
64
+ fail('typecheck-script-changed');
65
+ const issue=directEnvironmentIssue();if(issue)fail(issue);
66
+ return directCompiler(root,{verify});
67
+ }
68
+ // Provenance only: retain a digest, never proxy URLs/credentials or a raw dump.
69
+ // No npm process runs in this recipe; these keys are not compiler applicability.
70
+ export function observedNpmContext(env=process.env){
71
+ return hash(JSON.stringify(Object.entries(env).filter(([key])=>/^npm_/i.test(key)||/^(?:https?_proxy|all_proxy|no_proxy)$/i.test(key)).sort(([a],[b])=>a<b?-1:a>b?1:0)));
72
+ }
@@ -1,3 +1,4 @@
1
+ import {directProject,directQualification} from './direct-typescript.mjs';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import {spawnSync} from 'node:child_process';
@@ -159,7 +160,8 @@ function workspaceManifests(root,patterns) {
159
160
  if(matched.length>50)throw Error('more than 50 workspace packages found; select checks explicitly');
160
161
  return matched.map(file=>({file,pkg:readJson(path.join(root,file))}));
161
162
  }
162
- export function discoverNodeProject(root) {
163
+ export function discoverNodeProject(root,{recipe=null}={}) {
164
+ if(recipe&&recipe!=='typescript-direct')throw Error('unknown recipe; use typescript-direct');
163
165
  root=realObservedPath(root);
164
166
  const packageFile=path.join(root,'package.json');
165
167
  if(!exists(packageFile))throw Error('run init from a repository containing package.json');
@@ -186,6 +188,15 @@ export function discoverNodeProject(root) {
186
188
  if(workspaces&&classification.qualification==='typescript-noemit-v1')classification={level:'recording',
187
189
  reason:'workspace task and installed-input closure need explicit qualification'};
188
190
  const check={name:kind,script,kind,inputs:sourceInputs(root,kind,pm.lockfiles||[])};
191
+ if(recipe==='typescript-direct'&&kind==='typecheck'){
192
+ try{
193
+ if(pm.name!=='npm'||workspaces||observationIssue||pm.issue)throw Error('direct recipe requires a supported single-package npm checkout');
194
+ const compiler=directProject(root,script);
195
+ check.command=['@node','@typescript-compiler:.','--noEmit'];check.cwd='.';
196
+ classification={level:'ready',qualification:directQualification,
197
+ reason:`explicit local TypeScript ${compiler.version} compilation; npm launcher/lifecycle scripts are not executed; package.json stays unchanged`};
198
+ }catch(e){classification={level:'recording',reason:`Direct recipe unavailable (${e.code||e.message}); original npm script retained, without bypassing required behavior`};}
199
+ }
189
200
  if(pm.name==='pnpm'&&kind==='typecheck'&&!workspaces&&!pm.issue&&
190
201
  /^(?:tsc --noEmit|tsc -p (?:\.|tsconfig\.json)(?: --noEmit)?)$/.test(scripts[script].trim())){
191
202
  try{const contract=pnpmTypecheckInputs(root,'.',script);
@@ -0,0 +1,36 @@
1
+ // Admission for the standalone npm 10.9.2 -> tsc --noEmit contract only.
2
+ // Values are never returned: the caller separately fingerprints every original
3
+ // npm_config_* key/value (including case and empty values).
4
+ import path from 'node:path';
5
+ export const npmEnvironmentInterpretation = 'npm-tsc-environment@2';
6
+ const proxyKeys = new Set(['proxy', 'https_proxy', 'http_proxy', 'noproxy']);
7
+ export function npmTypecheckEnvironmentIssue(env, {npmVersion, platform = process.platform} = {}) {
8
+ if (env.NODE_OPTIONS || env.NODE_PATH || env.BASH_ENV || env.ENV)
9
+ return 'execution-environment-unsupported';
10
+ const seen = new Map();
11
+ for (const [key, value] of Object.entries(env)) {
12
+ if (/^(DYLD_|TSGO_)/i.test(key)) return 'execution-environment-unsupported';
13
+ if (!/^npm_config_/i.test(key)) continue;
14
+ if (/^npm_config_prefix$/i.test(key) && platform === 'win32' && path.win32.isAbsolute(value)) continue;
15
+ const suffix = key.slice('npm_config_'.length).toLowerCase();
16
+ if (!proxyKeys.has(suffix)) return 'execution-environment-unsupported';
17
+ // http_proxy is an unknown, inert pass-through key in this inspected npm
18
+ // version, not an alias for proxy. Other npm versions require source review.
19
+ if (npmVersion !== '10.9.2') return 'npm-proxy-version-unsupported';
20
+ if (seen.has(suffix) && seen.get(suffix) !== value)
21
+ return 'npm-proxy-context-ambiguous';
22
+ seen.set(suffix, value);
23
+ if (typeof value !== 'string' || /[\x00-\x1f\x7f]/.test(value))
24
+ return 'npm-proxy-value-unsupported';
25
+ // npm ignores empty environment config, but passes the original environment
26
+ // to the child. Empty and absent remain distinct caller identities.
27
+ if (value === '' || (suffix === 'proxy' && value === 'false')) continue;
28
+ if (suffix === 'noproxy') continue;
29
+ try {
30
+ const url = new URL(value);
31
+ if (!['http:', 'https:'].includes(url.protocol) || !url.hostname)
32
+ return 'npm-proxy-value-unsupported';
33
+ } catch { return 'npm-proxy-value-unsupported'; }
34
+ }
35
+ return null;
36
+ }
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import {createHash} from 'node:crypto';
5
5
  import {discover} from './plan.mjs';
6
6
  import {InputIndex} from './index.mjs';
7
+ import {directProject,directQualification} from './direct-typescript.mjs';
7
8
  import {queriesMatch} from './typescript-list.mjs';
8
9
  import {planGuard,inputKey} from './decision-validation.mjs';
9
10
  import {timing} from './decision-profile.mjs';
@@ -21,7 +22,14 @@ try {
21
22
  try{
22
23
  guard=planGuard(bundle);
23
24
  const queryStarted=performance.now();
24
- const queriesValid=!workerData.reuse.queries?.length||queriesMatch(workerData.reuse.queries);
25
+ // Cached membership queries load the compiler API; establish its reviewed
26
+ // implementation before that optimization can execute project code.
27
+ const directRoots=new Set(config.checks.filter(check=>check.qualification===directQualification)
28
+ .map(check=>directProject(config.root,check.script).tsRoot));
29
+ if(directRoots.size&&(workerData.reuse.queries||[]).some(([kind,,,options])=>
30
+ kind==='readDirectory'&&!directRoots.has(options?.tsRoot)))throw Error('retained compiler implementation changed');
31
+ const queriesValid=guard===workerData.reuse.guard&&
32
+ (!workerData.reuse.queries?.length||queriesMatch(workerData.reuse.queries));
25
33
  timing('worker.compiler_queries',queryStarted,{queries:workerData.reuse.queries?.length||0,matched:queriesValid?1:0});
26
34
  if(guard!==workerData.reuse.guard||!queriesValid)reuseReason='configuration_or_resolution_changed';
27
35
  else {
@@ -33,6 +33,7 @@ export function renderExplain(value,name) {
33
33
  return rows.map(row=>{
34
34
  const lines=[`${row.name}: ${checkLabel(row)}${row.result?` / ${row.result}`:''}`];
35
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.');
36
37
  lines.push(explanation(row));
37
38
  if(row.changed_inputs?.length)lines.push('Relevant inputs changed:',...row.changed_inputs.map(file=>` ${file}`));
38
39
  if(row.freshness==='STALE')lines.push(`Due again when verification is needed: redue run ${JSON.stringify(row.name)}`);
@@ -0,0 +1,34 @@
1
+ // Shared project selection only; evidence and caller-context semantics stay in the kernel.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import {createHash} from 'node:crypto';
6
+ import {realObservedPath} from './path-identity.mjs';
7
+ import {stateIdentity,userStateBase} from './platform-state.mjs';
8
+
9
+ export function discoverConfig(cwd=process.cwd()) {
10
+ let dir=path.resolve(cwd);const found=[];
11
+ for(;;){
12
+ for(const name of ['redue.config.json','vstate.config.json']) {
13
+ const file=path.join(dir,name);
14
+ if(fs.existsSync(file))found.push(file);
15
+ }
16
+ if(fs.existsSync(path.join(dir,'.git'))||path.dirname(dir)===dir)break;
17
+ dir=path.dirname(dir);
18
+ }
19
+ if(found.length>1)throw Error('multiple REDUE configurations apply; select the intended one with --config FILE');
20
+ return found[0]||path.resolve(cwd,'redue.config.json');
21
+ }
22
+ export function canonicalConfig(configFile) {
23
+ return path.join(realObservedPath(path.dirname(configFile)),path.basename(configFile));
24
+ }
25
+ export function projectState(root,configFile) {
26
+ return path.join(userStateBase(process.platform,process.env,os.homedir()),'vstate',
27
+ `vstate-${createHash('sha256').update(stateIdentity(root)+'\0'+stateIdentity(canonicalConfig(configFile))).digest('hex').slice(0,16)}`);
28
+ }
29
+ export function readProject(configFile) {
30
+ const config=JSON.parse(fs.readFileSync(configFile,'utf8'));
31
+ if(config.schema!==1||!Array.isArray(config.checks)||!config.checks.length)
32
+ throw Error('config needs schema 1 and at least one check');
33
+ return {config,root:realObservedPath(path.resolve(path.dirname(configFile),config.root||'.'))};
34
+ }
@@ -0,0 +1,65 @@
1
+ // Extracted from the existing npm TypeScript probe; shared compiler input logic.
2
+ import fs from 'node:fs';import path from 'node:path';import {createHash} from 'node:crypto';
3
+ import {createRequire} from 'node:module';import {spawnSync} from 'node:child_process';
4
+ import {compilerFiles} from './typescript-list.mjs';
5
+ import {withinPath as within,realObservedPath} from './path-identity.mjs';
6
+ import {timing} from './decision-profile.mjs';
7
+ const require=createRequire(import.meta.url),hash=value=>createHash('sha256').update(value).digest('hex');
8
+ const digestFile=file=>hash(fs.readFileSync(file));
9
+ const fail=code=>{throw Object.assign(Error(code),{code});};
10
+ export function typeScriptInputFacts(root,tsRoot,tsc){
11
+ const configReads=new Set(),visited=new Set();
12
+ let ts;try{ts=require(tsRoot);}catch{}
13
+ if(typeof ts?.getParsedCommandLineOfConfigFile==='function'){
14
+ const host={...ts.sys,readFile(file){configReads.add(path.resolve(file));
15
+ return ts.sys.readFile(file);},onUnRecoverableConfigFileDiagnostic(){}};
16
+ const parseConfig=file=>{
17
+ const absolute=path.resolve(file);
18
+ if(!within(absolute,root))fail('typescript-config-outside-repository');
19
+ if(visited.has(absolute))return;visited.add(absolute);
20
+ if(visited.size>64)fail('typescript-config-graph-too-large');
21
+ const parsed=ts.getParsedCommandLineOfConfigFile(absolute,{},host);
22
+ if(!parsed||parsed.errors?.length)fail('typescript-config-unavailable');
23
+ if(parsed.options?.plugins?.length)fail('typescript-plugin-unsupported');
24
+ if(parsed.options?.incremental||parsed.options?.composite)
25
+ fail('typescript-incremental-unsupported');
26
+ for(const reference of parsed.projectReferences||[])
27
+ parseConfig(reference.path.endsWith('.json')?reference.path:
28
+ path.join(reference.path,'tsconfig.json'));
29
+ };
30
+ parseConfig(path.join(root,'tsconfig.json'));
31
+ }else{
32
+ // TypeScript 7's native compiler does not expose the previous config API.
33
+ // Its CLI reports the effective config; restrict this path to one direct
34
+ // config rather than guessing about extends/reference provenance.
35
+ const config=path.join(root,'tsconfig.json'),raw=fs.readFileSync(config,'utf8');
36
+ if(/"(?:extends|references|plugins|incremental|composite)"\s*:/.test(raw))
37
+ fail('typescript-config-graph-unsupported');
38
+ const shown=spawnSync(process.execPath,[tsc,'--showConfig'],{cwd:root,
39
+ env:process.env,encoding:'utf8',timeout:4000,maxBuffer:16*1024*1024,
40
+ stdio:['ignore','pipe','pipe']});
41
+ if(shown.error||shown.signal||shown.status!==0)fail('typescript-config-unavailable');
42
+ const effective=JSON.parse(shown.stdout);
43
+ if(effective.compilerOptions?.plugins?.length||
44
+ effective.compilerOptions?.incremental||effective.compilerOptions?.composite)
45
+ fail('typescript-config-graph-unsupported');
46
+ configReads.add(config);
47
+ }
48
+ for(const file of configReads)if(!within(realObservedPath(file),root))
49
+ fail('typescript-config-outside-repository');
50
+ const listingStarted=performance.now();
51
+ const captured=process.argv.includes('--redue-checkpoint')?compilerFiles(tsRoot,root):null;
52
+ const listed=captured?{status:0,stdout:captured.files.join('\n')}:spawnSync(process.execPath,[tsc,'--noEmit','--listFilesOnly'],
53
+ {cwd:root,env:process.env,encoding:'utf8',timeout:process.platform==='win32'?10000:4000,
54
+ maxBuffer:16*1024*1024,
55
+ stdio:['ignore','pipe','pipe']});
56
+ timing('typescript_contract.list_files',listingStarted,{status:listed.status??null,
57
+ error:listed.error?.code||null});
58
+ if(listed.error||listed.signal||listed.status!==0)fail('typescript-input-list-unavailable');
59
+ const files=listed.stdout.trim().split(/\r?\n/).filter(Boolean)
60
+ .map(file=>path.resolve(file));
61
+ if(!files.length||files.some(file=>!within(realObservedPath(file),root)))
62
+ fail('typescript-input-outside-repository');
63
+ return {configs:[...configReads].sort().map(file=>[hash(file),digestFile(file)]),
64
+ files:[...new Set(files)].sort().map(file=>[hash(file),digestFile(file)]),queries:captured?.queries};
65
+ }
@@ -9,7 +9,8 @@ const require=createRequire(import.meta.url);
9
9
  const hash=value=>createHash('sha256').update(JSON.stringify(value)).digest('hex');
10
10
  export function queryAnswer(kind,file,options) {
11
11
  try{
12
- if(kind==='readDirectory')return require(options.tsRoot).sys.readDirectory(file,...options.args).sort();
12
+ if(kind==='readDirectory')return require(options.tsRoot).sys.readDirectory(file,
13
+ ...options.args.map((value,i)=>options.undefinedArguments?.includes(i)?undefined:value)).sort();
13
14
  if(kind==='readFile')return hash(fs.readFileSync(file,'utf8'));
14
15
  if(kind==='fileExists')return fs.statSync(file).isFile();
15
16
  if(kind==='directoryExists')return fs.statSync(file).isDirectory();
@@ -42,7 +43,10 @@ export function compilerFiles(tsRoot,cwd) {
42
43
  const getDirectories=file=>{remember('directories',file);return ts.sys.getDirectories(file);};
43
44
  const readDirectory=(dir,...args)=>{
44
45
  const result=ts.sys.readDirectory(dir,...args);
45
- const full=path.resolve(dir),options={tsRoot,args};
46
+ // JSON turns undefined array entries into null. TypeScript distinguishes
47
+ // them (an omitted exclude list is not a null list), so retain that fact.
48
+ const full=path.resolve(dir),options={tsRoot,args,argsVersion:1,
49
+ undefinedArguments:args.flatMap((value,i)=>value===undefined?[i]:[])};
46
50
  queries.set('readDirectory\0'+full+JSON.stringify(args),
47
51
  ['readDirectory',full,[...result].sort(),options]);
48
52
  return result;
@@ -1,11 +1,32 @@
1
+ import {directProject,directQualification,directEnvironment} from './direct-typescript.mjs';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import {realObservedPath} from './path-identity.mjs';
4
5
 
5
6
  // This is a machine-enforced contract, not a claim inferred from a script name.
6
- // The read-only runtime probe separately checks the effective TypeScript file
7
- // list, config graph, npm toolchain and caller context on every sync/run.
7
+ // Runtime validation checks effective compiler membership, configuration and
8
+ // toolchain identity. Supported discovery certificates still validate those
9
+ // filesystem queries; caller context is independently compared at the decision.
8
10
  export function qualifyTypeScript(root,selected,discovered={}) {
11
+ if(selected.qualification===directQualification){
12
+ const issues=[];
13
+ try{
14
+ const compiler=directProject(root,selected.script);
15
+ if(selected.command?.length!==3||selected.command[0]!==process.execPath||
16
+ selected.command[1]!==compiler.entry||selected.command[2]!=='--noEmit'||
17
+ selected.cwd&&selected.cwd!=='.')issues.push('direct compiler invocation changed');
18
+ }catch(e){issues.push(e.code||'direct compiler prerequisites unavailable');}
19
+ if(!selected.inputs?.includes('**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json,jsonc}')||
20
+ !selected.inputs?.includes('package.json')||!selected.inputs?.includes('tsconfig.json')||
21
+ !selected.installedInputs?.includes('node_modules/**'))issues.push('direct compiler source/config/installed inputs incomplete');
22
+ if(!selected.environment?.executableIdentity||
23
+ !directEnvironment.variables.every(key=>selected.environment?.variables?.includes(key))||
24
+ !directEnvironment.prefixes.every(key=>selected.environment?.prefixes?.includes(key)))
25
+ issues.push('direct compiler execution context incomplete');
26
+ if(selected.probes?.length!==1||!selected.probes?.some(argv=>path.basename(argv[1]||'')==='direct-typescript-probe.mjs'&&
27
+ argv[0]===process.execPath&&argv[2]===root&&argv[3]===selected.script))issues.push('direct compiler probe missing');
28
+ return {qualified:issues.length===0,issues};
29
+ }
9
30
  if(selected.qualification==='pnpm-tsc-v1'){
10
31
  const issues=[];
11
32
  const workspace=selected.workspace||'.';