delivery-friction-analyzer 0.18.0 → 0.19.0

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 CHANGED
@@ -9,25 +9,44 @@ Use it when you want to answer questions like:
9
9
  - Which files, surfaces, or PR classes create the most back-and-forth?
10
10
  - Which issues look preventable with better local checks, repo-specific AI instructions, skills, hooks, or smaller delivery slices?
11
11
 
12
- The analyzer runs locally with your GitHub credentials. Generated artifacts preserve source evidence, coverage caveats, and interpretation limits so reports can be inspected before they are shared.
12
+ For live GitHub analysis, the analyzer runs locally with your GitHub credentials. Generated artifacts preserve source evidence, coverage caveats, and interpretation limits so reports can be inspected before they are shared.
13
13
 
14
14
  ## Requirements
15
15
 
16
16
  - Node.js 20 or newer.
17
- - GitHub CLI (`gh`) installed and authenticated with access to the target repository.
18
- - A repository profile JSON for the repository you want to analyze. Interactive setup can create a starter profile for you.
17
+ - GitHub CLI (`gh`) installed and authenticated with access to the target repository for live GitHub analysis.
18
+ - A repository profile JSON for the repository you want to analyze in live GitHub mode. Interactive setup can create a starter profile for you.
19
19
 
20
20
  For public repositories, ordinary read access is usually enough. Private repositories need a `gh` token with enough read access for the requested source families. With a classic PAT, that usually means the `repo` scope. With a fine-grained token or GitHub App, grant read permissions for repository metadata and contents, pull requests, Actions, and checks where available. Missing or partial source coverage is recorded in the generated methodology and coverage artifacts instead of being treated as complete data.
21
21
 
22
22
  ## Quickstart
23
23
 
24
+ ### Run the bundled sample
25
+
26
+ Start with the synthetic tutorial sample. It runs locally without GitHub credentials and writes the same artifact family as live analysis:
27
+
28
+ ```sh
29
+ npx delivery-friction-analyzer --source sample --out reports/tutorial
30
+ ```
31
+
32
+ Open `reports/tutorial/friction-report.md` first. The sample output is labeled `Bundled synthetic sample, not live GitHub data` in the CLI completion, report, methodology, and source bundle.
33
+
34
+ Short excerpt from the generated sample report context:
35
+
36
+ > Source: Bundled synthetic sample, not live GitHub data.
37
+ >
38
+ > The generated sample report opens by pointing readers to Change scope, Review churn, and Repo guidance gap. It also shows why the first findings should not be overgeneralized: PR #104 is intentionally broad and outlier-sensitive, PR-open diff coverage is partial, and one PR has unavailable workflow-run history.
39
+
40
+ Screenshots are intentionally deferred until the project has a visual report surface worth capturing.
41
+
24
42
  ### Guided setup
25
43
 
26
- From this repository, install dependencies and let interactive setup create or confirm the repository profile for a GitHub repository you want to measure:
44
+ After reading the sample report, analyze a repository you own or can read. From this repository, install dependencies and let interactive setup create or confirm the repository profile for a GitHub repository you want to measure:
27
45
 
28
46
  ```sh
29
47
  npm install
30
48
  npm run analyze:github -- \
49
+ --source github \
31
50
  --repo owner/name \
32
51
  --limit 30 \
33
52
  --profile profiles/owner-name.json \
@@ -44,6 +63,7 @@ After you have a repository profile, run the analyzer against the target reposit
44
63
 
45
64
  ```sh
46
65
  npm run analyze:github -- \
66
+ --source github \
47
67
  --repo owner/name \
48
68
  --limit 30 \
49
69
  --profile profiles/owner-name.json \
@@ -56,6 +76,7 @@ To run the CLI from another project with the npm package, pass the same choices
56
76
 
57
77
  ```sh
58
78
  npx delivery-friction-analyzer \
79
+ --source github \
59
80
  --repo owner/name \
60
81
  --limit 30 \
61
82
  --profile path/to/repository-profile.json \
@@ -108,6 +129,8 @@ Each ranked bottleneck example includes source references, workflow-run conclusi
108
129
 
109
130
  ## Common Options
110
131
 
132
+ Use `--source sample` for the bundled synthetic tutorial data. Use `--source github` for live repository analysis. Existing live commands that pass `--repo`, `--profile`, or `--limit` without `--source` still run as GitHub analysis, but new users should prefer the explicit source flag.
133
+
111
134
  Use `--dry-run` or `--metadata-only` to validate repository access, profile JSON, output directory writability, and sampled API coverage without writing full report artifacts. The output directory may be created during this check, and a temporary probe file may be written and removed.
112
135
 
113
136
  Use `--no-csv` when you want the Markdown, JSON, source, normalized, metrics, and methodology artifacts without spreadsheet-friendly CSV exports.
@@ -7,6 +7,7 @@ Delivery Friction Analyzer publishes the unbundled source package to npm. This r
7
7
  The npm package allowlist includes:
8
8
 
9
9
  - runtime source in `src`;
10
+ - bundled tutorial sample runtime data in `examples/tutorial`;
10
11
  - JSON schemas in `schemas`;
11
12
  - public contract and reference docs in `docs/contracts` and `docs/reference`;
12
13
  - the internal validation repository profile at `fixtures/github/mcp-writing/profile.json`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delivery-friction-analyzer",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Local GitHub pull request analytics for delivery friction reports.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/release-log.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### 2026-06-27 — Explicit Sample CLI Source
6
+
7
+ - What changed: Added explicit `--source sample` CLI routing for the bundled tutorial sample, with sample-first README guidance and live-only option rejection before GitHub collection.
8
+ - Why it matters: New users can generate a deterministic local report without GitHub credentials, while existing live GitHub runs keep their source boundary and required target-option validation.
9
+ - Who is affected: First-time users trying the tutorial sample, maintainers reviewing CLI source routing, and users passing live-only flags such as repository targets, dry-run checks, or PR class exclusions.
10
+ - Action needed: Use `delivery-friction-analyzer --source sample --out reports/tutorial` for the bundled sample, or `delivery-friction-analyzer --source github --repo owner/name --limit 30 --profile path/to/profile.json --out reports/owner-name` for live analysis.
11
+ - PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/62
12
+
5
13
  ### 2026-06-27 — Tutorial Sample Bundle
6
14
 
7
15
  - What changed: Added the packaged tutorial sample source bundle, educational sample profile, and generated-aligned report excerpt for a fully synthetic repository.
@@ -25,8 +25,12 @@ import {
25
25
  } from "../profile/workflow.js";
26
26
 
27
27
  const RUN_PRESET_SCHEMA_VERSION = "analyze-github-run-preset.v1";
28
+ const SAMPLE_SOURCE_LABEL = "Bundled synthetic sample, not live GitHub data";
29
+ const SAMPLE_SOURCE_BUNDLE_URL = new URL("../../examples/tutorial/source-bundle.json", import.meta.url);
30
+ const SAMPLE_PROFILE_URL = new URL("../../examples/tutorial/profile.json", import.meta.url);
28
31
 
29
32
  const ALLOWED_OPTIONS = new Set([
33
+ "source",
30
34
  "repo",
31
35
  "limit",
32
36
  "profile",
@@ -60,6 +64,7 @@ const BOOLEAN_OPTIONS = new Set([
60
64
  ]);
61
65
 
62
66
  const CLI_OPTION_KEYS = Object.freeze({
67
+ source: "source",
63
68
  repo: "repository",
64
69
  limit: "limit",
65
70
  profile: "profilePath",
@@ -114,10 +119,13 @@ const CSV_ARTIFACT_KEYS = new Set([
114
119
  ]);
115
120
 
116
121
  export const USAGE = `Usage:
122
+ delivery-friction-analyzer --source sample --out <directory>
123
+ delivery-friction-analyzer --source github --repo <owner/name> --limit <1-100> --profile <path> --out <directory>
117
124
  delivery-friction-analyzer --repo <owner/name> --limit <1-100> --profile <path> --out <directory>
118
125
  delivery-friction-analyzer --repo <owner/name> --limit <1-100> --profile <path> --out <directory> --dry-run
119
126
 
120
127
  Options:
128
+ --source <sample|github> Choose the bundled synthetic sample or live GitHub analysis.
121
129
  --repo <owner/name> Target GitHub repository to analyze.
122
130
  --limit <1-100> Latest merged pull request count.
123
131
  --profile <path> Repository profile JSON used for file role classification.
@@ -137,6 +145,14 @@ Options:
137
145
  --save-preset <path> Save local run settings for non-interactive reruns.
138
146
  `;
139
147
 
148
+ export const SOURCE_SELECTION_GUIDANCE = `Choose what to analyze.
149
+
150
+ Try the bundled sample:
151
+ delivery-friction-analyzer --source sample --out reports/tutorial
152
+
153
+ Analyze a GitHub repository:
154
+ delivery-friction-analyzer --source github --repo owner/name --limit 30 --profile path/to/profile.json --out reports/owner-name`;
155
+
140
156
  function attachOptionSource(options, property, value) {
141
157
  Object.defineProperty(options, property, {
142
158
  value,
@@ -215,6 +231,7 @@ export function parseAnalyzeGithubArgs(argv) {
215
231
  }
216
232
 
217
233
  const parsed = {
234
+ source: options.source,
218
235
  repository: options.repo,
219
236
  limit: options.limit === undefined ? undefined : Number(options.limit),
220
237
  profilePath: options.profile,
@@ -228,6 +245,7 @@ export function parseAnalyzeGithubArgs(argv) {
228
245
  };
229
246
  if (options.preset !== undefined) parsed.presetPath = options.preset;
230
247
  if (options["save-preset"] !== undefined) parsed.savePresetPath = options["save-preset"];
248
+ if (parsed.source === undefined) delete parsed.source;
231
249
 
232
250
  return attachOptionSource(parsed, "explicitCliOptions", explicitOptions);
233
251
  }
@@ -408,6 +426,12 @@ function requireOptions(options) {
408
426
  }
409
427
  }
410
428
 
429
+ function requireSampleOptions(options) {
430
+ if (!options.outDir) {
431
+ throw new Error("Missing required option(s): --out");
432
+ }
433
+ }
434
+
411
435
  function validateRepositorySlug(repository) {
412
436
  if (typeof repository !== "string" || !REPOSITORY_SLUG.test(repository)) {
413
437
  throw new Error("repo must use owner/name with GitHub-safe owner and name segments.");
@@ -428,6 +452,71 @@ function validateExcludedPrClasses(excludedPrClasses = []) {
428
452
  }
429
453
  }
430
454
 
455
+ function validateSource(source) {
456
+ if (source !== "sample" && source !== "github") {
457
+ throw new Error("source must be one of: sample, github");
458
+ }
459
+ }
460
+
461
+ function hasLiveSourceIndicator(options) {
462
+ const providedOptions = new Set([
463
+ ...optionSourceSet(options, "explicitCliOptions"),
464
+ ...optionSourceSet(options, "presetOptionKeys"),
465
+ ]);
466
+ return Boolean(
467
+ options.repository
468
+ || options.limit !== undefined
469
+ || options.profilePath
470
+ || options.dryRun
471
+ || options.isValidationTarget
472
+ || options.interactive
473
+ || options.presetPath
474
+ || options.savePresetPath
475
+ || options.excludedPrClasses?.length
476
+ || providedOptions.has("dryRun")
477
+ || providedOptions.has("isValidationTarget")
478
+ || providedOptions.has("interactive")
479
+ || providedOptions.has("presetPath")
480
+ || providedOptions.has("savePresetPath")
481
+ || providedOptions.has("excludedPrClasses"),
482
+ );
483
+ }
484
+
485
+ function resolveAnalysisSource(options) {
486
+ if (options.source !== undefined) {
487
+ validateSource(options.source);
488
+ return options.source;
489
+ }
490
+ if (hasLiveSourceIndicator(options)) {
491
+ return "github";
492
+ }
493
+ throw new Error(SOURCE_SELECTION_GUIDANCE);
494
+ }
495
+
496
+ function rejectSampleLiveOptions(options) {
497
+ const incompatible = [];
498
+ const providedOptions = new Set([
499
+ ...optionSourceSet(options, "explicitCliOptions"),
500
+ ...optionSourceSet(options, "presetOptionKeys"),
501
+ ]);
502
+ const dryRunOptionLabel = providedOptions.has("dryRun") && options.dryRun
503
+ ? "--dry-run/--metadata-only"
504
+ : "--dry-run";
505
+ if (options.repository) incompatible.push("--repo");
506
+ if (options.limit !== undefined) incompatible.push("--limit");
507
+ if (options.profilePath) incompatible.push("--profile");
508
+ if (options.dryRun || providedOptions.has("dryRun")) incompatible.push(dryRunOptionLabel);
509
+ if (options.isValidationTarget || providedOptions.has("isValidationTarget")) incompatible.push("--validation-target");
510
+ if (options.interactive) incompatible.push("--interactive");
511
+ if (options.presetPath) incompatible.push("--preset");
512
+ if (options.savePresetPath) incompatible.push("--save-preset");
513
+ if (options.excludedPrClasses?.length || providedOptions.has("excludedPrClasses")) {
514
+ incompatible.push("--exclude-pr-class");
515
+ }
516
+ if (!incompatible.length) return;
517
+ throw new Error(`--source sample cannot be combined with live GitHub option(s): ${incompatible.join(", ")}. Use only output options such as --out, --json, --csv, or --no-csv.`);
518
+ }
519
+
431
520
  function configuredPrClasses(repositoryProfile) {
432
521
  const rules = Array.isArray(repositoryProfile?.prClasses) ? repositoryProfile.prClasses : [];
433
522
  return new Set(rules.map(rule => rule.class));
@@ -1444,6 +1533,7 @@ function summarizeResult({ dryRun, outDir, paths, sourceBundle, metrics, report,
1444
1533
  sampledLimit,
1445
1534
  outputDirectory: outDir,
1446
1535
  artifactPaths: dryRun ? null : paths,
1536
+ source: sourceBundle.source,
1447
1537
  targetRepository: sourceBundle.targetRepository,
1448
1538
  selection: sourceBundle.selection,
1449
1539
  collectionCoverage: sourceBundle.coverage,
@@ -1484,6 +1574,49 @@ function applyPrClassFilter(normalized, excludedPrClasses = []) {
1484
1574
  };
1485
1575
  }
1486
1576
 
1577
+ async function readBundledJson(url, label) {
1578
+ try {
1579
+ return JSON.parse(await readFile(url, "utf8"));
1580
+ } catch (error) {
1581
+ if (error instanceof SyntaxError) {
1582
+ throw new Error(`${label} must be valid JSON: ${error.message}`);
1583
+ }
1584
+ throw error;
1585
+ }
1586
+ }
1587
+
1588
+ async function buildAnalysisArtifacts({ sourceBundle, repositoryProfile, profilePath, csvEnabled, excludedPrClasses = [] }) {
1589
+ validateExcludedPrClassesAreConfigured(excludedPrClasses, repositoryProfile);
1590
+ const normalized = applyPrClassFilter(
1591
+ normalizeFixtureBundle(sourceBundle, { repositoryProfile }),
1592
+ excludedPrClasses,
1593
+ );
1594
+ const metrics = computeRepositoryMetrics(normalized);
1595
+ const report = attachCollectionCoverage(
1596
+ generateRepositoryFrictionReport(metrics, {
1597
+ workflowContext: repositoryProfile.workflow,
1598
+ contributorSource: normalized.contributorSource,
1599
+ }),
1600
+ sourceBundle,
1601
+ );
1602
+ const markdown = `${renderRepositoryFrictionMarkdown(report)}${collectionCoverageMarkdown(sourceBundle)}`;
1603
+ const methodology = renderRepositoryFrictionMethodology({
1604
+ report,
1605
+ sourceBundle,
1606
+ profilePath,
1607
+ artifactFileNames: ANALYZE_GITHUB_ARTIFACTS,
1608
+ csvEnabled,
1609
+ });
1610
+ const csv = csvEnabled
1611
+ ? generateEvidenceCsvArtifacts({
1612
+ metricsSummary: metrics,
1613
+ report,
1614
+ collectionCoverage: sourceBundle.coverage,
1615
+ })
1616
+ : {};
1617
+ return { normalized, metrics, report, markdown, methodology, csv };
1618
+ }
1619
+
1487
1620
  export async function runAnalyzeGithub(options, {
1488
1621
  provider = createGhCliProvider(),
1489
1622
  now = () => new Date().toISOString(),
@@ -1541,43 +1674,23 @@ export async function runAnalyzeGithub(options, {
1541
1674
  }
1542
1675
 
1543
1676
  onProgress?.("Normalizing source bundle and computing metrics.");
1544
- const normalized = applyPrClassFilter(
1545
- normalizeFixtureBundle(sourceBundle, { repositoryProfile }),
1546
- options.excludedPrClasses ?? [],
1547
- );
1548
- const metrics = computeRepositoryMetrics(normalized);
1549
- const report = attachCollectionCoverage(
1550
- generateRepositoryFrictionReport(metrics, {
1551
- workflowContext: repositoryProfile.workflow,
1552
- contributorSource: normalized.contributorSource,
1553
- }),
1554
- sourceBundle,
1555
- );
1556
- const markdown = `${renderRepositoryFrictionMarkdown(report)}${collectionCoverageMarkdown(sourceBundle)}`;
1557
- const methodology = renderRepositoryFrictionMethodology({
1558
- report,
1677
+ const artifacts = await buildAnalysisArtifacts({
1559
1678
  sourceBundle,
1560
1679
  profilePath: options.profilePath,
1561
- artifactFileNames: ANALYZE_GITHUB_ARTIFACTS,
1562
1680
  csvEnabled,
1681
+ repositoryProfile,
1682
+ excludedPrClasses: options.excludedPrClasses ?? [],
1563
1683
  });
1564
- const csv = csvEnabled
1565
- ? generateEvidenceCsvArtifacts({
1566
- metricsSummary: metrics,
1567
- report,
1568
- collectionCoverage: sourceBundle.coverage,
1569
- })
1570
- : {};
1571
1684
 
1572
1685
  onProgress?.("Writing local artifacts.");
1573
1686
  await writeAnalysisArtifacts(outDir, generatedPaths, {
1574
1687
  sourceBundle,
1575
- normalized,
1576
- metricsSummary: metrics,
1577
- reportJson: report,
1578
- reportMarkdown: markdown,
1579
- methodology,
1580
- csv,
1688
+ normalized: artifacts.normalized,
1689
+ metricsSummary: artifacts.metrics,
1690
+ reportJson: artifacts.report,
1691
+ reportMarkdown: artifacts.markdown,
1692
+ methodology: artifacts.methodology,
1693
+ csv: artifacts.csv,
1581
1694
  }, { disabledPaths });
1582
1695
 
1583
1696
  return summarizeResult({
@@ -1585,18 +1698,75 @@ export async function runAnalyzeGithub(options, {
1585
1698
  outDir,
1586
1699
  paths: generatedPaths,
1587
1700
  sourceBundle,
1588
- metrics,
1589
- report,
1701
+ metrics: artifacts.metrics,
1702
+ report: artifacts.report,
1590
1703
  requestedLimit: options.limit,
1591
1704
  sampledLimit: collectionLimit,
1592
1705
  csv: csvEnabled,
1593
- analysisFilter: normalized.analysisFilter ?? null,
1706
+ analysisFilter: artifacts.normalized.analysisFilter ?? null,
1594
1707
  savedProfilePath: options.savedProfilePath,
1595
1708
  savedRunPresetPath: options.savedRunPresetPath,
1596
1709
  prClassRulesWritten: options.prClassRulesWritten,
1597
1710
  });
1598
1711
  }
1599
1712
 
1713
+ export async function runAnalyzeSample(options, {
1714
+ onProgress = null,
1715
+ } = {}) {
1716
+ rejectSampleLiveOptions(options);
1717
+ requireSampleOptions(options);
1718
+ const csvEnabled = options.csv !== false;
1719
+
1720
+ onProgress?.("Loading bundled synthetic sample.");
1721
+ const [sourceBundle, repositoryProfile, outDir] = await Promise.all([
1722
+ readBundledJson(SAMPLE_SOURCE_BUNDLE_URL, "sample source bundle"),
1723
+ readBundledJson(SAMPLE_PROFILE_URL, "sample repository profile"),
1724
+ validateOutputDirectory(options.outDir),
1725
+ ]);
1726
+ if (sourceBundle.source?.kind !== "sample" || sourceBundle.source?.label !== SAMPLE_SOURCE_LABEL) {
1727
+ throw new Error(`sample source bundle source.kind must be sample and source.label must be ${SAMPLE_SOURCE_LABEL}.`);
1728
+ }
1729
+ validateProfile(repositoryProfile);
1730
+
1731
+ const generatedPaths = artifactPaths(outDir, { includeCsv: csvEnabled });
1732
+ const disabledPaths = csvEnabled ? {} : Object.fromEntries(
1733
+ Object.entries(artifactPaths(outDir, { includeCsv: true })).filter(([key]) => CSV_ARTIFACT_KEYS.has(key)),
1734
+ );
1735
+ await assertWritableArtifactTargets({ ...generatedPaths, ...disabledPaths });
1736
+
1737
+ onProgress?.("Normalizing source bundle and computing metrics.");
1738
+ const artifacts = await buildAnalysisArtifacts({
1739
+ sourceBundle,
1740
+ profilePath: "examples/tutorial/profile.json",
1741
+ csvEnabled,
1742
+ repositoryProfile,
1743
+ });
1744
+
1745
+ onProgress?.("Writing local artifacts.");
1746
+ await writeAnalysisArtifacts(outDir, generatedPaths, {
1747
+ sourceBundle,
1748
+ normalized: artifacts.normalized,
1749
+ metricsSummary: artifacts.metrics,
1750
+ reportJson: artifacts.report,
1751
+ reportMarkdown: artifacts.markdown,
1752
+ methodology: artifacts.methodology,
1753
+ csv: artifacts.csv,
1754
+ }, { disabledPaths });
1755
+
1756
+ return summarizeResult({
1757
+ dryRun: false,
1758
+ outDir,
1759
+ paths: generatedPaths,
1760
+ sourceBundle,
1761
+ metrics: artifacts.metrics,
1762
+ report: artifacts.report,
1763
+ requestedLimit: sourceBundle.selection?.requestedLimit,
1764
+ sampledLimit: sourceBundle.selection?.collectedCount,
1765
+ csv: csvEnabled,
1766
+ analysisFilter: null,
1767
+ });
1768
+ }
1769
+
1600
1770
  function writeProgress(message, stderr = process.stderr) {
1601
1771
  stderr.write(`${message}\n`);
1602
1772
  }
@@ -1637,6 +1807,7 @@ export function formatAnalyzeGithubCompletion(result) {
1637
1807
  lines.push(
1638
1808
  `Markdown report: ${paths.reportMarkdown}`,
1639
1809
  `Analysis complete for ${target}.`,
1810
+ `Source: ${result.source?.label ?? "not recorded"}.`,
1640
1811
  `Methodology: ${paths.methodology}`,
1641
1812
  `JSON report: ${paths.reportJson}`,
1642
1813
  `Metrics summary: ${paths.metricsSummary}`,
@@ -1712,7 +1883,28 @@ export async function runAnalyzeGithubCli(argv, {
1712
1883
  stdout.write(USAGE);
1713
1884
  return null;
1714
1885
  }
1886
+ if (parsedOptions.source !== undefined) {
1887
+ validateSource(parsedOptions.source);
1888
+ if (parsedOptions.source === "sample") {
1889
+ const runOptions = {
1890
+ onProgress: message => writeProgress(message, stderr),
1891
+ };
1892
+ const result = await runAnalyzeSample(parsedOptions, runOptions);
1893
+ writeAnalyzeGithubCompletion(result, { json: parsedOptions.json, stdout });
1894
+ return result;
1895
+ }
1896
+ }
1715
1897
  const options = await mergeRunPresetOptions(parsedOptions);
1898
+ const source = resolveAnalysisSource(options);
1899
+
1900
+ if (source === "sample") {
1901
+ const runOptions = {
1902
+ onProgress: message => writeProgress(message, stderr),
1903
+ };
1904
+ const result = await runAnalyzeSample(options, runOptions);
1905
+ writeAnalyzeGithubCompletion(result, { json: options.json, stdout });
1906
+ return result;
1907
+ }
1716
1908
 
1717
1909
  const providedOptionKeys = new Set([
1718
1910
  ...optionSourceSet(options, "explicitCliOptions"),