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
|
@@ -81,7 +81,21 @@ cites (never read; default `metadata.json`):
|
|
|
81
81
|
gap `Missing export: {name}`, or a Medium `missing-type` gap
|
|
82
82
|
`Missing type: {name}` when the --signatures result lists it in
|
|
83
83
|
`missingTypes`; its Source is the file and line the --surface
|
|
84
|
-
result's `exports[]` records for it, else `{meta}`.
|
|
84
|
+
result's `exports[]` records for it, else `{meta}`. When the
|
|
85
|
+
--surface result's `guards.inflation.fires`, its
|
|
86
|
+
`denominator.surfaceSet` is `scope.include` and its
|
|
87
|
+
`denominator.clause` is one whose ladder has a tier A rung
|
|
88
|
+
(stratified-scope, multi-entry), the `missing` names are
|
|
89
|
+
instead one Medium `denominator-inflation` gap `undocumented
|
|
90
|
+
names in the inflated scope.include union` at its
|
|
91
|
+
`inputs.brief` (else `skill-brief.yaml`), beside the guards
|
|
92
|
+
kind's gap at that brief: its issue gives the union's size
|
|
93
|
+
(`guards.inflation.scopeIncludeUnion`) and lists the first 50
|
|
94
|
+
names, each marked a type when the --signatures result lists
|
|
95
|
+
it in `missingTypes` and with the file and line `exports[]`
|
|
96
|
+
records for it, then how many more, and its remediation
|
|
97
|
+
corrects the denominator as the guard's gap says, then re-runs
|
|
98
|
+
Test Skill. Each `stale`
|
|
85
99
|
name the --stale result (classify-stale) marks `fabricated` is a
|
|
86
100
|
Critical `fabricated-signature` gap `Fabricated signature:
|
|
87
101
|
{name}` at its `source` (one with no `source` is INVALID_INPUT,
|
|
@@ -111,29 +125,59 @@ cites (never read; default `metadata.json`):
|
|
|
111
125
|
`Stale documentation: {name}` at the first line of SKILL.md,
|
|
112
126
|
then of references/*.md in path order, that writes the name
|
|
113
127
|
(validate-inventory.py's match, with --skill-dir), else
|
|
114
|
-
`SKILL.md`.
|
|
128
|
+
`SKILL.md`. One whose `excluded.outsideScope` row says the brief
|
|
129
|
+
scopes its file out (`excludeGlob`, the `scope.exclude` glob
|
|
130
|
+
that matches it, or `outsideInclude`, no `scope.include` glob
|
|
131
|
+
covering it) has its own issue, naming the entry point and the
|
|
132
|
+
file and line, and remediation: to keep the documentation,
|
|
133
|
+
narrow that exclude glob (a removal needs a `demoted-exclude`
|
|
134
|
+
amendment) or widen `scope.include`, else remove it. A row the
|
|
135
|
+
brief does not explain keeps the wording above. Scalar and
|
|
136
|
+
stack branches: a
|
|
115
137
|
`missingCount` above 0 is one Medium `missing-export` gap
|
|
116
138
|
`{missingCount} of {denominator} exports not documented` at
|
|
117
139
|
`{meta}`, except that a scalar count taken from the verified
|
|
118
140
|
numerator (`numeratorSource` `verified`) is replaced by one
|
|
119
141
|
`Missing export: {name}` gap per --numerator `absent` name, at
|
|
120
|
-
the first --provenance entry of that name, else `{meta}`.
|
|
121
|
-
|
|
142
|
+
the first --provenance entry of that name, else `{meta}`. A
|
|
143
|
+
scalar result that counts its set (`numeratorSource` `surface`)
|
|
144
|
+
records the `missing` names it lists in place of the count gap,
|
|
145
|
+
as the barrel branch does (the inflated-union summary gap
|
|
146
|
+
under the same gate included), and, when `missingCount` exceeds
|
|
147
|
+
them, one Medium `missing-export` gap `{residual} of
|
|
148
|
+
{denominator} exports not documented beyond the {surfaceSet}
|
|
149
|
+
set` at `{meta}`. A scalar result with `staleApplicable` true
|
|
150
|
+
records its `stale` names as the barrel branch does. Every
|
|
151
|
+
per-name gap carries `export`.
|
|
122
152
|
guards --input load-coverage-inputs.py's surface result; --metadata.
|
|
123
153
|
`guards.deflation.fires` is a Medium `metadata-drift` gap at
|
|
124
|
-
`{meta}` (else the result's `inputs.metadata`)
|
|
125
|
-
`
|
|
126
|
-
the
|
|
127
|
-
|
|
128
|
-
|
|
154
|
+
`{meta}` (else the result's `inputs.metadata`): on the
|
|
155
|
+
`tier_a_include` basis (`guards.deflation.basis`) titled
|
|
156
|
+
`denominator drift: ...`, giving the tier A count, the pct and
|
|
157
|
+
the stored count, asking to recount by re-running Create Skill
|
|
158
|
+
(which stores the runner's count only when
|
|
159
|
+
`effective_denominator_decision.store` holds and `scope.notes`
|
|
160
|
+
documents the stratification, and otherwise drops the stored
|
|
161
|
+
count) and naming what `denominator` counts until then (its
|
|
162
|
+
`surfaceSet`, or the stored count on the scalar branch); else
|
|
163
|
+
`denominator deflation: ...`. `guards.inflation.fires` is a
|
|
164
|
+
Medium `denominator-inflation` gap at the result's
|
|
165
|
+
`inputs.brief` (else `skill-brief.yaml`), whose remediation adds
|
|
166
|
+
or edits `scope.tier_a_include`, listing the files rather than
|
|
167
|
+
the root barrel when `guards.umbrella.umbrella` is true. When
|
|
168
|
+
both fire on the `scope.include` basis, both remediations name
|
|
169
|
+
that one fix and say the `scope.include` union is itself
|
|
170
|
+
inflated. No remediation sets `stats.effective_denominator` by
|
|
171
|
+
hand, which Update Skill's recount drops.
|
|
129
172
|
`guards.staleScope.fires` is one Medium `brief-scope-stale` gap
|
|
130
173
|
at that brief, whose issue names each `unmatchedInclude` glob,
|
|
131
174
|
each `restored` name with its file and each
|
|
132
|
-
`unmatchedTierAInclude` glob. The
|
|
133
|
-
say the brief has no
|
|
134
|
-
`guards.tierAInclude` lists no glob,
|
|
135
|
-
|
|
136
|
-
glob
|
|
175
|
+
`unmatchedTierAInclude` glob. The `scope.include`-basis
|
|
176
|
+
deflation gap and the inflation gap say the brief has no
|
|
177
|
+
`scope.tier_a_include` when `guards.tierAInclude` lists no glob,
|
|
178
|
+
that no glob matches a file when each is an
|
|
179
|
+
`unmatchedTierAInclude` glob, and else that no glob counts a
|
|
180
|
+
public name (a surface without
|
|
137
181
|
`guards.tierAInclude` reads the `unmatchedTierAInclude` globs
|
|
138
182
|
alone). A stored `stats.effective_denominator` of 0 (`pct`
|
|
139
183
|
null) is written as a count against 0, not a percentage, and
|
|
@@ -145,15 +189,27 @@ cites (never read; default `metadata.json`):
|
|
|
145
189
|
--input check-metadata-coherence.py's result; --metadata. Each
|
|
146
190
|
`findings[]` entry, titled as it is and with its `detail` as the
|
|
147
191
|
issue, at `{meta}`: Medium is a Medium `metadata-drift` gap, Info
|
|
148
|
-
an Info `multi-denominator` gap.
|
|
192
|
+
an Info `multi-denominator` gap. A `metadata-drift` gap whose
|
|
193
|
+
title names the documented surface explains the `Type::method`
|
|
194
|
+
entries its counts differ by; a barrel one sends the fix to
|
|
195
|
+
Create Skill, which rebuilds `exports[]` from the source.
|
|
149
196
|
provenance-line
|
|
150
197
|
--input skf-verify-provenance-completeness.py verify's result.
|
|
151
|
-
Each `stale[]` item
|
|
152
|
-
|
|
198
|
+
Each `stale[]` item, at its `{source_file}:{source_line}` with
|
|
199
|
+
`export`, by `reason`: `line-not-definition` is a Low
|
|
153
200
|
`provenance-line` gap `Provenance line is not the definition of
|
|
154
201
|
{export_name}` when `definition_lines` holds a line, else an Info
|
|
155
202
|
`provenance-unverified` gap `Provenance line not verified for
|
|
156
|
-
{export_name}
|
|
203
|
+
{export_name}`, and its issue names the base of a `folded_to`
|
|
204
|
+
variant; `file-missing` is a Low `provenance-line` gap
|
|
205
|
+
`Provenance file is missing for {export_name}`;
|
|
206
|
+
`line-out-of-bounds` a Low `provenance-line` gap `Provenance line
|
|
207
|
+
is out of range for {export_name}`; `line-invalid` an Info
|
|
208
|
+
`provenance-unverified` gap `Provenance line not verified for
|
|
209
|
+
{export_name}` at `{source_file}` alone. Its `missing`,
|
|
210
|
+
`orphaned` and `citations` are not gaps here, and no record is
|
|
211
|
+
appended for an export the ledger already records as a
|
|
212
|
+
`fabricated-signature` gap.
|
|
157
213
|
coherence --input aggregate-coherence.py's result. Each
|
|
158
214
|
`invalidReferences[]` entry at `SKILL.md:{line}`, by `status`:
|
|
159
215
|
`missing` is a Critical `broken-reference` gap, `inaccurate` a
|
|
@@ -220,7 +276,6 @@ import errno
|
|
|
220
276
|
import importlib.util
|
|
221
277
|
import json
|
|
222
278
|
import os
|
|
223
|
-
import posixpath
|
|
224
279
|
import re
|
|
225
280
|
import secrets
|
|
226
281
|
import sys
|
|
@@ -262,8 +317,13 @@ CATEGORIES: dict[str, tuple[str, str]] = {
|
|
|
262
317
|
"stale-documentation": ("Coverage", "documentation for an export the enumerated source surface lacks"),
|
|
263
318
|
"missing-type": ("Coverage", "a type or interface the skill does not document"),
|
|
264
319
|
"provenance-completeness": ("Coverage", "an export the skill documents that the provenance map lacks"),
|
|
265
|
-
"provenance-line": (
|
|
266
|
-
|
|
320
|
+
"provenance-line": (
|
|
321
|
+
"Coverage",
|
|
322
|
+
"a provenance line that is not the definition of its export, or a citation whose file or line the source "
|
|
323
|
+
"lacks",
|
|
324
|
+
),
|
|
325
|
+
"provenance-unverified": (
|
|
326
|
+
"Coverage", "a provenance line the line-check rules could not verify, or one that is not a line number"),
|
|
267
327
|
"metadata-drift": ("Coverage", "export counts in the metadata that diverge from each other"),
|
|
268
328
|
"denominator-inflation": ("Coverage", "a scope include union larger than the provenance map"),
|
|
269
329
|
"brief-scope-stale": (
|
|
@@ -281,7 +341,10 @@ CATEGORIES: dict[str, tuple[str, str]] = {
|
|
|
281
341
|
"a reference whose real path leaves the skill, its source and, for a stack, the skills folder",
|
|
282
342
|
),
|
|
283
343
|
"integration-pattern": ("Coherence", "an integration pattern that is incomplete"),
|
|
284
|
-
"split-body-mismatch": (
|
|
344
|
+
"split-body-mismatch": (
|
|
345
|
+
"Coherence",
|
|
346
|
+
"two files of the skill (SKILL.md or a references/ file) that disagree about an export",
|
|
347
|
+
),
|
|
285
348
|
"scripts-assets": ("Coherence", "a Scripts & Assets section that is missing or names a missing file"),
|
|
286
349
|
"scripts-assets-provenance": ("Coherence", "a script or asset file without a provenance entry"),
|
|
287
350
|
"structural": (
|
|
@@ -713,7 +776,8 @@ _SIBLINGS: dict[str, object] = {}
|
|
|
713
776
|
|
|
714
777
|
def _sibling(filename: str):
|
|
715
778
|
"""A script beside this one, loaded once: validate-inventory.py (the
|
|
716
|
-
documented-name match the stale-documentation Source uses
|
|
779
|
+
documented-name match the stale-documentation Source uses, and the
|
|
780
|
+
skill-citation scan the homonym pick reads)."""
|
|
717
781
|
module = _SIBLINGS.get(filename)
|
|
718
782
|
if module is None:
|
|
719
783
|
path = Path(__file__).resolve().parent / filename
|
|
@@ -825,13 +889,25 @@ def _documenting_line(skill_dir: Path, name: str) -> str | None:
|
|
|
825
889
|
return None
|
|
826
890
|
|
|
827
891
|
|
|
828
|
-
def
|
|
892
|
+
def _scoped_out_by(row: dict) -> bool:
|
|
893
|
+
"""Whether an `excluded.outsideScope` row says the brief scopes its file
|
|
894
|
+
out: a file, with the `scope.exclude` glob that matches it
|
|
895
|
+
(`excludeGlob`) or no `scope.include` glob covering it
|
|
896
|
+
(`outsideInclude`)."""
|
|
897
|
+
return (isinstance(row.get("file"), str) and bool(row["file"])
|
|
898
|
+
and ((isinstance(row.get("excludeGlob"), str) and bool(row["excludeGlob"]))
|
|
899
|
+
or row.get("outsideInclude") is True))
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _outside_scope(surface: dict | None) -> dict[str, dict] | None:
|
|
829
903
|
"""The names a surface result's `excluded.outsideScope` lists (defined in
|
|
830
|
-
a file
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
`
|
|
834
|
-
|
|
904
|
+
a file out of scope), each with its row: the first the brief explains
|
|
905
|
+
(`_scoped_out_by`), else the first with a file, else its first. None
|
|
906
|
+
with no surface, no such list, or a surface built without an extraction
|
|
907
|
+
(no `extraction` object) or from one that did not read the whole scope
|
|
908
|
+
(a `no-ast-grep` status, a `truncated` run, or `fallback.needed`): only
|
|
909
|
+
an extraction fills that list, so the names a brief scoped out are then
|
|
910
|
+
unknown."""
|
|
835
911
|
excluded = (surface or {}).get("excluded")
|
|
836
912
|
if excluded is None:
|
|
837
913
|
return None
|
|
@@ -849,7 +925,16 @@ def _outside_scope(surface: dict | None) -> set[str] | None:
|
|
|
849
925
|
if (extraction.get("status") == "no-ast-grep" or extraction.get("truncated")
|
|
850
926
|
or (isinstance(fallback, dict) and fallback.get("needed"))):
|
|
851
927
|
return None
|
|
852
|
-
|
|
928
|
+
def rank(row: dict) -> tuple[bool, bool]:
|
|
929
|
+
return _scoped_out_by(row), isinstance(row.get("file"), str) and bool(row["file"])
|
|
930
|
+
|
|
931
|
+
outside: dict[str, dict] = {}
|
|
932
|
+
for row in rows:
|
|
933
|
+
if isinstance(row, dict) and isinstance(row.get("name"), str):
|
|
934
|
+
known = outside.get(row["name"])
|
|
935
|
+
if known is None or rank(row) > rank(known):
|
|
936
|
+
outside[row["name"]] = row
|
|
937
|
+
return outside
|
|
853
938
|
|
|
854
939
|
|
|
855
940
|
# The extensions classify-stale reads a `defined_at` in.
|
|
@@ -860,60 +945,7 @@ _DEFINED_AT_RE = re.compile(
|
|
|
860
945
|
r"^.+(?:" + "|".join(re.escape(e) for e in sorted(_DEFINED_AT_EXTENSIONS)) + r"):[1-9]\d*$", re.IGNORECASE)
|
|
861
946
|
|
|
862
947
|
|
|
863
|
-
|
|
864
|
-
# `:L<n>` part is ignored). Groups: prefix, path, first line, the `L` of a range's end, the range's end.
|
|
865
|
-
# Keep identical to _SKILL_CITATION_RE in skf-verify-provenance-completeness.py.
|
|
866
|
-
_SKILL_CITATION_RE = re.compile(
|
|
867
|
-
r"\[(AST|SRC):([^\[\]\n]+?):L(\d+)(?:-(L?)(\d+))?\]"
|
|
868
|
-
)
|
|
869
|
-
# Any bracketed citation (`[AST:...]`, `[SRC:...]`, `[EXT:...]`): a line names a documented name outside them.
|
|
870
|
-
_ANY_CITATION_RE = re.compile(r"\[[A-Z]+:[^\[\]\n]*\]")
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
def _cited_path(path: str) -> str:
|
|
874
|
-
"""A cited or `declared_in` path as both compare: forward slashes, no `./`."""
|
|
875
|
-
path = path.strip().replace("\\", "/")
|
|
876
|
-
while path.startswith("./"):
|
|
877
|
-
path = path[2:]
|
|
878
|
-
return posixpath.normpath(path) if path else path
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
def skill_markdown_files(skill_dir: Path) -> list[Path]:
|
|
882
|
-
"""`SKILL.md` then every `references/**/*.md`, in a stable order.
|
|
883
|
-
Keep identical to skill_markdown_files in skf-verify-provenance-completeness.py."""
|
|
884
|
-
files: list[Path] = []
|
|
885
|
-
skill_md = skill_dir / "SKILL.md"
|
|
886
|
-
if skill_md.is_file():
|
|
887
|
-
files.append(skill_md)
|
|
888
|
-
refs = skill_dir / "references"
|
|
889
|
-
if refs.is_dir():
|
|
890
|
-
files.extend(sorted(p for p in refs.rglob("*.md") if p.is_file()))
|
|
891
|
-
return files
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
def _skill_cited_files(skill_dir: Path, name: str) -> set[str]:
|
|
895
|
-
"""The files the skill's `[AST:]` and `[SRC:]` citations name on a line of
|
|
896
|
-
SKILL.md or references/**/*.md that writes `name` outside its citations
|
|
897
|
-
(validate-inventory.py's match), or on the next non-blank line (a
|
|
898
|
-
heading, then its citation)."""
|
|
899
|
-
pattern = _sibling("validate-inventory.py").name_pattern(name)
|
|
900
|
-
cited: set[str] = set()
|
|
901
|
-
for path in skill_markdown_files(skill_dir):
|
|
902
|
-
try:
|
|
903
|
-
text = path.read_bytes().decode("utf-8-sig", errors="replace")
|
|
904
|
-
except OSError:
|
|
905
|
-
continue
|
|
906
|
-
lines = text.split("\n")
|
|
907
|
-
for number, line in enumerate(lines):
|
|
908
|
-
if not pattern.search(_ANY_CITATION_RE.sub(" ", line)):
|
|
909
|
-
continue
|
|
910
|
-
following = next((ln for ln in lines[number + 1:] if ln.strip()), "")
|
|
911
|
-
for cited_line in (line, following):
|
|
912
|
-
cited.update(_cited_path(m.group(2)) for m in _SKILL_CITATION_RE.finditer(cited_line))
|
|
913
|
-
return cited
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
def _documented_extra_at(name: str, item: dict, outside: set[str] | None,
|
|
948
|
+
def _documented_extra_at(name: str, item: dict, outside: dict[str, dict] | None,
|
|
917
949
|
skill_dir: str | None = None) -> tuple[str, int] | None:
|
|
918
950
|
"""Where the source declares a stale name that is a documented extra, given
|
|
919
951
|
a surface built from an extraction whose `excluded.outsideScope` does not
|
|
@@ -936,10 +968,12 @@ def _documented_extra_at(name: str, item: dict, outside: set[str] | None,
|
|
|
936
968
|
if not skill_dir or not isinstance(declared_in, list) or not all(
|
|
937
969
|
isinstance(d, str) and _DEFINED_AT_RE.match(d) for d in declared_in):
|
|
938
970
|
return None
|
|
939
|
-
|
|
971
|
+
# validate-inventory.py's skill-citation scan, which score-signatures.py's plan reads too.
|
|
972
|
+
citations = _sibling("validate-inventory.py")
|
|
973
|
+
by_file = {citations._cited_path(d.rsplit(":", 1)[0]): d for d in declared_in}
|
|
940
974
|
if len(by_file) < 2:
|
|
941
975
|
return None
|
|
942
|
-
cited = _skill_cited_files(Path(skill_dir), name)
|
|
976
|
+
cited = citations._skill_cited_files(Path(skill_dir), name)
|
|
943
977
|
chosen = [d for f, d in sorted(by_file.items()) if f in cited]
|
|
944
978
|
return (chosen[0], len(by_file)) if len(chosen) == 1 else None
|
|
945
979
|
|
|
@@ -961,6 +995,196 @@ def _missing_export(name: str, where: str | None, meta: str, kind: str = "export
|
|
|
961
995
|
export=name)
|
|
962
996
|
|
|
963
997
|
|
|
998
|
+
def _brief_of(surface: dict | None) -> str:
|
|
999
|
+
"""The brief a surface result read: its `inputs.brief`, else skill-brief.yaml."""
|
|
1000
|
+
inputs = (surface or {}).get("inputs")
|
|
1001
|
+
brief = inputs.get("brief") if isinstance(inputs, dict) else None
|
|
1002
|
+
return brief if isinstance(brief, str) and brief else _DEFAULT_BRIEF
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
# The most names the issue of the inflated-union summary gap lists (#795).
|
|
1006
|
+
_INFLATED_NAMES_LISTED = 50
|
|
1007
|
+
# The clauses whose ladder has a tier A rung, so that a brief's `scope.tier_a_include` corrects the
|
|
1008
|
+
# denominator an inflated `scope.include` union gave them (#795).
|
|
1009
|
+
# Keep identical to the CLAUSE_LADDERS clauses with a `tier_a_include` rung in load-coverage-inputs.py.
|
|
1010
|
+
_TIER_A_CLAUSES = frozenset({"stratified-scope", "multi-entry"})
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def _inflated_union(surface: dict | None) -> bool:
|
|
1014
|
+
"""Whether the barrel branch counts an inflated `scope.include` union a
|
|
1015
|
+
tier A list corrects: the surface's inflation guard fires, its
|
|
1016
|
+
`denominator.surfaceSet` is `scope.include` and its `denominator.clause`
|
|
1017
|
+
has a tier A rung. A set the surface lacked (`absentSet`) counted `all`
|
|
1018
|
+
in its place, and the specific-modules and empty-barrel clauses count
|
|
1019
|
+
`scope.include` whatever tier A list the brief has: each name there
|
|
1020
|
+
stays its own gap."""
|
|
1021
|
+
guards = (surface or {}).get("guards")
|
|
1022
|
+
inflation = guards.get("inflation") if isinstance(guards, dict) else None
|
|
1023
|
+
denominator = (surface or {}).get("denominator")
|
|
1024
|
+
return (isinstance(inflation, dict) and inflation.get("fires") is True and isinstance(denominator, dict)
|
|
1025
|
+
and denominator.get("surfaceSet") == "scope.include" and denominator.get("clause") in _TIER_A_CLAUSES)
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def _inflated_union_gap(names: list[str], where: dict[str, str], types: set[str], brief: str,
|
|
1029
|
+
union: object) -> dict:
|
|
1030
|
+
"""One gap at the brief for the names an inflated `scope.include` union
|
|
1031
|
+
counts and the skill does not document, in place of a gap per name: the
|
|
1032
|
+
union sweeps in infrastructure, so most of them belong to no authored
|
|
1033
|
+
surface (#795). It sits beside the guard's own `denominator-inflation`
|
|
1034
|
+
gap at that brief, whose fix it defers to."""
|
|
1035
|
+
def label(name: str) -> str:
|
|
1036
|
+
notes = (["a type"] if name in types else []) + ([f"`{where[name]}`"] if where.get(name) else [])
|
|
1037
|
+
return f"`{name}` ({', '.join(notes)})" if notes else f"`{name}`"
|
|
1038
|
+
|
|
1039
|
+
listed = [label(n) for n in names[:_INFLATED_NAMES_LISTED]]
|
|
1040
|
+
more = len(names) - len(listed)
|
|
1041
|
+
named = ", ".join(listed) + (f", and {more} more" if more else "")
|
|
1042
|
+
size = f" ({union} names)" if _count(union) is not None else ""
|
|
1043
|
+
return _gap(
|
|
1044
|
+
"Medium", "denominator-inflation", "undocumented names in the inflated scope.include union", brief,
|
|
1045
|
+
f"Correct the denominator first, as the denominator-inflation gap for `{brief}` says; then re-run Test "
|
|
1046
|
+
"Skill, and the names the corrected denominator counts that the skill does not document come back as gaps.",
|
|
1047
|
+
issue=f"the inflation guard fired: the `scope.include` union the denominator counts{size} sweeps in names "
|
|
1048
|
+
f"outside the authored surface, and SKILL.md and references/ do not document {len(names)} of them: "
|
|
1049
|
+
f"{named}")
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _scoped_out_gap(name: str, row: dict, doc: str | None, brief: str) -> dict:
|
|
1053
|
+
"""The Medium `stale-documentation` gap of a documented name an entry
|
|
1054
|
+
point exports from a file the brief scopes out (#796): its row names
|
|
1055
|
+
the `scope.exclude` glob that matches the file, or says no
|
|
1056
|
+
`scope.include` glob covers it, or both."""
|
|
1057
|
+
file, line, entry = row["file"], _count(row.get("line")), row.get("entry")
|
|
1058
|
+
at = f"{file}:{line}" if line else file
|
|
1059
|
+
glob = row.get("excludeGlob") if isinstance(row.get("excludeGlob"), str) and row.get("excludeGlob") else None
|
|
1060
|
+
uncovered = row.get("outsideInclude") is True
|
|
1061
|
+
causes = (["no `scope.include` glob covers that file"] if uncovered else []) + (
|
|
1062
|
+
[f"its `scope.exclude` glob `{glob}` matches {'it' if uncovered else 'that file'}"] if glob else [])
|
|
1063
|
+
fixes = (["widen `scope.include` to cover that file"] if uncovered else []) + (
|
|
1064
|
+
[f"narrow `{glob}` so it no longer matches that file (to remove the glob instead, record a `demoted-exclude` "
|
|
1065
|
+
"scope-expansion amendment in the brief)"] if glob else [])
|
|
1066
|
+
who = f"the entry point `{entry}`" if isinstance(entry, str) and entry else "an entry point"
|
|
1067
|
+
return _gap(
|
|
1068
|
+
"Medium", "stale-documentation", f"Stale documentation: {name}", doc or "SKILL.md",
|
|
1069
|
+
f"Decide whether `{name}` belongs on the surface: `{brief}` scopes out `{at}`, where it is defined, since "
|
|
1070
|
+
f"{' and '.join(causes)}. To keep its documentation, {' and '.join(fixes)}; else remove the documentation "
|
|
1071
|
+
f"of `{name}` at {doc or 'SKILL.md'}.",
|
|
1072
|
+
issue=f"{f'`{doc}`' if doc else 'SKILL.md'} documents `{name}`, which {who} exports and `{at}` defines, a "
|
|
1073
|
+
f"file `{brief}` scopes out (`excluded.outsideScope`)",
|
|
1074
|
+
export=name)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _named_inputs(surface: object, signatures: object) -> tuple[dict | None, set[str], set[str]]:
|
|
1078
|
+
"""The --surface result, the names --signatures lists in `missingTypes` and the names whose documented
|
|
1079
|
+
signature it compared (`comparedNames`), for a result that names its missing and stale exports."""
|
|
1080
|
+
types: set[str] = set()
|
|
1081
|
+
scored: set[str] = set()
|
|
1082
|
+
if signatures is not None:
|
|
1083
|
+
signatures = _object(signatures, "--signatures (score-signatures.py)", "missingTypes")
|
|
1084
|
+
types = set(_names(signatures, "missingTypes", "--signatures"))
|
|
1085
|
+
scored = set(_names(signatures, "comparedNames", "--signatures"))
|
|
1086
|
+
if surface is not None:
|
|
1087
|
+
surface = _object(surface, "--surface (load-coverage-inputs.py surface)", "exports")
|
|
1088
|
+
return surface, types, scored
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def _missing_gaps(cov: dict, surface: dict | None, types: set[str], meta: str) -> list[dict]:
|
|
1092
|
+
"""One gap per `missing` name, at the file and line the surface records for it, or, when the surface counts
|
|
1093
|
+
an inflated `scope.include` union a tier A list corrects (`_inflated_union`), one summary gap at the brief in
|
|
1094
|
+
their place (#795)."""
|
|
1095
|
+
where = _surface_sources(surface)
|
|
1096
|
+
missing = _names(cov, "missing", "--input")
|
|
1097
|
+
if missing and _inflated_union(surface):
|
|
1098
|
+
union = ((surface.get("guards") or {}).get("inflation") or {}).get("scopeIncludeUnion")
|
|
1099
|
+
return [_inflated_union_gap(missing, where, types, _brief_of(surface), union)]
|
|
1100
|
+
return [_missing_export(name, where.get(name), meta, "type" if name in types else "export") for name in missing]
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def _stale_gaps(cov: dict, surface: dict | None, stale: object, scored: set[str],
|
|
1104
|
+
skill_dir: str | None) -> list[dict]:
|
|
1105
|
+
"""One gap per `stale` name: fabricated, a documented extra, or stale documentation (with its own wording
|
|
1106
|
+
for a name defined in a file the brief scopes out, #796)."""
|
|
1107
|
+
records: list[dict] = []
|
|
1108
|
+
# `excluded` is read once a stale name may be a documented extra
|
|
1109
|
+
outside: dict[str, dict] | None = None
|
|
1110
|
+
outside_read = False
|
|
1111
|
+
classified: dict[str, dict] = {}
|
|
1112
|
+
if stale is not None:
|
|
1113
|
+
if not isinstance(stale, list):
|
|
1114
|
+
raise AdapterError("--stale is not classify-stale JSON (a list)")
|
|
1115
|
+
classified = {i["name"]: i for i in stale if isinstance(i, dict) and isinstance(i.get("name"), str)}
|
|
1116
|
+
for name in _names(cov, "stale", "--input"):
|
|
1117
|
+
item = classified.get(name) or {}
|
|
1118
|
+
cited = item.get("source")
|
|
1119
|
+
if item.get("fabricated") is True:
|
|
1120
|
+
if not isinstance(cited, str) or not cited:
|
|
1121
|
+
# a Critical gap needs the line it cites: never downgrade it
|
|
1122
|
+
raise AdapterError(f"--stale: {name!r} is fabricated with no source")
|
|
1123
|
+
records.append(_gap(
|
|
1124
|
+
"Critical", "fabricated-signature", f"Fabricated signature: {name}", cited,
|
|
1125
|
+
f"Remove `{name}` from SKILL.md and references/, or correct it to the export the source "
|
|
1126
|
+
f"defines: no file its provenance cites defines it (`{cited}`).",
|
|
1127
|
+
issue=f"SKILL.md documents `{name}`, which the source API surface lacks and the cited "
|
|
1128
|
+
"file does not define",
|
|
1129
|
+
export=name))
|
|
1130
|
+
continue
|
|
1131
|
+
if not outside_read:
|
|
1132
|
+
outside, outside_read = _outside_scope(surface), True
|
|
1133
|
+
extra = _documented_extra_at(name, item, outside, skill_dir)
|
|
1134
|
+
doc = _documenting_line(Path(skill_dir), name) if skill_dir else None
|
|
1135
|
+
if extra:
|
|
1136
|
+
declared, among = extra
|
|
1137
|
+
# a homonym: the one declaring file the skill's own citations name
|
|
1138
|
+
picked = (f"; of the {among} files that declare it, the skill cites only "
|
|
1139
|
+
f"`{declared.rsplit(':', 1)[0]}`") if among else ""
|
|
1140
|
+
# never routed: update-skill leaves an `observation` alone. Signature scoring compares a
|
|
1141
|
+
# documented signature of a name on the surface's records (a nested sub-package's) alone.
|
|
1142
|
+
checked = ("Signature scoring compares its documented signature with the surface's record of it."
|
|
1143
|
+
if name in scored else
|
|
1144
|
+
"Its documented signature was not checked against that declaration.")
|
|
1145
|
+
records.append(_gap(
|
|
1146
|
+
"Info", "observation", f"Documented extra: {name}", declared,
|
|
1147
|
+
f"No change: `{name}` is a documented extra, a name the source still declares at "
|
|
1148
|
+
f"`{declared}` outside the enumerated surface, and its documentation stays. {checked}",
|
|
1149
|
+
issue=f"{f'`{doc}`' if doc else 'the skill'} documents `{name}`, which the enumerated source API "
|
|
1150
|
+
f"surface lacks and the source still declares{picked}",
|
|
1151
|
+
export=name))
|
|
1152
|
+
continue
|
|
1153
|
+
if outside is not None and name in outside and _scoped_out_by(outside[name]):
|
|
1154
|
+
# an entry point exports it from a file the brief scopes out (#796)
|
|
1155
|
+
records.append(_scoped_out_gap(name, outside[name], doc, _brief_of(surface)))
|
|
1156
|
+
continue
|
|
1157
|
+
records.append(_gap(
|
|
1158
|
+
"Medium", "stale-documentation", f"Stale documentation: {name}", doc or "SKILL.md",
|
|
1159
|
+
f"Check `{name}` against the source and update or remove its documentation at "
|
|
1160
|
+
f"{doc or 'SKILL.md'}: the enumerated source surface does not export it.",
|
|
1161
|
+
issue=f"SKILL.md documents `{name}`, which the enumerated source API surface lacks",
|
|
1162
|
+
export=name))
|
|
1163
|
+
return records
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _residual_gap(cov: dict, meta: str) -> list[dict]:
|
|
1167
|
+
"""A counted set's residual: the exports the stored count counts beyond the names the set lists, one gap
|
|
1168
|
+
when `missingCount` exceeds the listed `missing` names."""
|
|
1169
|
+
listed = len(_names(cov, "missing", "--input"))
|
|
1170
|
+
missing, denominator = _count(cov.get("missingCount")), _count(cov.get("denominator"))
|
|
1171
|
+
residual = (missing or 0) - listed
|
|
1172
|
+
if residual <= 0:
|
|
1173
|
+
return []
|
|
1174
|
+
set_name = cov.get("surfaceSet") if isinstance(cov.get("surfaceSet"), str) else "named"
|
|
1175
|
+
size = _count(cov.get("surfaceSetSize"))
|
|
1176
|
+
names = f"{size} names" if size is not None else "names"
|
|
1177
|
+
return [_gap(
|
|
1178
|
+
"Medium", "missing-export", f"{residual} of {denominator} exports not documented beyond the {set_name} set",
|
|
1179
|
+
meta,
|
|
1180
|
+
f"Find the {residual} exports the stored count (`stats.effective_denominator`) counts beyond the {names} "
|
|
1181
|
+
f"of the `{set_name}` set (`{meta}` lists the exports) and document each with its signature; a stale "
|
|
1182
|
+
"stored count is recounted by re-running Create Skill.",
|
|
1183
|
+
issue=f"of the {denominator} exports the stored count counts, {missing} are not documented: the "
|
|
1184
|
+
f"`{set_name}` set names {listed} of them, and the other {residual} are exports the count holds "
|
|
1185
|
+
"beyond the set's names")]
|
|
1186
|
+
|
|
1187
|
+
|
|
964
1188
|
def from_coverage(cov: object, surface: object = None, signatures: object = None, numerator: object = None,
|
|
965
1189
|
stale: object = None, provenance: object = None, skill_dir: str | None = None,
|
|
966
1190
|
metadata: str | None = None) -> list[dict]:
|
|
@@ -970,70 +1194,16 @@ def from_coverage(cov: object, surface: object = None, signatures: object = None
|
|
|
970
1194
|
branch = cov["branch"]
|
|
971
1195
|
records: list[dict] = []
|
|
972
1196
|
if branch == "enumerated":
|
|
973
|
-
types
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
if signatures is not None:
|
|
977
|
-
signatures = _object(signatures, "--signatures (score-signatures.py)", "missingTypes")
|
|
978
|
-
types = set(_names(signatures, "missingTypes", "--signatures"))
|
|
979
|
-
scored = set(_names(signatures, "comparedNames", "--signatures"))
|
|
980
|
-
if surface is not None:
|
|
981
|
-
surface = _object(surface, "--surface (load-coverage-inputs.py surface)", "exports")
|
|
982
|
-
where = _surface_sources(surface)
|
|
983
|
-
# `excluded` is read once a stale name may be a documented extra
|
|
984
|
-
outside: set[str] | None = None
|
|
985
|
-
outside_read = False
|
|
986
|
-
for name in _names(cov, "missing", "--input"):
|
|
987
|
-
records.append(_missing_export(name, where.get(name), meta, "type" if name in types else "export"))
|
|
988
|
-
classified: dict[str, dict] = {}
|
|
989
|
-
if stale is not None:
|
|
990
|
-
if not isinstance(stale, list):
|
|
991
|
-
raise AdapterError("--stale is not classify-stale JSON (a list)")
|
|
992
|
-
classified = {i["name"]: i for i in stale if isinstance(i, dict) and isinstance(i.get("name"), str)}
|
|
993
|
-
for name in _names(cov, "stale", "--input"):
|
|
994
|
-
item = classified.get(name) or {}
|
|
995
|
-
cited = item.get("source")
|
|
996
|
-
if item.get("fabricated") is True:
|
|
997
|
-
if not isinstance(cited, str) or not cited:
|
|
998
|
-
# a Critical gap needs the line it cites: never downgrade it
|
|
999
|
-
raise AdapterError(f"--stale: {name!r} is fabricated with no source")
|
|
1000
|
-
records.append(_gap(
|
|
1001
|
-
"Critical", "fabricated-signature", f"Fabricated signature: {name}", cited,
|
|
1002
|
-
f"Remove `{name}` from SKILL.md and references/, or correct it to the export the source "
|
|
1003
|
-
f"defines: no file its provenance cites defines it (`{cited}`).",
|
|
1004
|
-
issue=f"SKILL.md documents `{name}`, which the source API surface lacks and the cited "
|
|
1005
|
-
"file does not define",
|
|
1006
|
-
export=name))
|
|
1007
|
-
continue
|
|
1008
|
-
if not outside_read:
|
|
1009
|
-
outside, outside_read = _outside_scope(surface), True
|
|
1010
|
-
extra = _documented_extra_at(name, item, outside, skill_dir)
|
|
1011
|
-
doc = _documenting_line(Path(skill_dir), name) if skill_dir else None
|
|
1012
|
-
if extra:
|
|
1013
|
-
declared, among = extra
|
|
1014
|
-
# a homonym: the one declaring file the skill's own citations name
|
|
1015
|
-
picked = (f"; of the {among} files that declare it, the skill cites only "
|
|
1016
|
-
f"`{declared.rsplit(':', 1)[0]}`") if among else ""
|
|
1017
|
-
# never routed: update-skill leaves an `observation` alone. Signature scoring compares a
|
|
1018
|
-
# documented signature of a name on the surface's records (a nested sub-package's) alone.
|
|
1019
|
-
checked = ("Signature scoring compares its documented signature with the surface's record of it."
|
|
1020
|
-
if name in scored else
|
|
1021
|
-
"Its documented signature was not checked against that declaration.")
|
|
1022
|
-
records.append(_gap(
|
|
1023
|
-
"Info", "observation", f"Documented extra: {name}", declared,
|
|
1024
|
-
f"No change: `{name}` is a documented extra, a name the source still declares at "
|
|
1025
|
-
f"`{declared}` outside the enumerated surface, and its documentation stays. {checked}",
|
|
1026
|
-
issue=f"{f'`{doc}`' if doc else 'the skill'} documents `{name}`, which the enumerated source API "
|
|
1027
|
-
f"surface lacks and the source still declares{picked}",
|
|
1028
|
-
export=name))
|
|
1029
|
-
continue
|
|
1030
|
-
records.append(_gap(
|
|
1031
|
-
"Medium", "stale-documentation", f"Stale documentation: {name}", doc or "SKILL.md",
|
|
1032
|
-
f"Check `{name}` against the source and update or remove its documentation at "
|
|
1033
|
-
f"{doc or 'SKILL.md'}: the enumerated source surface does not export it.",
|
|
1034
|
-
issue=f"SKILL.md documents `{name}`, which the enumerated source API surface lacks",
|
|
1035
|
-
export=name))
|
|
1197
|
+
surface, types, scored = _named_inputs(surface, signatures)
|
|
1198
|
+
records += _missing_gaps(cov, surface, types, meta)
|
|
1199
|
+
records += _stale_gaps(cov, surface, stale, scored, skill_dir)
|
|
1036
1200
|
elif branch in ("scalar", "stack"):
|
|
1201
|
+
# A scalar result that counts its set lists its missing names, and one on a surface read from the
|
|
1202
|
+
# source its stale names: each is recorded as the barrel branch's are.
|
|
1203
|
+
counted = branch == "scalar" and cov.get("numeratorSource") == "surface"
|
|
1204
|
+
stale_named = branch == "scalar" and cov.get("staleApplicable") is True
|
|
1205
|
+
if counted or stale_named:
|
|
1206
|
+
surface, types, scored = _named_inputs(surface, signatures)
|
|
1037
1207
|
if branch == "scalar" and cov.get("numeratorSource") == "verified":
|
|
1038
1208
|
if numerator is None:
|
|
1039
1209
|
raise AdapterError("the scalar count used the verified numerator: pass its result with "
|
|
@@ -1043,6 +1213,9 @@ def from_coverage(cov: object, surface: object = None, signatures: object = None
|
|
|
1043
1213
|
where = _provenance_sources(_object(provenance, "--provenance (provenance map)", "entries")
|
|
1044
1214
|
if provenance is not None else None)
|
|
1045
1215
|
records += [_missing_export(name, where.get(name), meta) for name in absent]
|
|
1216
|
+
elif counted:
|
|
1217
|
+
records += _missing_gaps(cov, surface, types, meta)
|
|
1218
|
+
records += _residual_gap(cov, meta)
|
|
1046
1219
|
else:
|
|
1047
1220
|
missing, denominator = _count(cov.get("missingCount")), _count(cov.get("denominator"))
|
|
1048
1221
|
if missing:
|
|
@@ -1052,6 +1225,8 @@ def from_coverage(cov: object, surface: object = None, signatures: object = None
|
|
|
1052
1225
|
f"name (`{meta}` lists the exports) and document each with its signature.",
|
|
1053
1226
|
issue=f"SKILL.md and references/ name {cov.get('documented')} of the {denominator} exports "
|
|
1054
1227
|
f"the {branch} denominator counts"))
|
|
1228
|
+
if stale_named:
|
|
1229
|
+
records += _stale_gaps(cov, surface, stale, scored, skill_dir)
|
|
1055
1230
|
elif branch != "docsOnly":
|
|
1056
1231
|
raise AdapterError(f"--input (reconcile-coverage.py): unknown branch {branch!r}")
|
|
1057
1232
|
return records
|
|
@@ -1086,42 +1261,86 @@ def from_guards(surface: object, metadata: str | None = None) -> list[dict]:
|
|
|
1086
1261
|
else:
|
|
1087
1262
|
no_tier_a = "no `scope.tier_a_include` glob in the brief counts a public name"
|
|
1088
1263
|
deflation = guards.get("deflation") or {}
|
|
1089
|
-
|
|
1264
|
+
inflation = guards.get("inflation") or {}
|
|
1265
|
+
stored = _count(deflation.get("effectiveDenominator")) or 0
|
|
1266
|
+
# No remediation sets `stats.effective_denominator` by hand: Update Skill's recount drops such a value (#803).
|
|
1267
|
+
# Both guards on the scope.include union name one fix, the brief's tier A list (#788).
|
|
1268
|
+
both = (deflation.get("fires") is True and inflation.get("fires") is True
|
|
1269
|
+
and deflation.get("basis") != "tier_a_include")
|
|
1270
|
+
# On an umbrella barrel the names are re-exports, each counted at the file that defines it, never the barrel.
|
|
1271
|
+
umbrella = (" rather than the root barrel, an umbrella of re-exports"
|
|
1272
|
+
if (guards.get("umbrella") or {}).get("umbrella") is True else "")
|
|
1273
|
+
verb = (f"Edit `scope.tier_a_include` in `{brief}` so its globs list" if brief_tier_a
|
|
1274
|
+
else f"Add `scope.tier_a_include` to `{brief}`, listing")
|
|
1275
|
+
# Only Create Skill always writes metadata.json, and it stores the runner's count only under this rule.
|
|
1276
|
+
stores = ("when `effective_denominator_decision.store` holds and the brief's `scope.notes` documents the "
|
|
1277
|
+
"stratification, and otherwise drops the stored count")
|
|
1278
|
+
joint = (f"{verb} the files that define the authored names{umbrella}: the `scope.include` union is itself "
|
|
1279
|
+
"inflated, so neither it nor the smaller stored count measures the authored surface. Then re-run "
|
|
1280
|
+
f"Create Skill to recount `stats.effective_denominator` in `{meta}` from the tier A set: it stores "
|
|
1281
|
+
f"that count {stores}.")
|
|
1282
|
+
if deflation.get("fires") is True and deflation.get("basis") == "tier_a_include":
|
|
1283
|
+
# A stored count the brief's tier A set disagrees with, either way: stale, so count it again (#813).
|
|
1284
|
+
pct = deflation.get("pct")
|
|
1285
|
+
if pct is None: # a stored 0: no percentage against it
|
|
1286
|
+
against = f" against a `stats.effective_denominator` of {stored}"
|
|
1287
|
+
else:
|
|
1288
|
+
below = isinstance(pct, (int, float)) and pct < 0
|
|
1289
|
+
against = (f", {-pct if below else pct}% {'below' if below else 'above'} `stats.effective_denominator` "
|
|
1290
|
+
f"({deflation.get('effectiveDenominator')})")
|
|
1291
|
+
# Until then: the set the denominator actually counts (a ladder with no tier A rung counts scope.include),
|
|
1292
|
+
# or, on an incomplete extraction's tier A set below the stored count, the stored count.
|
|
1293
|
+
denominator = surface.get("denominator") if isinstance(surface.get("denominator"), dict) else {}
|
|
1294
|
+
extraction = surface.get("extraction") if isinstance(surface.get("extraction"), dict) else {}
|
|
1295
|
+
if denominator.get("branch") == "scalar":
|
|
1296
|
+
until = " Until then the stored count stays the denominator" + (
|
|
1297
|
+
": the extraction is incomplete, so the tier A set can miss names." if extraction.get("status")
|
|
1298
|
+
== "incomplete" else ".")
|
|
1299
|
+
elif isinstance(denominator.get("surfaceSet"), str):
|
|
1300
|
+
until = f" Until then Test Skill counts the `{denominator['surfaceSet']}` set as the denominator."
|
|
1301
|
+
else:
|
|
1302
|
+
until = ""
|
|
1303
|
+
records.append(_gap(
|
|
1304
|
+
"Medium", "metadata-drift",
|
|
1305
|
+
"denominator drift: effective_denominator disagrees with the brief's tier_a_include set", meta,
|
|
1306
|
+
f"Re-run Create Skill to recount `stats.effective_denominator` in `{meta}`: it stores the runner's count "
|
|
1307
|
+
f"of the brief's `scope.tier_a_include` set {stores}.{until}",
|
|
1308
|
+
issue=f"the brief's `scope.tier_a_include` set counts {deflation.get('rederived')} exports{against}"))
|
|
1309
|
+
elif deflation.get("fires") is True:
|
|
1090
1310
|
if deflation.get("pct") is None: # a stored 0: no percentage above it
|
|
1091
|
-
above =
|
|
1092
|
-
f"{_count(deflation.get('effectiveDenominator')) or 0}")
|
|
1311
|
+
above = f" against a `stats.effective_denominator` of {stored}"
|
|
1093
1312
|
else:
|
|
1094
1313
|
above = (f", {deflation.get('pct')}% above `stats.effective_denominator` "
|
|
1095
1314
|
f"({deflation.get('effectiveDenominator')})")
|
|
1096
|
-
if
|
|
1315
|
+
if both:
|
|
1316
|
+
fix = joint
|
|
1317
|
+
elif deflation.get("pct") is None: # a 0 measures no tier: count it again
|
|
1097
1318
|
first = ("Edit `scope.tier_a_include` in the brief so its globs match the files that define the "
|
|
1098
1319
|
"authored tier" if brief_tier_a else "Check `scope.include` in the brief")
|
|
1099
1320
|
fix = (f"{first}, then re-run Create Skill so `stats.effective_denominator` in `{meta}` is counted "
|
|
1100
1321
|
"again.")
|
|
1101
1322
|
elif brief_tier_a:
|
|
1102
|
-
fix = (f"
|
|
1103
|
-
"or edit `scope.tier_a_include` in the
|
|
1104
|
-
"the smaller count measures.")
|
|
1323
|
+
fix = (f"Re-run Create Skill to recount `stats.effective_denominator` in `{meta}` from the public surface "
|
|
1324
|
+
f"the source re-derives (it stores that count {stores}), or edit `scope.tier_a_include` in the "
|
|
1325
|
+
"brief so its globs match the files of the authored tier the smaller count measures.")
|
|
1105
1326
|
else:
|
|
1106
|
-
fix = (f"
|
|
1107
|
-
"or add `scope.tier_a_include` to the
|
|
1108
|
-
"measures.")
|
|
1327
|
+
fix = (f"Re-run Create Skill to recount `stats.effective_denominator` in `{meta}` from the public surface "
|
|
1328
|
+
f"the source re-derives (it stores that count {stores}), or add `scope.tier_a_include` to the "
|
|
1329
|
+
"brief to name the authored tier the smaller count measures.")
|
|
1109
1330
|
records.append(_gap(
|
|
1110
1331
|
"Medium", "metadata-drift",
|
|
1111
1332
|
"denominator deflation: effective_denominator below source public surface without tier_a_include",
|
|
1112
1333
|
meta, fix,
|
|
1113
1334
|
issue=f"the re-derived surface counts {deflation.get('rederived')} exports{above}, and {no_tier_a}"))
|
|
1114
|
-
inflation = guards.get("inflation") or {}
|
|
1115
1335
|
if inflation.get("fires") is True:
|
|
1116
|
-
if
|
|
1117
|
-
fix =
|
|
1118
|
-
"is an umbrella of re-exports, so a `scope.tier_a_include` glob would still count them.")
|
|
1336
|
+
if both:
|
|
1337
|
+
fix = joint
|
|
1119
1338
|
elif brief_tier_a:
|
|
1120
1339
|
fix = (f"Edit `scope.tier_a_include` in `{brief}` so its globs match the files of the authored "
|
|
1121
|
-
"surface: the denominator then counts it instead of the coarse `scope.include` union.")
|
|
1340
|
+
f"surface{umbrella}: the denominator then counts it instead of the coarse `scope.include` union.")
|
|
1122
1341
|
else:
|
|
1123
|
-
fix = (f"Add `scope.tier_a_include` to `{brief}`, listing the files of the authored surface,
|
|
1124
|
-
"the denominator counts it instead of the coarse `scope.include` union.")
|
|
1342
|
+
fix = (f"Add `scope.tier_a_include` to `{brief}`, listing the files of the authored surface{umbrella}, "
|
|
1343
|
+
"so the denominator counts it instead of the coarse `scope.include` union.")
|
|
1125
1344
|
records.append(_gap(
|
|
1126
1345
|
"Medium", "denominator-inflation",
|
|
1127
1346
|
"denominator inflation: coarse scope.include union exceeds authored surface", brief, fix,
|
|
@@ -1198,11 +1417,24 @@ def from_metadata_coherence(result: object, metadata: str | None = None) -> list
|
|
|
1198
1417
|
if not isinstance(finding, dict) or not isinstance(finding.get("title"), str):
|
|
1199
1418
|
raise AdapterError(f"--input: findings[{index}] has no title")
|
|
1200
1419
|
detail = finding.get("detail") if isinstance(finding.get("detail"), str) else None
|
|
1201
|
-
if finding.get("severity") == "Medium":
|
|
1420
|
+
if finding.get("severity") == "Medium" and "documented-surface" in finding["title"]:
|
|
1421
|
+
# `stats.exports_documented` and the confidence_distribution sum count every provenance entry, while
|
|
1422
|
+
# the named-export count leaves out `Type::method` entries: a re-run reproduces that difference
|
|
1202
1423
|
records.append(_gap(
|
|
1203
1424
|
"Medium", "metadata-drift", finding["title"], meta,
|
|
1204
|
-
f"
|
|
1205
|
-
"
|
|
1425
|
+
f"No count in `{meta}` needs a fix when `stats.exports_documented` and the `confidence_distribution` "
|
|
1426
|
+
"sum agree: both count every provenance map entry, while the named-export count leaves out the "
|
|
1427
|
+
"`Type::method` entries, so the counts differ by the methods the skill documents. When those two "
|
|
1428
|
+
"disagree, Update Skill or Create Skill recounts both from the provenance map the next time it "
|
|
1429
|
+
"writes the skill; a hand edit does not hold.",
|
|
1430
|
+
issue=detail))
|
|
1431
|
+
elif finding.get("severity") == "Medium":
|
|
1432
|
+
records.append(_gap(
|
|
1433
|
+
"Medium", "metadata-drift", finding["title"], meta,
|
|
1434
|
+
f"Re-run Create Skill, which rebuilds `exports[]` and `stats.exports_public_api` in `{meta}` from "
|
|
1435
|
+
"the source. Update Skill does not repair this drift: each time it writes the skill it recounts "
|
|
1436
|
+
"`stats.exports_public_api` from the source and keeps `exports[]`, adding or dropping only the "
|
|
1437
|
+
"names its provenance map changes, so a metadata patch does not hold.",
|
|
1206
1438
|
issue=detail))
|
|
1207
1439
|
elif finding.get("severity") == "Info":
|
|
1208
1440
|
records.append(_gap(
|
|
@@ -1217,15 +1449,52 @@ def from_metadata_coherence(result: object, metadata: str | None = None) -> list
|
|
|
1217
1449
|
|
|
1218
1450
|
|
|
1219
1451
|
def from_provenance_line(result: object) -> list[dict]:
|
|
1220
|
-
"""verify's `
|
|
1452
|
+
"""verify's `stale[]` items: provenance line gaps, one per citation that does not resolve or is not the
|
|
1453
|
+
definition line."""
|
|
1221
1454
|
result = _object(result, "--input (skf-verify-provenance-completeness.py verify)", "stale")
|
|
1222
1455
|
records: list[dict] = []
|
|
1223
1456
|
for item in _list(result, "stale", "--input"):
|
|
1224
|
-
if not isinstance(item, dict)
|
|
1457
|
+
if not isinstance(item, dict):
|
|
1225
1458
|
continue
|
|
1459
|
+
reason = item.get("reason")
|
|
1226
1460
|
name, file, line = item.get("export_name"), item.get("source_file"), item.get("source_line")
|
|
1227
|
-
defs = [n for n in item.get("definition_lines") or [] if _count(n) is not None]
|
|
1228
1461
|
where = f"{file}:{line}"
|
|
1462
|
+
if reason == "file-missing":
|
|
1463
|
+
records.append(_gap(
|
|
1464
|
+
"Low", "provenance-line", f"Provenance file is missing for {name}", where,
|
|
1465
|
+
f"Set the provenance `source_file` and `source_line` of `{name}` to the file and line that define "
|
|
1466
|
+
"it in the pinned source, and move its citations there. update-skill `--from-test-report` does not "
|
|
1467
|
+
"repair this: it leaves the entry unchanged, with a warning that names the file.",
|
|
1468
|
+
issue=f"the provenance map records `{name}` at `{where}`, but the pinned source has no readable "
|
|
1469
|
+
f"file `{file}`; the export may live in another file, such as one with another extension.",
|
|
1470
|
+
export=name))
|
|
1471
|
+
continue
|
|
1472
|
+
if reason == "line-out-of-bounds":
|
|
1473
|
+
records.append(_gap(
|
|
1474
|
+
"Low", "provenance-line", f"Provenance line is out of range for {name}", where,
|
|
1475
|
+
f"Set the provenance `source_line` of `{name}` in `{file}` to the line that defines it and move "
|
|
1476
|
+
"its citations to that line; update-skill `--from-test-report` applies this when the file defines "
|
|
1477
|
+
"it on one line.",
|
|
1478
|
+
issue=f"the provenance map records `{name}` at `{where}`, which is not a line of `{file}` (past "
|
|
1479
|
+
"its end, or below 1).",
|
|
1480
|
+
export=name))
|
|
1481
|
+
continue
|
|
1482
|
+
if reason == "line-invalid": # no line number to cite: the Source is the file alone
|
|
1483
|
+
records.append(_gap(
|
|
1484
|
+
"Info", "provenance-unverified", f"Provenance line not verified for {name}", f"{file}",
|
|
1485
|
+
f"Set the provenance `source_line` of `{name}` in `{file}` to the number of the line that defines "
|
|
1486
|
+
"it.",
|
|
1487
|
+
issue=f"the provenance map records `{name}` in `{file}` at line `{line}`, which is not a line "
|
|
1488
|
+
"number, so no rule could check it; check by hand",
|
|
1489
|
+
export=name))
|
|
1490
|
+
continue
|
|
1491
|
+
if reason != "line-not-definition":
|
|
1492
|
+
continue
|
|
1493
|
+
defs = [n for n in item.get("definition_lines") or [] if _count(n) is not None]
|
|
1494
|
+
base = item.get("folded_to") if isinstance(item.get("folded_to"), str) and item.get("folded_to") else None
|
|
1495
|
+
# a bookkeeping variant the file never names was checked against its base (#847)
|
|
1496
|
+
folded = f" `{name}` is a bookkeeping variant `{file}` never names, checked against its base `{base}`." \
|
|
1497
|
+
if base else ""
|
|
1229
1498
|
if defs:
|
|
1230
1499
|
lines = ", ".join(str(n) for n in defs)
|
|
1231
1500
|
records.append(_gap(
|
|
@@ -1234,18 +1503,27 @@ def from_provenance_line(result: object) -> list[dict]:
|
|
|
1234
1503
|
"and move its citations to that line; update-skill `--from-test-report` applies this when the "
|
|
1235
1504
|
"file defines it on one line.",
|
|
1236
1505
|
issue=f"the provenance map records `{name}` at `{where}`, which is not the line that defines "
|
|
1237
|
-
f"it; the lines that define it are {lines}.",
|
|
1506
|
+
f"it; the lines that define it are {lines}.{folded}",
|
|
1238
1507
|
export=name))
|
|
1239
1508
|
else:
|
|
1240
1509
|
records.append(_gap(
|
|
1241
1510
|
"Info", "provenance-unverified", f"Provenance line not verified for {name}", where,
|
|
1242
|
-
f"Open `{file}` and confirm that line {line} defines `{name}`; if another line does, set
|
|
1243
|
-
"provenance `source_line` to it.",
|
|
1244
|
-
issue=f"the line-check rules found no definition line for `{name}` in `{file}`; check by hand"
|
|
1511
|
+
f"Open `{file}` and confirm that line {line} defines `{base or name}`; if another line does, set "
|
|
1512
|
+
"the provenance `source_line` to it.",
|
|
1513
|
+
issue=f"the line-check rules found no definition line for `{name}` in `{file}`; check by hand"
|
|
1514
|
+
+ (f".{folded}" if folded else ""),
|
|
1245
1515
|
export=name))
|
|
1246
1516
|
return records
|
|
1247
1517
|
|
|
1248
1518
|
|
|
1519
|
+
def without_fabricated(ledger: dict, records: list[dict]) -> list[dict]:
|
|
1520
|
+
"""`--from provenance-line`'s records but those of an export the ledger already records as a
|
|
1521
|
+
`fabricated-signature` gap: coverage-check §5b appends that record first, and an export the source no longer
|
|
1522
|
+
holds takes no provenance line fix."""
|
|
1523
|
+
fabricated = {r.get("export") for r in ledger["records"] if r.get("category") == "fabricated-signature"}
|
|
1524
|
+
return [r for r in records if not r.get("export") or r.get("export") not in fabricated]
|
|
1525
|
+
|
|
1526
|
+
|
|
1249
1527
|
def from_coherence(result: object) -> list[dict]:
|
|
1250
1528
|
"""aggregate-coherence.py's invalid references."""
|
|
1251
1529
|
result = _object(result, "--input (aggregate-coherence.py)", "invalidReferences")
|
|
@@ -1541,6 +1819,8 @@ def _cmd_append(args: argparse.Namespace) -> int:
|
|
|
1541
1819
|
# one waits here instead of writing over the records it adds.
|
|
1542
1820
|
with ledger_lock(path):
|
|
1543
1821
|
ledger = load_ledger(path) if path.exists() else new_ledger(path)
|
|
1822
|
+
if args.adapter == "provenance-line":
|
|
1823
|
+
records = without_fabricated(ledger, records)
|
|
1544
1824
|
appended, duplicates = append_records(ledger, args.stage, records)
|
|
1545
1825
|
save_ledger(path, ledger)
|
|
1546
1826
|
except LedgerError as exc:
|