@opensip-cli/graph 0.1.4 → 0.1.5
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/README.md +2 -2
- package/dist/__tests__/cli/graph-config.test.js +5 -3
- package/dist/__tests__/cli/graph-config.test.js.map +1 -1
- package/dist/__tests__/tool-branches.test.js +4 -1
- package/dist/__tests__/tool-branches.test.js.map +1 -1
- package/dist/cli/graph-config.d.ts.map +1 -1
- package/dist/cli/graph-config.js +20 -1
- package/dist/cli/graph-config.js.map +1 -1
- package/dist/cli/graph-feature-columns.d.ts +7 -0
- package/dist/cli/graph-feature-columns.d.ts.map +1 -0
- package/dist/cli/graph-feature-columns.js +10 -0
- package/dist/cli/graph-feature-columns.js.map +1 -0
- package/dist/cli/graph-multi-path-mode.d.ts +24 -0
- package/dist/cli/graph-multi-path-mode.d.ts.map +1 -0
- package/dist/cli/graph-multi-path-mode.js +64 -0
- package/dist/cli/graph-multi-path-mode.js.map +1 -0
- package/dist/cli/graph-run-outcome.d.ts +12 -0
- package/dist/cli/graph-run-outcome.d.ts.map +1 -0
- package/dist/cli/graph-run-outcome.js +2 -0
- package/dist/cli/graph-run-outcome.js.map +1 -0
- package/dist/cli/graph-session-contribution.d.ts +29 -0
- package/dist/cli/graph-session-contribution.d.ts.map +1 -0
- package/dist/cli/graph-session-contribution.js +58 -0
- package/dist/cli/graph-session-contribution.js.map +1 -0
- package/dist/cli/graph-sharded-engine.d.ts +77 -0
- package/dist/cli/graph-sharded-engine.d.ts.map +1 -0
- package/dist/cli/graph-sharded-engine.js +229 -0
- package/dist/cli/graph-sharded-engine.js.map +1 -0
- package/dist/cli/graph-workspace-mode.d.ts +11 -0
- package/dist/cli/graph-workspace-mode.d.ts.map +1 -0
- package/dist/cli/graph-workspace-mode.js +87 -0
- package/dist/cli/graph-workspace-mode.js.map +1 -0
- package/dist/cli/graph.d.ts +5 -129
- package/dist/cli/graph.d.ts.map +1 -1
- package/dist/cli/graph.js +13 -552
- package/dist/cli/graph.js.map +1 -1
- package/package.json +8 -8
package/dist/cli/graph.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// @fitness-ignore-file performance-anti-patterns -- spread in CLI report aggregation iterates bounded result sets (rule counts, entry-point lists).
|
|
5
5
|
// @fitness-ignore-file no-markdown-references -- docs/plans/* pointers in JSDoc are stable internal references.
|
|
6
6
|
// @fitness-ignore-file public-api-jsdoc -- GraphCommandOptions interface and executeGraph are already documented with rich JSDoc on each field; the check counts the top-level export line, not the fields.
|
|
7
|
-
// @fitness-ignore-file file-length-limit -- top-level graph command handler
|
|
7
|
+
// @fitness-ignore-file file-length-limit -- remaining top-level graph command handler still coordinates mode dispatch, output delivery, single-run engine selection, and CLI error mapping; large workspace/multi-path modes and sharded engine/session helpers are split out.
|
|
8
8
|
/**
|
|
9
9
|
* `opensip graph` — main subcommand handler.
|
|
10
10
|
*
|
|
@@ -25,40 +25,25 @@
|
|
|
25
25
|
* `--packages` flags were retired in favor of the polyglot surface
|
|
26
26
|
* above; see docs/plans/graph-cli-language-neutral-scoping/.
|
|
27
27
|
*/
|
|
28
|
-
import {
|
|
29
|
-
import { resolve } from 'node:path';
|
|
30
|
-
import { EXIT_CODES, passRate } from '@opensip-cli/contracts';
|
|
28
|
+
import { EXIT_CODES } from '@opensip-cli/contracts';
|
|
31
29
|
import { ConfigurationError, currentScope, logger, SystemError, ToolError, ValidationError, } from '@opensip-cli/core';
|
|
32
|
-
import { pickAdapter } from '../lang-adapter/registry.js';
|
|
33
|
-
import { CatalogRepo } from '../persistence/catalog-repo.js';
|
|
34
|
-
import { buildGraphSessionPayload } from '../persistence/session-payload.js';
|
|
35
30
|
import { resolveRecipeToRules } from '../recipes/resolve.js';
|
|
36
|
-
import {
|
|
37
|
-
import { currentRules } from '../rules/registry.js';
|
|
38
|
-
import { assertFinalizedAcrossBoundary, finalizeGraphSignals, } from './apply-suppressions.js';
|
|
31
|
+
import { finalizeGraphSignals } from './apply-suppressions.js';
|
|
39
32
|
import { buildGraphEnvelope } from './build-envelope.js';
|
|
33
|
+
import { DASHBOARD_FEATURE_COLUMNS } from './graph-feature-columns.js';
|
|
40
34
|
import { runCatalogJsonMode, runGateMode } from './graph-modes.js';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
35
|
+
import { executeMultiPathGraph } from './graph-multi-path-mode.js';
|
|
36
|
+
import { buildUnifiedReportLines, countFiles, resolutionBannerText } from './graph-report.js';
|
|
37
|
+
import { buildGraphSessionContribution } from './graph-session-contribution.js';
|
|
38
|
+
import { engineSelectionReason, realpathOrSelf, resolveEngineShards, runProfiledShardedBuild, } from './graph-sharded-engine.js';
|
|
39
|
+
import { executeWorkspaceGraph } from './graph-workspace-mode.js';
|
|
40
|
+
import { loadGraphConfig, resolveGraphRecipeSelection, runGraph } from './orchestrate.js';
|
|
46
41
|
import { positionalPathLabel, resolvePositionalPaths } from './positional-paths.js';
|
|
47
42
|
import { MemoryPressureError } from './pressure-monitor.js';
|
|
48
43
|
import { GraphProfileBuilder, writeGraphProfile } from './profile.js';
|
|
49
|
-
import { resolveAdaptersForRun } from './resolve-adapters.js';
|
|
50
|
-
import { buildWorkspaceJsonDocument, writeWorkspaceReport } from './workspace-report.js';
|
|
51
|
-
import { discoverPolyglotUnits, runWorkspaceUnitsInParallel } from './workspace-runner.js';
|
|
52
44
|
const EVT_GRAPH_COMPLETE = 'graph.cli.graph.complete';
|
|
53
45
|
const MODULE_GRAPH_CLI = 'graph:cli';
|
|
54
46
|
const MODULE_GRAPH_RENDER = 'graph:render';
|
|
55
|
-
/**
|
|
56
|
-
* The feature columns the decoupled dashboard renders (ADR-0006): only these
|
|
57
|
-
* are materialized into the persisted catalog, and only on the standard
|
|
58
|
-
* (catalog-producing) `graph` run. Export-only paths (sarif/catalog export)
|
|
59
|
-
* do not go through this dispatch, so they stay lean (no features persisted).
|
|
60
|
-
*/
|
|
61
|
-
const DASHBOARD_FEATURE_COLUMNS = ['blast', 'scc', 'packageCoupling'];
|
|
62
47
|
/**
|
|
63
48
|
* Run graph and return the run's {@link GraphRunOutcome} — the deliverable
|
|
64
49
|
* {@link SignalEnvelope} (so the composition root can cloud + `--report-to`
|
|
@@ -132,7 +117,7 @@ export async function executeGraph(opts, cli) {
|
|
|
132
117
|
}
|
|
133
118
|
const positionalPaths = resolvePositionalScope(opts);
|
|
134
119
|
if (positionalPaths.length > 1) {
|
|
135
|
-
const outcome = await executeMultiPathGraph({ opts, cli, rules, startedAt, profile }, positionalPaths);
|
|
120
|
+
const outcome = await executeMultiPathGraph({ opts, cli, rules, startedAt, profile, deliverGraphResult }, positionalPaths);
|
|
136
121
|
writeProfileIfRequested(opts, profile);
|
|
137
122
|
return outcome;
|
|
138
123
|
}
|
|
@@ -255,277 +240,6 @@ export async function executeGraph(opts, cli) {
|
|
|
255
240
|
* no worker script is available — a graceful fall-through to exact, the
|
|
256
241
|
* natural single-package path.
|
|
257
242
|
*/
|
|
258
|
-
/**
|
|
259
|
-
* Resolve a path to absolute (a RELATIVE input resolves against `base` —
|
|
260
|
-
* the command's `opts.cwd`, NOT `process.cwd()`, which may differ when the
|
|
261
|
-
* CLI is hosted), then realpath it (follow symlinks) — the SAME normalization
|
|
262
|
-
* the exact engine's `normalizeProjectDir` applies, so both engines see one
|
|
263
|
-
* canonical run root (F3). Falls back to the absolute path if realpath fails
|
|
264
|
-
* (e.g. the path doesn't exist yet — discovery reports the error downstream).
|
|
265
|
-
* Idempotent on an already-canonical path.
|
|
266
|
-
*/
|
|
267
|
-
function realpathOrSelf(input, base) {
|
|
268
|
-
// `resolve(base, input)` returns `input` unchanged when it is already absolute.
|
|
269
|
-
const absolute = resolve(base, input);
|
|
270
|
-
try {
|
|
271
|
-
return realpathSync(absolute);
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
/* v8 ignore next */
|
|
275
|
-
return absolute;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
async function resolveEngineShards(opts, cli, positionalPaths) {
|
|
279
|
-
if (opts.exact === true)
|
|
280
|
-
return { shards: [] };
|
|
281
|
-
if (positionalPaths.length > 0)
|
|
282
|
-
return { shards: [] };
|
|
283
|
-
return resolveShards(opts, cli);
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Human-readable explanation of the engine decision for the
|
|
287
|
-
* `graph.cli.graph.engine` observability event. Pure; mirrors
|
|
288
|
-
* {@link resolveEngineShards}'s branches.
|
|
289
|
-
*/
|
|
290
|
-
function engineSelectionReason(opts, positionalPaths, sharded) {
|
|
291
|
-
if (sharded)
|
|
292
|
-
return 'sharded-default';
|
|
293
|
-
if (opts.exact === true)
|
|
294
|
-
return 'exact-opt-out';
|
|
295
|
-
if (positionalPaths.length > 0)
|
|
296
|
-
return 'exact-positional-paths';
|
|
297
|
-
return 'exact-not-shardable';
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Resolve a project to its shards (one per workspace package). Returns an
|
|
301
|
-
* empty array — signalling the caller to use the single-process build —
|
|
302
|
-
* when the project isn't multi-package, when no worker script is
|
|
303
|
-
* available to spawn, or when discovery fails. Each unit's file set is
|
|
304
|
-
* enumerated via the graph adapter; partitions with no files are dropped.
|
|
305
|
-
*
|
|
306
|
-
* ADR-0032: reached for any run that did NOT pass `--exact` (see
|
|
307
|
-
* {@link resolveEngineShards}) — sharded is the default. A project that yields
|
|
308
|
-
* ≤1 non-empty shard falls back to the exact single-program engine naturally.
|
|
309
|
-
*/
|
|
310
|
-
async function resolveShards(opts, cli) {
|
|
311
|
-
const cliScript = opts.cliScript ?? process.argv[1];
|
|
312
|
-
if (typeof cliScript !== 'string' || cliScript.length === 0)
|
|
313
|
-
return { shards: [] };
|
|
314
|
-
let units;
|
|
315
|
-
try {
|
|
316
|
-
units = await discoverPolyglotUnits(opts.cwd, resolveAdaptersForRun(opts, cli));
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
/* v8 ignore next */
|
|
320
|
-
return resolveSyntheticFlatShards(opts);
|
|
321
|
-
}
|
|
322
|
-
if (units.length <= 1)
|
|
323
|
-
return resolveSyntheticFlatShards(opts);
|
|
324
|
-
// Phase 1 (graph-sharded-exact-parity): enumerate the canonical file set ONCE
|
|
325
|
-
// from project-wide root discovery — the SAME source + filter the exact engine
|
|
326
|
-
// uses — then PARTITION it across the discovered unit boundaries. The old loop
|
|
327
|
-
// re-derived each shard's files from that package's own tsconfig, which
|
|
328
|
-
// excludes the package's __fixtures__ tree and (for some) its test files, so
|
|
329
|
-
// the sharded engine silently dropped files the exact engine kept. Partitioning
|
|
330
|
-
// the canonical set guarantees both engines see the identical files.
|
|
331
|
-
const adapter = pickAdapter(opts.cwd);
|
|
332
|
-
let rootDiscovery;
|
|
333
|
-
try {
|
|
334
|
-
rootDiscovery = adapter.discoverFiles({ cwd: opts.cwd });
|
|
335
|
-
}
|
|
336
|
-
catch {
|
|
337
|
-
/* v8 ignore next */
|
|
338
|
-
return resolveSyntheticFlatShards(opts);
|
|
339
|
-
}
|
|
340
|
-
const canonicalFiles = resolveCanonicalFileSet(rootDiscovery.files);
|
|
341
|
-
const shards = partitionFilesIntoShards({
|
|
342
|
-
canonicalFiles,
|
|
343
|
-
units: units.map((u) => ({
|
|
344
|
-
id: u.id,
|
|
345
|
-
rootDir: u.rootDir,
|
|
346
|
-
...(u.configPath === undefined ? {} : { configPathAbs: u.configPath }),
|
|
347
|
-
})),
|
|
348
|
-
projectRoot: rootDiscovery.projectDirAbs,
|
|
349
|
-
rootConfigPathAbs: rootDiscovery.configPathAbs,
|
|
350
|
-
});
|
|
351
|
-
// Need at least two non-empty shards to justify the parallel/merge overhead.
|
|
352
|
-
if (shards.length > 1)
|
|
353
|
-
return { shards };
|
|
354
|
-
return resolveSyntheticFlatShards(opts);
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Resolve a project's shard set the SAME way a production `graph` run does
|
|
358
|
-
* (workspace units → canonical-file partition, else synthetic flat shards),
|
|
359
|
-
* exposed for the real-repo equivalence guardrail (`graph-equivalence-check`).
|
|
360
|
-
* Returns `[]` when the project isn't shardable (≤1 shard / no worker script) —
|
|
361
|
-
* the guardrail rejects that, since the comparison is only meaningful on a
|
|
362
|
-
* shardable multi-package repo. Reuses the private {@link resolveShards} so
|
|
363
|
-
* there is ONE shard-resolution model, never a drifting copy.
|
|
364
|
-
*/
|
|
365
|
-
export async function resolveShardsForCwd(cwd, cliScript, cli) {
|
|
366
|
-
const resolution = await resolveShards({ cwd, cliScript, noCache: true }, cli);
|
|
367
|
-
return resolution.shards;
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Flat-large fallback for single-tsconfig TypeScript repos. Workspace
|
|
371
|
-
* sharding is preferred because package boundaries are semantically real. When
|
|
372
|
-
* no workspace split exists and the TypeScript file count crosses the same
|
|
373
|
-
* threshold as heap preflight's 12 GB tier, synthesize directory-coherent
|
|
374
|
-
* shards and feed them into the existing sharded build.
|
|
375
|
-
*/
|
|
376
|
-
function resolveSyntheticFlatShards(opts) {
|
|
377
|
-
if (typeof opts.language === 'string' && opts.language.length > 0)
|
|
378
|
-
return { shards: [] };
|
|
379
|
-
const adapter = pickAdapter(opts.cwd);
|
|
380
|
-
if (adapter.id !== 'typescript')
|
|
381
|
-
return { shards: [] };
|
|
382
|
-
let discovery;
|
|
383
|
-
try {
|
|
384
|
-
discovery = adapter.discoverFiles({ cwd: opts.cwd });
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
387
|
-
return { shards: [] };
|
|
388
|
-
}
|
|
389
|
-
// Canonical set (Phase 1): drop fixtures before partitioning so the flat-large
|
|
390
|
-
// fallback shards match the exact engine's file set just like the workspace path.
|
|
391
|
-
const canonicalFiles = resolveCanonicalFileSet(discovery.files);
|
|
392
|
-
// Measure the partition compute (layout detection + strategy resolution +
|
|
393
|
-
// partitioning) where it runs — the profile run recorder does not exist yet
|
|
394
|
-
// (its `mode` label needs the shard count), so executeGraph records the
|
|
395
|
-
// timing afterwards via `recordStage` (ADR-0045 measurement plane).
|
|
396
|
-
const partitionStart = Date.now();
|
|
397
|
-
const layout = detectMonorepoLayout({
|
|
398
|
-
repoRoot: discovery.projectDirAbs,
|
|
399
|
-
files: canonicalFiles,
|
|
400
|
-
});
|
|
401
|
-
const selection = selectStrategyForLayout(layout);
|
|
402
|
-
if (layout.kind !== 'flat-large' || selection.mode !== 'synthetic-partition') {
|
|
403
|
-
return { shards: [] };
|
|
404
|
-
}
|
|
405
|
-
// Strategy precedence: `graph.partitionStrategy` (config/env, ADR-0045) >
|
|
406
|
-
// the layout-recommended default > 'hybrid'. `loadGraphConfig` is
|
|
407
|
-
// scope-first and cheap, so reading it here (off the hot path) is safe.
|
|
408
|
-
const graphConfig = loadGraphConfig(opts.cwd);
|
|
409
|
-
const strategy = graphConfig.partitionStrategy ?? selection.partitionStrategy ?? 'hybrid';
|
|
410
|
-
const partitions = partitionFlatRepo({
|
|
411
|
-
files: layout.files,
|
|
412
|
-
repoRoot: discovery.projectDirAbs,
|
|
413
|
-
strategy,
|
|
414
|
-
});
|
|
415
|
-
const shards = partitions
|
|
416
|
-
.filter((p) => p.files.length > 0)
|
|
417
|
-
.map((p) => ({
|
|
418
|
-
id: `partition:${p.id}`,
|
|
419
|
-
rootDir: discovery.projectDirAbs,
|
|
420
|
-
files: p.files,
|
|
421
|
-
configPathAbs: discovery.configPathAbs,
|
|
422
|
-
}));
|
|
423
|
-
if (shards.length <= 1)
|
|
424
|
-
return { shards: [] };
|
|
425
|
-
return {
|
|
426
|
-
shards,
|
|
427
|
-
partition: {
|
|
428
|
-
durationMs: Date.now() - partitionStart,
|
|
429
|
-
detail: `${strategy}: ${String(shards.length)} partition(s)`,
|
|
430
|
-
},
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
/** Drive the sharded build and adapt it to the RunGraphResult dispatch shape. */
|
|
434
|
-
async function runShardedBuild(ctx) {
|
|
435
|
-
const { opts, shards, projectRoot, cli, config, rules } = ctx;
|
|
436
|
-
const datastore = cli.scope.datastore();
|
|
437
|
-
const sharded = await runShardedGraph({
|
|
438
|
-
shards,
|
|
439
|
-
projectRoot,
|
|
440
|
-
cliScript: opts.cliScript ?? process.argv[1] ?? '',
|
|
441
|
-
adapter: pickAdapter(projectRoot, opts.language),
|
|
442
|
-
resolutionMode: opts.resolution ?? 'exact',
|
|
443
|
-
concurrency: opts.concurrency,
|
|
444
|
-
useCache: opts.noCache !== true,
|
|
445
|
-
config,
|
|
446
|
-
rules,
|
|
447
|
-
catalogRepo: datastore ? new CatalogRepo(datastore) : null,
|
|
448
|
-
emitFeatures: DASHBOARD_FEATURE_COLUMNS,
|
|
449
|
-
...(ctx.onProgress === undefined ? {} : { onProgress: ctx.onProgress }),
|
|
450
|
-
...(opts.language === undefined ? {} : { language: opts.language }),
|
|
451
|
-
});
|
|
452
|
-
return {
|
|
453
|
-
catalog: sharded.catalog,
|
|
454
|
-
indexes: sharded.indexes,
|
|
455
|
-
signals: sharded.signals,
|
|
456
|
-
resolutionStats: sharded.resolutionStats,
|
|
457
|
-
cacheHit: sharded.cacheHit,
|
|
458
|
-
features: sharded.features,
|
|
459
|
-
shardStats: sharded.shardStats,
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
async function runProfiledShardedBuild(profileRun, ctx) {
|
|
463
|
-
const started = Date.now();
|
|
464
|
-
const result = await runShardedBuild(ctx);
|
|
465
|
-
profileRun?.recordStage('sharded-build', Date.now() - started, `${String(ctx.shards.length)} shard(s)`);
|
|
466
|
-
return result;
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Resolve the build engine for the interactive live path — the SAME policy
|
|
470
|
-
* `executeGraph` uses (ADR-0032): the SHARDED engine when `--exact` is absent
|
|
471
|
-
* and the project yields >1 non-empty shard, the EXACT (single-program) engine
|
|
472
|
-
* otherwise. Returns the shard set (`length > 1` ⇒ sharded) so the live runner
|
|
473
|
-
* can choose engine-aware labels and pass the plain-data plan to
|
|
474
|
-
* `graph-run-worker` (ADR-0028). `isTTY` is NEVER consulted — the engine is a
|
|
475
|
-
* pure function of the request + shardability, identical to the static path.
|
|
476
|
-
*/
|
|
477
|
-
export async function resolveLiveEngineShards(args, cli) {
|
|
478
|
-
const opts = {
|
|
479
|
-
cwd: args.cwd,
|
|
480
|
-
noCache: args.noCache,
|
|
481
|
-
resolution: args.resolution,
|
|
482
|
-
exact: args.exact,
|
|
483
|
-
...(args.cliScript === undefined ? {} : { cliScript: args.cliScript }),
|
|
484
|
-
};
|
|
485
|
-
// No positional paths in the whole-project live view, so the engine decision
|
|
486
|
-
// is `--exact` + shardability alone.
|
|
487
|
-
const resolution = await resolveEngineShards(opts, cli, []);
|
|
488
|
-
return resolution.shards;
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Run the SHARDED live build and reduce it to the slim, serializable
|
|
492
|
-
* {@link LiveGraphOutput} the interactive runner consumes — IDENTICAL in shape
|
|
493
|
-
* to what the exact worker streams back, so live transports converge on one
|
|
494
|
-
* payload. In the normal path this runs inside `graph-run-worker`, keeping the
|
|
495
|
-
* render process free while the worker coordinates shard subprocesses and the
|
|
496
|
-
* synchronous merge/link/rules work. The same function remains the degraded
|
|
497
|
-
* in-process fallback when worker execution is explicitly disabled or unavailable.
|
|
498
|
-
* Progress events flow through `onProgress`, mapped onto the same seven
|
|
499
|
-
* canonical stages the exact engine emits.
|
|
500
|
-
*
|
|
501
|
-
* Crosses the single suppression chokepoint via {@link buildLiveGraphOutput}
|
|
502
|
-
* (against `args.cwd`, the build root) — so the live sharded path waives
|
|
503
|
-
* `@graph-ignore` directives IDENTICALLY to the static/exact paths (ADR-0014/0031).
|
|
504
|
-
*/
|
|
505
|
-
export async function runShardedLiveBuild(args, shards, datastore, onProgress) {
|
|
506
|
-
const result = await runShardedGraph({
|
|
507
|
-
shards,
|
|
508
|
-
projectRoot: args.cwd,
|
|
509
|
-
cliScript: args.cliScript ?? process.argv[1] ?? '',
|
|
510
|
-
adapter: pickAdapter(args.cwd),
|
|
511
|
-
resolutionMode: args.resolution ?? 'exact',
|
|
512
|
-
useCache: args.noCache !== true,
|
|
513
|
-
config: args.config ?? {},
|
|
514
|
-
// The live dispatch always resolves the recipe → rules; fall back to the
|
|
515
|
-
// full registered set if a programmatic caller omits them (parity with the
|
|
516
|
-
// exact path, where `runGraph` applies the same `?? currentRules()` default).
|
|
517
|
-
rules: args.rules ?? currentRules(),
|
|
518
|
-
catalogRepo: datastore ? new CatalogRepo(datastore) : null,
|
|
519
|
-
emitFeatures: DASHBOARD_FEATURE_COLUMNS,
|
|
520
|
-
onProgress,
|
|
521
|
-
});
|
|
522
|
-
return buildLiveGraphOutput({
|
|
523
|
-
catalog: result.catalog,
|
|
524
|
-
indexes: result.indexes,
|
|
525
|
-
signals: result.signals,
|
|
526
|
-
cacheHit: result.cacheHit,
|
|
527
|
-
}, args.cwd);
|
|
528
|
-
}
|
|
529
243
|
/** Profile bucket for the run shape: workspace fan-out, multi-path, or single graph. */
|
|
530
244
|
function profileMode(opts) {
|
|
531
245
|
if (opts.workspace === true)
|
|
@@ -590,69 +304,6 @@ function enforceLanguageMismatchPolicy(opts, catalog, paths) {
|
|
|
590
304
|
const pathLabel = paths.map((p) => positionalPathLabel(p, opts.cwd)).join(', ');
|
|
591
305
|
throw new ConfigurationError(`--language ${opts.language} matched 0 files under ${pathLabel}; check the flag or paths.`);
|
|
592
306
|
}
|
|
593
|
-
async function executeMultiPathGraph(ctx, paths) {
|
|
594
|
-
const { opts, cli, rules, startedAt, profile } = ctx;
|
|
595
|
-
const allSignals = [];
|
|
596
|
-
let combinedFiles = 0;
|
|
597
|
-
let totalSuppressed = 0;
|
|
598
|
-
let lastResult = null;
|
|
599
|
-
const config = loadGraphConfig(opts.cwd);
|
|
600
|
-
for (const p of paths) {
|
|
601
|
-
const profileRun = profile?.startRun({
|
|
602
|
-
label: positionalPathLabel(p, opts.cwd),
|
|
603
|
-
cwd: p,
|
|
604
|
-
mode: 'single-process',
|
|
605
|
-
});
|
|
606
|
-
const r = await runGraph({
|
|
607
|
-
cwd: p,
|
|
608
|
-
noCache: opts.noCache,
|
|
609
|
-
resolution: opts.resolution,
|
|
610
|
-
language: opts.language,
|
|
611
|
-
config,
|
|
612
|
-
rules,
|
|
613
|
-
datastore: cli.scope.datastore(),
|
|
614
|
-
emitFeatures: DASHBOARD_FEATURE_COLUMNS,
|
|
615
|
-
onProgress: profileRun?.onProgress,
|
|
616
|
-
});
|
|
617
|
-
profileRun?.finish(r);
|
|
618
|
-
lastResult = r;
|
|
619
|
-
// Each path's signals are relative to THAT path's root — so waive them
|
|
620
|
-
// against `p` here, before aggregating. A single post-aggregation pass
|
|
621
|
-
// (the old shape) could only use one base and would leak waivers for every
|
|
622
|
-
// path but one. Each per-path call crosses the single suppression
|
|
623
|
-
// chokepoint (finalizeGraphSignals); the aggregate is then re-branded once
|
|
624
|
-
// below via assertFinalizedAcrossBoundary (an assertion that every member
|
|
625
|
-
// was finalized, NOT a second suppression pass).
|
|
626
|
-
const finalized = await finalizeGraphSignals(r.signals, p);
|
|
627
|
-
totalSuppressed += finalized.suppressedCount;
|
|
628
|
-
allSignals.push(...finalized.signals);
|
|
629
|
-
if (r.catalog !== null)
|
|
630
|
-
combinedFiles += countFiles(r.catalog);
|
|
631
|
-
}
|
|
632
|
-
// D14: count files across every analyzed path. Zero files + a
|
|
633
|
-
// `--language` override → exit 2 with the canonical message.
|
|
634
|
-
if (typeof opts.language === 'string' && opts.language.length > 0 && combinedFiles === 0) {
|
|
635
|
-
throw new ConfigurationError(`--language ${opts.language} matched 0 files under ${paths.map((p) => positionalPathLabel(p, opts.cwd)).join(', ')}; check the flag or paths.`);
|
|
636
|
-
}
|
|
637
|
-
/* v8 ignore next */
|
|
638
|
-
if (lastResult === null)
|
|
639
|
-
return undefined;
|
|
640
|
-
const combined = {
|
|
641
|
-
catalog: lastResult.catalog,
|
|
642
|
-
indexes: lastResult.indexes,
|
|
643
|
-
signals: allSignals,
|
|
644
|
-
resolutionStats: lastResult.resolutionStats,
|
|
645
|
-
cacheHit: lastResult.cacheHit,
|
|
646
|
-
features: lastResult.features,
|
|
647
|
-
};
|
|
648
|
-
// `allSignals` is already waived per-path (each against its own root), so
|
|
649
|
-
// deliver directly — a second suppression pass would have no single correct
|
|
650
|
-
// root and risk re-resolving paths under the wrong base. Re-brand the
|
|
651
|
-
// aggregate FinalizedSignals (each member already crossed finalizeGraphSignals
|
|
652
|
-
// above) so deliverGraphResult's persist call gets the type it requires.
|
|
653
|
-
const finalizedAggregate = assertFinalizedAcrossBoundary(allSignals, totalSuppressed);
|
|
654
|
-
return await deliverGraphResult(opts, combined, cli, startedAt, finalizedAggregate);
|
|
655
|
-
}
|
|
656
307
|
/**
|
|
657
308
|
* Assemble the run's {@link SignalEnvelope} from its raw engine signals
|
|
658
309
|
* (ADR-0011). Centralises `runId`/`createdAt` resolution off the live scope so
|
|
@@ -839,198 +490,6 @@ async function renderGraphResult(opts, result, startedAt, cli) {
|
|
|
839
490
|
});
|
|
840
491
|
return envelope;
|
|
841
492
|
}
|
|
842
|
-
/**
|
|
843
|
-
* `graph --workspace` — fan a graph run out across every workspace
|
|
844
|
-
* unit returned by the adapters' `discoverWorkspaceUnits` hook. Per
|
|
845
|
-
* spec D8b, polyglot repos aggregate units from EVERY detected
|
|
846
|
-
* adapter (or the single adapter named by `--language`).
|
|
847
|
-
*/
|
|
848
|
-
async function executeWorkspaceGraph(opts, cli, profile) {
|
|
849
|
-
const cliScript = opts.cliScript ?? process.argv[1];
|
|
850
|
-
if (typeof cliScript !== 'string' || cliScript.length === 0) {
|
|
851
|
-
throw new ConfigurationError('--workspace: could not determine the CLI entry script (process.argv[1] is empty).');
|
|
852
|
-
}
|
|
853
|
-
const adapters = resolveAdaptersForRun(opts, cli);
|
|
854
|
-
const units = await discoverPolyglotUnits(opts.cwd, adapters);
|
|
855
|
-
if (units.length === 0) {
|
|
856
|
-
const adapterLabel = adapters.map((a) => a.id).join(', ') || '(no language adapters available)';
|
|
857
|
-
throw new ConfigurationError(`--workspace: no workspace units detected for [${adapterLabel}]. Use 'opensip graph' for whole-project analysis.`);
|
|
858
|
-
}
|
|
859
|
-
const profileRun = profile?.startRun({
|
|
860
|
-
label: 'workspace',
|
|
861
|
-
cwd: opts.cwd,
|
|
862
|
-
mode: 'workspace',
|
|
863
|
-
});
|
|
864
|
-
// Internal per-run timer for the workspace REPORT artifact (durationMs in the
|
|
865
|
-
// JSON document / report header + the profile stage). NOT a session timestamp:
|
|
866
|
-
// the generic session row's timing is host-owned (host-owned-run-timing Phase
|
|
867
|
-
// 3), stamped from the host RunTimer after this handler returns.
|
|
868
|
-
const startedAt = Date.now();
|
|
869
|
-
const result = await runWorkspaceUnitsInParallel({
|
|
870
|
-
cwd: opts.cwd,
|
|
871
|
-
units,
|
|
872
|
-
cliScript,
|
|
873
|
-
concurrency: opts.concurrency,
|
|
874
|
-
noCache: opts.noCache,
|
|
875
|
-
resolution: opts.resolution,
|
|
876
|
-
recipe: opts.recipe,
|
|
877
|
-
...(opts.language === undefined ? {} : { language: opts.language }),
|
|
878
|
-
});
|
|
879
|
-
const durationMs = Date.now() - startedAt;
|
|
880
|
-
profileRun?.recordStage('workspace-fanout', durationMs, `${String(units.length)} unit(s)`);
|
|
881
|
-
const allSignals = [];
|
|
882
|
-
for (const r of result.perUnit)
|
|
883
|
-
allSignals.push(...r.signals);
|
|
884
|
-
profileRun?.finishSummary({
|
|
885
|
-
cacheHit: false,
|
|
886
|
-
signals: allSignals.length,
|
|
887
|
-
});
|
|
888
|
-
// Build exactly one aggregate session contribution for the whole --workspace
|
|
889
|
-
// invocation (non-json path only). Matches the contract "one human-facing CLI
|
|
890
|
-
// invocation = one session" that fitness/sim already follow; the per-unit child
|
|
891
|
-
// processes don't contribute because they always run with --json (see
|
|
892
|
-
// dispatchGraphResult). The host run plane persists the returned `session`
|
|
893
|
-
// after this handler resolves — graph never writes the row itself
|
|
894
|
-
// (host-owned-run-timing Phase 3).
|
|
895
|
-
//
|
|
896
|
-
// Cloud signal sync (ADR-0008) is intentionally NOT emitted for --workspace
|
|
897
|
-
// (audit P1-2): the parent aggregates per-unit signals for the dashboard, not
|
|
898
|
-
// for the cloud, and the --json children skip emit to avoid fragmented
|
|
899
|
-
// per-unit batches. So the returned outcome carries a `session` but NO
|
|
900
|
-
// envelope. A whole-project `graph` run emits normally (the root's
|
|
901
|
-
// deliverSignals on the returned envelope).
|
|
902
|
-
let session;
|
|
903
|
-
if (opts.json === true) {
|
|
904
|
-
// ADR-0011: emit through the CLI seam, not process.stdout directly.
|
|
905
|
-
// cli.emitJson applies the same JSON.stringify(_, null, 2) + '\n'.
|
|
906
|
-
cli.emitJson(buildWorkspaceJsonDocument(result.perUnit, durationMs));
|
|
907
|
-
}
|
|
908
|
-
else {
|
|
909
|
-
await writeWorkspaceReport(result.perUnit, durationMs, cli);
|
|
910
|
-
session = buildWorkspaceSessionContribution(opts, allSignals);
|
|
911
|
-
}
|
|
912
|
-
// If any child failed to spawn or exited with an error, surface it
|
|
913
|
-
// as a runtime error. The parent itself succeeded if every child
|
|
914
|
-
// returned exit 0.
|
|
915
|
-
if (result.anyChildFailed) {
|
|
916
|
-
cli.setExitCode(EXIT_CODES.RUNTIME_ERROR);
|
|
917
|
-
process.stderr.write(`graph --workspace: at least one unit run failed; see per-unit output above.\n`);
|
|
918
|
-
}
|
|
919
|
-
else {
|
|
920
|
-
cli.setExitCode(EXIT_CODES.SUCCESS);
|
|
921
|
-
}
|
|
922
|
-
logger.info({
|
|
923
|
-
evt: EVT_GRAPH_COMPLETE,
|
|
924
|
-
module: MODULE_GRAPH_CLI,
|
|
925
|
-
units: result.perUnit.length,
|
|
926
|
-
findings: allSignals.length,
|
|
927
|
-
failed: result.anyChildFailed,
|
|
928
|
-
durationMs,
|
|
929
|
-
});
|
|
930
|
-
// The aggregate outcome carries the single session (non-json path) but NO
|
|
931
|
-
// envelope — the parent does not cloud-emit per-unit signals (audit P1-2).
|
|
932
|
-
// `undefined` on the --json carrier path keeps the host from persisting.
|
|
933
|
-
return session === undefined ? undefined : { session };
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Build the generic-session contribution for a single-process graph run from
|
|
937
|
-
* the branded {@link FinalizedSignals} (host-owned-run-timing Phase 3). The
|
|
938
|
-
* host run plane stamps timing + id and persists the row after the handler
|
|
939
|
-
* returns — graph never writes the generic `StoredSession` itself.
|
|
940
|
-
*
|
|
941
|
-
* Takes the branded {@link FinalizedSignals} (not a raw `Signal[]`): the only
|
|
942
|
-
* way to obtain one is to cross the single suppression chokepoint
|
|
943
|
-
* (`finalizeGraphSignals`, or `assertFinalizedAcrossBoundary` after the worker
|
|
944
|
-
* IPC boundary). This is the compile-time guardrail that makes the TTY-leak bug
|
|
945
|
-
* un-regressable — a caller that hands raw, un-waived signals here does not
|
|
946
|
-
* type-check, so the dashboard history can never record un-waived findings.
|
|
947
|
-
*
|
|
948
|
-
* The single-process path holds the raw engine signals (engine slugs), so the
|
|
949
|
-
* session payload's per-rule keys are engine slugs directly.
|
|
950
|
-
*/
|
|
951
|
-
function buildGraphSessionContribution(opts, finalized) {
|
|
952
|
-
return contributionFromSignals(opts, finalized.signals);
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* Build the aggregate generic-session contribution for a `--workspace` run.
|
|
956
|
-
*
|
|
957
|
-
* Child envelopes carry Option-A-mapped OpenSIP rule IDs; reverse-map back to
|
|
958
|
-
* engine slugs so the aggregate session payload's per-rule metric columns
|
|
959
|
-
* (keyed on engine slugs in the dashboard) keep working — exactly what the old
|
|
960
|
-
* `persistWorkspaceSession` did before handing off to the shared save.
|
|
961
|
-
*/
|
|
962
|
-
function buildWorkspaceSessionContribution(opts, signals) {
|
|
963
|
-
const engineSignals = signals.map((s) => {
|
|
964
|
-
const ruleId = mapOpenSipRuleIdToEngineSlug(s.ruleId);
|
|
965
|
-
return { ...s, ruleId, source: ruleId };
|
|
966
|
-
});
|
|
967
|
-
return contributionFromSignals(opts, engineSignals);
|
|
968
|
-
}
|
|
969
|
-
/**
|
|
970
|
-
* Shared contribution builder: derive graph's opaque session payload + the
|
|
971
|
-
* generic verdict (`score`/`passed`) from a run's engine-slug `Signal[]`. The
|
|
972
|
-
* payload is graph-owned detail (summary + rule-grouped per-signal findings);
|
|
973
|
-
* the generic session row holds zero graph vocabulary. `score`/`passed` mirror
|
|
974
|
-
* exactly what the former `saveGraphSession` computed (pass rate over
|
|
975
|
-
* passed/total rules; `passed` ⇔ no error-severity signals).
|
|
976
|
-
*/
|
|
977
|
-
/**
|
|
978
|
-
* Engine slugs of every rule a run evaluated — the session payload's full rule
|
|
979
|
-
* list, so a CLEAN run still records a PASS row per rule (the session detail
|
|
980
|
-
* then shows the complete rule list, exactly the way fitness shows every check,
|
|
981
|
-
* not just the failing ones).
|
|
982
|
-
*
|
|
983
|
-
* Prefer the EXPLICITLY-resolved rule set the run actually used (the `--recipe`
|
|
984
|
-
* subset, threaded from the dispatch seam as `args.rules`); otherwise read the
|
|
985
|
-
* current scope's full registry — this mirrors `runGraph`'s own
|
|
986
|
-
* `args.rules ?? currentRules()` resolution, so the recorded set is exactly what
|
|
987
|
-
* ran. Degrades to the empty set (fired-rules-only) when no graph scope is
|
|
988
|
-
* active — e.g. an isolated dispatch unit test; production always runs the
|
|
989
|
-
* handler inside the entered RunScope.
|
|
990
|
-
*
|
|
991
|
-
* Exported so BOTH contribution-building paths derive the evaluated set
|
|
992
|
-
* identically: the static `executeGraph` dispatch (below) AND the live Ink
|
|
993
|
-
* runner (`graph-runner.tsx`). Before it was shared, only the static path
|
|
994
|
-
* threaded it, so a clean run on the live (interactive) path persisted an empty
|
|
995
|
-
* `checks[]` and the report rendered "no results" instead of the rule list.
|
|
996
|
-
*/
|
|
997
|
-
export function evaluatedRuleSlugs(explicitRules) {
|
|
998
|
-
if (explicitRules)
|
|
999
|
-
return explicitRules.map((r) => r.slug);
|
|
1000
|
-
try {
|
|
1001
|
-
return currentRules().map((r) => r.slug);
|
|
1002
|
-
}
|
|
1003
|
-
catch {
|
|
1004
|
-
// @swallow-ok no graph scope (isolated dispatch unit test); degrade to the
|
|
1005
|
-
// fired-rule set. Production always runs the handler inside a RunScope.
|
|
1006
|
-
return [];
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
/**
|
|
1010
|
-
* Build graph's generic-session contribution from a run's engine-slug
|
|
1011
|
-
* `Signal[]` plus the engine slugs of the rules it evaluated. The SINGLE
|
|
1012
|
-
* assembly point for both the static dispatch path and the live Ink runner, so
|
|
1013
|
-
* the contribution shape (and the "every evaluated rule gets a row" behaviour)
|
|
1014
|
-
* can never drift between them again. The payload is graph-owned detail
|
|
1015
|
-
* (summary + rule-grouped per-signal findings); the generic session row holds
|
|
1016
|
-
* zero graph vocabulary. `score`/`passed` follow fit's semantics (pass rate over
|
|
1017
|
-
* passed/total rules; `passed` ⇔ no error-severity signal).
|
|
1018
|
-
*
|
|
1019
|
-
* `evaluatedSlugs` defaults to {@link evaluatedRuleSlugs}() (the scope's full
|
|
1020
|
-
* registry) for the static callers, which build inside the entered RunScope; the
|
|
1021
|
-
* live runner passes its `args.rules`-derived set explicitly.
|
|
1022
|
-
*/
|
|
1023
|
-
export function contributionFromSignals(opts, signals, evaluatedSlugs = evaluatedRuleSlugs()) {
|
|
1024
|
-
const payload = buildGraphSessionPayload(signals, evaluatedSlugs);
|
|
1025
|
-
return {
|
|
1026
|
-
tool: 'graph',
|
|
1027
|
-
cwd: opts.cwd,
|
|
1028
|
-
...(opts.recipe === undefined ? {} : { recipe: opts.recipe }),
|
|
1029
|
-
score: passRate(payload.summary),
|
|
1030
|
-
passed: payload.summary.errors === 0,
|
|
1031
|
-
payload,
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
493
|
export function handleGraphError(label, error, cli) {
|
|
1035
494
|
logger.error({
|
|
1036
495
|
evt: `graph.cli.${label}.error`,
|
|
@@ -1058,5 +517,7 @@ export function handleGraphError(label, error, cli) {
|
|
|
1058
517
|
}
|
|
1059
518
|
process.stderr.write(`${label}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1060
519
|
}
|
|
520
|
+
export { contributionFromSignals, evaluatedRuleSlugs } from './graph-session-contribution.js';
|
|
521
|
+
export { resolveLiveEngineShards, resolveShardsForCwd, runShardedLiveBuild, } from './graph-sharded-engine.js';
|
|
1061
522
|
export { buildUnifiedReportLines, buildLiveGraphOutput } from './graph-report.js';
|
|
1062
523
|
//# sourceMappingURL=graph.js.map
|