@veraxhq/verax 0.2.0 → 0.2.1
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/package.json +14 -4
- package/src/cli/commands/default.js +244 -86
- package/src/cli/commands/doctor.js +36 -4
- package/src/cli/commands/run.js +253 -69
- package/src/cli/entry.js +5 -5
- package/src/cli/util/detection-engine.js +4 -3
- package/src/cli/util/events.js +76 -0
- package/src/cli/util/expectation-extractor.js +11 -1
- package/src/cli/util/findings-writer.js +1 -0
- package/src/cli/util/observation-engine.js +69 -23
- package/src/cli/util/paths.js +3 -2
- package/src/cli/util/project-discovery.js +20 -0
- package/src/cli/util/redact.js +2 -2
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +12 -1
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/doctor.js +2 -2
- package/src/verax/cli/init.js +1 -1
- package/src/verax/cli/url-safety.js +12 -2
- package/src/verax/cli/wizard.js +13 -2
- package/src/verax/core/budget-engine.js +1 -1
- package/src/verax/core/decision-snapshot.js +2 -2
- package/src/verax/core/determinism-model.js +35 -6
- package/src/verax/core/incremental-store.js +15 -7
- package/src/verax/core/replay-validator.js +4 -4
- package/src/verax/core/replay.js +1 -1
- package/src/verax/core/silence-impact.js +1 -1
- package/src/verax/core/silence-model.js +9 -7
- package/src/verax/detect/comparison.js +8 -3
- package/src/verax/detect/confidence-engine.js +17 -17
- package/src/verax/detect/detection-engine.js +1 -1
- package/src/verax/detect/evidence-index.js +15 -65
- package/src/verax/detect/expectation-model.js +54 -3
- package/src/verax/detect/explanation-helpers.js +1 -1
- package/src/verax/detect/finding-detector.js +2 -2
- package/src/verax/detect/findings-writer.js +9 -16
- package/src/verax/detect/flow-detector.js +4 -4
- package/src/verax/detect/index.js +37 -11
- package/src/verax/detect/interactive-findings.js +3 -4
- package/src/verax/detect/signal-mapper.js +2 -2
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +4 -6
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +15 -3
- package/src/verax/intel/effect-detector.js +1 -1
- package/src/verax/intel/index.js +2 -2
- package/src/verax/intel/route-extractor.js +3 -3
- package/src/verax/intel/vue-navigation-extractor.js +81 -18
- package/src/verax/intel/vue-router-extractor.js +4 -2
- package/src/verax/learn/action-contract-extractor.js +3 -3
- package/src/verax/learn/ast-contract-extractor.js +53 -1
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +28 -14
- package/src/verax/learn/route-extractor.js +1 -1
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +1 -1
- package/src/verax/learn/static-extractor-navigation.js +1 -1
- package/src/verax/learn/static-extractor-validation.js +2 -2
- package/src/verax/learn/static-extractor.js +8 -7
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/browser.js +22 -3
- package/src/verax/observe/console-sensor.js +2 -2
- package/src/verax/observe/expectation-executor.js +2 -1
- package/src/verax/observe/focus-sensor.js +1 -1
- package/src/verax/observe/human-driver.js +29 -10
- package/src/verax/observe/index.js +10 -7
- package/src/verax/observe/interaction-discovery.js +27 -15
- package/src/verax/observe/interaction-runner.js +6 -6
- package/src/verax/observe/loading-sensor.js +6 -0
- package/src/verax/observe/navigation-sensor.js +1 -1
- package/src/verax/observe/settle.js +1 -0
- package/src/verax/observe/state-sensor.js +8 -4
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/traces-writer.js +27 -16
- package/src/verax/observe/ui-signal-sensor.js +7 -0
- package/src/verax/scan-summary-writer.js +5 -2
- package/src/verax/shared/artifact-manager.js +1 -1
- package/src/verax/shared/budget-profiles.js +2 -2
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/config-loader.js +1 -2
- package/src/verax/shared/dynamic-route-utils.js +12 -6
- package/src/verax/shared/retry-policy.js +1 -6
- package/src/verax/shared/root-artifacts.js +1 -1
- package/src/verax/shared/zip-artifacts.js +1 -0
- package/src/verax/validate/context-validator.js +1 -1
- package/src/verax/observe/index.js.backup +0 -1
- package/src/verax/validate/context-validator.js.bak +0 -0
package/src/cli/commands/run.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { resolve
|
|
1
|
+
import { resolve } from 'path';
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname } from 'path';
|
|
5
|
-
import { UsageError, DataError
|
|
5
|
+
import { UsageError, DataError } from '../util/errors.js';
|
|
6
6
|
import { generateRunId } from '../util/run-id.js';
|
|
7
7
|
import { getRunPaths, ensureRunDirectories } from '../util/paths.js';
|
|
8
8
|
import { atomicWriteJson, atomicWriteText } from '../util/atomic-write.js';
|
|
@@ -16,6 +16,7 @@ import { writeObserveJson } from '../util/observe-writer.js';
|
|
|
16
16
|
import { detectFindings } from '../util/detection-engine.js';
|
|
17
17
|
import { writeFindingsJson } from '../util/findings-writer.js';
|
|
18
18
|
import { writeSummaryJson } from '../util/summary-writer.js';
|
|
19
|
+
import { computeRuntimeBudget, withTimeout } from '../util/runtime-budget.js';
|
|
19
20
|
|
|
20
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
22
|
const __dirname = dirname(__filename);
|
|
@@ -61,6 +62,7 @@ export async function runCommand(options) {
|
|
|
61
62
|
|
|
62
63
|
// Setup event handlers
|
|
63
64
|
if (json) {
|
|
65
|
+
// In JSON mode, emit events as JSONL (one JSON object per line)
|
|
64
66
|
events.on('*', (event) => {
|
|
65
67
|
console.log(JSON.stringify(event));
|
|
66
68
|
});
|
|
@@ -75,6 +77,73 @@ export async function runCommand(options) {
|
|
|
75
77
|
let runId = null;
|
|
76
78
|
let paths = null;
|
|
77
79
|
let startedAt = null;
|
|
80
|
+
let watchdogTimer = null;
|
|
81
|
+
let budget = null;
|
|
82
|
+
let timedOut = false;
|
|
83
|
+
|
|
84
|
+
// Graceful finalization function
|
|
85
|
+
const finalizeOnTimeout = async (reason) => {
|
|
86
|
+
if (timedOut) return; // Prevent double finalization
|
|
87
|
+
timedOut = true;
|
|
88
|
+
|
|
89
|
+
events.stopHeartbeat();
|
|
90
|
+
|
|
91
|
+
if (paths && runId && startedAt) {
|
|
92
|
+
try {
|
|
93
|
+
const failedAt = new Date().toISOString();
|
|
94
|
+
atomicWriteJson(paths.runStatusJson, {
|
|
95
|
+
status: 'FAILED',
|
|
96
|
+
runId,
|
|
97
|
+
startedAt,
|
|
98
|
+
failedAt,
|
|
99
|
+
error: reason,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
atomicWriteJson(paths.runMetaJson, {
|
|
103
|
+
veraxVersion: getVersion(),
|
|
104
|
+
nodeVersion: process.version,
|
|
105
|
+
platform: process.platform,
|
|
106
|
+
cwd: projectRoot,
|
|
107
|
+
command: 'run',
|
|
108
|
+
args: { url, src, out },
|
|
109
|
+
url,
|
|
110
|
+
src: srcPath,
|
|
111
|
+
startedAt,
|
|
112
|
+
completedAt: failedAt,
|
|
113
|
+
error: reason,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
writeSummaryJson(paths.summaryJson, {
|
|
118
|
+
runId,
|
|
119
|
+
status: 'FAILED',
|
|
120
|
+
startedAt,
|
|
121
|
+
completedAt: failedAt,
|
|
122
|
+
command: 'run',
|
|
123
|
+
url,
|
|
124
|
+
notes: `Run timed out: ${reason}`,
|
|
125
|
+
}, {
|
|
126
|
+
expectationsTotal: 0,
|
|
127
|
+
attempted: 0,
|
|
128
|
+
observed: 0,
|
|
129
|
+
silentFailures: 0,
|
|
130
|
+
coverageGaps: 0,
|
|
131
|
+
unproven: 0,
|
|
132
|
+
informational: 0,
|
|
133
|
+
});
|
|
134
|
+
} catch (summaryError) {
|
|
135
|
+
// Ignore summary write errors during timeout handling
|
|
136
|
+
}
|
|
137
|
+
} catch (statusError) {
|
|
138
|
+
// Ignore errors when writing failure status
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
events.emit('error', {
|
|
143
|
+
message: reason,
|
|
144
|
+
type: 'timeout',
|
|
145
|
+
});
|
|
146
|
+
};
|
|
78
147
|
|
|
79
148
|
try {
|
|
80
149
|
// Generate run ID
|
|
@@ -148,14 +217,53 @@ export async function runCommand(options) {
|
|
|
148
217
|
error: null,
|
|
149
218
|
});
|
|
150
219
|
|
|
151
|
-
//
|
|
220
|
+
// Extract expectations first to compute budget
|
|
152
221
|
events.emit('phase:started', {
|
|
153
222
|
phase: 'Learn',
|
|
154
223
|
message: 'Analyzing project structure...',
|
|
155
224
|
});
|
|
156
225
|
|
|
157
|
-
|
|
158
|
-
|
|
226
|
+
events.startHeartbeat('Learn', json);
|
|
227
|
+
|
|
228
|
+
let expectations, skipped;
|
|
229
|
+
try {
|
|
230
|
+
// Extract expectations (quick operation, no timeout needed here)
|
|
231
|
+
const result = await extractExpectations(projectProfile, projectProfile.sourceRoot);
|
|
232
|
+
expectations = result.expectations;
|
|
233
|
+
skipped = result.skipped;
|
|
234
|
+
} finally {
|
|
235
|
+
events.stopHeartbeat();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Compute runtime budget based on expectations count
|
|
239
|
+
budget = computeRuntimeBudget({
|
|
240
|
+
expectationsCount: expectations.length,
|
|
241
|
+
mode: 'run',
|
|
242
|
+
framework: projectProfile.framework,
|
|
243
|
+
fileCount: projectProfile.fileCount || expectations.length,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Set up global watchdog timer
|
|
247
|
+
watchdogTimer = setTimeout(async () => {
|
|
248
|
+
await finalizeOnTimeout(`Global timeout exceeded: ${budget.totalMaxMs}ms`);
|
|
249
|
+
// Exit with code 0 (tool executed, just timed out)
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}, budget.totalMaxMs);
|
|
252
|
+
|
|
253
|
+
// Wrap Learn phase with timeout
|
|
254
|
+
try {
|
|
255
|
+
await withTimeout(
|
|
256
|
+
budget.learnMaxMs,
|
|
257
|
+
Promise.resolve(), // Learn phase already completed
|
|
258
|
+
'Learn'
|
|
259
|
+
);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error.message.includes('timeout')) {
|
|
262
|
+
await finalizeOnTimeout(`Learn phase timeout: ${budget.learnMaxMs}ms`);
|
|
263
|
+
process.exit(0);
|
|
264
|
+
}
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
159
267
|
|
|
160
268
|
// For now, emit a placeholder trace event
|
|
161
269
|
events.emit('phase:completed', {
|
|
@@ -163,39 +271,60 @@ export async function runCommand(options) {
|
|
|
163
271
|
message: 'Project analysis complete',
|
|
164
272
|
});
|
|
165
273
|
|
|
166
|
-
// Observe phase
|
|
274
|
+
// Observe phase with timeout
|
|
167
275
|
events.emit('phase:started', {
|
|
168
276
|
phase: 'Observe',
|
|
169
277
|
message: 'Launching browser and observing expectations...',
|
|
170
278
|
});
|
|
171
279
|
|
|
280
|
+
events.startHeartbeat('Observe', json);
|
|
281
|
+
|
|
172
282
|
let observeData = null;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
283
|
+
try {
|
|
284
|
+
if (expectations.length > 0) {
|
|
285
|
+
try {
|
|
286
|
+
observeData = await withTimeout(
|
|
287
|
+
budget.observeMaxMs,
|
|
288
|
+
observeExpectations(
|
|
289
|
+
expectations,
|
|
290
|
+
url,
|
|
291
|
+
paths.evidenceDir,
|
|
292
|
+
(progress) => {
|
|
293
|
+
events.emit(progress.event, progress);
|
|
294
|
+
}
|
|
295
|
+
),
|
|
296
|
+
'Observe'
|
|
297
|
+
);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error.message.includes('timeout')) {
|
|
300
|
+
events.emit('observe:error', {
|
|
301
|
+
message: `Observe phase timeout: ${budget.observeMaxMs}ms`,
|
|
302
|
+
});
|
|
303
|
+
observeData = {
|
|
304
|
+
observations: [],
|
|
305
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
306
|
+
observedAt: new Date().toISOString(),
|
|
307
|
+
};
|
|
308
|
+
} else {
|
|
309
|
+
events.emit('observe:error', {
|
|
310
|
+
message: error.message,
|
|
311
|
+
});
|
|
312
|
+
observeData = {
|
|
313
|
+
observations: [],
|
|
314
|
+
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
315
|
+
observedAt: new Date().toISOString(),
|
|
316
|
+
};
|
|
181
317
|
}
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
events.emit('observe:error', {
|
|
185
|
-
message: error.message,
|
|
186
|
-
});
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
187
320
|
observeData = {
|
|
188
321
|
observations: [],
|
|
189
322
|
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
190
323
|
observedAt: new Date().toISOString(),
|
|
191
324
|
};
|
|
192
325
|
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
observations: [],
|
|
196
|
-
stats: { attempted: 0, observed: 0, notObserved: 0 },
|
|
197
|
-
observedAt: new Date().toISOString(),
|
|
198
|
-
};
|
|
326
|
+
} finally {
|
|
327
|
+
events.stopHeartbeat();
|
|
199
328
|
}
|
|
200
329
|
|
|
201
330
|
events.emit('phase:completed', {
|
|
@@ -203,32 +332,53 @@ export async function runCommand(options) {
|
|
|
203
332
|
message: 'Browser observation complete',
|
|
204
333
|
});
|
|
205
334
|
|
|
206
|
-
// Detect phase
|
|
335
|
+
// Detect phase with timeout
|
|
207
336
|
events.emit('phase:started', {
|
|
208
337
|
phase: 'Detect',
|
|
209
338
|
message: 'Analyzing findings and detecting silent failures...',
|
|
210
339
|
});
|
|
211
340
|
|
|
341
|
+
events.startHeartbeat('Detect', json);
|
|
342
|
+
|
|
212
343
|
let detectData = null;
|
|
213
344
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
345
|
+
try {
|
|
346
|
+
// Use already-extracted expectations
|
|
347
|
+
const learnData = {
|
|
348
|
+
expectations,
|
|
349
|
+
skipped,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
detectData = await withTimeout(
|
|
353
|
+
budget.detectMaxMs,
|
|
354
|
+
detectFindings(learnData, observeData, projectRoot, (progress) => {
|
|
355
|
+
events.emit(progress.event, progress);
|
|
356
|
+
}),
|
|
357
|
+
'Detect'
|
|
358
|
+
);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error.message.includes('timeout')) {
|
|
361
|
+
events.emit('detect:error', {
|
|
362
|
+
message: `Detect phase timeout: ${budget.detectMaxMs}ms`,
|
|
363
|
+
});
|
|
364
|
+
detectData = {
|
|
365
|
+
findings: [],
|
|
366
|
+
stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
|
|
367
|
+
detectedAt: new Date().toISOString(),
|
|
368
|
+
};
|
|
369
|
+
} else {
|
|
370
|
+
events.emit('detect:error', {
|
|
371
|
+
message: error.message,
|
|
372
|
+
});
|
|
373
|
+
detectData = {
|
|
374
|
+
findings: [],
|
|
375
|
+
stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
|
|
376
|
+
detectedAt: new Date().toISOString(),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} finally {
|
|
381
|
+
events.stopHeartbeat();
|
|
232
382
|
}
|
|
233
383
|
|
|
234
384
|
events.emit('phase:completed', {
|
|
@@ -236,12 +386,20 @@ export async function runCommand(options) {
|
|
|
236
386
|
message: 'Silent failure detection complete',
|
|
237
387
|
});
|
|
238
388
|
|
|
389
|
+
// Clear watchdog timer on successful completion
|
|
390
|
+
if (watchdogTimer) {
|
|
391
|
+
clearTimeout(watchdogTimer);
|
|
392
|
+
watchdogTimer = null;
|
|
393
|
+
}
|
|
394
|
+
|
|
239
395
|
// Emit finalize phase
|
|
240
396
|
events.emit('phase:started', {
|
|
241
397
|
phase: 'Finalize Artifacts',
|
|
242
398
|
message: 'Writing run results...',
|
|
243
399
|
});
|
|
244
400
|
|
|
401
|
+
events.stopHeartbeat();
|
|
402
|
+
|
|
245
403
|
const completedAt = new Date().toISOString();
|
|
246
404
|
|
|
247
405
|
// Write completed status
|
|
@@ -267,6 +425,20 @@ export async function runCommand(options) {
|
|
|
267
425
|
error: null,
|
|
268
426
|
});
|
|
269
427
|
|
|
428
|
+
const runDurationMs = completedAt && startedAt ? (Date.parse(completedAt) - Date.parse(startedAt)) : 0;
|
|
429
|
+
const metrics = {
|
|
430
|
+
learnMs: observeData?.timings?.learnMs || 0,
|
|
431
|
+
observeMs: observeData?.timings?.observeMs || observeData?.timings?.totalMs || 0,
|
|
432
|
+
detectMs: detectData?.timings?.detectMs || detectData?.timings?.totalMs || 0,
|
|
433
|
+
totalMs: runDurationMs > 0 ? runDurationMs : (budget?.ms || 0)
|
|
434
|
+
};
|
|
435
|
+
const findingsCounts = detectData?.findingsCounts || {
|
|
436
|
+
HIGH: 0,
|
|
437
|
+
MEDIUM: 0,
|
|
438
|
+
LOW: 0,
|
|
439
|
+
UNKNOWN: 0,
|
|
440
|
+
};
|
|
441
|
+
|
|
270
442
|
// Write summary with stable digest
|
|
271
443
|
writeSummaryJson(paths.summaryJson, {
|
|
272
444
|
runId,
|
|
@@ -276,6 +448,8 @@ export async function runCommand(options) {
|
|
|
276
448
|
command: 'run',
|
|
277
449
|
url,
|
|
278
450
|
notes: 'Run completed successfully',
|
|
451
|
+
metrics,
|
|
452
|
+
findingsCounts,
|
|
279
453
|
}, {
|
|
280
454
|
expectationsTotal: expectations.length,
|
|
281
455
|
attempted: observeData.stats?.attempted || 0,
|
|
@@ -284,36 +458,18 @@ export async function runCommand(options) {
|
|
|
284
458
|
coverageGaps: detectData.stats?.coverageGaps || 0,
|
|
285
459
|
unproven: detectData.stats?.unproven || 0,
|
|
286
460
|
informational: detectData.stats?.informational || 0,
|
|
461
|
+
...metrics,
|
|
462
|
+
...findingsCounts,
|
|
287
463
|
});
|
|
288
464
|
|
|
289
465
|
// Write detect results (or empty if detection failed)
|
|
290
466
|
writeFindingsJson(paths.baseDir, detectData);
|
|
291
467
|
|
|
292
|
-
// Write traces (
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
phase: 'Detect Project',
|
|
298
|
-
},
|
|
299
|
-
{
|
|
300
|
-
type: 'phase:completed',
|
|
301
|
-
timestamp: new Date().toISOString(),
|
|
302
|
-
phase: 'Detect Project',
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
type: 'phase:started',
|
|
306
|
-
timestamp: new Date().toISOString(),
|
|
307
|
-
phase: 'Learn',
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
type: 'phase:completed',
|
|
311
|
-
timestamp: completedAt,
|
|
312
|
-
phase: 'Learn',
|
|
313
|
-
},
|
|
314
|
-
];
|
|
315
|
-
|
|
316
|
-
const tracesContent = traces.map(t => JSON.stringify(t)).join('\n') + '\n';
|
|
468
|
+
// Write traces (include all events including heartbeats)
|
|
469
|
+
const allEvents = events.getEvents();
|
|
470
|
+
const tracesContent = allEvents
|
|
471
|
+
.map(e => JSON.stringify(e))
|
|
472
|
+
.join('\n') + '\n';
|
|
317
473
|
atomicWriteText(paths.tracesJsonl, tracesContent);
|
|
318
474
|
|
|
319
475
|
// Write project profile
|
|
@@ -330,6 +486,26 @@ export async function runCommand(options) {
|
|
|
330
486
|
message: 'Run artifacts written',
|
|
331
487
|
});
|
|
332
488
|
|
|
489
|
+
// Emit final summary event
|
|
490
|
+
if (json) {
|
|
491
|
+
events.emit('run:complete', {
|
|
492
|
+
runId,
|
|
493
|
+
url,
|
|
494
|
+
command: 'run',
|
|
495
|
+
findingsCounts,
|
|
496
|
+
metrics,
|
|
497
|
+
digest: {
|
|
498
|
+
expectationsTotal: expectations.length,
|
|
499
|
+
attempted: observeData.stats?.attempted || 0,
|
|
500
|
+
observed: observeData.stats?.observed || 0,
|
|
501
|
+
silentFailures: detectData.stats?.silentFailures || 0,
|
|
502
|
+
coverageGaps: detectData.stats?.coverageGaps || 0,
|
|
503
|
+
unproven: detectData.stats?.unproven || 0,
|
|
504
|
+
informational: detectData.stats?.informational || 0,
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
333
509
|
// Print summary if not JSON mode
|
|
334
510
|
if (!json) {
|
|
335
511
|
console.log('\nRun complete.');
|
|
@@ -339,6 +515,14 @@ export async function runCommand(options) {
|
|
|
339
515
|
|
|
340
516
|
return { runId, paths, success: true };
|
|
341
517
|
} catch (error) {
|
|
518
|
+
// Clear watchdog timer on error
|
|
519
|
+
if (watchdogTimer) {
|
|
520
|
+
clearTimeout(watchdogTimer);
|
|
521
|
+
watchdogTimer = null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
events.stopHeartbeat();
|
|
525
|
+
|
|
342
526
|
// Mark run as FAILED if we have paths
|
|
343
527
|
if (paths && runId && startedAt) {
|
|
344
528
|
try {
|
package/src/cli/entry.js
CHANGED
|
@@ -22,7 +22,7 @@ import { defaultCommand } from './commands/default.js';
|
|
|
22
22
|
import { runCommand } from './commands/run.js';
|
|
23
23
|
import { inspectCommand } from './commands/inspect.js';
|
|
24
24
|
import { doctorCommand } from './commands/doctor.js';
|
|
25
|
-
import { getExitCode, UsageError
|
|
25
|
+
import { getExitCode, UsageError } from './util/errors.js';
|
|
26
26
|
|
|
27
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
28
|
const __dirname = dirname(__filename);
|
|
@@ -74,7 +74,7 @@ async function main() {
|
|
|
74
74
|
throw new UsageError('run command requires --url <url> argument');
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
await runCommand({ url, src, out, json, verbose });
|
|
78
78
|
process.exit(0);
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -87,7 +87,7 @@ async function main() {
|
|
|
87
87
|
const runPath = args[1];
|
|
88
88
|
const json = args.includes('--json');
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
await inspectCommand(runPath, { json });
|
|
91
91
|
process.exit(0);
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -96,7 +96,7 @@ async function main() {
|
|
|
96
96
|
const allowedFlags = new Set(['--json']);
|
|
97
97
|
const extraFlags = args.slice(1).filter((a) => a.startsWith('-') && !allowedFlags.has(a));
|
|
98
98
|
const json = args.includes('--json');
|
|
99
|
-
|
|
99
|
+
await doctorCommand({ json, extraFlags });
|
|
100
100
|
process.exit(0);
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -114,7 +114,7 @@ async function main() {
|
|
|
114
114
|
const json = args.includes('--json');
|
|
115
115
|
const verbose = args.includes('--verbose');
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
await defaultCommand({ url, src, out, json, verbose });
|
|
118
118
|
process.exit(0);
|
|
119
119
|
} catch (error) {
|
|
120
120
|
// Print error message
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Produces exactly one finding per expectation with deterministic confidence and impact.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export async function detectFindings(learnData, observeData, projectPath, onProgress) {
|
|
7
|
+
export async function detectFindings(learnData, observeData, projectPath, onProgress, options = {}) {
|
|
8
|
+
const log = options.silent ? () => {} : console.log;
|
|
8
9
|
const findings = [];
|
|
9
10
|
const stats = {
|
|
10
11
|
total: 0,
|
|
@@ -58,8 +59,8 @@ export async function detectFindings(learnData, observeData, projectPath, onProg
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// Terminal narration (one line per finding + summary)
|
|
61
|
-
narration.forEach((line) =>
|
|
62
|
-
|
|
62
|
+
narration.forEach((line) => log(line));
|
|
63
|
+
log(`SUMMARY findings=${findings.length} observed=${stats.observed} silent-failure=${stats.silentFailures} coverage-gap=${stats.coverageGaps} unproven=${stats.unproven}`);
|
|
63
64
|
|
|
64
65
|
// Emit completion event
|
|
65
66
|
if (onProgress) {
|
package/src/cli/util/events.js
CHANGED
|
@@ -5,6 +5,10 @@ export class RunEventEmitter {
|
|
|
5
5
|
constructor() {
|
|
6
6
|
this.events = [];
|
|
7
7
|
this.listeners = [];
|
|
8
|
+
this.heartbeatInterval = null;
|
|
9
|
+
this.heartbeatStartTime = null;
|
|
10
|
+
this.currentPhase = null;
|
|
11
|
+
this.heartbeatIntervalMs = 2500; // 2.5 seconds
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
on(event, handler) {
|
|
@@ -31,4 +35,76 @@ export class RunEventEmitter {
|
|
|
31
35
|
getEvents() {
|
|
32
36
|
return this.events;
|
|
33
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Start heartbeat for a phase
|
|
41
|
+
* @param {string} phase - Current phase name
|
|
42
|
+
* @param {boolean} jsonMode - Whether in JSON output mode
|
|
43
|
+
*/
|
|
44
|
+
startHeartbeat(phase, jsonMode = false) {
|
|
45
|
+
this.currentPhase = phase;
|
|
46
|
+
this.heartbeatStartTime = Date.now();
|
|
47
|
+
|
|
48
|
+
if (this.heartbeatInterval) {
|
|
49
|
+
clearInterval(this.heartbeatInterval);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.heartbeatInterval = setInterval(() => {
|
|
53
|
+
const elapsedMs = Date.now() - this.heartbeatStartTime;
|
|
54
|
+
const elapsedSeconds = Math.floor(elapsedMs / 1000);
|
|
55
|
+
|
|
56
|
+
const heartbeatEvent = {
|
|
57
|
+
type: 'heartbeat',
|
|
58
|
+
phase: this.currentPhase,
|
|
59
|
+
elapsedMs,
|
|
60
|
+
elapsedSeconds,
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Add to events array
|
|
65
|
+
this.events.push(heartbeatEvent);
|
|
66
|
+
|
|
67
|
+
// Emit to listeners
|
|
68
|
+
this.listeners.forEach(({ event: listenEvent, handler }) => {
|
|
69
|
+
if (listenEvent === 'heartbeat' || listenEvent === '*') {
|
|
70
|
+
if (jsonMode) {
|
|
71
|
+
// In JSON mode, emit as JSON line
|
|
72
|
+
handler(heartbeatEvent);
|
|
73
|
+
} else {
|
|
74
|
+
// Human-readable format: single line, overwrite-friendly
|
|
75
|
+
process.stdout.write(`\r…still working (phase=${this.currentPhase}, elapsed=${elapsedSeconds}s)`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}, this.heartbeatIntervalMs);
|
|
80
|
+
|
|
81
|
+
// CRITICAL: Unref the interval so it doesn't keep the process alive
|
|
82
|
+
// This allows tests to exit cleanly even if stopHeartbeat() is not called
|
|
83
|
+
if (this.heartbeatInterval && this.heartbeatInterval.unref) {
|
|
84
|
+
this.heartbeatInterval.unref();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Stop heartbeat
|
|
90
|
+
*/
|
|
91
|
+
stopHeartbeat() {
|
|
92
|
+
if (this.heartbeatInterval) {
|
|
93
|
+
clearInterval(this.heartbeatInterval);
|
|
94
|
+
this.heartbeatInterval = null;
|
|
95
|
+
}
|
|
96
|
+
// Clear the progress line in human mode
|
|
97
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
98
|
+
this.currentPhase = null;
|
|
99
|
+
this.heartbeatStartTime = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Update current phase for heartbeat
|
|
104
|
+
* @param {string} phase - New phase name
|
|
105
|
+
*/
|
|
106
|
+
updatePhase(phase) {
|
|
107
|
+
this.currentPhase = phase;
|
|
108
|
+
this.heartbeatStartTime = Date.now();
|
|
109
|
+
}
|
|
34
110
|
}
|
|
@@ -7,7 +7,7 @@ import { expIdFromHash, compareExpectations } from './idgen.js';
|
|
|
7
7
|
* Extracts explicit, static expectations from source files
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export async function extractExpectations(projectProfile,
|
|
10
|
+
export async function extractExpectations(projectProfile, _srcPath) {
|
|
11
11
|
const expectations = [];
|
|
12
12
|
const skipped = {
|
|
13
13
|
dynamic: 0,
|
|
@@ -68,6 +68,16 @@ function getScanPaths(projectProfile, sourceRoot) {
|
|
|
68
68
|
return [sourceRoot];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// Unknown framework or no framework detected - check if it's a static HTML project
|
|
72
|
+
// (This handles cases where framework detection failed but HTML files exist)
|
|
73
|
+
if (framework === 'unknown') {
|
|
74
|
+
const htmlFiles = readdirSync(sourceRoot, { withFileTypes: true })
|
|
75
|
+
.filter(e => e.isFile() && e.name.endsWith('.html'));
|
|
76
|
+
if (htmlFiles.length > 0) {
|
|
77
|
+
return [sourceRoot];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
// Unknown framework - scan src if it exists
|
|
72
82
|
const srcPath = resolve(sourceRoot, 'src');
|
|
73
83
|
try {
|
|
@@ -16,6 +16,7 @@ export function writeFindingsJson(runDir, findingsData) {
|
|
|
16
16
|
|
|
17
17
|
const payload = {
|
|
18
18
|
findings: findingsWithIds,
|
|
19
|
+
total: findingsData.stats?.total || 0,
|
|
19
20
|
stats: {
|
|
20
21
|
total: findingsData.stats?.total || 0,
|
|
21
22
|
silentFailures: findingsData.stats?.silentFailures || 0,
|