bmad-module-skill-forge 8.1.0 → 9.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/verifying-a-skill.md +2 -2
- package/package.json +1 -1
- package/src/shared/scripts/skf-extract-public-api.py +15 -8
- package/src/skf-test-skill/assets/output-section-formats.md +2 -2
- package/src/skf-test-skill/references/coverage-check-branches.md +2 -2
- package/src/skf-test-skill/references/coverage-check.md +8 -4
- package/src/skf-test-skill/references/inventory-contract.md +11 -7
- package/src/skf-test-skill/references/signature-comparison-rules.md +7 -5
- package/src/skf-test-skill/references/source-access-protocol.md +1 -1
- package/src/skf-test-skill/scripts/gap-ledger.py +44 -14
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +336 -59
- package/src/skf-test-skill/scripts/reconcile-coverage.py +46 -18
- package/src/skf-test-skill/scripts/score-signatures.py +527 -130
- package/src/skf-test-skill/scripts/validate-inventory.py +528 -68
|
@@ -76,24 +76,37 @@ reading files and writing one JSON object:
|
|
|
76
76
|
data block (json, yaml, text...) gives none.
|
|
77
77
|
Signatures (#888): an origin whose place writes the name's
|
|
78
78
|
signature in a fixed form that is surely complete also carries
|
|
79
|
-
it as `signature`,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
79
|
+
it as `signature`, which validate-inventory.py --floor refuses
|
|
80
|
+
an inventory for changing; any other place pins nothing, since
|
|
81
|
+
a wrong pin would be forced into the inventory. A signature is
|
|
82
|
+
two strings, each one's whitespace runs read as one space,
|
|
83
|
+
{"params", "return_type"} for a complete form or a Props cell
|
|
84
|
+
(below) and {"usage_signature", "return_type"} for a usage form
|
|
85
|
+
(and `signature_form` "partial" under a header's partial
|
|
86
|
+
label, below). `params` is the text inside the parameter group
|
|
87
|
+
(score-signatures.py's _group: the first pair of parentheses
|
|
88
|
+
outside every bracket pair and string), and `return_type` the
|
|
89
|
+
type written after it (score-signatures.py's _signature_parts),
|
|
90
|
+
"" for none. A form that writes neither (`f()`,
|
|
91
|
+
`useQuery<T>()`) gives none, so an empty call claims no form. A
|
|
92
|
+
span (a table cell's or a heading's) is complete when it is the
|
|
93
|
+
name (or no name, as a Signature cell's `(x: A) => B`), its type
|
|
94
|
+
parameters, one parameter group, then nothing or a return
|
|
95
|
+
separator (`->`, `=>`, `→` or `:`) and the return, with nothing
|
|
96
|
+
else: `useQuery(key).data`, `f(a) / f(a, b)`, a trailing
|
|
97
|
+
comment, a `;` or `,` outside the return's brackets, an
|
|
98
|
+
unclosed bracket, `Meta<T>` (no group) and `new Client(opts)`
|
|
99
|
+
give none. A span that is no complete form is a usage form when
|
|
100
|
+
it writes, before the name's complete form, at least one of, in
|
|
101
|
+
this order, an assignment (`x =`, `const { data } =`), `await`,
|
|
102
|
+
`new` and a receiver or module path (`cognee.`): its
|
|
103
|
+
`usage_signature` is the span through the parameter group, its
|
|
104
|
+
`return_type` the return after it (`await run(a, b):
|
|
105
|
+
Promise<R>` gives `await run(a, b)` and `Promise<R>`); or when
|
|
106
|
+
it is one self-closing JSX element of the name that sets a prop
|
|
107
|
+
(`<Canvas of={X} />`), its `return_type` "". An empty call
|
|
108
|
+
(`new Client()`) and a prop-less element (`<Canvas />`) give
|
|
109
|
+
none. By form:
|
|
97
110
|
table-row a row whose first cell names one name (a row
|
|
98
111
|
naming several gives none), each cell's `\\|`
|
|
99
112
|
read as `|`: the leading code span of its
|
|
@@ -102,15 +115,38 @@ reading files and writing one JSON object:
|
|
|
102
115
|
the one span of its first cell that gives the
|
|
103
116
|
name (another export's span there, as in
|
|
104
117
|
`fetch` (same as `http.request(opts)`), gives
|
|
105
|
-
nothing)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
nothing), each a complete form, else a usage
|
|
119
|
+
form. A Signature span with a group that is
|
|
120
|
+
neither (`client.get(k).data`, `f(a) //
|
|
121
|
+
overload`) gives none, so a shorter first-cell
|
|
122
|
+
call never stands in for it, and so does such a
|
|
123
|
+
first-cell span (`useData(key).data`). Only
|
|
124
|
+
then a Props, Params or Parameters cell, when
|
|
125
|
+
the whole cell is one code span (trailing prose
|
|
126
|
+
or several spans leave it the reader's): a
|
|
127
|
+
complete form (`(a, b)` gives `a, b`), else a
|
|
128
|
+
usage form, else none when it writes a
|
|
129
|
+
parameter group or opens a JSX element, or when
|
|
130
|
+
it only names a type or writes a placeholder
|
|
131
|
+
(`ButtonProps`, `ComponentProps<typeof Link>`,
|
|
132
|
+
`None`, `-`), else the span whole
|
|
133
|
+
as `params` with `return_type` ""
|
|
134
|
+
(`TitleProps = { of?: Of }`, `key: string`),
|
|
135
|
+
kept as written. A Signature, Props, Params
|
|
136
|
+
or Parameters header may give one partial
|
|
137
|
+
label in parentheses after its word, any case:
|
|
138
|
+
`Signature (simplified)`, `Props (simplified)`,
|
|
139
|
+
`(partial)`, `(key parameters)` or
|
|
140
|
+
`(key params)`. The form its cell gives then
|
|
141
|
+
also carries `signature_form` "partial", so the
|
|
142
|
+
label is the script's, not the reader's; the
|
|
143
|
+
first-cell form never does, as the label heads
|
|
144
|
+
that column only. Any other text after the word
|
|
145
|
+
(`Signature (TypeScript)`, `Props / Usage`)
|
|
146
|
+
leaves the column unread.
|
|
147
|
+
heading its code span, a complete form, else a usage
|
|
148
|
+
form (`new Client(opts)`: a name is cut as
|
|
149
|
+
above, so no other usage names one there)
|
|
114
150
|
declaration a function's (`function`, `def`, `fn` or Go's
|
|
115
151
|
`func` before its name; a `const` holding an
|
|
116
152
|
arrow gives none): its lines as one, as
|
|
@@ -174,10 +210,12 @@ reading files and writing one JSON object:
|
|
|
174
210
|
same name and file), less the outside_scope
|
|
175
211
|
names (defined in a file out of scope), which
|
|
176
212
|
`excluded.outsideScope` lists with their file,
|
|
177
|
-
line and entry point and,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
scope.
|
|
213
|
+
line and entry point and, for a namespace's
|
|
214
|
+
member, the namespace (`namespace`) and, when
|
|
215
|
+
the brief scopes the file out, why: the
|
|
216
|
+
scope.exclude glob that matches it
|
|
217
|
+
(`excludeGlob`) or no scope.include glob
|
|
218
|
+
covering it
|
|
181
219
|
(`outsideInclude`); exports[] only gives each name's
|
|
182
220
|
kind, line and signature line (a local export
|
|
183
221
|
list's item: its binding's declaration_line and
|
|
@@ -204,7 +242,10 @@ reading files and writing one JSON object:
|
|
|
204
242
|
line of its outside_scope row, when no include,
|
|
205
243
|
exclude or declined scope-expansion amendment
|
|
206
244
|
(latest action skipped or demoted-include, from
|
|
207
|
-
--brief) matches its file
|
|
245
|
+
--brief) matches its file; no recipe reads its
|
|
246
|
+
file, so its kind is the one --provenance gives
|
|
247
|
+
the name at that file (PROVENANCE_KINDS), else
|
|
248
|
+
none. A tier_a_include glob
|
|
208
249
|
the runner lists as matching no file
|
|
209
250
|
(scope.unmatched_tier_a_include, [] when the key
|
|
210
251
|
is absent) counts no name. An extraction the
|
|
@@ -225,11 +266,13 @@ reading files and writing one JSON object:
|
|
|
225
266
|
its entry file): as with an extraction, no
|
|
226
267
|
tier_a_include set is no tier A list
|
|
227
268
|
--name a name read by eye whose file is not known
|
|
228
|
-
--provenance the provenance entries' named exports (State 2)
|
|
269
|
+
--provenance the provenance entries' named exports (State 2),
|
|
270
|
+
and the kinds of restored names
|
|
229
271
|
--metadata the metadata.json exports[] names (States 2, 3)
|
|
230
272
|
A surface read from the source (--extraction, --quick or
|
|
231
273
|
--per-file) takes no name from --provenance and --metadata:
|
|
232
|
-
they are the baselines of the candidates and guards
|
|
274
|
+
they are the baselines of the candidates and guards, and the
|
|
275
|
+
map gives only a restored name its kind. Without
|
|
233
276
|
one, with --provenance and --metadata both, `state2` gives the
|
|
234
277
|
provenance count, the metadata count and their union count,
|
|
235
278
|
which compute-score.py turns into the State 2 divergence. --fold
|
|
@@ -268,6 +311,23 @@ without `*` reaches) and `root` (the names the root entry points reach),
|
|
|
268
311
|
each through the entry point of any of the name's definitions.
|
|
269
312
|
reconcile-coverage.py and score-signatures.py --surface-set pick one.
|
|
270
313
|
|
|
314
|
+
Other declarations (surface `otherDeclarations`, #918): for each name with
|
|
315
|
+
a record (a nested Python top's included), each declaration of the name
|
|
316
|
+
the extraction's exports[] holds (under the name, or under the name an
|
|
317
|
+
aliased record's defining file gives it, `local`, as the record lookup
|
|
318
|
+
reads it; a re-export row declares nothing) in a file none of the name's
|
|
319
|
+
records names and no scope.exclude glob matches, as {name, kind, file,
|
|
320
|
+
line, signatureLine}, its file with forward slashes and no leading `./`,
|
|
321
|
+
its line and signature line taken as a record's are; sorted, each
|
|
322
|
+
declaration once however its path is written, and [] with none, with no
|
|
323
|
+
extraction or with one the per-file scan replaces. Such a declaration is a
|
|
324
|
+
same-named function no entry point exports, as cognee's lightweight
|
|
325
|
+
`run_pipeline`, which a lazy `__getattr__` passes on beside the
|
|
326
|
+
orchestrator a namespace exports. They are no records: they join no set,
|
|
327
|
+
no count and no guard, and only score-signatures.py reads them, plan
|
|
328
|
+
binding a documented form to one when the form's own citations name it and
|
|
329
|
+
no other declaration of the name.
|
|
330
|
+
|
|
271
331
|
Candidates and guards (with an extraction, --brief or --metadata): each
|
|
272
332
|
set's count, the metadata `stats.effective_denominator` (--metadata), and
|
|
273
333
|
the extractor's own effective_denominator and its basis; for the
|
|
@@ -437,9 +497,32 @@ FLOOR_TABLE_HEADERS = frozenset({
|
|
|
437
497
|
"export", "exports", "symbol", "symbols", "function", "functions", "class", "classes", "interface", "interfaces",
|
|
438
498
|
"type", "types", "hook", "hooks", "component", "components", "helper", "helpers", "constant", "constants",
|
|
439
499
|
"enum", "enums", "api", "apis"})
|
|
440
|
-
# The header words (as
|
|
441
|
-
# (#888); a
|
|
500
|
+
# The header words (as _header_label reads them, a partial label cut) of a floor table's column that writes the row's
|
|
501
|
+
# signature (#888); a header with any other text after the word, such as `Signature (TypeScript)`, is not one.
|
|
442
502
|
FLOOR_SIGNATURE_HEADERS = frozenset({"signature", "signatures"})
|
|
503
|
+
# The header words (as _header_label reads them, a partial label cut) of a floor table's column that writes the row's
|
|
504
|
+
# props or parameters (#888): its cell is read only when it is exactly one code span, and only after the Signature
|
|
505
|
+
# and first-cell forms; a `Props / Usage` or `Key params` header is not one.
|
|
506
|
+
FLOOR_PROPS_HEADERS = frozenset({"props", "params", "parameters"})
|
|
507
|
+
# The partial labels (lower case, whitespace runs as one space) a header may give in parentheses after its word, as
|
|
508
|
+
# `Signature (simplified)` and `Props (simplified)` do (#888): each form read from that Signature, Props, Params or
|
|
509
|
+
# Parameters column is partial, so an optional parameter it leaves out is Info, not Critical, and the floor pins the
|
|
510
|
+
# label with the form.
|
|
511
|
+
FLOOR_PARTIAL_LABELS = frozenset({"simplified", "partial", "key parameters", "key params"})
|
|
512
|
+
# What a usage form writes before its name's complete form (#888), each part optional, in this order: an assignment
|
|
513
|
+
# (`x =`, `a, b =`, `const { data } =`, `let [a, b] =`), `await`, `new`, and a receiver or module path (`cognee.`,
|
|
514
|
+
# `client.messages.`).
|
|
515
|
+
USAGE_PREFIX_RE = re.compile(
|
|
516
|
+
r"(?:(?:(?:const|let|var)\s+)?(?:[A-Za-z_$][\w$]*(?:\s*,\s*[A-Za-z_$][\w$]*)*"
|
|
517
|
+
r"|\{(?:[^{}]|\{[^{}]*\})*\}|\[(?:[^\[\]]|\[[^\[\]]*\])*\])\s*=(?![=>])\s*)?"
|
|
518
|
+
r"(?:await\s+)?(?:new\s+)?(?:[A-Za-z_$][\w$]*\.)*")
|
|
519
|
+
# A span that opens a JSX element (`<Canvas`), which a Props, Params or Parameters cell writes as a usage, not as
|
|
520
|
+
# the row's props.
|
|
521
|
+
ELEMENT_OPEN_RE = re.compile(r"<[A-Za-z_$]")
|
|
522
|
+
# A Props, Params or Parameters span that only names a type or writes a placeholder, not the row's props or
|
|
523
|
+
# parameters (`ButtonProps`, `ComponentProps<typeof Link>`, `None`, `-`): a lone dash, or one identifier or dotted
|
|
524
|
+
# path, then at most one `<...>` type-argument list (group 1, which _type_parameters_only reads).
|
|
525
|
+
TYPE_ONLY_RE = re.compile(r"[-\u2013\u2014]|[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\s*(<.*>)?")
|
|
443
526
|
# A complete form's name, before its type parameters and parameter group (group 1, absent for none).
|
|
444
527
|
SPAN_NAME_RE = re.compile(r"\s*([A-Za-z_$][\w$]*)?\s*")
|
|
445
528
|
# What a complete form writes after its parameter group before its return: `->`, `=>`, `→` or `:` (not `::`).
|
|
@@ -635,6 +718,19 @@ def _header_word(cell: str) -> str:
|
|
|
635
718
|
return (m.group(2) if m else cell).strip().lower()
|
|
636
719
|
|
|
637
720
|
|
|
721
|
+
def _header_label(cell: str) -> tuple[str, bool]:
|
|
722
|
+
"""A header cell's word as _header_word reads it, and whether it carries a partial label (#888): one of
|
|
723
|
+
FLOOR_PARTIAL_LABELS in parentheses after the word (`Signature (simplified)`, `**Signatures (Key
|
|
724
|
+
Parameters)**`), any case, whitespace runs inside them as one space, the label then cut from the word. Any other
|
|
725
|
+
text after the word (`Signature (TypeScript)`) stays in it, so that column is read as before."""
|
|
726
|
+
word = _header_word(cell)
|
|
727
|
+
if word.endswith(")"):
|
|
728
|
+
head, opened, label = word[:-1].rpartition("(")
|
|
729
|
+
if opened and " ".join(label.split()) in FLOOR_PARTIAL_LABELS:
|
|
730
|
+
return head.rstrip(), True
|
|
731
|
+
return word, False
|
|
732
|
+
|
|
733
|
+
|
|
638
734
|
def _cells(row: str) -> list[str]:
|
|
639
735
|
"""A table row's cells, split on each unescaped `|`, each stripped."""
|
|
640
736
|
row = row.strip()
|
|
@@ -790,6 +886,64 @@ def _call_form(span: str, name: str | None) -> dict | None:
|
|
|
790
886
|
return _form(text, group)
|
|
791
887
|
|
|
792
888
|
|
|
889
|
+
def _element_form(text: str, name: str) -> dict | None:
|
|
890
|
+
"""{"usage_signature", "return_type"} of a span that is one self-closing JSX element of `name` setting at least
|
|
891
|
+
one prop (`<Canvas of={X} />`): the element, whitespace runs as one space, and ""; None for any other span, a
|
|
892
|
+
prop-less `<Canvas />`, an element with children, two elements and an unclosed brace or string among them."""
|
|
893
|
+
head = re.match(rf"<{re.escape(name)}\s+", text)
|
|
894
|
+
if head is None or not text.endswith("/>") or not text[head.end():-2].strip():
|
|
895
|
+
return None
|
|
896
|
+
depth, quote = 0, None # brace depth, and the quote a string is open on
|
|
897
|
+
for ch in text[head.end():-2]:
|
|
898
|
+
if quote:
|
|
899
|
+
quote = None if ch == quote else quote
|
|
900
|
+
elif ch in "\"'`":
|
|
901
|
+
quote = ch
|
|
902
|
+
elif ch in "{}":
|
|
903
|
+
depth += 1 if ch == "{" else -1
|
|
904
|
+
if depth < 0:
|
|
905
|
+
return None
|
|
906
|
+
elif depth == 0 and ch in "<>/":
|
|
907
|
+
return None
|
|
908
|
+
return None if depth or quote else {"usage_signature": " ".join(text.split()), "return_type": ""}
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def _usage_form(span: str, name: str) -> dict | None:
|
|
912
|
+
"""The signature a span writes as a usage of `name` (#888), whitespace runs as one space: at least one of, in this
|
|
913
|
+
order, an assignment (`x =`, `const { data } =`), `await`, `new` and a receiver or module path (`cognee.`), then
|
|
914
|
+
`name`'s complete form (_call_form): {"usage_signature": the span through the parameter group, "return_type": the
|
|
915
|
+
return after it, "" for none}; or one JSX element of `name` that sets a prop (_element_form). None for any other
|
|
916
|
+
span, an empty call (`new Client()`) and a prop-less element (`<Canvas />`) among them."""
|
|
917
|
+
text = span.strip()
|
|
918
|
+
if text.startswith("<"):
|
|
919
|
+
return _element_form(text, name)
|
|
920
|
+
prefix = USAGE_PREFIX_RE.match(text)
|
|
921
|
+
rest = text[prefix.end():]
|
|
922
|
+
if not prefix.group() or not re.match(rf"{re.escape(name)}(?![\w$])", rest):
|
|
923
|
+
return None
|
|
924
|
+
form = _call_form(rest, name)
|
|
925
|
+
if form is None:
|
|
926
|
+
return None
|
|
927
|
+
group = _sibling("score-signatures.py")._group(rest)
|
|
928
|
+
return {"usage_signature": " ".join(text[:prefix.end() + group[1] + 1].split()), "return_type": form["return_type"]}
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _span_form(span: str, name: str | None) -> dict | None:
|
|
932
|
+
"""The signature a span writes: its complete form (_call_form), else its usage form (_usage_form)."""
|
|
933
|
+
return _call_form(span, name) or (_usage_form(span, name) if name else None)
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def _labelled(form: dict | None, partial: bool) -> dict | None:
|
|
937
|
+
"""`form`, with `signature_form` "partial" when its column's header gives a partial label (#888)."""
|
|
938
|
+
return {**form, "signature_form": "partial"} if form and partial else form
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def _column(labels: list[tuple[str, bool]], words: frozenset) -> tuple[int | None, bool]:
|
|
942
|
+
"""The first header cell whose word (_header_label) is one of `words`, and its partial label; (None, False) for
|
|
943
|
+
none."""
|
|
944
|
+
return next(((i, labelled) for i, (word, labelled) in enumerate(labels) if word in words), (None, False))
|
|
945
|
+
|
|
946
|
+
|
|
793
947
|
def _leading_span(cell: str) -> str | None:
|
|
794
948
|
"""The code span a table cell opens with, None when it opens with none."""
|
|
795
949
|
run = len(cell) - len(cell.lstrip("`"))
|
|
@@ -801,22 +955,44 @@ def _row_form(header: list[str], row: list[str]) -> dict | None:
|
|
|
801
955
|
"""The signature a floor table's row writes for the one name its first cell names (a row naming several gives
|
|
802
956
|
none), each cell's `\\|` read as `|`: the leading code span of its Signature or Signatures cell, else, when that
|
|
803
957
|
span writes no parameter group, the one span of its first cell that gives that name (another export's span there
|
|
804
|
-
gives nothing), each a complete form (_call_form). A Signature span with a
|
|
805
|
-
none.
|
|
958
|
+
gives nothing), each a complete form (_call_form), else a usage form (_usage_form, #888). A Signature span with a
|
|
959
|
+
group that is neither gives none, and so does such a first-cell span. Only then its Props, Params or Parameters
|
|
960
|
+
cell, when the whole cell is one code span (trailing prose or several spans leave it the reader's): a complete
|
|
961
|
+
form, else a usage form, else none when it writes a parameter group or opens a JSX element, or when it only names
|
|
962
|
+
a type or writes a placeholder (`ButtonProps`, `ComponentProps<typeof Link>`, `None`, `-`), else the span whole as
|
|
963
|
+
`params`, `return_type` "", kept as written. A Signature, Props, Params or Parameters header may carry a partial
|
|
964
|
+
label (_header_label, #888): the form its cell gives then also gives `signature_form` "partial", the first-cell
|
|
965
|
+
form never."""
|
|
806
966
|
segments = _sibling("locate-export-segments.py")
|
|
967
|
+
scorer = _sibling("score-signatures.py")
|
|
807
968
|
names = {name for name in map(_floor_name, segments._inline_code(row[0])) if name}
|
|
808
969
|
if len(names) != 1:
|
|
809
970
|
return None
|
|
810
971
|
[name] = names
|
|
811
972
|
cells = [cell.replace("\\|", "|") for cell in row]
|
|
812
|
-
|
|
973
|
+
labels = [_header_label(cell) for cell in header]
|
|
974
|
+
at, partial = _column(labels, FLOOR_SIGNATURE_HEADERS)
|
|
813
975
|
span = _leading_span(cells[at]) if at is not None and at < len(cells) else None
|
|
814
|
-
if span is not None and (form :=
|
|
815
|
-
return form
|
|
816
|
-
if span is not None and
|
|
976
|
+
if span is not None and (form := _span_form(span, name)):
|
|
977
|
+
return _labelled(form, partial)
|
|
978
|
+
if span is not None and scorer._group(span.strip()) is not None:
|
|
817
979
|
return None # the cell writes a call the floor cannot pin: a shorter first-cell form would hide it
|
|
818
980
|
own = [span for span in segments._inline_code(cells[0]) if _floor_name(span) == name]
|
|
819
|
-
|
|
981
|
+
if len(own) == 1 and (form := _span_form(own[0], name)):
|
|
982
|
+
return form
|
|
983
|
+
if any(scorer._group(span.strip()) is not None for span in own):
|
|
984
|
+
return None # the first cell writes a call the floor cannot pin: the Props cell would stand in for it
|
|
985
|
+
at, partial = _column(labels, FLOOR_PROPS_HEADERS)
|
|
986
|
+
span = _whole_span(cells[at], segments) if at is not None and at < len(cells) else None
|
|
987
|
+
if span is None:
|
|
988
|
+
return None
|
|
989
|
+
if form := _span_form(span, name):
|
|
990
|
+
return _labelled(form, partial)
|
|
991
|
+
if scorer._group(span.strip()) is not None or ELEMENT_OPEN_RE.match(span.strip()) or not span.strip():
|
|
992
|
+
return None
|
|
993
|
+
if (only := TYPE_ONLY_RE.fullmatch(span.strip())) and _type_parameters_only(only.group(1) or ""):
|
|
994
|
+
return None # the span names a type or writes a placeholder, no props or parameters
|
|
995
|
+
return _labelled({"params": " ".join(span.split()), "return_type": ""}, partial)
|
|
820
996
|
|
|
821
997
|
|
|
822
998
|
def _function_declaration(line: str, language: str, js_family) -> int | None:
|
|
@@ -924,7 +1100,7 @@ def floor(skill_dir: str) -> dict:
|
|
|
924
1100
|
for index, _level, text, _above in headings:
|
|
925
1101
|
span = _whole_span(text, segments)
|
|
926
1102
|
if span is not None:
|
|
927
|
-
add(_floor_name(span), file_index, index, "heading", None,
|
|
1103
|
+
add(_floor_name(span), file_index, index, "heading", None, _span_form(span, _floor_name(span)))
|
|
928
1104
|
comments = _html_comment_lines(lines, start, code)
|
|
929
1105
|
for opening, closing, info in blocks:
|
|
930
1106
|
language = segments._language(info)
|
|
@@ -1066,6 +1242,37 @@ def _record(name, kind, file, line, signature_line, origin) -> dict:
|
|
|
1066
1242
|
"origin": origin}
|
|
1067
1243
|
|
|
1068
1244
|
|
|
1245
|
+
# A provenance map's export_type, read case-insensitively, as the kind of a restored name (#913): the type kinds
|
|
1246
|
+
# score-signatures.py's Type Coverage counts (a dataclass, NamedTuple or TypedDict is a class, a Protocol an
|
|
1247
|
+
# interface, a type alias a type), and `function` and `const`, which are no type (a TypeVar is no type). Any
|
|
1248
|
+
# other export_type (`re-export`, `module`, `decorator_namespace`, ...) gives no kind.
|
|
1249
|
+
PROVENANCE_KINDS = {
|
|
1250
|
+
**dict.fromkeys(("class", "dataclass", "namedtuple", "typeddict"), "class"),
|
|
1251
|
+
**dict.fromkeys(("protocol", "interface"), "interface"),
|
|
1252
|
+
"enum": "enum",
|
|
1253
|
+
**dict.fromkeys(("type", "type_alias"), "type"),
|
|
1254
|
+
"struct": "struct",
|
|
1255
|
+
"trait": "trait",
|
|
1256
|
+
**dict.fromkeys(("function", "method", "staticmethod", "classmethod"), "function"),
|
|
1257
|
+
**dict.fromkeys(("const", "constant", "variable", "sentinel", "typevar"), "const"),
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
def _restored_kinds(records: list[dict], provenance: dict | None) -> None:
|
|
1262
|
+
"""Give each restored record with no kind the kind PROVENANCE_KINDS reads
|
|
1263
|
+
from the first provenance entry, in map order, with its name and its
|
|
1264
|
+
file (both paths `_norm`ed) and a known kind; no other record changes."""
|
|
1265
|
+
entries = _entries(provenance)
|
|
1266
|
+
for record in records:
|
|
1267
|
+
if record["origin"] != "restored" or record["kind"] or not record["file"]:
|
|
1268
|
+
continue
|
|
1269
|
+
file = _norm(record["file"])
|
|
1270
|
+
record["kind"] = next(
|
|
1271
|
+
(kind for e in entries
|
|
1272
|
+
if e.get("export_name") == record["name"] and _norm(e.get("source_file")) == file
|
|
1273
|
+
and (kind := PROVENANCE_KINDS.get(str(e.get("export_type")).strip().lower()))), None)
|
|
1274
|
+
|
|
1275
|
+
|
|
1069
1276
|
def _globs(value) -> list[str]:
|
|
1070
1277
|
return [g for g in (_norm(v) for v in value if isinstance(v, str)) if g] if isinstance(value, list) else []
|
|
1071
1278
|
|
|
@@ -1186,6 +1393,50 @@ def _members_in_place(public: list[dict]) -> list[dict]:
|
|
|
1186
1393
|
return expanded
|
|
1187
1394
|
|
|
1188
1395
|
|
|
1396
|
+
def _declared_at(row: dict) -> tuple:
|
|
1397
|
+
"""(line, signature line) of an extraction's exports[] row, as a record
|
|
1398
|
+
takes them: a local `export { ... }` list's item at the declaration it
|
|
1399
|
+
names (#836), its `declaration_line` and `declaration_signature_line`
|
|
1400
|
+
(both from that one pair), when it has that line; else its own
|
|
1401
|
+
`source_line` and `signature_line`."""
|
|
1402
|
+
if isinstance(row.get("declaration_line"), int):
|
|
1403
|
+
return row["declaration_line"], row.get("declaration_signature_line")
|
|
1404
|
+
return row.get("source_line"), row.get("signature_line")
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
OTHER_DECLARATION_KEYS = ("name", "kind", "file", "line", "signatureLine")
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
def other_declarations(records: list[dict], declarations: list[dict], aliases: dict[str, set[str]],
|
|
1411
|
+
excludes: list[str]) -> list[dict]:
|
|
1412
|
+
"""The other declarations of the surface names (#918): for each name
|
|
1413
|
+
`records` holds, each of `declarations` (an extraction's exports[] rows,
|
|
1414
|
+
a re-export left out) recorded under the name, or under a name an
|
|
1415
|
+
aliased record's defining file gives it (`aliases`), in a file no
|
|
1416
|
+
record of the name names and no exclude glob matches, as {name, kind,
|
|
1417
|
+
file, line, signatureLine}, the file normalised (_norm), sorted, each
|
|
1418
|
+
declaration once however its path is written. They are no records:
|
|
1419
|
+
only score-signatures.py reads them."""
|
|
1420
|
+
files: dict[str, set[str]] = {}
|
|
1421
|
+
for record in records:
|
|
1422
|
+
files.setdefault(record["name"], set()).update([_norm(record["file"])] if record.get("file") else [])
|
|
1423
|
+
by_export: dict[str, list[dict]] = {}
|
|
1424
|
+
for declaration in declarations:
|
|
1425
|
+
by_export.setdefault(declaration["exportName"], []).append(declaration)
|
|
1426
|
+
found = set()
|
|
1427
|
+
for name, own in files.items():
|
|
1428
|
+
for looked in {name} | aliases.get(name, set()):
|
|
1429
|
+
for d in by_export.get(looked, []):
|
|
1430
|
+
path = _norm(d["file"])
|
|
1431
|
+
if path in own or any(glob_match(path, g) for g in excludes):
|
|
1432
|
+
continue
|
|
1433
|
+
line = d["line"] if isinstance(d["line"], int) and not isinstance(d["line"], bool) else None
|
|
1434
|
+
found.add((name, d["kind"] if isinstance(d["kind"], str) else None, path, line,
|
|
1435
|
+
d["signatureLine"] if isinstance(d["signatureLine"], str) else None))
|
|
1436
|
+
ordered = sorted(found, key=lambda t: (t[0], t[2], t[3] or 0, t[1] or "", t[4] or ""))
|
|
1437
|
+
return [dict(zip(OTHER_DECLARATION_KEYS, t)) for t in ordered]
|
|
1438
|
+
|
|
1439
|
+
|
|
1189
1440
|
# The runner's file issues for a file it could not open at all: no one can read it by eye either.
|
|
1190
1441
|
UNREADABLE_ISSUES = frozenset({"missing", "unreadable"})
|
|
1191
1442
|
|
|
@@ -1245,13 +1496,19 @@ def _outside_row(row: dict, scope: dict) -> dict:
|
|
|
1245
1496
|
it does (#796): `excludeGlob`, the first scope.exclude glob that matches
|
|
1246
1497
|
the file, and `outsideInclude`, true when scope.include has globs and
|
|
1247
1498
|
none matches it. Neither (null and false) when the brief does not
|
|
1248
|
-
explain it, such as a file no recipe reads or no file at all.
|
|
1499
|
+
explain it, such as a file no recipe reads or no file at all. A
|
|
1500
|
+
namespace's member also keeps the namespace it came through
|
|
1501
|
+
(`namespace`, #969) when the row gives it as a non-empty string, and
|
|
1502
|
+
no such key otherwise."""
|
|
1249
1503
|
file = row.get("file") if isinstance(row.get("file"), str) and row.get("file") else None
|
|
1250
1504
|
path = _norm(file) if file else None
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1505
|
+
out = {"name": row.get("name"), "file": file, "line": row.get("line"), "entry": row.get("entry"),
|
|
1506
|
+
"excludeGlob": next((g for g in scope["exclude"] if glob_match(path, g)), None) if path else None,
|
|
1507
|
+
"outsideInclude": bool(path and scope["include"]
|
|
1508
|
+
and not any(glob_match(path, g) for g in scope["include"]))}
|
|
1509
|
+
if isinstance(row.get("namespace"), str) and row["namespace"]:
|
|
1510
|
+
out["namespace"] = row["namespace"]
|
|
1511
|
+
return out
|
|
1255
1512
|
|
|
1256
1513
|
|
|
1257
1514
|
def from_extraction(data: dict, language: str | None = None, brief: dict | None = None) -> dict:
|
|
@@ -1274,7 +1531,8 @@ def from_extraction(data: dict, language: str | None = None, brief: dict | None
|
|
|
1274
1531
|
unmatched_tier_a = _globs(recorded.get("unmatched_tier_a_include"))
|
|
1275
1532
|
extraction = _extraction_summary(data, language)
|
|
1276
1533
|
if extraction["fallback"]["needed"]:
|
|
1277
|
-
return {"records": [], "nestedRecords": [], "
|
|
1534
|
+
return {"records": [], "nestedRecords": [], "declarations": [], "aliases": {}, "sets": {}, "outside": [],
|
|
1535
|
+
"nested": [],
|
|
1278
1536
|
"staleScope": {"unmatchedInclude": unmatched, "unmatchedTierAInclude": unmatched_tier_a,
|
|
1279
1537
|
"restored": []},
|
|
1280
1538
|
"gaps": [], "scope": scope, "scopeType": recorded.get("type"), "umbrella": dict(NO_UMBRELLA),
|
|
@@ -1325,6 +1583,8 @@ def from_extraction(data: dict, language: str | None = None, brief: dict | None
|
|
|
1325
1583
|
index = {(e.get("export_name"), e.get("source_file")): e
|
|
1326
1584
|
for e in data.get("exports") or [] if isinstance(e, dict)}
|
|
1327
1585
|
records, nested_records = [], []
|
|
1586
|
+
# The names an aliased record's defining file gives it, for the other declarations (#918).
|
|
1587
|
+
aliases: dict[str, set[str]] = {}
|
|
1328
1588
|
for info in public:
|
|
1329
1589
|
name, file = info["name"], info.get("file")
|
|
1330
1590
|
if (name, file) in outside_keys:
|
|
@@ -1336,19 +1596,27 @@ def from_extraction(data: dict, language: str | None = None, brief: dict | None
|
|
|
1336
1596
|
continue
|
|
1337
1597
|
# An aliased re-export (`export { Options as Config }`) is recorded
|
|
1338
1598
|
# under the name its defining file gives it, `local`.
|
|
1339
|
-
|
|
1599
|
+
local = info.get("local")
|
|
1600
|
+
if isinstance(local, str) and local and local != name:
|
|
1601
|
+
aliases.setdefault(name, set()).add(local)
|
|
1602
|
+
found = index.get((local or name, file)) or index.get((name, file))
|
|
1340
1603
|
kind = found.get("export_type") if found else None
|
|
1341
|
-
|
|
1342
|
-
# names (#836): the line and the signature line both come from that pair.
|
|
1343
|
-
if found and isinstance(found.get("declaration_line"), int):
|
|
1344
|
-
line, signature_line = found["declaration_line"], found.get("declaration_signature_line")
|
|
1345
|
-
elif found:
|
|
1346
|
-
line, signature_line = found.get("source_line"), found.get("signature_line")
|
|
1347
|
-
else:
|
|
1348
|
-
line, signature_line = info.get("line"), None
|
|
1604
|
+
line, signature_line = _declared_at(found) if found else (info.get("line"), None)
|
|
1349
1605
|
(nested_records if info.get("entry") in nested else records).append(
|
|
1350
1606
|
_record(name, None if kind == "re-export" else kind, file, line, signature_line, "extraction"))
|
|
1351
1607
|
|
|
1608
|
+
# Every declaration exports[] records, a re-export row left out (it declares nothing): the other
|
|
1609
|
+
# declarations of a surface name come from these (#918).
|
|
1610
|
+
declarations = []
|
|
1611
|
+
for e in data.get("exports") or []:
|
|
1612
|
+
if not isinstance(e, dict) or e.get("export_type") == "re-export":
|
|
1613
|
+
continue
|
|
1614
|
+
name, file = e.get("export_name"), e.get("source_file")
|
|
1615
|
+
if isinstance(name, str) and name and isinstance(file, str) and file.strip():
|
|
1616
|
+
line, signature_line = _declared_at(e)
|
|
1617
|
+
declarations.append({"exportName": name, "kind": e.get("export_type"), "file": file, "line": line,
|
|
1618
|
+
"signatureLine": signature_line})
|
|
1619
|
+
|
|
1352
1620
|
# The umbrella ratio reads the public rows alone: an other definition is no root barrel name there.
|
|
1353
1621
|
root_names = [p for p in _members_in_place(rows["public"]) if p.get("entry") in root_files]
|
|
1354
1622
|
reexported = [p for p in root_names if p.get("via") not in (None, "declaration")]
|
|
@@ -1362,6 +1630,8 @@ def from_extraction(data: dict, language: str | None = None, brief: dict | None
|
|
|
1362
1630
|
return {
|
|
1363
1631
|
"records": records,
|
|
1364
1632
|
"nestedRecords": nested_records,
|
|
1633
|
+
"declarations": declarations,
|
|
1634
|
+
"aliases": aliases,
|
|
1365
1635
|
"sets": sets,
|
|
1366
1636
|
"outside": [_outside_row(o, scope) for o in outside],
|
|
1367
1637
|
"nested": [{"entry": top, "within": nested[top], "names": nested_names[top]} for top in sorted(nested)],
|
|
@@ -1600,12 +1870,18 @@ def surface(args: argparse.Namespace) -> dict:
|
|
|
1600
1870
|
sets["all"] = sorted({r["name"] for r in records})
|
|
1601
1871
|
# A nested Python top's records count in the scope sets, never in `all`.
|
|
1602
1872
|
records += extracted["nestedRecords"] if extracted is not None else []
|
|
1873
|
+
# A restored name takes its kind from the provenance map at its file (#913): no set, count or guard changes.
|
|
1874
|
+
if provenance is not None:
|
|
1875
|
+
_restored_kinds(records, provenance)
|
|
1603
1876
|
records.sort(key=lambda r: (r["name"], str(r["file"] or ""), r["origin"]))
|
|
1604
1877
|
|
|
1605
1878
|
# The scope: the one the extractor recorded, else the brief's.
|
|
1606
1879
|
scope = extracted["scope"] if extracted is not None and any(extracted["scope"].values()) else None
|
|
1607
1880
|
if scope is None:
|
|
1608
1881
|
scope = brief["scope"] if brief is not None else _scope(None, None, None)
|
|
1882
|
+
# Each other declaration of a surface name (#918), for the signature plan alone: no record, set or count.
|
|
1883
|
+
others = (other_declarations(records, extracted["declarations"], extracted["aliases"], scope["exclude"])
|
|
1884
|
+
if extracted is not None else [])
|
|
1609
1885
|
# A tier A glob the runner lists as matching no file counts no name, and
|
|
1610
1886
|
# so does one whose folder or file differs from every scope.include
|
|
1611
1887
|
# glob's before any `*` or `?` (`gen/**` beside `src/**`) or one a
|
|
@@ -1749,6 +2025,7 @@ def surface(args: argparse.Namespace) -> dict:
|
|
|
1749
2025
|
"brief": args.brief, "fold": bool(args.fold),
|
|
1750
2026
|
},
|
|
1751
2027
|
"exports": records,
|
|
2028
|
+
"otherDeclarations": others,
|
|
1752
2029
|
"sets": sets,
|
|
1753
2030
|
**out,
|
|
1754
2031
|
"warnings": warnings,
|