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
|
@@ -39,13 +39,21 @@ reading files and writing one JSON object:
|
|
|
39
39
|
surface the enumerated source surface, from every input given (their
|
|
40
40
|
union):
|
|
41
41
|
--extraction skf-extract-public-api.py --mode full output:
|
|
42
|
-
the names in entry_point_diff.public,
|
|
42
|
+
the names in entry_point_diff.public, each
|
|
43
|
+
other in-scope definition of one
|
|
44
|
+
(entry_point_diff.other_definitions, #830) a
|
|
45
|
+
record of its own at its file, a
|
|
43
46
|
namespace with a `members` list given as its
|
|
44
47
|
members (the names its module passes on, each
|
|
45
48
|
at its file, once with a public name of the
|
|
46
49
|
same name and file), less the outside_scope
|
|
47
|
-
names (defined in a file
|
|
48
|
-
|
|
50
|
+
names (defined in a file out of scope), which
|
|
51
|
+
`excluded.outsideScope` lists with their file,
|
|
52
|
+
line and entry point and, when the brief
|
|
53
|
+
scopes the file out, why: the scope.exclude
|
|
54
|
+
glob that matches it (`excludeGlob`) or no
|
|
55
|
+
scope.include glob covering it
|
|
56
|
+
(`outsideInclude`); exports[] only gives each name's
|
|
49
57
|
kind, line and signature line, looked up by the
|
|
50
58
|
name its defining file gives it (an aliased
|
|
51
59
|
re-export's `local`) and that file.
|
|
@@ -112,10 +120,11 @@ reading files and writing one JSON object:
|
|
|
112
120
|
Name sets (surface `sets`, each sorted): `all` (the union above, less a
|
|
113
121
|
nested Python top's names);
|
|
114
122
|
`scope.include` and `tier_a_include` when the scope has those globs and a
|
|
115
|
-
record names a file: the names
|
|
123
|
+
record names a file: the names with a file (any of the name's records: an
|
|
124
|
+
extraction's definition file, each other in-scope definition's included,
|
|
116
125
|
the quick output's defined_in, else its source_file, a per-file result's
|
|
117
|
-
file, a provenance entry's source_file) matches a glob of the list
|
|
118
|
-
scope.exclude glob,
|
|
126
|
+
file, a provenance entry's source_file) that matches a glob of the list
|
|
127
|
+
and no scope.exclude glob,
|
|
119
128
|
and for `tier_a_include` a scope.include glob too when the scope has one,
|
|
120
129
|
so a name with no file is in `all` only and a restored name is in no
|
|
121
130
|
tier_a_include set (a tier_a_include glob an extraction lists as matching
|
|
@@ -127,29 +136,39 @@ exclude of `src/gen/**`; a list that counts no name, each glob left out
|
|
|
127
136
|
or matching only excluded files, files scope.include leaves out or files
|
|
128
137
|
no record names, gives no tier_a_include set); and, from an
|
|
129
138
|
extraction only, `subpaths` (the names a non-root `exports` map subpath
|
|
130
|
-
without `*` reaches) and `root` (the names the root entry points reach)
|
|
139
|
+
without `*` reaches) and `root` (the names the root entry points reach),
|
|
140
|
+
each through the entry point of any of the name's definitions.
|
|
131
141
|
reconcile-coverage.py and score-signatures.py --surface-set pick one.
|
|
132
142
|
|
|
133
143
|
Candidates and guards (with an extraction, --brief or --metadata): each
|
|
134
144
|
set's count, the metadata `stats.effective_denominator` (--metadata), and
|
|
135
145
|
the extractor's own effective_denominator and its basis. Both guards run
|
|
136
|
-
only on a surface read from the source. The deflation guard compares
|
|
137
|
-
re-derived
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
set
|
|
141
|
-
|
|
142
|
-
carries its
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
only on a surface read from the source. The deflation guard compares a
|
|
147
|
+
re-derived set with `stats.effective_denominator` and names it in `basis`.
|
|
148
|
+
It is the tier_a_include set (`tier_a_include`) when the clause's ladder
|
|
149
|
+
holds a stored count (stratified-scope, specific-modules, multi-entry), the
|
|
150
|
+
surface has that set and each name sits at the file that defines it (a
|
|
151
|
+
runner extraction, --fallback-scan, or quick output whose every name
|
|
152
|
+
carries its defined_in, with no --per-file or --name beside it): it fires
|
|
153
|
+
when the stored count differs from the set by more than DEFLATION_PCT
|
|
154
|
+
either way, since Create Skill stores the runner's tier A count. Otherwise
|
|
155
|
+
it is the scope.include set (`scope.include`, else `all` when the surface
|
|
156
|
+
has no scope.include set), firing above DEFLATION_PCT when there is no tier
|
|
157
|
+
A list (with an extraction or --fallback-scan, no tier_a_include set;
|
|
158
|
+
otherwise no tier_a_include glob left in the list, since a name read by eye
|
|
159
|
+
at Quick tier or State 4, and a quick name with no defined_in, carries its
|
|
160
|
+
entry file, not the file that defines it). A stored 0 is deflation, not a
|
|
161
|
+
denominator (`pct` null), and fires under its basis's condition when any
|
|
162
|
+
name is re-derived. The inflation guard compares the scope.include set
|
|
163
|
+
with the provenance entry count (--provenance) and fires above
|
|
164
|
+
INFLATION_PCT when there is no tier A list, as the scope.include basis
|
|
165
|
+
reads it; `guards.tierAInclude` lists the scope's
|
|
148
166
|
tier_a_include globs ([] when it has none) for gap-ledger.py's wording;
|
|
149
167
|
the umbrella ratio (an
|
|
150
168
|
extraction only) is the share of the root entry points' names that are
|
|
151
169
|
re-exports (a namespace with `members` counting as its members there
|
|
152
|
-
too, each a re-export
|
|
170
|
+
too, each a re-export, and an other in-scope definition left out), and
|
|
171
|
+
above UMBRELLA_RATIO the barrel is an
|
|
153
172
|
umbrella; the
|
|
154
173
|
stale-scope guard (an extraction with scope.include or tier_a_include only)
|
|
155
174
|
fires when the runner lists an include or tier_a_include glob that matches
|
|
@@ -162,7 +181,14 @@ included, and `*` and `?` stay inside one.
|
|
|
162
181
|
Denominator (surface `denominator`): the Source API Surface Definition's
|
|
163
182
|
clause (source-access-protocol.md), the branch reconcile-coverage.py takes
|
|
164
183
|
and the set it and score-signatures.py --surface-set read: {clause, branch,
|
|
165
|
-
surfaceSet, ladderStep, absentSet, rebuild, barrel}.
|
|
184
|
+
surfaceSet, ladderStep, absentSet, rebuild, barrel, setCounted}.
|
|
185
|
+
`setCounted` (null off the scalar branch) says whether the scalar branch
|
|
186
|
+
counts its `surfaceSet` as the numerator's set: true only for a scope set
|
|
187
|
+
(not `all`) whose every name sits at the file that defines it, as the
|
|
188
|
+
deflation guard's tier A basis requires (a runner extraction, the fallback
|
|
189
|
+
scan, quick output whose every name carries its defined_in), read whole (an
|
|
190
|
+
extraction neither `incomplete` nor truncated); otherwise the scalar branch
|
|
191
|
+
keeps its looked-up numerator. The inputs: the scope
|
|
166
192
|
type (the brief's, else the extraction's, else metadata.json's
|
|
167
193
|
`scope_type`), metadata.json's `skill_type` and `stats.effective_denominator`,
|
|
168
194
|
the extraction's `arms`, the deflation guard, and:
|
|
@@ -200,7 +226,13 @@ The last three walk a ladder, `ladderStep` naming the rung taken: rung 1,
|
|
|
200
226
|
`stats.effective_denominator` when metadata.json has one above 0 (a stored
|
|
201
227
|
0 is no denominator), is the `scalar` branch, whose `surfaceSet` (the set score-signatures.py scores) is
|
|
202
228
|
tier_a_include, else scope.include, else all, unless the deflation guard
|
|
203
|
-
fires: then the barrel branch over
|
|
229
|
+
fires: then the barrel branch over tier_a_include when the ladder has that
|
|
230
|
+
rung (stratified-scope, multi-entry) and the surface has the set, else over
|
|
231
|
+
scope.include (when the inflation guard fires too, that union is itself
|
|
232
|
+
inflated, and both gaps ask for the brief's tier A list, never a stored
|
|
233
|
+
count set by hand). On an incomplete extraction, which can miss tier A
|
|
234
|
+
names, a tier A set below the stored count fires the guard and leaves the
|
|
235
|
+
scalar branch. The next rungs are sets,
|
|
204
236
|
each taken when the surface has it, the last taken in any case:
|
|
205
237
|
tier_a_include then scope.include (stratified-scope), scope.include
|
|
206
238
|
(specific-modules), tier_a_include then subpaths (multi-entry). A set the
|
|
@@ -211,9 +243,13 @@ found no ast-grep (`no-ast-grep`), read no file in scope, read files only
|
|
|
211
243
|
in languages it has no recipe for (`files_without_recipes` and no export),
|
|
212
244
|
or found files no recipe reads while the skill's language (the --brief
|
|
213
245
|
`language`, else the metadata.json `language`) is not one RECIPE_LANGUAGES
|
|
214
|
-
holds, so the per-file scan has to run instead.
|
|
215
|
-
its
|
|
216
|
-
|
|
246
|
+
holds, so the per-file scan has to run instead. A run keeps its exports
|
|
247
|
+
whatever its status; `extraction.readByEye` lists the files to read by eye,
|
|
248
|
+
at any status: each file the runner could not parse or decode (a
|
|
249
|
+
`syntax-errors` or `not-utf8` file issue) and, on an `incomplete` run, each
|
|
250
|
+
file a failed call names. `extraction.unread` lists the files nobody can
|
|
251
|
+
read (a `missing` or `unreadable` file issue); a `no-recipes` file, such as
|
|
252
|
+
a stylesheet, holds nothing to read and is in neither list.
|
|
217
253
|
|
|
218
254
|
Usage:
|
|
219
255
|
uv run load-coverage-inputs.py census --skill-dir <dir> [--output <file>]
|
|
@@ -607,9 +643,14 @@ def _members_in_place(public: list[dict]) -> list[dict]:
|
|
|
607
643
|
return expanded
|
|
608
644
|
|
|
609
645
|
|
|
646
|
+
# The runner's file issues for a file it could not open at all: no one can read it by eye either.
|
|
647
|
+
UNREADABLE_ISSUES = frozenset({"missing", "unreadable"})
|
|
648
|
+
|
|
649
|
+
|
|
610
650
|
def _extraction_summary(data: dict, language: str | None) -> dict:
|
|
611
651
|
"""The surface's `extraction` object: the runner's status and counts,
|
|
612
|
-
the files to read by eye and the fallback
|
|
652
|
+
the files to read by eye, the files no one can read and the fallback
|
|
653
|
+
verdict."""
|
|
613
654
|
counts = data.get("counts") if isinstance(data.get("counts"), dict) else {}
|
|
614
655
|
arms = data.get("arms") if isinstance(data.get("arms"), dict) else {}
|
|
615
656
|
entry_points = data.get("entry_points") if isinstance(data.get("entry_points"), dict) else {}
|
|
@@ -626,8 +667,11 @@ def _extraction_summary(data: dict, language: str | None) -> dict:
|
|
|
626
667
|
reason = (f"no recipe reads the skill's language, {language} "
|
|
627
668
|
f"({', '.join(sorted(without))} in scope)")
|
|
628
669
|
errors = [e for e in data.get("errors") or [] if isinstance(e, dict)]
|
|
629
|
-
|
|
630
|
-
|
|
670
|
+
issues = [i for i in data.get("file_issues") or [] if isinstance(i, dict) and i.get("file")]
|
|
671
|
+
# A file missing from disk or one the runner could not open is no file to read by eye.
|
|
672
|
+
unread = sorted({str(i["file"]) for i in issues if i.get("issue") in UNREADABLE_ISSUES})
|
|
673
|
+
read_by_eye = sorted({str(i["file"]) for i in issues
|
|
674
|
+
if i.get("issue") != "no-recipes" and i.get("issue") not in UNREADABLE_ISSUES}
|
|
631
675
|
| {str(e["first_file"]) for e in errors if e.get("first_file")}
|
|
632
676
|
| {str(f) for e in errors if isinstance(e.get("unread"), list) for f in e["unread"] if f})
|
|
633
677
|
return {
|
|
@@ -644,6 +688,7 @@ def _extraction_summary(data: dict, language: str | None) -> dict:
|
|
|
644
688
|
"entryPointsFound": (bool(entry_points["files"]) if isinstance(entry_points.get("files"), list)
|
|
645
689
|
and data.get("files_in_scope") else None),
|
|
646
690
|
"readByEye": read_by_eye,
|
|
691
|
+
"unread": unread,
|
|
647
692
|
"fallback": {"needed": reason is not None, "reason": reason},
|
|
648
693
|
}
|
|
649
694
|
|
|
@@ -651,6 +696,21 @@ def _extraction_summary(data: dict, language: str | None) -> dict:
|
|
|
651
696
|
NO_UMBRELLA = {"entries": [], "names": 0, "reexported": 0, "ratio": None, "umbrella": False}
|
|
652
697
|
|
|
653
698
|
|
|
699
|
+
def _outside_row(row: dict, scope: dict) -> dict:
|
|
700
|
+
"""An outside_scope row as `excluded.outsideScope` lists it: its name,
|
|
701
|
+
file, line and entry point, and why the brief scopes the file out when
|
|
702
|
+
it does (#796): `excludeGlob`, the first scope.exclude glob that matches
|
|
703
|
+
the file, and `outsideInclude`, true when scope.include has globs and
|
|
704
|
+
none matches it. Neither (null and false) when the brief does not
|
|
705
|
+
explain it, such as a file no recipe reads or no file at all."""
|
|
706
|
+
file = row.get("file") if isinstance(row.get("file"), str) and row.get("file") else None
|
|
707
|
+
path = _norm(file) if file else None
|
|
708
|
+
return {"name": row.get("name"), "file": file, "line": row.get("line"), "entry": row.get("entry"),
|
|
709
|
+
"excludeGlob": next((g for g in scope["exclude"] if glob_match(path, g)), None) if path else None,
|
|
710
|
+
"outsideInclude": bool(path and scope["include"]
|
|
711
|
+
and not any(glob_match(path, g) for g in scope["include"]))}
|
|
712
|
+
|
|
713
|
+
|
|
654
714
|
def from_extraction(data: dict, language: str | None = None, brief: dict | None = None) -> dict:
|
|
655
715
|
"""The public surface of an extraction, less what lies outside scope,
|
|
656
716
|
plus the root exports a stale scope.include glob left out. A nested
|
|
@@ -678,8 +738,12 @@ def from_extraction(data: dict, language: str | None = None, brief: dict | None
|
|
|
678
738
|
"extraction": extraction}
|
|
679
739
|
|
|
680
740
|
diff = data.get("entry_point_diff") if isinstance(data.get("entry_point_diff"), dict) else {}
|
|
681
|
-
public
|
|
682
|
-
|
|
741
|
+
# Each in-scope definition of a public name is a record of its own (#830): the runner's other
|
|
742
|
+
# definitions join the public rows, so a set holds a name when any of its files matches, and
|
|
743
|
+
# the signature plan reads every declaration.
|
|
744
|
+
rows = {key: [p for p in diff.get(key) or [] if isinstance(p, dict) and isinstance(p.get("name"), str)]
|
|
745
|
+
for key in ("public", "other_definitions")}
|
|
746
|
+
public = _members_in_place(rows["public"] + rows["other_definitions"])
|
|
683
747
|
outside = [o for o in diff.get("outside_scope") or [] if isinstance(o, dict)]
|
|
684
748
|
gaps = [g for g in diff.get("extraction_gaps") or [] if isinstance(g, dict)]
|
|
685
749
|
|
|
@@ -690,7 +754,7 @@ def from_extraction(data: dict, language: str | None = None, brief: dict | None
|
|
|
690
754
|
# records (so the scope sets a specific-modules or stratified brief
|
|
691
755
|
# counts) and their outside_scope and extraction_gaps rows.
|
|
692
756
|
nested = _nested_tops(entry_files, set(scope["include"]) | set(scope["tier_a_include"]),
|
|
693
|
-
{p.get("entry") for p in public})
|
|
757
|
+
{p.get("entry") for p in rows["public"]})
|
|
694
758
|
nested_names = {top: sorted({p["name"] for p in public if p.get("entry") == top}) for top in nested}
|
|
695
759
|
|
|
696
760
|
subpath_files = {e.get("file") for e in entry_files
|
|
@@ -731,20 +795,21 @@ def from_extraction(data: dict, language: str | None = None, brief: dict | None
|
|
|
731
795
|
found.get("source_line") if found else info.get("line"),
|
|
732
796
|
found.get("signature_line") if found else None, "extraction"))
|
|
733
797
|
|
|
734
|
-
|
|
798
|
+
# The umbrella ratio reads the public rows alone: an other definition is no root barrel name there.
|
|
799
|
+
root_names = [p for p in _members_in_place(rows["public"]) if p.get("entry") in root_files]
|
|
735
800
|
reexported = [p for p in root_names if p.get("via") not in (None, "declaration")]
|
|
736
801
|
|
|
737
802
|
sets = {}
|
|
738
803
|
if subpath_files:
|
|
739
804
|
sets["subpaths"] = sorted({p["name"] for p in public if p.get("entry") in subpath_files})
|
|
740
805
|
if root_files:
|
|
741
|
-
sets["root"] = sorted({p["name"] for p in
|
|
806
|
+
sets["root"] = sorted({p["name"] for p in public if p.get("entry") in root_files})
|
|
742
807
|
|
|
743
808
|
return {
|
|
744
809
|
"records": records,
|
|
745
810
|
"nestedRecords": nested_records,
|
|
746
811
|
"sets": sets,
|
|
747
|
-
"outside": [
|
|
812
|
+
"outside": [_outside_row(o, scope) for o in outside],
|
|
748
813
|
"nested": [{"entry": top, "within": nested[top], "names": nested_names[top]} for top in sorted(nested)],
|
|
749
814
|
"staleScope": {"unmatchedInclude": unmatched, "unmatchedTierAInclude": unmatched_tier_a,
|
|
750
815
|
"restored": [{"name": o["name"], "file": o["file"], "line": o.get("line")}
|
|
@@ -828,12 +893,16 @@ def _over(clause: str, sets: dict, name: str, step: int | None, rebuild: str | N
|
|
|
828
893
|
|
|
829
894
|
def pick_denominator(sets: dict, stats_eff: int | None, deflation_fires: bool, barrel: str | None, arms: dict,
|
|
830
895
|
scope_type: str | None, skill_type: str | None, monorepo: bool, pattern_reference: bool,
|
|
831
|
-
curated: bool, source_read: bool) -> dict:
|
|
896
|
+
curated: bool, source_read: bool, set_trusted: bool = False) -> dict:
|
|
832
897
|
"""The Source API Surface Definition's clause for a skill, with the branch
|
|
833
|
-
and the name set reconcile-coverage.py and score-signatures.py read,
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
898
|
+
and the name set reconcile-coverage.py and score-signatures.py read, the
|
|
899
|
+
entry points' state it was picked by and, on the scalar branch, whether
|
|
900
|
+
it counts that set (`set_trusted`: each name at its defining file, read
|
|
901
|
+
whole; see "Denominator" above)."""
|
|
902
|
+
picked = _pick(sets, stats_eff, deflation_fires, barrel, arms, scope_type, skill_type, monorepo,
|
|
903
|
+
pattern_reference, curated, source_read)
|
|
904
|
+
counted = (set_trusted and picked["surfaceSet"] not in (None, "all")) if picked["branch"] == "scalar" else None
|
|
905
|
+
return {**picked, "barrel": barrel, "setCounted": counted}
|
|
837
906
|
|
|
838
907
|
|
|
839
908
|
def _pick(sets: dict, stats_eff: int | None, deflation_fires: bool, barrel: str | None, arms: dict,
|
|
@@ -868,7 +937,9 @@ def _pick(sets: dict, stats_eff: int | None, deflation_fires: bool, barrel: str
|
|
|
868
937
|
elif stats_eff:
|
|
869
938
|
# A stored 0 is no denominator: the next rung.
|
|
870
939
|
if deflation_fires:
|
|
871
|
-
|
|
940
|
+
# The ladder's tier A rung when it has one and the surface has the set, else scope.include.
|
|
941
|
+
rung = "tier_a_include" if "tier_a_include" in ladder and "tier_a_include" in sets else "scope.include"
|
|
942
|
+
return _over(clause, sets, rung, step)
|
|
872
943
|
scored = next((name for name in ("tier_a_include", "scope.include") if name in sets), "all")
|
|
873
944
|
return {"clause": clause, "branch": "scalar", "surfaceSet": scored, "ladderStep": step,
|
|
874
945
|
"absentSet": None, "rebuild": None}
|
|
@@ -1002,6 +1073,45 @@ def surface(args: argparse.Namespace) -> dict:
|
|
|
1002
1073
|
if tier_a_names:
|
|
1003
1074
|
sets["tier_a_include"] = tier_a_names
|
|
1004
1075
|
|
|
1076
|
+
# The clause: an extraction's entry-point status (one the per-file scan replaces says only that it found no
|
|
1077
|
+
# entry point), else a quick output's (an export is a barrel; none, and no warning, an empty one), else what
|
|
1078
|
+
# the workflow read (--barrel).
|
|
1079
|
+
barrel = None
|
|
1080
|
+
summary = extracted["extraction"] if extracted is not None else None
|
|
1081
|
+
if summary is not None and not summary["fallback"]["needed"]:
|
|
1082
|
+
barrel = summary["entryPointStatus"]
|
|
1083
|
+
elif summary is not None and summary["entryPointsFound"] is False:
|
|
1084
|
+
barrel = "no-entry-point"
|
|
1085
|
+
elif quick_outputs:
|
|
1086
|
+
barrel = "barrel" if any(q["exports"] for q in quick_outputs) else (
|
|
1087
|
+
None if any(q.get("warnings") for q in quick_outputs) else "empty-barrel")
|
|
1088
|
+
if barrel is None and args.barrel:
|
|
1089
|
+
barrel = BARREL_FLAG[args.barrel]
|
|
1090
|
+
recorded_type = extracted["scopeType"] if extracted is not None else None
|
|
1091
|
+
meta_type = meta.get("scope_type") if isinstance(meta, dict) and isinstance(meta.get("scope_type"), str) else None
|
|
1092
|
+
arms = summary["arms"] if summary is not None else {}
|
|
1093
|
+
# The extraction's verdict, the rule Create Skill stores the count by, else the workflow's judgment.
|
|
1094
|
+
curated = summary["curatedSubset"] if summary is not None and summary["curatedSubset"] is not None \
|
|
1095
|
+
else bool(args.curated_subset)
|
|
1096
|
+
scope_type = (brief or {}).get("type") or recorded_type or meta_type
|
|
1097
|
+
skill_type = meta.get("skill_type") if isinstance(meta, dict) else None
|
|
1098
|
+
monorepo = bool(args.monorepo or arms.get("monorepo"))
|
|
1099
|
+
# The clause alone (no stored count, no guard): only a clause whose ladder holds a stored count measures it
|
|
1100
|
+
# against the tier A set.
|
|
1101
|
+
clause = _pick(sets, None, False, barrel, arms, scope_type, skill_type, monorepo,
|
|
1102
|
+
bool(args.pattern_reference_intent), curated, source_read)["clause"]
|
|
1103
|
+
swap = False
|
|
1104
|
+
# Each name sits at the file that defines it (a runner extraction, the fallback scan, quick output whose every
|
|
1105
|
+
# name carries its defined_in); a name read by eye, or a quick name with no defined_in, sits at its entry file.
|
|
1106
|
+
runner = extracted is not None and not extracted["extraction"]["fallback"]["needed"]
|
|
1107
|
+
quick_traced = (bool(quick_outputs) and not args.per_file and not args.name
|
|
1108
|
+
and all(isinstance(e.get("defined_in"), str) and e["defined_in"]
|
|
1109
|
+
for q in quick_outputs for e in q["exports"] if isinstance(e, dict)))
|
|
1110
|
+
defining = runner or bool(args.fallback_scan) or quick_traced
|
|
1111
|
+
# The scalar branch counts its scope set only from such names read whole: an incomplete or truncated
|
|
1112
|
+
# extraction can miss names (#791, #792).
|
|
1113
|
+
whole = summary is None or (summary["status"] != "incomplete" and not summary["truncated"])
|
|
1114
|
+
|
|
1005
1115
|
if extracted is not None or brief is not None or meta is not None:
|
|
1006
1116
|
include_union = len(sets["scope.include"]) if "scope.include" in sets else len(sets["all"])
|
|
1007
1117
|
extraction = extracted["extraction"] if extracted is not None else {}
|
|
@@ -1021,14 +1131,27 @@ def surface(args: argparse.Namespace) -> dict:
|
|
|
1021
1131
|
# file, not the file that defines it).
|
|
1022
1132
|
no_tier_a = ("tier_a_include" not in sets if extracted is not None or args.fallback_scan
|
|
1023
1133
|
else not tier_a)
|
|
1024
|
-
|
|
1025
|
-
|
|
1134
|
+
# A clause whose ladder holds a stored count checks it against the tier A set, both ways, when each name
|
|
1135
|
+
# sits at the file that defines it (a runner extraction, the fallback scan, quick output whose every name
|
|
1136
|
+
# carries its defined_in): Create Skill stores the runner's tier A count, so one beyond the threshold is
|
|
1137
|
+
# stale (#813). A name read by eye, or a quick name with no defined_in, sits at its entry file.
|
|
1138
|
+
tier_a_basis = defining and clause in CLAUSE_LADDERS and "tier_a_include" in sets
|
|
1139
|
+
rederived = len(sets["tier_a_include"]) if tier_a_basis else include_union
|
|
1140
|
+
basis = "tier_a_include" if tier_a_basis else "scope.include" if "scope.include" in sets else "all"
|
|
1141
|
+
deflation = {"applicable": source_read and stats_eff is not None, "basis": basis,
|
|
1142
|
+
"effectiveDenominator": stats_eff, "rederived": rederived if source_read else None, "pct": None,
|
|
1143
|
+
"fires": False}
|
|
1026
1144
|
if deflation["applicable"] and stats_eff:
|
|
1027
|
-
deflation["pct"] = round2((
|
|
1028
|
-
deflation["fires"] =
|
|
1145
|
+
deflation["pct"] = round2((rederived - stats_eff) / stats_eff * 100)
|
|
1146
|
+
deflation["fires"] = (abs(deflation["pct"]) > DEFLATION_PCT if tier_a_basis
|
|
1147
|
+
else no_tier_a and deflation["pct"] > DEFLATION_PCT)
|
|
1029
1148
|
elif deflation["applicable"]:
|
|
1030
1149
|
# A stored 0 is deflation, not a denominator, once a name is re-derived.
|
|
1031
|
-
deflation["fires"] = no_tier_a and
|
|
1150
|
+
deflation["fires"] = (tier_a_basis or no_tier_a) and rederived > 0
|
|
1151
|
+
# An incomplete extraction can miss tier A names: a tier A set below the stored count is drift to report,
|
|
1152
|
+
# not a denominator to take.
|
|
1153
|
+
incomplete = runner and extracted["extraction"]["status"] == "incomplete"
|
|
1154
|
+
swap = deflation["fires"] and not (incomplete and tier_a_basis and (deflation["pct"] or 0) < 0)
|
|
1032
1155
|
entry_count = len(_entries(provenance)) if provenance is not None else 0
|
|
1033
1156
|
inflation = {"applicable": source_read and no_tier_a and bool(scope["include"]) and entry_count > 0,
|
|
1034
1157
|
"scopeIncludeUnion": include_union if scope["include"] else None,
|
|
@@ -1052,31 +1175,9 @@ def surface(args: argparse.Namespace) -> dict:
|
|
|
1052
1175
|
"umbrellaRatio": UMBRELLA_RATIO},
|
|
1053
1176
|
}
|
|
1054
1177
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
barrel = None
|
|
1059
|
-
summary = extracted["extraction"] if extracted is not None else None
|
|
1060
|
-
if summary is not None and not summary["fallback"]["needed"]:
|
|
1061
|
-
barrel = summary["entryPointStatus"]
|
|
1062
|
-
elif summary is not None and summary["entryPointsFound"] is False:
|
|
1063
|
-
barrel = "no-entry-point"
|
|
1064
|
-
elif quick_outputs:
|
|
1065
|
-
barrel = "barrel" if any(q["exports"] for q in quick_outputs) else (
|
|
1066
|
-
None if any(q.get("warnings") for q in quick_outputs) else "empty-barrel")
|
|
1067
|
-
if barrel is None and args.barrel:
|
|
1068
|
-
barrel = BARREL_FLAG[args.barrel]
|
|
1069
|
-
recorded_type = extracted["scopeType"] if extracted is not None else None
|
|
1070
|
-
meta_type = meta.get("scope_type") if isinstance(meta, dict) and isinstance(meta.get("scope_type"), str) else None
|
|
1071
|
-
arms = summary["arms"] if summary is not None else {}
|
|
1072
|
-
# The extraction's verdict, the rule Create Skill stores the count by, else the workflow's judgment.
|
|
1073
|
-
curated = summary["curatedSubset"] if summary is not None and summary["curatedSubset"] is not None \
|
|
1074
|
-
else bool(args.curated_subset)
|
|
1075
|
-
out["denominator"] = pick_denominator(
|
|
1076
|
-
sets, stats_eff, bool((out["guards"] or {}).get("deflation", {}).get("fires")), barrel, arms,
|
|
1077
|
-
(brief or {}).get("type") or recorded_type or meta_type,
|
|
1078
|
-
meta.get("skill_type") if isinstance(meta, dict) else None, bool(args.monorepo or arms.get("monorepo")),
|
|
1079
|
-
bool(args.pattern_reference_intent), curated, source_read)
|
|
1178
|
+
out["denominator"] = pick_denominator(sets, stats_eff, swap, barrel, arms, scope_type, skill_type, monorepo,
|
|
1179
|
+
bool(args.pattern_reference_intent), curated, source_read,
|
|
1180
|
+
defining and whole)
|
|
1080
1181
|
|
|
1081
1182
|
return {
|
|
1082
1183
|
"inputs": {
|
|
@@ -31,7 +31,7 @@ answer per input and judges no meaning. For each export name it lists:
|
|
|
31
31
|
as data or output (`json`, `yaml`, `text`, `console` ...) holds none.
|
|
32
32
|
|
|
33
33
|
A call is `awaited` when `await` stands before it (through its receiver:
|
|
34
|
-
`await client.fetchData(...)`), `.await` follows it (Rust), an asynchronous
|
|
34
|
+
`await client.fetchData(...)`), `.await` or `?.await` follows it (Rust), an asynchronous
|
|
35
35
|
loop or context manager consumes it (`async for row in stream(...)`, `async
|
|
36
36
|
with session(...)`, `for await (const row of stream(...))`), or it sits in
|
|
37
37
|
the arguments of a call that is itself awaited or is an event-loop runner
|
|
@@ -40,6 +40,26 @@ crossing into a function body. `chainedMember` names the member read or
|
|
|
40
40
|
called on its result right after it (`then`, `catch`, `join`, `get`), else
|
|
41
41
|
null: whether that waits for the result is the caller's call.
|
|
42
42
|
|
|
43
|
+
`awaitedVia` says how an awaited call is awaited, null when it is not:
|
|
44
|
+
|
|
45
|
+
- "direct": the await applies to the call itself (`directlyAwaited`):
|
|
46
|
+
`.await` follows it, or `await` stands before it with nothing applied to
|
|
47
|
+
its result (`await client.fetchData(...)`) but a member that waits for an
|
|
48
|
+
awaitable (`then`, `catch`, `finally`, `ConfigureAwait`), or it alone
|
|
49
|
+
fills the groups that `await` stands before (`await ((fetchData(x)))`,
|
|
50
|
+
`await (fetchData(x) as T)`) and nothing follows them but the end of the
|
|
51
|
+
statement, of a list or of a bracket.
|
|
52
|
+
- "chain": the await applies to an expression the call starts or sits in:
|
|
53
|
+
a member, subscript or call applied to its result
|
|
54
|
+
(`await use_context(e).embed(t)`, `await fetchData(x)[0]`) or a group
|
|
55
|
+
around it (`await (fetchData(x) or other())`).
|
|
56
|
+
- "consumer": an asynchronous loop or context manager consumes it.
|
|
57
|
+
- "argument" or "runner": it sits in the arguments of an awaited call, or of
|
|
58
|
+
an event-loop runner; `enclosingCallee` names that callee, whitespace
|
|
59
|
+
removed (`asyncio.gather`, `Promise.all`, `asyncio.run`), else null.
|
|
60
|
+
|
|
61
|
+
A backslash continuation counts as whitespace throughout.
|
|
62
|
+
|
|
43
63
|
Name match: the name as a fixed string, case-sensitive, never inside a
|
|
44
64
|
longer identifier (the Export match rule of skf-scan-skill-md-structure.py
|
|
45
65
|
usage-scope). A qualified name (`client.fetch`, `Store::load`) matches only
|
|
@@ -74,9 +94,11 @@ Output (stdout, one object):
|
|
|
74
94
|
"name": "fetchData",
|
|
75
95
|
"descriptionCount": 3, # all found; descriptions[] holds the first 12
|
|
76
96
|
"descriptions": [{"file", "line", "endLine", "kind", "section", "header", "text", "truncated"}],
|
|
77
|
-
"calls": [{"file", "line", "fence", "language", "awaited", "
|
|
97
|
+
"calls": [{"file", "line", "fence", "language", "awaited", "directlyAwaited", "awaitedVia",
|
|
98
|
+
"enclosingCallee", "chainedMember", "text"}],
|
|
78
99
|
"callCount": 2,
|
|
79
|
-
"awaitedCount": 1
|
|
100
|
+
"awaitedCount": 1, # calls whose `awaited` is true
|
|
101
|
+
"directlyAwaitedCount": 1 # calls whose `directlyAwaited` is true
|
|
80
102
|
}
|
|
81
103
|
],
|
|
82
104
|
"notCalled": ["parseConfig"], # names no example calls
|
|
@@ -126,9 +148,20 @@ LIST_ITEM_RE = re.compile(r"^\s*(?:[-*+]|\d{1,9}[.)])(?:[ \t]+|$)")
|
|
|
126
148
|
TABLE_ROW_RE = re.compile(r"^\s*\|")
|
|
127
149
|
TABLE_SEPARATOR_RE = re.compile(r"^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$")
|
|
128
150
|
GENERIC_OR_PATH = r"(?:[ \t]*::)?(?:[ \t]*<[^<>()\n]*(?:<[^<>()\n]*>[^<>()\n]*)*>)?"
|
|
129
|
-
|
|
151
|
+
# Whitespace, a Python backslash continuation included; LINE_SPACE stays on its line.
|
|
152
|
+
SPACE = r"(?:\s|\\\n)*"
|
|
153
|
+
LINE_SPACE = r"(?:[ \t]|\\\n)*"
|
|
154
|
+
AFTER_CALL_RE = re.compile(f"{SPACE}[?!]?\\.{SPACE}((?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*)")
|
|
130
155
|
AWAIT_BEFORE_RE = re.compile(f"(?<![{IDENT_CHARS}])await$")
|
|
131
|
-
AWAIT_AFTER_RE = re.compile(f"
|
|
156
|
+
AWAIT_AFTER_RE = re.compile(f"{SPACE}\\??\\.{SPACE}await(?![{IDENT_CHARS}])")
|
|
157
|
+
# A subscript or call applied to a call's result: `f(x)[0]`, `f(x)(y)`, `f(x)?.[0]`, `f(x)!(y)`.
|
|
158
|
+
APPLIED_RE = re.compile(f"{LINE_SPACE}(?:\\?\\.|!)?[\\[(]")
|
|
159
|
+
# What may close a group that holds only the call (`(f(x) as T)`, `(f(x)!)`), and what may follow
|
|
160
|
+
# the outermost group: the end of the statement, of a list or of a bracket.
|
|
161
|
+
GROUP_TAIL_RE = re.compile(f"{SPACE}(?:!|as(?![{IDENT_CHARS}])[^()]*)?{SPACE}")
|
|
162
|
+
GROUP_END_RE = re.compile(f"{LINE_SPACE}(?:[;,)\\]}}]|\\Z|\\n\\s*(?:[^\\s.?\\[(]|\\Z))")
|
|
163
|
+
# Members whose call waits for an awaitable: under `await`, the export returns one.
|
|
164
|
+
WAIT_MEMBERS = frozenset({"then", "catch", "finally", "ConfigureAwait"})
|
|
132
165
|
# An expression an asynchronous loop or context manager consumes: `async for
|
|
133
166
|
# x in NAME(...)`, `async with NAME(...)`, `for await (const x of NAME(...))`,
|
|
134
167
|
# `await foreach (var x in NAME(...))`. Matched against its line up to it.
|
|
@@ -515,7 +548,8 @@ def _matching_open(text, close_idx):
|
|
|
515
548
|
|
|
516
549
|
|
|
517
550
|
def _skip_space_back(text, i):
|
|
518
|
-
|
|
551
|
+
"""Back over whitespace, a Python backslash continuation included."""
|
|
552
|
+
while i > 0 and (text[i - 1] in " \t\n" or (text[i - 1] == "\\" and text.startswith("\n", i))):
|
|
519
553
|
i -= 1
|
|
520
554
|
return i
|
|
521
555
|
|
|
@@ -543,14 +577,37 @@ def _chain_start(text, pos):
|
|
|
543
577
|
i = start
|
|
544
578
|
|
|
545
579
|
|
|
580
|
+
def _await_before(text, start):
|
|
581
|
+
"""True when `await` stands right before the expression that starts at `start`."""
|
|
582
|
+
return bool(AWAIT_BEFORE_RE.search(text, 0, _skip_space_back(text, start)))
|
|
583
|
+
|
|
584
|
+
|
|
546
585
|
def _awaited_at(text, start):
|
|
547
586
|
"""True when `await` stands right before the expression that starts at `start`, or an
|
|
548
587
|
asynchronous loop or context manager consumes it."""
|
|
549
|
-
if
|
|
588
|
+
if _await_before(text, start):
|
|
550
589
|
return True
|
|
551
590
|
return bool(ASYNC_CONSUMER_RE.search(text[text.rfind("\n", 0, start) + 1:start]))
|
|
552
591
|
|
|
553
592
|
|
|
593
|
+
def _grouped_await(text, chain, close):
|
|
594
|
+
"""True for `await (call(...))` and `await ((call(...) as T))`: the call alone fills each group
|
|
595
|
+
around it (a trailing `as T` or `!` allowed), `await` stands before the outermost one, and
|
|
596
|
+
nothing but the end of the statement, of a list or of a bracket follows it."""
|
|
597
|
+
start, end = chain, close
|
|
598
|
+
while end is not None:
|
|
599
|
+
group = _skip_space_back(text, start) - 1
|
|
600
|
+
if group < 0 or text[group] != "(":
|
|
601
|
+
return False
|
|
602
|
+
end_group = _matching_close(text, group)
|
|
603
|
+
if end_group is None or not GROUP_TAIL_RE.fullmatch(text, end + 1, end_group):
|
|
604
|
+
return False
|
|
605
|
+
if _await_before(text, group):
|
|
606
|
+
return bool(GROUP_END_RE.match(text, end_group + 1))
|
|
607
|
+
start, end = group, end_group
|
|
608
|
+
return False
|
|
609
|
+
|
|
610
|
+
|
|
554
611
|
def _is_runner(callee):
|
|
555
612
|
"""True for `asyncio.run`, `loop.run_until_complete`, `rt.block_on` and the like."""
|
|
556
613
|
parts = [p for p in re.split(r"\.|::", callee) if p]
|
|
@@ -568,7 +625,12 @@ def _opens_body(text, brace_idx):
|
|
|
568
625
|
|
|
569
626
|
|
|
570
627
|
def _enclosing_awaits(text, start):
|
|
571
|
-
"""
|
|
628
|
+
"""How a call or group around the expression at `start` awaits it, within one function body.
|
|
629
|
+
|
|
630
|
+
("runner", callee) for a runner it is an argument of, ("argument", callee) for an awaited or
|
|
631
|
+
consumed call it is an argument of, ("chain", None) for an awaited group around it and
|
|
632
|
+
("consumer", None) for a consumed one; None when nothing around it awaits it.
|
|
633
|
+
"""
|
|
572
634
|
i, depth = start, 0
|
|
573
635
|
while i > 0:
|
|
574
636
|
i -= 1
|
|
@@ -583,17 +645,39 @@ def _enclosing_awaits(text, start):
|
|
|
583
645
|
elif c == "(":
|
|
584
646
|
k = _skip_space_back(text, i)
|
|
585
647
|
word = WORD_BEFORE_RE.search(text, 0, k)
|
|
648
|
+
callee = None
|
|
586
649
|
if word and word.group(1) not in EXPRESSION_WORDS:
|
|
587
650
|
expr = _chain_start(text, word.start())
|
|
588
|
-
|
|
589
|
-
|
|
651
|
+
callee = re.sub(r"(?:\s|\\\n)+", "", text[expr:k])
|
|
652
|
+
if _is_runner(callee):
|
|
653
|
+
return "runner", callee
|
|
590
654
|
else:
|
|
591
655
|
expr = i # a grouping `(`: `await (fetch())`
|
|
592
656
|
close = _matching_close(text, i)
|
|
593
|
-
|
|
594
|
-
|
|
657
|
+
awaits = _await_before(text, expr) or (close is not None and AWAIT_AFTER_RE.match(text, close + 1))
|
|
658
|
+
if awaits or _awaited_at(text, expr):
|
|
659
|
+
return ("argument", callee) if callee else ("chain" if awaits else "consumer", None)
|
|
595
660
|
i = expr
|
|
596
|
-
return
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _how_awaited(text, chain, close):
|
|
665
|
+
"""(awaitedVia, enclosingCallee, chainedMember) of the call whose expression starts at `chain`
|
|
666
|
+
and whose argument list closes at `close`."""
|
|
667
|
+
before = _await_before(text, chain)
|
|
668
|
+
member = AFTER_CALL_RE.match(text, close + 1) if close is not None else None
|
|
669
|
+
chained = member.group(1) if member else None
|
|
670
|
+
if close is not None and AWAIT_AFTER_RE.match(text, close + 1):
|
|
671
|
+
return "direct", None, None
|
|
672
|
+
if member or (close is not None and APPLIED_RE.match(text, close + 1)):
|
|
673
|
+
if before:
|
|
674
|
+
return ("direct" if chained in WAIT_MEMBERS else "chain"), None, chained
|
|
675
|
+
elif before or _grouped_await(text, chain, close):
|
|
676
|
+
return "direct", None, None
|
|
677
|
+
if _awaited_at(text, chain):
|
|
678
|
+
return "consumer", None, chained
|
|
679
|
+
via, callee = _enclosing_awaits(text, chain) or (None, None)
|
|
680
|
+
return via, callee, chained
|
|
597
681
|
|
|
598
682
|
|
|
599
683
|
def _closes_generic(tail):
|
|
@@ -670,15 +754,11 @@ def scan_block(rel, lines, opening, closing, info, patterns):
|
|
|
670
754
|
continue
|
|
671
755
|
if regex is not call_re:
|
|
672
756
|
continue # the last part of a qualified name counts only as a declaration
|
|
673
|
-
|
|
674
|
-
if close is not None:
|
|
675
|
-
if AWAIT_AFTER_RE.match(text, close + 1):
|
|
676
|
-
awaited = True
|
|
677
|
-
elif member := AFTER_CALL_RE.match(text, close + 1):
|
|
678
|
-
chained = member.group(1)
|
|
757
|
+
via, callee, chained = _how_awaited(text, chain, close)
|
|
679
758
|
found[name]["calls"].append({
|
|
680
759
|
"file": rel, "line": opening + row + 2, "fence": opening + 1, "language": language or None,
|
|
681
|
-
"awaited":
|
|
760
|
+
"awaited": via is not None, "directlyAwaited": via == "direct", "awaitedVia": via,
|
|
761
|
+
"enclosingCallee": callee, "chainedMember": chained,
|
|
682
762
|
"text": _cap(body[row].strip(), LINE_TEXT_CAP)[0],
|
|
683
763
|
})
|
|
684
764
|
return found
|
|
@@ -734,6 +814,7 @@ def locate(inp):
|
|
|
734
814
|
"calls": calls[name],
|
|
735
815
|
"callCount": len(calls[name]),
|
|
736
816
|
"awaitedCount": sum(1 for c in calls[name] if c["awaited"]),
|
|
817
|
+
"directlyAwaitedCount": sum(1 for c in calls[name] if c["directlyAwaited"]),
|
|
737
818
|
} for name in names if calls[name]]
|
|
738
819
|
not_called = [name for name in names if not calls[name]]
|
|
739
820
|
return {
|