@opensip-cli/graph 0.1.6 → 0.1.8

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.
Files changed (99) hide show
  1. package/README.md +2 -2
  2. package/dist/__tests__/cli/graph-execute.test.js +7 -5
  3. package/dist/__tests__/cli/graph-execute.test.js.map +1 -1
  4. package/dist/__tests__/cli/graph.test.js +18 -12
  5. package/dist/__tests__/cli/graph.test.js.map +1 -1
  6. package/dist/__tests__/internal-surface.test.js +3 -0
  7. package/dist/__tests__/internal-surface.test.js.map +1 -1
  8. package/dist/__tests__/persistence/session-replay.test.js +7 -4
  9. package/dist/__tests__/persistence/session-replay.test.js.map +1 -1
  10. package/dist/__tests__/rules/registry.test.js +2 -2
  11. package/dist/__tests__/rules/registry.test.js.map +1 -1
  12. package/dist/__tests__/test-utils/with-graph-scope.d.ts.map +1 -1
  13. package/dist/__tests__/test-utils/with-graph-scope.js +2 -2
  14. package/dist/__tests__/test-utils/with-graph-scope.js.map +1 -1
  15. package/dist/__tests__/tool-branches.test.d.ts +3 -3
  16. package/dist/__tests__/tool-branches.test.js +25 -14
  17. package/dist/__tests__/tool-branches.test.js.map +1 -1
  18. package/dist/__tests__/tool-register.test.js +61 -77
  19. package/dist/__tests__/tool-register.test.js.map +1 -1
  20. package/dist/__tests__/tool.test.d.ts +4 -4
  21. package/dist/__tests__/tool.test.js +31 -16
  22. package/dist/__tests__/tool.test.js.map +1 -1
  23. package/dist/cli/__tests__/build-envelope.test.js +77 -0
  24. package/dist/cli/__tests__/build-envelope.test.js.map +1 -1
  25. package/dist/cli/__tests__/graph-envelope-view.test.d.ts +2 -0
  26. package/dist/cli/__tests__/graph-envelope-view.test.d.ts.map +1 -0
  27. package/dist/cli/__tests__/graph-envelope-view.test.js +97 -0
  28. package/dist/cli/__tests__/graph-envelope-view.test.js.map +1 -0
  29. package/dist/cli/__tests__/graph-list.test.d.ts +2 -0
  30. package/dist/cli/__tests__/graph-list.test.d.ts.map +1 -0
  31. package/dist/cli/__tests__/graph-list.test.js +20 -0
  32. package/dist/cli/__tests__/graph-list.test.js.map +1 -0
  33. package/dist/cli/__tests__/shard-worker.test.js +22 -0
  34. package/dist/cli/__tests__/shard-worker.test.js.map +1 -1
  35. package/dist/cli/equivalence-check-command.d.ts.map +1 -1
  36. package/dist/cli/equivalence-check-command.js +0 -2
  37. package/dist/cli/equivalence-check-command.js.map +1 -1
  38. package/dist/cli/graph/graph-aux-command-specs.d.ts +54 -27
  39. package/dist/cli/graph/graph-aux-command-specs.d.ts.map +1 -1
  40. package/dist/cli/graph/graph-aux-command-specs.js +359 -265
  41. package/dist/cli/graph/graph-aux-command-specs.js.map +1 -1
  42. package/dist/cli/graph-envelope-view.d.ts +41 -0
  43. package/dist/cli/graph-envelope-view.d.ts.map +1 -0
  44. package/dist/cli/graph-envelope-view.js +133 -0
  45. package/dist/cli/graph-envelope-view.js.map +1 -0
  46. package/dist/cli/graph-list.d.ts +29 -0
  47. package/dist/cli/graph-list.d.ts.map +1 -0
  48. package/dist/cli/graph-list.js +44 -0
  49. package/dist/cli/graph-list.js.map +1 -0
  50. package/dist/cli/graph-report.d.ts +16 -6
  51. package/dist/cli/graph-report.d.ts.map +1 -1
  52. package/dist/cli/graph-report.js +10 -6
  53. package/dist/cli/graph-report.js.map +1 -1
  54. package/dist/cli/graph-runner.d.ts.map +1 -1
  55. package/dist/cli/graph-runner.js +23 -10
  56. package/dist/cli/graph-runner.js.map +1 -1
  57. package/dist/cli/graph-worker.d.ts.map +1 -1
  58. package/dist/cli/graph-worker.js +1 -0
  59. package/dist/cli/graph-worker.js.map +1 -1
  60. package/dist/cli/graph.d.ts.map +1 -1
  61. package/dist/cli/graph.js +30 -25
  62. package/dist/cli/graph.js.map +1 -1
  63. package/dist/cli/lookup.d.ts.map +1 -1
  64. package/dist/cli/lookup.js +0 -2
  65. package/dist/cli/lookup.js.map +1 -1
  66. package/dist/cli/orchestrate/__tests__/shard-runner-correlation.test.d.ts +22 -0
  67. package/dist/cli/orchestrate/__tests__/shard-runner-correlation.test.d.ts.map +1 -0
  68. package/dist/cli/orchestrate/__tests__/shard-runner-correlation.test.js +208 -0
  69. package/dist/cli/orchestrate/__tests__/shard-runner-correlation.test.js.map +1 -0
  70. package/dist/cli/orchestrate/shard-model.d.ts +17 -0
  71. package/dist/cli/orchestrate/shard-model.d.ts.map +1 -1
  72. package/dist/cli/orchestrate/shard-runner.d.ts +31 -1
  73. package/dist/cli/orchestrate/shard-runner.d.ts.map +1 -1
  74. package/dist/cli/orchestrate/shard-runner.js +215 -11
  75. package/dist/cli/orchestrate/shard-runner.js.map +1 -1
  76. package/dist/cli/orchestrate/sharded-graph.d.ts.map +1 -1
  77. package/dist/cli/orchestrate/sharded-graph.js +32 -3
  78. package/dist/cli/orchestrate/sharded-graph.js.map +1 -1
  79. package/dist/cli/shard-worker.d.ts.map +1 -1
  80. package/dist/cli/shard-worker.js +127 -11
  81. package/dist/cli/shard-worker.js.map +1 -1
  82. package/dist/cli/symbol-index.d.ts.map +1 -1
  83. package/dist/cli/symbol-index.js +0 -2
  84. package/dist/cli/symbol-index.js.map +1 -1
  85. package/dist/internal.d.ts +1 -0
  86. package/dist/internal.d.ts.map +1 -1
  87. package/dist/internal.js +5 -0
  88. package/dist/internal.js.map +1 -1
  89. package/dist/persistence/schema.d.ts.map +1 -1
  90. package/dist/persistence/schema.js +18 -0
  91. package/dist/persistence/schema.js.map +1 -1
  92. package/dist/persistence/session-replay.d.ts +9 -4
  93. package/dist/persistence/session-replay.d.ts.map +1 -1
  94. package/dist/persistence/session-replay.js +8 -12
  95. package/dist/persistence/session-replay.js.map +1 -1
  96. package/dist/tool.d.ts.map +1 -1
  97. package/dist/tool.js +37 -118
  98. package/dist/tool.js.map +1 -1
  99. package/package.json +22 -26
@@ -1,21 +1,27 @@
1
- // @fitness-ignore-file no-direct-stdout-in-tool-engine -- auxiliary subcommand status line: `graph-baseline-export` writes the JSON baseline to a file and prints a one-line "Exported graph baseline to <path>" confirmation (the --json path uses cli.emitJson). This is not the signal-envelope run output (ADR-0011), which routes through the composition root.
1
+ // @fitness-ignore-file no-direct-stdout-in-tool-engine -- auxiliary subcommand status line: `graph export --format baseline` writes the JSON baseline to a file and prints a one-line "Exported graph baseline to <path>" confirmation (the --json path uses cli.emitJson). This is not the signal-envelope run output (ADR-0011), which routes through the composition root.
2
2
  // @fitness-ignore-file detached-promises -- async command handlers invoke synchronous helpers (runCatalogJsonMode/runSarifExportMode/handleGraphError all return void); the heuristic flags them inside the async handlers. Matches the sibling graph CLI files (graph.ts, graph-modes.ts, orchestrate.ts).
3
3
  // @fitness-ignore-file only-documented-toolcli-seams -- same rationale as the no-direct-stdout waiver above: the one-line "Exported graph baseline to <path>" status confirmation after a file write; the --json path uses cli.emitJson. Not run output through a ToolCliContext seam.
4
4
  /**
5
- * graph-aux-command-specs — the declarative graph auxiliary commands (release
6
- * launch Phase 5 Task 5.2).
5
+ * graph-aux-command-specs — the declarative graph auxiliary commands.
7
6
  *
8
- * Replaces graph's hand-rolled `registerGraph*Command()` bodies. The host mounts
9
- * each spec via `mountCommandSpec`; the tool no longer touches Commander. Each
10
- * helper's raw `.option()`/`.argument()` calls translate 1:1 to
7
+ * The host mounts each spec via `mountCommandSpec`; the tool no longer touches
8
+ * Commander. Each helper's raw `.option()`/`.argument()` calls translate 1:1 to
11
9
  * `OptionSpec`/`ArgSpec`; positional arguments arrive on the parsed-opts object
12
10
  * under the `_args` key (the host's uniform positional convention — see
13
11
  * `mountCommandSpec`).
14
12
  *
13
+ * The canonical surface is the nested `<tool> <verb>` grammar — `graph recipes`
14
+ * / `graph lookup` / `graph index` / `graph list` / `graph export` all mount as
15
+ * children of the `graph` primary (`parent: 'graph'`). The legacy flat-root
16
+ * aliases (`graph-recipes` / `graph-lookup` / `graph-symbol-index` /
17
+ * `graph-baseline-export` / `catalog-export` / `sarif-export`) were removed once
18
+ * their deprecation window closed.
19
+ *
15
20
  * Output modes:
16
- * - `graph-recipes` → `command-result`: the handler returns the list result;
17
- * the host dispatches it through the shared seam (`--json` → JSON, else
18
- * render). Byte-identical to the former `if (json) emitJson else render` body.
21
+ * - `graph recipes` / `graph list` → `command-result`: the handler returns the
22
+ * list result; the host dispatches it through the shared seam (`--json` →
23
+ * JSON, else render). Byte-identical to the former `if (json) emitJson else
24
+ * render` body.
19
25
  * - every other aux command → `raw-stream`: each owns its full IO (writes a
20
26
  * file and/or prints a line, sets its own exit code, owns its `--json`
21
27
  * branch) — the documented non-Ink exception. The host renders nothing.
@@ -23,6 +29,7 @@
23
29
  import { commonFlags, EXIT_CODES } from '@opensip-cli/contracts';
24
30
  import { ConfigurationError, defineCommand, logger } from '@opensip-cli/core';
25
31
  import { executeEquivalenceCheck } from '../equivalence-check-command.js';
32
+ import { listGraphRules } from '../graph-list.js';
26
33
  import { runCatalogJsonMode } from '../graph-modes.js';
27
34
  import { listGraphRecipes } from '../graph-recipes.js';
28
35
  import { handleGraphError } from '../graph.js';
@@ -32,38 +39,202 @@ import { runSarifExportMode } from '../sarif-export.js';
32
39
  import { executeShardWorker } from '../shard-worker.js';
33
40
  import { executeSymbolIndex } from '../symbol-index.js';
34
41
  // Shared --cwd flag string for the auxiliary subcommands that declare it as a
35
- // tool option (symbol-index keeps a custom description; the export commands keep
36
- // the canonical one). Sourced from the ADR-0021 common-flag registry so the
37
- // string matches the run command's --cwd and cannot drift.
42
+ // tool option (the index command keeps a custom description). Sourced from the
43
+ // ADR-0021 common-flag registry so the string matches the run command's --cwd
44
+ // and cannot drift.
38
45
  const OPT_CWD = commonFlags.cwd.flags;
39
- const OPT_DESC_CWD = commonFlags.cwd.description;
40
- // Shared output mode for the file-/stdout-writing aux commands (every aux
41
- // command except graph-recipes owns its full IO). Extracted to a const so the
42
- // literal is declared once (sonarjs/no-duplicate-string) and stays a typed
43
- // CommandOutputMode member.
46
+ // Shared output mode for the file-/stdout-writing aux commands. Extracted to a
47
+ // const so the literal is declared once (sonarjs/no-duplicate-string) and stays
48
+ // a typed CommandOutputMode member.
44
49
  const RAW_STREAM = 'raw-stream';
50
+ // Shared `rawStreamReason` for the file-writing aux commands (the index command
51
+ // + the canonical `graph export`). Declared once so the literal is not
52
+ // duplicated (sonarjs/no-duplicate-string).
53
+ const REASON_FILE_EXPORT = 'file-export';
45
54
  /** Read the single trailing positional (`<name>` / `<specPath>`) off the parsed opts. */
46
55
  function firstArg(opts) {
47
56
  const args = (opts._args ?? []);
48
57
  return args[0] ?? '';
49
58
  }
50
- /** `graph-lookup` — look up function occurrences by simple name. */
51
- export const graphLookupCommandSpec = defineCommand({
52
- name: 'graph-lookup',
53
- description: 'Look up function occurrences by simple name from the persisted catalog',
54
- commonFlags: ['json'],
55
- args: [{ name: 'name', description: 'Function simple name to look up (e.g. "saveBaseline")' }],
56
- scope: 'project',
57
- output: RAW_STREAM,
58
- rawStreamReason: 'lookup',
59
- handler: async (rawOpts, cli) => {
60
- const opts = rawOpts;
61
- await executeLookup({ name: firstArg(opts), json: opts.json }, cli);
62
- },
63
- });
59
+ // =============================================================================
60
+ // EXPORT OPTION SPECS (the canonical `graph export --format <fmt>` command)
61
+ //
62
+ // The canonical `graph export` command declares `--format <fmt>` (required) plus
63
+ // the UNION of the per-format flags as OPTIONAL and validates the required
64
+ // subset per format at runtime. Each OptionSpec is declared once here.
65
+ // =============================================================================
66
+ /** `--out` for the baseline export — JSON fingerprints. */
67
+ const OPT_BASELINE_OUT = {
68
+ flag: '--out',
69
+ value: '<path>',
70
+ description: 'Output file path for the JSON baseline',
71
+ };
72
+ /** `--catalog-output` (catalog export). */
73
+ const OPT_CATALOG_OUTPUT = {
74
+ flag: '--catalog-output',
75
+ value: '<path>',
76
+ description: 'Output file path for the CatalogExport JSON',
77
+ };
78
+ /** `--output-sarif` (sarif export). */
79
+ const OPT_OUTPUT_SARIF = {
80
+ flag: '--output-sarif',
81
+ value: '<path>',
82
+ description: 'Output file path for the SARIF v2.1.0 document',
83
+ };
84
+ /** `--tenant-id` (catalog/sarif export). */
85
+ const OPT_TENANT_ID_CATALOG = {
86
+ flag: '--tenant-id',
87
+ value: '<id>',
88
+ description: 'Tenant scope stamped on every row + provenance',
89
+ };
90
+ /** `--repo-id` (catalog/sarif export). */
91
+ const OPT_REPO_ID_CATALOG = {
92
+ flag: '--repo-id',
93
+ value: '<id>',
94
+ description: 'Repository scope stamped on every row',
95
+ };
96
+ /** `--git-sha` (catalog export). */
97
+ const OPT_GIT_SHA = {
98
+ flag: '--git-sha',
99
+ value: '<sha>',
100
+ description: 'Commit SHA the catalog was extracted at',
101
+ };
102
+ /** `--run-id` (catalog/sarif export). */
103
+ const OPT_RUN_ID_CATALOG = {
104
+ flag: '--run-id',
105
+ value: '<uuid>',
106
+ description: 'Run id for provenance (auto-generated if absent)',
107
+ };
108
+ /** `--mode` (catalog export). */
109
+ const OPT_MODE = {
110
+ flag: '--mode',
111
+ value: '<mode>',
112
+ description: "'initial' (full rebuild) or 'incremental' (reuse cache when present)",
113
+ default: 'initial',
114
+ };
115
+ /** `--changed-file` (catalog export) — repeatable accumulator. */
116
+ const OPT_CHANGED_FILE = {
117
+ flag: '--changed-file',
118
+ value: '<relPath>',
119
+ description: 'Changed file (repeatable). Advisory today — the engine derives the true changed set from fingerprint diffs; recorded for observability.',
120
+ arrayDefault: [],
121
+ parse: (val, prev) => [...prev, val],
122
+ };
123
+ /** `--language` (catalog/sarif export). */
124
+ const OPT_LANGUAGE = {
125
+ flag: '--language',
126
+ value: '<name>',
127
+ description: 'Force a specific language adapter (suppresses auto-detection)',
128
+ };
129
+ /** `--resolution` (catalog/sarif export) — host-validated choices enum. */
130
+ const OPT_RESOLUTION = {
131
+ flag: '--resolution',
132
+ value: '<mode>',
133
+ description: 'Edge resolution tier: exact (semantic) or fast (syntactic, no type checker)',
134
+ default: 'exact',
135
+ choices: ['exact', 'fast'],
136
+ };
137
+ /** Run graph analysis and write OpenSIP-convention SARIF to `--output-sarif`. */
138
+ async function runGraphSarifExport(opts, cli) {
139
+ try {
140
+ const resolution = opts.resolution === 'fast' ? 'fast' : 'exact';
141
+ const result = await runGraph({
142
+ cwd: opts.cwd,
143
+ noCache: true,
144
+ resolution,
145
+ language: opts.language,
146
+ datastore: cli.scope.datastore(),
147
+ });
148
+ await runSarifExportMode({
149
+ outputSarif: opts.outputSarif,
150
+ tenantId: opts.tenantId,
151
+ repoId: opts.repoId,
152
+ runId: opts.runId,
153
+ }, result.signals, cli);
154
+ }
155
+ catch (error) {
156
+ handleGraphError('sarif-export', error, cli);
157
+ }
158
+ }
159
+ /** Run graph analysis and write the CatalogExport JSON to `--catalog-output`. */
160
+ async function runGraphCatalogExport(opts, cli) {
161
+ const startedAt = new Date().toISOString();
162
+ try {
163
+ // `--resolution`'s value is `exact`/`fast` by construction (declared
164
+ // `choices`); the mount layer rejected any other value before we got here.
165
+ const resolution = opts.resolution === 'fast' ? 'fast' : 'exact';
166
+ const incremental = opts.mode === 'incremental';
167
+ const changedFiles = opts.changedFile ?? [];
168
+ if (incremental && changedFiles.length > 0) {
169
+ // Advisory only: the incremental path self-derives the changed set from
170
+ // on-disk fingerprint diffs, so a caller-supplied set does not (yet)
171
+ // narrow the walk. Logged for observability.
172
+ logger.info({
173
+ evt: 'graph.cli.catalog_export.changed_files_advisory',
174
+ module: 'graph:cli',
175
+ runId: opts.runId,
176
+ changedFileCount: changedFiles.length,
177
+ });
178
+ }
179
+ const result = await runGraph({
180
+ cwd: opts.cwd,
181
+ noCache: !incremental,
182
+ resolution,
183
+ language: opts.language,
184
+ datastore: cli.scope.datastore(),
185
+ });
186
+ runCatalogJsonMode({
187
+ cwd: opts.cwd,
188
+ catalogOutput: opts.catalogOutput,
189
+ tenantId: opts.tenantId,
190
+ repoId: opts.repoId,
191
+ gitSha: opts.gitSha,
192
+ runId: opts.runId,
193
+ }, result, cli, startedAt);
194
+ }
195
+ catch (error) {
196
+ handleGraphError('catalog-export', error, cli);
197
+ }
198
+ }
199
+ /**
200
+ * Export the graph gate fingerprint baseline (JSON) to `--out` via the host
201
+ * baseline seam (ADR-0036). Maps the ConfigurationError "no baseline" path to
202
+ * exit 2 for both the `--json` and plain-text boundaries.
203
+ */
204
+ async function runGraphBaselineExport(opts, cli) {
205
+ try {
206
+ await cli.exportBaselineFingerprints('graph', opts.out);
207
+ }
208
+ catch (error) {
209
+ const message = error instanceof Error ? error.message : String(error);
210
+ const exitCode = error instanceof ConfigurationError
211
+ ? EXIT_CODES.CONFIGURATION_ERROR
212
+ : EXIT_CODES.RUNTIME_ERROR;
213
+ logger.warn({
214
+ evt: 'cli.graph.baseline_export.failed',
215
+ module: 'graph:cli',
216
+ message,
217
+ exitCode,
218
+ });
219
+ if (opts.json === true) {
220
+ cli.emitError({ message, exitCode });
221
+ return;
222
+ }
223
+ cli.setExitCode(exitCode);
224
+ process.stderr.write(`Error: ${message}\n`);
225
+ return;
226
+ }
227
+ const result = { type: 'graph-baseline-export', outPath: opts.out };
228
+ if (opts.json === true) {
229
+ cli.emitJson(result);
230
+ return;
231
+ }
232
+ process.stdout.write(`Exported graph baseline to ${opts.out}\n`);
233
+ }
64
234
  /** `graph-shard-worker` — [internal] build one shard from a spec file. */
65
235
  export const graphShardWorkerCommandSpec = defineCommand({
66
236
  name: 'graph-shard-worker',
237
+ visibility: 'internal',
67
238
  description: '[internal] Build one shard from a spec file and emit a ShardBuildResult JSON (spawned by the sharded build)',
68
239
  commonFlags: [],
69
240
  args: [{ name: 'specPath', description: 'Path to a JSON ShardWorkerSpec file' }],
@@ -84,6 +255,7 @@ export const graphShardWorkerCommandSpec = defineCommand({
84
255
  */
85
256
  export const graphEquivalenceCheckCommandSpec = defineCommand({
86
257
  name: 'graph-equivalence-check',
258
+ visibility: 'internal',
87
259
  description: '[internal] Verify the sharded build is byte-equivalent to the exact build on a real repo (gates production edge divergence against a committed budget)',
88
260
  commonFlags: [],
89
261
  options: [
@@ -111,280 +283,202 @@ export const graphEquivalenceCheckCommandSpec = defineCommand({
111
283
  await executeEquivalenceCheck({ cwd: opts.cwd, budget: opts.budget, updateBudget: opts.updateBudget }, cli);
112
284
  },
113
285
  });
114
- /** `graph-symbol-index` — emit a symbolindex.json artifact. */
115
- export const graphSymbolIndexCommandSpec = defineCommand({
116
- name: 'graph-symbol-index',
117
- description: 'Emit a symbolindex.json artifact (name→file:line and file→names) from the persisted catalog',
118
- commonFlags: [],
119
- options: [
120
- // --cwd keeps its command-specific description (the out path resolves
121
- // against it), so it is declared as a tool option rather than the common
122
- // flag. The literal default is `process.cwd()`, evaluated once at module
123
- // load (CLI startup) equivalent to the former register-time evaluation.
124
- {
125
- flag: OPT_CWD,
126
- description: 'Target directory (out path resolves against this)',
127
- default: process.cwd(),
128
- },
129
- {
130
- flag: '--out',
131
- value: '<path>',
132
- description: 'Output file path',
133
- default: 'symbolindex.json',
134
- },
135
- ],
136
- scope: 'project',
137
- output: RAW_STREAM,
138
- rawStreamReason: 'file-export',
139
- handler: (rawOpts, cli) => {
140
- const opts = rawOpts;
141
- executeSymbolIndex({ cwd: opts.cwd, out: opts.out }, cli);
142
- },
143
- });
144
- /** `graph-baseline-export` — export the graph gate baseline (JSON). */
145
- export const graphBaselineExportCommandSpec = defineCommand({
146
- name: 'graph-baseline-export',
147
- description: 'Export the graph gate baseline (JSON) from the datastore to a file',
286
+ /**
287
+ * The canonical graph export formats (taxonomy spec Q2: one `export` subcommand
288
+ * dispatching on `--format`, not nested argv). Declared as a `choices` enum so
289
+ * the host validates the value at mount and `graph export --format <bad>` is
290
+ * rejected before the handler runs.
291
+ */
292
+ export const GRAPH_EXPORT_FORMATS = ['sarif', 'catalog', 'baseline'];
293
+ /**
294
+ * Validate that the per-format required flags are present on the canonical
295
+ * `graph export` opts. Returns `true` when the required subset is satisfied;
296
+ * otherwise reports the missing flags (to the `--json` channel or stderr, like
297
+ * the shared export error paths) + sets exit 2 (CONFIGURATION_ERROR) and returns
298
+ * `false`. Keeps the per-format required-flag validation in one place without
299
+ * making all format-specific subsets simultaneously mandatory on one spec.
300
+ */
301
+ function requireExportFlags(format, present, required, cli) {
302
+ const missing = required.filter((flag) => {
303
+ // Flag names map to camelCase opt keys (`--output-sarif` → `outputSarif`).
304
+ const key = flag.replace(/^--/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase());
305
+ const value = present[key];
306
+ return value === undefined || value === '';
307
+ });
308
+ if (missing.length === 0)
309
+ return true;
310
+ const message = `graph export --format ${format} requires ${missing.join(', ')}.`;
311
+ const exitCode = EXIT_CODES.CONFIGURATION_ERROR;
312
+ logger.warn({ evt: 'cli.graph.export.missing_flags', module: 'graph:cli', format, missing });
313
+ if (present.json === true) {
314
+ cli.emitError({ message, exitCode });
315
+ return false;
316
+ }
317
+ cli.setExitCode(exitCode);
318
+ process.stderr.write(`Error: ${message}\n`);
319
+ return false;
320
+ }
321
+ /**
322
+ * `graph export --format sarif|catalog|baseline` — the canonical graph export
323
+ * command (taxonomy spec Q2). Mounts as a SUBCOMMAND of the `graph` primary
324
+ * (`parent: 'graph'`, via the nested-mount capability), so it shares the root
325
+ * with `fit export` without colliding (both declare `name: 'export'`).
326
+ *
327
+ * The legacy flat-root commands (`sarif-export`/`catalog-export`/
328
+ * `graph-baseline-export`) were removed. The canonical spec declares `--format`
329
+ * (required) + the UNION of the per-format flags as OPTIONAL and validates the
330
+ * required subset per format at runtime (`requireExportFlags` →
331
+ * ConfigurationError → exit 2).
332
+ */
333
+ export const graphExportCommandSpec = defineCommand({
334
+ name: 'export',
335
+ parent: 'graph',
336
+ description: 'Export graph analysis artifacts: --format sarif (SARIF v2.1.0 findings), catalog (CatalogExport JSON), or baseline (gate fingerprint JSON)',
148
337
  commonFlags: ['cwd', 'json'],
149
338
  options: [
150
339
  {
151
- flag: '--out',
152
- value: '<path>',
153
- description: 'Output file path for the JSON baseline',
340
+ flag: '--format',
341
+ value: '<fmt>',
342
+ description: 'Export artifact: sarif | catalog | baseline',
154
343
  required: true,
344
+ choices: [...GRAPH_EXPORT_FORMATS],
155
345
  },
346
+ // Union of the per-format flags, all OPTIONAL here (the required subset is
347
+ // validated per-format at runtime). --cwd is a common flag (declared above),
348
+ // so it is NOT repeated here.
349
+ OPT_OUTPUT_SARIF,
350
+ OPT_CATALOG_OUTPUT,
351
+ OPT_TENANT_ID_CATALOG,
352
+ OPT_REPO_ID_CATALOG,
353
+ OPT_GIT_SHA,
354
+ OPT_RUN_ID_CATALOG,
355
+ OPT_MODE,
356
+ OPT_CHANGED_FILE,
357
+ OPT_LANGUAGE,
358
+ OPT_RESOLUTION,
359
+ OPT_BASELINE_OUT,
156
360
  ],
157
361
  scope: 'project',
158
362
  output: RAW_STREAM,
159
- rawStreamReason: 'file-export',
363
+ rawStreamReason: REASON_FILE_EXPORT,
160
364
  handler: async (rawOpts, cli) => {
161
365
  const opts = rawOpts;
162
- // ADR-0036: the host owns the byte-identical fingerprint-JSON export. The seam
163
- // throws ConfigurationError (→ exit 2) when no baseline exists; map it here for
164
- // both the --json (structured) and plain-text boundaries, as today.
165
- try {
166
- await cli.exportBaselineFingerprints('graph', opts.out);
167
- }
168
- catch (error) {
169
- const message = error instanceof Error ? error.message : String(error);
170
- const exitCode = error instanceof ConfigurationError
171
- ? EXIT_CODES.CONFIGURATION_ERROR
172
- : EXIT_CODES.RUNTIME_ERROR;
173
- logger.warn({
174
- evt: 'cli.graph.baseline_export.failed',
175
- module: 'graph:cli',
176
- message,
177
- exitCode,
178
- });
179
- if (opts.json === true) {
180
- cli.emitError({ message, exitCode });
366
+ switch (opts.format) {
367
+ case 'sarif': {
368
+ if (!requireExportFlags('sarif', opts, ['--output-sarif', '--tenant-id', '--repo-id'], cli))
369
+ return;
370
+ await runGraphSarifExport(opts, cli);
371
+ return;
372
+ }
373
+ case 'catalog': {
374
+ if (!requireExportFlags('catalog', opts, ['--catalog-output', '--tenant-id', '--repo-id', '--git-sha'], cli))
375
+ return;
376
+ await runGraphCatalogExport(opts, cli);
377
+ return;
378
+ }
379
+ case 'baseline': {
380
+ if (!requireExportFlags('baseline', opts, ['--out'], cli))
381
+ return;
382
+ await runGraphBaselineExport(opts, cli);
181
383
  return;
182
384
  }
183
- cli.setExitCode(exitCode);
184
- process.stderr.write(`Error: ${message}\n`);
185
- return;
186
- }
187
- const result = { type: 'graph-baseline-export', outPath: opts.out };
188
- if (opts.json === true) {
189
- cli.emitJson(result);
190
- return;
191
385
  }
192
- process.stdout.write(`Exported graph baseline to ${opts.out}\n`);
193
386
  },
194
387
  });
388
+ // =============================================================================
389
+ // GROUPED <tool> <verb> CHILDREN (the canonical Tier-2 grammar)
390
+ //
391
+ // `graph recipes` / `graph lookup` / `graph index` / `graph list` mount as
392
+ // SUBCOMMANDS of the `graph` primary via the nested-mount capability
393
+ // (`parent: 'graph'`). They own their handler bodies directly (calling the
394
+ // shared engine functions) — the legacy flat `graph-recipes` / `graph-lookup` /
395
+ // `graph-symbol-index` aliases were removed.
396
+ // =============================================================================
195
397
  /**
196
- * `catalog-export` — dedicated subcommand carrying the catalog-JSON renderer +
197
- * machine flags (`--catalog-output`/`--tenant-id`/`--repo-id`/`--git-sha`). This
198
- * is the CLI contract the opensip `EngineSubprocessPort.runCatalogExport` spawns
199
- * (DEC-498). The flags live here, NOT on `graph` — the v1 `graph
200
- * --catalog-output` shape was retired by the split, so docs/consumers must
201
- * target `catalog-export`.
398
+ * `graph recipes` — list available graph recipes (mirrors `fit recipes`). Reuses
399
+ * the shared ListRecipesResult contract + viewListRecipes renderer.
400
+ * `command-result`: the host dispatches the returned result through the shared
401
+ * seam (`--json` JSON, else render).
202
402
  */
203
- export const graphCatalogExportCommandSpec = defineCommand({
204
- name: 'catalog-export',
205
- description: 'Run graph analysis and write the CatalogExport JSON document (symbols + edges + provenance) to a file',
206
- commonFlags: [],
207
- options: [
208
- {
209
- flag: '--catalog-output',
210
- value: '<path>',
211
- description: 'Output file path for the CatalogExport JSON',
212
- required: true,
213
- },
214
- {
215
- flag: '--tenant-id',
216
- value: '<id>',
217
- description: 'Tenant scope stamped on every row + provenance',
218
- required: true,
219
- },
220
- {
221
- flag: '--repo-id',
222
- value: '<id>',
223
- description: 'Repository scope stamped on every row',
224
- required: true,
225
- },
226
- {
227
- flag: '--git-sha',
228
- value: '<sha>',
229
- description: 'Commit SHA the catalog was extracted at',
230
- required: true,
231
- },
232
- {
233
- flag: '--run-id',
234
- value: '<uuid>',
235
- description: 'Run id for provenance (auto-generated if absent)',
236
- },
237
- {
238
- flag: '--mode',
239
- value: '<mode>',
240
- description: "'initial' (full rebuild) or 'incremental' (reuse cache when present)",
241
- default: 'initial',
242
- },
243
- {
244
- flag: '--changed-file',
245
- value: '<relPath>',
246
- description: 'Changed file (repeatable). Advisory today — the engine derives the true changed set from fingerprint diffs; recorded for observability.',
247
- arrayDefault: [],
248
- parse: (val, prev) => [...prev, val],
249
- },
250
- { flag: OPT_CWD, description: OPT_DESC_CWD, default: process.cwd() },
251
- {
252
- flag: '--language',
253
- value: '<name>',
254
- description: 'Force a specific language adapter (suppresses auto-detection)',
255
- },
256
- {
257
- flag: '--resolution',
258
- value: '<mode>',
259
- description: 'Edge resolution tier: exact (semantic) or fast (syntactic, no type checker)',
260
- default: 'exact',
261
- choices: ['exact', 'fast'],
262
- },
263
- ],
403
+ export const graphRecipesGroupedCommandSpec = defineCommand({
404
+ name: 'recipes',
405
+ parent: 'graph',
406
+ description: 'List available graph recipes',
407
+ commonFlags: ['json'],
408
+ scope: 'project',
409
+ output: 'command-result',
410
+ handler: async () => listGraphRecipes(),
411
+ });
412
+ /** `graph lookup <name>` — look up function occurrences by simple name. */
413
+ export const graphLookupGroupedCommandSpec = defineCommand({
414
+ name: 'lookup',
415
+ parent: 'graph',
416
+ description: 'Look up function occurrences by simple name from the persisted catalog',
417
+ commonFlags: ['json'],
418
+ args: [{ name: 'name', description: 'Function simple name to look up (e.g. "saveBaseline")' }],
264
419
  scope: 'project',
265
420
  output: RAW_STREAM,
266
- rawStreamReason: 'file-export',
421
+ rawStreamReason: 'lookup',
267
422
  handler: async (rawOpts, cli) => {
268
423
  const opts = rawOpts;
269
- const startedAt = new Date().toISOString();
270
- try {
271
- // `--resolution`'s value is `exact`/`fast` by construction (declared
272
- // `choices`); the mount layer rejected any other value before we got here.
273
- const resolution = opts.resolution === 'fast' ? 'fast' : 'exact';
274
- const incremental = opts.mode === 'incremental';
275
- const changedFiles = opts.changedFile ?? [];
276
- if (incremental && changedFiles.length > 0) {
277
- // Advisory only: the incremental path self-derives the changed set from
278
- // on-disk fingerprint diffs, so a caller-supplied set does not (yet)
279
- // narrow the walk. Logged for observability.
280
- logger.info({
281
- evt: 'graph.cli.catalog_export.changed_files_advisory',
282
- module: 'graph:cli',
283
- runId: opts.runId,
284
- changedFileCount: changedFiles.length,
285
- });
286
- }
287
- const result = await runGraph({
288
- cwd: opts.cwd,
289
- noCache: !incremental,
290
- resolution,
291
- language: opts.language,
292
- datastore: cli.scope.datastore(),
293
- });
294
- runCatalogJsonMode({
295
- cwd: opts.cwd,
296
- catalogOutput: opts.catalogOutput,
297
- tenantId: opts.tenantId,
298
- repoId: opts.repoId,
299
- gitSha: opts.gitSha,
300
- runId: opts.runId,
301
- }, result, cli, startedAt);
302
- }
303
- catch (error) {
304
- handleGraphError('catalog-export', error, cli);
305
- }
424
+ await executeLookup({ name: firstArg(opts), json: opts.json }, cli);
306
425
  },
307
426
  });
308
427
  /**
309
- * `sarif-export` — runs the pipeline and writes OpenSIP-convention SARIF to a
310
- * file, matching the opensip `EngineSubprocessPort.runSarifExport` contract
311
- * (DEC-498). Always a full run (findings, not incremental).
428
+ * `graph index` — emit a `symbolindex.json` artifact (name→file:line and
429
+ * file→names) from the persisted catalog. It BOTH builds and queries the symbol
430
+ * index.
431
+ *
432
+ * Q7 (open): `graph index` currently both builds and queries; the build/query
433
+ * verb split (`graph index build` | `graph index query`) is deferred. Do NOT add
434
+ * a `--build` flag here without resolving Q7.
312
435
  */
313
- export const graphSarifExportCommandSpec = defineCommand({
314
- name: 'sarif-export',
315
- description: 'Run graph analysis and write OpenSIP-convention SARIF v2.1.0 findings to a file',
436
+ export const graphIndexGroupedCommandSpec = defineCommand({
437
+ name: 'index',
438
+ parent: 'graph',
439
+ description: 'Emit a symbolindex.json artifact (name→file:line and file→names) from the persisted catalog',
316
440
  commonFlags: [],
317
441
  options: [
442
+ // --cwd keeps its command-specific description (the out path resolves
443
+ // against it), so it is declared as a tool option rather than the common
444
+ // flag. The literal default is `process.cwd()`, evaluated once at module
445
+ // load (CLI startup) — equivalent to the former register-time evaluation.
318
446
  {
319
- flag: '--output-sarif',
320
- value: '<path>',
321
- description: 'Output file path for the SARIF v2.1.0 document',
322
- required: true,
323
- },
324
- { flag: '--tenant-id', value: '<id>', description: 'Tenant scope for the run', required: true },
325
- {
326
- flag: '--repo-id',
327
- value: '<id>',
328
- description: 'Repository scope for the run',
329
- required: true,
330
- },
331
- {
332
- flag: '--run-id',
333
- value: '<uuid>',
334
- description: 'Run id for trace correlation (auto-generated if absent)',
335
- },
336
- { flag: OPT_CWD, description: OPT_DESC_CWD, default: process.cwd() },
337
- {
338
- flag: '--language',
339
- value: '<name>',
340
- description: 'Force a specific language adapter (suppresses auto-detection)',
447
+ flag: OPT_CWD,
448
+ description: 'Target directory (out path resolves against this)',
449
+ default: process.cwd(),
341
450
  },
342
451
  {
343
- flag: '--resolution',
344
- value: '<mode>',
345
- description: 'Edge resolution tier: exact (semantic) or fast (syntactic, no type checker)',
346
- default: 'exact',
347
- choices: ['exact', 'fast'],
452
+ flag: '--out',
453
+ value: '<path>',
454
+ description: 'Output file path',
455
+ default: 'symbolindex.json',
348
456
  },
349
457
  ],
350
458
  scope: 'project',
351
459
  output: RAW_STREAM,
352
- rawStreamReason: 'file-export',
353
- handler: async (rawOpts, cli) => {
460
+ rawStreamReason: REASON_FILE_EXPORT,
461
+ handler: (rawOpts, cli) => {
354
462
  const opts = rawOpts;
355
- try {
356
- const resolution = opts.resolution === 'fast' ? 'fast' : 'exact';
357
- const result = await runGraph({
358
- cwd: opts.cwd,
359
- noCache: true,
360
- resolution,
361
- language: opts.language,
362
- datastore: cli.scope.datastore(),
363
- });
364
- await runSarifExportMode({
365
- outputSarif: opts.outputSarif,
366
- tenantId: opts.tenantId,
367
- repoId: opts.repoId,
368
- runId: opts.runId,
369
- }, result.signals, cli);
370
- }
371
- catch (error) {
372
- handleGraphError('sarif-export', error, cli);
373
- }
463
+ executeSymbolIndex({ cwd: opts.cwd, out: opts.out }, cli);
374
464
  },
375
465
  });
376
466
  /**
377
- * `graph-recipes` — list available graph recipes (mirrors fit-recipes). Reuses
378
- * the shared ListRecipesResult contract + viewListRecipes renderer.
379
- * `command-result`: the host dispatches the returned result through the shared
380
- * seam (`--json` JSON, else render).
467
+ * `graph list` — list available graph rules (the natural analog of `fit list`,
468
+ * which lists checks): graph *rules* are the listable surface.
469
+ *
470
+ * `command-result`: the handler returns a `ListChecksResult`; the host
471
+ * dispatches it through the shared seam (`--json` → JSON, else the shared
472
+ * `viewListChecks` renderer with the graph-supplied title) — the same path
473
+ * `graph recipes` / `fit list` use.
381
474
  */
382
- export const graphRecipesCommandSpec = defineCommand({
383
- name: 'graph-recipes',
384
- description: 'List available graph recipes',
385
- commonFlags: ['json'],
475
+ export const graphListCommandSpec = defineCommand({
476
+ name: 'list',
477
+ parent: 'graph',
478
+ description: 'List available graph rules',
479
+ commonFlags: ['cwd', 'json'],
386
480
  scope: 'project',
387
481
  output: 'command-result',
388
- handler: async () => listGraphRecipes(),
482
+ handler: async () => listGraphRules(),
389
483
  });
390
484
  //# sourceMappingURL=graph-aux-command-specs.js.map