delivery-friction-analyzer 0.17.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/docs/reference/repository-profile.md +12 -0
- package/examples/tutorial/profile.json +110 -0
- package/examples/tutorial/report-excerpt.md +7 -0
- package/examples/tutorial/source-bundle.json +388 -0
- package/package.json +2 -1
- package/release-log.md +16 -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`;
|
|
@@ -41,6 +41,12 @@ This keeps validation-target details in profile data rather than hardcoded produ
|
|
|
41
41
|
|
|
42
42
|
Each file rule must include a unique lowercase `id`, a non-empty `match` object, a supported `category`, and a supported `role`. Optional `functionalSurface` values use the same lowercase identifier shape as rule IDs. Optional `generated` values must be booleans. Unsupported rule or matcher keys fail validation, and invalid JavaScript regexes fail before collection with the rule ID and matcher field in the error.
|
|
43
43
|
|
|
44
|
+
File rules may include an optional `notes` string. Notes are human-readable
|
|
45
|
+
profile context for maintainers and reviewers. They can explain why a path rule
|
|
46
|
+
exists, what product surface it represents, or why a generated or low-signal
|
|
47
|
+
path is intentionally classified. Notes do not affect matching order,
|
|
48
|
+
classification output, scoring, collection, reports, or CSV exports.
|
|
49
|
+
|
|
44
50
|
## Pull Request Classes
|
|
45
51
|
|
|
46
52
|
`prClasses` is optional. Rules are evaluated in order and the first matching rule wins. The current profile contract supports title-only matchers:
|
|
@@ -64,6 +70,12 @@ Class identifiers are validated as lower-kebab-case or lower_snake_case strings.
|
|
|
64
70
|
|
|
65
71
|
PR class evidence is interpretive and profile-driven. It helps reports show class distributions, dominance notes, and explicit `--exclude-pr-class` filtering when you request filtering, but configured PR class rules do not change default scoring, ranking formulas, collection, or CSV export shape by themselves.
|
|
66
72
|
|
|
73
|
+
PR class rules may include an optional `notes` string. Use notes to document the
|
|
74
|
+
repository convention behind a class rule, such as Conventional Commit-style
|
|
75
|
+
title prefixes or release-train naming. Notes are valid JSON profile data for
|
|
76
|
+
humans only; they do not change title matching, PR class assignment, filtering,
|
|
77
|
+
scoring, collection, reports, or CSV exports.
|
|
78
|
+
|
|
67
79
|
Interactive setup can add a release PR class rule from a confirmed title convention using the current title-only matcher shape. Branch strategy answers stay in `workflow` context only; they do not create branch-based PR class matching. Branch-based class matching is deferred until a future matcher contract supports branch fields explicitly.
|
|
68
80
|
|
|
69
81
|
### Copyable PR Class Examples
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "repository-profile.v1",
|
|
3
|
+
"repository": {
|
|
4
|
+
"owner": "example-org",
|
|
5
|
+
"name": "delivery-dashboard"
|
|
6
|
+
},
|
|
7
|
+
"rules": [
|
|
8
|
+
{
|
|
9
|
+
"id": "frontend-ui-paths",
|
|
10
|
+
"match": { "prefix": "apps/web/" },
|
|
11
|
+
"category": "code",
|
|
12
|
+
"role": "product_ui",
|
|
13
|
+
"functionalSurface": "dashboard_ui",
|
|
14
|
+
"generated": false,
|
|
15
|
+
"notes": "Maps the customer-facing dashboard app to the UI surface so broad UI changes are not treated as unknown code."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "api-runtime-paths",
|
|
19
|
+
"match": { "prefix": "services/api/" },
|
|
20
|
+
"category": "code",
|
|
21
|
+
"role": "core_product_code",
|
|
22
|
+
"functionalSurface": "delivery_api",
|
|
23
|
+
"generated": false,
|
|
24
|
+
"notes": "Represents backend delivery workflow behavior that usually needs API-focused review."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "worker-runtime-paths",
|
|
28
|
+
"match": { "prefix": "workers/" },
|
|
29
|
+
"category": "code",
|
|
30
|
+
"role": "core_product_code",
|
|
31
|
+
"functionalSurface": "background_jobs",
|
|
32
|
+
"generated": false
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "integration-tests",
|
|
36
|
+
"match": { "prefix": "tests/integration/" },
|
|
37
|
+
"category": "tests",
|
|
38
|
+
"role": "tests",
|
|
39
|
+
"functionalSurface": "test_suite",
|
|
40
|
+
"generated": false
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "workflow-config",
|
|
44
|
+
"match": { "prefix": ".github/workflows/" },
|
|
45
|
+
"category": "infrastructure",
|
|
46
|
+
"role": "infrastructure",
|
|
47
|
+
"functionalSurface": "ci_workflows",
|
|
48
|
+
"generated": false,
|
|
49
|
+
"notes": "Classifies CI workflow edits separately from product runtime changes."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "planning-notes",
|
|
53
|
+
"match": { "prefix": "docs/initiatives/" },
|
|
54
|
+
"category": "docs",
|
|
55
|
+
"role": "planning_docs",
|
|
56
|
+
"functionalSurface": "planning",
|
|
57
|
+
"generated": false
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "release-notes",
|
|
61
|
+
"match": { "exact": "CHANGELOG.md" },
|
|
62
|
+
"category": "docs",
|
|
63
|
+
"role": "release_notes",
|
|
64
|
+
"functionalSurface": "release_docs",
|
|
65
|
+
"generated": false
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "generated-api-client",
|
|
69
|
+
"match": { "prefix": "packages/generated-client/" },
|
|
70
|
+
"category": "generated",
|
|
71
|
+
"role": "generated_or_vendored",
|
|
72
|
+
"functionalSurface": "generated_client",
|
|
73
|
+
"generated": true,
|
|
74
|
+
"notes": "Shows how generated code can stay visible while receiving low-signal weighting."
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
"prClasses": [
|
|
78
|
+
{
|
|
79
|
+
"id": "feature-title",
|
|
80
|
+
"class": "feature",
|
|
81
|
+
"match": { "titleRegex": "^feat(?:\\([^)]+\\))?:" },
|
|
82
|
+
"notes": "Use when feature PR titles follow a Conventional Commit-style prefix."
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "fix-title",
|
|
86
|
+
"class": "fix",
|
|
87
|
+
"match": { "titleRegex": "^fix(?:\\([^)]+\\))?:" },
|
|
88
|
+
"notes": "Separates corrective work from feature delivery in the class distribution."
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "test-title",
|
|
92
|
+
"class": "test",
|
|
93
|
+
"match": { "titleRegex": "^test(?:\\([^)]+\\))?:" }
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "maintenance-title",
|
|
97
|
+
"class": "maintenance",
|
|
98
|
+
"match": { "titleRegex": "^(?:chore|ci|build|refactor)(?:\\([^)]+\\))?:" }
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"workflow": {
|
|
102
|
+
"primaryMergeMethod": "squash_merge",
|
|
103
|
+
"releaseStrategy": "release_prs",
|
|
104
|
+
"branchStrategy": "trunk_based"
|
|
105
|
+
},
|
|
106
|
+
"contributors": {
|
|
107
|
+
"sourceType": "all_contributors",
|
|
108
|
+
"path": ".all-contributorsrc"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Tutorial Sample Report Excerpt
|
|
2
|
+
|
|
3
|
+
Source: Bundled synthetic sample, not live GitHub data.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
Use the excerpt in public docs to set expectations: the sample is synthetic tutorial evidence, not observed GitHub data, and its value is showing how caveats, profile classifications, PR classes, workflow context, and CSV evidence appear in a local report bundle.
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "source-bundle.v1",
|
|
3
|
+
"collectedAt": "2026-06-15T12:00:00Z",
|
|
4
|
+
"source": {
|
|
5
|
+
"kind": "sample",
|
|
6
|
+
"label": "Bundled synthetic sample, not live GitHub data",
|
|
7
|
+
"metadata": {
|
|
8
|
+
"tutorial": true,
|
|
9
|
+
"publicSafety": "fully synthetic placeholder data"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"collector": {
|
|
13
|
+
"name": "tutorial-sample-collector",
|
|
14
|
+
"provider": "bundled-sample"
|
|
15
|
+
},
|
|
16
|
+
"targetRepository": {
|
|
17
|
+
"owner": "example-org",
|
|
18
|
+
"name": "delivery-dashboard",
|
|
19
|
+
"defaultBranch": "main",
|
|
20
|
+
"visibility": "public",
|
|
21
|
+
"analysisPullRequestLimit": 4,
|
|
22
|
+
"isValidationTarget": false
|
|
23
|
+
},
|
|
24
|
+
"repositoryMetadata": {
|
|
25
|
+
"id": "sample-repository-001",
|
|
26
|
+
"name": "delivery-dashboard",
|
|
27
|
+
"owner": "example-org",
|
|
28
|
+
"fullName": "example-org/delivery-dashboard",
|
|
29
|
+
"defaultBranch": "main",
|
|
30
|
+
"visibility": "public",
|
|
31
|
+
"isPrivate": false,
|
|
32
|
+
"htmlUrl": "https://example.com/example-org/delivery-dashboard"
|
|
33
|
+
},
|
|
34
|
+
"selection": {
|
|
35
|
+
"strategy": "bundled_tutorial_pull_requests",
|
|
36
|
+
"requestedLimit": 4,
|
|
37
|
+
"collectedCount": 4,
|
|
38
|
+
"source": "bundled tutorial sample"
|
|
39
|
+
},
|
|
40
|
+
"coverage": {
|
|
41
|
+
"status": "partial",
|
|
42
|
+
"sourceFamilies": [
|
|
43
|
+
{
|
|
44
|
+
"family": "repository_metadata",
|
|
45
|
+
"source": "bundled tutorial sample",
|
|
46
|
+
"status": "available",
|
|
47
|
+
"attempts": 1,
|
|
48
|
+
"diagnostics": [],
|
|
49
|
+
"downstreamImpact": "Fictional repository identity is available for report labeling."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"family": "pull_request_inventory",
|
|
53
|
+
"source": "bundled tutorial sample",
|
|
54
|
+
"status": "available",
|
|
55
|
+
"attempts": 1,
|
|
56
|
+
"diagnostics": [],
|
|
57
|
+
"downstreamImpact": "Four fictional merged pull requests are available for normalization and metrics."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"family": "pull_request_details",
|
|
61
|
+
"source": "bundled tutorial sample",
|
|
62
|
+
"status": "available",
|
|
63
|
+
"attempts": 1,
|
|
64
|
+
"diagnostics": [],
|
|
65
|
+
"downstreamImpact": "Synthetic lifecycle, files, reviews, checks, and workflow summaries are populated."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"family": "review_threads",
|
|
69
|
+
"source": "bundled tutorial sample",
|
|
70
|
+
"status": "available",
|
|
71
|
+
"attempts": 1,
|
|
72
|
+
"diagnostics": [],
|
|
73
|
+
"downstreamImpact": "Review-thread counts and comment sources are populated without comment text."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"family": "workflow_runs",
|
|
77
|
+
"source": "bundled tutorial sample",
|
|
78
|
+
"status": "partial",
|
|
79
|
+
"attempts": 1,
|
|
80
|
+
"diagnostics": ["One PR intentionally has unavailable workflow-run history to demonstrate coverage caveats."],
|
|
81
|
+
"downstreamImpact": "Validation-gap metrics use observed runs where present and preserve unavailable coverage where missing."
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"family": "pr_open_diff",
|
|
85
|
+
"source": "bundled tutorial sample",
|
|
86
|
+
"status": "partial",
|
|
87
|
+
"attempts": 1,
|
|
88
|
+
"diagnostics": ["One PR intentionally lacks PR-open diff data to demonstrate partial diff-growth coverage."],
|
|
89
|
+
"downstreamImpact": "Diff-growth ratios are computed only for PRs with open-time snapshots."
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"family": "languages",
|
|
93
|
+
"source": "bundled tutorial sample",
|
|
94
|
+
"status": "available",
|
|
95
|
+
"attempts": 1,
|
|
96
|
+
"diagnostics": [],
|
|
97
|
+
"downstreamImpact": "Language distribution is illustrative context only."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"family": "contributor_source",
|
|
101
|
+
"source": "bundled tutorial sample",
|
|
102
|
+
"status": "partial",
|
|
103
|
+
"attempts": 1,
|
|
104
|
+
"diagnostics": ["Synthetic contributor hints omit one reviewer to demonstrate partial profile context."],
|
|
105
|
+
"downstreamImpact": "Contributor-aware comment-source hints are available; scoring and person-level outputs are unchanged."
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"languageDistribution": {
|
|
110
|
+
"source": "bundled tutorial sample",
|
|
111
|
+
"bytesByLanguage": {
|
|
112
|
+
"TypeScript": 420000,
|
|
113
|
+
"JavaScript": 180000,
|
|
114
|
+
"Markdown": 52000,
|
|
115
|
+
"YAML": 18000
|
|
116
|
+
},
|
|
117
|
+
"coverage": {
|
|
118
|
+
"family": "languages",
|
|
119
|
+
"source": "bundled tutorial sample",
|
|
120
|
+
"status": "available",
|
|
121
|
+
"attempts": 1,
|
|
122
|
+
"diagnostics": [],
|
|
123
|
+
"downstreamImpact": "Language distribution is illustrative context only."
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"contributorSource": {
|
|
127
|
+
"sourceType": "all_contributors",
|
|
128
|
+
"path": ".all-contributorsrc",
|
|
129
|
+
"coverage": {
|
|
130
|
+
"family": "contributor_source",
|
|
131
|
+
"source": "bundled tutorial sample",
|
|
132
|
+
"status": "partial",
|
|
133
|
+
"attempts": 1,
|
|
134
|
+
"diagnostics": ["Synthetic contributor hints omit one reviewer to demonstrate partial profile context."],
|
|
135
|
+
"downstreamImpact": "Contributor-aware comment-source hints are available; scoring and person-level outputs are unchanged."
|
|
136
|
+
},
|
|
137
|
+
"hintCount": 3
|
|
138
|
+
},
|
|
139
|
+
"pullRequests": [
|
|
140
|
+
{
|
|
141
|
+
"number": 101,
|
|
142
|
+
"title": "feat: add route health dashboard",
|
|
143
|
+
"author": { "login": "sample-author-ui", "type": "User" },
|
|
144
|
+
"url": "https://example.com/pull/101",
|
|
145
|
+
"state": "MERGED",
|
|
146
|
+
"createdAt": "2026-05-04T09:00:00Z",
|
|
147
|
+
"mergedAt": "2026-05-05T16:30:00Z",
|
|
148
|
+
"updatedAt": "2026-05-05T16:45:00Z",
|
|
149
|
+
"baseRefName": "main",
|
|
150
|
+
"headRefName": "sample/route-health-dashboard",
|
|
151
|
+
"headRefOid": "sample101merge",
|
|
152
|
+
"additions": 410,
|
|
153
|
+
"deletions": 96,
|
|
154
|
+
"changedFiles": 5,
|
|
155
|
+
"prOpenDiff": { "source": "direct", "confidence": "high", "additions": 190, "deletions": 44, "changedFiles": 5 },
|
|
156
|
+
"commits": [
|
|
157
|
+
{ "oid": "sample101a", "authoredDate": "2026-05-04T08:40:00Z", "committedDate": "2026-05-04T08:42:00Z", "messageHeadline": "feat: add route health dashboard" },
|
|
158
|
+
{ "oid": "sample101b", "authoredDate": "2026-05-05T12:15:00Z", "committedDate": "2026-05-05T12:16:00Z", "messageHeadline": "test: cover delayed route states" }
|
|
159
|
+
],
|
|
160
|
+
"files": [
|
|
161
|
+
{ "path": "apps/web/src/routes/RouteHealthPanel.tsx", "additions": 140, "deletions": 22, "changeType": "ADDED" },
|
|
162
|
+
{ "path": "apps/web/src/routes/RouteHealthChart.tsx", "additions": 94, "deletions": 16, "changeType": "ADDED" },
|
|
163
|
+
{ "path": "services/api/src/routes/health-summary.ts", "additions": 78, "deletions": 18, "changeType": "MODIFIED" },
|
|
164
|
+
{ "path": "tests/integration/route-health.test.ts", "additions": 82, "deletions": 20, "changeType": "ADDED" },
|
|
165
|
+
{ "path": "CHANGELOG.md", "additions": 16, "deletions": 20, "changeType": "MODIFIED" }
|
|
166
|
+
],
|
|
167
|
+
"reviews": [
|
|
168
|
+
{ "id": "sample-review-101-a", "author": { "login": "sample-reviewer-a", "type": "User" }, "submittedAt": "2026-05-04T14:20:00Z", "state": "CHANGES_REQUESTED", "commitOid": "sample101a", "generatedCommentCount": 0, "failedAttempt": true },
|
|
169
|
+
{ "id": "sample-review-101-b", "author": { "login": "sample-reviewer-a", "type": "User" }, "submittedAt": "2026-05-05T15:20:00Z", "state": "APPROVED", "commitOid": "sample101b", "generatedCommentCount": 0, "failedAttempt": false }
|
|
170
|
+
],
|
|
171
|
+
"reviewThreads": {
|
|
172
|
+
"source": "bundled tutorial sample:review_threads",
|
|
173
|
+
"totalCount": 2,
|
|
174
|
+
"nodes": [
|
|
175
|
+
{ "id": "sample-thread-101-a", "isResolved": true, "isOutdated": false, "path": "apps/web/src/routes/RouteHealthPanel.tsx", "line": 88, "comments": [
|
|
176
|
+
{ "databaseId": "sample-comment-101-a", "author": { "login": "sample-reviewer-a", "type": "User" }, "path": "apps/web/src/routes/RouteHealthPanel.tsx", "line": 88, "originalLine": 88, "createdAt": "2026-05-04T14:21:00Z", "updatedAt": "2026-05-04T14:21:00Z", "url": "https://example.com/pull/101#discussion-sample-101-a" },
|
|
177
|
+
{ "databaseId": "sample-comment-101-b", "author": { "login": "sample-author-ui", "type": "User" }, "path": "apps/web/src/routes/RouteHealthPanel.tsx", "line": 88, "originalLine": 88, "createdAt": "2026-05-05T12:18:00Z", "updatedAt": "2026-05-05T12:18:00Z", "url": "https://example.com/pull/101#discussion-sample-101-b" }
|
|
178
|
+
] },
|
|
179
|
+
{ "id": "sample-thread-101-b", "isResolved": true, "isOutdated": true, "path": "services/api/src/routes/health-summary.ts", "line": 41, "comments": [
|
|
180
|
+
{ "databaseId": "sample-comment-101-c", "author": { "login": "sample-reviewer-b", "type": "User" }, "path": "services/api/src/routes/health-summary.ts", "line": 41, "originalLine": 41, "createdAt": "2026-05-04T15:02:00Z", "updatedAt": "2026-05-04T15:02:00Z", "url": "https://example.com/pull/101#discussion-sample-101-c" }
|
|
181
|
+
] }
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
"statusCheckRollup": [
|
|
185
|
+
{ "__typename": "CheckRun", "name": "unit", "context": null, "workflowName": "CI", "status": "COMPLETED", "conclusion": "SUCCESS", "startedAt": "2026-05-04T09:10:00Z", "completedAt": "2026-05-04T09:16:00Z" },
|
|
186
|
+
{ "__typename": "CheckRun", "name": "integration", "context": null, "workflowName": "CI", "status": "COMPLETED", "conclusion": "FAILURE", "startedAt": "2026-05-04T09:12:00Z", "completedAt": "2026-05-04T09:24:00Z" },
|
|
187
|
+
{ "__typename": "CheckRun", "name": "integration", "context": null, "workflowName": "CI", "status": "COMPLETED", "conclusion": "SUCCESS", "startedAt": "2026-05-05T12:20:00Z", "completedAt": "2026-05-05T12:31:00Z" }
|
|
188
|
+
],
|
|
189
|
+
"workflowRuns": {
|
|
190
|
+
"source": "bundled tutorial sample:workflow_runs",
|
|
191
|
+
"totalCount": 2,
|
|
192
|
+
"conclusions": { "success": 1, "failure": 1 },
|
|
193
|
+
"runs": [
|
|
194
|
+
{ "id": "sample-run-101-a", "name": "CI", "workflowName": "CI", "headSha": "sample101a", "headBranch": "sample/route-health-dashboard", "event": "pull_request", "status": "completed", "conclusion": "failure", "createdAt": "2026-05-04T09:10:00Z", "updatedAt": "2026-05-04T09:24:00Z", "runStartedAt": "2026-05-04T09:12:00Z", "htmlUrl": "https://example.com/actions/runs/sample-run-101-a" },
|
|
195
|
+
{ "id": "sample-run-101-b", "name": "CI", "workflowName": "CI", "headSha": "sample101b", "headBranch": "sample/route-health-dashboard", "event": "pull_request", "status": "completed", "conclusion": "success", "createdAt": "2026-05-05T12:20:00Z", "updatedAt": "2026-05-05T12:31:00Z", "runStartedAt": "2026-05-05T12:21:00Z", "htmlUrl": "https://example.com/actions/runs/sample-run-101-b" }
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
"coverage": {
|
|
199
|
+
"prOpenDiff": { "family": "pr_open_diff", "source": "bundled tutorial sample", "status": "available", "attempts": 1, "diagnostics": [], "downstreamImpact": "Diff-growth metrics can compare open and merge sizes." },
|
|
200
|
+
"reviewThreads": { "family": "review_threads", "source": "bundled tutorial sample", "status": "available", "attempts": 1, "diagnostics": [], "downstreamImpact": "Review-thread evidence is available." },
|
|
201
|
+
"workflowRuns": { "family": "workflow_runs", "source": "bundled tutorial sample", "status": "available", "attempts": 1, "diagnostics": [], "downstreamImpact": "Workflow-run evidence is available." }
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"number": 102,
|
|
206
|
+
"title": "fix: retry stalled dispatch jobs",
|
|
207
|
+
"author": { "login": "sample-author-api", "type": "User" },
|
|
208
|
+
"url": "https://example.com/pull/102",
|
|
209
|
+
"state": "MERGED",
|
|
210
|
+
"createdAt": "2026-05-07T10:00:00Z",
|
|
211
|
+
"mergedAt": "2026-05-07T18:10:00Z",
|
|
212
|
+
"updatedAt": "2026-05-07T18:20:00Z",
|
|
213
|
+
"baseRefName": "main",
|
|
214
|
+
"headRefName": "sample/retry-dispatch-jobs",
|
|
215
|
+
"headRefOid": "sample102merge",
|
|
216
|
+
"additions": 126,
|
|
217
|
+
"deletions": 34,
|
|
218
|
+
"changedFiles": 4,
|
|
219
|
+
"prOpenDiff": { "source": "direct", "confidence": "high", "additions": 120, "deletions": 30, "changedFiles": 4 },
|
|
220
|
+
"commits": [
|
|
221
|
+
{ "oid": "sample102a", "authoredDate": "2026-05-07T09:45:00Z", "committedDate": "2026-05-07T09:46:00Z", "messageHeadline": "fix: retry stalled dispatch jobs" },
|
|
222
|
+
{ "oid": "sample102b", "authoredDate": "2026-05-07T15:10:00Z", "committedDate": "2026-05-07T15:12:00Z", "messageHeadline": "test: cover retry backoff" }
|
|
223
|
+
],
|
|
224
|
+
"files": [
|
|
225
|
+
{ "path": "workers/dispatch/retry.ts", "additions": 58, "deletions": 18, "changeType": "MODIFIED" },
|
|
226
|
+
{ "path": "services/api/src/jobs/dispatch.ts", "additions": 24, "deletions": 8, "changeType": "MODIFIED" },
|
|
227
|
+
{ "path": "tests/integration/dispatch-retry.test.ts", "additions": 42, "deletions": 8, "changeType": "ADDED" },
|
|
228
|
+
{ "path": "docs/initiatives/retry-jobs/checklist.md", "additions": 2, "deletions": 0, "changeType": "ADDED" }
|
|
229
|
+
],
|
|
230
|
+
"reviews": [
|
|
231
|
+
{ "id": "sample-review-102-a", "author": { "login": "sample-reviewer-b", "type": "User" }, "submittedAt": "2026-05-07T13:00:00Z", "state": "COMMENTED", "commitOid": "sample102a", "generatedCommentCount": 0, "failedAttempt": false },
|
|
232
|
+
{ "id": "sample-review-102-b", "author": { "login": "sample-reviewer-b", "type": "User" }, "submittedAt": "2026-05-07T17:40:00Z", "state": "APPROVED", "commitOid": "sample102b", "generatedCommentCount": 0, "failedAttempt": false }
|
|
233
|
+
],
|
|
234
|
+
"reviewThreads": {
|
|
235
|
+
"source": "bundled tutorial sample:review_threads",
|
|
236
|
+
"totalCount": 1,
|
|
237
|
+
"nodes": [
|
|
238
|
+
{ "id": "sample-thread-102-a", "isResolved": true, "isOutdated": false, "path": "workers/dispatch/retry.ts", "line": 30, "comments": [
|
|
239
|
+
{ "databaseId": "sample-comment-102-a", "author": { "login": "sample-reviewer-b", "type": "User" }, "path": "workers/dispatch/retry.ts", "line": 30, "originalLine": 30, "createdAt": "2026-05-07T13:01:00Z", "updatedAt": "2026-05-07T13:01:00Z", "url": "https://example.com/pull/102#discussion-sample-102-a" },
|
|
240
|
+
{ "databaseId": "sample-comment-102-b", "author": { "login": "sample-author-api", "type": "User" }, "path": "workers/dispatch/retry.ts", "line": 30, "originalLine": 30, "createdAt": "2026-05-07T15:15:00Z", "updatedAt": "2026-05-07T15:15:00Z", "url": "https://example.com/pull/102#discussion-sample-102-b" }
|
|
241
|
+
] }
|
|
242
|
+
]
|
|
243
|
+
},
|
|
244
|
+
"statusCheckRollup": [
|
|
245
|
+
{ "__typename": "CheckRun", "name": "unit", "context": null, "workflowName": "CI", "status": "COMPLETED", "conclusion": "SUCCESS", "startedAt": "2026-05-07T10:10:00Z", "completedAt": "2026-05-07T10:16:00Z" }
|
|
246
|
+
],
|
|
247
|
+
"workflowRuns": {
|
|
248
|
+
"source": "bundled tutorial sample:workflow_runs",
|
|
249
|
+
"totalCount": 1,
|
|
250
|
+
"conclusions": { "success": 1 },
|
|
251
|
+
"runs": [
|
|
252
|
+
{ "id": "sample-run-102-a", "name": "CI", "workflowName": "CI", "headSha": "sample102b", "headBranch": "sample/retry-dispatch-jobs", "event": "pull_request", "status": "completed", "conclusion": "success", "createdAt": "2026-05-07T15:20:00Z", "updatedAt": "2026-05-07T15:29:00Z", "runStartedAt": "2026-05-07T15:21:00Z", "htmlUrl": "https://example.com/actions/runs/sample-run-102-a" }
|
|
253
|
+
]
|
|
254
|
+
},
|
|
255
|
+
"coverage": {
|
|
256
|
+
"prOpenDiff": { "family": "pr_open_diff", "source": "bundled tutorial sample", "status": "available", "attempts": 1, "diagnostics": [], "downstreamImpact": "Diff-growth metrics can compare open and merge sizes." },
|
|
257
|
+
"reviewThreads": { "family": "review_threads", "source": "bundled tutorial sample", "status": "available", "attempts": 1, "diagnostics": [], "downstreamImpact": "Review-thread evidence is available." },
|
|
258
|
+
"workflowRuns": { "family": "workflow_runs", "source": "bundled tutorial sample", "status": "available", "attempts": 1, "diagnostics": [], "downstreamImpact": "Workflow-run evidence is available." }
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"number": 103,
|
|
263
|
+
"title": "test: add billing webhook contract coverage",
|
|
264
|
+
"author": { "login": "sample-author-test", "type": "User" },
|
|
265
|
+
"url": "https://example.com/pull/103",
|
|
266
|
+
"state": "MERGED",
|
|
267
|
+
"createdAt": "2026-05-10T11:00:00Z",
|
|
268
|
+
"mergedAt": "2026-05-10T14:40:00Z",
|
|
269
|
+
"updatedAt": "2026-05-10T14:45:00Z",
|
|
270
|
+
"baseRefName": "main",
|
|
271
|
+
"headRefName": "sample/billing-webhook-tests",
|
|
272
|
+
"headRefOid": "sample103merge",
|
|
273
|
+
"additions": 88,
|
|
274
|
+
"deletions": 10,
|
|
275
|
+
"changedFiles": 3,
|
|
276
|
+
"prOpenDiff": { "source": "unavailable", "confidence": "unavailable", "reason": "Open-time snapshot intentionally omitted from the bundled sample." },
|
|
277
|
+
"commits": [
|
|
278
|
+
{ "oid": "sample103a", "authoredDate": "2026-05-10T10:40:00Z", "committedDate": "2026-05-10T10:41:00Z", "messageHeadline": "test: add billing webhook contract coverage" }
|
|
279
|
+
],
|
|
280
|
+
"files": [
|
|
281
|
+
{ "path": "tests/integration/billing-webhooks.test.ts", "additions": 72, "deletions": 4, "changeType": "ADDED" },
|
|
282
|
+
{ "path": "services/api/src/billing/webhook-fixtures.ts", "additions": 10, "deletions": 4, "changeType": "MODIFIED" },
|
|
283
|
+
{ "path": "packages/generated-client/billing.ts", "additions": 6, "deletions": 2, "changeType": "MODIFIED" }
|
|
284
|
+
],
|
|
285
|
+
"reviews": [
|
|
286
|
+
{ "id": "sample-review-103-a", "author": { "login": "sample-reviewer-a", "type": "User" }, "submittedAt": "2026-05-10T13:30:00Z", "state": "APPROVED", "commitOid": "sample103a", "generatedCommentCount": 0, "failedAttempt": false }
|
|
287
|
+
],
|
|
288
|
+
"reviewThreads": { "source": "bundled tutorial sample:review_threads", "totalCount": 0, "nodes": [] },
|
|
289
|
+
"statusCheckRollup": [
|
|
290
|
+
{ "__typename": "CheckRun", "name": "unit", "context": null, "workflowName": "CI", "status": "COMPLETED", "conclusion": "SUCCESS", "startedAt": "2026-05-10T11:10:00Z", "completedAt": "2026-05-10T11:15:00Z" }
|
|
291
|
+
],
|
|
292
|
+
"workflowRuns": {
|
|
293
|
+
"source": "unavailable",
|
|
294
|
+
"totalCount": null,
|
|
295
|
+
"conclusions": {},
|
|
296
|
+
"runs": []
|
|
297
|
+
},
|
|
298
|
+
"coverage": {
|
|
299
|
+
"prOpenDiff": { "family": "pr_open_diff", "source": "bundled tutorial sample", "status": "unavailable", "attempts": 1, "diagnostics": ["Open-time snapshot intentionally omitted from the bundled sample."], "downstreamImpact": "Diff-growth metrics are unavailable for this PR." },
|
|
300
|
+
"reviewThreads": { "family": "review_threads", "source": "bundled tutorial sample", "status": "available", "attempts": 1, "diagnostics": [], "downstreamImpact": "Review-thread evidence is available and shows zero threads." },
|
|
301
|
+
"workflowRuns": { "family": "workflow_runs", "source": "bundled tutorial sample", "status": "unavailable", "attempts": 1, "diagnostics": ["Workflow-run history intentionally omitted for this PR."], "downstreamImpact": "Validation-gap metrics preserve unavailable workflow coverage." }
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"number": 104,
|
|
306
|
+
"title": "feat: consolidate dispatch settings",
|
|
307
|
+
"author": { "login": "sample-author-platform", "type": "User" },
|
|
308
|
+
"url": "https://example.com/pull/104",
|
|
309
|
+
"state": "MERGED",
|
|
310
|
+
"createdAt": "2026-05-14T08:00:00Z",
|
|
311
|
+
"mergedAt": "2026-05-17T19:00:00Z",
|
|
312
|
+
"updatedAt": "2026-05-17T19:15:00Z",
|
|
313
|
+
"baseRefName": "main",
|
|
314
|
+
"headRefName": "sample/consolidate-dispatch-settings",
|
|
315
|
+
"headRefOid": "sample104merge",
|
|
316
|
+
"additions": 980,
|
|
317
|
+
"deletions": 420,
|
|
318
|
+
"changedFiles": 8,
|
|
319
|
+
"prOpenDiff": { "source": "direct", "confidence": "high", "additions": 260, "deletions": 90, "changedFiles": 6 },
|
|
320
|
+
"commits": [
|
|
321
|
+
{ "oid": "sample104a", "authoredDate": "2026-05-14T07:45:00Z", "committedDate": "2026-05-14T07:46:00Z", "messageHeadline": "feat: consolidate dispatch settings" },
|
|
322
|
+
{ "oid": "sample104b", "authoredDate": "2026-05-15T10:20:00Z", "committedDate": "2026-05-15T10:22:00Z", "messageHeadline": "fix: split overloaded settings model" },
|
|
323
|
+
{ "oid": "sample104c", "authoredDate": "2026-05-16T16:30:00Z", "committedDate": "2026-05-16T16:31:00Z", "messageHeadline": "test: cover settings migration" },
|
|
324
|
+
{ "oid": "sample104d", "authoredDate": "2026-05-17T13:00:00Z", "committedDate": "2026-05-17T13:02:00Z", "messageHeadline": "ci: repair migration fixture" }
|
|
325
|
+
],
|
|
326
|
+
"files": [
|
|
327
|
+
{ "path": "apps/web/src/settings/DispatchSettingsPage.tsx", "additions": 210, "deletions": 90, "changeType": "MODIFIED" },
|
|
328
|
+
{ "path": "apps/web/src/settings/DispatchSettingsForm.tsx", "additions": 180, "deletions": 70, "changeType": "ADDED" },
|
|
329
|
+
{ "path": "services/api/src/settings/dispatch-settings.ts", "additions": 190, "deletions": 88, "changeType": "MODIFIED" },
|
|
330
|
+
{ "path": "workers/dispatch/settings-sync.ts", "additions": 150, "deletions": 64, "changeType": "ADDED" },
|
|
331
|
+
{ "path": "tests/integration/dispatch-settings.test.ts", "additions": 120, "deletions": 30, "changeType": "ADDED" },
|
|
332
|
+
{ "path": ".github/workflows/dispatch-settings.yml", "additions": 62, "deletions": 20, "changeType": "ADDED" },
|
|
333
|
+
{ "path": "docs/initiatives/dispatch-settings/prd.md", "additions": 48, "deletions": 22, "changeType": "MODIFIED" },
|
|
334
|
+
{ "path": "packages/generated-client/settings.ts", "additions": 20, "deletions": 36, "changeType": "MODIFIED" }
|
|
335
|
+
],
|
|
336
|
+
"reviews": [
|
|
337
|
+
{ "id": "sample-review-104-a", "author": { "login": "sample-reviewer-a", "type": "User" }, "submittedAt": "2026-05-14T16:00:00Z", "state": "CHANGES_REQUESTED", "commitOid": "sample104a", "generatedCommentCount": 0, "failedAttempt": true },
|
|
338
|
+
{ "id": "sample-review-104-b", "author": { "login": "sample-reviewer-b", "type": "User" }, "submittedAt": "2026-05-15T14:00:00Z", "state": "CHANGES_REQUESTED", "commitOid": "sample104b", "generatedCommentCount": 0, "failedAttempt": true },
|
|
339
|
+
{ "id": "sample-review-104-c", "author": { "login": "sample-reviewer-a", "type": "User" }, "submittedAt": "2026-05-16T18:30:00Z", "state": "COMMENTED", "commitOid": "sample104c", "generatedCommentCount": 0, "failedAttempt": false },
|
|
340
|
+
{ "id": "sample-review-104-d", "author": { "login": "sample-reviewer-b", "type": "User" }, "submittedAt": "2026-05-17T17:45:00Z", "state": "APPROVED", "commitOid": "sample104d", "generatedCommentCount": 0, "failedAttempt": false }
|
|
341
|
+
],
|
|
342
|
+
"reviewThreads": {
|
|
343
|
+
"source": "bundled tutorial sample:review_threads",
|
|
344
|
+
"totalCount": 3,
|
|
345
|
+
"nodes": [
|
|
346
|
+
{ "id": "sample-thread-104-a", "isResolved": true, "isOutdated": true, "path": "services/api/src/settings/dispatch-settings.ts", "line": 65, "comments": [
|
|
347
|
+
{ "databaseId": "sample-comment-104-a", "author": { "login": "sample-reviewer-a", "type": "User" }, "path": "services/api/src/settings/dispatch-settings.ts", "line": 65, "originalLine": 65, "createdAt": "2026-05-14T16:02:00Z", "updatedAt": "2026-05-14T16:02:00Z", "url": "https://example.com/pull/104#discussion-sample-104-a" },
|
|
348
|
+
{ "databaseId": "sample-comment-104-b", "author": { "login": "sample-author-platform", "type": "User" }, "path": "services/api/src/settings/dispatch-settings.ts", "line": 65, "originalLine": 65, "createdAt": "2026-05-15T10:24:00Z", "updatedAt": "2026-05-15T10:24:00Z", "url": "https://example.com/pull/104#discussion-sample-104-b" }
|
|
349
|
+
] },
|
|
350
|
+
{ "id": "sample-thread-104-b", "isResolved": true, "isOutdated": false, "path": "apps/web/src/settings/DispatchSettingsForm.tsx", "line": 112, "comments": [
|
|
351
|
+
{ "databaseId": "sample-comment-104-c", "author": { "login": "sample-reviewer-b", "type": "User" }, "path": "apps/web/src/settings/DispatchSettingsForm.tsx", "line": 112, "originalLine": 112, "createdAt": "2026-05-15T14:05:00Z", "updatedAt": "2026-05-15T14:05:00Z", "url": "https://example.com/pull/104#discussion-sample-104-c" },
|
|
352
|
+
{ "databaseId": "sample-comment-104-d", "author": { "login": "sample-author-platform", "type": "User" }, "path": "apps/web/src/settings/DispatchSettingsForm.tsx", "line": 112, "originalLine": 112, "createdAt": "2026-05-16T16:40:00Z", "updatedAt": "2026-05-16T16:40:00Z", "url": "https://example.com/pull/104#discussion-sample-104-d" }
|
|
353
|
+
] },
|
|
354
|
+
{ "id": "sample-thread-104-c", "isResolved": false, "isOutdated": false, "path": ".github/workflows/dispatch-settings.yml", "line": 18, "comments": [
|
|
355
|
+
{ "databaseId": "sample-comment-104-e", "author": { "login": "sample-ci-bot", "type": "Bot" }, "path": ".github/workflows/dispatch-settings.yml", "line": 18, "originalLine": 18, "createdAt": "2026-05-17T13:10:00Z", "updatedAt": "2026-05-17T13:10:00Z", "url": "https://example.com/pull/104#discussion-sample-104-e" }
|
|
356
|
+
] }
|
|
357
|
+
]
|
|
358
|
+
},
|
|
359
|
+
"statusCheckRollup": [
|
|
360
|
+
{ "__typename": "CheckRun", "name": "unit", "context": null, "workflowName": "CI", "status": "COMPLETED", "conclusion": "FAILURE", "startedAt": "2026-05-14T08:10:00Z", "completedAt": "2026-05-14T08:16:00Z" },
|
|
361
|
+
{ "__typename": "CheckRun", "name": "integration", "context": null, "workflowName": "CI", "status": "COMPLETED", "conclusion": "FAILURE", "startedAt": "2026-05-14T08:12:00Z", "completedAt": "2026-05-14T08:28:00Z" },
|
|
362
|
+
{ "__typename": "CheckRun", "name": "migration", "context": null, "workflowName": "Migration checks", "status": "COMPLETED", "conclusion": "SUCCESS", "startedAt": "2026-05-17T13:10:00Z", "completedAt": "2026-05-17T13:24:00Z" }
|
|
363
|
+
],
|
|
364
|
+
"workflowRuns": {
|
|
365
|
+
"source": "bundled tutorial sample:workflow_runs",
|
|
366
|
+
"totalCount": 4,
|
|
367
|
+
"conclusions": { "success": 1, "failure": 2, "cancelled": 1 },
|
|
368
|
+
"runs": [
|
|
369
|
+
{ "id": "sample-run-104-a", "name": "CI", "workflowName": "CI", "headSha": "sample104a", "headBranch": "sample/consolidate-dispatch-settings", "event": "pull_request", "status": "completed", "conclusion": "failure", "createdAt": "2026-05-14T08:10:00Z", "updatedAt": "2026-05-14T08:28:00Z", "runStartedAt": "2026-05-14T08:12:00Z", "htmlUrl": "https://example.com/actions/runs/sample-run-104-a" },
|
|
370
|
+
{ "id": "sample-run-104-b", "name": "CI", "workflowName": "CI", "headSha": "sample104b", "headBranch": "sample/consolidate-dispatch-settings", "event": "pull_request", "status": "completed", "conclusion": "failure", "createdAt": "2026-05-15T10:30:00Z", "updatedAt": "2026-05-15T10:48:00Z", "runStartedAt": "2026-05-15T10:31:00Z", "htmlUrl": "https://example.com/actions/runs/sample-run-104-b" },
|
|
371
|
+
{ "id": "sample-run-104-c", "name": "CI", "workflowName": "CI", "headSha": "sample104c", "headBranch": "sample/consolidate-dispatch-settings", "event": "pull_request", "status": "completed", "conclusion": "cancelled", "createdAt": "2026-05-16T16:36:00Z", "updatedAt": "2026-05-16T16:39:00Z", "runStartedAt": "2026-05-16T16:37:00Z", "htmlUrl": "https://example.com/actions/runs/sample-run-104-c" },
|
|
372
|
+
{ "id": "sample-run-104-d", "name": "Migration checks", "workflowName": "Migration checks", "headSha": "sample104d", "headBranch": "sample/consolidate-dispatch-settings", "event": "pull_request", "status": "completed", "conclusion": "success", "createdAt": "2026-05-17T13:10:00Z", "updatedAt": "2026-05-17T13:24:00Z", "runStartedAt": "2026-05-17T13:11:00Z", "htmlUrl": "https://example.com/actions/runs/sample-run-104-d" }
|
|
373
|
+
]
|
|
374
|
+
},
|
|
375
|
+
"coverage": {
|
|
376
|
+
"prOpenDiff": { "family": "pr_open_diff", "source": "bundled tutorial sample", "status": "available", "attempts": 1, "diagnostics": [], "downstreamImpact": "Diff-growth metrics can compare open and merge sizes." },
|
|
377
|
+
"reviewThreads": { "family": "review_threads", "source": "bundled tutorial sample", "status": "available", "attempts": 1, "diagnostics": [], "downstreamImpact": "Review-thread evidence is available." },
|
|
378
|
+
"workflowRuns": { "family": "workflow_runs", "source": "bundled tutorial sample", "status": "partial", "attempts": 1, "diagnostics": ["One cancelled run is included to demonstrate validation caveats."], "downstreamImpact": "Validation-gap metrics include observed failed and cancelled workflow runs." }
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
],
|
|
382
|
+
"raw": {
|
|
383
|
+
"sample": {
|
|
384
|
+
"description": "Fictional tutorial data for Delivery Friction Analyzer.",
|
|
385
|
+
"containsPrivateData": false
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "delivery-friction-analyzer",
|
|
3
|
-
"version": "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",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"src",
|
|
12
12
|
"schemas",
|
|
13
|
+
"examples/tutorial",
|
|
13
14
|
"docs/contracts",
|
|
14
15
|
"docs/reference",
|
|
15
16
|
"fixtures/github/mcp-writing/profile.json",
|
package/release-log.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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
|
+
|
|
13
|
+
### 2026-06-27 — Tutorial Sample Bundle
|
|
14
|
+
|
|
15
|
+
- What changed: Added the packaged tutorial sample source bundle, educational sample profile, and generated-aligned report excerpt for a fully synthetic repository.
|
|
16
|
+
- Why it matters: Maintainers can review and ship a public-safe sample report experience without relying on private repository access or live GitHub data.
|
|
17
|
+
- Who is affected: Maintainers preparing the tutorial sample experience and future users who inspect the bundled sample artifacts after sample CLI wiring lands.
|
|
18
|
+
- Action needed: None.
|
|
19
|
+
- PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/61
|
|
20
|
+
|
|
5
21
|
### 2026-06-26 — Generic Source Bundle Report Wording
|
|
6
22
|
|
|
7
23
|
- What changed: Report Markdown, methodology-adjacent contract docs, and CSV coverage expectations now use generic source-bundle and source-family wording for `source-bundle.v1`, including `collectionCoverage.status` and `collectionCoverage.sourceFamilies`.
|
|
@@ -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"),
|