bmad-module-skill-forge 3.0.0 → 4.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/docs/_data/pinned.yaml +1 -1
- package/docs/examples.md +1 -1
- package/docs/troubleshooting.md +8 -0
- package/docs/verifying-a-skill.md +7 -3
- package/docs/workflows.md +3 -3
- package/package.json +1 -1
- package/src/knowledge/qmd-registry.md +1 -1
- package/src/knowledge/version-paths.md +1 -1
- package/src/shared/health-check.md +2 -50
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +2 -2
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +2 -2
- package/src/shared/scripts/skf-atomic-write.py +212 -59
- package/src/shared/scripts/skf-build-change-manifest.py +593 -47
- package/src/shared/scripts/skf-classify-changed-files.py +78 -0
- package/src/shared/scripts/skf-compare-file-hashes.py +266 -104
- package/src/shared/scripts/skf-derive-assembly-shape.py +18 -1
- package/src/shared/scripts/skf-detect-scripts-assets.py +196 -54
- package/src/shared/scripts/skf-extract-public-api.py +1309 -157
- package/src/shared/scripts/skf-extraction-inventory.py +580 -46
- package/src/shared/scripts/skf-extraction-snapshot.py +158 -33
- package/src/shared/scripts/skf-forge-tier-rw.py +26 -3
- package/src/shared/scripts/skf-github-probe.py +47 -8
- package/src/shared/scripts/skf-hash-content.py +14 -8
- package/src/shared/scripts/skf-load-provenance.py +3 -1
- package/src/shared/scripts/skf-merge-doc-urls.py +142 -19
- package/src/shared/scripts/skf-preflight.py +15 -1
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +25 -1
- package/src/shared/scripts/skf-resolve-authoritative-files.py +29 -3
- package/src/shared/scripts/skf-structural-diff.py +133 -18
- package/src/shared/scripts/skf-update-active-symlink.py +115 -62
- package/src/shared/scripts/skf-validate-brief-schema.py +19 -1
- package/src/shared/scripts/skf-verify-provenance-completeness.py +977 -49
- package/src/shared/scripts/skf-write-skill-brief.py +41 -8
- package/src/skf-analyze-source/references/map-unit-exports.md +1 -1
- package/src/skf-audit-skill/assets/drift-report-template.md +1 -0
- package/src/skf-audit-skill/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/references/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +2 -1
- package/src/skf-audit-skill/references/re-index.md +14 -9
- package/src/skf-audit-skill/references/report.md +2 -2
- package/src/skf-audit-skill/references/semantic-diff.md +15 -15
- package/src/skf-audit-skill/references/structural-diff.md +10 -5
- package/src/skf-audit-skill/scripts/render-drift-tables.py +29 -2
- package/src/skf-brief-skill/SKILL.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -9
- package/src/skf-brief-skill/references/analyze-target.md +6 -6
- package/src/skf-brief-skill/references/confirm-brief.md +1 -1
- package/src/skf-brief-skill/references/scope-definition.md +4 -4
- package/src/skf-brief-skill/references/step-auto-brief.md +11 -29
- package/src/skf-brief-skill/references/version-resolution.md +5 -9
- package/src/skf-create-skill/assets/compile-assembly-rules.md +5 -3
- package/src/skf-create-skill/assets/skill-sections.md +6 -6
- package/src/skf-create-skill/references/authoritative-files-protocol.md +2 -23
- package/src/skf-create-skill/references/compile.md +31 -60
- package/src/skf-create-skill/references/component-extraction.md +3 -3
- package/src/skf-create-skill/references/enrich.md +2 -2
- package/src/skf-create-skill/references/entry-points-by-hand.md +24 -7
- package/src/skf-create-skill/references/extract.md +22 -19
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +9 -9
- package/src/skf-create-skill/references/extraction-patterns.md +8 -9
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +16 -5
- package/src/skf-create-skill/references/source-resolution-protocols.md +12 -17
- package/src/skf-create-skill/references/sub/fetch-docs.md +1 -1
- package/src/skf-drop-skill/references/execute.md +5 -1
- package/src/skf-drop-skill/references/health-check.md +2 -0
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +2 -2
- package/src/skf-drop-skill/references/select.md +7 -3
- package/src/skf-test-skill/references/coherence-check.md +1 -1
- package/src/skf-test-skill/references/coverage-check-tiers.md +5 -5
- package/src/skf-test-skill/references/coverage-check.md +39 -57
- package/src/skf-test-skill/references/init.md +3 -5
- package/src/skf-test-skill/references/report.md +10 -8
- package/src/skf-test-skill/references/scoring-rules.md +6 -5
- package/src/skf-test-skill/references/source-access-protocol.md +14 -17
- package/src/skf-test-skill/scripts/build-result-context.py +114 -21
- package/src/skf-test-skill/scripts/gap-ledger.py +315 -30
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +534 -96
- package/src/skf-test-skill/scripts/reconcile-coverage.py +276 -2
- package/src/skf-test-skill/scripts/score-signatures.py +36 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +7 -5
- package/src/skf-update-skill/references/detect-changes.md +32 -13
- package/src/skf-update-skill/references/gap-driven.md +3 -3
- package/src/skf-update-skill/references/init.md +6 -2
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +2 -2
- package/src/skf-update-skill/references/re-extract.md +2 -2
- package/src/skf-update-skill/references/report.md +3 -1
- package/src/skf-update-skill/references/write.md +43 -23
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +157 -42
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# /// script
|
|
3
3
|
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = ["pyyaml"]
|
|
4
5
|
# ///
|
|
5
6
|
"""Deterministic NEW_FILE detection for skf-update-skill (detect-changes Category D).
|
|
6
7
|
|
|
@@ -10,39 +11,70 @@ file that is present in source yet absent from the provenance map: a NEW_FILE.
|
|
|
10
11
|
Deriving that set is a set-difference plus a filter with exactly one correct
|
|
11
12
|
answer per input: take every `source_file` in the scripts/assets inventory
|
|
12
13
|
(emitted by `skf-detect-scripts-assets detect`), subtract the paths already in
|
|
13
|
-
`file_entries[].source_file`, and set aside any user-authored `[MANUAL]` path
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
`file_entries[].source_file`, and set aside any user-authored `[MANUAL]` path,
|
|
15
|
+
any path the map's `entries[]` cite (code Category A and B track) and, with
|
|
16
|
+
--brief, any path the skill brief leaves out. Doing that subtraction in the
|
|
17
|
+
prompt drifts across runs; doing it here is byte-stable.
|
|
16
18
|
|
|
17
19
|
Usage:
|
|
18
|
-
skf-detect-scripts-assets.py detect <source-root>
|
|
19
|
-
| skf-new-file-diff.py <provenance-map-path>
|
|
20
|
+
skf-detect-scripts-assets.py detect <source-root> \\
|
|
21
|
+
| skf-new-file-diff.py <provenance-map-path> [--brief <brief-path>]
|
|
20
22
|
|
|
21
23
|
Reads the detect JSON on stdin (needs `scripts_inventory[]` and
|
|
22
24
|
`assets_inventory[]`, each row carrying `source_file`). Reads the provenance
|
|
23
|
-
map at the path argument (canonical `{file_entries: [...]
|
|
24
|
-
array of entries).
|
|
25
|
+
map at the path argument (canonical `{file_entries: [...], entries: [...]}`
|
|
26
|
+
object, or a bare array of file entries). A map with no `file_entries`, or a
|
|
27
|
+
null one, tracks no file (a skill with no script, asset or promoted document,
|
|
28
|
+
or a map written before the field existed); a `file_entries` or `entries`
|
|
29
|
+
that is neither null nor an array is bad input.
|
|
30
|
+
|
|
31
|
+
--brief <brief-path> (the skill's skill-brief.yaml) keeps a detected path
|
|
32
|
+
only when the brief takes it, by update-skill Category A's in-scope test
|
|
33
|
+
(skf-classify-changed-files.py load_scope, loaded from the shared
|
|
34
|
+
scripts folder, the one brief-scope test the script and asset detector's
|
|
35
|
+
and audit-skill's --brief use too, with the glob rules of
|
|
36
|
+
skf-resolve-authoritative-files.py): each `scope.include` and
|
|
37
|
+
`scope.exclude` pattern normalized (forward slashes, no leading `./`),
|
|
38
|
+
and a path in scope when an include
|
|
39
|
+
matches it and no exclude does. With an empty `scope.include`, a path is in
|
|
40
|
+
scope when no exclude matches it and its extension is one a file the map's
|
|
41
|
+
`entries[]` cite has, or one the brief's `language` uses. The brief's
|
|
42
|
+
`scripts_intent` and `assets_intent` are honoured too: `none` drops every
|
|
43
|
+
detected path of that kind, and an absent or free-text intent detects.
|
|
44
|
+
An empty --brief (an unbound brief path) is a usage error, never an
|
|
45
|
+
unscoped run. Every path, the map's and the detector's, is compared
|
|
46
|
+
stripped, with forward slashes and no leading `./`.
|
|
25
47
|
|
|
26
48
|
Output JSON (stdout):
|
|
27
49
|
{
|
|
28
50
|
"new_files": [ {"source_file": "...", "kind": "script|asset"}, ... ],
|
|
29
51
|
"skipped_manual": [ "...", ... ], # under scripts/[MANUAL]/ or assets/[MANUAL]/
|
|
30
52
|
"already_tracked": [ "...", ... ], # present in file_entries
|
|
53
|
+
"tracked_code": [ "...", ... ], # cited by entries[]: code, not a new file
|
|
54
|
+
"out_of_scope": [ "...", ... ], # --brief: outside the brief's scope
|
|
55
|
+
"intent_none": [ "...", ... ], # --brief: a kind whose intent is none
|
|
31
56
|
"stats": {"inventory_total": N, "new": N,
|
|
32
57
|
"skipped_manual": N, "already_tracked": N}
|
|
33
58
|
}
|
|
34
59
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
60
|
+
Each detected path lands in exactly one list, tested in this order:
|
|
61
|
+
already_tracked, skipped_manual, tracked_code, out_of_scope, intent_none,
|
|
62
|
+
and new_files for the rest. All arrays are sorted by
|
|
63
|
+
source_file; new_files carries the kind of the inventory it came from
|
|
64
|
+
(scripts -> "script", assets -> "asset"). A path found in both inventories is
|
|
65
|
+
counted once, resolved as "script". The three set-aside lists (tracked_code,
|
|
66
|
+
out_of_scope, intent_none) are never counted in `stats`, whose
|
|
67
|
+
inventory_total still counts every detected path.
|
|
38
68
|
|
|
39
69
|
Exit codes:
|
|
40
70
|
0 success (and --help)
|
|
41
71
|
2 bad input (a usage error, unreadable/invalid stdin JSON, missing/invalid
|
|
42
|
-
provenance map
|
|
72
|
+
provenance map, a missing or unreadable brief, or a shared helper that
|
|
73
|
+
cannot be loaded)
|
|
43
74
|
"""
|
|
44
75
|
|
|
45
76
|
import argparse
|
|
77
|
+
import importlib.util
|
|
46
78
|
import json
|
|
47
79
|
import re
|
|
48
80
|
import sys
|
|
@@ -51,6 +83,13 @@ from pathlib import Path
|
|
|
51
83
|
# scripts/[MANUAL]/... or assets/[MANUAL]/... anywhere in the (posix) path
|
|
52
84
|
_MANUAL_RE = re.compile(r"(?:^|/)(?:scripts|assets)/\[MANUAL\]/")
|
|
53
85
|
|
|
86
|
+
# The shared helper that holds the scope test, installed beside this skill's
|
|
87
|
+
# folder: it loads skf-resolve-authoritative-files.py beside it.
|
|
88
|
+
SHARED_SCRIPTS = Path(__file__).resolve().parent.parent.parent / "shared" / "scripts"
|
|
89
|
+
CLASSIFIER = "skf-classify-changed-files.py"
|
|
90
|
+
|
|
91
|
+
_SHARED: dict[str, object] = {}
|
|
92
|
+
|
|
54
93
|
|
|
55
94
|
def _fail(msg: str) -> "NoReturn": # type: ignore[valid-type]
|
|
56
95
|
print(json.dumps({"error": msg}), file=sys.stderr)
|
|
@@ -58,37 +97,88 @@ def _fail(msg: str) -> "NoReturn": # type: ignore[valid-type]
|
|
|
58
97
|
|
|
59
98
|
|
|
60
99
|
def _posix(p: str) -> str:
|
|
61
|
-
|
|
100
|
+
"""A path as skf-resolve-authoritative-files.py's normalize_rel_path
|
|
101
|
+
writes it: stripped, forward slashes, no leading `./`."""
|
|
102
|
+
p = p.strip().replace("\\", "/")
|
|
103
|
+
while p.startswith("./"):
|
|
104
|
+
p = p[2:]
|
|
105
|
+
return p
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _shared(filename: str):
|
|
109
|
+
"""A script of SHARED_SCRIPTS, loaded once; a failure to load it is bad input (exit 2)."""
|
|
110
|
+
module = _SHARED.get(filename)
|
|
111
|
+
if module is None:
|
|
112
|
+
path = SHARED_SCRIPTS / filename
|
|
113
|
+
try:
|
|
114
|
+
spec = importlib.util.spec_from_file_location("skf_" + filename[:-3].replace("-", "_"), path)
|
|
115
|
+
if spec is None or spec.loader is None or not path.is_file():
|
|
116
|
+
raise ImportError(f"{filename} not found in {SHARED_SCRIPTS}")
|
|
117
|
+
module = importlib.util.module_from_spec(spec)
|
|
118
|
+
spec.loader.exec_module(module)
|
|
119
|
+
except Exception as exc: # whatever the script raises, the run reports it
|
|
120
|
+
_fail(f"cannot load {filename} from {SHARED_SCRIPTS}: {exc}; re-install SKF")
|
|
121
|
+
_SHARED[filename] = module
|
|
122
|
+
return module
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _source_files(rows: list) -> set[str]:
|
|
126
|
+
found: set[str] = set()
|
|
127
|
+
for entry in rows:
|
|
128
|
+
if isinstance(entry, dict):
|
|
129
|
+
sf = entry.get("source_file")
|
|
130
|
+
if isinstance(sf, str) and _posix(sf):
|
|
131
|
+
found.add(_posix(sf))
|
|
132
|
+
return found
|
|
62
133
|
|
|
63
134
|
|
|
64
|
-
def
|
|
65
|
-
"""
|
|
135
|
+
def load_map_paths(provenance_path: Path) -> tuple[set[str], set[str]]:
|
|
136
|
+
"""(file_entries[].source_file, entries[].source_file) of the provenance map.
|
|
137
|
+
|
|
138
|
+
A missing or null `file_entries` (or `entries`) is an empty list; one
|
|
139
|
+
that is neither null nor an array is bad input. A bare array is the
|
|
140
|
+
file entries themselves.
|
|
141
|
+
"""
|
|
66
142
|
try:
|
|
67
143
|
data = json.loads(provenance_path.read_text(encoding="utf-8"))
|
|
68
144
|
except (json.JSONDecodeError, OSError) as exc:
|
|
69
145
|
_fail(f"failed to read provenance map {provenance_path}: {exc}")
|
|
70
146
|
|
|
71
147
|
if isinstance(data, list):
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
entries = data.get("file_entries")
|
|
75
|
-
if entries is None:
|
|
76
|
-
_fail(f"provenance map {provenance_path} has no `file_entries` field")
|
|
77
|
-
if not isinstance(entries, list):
|
|
78
|
-
_fail(f"`file_entries` in {provenance_path} is not an array")
|
|
79
|
-
else:
|
|
148
|
+
return _source_files(data), set()
|
|
149
|
+
if not isinstance(data, dict):
|
|
80
150
|
_fail(
|
|
81
151
|
f"provenance map {provenance_path} must be an object or array; "
|
|
82
152
|
f"got {type(data).__name__}"
|
|
83
153
|
)
|
|
154
|
+
rows = {}
|
|
155
|
+
for key in ("file_entries", "entries"):
|
|
156
|
+
value = data.get(key)
|
|
157
|
+
if value is None:
|
|
158
|
+
value = [] # a map that tracks no such row
|
|
159
|
+
if not isinstance(value, list):
|
|
160
|
+
_fail(f"`{key}` in {provenance_path} is not an array")
|
|
161
|
+
rows[key] = value
|
|
162
|
+
return _source_files(rows["file_entries"]), _source_files(rows["entries"])
|
|
84
163
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
164
|
+
|
|
165
|
+
def load_tracked_source_files(provenance_path: Path) -> set[str]:
|
|
166
|
+
"""Return the set of file_entries[].source_file already in the provenance map."""
|
|
167
|
+
return load_map_paths(provenance_path)[0]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def load_scope(brief_path: Path, cited: set[str]):
|
|
171
|
+
"""The brief's Scope (skf-classify-changed-files.py load_scope): its
|
|
172
|
+
takes(path) is Category A's in-scope test and its wants(kind) the
|
|
173
|
+
brief's intent. A brief it cannot find or read, or a shared helper it
|
|
174
|
+
cannot load, is bad input (exit 2)."""
|
|
175
|
+
classifier = _shared(CLASSIFIER)
|
|
176
|
+
try:
|
|
177
|
+
return classifier.load_scope(brief_path, cited)
|
|
178
|
+
except ValueError as exc: # a brief it cannot find or read
|
|
179
|
+
_fail(str(exc))
|
|
180
|
+
except Exception as exc: # an out-of-date classifier, or a resolver beside it that cannot load
|
|
181
|
+
_fail(f"cannot load the brief's scope test from {SHARED_SCRIPTS}: {exc}; re-install SKF")
|
|
92
182
|
|
|
93
183
|
|
|
94
184
|
def collect_inventory(detect: dict) -> list[tuple[str, str]]:
|
|
@@ -114,38 +204,53 @@ def collect_inventory(detect: dict) -> list[tuple[str, str]]:
|
|
|
114
204
|
return sorted(seen.items())
|
|
115
205
|
|
|
116
206
|
|
|
117
|
-
def diff(detect: dict, tracked: set[str]
|
|
207
|
+
def diff(detect: dict, tracked: set[str], cited: set[str] | frozenset = frozenset(),
|
|
208
|
+
scope=None) -> dict:
|
|
209
|
+
"""Sort the detected paths into the output lists; `scope` is load_scope's
|
|
210
|
+
Scope (--brief) or None."""
|
|
118
211
|
new_files: list[dict] = []
|
|
119
|
-
|
|
120
|
-
|
|
212
|
+
lists: dict[str, list[str]] = {name: [] for name in (
|
|
213
|
+
"skipped_manual", "already_tracked", "tracked_code", "out_of_scope", "intent_none")}
|
|
121
214
|
|
|
122
215
|
inventory = collect_inventory(detect)
|
|
123
216
|
for sf, kind in inventory:
|
|
124
217
|
if sf in tracked:
|
|
125
|
-
already_tracked.append(sf)
|
|
218
|
+
lists["already_tracked"].append(sf)
|
|
126
219
|
elif _MANUAL_RE.search(sf):
|
|
127
|
-
skipped_manual.append(sf)
|
|
220
|
+
lists["skipped_manual"].append(sf)
|
|
221
|
+
elif sf in cited:
|
|
222
|
+
lists["tracked_code"].append(sf)
|
|
223
|
+
elif scope is not None and not scope.takes(sf):
|
|
224
|
+
lists["out_of_scope"].append(sf)
|
|
225
|
+
elif scope is not None and not scope.wants(kind):
|
|
226
|
+
lists["intent_none"].append(sf)
|
|
128
227
|
else:
|
|
129
228
|
new_files.append({"source_file": sf, "kind": kind})
|
|
130
229
|
|
|
131
230
|
return {
|
|
132
231
|
"new_files": new_files,
|
|
133
|
-
"skipped_manual": sorted(skipped_manual),
|
|
134
|
-
"already_tracked": sorted(already_tracked),
|
|
232
|
+
"skipped_manual": sorted(lists["skipped_manual"]),
|
|
233
|
+
"already_tracked": sorted(lists["already_tracked"]),
|
|
234
|
+
"tracked_code": sorted(lists["tracked_code"]),
|
|
235
|
+
"out_of_scope": sorted(lists["out_of_scope"]),
|
|
236
|
+
"intent_none": sorted(lists["intent_none"]),
|
|
135
237
|
"stats": {
|
|
136
238
|
"inventory_total": len(inventory),
|
|
137
239
|
"new": len(new_files),
|
|
138
|
-
"skipped_manual": len(skipped_manual),
|
|
139
|
-
"already_tracked": len(already_tracked),
|
|
240
|
+
"skipped_manual": len(lists["skipped_manual"]),
|
|
241
|
+
"already_tracked": len(lists["already_tracked"]),
|
|
140
242
|
},
|
|
141
243
|
}
|
|
142
244
|
|
|
143
245
|
|
|
246
|
+
USAGE = "usage: skf-new-file-diff.py <provenance-map-path> [--brief <brief-path>] (detect JSON on stdin)"
|
|
247
|
+
|
|
248
|
+
|
|
144
249
|
class _JsonErrorParser(argparse.ArgumentParser):
|
|
145
250
|
"""Report a usage error as the JSON error and exit 2 every other bad input gets."""
|
|
146
251
|
|
|
147
252
|
def error(self, message: str) -> "NoReturn": # type: ignore[valid-type]
|
|
148
|
-
_fail(f"
|
|
253
|
+
_fail(f"{USAGE}: {message}")
|
|
149
254
|
|
|
150
255
|
|
|
151
256
|
def _build_parser() -> argparse.ArgumentParser:
|
|
@@ -153,17 +258,26 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
153
258
|
prog="skf-new-file-diff.py",
|
|
154
259
|
description=__doc__,
|
|
155
260
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
261
|
+
allow_abbrev=False,
|
|
156
262
|
)
|
|
157
263
|
parser.add_argument(
|
|
158
264
|
"provenance_map",
|
|
159
265
|
metavar="provenance-map-path",
|
|
160
266
|
help="the skill's provenance-map.json (the detect JSON is read on stdin)",
|
|
161
267
|
)
|
|
268
|
+
parser.add_argument(
|
|
269
|
+
"--brief",
|
|
270
|
+
metavar="brief-path",
|
|
271
|
+
help="the skill brief: keep only the paths its scope takes and its intents want",
|
|
272
|
+
)
|
|
162
273
|
return parser
|
|
163
274
|
|
|
164
275
|
|
|
165
276
|
def main(argv: list[str]) -> int:
|
|
166
|
-
|
|
277
|
+
parser = _build_parser()
|
|
278
|
+
args = parser.parse_args(argv[1:])
|
|
279
|
+
if args.brief is not None and not args.brief.strip():
|
|
280
|
+
parser.error("--brief is empty (an unbound brief path?): pass the brief, or leave --brief out")
|
|
167
281
|
|
|
168
282
|
provenance_path = Path(args.provenance_map)
|
|
169
283
|
if not provenance_path.exists():
|
|
@@ -179,8 +293,9 @@ def main(argv: list[str]) -> int:
|
|
|
179
293
|
if not isinstance(detect, dict):
|
|
180
294
|
_fail(f"detect JSON must be an object; got {type(detect).__name__}")
|
|
181
295
|
|
|
182
|
-
tracked =
|
|
183
|
-
|
|
296
|
+
tracked, cited = load_map_paths(provenance_path)
|
|
297
|
+
scope = load_scope(Path(args.brief), cited) if args.brief is not None else None
|
|
298
|
+
result = diff(detect, tracked, cited, scope)
|
|
184
299
|
print(json.dumps(result, indent=2))
|
|
185
300
|
return 0
|
|
186
301
|
|