@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 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; workspace
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. It recognizes canonical
19
- `tsc --noEmit` on local npm installations as a candidate for automatic
20
- qualification. Before CURRENT is possible, a read-only probe checks the
21
- TypeScript configuration and effective file list, local compiler, installed
22
- contents, npm/Node identity, and relevant caller context. Unsupported plugins,
23
- preloads, config graphs, or outside-root inputs leave the check UNVERIFIED with
24
- an explanation. A discovered script can still be run and recorded in that
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@redue/cli",
3
3
  "private": false,
4
- "version": "0.1.0-alpha.2",
4
+ "version": "0.1.0-alpha.4",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "tag": "alpha"
@@ -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 {const value=JSON.parse(fs.readFileSync(path.join(state,'status.json')));
12
- const checks=(value.checks||[]).map(row=>({...row,freshness:'UNVERIFIED',
13
- reason,reuse_eligible:false,declared_inputs_match:null}));
14
- return {...value,state:'unverified',current:0,stale:0,failed:0,
15
- unverified:Math.max(1,checks.length),checks,
16
- observation:{...value.observation,healthy:false,reason}};
17
- }catch{return unknown(reason,checks);}
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{/* next read remains conservative */}
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 {receipts=readReceipts(state).checks;receiptStateError=null;}
81
- catch(e){receipts={};receiptStateError=e.message;event('receipt_state_unavailable',
82
- {classification:e.name||'Error'});}
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,
@@ -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:'ready',reason:'canonical local tsc --noEmit; checked again before reuse',
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.level==='ready')classification={level:'recording',
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&&
@@ -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.schema!==STATE_SCHEMA||!value.checks||typeof value.checks!=='object')
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
- // 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;
@@ -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
- 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};
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
  }