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

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