delivery-friction-analyzer 0.18.0 → 0.20.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/contracts/target-repository.md +7 -1
- package/docs/reference/release-automation.md +1 -0
- package/package.json +1 -1
- package/release-log.md +16 -0
- package/src/cli/analyze-github.js +274 -39
- package/src/collect/github-source-bundle.js +83 -4
- package/src/contracts/target-repository.js +11 -2
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.
|
|
@@ -19,7 +19,13 @@ Schema: `schemas/target-repository.schema.json`. Live analysis selection is late
|
|
|
19
19
|
|
|
20
20
|
The validator rejects a target repository that exactly matches the configured product repository. For this repository, the product repository is `hannasdev/delivery-friction-analyzer`; `hannasdev/mcp-writing` is an internal validation target and fixture source.
|
|
21
21
|
|
|
22
|
-
Live GitHub analysis enforces this separation before GitHub collection starts. If `--repo` names this tool's product repository, the command fails before provider calls, explains that the guard prevents accidental self-analysis during normal live runs rather than protecting already readable GitHub data,
|
|
22
|
+
Live GitHub analysis enforces this separation before GitHub collection starts. If `--repo` names this tool's product repository, the command fails before provider calls, explains that the guard prevents accidental self-analysis during normal live runs rather than protecting already readable GitHub data, recommends the bundled sample or another repository first, and confirms that no GitHub data was collected.
|
|
23
|
+
|
|
24
|
+
`--validation-target` only marks output metadata for internal validation or fixture-source runs. It does not bypass product-repository validation and does not change which repository may be analyzed.
|
|
25
|
+
|
|
26
|
+
`--allow-product-repository` is an explicit live-analysis override for intentional self-analysis of this product repository. It is not the tutorial path and is rejected in sample mode. When the override is present for the product repository, the CLI must prove required GitHub data is readable before collection or artifact writes. Required preflight reads are repository metadata, pull request inventory, and pull request details for at least one selected merged pull request. Review threads, workflow runs, languages, contributor source, and PR-open diff remain normal coverage families; partial or unavailable optional coverage should be reported as caveats after the required assertion succeeds rather than failing the preflight.
|
|
27
|
+
|
|
28
|
+
The product repository identity is repo-local implementation configuration, not a general public CLI setting.
|
|
23
29
|
|
|
24
30
|
## Degraded Behavior
|
|
25
31
|
|
|
@@ -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,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
### 2026-06-27 — Product Repository Override
|
|
6
|
+
|
|
7
|
+
- What changed: Added the explicit `--allow-product-repository` live-analysis override for intentional analysis of this tool's own repository, with required read-access checks before collection or artifact writes.
|
|
8
|
+
- Why it matters: Maintainers can deliberately validate the analyzer against its public product repository without weakening the normal guardrail that steers first-time users toward the bundled sample or another target repository.
|
|
9
|
+
- Who is affected: Maintainers intentionally running self-analysis, and users who hit the product-repository guard during live GitHub setup.
|
|
10
|
+
- Action needed: Use the bundled sample or another repository for normal first-run analysis; add `--allow-product-repository` only for intentional self-analysis after confirming GitHub access.
|
|
11
|
+
- PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/63
|
|
12
|
+
|
|
13
|
+
### 2026-06-27 — Explicit Sample CLI Source
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
- 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.
|
|
19
|
+
- PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/62
|
|
20
|
+
|
|
5
21
|
### 2026-06-27 — Tutorial Sample Bundle
|
|
6
22
|
|
|
7
23
|
- What changed: Added the packaged tutorial sample source bundle, educational sample profile, and generated-aligned report excerpt for a fully synthetic repository.
|
|
@@ -4,8 +4,16 @@ import { access, lstat, mkdir, open, readFile, rename, rm, stat, writeFile } fro
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
assertProductRepositoryReadableData,
|
|
9
|
+
collectGitHubSourceBundle,
|
|
10
|
+
} from "../collect/github-source-bundle.js";
|
|
8
11
|
import { createGhCliProvider } from "../collect/gh-provider.js";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_PRODUCT_REPOSITORY,
|
|
14
|
+
isProductRepositoryTarget,
|
|
15
|
+
productRepositoryTargetError,
|
|
16
|
+
} from "../contracts/target-repository.js";
|
|
9
17
|
import { computeRepositoryMetrics } from "../metrics/friction.js";
|
|
10
18
|
import { normalizeFixtureBundle } from "../normalize/github-fixture.js";
|
|
11
19
|
import {
|
|
@@ -25,8 +33,12 @@ import {
|
|
|
25
33
|
} from "../profile/workflow.js";
|
|
26
34
|
|
|
27
35
|
const RUN_PRESET_SCHEMA_VERSION = "analyze-github-run-preset.v1";
|
|
36
|
+
const SAMPLE_SOURCE_LABEL = "Bundled synthetic sample, not live GitHub data";
|
|
37
|
+
const SAMPLE_SOURCE_BUNDLE_URL = new URL("../../examples/tutorial/source-bundle.json", import.meta.url);
|
|
38
|
+
const SAMPLE_PROFILE_URL = new URL("../../examples/tutorial/profile.json", import.meta.url);
|
|
28
39
|
|
|
29
40
|
const ALLOWED_OPTIONS = new Set([
|
|
41
|
+
"source",
|
|
30
42
|
"repo",
|
|
31
43
|
"limit",
|
|
32
44
|
"profile",
|
|
@@ -44,6 +56,7 @@ const ALLOWED_OPTIONS = new Set([
|
|
|
44
56
|
"interactive",
|
|
45
57
|
"preset",
|
|
46
58
|
"save-preset",
|
|
59
|
+
"allow-product-repository",
|
|
47
60
|
]);
|
|
48
61
|
|
|
49
62
|
const BOOLEAN_OPTIONS = new Set([
|
|
@@ -57,9 +70,11 @@ const BOOLEAN_OPTIONS = new Set([
|
|
|
57
70
|
"json",
|
|
58
71
|
"no-json",
|
|
59
72
|
"interactive",
|
|
73
|
+
"allow-product-repository",
|
|
60
74
|
]);
|
|
61
75
|
|
|
62
76
|
const CLI_OPTION_KEYS = Object.freeze({
|
|
77
|
+
source: "source",
|
|
63
78
|
repo: "repository",
|
|
64
79
|
limit: "limit",
|
|
65
80
|
profile: "profilePath",
|
|
@@ -77,6 +92,7 @@ const CLI_OPTION_KEYS = Object.freeze({
|
|
|
77
92
|
interactive: "interactive",
|
|
78
93
|
preset: "presetPath",
|
|
79
94
|
"save-preset": "savePresetPath",
|
|
95
|
+
"allow-product-repository": "allowProductRepository",
|
|
80
96
|
});
|
|
81
97
|
|
|
82
98
|
const RUN_PRESET_OPTION_KEYS = Object.freeze([
|
|
@@ -114,10 +130,13 @@ const CSV_ARTIFACT_KEYS = new Set([
|
|
|
114
130
|
]);
|
|
115
131
|
|
|
116
132
|
export const USAGE = `Usage:
|
|
133
|
+
delivery-friction-analyzer --source sample --out <directory>
|
|
134
|
+
delivery-friction-analyzer --source github --repo <owner/name> --limit <1-100> --profile <path> --out <directory>
|
|
117
135
|
delivery-friction-analyzer --repo <owner/name> --limit <1-100> --profile <path> --out <directory>
|
|
118
136
|
delivery-friction-analyzer --repo <owner/name> --limit <1-100> --profile <path> --out <directory> --dry-run
|
|
119
137
|
|
|
120
138
|
Options:
|
|
139
|
+
--source <sample|github> Choose the bundled synthetic sample or live GitHub analysis.
|
|
121
140
|
--repo <owner/name> Target GitHub repository to analyze.
|
|
122
141
|
--limit <1-100> Latest merged pull request count.
|
|
123
142
|
--profile <path> Repository profile JSON used for file role classification.
|
|
@@ -127,6 +146,8 @@ Options:
|
|
|
127
146
|
--metadata-only Alias for --dry-run.
|
|
128
147
|
--validation-target Mark output metadata as an internal validation run; does not bypass target validation.
|
|
129
148
|
--no-validation-target Disable validation-target mode when a preset enabled it.
|
|
149
|
+
--allow-product-repository
|
|
150
|
+
Explicit live-analysis override for intentional self-analysis of this product repository.
|
|
130
151
|
--exclude-pr-class <cls> Exclude a PR class from normalized, metrics, report, methodology, and CSV artifacts. Repeat or comma-separate values.
|
|
131
152
|
--csv Enable curated CSV evidence exports when a preset disabled them.
|
|
132
153
|
--no-csv Suppress curated CSV evidence exports.
|
|
@@ -137,6 +158,14 @@ Options:
|
|
|
137
158
|
--save-preset <path> Save local run settings for non-interactive reruns.
|
|
138
159
|
`;
|
|
139
160
|
|
|
161
|
+
export const SOURCE_SELECTION_GUIDANCE = `Choose what to analyze.
|
|
162
|
+
|
|
163
|
+
Try the bundled sample:
|
|
164
|
+
delivery-friction-analyzer --source sample --out reports/tutorial
|
|
165
|
+
|
|
166
|
+
Analyze a GitHub repository:
|
|
167
|
+
delivery-friction-analyzer --source github --repo owner/name --limit 30 --profile path/to/profile.json --out reports/owner-name`;
|
|
168
|
+
|
|
140
169
|
function attachOptionSource(options, property, value) {
|
|
141
170
|
Object.defineProperty(options, property, {
|
|
142
171
|
value,
|
|
@@ -199,6 +228,7 @@ export function parseAnalyzeGithubArgs(argv) {
|
|
|
199
228
|
if (key === "json") options.json = true;
|
|
200
229
|
if (key === "no-json") options.json = false;
|
|
201
230
|
if (key === "interactive") options.interactive = true;
|
|
231
|
+
if (key === "allow-product-repository") options.allowProductRepository = true;
|
|
202
232
|
continue;
|
|
203
233
|
}
|
|
204
234
|
|
|
@@ -215,6 +245,7 @@ export function parseAnalyzeGithubArgs(argv) {
|
|
|
215
245
|
}
|
|
216
246
|
|
|
217
247
|
const parsed = {
|
|
248
|
+
source: options.source,
|
|
218
249
|
repository: options.repo,
|
|
219
250
|
limit: options.limit === undefined ? undefined : Number(options.limit),
|
|
220
251
|
profilePath: options.profile,
|
|
@@ -226,8 +257,10 @@ export function parseAnalyzeGithubArgs(argv) {
|
|
|
226
257
|
json: options.json,
|
|
227
258
|
interactive: options.interactive,
|
|
228
259
|
};
|
|
260
|
+
if (options.allowProductRepository) parsed.allowProductRepository = true;
|
|
229
261
|
if (options.preset !== undefined) parsed.presetPath = options.preset;
|
|
230
262
|
if (options["save-preset"] !== undefined) parsed.savePresetPath = options["save-preset"];
|
|
263
|
+
if (parsed.source === undefined) delete parsed.source;
|
|
231
264
|
|
|
232
265
|
return attachOptionSource(parsed, "explicitCliOptions", explicitOptions);
|
|
233
266
|
}
|
|
@@ -408,6 +441,12 @@ function requireOptions(options) {
|
|
|
408
441
|
}
|
|
409
442
|
}
|
|
410
443
|
|
|
444
|
+
function requireSampleOptions(options) {
|
|
445
|
+
if (!options.outDir) {
|
|
446
|
+
throw new Error("Missing required option(s): --out");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
411
450
|
function validateRepositorySlug(repository) {
|
|
412
451
|
if (typeof repository !== "string" || !REPOSITORY_SLUG.test(repository)) {
|
|
413
452
|
throw new Error("repo must use owner/name with GitHub-safe owner and name segments.");
|
|
@@ -428,6 +467,84 @@ function validateExcludedPrClasses(excludedPrClasses = []) {
|
|
|
428
467
|
}
|
|
429
468
|
}
|
|
430
469
|
|
|
470
|
+
function validateSource(source) {
|
|
471
|
+
if (source !== "sample" && source !== "github") {
|
|
472
|
+
throw new Error("source must be one of: sample, github");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function hasLiveSourceIndicator(options) {
|
|
477
|
+
const providedOptions = new Set([
|
|
478
|
+
...optionSourceSet(options, "explicitCliOptions"),
|
|
479
|
+
...optionSourceSet(options, "presetOptionKeys"),
|
|
480
|
+
]);
|
|
481
|
+
return Boolean(
|
|
482
|
+
options.repository
|
|
483
|
+
|| options.limit !== undefined
|
|
484
|
+
|| options.profilePath
|
|
485
|
+
|| options.dryRun
|
|
486
|
+
|| options.isValidationTarget
|
|
487
|
+
|| options.interactive
|
|
488
|
+
|| options.presetPath
|
|
489
|
+
|| options.savePresetPath
|
|
490
|
+
|| options.allowProductRepository
|
|
491
|
+
|| options.excludedPrClasses?.length
|
|
492
|
+
|| providedOptions.has("dryRun")
|
|
493
|
+
|| providedOptions.has("isValidationTarget")
|
|
494
|
+
|| providedOptions.has("interactive")
|
|
495
|
+
|| providedOptions.has("presetPath")
|
|
496
|
+
|| providedOptions.has("savePresetPath")
|
|
497
|
+
|| providedOptions.has("allowProductRepository")
|
|
498
|
+
|| providedOptions.has("excludedPrClasses"),
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function resolveAnalysisSource(options) {
|
|
503
|
+
if (options.source !== undefined) {
|
|
504
|
+
validateSource(options.source);
|
|
505
|
+
return options.source;
|
|
506
|
+
}
|
|
507
|
+
if (hasLiveSourceIndicator(options)) {
|
|
508
|
+
return "github";
|
|
509
|
+
}
|
|
510
|
+
throw new Error(SOURCE_SELECTION_GUIDANCE);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function rejectSampleLiveOptions(options) {
|
|
514
|
+
const incompatible = [];
|
|
515
|
+
const providedOptions = new Set([
|
|
516
|
+
...optionSourceSet(options, "explicitCliOptions"),
|
|
517
|
+
...optionSourceSet(options, "presetOptionKeys"),
|
|
518
|
+
]);
|
|
519
|
+
const dryRunOptionLabel = providedOptions.has("dryRun") && options.dryRun
|
|
520
|
+
? "--dry-run/--metadata-only"
|
|
521
|
+
: "--dry-run";
|
|
522
|
+
if (options.repository) incompatible.push("--repo");
|
|
523
|
+
if (options.limit !== undefined) incompatible.push("--limit");
|
|
524
|
+
if (options.profilePath) incompatible.push("--profile");
|
|
525
|
+
if (options.dryRun || providedOptions.has("dryRun")) incompatible.push(dryRunOptionLabel);
|
|
526
|
+
if (options.isValidationTarget || providedOptions.has("isValidationTarget")) incompatible.push("--validation-target");
|
|
527
|
+
if (options.interactive) incompatible.push("--interactive");
|
|
528
|
+
if (options.presetPath) incompatible.push("--preset");
|
|
529
|
+
if (options.savePresetPath) incompatible.push("--save-preset");
|
|
530
|
+
if (options.allowProductRepository || providedOptions.has("allowProductRepository")) {
|
|
531
|
+
incompatible.push("--allow-product-repository");
|
|
532
|
+
}
|
|
533
|
+
if (options.excludedPrClasses?.length || providedOptions.has("excludedPrClasses")) {
|
|
534
|
+
incompatible.push("--exclude-pr-class");
|
|
535
|
+
}
|
|
536
|
+
if (!incompatible.length) return;
|
|
537
|
+
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.`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function targetInputFromRepositorySlug(repository) {
|
|
541
|
+
const match = REPOSITORY_SLUG.exec(repository);
|
|
542
|
+
if (!match) {
|
|
543
|
+
throw new Error("repo must use owner/name with GitHub-safe owner and name segments.");
|
|
544
|
+
}
|
|
545
|
+
return { owner: match[1], name: match[2] };
|
|
546
|
+
}
|
|
547
|
+
|
|
431
548
|
function configuredPrClasses(repositoryProfile) {
|
|
432
549
|
const rules = Array.isArray(repositoryProfile?.prClasses) ? repositoryProfile.prClasses : [];
|
|
433
550
|
return new Set(rules.map(rule => rule.class));
|
|
@@ -1444,6 +1561,7 @@ function summarizeResult({ dryRun, outDir, paths, sourceBundle, metrics, report,
|
|
|
1444
1561
|
sampledLimit,
|
|
1445
1562
|
outputDirectory: outDir,
|
|
1446
1563
|
artifactPaths: dryRun ? null : paths,
|
|
1564
|
+
source: sourceBundle.source,
|
|
1447
1565
|
targetRepository: sourceBundle.targetRepository,
|
|
1448
1566
|
selection: sourceBundle.selection,
|
|
1449
1567
|
collectionCoverage: sourceBundle.coverage,
|
|
@@ -1484,6 +1602,49 @@ function applyPrClassFilter(normalized, excludedPrClasses = []) {
|
|
|
1484
1602
|
};
|
|
1485
1603
|
}
|
|
1486
1604
|
|
|
1605
|
+
async function readBundledJson(url, label) {
|
|
1606
|
+
try {
|
|
1607
|
+
return JSON.parse(await readFile(url, "utf8"));
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
if (error instanceof SyntaxError) {
|
|
1610
|
+
throw new Error(`${label} must be valid JSON: ${error.message}`);
|
|
1611
|
+
}
|
|
1612
|
+
throw error;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
async function buildAnalysisArtifacts({ sourceBundle, repositoryProfile, profilePath, csvEnabled, excludedPrClasses = [] }) {
|
|
1617
|
+
validateExcludedPrClassesAreConfigured(excludedPrClasses, repositoryProfile);
|
|
1618
|
+
const normalized = applyPrClassFilter(
|
|
1619
|
+
normalizeFixtureBundle(sourceBundle, { repositoryProfile }),
|
|
1620
|
+
excludedPrClasses,
|
|
1621
|
+
);
|
|
1622
|
+
const metrics = computeRepositoryMetrics(normalized);
|
|
1623
|
+
const report = attachCollectionCoverage(
|
|
1624
|
+
generateRepositoryFrictionReport(metrics, {
|
|
1625
|
+
workflowContext: repositoryProfile.workflow,
|
|
1626
|
+
contributorSource: normalized.contributorSource,
|
|
1627
|
+
}),
|
|
1628
|
+
sourceBundle,
|
|
1629
|
+
);
|
|
1630
|
+
const markdown = `${renderRepositoryFrictionMarkdown(report)}${collectionCoverageMarkdown(sourceBundle)}`;
|
|
1631
|
+
const methodology = renderRepositoryFrictionMethodology({
|
|
1632
|
+
report,
|
|
1633
|
+
sourceBundle,
|
|
1634
|
+
profilePath,
|
|
1635
|
+
artifactFileNames: ANALYZE_GITHUB_ARTIFACTS,
|
|
1636
|
+
csvEnabled,
|
|
1637
|
+
});
|
|
1638
|
+
const csv = csvEnabled
|
|
1639
|
+
? generateEvidenceCsvArtifacts({
|
|
1640
|
+
metricsSummary: metrics,
|
|
1641
|
+
report,
|
|
1642
|
+
collectionCoverage: sourceBundle.coverage,
|
|
1643
|
+
})
|
|
1644
|
+
: {};
|
|
1645
|
+
return { normalized, metrics, report, markdown, methodology, csv };
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1487
1648
|
export async function runAnalyzeGithub(options, {
|
|
1488
1649
|
provider = createGhCliProvider(),
|
|
1489
1650
|
now = () => new Date().toISOString(),
|
|
@@ -1495,12 +1656,26 @@ export async function runAnalyzeGithub(options, {
|
|
|
1495
1656
|
validateLimit(options.limit);
|
|
1496
1657
|
validateExcludedPrClasses(options.excludedPrClasses);
|
|
1497
1658
|
const csvEnabled = options.csv !== false;
|
|
1659
|
+
const effectiveProductRepository = productRepository ?? DEFAULT_PRODUCT_REPOSITORY;
|
|
1660
|
+
const targetInput = targetInputFromRepositorySlug(options.repository);
|
|
1661
|
+
const targetsProductRepository = isProductRepositoryTarget(targetInput, effectiveProductRepository);
|
|
1662
|
+
if (targetsProductRepository && !options.allowProductRepository) {
|
|
1663
|
+
throw new Error(productRepositoryTargetError(targetInput));
|
|
1664
|
+
}
|
|
1665
|
+
onProgress?.("Validating profile.");
|
|
1666
|
+
const repositoryProfile = await readProfile(options.profilePath);
|
|
1667
|
+
if (targetsProductRepository) {
|
|
1668
|
+
onProgress?.("Checking required GitHub read access for product-repository analysis.");
|
|
1669
|
+
await assertProductRepositoryReadableData({
|
|
1670
|
+
repository: options.repository,
|
|
1671
|
+
limit: options.dryRun ? Math.min(options.limit, 1) : options.limit,
|
|
1672
|
+
provider,
|
|
1673
|
+
productRepository: effectiveProductRepository,
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1498
1676
|
|
|
1499
|
-
onProgress?.("Validating
|
|
1500
|
-
const
|
|
1501
|
-
readProfile(options.profilePath),
|
|
1502
|
-
validateOutputDirectory(options.outDir),
|
|
1503
|
-
]);
|
|
1677
|
+
onProgress?.("Validating output directory.");
|
|
1678
|
+
const outDir = await validateOutputDirectory(options.outDir);
|
|
1504
1679
|
validateExcludedPrClassesAreConfigured(options.excludedPrClasses ?? [], repositoryProfile);
|
|
1505
1680
|
const generatedPaths = artifactPaths(outDir, { includeCsv: csvEnabled });
|
|
1506
1681
|
const disabledPaths = csvEnabled ? {} : Object.fromEntries(
|
|
@@ -1521,7 +1696,8 @@ export async function runAnalyzeGithub(options, {
|
|
|
1521
1696
|
collectedAt: now(),
|
|
1522
1697
|
isValidationTarget: options.isValidationTarget,
|
|
1523
1698
|
contributors: repositoryProfile.contributors,
|
|
1524
|
-
productRepository,
|
|
1699
|
+
productRepository: effectiveProductRepository,
|
|
1700
|
+
allowProductRepository: Boolean(options.allowProductRepository),
|
|
1525
1701
|
});
|
|
1526
1702
|
|
|
1527
1703
|
if (options.dryRun) {
|
|
@@ -1541,43 +1717,23 @@ export async function runAnalyzeGithub(options, {
|
|
|
1541
1717
|
}
|
|
1542
1718
|
|
|
1543
1719
|
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,
|
|
1720
|
+
const artifacts = await buildAnalysisArtifacts({
|
|
1559
1721
|
sourceBundle,
|
|
1560
1722
|
profilePath: options.profilePath,
|
|
1561
|
-
artifactFileNames: ANALYZE_GITHUB_ARTIFACTS,
|
|
1562
1723
|
csvEnabled,
|
|
1724
|
+
repositoryProfile,
|
|
1725
|
+
excludedPrClasses: options.excludedPrClasses ?? [],
|
|
1563
1726
|
});
|
|
1564
|
-
const csv = csvEnabled
|
|
1565
|
-
? generateEvidenceCsvArtifacts({
|
|
1566
|
-
metricsSummary: metrics,
|
|
1567
|
-
report,
|
|
1568
|
-
collectionCoverage: sourceBundle.coverage,
|
|
1569
|
-
})
|
|
1570
|
-
: {};
|
|
1571
1727
|
|
|
1572
1728
|
onProgress?.("Writing local artifacts.");
|
|
1573
1729
|
await writeAnalysisArtifacts(outDir, generatedPaths, {
|
|
1574
1730
|
sourceBundle,
|
|
1575
|
-
normalized,
|
|
1576
|
-
metricsSummary: metrics,
|
|
1577
|
-
reportJson: report,
|
|
1578
|
-
reportMarkdown: markdown,
|
|
1579
|
-
methodology,
|
|
1580
|
-
csv,
|
|
1731
|
+
normalized: artifacts.normalized,
|
|
1732
|
+
metricsSummary: artifacts.metrics,
|
|
1733
|
+
reportJson: artifacts.report,
|
|
1734
|
+
reportMarkdown: artifacts.markdown,
|
|
1735
|
+
methodology: artifacts.methodology,
|
|
1736
|
+
csv: artifacts.csv,
|
|
1581
1737
|
}, { disabledPaths });
|
|
1582
1738
|
|
|
1583
1739
|
return summarizeResult({
|
|
@@ -1585,18 +1741,75 @@ export async function runAnalyzeGithub(options, {
|
|
|
1585
1741
|
outDir,
|
|
1586
1742
|
paths: generatedPaths,
|
|
1587
1743
|
sourceBundle,
|
|
1588
|
-
metrics,
|
|
1589
|
-
report,
|
|
1744
|
+
metrics: artifacts.metrics,
|
|
1745
|
+
report: artifacts.report,
|
|
1590
1746
|
requestedLimit: options.limit,
|
|
1591
1747
|
sampledLimit: collectionLimit,
|
|
1592
1748
|
csv: csvEnabled,
|
|
1593
|
-
analysisFilter: normalized.analysisFilter ?? null,
|
|
1749
|
+
analysisFilter: artifacts.normalized.analysisFilter ?? null,
|
|
1594
1750
|
savedProfilePath: options.savedProfilePath,
|
|
1595
1751
|
savedRunPresetPath: options.savedRunPresetPath,
|
|
1596
1752
|
prClassRulesWritten: options.prClassRulesWritten,
|
|
1597
1753
|
});
|
|
1598
1754
|
}
|
|
1599
1755
|
|
|
1756
|
+
export async function runAnalyzeSample(options, {
|
|
1757
|
+
onProgress = null,
|
|
1758
|
+
} = {}) {
|
|
1759
|
+
rejectSampleLiveOptions(options);
|
|
1760
|
+
requireSampleOptions(options);
|
|
1761
|
+
const csvEnabled = options.csv !== false;
|
|
1762
|
+
|
|
1763
|
+
onProgress?.("Loading bundled synthetic sample.");
|
|
1764
|
+
const [sourceBundle, repositoryProfile, outDir] = await Promise.all([
|
|
1765
|
+
readBundledJson(SAMPLE_SOURCE_BUNDLE_URL, "sample source bundle"),
|
|
1766
|
+
readBundledJson(SAMPLE_PROFILE_URL, "sample repository profile"),
|
|
1767
|
+
validateOutputDirectory(options.outDir),
|
|
1768
|
+
]);
|
|
1769
|
+
if (sourceBundle.source?.kind !== "sample" || sourceBundle.source?.label !== SAMPLE_SOURCE_LABEL) {
|
|
1770
|
+
throw new Error(`sample source bundle source.kind must be sample and source.label must be ${SAMPLE_SOURCE_LABEL}.`);
|
|
1771
|
+
}
|
|
1772
|
+
validateProfile(repositoryProfile);
|
|
1773
|
+
|
|
1774
|
+
const generatedPaths = artifactPaths(outDir, { includeCsv: csvEnabled });
|
|
1775
|
+
const disabledPaths = csvEnabled ? {} : Object.fromEntries(
|
|
1776
|
+
Object.entries(artifactPaths(outDir, { includeCsv: true })).filter(([key]) => CSV_ARTIFACT_KEYS.has(key)),
|
|
1777
|
+
);
|
|
1778
|
+
await assertWritableArtifactTargets({ ...generatedPaths, ...disabledPaths });
|
|
1779
|
+
|
|
1780
|
+
onProgress?.("Normalizing source bundle and computing metrics.");
|
|
1781
|
+
const artifacts = await buildAnalysisArtifacts({
|
|
1782
|
+
sourceBundle,
|
|
1783
|
+
profilePath: "examples/tutorial/profile.json",
|
|
1784
|
+
csvEnabled,
|
|
1785
|
+
repositoryProfile,
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
onProgress?.("Writing local artifacts.");
|
|
1789
|
+
await writeAnalysisArtifacts(outDir, generatedPaths, {
|
|
1790
|
+
sourceBundle,
|
|
1791
|
+
normalized: artifacts.normalized,
|
|
1792
|
+
metricsSummary: artifacts.metrics,
|
|
1793
|
+
reportJson: artifacts.report,
|
|
1794
|
+
reportMarkdown: artifacts.markdown,
|
|
1795
|
+
methodology: artifacts.methodology,
|
|
1796
|
+
csv: artifacts.csv,
|
|
1797
|
+
}, { disabledPaths });
|
|
1798
|
+
|
|
1799
|
+
return summarizeResult({
|
|
1800
|
+
dryRun: false,
|
|
1801
|
+
outDir,
|
|
1802
|
+
paths: generatedPaths,
|
|
1803
|
+
sourceBundle,
|
|
1804
|
+
metrics: artifacts.metrics,
|
|
1805
|
+
report: artifacts.report,
|
|
1806
|
+
requestedLimit: sourceBundle.selection?.requestedLimit,
|
|
1807
|
+
sampledLimit: sourceBundle.selection?.collectedCount,
|
|
1808
|
+
csv: csvEnabled,
|
|
1809
|
+
analysisFilter: null,
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1600
1813
|
function writeProgress(message, stderr = process.stderr) {
|
|
1601
1814
|
stderr.write(`${message}\n`);
|
|
1602
1815
|
}
|
|
@@ -1637,6 +1850,7 @@ export function formatAnalyzeGithubCompletion(result) {
|
|
|
1637
1850
|
lines.push(
|
|
1638
1851
|
`Markdown report: ${paths.reportMarkdown}`,
|
|
1639
1852
|
`Analysis complete for ${target}.`,
|
|
1853
|
+
`Source: ${result.source?.label ?? "not recorded"}.`,
|
|
1640
1854
|
`Methodology: ${paths.methodology}`,
|
|
1641
1855
|
`JSON report: ${paths.reportJson}`,
|
|
1642
1856
|
`Metrics summary: ${paths.metricsSummary}`,
|
|
@@ -1712,7 +1926,28 @@ export async function runAnalyzeGithubCli(argv, {
|
|
|
1712
1926
|
stdout.write(USAGE);
|
|
1713
1927
|
return null;
|
|
1714
1928
|
}
|
|
1929
|
+
if (parsedOptions.source !== undefined) {
|
|
1930
|
+
validateSource(parsedOptions.source);
|
|
1931
|
+
if (parsedOptions.source === "sample") {
|
|
1932
|
+
const runOptions = {
|
|
1933
|
+
onProgress: message => writeProgress(message, stderr),
|
|
1934
|
+
};
|
|
1935
|
+
const result = await runAnalyzeSample(parsedOptions, runOptions);
|
|
1936
|
+
writeAnalyzeGithubCompletion(result, { json: parsedOptions.json, stdout });
|
|
1937
|
+
return result;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1715
1940
|
const options = await mergeRunPresetOptions(parsedOptions);
|
|
1941
|
+
const source = resolveAnalysisSource(options);
|
|
1942
|
+
|
|
1943
|
+
if (source === "sample") {
|
|
1944
|
+
const runOptions = {
|
|
1945
|
+
onProgress: message => writeProgress(message, stderr),
|
|
1946
|
+
};
|
|
1947
|
+
const result = await runAnalyzeSample(options, runOptions);
|
|
1948
|
+
writeAnalyzeGithubCompletion(result, { json: options.json, stdout });
|
|
1949
|
+
return result;
|
|
1950
|
+
}
|
|
1716
1951
|
|
|
1717
1952
|
const providedOptionKeys = new Set([
|
|
1718
1953
|
...optionSourceSet(options, "explicitCliOptions"),
|
|
@@ -49,7 +49,7 @@ function visibilityOf(repository) {
|
|
|
49
49
|
return "unknown";
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function mapTargetRepository({ owner, name, repositoryMetadata, analysisPullRequestLimit, isValidationTarget, productRepository }) {
|
|
52
|
+
function mapTargetRepository({ owner, name, repositoryMetadata, analysisPullRequestLimit, isValidationTarget, productRepository, allowProductRepository }) {
|
|
53
53
|
const targetRepository = {
|
|
54
54
|
owner,
|
|
55
55
|
name,
|
|
@@ -58,13 +58,90 @@ function mapTargetRepository({ owner, name, repositoryMetadata, analysisPullRequ
|
|
|
58
58
|
analysisPullRequestLimit,
|
|
59
59
|
isValidationTarget,
|
|
60
60
|
};
|
|
61
|
-
const errors = validateTargetRepository(targetRepository, { productRepository });
|
|
61
|
+
const errors = validateTargetRepository(targetRepository, { productRepository, allowProductRepository });
|
|
62
62
|
if (errors.length > 0) {
|
|
63
63
|
throw new Error(`collected target repository metadata is invalid: ${errors.join(" ")}`);
|
|
64
64
|
}
|
|
65
65
|
return targetRepository;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function formatReadableDataFailure({ repository, family, action, error }) {
|
|
69
|
+
const diagnostic = redactDiagnostic(error?.message ?? error);
|
|
70
|
+
return `Cannot analyze ${repository} with --allow-product-repository because required GitHub read access could not be proven for ${family}. ${action}. Required preflight must read repository metadata, pull request inventory, and pull request details before collection or artifact writes.${diagnostic ? ` GitHub response: ${diagnostic}` : ""}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function requireReadableProbe({ repository, family, action, run }) {
|
|
74
|
+
try {
|
|
75
|
+
return await run();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(formatReadableDataFailure({
|
|
78
|
+
repository,
|
|
79
|
+
family,
|
|
80
|
+
action,
|
|
81
|
+
error,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function assertProductRepositoryReadableData({
|
|
87
|
+
repository,
|
|
88
|
+
owner,
|
|
89
|
+
name,
|
|
90
|
+
limit,
|
|
91
|
+
provider,
|
|
92
|
+
productRepository = DEFAULT_PRODUCT_REPOSITORY,
|
|
93
|
+
} = {}) {
|
|
94
|
+
if (!provider) {
|
|
95
|
+
throw new Error("provider is required.");
|
|
96
|
+
}
|
|
97
|
+
requirePullRequestLimit(limit);
|
|
98
|
+
const targetInput = repository ? parseRepositoryInput(repository) : { owner, name };
|
|
99
|
+
if (!isProductRepositoryTarget(targetInput, productRepository)) {
|
|
100
|
+
return { requiredFamilies: [] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const repositoryLabel = `${targetInput.owner}/${targetInput.name}`;
|
|
104
|
+
await requireReadableProbe({
|
|
105
|
+
repository: repositoryLabel,
|
|
106
|
+
family: "repository metadata",
|
|
107
|
+
action: "Authenticate with GitHub CLI or choose a repository your token can read",
|
|
108
|
+
run: () => provider.getRepository(targetInput),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const inventory = await requireReadableProbe({
|
|
112
|
+
repository: repositoryLabel,
|
|
113
|
+
family: "pull request inventory",
|
|
114
|
+
action: "Ensure `gh pr list --repo owner/name` can list merged pull requests",
|
|
115
|
+
run: () => provider.listMergedPullRequests({ ...targetInput, limit }),
|
|
116
|
+
});
|
|
117
|
+
if (!Array.isArray(inventory)) {
|
|
118
|
+
throw new Error(`Cannot analyze ${repositoryLabel} with --allow-product-repository because required GitHub read access could not be proven for pull request inventory. GitHub returned an unreadable pull request inventory shape. Required preflight must read repository metadata, pull request inventory, and pull request details before collection or artifact writes.`);
|
|
119
|
+
}
|
|
120
|
+
const selectedInventory = [...inventory]
|
|
121
|
+
.sort((left, right) => String(right.mergedAt ?? "").localeCompare(String(left.mergedAt ?? "")))
|
|
122
|
+
.slice(0, limit);
|
|
123
|
+
const firstPullRequest = selectedInventory[0];
|
|
124
|
+
if (!firstPullRequest?.number) {
|
|
125
|
+
throw new Error(`Cannot analyze ${repositoryLabel} with --allow-product-repository because required GitHub read access could not be proven for pull request details. The pull request inventory returned no merged pull requests to inspect. Choose another repository or rerun after merged pull requests are readable.`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await requireReadableProbe({
|
|
129
|
+
repository: repositoryLabel,
|
|
130
|
+
family: "pull request details",
|
|
131
|
+
action: "Ensure `gh pr view --repo owner/name <number> --json ...` can read merged pull request details",
|
|
132
|
+
run: () => provider.getPullRequest({ ...targetInput, number: firstPullRequest.number }),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
requiredFamilies: [
|
|
137
|
+
"repository_metadata",
|
|
138
|
+
"pull_request_inventory",
|
|
139
|
+
"pull_request_details",
|
|
140
|
+
],
|
|
141
|
+
probedPullRequestNumber: firstPullRequest.number,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
68
145
|
function mapRepositoryMetadata(repository) {
|
|
69
146
|
return {
|
|
70
147
|
id: repository.id ?? null,
|
|
@@ -378,6 +455,7 @@ export async function collectGitHubSourceBundle({
|
|
|
378
455
|
isValidationTarget = false,
|
|
379
456
|
contributors = null,
|
|
380
457
|
productRepository = DEFAULT_PRODUCT_REPOSITORY,
|
|
458
|
+
allowProductRepository = false,
|
|
381
459
|
} = {}) {
|
|
382
460
|
if (!provider) {
|
|
383
461
|
throw new Error("provider is required.");
|
|
@@ -385,7 +463,7 @@ export async function collectGitHubSourceBundle({
|
|
|
385
463
|
const targetPullRequestLimit = analysisPullRequestLimit ?? limit;
|
|
386
464
|
requirePullRequestLimit(targetPullRequestLimit);
|
|
387
465
|
const targetInput = repository ? parseRepositoryInput(repository) : { owner, name };
|
|
388
|
-
if (isProductRepositoryTarget(targetInput, productRepository)) {
|
|
466
|
+
if (isProductRepositoryTarget(targetInput, productRepository) && !allowProductRepository) {
|
|
389
467
|
throw new Error(productRepositoryTargetError(targetInput));
|
|
390
468
|
}
|
|
391
469
|
const targetNameErrors = validateTargetRepository({
|
|
@@ -394,7 +472,7 @@ export async function collectGitHubSourceBundle({
|
|
|
394
472
|
visibility: "unknown",
|
|
395
473
|
analysisPullRequestLimit: targetPullRequestLimit,
|
|
396
474
|
isValidationTarget,
|
|
397
|
-
}, { productRepository }).filter(error => !error.includes("defaultBranch") && !error.includes("visibility"));
|
|
475
|
+
}, { productRepository, allowProductRepository }).filter(error => !error.includes("defaultBranch") && !error.includes("visibility"));
|
|
398
476
|
if (targetNameErrors.length > 0) {
|
|
399
477
|
throw new Error(targetNameErrors.join(" "));
|
|
400
478
|
}
|
|
@@ -406,6 +484,7 @@ export async function collectGitHubSourceBundle({
|
|
|
406
484
|
analysisPullRequestLimit: targetPullRequestLimit,
|
|
407
485
|
isValidationTarget,
|
|
408
486
|
productRepository,
|
|
487
|
+
allowProductRepository,
|
|
409
488
|
});
|
|
410
489
|
const repositoryCoverage = coverageEntry({
|
|
411
490
|
family: "repository_metadata",
|
|
@@ -13,7 +13,7 @@ function validateRepoPart(value, label) {
|
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function validateTargetRepository(input, { productRepository } = {}) {
|
|
16
|
+
export function validateTargetRepository(input, { productRepository, allowProductRepository = false } = {}) {
|
|
17
17
|
const errors = [];
|
|
18
18
|
if (!input || typeof input !== "object") {
|
|
19
19
|
return ["target repository input must be an object."];
|
|
@@ -46,6 +46,8 @@ export function validateTargetRepository(input, { productRepository } = {}) {
|
|
|
46
46
|
const normalizedProductName = typeof productRepository?.name === "string" ? productRepository.name.toLowerCase() : null;
|
|
47
47
|
|
|
48
48
|
if (
|
|
49
|
+
!allowProductRepository
|
|
50
|
+
&&
|
|
49
51
|
normalizedInputOwner
|
|
50
52
|
&& normalizedInputName
|
|
51
53
|
&& normalizedProductOwner
|
|
@@ -99,5 +101,12 @@ export function productRepositoryTargetError(input) {
|
|
|
99
101
|
const repository = typeof input?.owner === "string" && typeof input?.name === "string"
|
|
100
102
|
? `${input.owner}/${input.name}`
|
|
101
103
|
: "the requested repository";
|
|
102
|
-
return `Cannot analyze ${repository} because it is this tool's product repository. The guard prevents accidental self-analysis during normal live runs; it is not a data-security boundary.
|
|
104
|
+
return `Cannot analyze ${repository} because it is this tool's product repository. The guard prevents accidental self-analysis during normal live runs; it is not a data-security boundary.
|
|
105
|
+
|
|
106
|
+
To learn the tool, run the bundled sample:
|
|
107
|
+
delivery-friction-analyzer --source sample --out reports/tutorial
|
|
108
|
+
|
|
109
|
+
To analyze a different repository, pass --repo owner/name.
|
|
110
|
+
|
|
111
|
+
If you intentionally want to analyze this product repository, rerun with --allow-product-repository. The CLI will check that required data is readable before writing artifacts. No GitHub data was collected.`;
|
|
103
112
|
}
|