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 +27 -4
- package/docs/reference/release-automation.md +1 -0
- package/package.json +1 -1
- package/release-log.md +8 -0
- package/src/cli/analyze-github.js +224 -32
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
|
-
|
|
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
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
|
|
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"),
|