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
package/docs/_data/pinned.yaml
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
# Enforced two ways: (1) release.yaml bumps this line in the same commit
|
|
33
33
|
# as package.json + marketplace.json on every release; (2) validate-docs-drift.js
|
|
34
34
|
# cross-checks this value against package.json.version and fails on mismatch.
|
|
35
|
-
skf_version: "
|
|
35
|
+
skf_version: "9.0.0"
|
|
36
36
|
|
|
37
37
|
# Path to the oh-my-skills repo, resolved relative to this repo's root.
|
|
38
38
|
# Override at runtime with the OMS environment variable if oh-my-skills
|
|
@@ -50,7 +50,7 @@ The 1.6.2 map passes the [line, label and citation checks](#workflow-time-enforc
|
|
|
50
50
|
|
|
51
51
|
Open `{source_repo}` at `{source_commit}`, jump to `{source_file}` line `{source_line}`. The signature in `SKILL.md` should match what you see in the source.
|
|
52
52
|
|
|
53
|
-
If it doesn't, **that's a bug**. For a skill you built, Test Skill (`@Ferris TS`) rates a signature mismatch as a Critical gap, which stops the test at its [hard gate](#hard-gate). It compares only what a signature writes: a component by its props alone, so `FC<P>` and `(props: P) => JSX.Element` are one signature, and a type or a return type the skill does not write, or type arguments it leaves out (`dict` against `dict[str, Any]`), are not compared. When the skill documents a name in several places, such as a brief usage in `SKILL.md` and a full props list in a reference file, each form that lists parameters
|
|
53
|
+
If it doesn't, **that's a bug**. For a skill you built, Test Skill (`@Ferris TS`) rates a signature mismatch as a Critical gap, which stops the test at its [hard gate](#hard-gate). It compares only what a signature writes: a component by its props alone, so `FC<P>` and `(props: P) => JSX.Element` are one signature, and a type or a return type the skill does not write, or type arguments it leaves out (`dict` against `dict[str, Any]`), are not compared. When the skill documents a name in several places, such as a brief usage in `SKILL.md` and a full props list in a reference file, each form that lists parameters, gives a usage or states a return type is compared, and a `const` holding a component, such as a `forwardRef(...)` result, is compared by the usage or declaration the skill writes for it. When the source declares two functions of one name, a form is compared only with the one its own `[AST:]` or `[SRC:]` citation names, at a line or in a range that holds its line or a later one before the next declaration the extraction records in that file. Test Skill reads that citation on the form's line, or on the next non-blank line unless that line writes the name itself, and, for a form in a code block, in a comment just above it in the block or, when the block documents no other form of the name, in a comment on the block's first non-blank line or on the first non-blank line after the block, unless that line writes the name. A line that writes two forms of the name that differ, such as a table row that quotes the other function's form as a contrast, gives none of those forms a citation. A form whose citations name neither function, or both, is compared as before, with the declarations the package exports. Two differences a caller never meets are rated lower and still count as matching: a renamed parameter in a TypeScript, JavaScript, Go, Rust, Java, C or C++ file, whose callers pass arguments by position, is a Low gap, and an optional parameter left out of a signature the skill labels partial (such as `Props (simplified)`) is an Info observation. A renamed Python parameter stays Critical, because Test Skill cannot see which parameters a caller must pass by position, and so does a renamed Vue component prop, which callers set by name. Update Skill (`@Ferris US`) with `--from-test-report` repairs the gaps a test report lists. If you think SKF extracted it wrong, [open an issue](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/new/choose). Falsifiability isn't a feature. It's the whole deal.
|
|
54
54
|
|
|
55
55
|
### Workflow-time enforcement
|
|
56
56
|
|
|
@@ -135,7 +135,7 @@ The score is a weighted sum of up to five categories. The weights below are the
|
|
|
135
135
|
| Category | Weight | What it measures |
|
|
136
136
|
|---|---|---|
|
|
137
137
|
| **Export Coverage** | 36% | Percentage of source exports documented in `SKILL.md` |
|
|
138
|
-
| **Signature Accuracy** | 22% | Documented function signatures match actual source signatures (parameter names, types, order, return types). A renamed parameter in a TypeScript, JavaScript, Go, Rust, Java, C or C++ file, whose callers pass every argument by position, still matches, and so does a signature the skill labels partial that leaves out only optional parameters. A component is compared by its props alone, a type or return type a signature does not write is not compared, and each form a name is documented in is compared on its own |
|
|
138
|
+
| **Signature Accuracy** | 22% | Documented function signatures match actual source signatures (parameter names, types, order, return types). A renamed parameter in a TypeScript, JavaScript, Go, Rust, Java, C or C++ file, whose callers pass every argument by position, still matches, and so does a signature the skill labels partial that leaves out only optional parameters. A component is compared by its props alone, a type or return type a signature does not write is not compared, and each form a name is documented in is compared on its own, with the declaration its own citation names when the source has two of that name |
|
|
139
139
|
| **Type Coverage** | 14% | Types and interfaces referenced in exports are fully documented |
|
|
140
140
|
| **Coherence** | 18% | Cross-references resolve, integration patterns are complete (contextual mode only) |
|
|
141
141
|
| **External Validation** | 10% | skill-check quality score (0–100), averaged with the Tessl Review score (0–100) when you opt in to Tessl Review |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "bmad-module-skill-forge",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "9.0.0",
|
|
5
5
|
"description": "BMAD module — Turn code and docs into instructions AI agents can actually follow. Progressive capability tiers (Quick/Forge/Forge+/Deep).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"bmad",
|
|
@@ -457,8 +457,9 @@ them). A namespace with `members` is diffed as its members: each is found
|
|
|
457
457
|
when a recipe found its name, or its `local` one, in its file (one with
|
|
458
458
|
no file by its name alone, as a public name is), and has no row of its
|
|
459
459
|
own when a public name (or another namespace's member) has its name and
|
|
460
|
-
file;
|
|
461
|
-
|
|
460
|
+
file; a member's own outside_scope row names its namespace (`namespace`);
|
|
461
|
+
one with null `members`, and a member that is such a module, is no gap.
|
|
462
|
+
In a family with no entry point, or only empty ones, every name the
|
|
462
463
|
recipes found is public. A JS/TS module exports, besides the names the
|
|
463
464
|
recipes match, those its column-0 `export` statements declare in a form
|
|
464
465
|
no recipe matches, read from its text: `export let`, `export declare
|
|
@@ -582,7 +583,8 @@ Output JSON (stdout, or -o):
|
|
|
582
583
|
"local", "file", "line"}, ...], # by name, then file
|
|
583
584
|
"internal": [{"name", "language", "source_file", "source_line"}, ...],
|
|
584
585
|
"extraction_gaps": [{"name", "language", "entry", "file", "line"}, ...],
|
|
585
|
-
"outside_scope": [{"name", "language", "entry", "file", "line"
|
|
586
|
+
"outside_scope": [{"name", "language", "entry", "file", "line",
|
|
587
|
+
"namespace"}, ...]}, # namespace: a member's row only
|
|
586
588
|
"reexport_targets": [{"name", "language", "local", "from", "entry",
|
|
587
589
|
"file", "line", "via"}, ...],
|
|
588
590
|
"counts": {"exports_public_api": N, # (family, name, file) keys
|
|
@@ -4439,9 +4441,11 @@ def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries
|
|
|
4439
4441
|
`members` has each member diffed in its place, found when a recipe
|
|
4440
4442
|
found its name, or its `local` one, in its file, and given no row of
|
|
4441
4443
|
its own when a declared name has its name and file (or another
|
|
4442
|
-
namespace gave it one)
|
|
4443
|
-
|
|
4444
|
-
|
|
4444
|
+
namespace gave it one), else a row that names the namespace
|
|
4445
|
+
(`namespace`, which only an outside_scope row writes out); one whose
|
|
4446
|
+
members are unknown (None), or a member that is such a module
|
|
4447
|
+
(`opaque`), is no gap. In a family with no entry point, or only empty
|
|
4448
|
+
ones, every export is public."""
|
|
4445
4449
|
statuses: dict[str, str] = {}
|
|
4446
4450
|
public: dict[tuple[str, str], dict] = {}
|
|
4447
4451
|
internal: list[dict] = []
|
|
@@ -4495,7 +4499,7 @@ def entry_point_diff(exports: list[dict], surfaces: dict[str, _Surface], entries
|
|
|
4495
4499
|
continue
|
|
4496
4500
|
given.add(key)
|
|
4497
4501
|
row = {"name": member["name"], "entry": info["entry"], "file": member["file"],
|
|
4498
|
-
"line": member["line"]}
|
|
4502
|
+
"line": member["line"], "namespace": info["name"]}
|
|
4499
4503
|
(gaps if member["file"] is None or member["file"] in in_scope else outside).append((family, row))
|
|
4500
4504
|
return statuses, public, internal, gaps, outside, others
|
|
4501
4505
|
|
|
@@ -4946,8 +4950,11 @@ def run_full(args: argparse.Namespace) -> tuple[dict, int]:
|
|
|
4946
4950
|
"extraction_gaps": [{"name": info["name"], "language": family, "entry": info["entry"],
|
|
4947
4951
|
"file": info["file"], "line": info["line"]}
|
|
4948
4952
|
for family, info in sorted(gaps, key=lambda g: (g[1]["name"], g[0]))],
|
|
4953
|
+
# a namespace's member names its namespace (#969); a declared name's row keeps five keys
|
|
4949
4954
|
"outside_scope": [{"name": info["name"], "language": family, "entry": info["entry"],
|
|
4950
|
-
"file": info["file"], "line": info["line"]
|
|
4955
|
+
"file": info["file"], "line": info["line"],
|
|
4956
|
+
**({"namespace": info["namespace"]} if isinstance(info.get("namespace"), str)
|
|
4957
|
+
and info["namespace"] else {})}
|
|
4951
4958
|
for family, info in sorted(outside, key=lambda g: (g[1]["name"], g[0]))],
|
|
4952
4959
|
},
|
|
4953
4960
|
"reexport_targets": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
## Coherence Analysis — Naive Mode: Reference Consistency (split-body)
|
|
22
22
|
|
|
23
|
-
Only rendered when `references/` directory exists alongside SKILL.md. Each row is a split-body gap coverage-check recorded. **Other Side** is the other file of its pair, at its line: `SKILL.md:{skill_md_line}`, or `{other_file}:{other_line}` for two references. Its Source is at **Other Side** when the pinned source contradicts that side and agrees with **Reference File**, else at `{Reference File}:{Reference Line}`. **Contradicted Side** is the path of the one file the source contradicts (`SKILL.md` or a `references/` file's path), `none` when both forms agree with it, or `not settled` when it contradicts both or no source settled it.
|
|
23
|
+
Only rendered when `references/` directory exists alongside SKILL.md. Each row is a split-body gap coverage-check recorded. **Other Side** is the other file of its pair, at its line: `SKILL.md:{skill_md_line}`, or `{other_file}:{other_line}` for two references. Its Source is at **Other Side** when the pinned source contradicts that side and agrees with **Reference File**, else at `{Reference File}:{Reference Line}`. **Contradicted Side** is the path of the one file the source contradicts (`SKILL.md` or a `references/` file's path), `none` when both forms agree with it, or `not settled` when it contradicts both or no source settled it. **Exports Cross-Checked** is `crossChecked.count` from `{run_dir}/inventory.json`, which `scripts/validate-inventory.py` writes: the inventory's names that two or more of the files it reads write, never counted by hand.
|
|
24
24
|
|
|
25
25
|
```markdown
|
|
26
26
|
### Reference Consistency (split-body)
|
|
@@ -29,7 +29,7 @@ Only rendered when `references/` directory exists alongside SKILL.md. Each row i
|
|
|
29
29
|
|---|---------------|--------|-------|------------|---------------|-------------------|
|
|
30
30
|
| {per-mismatch rows} |
|
|
31
31
|
|
|
32
|
-
**Exports Cross-Checked:** {count}
|
|
32
|
+
**Exports Cross-Checked:** {crossChecked.count}
|
|
33
33
|
**Mismatches Found:** {count}
|
|
34
34
|
```
|
|
35
35
|
|
|
@@ -41,7 +41,7 @@ Run the command of the branch §2b picked; §2c's exits apply to it:
|
|
|
41
41
|
uv run {reconcileScript} --denominator-source stack --coverage-inputs "{run_dir}/coverage-inputs.json" --skill-dir "{resolved_skill_package}" --output "{run_dir}/coverage.json"
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
**The scalar branch counts §2b's set** only when `surface.json` `denominator.setCounted` is true: a scope set (not `all`) whose every name sits at the file that defines it (a complete runner extraction, the fallback scan, or quick output that traces each name), read whole. Then `numeratorSource` is `surface`: `documented` counts the documented names the set holds (`documentedNames`), so a documented name the set lacks never offsets an undocumented one; `missing` lists the set's undocumented names; §5's render draws the per-export table; and the denominator stays the stored count, so when the set's size (`surfaceSetSize`) differs from it, `warnings` names both. Otherwise (`all`, a set read by eye or from an incomplete extraction, or an empty set, which warns) the branch looks each documented name up in the skill, `--verified` applies, and `missing` lists no name. On any surface read from the source (`guards.deflation.applicable` true), `stale` lists the documented names the whole surface (the set
|
|
44
|
+
**The scalar branch counts §2b's set** only when `surface.json` `denominator.setCounted` is true: a scope set (not `all`) whose every name sits at the file that defines it (a complete runner extraction, the fallback scan, or quick output that traces each name), read whole. Then `numeratorSource` is `surface`: `documented` counts the documented names the set holds (`documentedNames`), so a documented name the set lacks never offsets an undocumented one; `missing` lists the set's undocumented names; §5's render draws the per-export table; and the denominator stays the stored count, so when the set's size (`surfaceSetSize`) differs from it, `warnings` names both. Otherwise (`all`, a set read by eye or from an incomplete extraction, or an empty set, which warns) the branch looks each documented name up in the skill, `--verified` applies, and `missing` lists no name. On any surface read from the source (`guards.deflation.applicable` true), `stale` lists the documented names the whole surface lacks (the set, the `all` set and each name on a `surface.json` `exports` record, a nested entry's included: the barrel branch finds `stale` against the same surface), with `staleApplicable` true, so §2c's Stale names bullet applies: a documented name outside the set that the source still exports is neither counted nor stale. §5's render lists each `warnings` entry.
|
|
45
45
|
|
|
46
46
|
**A surplus is a signal, not a pass.** Where the numerator and denominator count independent sets (the stack branch, and the scalar branch when it counts no set), `documented` can exceed `denominator`, as it can when a counted set holds more names than the stored count: `coverage.json` then reports `numeratorSurplus` above 0 and `coverageCapped: true`, holds `exportCoverage` at 100 and keeps the raw ratio in `coverageUncapped`, and §5's render states the surplus beside both counts. A large surplus is the deflated-denominator signature: check `guards.deflation`, whose `basis` names the set it measured the stored count against, before accepting the 100.
|
|
47
47
|
|
|
@@ -78,4 +78,4 @@ A non-zero exit leaves the script's error, or nothing, in that file: delete it,
|
|
|
78
78
|
|
|
79
79
|
## Split-Body Sources
|
|
80
80
|
|
|
81
|
-
The Source is `{reference_file}:{reference_line}`, of two references the one that sorts last, unless the pinned source settles it for the other side. At State 1 and the Forge tier or above, with `workspaceDrift` not `overridden`, load `{signatureRulesFile}` and set the two forms its `issue` gives against the export's declaration at the `file` and `line` `surface.json` gives, by its rules, when that one declaration decides (no overload set, no second declaring file): when `{reference_file}`'s form agrees and the other side's does not, the Source is the other side, `SKILL.md:{skill_md_line}` or `{other_file}:{other_line}`. Its remediation names the file to edit and the file and line whose form it takes, citing the declaration's `file:line`; when both forms agree with it, it says so and names what differs; when neither does, or nothing decides, it says no source settled it and names `{reference_file}` to edit, taking the other side's form (`{other_file}`'s, for two references).
|
|
81
|
+
The Source is `{reference_file}:{reference_line}`, of two references the one that sorts last, unless the pinned source settles it for the other side. At State 1 and the Forge tier or above, with `workspaceDrift` not `overridden`, load `{signatureRulesFile}` and set the two forms its `issue` gives against the export's declaration at the `file` and `line` `surface.json` gives, by its rules, when that one declaration decides (no overload set, no second declaring file, no `otherDeclarations` entry of the export's name): when `{reference_file}`'s form agrees and the other side's does not, the Source is the other side, `SKILL.md:{skill_md_line}` or `{other_file}:{other_line}`. Its remediation names the file to edit and the file and line whose form it takes, citing the declaration's `file:line`; when both forms agree with it, it says so and names what differs; when neither does, or nothing decides, it says no source settled it and names `{reference_file}` to edit, taking the other side's form (`{other_file}`'s, for two references).
|
|
@@ -86,7 +86,7 @@ uv run {coverageInputsScript} floor --skill-dir "{resolved_skill_package}" --out
|
|
|
86
86
|
|
|
87
87
|
Exit 1 printed its error on stderr: HALT with it (`halt_reason: "helper-failed"`, phase `coverage-check:inventory`).
|
|
88
88
|
|
|
89
|
-
Delegate the reading of the skill under test to a subagent, given `{run_dir}` and the path to SKILL.md and to its `references/` directory when one exists: it reads SKILL.md, and every `references/*.md` file too when SKILL.md's `## Full` headings are absent or stubs. It is given `{run_dir}/inventory-floor.json` too, and lists each name there with an origin in a file it reads, choosing its kind from the name's `kindLabel`, the skill's own label, when it has one. An origin's `signature` is that place's entry, its non-blank
|
|
89
|
+
Delegate the reading of the skill under test to a subagent, given `{run_dir}` and the path to SKILL.md and to its `references/` directory when one exists: it reads SKILL.md, and every `references/*.md` file too when SKILL.md's `## Full` headings are absent or stubs. It is given `{run_dir}/inventory-floor.json` too, and lists each name there with an origin in a file it reads, choosing its kind from the name's `kindLabel`, the skill's own label, when it has one. An origin's `signature` is that place's entry, at that place, its non-blank fields kept as given; a blank one is the reader's. For each export two or more of the files it reads document, it compares the SKILL.md body with each reference file, and two reference files with each other only when the body does not settle them (it does not document the export, or agrees with both), by the rules of `{signatureRulesFile}`, whose absolute path (under `{skill-root}`) it is given: parameters, the return type, and descriptions that contradict (brief against detailed is fine). That file alone says what a form may leave out. It writes only this JSON, with no prose and no commentary, to `{run_dir}/inventory-response.txt` with the Write tool, never a shell, and returns that path, or returns the JSON itself when it cannot write files. The JSON's shape and the contract the validator checks are in `{inventoryContractFile}`, whose absolute path (under `{skill-root}`) the subagent is given.
|
|
90
90
|
|
|
91
91
|
When the subagent returned the JSON rather than the path, or the file is absent or empty afterwards, save its response, as it came back, to `{run_dir}/inventory-response.txt` with the Write tool. The documented inventory is the one §1a validates: do not load SKILL.md or the reference files into the parent context. Without subagents, the parent loads `{inventoryContractFile}` and builds the same JSON from the same files and `inventory-floor.json` and saves it to that file; §1a's checks still run on it.
|
|
92
92
|
|
|
@@ -98,7 +98,7 @@ Before anything consumes the inventory, `{validateInventoryScript}` checks it ag
|
|
|
98
98
|
uv run {validateInventoryScript} --input "{run_dir}/inventory-response.txt" --skill-package "{resolved_skill_package}" --output "{run_dir}/inventory.json" --floor "{run_dir}/inventory-floor.json"
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
- `valid` true (exit 0): every later command reads `{run_dir}/inventory.json`.
|
|
101
|
+
- `valid` true (exit 0): every later command reads `{run_dir}/inventory.json`. Each of its `warnings` that says "the form is compared as one whose citations name no declaration" names a form whose place (`at`) is missing or bad, dropped so §2 binds it to no declaration: list their count and names in §5's notes, the forms left unbound for want of a place.
|
|
102
102
|
- `valid` false (exit 2), or exit 1: load `{coverageBranchesFile}` and follow its **Inventory Failures** section. Invalid a second time: HALT (`halt_reason: "inventory-invalid"`, phase `coverage-check:inventory`): "coverage-check: the subagent inventory failed {schema validation | the ground-truth spot-check | the floor check} twice: {the violations joined}."
|
|
103
103
|
|
|
104
104
|
### 1b. Cross-Check Split-Body Consistency
|
|
@@ -150,10 +150,14 @@ For each `files[]` entry, delegate one subagent with its checks and the absolute
|
|
|
150
150
|
|
|
151
151
|
Tag a mismatch's `difference` as `{signatureRulesFile}` says: `score` (§2b) checks the facts. One tagged `annotation-contradicted` also gives `evidence_line`, the body line that returns the documented type.
|
|
152
152
|
|
|
153
|
+
`plan` gives a check `candidates` when the surface records its name in several files and the skill's citations do not settle one.
|
|
154
|
+
|
|
153
155
|
A check with `candidates` reads each and reports a mismatch only when the documented signature matches none: one, at the closest candidate, its `file` and `line` on the mismatch object (the result's `file` stays its `files[]` entry) and its `difference` tagged against it.
|
|
154
156
|
|
|
155
157
|
A check with `alternatives` (the other forms the skill documents for the name) is compared with each of them too, as `{signatureRulesFile}` says, each mismatch naming its form as `form`.
|
|
156
158
|
|
|
159
|
+
A check's `bound` lists each form (its `form` index) whose own citations name one declaration of the name: one of its surface records, or of its `surface.json` `otherDeclarations` (a same-named declaration no entry point exports, such as one a lazy re-export passes on). Its own citations are the `[AST:]` and `[SRC:]` citations `{validateInventoryScript}` reads at the inventory's `at`: on that line, on the line after it and around its fenced block, as its `form_citations` says. A citation names a declaration when it cites the declaration's file at a line or in an `L` range that holds its line or a later one before the next declaration the surface records in its file; one with no line names every declaration in its file. Compare a bound form with its bound declaration alone, read in full at its `file` and `line` as above, whatever the check's `candidates`, and report its mismatch there, `file` and `line` on the mismatch object.
|
|
160
|
+
|
|
157
161
|
Save each response to `{run_dir}/signatures-<n>.txt` with the Write tool. Without subagents, load `{signatureRulesFile}`, compare in the main thread by its rules and save the same JSON.
|
|
158
162
|
|
|
159
163
|
**Deep Tier (ast-grep + gh + QMD):** the Forge tier checks, and no coverage check of its own: init.md §5b's drift guard checks the local tree against the pinned commit, at any tier, when both exist; the recipe runner traces each re-export to the file that defines it when it runs (the fallback per-file scan does not); and the Forge tier's signature check compares each documented signature with its declaration. A documentation error you see that no script records (an import path whose module does not export the name, a type member that differs from its declaration) goes in coverage-check §5b's hand-written records as an Info `observation` titled `Not checked by script: {name}`, with `export` `{name}`, its Source at the documenting line and the remediation "Correct the documentation to match the source; no script checks this."
|
|
@@ -200,7 +204,7 @@ Do not write the Coverage Analysis section or attach a score: this is an indeter
|
|
|
200
204
|
uv run {scoreSignaturesScript} score --inventory "{run_dir}/inventory.json" --surface "{run_dir}/surface.json" --surface-set <set> --plan "{run_dir}/signature-plan.json" [--source-root "{source_path}"] --results "{run_dir}/signatures-<n>.txt" --output "{run_dir}/signatures.json" --gaps-output "{run_dir}/signature-gaps.json"
|
|
201
205
|
```
|
|
202
206
|
|
|
203
|
-
It grades each mismatch of a compared name from facts it checks, never a `difference` alone, into `signature-gaps.json`: a Low `signature-mismatch` gap for a `parameter-name` difference in a file the plan offered when it is TS/JS, Go, Rust, Java, C or C++, an Info `observation` titled `Partial signature: {name}` for an `omitted-optional` one on a `partial` form, an Info `observation` titled `Annotation contradicted: {name}` (no change to the skill: report the annotation upstream) for an `annotation-contradicted` one whose facts check (a documented return type, the same parameters on both sides, and an `evidence_line` between the declaration and the next one, whose line returns in the `--source-root` file), a Critical `signature-mismatch` gap otherwise (Python included). A signature gap is titled `Signature mismatch: {name}`, with `export` `{name}`, its Source at `{file}:{line}` and both signatures in its Issue (for a name documented in several forms, each gap's title ends `(form {n} of {m})` and its Issue names that form); §5b appends that file to the ledger. A Low or Info one counts as matching, and §5 renders it so. §5's render lists its `warnings` (a mismatch for a name never compared, a category with nothing to compare).
|
|
207
|
+
It grades each mismatch of a compared name from facts it checks, never a `difference` alone, into `signature-gaps.json`: a Low `signature-mismatch` gap for a `parameter-name` difference in a file the plan offered when it is TS/JS, Go, Rust, Java, C or C++, an Info `observation` titled `Partial signature: {name}` for an `omitted-optional` one on a `partial` form, an Info `observation` titled `Annotation contradicted: {name}` (no change to the skill: report the annotation upstream) for an `annotation-contradicted` one whose facts check (a documented return type, the same parameters on both sides, and an `evidence_line` between the declaration and the next one, whose line returns in the `--source-root` file), a Critical `signature-mismatch` gap otherwise (Python included). A signature gap is titled `Signature mismatch: {name}`, with `export` `{name}`, its Source at `{file}:{line}` and both signatures in its Issue (for a name documented in several forms, each gap's title ends `(form {n} of {m})` and its Issue names that form); §5b appends that file to the ledger. A Low or Info one counts as matching, and §5 renders it so. A bound form's mismatch is recorded at its bound declaration, a judge that names another file listed in `warnings`; a bound declaration joins no name, set or count. §5's render lists its `warnings` (a mismatch for a name never compared, a category with nothing to compare).
|
|
204
208
|
|
|
205
209
|
### 2c. Reconcile Documented vs Source Surface (Deterministic Intersection)
|
|
206
210
|
|
|
@@ -285,7 +289,7 @@ Exit 0: write its output unchanged where the section below places it. Exit 2 is
|
|
|
285
289
|
|
|
286
290
|
{the render output}
|
|
287
291
|
|
|
288
|
-
{the notes §
|
|
292
|
+
{the notes §1a to §4b add to this section}
|
|
289
293
|
```
|
|
290
294
|
|
|
291
295
|
### 5b. Record the Coverage Gaps
|
|
@@ -5,14 +5,14 @@ The JSON coverage-check.md §1's reader writes to `{run_dir}/inventory-response.
|
|
|
5
5
|
```json
|
|
6
6
|
{
|
|
7
7
|
"exports": [
|
|
8
|
-
{"name": "functionName", "kind": "function", "params": "...", "return_type": "...", "description": "..."},
|
|
8
|
+
{"name": "functionName", "kind": "function", "params": "...", "return_type": "...", "description": "...", "at": "SKILL.md:42"},
|
|
9
9
|
{"name": "ClassName", "kind": "class", "methods": ["..."], "properties": ["..."], "description": "..."},
|
|
10
10
|
{"name": "TypeName", "kind": "type", "fields": ["..."], "description": "..."},
|
|
11
|
-
{"name": "useHook", "kind": "hook", "usage_signature": "...", "description": "...", "signature_form": "partial"},
|
|
12
|
-
{"name": "ComponentName", "kind": "constant", "usage_signature": "<ComponentName prop={value} />", "description": "..."},
|
|
13
|
-
{"name": "Card", "kind": "function", "params": "{ title }", "description": "..."},
|
|
14
|
-
{"name": "Card", "kind": "function", "params": "{ title: string; footer?: ReactNode }", "description": "..."},
|
|
15
|
-
{"name": "Note", "kind": "class", "params": "each with the base fields id, scope=Scope(); content, title=None", "description": "..."}
|
|
11
|
+
{"name": "useHook", "kind": "hook", "usage_signature": "...", "description": "...", "signature_form": "partial", "at": "SKILL.md:57"},
|
|
12
|
+
{"name": "ComponentName", "kind": "constant", "usage_signature": "<ComponentName prop={value} />", "description": "...", "at": "SKILL.md:64"},
|
|
13
|
+
{"name": "Card", "kind": "function", "params": "{ title }", "description": "...", "at": "SKILL.md:71"},
|
|
14
|
+
{"name": "Card", "kind": "function", "params": "{ title: string; footer?: ReactNode }", "description": "...", "at": "references/api-reference.md:30"},
|
|
15
|
+
{"name": "Note", "kind": "class", "params": "each with the base fields id, scope=Scope(); content, title=None", "description": "...", "at": "references/api-reference.md:51"}
|
|
16
16
|
],
|
|
17
17
|
"references": ["references/api-reference.md"],
|
|
18
18
|
"cross_check_mismatches": [
|
|
@@ -22,6 +22,10 @@ The JSON coverage-check.md §1's reader writes to `{run_dir}/inventory-response.
|
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
The contract `validate-inventory.py` checks (coverage-check.md §1a): each `kind` is one of `function`, `class`, `type`, `constant`, `hook`, `interface`, `method`, `struct`, `enum`, `trait`, `macro` or `adapter`; every entry carries the `description` the skill gives it (an empty string when it gives none); a function or method carries `params` (`""` or `[]` when it takes none) and `return_type` as the skill documents them, and leaves a key out when the skill documents nothing for it. A name written with empty parentheses in running text or a description cell is a mention, not a form: its entry leaves `params` and `usage_signature` out (a cell reading "`instrument()` wraps functions"). A description cell is any table cell but an export table's first cell and a cell whose column writes the call or signature, by its role (Signature, Props, Params, Parameters, Arguments, Usage, Syntax, Example and the like); a heading or an export table's first cell that writes `name()` is a form, recorded as written. A `method`'s `name` is the bare method name and its `owner` the class or object it is called on (`agents` for `cognee.agents.get`, where the skill documents `agents` as a class; the class for one reached through a property, `client.messages`); `owner` is not validated. A function the skill documents in a module is a `function` named bare, never a `method`, even when it is called through the module's name (`register_sql_connection` for `cognee.tools.register_sql_connection`, where the skill documents `tools` as a module). Each `methods` item starts with the method's name, optionally after `async` (`async next_uuid(dep=None)`). A `const` holding a component (a `forwardRef`, `memo` or other wrapper call, or an alias like `const Tabs = TabsPrimitive.Root`) is a `constant` with the declaration or usage form the skill writes as `usage_signature`; a data constant carries no `usage_signature`, `params` or `return_type`. A name documented in several places is listed once per place, each entry with that place's form (`Card` above), and keeps one `kind` in every place. A place `inventory-floor.json` gives a `signature` (a cited function declaration, a Signature cell, or a heading or an export table's first cell that writes one call) has that form as its entry, whatever its kind, its non-blank
|
|
25
|
+
The contract `validate-inventory.py` checks (coverage-check.md §1a): each `kind` is one of `function`, `class`, `type`, `constant`, `hook`, `interface`, `method`, `struct`, `enum`, `trait`, `macro` or `adapter`; every entry carries the `description` the skill gives it (an empty string when it gives none); a function or method carries `params` (`""` or `[]` when it takes none) and `return_type` as the skill documents them, and leaves a key out when the skill documents nothing for it. A name written with empty parentheses in running text or a description cell is a mention, not a form: its entry leaves `params` and `usage_signature` out (a cell reading "`instrument()` wraps functions"). A description cell is any table cell but an export table's first cell and a cell whose column writes the call or signature, by its role (Signature, Props, Params, Parameters, Arguments, Usage, Syntax, Example and the like); a heading or an export table's first cell that writes `name()` is a form, recorded as written. A function's or a class's form the skill writes as a call expression (after `await`, `new` or an assignment, through a receiver or module path such as `coco.`, or as a statement in a fenced example), at a place `inventory-floor.json` gives no `signature` (below), is recorded whole in `usage_signature`, never in `params`, whatever names its arguments use, and a return type the skill writes for it in `return_type`, while a class's form written as a parameter list or a signature stays in `params` (`Note` above): signature-comparison-rules.md reads a usage as a call and a list of bare names in `params` as parameter names. A `method`'s `name` is the bare method name and its `owner` the class or object it is called on (`agents` for `cognee.agents.get`, where the skill documents `agents` as a class; the class for one reached through a property, `client.messages`); `owner` is not validated. A function the skill documents in a module is a `function` named bare, never a `method`, even when it is called through the module's name (`register_sql_connection` for `cognee.tools.register_sql_connection`, where the skill documents `tools` as a module). Each `methods` item starts with the method's name, optionally after `async` (`async next_uuid(dep=None)`). A `const` holding a component (a `forwardRef`, `memo` or other wrapper call, or an alias like `const Tabs = TabsPrimitive.Root`) is a `constant` with the declaration or usage form the skill writes as `usage_signature`; a data constant carries no `usage_signature`, `params` or `return_type`. A name documented in several places is listed once per place, each entry with that place's form (`Card` above), and keeps one `kind` in every place. An entry whose `params` or `usage_signature` the signature check compares (a list, a non-blank string, or a function's `params`, `""` included), except a `method`, gives that place as `at`, `"<path>:<line>"`: its file as the package names it (`SKILL.md`, or a file `references` lists) and the line its form starts on, the table row, the heading, the prose line or, in a fenced block, the line the declaration or call starts on (a `def` or `class` line, not a decorator above it), never a fence. That line writes the entry's `name`, or, in a fenced block, the block does. The signature check compares a form with the declaration its own `[AST:]` or `[SRC:]` citations name, read at that place, when they name exactly one, at its line or below it, before the next declaration the surface records in its file (otherwise as it compares any form). For a name declared more than once, a place that writes two of its forms that differ (a row that documents one declaration's form and quotes another's as a contrast) gives none of them a citation, so each is bound only by its other places; a form that writes only a `return_type` gives no `at`, so it is compared as one whose citations name no declaration. A missing or bad `at` (malformed, in another file, on a line that does not write the name, or not the place `inventory-floor.json` gives that form's `signature`) leaves its form unbound in the same way, and the validator lists it in its `warnings` instead of refusing the inventory. A place `inventory-floor.json` gives a `signature` (a cited function declaration, a Signature cell, a Props, Params or Parameters cell that is one code span, or a heading or an export table's first cell that writes one call; a usage form, such as `new Client(opts)`, in `usage_signature`) has that form as its entry, with that place as its `at`, whatever its kind, its non-blank fields kept as given (a `params` or `usage_signature` may open with the shared text the next sentence gives, then `; `); a blank one is the reader's, and a form the skill writes only in prose is an entry of its own; a `signature` read from a cell under a partial label (`Signature (simplified)`, `Props (simplified)`, `(partial)`, `(key parameters)`) also gives `signature_form` `"partial"`, which that entry copies, and the validator refuses an inventory with no entry there that does. Where the skill documents several names and states members they share once, in the same place (a table row, a paragraph) or over a table for all its rows (a sentence or caption just above it, or its column header), each name's entry carries that shared text in its `params` (in its `usage_signature`, for a usage form), never in a `methods` item, which still starts with the method's name: the shared text first, then `; `, then the name's own members as the skill writes them, without the name, never the name's own members alone (`Note` above, from a row that documents `Note` and `Task`, each with the base fields `id, scope=Scope()`, as `Note(content, title=None)` and `Task(title, done=False)`). When the skill places one of the name's own members before or among the shared ones (`leading`, `first`), the entry writes the name's whole list in the order the skill gives instead, the shared members spelled out and no `; `, unless `inventory-floor.json` gives that place a `signature`, whose `params` then stays after the shared text and `; `. An entry whose signature the skill labels partial (simplified, key parameters, a `...` standing in for omitted members, not rest or variadic syntax; a label in a table's column header, such as `Props (simplified)`, labels each of its rows) carries `signature_form: "partial"`, any other `"full"` or no key: copy the label, never judge the form. A table that documents one export's props, a row per prop with its type (Create Skill's `| Prop | Type | Default | Description |`, or one with a Required column), is a place of its own: its entry, whatever its kind, gives as `at` the nearest heading or caption above the table that writes the name (a row writes a prop, not the name) and as `params` one object type of the rows in row order, `{ prop: Type; ... }`, `prop` alone for an empty Type cell and each `\|` read as `|`, with no default, `?` or Required mark, so it states no optionality (signature-comparison-rules.md compares it as a typed props list); a partial label in its column header still labels it partial.
|
|
26
|
+
|
|
27
|
+
A name the skill writes only in a type position (a parameter type, a return type or a type argument inside another entry's documented type or signature, or a parameter table's Type cell) is a type mention, not an export the skill documents, even when its code span is exactly the name, and gets no entry, unless the skill also writes it as one outside a type position: a place `inventory-floor.json` gives it, a heading, an export table's first cell, a code span that is the name or the name then `(`, or a fenced declaration. A `const` whose value is a function (annotated with a function type, bound to an arrow, a `function` or a call that returns one, or written with a parameter list) carries the function form the skill writes for it as `params` (`[]`, never `""`, when it takes none) and `return_type`, whatever its kind (`constant` for the skill's `const` label), wherever the skill writes it, a description cell included; a bare `name()` there is still a mention; only a constant holding neither a component nor a function is a data constant. A function or class an example defines as the reader's own code is not a form of a same-named export: when its fenced block also defines names the skill does not document as exports, at a place `inventory-floor.json` gives no `signature`, its entry there leaves `params`, `return_type` and `usage_signature` out, as a mention's does, so it is never compared and records no gap; a declaration the floor gives a `signature` stays that place's form.
|
|
26
28
|
|
|
27
29
|
A `cross_check_mismatches` entry gives its `export`, `reference_file`, `reference_line` and an `issue` that names each form by its file's path, and the pair's other side: the SKILL.md body's line as `skill_md_line`, or, for two references, the one that sorts first in codepoint order, by path and then line, as `other_file` and `other_line`, with the one that sorts last as `reference_file` and `reference_line`. Each file is its path in the package, such as `references/api-reference.md`.
|
|
30
|
+
|
|
31
|
+
`validate-inventory.py` adds `crossChecked` to the inventory it writes, `{"count": <int>, "names": [<name>, ...]}`: each inventory name, once, that two or more of the files it reads write (SKILL.md and each file `references` lists), by the spot-check's name match outside `[AST:]`, `[SRC:]` and `[EXT:]` citations, so a mention in prose counts. The reader writes no `crossChecked`; the validator replaces one it finds.
|
|
@@ -10,9 +10,9 @@ A name written with empty parentheses in running text or a description cell is a
|
|
|
10
10
|
|
|
11
11
|
Three kinds of form do not claim a complete parameter list:
|
|
12
12
|
|
|
13
|
-
- A call or usage example passes values (literals, variables, `name=value`): `RecursiveSplitter()`, `embed(text, model=m)`, `<Canvas of={X} />`. It is compared as a call: each argument it passes must exist, by position or by name, and every required parameter must be passed; an optional parameter it leaves out is no difference. An empty call `X()` written as a call example (in a heading, an export table's first cell, a call or signature column or a fenced example, never a mention) is one too, so it matches a source whose parameters are all optional. A component usage `<X a={x} b="y" />` reads as a list of bare names of the props it sets, and a call `f(a, b)` as its arguments in order, each the parameter at its position (or, written `name=value`, the one it names). A JSX spread (`{...props}`) or children can pass a required prop, and a call's `...args` a required parameter, so neither leaves one out. An entry's `usage_signature` written as a usage is read this way, and each usage in a list of usages is compared as a form of its own, under its entry's `form` index.
|
|
13
|
+
- A call or usage example passes values (literals, variables, `name=value`): `RecursiveSplitter()`, `embed(text, model=m)`, `<Canvas of={X} />`. It is compared as a call: each argument it passes must exist, by position or by name, and every required parameter must be passed; an optional parameter it leaves out is no difference. An empty call `X()` written as a call example (in a heading, an export table's first cell, a call or signature column or a fenced example, never a mention) is one too, so it matches a source whose parameters are all optional. A component usage `<X a={x} b="y" />` reads as a list of bare names of the props it sets, and a call `f(a, b)` as its arguments in order, each the parameter at its position (or, written `name=value`, the one it names). A JSX spread (`{...props}`) or children can pass a required prop, and a call's `...args` a required parameter, so neither leaves one out. An entry's `usage_signature` written as a usage is read this way, and each usage in a list of usages is compared as a form of its own, under its entry's `form` index. A call written with placeholder names (`await coco.mount([subpath,] process_fn, *args, **kwargs)`), which the inventory contract records as a usage, is read the same way: its names are arguments at their positions, never parameter names, and a bracketed argument (`[subpath,]`) is optional, so the call is compared with and without it, each as a call.
|
|
14
14
|
- An ellipsis (`...` or `…`) in a list stands for members it does not show and claims nothing about them: `(...)` and `(tasks, data, dataset, ...)` leave out no parameter.
|
|
15
|
-
- A list of bare names, with no type, default or `?` (`connect(host, port)`, `({ themes, defaultTheme })`), names parameters without claiming the list is complete: an optional member it leaves out is no difference, a required one is a mismatch.
|
|
15
|
+
- A list of bare names, with no type, default or `?` (`connect(host, port)`, `({ themes, defaultTheme })`), names parameters without claiming the list is complete: an optional member it leaves out is no difference, a required one is a mismatch. An entry's `params` written as such a list is read this way, whatever the names; a call expression comes as a usage (inventory-contract.md) and is read as a call (above).
|
|
16
16
|
|
|
17
17
|
A form that writes a type or a default for a parameter (`(date: Date) => string`) claims its list is complete: an optional parameter it leaves out is an `omitted-optional` difference (Difference tags).
|
|
18
18
|
|
|
@@ -24,13 +24,15 @@ In a language without keyword arguments (TS/JS, Go, Rust, Java, C, C++), leaving
|
|
|
24
24
|
|
|
25
25
|
The names of Python's `*args` and `**kwargs` are never compared (`**kwargs` against `**options` is no difference), and the parameters after `*` or `*args` are compared by name, not order. A positional-only `/` or keyword-only `*` marker the form leaves out is not compared, and one it adds where the source has none is no difference: calls that follow the form are valid.
|
|
26
26
|
|
|
27
|
+
A call to a class is compared with its constructor: in Python, the parameters after `self` of the `__init__` the class declares, else those after `cls` of its `__new__`, else its nearest base's in method resolution order (`object`'s takes none); a dataclass, attrs class, pydantic model or NamedTuple that declares no `__init__` with the fields it generates, in declaration order, base fields first, a default marking a field optional, a field kept out of the constructor (`init=False`, a `ClassVar`) none of them, and a pydantic model's keyword-only; a TS/JS class with its `constructor(...)`, else its nearest base's; a class in another language with the constructor it declares. Any other form that documents a class is compared with that same constructor. A component is compared by its props (Components), not its constructor. A call to an instance is compared with its class's `__call__` (`fn = _FunctionDecorator()`, then `fn(memo=True)`), its overloads read as Overload sets says, and a name bound to a class with that class's constructor (`Symbol = core.Symbol`, whose stub declares `__new__(cls, name: str)`). A leading receiver the source writes (`self`, `cls`, `&self`, `this: T`) is never compared, and a form's own leading receiver is dropped only when the source leads with one too: `(self, a)` against `def f(a)` is a mismatch. An argument past the parameters a declaration takes by position exists when it takes `*args`, a TS/JS `...args` or Go's `...T`, and a keyword no parameter names exists when it takes `**kwargs`, so `Task(fn, max_chunk_size=512)` matches `__init__(self, executable, *args, **kwargs)`; a pass-through `__init__(self, *args, **kwargs)` is compared as written, so any call matches it. A class whose constructor is written only in a package outside the source tree (one that declares none over another package's base, such as `Exception`, or that a metaclass from there builds) has its parameters left uncompared and counts as matching, as such a declaration does (below); a class with generated fields is still compared with them, and one whose bases declare no constructor before `object` takes none.
|
|
28
|
+
|
|
27
29
|
The source's declaration decides: the documented form is compared with the parameters, the optionality and the annotations the signature declares, with one exception, a return annotation the body contradicts (Annotations).
|
|
28
30
|
|
|
29
31
|
A const annotated with a function type declares that type's parameters: `export const build: ViteBuilder['build'] = async ({ options }) => ...` takes the one parameter `ViteBuilder['build']` names, and the arrow's destructuring is that same parameter. A form may name it as the type does or destructure it as the arrow does, and a destructuring that names only some of the type's properties, in the arrow or in the form, does not read as a narrower type. A declaration whose parameter list is written only in a package outside the source tree (a binding destructured from a value cast to `typeof domTestingLibrary`, or `const spyOn: typeof vitestSpyOn = (...args) => ...`) has its parameters, their optionality and its return left uncompared and counts as matching, as a component whose props type lives there does (Components).
|
|
30
32
|
|
|
31
|
-
In the split-body cross-check both forms are documented: each is read by these rules, a type, return type or type arguments one of them leaves out are not compared, and a parameter's optionality is compared only when both forms state it. An ellipsis claims nothing about the members it stands for, required ones included. A call, a list of bare names or a form its own file labels partial (each side's label is read from that side's file, not from the export's one `signature_form`) may leave out a parameter the other form does not state as required, unless leaving it out shifts a caller's positions (a language without keyword arguments, as above); any other form claims its list is complete, so leaving out a parameter the other writes is a mismatch (inventory-contract.md's examples). Each export's form is read alone: what the rows of other exports write, in the same table or file, states nothing about it, so `(action, sink, tracking_record, child_invalidation)` matches `child_invalidation: X | None = None` even when every other row of its table writes its defaults.
|
|
33
|
+
In the split-body cross-check both forms are documented: each is read by these rules, a type, return type or type arguments one of them leaves out are not compared, and a parameter's optionality is compared only when both forms state it. An ellipsis claims nothing about the members it stands for, required ones included. A call, a list of bare names or a form its own file labels partial (each side's label is read from that side's file, not from the export's one `signature_form`) may leave out a parameter the other form does not state as required, unless leaving it out shifts a caller's positions (a language without keyword arguments, as above); any other form claims its list is complete, so leaving out a parameter the other writes is a mismatch (inventory-contract.md's examples). Each export's form is read alone: what the rows of other exports write, in the same table or file, states nothing about it, so `(action, sink, tracking_record, child_invalidation)` matches `child_invalidation: X | None = None` even when every other row of its table writes its defaults. Forms of one name attributed to different declarations of it are two exports, never compared with each other in the split-body cross-check: their own citations, read at each form's inventory `at` as inventory-contract.md says, name two declarations, or coverage-check.md §2's `bound` binds them to two, a declaration being one of the name's surface records or one `surface.json` lists in `otherDeclarations` (each judge reads the signals it has). A judge without the surface (coverage-check.md §1's reader) counts two declarations only when the two forms' own citations name two different files and the skill presents the forms as different things (another owner, module or import path); a citation of a re-export (a package `__init__` or index file the skill shows passing the name on) names no declaration, and when unsure the forms are compared as before. Each is compared only with the forms attributed to its own declaration, a form attributed to neither is a mismatch only when it contradicts the forms of both, and §2 holds a bound form to its bound declaration alone and an unbound one to the check's candidates (below).
|
|
32
34
|
|
|
33
|
-
A name the skill documents in several forms (a brief usage in SKILL.md, a full props list in a reference file) comes with `alternatives`: coverage-check.md §2's check lists them after its `documented` form, and the fallback per-file scan's `documentedAlternatives` holds them by name. Compare the declaration with each form the check offers, each by its own reading rules, and report a mismatch for each form that does not match, with its index as `form` (0 is `documented`, 1 the first alternative), its `documented_sig` written as that form and its `difference` tagged against it. With `candidates`, a form matches when it matches any candidate, and its mismatch is reported at the closest candidate.
|
|
35
|
+
A name the skill documents in several forms (a brief usage in SKILL.md, a full props list in a reference file) comes with `alternatives`: coverage-check.md §2's check lists them after its `documented` form, and the fallback per-file scan's `documentedAlternatives` holds them by name. Compare the declaration with each form the check offers, each by its own reading rules, and report a mismatch for each form that does not match, with its index as `form` (0 is `documented`, 1 the first alternative), its `documented_sig` written as that form and its `difference` tagged against it. With `candidates`, a form matches when it matches any candidate, and its mismatch is reported at the closest candidate. A form the check binds (`bound`: its own citations name one declaration of the name) is compared with its bound declaration alone, and its mismatch is reported there, at that declaration's file and line, whatever the check's location and candidates.
|
|
34
36
|
|
|
35
37
|
## Components
|
|
36
38
|
|
|
@@ -48,7 +50,7 @@ A props list states optionality as Reading a form says: `({ of, meta, story })`,
|
|
|
48
50
|
|
|
49
51
|
- A parameter written without a type is not compared on type: its name and order are, and its optionality as Reading a form says. Bare `*args` matches `*args: P.args`, and `**kwargs` matches `**kwargs: P.kwargs`.
|
|
50
52
|
- A return type the form does not state is not compared. A form states one when its entry has a non-blank `return_type` or the form writes one: after `->`, `=>`, `→` or `):`, or in its language's own position (Go's result list after the parameters, the type before the name in C, C++ and Java). A blank `return_type` (`""`) states none, so an entry with `params: ""` and no return type matches `def f() -> None`.
|
|
51
|
-
- A return written as names of values rather than types, a parenthesized list of names such as `→ (nodes, edges)` (after the arrow or in `return_type`), states no return type, so no return is compared. A type name written alone stays a type, lowercase ones included: `-> list` and `→ str` state one.
|
|
53
|
+
- A return written as names of values rather than types, a parenthesized list of names such as `→ (nodes, edges)` (after the arrow or in `return_type`), states no return type, so no return is compared. A type name written alone stays a type, lowercase ones included: `-> list` and `→ str` state one. A bracketed or braced list of value names, such as `→ [state, setState]` or `=> { data, error }`, likewise states none, while a single name (`→ result`), a list that writes a type among its names (`(nodes: list[Node], edges)`) and a description followed by a type (`HTML str`) each state one.
|
|
52
54
|
- A type the source leaves unannotated, on a parameter or a return, is not compared, except that a TS/JS parameter with a literal default has that literal's type: `pageSize = 20` is a `number`, so `pageSize?: string` is a mismatch. An unannotated return stays uncompared.
|
|
53
55
|
- A documented parameter type that names some of the members of the source's union is no mismatch, since a caller passing it makes a valid call (`schema: VectorSchemaProvider` against `VectorSchemaProvider | MultiVectorSchemaProvider`); a documented return type must cover every member the source can return.
|
|
54
56
|
- Type arguments the form leaves out are not compared: `dict` matches `dict[str, Any]`, and `list[dict]` matches `list[dict[str, Any]]`. Type arguments it writes are: `dict[str, int]` against `dict[str, str]` is a mismatch.
|
|
@@ -43,7 +43,7 @@ A map whose raw count equals `stats.effective_denominator` is already canonical:
|
|
|
43
43
|
|
|
44
44
|
### What the Surface Holds
|
|
45
45
|
|
|
46
|
-
The surface holds the names the entry points export, less those defined in a file the brief scopes out (`excluded.outsideScope`); the recipes' internal matches are never part of it. A Python `__init__.py` nested in the folder of another package that exports a name is no barrel: its names (`excluded.nestedEntries`) leave only the `all` and `root` sets and the umbrella ratio, and `plan` still compares their signatures. When a `scope.include` glob matches no file (`guards.staleScope.fires`), the root exports defined in a file no glob covers and the brief does not exclude are back in it (`guards.staleScope.restored`). A stale `scope.tier_a_include` glob fires the guard too (`guards.staleScope.unmatchedTierAInclude`) and counts no name.
|
|
46
|
+
The surface holds the names the entry points export, less those defined in a file the brief scopes out (`excluded.outsideScope`); the recipes' internal matches are never part of it. A Python `__init__.py` nested in the folder of another package that exports a name is no barrel: its names (`excluded.nestedEntries`) leave only the `all` and `root` sets and the umbrella ratio, and `plan` still compares their signatures. When a `scope.include` glob matches no file (`guards.staleScope.fires`), the root exports defined in a file no glob covers and the brief does not exclude are back in it (`guards.staleScope.restored`), each with the kind the provenance map gives its name at its file when the map is given (a dataclass, NamedTuple or TypedDict is a class, a Protocol an interface, a type alias a type, a TypeVar no type), and Type Coverage leaves out one still without a kind and says so in its warnings. A stale `scope.tier_a_include` glob fires the guard too (`guards.staleScope.unmatchedTierAInclude`) and counts no name. `otherDeclarations` lists, for each name the surface holds, the other declarations of it the extraction records in a file none of the name's records names and the brief does not exclude, such as cognee's lightweight `run_pipeline`, which a lazy `__getattr__` passes on beside the orchestrator the entry point exports. They are not part of the surface: they join no set or count, and `plan` compares a documented form with one only when the form's own citations name it and no other declaration of the name (coverage-check.md §2's `bound`).
|
|
47
47
|
|
|
48
48
|
## Source Access Resolution
|
|
49
49
|
|
|
@@ -106,9 +106,12 @@ cites (never read; default `metadata.json`):
|
|
|
106
106
|
`extraction` object, not `no-ast-grep`, `truncated` or
|
|
107
107
|
`fallback.needed`: only such an extraction fills
|
|
108
108
|
`excluded.outsideScope`, the names a brief scoped out) and its
|
|
109
|
-
`excluded.outsideScope` list does not list
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
`excluded.outsideScope` list does not list it (a row of a
|
|
110
|
+
namespace's member named like its namespace, whose `namespace`
|
|
111
|
+
is its `name`, #969, does not count unless its `file` is the
|
|
112
|
+
one the gap would be at): an Info `observation` gap
|
|
113
|
+
`Documented extra: {name}` at `defined_at`. With no
|
|
114
|
+
`defined_at`, a homonym (its `declared_in`
|
|
112
115
|
lists several declaring files) is one too, with --skill-dir,
|
|
113
116
|
when the skill's `[AST:path:L<n>]` and `[SRC:path:L<n>]`
|
|
114
117
|
citations on the lines of SKILL.md and references/**/*.md that
|
|
@@ -148,13 +151,16 @@ cites (never read; default `metadata.json`):
|
|
|
148
151
|
Floor's InputError (no SKILL.md, or a top-level file it cannot
|
|
149
152
|
read or decode) leaves tier 1 empty, and a skill file that
|
|
150
153
|
cannot be read leaves tiers 2 to 5 empty. One whose
|
|
151
|
-
`excluded.outsideScope` row says the brief
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
`excluded.outsideScope` row says the brief scopes its file out
|
|
155
|
+
(`excludeGlob`, the `scope.exclude` glob that matches it, or
|
|
156
|
+
`outsideInclude`, no `scope.include` glob covering it), unless
|
|
157
|
+
that row is a namespace's member named like its namespace (its
|
|
158
|
+
`namespace` is its `name`, #969) whose `file` is not the one a
|
|
159
|
+
documented extra's gap would be at, has its own issue, naming the
|
|
160
|
+
entry point and the file and line, and remediation: to keep the
|
|
161
|
+
documentation, narrow that exclude glob (a removal needs a
|
|
162
|
+
`demoted-exclude` amendment) or widen `scope.include`, else
|
|
163
|
+
remove it. A row the
|
|
158
164
|
brief does not explain keeps the wording above. Every stale
|
|
159
165
|
name's issue (fabricated, documented extra, stale documentation
|
|
160
166
|
or scoped out) opens with the line that documents it, as
|
|
@@ -1022,7 +1028,8 @@ def _scoped_out_by(row: dict) -> bool:
|
|
|
1022
1028
|
or row.get("outsideInclude") is True))
|
|
1023
1029
|
|
|
1024
1030
|
|
|
1025
|
-
def _outside_scope(surface: dict | None
|
|
1031
|
+
def _outside_scope(surface: dict | None, stale: dict[str, dict] | None = None,
|
|
1032
|
+
skill_dir: str | None = None) -> dict[str, dict] | None:
|
|
1026
1033
|
"""The names a surface result's `excluded.outsideScope` lists (defined in
|
|
1027
1034
|
a file out of scope), each with its row: the first the brief explains
|
|
1028
1035
|
(`_scoped_out_by`), else the first with a file, else its first. None
|
|
@@ -1030,7 +1037,13 @@ def _outside_scope(surface: dict | None) -> dict[str, dict] | None:
|
|
|
1030
1037
|
(no `extraction` object) or from one that did not read the whole scope
|
|
1031
1038
|
(a `no-ast-grep` status, a `truncated` run, or `fallback.needed`): only
|
|
1032
1039
|
an extraction fills that list, so the names a brief scoped out are then
|
|
1033
|
-
unknown.
|
|
1040
|
+
unknown. A namespace's member that has the namespace's own name (its
|
|
1041
|
+
`namespace` is its `name`, #969), such as a function a submodule
|
|
1042
|
+
imports from a file out of scope, is no row here: the documented name
|
|
1043
|
+
is the submodule, which no brief scoped out, while another row of the
|
|
1044
|
+
name still lists it. It stays when its `file` is the one the classified
|
|
1045
|
+
stale item of its name (`stale`) says the source declares it in, where
|
|
1046
|
+
`_documented_extra_at` would put the gap: that name is the member."""
|
|
1034
1047
|
excluded = (surface or {}).get("excluded")
|
|
1035
1048
|
if excluded is None:
|
|
1036
1049
|
return None
|
|
@@ -1054,12 +1067,27 @@ def _outside_scope(surface: dict | None) -> dict[str, dict] | None:
|
|
|
1054
1067
|
outside: dict[str, dict] = {}
|
|
1055
1068
|
for row in rows:
|
|
1056
1069
|
if isinstance(row, dict) and isinstance(row.get("name"), str):
|
|
1070
|
+
if (isinstance(row.get("namespace"), str) and row["namespace"] and row["namespace"] == row["name"]
|
|
1071
|
+
and not _declared_in_file(row, (stale or {}).get(row["name"]) or {}, skill_dir)):
|
|
1072
|
+
continue # the member, not the module the skill documents by that name
|
|
1057
1073
|
known = outside.get(row["name"])
|
|
1058
1074
|
if known is None or rank(row) > rank(known):
|
|
1059
1075
|
outside[row["name"]] = row
|
|
1060
1076
|
return outside
|
|
1061
1077
|
|
|
1062
1078
|
|
|
1079
|
+
def _declared_in_file(row: dict, item: dict, skill_dir: str | None) -> bool:
|
|
1080
|
+
"""Whether a stale item says the source declares its name in an
|
|
1081
|
+
`excluded.outsideScope` row's file: its `defined_at`, or its one cited
|
|
1082
|
+
`declared_in` item, the place `_documented_extra_at` reads whatever the
|
|
1083
|
+
brief scopes out (an empty `outside`)."""
|
|
1084
|
+
at = _documented_extra_at(row["name"], item, {}, skill_dir)
|
|
1085
|
+
if not at or not isinstance(row.get("file"), str) or not row["file"]:
|
|
1086
|
+
return False
|
|
1087
|
+
cited_path = _sibling("validate-inventory.py")._cited_path
|
|
1088
|
+
return cited_path(at[0].rsplit(":", 1)[0]) == cited_path(row["file"])
|
|
1089
|
+
|
|
1090
|
+
|
|
1063
1091
|
# The extensions classify-stale reads a `defined_at` in.
|
|
1064
1092
|
# Keep identical to PYTHON_EXTENSIONS | TSJS_EXTENSIONS in skf-verify-provenance-completeness.py.
|
|
1065
1093
|
_DEFINED_AT_EXTENSIONS = frozenset({".py", ".pyi", ".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"})
|
|
@@ -1079,7 +1107,9 @@ def _documented_extra_at(name: str, item: dict, outside: dict[str, dict] | None,
|
|
|
1079
1107
|
single cited declaring file (two or more cited, none, or no --skill-dir),
|
|
1080
1108
|
a `defined_at` or `declared_in` item not a Python or TS/JS `file:line`, a
|
|
1081
1109
|
dotted name, no surface or one without an extraction, or a name a brief
|
|
1082
|
-
scoped out
|
|
1110
|
+
scoped out (a namespace's member named like its namespace scopes out
|
|
1111
|
+
nothing: `_outside_scope` drops its row, #969, unless its file is the
|
|
1112
|
+
one this returns)."""
|
|
1083
1113
|
if outside is None or name in outside or "." in name:
|
|
1084
1114
|
return None
|
|
1085
1115
|
defined_at = item.get("defined_at")
|
|
@@ -1256,7 +1286,7 @@ def _stale_gaps(cov: dict, surface: dict | None, stale: object, scored: set[str]
|
|
|
1256
1286
|
export=name))
|
|
1257
1287
|
continue
|
|
1258
1288
|
if not outside_read:
|
|
1259
|
-
outside, outside_read = _outside_scope(surface), True
|
|
1289
|
+
outside, outside_read = _outside_scope(surface, classified, skill_dir), True
|
|
1260
1290
|
extra = _documented_extra_at(name, item, outside, skill_dir)
|
|
1261
1291
|
if extra:
|
|
1262
1292
|
declared, among = extra
|