bmad-module-skill-forge 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +3 -3
- package/docs/_data/pinned.yaml +19 -13
- package/docs/architecture.md +2 -2
- package/docs/concepts.md +4 -4
- package/docs/examples.md +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/verifying-a-skill.md +41 -20
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +2 -2
- package/package.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 +881 -168
- 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 +6 -5
- package/src/shared/scripts/skf-extract-public-api.py +439 -107
- 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 +135 -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 +470 -38
- package/src/shared/tool-requirements.yaml +8 -2
- package/src/skf-analyze-source/SKILL.md +2 -2
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-audit-skill/SKILL.md +2 -2
- package/src/skf-audit-skill/references/re-index.md +2 -2
- package/src/skf-brief-skill/SKILL.md +2 -2
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-campaign/SKILL.md +2 -2
- package/src/skf-campaign/references/step-05-skill-loop.md +6 -1
- package/src/skf-create-skill/SKILL.md +2 -2
- package/src/skf-create-skill/assets/skill-sections.md +1 -1
- package/src/skf-create-skill/references/extract.md +2 -1
- package/src/skf-create-skill/references/extraction-patterns.md +3 -3
- package/src/skf-create-skill/references/report.md +6 -1
- package/src/skf-create-stack-skill/SKILL.md +2 -2
- package/src/skf-create-stack-skill/references/validate.md +1 -1
- package/src/skf-drop-skill/SKILL.md +2 -2
- package/src/skf-export-skill/SKILL.md +2 -2
- package/src/skf-export-skill/assets/snippet-format.md +5 -5
- package/src/skf-export-skill/references/generate-snippet.md +22 -15
- package/src/skf-export-skill/references/load-skill.md +1 -1
- package/src/skf-export-skill/references/token-report.md +29 -9
- package/src/skf-forger/SKILL.md +2 -2
- package/src/skf-quick-skill/SKILL.md +2 -2
- package/src/skf-refine-architecture/SKILL.md +6 -8
- package/src/skf-rename-skill/SKILL.md +2 -2
- package/src/skf-setup/SKILL.md +2 -2
- package/src/skf-test-skill/SKILL.md +2 -2
- package/src/skf-test-skill/assets/output-section-formats.md +6 -4
- package/src/skf-test-skill/references/coherence-check.md +4 -3
- package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
- package/src/skf-test-skill/references/coverage-check-tiers.md +1 -1
- package/src/skf-test-skill/references/coverage-check.md +45 -69
- package/src/skf-test-skill/references/init.md +2 -1
- package/src/skf-test-skill/references/migration-section-rules.md +26 -14
- package/src/skf-test-skill/references/report.md +2 -2
- package/src/skf-test-skill/references/scoring-rules.md +5 -4
- package/src/skf-test-skill/references/source-access-protocol.md +6 -6
- package/src/skf-test-skill/scripts/gap-ledger.py +378 -163
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +171 -70
- package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
- package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
- package/src/skf-test-skill/scripts/score-signatures.py +329 -54
- package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
- package/src/skf-test-skill/scripts/validate-inventory.py +108 -9
- package/src/skf-test-skill/templates/test-report-template.md +1 -0
- package/src/skf-update-skill/SKILL.md +2 -2
- package/src/skf-update-skill/references/detect-changes.md +1 -1
- package/src/skf-update-skill/references/gap-driven.md +36 -21
- 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 +5 -4
- package/src/skf-update-skill/references/write.md +39 -18
- package/src/skf-verify-stack/SKILL.md +2 -2
- package/tools/cli/lib/tool-check.js +158 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# /// script
|
|
3
3
|
# requires-python = ">=3.11"
|
|
4
|
-
# dependencies = []
|
|
4
|
+
# dependencies = ["pyyaml"]
|
|
5
5
|
# ///
|
|
6
6
|
"""Print the stdin payload of a shared SKF helper, read from the files on disk.
|
|
7
7
|
|
|
@@ -13,7 +13,7 @@ This script reads the files and prints the payload, and the step pipes it
|
|
|
13
13
|
into the helper:
|
|
14
14
|
|
|
15
15
|
uv run stage-helper-payload.py detect-workspaces --source-root <dir> \\
|
|
16
|
-
| uv run skf-detect-workspaces.py
|
|
16
|
+
[--brief <skill-brief.yaml>] | uv run skf-detect-workspaces.py
|
|
17
17
|
|
|
18
18
|
(The Quick-tier export scan needs no payload: skf-extract-public-api.py
|
|
19
19
|
--mode quick reads its files itself, with --manifest-file and --entry-file.)
|
|
@@ -24,28 +24,48 @@ detect-workspaces prints {"tree": [...], "manifests": {...}}:
|
|
|
24
24
|
it, sorted. Folders named in SKIPPED_DIRS (installed
|
|
25
25
|
dependencies, virtual environments, build output, caches) and
|
|
26
26
|
hidden folders (a name that starts with ".") are not read, and a
|
|
27
|
-
linked folder is not followed.
|
|
27
|
+
linked folder is not followed. With --brief, the tree is the
|
|
28
|
+
detector's scoped_tree of it under the brief's scope.exclude
|
|
29
|
+
globs (#780): a workspace member counts only through the files
|
|
30
|
+
the brief keeps, so excluding a member's source files drops it,
|
|
31
|
+
excluding only its manifest does not, every root manifest
|
|
32
|
+
stays, and a workspace with no member left is no workspace.
|
|
28
33
|
manifests the text of each file directly in --source-root that is not
|
|
29
34
|
hidden, holds at most MAX_ROOT_FILE_BYTES and reads as UTF-8, by
|
|
30
|
-
its name. The detector reads the
|
|
31
|
-
(package.json, pnpm-workspace.yaml,
|
|
32
|
-
and ignores the rest, so this script
|
|
33
|
-
names to fall out of step with the
|
|
35
|
+
its name, whatever the brief excludes. The detector reads the
|
|
36
|
+
root manifests it knows (package.json, pnpm-workspace.yaml,
|
|
37
|
+
lerna.json, Cargo.toml ...) and ignores the rest, so this script
|
|
38
|
+
keeps no list of their names to fall out of step with the
|
|
39
|
+
detector's.
|
|
40
|
+
|
|
41
|
+
--brief reads the brief with load-coverage-inputs.py's read_brief (the
|
|
42
|
+
script beside this one, which loads skf-resolve-authoritative-files.py's
|
|
43
|
+
parse_brief_yaml from the shared scripts folder), and matches each
|
|
44
|
+
scope.exclude glob by its glob_match, skf-resolve-authoritative-files.py's
|
|
45
|
+
rule (`**` spans any number of path segments, none included; `*` and `?`
|
|
46
|
+
stay inside one), the rule skf-extract-public-api.py applies to the same
|
|
47
|
+
brief. skf-detect-workspaces.py's scoped_tree, loaded from the shared
|
|
48
|
+
scripts folder (SHARED_SCRIPTS), keeps the paths: the monorepo arm of
|
|
49
|
+
skf-extract-public-api.py reads the same function.
|
|
34
50
|
|
|
35
51
|
The payload is one line of ASCII JSON on stdout, so a pipe carries the same
|
|
36
52
|
bytes on every platform.
|
|
37
53
|
|
|
38
54
|
Exit codes:
|
|
39
55
|
0 the payload is printed
|
|
40
|
-
1 --source-root is not a folder
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
1 --source-root is not a folder, or --brief cannot be read (missing, not
|
|
57
|
+
UTF-8, not YAML, a value YAML cannot build such as an invalid date, not
|
|
58
|
+
a mapping, PyYAML not installed) or a script it loads is missing; one
|
|
59
|
+
JSON line {"status": "error", "error": ...} goes to stderr and nothing
|
|
60
|
+
to stdout, so the helper the payload was piped into reads no input and
|
|
61
|
+
fails too
|
|
43
62
|
2 bad arguments (argparse: a missing or unknown argument, usage on stderr)
|
|
44
63
|
"""
|
|
45
64
|
|
|
46
65
|
from __future__ import annotations
|
|
47
66
|
|
|
48
67
|
import argparse
|
|
68
|
+
import importlib.util
|
|
49
69
|
import json
|
|
50
70
|
import os
|
|
51
71
|
import sys
|
|
@@ -57,6 +77,9 @@ SKIPPED_DIRS = frozenset({
|
|
|
57
77
|
"venv", "site-packages", "__pycache__", "target",
|
|
58
78
|
})
|
|
59
79
|
MAX_ROOT_FILE_BYTES = 1024 * 1024
|
|
80
|
+
SCRIPTS = Path(__file__).resolve().parent
|
|
81
|
+
# skf-detect-workspaces.py's folder, installed and in src/ alike.
|
|
82
|
+
SHARED_SCRIPTS = SCRIPTS.parent.parent / "shared" / "scripts"
|
|
60
83
|
|
|
61
84
|
|
|
62
85
|
class StageError(Exception):
|
|
@@ -70,6 +93,29 @@ def _source_root(value: str) -> Path:
|
|
|
70
93
|
return root
|
|
71
94
|
|
|
72
95
|
|
|
96
|
+
def _load(folder: Path, filename: str):
|
|
97
|
+
"""A script in `folder`, loaded as a module, as load-coverage-inputs.py loads its siblings."""
|
|
98
|
+
path = folder / filename
|
|
99
|
+
spec = importlib.util.spec_from_file_location("skf_" + filename[:-3].replace("-", "_"), path)
|
|
100
|
+
if spec is None or spec.loader is None or not path.is_file():
|
|
101
|
+
raise StageError(f"{filename} not found in {folder}")
|
|
102
|
+
module = importlib.util.module_from_spec(spec)
|
|
103
|
+
spec.loader.exec_module(module)
|
|
104
|
+
return module
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def scoped_tree(tree: list[str], brief: str) -> list[str]:
|
|
108
|
+
"""The paths of `tree` workspace detection reads under the brief at `brief`."""
|
|
109
|
+
inputs = _load(SCRIPTS, "load-coverage-inputs.py")
|
|
110
|
+
try:
|
|
111
|
+
excludes = inputs.read_brief(brief)["scope"]["exclude"]
|
|
112
|
+
except inputs.InputError as exc:
|
|
113
|
+
raise StageError(str(exc)) from exc
|
|
114
|
+
except ValueError as exc: # a value YAML cannot build, such as an invalid date
|
|
115
|
+
raise StageError(f"--brief {brief} is not valid YAML: {' '.join(str(exc).split())}") from exc
|
|
116
|
+
return _load(SHARED_SCRIPTS, "skf-detect-workspaces.py").scoped_tree(tree, excludes, inputs.glob_match)
|
|
117
|
+
|
|
118
|
+
|
|
73
119
|
# --------------------------------------------------------------------------
|
|
74
120
|
# detect-workspaces
|
|
75
121
|
# --------------------------------------------------------------------------
|
|
@@ -105,9 +151,13 @@ def root_texts(root: Path) -> dict[str, str]:
|
|
|
105
151
|
return texts
|
|
106
152
|
|
|
107
153
|
|
|
108
|
-
def workspaces_payload(source_root: str) -> dict:
|
|
154
|
+
def workspaces_payload(source_root: str, brief: str | None = None) -> dict:
|
|
155
|
+
"""The tree (with `brief`, the part of it the brief keeps) and the root files."""
|
|
109
156
|
root = _source_root(source_root)
|
|
110
|
-
|
|
157
|
+
tree = list_tree(root)
|
|
158
|
+
if brief is not None:
|
|
159
|
+
tree = scoped_tree(tree, brief)
|
|
160
|
+
return {"tree": tree, "manifests": root_texts(root)}
|
|
111
161
|
|
|
112
162
|
|
|
113
163
|
# --------------------------------------------------------------------------
|
|
@@ -116,7 +166,7 @@ def workspaces_payload(source_root: str) -> dict:
|
|
|
116
166
|
|
|
117
167
|
|
|
118
168
|
def _cmd_detect_workspaces(args: argparse.Namespace) -> dict:
|
|
119
|
-
return workspaces_payload(args.source_root)
|
|
169
|
+
return workspaces_payload(args.source_root, args.brief)
|
|
120
170
|
|
|
121
171
|
|
|
122
172
|
def _build_parser() -> argparse.ArgumentParser:
|
|
@@ -128,6 +178,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
128
178
|
|
|
129
179
|
p = sub.add_parser("detect-workspaces", help="the tree and root files for skf-detect-workspaces.py")
|
|
130
180
|
p.add_argument("--source-root", required=True, help="the local source folder")
|
|
181
|
+
p.add_argument("--brief", help="the skill brief, whose scope.exclude globs leave paths out of the tree")
|
|
131
182
|
p.set_defaults(func=_cmd_detect_workspaces)
|
|
132
183
|
|
|
133
184
|
return parser
|
|
@@ -10,7 +10,8 @@ trust subagent output blindly: before any downstream step consumes the
|
|
|
10
10
|
documented inventory, the shape is validated. That validation is pure
|
|
11
11
|
structural checking — strip wrapping fences, parse JSON, assert required keys
|
|
12
12
|
and types, assert each export entry is a dict with a non-empty `name` and a
|
|
13
|
-
`kind` drawn from a fixed enum
|
|
13
|
+
`kind` drawn from a fixed enum (and, when present, a `signature_form` of
|
|
14
|
+
`partial` or `full`), and assert each cross-check mismatch carries its
|
|
14
15
|
five fields — with exactly one correct pass/fail per input and no
|
|
15
16
|
interpretation of meaning. It is therefore script work, not prompt work; the
|
|
16
17
|
prompt reads this verdict and owns only the HALT decision.
|
|
@@ -26,6 +27,15 @@ violation, and the prompt re-dispatches the subagent with them, as it does
|
|
|
26
27
|
for a schema violation. No grep runs in the prompt, so an export named
|
|
27
28
|
`$state` or `a.b` is never a shell variable or a regex.
|
|
28
29
|
|
|
30
|
+
An entry's optional `signature_form` says how the skill presents its
|
|
31
|
+
signature: `partial` only when the skill labels the form partial (a
|
|
32
|
+
simplified form, a key-parameter summary, a `...` that stands in for
|
|
33
|
+
omitted members, not rest or variadic syntax), `full` otherwise.
|
|
34
|
+
A JSON null counts as absent, and an absent one as `full`. Any other value
|
|
35
|
+
is a violation naming the entry. score-signatures.py reads it: an optional
|
|
36
|
+
member a partial form leaves out is an Info observation, not a wrong
|
|
37
|
+
signature.
|
|
38
|
+
|
|
29
39
|
Distinct from reconcile-coverage.py (coverage numerator arithmetic) and
|
|
30
40
|
compute-score.py (weight tables): this script only validates the *shape* of the
|
|
31
41
|
subagent inventory and, on success, echoes the fence-stripped parsed inventory
|
|
@@ -50,6 +60,14 @@ skf-scan-skill-md-structure.py usage-scope): on a side where the name ends
|
|
|
50
60
|
in a letter, digit, `_` or `$`, the next character may not be one of those.
|
|
51
61
|
So `get` is found in `get(url)` but not in `target` or `getAll`.
|
|
52
62
|
|
|
63
|
+
The skill-citation scan is shared too: `_skill_cited_files` (with
|
|
64
|
+
`_cited_path`, `skill_markdown_files` and `_SKILL_CITATION_RE`) gives the
|
|
65
|
+
files the skill's `[AST:]` and `[SRC:]` citations name on a line of SKILL.md
|
|
66
|
+
or references/**/*.md that writes a name with the match above, or on the
|
|
67
|
+
next non-blank line. gap-ledger.py reads it to pick the declaration of a
|
|
68
|
+
stale homonym, score-signatures.py to plan a signature check at the
|
|
69
|
+
declaration the skill cites.
|
|
70
|
+
|
|
53
71
|
CLI usage (mirrors reconcile-coverage.py):
|
|
54
72
|
uv run validate-inventory.py --input <response file> [--output <inventory file>]
|
|
55
73
|
[--skill-package <package folder>]
|
|
@@ -57,13 +75,16 @@ CLI usage (mirrors reconcile-coverage.py):
|
|
|
57
75
|
uv run validate-inventory.py --json-input '<raw response>' # explicit flag
|
|
58
76
|
echo '<raw response>' | uv run validate-inventory.py --stdin # piped input
|
|
59
77
|
|
|
60
|
-
--input reads the raw response from a file (UTF-8): coverage-check.md
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
--input reads the raw response from a file (UTF-8): the coverage-check.md §1
|
|
79
|
+
subagent writes its response to the path it is given with the Write tool,
|
|
80
|
+
and the parent saves it there with the Write tool when the subagent returned
|
|
81
|
+
it instead or left the file absent or empty. coverage-check.md passes that
|
|
82
|
+
path, so a quote or an apostrophe in a description never meets a shell.
|
|
83
|
+
--output writes the validated inventory object to a file (UTF-8
|
|
84
|
+
JSON) when it is valid, for the steps that read it by path; an invalid
|
|
85
|
+
inventory removes a file left there by an earlier attempt. --skill-package
|
|
86
|
+
names the skill package under test (the folder that holds SKILL.md) and
|
|
87
|
+
adds the spot-check.
|
|
67
88
|
|
|
68
89
|
Input: the subagent's RAW response text (JSON, optionally wrapped in a markdown
|
|
69
90
|
code fence — a leading line of three backticks with an optional language tag and
|
|
@@ -73,7 +94,7 @@ Output (stdout, one object):
|
|
|
73
94
|
{
|
|
74
95
|
"valid": <bool>, # true only when parse + every schema check pass
|
|
75
96
|
"violations": [<str>, ...], # human-readable failures; empty when valid
|
|
76
|
-
"rejectedCount": <int>, #
|
|
97
|
+
"rejectedCount": <int>, # exports[] entries with no valid name or kind
|
|
77
98
|
"exportsCount": <int>, # len(exports) when parsed; 0 otherwise
|
|
78
99
|
"inventory": {...} | null, # fence-stripped parsed inventory when valid; null otherwise
|
|
79
100
|
"completeness": { # present when valid
|
|
@@ -102,6 +123,7 @@ from __future__ import annotations
|
|
|
102
123
|
import argparse
|
|
103
124
|
import json
|
|
104
125
|
import os
|
|
126
|
+
import posixpath
|
|
105
127
|
import re
|
|
106
128
|
import sys
|
|
107
129
|
from pathlib import Path
|
|
@@ -126,6 +148,10 @@ VALID_KINDS = (
|
|
|
126
148
|
"adapter",
|
|
127
149
|
)
|
|
128
150
|
|
|
151
|
+
# The optional exports[].signature_form: `partial` only when the skill labels
|
|
152
|
+
# the documented form partial, else `full` (or the key left out).
|
|
153
|
+
SIGNATURE_FORMS = ("partial", "full")
|
|
154
|
+
|
|
129
155
|
REQUIRED_MISMATCH_FIELDS = (
|
|
130
156
|
"export",
|
|
131
157
|
"skill_md_line",
|
|
@@ -208,6 +234,69 @@ def names_present(names, doc_text):
|
|
|
208
234
|
return sorted(present)
|
|
209
235
|
|
|
210
236
|
|
|
237
|
+
# --- Skill citations ----------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
# A source citation the skill writes, `[AST:path:L12]`, `[SRC:path:L12-34]` or `[SRC:path:L12-L34]` (one with no
|
|
240
|
+
# `:L<n>` part is ignored). Groups: prefix, path, first line, the `L` of a range's end, the range's end.
|
|
241
|
+
# Keep identical to _SKILL_CITATION_RE in skf-verify-provenance-completeness.py.
|
|
242
|
+
_SKILL_CITATION_RE = re.compile(
|
|
243
|
+
r"\[(AST|SRC):([^\[\]\n]+?):L(\d+)(?:-(L?)(\d+))?\]"
|
|
244
|
+
)
|
|
245
|
+
# Any bracketed citation (`[AST:...]`, `[SRC:...]`, `[EXT:...]`): a line names a documented name outside them.
|
|
246
|
+
_ANY_CITATION_RE = re.compile(r"\[[A-Z]+:[^\[\]\n]*\]")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _cited_path(path: str) -> str:
|
|
250
|
+
"""A cited, `declared_in` or surface path as each compares: forward slashes, no `./`."""
|
|
251
|
+
path = path.strip().replace("\\", "/")
|
|
252
|
+
while path.startswith("./"):
|
|
253
|
+
path = path[2:]
|
|
254
|
+
return posixpath.normpath(path) if path else path
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def skill_markdown_files(skill_dir: Path) -> list[Path]:
|
|
258
|
+
"""`SKILL.md` then every `references/**/*.md`, in a stable order.
|
|
259
|
+
Keep identical to skill_markdown_files in skf-verify-provenance-completeness.py."""
|
|
260
|
+
files: list[Path] = []
|
|
261
|
+
skill_md = skill_dir / "SKILL.md"
|
|
262
|
+
if skill_md.is_file():
|
|
263
|
+
files.append(skill_md)
|
|
264
|
+
refs = skill_dir / "references"
|
|
265
|
+
if refs.is_dir():
|
|
266
|
+
files.extend(sorted(p for p in refs.rglob("*.md") if p.is_file()))
|
|
267
|
+
return files
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def skill_markdown_lines(skill_dir: Path) -> list[list[str]]:
|
|
271
|
+
"""The lines of each file skill_markdown_files lists (an unreadable one is
|
|
272
|
+
left out), read once for every name _skill_cited_files looks up."""
|
|
273
|
+
texts: list[list[str]] = []
|
|
274
|
+
for path in skill_markdown_files(skill_dir):
|
|
275
|
+
try:
|
|
276
|
+
texts.append(path.read_bytes().decode("utf-8-sig", errors="replace").split("\n"))
|
|
277
|
+
except OSError:
|
|
278
|
+
continue
|
|
279
|
+
return texts
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _skill_cited_files(skill_dir: Path, name: str, texts: list[list[str]] | None = None) -> set[str]:
|
|
283
|
+
"""The files the skill's `[AST:]` and `[SRC:]` citations name on a line of
|
|
284
|
+
SKILL.md or references/**/*.md that writes `name` outside its citations
|
|
285
|
+
(the documented-name match), or on the next non-blank line (a heading,
|
|
286
|
+
then its citation). `texts`, skill_markdown_lines(skill_dir) read once,
|
|
287
|
+
spares a caller that looks up many names reading the files again."""
|
|
288
|
+
pattern = name_pattern(name)
|
|
289
|
+
cited: set[str] = set()
|
|
290
|
+
for lines in skill_markdown_lines(skill_dir) if texts is None else texts:
|
|
291
|
+
for number, line in enumerate(lines):
|
|
292
|
+
if not pattern.search(_ANY_CITATION_RE.sub(" ", line)):
|
|
293
|
+
continue
|
|
294
|
+
following = next((ln for ln in lines[number + 1:] if ln.strip()), "")
|
|
295
|
+
for cited_line in (line, following):
|
|
296
|
+
cited.update(_cited_path(m.group(2)) for m in _SKILL_CITATION_RE.finditer(cited_line))
|
|
297
|
+
return cited
|
|
298
|
+
|
|
299
|
+
|
|
211
300
|
# --- Spot-check -------------------------------------------------------------
|
|
212
301
|
|
|
213
302
|
|
|
@@ -392,6 +481,16 @@ def validate_inventory(raw, skill_package_path=None):
|
|
|
392
481
|
f"{{{', '.join(VALID_KINDS)}}})"
|
|
393
482
|
)
|
|
394
483
|
|
|
484
|
+
# The optional signature_form names one of two forms, or is left out.
|
|
485
|
+
for idx, entry in enumerate(exports):
|
|
486
|
+
if isinstance(entry, dict) and entry.get("signature_form") is not None \
|
|
487
|
+
and entry["signature_form"] not in SIGNATURE_FORMS:
|
|
488
|
+
label = f" (`{entry['name']}`)" if isinstance(entry.get("name"), str) and entry["name"] else ""
|
|
489
|
+
violations.append(
|
|
490
|
+
f"exports[{idx}]{label}: `signature_form` must be `partial` or `full` when present, "
|
|
491
|
+
f"not {json.dumps(entry['signature_form'])}"
|
|
492
|
+
)
|
|
493
|
+
|
|
395
494
|
# Non-empty cross-check mismatch entries must carry all five fields.
|
|
396
495
|
for idx, entry in enumerate(cross):
|
|
397
496
|
if not isinstance(entry, dict):
|
|
@@ -79,10 +79,10 @@ Headless callers: the inputs, flags, gate map, outputs and the `SKF_UPDATE_RESUL
|
|
|
79
79
|
SKF_JSON
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
4. **Resolve workflow customization.**
|
|
82
|
+
4. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
|
-
uv run {
|
|
85
|
+
uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-update-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Record the warning in the run log at once: write `customization_resolver_unavailable: <reason>` to `{run_dir}/resolver-warning.txt` with a file write (a resolver error can hold quotes or `$( )`, so never echo it or type it into an argument), then run `uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warning "$(cat "{run_dir}/resolver-warning.txt")"` from `{project-root}`.
|
|
@@ -313,7 +313,7 @@ Pass `--provenance-map` whenever init.md §4 found one; `--brief` (as §1c left
|
|
|
313
313
|
|
|
314
314
|
Pass `--language` when metadata.json records it, and `--scope-type` when §1 bound `{scope_type}`. It takes no `--brief`: Category A applied the scope, and a tracked file the brief's scope leaves out (gap-driven.md §1's rule R1 excludes a rescoped export's file) keeps its exports. `--head-cap 0` keeps every match, since a dropped one would read as a deleted export. Step 3 reads this file and never runs the runner again. **Exit 1** (`incomplete`, its JSON written): run it once more; when still incomplete, step 2 below reads by eye each listed file with no export in `exports[]` or that an `errors[]` item lists in `unread`, and tells the user which files were read by eye. **Exit 2** (an input error, such as a file list it cannot read, or a failure it did not foresee, named in one stderr line with no JSON; `uv` failing to start it exits 2 too): HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-b"`, the stderr line that holds `error:` as `reason`). No by-eye read stands in for it: the skill is not written yet, and reading every file by eye would hide the error. **Exit 3 (`no-ast-grep`), no JSON on another exit, no candidate resolves, or Quick tier:** write `{"exports": []}` to `{run_dir}/extraction.json`, with `"scope": {"type": "{scope_type}"}` when §1 bound `{scope_type}`, so step 5's `apply` can warn that a `public-api` surface was not applied; step 2 reads every listed file by eye (at Quick tier, by text pattern). Tell the user which files were read by eye.
|
|
315
315
|
|
|
316
|
-
Then, at every tier (in degraded mode there is no map: skip it), list the map's entries the runner left out that their file still declares: the runner never reports a module, an alias that renames, a dunder such as `__version__` or another underscore name, and step 3's diff would read each as a deleted export. From `{project-root}`, run:
|
|
316
|
+
Then, at every tier (in degraded mode there is no map: skip it), list the map's entries the runner left out that their file still declares: the runner never reports a module, an alias that renames, a dunder such as `__version__` or another Python or Rust underscore name, and step 3's diff would read each as a deleted export. From `{project-root}`, run:
|
|
317
317
|
|
|
318
318
|
```bash
|
|
319
319
|
uv run {buildChangeManifestHelper} baseline-gaps \
|
|
@@ -82,20 +82,33 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
|
|
|
82
82
|
- **Exit 0:** bullet 3 reads `{run_dir}/gaps.json` and passes each of its `warnings[]` on as `test-report: <entry>`.
|
|
83
83
|
- **Exit 1** (`status: "error"` in `{run_dir}/gaps.json`, with `code` `REPORT_MISSING`, `LEDGER_MISSING`, `LEDGER_INVALID` or `INVALID_INPUT`), **any other exit, no JSON, or no candidate resolves:** HALT with status `blocked` and display "**The test report's gaps could not be read:** {code}: {error}" ("skf-parse-gaps.py is missing; re-install SKF" when no candidate resolved). The halt procedure takes `phase: "detect-changes:parse-gaps"`, `path: "{test_report_path}"`, `reason: "{code}: {error}"`.
|
|
84
84
|
|
|
85
|
-
2. **Translate each gap by its `category`** (the gap ledger's closed set of slugs), never by its severity: bullet 3's helper routes each one through the table below. A report older than the ledger gives no `category`: when bullet 3 asks, choose the row whose gap type its title and issue describe.
|
|
85
|
+
2. **Translate each gap by its `category`** (the gap ledger's closed set of slugs), never by its severity: bullet 3's helper routes each one through the table below. A report older than the ledger gives no `category`: when bullet 3 asks, choose the row whose gap type its title and issue describe, or the Not routed slug that names it.
|
|
86
86
|
|
|
87
87
|
| `category` | Gap type (a report older than the ledger) | Change Category |
|
|
88
88
|
|------------|-------------------------------------------|-----------------|
|
|
89
89
|
| `missing-export`, `missing-type` | Missing export, type or interface documentation | NEW_EXPORT (an undocumented public export or type), unless the remediation says the export is internal or out of scope: then DELETED_EXPORT (rescope), see rule R1 |
|
|
90
90
|
| `split-body-mismatch` | Split-body inconsistency: the SKILL.md body and a `references/*.md` file document one export differently, with a `Source:` inside the skill package | STRUCTURAL_FIX, see rule R2 (for an older report, this row takes precedence over the Signature mismatch row below) |
|
|
91
|
-
| `signature-mismatch`, `fabricated-signature
|
|
91
|
+
| `signature-mismatch`, `fabricated-signature` | Signature mismatch | MODIFIED_EXPORT (the documentation needs updating from source) |
|
|
92
92
|
| `structural`, `broken-reference`, `inaccurate-reference`, `reference-escape`, `integration-pattern`, `migration-section`, `scripts-assets` | Structural or coherence drift in the output files only, no source change | STRUCTURAL_FIX, see rule R2 |
|
|
93
93
|
| `provenance-completeness` | Export documented in SKILL.md/references but **missing from the provenance-map** | NEW_EXPORT (provenance-completeness), see rule R3 |
|
|
94
94
|
| `provenance-line` | Provenance line is not the export's definition | MOVED_EXPORT (provenance line), see rule R5 |
|
|
95
|
-
| `metadata
|
|
95
|
+
| `metadata` | Missing metadata or examples | metadata update, see rule R4 |
|
|
96
96
|
| any other | any other gap | not routed |
|
|
97
97
|
|
|
98
|
-
**Not routed:** update-skill has no repair for the other categories.
|
|
98
|
+
**Not routed:** update-skill has no repair for the other categories. Such a gap takes no manifest entry, so this run writes nothing for it: no provenance entry, no `exports[]` name and no documentation removed. Bullet 3's helper lists it with the reason below, which says who repairs it. A gap with no category, or one no table holds, takes "update-skill has no repair for this category, so a person follows the gap's remediation", and one whose export is answered null "it names no export for update-skill to repair, so a person follows the gap's remediation".
|
|
99
|
+
|
|
100
|
+
- `stale-documentation` (the skill documents a name the source's enumerated surface lacks and that is not a documented extra, an Info `observation`): the source has no line to repair the name from, so a person removes its documentation when the name is gone from the source, fixes the brief's scope when the surface missed it, or, when the source still declares the name, keeps the documentation and re-runs Test Skill at a tier that classifies it.
|
|
101
|
+
- `metadata-drift` (divergent export counts in the metadata): update-skill recounts `stats.exports_public_api`, `stats.exports_documented` and `stats.effective_denominator` each time it writes metadata.json, so a metadata patch to them does not hold (under `--allow-workspace-drift` and for a docs-only skill it keeps the recorded public API counts and `stats.effective_denominator` instead); Create Skill rebuilds diverging barrel counts from the source and recounts a `stats.effective_denominator` the brief's `scope.tier_a_include` set disagrees with (denominator drift), a person fixes denominator deflation on a brief with no tier A list by adding `scope.tier_a_include` to it, and documented-surface counts that differ by the `Type::method` entries the named-export count leaves out need no fix.
|
|
102
|
+
- `provenance-unverified`: the line-check rules could not verify the line, so a person checks by hand that it defines the export and, when another line does, sets its provenance `source_line` to that line.
|
|
103
|
+
- `denominator-inflation`: it concerns the brief's scope and the count design, which a person decides in the skill brief, as the gap's remediation says.
|
|
104
|
+
- `brief-scope-stale`: a person edits the brief's scope globs by hand so each matches the files it meant, since update-skill does not rewrite them.
|
|
105
|
+
- `multi-denominator`: no fix is needed, since the barrel and the documented surface measure different sets by design and the report records both counts.
|
|
106
|
+
- `numerator-inflation`: a person documents the absent exports in SKILL.md or references/, reading each from the source.
|
|
107
|
+
- `scripts-assets-provenance`: a person adds the file's row to the provenance map's `file_entries`, or removes the file from the skill.
|
|
108
|
+
- `observation`: a person applies it only when they agree, since it is advisory, and a documented extra needs no change.
|
|
109
|
+
- `discovery`: a person improves the skill's description so realistic prompts route to it, or runs the discovery test Test Skill skipped.
|
|
110
|
+
- `description`: a person edits the description in the SKILL.md frontmatter as the gap suggests.
|
|
111
|
+
- `external-validator`: a person applies the skill-check diagnostic or Tessl Review suggestion to SKILL.md.
|
|
99
112
|
|
|
100
113
|
**Translation rules (referenced by the table above):**
|
|
101
114
|
|
|
@@ -110,9 +123,9 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
|
|
|
110
123
|
A rescope is honest only if the reduction is expressed in the brief's scope: on `true`, bullet 3's helper will give the manifest entry a `rescope` object holding the `scope.amendments[]` entry (`category: "scope-expansion"`, `action: "excluded"`, `path`, `reason` (the remediation), `date`, `workflow: "skf-update-skill"`) **and** the export's source path for `brief.scope.exclude`, then route the entry to merge Priority 1 (removal) so steps 4 and 5 remove it and recompute stats from the amended scope. This step writes neither to the brief: step 4 §6b writes both, the first write of the repair, and skips an amendment or an exclude path the brief already holds, so a re-run never adds them twice and a run that stops before step 4 (a halt, `--dry-run`, `--detect-only`) leaves the brief as it was. In those read-only modes its `--read-only` adds `proposed-amendment: excluded {path} (scope-expansion); not written: {--dry-run or --detect-only}` to its `warnings[]`. Never close a coverage gap by editing `metadata.stats` to equal the documented count: that is denominator deflation and `skf-test-skill` will reject it.
|
|
111
124
|
- **R2: STRUCTURAL_FIX.** A finding whose fix edits the generated markdown only, with no source change and no provenance entry change. It carries `remediation` text describing the surgical markdown edit and routes to merge Priority 8 (generated-markdown edit only); it never adds, modifies, or removes a provenance `entries[]` row. Two kinds of finding qualify:
|
|
112
125
|
- a coherence finding from `skf-scan-skill-md-structure.py` (e.g., `table_drift`, `unbalanced_fences`, a broken intra-skill anchor) that touches the generated output file only;
|
|
113
|
-
- a split-body consistency finding (test-skill coverage-check §1b `cross_check_mismatches`): the SKILL.md body and a `references/*.md` file document one export differently. test-skill rates it High, as it rates a signature mismatch against the source, yet it needs nothing from the source:
|
|
114
|
-
- **R3: provenance-completeness.** An export documented in SKILL.md/`references/` but absent from the provenance-map. These are documented-and-known, so `unknown` is never correct: route to §4a's targeted re-extraction **regardless of severity**, gated only on `source_root` being pinned and readable (§4).
|
|
115
|
-
- **R4: metadata update.** A metadata
|
|
126
|
+
- a split-body consistency finding (test-skill coverage-check §1b `cross_check_mismatches`): the SKILL.md body and a `references/*.md` file document one export differently. test-skill rates it High, as it rates a signature mismatch against the source, yet it needs nothing from the source: its `Source:` names the side the pinned source contradicts (test-skill coverage-check §5b), which bullet 3's helper keeps as the entry's `package_citation` when the ledger recorded its category, and merge Priority 8 says which file it edits. Its `category` is `split-body-mismatch`; in a report older than the ledger, recognize it by its `Source:`, which points inside the skill package (at the skill's own `SKILL.md` or at one of its `references/*.md` files, whether the path is written relative to the package or through `{skill_package}`), while its issue sets the SKILL.md body against a `references/*.md` file. Test for it before the High "Signature mismatch" row: a High signature gap whose `Source:` points into the source tree, or that has no `Source:`, stays `MODIFIED_EXPORT`.
|
|
127
|
+
- **R3: provenance-completeness.** An export documented in SKILL.md/`references/` but absent from the provenance-map. These are documented-and-known, so `unknown` is never correct: route to §4a's targeted re-extraction **regardless of severity**, gated only on `source_root` being pinned and readable (§4). The one exception: when the map holds the name at another file than the gap cites (`map_match` `not-found` with a candidate that has a `source_file`), its provenance is not absent, so §4 records it `unknown` (`cited-elsewhere`) and never re-extracts it (§4 bullet 1).
|
|
128
|
+
- **R4: metadata update.** A `metadata` gap's patch: it changes no export's source and sets a field write.md §2's `metadata` call keeps (e.g., missing optional metadata), never a count that call recounts or `exports[]`, which is why a `metadata-drift` gap is not routed. Add no `effective_denominator_*` stats key: the recount drops each one when the denominator changes. Routes to merge Priority 8b and is applied by write.md §2 *before* the automatic stat recount (see merge.md §3 / write.md §2).
|
|
116
129
|
- **R5: MOVED_EXPORT (provenance line).** A `provenance-line` gap (in a report older than the ledger, a Low gap titled `Provenance line is not the definition of {export_name}`, test-skill coverage-check §4c) says the provenance map's `source_line` for that export is not the line that defines it. Its `Source:` is the map's `{source_file}:{source_line}`, so `source_citation` is set, and its remediation lists the file's definition lines. This row takes precedence over the Low `metadata update` row (R4). Route the entry to §4's spot-check only, with no public-reachability gate and no §4a.
|
|
117
130
|
|
|
118
131
|
3. **Build the change manifest through `{parseGapsHelper}`**, never by hand. From `{project-root}`, run:
|
|
@@ -129,16 +142,16 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
|
|
|
129
142
|
Pass `--judgments` once the file exists, and `--read-only` with the run's flag in a `--detect-only` or `--dry-run` run. It routes each gap by the table and rules R1 to R5 and writes `{run_dir}/change-manifest.json`, where §4, merge and step 5 read it, as `{"mode": "gap-driven", "entries": [...]}`, each entry with the fields the "Translate" section of its docstring lists: §4 partitions on its `change_category` and merge.md §3 dispatches on it. Act on its `status`:
|
|
130
143
|
|
|
131
144
|
- **`needs-judgment`** (it wrote nothing): for each `needs_judgment[]` item, judge from its gap's `title`, `issue` and `remediation`, which the item carries, add your answer to each of its `needs` to `{run_dir}/gap-judgments.json` under the item's `gap_id` (`{"<gap_id>": {"<need>": <answer>}}`), then run it again:
|
|
132
|
-
- `category` (a report older than the ledger): the slug of the row bullet 2 says the gap's title and issue describe (`missing-export` or `missing-type` for a missing export or type), or null for a gap
|
|
145
|
+
- `category` (a report older than the ledger): the slug of the row bullet 2 says the gap's title and issue describe (`missing-export` or `missing-type` for a missing export or type), else the Not routed slug that names it (`stale-documentation` for documentation of a name the source surface lacks, `metadata-drift` for divergent export counts in the metadata), so the gap takes that category's reason, or null for a gap neither names;
|
|
133
146
|
- `name`: the export the gap's title names, or null when it names none, which leaves the gap unrouted;
|
|
134
147
|
- `rescope`: rule R1's answer, true or false;
|
|
135
148
|
- `source_file`: the source file, relative to `{source_root}`, that defines the export (a rescope's exclude path) or that a rule R3 gap's documentation cites, or null.
|
|
136
|
-
- **`written`:** set `gap_count` from its `gap_count`, keep its `not_routed[]` (the `id`, `title` and `
|
|
149
|
+
- **`written`:** set `gap_count` from its `gap_count`, keep its `not_routed[]` (the `id`, `title`, `category` and `reason` of each gap the table does not route, the reason saying why update-skill does not repair it and who does) for §2, and add each of its `warnings[]` to `warnings[]`: the parse's, as `test-report: <entry>`, `test-report: not routed: {id} ({category}): {reason}` for each gap not routed, and a read-only run's proposed amendments. When you answer `rescope` for a gap it did not ask about (rule R1: an export you found upstream internal or `#[doc(hidden)]`), add the answer to `{run_dir}/gap-judgments.json` and run it again before §2, taking that run's output in place of this one.
|
|
137
150
|
- **Exit 1** (`status: "error"`) **or no JSON:** HALT with status `blocked` and display "**The test report's gaps could not be translated:** {code}: {error}". The halt procedure takes `phase: "detect-changes:parse-gaps"`, `path: "{test_report_path}"`, `reason: "{code}: {error}"`.
|
|
138
151
|
|
|
139
152
|
### 2. Display the Gap Summary and Route
|
|
140
153
|
|
|
141
|
-
Display "**Gap-driven update mode.** Translated {gap_count} test report findings into the change manifest; source drift detection skipped.", then list each gap §1 did not route, as `{id}: {title} ({category})`, and say this run does not repair them. Then take the branch that applies:
|
|
154
|
+
Display "**Gap-driven update mode.** Translated {gap_count} test report findings into the change manifest; source drift detection skipped.", then list each gap §1 did not route, as `{id}: {title} ({category}): {reason}`, and say this run does not repair them. Then take the branch that applies:
|
|
142
155
|
|
|
143
156
|
- **`detect_only_mode == true`** → display "**Detect-only mode: skipping re-extract, merge and write.** Loading report..." and load, read the full file, then execute `{reportFile}` (report.md), which emits status `detect-only`.
|
|
144
157
|
- **No gap translated** (`gap_count` is 0, an empty manifest) → load, read the full file, then execute `{reportFile}` (report.md), which emits status `no-changes`.
|
|
@@ -182,8 +195,8 @@ Bind `{workspace_drift_status}` ← `status` and `{head_short_sha}` ← `head_sh
|
|
|
182
195
|
|
|
183
196
|
- `a public API recount from the tree (rule R1)`: a rescope (`DELETED_EXPORT`);
|
|
184
197
|
- `a line from the tree (rule R3)`: a provenance-completeness gap (`provenance_completeness: true`), which §4 bullet 1 routes to §4a;
|
|
185
|
-
- `a line and a signature from the tree`: an export whose `map_match` is `not-found`;
|
|
186
|
-
- `a signature from the tree`: an export the map holds (`found` or `ambiguous`).
|
|
198
|
+
- `a line and a signature from the tree`: an export whose `map_match` is `not-found` with no candidate that has a `source_file`;
|
|
199
|
+
- `a signature from the tree`: an export the map holds (`found` or `ambiguous`, or `not-found` with a candidate that has a `source_file`: a citation of another file than the map's entries of its name).
|
|
187
200
|
|
|
188
201
|
When its `status` is not `drift-blocked`, go on with its output in §4. Otherwise HALT with status `halted-for-workspace-drift` before merge runs, so merge writes nothing and §4a never runs. A rescope's amendment is not in the skill brief yet (rule R1): step 4 §6b writes it, so this halt leaves the brief as it was, and a re-run asks again. Display, with `{source_commit}` and `{source_ref}` read from metadata.json:
|
|
189
202
|
|
|
@@ -231,10 +244,10 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
|
|
|
231
244
|
- **`drift-blocked`:** §3's drift gate halts.
|
|
232
245
|
- **`blocked`** (`rescope_without_amendment[]`): a `DELETED_EXPORT` whose manifest entry carries no `rescope` object (its `scope.amendments[]` entry, `action: "excluded"`, and its `scope.exclude` path, rule R1). HALT with status `blocked` (halt procedure: `phase: "re-extract:rescope"`, `reason: "rescope-without-amendment: {name}"`): a rescope without a brief scope amendment is denominator deflation and must not be written.
|
|
233
246
|
- **`needs-judgment`:** each item is a spot-check whose `file` the definition-line rules cover no language of. Read the file and add, under the item's `gap_id` in `{run_dir}/gap-judgments.json`, its `declaring_line`: the line that declares the export itself, never a decorator or comment above it, or null when no line does. Then run it again.
|
|
234
|
-
- **`planned`:** when `reextract.entries` is not empty, run §4a for them: the plan wrote their files to `{run_dir}/remediation-files.json
|
|
247
|
+
- **`planned`:** when `reextract.entries` or `reextract.signatures` is not empty, run §4a for them: the plan wrote their files to `{run_dir}/remediation-files.json`, each entry's `resolved_paths[]` and then the `file` of each export `reextract.signatures` lists (a `NEW_EXPORT` or `MODIFIED_EXPORT` its spot-check located, `verified` or `moved`, at the `line` it gives), whose signature bullet 2 takes from §4a's record of it there. A `NEW_EXPORT` whose `reachability` `{run_dir}/gap-judgments.json` does not answer yet is listed too, so its scan goes unused when bullet 2's gate finds it internal. Add none of its `warnings[]`: bullet 2's call prints them again.
|
|
235
248
|
- **Exit 1 or no JSON:** HALT with status `blocked` (halt procedure: `phase: "re-extract:gap-records"`, its stderr as `reason`).
|
|
236
249
|
|
|
237
|
-
A severity is blocking unless it is `Medium`, `Low` or `Info`, compared case-insensitively, so a missing or unrecognized one is blocking, like `Critical` and `High`. A blocking gap must produce provenance read from the source or halt in this file before merge: only a `Medium`, `Low` or `Info` gap may degrade to `unknown`.
|
|
250
|
+
A severity is blocking unless it is `Medium`, `Low` or `Info`, compared case-insensitively, so a missing or unrecognized one is blocking, like `Critical` and `High`. A blocking gap whose export the map does not hold must produce provenance read from the source or halt in this file before merge: for such an export, only a `Medium`, `Low` or `Info` gap may degrade to `unknown`. A `NEW_EXPORT` or `MODIFIED_EXPORT` whose export the map holds (its `map_match` is `found` or `ambiguous`, or `not-found` with a candidate that has a `source_file`: the gap cites another file than the map's entries of its name) never goes to §4a's targeted re-extraction (`reextract.entries`), whatever its severity: when the map holds several entries of its name (`ambiguous`, so nothing is checked), its spot-check at the entry ends `unknown` or `missing` (the entry's file is gone), or the check at a citation of another file pins no line (`unknown`, `cited-elsewhere`: that file's declaration may be another package's, so it is never re-extracted), its entry stays unchanged. Step 5's `apply` warns for it, naming the cause of an `unknown` (several map entries of its name, no definition line or several at the entry, a file no rule covers, a failed or skipped check, a citation of another file than the map's entries of its name). For a blocking gap the `provenance:` warning also names the gap and the one fix a re-run acts on: for several entries of its name, fix them by hand, or give the gap a `file:line` Source at the definition when that file holds only one of them, so the re-run's lookup takes that one; otherwise fix the entry by hand (set its `source_file` and `source_line` to the line that defines it), since the re-run checks the same entry again. Then re-run update-skill. For a `cited-elsewhere` gap, whatever its severity, the warning names the gap, the cited file and its check's cause (such as `missing` or `no-definition`), and where the map holds the name, and the fix for each side that may be wrong: when the map is right, re-run Test Skill, since the test compared another file's declaration, and if the gap comes back, exclude the cited file in the brief's scope when the skill does not cover it (not when the cited file is gone); when an export moved, fix that map entry by hand, then re-run update-skill. When its check read nothing at the cited file (no source tree, the verifier missing, a failed check), the warning says instead to give update-skill the source tree, re-install SKF, or fix what the check failed on, then re-run update-skill.
|
|
238
251
|
2. **Write the gap-driven records** to `{run_dir}/reextract-records.json`, before §5 sends the run on: merge reads them, and step 5's `apply` writes the provenance map from them. Run the same call without `--plan` and `--files-out`:
|
|
239
252
|
|
|
240
253
|
```bash
|
|
@@ -249,9 +262,9 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
|
|
|
249
262
|
-o "{run_dir}/reextract-records.json"
|
|
250
263
|
```
|
|
251
264
|
|
|
252
|
-
Pass `--remediation-records`, `--evidence` and `--tier` when §4a ran. It writes one verification record per export-bearing entry, the confidence breakdown and counts, and in `files` each record of `{run_dir}/remediation-records.json` it matched by name to an entry §4a scanned for, within that entry's own `resolved_paths[]`, copied as `records` wrote it: never type a record or count one by hand. With `--evidence` it appends its `targeted_reextraction` record to `{run_dir}/evidence-records.jsonl` as one JSON line keyed `targeted_reextraction`, which the evidence report (step 5 §4) shows beside the verified, moved and missing tally. Act on its `status`:
|
|
265
|
+
Pass `--remediation-records`, `--evidence` and `--tier` when §4a ran. It writes one verification record per export-bearing entry, the confidence breakdown and counts, and in `files` each record of `{run_dir}/remediation-records.json` it matched by name, or by the public name the runner gives its local name in that file, to an entry §4a scanned for, within that entry's own `resolved_paths[]`, copied as `records` wrote it (one matched by that public name is copied under it, with its own name as `local`), and the record of each export `reextract.signatures` lists, whose `export_type`, `params`, `return_type`, `extraction_method` and `ast_node_type` it puts on that export's verification record, with a `provenance: ... signature not repaired` warning for one it finds no record of at its line: never type a record or count one by hand. The public-name match reads the runner's entry-point diff, so it holds at Forge tier and above: at Quick tier, or when §4a reads the files by eye after the runner exits 3, a gap that names a renamed re-export by its public name matches no record and is `unresolved` below. With `--evidence` it appends its `targeted_reextraction` record to `{run_dir}/evidence-records.jsonl` as one JSON line keyed `targeted_reextraction`, which the evidence report (step 5 §4) shows beside the verified, moved and missing tally. Act on its `status`:
|
|
253
266
|
|
|
254
|
-
- **`unresolved`:** an entry routed to §4a has no record of its name in its path set, or had no path to scan (`files_scanned: 0`). A found export the public-reachability gate below finds internal is a resolution, not a failure. HALT with status `halted-for-remediation-path`, under `--dry-run` too (the run stops here instead of exiting `dry-run`), and display a consolidated report of every `unresolved[]` entry:
|
|
267
|
+
- **`unresolved`:** an entry routed to §4a has no record of its name in its path set (nor one the runner re-exports under that name there), or had no path to scan (`files_scanned: 0`). A found export the public-reachability gate below finds internal is a resolution, not a failure. HALT with status `halted-for-remediation-path`, under `--dry-run` too (the run stops here instead of exiting `dry-run`), and display a consolidated report of every `unresolved[]` entry:
|
|
255
268
|
|
|
256
269
|
```
|
|
257
270
|
Targeted re-extraction failed for {N} gap(s).
|
|
@@ -289,16 +302,16 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
|
|
|
289
302
|
|
|
290
303
|
### 4a. Targeted Re-Extraction Branch (Helper, Called from §4 Bullet 1)
|
|
291
304
|
|
|
292
|
-
**Do not execute this section sequentially.** It is a helper procedure §4 bullet 1 invokes for the entries its plan lists in `reextract.entries`. A run in which §4 bullet 1 routes no entry here skips this section entirely.
|
|
305
|
+
**Do not execute this section sequentially.** It is a helper procedure §4 bullet 1 invokes for the entries its plan lists in `reextract.entries` and `reextract.signatures`. A run in which §4 bullet 1 routes no entry here skips this section entirely.
|
|
293
306
|
|
|
294
|
-
**Used by:** §4 bullet 1's plan, for the entries it lists in `reextract.entries`: each scans its `resolved_paths[]` (an entry with none has nothing to scan), and §4 bullet 2 matches what this section extracts to each entry by name.
|
|
307
|
+
**Used by:** §4 bullet 1's plan, for the entries it lists in `reextract.entries`: each scans its `resolved_paths[]` (an entry with none has nothing to scan), and §4 bullet 2 matches what this section extracts to each entry by name. For each export it lists in `reextract.signatures`, §4 bullet 2 takes the signature this section extracts at its `file` and `line`.
|
|
295
308
|
|
|
296
|
-
**Purpose:** produce AST-backed provenance for a gap with a blocking `severity` (§4 bullet 1) and no citation that pins a line, so step 5 §3 never writes `source_file: null` for a blocking finding. Honors the workflow-level rule **Never hallucinate** (every statement must have AST provenance) against the most common gap-driven trigger: a failing test report whose Gap Report `Source:` field is a region reference (e.g., `@storybook/addon-docs control primitives`) rather than a `file:line` pair.
|
|
309
|
+
**Purpose:** produce AST-backed provenance for a gap with a blocking `severity` (§4 bullet 1) and no citation that pins a line, so step 5 §3 never writes `source_file: null` for a blocking finding. Honors the workflow-level rule **Never hallucinate** (every statement must have AST provenance) against the most common gap-driven trigger: a failing test report whose Gap Report `Source:` field is a region reference (e.g., `@storybook/addon-docs control primitives`) rather than a `file:line` pair. For an export a spot-check located, it reads the signature SKILL.md and the provenance map take, so neither keeps a stale one nor takes one from merge's prose.
|
|
297
310
|
|
|
298
311
|
**Procedure:**
|
|
299
312
|
|
|
300
313
|
1. **Source access:** read the files under `{source_root}`, the tree §3 confirmed holds the pinned commit, never through the gh contents API, zread or deepwiki, which may serve another commit.
|
|
301
|
-
2. **The file set** is `{run_dir}/remediation-files.json`, which §4 bullet 1's plan wrote: the `resolved_paths[]` of every entry routed here, as they are. §1's `{parseGapsHelper}` resolved them under `{source_root}` and refused, into `rejected_paths[]`, every path that leaves it (`outside-root`, or `symlink-outside-root` through a link) or names no file (`not-found`, `no-match`). Remediation text is user-editable: never scan a refused path, and never expand or check a path by hand.
|
|
314
|
+
2. **The file set** is `{run_dir}/remediation-files.json`, which §4 bullet 1's plan wrote: the `resolved_paths[]` of every entry routed here, as they are. §1's `{parseGapsHelper}` resolved them under `{source_root}` and refused, into `rejected_paths[]`, every path that leaves it (`outside-root`, or `symlink-outside-root` through a link) or names no file (`not-found`, `no-match`). The plan adds the `file` of each export `reextract.signatures` lists, where its spot-check located it under `{source_root}`; of a file only those exports bring, read by eye (item 3) only the exports they name. Remediation text is user-editable: never scan a refused path, and never expand or check a path by hand.
|
|
302
315
|
3. **Extract:** read the file set with the tier-appropriate extractor: `records` below labels each export by the tool that produced it, T1 (`extraction_method: ast-grep`) for an ast-grep match and T1-low (`extraction_method: source-read`) for an export read by eye. At Forge tier and above, run the recipe runner over the file set (resolve `{extractPublicApiHelper}` ← first existing path in `{extractPublicApiProbeOrder}`), from `{project-root}`:
|
|
303
316
|
|
|
304
317
|
```bash
|
|
@@ -326,7 +339,9 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
|
|
|
326
339
|
|
|
327
340
|
Display the summary below, with the `counts` §4 bullet 2 printed:
|
|
328
341
|
|
|
329
|
-
"**Gap-driven re-extraction.** Verified {verified_count}/{gap_count} citations against live source. Moved: {moved_count}. Missing: {missing_count}. Re-extracted (via remediation paths or provenance-completeness, §4a): {re_extracted_count}. Rescoped (removed from surface): {rescoped_count}. Unknown (not in provenance map, a `Medium`, `Low` or `Info` gap §4a found nothing for, no single definition line, or a line the drift override kept from being moved): {unknown_count}."
|
|
342
|
+
"**Gap-driven re-extraction.** Verified {verified_count}/{gap_count} citations against live source. Moved: {moved_count}. Missing (entry left unchanged): {missing_count}. Re-extracted (via remediation paths or provenance-completeness, §4a): {re_extracted_count}. Rescoped (removed from surface): {rescoped_count}. Unknown (not in provenance map, a `Medium`, `Low` or `Info` gap §4a found nothing for, several map entries of its name, a citation of another file than the map's entries of its name, no single definition line, or a line the drift override kept from being moved): {unknown_count}."
|
|
343
|
+
|
|
344
|
+
A `NEW_EXPORT` or `MODIFIED_EXPORT` the provenance map holds that ends `missing` or `unknown` keeps its entry unchanged, whatever its severity: step 5's `apply` warns for each, with the cause of an `unknown`, and for a blocking gap says what to fix (§4 bullet 1). One whose gap cites another file than the map's entries of its name (`cited-elsewhere`) is never re-extracted, and its warning says what to fix whatever its severity: re-run Test Skill when the map is right, or fix the map entry by hand when the export moved.
|
|
330
345
|
|
|
331
346
|
When `{workspace_drift_status}` is `overridden`, add: "Every check read HEAD {head_short_sha}, not pinned {pinned_short_sha}: no line was moved or pinned."
|
|
332
347
|
|