@redue/cli 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
@@ -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';
@@ -116,7 +117,7 @@ function classify(kind,scriptName,script,pkg,managerInfo,root) {
116
117
  managerInfo.name==='npm'&&
117
118
  exists(path.join(root,'tsconfig.json'))&&
118
119
  exists(path.join(root,'node_modules/typescript/package.json')))
119
- return {level:'ready',reason:'canonical local tsc --noEmit; checked again before reuse',
120
+ return {level:'recording',reason:'npm launcher uses unobserved remote/cache/time inputs; execution can be recorded, but cannot be reused',
120
121
  qualification:'typescript-noemit-v1'};
121
122
  if(kind==='typecheck')return {level:'recording',reason:
122
123
  'TypeScript invocation, installation, or configuration is outside the automatic noEmit contract'};
@@ -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');
@@ -183,9 +185,18 @@ export function discoverNodeProject(root) {
183
185
  if(!script)continue;
184
186
  let classification=observationIssue?{level:'unsupported',reason:observationIssue}:
185
187
  classify(kind,script,scripts[script],pkg,pm,root);
186
- if(workspaces&&classification.level==='ready')classification={level:'recording',
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
+ }
@@ -1,188 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Read-only applicability probe for the deliberately narrow npm/tsc --noEmit
3
- // contract. It emits one digest; failures emit only a fixed diagnostic code.
4
- import {compilerFiles} from './typescript-list.mjs';
5
- import {contextOnly,probeResult} from './probe-result.mjs';
6
- import fs from 'node:fs';
7
- import path from 'node:path';
8
- import os from 'node:os';
9
- import {createHash} from 'node:crypto';
10
- import {createRequire} from 'node:module';
11
- import {spawnSync} from 'node:child_process';
12
- import {withinPath as within,realObservedPath} from './path-identity.mjs';
13
- import {timing} from './decision-profile.mjs';
14
-
15
- const [rootArg,scriptName,npmArg]=process.argv.slice(2);
16
- const hash=value=>createHash('sha256').update(value).digest('hex');
17
- const fail=code=>{throw Object.assign(Error(code),{code});};
18
- const digestFile=file=>hash(fs.readFileSync(file));
19
- const require=createRequire(import.meta.url);
20
- function treeHash(dir){
21
- const digest=createHash('sha256');let count=0;
22
- const visit=(folder,rel='')=>{
23
- for(const entry of fs.readdirSync(folder,{withFileTypes:true}).sort((a,b)=>
24
- a.name.localeCompare(b.name))){
25
- const name=rel?rel+'/'+entry.name:entry.name,full=path.join(folder,entry.name);
26
- if(entry.isDirectory())visit(full,name);
27
- else if(entry.isFile()){digest.update(name+'\0'+digestFile(full)+'\0');count++;}
28
- else fail('npm-installation-unobservable');
29
- }
30
- };
31
- visit(dir);return [digest.digest('hex'),count];
32
- }
33
- function npmRcKeys(file){
34
- if(!fs.existsSync(file))return null;
35
- const keys=[];
36
- for(const line of fs.readFileSync(file,'utf8').split(/\r?\n/)){
37
- const trimmed=line.trim();if(!trimmed||/^[#;]/.test(trimmed))continue;
38
- const at=trimmed.indexOf('=');if(at<0)fail('npm-configuration-unsupported');
39
- const key=trimmed.slice(0,at).trim();
40
- if(key!=='registry'&&!/^\/\/[^\s]+\/:_authToken$/.test(key))
41
- fail('npm-configuration-unsupported');
42
- keys.push(key);
43
- }
44
- return keys.sort();
45
- }
46
- try{
47
- if(!rootArg||!scriptName||!npmArg)fail('probe-arguments');
48
- const root=realObservedPath(rootArg),npm=realObservedPath(npmArg);
49
- const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));
50
- if(pkg.scripts?.[scriptName]?.trim()!=='tsc --noEmit'||
51
- pkg.scripts?.['pre'+scriptName]||pkg.scripts?.['post'+scriptName])
52
- fail('typecheck-script-changed');
53
- if(process.env.NODE_OPTIONS||process.env.NODE_PATH||process.env.BASH_ENV||
54
- process.env.ENV||Object.entries(process.env).some(([key,value])=>{
55
- if(/^npm_config_prefix$/i.test(key)&&process.platform==='win32'){
56
- // The Windows runner supplies a global npm prefix independently of
57
- // the selected local compiler. Its value is part of caller context.
58
- return !path.isAbsolute(value);
59
- }
60
- return /^(npm_config_|DYLD_|TSGO_)/i.test(key);
61
- }))fail('execution-environment-unsupported');
62
- let npmRoot;
63
- if(process.platform==='win32'){
64
- if(path.basename(npm).toLowerCase()!=='npm.cmd')
65
- fail('npm-installation-layout-unsupported');
66
- npmRoot=path.join(path.dirname(npm),'node_modules','npm');
67
- const body=fs.readFileSync(npm,'utf8');
68
- if(!body.includes('node_modules\\npm\\bin\\npm-cli.js')||
69
- !body.includes('node_modules\\npm\\bin\\npm-prefix.js')||
70
- !body.includes('"%NODE_EXE%" "%NPM_CLI_JS%" %*'))
71
- fail('npm-installation-layout-unsupported');
72
- const prefix=spawnSync(process.execPath,[path.join(npmRoot,'bin','npm-prefix.js')],
73
- {cwd:root,env:process.env,encoding:'utf8',timeout:4000,stdio:['ignore','pipe','ignore']});
74
- const selectedPrefix=prefix.stdout?.trim();
75
- if(prefix.error||prefix.status!==0||!selectedPrefix||
76
- !path.isAbsolute(selectedPrefix))fail('npm-installation-layout-unsupported');
77
- // npm.cmd selects the prefix's CLI only when that file exists. Otherwise
78
- // it runs the bundled CLI beside npm.cmd; observe the same branch here.
79
- const alternate=path.join(selectedPrefix,'node_modules','npm','bin','npm-cli.js');
80
- if(fs.existsSync(alternate)&&
81
- realObservedPath(alternate)!==realObservedPath(path.join(npmRoot,'bin','npm-cli.js')))
82
- fail('npm-installation-layout-unsupported');
83
- }else npmRoot=path.dirname(path.dirname(npm));
84
- if(path.basename(npmRoot)!=='npm'||path.basename(path.dirname(npmRoot))!=='node_modules')
85
- fail('npm-installation-layout-unsupported');
86
- const home=process.platform==='win32'?os.homedir():process.env.HOME;
87
- if(!home||!path.isAbsolute(home))fail('home-context-unavailable');
88
- const npmConfig=[path.join(root,'.npmrc'),path.join(home,'.npmrc')]
89
- .map(file=>[hash(file),npmRcKeys(file)]);
90
- const contextFacts={
91
- npmTree:treeHash(npmRoot),npmConfig,
92
- node:hash(JSON.stringify([process.version,realObservedPath(process.execPath),
93
- fs.statSync(process.execPath).size])),
94
- shell:process.platform==='win32'?hash(JSON.stringify([
95
- realObservedPath(process.env.ComSpec||process.env.COMSPEC||''),
96
- digestFile(process.env.ComSpec||process.env.COMSPEC||''),
97
- process.env.PATHEXT||'',digestFile(npm),
98
- digestFile(path.join(root,'node_modules','.bin','tsc.cmd'))])):
99
- digestFile('/bin/sh')};
100
- const contextHash=hash(JSON.stringify(contextFacts));
101
- if(contextOnly(contextHash))process.exit(0);
102
- const tsRoot=path.join(root,'node_modules','typescript');
103
- let tsc;
104
- if(process.platform==='win32'){
105
- const shim=path.join(root,'node_modules','.bin','tsc.cmd');
106
- const body=fs.readFileSync(shim,'utf8').replace(/\r\n/g,'\n').trim();
107
- const expected=`@ECHO off
108
- GOTO start
109
- :find_dp0
110
- SET dp0=%~dp0
111
- EXIT /b
112
- :start
113
- SETLOCAL
114
- CALL :find_dp0
115
-
116
- IF EXIST "%dp0%\\node.exe" (
117
- SET "_prog=%dp0%\\node.exe"
118
- ) ELSE (
119
- SET "_prog=node"
120
- SET PATHEXT=%PATHEXT:;.JS;=;%
121
- )
122
-
123
- endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\\..\\typescript\\bin\\tsc" %*`;
124
- if(body!==expected)fail('typescript-executable-changed');
125
- tsc=realObservedPath(path.join(tsRoot,'bin','tsc'));
126
- }else{
127
- tsc=realObservedPath(path.join(root,'node_modules','.bin','tsc'));
128
- if(tsc!==path.join(tsRoot,'bin','tsc'))fail('typescript-executable-changed');
129
- }
130
- const configReads=new Set(),visited=new Set();
131
- let ts;try{ts=require(tsRoot);}catch{}
132
- if(typeof ts?.getParsedCommandLineOfConfigFile==='function'){
133
- const host={...ts.sys,readFile(file){configReads.add(path.resolve(file));
134
- return ts.sys.readFile(file);},onUnRecoverableConfigFileDiagnostic(){}};
135
- const parseConfig=file=>{
136
- const absolute=path.resolve(file);
137
- if(!within(absolute,root))fail('typescript-config-outside-repository');
138
- if(visited.has(absolute))return;visited.add(absolute);
139
- if(visited.size>64)fail('typescript-config-graph-too-large');
140
- const parsed=ts.getParsedCommandLineOfConfigFile(absolute,{},host);
141
- if(!parsed||parsed.errors?.length)fail('typescript-config-unavailable');
142
- if(parsed.options?.plugins?.length)fail('typescript-plugin-unsupported');
143
- if(parsed.options?.incremental||parsed.options?.composite)
144
- fail('typescript-incremental-unsupported');
145
- for(const reference of parsed.projectReferences||[])
146
- parseConfig(reference.path.endsWith('.json')?reference.path:
147
- path.join(reference.path,'tsconfig.json'));
148
- };
149
- parseConfig(path.join(root,'tsconfig.json'));
150
- }else{
151
- // TypeScript 7's native compiler does not expose the previous config API.
152
- // Its CLI reports the effective config; restrict this path to one direct
153
- // config rather than guessing about extends/reference provenance.
154
- const config=path.join(root,'tsconfig.json'),raw=fs.readFileSync(config,'utf8');
155
- if(/"(?:extends|references|plugins|incremental|composite)"\s*:/.test(raw))
156
- fail('typescript-config-graph-unsupported');
157
- const shown=spawnSync(process.execPath,[tsc,'--showConfig'],{cwd:root,
158
- env:process.env,encoding:'utf8',timeout:4000,maxBuffer:16*1024*1024,
159
- stdio:['ignore','pipe','pipe']});
160
- if(shown.error||shown.signal||shown.status!==0)fail('typescript-config-unavailable');
161
- const effective=JSON.parse(shown.stdout);
162
- if(effective.compilerOptions?.plugins?.length||
163
- effective.compilerOptions?.incremental||effective.compilerOptions?.composite)
164
- fail('typescript-config-graph-unsupported');
165
- configReads.add(config);
166
- }
167
- for(const file of configReads)if(!within(realObservedPath(file),root))
168
- fail('typescript-config-outside-repository');
169
- const listingStarted=performance.now();
170
- const captured=process.argv.includes('--redue-checkpoint')?compilerFiles(tsRoot,root):null;
171
- const listed=captured?{status:0,stdout:captured.files.join('\n')}:spawnSync(process.execPath,[tsc,'--noEmit','--listFilesOnly'],
172
- {cwd:root,env:process.env,encoding:'utf8',timeout:process.platform==='win32'?10000:4000,
173
- maxBuffer:16*1024*1024,
174
- stdio:['ignore','pipe','pipe']});
175
- timing('typescript_contract.list_files',listingStarted,{status:listed.status??null,
176
- error:listed.error?.code||null});
177
- if(listed.error||listed.signal||listed.status!==0)fail('typescript-input-list-unavailable');
178
- const files=listed.stdout.trim().split(/\r?\n/).filter(Boolean)
179
- .map(file=>path.resolve(file));
180
- if(!files.length||files.some(file=>!within(realObservedPath(file),root)))
181
- fail('typescript-input-outside-repository');
182
- const facts={schema:1,root:hash(root),script:scriptName,
183
- configs:[...configReads].sort().map(file=>[hash(file),digestFile(file)]),
184
- files:[...new Set(files)].sort().map(file=>[hash(file),digestFile(file)]),
185
- ...contextFacts};
186
- probeResult(hash(JSON.stringify(facts)),contextHash,captured?.queries);
187
- }catch(e){console.error(`VSTATE_REASON:${e.code&&/^[a-z-]+$/.test(e.code)?e.code:
188
- 'typescript-contract-unavailable'}`);process.exitCode=2;}
2
+ // Retired npm-launcher interpretation. Do not return even a context-only digest:
3
+ // old receipts and cached compiler certificates cannot qualify this obligation.
4
+ console.error('VSTATE_REASON:npm-launcher-unobserved-inputs');
5
+ process.exitCode=2;
@@ -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
+ }
@@ -1,3 +1,4 @@
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';
@@ -6,6 +7,25 @@ import {realObservedPath} from './path-identity.mjs';
6
7
  // The read-only runtime probe separately checks the effective TypeScript file
7
8
  // list, config graph, npm toolchain and caller context on every sync/run.
8
9
  export function qualifyTypeScript(root,selected,discovered={}) {
10
+ if(selected.qualification===directQualification){
11
+ const issues=[];
12
+ try{
13
+ const compiler=directProject(root,selected.script);
14
+ if(selected.command?.length!==3||selected.command[0]!==process.execPath||
15
+ selected.command[1]!==compiler.entry||selected.command[2]!=='--noEmit'||
16
+ selected.cwd&&selected.cwd!=='.')issues.push('direct compiler invocation changed');
17
+ }catch(e){issues.push(e.code||'direct compiler prerequisites unavailable');}
18
+ if(!selected.inputs?.includes('**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json,jsonc}')||
19
+ !selected.inputs?.includes('package.json')||!selected.inputs?.includes('tsconfig.json')||
20
+ !selected.installedInputs?.includes('node_modules/**'))issues.push('direct compiler source/config/installed inputs incomplete');
21
+ if(!selected.environment?.executableIdentity||
22
+ !directEnvironment.variables.every(key=>selected.environment?.variables?.includes(key))||
23
+ !directEnvironment.prefixes.every(key=>selected.environment?.prefixes?.includes(key)))
24
+ issues.push('direct compiler execution context incomplete');
25
+ if(selected.probes?.length!==1||!selected.probes?.some(argv=>path.basename(argv[1]||'')==='direct-typescript-probe.mjs'&&
26
+ argv[0]===process.execPath&&argv[2]===root&&argv[3]===selected.script))issues.push('direct compiler probe missing');
27
+ return {qualified:issues.length===0,issues};
28
+ }
9
29
  if(selected.qualification==='pnpm-tsc-v1'){
10
30
  const issues=[];
11
31
  const workspace=selected.workspace||'.';
@@ -50,47 +70,7 @@ export function qualifyTypeScript(root,selected,discovered={}) {
50
70
  return {qualified:issues.length===0,issues};
51
71
  }
52
72
  if(selected.qualification!=='typescript-noemit-v1')return null;
53
- const issues=[];
54
- let pkg;
55
- try{pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));}
56
- catch{issues.push('package manifest unavailable');}
57
- if(!selected.script||pkg?.scripts?.[selected.script]?.trim()!=='tsc --noEmit'||
58
- pkg?.scripts?.['pre'+selected.script]||pkg?.scripts?.['post'+selected.script])
59
- issues.push('typecheck script is not the supported standalone tsc --noEmit invocation');
60
- if(selected.command?.length!==3||selected.command[1]!=='run'||
61
- selected.command[2]!==selected.script)
62
- issues.push('package-manager invocation differs from the discovered script');
63
- try{const npm=fs.realpathSync(selected.command[0]);
64
- const supported=process.platform==='win32'?path.basename(npm).toLowerCase()==='npm.cmd'&&
65
- fs.existsSync(path.join(path.dirname(npm),'node_modules','npm','bin','npm-cli.js')):
66
- path.basename(npm)==='npm-cli.js'&&
67
- path.basename(path.dirname(path.dirname(npm)))==='npm';
68
- if(!supported)
69
- issues.push('npm executable layout is not supported');}
70
- catch{issues.push('npm executable unavailable');}
71
- if(!fs.existsSync(path.join(root,'tsconfig.json')))
72
- issues.push('tsconfig.json is missing');
73
- if(!fs.existsSync(path.join(root,'node_modules','typescript','package.json'))||
74
- !fs.existsSync(path.join(root,'node_modules','.bin',
75
- process.platform==='win32'?'tsc.cmd':'tsc')))
76
- issues.push('local TypeScript installation is missing');
77
- if(!selected.inputs?.includes('**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json,jsonc}')||
78
- !selected.inputs?.includes('package.json')||
79
- !selected.inputs?.includes('tsconfig.json'))
80
- issues.push('TypeScript source/config file-set declaration is incomplete');
81
- if(!selected.installedInputs?.includes('node_modules/**'))
82
- issues.push('installed package contents and membership are not declared');
83
- if(!selected.environment?.executableIdentity||
84
- !['node','npm'].every(name=>selected.environment?.pathExecutables?.includes(name))||
85
- !['NODE_OPTIONS','NODE_PATH','HOME','BASH_ENV','ENV'].every(name=>
86
- selected.environment?.variables?.includes(name))||
87
- !['npm_config_','DYLD_','TSGO_'].every(name=>
88
- selected.environment?.prefixes?.includes(name)))
89
- issues.push('caller toolchain or execution context declaration is incomplete');
90
- if(!selected.probes?.some(argv=>path.basename(argv[1]||'')==='typescript-contract-probe.mjs'&&
91
- argv[2]&&fs.existsSync(argv[2])&&
92
- realObservedPath(argv[2])===root&&argv[3]===selected.script&&
93
- argv[4]===selected.command[0]))
94
- issues.push('TypeScript input and toolchain probe is missing');
95
- return {qualified:issues.length===0,issues};
73
+ // Retired automatic launcher contract: npm's notifier consumes unobserved
74
+ // remote/cache/time state. A successful execution is still an outcome.
75
+ return {qualified:false,issues:['npm launcher applicability is unqualified: update notification uses unobserved remote/cache/time inputs; run the npm script when needed']};
96
76
  }