bmad-module-skill-forge 3.0.0 → 5.0.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/.claude-plugin/marketplace.json +1 -1
- package/README.md +3 -3
- package/docs/_data/pinned.yaml +19 -13
- package/docs/architecture.md +2 -2
- package/docs/concepts.md +4 -4
- package/docs/examples.md +33 -26
- package/docs/getting-started.md +3 -3
- package/docs/how-it-works.md +2 -2
- package/docs/index.md +1 -1
- package/docs/skill-model.md +41 -39
- package/docs/troubleshooting.md +8 -0
- package/docs/verifying-a-skill.md +46 -21
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +5 -5
- package/package.json +1 -1
- package/src/knowledge/qmd-registry.md +1 -1
- package/src/knowledge/version-paths.md +1 -1
- package/src/shared/health-check.md +2 -50
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +2 -2
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +12 -4
- package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +212 -59
- package/src/shared/scripts/skf-build-change-manifest.py +1437 -178
- package/src/shared/scripts/skf-classify-changed-files.py +78 -0
- package/src/shared/scripts/skf-compare-file-hashes.py +266 -104
- package/src/shared/scripts/skf-count-tokens.py +54 -10
- package/src/shared/scripts/skf-derive-assembly-shape.py +18 -1
- package/src/shared/scripts/skf-detect-scripts-assets.py +196 -54
- package/src/shared/scripts/skf-detect-workspaces.py +67 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +6 -5
- package/src/shared/scripts/skf-extract-public-api.py +1673 -189
- package/src/shared/scripts/skf-extraction-inventory.py +584 -46
- package/src/shared/scripts/skf-extraction-snapshot.py +158 -33
- package/src/shared/scripts/skf-forge-tier-rw.py +26 -3
- package/src/shared/scripts/skf-github-probe.py +47 -8
- package/src/shared/scripts/skf-hash-content.py +14 -8
- package/src/shared/scripts/skf-load-provenance.py +3 -1
- package/src/shared/scripts/skf-merge-doc-urls.py +142 -19
- package/src/shared/scripts/skf-parse-gaps.py +135 -26
- package/src/shared/scripts/skf-preflight.py +15 -1
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +25 -1
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- package/src/shared/scripts/skf-resolve-authoritative-files.py +29 -3
- package/src/shared/scripts/skf-resolve-customization.py +197 -0
- package/src/shared/scripts/skf-shard-body.py +239 -41
- package/src/shared/scripts/skf-structural-diff.py +133 -18
- package/src/shared/scripts/skf-update-active-symlink.py +115 -62
- package/src/shared/scripts/skf-validate-brief-schema.py +19 -1
- package/src/shared/scripts/skf-validate-output.py +12 -3
- package/src/shared/scripts/skf-verify-provenance-completeness.py +1439 -79
- package/src/shared/scripts/skf-write-skill-brief.py +41 -8
- package/src/shared/tool-requirements.yaml +8 -2
- package/src/skf-analyze-source/SKILL.md +2 -2
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/references/map-unit-exports.md +1 -1
- package/src/skf-audit-skill/SKILL.md +2 -2
- package/src/skf-audit-skill/assets/drift-report-template.md +1 -0
- package/src/skf-audit-skill/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/references/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +2 -1
- package/src/skf-audit-skill/references/re-index.md +14 -9
- package/src/skf-audit-skill/references/report.md +2 -2
- package/src/skf-audit-skill/references/semantic-diff.md +15 -15
- package/src/skf-audit-skill/references/structural-diff.md +10 -5
- package/src/skf-audit-skill/scripts/render-drift-tables.py +29 -2
- package/src/skf-brief-skill/SKILL.md +3 -3
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +4 -11
- package/src/skf-brief-skill/references/analyze-target.md +6 -6
- package/src/skf-brief-skill/references/confirm-brief.md +1 -1
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-brief-skill/references/step-auto-brief.md +11 -29
- package/src/skf-brief-skill/references/version-resolution.md +5 -9
- package/src/skf-campaign/SKILL.md +2 -2
- package/src/skf-campaign/references/step-05-skill-loop.md +6 -1
- package/src/skf-create-skill/SKILL.md +2 -2
- package/src/skf-create-skill/assets/compile-assembly-rules.md +5 -3
- package/src/skf-create-skill/assets/skill-sections.md +7 -7
- package/src/skf-create-skill/references/authoritative-files-protocol.md +2 -23
- package/src/skf-create-skill/references/compile.md +31 -60
- package/src/skf-create-skill/references/component-extraction.md +3 -3
- package/src/skf-create-skill/references/enrich.md +2 -2
- package/src/skf-create-skill/references/entry-points-by-hand.md +24 -7
- package/src/skf-create-skill/references/extract.md +23 -19
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +9 -9
- package/src/skf-create-skill/references/extraction-patterns.md +10 -11
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +21 -5
- package/src/skf-create-skill/references/source-resolution-protocols.md +12 -17
- package/src/skf-create-skill/references/sub/fetch-docs.md +1 -1
- package/src/skf-create-stack-skill/SKILL.md +2 -2
- package/src/skf-create-stack-skill/references/validate.md +1 -1
- package/src/skf-drop-skill/SKILL.md +2 -2
- package/src/skf-drop-skill/references/execute.md +5 -1
- package/src/skf-drop-skill/references/health-check.md +2 -0
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +2 -2
- package/src/skf-drop-skill/references/select.md +7 -3
- package/src/skf-export-skill/SKILL.md +2 -2
- package/src/skf-export-skill/assets/snippet-format.md +5 -5
- package/src/skf-export-skill/references/generate-snippet.md +22 -15
- package/src/skf-export-skill/references/load-skill.md +1 -1
- package/src/skf-export-skill/references/token-report.md +29 -9
- package/src/skf-forger/SKILL.md +2 -2
- package/src/skf-quick-skill/SKILL.md +2 -2
- package/src/skf-refine-architecture/SKILL.md +6 -8
- package/src/skf-rename-skill/SKILL.md +2 -2
- package/src/skf-setup/SKILL.md +2 -2
- package/src/skf-test-skill/SKILL.md +2 -2
- package/src/skf-test-skill/assets/output-section-formats.md +6 -4
- package/src/skf-test-skill/references/coherence-check.md +5 -4
- package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
- package/src/skf-test-skill/references/coverage-check-tiers.md +5 -5
- package/src/skf-test-skill/references/coverage-check.md +69 -111
- package/src/skf-test-skill/references/init.md +5 -6
- package/src/skf-test-skill/references/migration-section-rules.md +26 -14
- package/src/skf-test-skill/references/report.md +11 -9
- package/src/skf-test-skill/references/scoring-rules.md +8 -6
- package/src/skf-test-skill/references/source-access-protocol.md +15 -18
- package/src/skf-test-skill/scripts/build-result-context.py +114 -21
- package/src/skf-test-skill/scripts/gap-ledger.py +580 -80
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +646 -107
- package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
- package/src/skf-test-skill/scripts/reconcile-coverage.py +450 -23
- package/src/skf-test-skill/scripts/score-signatures.py +356 -61
- package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +108 -9
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +7 -5
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/SKILL.md +2 -2
- package/src/skf-update-skill/references/detect-changes.md +32 -13
- package/src/skf-update-skill/references/gap-driven.md +37 -22
- package/src/skf-update-skill/references/init.md +6 -2
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +17 -12
- package/src/skf-update-skill/references/re-extract.md +4 -2
- package/src/skf-update-skill/references/report.md +7 -4
- package/src/skf-update-skill/references/write.md +77 -36
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +157 -42
- package/src/skf-verify-stack/SKILL.md +2 -2
- package/tools/cli/lib/tool-check.js +158 -2
|
@@ -19,11 +19,25 @@ It covers the four branches §2c enumerates:
|
|
|
19
19
|
Missing = barrel_set − documented_set, Stale = documented_set − barrel_set,
|
|
20
20
|
Export Coverage = |Documented| / |barrel_set| * 100.
|
|
21
21
|
|
|
22
|
-
* "scalar" (§2b
|
|
23
|
-
|
|
24
|
-
Documented =
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
* "scalar" (§2b's scalar branch, stats.effective_denominator): the
|
|
23
|
+
denominator is effective_denominator, Missing = max(0, denominator −
|
|
24
|
+
Documented), Export Coverage = min(100, Documented / denominator * 100).
|
|
25
|
+
The numerator's set: when the surface counts its named set S
|
|
26
|
+
(`setCounted`, the surface's `denominator.setCounted`: a scope set, not
|
|
27
|
+
`all`, each of whose names sits at the file that defines it, read
|
|
28
|
+
whole), Documented = |documented_set ∩ S| (`numeratorSource`
|
|
29
|
+
"surface"; the doc-text lookup and `verifiedNumerator` are not read),
|
|
30
|
+
`missing` = S − documented_set, and a warning when |S| differs from the
|
|
31
|
+
denominator. Otherwise (`all`, a set read by eye or from an incomplete
|
|
32
|
+
extraction, no surface, or an empty S, which warns): look each
|
|
33
|
+
documented name up in SKILL.md ∪ references/*.md, Documented = count
|
|
34
|
+
present, or `verifiedNumerator`; `missing` lists no name.
|
|
35
|
+
Stale: on a surface read from the source (`sourceRead`, its
|
|
36
|
+
`guards.deflation.applicable`, or a counted set), documented_set − (S ∪
|
|
37
|
+
the whole public surface, `allSet`, the `all` set: a nested package's
|
|
38
|
+
name is in the scope sets alone), so a documented name outside S that
|
|
39
|
+
the source exports is neither counted nor stale; else not enumerable
|
|
40
|
+
(empty).
|
|
27
41
|
|
|
28
42
|
* "stack" (skill_type == "stack", empty source barrel): look each
|
|
29
43
|
composition-surface name (provenance-map cited contracts, ::-excluded, or
|
|
@@ -39,8 +53,9 @@ It covers the four branches §2c enumerates:
|
|
|
39
53
|
description and, for a function or method, params and a return type
|
|
40
54
|
(validate-inventory.py's rule).
|
|
41
55
|
|
|
42
|
-
The two lookup branches
|
|
43
|
-
|
|
56
|
+
The two lookup branches (and a scalar set larger than its stored count)
|
|
57
|
+
bound their outputs because the numerator and the denominator are
|
|
58
|
+
independent measures of different sets: the lookup count can
|
|
44
59
|
exceed a consumer-surface denominator without either being wrong. Unbounded,
|
|
45
60
|
that produced a negative Missing and a >100% coverage that compute-score.py
|
|
46
61
|
rejects as out of range. `documented` is left as the true count (it is reported
|
|
@@ -68,7 +83,12 @@ each sets the input field it names and wins over the same field in the JSON:
|
|
|
68
83
|
--inventory PATH exports: the validated inventory validate-inventory.py
|
|
69
84
|
wrote with --output
|
|
70
85
|
--surface PATH barrelSet: one name set of load-coverage-inputs.py
|
|
71
|
-
surface, chosen by --surface-set (default "all")
|
|
86
|
+
surface, chosen by --surface-set (default "all"),
|
|
87
|
+
with what the scalar branch reads: surfaceSet (that
|
|
88
|
+
name), allSet (its `all` set), sourceRead (its
|
|
89
|
+
`guards.deflation.applicable`) and setCounted (its
|
|
90
|
+
`denominator.setCounted`, when --surface-set names
|
|
91
|
+
its `denominator.surfaceSet`)
|
|
72
92
|
--coverage-inputs PATH load-coverage-inputs.py metadata output: for the
|
|
73
93
|
scalar branch, denominatorValue from its
|
|
74
94
|
`effectiveDenominator` (stats.effective_denominator);
|
|
@@ -76,7 +96,7 @@ each sets the input field it names and wins over the same field in the JSON:
|
|
|
76
96
|
denominatorValue from its `stack` object
|
|
77
97
|
--verified PATH scalar: verify-declared-numerator.py's result; when it
|
|
78
98
|
found the declared count inflated, its `verified`
|
|
79
|
-
count is the numerator
|
|
99
|
+
count is the numerator (not on a counted set)
|
|
80
100
|
--output PATH also write the result to this file (UTF-8 JSON); a
|
|
81
101
|
refused input removes a file left there earlier
|
|
82
102
|
|
|
@@ -84,8 +104,12 @@ Input schema (one object):
|
|
|
84
104
|
{
|
|
85
105
|
"denominatorSource": "barrel" | "scalar" | "stack" | "docsOnly", # required
|
|
86
106
|
"exports": [ {"name": "...", "kind": "..."}, ... ], # §1 inventory
|
|
87
|
-
"barrelSet": ["a", "b", ...], # barrel: resolved name set
|
|
107
|
+
"barrelSet": ["a", "b", ...], # barrel, scalar: resolved name set
|
|
88
108
|
"perFileResults": [ {"exports_found": ["a", ...]} ], # barrel: union'd if no barrelSet
|
|
109
|
+
"surfaceSet": "tier_a_include", # scalar: barrelSet's name (null or "" is "all")
|
|
110
|
+
"allSet": ["a", "b", "c", ...], # scalar: the whole public surface
|
|
111
|
+
"sourceRead": <bool>, # scalar: the surface was read from the source
|
|
112
|
+
"setCounted": <bool>, # scalar: count barrelSet (a trusted scope set)
|
|
89
113
|
"denominatorValue": <int>, # scalar/stack: resolved denominator
|
|
90
114
|
"compositionNames": ["lib::x", ...], # stack: names to look up
|
|
91
115
|
"skillPackagePath": "/path/to/skill", # scalar/stack: SKILL.md ∪ references
|
|
@@ -98,16 +122,20 @@ Output (stdout, one object):
|
|
|
98
122
|
"denominatorSource": "barrel" | "scalar" | "stack" | "docsOnly",
|
|
99
123
|
"denominator": <int>,
|
|
100
124
|
"documented": <int>,
|
|
101
|
-
"
|
|
125
|
+
"documentedNames": [names], # enumerated, scalar counting its set: the documented names the numerator counts
|
|
126
|
+
"missing": [names], # enumerated, scalar counting its set: source names not documented; others: []
|
|
102
127
|
"missingCount": <int>, # always present; never negative
|
|
103
|
-
"stale": [names], # enumerated: documented names not in source
|
|
128
|
+
"stale": [names], # enumerated, scalar on a source-read surface: documented names not in source
|
|
104
129
|
"staleCount": <int>, # always present
|
|
105
|
-
"staleApplicable": <bool>, #
|
|
130
|
+
"staleApplicable": <bool>, # true for enumerated and scalar on a source-read surface
|
|
106
131
|
"exportCoverage": <float>, # scalar/stack: capped at 100
|
|
107
132
|
"numeratorSurplus": <int>, # scalar/stack only: max(0, documented − denominator)
|
|
108
133
|
"coverageUncapped": <float>, # scalar/stack only: the ratio before the cap
|
|
109
134
|
"coverageCapped": <bool>, # scalar/stack only: true when the cap bound the ratio
|
|
110
|
-
"numeratorSource": "lookup" | "verified", # scalar only
|
|
135
|
+
"numeratorSource": "lookup" | "verified" | "surface", # scalar only
|
|
136
|
+
"surfaceSet": "<name>", # scalar counting its set: the set it counted
|
|
137
|
+
"surfaceSetSize": <int>, # scalar counting its set: |S|
|
|
138
|
+
"warnings": [str], # scalar on a source-read surface: an empty S, or |S| unlike the denominator
|
|
111
139
|
"incomplete": [{"name", "kind", "missing"}] # docsOnly only: the incomplete items
|
|
112
140
|
}
|
|
113
141
|
|
|
@@ -115,6 +143,56 @@ Exit codes:
|
|
|
115
143
|
0 reconciliation emitted successfully
|
|
116
144
|
1 no input, input that does not parse as JSON, or an input file that cannot be read
|
|
117
145
|
2 input parsed but schema/semantics invalid (error object emitted as JSON)
|
|
146
|
+
|
|
147
|
+
Subcommand render (coverage-check.md §5 writes its output unchanged):
|
|
148
|
+
uv run reconcile-coverage.py render --coverage <coverage.json>
|
|
149
|
+
[--surface <surface.json>] [--signatures <signatures.json>]
|
|
150
|
+
|
|
151
|
+
prints the Markdown of the Coverage Analysis section that follows its
|
|
152
|
+
header lines, from the run files:
|
|
153
|
+
**Files Analyzed:** the distinct files the --surface export records name
|
|
154
|
+
(N/A without --surface: a docs-only run has none)
|
|
155
|
+
### Export Coverage on the enumerated branch, one row per name the
|
|
156
|
+
denominator counts, per stale name and per name whose
|
|
157
|
+
signature mismatches, by name:
|
|
158
|
+
Type the first kind the surface records for the name, else -
|
|
159
|
+
Documented no for a missing name, else yes
|
|
160
|
+
Signature no for a Critical mismatch, yes (Low) or yes (Info)
|
|
161
|
+
for a name whose mismatches score-signatures.py
|
|
162
|
+
graded Low or Info alone (they count as matching;
|
|
163
|
+
Low when it has both), yes for a compared name that
|
|
164
|
+
matches, unverified for a documented name when no
|
|
165
|
+
--signatures was given, - otherwise
|
|
166
|
+
Source Location for a name whose signature mismatches, the file:line
|
|
167
|
+
of its most severe mismatch (the declaration it was
|
|
168
|
+
compared with); else the file:line the surface
|
|
169
|
+
records for the name, a record with a line preferred
|
|
170
|
+
(gap-ledger.py's rule for a missing export's
|
|
171
|
+
Source), else -
|
|
172
|
+
Status FAIL for a Critical signature mismatch (a mismatch
|
|
173
|
+
with no `severity`, from an older signatures.json,
|
|
174
|
+
counts as one), WARN for a missing or stale name or
|
|
175
|
+
a mismatch graded Low or Info alone, PASS otherwise
|
|
176
|
+
A scalar result with `documentedNames` (a counted
|
|
177
|
+
set) gets the same table. The stack branch, and the
|
|
178
|
+
scalar one with no set, count without naming the
|
|
179
|
+
exports, and a docs-only run has no source
|
|
180
|
+
surface: one line says so instead (NO_TABLE).
|
|
181
|
+
### Coverage Summary the --coverage counts and warnings, the
|
|
182
|
+
--signatures count of
|
|
183
|
+
Critical mismatches (with the Low and Info ones,
|
|
184
|
+
counted as matching, beside it when there are any)
|
|
185
|
+
and warnings, the numerator surplus when the ratio
|
|
186
|
+
was capped, and the names the --surface leaves out
|
|
187
|
+
or puts back: `excluded.outsideScope` (scoped out
|
|
188
|
+
by the brief), `excluded.nestedEntries` (a nested
|
|
189
|
+
Python entry point's names) and
|
|
190
|
+
`guards.staleScope.restored`, each line only when
|
|
191
|
+
it names one
|
|
192
|
+
### Category Scores Export Coverage, and Signature Accuracy and Type
|
|
193
|
+
Coverage from --signatures (N/A without it)
|
|
194
|
+
Exit 0 when it printed; 1 when an input cannot be read or is not the
|
|
195
|
+
result of the script that writes it (one line on stderr); 2 a usage error.
|
|
118
196
|
"""
|
|
119
197
|
|
|
120
198
|
from __future__ import annotations
|
|
@@ -127,6 +205,8 @@ import sys
|
|
|
127
205
|
from pathlib import Path
|
|
128
206
|
|
|
129
207
|
VALID_SOURCES = ("barrel", "scalar", "stack", "docsOnly")
|
|
208
|
+
# The grades score-signatures.py gives a caller-invisible signature difference, most severe first.
|
|
209
|
+
NON_BLOCKING_MISMATCH = ("Low", "Info")
|
|
130
210
|
|
|
131
211
|
_INVENTORY_MODULE = None
|
|
132
212
|
|
|
@@ -231,9 +311,65 @@ def _validate(inp):
|
|
|
231
311
|
verified = inp.get("verifiedNumerator")
|
|
232
312
|
if verified is not None and (not isinstance(verified, int) or isinstance(verified, bool) or verified < 0):
|
|
233
313
|
return "verifiedNumerator must be an integer >= 0"
|
|
314
|
+
if source == "scalar" and inp.get("barrelSet") is not None:
|
|
315
|
+
if not isinstance(inp["barrelSet"], list):
|
|
316
|
+
return "scalar branch: `barrelSet` must be a list of names"
|
|
317
|
+
set_name = _surface_set_name(inp)
|
|
318
|
+
if set_name is None:
|
|
319
|
+
return "scalar branch: `surfaceSet` must be a set name (a string)"
|
|
320
|
+
all_set = inp.get("allSet")
|
|
321
|
+
if all_set is not None and not isinstance(all_set, list):
|
|
322
|
+
return "scalar branch: `allSet` must be a list of names"
|
|
323
|
+
if all_set is None and set_name != "all" and _reads_surface(inp):
|
|
324
|
+
return ("scalar branch on a surface read from the source requires `allSet` (the surface's "
|
|
325
|
+
"`all` set) to find the stale names")
|
|
234
326
|
return None
|
|
235
327
|
|
|
236
328
|
|
|
329
|
+
def _surface_set_name(inp):
|
|
330
|
+
"""`surfaceSet`, normalized once: null or empty is `all`; None for a value that is not a string.
|
|
331
|
+
|
|
332
|
+
>>> [_surface_set_name({"surfaceSet": v}) for v in (None, "", "tier_a_include", 3)]
|
|
333
|
+
['all', 'all', 'tier_a_include', None]
|
|
334
|
+
"""
|
|
335
|
+
name = inp.get("surfaceSet")
|
|
336
|
+
if name is None or name == "":
|
|
337
|
+
return "all"
|
|
338
|
+
return name if isinstance(name, str) else None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _reads_surface(inp):
|
|
342
|
+
"""True when the scalar branch reads its surface: a `barrelSet` read from the source (a counted set is)."""
|
|
343
|
+
return inp.get("barrelSet") is not None and (inp.get("sourceRead") is True or inp.get("setCounted") is True)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _scalar_surface(inp, denominator):
|
|
347
|
+
"""What the scalar branch reads from a surface read from the source: the stale names, the documented names
|
|
348
|
+
(S ∪ `allSet`, where a nested package's name sits in the scope sets alone) lacks, and, when it counts its
|
|
349
|
+
named set S (`setCounted`, a non-empty scope set), the documented names S holds, the names of S the skill
|
|
350
|
+
does not document and the set's size, with the warnings. The denominator stays the stored count."""
|
|
351
|
+
documented_set = build_documented_set(inp.get("exports"))
|
|
352
|
+
named = {n for n in inp["barrelSet"] if isinstance(n, str) and n}
|
|
353
|
+
set_name = _surface_set_name(inp)
|
|
354
|
+
whole = named | {n for n in inp.get("allSet") or [] if isinstance(n, str) and n}
|
|
355
|
+
warnings = []
|
|
356
|
+
counted = inp.get("setCounted") is True and set_name != "all"
|
|
357
|
+
if counted and not named:
|
|
358
|
+
# An empty set would read 0% by construction: the lookup counts instead.
|
|
359
|
+
warnings.append(f"the `{set_name}` set names no export: the numerator is looked up in the skill instead")
|
|
360
|
+
counted = False
|
|
361
|
+
out = {"counted": counted, "stale": sorted(documented_set - whole), "warnings": warnings}
|
|
362
|
+
if counted:
|
|
363
|
+
if len(named) != denominator:
|
|
364
|
+
warnings.append(
|
|
365
|
+
f"the `{set_name}` set holds {len(named)} names and the stored count "
|
|
366
|
+
f"(`stats.effective_denominator`) is {denominator}: the denominator stays the stored count, so "
|
|
367
|
+
"`missing` and `missingCount` can differ")
|
|
368
|
+
out.update({"documentedNames": sorted(documented_set & named), "missing": sorted(named - documented_set),
|
|
369
|
+
"surfaceSet": set_name, "surfaceSetSize": len(named)})
|
|
370
|
+
return out
|
|
371
|
+
|
|
372
|
+
|
|
237
373
|
def _docs_only(inp):
|
|
238
374
|
tally = _inventory().completeness(inp["exports"])
|
|
239
375
|
total = tally["total"]
|
|
@@ -286,6 +422,7 @@ def reconcile(inp, doc_text=None):
|
|
|
286
422
|
"denominatorSource": source,
|
|
287
423
|
"denominator": denominator,
|
|
288
424
|
"documented": documented,
|
|
425
|
+
"documentedNames": sorted(documented_names),
|
|
289
426
|
"missing": missing,
|
|
290
427
|
"missingCount": len(missing),
|
|
291
428
|
"stale": stale,
|
|
@@ -303,7 +440,14 @@ def reconcile(inp, doc_text=None):
|
|
|
303
440
|
)
|
|
304
441
|
|
|
305
442
|
numerator_source = "lookup"
|
|
306
|
-
if source == "scalar" and inp
|
|
443
|
+
surface = _scalar_surface(inp, denominator) if source == "scalar" and _reads_surface(inp) else None
|
|
444
|
+
if surface is not None and surface["counted"]:
|
|
445
|
+
# The named set the surface counts: the numerator counts only its documented names, so a documented
|
|
446
|
+
# extra never offsets an undocumented export. Neither the doc-text lookup nor §2b's verified count is
|
|
447
|
+
# read: both count names without knowing which the set holds.
|
|
448
|
+
documented = len(surface["documentedNames"])
|
|
449
|
+
numerator_source = "surface"
|
|
450
|
+
elif source == "scalar" and inp.get("verifiedNumerator") is not None:
|
|
307
451
|
# §2b's numerator ground truth found the declared count padded: its
|
|
308
452
|
# verified count is authoritative and replaces the lookup.
|
|
309
453
|
documented = inp["verifiedNumerator"]
|
|
@@ -317,12 +461,14 @@ def reconcile(inp, doc_text=None):
|
|
|
317
461
|
candidates = [n for n in inp["compositionNames"] if isinstance(n, str) and n]
|
|
318
462
|
documented = len(names_present(candidates, doc_text))
|
|
319
463
|
|
|
320
|
-
# The
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
464
|
+
# The numerator and the resolved denominator can measure different sets,
|
|
465
|
+
# so `documented` can legitimately exceed `denominator`: a looked-up
|
|
466
|
+
# numerator counts every documented name the skill writes, migration
|
|
467
|
+
# aliases, re-exported sibling symbols and other extras included, against
|
|
468
|
+
# a consumer-surface denominator, and a counted set can hold more names
|
|
469
|
+
# than the stored count. Left unbounded that yields a negative Missing and
|
|
470
|
+
# >100% coverage (which compute-score.py then rejects outright as out of
|
|
471
|
+
# range).
|
|
326
472
|
#
|
|
327
473
|
# `documented` stays the true count: it is reported as "Documented in
|
|
328
474
|
# SKILL.md", so capping it would print a number that was never measured.
|
|
@@ -347,9 +493,273 @@ def reconcile(inp, doc_text=None):
|
|
|
347
493
|
}
|
|
348
494
|
if source == "scalar":
|
|
349
495
|
result["numeratorSource"] = numerator_source
|
|
496
|
+
if surface is not None:
|
|
497
|
+
# Stale is valid against the whole surface whether or not the branch counts its set.
|
|
498
|
+
result.update({"stale": surface["stale"], "staleCount": len(surface["stale"]), "staleApplicable": True})
|
|
499
|
+
if surface["counted"]:
|
|
500
|
+
result.update({key: surface[key] for key in ("documentedNames", "missing", "surfaceSet",
|
|
501
|
+
"surfaceSetSize")})
|
|
502
|
+
result["warnings"] = surface["warnings"]
|
|
350
503
|
return result
|
|
351
504
|
|
|
352
505
|
|
|
506
|
+
# --- The Coverage Analysis section (render) -----------------------------------
|
|
507
|
+
|
|
508
|
+
BRANCHES = ("enumerated", "scalar", "stack", "docsOnly")
|
|
509
|
+
# Why a branch names no export: the line it writes in place of the table, and its stale count.
|
|
510
|
+
NO_NAMES = {
|
|
511
|
+
"scalar": "the scalar denominator (`stats.effective_denominator`) counts the exports without naming them",
|
|
512
|
+
"stack": "the stack denominator counts the composition surface without naming each export",
|
|
513
|
+
"docsOnly": "a docs-only run has no source surface",
|
|
514
|
+
}
|
|
515
|
+
NO_TABLE = {
|
|
516
|
+
"scalar": f"No per-export table: {NO_NAMES['scalar']}. The summary below gives the counts.",
|
|
517
|
+
"stack": f"No per-export table: {NO_NAMES['stack']}. The summary below gives the counts.",
|
|
518
|
+
"docsOnly": f"No per-export table: {NO_NAMES['docsOnly']} to compare against. The summary below gives its "
|
|
519
|
+
"documentation completeness.",
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
_GAP_LEDGER_MODULE = None
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
class RenderError(Exception):
|
|
526
|
+
"""A render input that is not the result of the script that writes it."""
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _gap_ledger():
|
|
530
|
+
"""gap-ledger.py beside this script: its rule for a missing export's Source gives the Source Location."""
|
|
531
|
+
global _GAP_LEDGER_MODULE
|
|
532
|
+
if _GAP_LEDGER_MODULE is None:
|
|
533
|
+
sibling = Path(__file__).resolve().parent / "gap-ledger.py"
|
|
534
|
+
spec = importlib.util.spec_from_file_location("skf_gap_ledger_render", sibling)
|
|
535
|
+
if spec is None or spec.loader is None or not sibling.is_file():
|
|
536
|
+
raise RenderError(f"gap-ledger.py not found beside {Path(__file__).name}")
|
|
537
|
+
module = importlib.util.module_from_spec(spec)
|
|
538
|
+
spec.loader.exec_module(module)
|
|
539
|
+
_GAP_LEDGER_MODULE = module
|
|
540
|
+
return _GAP_LEDGER_MODULE
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _pct(value):
|
|
544
|
+
"""A score as the report writes it.
|
|
545
|
+
|
|
546
|
+
>>> _pct(60.0), _pct(66.67), _pct(None)
|
|
547
|
+
('60%', '66.67%', 'N/A')
|
|
548
|
+
"""
|
|
549
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
550
|
+
return "N/A"
|
|
551
|
+
return f"{int(value)}%" if float(value).is_integer() else f"{value}%"
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _cell(text):
|
|
555
|
+
"""A table cell: a `|` would end it."""
|
|
556
|
+
return str(text).replace("|", "\\|")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _string_list(data, key, label):
|
|
560
|
+
value = data.get(key)
|
|
561
|
+
if value is None:
|
|
562
|
+
return []
|
|
563
|
+
if not isinstance(value, list):
|
|
564
|
+
raise RenderError(f"{label}: `{key}` is not a list")
|
|
565
|
+
return [v for v in value if isinstance(v, str) and v]
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _incomplete(item):
|
|
569
|
+
"""A docs-only item and the fields it lacks.
|
|
570
|
+
|
|
571
|
+
>>> _incomplete({"name": "load", "kind": "function", "missing": ["params", "return_type"]})
|
|
572
|
+
'`load` (function) lacks `params`, `return_type`'
|
|
573
|
+
"""
|
|
574
|
+
fields = ", ".join(f"`{field}`" for field in item.get("missing") or [])
|
|
575
|
+
return f"`{item.get('name')}` ({item.get('kind')}) lacks {fields}"
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _left_out(surface):
|
|
579
|
+
"""The Coverage Summary lines for the names the surface leaves out or puts back, each only when it names one.
|
|
580
|
+
|
|
581
|
+
>>> for line in _left_out({
|
|
582
|
+
... "excluded": {"outsideScope": [{"name": "gen", "file": "src/gen.ts"}],
|
|
583
|
+
... "nestedEntries": [{"entry": "pkg/api/__init__.py", "within": "pkg/__init__.py",
|
|
584
|
+
... "names": ["router"]}]},
|
|
585
|
+
... "guards": {"staleScope": {"restored": [{"name": "old", "file": "src/old.ts", "line": 4}]}}}):
|
|
586
|
+
... print(line)
|
|
587
|
+
- **Scoped Out by the Brief:** `gen` (src/gen.ts)
|
|
588
|
+
- **Nested Entry Points:** `pkg/api/__init__.py` inside `pkg/__init__.py`: `router`
|
|
589
|
+
- **Restored by the Stale-Scope Guard:** `old` (src/old.ts:4)
|
|
590
|
+
>>> _left_out({"excluded": {"outsideScope": [], "nestedEntries": []}, "guards": None})
|
|
591
|
+
[]
|
|
592
|
+
"""
|
|
593
|
+
def items(value):
|
|
594
|
+
return [i for i in value if isinstance(i, dict)] if isinstance(value, list) else []
|
|
595
|
+
|
|
596
|
+
def where(item):
|
|
597
|
+
file, line = item.get("file"), item.get("line")
|
|
598
|
+
if not isinstance(file, str) or not file:
|
|
599
|
+
return ""
|
|
600
|
+
return f" ({file}:{line})" if isinstance(line, int) and not isinstance(line, bool) else f" ({file})"
|
|
601
|
+
|
|
602
|
+
excluded = surface.get("excluded") if isinstance(surface.get("excluded"), dict) else {}
|
|
603
|
+
guards = surface.get("guards") if isinstance(surface.get("guards"), dict) else {}
|
|
604
|
+
stale_scope = guards.get("staleScope") if isinstance(guards.get("staleScope"), dict) else {}
|
|
605
|
+
lines = []
|
|
606
|
+
scoped = [f"`{i['name']}`{where(i)}" for i in items(excluded.get("outsideScope"))
|
|
607
|
+
if isinstance(i.get("name"), str)]
|
|
608
|
+
if scoped:
|
|
609
|
+
lines.append(f"- **Scoped Out by the Brief:** {', '.join(scoped)}")
|
|
610
|
+
nested = [f"`{i['entry']}` inside `{i.get('within')}`: "
|
|
611
|
+
+ ", ".join(f"`{n}`" for n in i.get("names") or [] if isinstance(n, str))
|
|
612
|
+
for i in items(excluded.get("nestedEntries")) if isinstance(i.get("entry"), str)]
|
|
613
|
+
if nested:
|
|
614
|
+
lines.append(f"- **Nested Entry Points:** {'; '.join(nested)}")
|
|
615
|
+
restored = [f"`{i['name']}`{where(i)}" for i in items(stale_scope.get("restored"))
|
|
616
|
+
if isinstance(i.get("name"), str)]
|
|
617
|
+
if restored:
|
|
618
|
+
lines.append(f"- **Restored by the Stale-Scope Guard:** {', '.join(restored)}")
|
|
619
|
+
return lines
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _rows(coverage, surface, signatures):
|
|
623
|
+
"""The Export Coverage table of the enumerated branch."""
|
|
624
|
+
if "documentedNames" not in coverage:
|
|
625
|
+
raise RenderError("--coverage has no `documentedNames`: write it again with this reconcile-coverage.py")
|
|
626
|
+
documented = set(_string_list(coverage, "documentedNames", "--coverage"))
|
|
627
|
+
missing = set(_string_list(coverage, "missing", "--coverage"))
|
|
628
|
+
stale = set(_string_list(coverage, "stale", "--coverage"))
|
|
629
|
+
mismatched, critical, compared, graded, compared_at = set(), set(), set(), {}, {}
|
|
630
|
+
if signatures is not None:
|
|
631
|
+
named = [m for m in signatures.get("mismatches") or []
|
|
632
|
+
if isinstance(m, dict) and isinstance(m.get("name"), str)]
|
|
633
|
+
mismatched = {m["name"] for m in named}
|
|
634
|
+
# A Low or an Info mismatch (a caller-invisible difference) warns; any other fails.
|
|
635
|
+
critical = {m["name"] for m in named if m.get("severity") not in NON_BLOCKING_MISMATCH}
|
|
636
|
+
# The more severe non-blocking grade of each name: Low over Info.
|
|
637
|
+
for severity in reversed(NON_BLOCKING_MISMATCH):
|
|
638
|
+
graded.update({m["name"]: severity for m in named if m.get("severity") == severity})
|
|
639
|
+
compared = set(_string_list(signatures, "comparedNames", "--signatures"))
|
|
640
|
+
# A mismatched name sits at the declaration its most severe mismatch was compared with, which the
|
|
641
|
+
# surface's first record of a name it records in several files need not be.
|
|
642
|
+
for m in sorted(named, key=lambda m: (NON_BLOCKING_MISMATCH.index(m["severity"]) + 1
|
|
643
|
+
if m.get("severity") in NON_BLOCKING_MISMATCH else 0)):
|
|
644
|
+
if isinstance(m.get("file"), str) and m["file"] and isinstance(m.get("line"), int):
|
|
645
|
+
compared_at.setdefault(m["name"], f"{m['file']}:{m['line']}")
|
|
646
|
+
kinds, sources = {}, {}
|
|
647
|
+
if surface is not None:
|
|
648
|
+
for record in surface["exports"]:
|
|
649
|
+
if isinstance(record, dict) and isinstance(record.get("name"), str) and record.get("kind"):
|
|
650
|
+
kinds.setdefault(record["name"], str(record["kind"]))
|
|
651
|
+
sources = _gap_ledger()._surface_sources(surface)
|
|
652
|
+
sources.update(compared_at)
|
|
653
|
+
rows = ["| Export | Type | Documented | Signature | Source Location | Status |",
|
|
654
|
+
"|--------|------|------------|-----------|-----------------|--------|"]
|
|
655
|
+
for name in sorted(documented | missing | stale | mismatched):
|
|
656
|
+
if name in critical:
|
|
657
|
+
signature = "no"
|
|
658
|
+
elif name in mismatched:
|
|
659
|
+
signature = f"yes ({graded[name]})"
|
|
660
|
+
elif name in compared:
|
|
661
|
+
signature = "yes"
|
|
662
|
+
elif signatures is None and name not in missing:
|
|
663
|
+
signature = "unverified"
|
|
664
|
+
else:
|
|
665
|
+
signature = "-"
|
|
666
|
+
status = ("FAIL" if name in critical
|
|
667
|
+
else "WARN" if name in mismatched or name in missing or name in stale else "PASS")
|
|
668
|
+
rows.append(f"| `{_cell(name)}` | {_cell(kinds.get(name, '-'))} | {'no' if name in missing else 'yes'} "
|
|
669
|
+
f"| {signature} | {_cell(sources.get(name, '-'))} | {status} |")
|
|
670
|
+
return rows
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def render(coverage, surface=None, signatures=None):
|
|
674
|
+
"""The Coverage Analysis section after its header lines, as Markdown (see the module docstring)."""
|
|
675
|
+
if not isinstance(coverage, dict) or coverage.get("branch") not in BRANCHES:
|
|
676
|
+
raise RenderError("--coverage is not reconcile-coverage.py's result (no known `branch`)")
|
|
677
|
+
if surface is not None and not (isinstance(surface, dict) and isinstance(surface.get("exports"), list)):
|
|
678
|
+
raise RenderError("--surface is not load-coverage-inputs.py surface's result (no `exports` list)")
|
|
679
|
+
if signatures is not None and not (isinstance(signatures, dict) and signatures.get("valid") is True):
|
|
680
|
+
raise RenderError("--signatures is not score-signatures.py score's result (no `valid: true`)")
|
|
681
|
+
branch = coverage["branch"]
|
|
682
|
+
docs_only = branch == "docsOnly"
|
|
683
|
+
|
|
684
|
+
if surface is None:
|
|
685
|
+
files = "N/A: no source surface"
|
|
686
|
+
else:
|
|
687
|
+
files = len({r["file"] for r in surface["exports"]
|
|
688
|
+
if isinstance(r, dict) and isinstance(r.get("file"), str) and r["file"]})
|
|
689
|
+
lines = [f"**Files Analyzed:** {files}", "", "### Export Coverage", ""]
|
|
690
|
+
# A scalar result that counts its set names its exports as the enumerated branch does.
|
|
691
|
+
named = branch == "enumerated" or (branch == "scalar" and "documentedNames" in coverage)
|
|
692
|
+
lines += _rows(coverage, surface, signatures) if named else [NO_TABLE[branch]]
|
|
693
|
+
|
|
694
|
+
denominator, documented = coverage.get("denominator"), coverage.get("documented")
|
|
695
|
+
missing_count, coverage_pct = coverage.get("missingCount"), _pct(coverage.get("exportCoverage"))
|
|
696
|
+
lines += ["", "### Coverage Summary", ""]
|
|
697
|
+
if docs_only:
|
|
698
|
+
incomplete = [_incomplete(item) for item in coverage.get("incomplete") or [] if isinstance(item, dict)]
|
|
699
|
+
lines += [f"- **Exports Found:** {denominator} documented items",
|
|
700
|
+
f"- **Documented:** {documented} complete ({coverage_pct})",
|
|
701
|
+
f"- **Missing Documentation:** {missing_count} incomplete"
|
|
702
|
+
+ (f": {'; '.join(incomplete)}" if incomplete else "")]
|
|
703
|
+
else:
|
|
704
|
+
capped = coverage.get("coverageCapped")
|
|
705
|
+
floored = " (a floored residual: the numerator exceeds the denominator)" if capped else ""
|
|
706
|
+
lines += [f"- **Exports Found:** {denominator}",
|
|
707
|
+
f"- **Documented:** {documented} ({coverage_pct})",
|
|
708
|
+
f"- **Missing Documentation:** {missing_count}{floored}"]
|
|
709
|
+
mismatches = signatures.get("mismatches") if signatures is not None else None
|
|
710
|
+
if isinstance(mismatches, list):
|
|
711
|
+
named = [m for m in mismatches if isinstance(m, dict)]
|
|
712
|
+
lenient = sum(1 for m in named if m.get("severity") in NON_BLOCKING_MISMATCH)
|
|
713
|
+
lines.append(f"- **Signature Mismatches:** {len(named) - lenient}"
|
|
714
|
+
+ (f", plus {lenient} graded Low or Info and counted as matching" if lenient else ""))
|
|
715
|
+
else:
|
|
716
|
+
lines.append("- **Signature Mismatches:** N/A")
|
|
717
|
+
if coverage.get("staleApplicable"):
|
|
718
|
+
lines.append(f"- **Stale Documentation:** {coverage.get('staleCount')}, documented extras included")
|
|
719
|
+
else:
|
|
720
|
+
lines.append(f"- **Stale Documentation:** N/A: {NO_NAMES.get(branch, 'the denominator names no stale export')}")
|
|
721
|
+
noted = _string_list(coverage, "warnings", "--coverage")
|
|
722
|
+
if noted:
|
|
723
|
+
lines.append("- **Coverage Warnings:**")
|
|
724
|
+
lines += [f" - {w}" for w in noted]
|
|
725
|
+
warnings = _string_list(signatures, "warnings", "--signatures") if signatures is not None else []
|
|
726
|
+
if warnings:
|
|
727
|
+
lines.append("- **Scoring Warnings:**")
|
|
728
|
+
lines += [f" - {w}" for w in warnings]
|
|
729
|
+
if coverage.get("coverageCapped"):
|
|
730
|
+
lines.append(f"- **Numerator Surplus:** {coverage.get('numeratorSurplus')}, uncapped coverage "
|
|
731
|
+
f"{_pct(coverage.get('coverageUncapped'))}")
|
|
732
|
+
if surface is not None:
|
|
733
|
+
lines += _left_out(surface)
|
|
734
|
+
|
|
735
|
+
scores = signatures or {}
|
|
736
|
+
lines += ["", "### Category Scores", "", "| Category | Score |", "|----------|-------|",
|
|
737
|
+
f"| Export Coverage | {coverage_pct} |",
|
|
738
|
+
f"| Signature Accuracy | {_pct(scores.get('signatureAccuracy'))} |",
|
|
739
|
+
f"| Type Coverage | {_pct(scores.get('typeCoverage'))} |"]
|
|
740
|
+
return "\n".join(lines) + "\n"
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _render_main(argv):
|
|
744
|
+
parser = argparse.ArgumentParser(
|
|
745
|
+
prog="reconcile-coverage render",
|
|
746
|
+
description="Print the Coverage Analysis section's Markdown from the run files (coverage-check.md §5).",
|
|
747
|
+
)
|
|
748
|
+
parser.add_argument("--coverage", required=True, metavar="PATH", help="this script's result (coverage.json)")
|
|
749
|
+
parser.add_argument("--surface", metavar="PATH", help="load-coverage-inputs.py surface output (surface.json)")
|
|
750
|
+
parser.add_argument("--signatures", metavar="PATH", help="score-signatures.py score output (signatures.json)")
|
|
751
|
+
args = parser.parse_args(argv)
|
|
752
|
+
try:
|
|
753
|
+
text = render(_read_json_file(args.coverage, "--coverage"),
|
|
754
|
+
_read_json_file(args.surface, "--surface") if args.surface else None,
|
|
755
|
+
_read_json_file(args.signatures, "--signatures") if args.signatures else None)
|
|
756
|
+
except (InputFileError, RenderError) as exc:
|
|
757
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
758
|
+
return 1
|
|
759
|
+
sys.stdout.write(text)
|
|
760
|
+
return 0
|
|
761
|
+
|
|
762
|
+
|
|
353
763
|
# --- Run-folder inputs --------------------------------------------------------
|
|
354
764
|
|
|
355
765
|
|
|
@@ -388,6 +798,17 @@ def apply_file_inputs(inp, args):
|
|
|
388
798
|
if not isinstance(names, list):
|
|
389
799
|
raise InputFileError(f"--surface {args.surface} has no name set `{args.surface_set}`")
|
|
390
800
|
inp["barrelSet"] = names
|
|
801
|
+
# The scalar branch counts the set only when the surface trusts it (`denominator.setCounted`, for the set
|
|
802
|
+
# it picked), and finds a stale name on any surface read from the source, against the whole public
|
|
803
|
+
# surface (the `all` set), not the set it counts.
|
|
804
|
+
inp["surfaceSet"] = args.surface_set
|
|
805
|
+
if isinstance(sets.get("all"), list):
|
|
806
|
+
inp["allSet"] = sets["all"]
|
|
807
|
+
guards = data.get("guards")
|
|
808
|
+
deflation = guards.get("deflation") if isinstance(guards, dict) else None
|
|
809
|
+
inp["sourceRead"] = isinstance(deflation, dict) and deflation.get("applicable") is True
|
|
810
|
+
picked = data.get("denominator") if isinstance(data.get("denominator"), dict) else {}
|
|
811
|
+
inp["setCounted"] = picked.get("setCounted") is True and picked.get("surfaceSet") == args.surface_set
|
|
391
812
|
if args.coverage_inputs is not None:
|
|
392
813
|
data = _read_json_file(args.coverage_inputs, "--coverage-inputs")
|
|
393
814
|
if not isinstance(data, dict):
|
|
@@ -432,7 +853,10 @@ def _build_parser():
|
|
|
432
853
|
epilog=(
|
|
433
854
|
"Example (barrel branch):\n"
|
|
434
855
|
" uv run reconcile-coverage.py --denominator-source barrel "
|
|
435
|
-
"--inventory run/inventory.json --surface run/surface.json --output run/coverage.json"
|
|
856
|
+
"--inventory run/inventory.json --surface run/surface.json --output run/coverage.json\n"
|
|
857
|
+
"Render the Coverage Analysis section (see `render --help`):\n"
|
|
858
|
+
" uv run reconcile-coverage.py render --coverage run/coverage.json "
|
|
859
|
+
"[--surface run/surface.json] [--signatures run/signatures.json]"
|
|
436
860
|
),
|
|
437
861
|
)
|
|
438
862
|
src = parser.add_mutually_exclusive_group()
|
|
@@ -459,7 +883,7 @@ def _build_parser():
|
|
|
459
883
|
files.add_argument("--inventory", metavar="PATH", help="the validated inventory file")
|
|
460
884
|
files.add_argument("--surface", metavar="PATH", help="load-coverage-inputs.py surface output")
|
|
461
885
|
files.add_argument("--surface-set", default="all", metavar="NAME",
|
|
462
|
-
help="the name set of --surface
|
|
886
|
+
help="the name set of --surface the barrel or scalar branch counts (default: all)")
|
|
463
887
|
files.add_argument("--coverage-inputs", metavar="PATH",
|
|
464
888
|
help="load-coverage-inputs.py metadata output (scalar and stack branches)")
|
|
465
889
|
files.add_argument("--verified", metavar="PATH",
|
|
@@ -492,6 +916,9 @@ def _write_output(path, result):
|
|
|
492
916
|
|
|
493
917
|
|
|
494
918
|
def main(argv=None):
|
|
919
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
920
|
+
if argv[:1] == ["render"]:
|
|
921
|
+
return _render_main(argv[1:])
|
|
495
922
|
parser = _build_parser()
|
|
496
923
|
args = parser.parse_args(argv)
|
|
497
924
|
raw = _resolve_input(args)
|