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
|
@@ -9,7 +9,7 @@ coverage-check.md decides whether a bookkeeping variant is a real export,
|
|
|
9
9
|
and makes the judgments the denominator clause needs where no file holds
|
|
10
10
|
them (--pattern-reference-intent, and --curated-subset and --barrel where
|
|
11
11
|
the extraction does not say); the clause, everything it counts, folds or
|
|
12
|
-
unions on the way runs here, so no count is made by hand.
|
|
12
|
+
unions on the way runs here, so no count is made by hand. Four subcommands, each
|
|
13
13
|
reading files and writing one JSON object:
|
|
14
14
|
|
|
15
15
|
census docs-only detection. Counts the citations SKILL.md and each .md
|
|
@@ -18,6 +18,129 @@ reading files and writing one JSON object:
|
|
|
18
18
|
(`[QMD:` and `[DOC:` are counted and decide nothing). A skill is
|
|
19
19
|
docs-only when it cites `[EXT:` and no local form.
|
|
20
20
|
|
|
21
|
+
floor the documented inventory's floor (#790): every name SKILL.md
|
|
22
|
+
and each .md file directly in references/ (a subfolder is not
|
|
23
|
+
read) write in a fixed form, which validate-inventory.py --floor
|
|
24
|
+
refuses an inventory for leaving out. No kind is mapped, and a
|
|
25
|
+
signature is read only where the place writes it in a fixed
|
|
26
|
+
form (Signatures, below). A name is a plain
|
|
27
|
+
identifier (`[A-Za-z_$][\\w$]*`) once a leading `new ` and
|
|
28
|
+
everything from the first `(` or `<` are cut, so `Meta<T>`,
|
|
29
|
+
`fetch(url): Promise<T>` and `new Client(opts)` give `Meta`,
|
|
30
|
+
`fetch` and `Client`. A dotted or `::` name gives none, so a
|
|
31
|
+
namespace-style skill such as zod (`z.object`) gets no table
|
|
32
|
+
floor. The forms, read outside the frontmatter and HTML
|
|
33
|
+
comments (a table row or heading inside fenced code gives none):
|
|
34
|
+
table-row each inline code span in the first cell of a
|
|
35
|
+
table row (cells split on an unescaped `|`)
|
|
36
|
+
whose header row's first cell is, in any case,
|
|
37
|
+
singular or plural and bare or wrapped in `**`
|
|
38
|
+
or `__`, Export, Symbol, Function, Class,
|
|
39
|
+
Interface, Type, Hook, Component, Helper,
|
|
40
|
+
Constant, Enum or API (a Name, Method, Prop,
|
|
41
|
+
Param, Import or Subpath table gives none). The
|
|
42
|
+
row's Kind cell, as written, is the name's
|
|
43
|
+
`kindLabel`: the skill's own label, never mapped
|
|
44
|
+
to a kind (the first in origin order when two
|
|
45
|
+
rows label the name differently; null with no
|
|
46
|
+
Kind column)
|
|
47
|
+
heading an ATX heading whose whole text is one code
|
|
48
|
+
span, as in ``### `Meta<T>` `` (a heading with
|
|
49
|
+
other words, ``## Testing with `mount` ``, gives
|
|
50
|
+
none)
|
|
51
|
+
declaration a column-0 public declaration in a fenced block
|
|
52
|
+
whose first non-blank line is a comment (`//`,
|
|
53
|
+
`#`, `/* */`, `--`, `<!-- -->` or `;`) holding an
|
|
54
|
+
`[AST:` or `[SRC:` citation, or naming a path one
|
|
55
|
+
such citation of the skill names (its `:12`,
|
|
56
|
+
`:L12` or `:12-30` left out). A citation counts
|
|
57
|
+
only with its `:L<n>` (`[AST:src/a.ts:L12]`, not
|
|
58
|
+
`[AST:src/a.ts]`), and one on the line above the
|
|
59
|
+
fence heads nothing. Comments and strings are
|
|
60
|
+
masked first. TS/JS (not Svelte, whose `export
|
|
61
|
+
let` is a prop): `export` then `function`,
|
|
62
|
+
`const`, `let`, `var`, `class`, `interface`,
|
|
63
|
+
`type` or `enum` (`declare`, `abstract` and
|
|
64
|
+
`async` may come between), or `export default
|
|
65
|
+
function` or `class` with a name; Python: `def`,
|
|
66
|
+
`async def` or `class`, a name not starting with
|
|
67
|
+
`_`; Rust: a `pub` (not `pub(crate)`) `fn`,
|
|
68
|
+
`struct`, `enum`, `trait`, `type`, `const` or
|
|
69
|
+
`static`, and a `macro_rules!` under
|
|
70
|
+
`#[macro_export]`; Go: `func`, `type`, `var` or
|
|
71
|
+
`const` with a capitalized name, each name of a
|
|
72
|
+
`type (`, `var (` or `const (` group too. A
|
|
73
|
+
block in another language, an untagged one or a
|
|
74
|
+
data block (json, yaml, text...) gives none.
|
|
75
|
+
Signatures (#888): an origin whose place writes the name's
|
|
76
|
+
signature in a fixed form that is surely complete also carries
|
|
77
|
+
it as `signature`, {"params", "return_type"}, two strings, each
|
|
78
|
+
one's whitespace runs read as one space, which
|
|
79
|
+
validate-inventory.py --floor refuses an inventory for
|
|
80
|
+
changing; any other place pins nothing, since a wrong pin would
|
|
81
|
+
be forced into the inventory. `params` is the text inside the
|
|
82
|
+
parameter group (score-signatures.py's _group: the first pair
|
|
83
|
+
of parentheses outside every bracket pair and string), and
|
|
84
|
+
`return_type` the type written after it (score-signatures.py's
|
|
85
|
+
_signature_parts), "" for none. A form that writes neither
|
|
86
|
+
(`f()`, `useQuery<T>()`) gives none, so an empty call claims no
|
|
87
|
+
form. A span (a table cell's or a heading's) is complete when
|
|
88
|
+
it is the name (or no name, as a Signature cell's `(x: A) =>
|
|
89
|
+
B`), its type parameters, one parameter group, then nothing or
|
|
90
|
+
a return separator (`->`, `=>`, `→` or `:`) and the return, with
|
|
91
|
+
nothing else: `useQuery(key).data`, `f(a) / f(a, b)`, a
|
|
92
|
+
trailing comment, a `;` or `,` outside the return's brackets,
|
|
93
|
+
an unclosed bracket, `Meta<T>` (no group) and `new Client(opts)`
|
|
94
|
+
give none. By form:
|
|
95
|
+
table-row a row whose first cell names one name (a row
|
|
96
|
+
naming several gives none), each cell's `\\|`
|
|
97
|
+
read as `|`: the leading code span of its
|
|
98
|
+
Signature or Signatures cell, else, when that
|
|
99
|
+
span writes no parameter group (`Typed<X>`),
|
|
100
|
+
the one span of its first cell that gives the
|
|
101
|
+
name (another export's span there, as in
|
|
102
|
+
`fetch` (same as `http.request(opts)`), gives
|
|
103
|
+
nothing). A Signature span with a group that is
|
|
104
|
+
no complete form (`client.get(k) -> str`,
|
|
105
|
+
`new Client(opts)`) gives none, so a shorter
|
|
106
|
+
first-cell call never stands in for it. A header
|
|
107
|
+
cell matches whole, so a labelled one is not a
|
|
108
|
+
Signature column, and a Props, Params or
|
|
109
|
+
Parameters cell is never read (its shared
|
|
110
|
+
members and usage forms stay the reader's).
|
|
111
|
+
heading its code span
|
|
112
|
+
declaration a function's (`function`, `def`, `fn` or Go's
|
|
113
|
+
`func` before its name; a `const` holding an
|
|
114
|
+
arrow gives none): its lines as one, as
|
|
115
|
+
skf-extract-public-api.py's
|
|
116
|
+
_declaration_signature joins them (a parameter
|
|
117
|
+
list split over lines joined, line comments
|
|
118
|
+
left out), the body cut off first, at the first
|
|
119
|
+
`:` (Python) or `{`, `;` or ` where ` (any other
|
|
120
|
+
language) outside every bracket pair and string
|
|
121
|
+
after the parameter group, a `{` directly after
|
|
122
|
+
a TS return `:` opening the return type
|
|
123
|
+
(`(): { a: T } {` returns `{ a: T }`). None for
|
|
124
|
+
one with anything but type parameters between
|
|
125
|
+
its name and the group (a Go `func(...)`
|
|
126
|
+
return), one whose opener follows, or whose
|
|
127
|
+
uncut text ends with, `interface`, `struct`,
|
|
128
|
+
`is`, `keyof`, `|`, `&`, `?`, `:`, `\\`, `=>` or
|
|
129
|
+
`->` (the opener or the line end is inside the
|
|
130
|
+
return type: Go's `interface{}`, a TS `asserts
|
|
131
|
+
x is {...}`, `() => {...}` or conditional type,
|
|
132
|
+
`): Promise<A> |`), or one whose brackets or
|
|
133
|
+
strings do not all close.
|
|
134
|
+
A form written only in prose is never read: it stays the
|
|
135
|
+
reader's. Each origin keeps the first signature its place
|
|
136
|
+
writes.
|
|
137
|
+
Output: {"files": [...], "names": [{"name", "kindLabel",
|
|
138
|
+
"origins": [{"file", "line", "form", "signature"?}]}],
|
|
139
|
+
"summary": {"names", "byForm"}}, names sorted, each one's
|
|
140
|
+
origins in file order then line, `signature` only on an origin
|
|
141
|
+
that has one, `byForm` counting the origins of each form; the
|
|
142
|
+
same skill gives the same bytes.
|
|
143
|
+
|
|
21
144
|
metadata the metadata.json and provenance-map counts. Cluster A
|
|
22
145
|
(`stats.exports_public_api`, the `exports[]` length) and Cluster
|
|
23
146
|
B (`stats.exports_documented`, the provenance entry names, the
|
|
@@ -54,7 +177,10 @@ reading files and writing one JSON object:
|
|
|
54
177
|
glob that matches it (`excludeGlob`) or no
|
|
55
178
|
scope.include glob covering it
|
|
56
179
|
(`outsideInclude`); exports[] only gives each name's
|
|
57
|
-
kind, line and signature line
|
|
180
|
+
kind, line and signature line (a local export
|
|
181
|
+
list's item: its binding's declaration_line and
|
|
182
|
+
declaration_signature_line, when the extractor
|
|
183
|
+
records them), looked up by the
|
|
58
184
|
name its defining file gives it (an aliased
|
|
59
185
|
re-export's `local`) and that file.
|
|
60
186
|
extraction_gaps are listed to read by eye, and
|
|
@@ -142,7 +268,13 @@ reconcile-coverage.py and score-signatures.py --surface-set pick one.
|
|
|
142
268
|
|
|
143
269
|
Candidates and guards (with an extraction, --brief or --metadata): each
|
|
144
270
|
set's count, the metadata `stats.effective_denominator` (--metadata), and
|
|
145
|
-
the extractor's own effective_denominator and its basis
|
|
271
|
+
the extractor's own effective_denominator and its basis; for the
|
|
272
|
+
Denominator annotations, `tierAIncludeFiles` and `scopeIncludeFiles` (the
|
|
273
|
+
distinct files the set's names sit at: the files that define them when
|
|
274
|
+
each name sits at its defining file) and `subpathEntries` (an extraction's
|
|
275
|
+
distinct in-scope non-root, non-wildcard subpaths, each package's subpath
|
|
276
|
+
once however many files it names), each null when its set is absent.
|
|
277
|
+
Both guards run
|
|
146
278
|
only on a surface read from the source. The deflation guard compares a
|
|
147
279
|
re-derived set with `stats.effective_denominator` and names it in `basis`.
|
|
148
280
|
It is the tier_a_include set (`tier_a_include`) when the clause's ladder
|
|
@@ -253,6 +385,7 @@ a stylesheet, holds nothing to read and is in neither list.
|
|
|
253
385
|
|
|
254
386
|
Usage:
|
|
255
387
|
uv run load-coverage-inputs.py census --skill-dir <dir> [--output <file>]
|
|
388
|
+
uv run load-coverage-inputs.py floor --skill-dir <dir> [--output <file>]
|
|
256
389
|
uv run load-coverage-inputs.py metadata --metadata <metadata.json> \\
|
|
257
390
|
[--provenance <provenance-map.json>] [--output <file>]
|
|
258
391
|
uv run load-coverage-inputs.py surface [--extraction <file>] [--quick <file>]... \\
|
|
@@ -264,7 +397,8 @@ Usage:
|
|
|
264
397
|
Exit codes:
|
|
265
398
|
0 the result was printed (and written to --output)
|
|
266
399
|
1 an input file is missing, unreadable or not the JSON (or the YAML
|
|
267
|
-
brief) it should be
|
|
400
|
+
brief) it should be, or the --skill-dir holds no SKILL.md or a skill
|
|
401
|
+
file that is not UTF-8 (one line on stderr)
|
|
268
402
|
2 a per-file result breaks the schema: {"valid": false, "violations":
|
|
269
403
|
[...]} names each breach and nothing is written to --output; or a
|
|
270
404
|
usage error (argparse)
|
|
@@ -294,6 +428,57 @@ CITATION_FORMS = {
|
|
|
294
428
|
"DOC": "[DOC:",
|
|
295
429
|
}
|
|
296
430
|
LOCAL_FORMS = ("AST", "SRC", "from skill")
|
|
431
|
+
# floor: the forms a documented name takes, in the order an origin on one line sorts by.
|
|
432
|
+
FLOOR_FORMS = ("table-row", "heading", "declaration")
|
|
433
|
+
# The first header cells (lower case, singular or plural) of a table whose first column names exports.
|
|
434
|
+
FLOOR_TABLE_HEADERS = frozenset({
|
|
435
|
+
"export", "exports", "symbol", "symbols", "function", "functions", "class", "classes", "interface", "interfaces",
|
|
436
|
+
"type", "types", "hook", "hooks", "component", "components", "helper", "helpers", "constant", "constants",
|
|
437
|
+
"enum", "enums", "api", "apis"})
|
|
438
|
+
# The header words (as _header_word reads them, whole) of a floor table's column that writes the row's signature
|
|
439
|
+
# (#888); a labelled header, such as `Signature (simplified)`, is not one.
|
|
440
|
+
FLOOR_SIGNATURE_HEADERS = frozenset({"signature", "signatures"})
|
|
441
|
+
# A complete form's name, before its type parameters and parameter group (group 1, absent for none).
|
|
442
|
+
SPAN_NAME_RE = re.compile(r"\s*([A-Za-z_$][\w$]*)?\s*")
|
|
443
|
+
# What a complete form writes after its parameter group before its return: `->`, `=>`, `→` or `:` (not `::`).
|
|
444
|
+
RETURN_SEPARATOR_RE = re.compile(r"\s*(?:->|=>|→|:(?!:))")
|
|
445
|
+
# A declaration that gets a form: a function keyword before its name, on its masked line.
|
|
446
|
+
FUNCTION_KEYWORD_RE = re.compile(r"(?<![\w$])(?:function|def|fn|func)(?![\w$])")
|
|
447
|
+
# After a declaration's parameter group: the `:` a TS return type follows (a `{` right after it opens the type, not
|
|
448
|
+
# the body), and a Rust `where` clause, which the body comes after.
|
|
449
|
+
RETURN_COLON_RE = re.compile(r"\s*:\s*")
|
|
450
|
+
WHERE_CLAUSE_RE = re.compile(r"\s+where(?![\w$])")
|
|
451
|
+
# A declaration's text that ends in the middle of its return type: a body opener after one of these is part of that
|
|
452
|
+
# type, cut short (Go's `interface{}` and `struct{...}`, a TS union, intersection, `asserts x is {...}`, `keyof {...}`,
|
|
453
|
+
# conditional or function type), and so is a declaration that ends with one (`): Promise<A> |`, Python's `\`).
|
|
454
|
+
OPENER_IN_TYPE_RE = re.compile(r"(?:(?<![\w$])(?:interface|struct|is|keyof)|[|&?:\\]|=>|->)\s*$")
|
|
455
|
+
# A header cell wrapped in `**` or `__` (group 2 its text).
|
|
456
|
+
EMPHASIS_RE = re.compile(r"(\*\*|__)(.+)\1")
|
|
457
|
+
PLAIN_IDENTIFIER_RE = re.compile(r"[A-Za-z_$][\w$]*")
|
|
458
|
+
TABLE_CELL_RE = re.compile(r"(?<!\\)\|")
|
|
459
|
+
# A comment's opening marker: `//`, `#`, `/*`, `--`, `<!--` or `;`.
|
|
460
|
+
COMMENT_MARKER_RE = re.compile(r"[ \t]*(?://+|#+|/\*+|--|<!--|;+)")
|
|
461
|
+
# A cited path's line suffix: `:12`, `:L12`, `:12-30` or `:L12-L30`.
|
|
462
|
+
LINE_SUFFIX_RE = re.compile(r":L?\d+(?:-L?\d+)?$")
|
|
463
|
+
PYTHON_LANGUAGES = frozenset({"python", "py", "python3"})
|
|
464
|
+
RUST_LANGUAGES = frozenset({"rust", "rs"})
|
|
465
|
+
GO_LANGUAGES = frozenset({"go", "golang"})
|
|
466
|
+
# Column-0 public declarations (group 1 the name), matched on the block with comments and strings masked.
|
|
467
|
+
JS_DECLARATION_RE = re.compile(
|
|
468
|
+
r"export\s+(?:default\s+(?:abstract\s+)?(?:async\s+)?(?:function(?:\s*\*\s*|\s+)|class\s+)"
|
|
469
|
+
r"|(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?"
|
|
470
|
+
r"(?:function(?:\s*\*\s*|\s+)|const\s+enum\s+|const\s+|let\s+|var\s+|class\s+|interface\s+|type\s+|enum\s+))"
|
|
471
|
+
r"(?!(?:extends|implements)(?![\w$]))([A-Za-z_$][\w$]*)")
|
|
472
|
+
PYTHON_DECLARATION_RE = re.compile(r"(?:async\s+def|def|class)\s+([A-Za-z_]\w*)")
|
|
473
|
+
RUST_DECLARATION_RE = re.compile(
|
|
474
|
+
r"pub\s+(?:(?:async|unsafe|const|extern)\s+)*(?:fn|struct|enum|trait|type|const|static)\s+(?:mut\s+)?"
|
|
475
|
+
r"([A-Za-z_]\w*)")
|
|
476
|
+
RUST_MACRO_EXPORT_RE = re.compile(r"#\[macro_export\]")
|
|
477
|
+
RUST_MACRO_RE = re.compile(r"(?:#\[[^\]]*\][ \t]*)*macro_rules![ \t]*([A-Za-z_]\w*)")
|
|
478
|
+
# Go: a single declaration (group 1 its keyword, group 2 its names) or a group's opening line (group 1).
|
|
479
|
+
GO_DECLARATION_RE = re.compile(r"(func|type|var|const)[ \t]+([A-Za-z_]\w*(?:[ \t]*,[ \t]*[A-Za-z_]\w*)*)")
|
|
480
|
+
GO_GROUP_RE = re.compile(r"(type|var|const)[ \t]*\([ \t]*$")
|
|
481
|
+
GO_GROUP_NAMES_RE = re.compile(r"[ \t]+([A-Za-z_]\w*(?:[ \t]*,[ \t]*[A-Za-z_]\w*)*)")
|
|
297
482
|
# Bookkeeping variants folded to their base name, in rule order.
|
|
298
483
|
FOLD_SUFFIXES = ("_def", "_exact")
|
|
299
484
|
FOLD_PREFIX = "a11y_"
|
|
@@ -334,7 +519,9 @@ SHARED_SCRIPTS = Path(__file__).resolve().parent.parent.parent / "shared" / "scr
|
|
|
334
519
|
|
|
335
520
|
def _sibling(filename: str, folder: Path | None = None):
|
|
336
521
|
"""A script beside this one, loaded once: validate-inventory.py (the doc
|
|
337
|
-
text
|
|
522
|
+
text, and the skill citations floor reads), locate-export-segments.py
|
|
523
|
+
(the line model floor reads the skill with) and score-signatures.py (the
|
|
524
|
+
per-file result schema); or, with
|
|
338
525
|
`folder`, one in that folder: SHARED_SCRIPTS'
|
|
339
526
|
skf-resolve-authoritative-files.py (a skill brief's YAML)."""
|
|
340
527
|
module = _SIBLINGS.get(filename)
|
|
@@ -421,6 +608,348 @@ def census(skill_dir: str) -> dict:
|
|
|
421
608
|
}
|
|
422
609
|
|
|
423
610
|
|
|
611
|
+
# --------------------------------------------------------------------------
|
|
612
|
+
# floor
|
|
613
|
+
# --------------------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _floor_name(text: str) -> str | None:
|
|
617
|
+
"""The plain identifier `text` writes once a leading `new ` and everything from the first `(` or `<` are
|
|
618
|
+
cut; None for any other text, a dotted or `::` name among them."""
|
|
619
|
+
text = text.strip()
|
|
620
|
+
if text.startswith("new "):
|
|
621
|
+
text = text[4:]
|
|
622
|
+
text = re.split(r"[(<]", text, maxsplit=1)[0].strip()
|
|
623
|
+
return text if PLAIN_IDENTIFIER_RE.fullmatch(text) else None
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _header_word(cell: str) -> str:
|
|
627
|
+
"""A header cell's word, lower case, unwrapped from `**` or `__`."""
|
|
628
|
+
m = EMPHASIS_RE.fullmatch(cell)
|
|
629
|
+
return (m.group(2) if m else cell).strip().lower()
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _cells(row: str) -> list[str]:
|
|
633
|
+
"""A table row's cells, split on each unescaped `|`, each stripped."""
|
|
634
|
+
row = row.strip()
|
|
635
|
+
if row.startswith("|"):
|
|
636
|
+
row = row[1:]
|
|
637
|
+
if row.endswith("|") and not row.endswith("\\|"):
|
|
638
|
+
row = row[:-1]
|
|
639
|
+
return [cell.strip() for cell in TABLE_CELL_RE.split(row)]
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _whole_span(text: str, segments) -> str | None:
|
|
643
|
+
"""The code span `text` is, when the whole of it is one; else None."""
|
|
644
|
+
spans = segments._inline_code(text)
|
|
645
|
+
if len(spans) != 1:
|
|
646
|
+
return None
|
|
647
|
+
run = len(text) - len(text.lstrip("`"))
|
|
648
|
+
return spans[0] if text == "`" * run + spans[0] + "`" * run else None
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _comment_text(line: str) -> str | None:
|
|
652
|
+
"""A comment line's text, its markers stripped (`// x`, `# x`, `/* x */`, `-- x`, `<!-- x -->`, `; x`);
|
|
653
|
+
None for a line that is no comment. Linear in the line's length."""
|
|
654
|
+
m = COMMENT_MARKER_RE.match(line)
|
|
655
|
+
if not m:
|
|
656
|
+
return None
|
|
657
|
+
text = line[m.end():].strip()
|
|
658
|
+
if text.endswith("-->"):
|
|
659
|
+
text = text[:-3]
|
|
660
|
+
elif text.endswith("*/"):
|
|
661
|
+
text = text[:-2].rstrip("*")
|
|
662
|
+
return text.strip()
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _headed(lines: list[str], opening: int, closing: int, cited: set[str], inventory) -> bool:
|
|
666
|
+
"""Whether a source citation heads the fenced block opening at `opening`: its first non-blank line is a
|
|
667
|
+
comment holding a citation, or naming a path the skill cites (a `:12`, `:L12` or `:12-30` suffix left
|
|
668
|
+
out). A citation on the line above the fence heads nothing."""
|
|
669
|
+
first = next((lines[i] for i in range(opening + 1, closing) if lines[i].strip()), None)
|
|
670
|
+
text = _comment_text(first) if first is not None else None
|
|
671
|
+
if not text:
|
|
672
|
+
return False
|
|
673
|
+
if inventory._SKILL_CITATION_RE.search(text):
|
|
674
|
+
return True
|
|
675
|
+
suffix = LINE_SUFFIX_RE.search(text)
|
|
676
|
+
path = text[:suffix.start()] if suffix else text
|
|
677
|
+
return bool(path) and not any(c.isspace() for c in path) and inventory._cited_path(path) in cited
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _html_comment_lines(lines: list[str], start: int, code: list[bool]) -> set[int]:
|
|
681
|
+
"""The lines inside an HTML comment, the fenced lines it holds included, read as _prose reads one: a line
|
|
682
|
+
outside fenced code that starts `<!--` opens it, the next such line holding `-->` closes it."""
|
|
683
|
+
inside, open_ = set(), False
|
|
684
|
+
for i in range(start, len(lines)):
|
|
685
|
+
if open_:
|
|
686
|
+
inside.add(i)
|
|
687
|
+
open_ = code[i] or "-->" not in lines[i]
|
|
688
|
+
elif not code[i] and lines[i].strip().startswith("<!--"):
|
|
689
|
+
inside.add(i)
|
|
690
|
+
open_ = "-->" not in lines[i].strip()[4:]
|
|
691
|
+
return inside
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _names(text: str) -> list[str]:
|
|
695
|
+
return [name.strip() for name in text.split(",")]
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _declared_names(lines: list[str], language: str, js_family) -> list[tuple[int, str]]:
|
|
699
|
+
"""(offset, name) of each column-0 public declaration in a block's masked lines, by its language."""
|
|
700
|
+
found: list[tuple[int, str]] = []
|
|
701
|
+
if language in js_family:
|
|
702
|
+
found = [(i, m.group(1)) for i, line in enumerate(lines) if (m := JS_DECLARATION_RE.match(line))]
|
|
703
|
+
elif language in PYTHON_LANGUAGES:
|
|
704
|
+
found = [(i, m.group(1)) for i, line in enumerate(lines)
|
|
705
|
+
if (m := PYTHON_DECLARATION_RE.match(line)) and not m.group(1).startswith("_")]
|
|
706
|
+
elif language in RUST_LANGUAGES:
|
|
707
|
+
exported = False # a `#[macro_export]` above, with only blank or attribute lines between
|
|
708
|
+
for i, line in enumerate(lines):
|
|
709
|
+
if m := RUST_DECLARATION_RE.match(line):
|
|
710
|
+
found.append((i, m.group(1)))
|
|
711
|
+
exported = exported or bool(RUST_MACRO_EXPORT_RE.match(line))
|
|
712
|
+
macro = RUST_MACRO_RE.match(line)
|
|
713
|
+
if macro and exported:
|
|
714
|
+
found.append((i, macro.group(1)))
|
|
715
|
+
if macro or (line.strip() and not line.startswith("#[")):
|
|
716
|
+
exported = False
|
|
717
|
+
elif language in GO_LANGUAGES:
|
|
718
|
+
group, depth = False, 0 # inside a `type (`, `var (` or `const (` group, and its bracket depth there
|
|
719
|
+
for i, line in enumerate(lines):
|
|
720
|
+
if group:
|
|
721
|
+
if depth == 0 and line.lstrip().startswith(")"):
|
|
722
|
+
group = False
|
|
723
|
+
continue
|
|
724
|
+
if depth == 0 and (m := GO_GROUP_NAMES_RE.match(line)):
|
|
725
|
+
found += [(i, name) for name in _names(m.group(1)) if name[0].isupper()]
|
|
726
|
+
depth = max(0, depth + sum(map(line.count, "{([")) - sum(map(line.count, "})]")))
|
|
727
|
+
elif GO_GROUP_RE.match(line):
|
|
728
|
+
group, depth = True, 0
|
|
729
|
+
elif m := GO_DECLARATION_RE.match(line):
|
|
730
|
+
found += [(i, name) for name in _names(m.group(2)) if name[0].isupper()]
|
|
731
|
+
return found
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _form(text: str, group: tuple[int, int], extension: str = "") -> dict | None:
|
|
735
|
+
"""{"params", "return_type"} of a form whose parameter group is `group`, whitespace runs read as one space: the
|
|
736
|
+
text inside the group, and the return after it (score-signatures.py's _signature_parts), "" for none; None for a
|
|
737
|
+
form that writes neither (`f()`)."""
|
|
738
|
+
form = {"params": " ".join(text[group[0] + 1:group[1]].split()),
|
|
739
|
+
"return_type": " ".join(_sibling("score-signatures.py")._signature_parts(text, extension)[1].split())}
|
|
740
|
+
return form if form["params"] or form["return_type"] else None
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _type_parameters_only(text: str) -> bool:
|
|
744
|
+
"""Whether `text` is empty or one `<...>` or `[...]` pair, the type parameters a form writes before its group."""
|
|
745
|
+
text = text.strip()
|
|
746
|
+
if not text:
|
|
747
|
+
return True
|
|
748
|
+
if text[0] not in "<[" or text[-1] != {"<": ">", "[": "]"}[text[0]]:
|
|
749
|
+
return False
|
|
750
|
+
depth = 0
|
|
751
|
+
for i, ch in enumerate(text):
|
|
752
|
+
if ch in "<[(":
|
|
753
|
+
depth += 1
|
|
754
|
+
elif ch in ">])" and not (ch == ">" and text[i - 1:i] in ("-", "=")):
|
|
755
|
+
depth -= 1
|
|
756
|
+
if depth == 0 and i != len(text) - 1:
|
|
757
|
+
return False
|
|
758
|
+
return depth == 0
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def _call_form(span: str, name: str | None) -> dict | None:
|
|
762
|
+
"""The signature a span writes when it is a complete form (_form): `name` or no name (a Signature cell's
|
|
763
|
+
`(x: A) => B`), its type parameters, one parameter group (score-signatures.py's _group), then nothing, or a
|
|
764
|
+
return separator (`->`, `=>`, `→` or `:`) and the return, with nothing else: no other text after the group
|
|
765
|
+
(`useQuery(key).data`, `f(a) / f(a, b)`, a trailing comment), no `;`, `,`, `#`, `//` or `/*` in the return
|
|
766
|
+
outside its brackets and strings, and every bracket and string closed. None for any other span, a constructor
|
|
767
|
+
call (`new Client(opts)`) and a span with no parameter group (`Meta<T>`) among them."""
|
|
768
|
+
scorer = _sibling("score-signatures.py")
|
|
769
|
+
text = span.strip()
|
|
770
|
+
group = scorer._group(text)
|
|
771
|
+
if group is None or text.startswith("new ") or scorer._levels(text + " ")[-1] != 0:
|
|
772
|
+
return None
|
|
773
|
+
head = SPAN_NAME_RE.match(text, 0, group[0])
|
|
774
|
+
if head.group(1) not in (None, name) or not _type_parameters_only(text[head.end():group[0]]):
|
|
775
|
+
return None
|
|
776
|
+
tail = text[group[1] + 1:]
|
|
777
|
+
separator = RETURN_SEPARATOR_RE.match(tail)
|
|
778
|
+
returned = tail[separator.end():] if separator else ""
|
|
779
|
+
if (separator is None and tail.strip()) or (separator is not None and not returned.strip()):
|
|
780
|
+
return None
|
|
781
|
+
levels = scorer._levels(returned)
|
|
782
|
+
if any(levels[i] == 0 and (ch in ";,#" or returned.startswith(("//", "/*"), i)) for i, ch in enumerate(returned)):
|
|
783
|
+
return None
|
|
784
|
+
return _form(text, group)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _leading_span(cell: str) -> str | None:
|
|
788
|
+
"""The code span a table cell opens with, None when it opens with none."""
|
|
789
|
+
run = len(cell) - len(cell.lstrip("`"))
|
|
790
|
+
spans = _sibling("locate-export-segments.py")._inline_code(cell) if run else []
|
|
791
|
+
return spans[0] if spans and cell.startswith("`" * run + spans[0] + "`" * run) else None
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _row_form(header: list[str], row: list[str]) -> dict | None:
|
|
795
|
+
"""The signature a floor table's row writes for the one name its first cell names (a row naming several gives
|
|
796
|
+
none), each cell's `\\|` read as `|`: the leading code span of its Signature or Signatures cell, else, when that
|
|
797
|
+
span writes no parameter group, the one span of its first cell that gives that name (another export's span there
|
|
798
|
+
gives nothing), each a complete form (_call_form). A Signature span with a group that is no complete form gives
|
|
799
|
+
none. A Props, Params or Parameters cell is never read."""
|
|
800
|
+
segments = _sibling("locate-export-segments.py")
|
|
801
|
+
names = {name for name in map(_floor_name, segments._inline_code(row[0])) if name}
|
|
802
|
+
if len(names) != 1:
|
|
803
|
+
return None
|
|
804
|
+
[name] = names
|
|
805
|
+
cells = [cell.replace("\\|", "|") for cell in row]
|
|
806
|
+
at = next((i for i, cell in enumerate(header) if _header_word(cell) in FLOOR_SIGNATURE_HEADERS), None)
|
|
807
|
+
span = _leading_span(cells[at]) if at is not None and at < len(cells) else None
|
|
808
|
+
if span is not None and (form := _call_form(span, name)):
|
|
809
|
+
return form
|
|
810
|
+
if span is not None and _sibling("score-signatures.py")._group(span.strip()) is not None:
|
|
811
|
+
return None # the cell writes a call the floor cannot pin: a shorter first-cell form would hide it
|
|
812
|
+
own = [span for span in segments._inline_code(cells[0]) if _floor_name(span) == name]
|
|
813
|
+
return _call_form(own[0], name) if len(own) == 1 else None
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _function_declaration(line: str, language: str, js_family) -> int | None:
|
|
817
|
+
"""Where the name ends on a block's masked line, one _declared_names reads, that declares a function (`function`,
|
|
818
|
+
`def`, `fn` or Go's `func` before its name); None for a line that declares no function."""
|
|
819
|
+
if language in js_family:
|
|
820
|
+
m, at = JS_DECLARATION_RE.match(line), 1
|
|
821
|
+
elif language in PYTHON_LANGUAGES:
|
|
822
|
+
m, at = PYTHON_DECLARATION_RE.match(line), 1
|
|
823
|
+
elif language in RUST_LANGUAGES:
|
|
824
|
+
m, at = RUST_DECLARATION_RE.match(line), 1
|
|
825
|
+
elif language in GO_LANGUAGES:
|
|
826
|
+
m, at = GO_DECLARATION_RE.match(line), 2
|
|
827
|
+
else:
|
|
828
|
+
return False
|
|
829
|
+
return m.end(at) if m and FUNCTION_KEYWORD_RE.search(line, 0, m.start(at)) else None
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def _declaration_form(rows: list[str], offset: int, language: str, name_end: int) -> dict | None:
|
|
833
|
+
"""The signature the function declaration at `offset` of a block's unmasked `rows` writes: the declaration on one
|
|
834
|
+
line (skf-extract-public-api.py's _declaration_signature: a parameter list split over lines joined, line comments
|
|
835
|
+
left out), its body cut at the first `:` (Python) or `{`, `;` or ` where ` (any other language) outside every
|
|
836
|
+
bracket pair and string after the parameter group, a `{` directly after a TS return `:` opening the return type,
|
|
837
|
+
then read as _form reads it. None when anything but type parameters sits between the name's end (`name_end`,
|
|
838
|
+
the masked line's offset, which masking keeps) and the group (a Go `func(...)` return read as the group), when
|
|
839
|
+
the opener follows, or the uncut text ends with, `interface`, `struct`, `is`, `keyof`, `|`, `&`, `?`, `:`, `\\`,
|
|
840
|
+
`=>` or `->` (it is inside the return type: Go's `interface{}`, a TS `asserts x is {...}`, `() => {...}` or
|
|
841
|
+
conditional type, a line that ends mid-type), or a bracket or string it opens does not close."""
|
|
842
|
+
scorer = _sibling("score-signatures.py")
|
|
843
|
+
extractor = _sibling("skf-extract-public-api.py", SHARED_SCRIPTS)
|
|
844
|
+
python, rust = language in PYTHON_LANGUAGES, language in RUST_LANGUAGES
|
|
845
|
+
extension = ".rs" if rust else ""
|
|
846
|
+
text = extractor._declaration_signature(rows, offset, "python" if python else "rust" if rust else language)
|
|
847
|
+
group = scorer._group(text, extension)
|
|
848
|
+
if group is None or not _type_parameters_only(text[name_end:group[0]]):
|
|
849
|
+
return None
|
|
850
|
+
levels = scorer._levels(text, extension)
|
|
851
|
+
for i in range(group[1] + 1, len(text)):
|
|
852
|
+
if levels[i] != 0:
|
|
853
|
+
continue
|
|
854
|
+
if python:
|
|
855
|
+
body = text[i] == ":"
|
|
856
|
+
else:
|
|
857
|
+
body = (text[i] == ";" or bool(WHERE_CLAUSE_RE.match(text, i))
|
|
858
|
+
or (text[i] == "{" and not RETURN_COLON_RE.fullmatch(text, group[1] + 1, i)))
|
|
859
|
+
if body:
|
|
860
|
+
if OPENER_IN_TYPE_RE.search(text, 0, i):
|
|
861
|
+
return None
|
|
862
|
+
text = text[:i]
|
|
863
|
+
break
|
|
864
|
+
else:
|
|
865
|
+
if OPENER_IN_TYPE_RE.search(text):
|
|
866
|
+
return None
|
|
867
|
+
if scorer._levels(text + " ", extension)[-1] != 0:
|
|
868
|
+
return None
|
|
869
|
+
return _form(text, group, extension)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def floor(skill_dir: str) -> dict:
|
|
873
|
+
"""The names the skill documents in a fixed form, each with its origins (see the module docstring)."""
|
|
874
|
+
root = Path(skill_dir)
|
|
875
|
+
if not (root / "SKILL.md").is_file():
|
|
876
|
+
raise InputError(f"--skill-dir {skill_dir} holds no SKILL.md")
|
|
877
|
+
segments = _sibling("locate-export-segments.py")
|
|
878
|
+
inventory = _sibling("validate-inventory.py")
|
|
879
|
+
refs = root / "references"
|
|
880
|
+
files = ["SKILL.md"] + ([f"references/{p.name}" for p in sorted(refs.glob("*.md")) if p.is_file()]
|
|
881
|
+
if refs.is_dir() else [])
|
|
882
|
+
texts = []
|
|
883
|
+
for rel in files:
|
|
884
|
+
try:
|
|
885
|
+
texts.append(segments._split_lines((root / rel).read_bytes().decode("utf-8-sig")))
|
|
886
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
887
|
+
raise InputError(f"cannot read {rel} under {skill_dir}: {exc}") from exc
|
|
888
|
+
cited = {inventory._cited_path(m.group(2))
|
|
889
|
+
for lines in texts for line in lines for m in inventory._SKILL_CITATION_RE.finditer(line)}
|
|
890
|
+
|
|
891
|
+
found: dict[str, set[tuple[int, int, str, str | None]]] = {}
|
|
892
|
+
signatures: dict[tuple[str, int, int, str], dict] = {} # the first per (name, file, line, form), in span order
|
|
893
|
+
|
|
894
|
+
def add(name: str | None, file_index: int, index: int, form: str, label: str | None = None,
|
|
895
|
+
signature: dict | None = None) -> None:
|
|
896
|
+
if name:
|
|
897
|
+
found.setdefault(name, set()).add((file_index, index + 1, form, label))
|
|
898
|
+
if signature is not None:
|
|
899
|
+
signatures.setdefault((name, file_index, index + 1, form), signature)
|
|
900
|
+
|
|
901
|
+
js_family = segments.JS_FAMILY - {"svelte"} # a Svelte block's `export let` is a component prop
|
|
902
|
+
for file_index, lines in enumerate(texts):
|
|
903
|
+
start = segments._frontmatter_end(lines)
|
|
904
|
+
blocks, code = segments._fenced_blocks(lines, start)
|
|
905
|
+
headings, prose = segments._prose(lines, start, code)
|
|
906
|
+
for block in prose:
|
|
907
|
+
if block["kind"] != "table-row" or block["header"] is None:
|
|
908
|
+
continue
|
|
909
|
+
header = _cells(block["header"])
|
|
910
|
+
if _header_word(header[0]) not in FLOOR_TABLE_HEADERS:
|
|
911
|
+
continue
|
|
912
|
+
row = _cells(lines[block["start"]])
|
|
913
|
+
kind_at = next((i for i, cell in enumerate(header) if _header_word(cell) == "kind"), None)
|
|
914
|
+
label = row[kind_at] if kind_at is not None and kind_at < len(row) and row[kind_at] else None
|
|
915
|
+
signature = _row_form(header, row)
|
|
916
|
+
for span in segments._inline_code(row[0]):
|
|
917
|
+
add(_floor_name(span), file_index, block["start"], "table-row", label, signature)
|
|
918
|
+
for index, _level, text, _above in headings:
|
|
919
|
+
span = _whole_span(text, segments)
|
|
920
|
+
if span is not None:
|
|
921
|
+
add(_floor_name(span), file_index, index, "heading", None, _call_form(span, _floor_name(span)))
|
|
922
|
+
comments = _html_comment_lines(lines, start, code)
|
|
923
|
+
for opening, closing, info in blocks:
|
|
924
|
+
language = segments._language(info)
|
|
925
|
+
if (opening in comments or language in segments.DATA_LANGUAGES
|
|
926
|
+
or not _headed(lines, opening, closing, cited, inventory)):
|
|
927
|
+
continue
|
|
928
|
+
masked = segments.mask_code("\n".join(lines[opening + 1:closing]), language).split("\n")
|
|
929
|
+
for offset, name in _declared_names(masked, language, js_family):
|
|
930
|
+
name_end = _function_declaration(masked[offset], language, js_family)
|
|
931
|
+
signature = (_declaration_form(lines[opening + 1:closing], offset, language, name_end)
|
|
932
|
+
if name_end is not None else None)
|
|
933
|
+
add(name, file_index, opening + 1 + offset, "declaration", None, signature)
|
|
934
|
+
|
|
935
|
+
names = []
|
|
936
|
+
by_form = dict.fromkeys(FLOOR_FORMS, 0)
|
|
937
|
+
for name in sorted(found):
|
|
938
|
+
origins = sorted(found[name], key=lambda o: (o[0], o[1], FLOOR_FORMS.index(o[2]), o[3] or ""))
|
|
939
|
+
seen, kept = set(), []
|
|
940
|
+
for file_index, line, form, _label in origins:
|
|
941
|
+
if (file_index, line, form) not in seen:
|
|
942
|
+
seen.add((file_index, line, form))
|
|
943
|
+
origin = {"file": files[file_index], "line": line, "form": form}
|
|
944
|
+
if (name, file_index, line, form) in signatures:
|
|
945
|
+
origin["signature"] = signatures[(name, file_index, line, form)]
|
|
946
|
+
kept.append(origin)
|
|
947
|
+
by_form[form] += 1
|
|
948
|
+
label = next((o[3] for o in origins if o[2] == "table-row" and o[3]), None)
|
|
949
|
+
names.append({"name": name, "kindLabel": label, "origins": kept})
|
|
950
|
+
return {"files": files, "names": names, "summary": {"names": len(names), "byForm": by_form}}
|
|
951
|
+
|
|
952
|
+
|
|
424
953
|
# --------------------------------------------------------------------------
|
|
425
954
|
# metadata
|
|
426
955
|
# --------------------------------------------------------------------------
|
|
@@ -577,6 +1106,14 @@ def matching(records: list[dict], globs: list[str], excludes: list[str]) -> list
|
|
|
577
1106
|
and not any(glob_match(_norm(r["file"]), g) for g in excludes)})
|
|
578
1107
|
|
|
579
1108
|
|
|
1109
|
+
def matching_files(records: list[dict], globs: list[str], excludes: list[str]) -> list[str]:
|
|
1110
|
+
"""The distinct files `matching` takes its names from: each record's file one of `globs` matches and no
|
|
1111
|
+
exclude glob does."""
|
|
1112
|
+
return sorted({r["file"] for r in records if r.get("file")
|
|
1113
|
+
and any(glob_match(_norm(r["file"]), g) for g in globs)
|
|
1114
|
+
and not any(glob_match(_norm(r["file"]), g) for g in excludes)})
|
|
1115
|
+
|
|
1116
|
+
|
|
580
1117
|
def _apart(a: str, b: str) -> bool:
|
|
581
1118
|
"""True when, before either has a `*` or `?`, the two globs spell a different segment at the same
|
|
582
1119
|
place, so no file can match both. Any other pair counts as overlapping, even one no file can match
|
|
@@ -735,7 +1272,7 @@ def from_extraction(data: dict, language: str | None = None, brief: dict | None
|
|
|
735
1272
|
"staleScope": {"unmatchedInclude": unmatched, "unmatchedTierAInclude": unmatched_tier_a,
|
|
736
1273
|
"restored": []},
|
|
737
1274
|
"gaps": [], "scope": scope, "scopeType": recorded.get("type"), "umbrella": dict(NO_UMBRELLA),
|
|
738
|
-
"extraction": extraction}
|
|
1275
|
+
"subpathEntries": None, "extraction": extraction}
|
|
739
1276
|
|
|
740
1277
|
diff = data.get("entry_point_diff") if isinstance(data.get("entry_point_diff"), dict) else {}
|
|
741
1278
|
# Each in-scope definition of a public name is a record of its own (#830): the runner's other
|
|
@@ -760,6 +1297,11 @@ def from_extraction(data: dict, language: str | None = None, brief: dict | None
|
|
|
760
1297
|
subpath_files = {e.get("file") for e in entry_files
|
|
761
1298
|
if e.get("subpath") not in ROOT_SUBPATHS and "*" not in str(e.get("subpath"))}
|
|
762
1299
|
root_files = {e.get("file") for e in entry_files if e.get("subpath") in ROOT_SUBPATHS} - set(nested)
|
|
1300
|
+
# The distinct in-scope subpaths of those (a package's subpath once, however many files its conditions
|
|
1301
|
+
# name), for the multi-entry annotation.
|
|
1302
|
+
subpath_entries = {(e.get("package"), e.get("subpath")) for e in entry_files
|
|
1303
|
+
if e.get("subpath") not in ROOT_SUBPATHS and "*" not in str(e.get("subpath"))
|
|
1304
|
+
and e.get("in_scope") is not False}
|
|
763
1305
|
|
|
764
1306
|
# A scope.include glob that matches no file: the brief predates the
|
|
765
1307
|
# source, so a root export defined in a file no glob covers comes back,
|
|
@@ -790,10 +1332,16 @@ def from_extraction(data: dict, language: str | None = None, brief: dict | None
|
|
|
790
1332
|
# under the name its defining file gives it, `local`.
|
|
791
1333
|
found = index.get((info.get("local") or name, file)) or index.get((name, file))
|
|
792
1334
|
kind = found.get("export_type") if found else None
|
|
1335
|
+
# A local `export { ... }` list's item is planned at the declaration it
|
|
1336
|
+
# names (#836): the line and the signature line both come from that pair.
|
|
1337
|
+
if found and isinstance(found.get("declaration_line"), int):
|
|
1338
|
+
line, signature_line = found["declaration_line"], found.get("declaration_signature_line")
|
|
1339
|
+
elif found:
|
|
1340
|
+
line, signature_line = found.get("source_line"), found.get("signature_line")
|
|
1341
|
+
else:
|
|
1342
|
+
line, signature_line = info.get("line"), None
|
|
793
1343
|
(nested_records if info.get("entry") in nested else records).append(
|
|
794
|
-
_record(name, None if kind == "re-export" else kind, file,
|
|
795
|
-
found.get("source_line") if found else info.get("line"),
|
|
796
|
-
found.get("signature_line") if found else None, "extraction"))
|
|
1344
|
+
_record(name, None if kind == "re-export" else kind, file, line, signature_line, "extraction"))
|
|
797
1345
|
|
|
798
1346
|
# The umbrella ratio reads the public rows alone: an other definition is no root barrel name there.
|
|
799
1347
|
root_names = [p for p in _members_in_place(rows["public"]) if p.get("entry") in root_files]
|
|
@@ -824,6 +1372,8 @@ def from_extraction(data: dict, language: str | None = None, brief: dict | None
|
|
|
824
1372
|
"ratio": round2(len(reexported) / len(root_names)) if root_names else None,
|
|
825
1373
|
"umbrella": bool(root_names) and len(reexported) / len(root_names) > UMBRELLA_RATIO,
|
|
826
1374
|
},
|
|
1375
|
+
# The in-scope subpaths the `subpaths` set reads (None when there is no such set).
|
|
1376
|
+
"subpathEntries": len(subpath_entries) if subpath_files else None,
|
|
827
1377
|
"extraction": extraction,
|
|
828
1378
|
}
|
|
829
1379
|
|
|
@@ -1065,13 +1615,17 @@ def surface(args: argparse.Namespace) -> dict:
|
|
|
1065
1615
|
and not (scope["include"] and all(_apart(g, i) for i in scope["include"]))
|
|
1066
1616
|
and not any(_inside(g, e) for e in scope["exclude"])]
|
|
1067
1617
|
filed = [r for r in records if r["file"]]
|
|
1618
|
+
# The files each scope set's names sit at, for the Denominator annotations' file counts.
|
|
1619
|
+
set_files: dict[str, list[str]] = {}
|
|
1068
1620
|
if filed and scope["include"]:
|
|
1069
1621
|
sets["scope.include"] = matching(filed, scope["include"], scope["exclude"])
|
|
1622
|
+
set_files["scope.include"] = matching_files(filed, scope["include"], scope["exclude"])
|
|
1070
1623
|
in_include = [r for r in filed if not scope["include"]
|
|
1071
1624
|
or any(glob_match(_norm(r["file"]), g) for g in scope["include"])]
|
|
1072
1625
|
tier_a_names = matching(in_include, tier_a, scope["exclude"]) if tier_a else []
|
|
1073
1626
|
if tier_a_names:
|
|
1074
1627
|
sets["tier_a_include"] = tier_a_names
|
|
1628
|
+
set_files["tier_a_include"] = matching_files(in_include, tier_a, scope["exclude"])
|
|
1075
1629
|
|
|
1076
1630
|
# The clause: an extraction's entry-point status (one the per-file scan replaces says only that it found no
|
|
1077
1631
|
# entry point), else a quick output's (an export is a barrel; none, and no warning, an empty one), else what
|
|
@@ -1119,7 +1673,10 @@ def surface(args: argparse.Namespace) -> dict:
|
|
|
1119
1673
|
"statsEffectiveDenominator": stats_eff,
|
|
1120
1674
|
"tierAIncludeUnion": len(sets["tier_a_include"]) if "tier_a_include" in sets else None,
|
|
1121
1675
|
"scopeIncludeUnion": len(sets["scope.include"]) if "scope.include" in sets else None,
|
|
1676
|
+
"tierAIncludeFiles": len(set_files["tier_a_include"]) if "tier_a_include" in sets else None,
|
|
1677
|
+
"scopeIncludeFiles": len(set_files["scope.include"]) if "scope.include" in sets else None,
|
|
1122
1678
|
"subpathUnion": len(sets["subpaths"]) if "subpaths" in sets else None,
|
|
1679
|
+
"subpathEntries": extracted["subpathEntries"] if extracted is not None else None,
|
|
1123
1680
|
"rootBarrel": len(sets["root"]) if "root" in sets else None,
|
|
1124
1681
|
"extractorEffectiveDenominator": extraction.get("effectiveDenominator"),
|
|
1125
1682
|
"extractorBasis": extraction.get("effectiveDenominatorBasis"),
|
|
@@ -1201,6 +1758,10 @@ def _cmd_census(args: argparse.Namespace) -> dict:
|
|
|
1201
1758
|
return census(args.skill_dir)
|
|
1202
1759
|
|
|
1203
1760
|
|
|
1761
|
+
def _cmd_floor(args: argparse.Namespace) -> dict:
|
|
1762
|
+
return floor(args.skill_dir)
|
|
1763
|
+
|
|
1764
|
+
|
|
1204
1765
|
def _cmd_metadata(args: argparse.Namespace) -> dict:
|
|
1205
1766
|
meta = _object(args.metadata, "--metadata")
|
|
1206
1767
|
provenance = _object(args.provenance, "--provenance") if args.provenance else None
|
|
@@ -1219,6 +1780,11 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1219
1780
|
p.add_argument("--output", help="also write the result to this file")
|
|
1220
1781
|
p.set_defaults(func=_cmd_census)
|
|
1221
1782
|
|
|
1783
|
+
p = sub.add_parser("floor", help="the names the skill documents in a fixed form, which the inventory keeps")
|
|
1784
|
+
p.add_argument("--skill-dir", required=True, help="the skill package: SKILL.md and references/")
|
|
1785
|
+
p.add_argument("--output", help="also write the result to this file")
|
|
1786
|
+
p.set_defaults(func=_cmd_floor)
|
|
1787
|
+
|
|
1222
1788
|
p = sub.add_parser("metadata", help="the metadata.json and provenance-map counts")
|
|
1223
1789
|
p.add_argument("--metadata", required=True, help="the skill's metadata.json")
|
|
1224
1790
|
p.add_argument("--provenance", help="the provenance map, when there is one")
|