bmad-module-skill-forge 6.0.0 → 8.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 +1 -1
- package/docs/_data/pinned.yaml +9 -10
- package/docs/campaign.md +2 -2
- package/docs/skill-model.md +3 -3
- package/docs/verifying-a-skill.md +17 -18
- package/docs/workflows.md +10 -10
- package/package.json +1 -1
- package/src/shared/references/description-guard-protocol.md +12 -10
- package/src/shared/references/output-contract-schema.md +1 -1
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +38 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +5 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +8 -3
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +6 -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 +7 -2
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +1 -1
- package/src/shared/scripts/skf-parse-gaps.py +4 -1
- package/src/shared/scripts/skf-quick-batch.py +24 -7
- package/src/shared/scripts/skf-verify-provenance-completeness.py +809 -85
- package/src/skf-analyze-source/customize.toml +3 -2
- package/src/skf-analyze-source/references/auto-docs-only.md +2 -2
- package/src/skf-analyze-source/references/generate-briefs.md +5 -3
- package/src/skf-analyze-source/references/headless-contract.md +2 -2
- package/src/skf-analyze-source/references/step-auto-scope.md +2 -2
- package/src/skf-audit-skill/customize.toml +4 -3
- package/src/skf-audit-skill/references/report.md +1 -1
- package/src/skf-campaign/assets/campaign-state-schema.json +58 -0
- package/src/skf-campaign/references/campaign-contracts.md +1 -1
- package/src/skf-campaign/references/step-06-batch.md +2 -2
- package/src/skf-campaign/references/step-07-capstone.md +3 -3
- package/src/skf-campaign/references/step-08-verify.md +3 -3
- package/src/skf-campaign/references/step-09-refine.md +3 -3
- package/src/skf-campaign/references/step-11-maintenance.md +1 -1
- package/src/skf-campaign/scripts/campaign-render-batch.py +43 -10
- package/src/skf-campaign/scripts/campaign-report.py +60 -4
- package/src/skf-campaign/scripts/campaign-state.py +82 -17
- package/src/skf-campaign/templates/campaign-report-template.md +4 -0
- package/src/skf-create-skill/SKILL.md +2 -2
- package/src/skf-create-skill/assets/skill-sections.md +2 -2
- package/src/skf-create-skill/customize.toml +4 -4
- package/src/skf-create-skill/references/batch-mode.md +3 -1
- package/src/skf-create-skill/references/report.md +9 -4
- package/src/skf-create-skill/references/validate.md +49 -29
- package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
- package/src/skf-create-stack-skill/references/report.md +3 -3
- package/src/skf-create-stack-skill/references/validate.md +21 -8
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-export-skill/references/summary.md +1 -1
- package/src/skf-quick-skill/SKILL.md +1 -1
- package/src/skf-quick-skill/references/batch-mode.md +30 -2
- package/src/skf-quick-skill/references/compile.md +2 -0
- package/src/skf-quick-skill/references/finalize.md +3 -2
- package/src/skf-quick-skill/references/halt-contract.md +3 -3
- package/src/skf-quick-skill/references/quick-extract.md +2 -1
- package/src/skf-quick-skill/references/write-and-validate.md +27 -11
- package/src/skf-refine-architecture/customize.toml +1 -1
- package/src/skf-refine-architecture/references/report.md +2 -2
- package/src/skf-rename-skill/references/report.md +1 -1
- package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
- package/src/skf-test-skill/references/coverage-check.md +4 -4
- package/src/skf-test-skill/references/inventory-contract.md +1 -1
- package/src/skf-test-skill/references/invocation-contract.md +1 -1
- package/src/skf-test-skill/references/report.md +1 -1
- package/src/skf-test-skill/references/scoring-rules.md +2 -2
- package/src/skf-test-skill/references/signature-comparison-rules.md +7 -2
- package/src/skf-test-skill/scripts/gap-ledger.py +196 -46
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +253 -17
- package/src/skf-test-skill/scripts/score-signatures.py +9 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +91 -12
- package/src/skf-update-skill/customize.toml +1 -1
- package/src/skf-update-skill/references/gap-driven.md +1 -1
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +1 -1
- package/src/skf-update-skill/references/report.md +27 -9
- package/src/skf-update-skill/references/write.md +24 -12
- package/src/skf-verify-stack/references/report.md +1 -1
|
@@ -115,25 +115,46 @@ cites (never read; default `metadata.json`):
|
|
|
115
115
|
name it (outside the citations), or on the next non-blank line
|
|
116
116
|
after one, cite exactly one of those files: the gap
|
|
117
117
|
is at that file's `declared_in` item, and two cited or none
|
|
118
|
-
leave it Medium. The documented extra's
|
|
119
|
-
that documents it (with --skill-dir) and its remediation asks no
|
|
118
|
+
leave it Medium. The documented extra's remediation asks no
|
|
120
119
|
change (update-skill never routes it) and says whether signature
|
|
121
120
|
scoring compared its documented signature (a name the
|
|
122
121
|
--signatures result lists in `comparedNames`). `excluded` is
|
|
123
122
|
read once a stale name that is not fabricated needs it, and a
|
|
124
123
|
malformed one is then INVALID_INPUT. Any other is a Medium `stale-documentation` gap
|
|
125
|
-
`Stale documentation: {name}` at the
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
`Stale documentation: {name}` at the line that documents the
|
|
125
|
+
name (with --skill-dir), else `SKILL.md`. That line is the
|
|
126
|
+
first that writes it as an export, the first tier that holds
|
|
127
|
+
the name winning: (1) its first origin in the documented
|
|
128
|
+
inventory's floor (load-coverage-inputs.py floor); (2) a
|
|
129
|
+
column-0 declaration of it in a fenced block of a language
|
|
130
|
+
floor's `_declared_names` reads (JS/TS, Python, Rust, Go),
|
|
131
|
+
cited or not, outside an HTML comment and a data block (json, yaml...);
|
|
132
|
+
(3) a heading, or the first cell of a table whose header's
|
|
133
|
+
first cell names exports (floor's table headers), whose text is
|
|
134
|
+
the name or holds a code span floor reads as the name; (4) a
|
|
135
|
+
line whose inline code span is the name, or the name directly
|
|
136
|
+
followed by `(`. Tiers 2 to 4 read SKILL.md, then every .md file
|
|
137
|
+
under references/, subfolders included, in path order, outside
|
|
138
|
+
the frontmatter (3 and 4 outside fenced code and HTML comments
|
|
139
|
+
too); within a tier the first file, then its lowest line, wins.
|
|
140
|
+
A bare word, or a span that holds the name among other words
|
|
141
|
+
(`uitripled add`), documents nothing; with no such line, or
|
|
142
|
+
without --skill-dir, the Source is `SKILL.md`. Floor's
|
|
143
|
+
InputError (no SKILL.md, or a top-level file it cannot read or
|
|
144
|
+
decode) leaves tier 1 empty, and a skill file that cannot be
|
|
145
|
+
read leaves tiers 2 to 4 empty. One whose
|
|
146
|
+
`excluded.outsideScope` row says the brief
|
|
129
147
|
scopes its file out (`excludeGlob`, the `scope.exclude` glob
|
|
130
148
|
that matches it, or `outsideInclude`, no `scope.include` glob
|
|
131
149
|
covering it) has its own issue, naming the entry point and the
|
|
132
150
|
file and line, and remediation: to keep the documentation,
|
|
133
151
|
narrow that exclude glob (a removal needs a `demoted-exclude`
|
|
134
152
|
amendment) or widen `scope.include`, else remove it. A row the
|
|
135
|
-
brief does not explain keeps the wording above.
|
|
136
|
-
|
|
153
|
+
brief does not explain keeps the wording above. Every stale
|
|
154
|
+
name's issue (fabricated, documented extra, stale documentation
|
|
155
|
+
or scoped out) opens with the line that documents it, as
|
|
156
|
+
`file:line` (with --skill-dir), else with `the skill`, whatever
|
|
157
|
+
its Source. Scalar and stack branches: a
|
|
137
158
|
`missingCount` above 0 is one Medium `missing-export` gap
|
|
138
159
|
`{missingCount} of {denominator} exports not documented` at
|
|
139
160
|
`{meta}`, except that a scalar count taken from the verified
|
|
@@ -194,15 +215,26 @@ cites (never read; default `metadata.json`):
|
|
|
194
215
|
entries its counts differ by; a barrel one sends the fix to
|
|
195
216
|
Create Skill, which rebuilds `exports[]` from the source.
|
|
196
217
|
provenance-line
|
|
197
|
-
--input skf-verify-provenance-completeness.py verify's result
|
|
218
|
+
--input skf-verify-provenance-completeness.py verify's result;
|
|
219
|
+
--stale.
|
|
198
220
|
Each `stale[]` item, at its `{source_file}:{source_line}` with
|
|
199
221
|
`export`, by `reason`: `line-not-definition` is a Low
|
|
200
222
|
`provenance-line` gap `Provenance line is not the definition of
|
|
201
223
|
{export_name}` when `definition_lines` holds a line, else an Info
|
|
202
224
|
`provenance-unverified` gap `Provenance line not verified for
|
|
203
225
|
{export_name}`, and its issue names the base of a `folded_to`
|
|
204
|
-
variant
|
|
205
|
-
`
|
|
226
|
+
variant. When the --stale result (classify-stale) marks
|
|
227
|
+
`{export_name}` `aliased` with a `file:line` `defined_at`, that
|
|
228
|
+
Info gap's issue and remediation name the alias at `defined_at`,
|
|
229
|
+
and the remediation asks first to record the rename if no other
|
|
230
|
+
entry already has its `export_name` (`export_name` the item's
|
|
231
|
+
`renames`, else "a name a cited file declares", `reexported_as`
|
|
232
|
+
the export), else to cite the alias, moving the citations either
|
|
233
|
+
way; when `renames` is `default`, which the map keys on
|
|
234
|
+
`export_name` and so holds once, it asks only to cite the alias.
|
|
235
|
+
A --stale result that is not a list is INVALID_INPUT.
|
|
236
|
+
`file-missing` is a Low `provenance-line`
|
|
237
|
+
gap `Provenance file is missing for {export_name}`;
|
|
206
238
|
`line-out-of-bounds` a Low `provenance-line` gap `Provenance line
|
|
207
239
|
is out of range for {export_name}`; `line-invalid` an Info
|
|
208
240
|
`provenance-unverified` gap `Provenance line not verified for
|
|
@@ -747,7 +779,7 @@ ADAPTER_OPTIONS: dict[str, frozenset[str]] = {
|
|
|
747
779
|
"guards": frozenset({"metadata"}),
|
|
748
780
|
"numerator": frozenset({"metadata"}),
|
|
749
781
|
"metadata-coherence": frozenset({"metadata"}),
|
|
750
|
-
"provenance-line": frozenset(),
|
|
782
|
+
"provenance-line": frozenset({"stale"}),
|
|
751
783
|
"coherence": frozenset(),
|
|
752
784
|
"structure": frozenset({"served"}),
|
|
753
785
|
}
|
|
@@ -776,8 +808,10 @@ _SIBLINGS: dict[str, object] = {}
|
|
|
776
808
|
|
|
777
809
|
def _sibling(filename: str):
|
|
778
810
|
"""A script beside this one, loaded once: validate-inventory.py (the
|
|
779
|
-
|
|
780
|
-
|
|
811
|
+
skill-citation scan the homonym pick reads), load-coverage-inputs.py
|
|
812
|
+
(the documented inventory's floor, and its fence and table readers)
|
|
813
|
+
and locate-export-segments.py (the skill's files and line model), the
|
|
814
|
+
last two for the line that documents a stale name."""
|
|
781
815
|
module = _SIBLINGS.get(filename)
|
|
782
816
|
if module is None:
|
|
783
817
|
path = Path(__file__).resolve().parent / filename
|
|
@@ -872,21 +906,90 @@ def _provenance_sources(provenance: dict | None) -> dict[str, str]:
|
|
|
872
906
|
return sources
|
|
873
907
|
|
|
874
908
|
|
|
909
|
+
def _documenting_lines(skill_dir: Path) -> dict[str, str]:
|
|
910
|
+
"""name -> the `file:line` that documents it as an export, the first tier
|
|
911
|
+
that holds the name winning:
|
|
912
|
+
|
|
913
|
+
1. its first origin in the documented inventory's floor
|
|
914
|
+
(load-coverage-inputs.py `floor`: SKILL.md and the top-level
|
|
915
|
+
references/*.md);
|
|
916
|
+
2. a column-0 public declaration of it in a fenced code block (floor's
|
|
917
|
+
`_declared_names`), cited or not, outside an HTML comment and a data
|
|
918
|
+
block;
|
|
919
|
+
3. a heading, or the first cell of a table whose header's first cell is
|
|
920
|
+
one of floor's `FLOOR_TABLE_HEADERS`, whose stripped text is the name
|
|
921
|
+
or holds a code span floor's `_floor_name` reads as the name;
|
|
922
|
+
4. a line whose inline code span, stripped, is the name, or the name
|
|
923
|
+
directly followed by `(`.
|
|
924
|
+
|
|
925
|
+
Tiers 2 to 4 read SKILL.md, then every .md file under references/,
|
|
926
|
+
subfolders included, in path order (locate-export-segments.py
|
|
927
|
+
`skill_files`), never the frontmatter, tiers 3 and 4 outside fenced code
|
|
928
|
+
and HTML comments; within a tier the first file, then its lowest line,
|
|
929
|
+
wins. A bare word, or a span that holds the name among other words
|
|
930
|
+
(`uitripled add`), documents nothing. An input a tier cannot read is
|
|
931
|
+
skipped: floor's InputError (no SKILL.md, a file that is not UTF-8)
|
|
932
|
+
leaves tier 1 empty, an unreadable file tiers 2 to 4."""
|
|
933
|
+
coverage = _sibling("load-coverage-inputs.py")
|
|
934
|
+
segments = _sibling("locate-export-segments.py")
|
|
935
|
+
tiers: list[dict[str, str]] = [{}, {}, {}, {}]
|
|
936
|
+
try:
|
|
937
|
+
for entry in coverage.floor(str(skill_dir))["names"]:
|
|
938
|
+
origin = entry["origins"][0]
|
|
939
|
+
tiers[0].setdefault(entry["name"], f"{origin['file']}:{origin['line']}")
|
|
940
|
+
except coverage.InputError:
|
|
941
|
+
pass
|
|
942
|
+
try:
|
|
943
|
+
files = segments.skill_files(skill_dir)
|
|
944
|
+
except OSError:
|
|
945
|
+
files = []
|
|
946
|
+
# floor's fence filter: a fence in an HTML comment or a data block declares nothing, and a Svelte block's
|
|
947
|
+
# `export let` is a component prop. Unlike floor, an unheaded fence counts.
|
|
948
|
+
js_family = segments.JS_FAMILY - {"svelte"}
|
|
949
|
+
for rel, lines in files:
|
|
950
|
+
start = segments._frontmatter_end(lines)
|
|
951
|
+
blocks, code = segments._fenced_blocks(lines, start)
|
|
952
|
+
comments = coverage._html_comment_lines(lines, start, code)
|
|
953
|
+
for opening, closing, info in blocks:
|
|
954
|
+
language = segments._language(info)
|
|
955
|
+
if opening in comments or language in segments.DATA_LANGUAGES:
|
|
956
|
+
continue
|
|
957
|
+
masked = segments.mask_code("\n".join(lines[opening + 1:closing]), language).split("\n")
|
|
958
|
+
for offset, declared in coverage._declared_names(masked, language, js_family):
|
|
959
|
+
tiers[1].setdefault(declared, f"{rel}:{opening + offset + 2}")
|
|
960
|
+
headings, prose = segments._prose(lines, start, code)
|
|
961
|
+
shaped = [(index, text) for index, _level, text, _above in headings]
|
|
962
|
+
written = list(shaped)
|
|
963
|
+
for block in prose:
|
|
964
|
+
written += [(i, lines[i]) for i in range(block["start"], block["end"] + 1)]
|
|
965
|
+
if block["kind"] == "table-row" and block["header"] is not None:
|
|
966
|
+
if coverage._header_word(coverage._cells(block["header"])[0]) in coverage.FLOOR_TABLE_HEADERS:
|
|
967
|
+
shaped.append((block["start"], coverage._cells(lines[block["start"]])[0]))
|
|
968
|
+
for index, text in sorted(shaped):
|
|
969
|
+
for key in [text.strip()] + [coverage._floor_name(span) for span in segments._inline_code(text)]:
|
|
970
|
+
if key:
|
|
971
|
+
tiers[2].setdefault(key, f"{rel}:{index + 1}")
|
|
972
|
+
for index, text in sorted(written):
|
|
973
|
+
for span in map(str.strip, segments._inline_code(text)):
|
|
974
|
+
for key in (span, span.split("(", 1)[0] if "(" in span else None):
|
|
975
|
+
if key:
|
|
976
|
+
tiers[3].setdefault(key, f"{rel}:{index + 1}")
|
|
977
|
+
found: dict[str, str] = {}
|
|
978
|
+
for tier in tiers:
|
|
979
|
+
for key, line in tier.items():
|
|
980
|
+
found.setdefault(key, line)
|
|
981
|
+
return found
|
|
982
|
+
|
|
983
|
+
|
|
875
984
|
def _documenting_line(skill_dir: Path, name: str) -> str | None:
|
|
876
|
-
"""The
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
except OSError:
|
|
885
|
-
continue
|
|
886
|
-
for number, line in enumerate(text.split("\n"), start=1):
|
|
887
|
-
if pattern.search(line):
|
|
888
|
-
return f"{path.relative_to(skill_dir).as_posix()}:{number}"
|
|
889
|
-
return None
|
|
985
|
+
"""The `file:line` that documents `name` as an export in the skill at
|
|
986
|
+
`skill_dir` (`_documenting_lines`), else None."""
|
|
987
|
+
return _documenting_lines(skill_dir).get(name)
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _documents(doc: str | None, name: str) -> str:
|
|
991
|
+
"""The subject of a stale name's Issue: the line that documents it, else the skill."""
|
|
992
|
+
return f"{f'`{doc}`' if doc else 'the skill'} documents `{name}`"
|
|
890
993
|
|
|
891
994
|
|
|
892
995
|
def _scoped_out_by(row: dict) -> bool:
|
|
@@ -1069,8 +1172,8 @@ def _scoped_out_gap(name: str, row: dict, doc: str | None, brief: str) -> dict:
|
|
|
1069
1172
|
f"Decide whether `{name}` belongs on the surface: `{brief}` scopes out `{at}`, where it is defined, since "
|
|
1070
1173
|
f"{' and '.join(causes)}. To keep its documentation, {' and '.join(fixes)}; else remove the documentation "
|
|
1071
1174
|
f"of `{name}` at {doc or 'SKILL.md'}.",
|
|
1072
|
-
issue=f"{
|
|
1073
|
-
|
|
1175
|
+
issue=f"{_documents(doc, name)}, which {who} exports and `{at}` defines, a file `{brief}` scopes out "
|
|
1176
|
+
"(`excluded.outsideScope`)",
|
|
1074
1177
|
export=name)
|
|
1075
1178
|
|
|
1076
1179
|
|
|
@@ -1113,9 +1216,13 @@ def _stale_gaps(cov: dict, surface: dict | None, stale: object, scored: set[str]
|
|
|
1113
1216
|
if not isinstance(stale, list):
|
|
1114
1217
|
raise AdapterError("--stale is not classify-stale JSON (a list)")
|
|
1115
1218
|
classified = {i["name"]: i for i in stale if isinstance(i, dict) and isinstance(i.get("name"), str)}
|
|
1116
|
-
|
|
1219
|
+
names = _names(cov, "stale", "--input")
|
|
1220
|
+
# the lines that document each name, read once for every stale name
|
|
1221
|
+
documenting = _documenting_lines(Path(skill_dir)) if skill_dir and names else {}
|
|
1222
|
+
for name in names:
|
|
1117
1223
|
item = classified.get(name) or {}
|
|
1118
1224
|
cited = item.get("source")
|
|
1225
|
+
doc = documenting.get(name)
|
|
1119
1226
|
if item.get("fabricated") is True:
|
|
1120
1227
|
if not isinstance(cited, str) or not cited:
|
|
1121
1228
|
# a Critical gap needs the line it cites: never downgrade it
|
|
@@ -1124,14 +1231,13 @@ def _stale_gaps(cov: dict, surface: dict | None, stale: object, scored: set[str]
|
|
|
1124
1231
|
"Critical", "fabricated-signature", f"Fabricated signature: {name}", cited,
|
|
1125
1232
|
f"Remove `{name}` from SKILL.md and references/, or correct it to the export the source "
|
|
1126
1233
|
f"defines: no file its provenance cites defines it (`{cited}`).",
|
|
1127
|
-
issue=f"
|
|
1128
|
-
"
|
|
1234
|
+
issue=f"{_documents(doc, name)}, which the source API surface lacks and the cited file does "
|
|
1235
|
+
"not define",
|
|
1129
1236
|
export=name))
|
|
1130
1237
|
continue
|
|
1131
1238
|
if not outside_read:
|
|
1132
1239
|
outside, outside_read = _outside_scope(surface), True
|
|
1133
1240
|
extra = _documented_extra_at(name, item, outside, skill_dir)
|
|
1134
|
-
doc = _documenting_line(Path(skill_dir), name) if skill_dir else None
|
|
1135
1241
|
if extra:
|
|
1136
1242
|
declared, among = extra
|
|
1137
1243
|
# a homonym: the one declaring file the skill's own citations name
|
|
@@ -1146,8 +1252,8 @@ def _stale_gaps(cov: dict, surface: dict | None, stale: object, scored: set[str]
|
|
|
1146
1252
|
"Info", "observation", f"Documented extra: {name}", declared,
|
|
1147
1253
|
f"No change: `{name}` is a documented extra, a name the source still declares at "
|
|
1148
1254
|
f"`{declared}` outside the enumerated surface, and its documentation stays. {checked}",
|
|
1149
|
-
issue=f"{
|
|
1150
|
-
f"
|
|
1255
|
+
issue=f"{_documents(doc, name)}, which the enumerated source API surface lacks and the source "
|
|
1256
|
+
f"still declares{picked}",
|
|
1151
1257
|
export=name))
|
|
1152
1258
|
continue
|
|
1153
1259
|
if outside is not None and name in outside and _scoped_out_by(outside[name]):
|
|
@@ -1158,7 +1264,7 @@ def _stale_gaps(cov: dict, surface: dict | None, stale: object, scored: set[str]
|
|
|
1158
1264
|
"Medium", "stale-documentation", f"Stale documentation: {name}", doc or "SKILL.md",
|
|
1159
1265
|
f"Check `{name}` against the source and update or remove its documentation at "
|
|
1160
1266
|
f"{doc or 'SKILL.md'}: the enumerated source surface does not export it.",
|
|
1161
|
-
issue=f"
|
|
1267
|
+
issue=f"{_documents(doc, name)}, which the enumerated source API surface lacks",
|
|
1162
1268
|
export=name))
|
|
1163
1269
|
return records
|
|
1164
1270
|
|
|
@@ -1448,10 +1554,48 @@ def from_metadata_coherence(result: object, metadata: str | None = None) -> list
|
|
|
1448
1554
|
return records
|
|
1449
1555
|
|
|
1450
1556
|
|
|
1451
|
-
def
|
|
1557
|
+
def _aliased_items(stale: object) -> dict[str, dict]:
|
|
1558
|
+
"""name -> its classify-stale item (`--stale`) when it is `aliased` with a `file:line` `defined_at`: a rename
|
|
1559
|
+
elsewhere declares the name, so a provenance line gap of it names the alias. Raises AdapterError, as the
|
|
1560
|
+
coverage adapter does, when the result is not a list."""
|
|
1561
|
+
if stale is None:
|
|
1562
|
+
return {}
|
|
1563
|
+
if not isinstance(stale, list):
|
|
1564
|
+
raise AdapterError("--stale is not classify-stale JSON (a list)")
|
|
1565
|
+
return {i["name"]: i for i in stale
|
|
1566
|
+
if isinstance(i, dict) and isinstance(i.get("name"), str) and i.get("reason") == "aliased"
|
|
1567
|
+
and isinstance(i.get("defined_at"), str) and i["defined_at"].rpartition(":")[0]
|
|
1568
|
+
and i["defined_at"].rpartition(":")[2].isdigit()}
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
def _alias_fix(name: str, file: str, item: dict) -> str:
|
|
1572
|
+
"""The remediation of an unverified provenance line of a name classify-stale found `aliased` (`item`): record
|
|
1573
|
+
the rename if no other entry already has its `export_name`, else cite the alias. The map keys on
|
|
1574
|
+
`export_name`, so it holds one `default` entry at most (a barrel of `export { default as X }` lines renames
|
|
1575
|
+
several): a rename of `default` takes the alias alone. It never says which cited file declares the renamed
|
|
1576
|
+
name: any checked entry's may."""
|
|
1577
|
+
alias, local = item["defined_at"], item.get("renames")
|
|
1578
|
+
alias_file, _, alias_line = alias.rpartition(":")
|
|
1579
|
+
named = isinstance(local, str) and bool(local)
|
|
1580
|
+
renamed = f"`{local}`" if named else "a name a cited file declares"
|
|
1581
|
+
head = f"`{name}` is an alias: `{alias}` renames {renamed} to it, and no line of `{file}` defines `{name}`. "
|
|
1582
|
+
cite = (f"the alias: set `source_file` to `{alias_file}` and `source_line` to `{alias_line}`, and move the "
|
|
1583
|
+
f"citations of `{name}` there.")
|
|
1584
|
+
if local == "default":
|
|
1585
|
+
return f"{head}Cite {cite}"
|
|
1586
|
+
guard = f"`export_name` {renamed}" if named else "that name as its `export_name`"
|
|
1587
|
+
return (f"{head}If no other entry already has {guard}, record the rename in this provenance entry: set "
|
|
1588
|
+
f"`export_name` to {renamed}, add `reexported_as` `{name}`, cite the line that declares {renamed} and "
|
|
1589
|
+
f"move the citations of `{name}` to that line. Or cite {cite}")
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
def from_provenance_line(result: object, stale: object = None) -> list[dict]:
|
|
1452
1593
|
"""verify's `stale[]` items: provenance line gaps, one per citation that does not resolve or is not the
|
|
1453
|
-
definition line.
|
|
1594
|
+
definition line. `stale` (classify-stale's result, `--stale`, or None) gives the names a rename declares: an
|
|
1595
|
+
unverified line of one names the alias and, unless the name it renames is `default`, how to record the rename
|
|
1596
|
+
(#911)."""
|
|
1454
1597
|
result = _object(result, "--input (skf-verify-provenance-completeness.py verify)", "stale")
|
|
1598
|
+
aliased = _aliased_items(stale)
|
|
1455
1599
|
records: list[dict] = []
|
|
1456
1600
|
for item in _list(result, "stale", "--input"):
|
|
1457
1601
|
if not isinstance(item, dict):
|
|
@@ -1506,12 +1650,16 @@ def from_provenance_line(result: object) -> list[dict]:
|
|
|
1506
1650
|
f"it; the lines that define it are {lines}.{folded}",
|
|
1507
1651
|
export=name))
|
|
1508
1652
|
else:
|
|
1509
|
-
|
|
1510
|
-
|
|
1653
|
+
alias = aliased[name] if isinstance(name, str) and name in aliased else None
|
|
1654
|
+
fix = _alias_fix(name, file, alias) if alias else (
|
|
1511
1655
|
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
|
-
|
|
1514
|
-
|
|
1656
|
+
"the provenance `source_line` to it.")
|
|
1657
|
+
issue = (f"no line of `{file}` defines `{name}`: the source declares it only as the alias at "
|
|
1658
|
+
f"`{alias['defined_at']}`" if alias else
|
|
1659
|
+
f"the line-check rules found no definition line for `{name}` in `{file}`; check by hand")
|
|
1660
|
+
records.append(_gap(
|
|
1661
|
+
"Info", "provenance-unverified", f"Provenance line not verified for {name}", where, fix,
|
|
1662
|
+
issue=issue + (f".{folded}" if folded else ""),
|
|
1515
1663
|
export=name))
|
|
1516
1664
|
return records
|
|
1517
1665
|
|
|
@@ -1680,7 +1828,8 @@ def build_records(args: argparse.Namespace) -> list[dict]:
|
|
|
1680
1828
|
if kind == "metadata-coherence":
|
|
1681
1829
|
return from_metadata_coherence(data, args.metadata)
|
|
1682
1830
|
if kind == "provenance-line":
|
|
1683
|
-
|
|
1831
|
+
stale = read_result(args.stale, "--stale") if args.stale is not None else None
|
|
1832
|
+
return from_provenance_line(data, stale)
|
|
1684
1833
|
return from_coherence(data)
|
|
1685
1834
|
|
|
1686
1835
|
|
|
@@ -1897,7 +2046,8 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1897
2046
|
"the names outside scope)")
|
|
1898
2047
|
p.add_argument("--signatures", help="coverage: score-signatures.py score's result (missingTypes)")
|
|
1899
2048
|
p.add_argument("--numerator", help="coverage: verify-declared-numerator.py's result (absent names)")
|
|
1900
|
-
p.add_argument("--stale", help="coverage: classify-stale's result (fabricated signatures, documented extras)"
|
|
2049
|
+
p.add_argument("--stale", help="coverage: classify-stale's result (fabricated signatures, documented extras); "
|
|
2050
|
+
"provenance-line: the same (the names a rename declares)")
|
|
1901
2051
|
p.add_argument("--provenance", help="coverage: the provenance map (an absent name's Source)")
|
|
1902
2052
|
p.add_argument("--skill-dir", help="coverage: the skill package (a stale name's documenting line)")
|
|
1903
2053
|
p.add_argument("--metadata", help="the metadata.json path a record cites as its Source (not read)")
|