@redue/cli 0.1.0-alpha.10 → 0.1.0-alpha.12

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/src/cli.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
3
  import {directQualification,directInterpretation,observedNpmContext} from './direct-typescript.mjs';
4
- import {directRunCertificateAllowed,validateDirectRunCertificate} from './direct-run-certificate.mjs';
4
+ import {directRunCertificateAllowed,directRunCertificate,directRunRecordable,directRunRecording,probeQuestions,validateRunRecord} from './direct-run-certificate.mjs';
5
+ import {runFactChange} from './typescript-list.mjs';
5
6
  import path from 'node:path';
6
7
  import net from 'node:net';
7
8
  import os from 'node:os';
@@ -225,7 +226,7 @@ async function execute() {
225
226
  timing('cli.start_checkpoint',startSnapshotStarted,{available:before?.snapshots?.[name]?1:0});
226
227
  // Missing discovery metadata can recover through a full probe. A missing
227
228
  // healthy start boundary cannot: freeze this decision before any recovery.
228
- // Only the reviewed macOS direct recipe enters this qualification shortcut.
229
+ // Only the reviewed direct recipe on macOS and Linux enters this shortcut.
229
230
  const unobservedDirectRun=callerCertificate&&
230
231
  before?.snapshots?.[name]?.observationHealthy!==true;
231
232
  if(unobservedDirectRun){
@@ -235,7 +236,22 @@ async function execute() {
235
236
  await testRunBoundary('after-start-checkpoint');
236
237
  // Start the observed interval before caller validation, not after it. Any
237
238
  // intervening edit (even restored before launch) remains disqualifying.
238
- if(callerCertificate&&!unobservedDirectRun)probeBefore=validateDirectRunCertificate(plan,selected,before?.snapshots?.[name])??probeHash(probePlan);
239
+ // With a certificate, the compile records its own answers instead of two probes.
240
+ // Without one after an edit, the recording can become the certificate at the
241
+ // end checkpoint; that checkpoint probes in full if it cannot.
242
+ let certificate=null,recording=null;
243
+ if(callerCertificate&&!unobservedDirectRun){
244
+ certificate=directRunCertificate(plan,selected,before?.snapshots?.[name],root);
245
+ if(!certificate)recording=directRunRecording(plan,selected,before?.snapshots?.[name],root);
246
+ probeBefore=certificate?.value??(recording?null:probeHash(probePlan));
247
+ }
248
+ // The record arrives on fd 3, so the compiler's environment is unchanged.
249
+ // Every direct run is recorded: even one checked by full probes needs its
250
+ // lookups outside the certificate, which no probe makes.
251
+ const recorded=directRunRecordable(selected)&&!unobservedDirectRun;
252
+ const launched=recorded?[command[0],'--require',path.join(here,'run-recorder.cjs'),
253
+ ...command.slice(1)]:command;
254
+ const record=[];
239
255
  // A Windows full reconciliation may finish while queued watcher callbacks
240
256
  // are still arriving. Fold callbacks delivered before process launch into
241
257
  // the starting checkpoint; callbacks after launch still disqualify reuse.
@@ -256,10 +272,11 @@ async function execute() {
256
272
  if(requestedSignal){resolve({status:'interrupted',exitCode:null,signal:requestedSignal});return;}
257
273
  let child,spawnError=null;
258
274
  try{runLock.phase('launching');
259
- const launch=windowsLaunch(command);
275
+ const launch=windowsLaunch(launched);
260
276
  child=spawn(launch.file,launch.args,{cwd:plan.cwd,
261
- stdio:'inherit',env:process.env,detached:process.platform==='linux',
262
- ...launch.options});}
277
+ stdio:recorded?['inherit','inherit','inherit','pipe']:'inherit',env:process.env,
278
+ detached:process.platform==='linux',...launch.options});
279
+ child.stdio[3]?.on('data',chunk=>record.push(chunk));}
263
280
  catch(e){resolve({status:'start_failed',exitCode:null,signal:null,errorCode:e.code||e.name});return;}
264
281
  activeChild=child;runLock.phase('running',child.pid??null);
265
282
  mark('cli.child_started',{childPid:child.pid});
@@ -293,18 +310,52 @@ async function execute() {
293
310
  const target={source:'direct-command',status:invocation.status==='exited'?'direct_executed':'unknown'};
294
311
  const afterContextStarted=performance.now(),environmentAfter=contextHashes()[name];
295
312
  timing('cli.context_after',afterContextStarted);
296
- const probeAfter=unobservedDirectRun?null:((callerCertificate?validateDirectRunCertificate(plan,selected,before?.snapshots?.[name]):null)??probeHash(probePlan));
313
+ let probeAfter=null;
314
+ const recordText=Buffer.concat(record).toString('utf8');
315
+ // Lookups the certificate does not cover, kept with the receipt.
316
+ let runFacts=[];
317
+ if(certificate){
318
+ const issue=validateRunRecord(recordText,certificate,runFacts);
319
+ if(issue)captureIssues.push(`compiler inputs differ from discovery certificate (${issue})`);
320
+ probeAfter=certificate.value;
321
+ }else if(!unobservedDirectRun&&!recording)probeAfter=probeHash(probePlan);
322
+ // The daemon reads the recording from owned state; control requests are small.
323
+ const recordFile=recording&&path.join(state,`run-record-${recording}.json`);
324
+ if(recordFile)try{fs.writeFileSync(recordFile,recordText,{mode:0o600});}catch{}
297
325
  const endSnapshotStarted=performance.now();
298
326
  if(!unobservedDirectRun){
299
- try{after=await call({action:'snapshot',name,contextHashes:contextHashes()},captureTimeout);}
327
+ try{after=await call({action:'snapshot',name,contextHashes:contextHashes(),
328
+ ...(recording?{runCertificate:1,adopt:recording}:recorded&&!certificate?{runCertificate:1,runFacts:1}:{})},captureTimeout);}
300
329
  catch{captureIssues.push('end observation unavailable');}
330
+ finally{if(recordFile)fs.rmSync(recordFile,{force:true});}
301
331
  timing('cli.end_checkpoint',endSnapshotStarted,{available:after?.snapshots?.[name]?1:0});
302
332
  }
303
333
  const end=after?.snapshots?.[name];
334
+ // The end certificate, recorded or probed, must cover what this compile received.
335
+ // With no input event, gap or content change in between, it also holds at the start.
336
+ // A run checked by full probes is compared with it too, for its run facts.
337
+ if(recorded&&!certificate){
338
+ const endCertificate=directRunCertificate(plan,selected,end,root),
339
+ questions=endCertificate||(!recording&&probeQuestions(plan,selected,end,root));
340
+ if(!questions)captureIssues.push('no discovery certificate at the end checkpoint');
341
+ else{
342
+ const issue=validateRunRecord(recordText,questions,runFacts=[]);
343
+ if(issue)captureIssues.push(`compiler inputs differ from discovery certificate (${issue})`);
344
+ if(recording)probeBefore=probeAfter=endCertificate.value;
345
+ }
346
+ }
347
+ // The receipt holds only run facts that still answer as they did during the run.
348
+ runFacts.sort();
349
+ if(runFacts.length>10000)captureIssues.push('too many lookups outside the discovery certificate');
350
+ else{const changed=runFactChange(runFacts);
351
+ if(changed)captureIssues.push(`a lookup outside the discovery certificate changed during execution: ${changed}`);}
352
+ if(recording&&snap?.observationGaps!==end?.observationGaps)
353
+ captureIssues.push('observation gap during execution');
304
354
  if(!unobservedDirectRun&&(!end||!end.observationHealthy))captureIssues.push('end checkpoint unavailable');
305
355
  if(JSON.stringify(environmentBefore)!==JSON.stringify(environmentAfter))
306
356
  captureIssues.push('declared environment changed during execution');
307
- if(!unobservedDirectRun&&(!probeBefore||!probeAfter||probeBefore!==snap?.probeHash||probeAfter!==end?.probeHash))
357
+ if(!unobservedDirectRun&&(!probeBefore||!probeAfter||probeBefore!==(recording?end:snap)?.probeHash||
358
+ probeAfter!==end?.probeHash))
308
359
  captureIssues.push('declared state probe unavailable or changed');
309
360
  if(before?.observation?.generation!==after?.observation?.generation)
310
361
  captureIssues.push('observer generation changed during execution');
@@ -312,10 +363,14 @@ async function execute() {
312
363
  captureIssues.push('input plan rebuilt during execution');
313
364
  if(snap?.inputEventSerial!==end?.inputEventSerial)
314
365
  captureIssues.push('declared input event occurred during execution');
315
- if(!snap||!end||snap.planId!==end.planId||snap.fingerprint!==end.fingerprint||
366
+ // A recorded certificate changes only the probe part of the fingerprint.
367
+ if(!snap||!end||snap.planId!==end.planId||(recording?snap.inputKey!==end.inputKey:snap.fingerprint!==end.fingerprint)||
316
368
  snap.revision!==end.revision)captureIssues.push('declared inputs changed or were not observed');
317
369
  const stable=captureIssues.length===0;
318
- const receipt={result,invocation,target,stable,fingerprintVersion:1,
370
+ // A recorded run's receipt carries its run facts, even none; observers that
371
+ // predate run facts do not reuse version 2.
372
+ const receipt={result,invocation,target,stable,fingerprintVersion:recorded?2:1,
373
+ ...(recorded?{runFacts}:{}),
319
374
  resolutionLinks:plan.resolutionLinks||[],
320
375
  coverage:{qualified:plan.unresolved.length===0,unresolved:plan.unresolved},
321
376
  checkpoint:{status:stable?'stable':'unavailable_or_changed',issues:captureIssues,
@@ -323,7 +378,7 @@ async function execute() {
323
378
  ...(process.env.REDUE_PUBLIC_INPUT_TRACE==='1'?{
324
379
  startInputEventSerial:snap?.inputEventSerial??null,
325
380
  endInputEventSerial:end?.inputEventSerial??null}: {})},
326
- planId:snap?.planId??plan.id,fingerprint:snap?.fingerprint??null,
381
+ planId:snap?.planId??plan.id,fingerprint:(recording?end:snap)?.fingerprint??null,
327
382
  provider:plan.provider,environmentHashes:environmentBefore,
328
383
  ...(observedContext?{observedContext,verificationRecipe:directInterpretation}:{}),
329
384
  files:snap?.files??null,observerGeneration:before?.observation?.generation??null,