@opensip-cli/graph 0.1.4 → 0.1.6
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/__tests__/graph-command-plan.test.d.ts +2 -0
- package/dist/cli/__tests__/graph-command-plan.test.d.ts.map +1 -0
- package/dist/cli/__tests__/graph-command-plan.test.js +46 -0
- package/dist/cli/__tests__/graph-command-plan.test.js.map +1 -0
- package/dist/cli/graph-command-plan.d.ts +23 -0
- package/dist/cli/graph-command-plan.d.ts.map +1 -0
- package/dist/cli/graph-command-plan.js +43 -0
- package/dist/cli/graph-command-plan.js.map +1 -0
- 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-single-run-mode.d.ts +23 -0
- package/dist/cli/graph-single-run-mode.d.ts.map +1 -0
- package/dist/cli/graph-single-run-mode.js +107 -0
- package/dist/cli/graph-single-run-mode.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 +7 -131
- package/dist/cli/graph.d.ts.map +1 -1
- package/dist/cli/graph.js +18 -681
- 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,24 @@
|
|
|
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 {
|
|
30
|
-
import { EXIT_CODES, passRate } from '@opensip-cli/contracts';
|
|
31
|
-
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';
|
|
28
|
+
import { EXIT_CODES } from '@opensip-cli/contracts';
|
|
29
|
+
import { ConfigurationError, currentScope, logger, ToolError, ValidationError, } from '@opensip-cli/core';
|
|
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 { planGraphExecution } from './graph-command-plan.js';
|
|
40
34
|
import { runCatalogJsonMode, runGateMode } from './graph-modes.js';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
35
|
+
import { executeMultiPathGraph } from './graph-multi-path-mode.js';
|
|
36
|
+
import { buildUnifiedReportLines, resolutionBannerText } from './graph-report.js';
|
|
37
|
+
import { buildGraphSessionContribution } from './graph-session-contribution.js';
|
|
38
|
+
import { executeSinglePathGraph } from './graph-single-run-mode.js';
|
|
39
|
+
import { executeWorkspaceGraph } from './graph-workspace-mode.js';
|
|
40
|
+
import { resolveGraphRecipeSelection } from './orchestrate.js';
|
|
47
41
|
import { MemoryPressureError } from './pressure-monitor.js';
|
|
48
42
|
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
43
|
const EVT_GRAPH_COMPLETE = 'graph.cli.graph.complete';
|
|
53
44
|
const MODULE_GRAPH_CLI = 'graph:cli';
|
|
54
45
|
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
46
|
/**
|
|
63
47
|
* Run graph and return the run's {@link GraphRunOutcome} — the deliverable
|
|
64
48
|
* {@link SignalEnvelope} (so the composition root can cloud + `--report-to`
|
|
@@ -105,7 +89,7 @@ export async function executeGraph(opts, cli) {
|
|
|
105
89
|
});
|
|
106
90
|
// (profile / startedAtForProfile already declared at top of fn for branch visibility)
|
|
107
91
|
try {
|
|
108
|
-
|
|
92
|
+
const plan = planGraphExecution(opts);
|
|
109
93
|
// Resolve the recipe once at the top of the run (CLI layer owns selection;
|
|
110
94
|
// the engine stays recipe-agnostic). Tool-scoped (ADR-0022): precedence is
|
|
111
95
|
// `--recipe` flag > `graph.recipe` > `default`.
|
|
@@ -125,107 +109,17 @@ export async function executeGraph(opts, cli) {
|
|
|
125
109
|
// request-scoped parsed-options bag the pre-action hook already augments, so
|
|
126
110
|
// this is the single point that owns graph's recipe normalization.
|
|
127
111
|
opts.recipe = recipeSelection.name;
|
|
128
|
-
if (
|
|
112
|
+
if (plan.shape === 'workspace') {
|
|
129
113
|
const outcome = await executeWorkspaceGraph(opts, cli, profile);
|
|
130
114
|
writeProfileIfRequested(opts, profile);
|
|
131
115
|
return outcome;
|
|
132
116
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const outcome = await executeMultiPathGraph({ opts, cli, rules, startedAt, profile }, positionalPaths);
|
|
117
|
+
if (plan.shape === 'multi-path') {
|
|
118
|
+
const outcome = await executeMultiPathGraph({ opts, cli, rules, startedAt, profile, deliverGraphResult }, plan.positionalPaths);
|
|
136
119
|
writeProfileIfRequested(opts, profile);
|
|
137
120
|
return outcome;
|
|
138
121
|
}
|
|
139
|
-
|
|
140
|
-
// EXACT engine normalizes its project dir via realpathSync internally
|
|
141
|
-
// (graph-typescript normalize-project-dir); the SHARDED worker derives
|
|
142
|
-
// project-relative `code.file` paths against this `projectRoot`. Under a
|
|
143
|
-
// symlinked cwd a RAW root would make the sharded paths gain `../..` prefixes
|
|
144
|
-
// while exact stays canonical → the two engines emit different `code.file`,
|
|
145
|
-
// and since the engine choice is environment-sensitive, a `--gate-save`
|
|
146
|
-
// baseline written by one engine would flag everything new under the other.
|
|
147
|
-
// Canonicalizing here (idempotent for non-symlinks) keeps both engines'
|
|
148
|
-
// emitted paths byte-identical. Shard discovery already realpaths internally
|
|
149
|
-
// (discoverFiles → normalizeProjectDir), so the shard files and this root now
|
|
150
|
-
// share the same canonical base.
|
|
151
|
-
const runCwd = realpathOrSelf(positionalPaths[0] ?? opts.cwd, opts.cwd);
|
|
152
|
-
// Honor the project's `graph:` config block (rule knobs like
|
|
153
|
-
// minCrossPackageDuplicatePackages). Resolved from the original cwd so a
|
|
154
|
-
// positional subtree run still picks up the project-root config.
|
|
155
|
-
const config = loadGraphConfig(opts.cwd);
|
|
156
|
-
// Determinism (ADR-0033, superseding ADR-0032/0031): the build engine is
|
|
157
|
-
// chosen by an explicit, deterministic policy — the SHARDED engine is the
|
|
158
|
-
// DEFAULT (both engines resolve through one shared model — exact = the
|
|
159
|
-
// 1-shard case — held equivalent by the directional soundness invariant +
|
|
160
|
-
// completeness floor; ADR-0033), and
|
|
161
|
-
// `--exact` opts OUT to the single-program engine. It is NOT chosen by
|
|
162
|
-
// `process.stdout.isTTY` or on-disk discovery state, so a bare `graph` builds
|
|
163
|
-
// the same catalog whether run in a terminal, piped, or under
|
|
164
|
-
// `--gate-*`/`--json`. When the project can't shard (no worker script,
|
|
165
|
-
// single-unit, discovery failure) we fall back to the exact engine — the
|
|
166
|
-
// natural single-package/small-repo path — rather than failing.
|
|
167
|
-
const resolution = await resolveEngineShards(opts, cli, positionalPaths);
|
|
168
|
-
const shards = resolution.shards;
|
|
169
|
-
logger.info({
|
|
170
|
-
evt: 'graph.cli.graph.engine',
|
|
171
|
-
module: MODULE_GRAPH_CLI,
|
|
172
|
-
mode: shards.length > 1 ? 'sharded' : 'exact',
|
|
173
|
-
requestedExact: opts.exact === true,
|
|
174
|
-
shards: shards.length,
|
|
175
|
-
reason: engineSelectionReason(opts, positionalPaths, shards.length > 1),
|
|
176
|
-
});
|
|
177
|
-
const profileRun = profile?.startRun({
|
|
178
|
-
label: positionalPaths.length === 0 ? 'root' : positionalPathLabel(runCwd, opts.cwd),
|
|
179
|
-
cwd: runCwd,
|
|
180
|
-
mode: shards.length > 1 ? 'sharded' : 'single-process',
|
|
181
|
-
});
|
|
182
|
-
// The synthetic partitioner runs BEFORE the profile run recorder exists
|
|
183
|
-
// (its `mode` label needs the shard count), so its wall time is measured
|
|
184
|
-
// where it runs and recorded here (ADR-0045 measurement plane).
|
|
185
|
-
if (resolution.partition !== undefined) {
|
|
186
|
-
profileRun?.recordStage('partition', resolution.partition.durationMs, resolution.partition.detail);
|
|
187
|
-
}
|
|
188
|
-
const result = shards.length > 1
|
|
189
|
-
? await runProfiledShardedBuild(profileRun, {
|
|
190
|
-
opts,
|
|
191
|
-
shards,
|
|
192
|
-
projectRoot: runCwd,
|
|
193
|
-
cli,
|
|
194
|
-
config,
|
|
195
|
-
rules,
|
|
196
|
-
})
|
|
197
|
-
: await runGraph({
|
|
198
|
-
cwd: runCwd,
|
|
199
|
-
noCache: opts.noCache,
|
|
200
|
-
resolution: opts.resolution,
|
|
201
|
-
language: opts.language,
|
|
202
|
-
config,
|
|
203
|
-
rules,
|
|
204
|
-
datastore: cli.scope.datastore(),
|
|
205
|
-
emitFeatures: DASHBOARD_FEATURE_COLUMNS,
|
|
206
|
-
onProgress: profileRun?.onProgress,
|
|
207
|
-
});
|
|
208
|
-
profileRun?.finish(result);
|
|
209
|
-
// Propagate shard failures so incomplete catalogs do not silently produce
|
|
210
|
-
// baselines or pass --gate-compare (per-audit: failedShardIds was computed
|
|
211
|
-
// but never surfaced to the gate or as a hard error).
|
|
212
|
-
if (shards.length > 1) {
|
|
213
|
-
const sharded = result;
|
|
214
|
-
if (sharded.failedShardIds && sharded.failedShardIds.length > 0) {
|
|
215
|
-
throw new SystemError(`Sharded graph build had ${sharded.failedShardIds.length} shard failure(s); ` +
|
|
216
|
-
`catalog and any --gate-* / baseline artifacts are incomplete. ` +
|
|
217
|
-
`See 'graph.sharded.shard_failed' log events for per-shard details.`, { code: 'GRAPH.SHARD.FAILURES' });
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
enforceLanguageMismatchPolicy(opts, result.catalog, [runCwd]);
|
|
221
|
-
currentScope()?.diagnostics?.event('execute', 'debug', 'graph build complete', {
|
|
222
|
-
mode: shards.length > 1 ? 'sharded' : 'exact',
|
|
223
|
-
shards: shards.length,
|
|
224
|
-
});
|
|
225
|
-
// `runCwd` (= positionalPaths[0] ?? opts.cwd) is the build root the signals
|
|
226
|
-
// are relative to — the correct base for resolving `@graph-ignore` directive
|
|
227
|
-
// files. For the sharded build it equals `projectRoot` passed above.
|
|
228
|
-
const outcome = await dispatchGraphResult(opts, result, cli, startedAt, runCwd);
|
|
122
|
+
const outcome = await executeSinglePathGraph({ opts, cli, rules, startedAt, profile, dispatchGraphResult }, plan.positionalPaths);
|
|
229
123
|
writeProfileIfRequested(opts, profile);
|
|
230
124
|
return outcome;
|
|
231
125
|
}
|
|
@@ -255,277 +149,6 @@ export async function executeGraph(opts, cli) {
|
|
|
255
149
|
* no worker script is available — a graceful fall-through to exact, the
|
|
256
150
|
* natural single-package path.
|
|
257
151
|
*/
|
|
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
152
|
/** Profile bucket for the run shape: workspace fan-out, multi-path, or single graph. */
|
|
530
153
|
function profileMode(opts) {
|
|
531
154
|
if (opts.workspace === true)
|
|
@@ -557,102 +180,6 @@ function writeProfileIfRequested(opts, profile) {
|
|
|
557
180
|
output: outPath,
|
|
558
181
|
});
|
|
559
182
|
}
|
|
560
|
-
function validateMutuallyExclusiveFlags(opts) {
|
|
561
|
-
if (opts.gateSave === true && opts.gateCompare === true) {
|
|
562
|
-
throw new ConfigurationError('--gate-save and --gate-compare are mutually exclusive.');
|
|
563
|
-
}
|
|
564
|
-
if (opts.workspace === true && (opts.paths?.length ?? 0) > 0) {
|
|
565
|
-
throw new ConfigurationError('--workspace and positional paths are mutually exclusive. Use one or the other.');
|
|
566
|
-
}
|
|
567
|
-
if (opts.workspace === true && (opts.gateSave === true || opts.gateCompare === true)) {
|
|
568
|
-
throw new ConfigurationError('--workspace and --gate-save/--gate-compare are mutually exclusive. ' +
|
|
569
|
-
'Gates and baselines apply to production code; --workspace intentionally scans the full project (including dependencies and test fixtures).');
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
function resolvePositionalScope(opts) {
|
|
573
|
-
if (!opts.paths || opts.paths.length === 0)
|
|
574
|
-
return [];
|
|
575
|
-
return resolvePositionalPaths(opts.paths, opts.cwd);
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* D14 mixed policy. When `--language X` was specified and the run
|
|
579
|
-
* discovered zero files matching that adapter, exit 2 with the
|
|
580
|
-
* canonical error. Auto-detection paths (no `--language`) do NOT
|
|
581
|
-
* trigger this check — a "zero files" outcome there is a valid
|
|
582
|
-
* (non-error) state.
|
|
583
|
-
*/
|
|
584
|
-
function enforceLanguageMismatchPolicy(opts, catalog, paths) {
|
|
585
|
-
if (typeof opts.language !== 'string' || opts.language.length === 0)
|
|
586
|
-
return;
|
|
587
|
-
const fileCount = catalog === null ? 0 : countFiles(catalog);
|
|
588
|
-
if (fileCount > 0)
|
|
589
|
-
return;
|
|
590
|
-
const pathLabel = paths.map((p) => positionalPathLabel(p, opts.cwd)).join(', ');
|
|
591
|
-
throw new ConfigurationError(`--language ${opts.language} matched 0 files under ${pathLabel}; check the flag or paths.`);
|
|
592
|
-
}
|
|
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
183
|
/**
|
|
657
184
|
* Assemble the run's {@link SignalEnvelope} from its raw engine signals
|
|
658
185
|
* (ADR-0011). Centralises `runId`/`createdAt` resolution off the live scope so
|
|
@@ -839,198 +366,6 @@ async function renderGraphResult(opts, result, startedAt, cli) {
|
|
|
839
366
|
});
|
|
840
367
|
return envelope;
|
|
841
368
|
}
|
|
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
369
|
export function handleGraphError(label, error, cli) {
|
|
1035
370
|
logger.error({
|
|
1036
371
|
evt: `graph.cli.${label}.error`,
|
|
@@ -1058,5 +393,7 @@ export function handleGraphError(label, error, cli) {
|
|
|
1058
393
|
}
|
|
1059
394
|
process.stderr.write(`${label}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1060
395
|
}
|
|
396
|
+
export { contributionFromSignals, evaluatedRuleSlugs } from './graph-session-contribution.js';
|
|
397
|
+
export { resolveLiveEngineShards, resolveShardsForCwd, runShardedLiveBuild, } from './graph-sharded-engine.js';
|
|
1061
398
|
export { buildUnifiedReportLines, buildLiveGraphOutput } from './graph-report.js';
|
|
1062
399
|
//# sourceMappingURL=graph.js.map
|