bmad-module-skill-forge 4.0.0 → 6.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/agents.md +1 -1
- package/docs/architecture.md +5 -5
- package/docs/campaign.md +1 -1
- package/docs/concepts.md +4 -4
- package/docs/examples.md +32 -25
- 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 +9 -3
- package/docs/verifying-a-skill.md +42 -21
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +6 -6
- package/package.json +1 -1
- package/src/knowledge/tool-resolution.md +6 -6
- package/src/shared/references/headless-gate-convention.md +4 -1
- package/src/shared/references/output-contract-schema.md +3 -3
- package/src/shared/references/pipeline-contracts.md +2 -1
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +11 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
- package/src/shared/scripts/skf-build-change-manifest.py +1180 -189
- package/src/shared/scripts/skf-classify-changed-files.py +15 -10
- package/src/shared/scripts/skf-count-tokens.py +54 -10
- package/src/shared/scripts/skf-detect-workspaces.py +67 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +64 -20
- package/src/shared/scripts/skf-extract-public-api.py +472 -120
- package/src/shared/scripts/skf-extraction-inventory.py +10 -6
- package/src/shared/scripts/skf-extraction-snapshot.py +6 -6
- package/src/shared/scripts/skf-parse-gaps.py +136 -26
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- 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-validate-output.py +12 -3
- package/src/shared/scripts/skf-verify-provenance-completeness.py +574 -45
- package/src/shared/tool-requirements.yaml +8 -2
- package/src/skf-analyze-source/SKILL.md +3 -2
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/references/generate-briefs.md +1 -1
- package/src/skf-analyze-source/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/SKILL.md +4 -3
- package/src/skf-audit-skill/references/re-index.md +2 -2
- package/src/skf-audit-skill/references/report.md +1 -1
- package/src/skf-brief-skill/SKILL.md +3 -2
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/invocation-contract.md +2 -2
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-campaign/SKILL.md +3 -2
- package/src/skf-campaign/references/campaign-contracts.md +2 -0
- package/src/skf-campaign/references/step-05-skill-loop.md +8 -3
- package/src/skf-campaign/references/step-10-export.md +3 -2
- package/src/skf-create-skill/SKILL.md +4 -3
- package/src/skf-create-skill/assets/skill-sections.md +1 -1
- package/src/skf-create-skill/references/ccc-index-check.md +1 -1
- package/src/skf-create-skill/references/component-extraction.md +1 -1
- package/src/skf-create-skill/references/extract.md +3 -2
- package/src/skf-create-skill/references/extraction-patterns.md +3 -3
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +8 -3
- package/src/skf-create-skill/references/sub/ccc-discover.md +1 -1
- package/src/skf-create-skill/references/validate.md +1 -1
- package/src/skf-create-stack-skill/SKILL.md +3 -2
- package/src/skf-create-stack-skill/references/generate-output.md +1 -1
- package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
- package/src/skf-create-stack-skill/references/parallel-extract.md +2 -2
- package/src/skf-create-stack-skill/references/report.md +1 -1
- package/src/skf-create-stack-skill/references/validate.md +1 -1
- package/src/skf-drop-skill/SKILL.md +3 -2
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +1 -1
- package/src/skf-export-skill/SKILL.md +3 -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/invocation-contract.md +1 -1
- package/src/skf-export-skill/references/load-skill.md +1 -1
- package/src/skf-export-skill/references/summary.md +1 -1
- package/src/skf-export-skill/references/token-report.md +29 -9
- package/src/skf-forger/SKILL.md +3 -2
- package/src/skf-forger/references/pipeline-mode.md +2 -2
- package/src/skf-quick-skill/SKILL.md +4 -3
- package/src/skf-quick-skill/references/finalize.md +1 -1
- package/src/skf-quick-skill/references/halt-contract.md +2 -2
- package/src/skf-quick-skill/references/write-and-validate.md +1 -1
- package/src/skf-refine-architecture/SKILL.md +8 -9
- package/src/skf-refine-architecture/customize.toml +1 -1
- package/src/skf-refine-architecture/references/report.md +1 -1
- package/src/skf-rename-skill/SKILL.md +3 -2
- package/src/skf-rename-skill/references/invocation-contract.md +2 -2
- package/src/skf-setup/SKILL.md +3 -2
- package/src/skf-setup/references/ccc-index.md +1 -1
- package/src/skf-setup/references/invocation-contract.md +1 -1
- package/src/skf-test-skill/SKILL.md +3 -2
- package/src/skf-test-skill/assets/output-section-formats.md +6 -4
- package/src/skf-test-skill/references/coherence-check.md +11 -8
- package/src/skf-test-skill/references/coverage-check-branches.md +81 -0
- package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
- package/src/skf-test-skill/references/coverage-check.md +60 -97
- package/src/skf-test-skill/references/init.md +2 -1
- package/src/skf-test-skill/references/inventory-contract.md +27 -0
- package/src/skf-test-skill/references/invocation-contract.md +3 -3
- package/src/skf-test-skill/references/migration-section-rules.md +26 -14
- package/src/skf-test-skill/references/report.md +5 -5
- package/src/skf-test-skill/references/scoring-rules.md +8 -7
- package/src/skf-test-skill/references/signature-comparison-rules.md +72 -0
- package/src/skf-test-skill/references/source-access-protocol.md +14 -10
- package/src/skf-test-skill/references/step-hard-gate.md +1 -0
- package/src/skf-test-skill/scripts/build-result-context.py +20 -3
- package/src/skf-test-skill/scripts/gap-ledger.py +456 -176
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +516 -79
- package/src/skf-test-skill/scripts/locate-export-segments.py +685 -76
- package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
- package/src/skf-test-skill/scripts/score-signatures.py +1205 -65
- package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +350 -35
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/SKILL.md +3 -2
- package/src/skf-update-skill/references/detect-changes.md +1 -1
- package/src/skf-update-skill/references/gap-driven.md +54 -33
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +15 -10
- package/src/skf-update-skill/references/re-extract.md +3 -1
- package/src/skf-update-skill/references/report.md +6 -5
- package/src/skf-update-skill/references/write.md +42 -20
- package/src/skf-verify-stack/SKILL.md +4 -3
- 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's scalar branch, stats.effective_denominator
|
|
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,17 +122,20 @@ Output (stdout, one object):
|
|
|
98
122
|
"denominatorSource": "barrel" | "scalar" | "stack" | "docsOnly",
|
|
99
123
|
"denominator": <int>,
|
|
100
124
|
"documented": <int>,
|
|
101
|
-
"documentedNames": [names], # enumerated
|
|
102
|
-
"missing": [names], # enumerated: source names not documented; others: []
|
|
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: []
|
|
103
127
|
"missingCount": <int>, # always present; never negative
|
|
104
|
-
"stale": [names], # enumerated: documented names not in source
|
|
128
|
+
"stale": [names], # enumerated, scalar on a source-read surface: documented names not in source
|
|
105
129
|
"staleCount": <int>, # always present
|
|
106
|
-
"staleApplicable": <bool>, #
|
|
130
|
+
"staleApplicable": <bool>, # true for enumerated and scalar on a source-read surface
|
|
107
131
|
"exportCoverage": <float>, # scalar/stack: capped at 100
|
|
108
132
|
"numeratorSurplus": <int>, # scalar/stack only: max(0, documented − denominator)
|
|
109
133
|
"coverageUncapped": <float>, # scalar/stack only: the ratio before the cap
|
|
110
134
|
"coverageCapped": <bool>, # scalar/stack only: true when the cap bound the ratio
|
|
111
|
-
"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
|
|
112
139
|
"incomplete": [{"name", "kind", "missing"}] # docsOnly only: the incomplete items
|
|
113
140
|
}
|
|
114
141
|
|
|
@@ -130,18 +157,31 @@ header lines, from the run files:
|
|
|
130
157
|
signature mismatches, by name:
|
|
131
158
|
Type the first kind the surface records for the name, else -
|
|
132
159
|
Documented no for a missing name, else yes
|
|
133
|
-
Signature no for a mismatch, 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
|
|
134
164
|
matches, unverified for a documented name when no
|
|
135
165
|
--signatures was given, - otherwise
|
|
136
|
-
Source Location
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
143
180
|
surface: one line says so instead (NO_TABLE).
|
|
144
|
-
### Coverage Summary the --coverage counts, the
|
|
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)
|
|
145
185
|
and warnings, the numerator surplus when the ratio
|
|
146
186
|
was capped, and the names the --surface leaves out
|
|
147
187
|
or puts back: `excluded.outsideScope` (scoped out
|
|
@@ -165,6 +205,8 @@ import sys
|
|
|
165
205
|
from pathlib import Path
|
|
166
206
|
|
|
167
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")
|
|
168
210
|
|
|
169
211
|
_INVENTORY_MODULE = None
|
|
170
212
|
|
|
@@ -269,9 +311,65 @@ def _validate(inp):
|
|
|
269
311
|
verified = inp.get("verifiedNumerator")
|
|
270
312
|
if verified is not None and (not isinstance(verified, int) or isinstance(verified, bool) or verified < 0):
|
|
271
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")
|
|
272
326
|
return None
|
|
273
327
|
|
|
274
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
|
+
|
|
275
373
|
def _docs_only(inp):
|
|
276
374
|
tally = _inventory().completeness(inp["exports"])
|
|
277
375
|
total = tally["total"]
|
|
@@ -342,7 +440,14 @@ def reconcile(inp, doc_text=None):
|
|
|
342
440
|
)
|
|
343
441
|
|
|
344
442
|
numerator_source = "lookup"
|
|
345
|
-
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:
|
|
346
451
|
# §2b's numerator ground truth found the declared count padded: its
|
|
347
452
|
# verified count is authoritative and replaces the lookup.
|
|
348
453
|
documented = inp["verifiedNumerator"]
|
|
@@ -356,12 +461,14 @@ def reconcile(inp, doc_text=None):
|
|
|
356
461
|
candidates = [n for n in inp["compositionNames"] if isinstance(n, str) and n]
|
|
357
462
|
documented = len(names_present(candidates, doc_text))
|
|
358
463
|
|
|
359
|
-
# The
|
|
360
|
-
#
|
|
361
|
-
#
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
#
|
|
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).
|
|
365
472
|
#
|
|
366
473
|
# `documented` stays the true count: it is reported as "Documented in
|
|
367
474
|
# SKILL.md", so capping it would print a number that was never measured.
|
|
@@ -386,6 +493,13 @@ def reconcile(inp, doc_text=None):
|
|
|
386
493
|
}
|
|
387
494
|
if source == "scalar":
|
|
388
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"]
|
|
389
503
|
return result
|
|
390
504
|
|
|
391
505
|
|
|
@@ -512,29 +626,45 @@ def _rows(coverage, surface, signatures):
|
|
|
512
626
|
documented = set(_string_list(coverage, "documentedNames", "--coverage"))
|
|
513
627
|
missing = set(_string_list(coverage, "missing", "--coverage"))
|
|
514
628
|
stale = set(_string_list(coverage, "stale", "--coverage"))
|
|
515
|
-
mismatched, compared = set(), set()
|
|
629
|
+
mismatched, critical, compared, graded, compared_at = set(), set(), set(), {}, {}
|
|
516
630
|
if signatures is not None:
|
|
517
|
-
|
|
518
|
-
|
|
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})
|
|
519
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']}")
|
|
520
646
|
kinds, sources = {}, {}
|
|
521
647
|
if surface is not None:
|
|
522
648
|
for record in surface["exports"]:
|
|
523
649
|
if isinstance(record, dict) and isinstance(record.get("name"), str) and record.get("kind"):
|
|
524
650
|
kinds.setdefault(record["name"], str(record["kind"]))
|
|
525
651
|
sources = _gap_ledger()._surface_sources(surface)
|
|
652
|
+
sources.update(compared_at)
|
|
526
653
|
rows = ["| Export | Type | Documented | Signature | Source Location | Status |",
|
|
527
654
|
"|--------|------|------------|-----------|-----------------|--------|"]
|
|
528
655
|
for name in sorted(documented | missing | stale | mismatched):
|
|
529
|
-
if name in
|
|
656
|
+
if name in critical:
|
|
530
657
|
signature = "no"
|
|
658
|
+
elif name in mismatched:
|
|
659
|
+
signature = f"yes ({graded[name]})"
|
|
531
660
|
elif name in compared:
|
|
532
661
|
signature = "yes"
|
|
533
662
|
elif signatures is None and name not in missing:
|
|
534
663
|
signature = "unverified"
|
|
535
664
|
else:
|
|
536
665
|
signature = "-"
|
|
537
|
-
status = "FAIL" if name in
|
|
666
|
+
status = ("FAIL" if name in critical
|
|
667
|
+
else "WARN" if name in mismatched or name in missing or name in stale else "PASS")
|
|
538
668
|
rows.append(f"| `{_cell(name)}` | {_cell(kinds.get(name, '-'))} | {'no' if name in missing else 'yes'} "
|
|
539
669
|
f"| {signature} | {_cell(sources.get(name, '-'))} | {status} |")
|
|
540
670
|
return rows
|
|
@@ -557,7 +687,9 @@ def render(coverage, surface=None, signatures=None):
|
|
|
557
687
|
files = len({r["file"] for r in surface["exports"]
|
|
558
688
|
if isinstance(r, dict) and isinstance(r.get("file"), str) and r["file"]})
|
|
559
689
|
lines = [f"**Files Analyzed:** {files}", "", "### Export Coverage", ""]
|
|
560
|
-
|
|
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]]
|
|
561
693
|
|
|
562
694
|
denominator, documented = coverage.get("denominator"), coverage.get("documented")
|
|
563
695
|
missing_count, coverage_pct = coverage.get("missingCount"), _pct(coverage.get("exportCoverage"))
|
|
@@ -575,11 +707,21 @@ def render(coverage, surface=None, signatures=None):
|
|
|
575
707
|
f"- **Documented:** {documented} ({coverage_pct})",
|
|
576
708
|
f"- **Missing Documentation:** {missing_count}{floored}"]
|
|
577
709
|
mismatches = signatures.get("mismatches") if signatures is not None else None
|
|
578
|
-
|
|
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")
|
|
579
717
|
if coverage.get("staleApplicable"):
|
|
580
718
|
lines.append(f"- **Stale Documentation:** {coverage.get('staleCount')}, documented extras included")
|
|
581
719
|
else:
|
|
582
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]
|
|
583
725
|
warnings = _string_list(signatures, "warnings", "--signatures") if signatures is not None else []
|
|
584
726
|
if warnings:
|
|
585
727
|
lines.append("- **Scoring Warnings:**")
|
|
@@ -656,6 +798,17 @@ def apply_file_inputs(inp, args):
|
|
|
656
798
|
if not isinstance(names, list):
|
|
657
799
|
raise InputFileError(f"--surface {args.surface} has no name set `{args.surface_set}`")
|
|
658
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
|
|
659
812
|
if args.coverage_inputs is not None:
|
|
660
813
|
data = _read_json_file(args.coverage_inputs, "--coverage-inputs")
|
|
661
814
|
if not isinstance(data, dict):
|
|
@@ -730,7 +883,7 @@ def _build_parser():
|
|
|
730
883
|
files.add_argument("--inventory", metavar="PATH", help="the validated inventory file")
|
|
731
884
|
files.add_argument("--surface", metavar="PATH", help="load-coverage-inputs.py surface output")
|
|
732
885
|
files.add_argument("--surface-set", default="all", metavar="NAME",
|
|
733
|
-
help="the name set of --surface
|
|
886
|
+
help="the name set of --surface the barrel or scalar branch counts (default: all)")
|
|
734
887
|
files.add_argument("--coverage-inputs", metavar="PATH",
|
|
735
888
|
help="load-coverage-inputs.py metadata output (scalar and stack branches)")
|
|
736
889
|
files.add_argument("--verified", metavar="PATH",
|