delivery-friction-analyzer 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,7 +92,10 @@ Use these when you want to audit, automate, or build follow-up analysis:
92
92
  - `friction-report.json`: machine-readable report data.
93
93
  - `metrics-summary.json`: computed metrics used by the report.
94
94
  - `normalized.json`: normalized repository, PR, file, review, and validation entities.
95
- - `source-bundle.json`: collected source data for auditability.
95
+ - `source-bundle.json`: collected source data for auditability. Its canonical
96
+ analyzer contract is documented in `docs/contracts/source-bundle.md` and
97
+ checked by `schemas/github-source-bundle.schema.json`; it is not a full GitHub
98
+ API payload schema.
96
99
 
97
100
  When CSV exports are enabled, the bundle also includes spreadsheet-friendly evidence files:
98
101
 
@@ -0,0 +1,35 @@
1
+ # Source Bundle Contract
2
+
3
+ Schema: `schemas/github-source-bundle.schema.json`.
4
+
5
+ `source-bundle.json` is the analyzer's canonical GitHub collection artifact. It
6
+ is source-shaped evidence for later normalization, metrics, reports, and audit
7
+ work, but it is not a full GitHub REST or GraphQL API dump.
8
+
9
+ The `github-source-bundle.v1` schema covers collector-owned fields:
10
+
11
+ - collection metadata, target repository, repository metadata, latest-merged PR
12
+ selection, API coverage diagnostics, and language distribution context;
13
+ - optional sanitized contributor-source metadata: source type, repository path,
14
+ coverage/status diagnostics, and parsed hint count;
15
+ - pull request fields consumed downstream: identity, title, author object, URL,
16
+ state, timestamps, refs, final diff counts, PR-open diff source/confidence,
17
+ commits, files, reviews, review threads/comments, status check rollup,
18
+ workflow-run summary, and per-PR coverage.
19
+
20
+ The schema intentionally preserves unavailable and partial coverage. For
21
+ example, unavailable PR-open diff data keeps `source: "unavailable"` and
22
+ `confidence: "unavailable"` without inventing additions, deletions, or changed
23
+ file counts. Unavailable workflow-run history uses `totalCount: null` rather
24
+ than pretending zero runs were observed.
25
+
26
+ Contributor-source metadata must not persist raw contributor file contents or
27
+ parsed login lists. Parsed contributor hints may be used transiently during a
28
+ run, but generated artifacts keep only the sanitized metadata and `hintCount`.
29
+
30
+ The schema is strict for analyzer-owned wrapper objects and mapped entities. It
31
+ does not attempt to schema every raw GitHub field, and normal upstream GitHub API
32
+ additions should not require schema updates unless the collector maps them into
33
+ the canonical bundle. Future raw or provider-specific payloads must live under
34
+ an explicit `raw` subtree so downstream normalization stays tied to canonical
35
+ fields.
@@ -53,3 +53,7 @@ Availability decision:
53
53
  - `gh api 'repos/hannasdev/mcp-writing/actions/runs?branch=feat/extended-vocabulary-resolution&event=pull_request&per_page=30'`
54
54
 
55
55
  Fixtures store compact, redacted source-shaped data rather than full raw payloads.
56
+ Live `source-bundle.json` artifacts use the analyzer-owned
57
+ `github-source-bundle.v1` contract in `schemas/github-source-bundle.schema.json`;
58
+ that schema covers the canonical fields consumed downstream, not the full GitHub
59
+ REST or GraphQL API response shape.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delivery-friction-analyzer",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Local GitHub pull request analytics for delivery friction reports.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/release-log.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### 2026-06-21 — Source Bundle Schema Contract
6
+
7
+ - What changed: Live GitHub `source-bundle.json` artifacts now have a documented analyzer-owned schema that covers the canonical fields used downstream, including coverage metadata and sanitized contributor-source context.
8
+ - Why it matters: Maintainers can tell when collector output drifts from the supported contract without treating the bundle as a complete raw GitHub API dump.
9
+ - Who is affected: Maintainers reviewing source bundles, contract tests, collector changes, or packaged schema/docs.
10
+ - Action needed: None.
11
+ - PR: #56
12
+
5
13
  ### 2026-06-21 — Runtime Contract Preflight
6
14
 
7
15
  - What changed: GitHub analysis now rejects this tool's product repository before collection and validates repository profiles more completely before provider calls, including file-rule shape, duplicate IDs, unsupported keys, invalid values, and malformed regexes.
@@ -0,0 +1,470 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://delivery-friction-analyzer.local/schemas/github-source-bundle.schema.json",
4
+ "title": "GitHubSourceBundle",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schemaVersion",
9
+ "collectedAt",
10
+ "collector",
11
+ "targetRepository",
12
+ "repositoryMetadata",
13
+ "selection",
14
+ "coverage",
15
+ "languageDistribution",
16
+ "pullRequests"
17
+ ],
18
+ "properties": {
19
+ "schemaVersion": { "const": "github-source-bundle.v1" },
20
+ "collectedAt": { "type": "string" },
21
+ "collector": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": ["name", "provider"],
25
+ "properties": {
26
+ "name": { "const": "github-live-collector" },
27
+ "provider": { "type": "string", "minLength": 1 }
28
+ }
29
+ },
30
+ "targetRepository": { "$ref": "target-repository.schema.json" },
31
+ "repositoryMetadata": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "required": ["id", "name", "owner", "fullName", "defaultBranch", "visibility", "isPrivate", "htmlUrl"],
35
+ "properties": {
36
+ "id": { "type": ["integer", "string", "null"] },
37
+ "name": { "type": ["string", "null"] },
38
+ "owner": { "type": ["string", "null"] },
39
+ "fullName": { "type": ["string", "null"] },
40
+ "defaultBranch": { "type": ["string", "null"] },
41
+ "visibility": { "enum": ["public", "private", "unknown"] },
42
+ "isPrivate": { "type": ["boolean", "null"] },
43
+ "htmlUrl": { "type": ["string", "null"] }
44
+ }
45
+ },
46
+ "selection": {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "required": ["strategy", "requestedLimit", "collectedCount", "source"],
50
+ "properties": {
51
+ "strategy": { "const": "latest_merged_pull_requests" },
52
+ "requestedLimit": { "type": "integer", "minimum": 1, "maximum": 100 },
53
+ "collectedCount": { "type": "integer", "minimum": 0, "maximum": 100 },
54
+ "source": { "type": "string", "minLength": 1 }
55
+ }
56
+ },
57
+ "coverage": {
58
+ "type": "object",
59
+ "additionalProperties": false,
60
+ "required": ["status", "apiFamilies"],
61
+ "properties": {
62
+ "status": { "$ref": "#/$defs/coverageStatus" },
63
+ "apiFamilies": {
64
+ "type": "array",
65
+ "items": { "$ref": "#/$defs/apiCoverageEntry" }
66
+ }
67
+ }
68
+ },
69
+ "languageDistribution": {
70
+ "type": "object",
71
+ "additionalProperties": false,
72
+ "required": ["source", "bytesByLanguage", "coverage"],
73
+ "properties": {
74
+ "source": { "const": "rest:/repos/{owner}/{repo}/languages" },
75
+ "bytesByLanguage": {
76
+ "type": "object",
77
+ "additionalProperties": { "type": "integer", "minimum": 0 }
78
+ },
79
+ "coverage": {
80
+ "allOf": [
81
+ { "$ref": "#/$defs/coverageEntry" },
82
+ {
83
+ "type": "object",
84
+ "properties": {
85
+ "family": { "const": "languages" },
86
+ "source": { "const": "rest:/repos/{owner}/{repo}/languages" }
87
+ }
88
+ }
89
+ ]
90
+ }
91
+ }
92
+ },
93
+ "contributorSource": {
94
+ "type": "object",
95
+ "additionalProperties": false,
96
+ "required": ["sourceType", "path", "coverage", "hintCount"],
97
+ "properties": {
98
+ "sourceType": { "enum": ["all_contributors"] },
99
+ "path": { "type": "string", "minLength": 1 },
100
+ "coverage": {
101
+ "allOf": [
102
+ { "$ref": "#/$defs/coverageEntry" },
103
+ {
104
+ "type": "object",
105
+ "properties": {
106
+ "family": { "const": "contributor_source" },
107
+ "source": { "const": "rest:/repos/{owner}/{repo}/contents/{path}" }
108
+ }
109
+ }
110
+ ]
111
+ },
112
+ "hintCount": { "type": "integer", "minimum": 0 }
113
+ }
114
+ },
115
+ "pullRequests": {
116
+ "type": "array",
117
+ "items": { "$ref": "#/$defs/pullRequest" }
118
+ },
119
+ "raw": {
120
+ "type": "object",
121
+ "additionalProperties": true
122
+ }
123
+ },
124
+ "$defs": {
125
+ "coverageStatus": {
126
+ "enum": ["available", "partial", "unavailable", "malformed", "unsupported", "rate_limited"]
127
+ },
128
+ "nullableString": { "type": ["string", "null"] },
129
+ "author": {
130
+ "type": ["object", "null"],
131
+ "properties": {
132
+ "login": { "type": ["string", "null"] },
133
+ "type": { "type": ["string", "null"] }
134
+ },
135
+ "additionalProperties": true
136
+ },
137
+ "coverageEntry": {
138
+ "type": "object",
139
+ "additionalProperties": false,
140
+ "required": ["family", "source", "status", "attempts", "diagnostics", "downstreamImpact"],
141
+ "properties": {
142
+ "family": { "type": "string", "minLength": 1 },
143
+ "source": { "type": "string", "minLength": 1 },
144
+ "status": { "$ref": "#/$defs/coverageStatus" },
145
+ "attempts": { "type": "integer", "minimum": 0 },
146
+ "diagnostics": {
147
+ "type": "array",
148
+ "items": { "type": "string" }
149
+ },
150
+ "downstreamImpact": { "type": ["string", "null"] }
151
+ }
152
+ },
153
+ "apiCoverageEntry": {
154
+ "allOf": [
155
+ { "$ref": "#/$defs/coverageEntry" },
156
+ {
157
+ "type": "object",
158
+ "properties": {
159
+ "family": {
160
+ "enum": [
161
+ "repository_metadata",
162
+ "languages",
163
+ "pull_request_inventory",
164
+ "pull_request_details",
165
+ "review_threads",
166
+ "workflow_runs",
167
+ "pr_open_diff",
168
+ "contributor_source"
169
+ ]
170
+ }
171
+ }
172
+ }
173
+ ]
174
+ },
175
+ "pullRequest": {
176
+ "type": "object",
177
+ "additionalProperties": false,
178
+ "required": [
179
+ "number",
180
+ "title",
181
+ "author",
182
+ "url",
183
+ "state",
184
+ "createdAt",
185
+ "mergedAt",
186
+ "updatedAt",
187
+ "baseRefName",
188
+ "headRefName",
189
+ "headRefOid",
190
+ "additions",
191
+ "deletions",
192
+ "changedFiles",
193
+ "prOpenDiff",
194
+ "commits",
195
+ "files",
196
+ "reviews",
197
+ "reviewThreads",
198
+ "statusCheckRollup",
199
+ "workflowRuns",
200
+ "coverage"
201
+ ],
202
+ "properties": {
203
+ "number": { "type": "integer" },
204
+ "title": { "type": "string" },
205
+ "author": { "$ref": "#/$defs/author" },
206
+ "url": { "type": "string" },
207
+ "state": { "enum": ["OPEN", "CLOSED", "MERGED"] },
208
+ "createdAt": { "$ref": "#/$defs/nullableString" },
209
+ "mergedAt": { "$ref": "#/$defs/nullableString" },
210
+ "updatedAt": { "$ref": "#/$defs/nullableString" },
211
+ "baseRefName": { "$ref": "#/$defs/nullableString" },
212
+ "headRefName": { "$ref": "#/$defs/nullableString" },
213
+ "headRefOid": { "$ref": "#/$defs/nullableString" },
214
+ "additions": { "type": "integer", "minimum": 0 },
215
+ "deletions": { "type": "integer", "minimum": 0 },
216
+ "changedFiles": { "type": "integer", "minimum": 0 },
217
+ "prOpenDiff": { "$ref": "#/$defs/prOpenDiff" },
218
+ "commits": {
219
+ "type": "array",
220
+ "items": { "$ref": "#/$defs/commit" }
221
+ },
222
+ "files": {
223
+ "type": "array",
224
+ "items": { "$ref": "#/$defs/changedFile" }
225
+ },
226
+ "reviews": {
227
+ "type": "array",
228
+ "items": { "$ref": "#/$defs/review" }
229
+ },
230
+ "reviewThreads": { "$ref": "#/$defs/reviewThreads" },
231
+ "statusCheckRollup": {
232
+ "type": "array",
233
+ "items": { "$ref": "#/$defs/statusCheck" }
234
+ },
235
+ "workflowRuns": { "$ref": "#/$defs/workflowRuns" },
236
+ "coverage": { "$ref": "#/$defs/prCoverage" }
237
+ }
238
+ },
239
+ "prOpenDiff": {
240
+ "type": "object",
241
+ "additionalProperties": false,
242
+ "required": ["source", "confidence"],
243
+ "allOf": [
244
+ {
245
+ "if": {
246
+ "type": "object",
247
+ "anyOf": [
248
+ { "required": ["additions"] },
249
+ { "required": ["deletions"] },
250
+ { "required": ["changedFiles"] }
251
+ ]
252
+ },
253
+ "then": { "required": ["additions", "deletions", "changedFiles"] }
254
+ },
255
+ {
256
+ "if": {
257
+ "type": "object",
258
+ "required": ["source"],
259
+ "properties": { "source": { "enum": ["snapshot_only", "unavailable"] } }
260
+ },
261
+ "then": {
262
+ "not": {
263
+ "anyOf": [
264
+ { "required": ["additions"] },
265
+ { "required": ["deletions"] },
266
+ { "required": ["changedFiles"] }
267
+ ]
268
+ }
269
+ }
270
+ }
271
+ ],
272
+ "properties": {
273
+ "source": { "enum": ["direct", "reconstructed", "snapshot_only", "unavailable"] },
274
+ "confidence": { "type": "string" },
275
+ "reason": { "type": "string" },
276
+ "additions": { "type": "integer", "minimum": 0 },
277
+ "deletions": { "type": "integer", "minimum": 0 },
278
+ "changedFiles": { "type": "integer", "minimum": 0 }
279
+ }
280
+ },
281
+ "commit": {
282
+ "type": "object",
283
+ "additionalProperties": false,
284
+ "required": ["oid", "authoredDate", "committedDate", "messageHeadline"],
285
+ "properties": {
286
+ "oid": { "$ref": "#/$defs/nullableString" },
287
+ "authoredDate": { "$ref": "#/$defs/nullableString" },
288
+ "committedDate": { "$ref": "#/$defs/nullableString" },
289
+ "messageHeadline": { "$ref": "#/$defs/nullableString" }
290
+ }
291
+ },
292
+ "changedFile": {
293
+ "type": "object",
294
+ "additionalProperties": false,
295
+ "required": ["path", "additions", "deletions", "changeType"],
296
+ "properties": {
297
+ "path": { "$ref": "#/$defs/nullableString" },
298
+ "additions": { "type": "integer", "minimum": 0 },
299
+ "deletions": { "type": "integer", "minimum": 0 },
300
+ "changeType": { "$ref": "#/$defs/nullableString" }
301
+ }
302
+ },
303
+ "review": {
304
+ "type": "object",
305
+ "additionalProperties": false,
306
+ "required": ["id", "author", "submittedAt", "state", "commitOid", "generatedCommentCount", "failedAttempt"],
307
+ "properties": {
308
+ "id": { "$ref": "#/$defs/nullableString" },
309
+ "author": { "$ref": "#/$defs/author" },
310
+ "submittedAt": { "$ref": "#/$defs/nullableString" },
311
+ "state": { "$ref": "#/$defs/nullableString" },
312
+ "commitOid": { "$ref": "#/$defs/nullableString" },
313
+ "generatedCommentCount": { "type": ["integer", "null"], "minimum": 0 },
314
+ "failedAttempt": { "type": "boolean" }
315
+ }
316
+ },
317
+ "reviewThreads": {
318
+ "type": "object",
319
+ "additionalProperties": false,
320
+ "required": ["source", "totalCount", "nodes"],
321
+ "properties": {
322
+ "source": { "enum": ["graphql:repository.pullRequest.reviewThreads", "unavailable"] },
323
+ "totalCount": { "type": "integer", "minimum": 0 },
324
+ "nodes": {
325
+ "type": "array",
326
+ "items": { "$ref": "#/$defs/reviewThread" }
327
+ }
328
+ }
329
+ },
330
+ "reviewThread": {
331
+ "type": "object",
332
+ "additionalProperties": false,
333
+ "required": ["id", "isResolved", "isOutdated", "path", "line", "comments"],
334
+ "properties": {
335
+ "id": { "type": "string" },
336
+ "isResolved": { "type": "boolean" },
337
+ "isOutdated": { "type": "boolean" },
338
+ "path": { "$ref": "#/$defs/nullableString" },
339
+ "line": { "type": ["integer", "null"], "minimum": 1 },
340
+ "comments": {
341
+ "type": "array",
342
+ "items": { "$ref": "#/$defs/reviewThreadComment" }
343
+ }
344
+ }
345
+ },
346
+ "reviewThreadComment": {
347
+ "type": "object",
348
+ "additionalProperties": false,
349
+ "required": ["databaseId", "author", "path", "line", "originalLine", "createdAt", "updatedAt", "url"],
350
+ "properties": {
351
+ "databaseId": { "type": ["integer", "string", "null"] },
352
+ "author": { "$ref": "#/$defs/author" },
353
+ "path": { "$ref": "#/$defs/nullableString" },
354
+ "line": { "type": ["integer", "null"], "minimum": 1 },
355
+ "originalLine": { "type": ["integer", "null"], "minimum": 1 },
356
+ "createdAt": { "$ref": "#/$defs/nullableString" },
357
+ "updatedAt": { "$ref": "#/$defs/nullableString" },
358
+ "url": { "$ref": "#/$defs/nullableString" }
359
+ }
360
+ },
361
+ "statusCheck": {
362
+ "type": "object",
363
+ "additionalProperties": false,
364
+ "required": ["__typename", "name", "context", "workflowName", "status", "conclusion", "startedAt", "completedAt"],
365
+ "properties": {
366
+ "__typename": { "enum": ["CheckRun", "StatusContext"] },
367
+ "name": { "$ref": "#/$defs/nullableString" },
368
+ "context": { "$ref": "#/$defs/nullableString" },
369
+ "workflowName": { "$ref": "#/$defs/nullableString" },
370
+ "status": { "$ref": "#/$defs/nullableString" },
371
+ "conclusion": { "$ref": "#/$defs/nullableString" },
372
+ "startedAt": { "$ref": "#/$defs/nullableString" },
373
+ "completedAt": { "$ref": "#/$defs/nullableString" }
374
+ }
375
+ },
376
+ "workflowRuns": {
377
+ "type": "object",
378
+ "additionalProperties": false,
379
+ "required": ["source", "totalCount", "conclusions", "runs"],
380
+ "properties": {
381
+ "source": { "enum": ["rest:/repos/{owner}/{repo}/actions/runs?branch={branch}&event=pull_request", "unavailable"] },
382
+ "totalCount": { "type": ["integer", "null"], "minimum": 0 },
383
+ "conclusions": {
384
+ "type": "object",
385
+ "additionalProperties": { "type": "integer", "minimum": 0 }
386
+ },
387
+ "runs": {
388
+ "type": "array",
389
+ "items": { "$ref": "#/$defs/workflowRun" }
390
+ }
391
+ }
392
+ },
393
+ "workflowRun": {
394
+ "type": "object",
395
+ "additionalProperties": false,
396
+ "required": [
397
+ "id",
398
+ "name",
399
+ "workflowName",
400
+ "headSha",
401
+ "headBranch",
402
+ "event",
403
+ "status",
404
+ "conclusion",
405
+ "createdAt",
406
+ "updatedAt",
407
+ "runStartedAt",
408
+ "htmlUrl"
409
+ ],
410
+ "properties": {
411
+ "id": { "type": ["integer", "string", "null"] },
412
+ "name": { "$ref": "#/$defs/nullableString" },
413
+ "workflowName": { "$ref": "#/$defs/nullableString" },
414
+ "headSha": { "$ref": "#/$defs/nullableString" },
415
+ "headBranch": { "$ref": "#/$defs/nullableString" },
416
+ "event": { "$ref": "#/$defs/nullableString" },
417
+ "status": { "$ref": "#/$defs/nullableString" },
418
+ "conclusion": { "$ref": "#/$defs/nullableString" },
419
+ "createdAt": { "$ref": "#/$defs/nullableString" },
420
+ "updatedAt": { "$ref": "#/$defs/nullableString" },
421
+ "runStartedAt": { "$ref": "#/$defs/nullableString" },
422
+ "htmlUrl": { "$ref": "#/$defs/nullableString" }
423
+ }
424
+ },
425
+ "prCoverage": {
426
+ "type": "object",
427
+ "additionalProperties": false,
428
+ "required": ["prOpenDiff", "reviewThreads", "workflowRuns"],
429
+ "properties": {
430
+ "prOpenDiff": {
431
+ "allOf": [
432
+ { "$ref": "#/$defs/coverageEntry" },
433
+ {
434
+ "type": "object",
435
+ "properties": {
436
+ "family": { "const": "pr_open_diff" },
437
+ "source": { "const": "historical_snapshot" },
438
+ "status": { "enum": ["available", "partial", "unavailable", "rate_limited"] }
439
+ }
440
+ }
441
+ ]
442
+ },
443
+ "reviewThreads": {
444
+ "allOf": [
445
+ { "$ref": "#/$defs/coverageEntry" },
446
+ {
447
+ "type": "object",
448
+ "properties": {
449
+ "family": { "const": "review_threads" },
450
+ "source": { "const": "graphql:repository.pullRequest.reviewThreads" }
451
+ }
452
+ }
453
+ ]
454
+ },
455
+ "workflowRuns": {
456
+ "allOf": [
457
+ { "$ref": "#/$defs/coverageEntry" },
458
+ {
459
+ "type": "object",
460
+ "properties": {
461
+ "family": { "const": "workflow_runs" },
462
+ "source": { "const": "rest:/repos/{owner}/{repo}/actions/runs?branch={branch}&event=pull_request" }
463
+ }
464
+ }
465
+ ]
466
+ }
467
+ }
468
+ }
469
+ }
470
+ }