bmad-module-skill-forge 5.0.0 → 7.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/agents.md +1 -1
- package/docs/architecture.md +3 -3
- package/docs/campaign.md +1 -1
- package/docs/troubleshooting.md +9 -3
- package/docs/verifying-a-skill.md +9 -9
- package/docs/workflows.md +7 -7
- package/package.json +1 -1
- package/src/knowledge/tool-resolution.md +6 -6
- package/src/shared/references/headless-gate-convention.md +4 -1
- package/src/shared/references/output-contract-schema.md +3 -3
- package/src/shared/references/pipeline-contracts.md +2 -1
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
- package/src/shared/scripts/skf-build-change-manifest.py +332 -54
- package/src/shared/scripts/skf-classify-changed-files.py +15 -10
- package/src/shared/scripts/skf-emit-result-envelope.py +58 -15
- package/src/shared/scripts/skf-extract-public-api.py +33 -13
- package/src/shared/scripts/skf-parse-gaps.py +3 -2
- package/src/shared/scripts/skf-quick-batch.py +16 -3
- package/src/shared/scripts/skf-verify-provenance-completeness.py +317 -44
- package/src/skf-analyze-source/SKILL.md +1 -0
- 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 +3 -3
- package/src/skf-analyze-source/references/step-auto-scope.md +2 -2
- package/src/skf-audit-skill/SKILL.md +2 -1
- package/src/skf-audit-skill/references/report.md +1 -1
- package/src/skf-brief-skill/SKILL.md +1 -0
- package/src/skf-brief-skill/references/invocation-contract.md +2 -2
- package/src/skf-campaign/SKILL.md +1 -0
- package/src/skf-campaign/references/campaign-contracts.md +3 -1
- package/src/skf-campaign/references/step-05-skill-loop.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-10-export.md +3 -2
- package/src/skf-campaign/scripts/campaign-state.py +22 -5
- package/src/skf-create-skill/SKILL.md +3 -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/ccc-index-check.md +1 -1
- package/src/skf-create-skill/references/component-extraction.md +1 -1
- package/src/skf-create-skill/references/extract.md +1 -1
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +9 -4
- package/src/skf-create-skill/references/sub/ccc-discover.md +1 -1
- package/src/skf-create-skill/references/validate.md +2 -2
- package/src/skf-create-stack-skill/SKILL.md +1 -0
- package/src/skf-create-stack-skill/references/generate-output.md +1 -1
- package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
- package/src/skf-create-stack-skill/references/parallel-extract.md +2 -2
- package/src/skf-create-stack-skill/references/report.md +1 -1
- package/src/skf-create-stack-skill/references/validate.md +12 -4
- package/src/skf-drop-skill/SKILL.md +1 -0
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +1 -1
- package/src/skf-export-skill/SKILL.md +1 -0
- package/src/skf-export-skill/references/invocation-contract.md +1 -1
- package/src/skf-export-skill/references/summary.md +1 -1
- package/src/skf-forger/SKILL.md +1 -0
- package/src/skf-forger/references/pipeline-mode.md +2 -2
- package/src/skf-quick-skill/SKILL.md +2 -1
- package/src/skf-quick-skill/references/batch-mode.md +2 -0
- package/src/skf-quick-skill/references/finalize.md +1 -1
- package/src/skf-quick-skill/references/halt-contract.md +2 -2
- package/src/skf-quick-skill/references/write-and-validate.md +20 -10
- package/src/skf-refine-architecture/SKILL.md +2 -1
- package/src/skf-refine-architecture/customize.toml +1 -1
- package/src/skf-refine-architecture/references/report.md +1 -1
- package/src/skf-rename-skill/SKILL.md +1 -0
- package/src/skf-rename-skill/references/invocation-contract.md +2 -2
- package/src/skf-setup/SKILL.md +1 -0
- package/src/skf-setup/references/ccc-index.md +1 -1
- package/src/skf-setup/references/invocation-contract.md +1 -1
- package/src/skf-test-skill/SKILL.md +1 -0
- package/src/skf-test-skill/assets/output-section-formats.md +4 -4
- package/src/skf-test-skill/references/coherence-check.md +8 -6
- package/src/skf-test-skill/references/coverage-check-branches.md +23 -3
- package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
- package/src/skf-test-skill/references/coverage-check.md +30 -43
- package/src/skf-test-skill/references/init.md +1 -1
- package/src/skf-test-skill/references/inventory-contract.md +27 -0
- package/src/skf-test-skill/references/invocation-contract.md +3 -3
- package/src/skf-test-skill/references/report.md +3 -3
- package/src/skf-test-skill/references/scoring-rules.md +7 -7
- package/src/skf-test-skill/references/signature-comparison-rules.md +74 -0
- package/src/skf-test-skill/references/source-access-protocol.md +12 -8
- package/src/skf-test-skill/references/step-hard-gate.md +1 -0
- package/src/skf-test-skill/scripts/build-result-context.py +20 -3
- package/src/skf-test-skill/scripts/gap-ledger.py +78 -13
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +575 -9
- package/src/skf-test-skill/scripts/locate-export-segments.py +591 -63
- package/src/skf-test-skill/scripts/score-signatures.py +937 -76
- package/src/skf-test-skill/scripts/validate-inventory.py +325 -30
- package/src/skf-update-skill/SKILL.md +1 -0
- package/src/skf-update-skill/customize.toml +1 -1
- package/src/skf-update-skill/references/gap-driven.md +28 -22
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +4 -4
- package/src/skf-update-skill/references/report.md +11 -4
- package/src/skf-update-skill/references/write.md +6 -5
- package/src/skf-verify-stack/SKILL.md +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# /// script
|
|
2
2
|
# requires-python = ">=3.11"
|
|
3
|
-
# dependencies = []
|
|
3
|
+
# dependencies = ["pyyaml"]
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Build Change Manifest — aggregate category A/B/C/D detection results.
|
|
6
6
|
|
|
@@ -81,14 +81,18 @@ Subcommands:
|
|
|
81
81
|
(the exports marked public: false), "warnings"}.
|
|
82
82
|
|
|
83
83
|
gap-records --manifest <file> --provenance-map <file> [--source-root <dir>]
|
|
84
|
-
[--drift-status ok|skipped|overridden]
|
|
85
|
-
[--
|
|
84
|
+
[--brief <file>] [--drift-status ok|skipped|overridden]
|
|
85
|
+
[--judgments <file>] [--remediation-records <file>]
|
|
86
|
+
[--evidence <file> --tier <tier>]
|
|
86
87
|
(--plan [--files-out <file>] | -o <file>)
|
|
87
88
|
A gap-driven repair's spot-checks, routing and verification records
|
|
88
89
|
(see "Gap records" below): --plan prints what to answer and which
|
|
89
90
|
files §4a's extraction scans (targeted re-extraction, and the
|
|
90
91
|
signatures of the exports a spot-check located); without it, the
|
|
91
92
|
records are written to -o through a temporary file and a rename.
|
|
93
|
+
With --brief (the skill brief), no file a gap cites or its
|
|
94
|
+
Remediation names that the brief's scope leaves out is checked,
|
|
95
|
+
scanned or repaired from.
|
|
92
96
|
|
|
93
97
|
baseline-gaps --provenance-map <file> --extraction <file> --files <file>
|
|
94
98
|
--source-root <dir> -o <file>
|
|
@@ -325,8 +329,8 @@ Apply (update-skill write.md §3):
|
|
|
325
329
|
export_name, gap_category, severity, verification, in_map (the map holds
|
|
326
330
|
an entry of that name), map_entry ({source_file, source_line} of the one
|
|
327
331
|
entry the spot-check took, else null), source_citation, new_location,
|
|
328
|
-
unknown_reason, citation_check, map_locations (see "Gap
|
|
329
|
-
pinned_definition_lines, reachability, and the located
|
|
332
|
+
unknown_reason, citation_check, map_locations, outside_scope (see "Gap
|
|
333
|
+
records"), pinned_definition_lines, reachability, and the located
|
|
330
334
|
export's export_type, params, return_type, extraction_method and
|
|
331
335
|
ast_node_type (from §4a's record at its line, see "Gap records"; null
|
|
332
336
|
when none holds it). It finds an export's entry by its name, else by the
|
|
@@ -351,6 +355,20 @@ Apply (update-skill write.md §3):
|
|
|
351
355
|
per name and file, with the signature (below)
|
|
352
356
|
and the source-read labels whatever its record's
|
|
353
357
|
tool
|
|
358
|
+
unknown, outside-scope unchanged and no entry added, whatever its
|
|
359
|
+
severity (never refused), with a warning that
|
|
360
|
+
names its gap_id, its unknown_reason and its
|
|
361
|
+
words, its outside_scope (the files the brief's
|
|
362
|
+
scope leaves out), the cause its citation_check
|
|
363
|
+
keeps for a citation the scope takes (with its
|
|
364
|
+
words) and the fix: when the skill covers that
|
|
365
|
+
file, change the brief's scope so it takes it
|
|
366
|
+
(its scope.include or scope.exclude), then
|
|
367
|
+
re-run update-skill; otherwise re-run Test
|
|
368
|
+
Skill (the test compared a declaration the
|
|
369
|
+
skill does not cover). A citation_check that
|
|
370
|
+
read nothing (UNREAD) takes its own fix in
|
|
371
|
+
their place, as below
|
|
354
372
|
unknown, not in_map, a NEW_EXPORT or MODIFIED_EXPORT, a Medium, Low or
|
|
355
373
|
Info severity an entry from --merge-records with no
|
|
356
374
|
source_file or source_line
|
|
@@ -371,6 +389,12 @@ Apply (update-skill write.md §3):
|
|
|
371
389
|
any other, fix the entry by hand (its
|
|
372
390
|
source_file and source_line), since the
|
|
373
391
|
re-run checks the same entry again. A
|
|
392
|
+
missing MOVED_EXPORT, whatever its severity,
|
|
393
|
+
takes that warning too (the gone file and
|
|
394
|
+
the hand fix): rule R5 moves a line, never
|
|
395
|
+
a file. A MOVED_EXPORT's warning names the
|
|
396
|
+
pinned_definition_lines only when the test
|
|
397
|
+
report listed any. A
|
|
374
398
|
cited-elsewhere one, whatever its severity,
|
|
375
399
|
takes in their place a warning that names
|
|
376
400
|
its gap_id, its source_citation, its
|
|
@@ -385,6 +409,12 @@ Apply (update-skill write.md §3):
|
|
|
385
409
|
moved, fix the entry by hand (with several
|
|
386
410
|
map_locations, whichever of those entries
|
|
387
411
|
moved), then re-run update-skill. A
|
|
412
|
+
citation_check outside-scope (no check ran:
|
|
413
|
+
the brief's scope leaves the cited file out)
|
|
414
|
+
takes no advice to exclude it, and its fix
|
|
415
|
+
for a move is to change the brief's scope so
|
|
416
|
+
it takes that file (its scope.include or
|
|
417
|
+
scope.exclude) and fix the entry by hand. A
|
|
388
418
|
citation_check that read nothing (UNREAD:
|
|
389
419
|
no-source-root, verifier-missing,
|
|
390
420
|
spot-check-failed) takes its own fix in
|
|
@@ -570,7 +600,25 @@ Gap records (update-skill gap-driven.md §3, §4 and §4a):
|
|
|
570
600
|
STRUCTURAL_FIX and metadata update entries are forwarded to merge as
|
|
571
601
|
they are (`forwarded`); every other entry is export-bearing. A severity
|
|
572
602
|
is blocking unless it is Medium, Low or Info (NON_BLOCKING), compared
|
|
573
|
-
case-insensitively, so a missing one is blocking.
|
|
603
|
+
case-insensitively, so a missing one is blocking.
|
|
604
|
+
|
|
605
|
+
The brief's scope (--brief, the skill brief) bounds the repair (#824),
|
|
606
|
+
by skf-classify-changed-files.py's load_scope and Scope.takes, loaded
|
|
607
|
+
from this folder (Category A's in-scope test: an exclude wins over an
|
|
608
|
+
include, and an empty include takes only a path with the suffix of a
|
|
609
|
+
path the gaps or the map's entries name or of the brief's language,
|
|
610
|
+
so a file with no suffix is not taken), with Category A's
|
|
611
|
+
rule that a file the map's entries[] cite is always taken. Each path is
|
|
612
|
+
tested relative to --source-root, POSIX, its `.` and `..` parts
|
|
613
|
+
resolved (an absolute path under the root loses the root's prefix).
|
|
614
|
+
An entry's outside_scope is its cited file and each of its
|
|
615
|
+
resolved_paths the scope leaves out: none of them is checked, scanned
|
|
616
|
+
or repaired from, so a declaration of another package the skill does
|
|
617
|
+
not cover is never read for it. A map entry's own file is checked
|
|
618
|
+
wherever it sits (found and MOVED_EXPORT routing does not test it).
|
|
619
|
+
Without --brief (a skill with no brief) there is no scope test and
|
|
620
|
+
nothing is left out. A --brief that names no file or cannot be read
|
|
621
|
+
is an input error (exit 1). In order:
|
|
574
622
|
|
|
575
623
|
1. Drift gate (--drift-status overridden): each export-bearing entry
|
|
576
624
|
but a MOVED_EXPORT needs the tree, with the first reason that fits:
|
|
@@ -579,8 +627,11 @@ Gap records (update-skill gap-driven.md §3, §4 and §4a):
|
|
|
579
627
|
whose map_match is not-found with no candidate that has a
|
|
580
628
|
source_file "a line and a signature from the tree", any other (a
|
|
581
629
|
not-found with such a candidate, a citation of another file than
|
|
582
|
-
the map's entries, among them) "a signature from the tree".
|
|
583
|
-
|
|
630
|
+
the map's entries, among them) "a signature from the tree". An
|
|
631
|
+
entry the brief's scope keeps from a repair before anything is read
|
|
632
|
+
for it (step 3 records it outside-scope, or cited-elsewhere with
|
|
633
|
+
citation_check outside-scope, with no check run and nothing to
|
|
634
|
+
scan) passes. Any other such entry: {"status":
|
|
584
635
|
"drift-blocked", "drift_blocked": [{gap_id, name, change_category,
|
|
585
636
|
severity, reason}]}, and no spot-check runs.
|
|
586
637
|
2. A DELETED_EXPORT without a `rescope` that holds its `amendment` and
|
|
@@ -589,7 +640,10 @@ Gap records (update-skill gap-driven.md §3, §4 and §4a):
|
|
|
589
640
|
lines rules, loaded from this folder, under --source-root:
|
|
590
641
|
DELETED_EXPORT rescoped, no check
|
|
591
642
|
MOVED_EXPORT checked at its map entry when map_match is
|
|
592
|
-
found (unknown otherwise)
|
|
643
|
+
found (unknown otherwise), a bookkeeping
|
|
644
|
+
variant its file never names against its
|
|
645
|
+
base (the verifier's fold; no other
|
|
646
|
+
category folds); under the
|
|
593
647
|
override a `moved` becomes unknown with
|
|
594
648
|
unknown_reason drift-override; its
|
|
595
649
|
pinned_definition_lines are the numbers of
|
|
@@ -599,7 +653,15 @@ Gap records (update-skill gap-driven.md §3, §4 and §4a):
|
|
|
599
653
|
ambiguous unknown, unknown_reason ambiguous (several
|
|
600
654
|
map entries of its name: no check runs)
|
|
601
655
|
not-found checked at its source_citation (no export
|
|
602
|
-
type) when it has one
|
|
656
|
+
type) when it has one, but never at a
|
|
657
|
+
cited file its outside_scope holds: no
|
|
658
|
+
check runs there, and it is unknown,
|
|
659
|
+
unknown_reason cited-elsewhere with
|
|
660
|
+
citation_check outside-scope when the map
|
|
661
|
+
holds entries of its name with a
|
|
662
|
+
source_file (never re-extracted), else
|
|
663
|
+
unknown_reason outside-scope, routed on
|
|
664
|
+
as below. When the map holds
|
|
603
665
|
entries of its name (in_map, which a
|
|
604
666
|
not-found has only for a citation of
|
|
605
667
|
another file than theirs), one of them
|
|
@@ -613,17 +675,29 @@ Gap records (update-skill gap-driven.md §3, §4 and §4a):
|
|
|
613
675
|
check's cause (missing, or its
|
|
614
676
|
unknown_reason) and its map_locations the
|
|
615
677
|
`file:line` (or file) of each candidate
|
|
616
|
-
with a source_file,
|
|
617
|
-
record. Candidates with no
|
|
618
|
-
(placeholders) do not count.
|
|
619
|
-
|
|
678
|
+
with a source_file, the map_locations null
|
|
679
|
+
on any other record. Candidates with no
|
|
680
|
+
source_file (placeholders) do not count.
|
|
681
|
+
Otherwise,
|
|
682
|
+
when its outside_scope is not empty and
|
|
683
|
+
the scope takes none of its
|
|
684
|
+
resolved_paths: unknown, unknown_reason
|
|
685
|
+
outside-scope, whatever its severity, with
|
|
686
|
+
nothing to scan (never in the plan); its
|
|
687
|
+
citation_check keeps the cause of a check
|
|
688
|
+
at a citation the scope takes (missing, or
|
|
689
|
+
its unknown_reason), null with none. The
|
|
690
|
+
citation_check is null on every record but
|
|
691
|
+
a cited-elsewhere or outside-scope one.
|
|
692
|
+
Otherwise, when that pins no line
|
|
620
693
|
(unknown or missing), or with no
|
|
621
694
|
citation: targeted re-extraction (§4a) for a
|
|
622
695
|
provenance_completeness entry with a source
|
|
623
696
|
root, for a blocking one whatever its
|
|
624
697
|
resolved_paths, and for a missing-export or
|
|
625
698
|
missing-type one with resolved_paths; else
|
|
626
|
-
unknown
|
|
699
|
+
unknown. Targeted re-extraction scans only
|
|
700
|
+
the resolved_paths the scope takes
|
|
627
701
|
A check gives `verified` (the line is a definition line, or a module
|
|
628
702
|
or package entry whose file exists), `moved` (one definition line,
|
|
629
703
|
not the recorded one: new_location), `missing` (no such file) or
|
|
@@ -646,19 +720,24 @@ Gap records (update-skill gap-driven.md §3, §4 and §4a):
|
|
|
646
720
|
candidate that has a source_file) never goes to targeted
|
|
647
721
|
re-extraction, whatever its severity: one that ends missing or
|
|
648
722
|
unknown keeps its map entries unchanged and adds none, and apply
|
|
649
|
-
warns (see "Apply").
|
|
723
|
+
warns (see "Apply"). One recorded unknown, outside-scope (its record's
|
|
724
|
+
outside_scope lists the files the scope leaves out) adds no entry
|
|
725
|
+
either, a placeholder included, whatever its severity, and apply
|
|
726
|
+
warns.
|
|
650
727
|
A NEW_EXPORT or MODIFIED_EXPORT a check gives verified or moved, at
|
|
651
728
|
its map entry or at a citation the map holds no entry of its name
|
|
652
729
|
for, is located: its signature_at is the checked file and the
|
|
653
730
|
recorded line (verified) or the new one (moved). One whose citation
|
|
654
731
|
names another file than the map's entries of its name is not (apply
|
|
655
732
|
warns, as before), nor one whose file is absolute or outside
|
|
656
|
-
--source-root, links resolved
|
|
657
|
-
|
|
658
|
-
|
|
733
|
+
--source-root, links resolved (a cited file the brief's scope leaves
|
|
734
|
+
out is never checked, so never located), nor a NEW_EXPORT
|
|
735
|
+
--judgments answers internal-unreachable (one not yet answered is
|
|
736
|
+
located, so its scan may go unused).
|
|
659
737
|
--plan stops here: {"status": "planned", "reextract": {"entries":
|
|
660
|
-
[{gap_id, name, severity, resolved_paths
|
|
661
|
-
their resolved_paths, then of the located
|
|
738
|
+
[{gap_id, name, severity, resolved_paths (those the scope takes)}],
|
|
739
|
+
"files": [the union of their resolved_paths, then of the located
|
|
740
|
+
exports' files],
|
|
662
741
|
"signatures": [{gap_id, name, file, line} of each located export]},
|
|
663
742
|
"warnings"}, the files also written to --files-out as a JSON array.
|
|
664
743
|
4. Targeted re-extraction (--remediation-records, `records`' output
|
|
@@ -677,7 +756,10 @@ Gap records (update-skill gap-driven.md §3, §4 and §4a):
|
|
|
677
756
|
a renamed re-export by its public name matches no record.
|
|
678
757
|
An entry with no match, or no path to scan, is unresolved, except a
|
|
679
758
|
missing-export or missing-type one that is not blocking, which is
|
|
680
|
-
unknown.
|
|
759
|
+
unknown. An entry with no match whose outside_scope is not empty is
|
|
760
|
+
unknown, unknown_reason outside-scope (a file the scope leaves out
|
|
761
|
+
may define it), whatever its severity and category, never
|
|
762
|
+
unresolved. Any unresolved entry: {"status": "unresolved",
|
|
681
763
|
"unresolved": [{gap_id, name, severity, remediation_paths,
|
|
682
764
|
rejected_paths, files_scanned, exports_found_in_scan}]}.
|
|
683
765
|
Each located export takes the one record of its name (or of its map
|
|
@@ -713,8 +795,10 @@ Gap records (update-skill gap-driven.md §3, §4 and §4a):
|
|
|
713
795
|
[one record per export-bearing entry: gap_id, export_name,
|
|
714
796
|
gap_category, severity, verification, in_map, map_entry,
|
|
715
797
|
provenance_citation, source_citation, new_location, unknown_reason,
|
|
716
|
-
citation_check, map_locations,
|
|
717
|
-
|
|
798
|
+
citation_check, map_locations, outside_scope (the files a gap cites or
|
|
799
|
+
its Remediation names that the brief's scope leaves out, whatever the
|
|
800
|
+
record's outcome; null when it leaves out none, or with no --brief),
|
|
801
|
+
pinned_definition_lines, resolution_source, reachability, and
|
|
718
802
|
export_type, params, return_type, extraction_method and ast_node_type
|
|
719
803
|
(a located export's record's, step 4; else null, and apply takes the
|
|
720
804
|
signature of an entry it adds from merge.md's merge-records.json)],
|
|
@@ -773,6 +857,7 @@ import hashlib
|
|
|
773
857
|
import importlib.util
|
|
774
858
|
import json
|
|
775
859
|
import os
|
|
860
|
+
import posixpath
|
|
776
861
|
import re
|
|
777
862
|
import subprocess
|
|
778
863
|
import sys
|
|
@@ -1999,17 +2084,22 @@ def _apply_gap_driven(pmap: _Map, records: dict, merge_records: dict | None, dri
|
|
|
1999
2084
|
and reason in UNKNOWN_REASONS else ""
|
|
2000
2085
|
|
|
2001
2086
|
def unchecked(r: dict) -> str | None:
|
|
2002
|
-
"""The warning for
|
|
2003
|
-
|
|
2087
|
+
"""The warning for an export the map holds that its spot-check could not locate, which stays unchanged: a
|
|
2088
|
+
blocking NEW_EXPORT or MODIFIED_EXPORT (missing, or unknown for a cause its unknown_reason names), or a
|
|
2089
|
+
MOVED_EXPORT whose map entry's file is gone (missing, rule R5), whatever its severity. It gives the gap, the
|
|
2004
2090
|
export, the outcome, its cause and the one fix a re-run acts on; None for any other record."""
|
|
2005
|
-
outcome = r.get("verification")
|
|
2006
|
-
|
|
2007
|
-
|
|
2091
|
+
outcome, category = r.get("verification"), r.get("gap_category")
|
|
2092
|
+
gone = outcome == "missing" and category == "MOVED_EXPORT" # R5 cannot point an entry at another file
|
|
2093
|
+
if not in_map(r) or not gone and (
|
|
2094
|
+
outcome not in ("missing", "unknown") or not _blocking(r.get("severity"))
|
|
2095
|
+
or category not in ("NEW_EXPORT", "MODIFIED_EXPORT")):
|
|
2008
2096
|
return None
|
|
2009
2097
|
name, gap = _name_of(r), r.get("gap_id")
|
|
2010
2098
|
path = _location(r.get("map_entry"))[0]
|
|
2011
2099
|
cause = f"its map entry's file{f' {path}' if path else ''} is gone" if outcome == "missing" else why(r)
|
|
2012
|
-
|
|
2100
|
+
lines = r.get("pinned_definition_lines") if gone else None
|
|
2101
|
+
detail = "; ".join(part for part in (
|
|
2102
|
+
cause, note, f"definition lines per the test report: {lines}" if lines else "") if part)
|
|
2013
2103
|
if r.get("unknown_reason") == "ambiguous": # a Source at the definition lets the re-run's lookup pick one
|
|
2014
2104
|
fix = ("its entries are left unchanged. Fix its entries by hand, or give the gap a `file:line` Source at "
|
|
2015
2105
|
"its definition when that file holds only one of them, then re-run update-skill")
|
|
@@ -2028,7 +2118,8 @@ def _apply_gap_driven(pmap: _Map, records: dict, merge_records: dict | None, dri
|
|
|
2028
2118
|
cited_file, cited_line = _location(r.get("source_citation"))
|
|
2029
2119
|
cited = _where(cited_file, cited_line) or cited_file
|
|
2030
2120
|
check = r.get("citation_check") if isinstance(r.get("citation_check"), str) else None
|
|
2031
|
-
words = "no such file" if check == "missing" else
|
|
2121
|
+
words = "no such file" if check == "missing" else OUTSIDE_SCOPE_CHECK if check == "outside-scope" \
|
|
2122
|
+
else UNKNOWN_REASONS.get(check or "")
|
|
2032
2123
|
where = [w for w in r.get("map_locations") or [] if isinstance(w, str) and w]
|
|
2033
2124
|
detail = "; ".join(part for part in (
|
|
2034
2125
|
why(r), f"it cites {cited}" if cited else "",
|
|
@@ -2040,16 +2131,49 @@ def _apply_gap_driven(pmap: _Map, records: dict, merge_records: dict | None, dri
|
|
|
2040
2131
|
"re-extracted for it."
|
|
2041
2132
|
if check in UNREAD: # nothing was read at the cited file: make the check read it first
|
|
2042
2133
|
return f"{head} Nothing was read at the cited file: {UNREAD[check]}, then re-run update-skill"
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2134
|
+
if check == "outside-scope": # the scope already leaves the cited file out: only a move there brings it in
|
|
2135
|
+
moved = f"when an export moved there, {TAKE_FILE} and fix by hand whichever of those entries moved" \
|
|
2136
|
+
if several else f"when the export moved there, {TAKE_FILE} and fix the map entry by hand"
|
|
2137
|
+
else:
|
|
2138
|
+
moved = "when an export moved, fix by hand whichever of those entries moved" if several \
|
|
2139
|
+
else "when the export moved, fix the map entry by hand"
|
|
2140
|
+
scope = "" if check in ("missing", "outside-scope") else (
|
|
2141
|
+
", and if the gap comes back, exclude the cited file in the brief's scope when the skill does not cover it")
|
|
2047
2142
|
return (f"{head} When the map is right, re-run Test Skill, since the test compared another file's "
|
|
2048
2143
|
f"declaration{scope}; {moved} (set its source_file and source_line to the line that defines it), "
|
|
2049
2144
|
f"then re-run update-skill")
|
|
2050
2145
|
|
|
2146
|
+
def scoped_out(r: dict) -> bool:
|
|
2147
|
+
"""True for an `unknown` record whose gap the brief's scope keeps from a repair (outside-scope)."""
|
|
2148
|
+
return r.get("verification") == "unknown" and r.get("unknown_reason") == "outside-scope"
|
|
2149
|
+
|
|
2150
|
+
def outside(r: dict) -> str | None:
|
|
2151
|
+
"""The warning for a NEW_EXPORT or MODIFIED_EXPORT recorded unknown, outside-scope, whatever its severity: the
|
|
2152
|
+
gap, the export, the files the brief's scope leaves out and the fix for each case; None for any other
|
|
2153
|
+
record."""
|
|
2154
|
+
if not scoped_out(r):
|
|
2155
|
+
return None
|
|
2156
|
+
name, gap = _name_of(r), r.get("gap_id")
|
|
2157
|
+
left = [p for p in r.get("outside_scope") or [] if isinstance(p, str) and p]
|
|
2158
|
+
# the cause of a check at an in-scope citation that pinned no line (its file the scope takes)
|
|
2159
|
+
check = r.get("citation_check") if isinstance(r.get("citation_check"), str) else None
|
|
2160
|
+
words = "no such file" if check == "missing" else UNKNOWN_REASONS.get(check or "")
|
|
2161
|
+
cited = _where(*_location(r.get("source_citation"))) or _location(r.get("source_citation"))[0]
|
|
2162
|
+
checked = f"its check there: {check}{f' ({words})' if words else ''}" if check else ""
|
|
2163
|
+
detail = "; ".join(part for part in (
|
|
2164
|
+
why(r), f"left out: {', '.join(left)}" if left else "", f"it cites {cited}" if check and cited else "",
|
|
2165
|
+
checked, note) if part)
|
|
2166
|
+
head = f"provenance: {name}{f' ({gap})' if gap else ''}: unknown ({detail}); nothing is added or re-extracted " \
|
|
2167
|
+
"for it."
|
|
2168
|
+
if check in UNREAD: # nothing was read at the cited file the scope takes: make the check read it first
|
|
2169
|
+
return f"{head} Nothing was read at the cited file: {UNREAD[check]}, then re-run update-skill"
|
|
2170
|
+
covers = "When the skill covers one of those files, change the brief's scope so it takes it" if len(left) > 1 \
|
|
2171
|
+
else "When the skill covers that file, change the brief's scope so it takes it"
|
|
2172
|
+
return (f"{head} {covers} (its scope.include or scope.exclude), then re-run update-skill; otherwise re-run "
|
|
2173
|
+
"Test Skill, since the test compared a declaration the skill does not cover")
|
|
2174
|
+
|
|
2051
2175
|
blocking = [{"export_name": _name_of(r), "severity": r.get("severity")} for r in verification
|
|
2052
|
-
if r.get("verification") == "unknown" and not in_map(r)
|
|
2176
|
+
if r.get("verification") == "unknown" and not in_map(r) and not scoped_out(r)
|
|
2053
2177
|
and r.get("gap_category") in ("NEW_EXPORT", "MODIFIED_EXPORT") and _blocking(r.get("severity"))]
|
|
2054
2178
|
if blocking:
|
|
2055
2179
|
return blocking
|
|
@@ -2059,7 +2183,8 @@ def _apply_gap_driven(pmap: _Map, records: dict, merge_records: dict | None, dri
|
|
|
2059
2183
|
entry_file, entry_line = _location(r.get("map_entry"))
|
|
2060
2184
|
index = pmap.find(name, entry_file, entry_line, reexport=True) if r.get("map_entry") else None
|
|
2061
2185
|
lines = r.get("pinned_definition_lines")
|
|
2062
|
-
|
|
2186
|
+
# rule R5's lines, only when the test report listed any (a citation that did not resolve lists none)
|
|
2187
|
+
r5 = f"; definition lines per the test report: {lines}" if category == "MOVED_EXPORT" and lines else ""
|
|
2063
2188
|
if outcome == "rescoped":
|
|
2064
2189
|
if index is not None:
|
|
2065
2190
|
pmap.remove([index])
|
|
@@ -2101,14 +2226,15 @@ def _apply_gap_driven(pmap: _Map, records: dict, merge_records: dict | None, dri
|
|
|
2101
2226
|
"export_name": name, **signature(r)[0],
|
|
2102
2227
|
"source_file": path, "source_line": line, **_labels("source-read", None)},
|
|
2103
2228
|
keep=("ast_node_type",)), pmap.library_for(path, name)))
|
|
2104
|
-
elif outcome == "unknown" and not in_map(r) and category in ("NEW_EXPORT", "MODIFIED_EXPORT")
|
|
2229
|
+
elif outcome == "unknown" and not in_map(r) and category in ("NEW_EXPORT", "MODIFIED_EXPORT") \
|
|
2230
|
+
and not scoped_out(r): # an outside-scope one takes no placeholder: it may be another package's
|
|
2105
2231
|
pmap.add(_new_entry(_without_none({
|
|
2106
2232
|
"export_name": name, **signature(r)[0],
|
|
2107
2233
|
**_labels("source-read", None)}, keep=("ast_node_type",)), pmap.library_for(None, name)))
|
|
2108
2234
|
else:
|
|
2109
2235
|
if r.get("unknown_reason") == "drift-override":
|
|
2110
2236
|
warnings.append(f"provenance: {name}: unknown ({note}; no line taken from HEAD{r5})")
|
|
2111
|
-
elif (warning := elsewhere(r) or unchecked(r)) is not None:
|
|
2237
|
+
elif (warning := elsewhere(r) or outside(r) or unchecked(r)) is not None:
|
|
2112
2238
|
warnings.append(warning)
|
|
2113
2239
|
elif drift:
|
|
2114
2240
|
warnings.append(f"provenance: {name}: {outcome} ({why(r) + '; ' if why(r) else ''}{note}{r5})")
|
|
@@ -2765,7 +2891,13 @@ UNKNOWN_REASONS = {
|
|
|
2765
2891
|
"no-source-root": "no source tree to check",
|
|
2766
2892
|
"drift-override": "the drift override kept its line from being moved",
|
|
2767
2893
|
"cited-elsewhere": "the gap cites another file than the provenance map's entries of its name",
|
|
2894
|
+
"outside-scope": "the brief's scope leaves out a file the gap cites or its Remediation names, so the export is "
|
|
2895
|
+
"not read there",
|
|
2768
2896
|
}
|
|
2897
|
+
# A cited-elsewhere entry's citation_check when the brief's scope leaves the cited file out, so no check ran there.
|
|
2898
|
+
OUTSIDE_SCOPE_CHECK = "not run: the brief's scope leaves it out"
|
|
2899
|
+
# The fix for a file the brief's scope leaves out that the skill covers: an include may miss it, or an exclude take it.
|
|
2900
|
+
TAKE_FILE = "change the brief's scope so it takes that file (its scope.include or scope.exclude)"
|
|
2769
2901
|
# A cited-elsewhere entry's citation_check that read nothing at the cited file, with the fix that lets a re-run read it.
|
|
2770
2902
|
UNREAD = {
|
|
2771
2903
|
"no-source-root": "give update-skill the source tree",
|
|
@@ -2795,6 +2927,95 @@ def _verifier():
|
|
|
2795
2927
|
return _VERIFIER or None
|
|
2796
2928
|
|
|
2797
2929
|
|
|
2930
|
+
CLASSIFIER_FILE = "skf-classify-changed-files.py" # load_scope: the brief's scope (--brief)
|
|
2931
|
+
|
|
2932
|
+
|
|
2933
|
+
def _brief_scope(path: Path, cited: set[str] | frozenset = frozenset()):
|
|
2934
|
+
"""The skill brief's scope by skf-classify-changed-files.py's load_scope, loaded from this folder: its
|
|
2935
|
+
takes(path) is Category A's in-scope test (an exclude wins over an include; an empty include takes a path only
|
|
2936
|
+
when its suffix is the suffix of a `cited` path, the files the gaps and the map's entries name, or of the brief's
|
|
2937
|
+
language, so a file with no suffix is never taken, and an exclude still leaves a path out).
|
|
2938
|
+
Raises ValueError, on one line, for a brief that names no file or cannot be read, and for a scope test that
|
|
2939
|
+
cannot be loaded."""
|
|
2940
|
+
try:
|
|
2941
|
+
spec = importlib.util.spec_from_file_location(
|
|
2942
|
+
"skf_classify_changed_files_for_gap_records", Path(__file__).resolve().parent / CLASSIFIER_FILE)
|
|
2943
|
+
if spec is None or spec.loader is None:
|
|
2944
|
+
raise ImportError(f"{CLASSIFIER_FILE} not found")
|
|
2945
|
+
classifier = importlib.util.module_from_spec(spec)
|
|
2946
|
+
spec.loader.exec_module(classifier)
|
|
2947
|
+
except Exception as exc: # noqa: BLE001 - a classifier (or the resolver it loads) missing or failing
|
|
2948
|
+
raise ValueError(f"cannot load the brief's scope test, {CLASSIFIER_FILE} beside "
|
|
2949
|
+
f"{Path(__file__).name}: {exc}; re-install SKF") from exc
|
|
2950
|
+
try:
|
|
2951
|
+
return classifier.load_scope(path, cited)
|
|
2952
|
+
except ValueError as exc: # a brief it cannot find or read (a YAML error spans several lines)
|
|
2953
|
+
raise ValueError(re.sub(r"\s*[\r\n]\s*", " ", str(exc).strip())) from exc
|
|
2954
|
+
except Exception as exc: # noqa: BLE001 - an out-of-date classifier, or a resolver beside it that fails
|
|
2955
|
+
raise ValueError(f"cannot load the brief's scope test, {CLASSIFIER_FILE} beside "
|
|
2956
|
+
f"{Path(__file__).name}: {exc}; re-install SKF") from exc
|
|
2957
|
+
|
|
2958
|
+
|
|
2959
|
+
def _takes(scope, path: str) -> bool:
|
|
2960
|
+
"""True when the brief's scope takes `path`. Raises ValueError when its test fails (the resolver it calls cannot
|
|
2961
|
+
be loaded or is out of date)."""
|
|
2962
|
+
try:
|
|
2963
|
+
return bool(scope.takes(path))
|
|
2964
|
+
except Exception as exc: # noqa: BLE001 - whatever the scope test raises, the run reports it on one line
|
|
2965
|
+
raise ValueError(f"the brief's scope test failed on {path}: {exc}; re-install SKF") from exc
|
|
2966
|
+
|
|
2967
|
+
|
|
2968
|
+
def _scope_rel(path: object, root: Path | None) -> str | None:
|
|
2969
|
+
"""`path` as the scope test reads it: POSIX, relative to the source root (an absolute path under it loses the
|
|
2970
|
+
root's prefix), its `.` and `..` parts resolved lexically (posixpath.normpath); None for no path."""
|
|
2971
|
+
text = _norm_path(path)
|
|
2972
|
+
if text is None:
|
|
2973
|
+
return None
|
|
2974
|
+
if root is not None:
|
|
2975
|
+
bases = [root.as_posix()]
|
|
2976
|
+
try:
|
|
2977
|
+
bases.append(root.resolve().as_posix())
|
|
2978
|
+
except OSError:
|
|
2979
|
+
pass
|
|
2980
|
+
for base in dict.fromkeys(b.rstrip("/") for b in bases if b.rstrip("/")):
|
|
2981
|
+
if text.startswith(base + "/"):
|
|
2982
|
+
text = text[len(base) + 1:]
|
|
2983
|
+
break
|
|
2984
|
+
return posixpath.normpath(text)
|
|
2985
|
+
|
|
2986
|
+
|
|
2987
|
+
def _scope_test(scope, provenance: dict | None, root: Path | None):
|
|
2988
|
+
"""The test gap-records applies to each file a gap cites or its Remediation names: True for a file the
|
|
2989
|
+
provenance map's entries[] cite (Category A takes each file the map tracks whatever the scope says) and for one
|
|
2990
|
+
the brief's scope takes, each path read by _scope_rel."""
|
|
2991
|
+
tracked = {_scope_rel(_file_of(e), root) for e in _entries_of(provenance)} - {None}
|
|
2992
|
+
|
|
2993
|
+
def takes(path: str) -> bool:
|
|
2994
|
+
rel = _scope_rel(path, root)
|
|
2995
|
+
return rel is not None and (rel in tracked or _takes(scope, rel))
|
|
2996
|
+
|
|
2997
|
+
return takes
|
|
2998
|
+
|
|
2999
|
+
|
|
3000
|
+
def _map_paths(provenance: object) -> set[str]:
|
|
3001
|
+
"""The files the provenance map's entries[] cite (load_scope's `cited`, with the gaps' paths)."""
|
|
3002
|
+
return {p for p in (_file_of(e) for e in _entries_of(provenance)) if p}
|
|
3003
|
+
|
|
3004
|
+
|
|
3005
|
+
def _gap_paths(manifest: object) -> set[str]:
|
|
3006
|
+
"""Every file a manifest's entries name: each citation's file and resolved path (load_scope's `cited`)."""
|
|
3007
|
+
entries = manifest.get("entries") if isinstance(manifest, dict) else None
|
|
3008
|
+
paths = set()
|
|
3009
|
+
for entry in entries if isinstance(entries, list) else []:
|
|
3010
|
+
if not isinstance(entry, dict):
|
|
3011
|
+
continue
|
|
3012
|
+
citation = entry.get("source_citation") if isinstance(entry.get("source_citation"), dict) else {}
|
|
3013
|
+
for path in [citation.get("file"), *(entry.get("resolved_paths") or [])]:
|
|
3014
|
+
if _norm_path(path):
|
|
3015
|
+
paths.add(_norm_path(path))
|
|
3016
|
+
return paths
|
|
3017
|
+
|
|
3018
|
+
|
|
2798
3019
|
def _blocking(severity: object) -> bool:
|
|
2799
3020
|
return str(severity or "").strip().lower() not in NON_BLOCKING
|
|
2800
3021
|
|
|
@@ -2832,9 +3053,11 @@ def _drift_reason(entry: dict) -> str | None:
|
|
|
2832
3053
|
|
|
2833
3054
|
|
|
2834
3055
|
def _spot_check(root: Path | None, path: str | None, name: str, line: int | None, export_type: object,
|
|
2835
|
-
answers: dict) -> tuple[str, int | None, bool, str | None, str | None]:
|
|
3056
|
+
answers: dict, fold: bool = False) -> tuple[str, int | None, bool, str | None, str | None]:
|
|
2836
3057
|
"""(outcome, the line a `moved` export moves to, whether it waits on a `declaring_line` answer, why the
|
|
2837
|
-
check failed, why it is `unknown`: an UNKNOWN_REASONS key).
|
|
3058
|
+
check failed, why it is `unknown`: an UNKNOWN_REASONS key). `fold` (rule R5's MOVED_EXPORT only) checks a
|
|
3059
|
+
bookkeeping variant its file never names against its base; any other gap judges the name as written, so a
|
|
3060
|
+
fabricated variant is never located at its base's line."""
|
|
2838
3061
|
if root is None:
|
|
2839
3062
|
return "unknown", None, False, None, "no-source-root"
|
|
2840
3063
|
verifier = _verifier()
|
|
@@ -2844,7 +3067,7 @@ def _spot_check(root: Path | None, path: str | None, name: str, line: int | None
|
|
|
2844
3067
|
return "unknown", None, False, None, "no-line"
|
|
2845
3068
|
try:
|
|
2846
3069
|
report = verifier.definition_lines_report(path, name, root, line,
|
|
2847
|
-
export_type if isinstance(export_type, str) else None)
|
|
3070
|
+
export_type if isinstance(export_type, str) else None, fold)
|
|
2848
3071
|
except Exception as exc: # noqa: BLE001 - a file it cannot read, or any verifier failure: this entry only
|
|
2849
3072
|
return "unknown", None, False, f"{type(exc).__name__}: {exc}", "spot-check-failed"
|
|
2850
3073
|
check = report.get("line_check")
|
|
@@ -2894,7 +3117,7 @@ def _where(path: object, line: object) -> str | None:
|
|
|
2894
3117
|
class _GapEntry:
|
|
2895
3118
|
"""One export-bearing manifest entry as gap-records routes it."""
|
|
2896
3119
|
|
|
2897
|
-
def __init__(self, entry: dict, answers: dict):
|
|
3120
|
+
def __init__(self, entry: dict, answers: dict, takes=None):
|
|
2898
3121
|
self.entry = entry
|
|
2899
3122
|
self.answers = answers
|
|
2900
3123
|
self.name = _name_of(entry) or ""
|
|
@@ -2912,9 +3135,17 @@ class _GapEntry:
|
|
|
2912
3135
|
self.map_locations = list(dict.fromkeys(
|
|
2913
3136
|
_where(c.get("source_file"), c.get("source_line")) or _norm_path(c.get("source_file"))
|
|
2914
3137
|
for c in match.get("candidates") or [] if isinstance(c, dict) and _norm_path(c.get("source_file"))))
|
|
2915
|
-
|
|
3138
|
+
# a cited-elsewhere or outside-scope entry's citation check: missing, its reason, or outside-scope
|
|
3139
|
+
self.citation_check: str | None = None
|
|
3140
|
+
self.cited_cause: str | None = None # why a check at an in-scope citation pinned no line: missing, its reason
|
|
2916
3141
|
citation = entry.get("source_citation")
|
|
2917
3142
|
self.citation = citation if isinstance(citation, dict) else None
|
|
3143
|
+
self.cited_file = _norm_path(self.citation.get("file")) if self.citation else None
|
|
3144
|
+
# the cited file and each resolved path the brief's scope leaves out (#824): none is checked, scanned or
|
|
3145
|
+
# repaired from; with no brief there is no scope test (`takes` None), and nothing is left out
|
|
3146
|
+
self.outside_scope: list[str] = [] if takes is None else list(dict.fromkeys(
|
|
3147
|
+
p for p in (self.cited_file, *(_norm_path(p) for p in entry.get("resolved_paths") or []))
|
|
3148
|
+
if p and not takes(p)))
|
|
2918
3149
|
self.recorded: tuple[str | None, int | None] = (None, None) # the file and line a check read
|
|
2919
3150
|
self.outcome: str | None = None
|
|
2920
3151
|
self.new_line: int | None = None
|
|
@@ -2930,14 +3161,31 @@ class _GapEntry:
|
|
|
2930
3161
|
def blocking(self) -> bool:
|
|
2931
3162
|
return _blocking(self.entry.get("severity"))
|
|
2932
3163
|
|
|
3164
|
+
@property
|
|
3165
|
+
def scoped_out(self) -> bool:
|
|
3166
|
+
"""True for an entry route() records outside-scope (or cited-elsewhere with citation_check outside-scope)
|
|
3167
|
+
before anything is read for it: no in-scope citation to check and no in-scope file to scan. The drift gate
|
|
3168
|
+
passes it, since nothing it writes comes from the tree."""
|
|
3169
|
+
if self.category not in ("NEW_EXPORT", "MODIFIED_EXPORT") or self.match_status != "not-found":
|
|
3170
|
+
return False
|
|
3171
|
+
cited_out = self.cited_file is not None and self.cited_file in self.outside_scope
|
|
3172
|
+
if self.citation and not cited_out: # an in-scope citation is checked
|
|
3173
|
+
return False
|
|
3174
|
+
if self.map_locations: # with the cited file left out: cited-elsewhere, never checked; with none, as before
|
|
3175
|
+
return cited_out
|
|
3176
|
+
return bool(self.outside_scope) and not self.paths
|
|
3177
|
+
|
|
2933
3178
|
@property
|
|
2934
3179
|
def paths(self) -> list[str]:
|
|
2935
|
-
|
|
3180
|
+
"""The resolved paths targeted re-extraction scans: each one the brief's scope takes."""
|
|
3181
|
+
return [p for p in (_norm_path(p) for p in self.entry.get("resolved_paths") or [])
|
|
3182
|
+
if p and p not in self.outside_scope]
|
|
2936
3183
|
|
|
2937
3184
|
def check(self, root: Path | None, path: object, line: object, export_type: object) -> None:
|
|
2938
3185
|
self.recorded = (_norm_path(path), _line_of(line))
|
|
2939
3186
|
self.outcome, self.new_line, self.waits, self.failure, self.unknown_reason = _spot_check(
|
|
2940
|
-
root, self.recorded[0], self.declared, self.recorded[1], export_type, self.answers
|
|
3187
|
+
root, self.recorded[0], self.declared, self.recorded[1], export_type, self.answers,
|
|
3188
|
+
self.category == "MOVED_EXPORT")
|
|
2941
3189
|
|
|
2942
3190
|
def locate(self, root: Path | None) -> None:
|
|
2943
3191
|
"""Record where a check located the export (the checked file, at the recorded line or the one it moved
|
|
@@ -2978,18 +3226,31 @@ class _GapEntry:
|
|
|
2978
3226
|
if self.match_status == "ambiguous": # several map entries of its name: no check, apply warns
|
|
2979
3227
|
self.outcome, self.unknown_reason = "unknown", "ambiguous"
|
|
2980
3228
|
return
|
|
2981
|
-
if self.citation:
|
|
3229
|
+
if self.citation and self.cited_file is not None and self.cited_file in self.outside_scope:
|
|
3230
|
+
# the brief's scope leaves the cited file out: the skill does not cover its declaration, so no check runs
|
|
3231
|
+
# there (#824); the fallbacks below scan only the files the scope takes
|
|
3232
|
+
if self.map_locations: # the map holds its name at other files: cited elsewhere, and never checked
|
|
3233
|
+
self.outcome, self.unknown_reason, self.citation_check = "unknown", "cited-elsewhere", "outside-scope"
|
|
3234
|
+
return
|
|
3235
|
+
self.outcome, self.unknown_reason = "unknown", "outside-scope"
|
|
3236
|
+
elif self.citation:
|
|
2982
3237
|
self.check(root, self.citation.get("file"), self.citation.get("line"), None)
|
|
2983
3238
|
if self.waits or self.outcome in ("verified", "moved"):
|
|
2984
3239
|
if not self.in_map: # a citation of another file than the map's entries of its name takes none
|
|
2985
3240
|
self.locate(root)
|
|
2986
3241
|
return
|
|
3242
|
+
self.cited_cause = "missing" if self.outcome == "missing" else self.unknown_reason
|
|
2987
3243
|
if self.match_status == "not-found" and self.map_locations:
|
|
2988
3244
|
# the map holds its name at other files: the cited file's declaration may be another package's, so it is
|
|
2989
3245
|
# never re-extracted there (#824); the record keeps the check's cause, and apply warns with what to fix
|
|
2990
3246
|
self.citation_check = "missing" if self.outcome == "missing" else self.unknown_reason
|
|
2991
3247
|
self.outcome, self.unknown_reason = "unknown", "cited-elsewhere"
|
|
2992
3248
|
return
|
|
3249
|
+
if self.outside_scope and not self.paths:
|
|
3250
|
+
# no file the brief's scope takes is left to scan: never in the plan (#824); the record keeps the cause of
|
|
3251
|
+
# a check at an in-scope citation, which apply's warning names
|
|
3252
|
+
self.outcome, self.unknown_reason, self.citation_check = "unknown", "outside-scope", self.cited_cause
|
|
3253
|
+
return
|
|
2993
3254
|
if self.entry.get("provenance_completeness") and root is not None:
|
|
2994
3255
|
self.reextract = True
|
|
2995
3256
|
elif self.blocking:
|
|
@@ -3050,6 +3311,7 @@ class _GapEntry:
|
|
|
3050
3311
|
signed = self.signature() or {}
|
|
3051
3312
|
recorded = _where(*self.recorded)
|
|
3052
3313
|
elsewhere = self.outcome == "unknown" and self.unknown_reason == "cited-elsewhere"
|
|
3314
|
+
outside = self.outcome == "unknown" and self.unknown_reason == "outside-scope"
|
|
3053
3315
|
if self.outcome == "re-extracted":
|
|
3054
3316
|
citation = self.new_location()
|
|
3055
3317
|
elif self.outcome in ("verified", "moved", "missing", "rescoped") and recorded:
|
|
@@ -3067,8 +3329,9 @@ class _GapEntry:
|
|
|
3067
3329
|
"source_citation": self.citation,
|
|
3068
3330
|
"new_location": self.new_location(),
|
|
3069
3331
|
"unknown_reason": self.unknown_reason if self.outcome == "unknown" else None,
|
|
3070
|
-
"citation_check": self.citation_check if elsewhere else None,
|
|
3332
|
+
"citation_check": self.citation_check if elsewhere or outside else None,
|
|
3071
3333
|
"map_locations": self.map_locations if elsewhere else None,
|
|
3334
|
+
"outside_scope": self.outside_scope or None, # whatever the outcome: each file the scope left out
|
|
3072
3335
|
"pinned_definition_lines": _pinned_lines(self.entry.get("remediation"))
|
|
3073
3336
|
if self.category == "MOVED_EXPORT" else None,
|
|
3074
3337
|
"resolution_source": "remediation-paths" if self.outcome == "re-extracted" else None,
|
|
@@ -3106,8 +3369,9 @@ class _GapEntry:
|
|
|
3106
3369
|
|
|
3107
3370
|
def gap_records(manifest: dict, provenance: dict | None, *, source_root: Path | None, drift_status: str = "ok",
|
|
3108
3371
|
judgments: dict | None = None, remediation: dict | None = None, plan: bool = False,
|
|
3109
|
-
tier: str | None = None) -> tuple[dict | None, dict]:
|
|
3110
|
-
"""(reextract-records.json or None, the summary): see "Gap records" in the module docstring.
|
|
3372
|
+
tier: str | None = None, scope=None) -> tuple[dict | None, dict]:
|
|
3373
|
+
"""(reextract-records.json or None, the summary): see "Gap records" in the module docstring. `scope` is the
|
|
3374
|
+
brief's (_brief_scope), or None for a skill with no brief."""
|
|
3111
3375
|
if not isinstance(manifest, dict) or not isinstance(manifest.get("entries"), list):
|
|
3112
3376
|
raise ValueError("the change manifest has no `entries` array: is it translate's output?")
|
|
3113
3377
|
entries = [e for e in manifest["entries"] if isinstance(e, dict)]
|
|
@@ -3115,13 +3379,14 @@ def gap_records(manifest: dict, provenance: dict | None, *, source_root: Path |
|
|
|
3115
3379
|
root = source_root if source_root is not None and source_root.is_dir() else None
|
|
3116
3380
|
forwarded = [{"gap_id": e.get("gap_id"), "change_category": e.get("change_category")}
|
|
3117
3381
|
for e in entries if e.get("change_category") not in EXPORT_CATEGORIES]
|
|
3118
|
-
|
|
3382
|
+
takes = None if scope is None else _scope_test(scope, provenance, source_root)
|
|
3383
|
+
routed = [_GapEntry(e, judgments.get(e.get("gap_id")) or {}, takes) for e in entries
|
|
3119
3384
|
if e.get("change_category") in EXPORT_CATEGORIES]
|
|
3120
3385
|
drift = drift_status == "overridden"
|
|
3121
3386
|
if drift:
|
|
3122
3387
|
blocked = [{"gap_id": g.entry.get("gap_id"), "name": g.name, "change_category": g.category,
|
|
3123
3388
|
"severity": g.entry.get("severity"), "reason": _drift_reason(g.entry)}
|
|
3124
|
-
for g in routed if _drift_reason(g.entry)]
|
|
3389
|
+
for g in routed if _drift_reason(g.entry) and not g.scoped_out] # nothing is read for it
|
|
3125
3390
|
if blocked:
|
|
3126
3391
|
return None, {"status": "drift-blocked", "drift_blocked": blocked}
|
|
3127
3392
|
bare = [g.name for g in routed if g.category == "DELETED_EXPORT" and not (
|
|
@@ -3168,6 +3433,9 @@ def gap_records(manifest: dict, provenance: dict | None, *, source_root: Path |
|
|
|
3168
3433
|
files_scanned, found = g.match(blocks)
|
|
3169
3434
|
if g.record is not None:
|
|
3170
3435
|
continue
|
|
3436
|
+
if g.outside_scope: # none of the files the scope takes holds it, and a file it leaves out may (#824)
|
|
3437
|
+
g.outcome, g.unknown_reason, g.citation_check = "unknown", "outside-scope", g.cited_cause
|
|
3438
|
+
continue
|
|
3171
3439
|
if not g.blocking and g.entry.get("category") in DOCUMENT_CATEGORIES:
|
|
3172
3440
|
g.outcome = "unknown"
|
|
3173
3441
|
continue
|
|
@@ -3572,15 +3840,22 @@ def _cmd_gap_records(args: argparse.Namespace) -> int:
|
|
|
3572
3840
|
raise ValueError("--evidence and --tier go together")
|
|
3573
3841
|
judgments = _gap_judgments(_load_json_file(Path(args.judgments), "--judgments file")) \
|
|
3574
3842
|
if args.judgments else {}
|
|
3843
|
+
manifest = _load_json_file(Path(args.manifest), "--manifest file")
|
|
3844
|
+
provenance = _load_json_file(Path(args.provenance_map), "--provenance-map file")
|
|
3845
|
+
if args.brief is not None and not args.brief.strip():
|
|
3846
|
+
raise ValueError("--brief is empty (an unbound brief path?): pass the skill brief, or leave --brief out")
|
|
3847
|
+
scope = _brief_scope(Path(args.brief), _gap_paths(manifest) | _map_paths(provenance)) \
|
|
3848
|
+
if args.brief is not None else None
|
|
3575
3849
|
records, summary = gap_records(
|
|
3576
|
-
|
|
3577
|
-
|
|
3850
|
+
manifest,
|
|
3851
|
+
provenance,
|
|
3578
3852
|
source_root=Path(args.source_root) if args.source_root else None,
|
|
3579
3853
|
drift_status=args.drift_status,
|
|
3580
3854
|
judgments=judgments,
|
|
3581
3855
|
remediation=_optional_json(args.remediation_records, "--remediation-records file"),
|
|
3582
3856
|
plan=args.plan,
|
|
3583
3857
|
tier=args.tier,
|
|
3858
|
+
scope=scope,
|
|
3584
3859
|
)
|
|
3585
3860
|
if summary["status"] == "planned" and args.files_out:
|
|
3586
3861
|
_write_json_atomic(Path(args.files_out), summary["reextract"]["files"])
|
|
@@ -3803,6 +4078,9 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
3803
4078
|
p_gap.add_argument("--manifest", required=True, metavar="FILE", help="skf-parse-gaps.py translate's manifest")
|
|
3804
4079
|
p_gap.add_argument("--provenance-map", required=True, metavar="FILE", help="the map the repair starts from")
|
|
3805
4080
|
p_gap.add_argument("--source-root", metavar="DIR", help="the source tree the spot-checks read")
|
|
4081
|
+
p_gap.add_argument("--brief", metavar="FILE",
|
|
4082
|
+
help="the skill brief (skill-brief.yaml): no file a gap cites or its Remediation names that its "
|
|
4083
|
+
"scope leaves out is checked, scanned or repaired from; without it there is no scope test")
|
|
3806
4084
|
p_gap.add_argument("--drift-status", choices=DRIFT_STATUSES, default="ok",
|
|
3807
4085
|
help="the workspace drift check's status; overridden runs the drift gate")
|
|
3808
4086
|
p_gap.add_argument("--judgments", metavar="FILE", help="the answers to its needs_judgment[], keyed by gap id")
|