@redue/cli 0.1.0-alpha.2 → 0.1.0-alpha.4
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.
- package/CHANGELOG.md +21 -0
- package/README.md +9 -1
- package/bin/redue.mjs +1 -0
- package/docs/user/getting-started.md +9 -1
- package/docs/user/npm-launcher-notice.md +42 -0
- package/docs/user/receipt-selection-notice.md +60 -0
- package/docs/user/supported-projects.md +7 -8
- package/package.json +1 -1
- package/src/cached-state.mjs +22 -7
- package/src/cli.mjs +14 -3
- package/src/daemon.mjs +20 -5
- package/src/node-onboarding.mjs +2 -2
- package/src/state-store.mjs +37 -1
- package/src/typescript-contract-probe.mjs +4 -187
- package/src/typescript-qualification.mjs +3 -43
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.4 — 2026-10-07
|
|
4
|
+
|
|
5
|
+
- Withdraw automatic npm-launcher applicability because update notifications consume
|
|
6
|
+
unobserved remote/cache/time state. Npm scripts still run and record outcomes.
|
|
7
|
+
- Preserve historical PASS/FAIL and require stop/start when upgrading an observer.
|
|
8
|
+
Do not delete receipts. See the [maintainer notice](docs/user/npm-launcher-notice.md).
|
|
9
|
+
- Supported Yarn/pnpm and explicit reviewed contracts are unchanged. No agent
|
|
10
|
+
activation or npm direct-compiler recipe is included in this correction.
|
|
11
|
+
|
|
12
|
+
## 0.1.0-alpha.3 — 2026-10-06
|
|
13
|
+
|
|
14
|
+
Install: `npm install --global @redue/cli@alpha`.
|
|
15
|
+
|
|
16
|
+
- Fix selected execution outcomes remaining hidden behind an older observer/cache
|
|
17
|
+
after a missed reload notification. A newer failure must not leave an older
|
|
18
|
+
passing receipt eligible for reuse.
|
|
19
|
+
- Withhold applicability during incomplete receipt persistence, unreadable or
|
|
20
|
+
changing selection, and client connections to an older observer.
|
|
21
|
+
- Preserve immutable run history. Upgrade requires an observer stop/start;
|
|
22
|
+
receipts need not be deleted. See the [maintainer notice](docs/user/receipt-selection-notice.md).
|
|
23
|
+
|
|
3
24
|
## 0.1.0-alpha.2 — 2026-10-06
|
|
4
25
|
|
|
5
26
|
Install: `npm install --global @redue/cli@alpha`.
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# REDUE
|
|
2
2
|
|
|
3
|
+
Maintainer notice: alpha.4 withdraws automatic npm-launcher applicability.
|
|
4
|
+
Npm commands still execute and record historical outcomes, but remain UNVERIFIED.
|
|
5
|
+
[Why this changed and how to upgrade without deleting receipts](docs/user/npm-launcher-notice.md).
|
|
6
|
+
Alpha.2 users should also review the [receipt correction](docs/user/receipt-selection-notice.md).
|
|
7
|
+
|
|
3
8
|
**Know what still holds. Redo what's due.**
|
|
4
9
|
|
|
5
10
|
You ran the checks. Then the code changed—or a new coding agent started with no
|
|
@@ -9,6 +14,8 @@ REDUE keeps persistent verification state for your codebase so developers and
|
|
|
9
14
|
coding agents can see which previous checks still apply to the code they have now.
|
|
10
15
|
It records evidence, not proofs of correctness.
|
|
11
16
|
|
|
17
|
+
For a qualified supported Yarn/pnpm check (ordinary npm scripts are recording-only):
|
|
18
|
+
|
|
12
19
|
```text
|
|
13
20
|
$ redue status --sync
|
|
14
21
|
✓ typecheck CURRENT / PASS
|
|
@@ -82,7 +89,8 @@ has been verified.
|
|
|
82
89
|
- macOS, Linux, and Windows on supported local filesystems; Node 22+.
|
|
83
90
|
- Linux also requires Python 3 for observation. No WSL or Git Bash needed on Windows.
|
|
84
91
|
- npm, pinned Yarn `node-modules`, and pinned pnpm 12 isolated/hoisted layouts.
|
|
85
|
-
- Automatic qualification for supported TypeScript invocations;
|
|
92
|
+
- Automatic qualification for the supported Yarn/pnpm TypeScript invocations; npm
|
|
93
|
+
launcher checks are recording-only in alpha.4. Workspace
|
|
86
94
|
boundaries follow the files actually consumed, including built declarations.
|
|
87
95
|
- Existing test/lint/build and arbitrary commands can record results while
|
|
88
96
|
their applicability remains unqualified.
|
package/bin/redue.mjs
CHANGED
|
@@ -266,6 +266,7 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
266
266
|
coverageReasons:check.coverageReasons||Object.fromEntries(
|
|
267
267
|
categories.map(category=>[category,reason])),
|
|
268
268
|
qualification:check.qualification||null,script:check.script||null,
|
|
269
|
+
...(npmAutomatic?{qualificationInterpretation:'npm-launcher-withheld@1'}:{}),
|
|
269
270
|
workspace:check.workspace||null,kind:check.kind||null};
|
|
270
271
|
});
|
|
271
272
|
const runtime={schema:1,provider:'declared-project@1',root,state,checks};
|
|
@@ -57,6 +57,12 @@ prefix. Uninstalling the package does not remove local verification evidence;
|
|
|
57
57
|
`redue remove-state` is a separate, explicit action. Alpha.1 remains available as
|
|
58
58
|
historical software and is superseded for package identity, not a runtime defect.
|
|
59
59
|
|
|
60
|
+
## Upgrading to alpha.4
|
|
61
|
+
|
|
62
|
+
Stop every observer using the installation before upgrading, then restart each
|
|
63
|
+
with the same config/state selection. See [the npm launcher notice](npm-launcher-notice.md)
|
|
64
|
+
for exact commands. Do not remove state or delete receipts.
|
|
65
|
+
|
|
60
66
|
## First project
|
|
61
67
|
|
|
62
68
|
From the project root:
|
|
@@ -74,7 +80,9 @@ Use a check actually printed by `init`. For a workspace:
|
|
|
74
80
|
`redue init --workspace @example/package --check typecheck`.
|
|
75
81
|
Initialization previews detected commands and qualification confidence before writing
|
|
76
82
|
`redue.config.json`. An existing config is never overwritten. No checks are run and
|
|
77
|
-
no dependencies installed. A missing/unsupported boundary is explained.
|
|
83
|
+
no dependencies installed. A missing/unsupported boundary is explained. Npm-launcher typechecks are
|
|
84
|
+
recording-only in alpha.4; a passed execution is historical PASS with UNVERIFIED
|
|
85
|
+
applicability, not reusable CURRENT.
|
|
78
86
|
|
|
79
87
|
“Ready to qualify” means REDUE has a supported recipe whose prerequisites will be
|
|
80
88
|
validated. It is not a receipt. “Recording-only” can record PASS/FAIL without claiming
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# npm launcher applicability correction — alpha.4
|
|
2
|
+
|
|
3
|
+
Alpha.4 withdraws automatic applicability for npm-backed TypeScript checks using
|
|
4
|
+
`typescript-noemit-v1`. Alpha.3 was demonstrated to admit an npm invocation whose
|
|
5
|
+
update notification consumes unobserved remote responses, cache and time. Those
|
|
6
|
+
inputs can change the outer npm result without changing the tracked project or
|
|
7
|
+
caller identity. The controlled reproduction does not establish a public registry
|
|
8
|
+
malfunction, affected-user count, or compiler completion in the failing npm case.
|
|
9
|
+
|
|
10
|
+
Npm commands still execute exactly as configured through `redue run CHECK`.
|
|
11
|
+
They still record PASS/FAIL. Previous PASS remains historical PASS; applicability
|
|
12
|
+
is now UNVERIFIED, including after synchronization or a successful new npm run.
|
|
13
|
+
This is a correction to the claimed boundary, not evidence that the code failed.
|
|
14
|
+
Supported Yarn/pnpm and independently reviewed explicit contracts are unchanged.
|
|
15
|
+
|
|
16
|
+
Until upgraded, do not rely solely on automatic npm CURRENT to skip the npm
|
|
17
|
+
invocation. A different verification recipe requires explicit selection and fresh
|
|
18
|
+
evidence; it cannot reinterpret an old npm receipt. Alpha.4 does not ship the
|
|
19
|
+
experimental npm direct-compiler recipe or agent activation.
|
|
20
|
+
|
|
21
|
+
## Upgrade without losing history
|
|
22
|
+
|
|
23
|
+
First stop **every observer using this installation**, using each original config
|
|
24
|
+
and any explicit state directory. Then upgrade that same installation and restart
|
|
25
|
+
each observer with the same selections:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
redue --config /path/to/project/redue.config.json stop
|
|
29
|
+
npm install --global @redue/cli@0.1.0-alpha.4
|
|
30
|
+
redue --config /path/to/project/redue.config.json start
|
|
31
|
+
redue --config /path/to/project/redue.config.json status --sync
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Repeat the stop/start commands for each project. If you originally used
|
|
35
|
+
`--state-dir`, supply the identical directory on every REDUE command. If installed
|
|
36
|
+
with a custom npm `--prefix`, supply the same prefix during installation. On Windows
|
|
37
|
+
use `npm.cmd`/`redue.cmd` if required by existing PowerShell policy; do not change it.
|
|
38
|
+
|
|
39
|
+
**Do not delete receipts or use `remove-state` as an upgrade step.** An upgraded
|
|
40
|
+
client refuses an obsolete running observer interpretation and asks for stop/start.
|
|
41
|
+
Restart reconciles existing state and preserves immutable historical runs. This
|
|
42
|
+
correction does not silently change your project config or configured command.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Receipt selection correction in alpha.3
|
|
2
|
+
|
|
3
|
+
**Known affected release: `@redue/cli@0.1.0-alpha.2`. Fixed in
|
|
4
|
+
`@redue/cli@0.1.0-alpha.3`.** No exploit or affected-user count is established.
|
|
5
|
+
REDUE remains an alpha.
|
|
6
|
+
|
|
7
|
+
If a wrapper persisted a newer outcome but its observer reload notification
|
|
8
|
+
failed, the observer could continue selecting an older passing receipt. Cached
|
|
9
|
+
and synchronized status could then present that older result as CURRENT and
|
|
10
|
+
eligible for reuse. A separate reproduced write-failure window left a new
|
|
11
|
+
immutable outcome recorded while replacing the selected receipt failed.
|
|
12
|
+
|
|
13
|
+
Until upgraded, do not rely solely on affected CURRENT output to skip checks.
|
|
14
|
+
Run verification when needed and inspect its actual outcome; a successful status
|
|
15
|
+
command is not proof that the latest execution passed.
|
|
16
|
+
|
|
17
|
+
## What changes
|
|
18
|
+
|
|
19
|
+
The observer refreshes REDUE-owned receipt selection before publishing status.
|
|
20
|
+
Clients check that a response represents the current readable selection. Missing,
|
|
21
|
+
unreadable, malformed, incompatible, or replaced selection cannot authorize
|
|
22
|
+
reuse. A new client connecting to an alpha.2 observer returns UNVERIFIED and
|
|
23
|
+
asks for a restart, even when the old response says CURRENT.
|
|
24
|
+
|
|
25
|
+
A small owned pending-update record also withholds applicability after an
|
|
26
|
+
incomplete receipt write. Successful recording clears only that check's pending
|
|
27
|
+
entry. It does not discard another check's incomplete outcome or automatically
|
|
28
|
+
promote an orphaned journal entry. Inspect the failed run and storage error, fix
|
|
29
|
+
the storage problem, then record that check again. An unreadable/malformed pending
|
|
30
|
+
record requires operator inspection; do not delete receipts to obtain green status.
|
|
31
|
+
|
|
32
|
+
Historical PASS/FAIL and immutable `runs-v1` files remain intact. Stable applicable
|
|
33
|
+
FAIL remains a failure and is never reuse-eligible. This guard tracks owned
|
|
34
|
+
receipt replacement; it does not replace input-content validation or promise an
|
|
35
|
+
atomic decision against concurrent edits/writes.
|
|
36
|
+
|
|
37
|
+
## Upgrade and restart
|
|
38
|
+
|
|
39
|
+
From each initialized project, stop its observer before replacing the installation.
|
|
40
|
+
After all observers for that installation have stopped, upgrade it once, then
|
|
41
|
+
restart each observer with its existing configuration and state selection:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
redue stop
|
|
45
|
+
npm install --global @redue/cli@0.1.0-alpha.3
|
|
46
|
+
redue --version
|
|
47
|
+
redue start
|
|
48
|
+
redue status --sync --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use the same npm `--prefix`, project `--config`, and `--state-dir` you already use,
|
|
52
|
+
if customized. On Windows use `npm.cmd`/`redue.cmd` where required by the existing
|
|
53
|
+
shell policy. Stop all observers belonging to the installation before replacing
|
|
54
|
+
it, then start each with the upgraded binary. For an independently installed
|
|
55
|
+
candidate, its `stop` command can stop the older observer in the same explicitly
|
|
56
|
+
selected owned state. Do not run `remove-state`; receipts need not be deleted.
|
|
57
|
+
|
|
58
|
+
Restart preserves historical receipts and reconciles inputs. It does not promise
|
|
59
|
+
CURRENT: a newer failure, unstable execution, incomplete contract, changed input,
|
|
60
|
+
or observation uncertainty remains visible under the normal rules.
|
|
@@ -15,14 +15,13 @@ At the repository root, `init` looks for existing typecheck, test, lint, and
|
|
|
15
15
|
build scripts. It prefers familiar script names, then unique recognizable
|
|
16
16
|
`tsc`, Vitest/Jest, ESLint, or Vite commands under other names. Multiple
|
|
17
17
|
matches are reported as ambiguous rather than guessed. It never creates a new
|
|
18
|
-
project script.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
state. Root and package-level npm/Yarn workspace scripts are discovered from
|
|
18
|
+
project script. Canonical `tsc --noEmit` npm scripts are discovered and recorded, but their
|
|
19
|
+
automatic applicability is withdrawn in alpha.4: npm's update notification uses
|
|
20
|
+
remote/cache/time inputs outside the observed contract. Even a successful run
|
|
21
|
+
remains UNVERIFIED/PASS. Historical receipts are retained. See the
|
|
22
|
+
[maintainer notice](npm-launcher-notice.md). A separately selected verification
|
|
23
|
+
obligation would require an explicit configuration and fresh evidence; alpha.4
|
|
24
|
+
does not provide the unreleased npm direct-compiler recipe. Root and package-level npm/Yarn workspace scripts are discovered from
|
|
26
25
|
`package.json` workspace declarations. Pinned Yarn 4 `node-modules`/classic
|
|
27
26
|
workspaces with a standalone `tsc -p .` script, local TypeScript, a conventional
|
|
28
27
|
`src` include, and Node module resolution can qualify automatically. For one
|
package/package.json
CHANGED
package/src/cached-state.mjs
CHANGED
|
@@ -2,22 +2,37 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import {processAlive} from './process-liveness.mjs';
|
|
5
|
+
import {readReceipts,readReceiptSnapshot,receiptRevision} from './state-store.mjs';
|
|
5
6
|
const unknown=(reason,checks)=>({schema:1,state:'unverified',current:0,stale:0,failed:0,observed_failed:0,
|
|
6
7
|
unverified:checks.length,
|
|
7
8
|
checks:checks.map(c=>({name:c.name,result:null,freshness:'UNVERIFIED',reason,
|
|
8
9
|
invocation:null,changed_inputs:[],reuse_eligible:false})),
|
|
9
10
|
observation:{healthy:false,reason}});
|
|
10
11
|
export function unavailableCached(state,reason,checks=[]) {
|
|
11
|
-
try
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
let value;try{value=JSON.parse(fs.readFileSync(path.join(state,'status.json')));}
|
|
13
|
+
catch{value=unknown(reason,checks);}
|
|
14
|
+
if(!value||!Array.isArray(value.checks))value=unknown(reason,checks);
|
|
15
|
+
let receipts=null,revision=null;
|
|
16
|
+
try{const snapshot=readReceiptSnapshot(state);receipts=snapshot.value.checks;revision=snapshot.revision;}
|
|
17
|
+
catch(error){reason+=`; latest receipt unavailable; prior outcome only: ${error.message}`;
|
|
18
|
+
try{receipts=readReceipts(state).checks;}catch{/* retain cached historical rows */}}
|
|
19
|
+
const rows=(value.checks||[]).map(row=>{
|
|
20
|
+
const latest=receipts?.[row.name];
|
|
21
|
+
return {...row,...(receipts?{result:latest?.result||null,invocation:latest?.invocation||null,
|
|
22
|
+
target_provenance:latest?.target||null,coverage_at_run:latest?.coverage||null}:{}),
|
|
23
|
+
freshness:'UNVERIFIED',reason,reuse_eligible:false,declared_inputs_match:null};
|
|
24
|
+
});
|
|
25
|
+
return {...value,receipt_revision:revision,state:'unverified',current:0,stale:0,failed:0,
|
|
26
|
+
observed_failed:rows.filter(row=>row.result==='FAIL').length,
|
|
27
|
+
unverified:Math.max(1,rows.length),checks:rows,
|
|
28
|
+
observation:{...value.observation,healthy:false,reason}};
|
|
18
29
|
}
|
|
19
30
|
export function readCachedStatus(state,checks=[]) {
|
|
20
31
|
try{const value=JSON.parse(fs.readFileSync(path.join(state,'status.json')));
|
|
32
|
+
if(typeof value.receipt_revision!=='string')
|
|
33
|
+
return unavailableCached(state,'receipt selection is unconfirmed; synchronize status or restart an older observer',checks);
|
|
34
|
+
if(value.receipt_revision!==receiptRevision(state))
|
|
35
|
+
return unavailableCached(state,'receipt selection changed; synchronize status to evaluate the latest outcome',checks);
|
|
21
36
|
if(Date.now()>=value.updated_at&&Date.now()<=value.expires_at&&
|
|
22
37
|
value.expires_at-value.updated_at<=3000){
|
|
23
38
|
if(processAlive(value.pid))return value;
|
package/src/cli.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import {createHash,randomUUID} from 'node:crypto';
|
|
|
15
15
|
import {assertOwnedStatePlacement} from './owned-state.mjs';
|
|
16
16
|
import {permittedRoots} from './installed-inputs.mjs';
|
|
17
17
|
import {acquireRunLock,inspectRunLock,recoverRunLock} from './run-lock.mjs';
|
|
18
|
-
import {atomicJson,commitReceipt} from './state-store.mjs';
|
|
18
|
+
import {atomicJson,commitReceipt,receiptRevision} from './state-store.mjs';
|
|
19
19
|
import {timing} from './decision-profile.mjs';
|
|
20
20
|
import {readCachedStatus,unavailableCached} from './cached-state.mjs';
|
|
21
21
|
import {renderRun} from './presentation.mjs';
|
|
@@ -284,7 +284,7 @@ async function execute() {
|
|
|
284
284
|
const persistenceStarted=performance.now();
|
|
285
285
|
commitReceipt(state,name,receipt);
|
|
286
286
|
timing('cli.receipt_persistence',persistenceStarted);
|
|
287
|
-
try{await call({action:'reload'});}catch{/*
|
|
287
|
+
try{await call({action:'reload'});}catch{/* receipt revisions invalidate older status even when reload IPC is unavailable */}
|
|
288
288
|
const summary={check:name,result,target:target.status,
|
|
289
289
|
invocation:invocation.status,...(invocation.errorCode?{error_code:invocation.errorCode}:{}),
|
|
290
290
|
stable,coverage_qualified:receipt.coverage.qualified};
|
|
@@ -376,7 +376,18 @@ async function main(){
|
|
|
376
376
|
if(action==='sync'||action==='detail'){
|
|
377
377
|
const started=performance.now();
|
|
378
378
|
let value;try{value=await decisionCall({action:'sync',contextHashes:contextHashes()},
|
|
379
|
-
Number(process.env.VSTATE_SYNC_TIMEOUT_MS||decisionDeadlineMs));
|
|
379
|
+
Number(process.env.VSTATE_SYNC_TIMEOUT_MS||decisionDeadlineMs));
|
|
380
|
+
// A client upgrade can encounter an older running observer. Its successful
|
|
381
|
+
// control response does not establish that it evaluated the latest receipt.
|
|
382
|
+
if(value.receipt_revision===null){
|
|
383
|
+
value=unavailableCached(state,'latest receipt selection unavailable; inspect receipt state before reuse',config.checks);
|
|
384
|
+
}else if(typeof value.receipt_revision!=='string'){
|
|
385
|
+
value=unavailableCached(state,'observer cannot confirm latest receipt selection; stop and start the observer after upgrading',config.checks);
|
|
386
|
+
process.exitCode=2;
|
|
387
|
+
}else if(value.receipt_revision!==receiptRevision(state)){
|
|
388
|
+
value=unavailableCached(state,'receipt selection changed during synchronization; retry synchronized status',config.checks);
|
|
389
|
+
process.exitCode=2;
|
|
390
|
+
}}
|
|
380
391
|
catch(e){value=unavailableCached(state,e.message,config.checks);process.exitCode=2;}
|
|
381
392
|
timing('cli.synchronized_status',started,{available:value.observation?.healthy?1:0});
|
|
382
393
|
if(action==='sync')console.log(JSON.stringify(value));
|
package/src/daemon.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import {sha} from './plan.mjs';
|
|
|
14
14
|
import {InputIndex} from './index.mjs';
|
|
15
15
|
import {permittedRoots} from './installed-inputs.mjs';
|
|
16
16
|
import {assertOwnedStatePlacement} from './owned-state.mjs';
|
|
17
|
-
import {atomicJson,readReceipts} from './state-store.mjs';
|
|
17
|
+
import {atomicJson,readReceipts,readReceiptSnapshot,receiptRevision} from './state-store.mjs';
|
|
18
18
|
import {history} from './observation.mjs';
|
|
19
19
|
import {inputKey} from './decision-validation.mjs';
|
|
20
20
|
import {timing} from './decision-profile.mjs';
|
|
@@ -33,6 +33,7 @@ fs.writeFileSync(path.join(lock,'pid'),String(process.pid),{mode:0o600});
|
|
|
33
33
|
const generation=randomUUID(), startedAt=Date.now();
|
|
34
34
|
let index, receipts={}, watcher, healthy=false, reason='initial reconciliation', stopping=false;
|
|
35
35
|
let receiptStateError=null;
|
|
36
|
+
let receiptStateRevision=null;
|
|
36
37
|
const external=new Map();
|
|
37
38
|
for(const check of config.checks)for(const boundary of permittedRoots(root,check.allowedExternalRoots||[]))
|
|
38
39
|
if(!external.has(boundary))external.set(boundary,{root:boundary,watcher:null,index:null,
|
|
@@ -77,9 +78,20 @@ function event(kind,fields={}) {
|
|
|
77
78
|
repository:root,...fields})+'\n');
|
|
78
79
|
}
|
|
79
80
|
function loadReceipts() {
|
|
80
|
-
try {
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
try {const snapshot=readReceiptSnapshot(state);receipts=snapshot.value.checks;
|
|
82
|
+
receiptStateRevision=snapshot.revision;receiptStateError=null;}
|
|
83
|
+
catch(e){receiptStateRevision=null;
|
|
84
|
+
// A readable selector can still supply explicitly historical outcomes after
|
|
85
|
+
// restart. The failed snapshot guard remains authoritative for applicability.
|
|
86
|
+
try{receipts=readReceipts(state).checks;}catch{/* retain previous historical rows */}
|
|
87
|
+
const next=`latest receipt unavailable; prior outcome only: ${e.message}`;
|
|
88
|
+
if(receiptStateError!==next)event('receipt_state_unavailable',
|
|
89
|
+
{classification:e.name||'Error'});
|
|
90
|
+
receiptStateError=next;}
|
|
91
|
+
}
|
|
92
|
+
function refreshReceipts() {
|
|
93
|
+
try {if(receiptStateRevision===null||receiptRevision(state)!==receiptStateRevision)loadReceipts();}
|
|
94
|
+
catch {loadReceipts();}
|
|
83
95
|
}
|
|
84
96
|
function persistedPlan(bundle) {
|
|
85
97
|
return {schema:1,provider:bundle.provider,root:bundle.root,discoveredAt:bundle.discoveredAt,
|
|
@@ -298,13 +310,16 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
298
310
|
});
|
|
299
311
|
}
|
|
300
312
|
function status(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=false) {
|
|
313
|
+
// Reload notifications accelerate visibility but are not required for it: a
|
|
314
|
+
// wrapper may persist its outcome even when its control connection is denied.
|
|
315
|
+
refreshReceipts();
|
|
301
316
|
const checks=rows(contextHashes,synchronizedInstalled,synchronizedFilesystem), current=checks.filter(r=>r.freshness==='CURRENT'&&r.result==='PASS').length,
|
|
302
317
|
failed=checks.filter(r=>r.freshness==='CURRENT'&&r.result==='FAIL').length,
|
|
303
318
|
observedFailed=checks.filter(r=>r.result==='FAIL').length,
|
|
304
319
|
stale=checks.filter(r=>r.freshness==='STALE').length,
|
|
305
320
|
unverified=checks.filter(r=>r.freshness==='UNVERIFIED').length||(!checks.length?1:0);
|
|
306
321
|
const stateName=failed?'failed':unverified?'unverified':stale?'stale':'current';
|
|
307
|
-
const now=Date.now();return {schema:1,state:stateName,current,failed,observed_failed:observedFailed,
|
|
322
|
+
const now=Date.now();return {schema:1,receipt_revision:receiptStateRevision,state:stateName,current,failed,observed_failed:observedFailed,
|
|
308
323
|
stale,unverified,checks,
|
|
309
324
|
observation:{healthy:healthy&&!planning&&!recovering&&!index?.unavailable,
|
|
310
325
|
generation,observed_at:lastObservation,
|
package/src/node-onboarding.mjs
CHANGED
|
@@ -116,7 +116,7 @@ function classify(kind,scriptName,script,pkg,managerInfo,root) {
|
|
|
116
116
|
managerInfo.name==='npm'&&
|
|
117
117
|
exists(path.join(root,'tsconfig.json'))&&
|
|
118
118
|
exists(path.join(root,'node_modules/typescript/package.json')))
|
|
119
|
-
return {level:'
|
|
119
|
+
return {level:'recording',reason:'npm launcher uses unobserved remote/cache/time inputs; execution can be recorded, but cannot be reused',
|
|
120
120
|
qualification:'typescript-noemit-v1'};
|
|
121
121
|
if(kind==='typecheck')return {level:'recording',reason:
|
|
122
122
|
'TypeScript invocation, installation, or configuration is outside the automatic noEmit contract'};
|
|
@@ -183,7 +183,7 @@ export function discoverNodeProject(root) {
|
|
|
183
183
|
if(!script)continue;
|
|
184
184
|
let classification=observationIssue?{level:'unsupported',reason:observationIssue}:
|
|
185
185
|
classify(kind,script,scripts[script],pkg,pm,root);
|
|
186
|
-
if(workspaces&&classification.
|
|
186
|
+
if(workspaces&&classification.qualification==='typescript-noemit-v1')classification={level:'recording',
|
|
187
187
|
reason:'workspace task and installed-input closure need explicit qualification'};
|
|
188
188
|
const check={name:kind,script,kind,inputs:sourceInputs(root,kind,pm.lockfiles||[])};
|
|
189
189
|
if(pm.name==='pnpm'&&kind==='typecheck'&&!workspaces&&!pm.issue&&
|
package/src/state-store.mjs
CHANGED
|
@@ -19,19 +19,55 @@ export function readReceipts(state){
|
|
|
19
19
|
let value;
|
|
20
20
|
try{value=JSON.parse(fs.readFileSync(path.join(state,'receipts-v1.json'),'utf8'));}
|
|
21
21
|
catch(e){if(e.code==='ENOENT')return {schema:STATE_SCHEMA,checks:{}};throw e;}
|
|
22
|
-
if(value
|
|
22
|
+
if(!value||typeof value!=='object'||Array.isArray(value)||value.schema!==STATE_SCHEMA||
|
|
23
|
+
!value.checks||typeof value.checks!=='object'||Array.isArray(value.checks))
|
|
23
24
|
throw Error(`incompatible receipt state in ${state}; inspect or remove only this vstate state directory`);
|
|
24
25
|
return value;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
// This identity detects replacement of REDUE's owned, atomically written receipt
|
|
29
|
+
// selector. It is not an input-content fingerprint or permission to reuse inputs.
|
|
30
|
+
export function receiptRevision(state){
|
|
31
|
+
// A failed/crashed owned writer may have persisted a new immutable outcome
|
|
32
|
+
// without replacing the selector. Never certify its older selection.
|
|
33
|
+
try {fs.lstatSync(path.join(state,'receipt-selection-pending-v1.json'));
|
|
34
|
+
throw Error('receipt selection update incomplete; inspect the failed run, then record a new execution to recover');
|
|
35
|
+
}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
36
|
+
try {const stat=fs.statSync(path.join(state,'receipts-v1.json'),{bigint:true});
|
|
37
|
+
if(!stat.isFile())throw Error('receipt selector is not a file');
|
|
38
|
+
return JSON.stringify(['dev','ino','size','mtimeNs','ctimeNs'].map(key=>String(stat[key])));
|
|
39
|
+
}catch(error){if(error.code==='ENOENT')return 'absent';throw error;}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readReceiptSnapshot(state){
|
|
43
|
+
const revision=receiptRevision(state),value=readReceipts(state);
|
|
44
|
+
if(receiptRevision(state)!==revision)
|
|
45
|
+
throw Error('receipt selection changed during read; retry status');
|
|
46
|
+
return {revision,value};
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
export function commitReceipt(state,name,receipt){
|
|
28
50
|
const prior=readReceipts(state),runId=receipt.invocation?.runId;
|
|
29
51
|
if(!/^[0-9a-f-]{36}$/.test(runId||''))throw Error('receipt lacks a valid run ID');
|
|
30
52
|
const runs=path.join(state,'runs-v1');
|
|
31
53
|
fs.mkdirSync(runs,{recursive:true,mode:0o700});
|
|
54
|
+
// The caller holds the state run lock. Leave this marker on any write failure;
|
|
55
|
+
// only a later successful owned commit may clear it. No journal scan guesses
|
|
56
|
+
// which incomplete execution should become the selected evidence.
|
|
57
|
+
const pending=path.join(state,'receipt-selection-pending-v1.json');
|
|
58
|
+
let updates={schema:STATE_SCHEMA,checks:{}};
|
|
59
|
+
try {updates=JSON.parse(fs.readFileSync(pending,'utf8'));
|
|
60
|
+
if(updates?.schema!==STATE_SCHEMA||!updates.checks||typeof updates.checks!=='object'||Array.isArray(updates.checks))
|
|
61
|
+
throw Error('incompatible incomplete receipt update; inspect owned state before recording');
|
|
62
|
+
}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
63
|
+
updates.checks[name]={runId};
|
|
64
|
+
atomicJson(pending,updates);
|
|
32
65
|
// One immutable outcome per invocation. Never overwrite an existing run.
|
|
33
66
|
fs.writeFileSync(path.join(runs,runId+'.json'),
|
|
34
67
|
JSON.stringify({schema:STATE_SCHEMA,check:name,receipt}),{flag:'wx',mode:0o600});
|
|
35
68
|
prior.checks[name]=receipt;
|
|
36
69
|
atomicJson(path.join(state,'receipts-v1.json'),prior);
|
|
70
|
+
delete updates.checks[name];
|
|
71
|
+
if(Object.keys(updates.checks).length)atomicJson(pending,updates);
|
|
72
|
+
else fs.unlinkSync(pending);
|
|
37
73
|
}
|
|
@@ -1,188 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
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;
|
|
@@ -50,47 +50,7 @@ export function qualifyTypeScript(root,selected,discovered={}) {
|
|
|
50
50
|
return {qualified:issues.length===0,issues};
|
|
51
51
|
}
|
|
52
52
|
if(selected.qualification!=='typescript-noemit-v1')return null;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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};
|
|
53
|
+
// Retired automatic launcher contract: npm's notifier consumes unobserved
|
|
54
|
+
// remote/cache/time state. A successful execution is still an outcome.
|
|
55
|
+
return {qualified:false,issues:['npm launcher applicability is unqualified: update notification uses unobserved remote/cache/time inputs; run the npm script when needed']};
|
|
96
56
|
}
|