bmad-module-skill-forge 8.0.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.
Files changed (39) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/_data/pinned.yaml +7 -7
  3. package/docs/verifying-a-skill.md +9 -9
  4. package/docs/workflows.md +1 -1
  5. package/package.json +1 -1
  6. package/src/shared/scripts/skf-extract-public-api.py +15 -8
  7. package/src/shared/scripts/skf-verify-provenance-completeness.py +331 -105
  8. package/src/skf-analyze-source/SKILL.md +1 -1
  9. package/src/skf-audit-skill/SKILL.md +1 -1
  10. package/src/skf-brief-skill/SKILL.md +1 -1
  11. package/src/skf-campaign/SKILL.md +1 -1
  12. package/src/skf-create-skill/SKILL.md +1 -1
  13. package/src/skf-create-stack-skill/SKILL.md +1 -1
  14. package/src/skf-drop-skill/SKILL.md +1 -1
  15. package/src/skf-export-skill/SKILL.md +1 -1
  16. package/src/skf-forger/SKILL.md +1 -1
  17. package/src/skf-quick-skill/SKILL.md +1 -1
  18. package/src/skf-refine-architecture/SKILL.md +1 -1
  19. package/src/skf-rename-skill/SKILL.md +1 -1
  20. package/src/skf-setup/SKILL.md +1 -1
  21. package/src/skf-test-skill/SKILL.md +1 -1
  22. package/src/skf-test-skill/assets/output-section-formats.md +2 -2
  23. package/src/skf-test-skill/customize.toml +10 -0
  24. package/src/skf-test-skill/references/coverage-check-branches.md +2 -2
  25. package/src/skf-test-skill/references/coverage-check.md +8 -4
  26. package/src/skf-test-skill/references/init.md +1 -1
  27. package/src/skf-test-skill/references/inventory-contract.md +11 -7
  28. package/src/skf-test-skill/references/report.md +2 -1
  29. package/src/skf-test-skill/references/scoring-rules.md +2 -2
  30. package/src/skf-test-skill/references/signature-comparison-rules.md +7 -5
  31. package/src/skf-test-skill/references/source-access-protocol.md +1 -1
  32. package/src/skf-test-skill/scripts/gap-ledger.py +105 -53
  33. package/src/skf-test-skill/scripts/load-coverage-inputs.py +336 -59
  34. package/src/skf-test-skill/scripts/reconcile-coverage.py +46 -18
  35. package/src/skf-test-skill/scripts/score-signatures.py +527 -130
  36. package/src/skf-test-skill/scripts/validate-inventory.py +528 -68
  37. package/src/skf-test-skill/templates/test-report-template.md +4 -3
  38. package/src/skf-update-skill/SKILL.md +1 -1
  39. package/src/skf-verify-stack/SKILL.md +1 -1
@@ -87,7 +87,7 @@ These rules apply to every step in this workflow:
87
87
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
88
88
  ```
89
89
 
90
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-analyze-source.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran) for step 5 to record in the run sink.
90
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-analyze-source.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran) for step 5 to record in the run sink.
91
91
 
92
92
  Then bind, so stage files use the variable with no conditional at the usage site:
93
93
 
@@ -79,7 +79,7 @@ Stage 1b returns to init.md at §5b's **Record for report**, then §6. Step 1 §
79
79
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
80
80
  ```
81
81
 
82
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-audit-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Then keep the reason as `{customization_resolver_unavailable}`, which step 6 hands to the emitter as a warning.
82
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-audit-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Then keep the reason as `{customization_resolver_unavailable}`, which step 6 hands to the emitter as a warning.
83
83
 
84
84
  Bind the two scalars the stages read, each to its bundled default when the merged value is empty or absent:
85
85
 
@@ -65,7 +65,7 @@ Give the halt's own `halt_reason`: the helper derives `exit_code` from it. `skil
65
65
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
66
66
  ```
67
67
 
68
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-brief-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case add `customization_resolver_unavailable: <reason>` to `workflow_warnings[]` as the run's first warning.
68
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-brief-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case add `customization_resolver_unavailable: <reason>` to `workflow_warnings[]` as the run's first warning.
69
69
 
70
70
  Bind the values the stage files use, taking the bundled default when the merged value is empty or absent:
71
71
 
@@ -37,7 +37,7 @@ Run these steps once, in order, before dispatching to Mode Routing.
37
37
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
38
38
  ```
39
39
 
40
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-campaign.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case, unless the invocation is `campaign status` (which writes nothing), log `customization_resolver_unavailable: <reason>` as an `event` in the decision log once `{campaignWorkspacePath}` is bound below (`references/campaign-contracts.md`, Decision Log).
40
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-campaign.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case, unless the invocation is `campaign status` (which writes nothing), log `customization_resolver_unavailable: <reason>` as an `event` in the decision log once `{campaignWorkspacePath}` is bound below (`references/campaign-contracts.md`, Decision Log).
41
41
 
42
42
  Resolve each scalar now (so step files never repeat the conditional) and stash as workflow-context variables:
43
43
 
@@ -82,7 +82,7 @@ These rules apply to every step in this workflow:
82
82
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
83
83
  ```
84
84
 
85
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-create-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran) for step 1 §0 to record in each brief's run folder.
85
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-create-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran) for step 1 §0 to record in each brief's run folder.
86
86
 
87
87
  Apply the resolved values so the surface is not a silent no-op: execute each entry in `workflow.activation_steps_prepend` in order now; treat every entry in `workflow.persistent_facts` as standing context for the whole run (entries prefixed `file:` are paths or globs whose contents load as facts), except a `!` entry and the entry it names (`!file:{project-root}/**/project-context.md` drops the bundled project-context default); and stash `{onCompleteCommand}` ← `workflow.on_complete` (empty string = no-op) for step 8 to invoke once each brief's result contract is written. After activation completes, execute each entry in `workflow.activation_steps_append` in order.
88
88
 
@@ -87,7 +87,7 @@ The sink, `{run_dir}/warnings.jsonl`, is the list: step 7 §8b lists it in `evid
87
87
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
88
88
  ```
89
89
 
90
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-create-stack-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): `references/init.md` records it as the run's first warning once its pre-flight has created `{run_dir}`.
90
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-create-stack-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): `references/init.md` records it as the run's first warning once its pre-flight has created `{run_dir}`.
91
91
 
92
92
  Apply the path-scalar fallback now so stage files don't have to repeat the conditional logic. For each of the two scalars, if the merged value is empty or absent, use the bundled default:
93
93
 
@@ -66,7 +66,7 @@ Headless callers and pipelines: the inputs, flags, gate map, exit codes and `SKF
66
66
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
67
67
  ```
68
68
 
69
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-drop-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): the run folder does not exist yet, so step 4 records it.
69
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-drop-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): the run folder does not exist yet, so step 4 records it.
70
70
 
71
71
  Bind each scalar to its bundled default when the merged value is empty or absent:
72
72
 
@@ -65,7 +65,7 @@ Headless callers and pipelines: the inputs, flags, gates, outputs and exit codes
65
65
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
66
66
  ```
67
67
 
68
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-export-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): a headless run records it once step 4 creates its run folder, and an interactive run only prints it.
68
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-export-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): a headless run records it once step 4 creates its run folder, and an interactive run only prints it.
69
69
 
70
70
  Bind each scalar to its bundled default when the merged value is empty or absent:
71
71
 
@@ -93,7 +93,7 @@ Run these steps once, in order, before the first reply.
93
93
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key agent
94
94
  ```
95
95
 
96
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-forger.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Ferris keeps no run log of his own, so that line opens the greeting.
96
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-forger.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Ferris keeps no run log of his own, so that line opens the greeting.
97
97
 
98
98
  The bundled arrays are empty and the roster values stay as shipped: the persona and the menu are this file's. An override may add to the arrays, and each one applies: run `agent.activation_steps_prepend` in order now; treat each `agent.persistent_facts` entry as standing context for the session (a `file:` entry loads its file or glob contents as facts; an entry prefixed `!` drops each earlier entry it names and loads nothing itself); then run `agent.activation_steps_append` once step 6 has greeted, before it dispatches a pick.
99
99
 
@@ -86,7 +86,7 @@ These rules apply to every step in this workflow:
86
86
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
87
87
  ```
88
88
 
89
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-quick-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran) and, when `{emitEnvelopeHelper}` has a path, record it in the run sink: write `customization_resolver_unavailable: {customization_resolver_unavailable}` to `{run_dir}/resolver-warning.txt` with a file write, never `echo` (the reason can hold quotes or `$( )`), then run `uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warning "$(cat "{run_dir}/resolver-warning.txt")"`. Under `--batch`, `references/batch-mode.md` §2 records that file in each target's run folder.
89
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-quick-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran) and, when `{emitEnvelopeHelper}` has a path, record it in the run sink: write `customization_resolver_unavailable: {customization_resolver_unavailable}` to `{run_dir}/resolver-warning.txt` with a file write, never `echo` (the reason can hold quotes or `$( )`), then run `uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warning "$(cat "{run_dir}/resolver-warning.txt")"`. Under `--batch`, `references/batch-mode.md` §2 records that file in each target's run folder.
90
90
 
91
91
  Bind these three as workflow-context variables, taking the default when the merged value is empty or absent:
92
92
 
@@ -77,7 +77,7 @@ When `{headless_mode}` is true, step 6 prints one `SKF_REFINE_ARCHITECTURE_RESUL
77
77
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
78
78
  ```
79
79
 
80
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-refine-architecture.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` for §5 to record.
80
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-refine-architecture.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` for §5 to record.
81
81
 
82
82
  Bind each path scalar, taking the bundled default when the merged value is empty or absent:
83
83
 
@@ -70,7 +70,7 @@ Headless callers: the inputs, flags, gate map, outputs, concurrency rule and `SK
70
70
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
71
71
  ```
72
72
 
73
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-rename-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran) for select.md §1 to record once it has created `{run_dir}`.
73
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-rename-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran) for select.md §1 to record once it has created `{run_dir}`.
74
74
 
75
75
  Apply the scalar fallback now so stage files don't have to repeat the conditional logic. For each of the two scalars, if the merged value is empty or absent, the bundled default applies:
76
76
 
@@ -72,7 +72,7 @@ You are a system executor: run each step in sequence, write the configuration fi
72
72
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
73
73
  ```
74
74
 
75
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-setup.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Bind `{customization_resolver_unavailable}` ← that reason (null when the resolver ran), which `references/report.md` and every later halt hand to the emitter; print the warning only when `{quiet_mode}` is false.
75
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-setup.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Bind `{customization_resolver_unavailable}` ← that reason (null when the resolver ran), which `references/report.md` and every later halt hand to the emitter; print the warning only when `{quiet_mode}` is false.
76
76
 
77
77
  Apply the resolved values, with no narration of your own when `{quiet_mode}` is true: run each `workflow.activation_steps_prepend` entry now; treat each `workflow.persistent_facts` entry as standing context (a `file:` entry loads its file or glob contents); bind `{onCompleteCommand}` ← `workflow.on_complete` (empty: no hook) for `references/report.md` §5; then run each `workflow.activation_steps_append` entry, before the first stage.
78
78
 
@@ -70,7 +70,7 @@ Headless callers: the full argument set (`Inputs`), the outputs, the exit codes
70
70
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
71
71
  ```
72
72
 
73
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-test-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}`: report.md §4c hands it to the result as a warning.
73
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-test-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `SKF helper skf-resolve-customization.py not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}`: report.md §4c hands it to the result as a warning.
74
74
 
75
75
  Apply the scalar fallbacks now so stage files don't have to repeat the conditional logic. For each scalar, if the merged value is empty or absent, use the bundled default:
76
76
 
@@ -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
 
@@ -48,6 +48,16 @@ persistent_facts = [
48
48
  #
49
49
  # Lift the canonical asset paths so orgs can substitute house-style copies
50
50
  # without forking the skill. Empty string = use the bundled default.
51
+ #
52
+ # The test report template init copies to the run's report. Default:
53
+ # templates/test-report-template.md. A house-style copy may reword the prose,
54
+ # and must keep:
55
+ # - the title line `# Test Report: {{skillName}}`, which init fills with
56
+ # the skill name;
57
+ # - the six `## ...` section headings, each with the `<!-- Populated by`
58
+ # comment under it, since each stage replaces its own placeholder.
59
+ # The report check halts the run (`report-anchor-missing`) on a report
60
+ # without them.
51
61
 
52
62
  test_report_template_path = ""
53
63
 
@@ -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 and the `all` set) lacks, 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.
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 `params` and `return_type` 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.
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 §2 to §4b add to this section}
292
+ {the notes §1a to §4b add to this section}
289
293
  ```
290
294
 
291
295
  ### 5b. Record the Coverage Gaps
@@ -320,7 +320,7 @@ nextWorkflow: ''
320
320
  ---
321
321
  ```
322
322
 
323
- Replace the template's frontmatter with this block (the two hold the same keys). `toolingStatus` is the `tooling_status` §3b set: score.md §3a reads it back for Cap 1. `threshold` and `thresholdSource` are the pass threshold §1b resolved and its source: score.md §1 reads them back, and score.md §7 rewrites `threshold` with the threshold the verdict was settled against. `analysisConfidence` stays empty until score.md §7, or step-hard-gate.md §3 on a run the hard gate blocks, writes the source access coverage-check resolved: `full`, `provenance-map`, `metadata-only`, `remote-only` or `docs-only`. report.md §4c halts on a report that holds none of them.
323
+ Replace the template's frontmatter with this block (the two hold the same keys). Then replace `{{skillName}}` in the template's H1 with `{skill_name}`, so the report's title is `# Test Report: {skill_name}`: report.md §4c halts on a report without that line. `toolingStatus` is the `tooling_status` §3b set: score.md §3a reads it back for Cap 1. `threshold` and `thresholdSource` are the pass threshold §1b resolved and its source: score.md §1 reads them back, and score.md §7 rewrites `threshold` with the threshold the verdict was settled against. `analysisConfidence` stays empty until score.md §7, or step-hard-gate.md §3 on a run the hard gate blocks, writes the source access coverage-check resolved: `full`, `provenance-map`, `metadata-only`, `remote-only` or `docs-only`. report.md §4c halts on a report that holds none of them.
324
324
 
325
325
  ### 7. Report Initialization Status
326
326
 
@@ -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 `params` and `return_type` kept as given (a `params` 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. 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)`). 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.
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.
@@ -186,9 +186,10 @@ for heading in '## Test Summary' '## Coverage Analysis' '## Coherence Analysis'
186
186
  printf '%s: ' "$heading"; grep -cxF "$heading" "{outputFile}"
187
187
  done
188
188
  printf 'placeholders: '; grep -c '^<!-- Populated by' "{outputFile}"
189
+ printf 'title: '; grep -cxF '# Test Report: {skill_name}' "{outputFile}"
189
190
  ```
190
191
 
191
- Each heading must print 1, standing alone on exactly one line, and `placeholders` must print 0. A run the hard gate blocked has all six: step 4c §3 wrote the Completeness Score section in scoring's place. On a heading whose count is not 1, or a placeholder left, HALT (`halt_reason: "report-anchor-missing"`, phase `report:anchors`) with "report anchor missing: {each heading whose count is not 1, with its count}, or {N} placeholder comments left; the section was not written once by its owning step".
192
+ Each heading must print 1, standing alone on exactly one line, `placeholders` must print 0, and `title` must print 1 (the H1 init.md §6c filled). A run the hard gate blocked has all six: step 4c §3 wrote the Completeness Score section in scoring's place. On a heading whose count is not 1, a placeholder left, or a `title` count other than 1, HALT (`halt_reason: "report-anchor-missing"`, phase `report:anchors`) with "report anchor missing: {each heading whose count is not 1, with its count}, {placeholder_count} placeholder comments left, or a `title` count of {title_count} (the report does not hold `# Test Report: {skill_name}` once); the section or title was not written once by its owning step".
192
193
 
193
194
  **Find the health check** unless `no_health_check` is true (the `--no-health-check` flag): resolve `{healthCheckFile}` ← first existing path in `{healthCheckProbeOrder}`. It ends the run (§7), so check it now, while a HALT still leaves the previous run's report and result files in place. If neither candidate exists, HALT (`halt_reason: "health-check-missing"`, phase `report:health-check`):
194
195
 
@@ -19,7 +19,7 @@ This table is the single source of truth for classifying a gap. Every stage reco
19
19
  | Severity | Category | Criteria |
20
20
  |----------|-----------------------------|----------|
21
21
  | Critical | `signature-mismatch` | Wrong signature: a documented signature that differs from the source in its parameters (name, type, order, optionality) or its return type, as signature-comparison-rules.md reads each form (a component by its props; an annotation either side leaves out is not compared), in any way but those the Low `signature-mismatch` row and the Info `observation` row grade, such as a renamed Python parameter (the script cannot see which parameters a caller must pass by position), an optional parameter left out of a form the skill does not label partial, or a mismatch with no `difference` (coverage-check §2 and §2b) |
22
- | Critical | `fabricated-signature` | Fabricated signature: a documented export absent at the source line the skill cites for it. Decided only where ast-grep enumerated the exports at the pinned commit: the name is outside that surface, and the cited file is missing or defines no such name, and no item of an export list (`export { L as NAME }`, `export type { ... }`) or of a Python `from X import` clause elsewhere renames to it a name that file declares; any other rename (`export { default as NAME }`, `const NAME = L`, `type NAME = L`, an import then an export, Python `NAME = L`) stays fabricated (coverage-check §2c) |
22
+ | Critical | `fabricated-signature` | Fabricated signature: a documented export absent at the source line the skill cites for it. Decided only where ast-grep enumerated the exports at the pinned commit: the name is outside that surface, and the cited file is missing or defines no such name, and no item of an export list (`export { L as NAME }`, `export type { ... }`) or of a Python `from X import` clause elsewhere renames to it a name that file declares, nor does any other rename the `aliased` clause of the Info `observation` row clears, directly or through a chain of renames; a rename that clause does not clear stays fabricated (coverage-check §2c) |
23
23
  | Critical | `broken-reference` | Broken reference (contextual mode): a reference whose target does not exist, such as a file not at the stated path, a skill not in the skills output folder, a type no module declares, or a `scripts/` or `assets/` file the skill names (coherence-check §4) |
24
24
  | High | `inaccurate-reference` | Inaccurate reference (contextual mode): the target exists but does not match, such as a type the cited module does not export, a signature that differs, or an integration pattern that does not match the implementation (coherence-check §4) |
25
25
  | High | `reference-escape` | A reference whose real path leaves the skill, its source and, for a stack, the skills output folder (coherence-check §4) |
@@ -45,7 +45,7 @@ This table is the single source of truth for classifying a gap. Every stage reco
45
45
  | Low | `metadata` | Missing optional metadata or examples |
46
46
  | Low | `description` | Description trigger optimization recommended (third-person voice, negative triggers, or keyword coverage gaps), a Tessl Review description suggestion included |
47
47
  | Low | `external-validator` | A skill-check diagnostic or a Tessl Review validation finding the run left unresolved (external-validators §5b) |
48
- | Info | `observation` | Style suggestions, non-blocking observations, and a documented extra: a documented name outside the enumerated surface that is not a fabricated signature and that the source still declares at column 0 (a module so named counts, unless the inventory gives the name a `function`, `hook`, `class`, `interface`, `enum` or `type` kind, no provenance map entry of the name types it a `module` or `package` and the module is not a TS/JS file that is itself the export, with an `export default` or a `module.exports =`; among several declaring files, the one the skill cites on a line naming it; an `aliased` name, which a line elsewhere renames from the declaration its cited file holds (an export-list item `L as NAME` (`export { default as NAME } from` included, a whole-string side unquoted, a comment allowed before `export`), a plain `export { NAME }` of an import that renames (`import { L as NAME }`, `import NAME from`), a `const`, `let`, `var` or `type NAME = L` with a bare identifier `L`, a Python `from X import L as NAME` at column 0 or one level in a module-level `try`, `except`, `else`, `if` or `elif` block, or a Python `NAME = L`), at that line, a multi-line list's item at its own line; when the rename's relative or in-package `from` module, or the import that binds `L`, resolves to a file that declares `L`, that file must be the cited one, so a rename of another file's same-named declaration stays a fabricated signature, as do a type assertion, any other expression and an import inside a function or a class), found only in a Python or TS/JS source where classify-stale ran (the Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden` and a bound provenance map) and `surface.json` has a complete `extraction`: `Documented extra: {name}`, at that line, which update-skill never routes (coverage-check §2c). Also a partial signature: a mismatch the signature judge tags `omitted-optional` (defined in signature-comparison-rules.md, Difference tags) on a form the skill labels partial (simplified, key parameters, a `...` that stands in for omitted members, not rest or variadic syntax), which the inventory marks `signature_form: "partial"`, as score-signatures.py checks: `Partial signature: {name}`, at the declaration, counted as matching in Signature Accuracy (coverage-check §2 and §2b). Also a return annotation the body contradicts: a mismatch the signature judge tags `annotation-contradicted` (defined in signature-comparison-rules.md, Difference tags) whose documented signature states a return type and writes the source's parameters, and whose `evidence_line`, the body line that returns the documented type, comes after the declaration and before the next one and, with the local source, starts with `return` (or is a one-line declaration's own expression body), as score-signatures.py checks: `Annotation contradicted: {name}`, at the declaration, counted as matching in Signature Accuracy, with no change to the skill (the annotation is reported upstream) (coverage-check §2 and §2b) Also a documentation error a Deep-tier run sees that no script checks (an import path whose module does not export the name, a type member that differs from its declaration): `Not checked by script: {name}`, at the documenting line, written by hand (coverage-check §2 and §5b) |
48
+ | Info | `observation` | Style suggestions, non-blocking observations, and a documented extra: a documented name outside the enumerated surface that is not a fabricated signature and that the source still declares at column 0 (a module so named counts, unless the inventory gives the name a `function`, `hook`, `class`, `interface`, `enum` or `type` kind, no provenance map entry of the name types it a `module` or `package` and the module is not a TS/JS file that is itself the export, with an `export default` or a `module.exports =`; among several declaring files, the one the skill cites on a line naming it; an `aliased` name, which a line elsewhere renames from the declaration its cited file holds, directly or through a chain of renames (an export-list item `L as NAME` (`export { default as NAME } from` included, a whole-string side unquoted, a comment allowed before `export`), a plain `export { NAME }` of an import that renames (`import { L as NAME }`, `import NAME from`), a `const`, `let`, `var` or `type NAME = L` with a bare identifier `L` (a `const`, `let` or `var` annotated with a one-line type that holds no `;` and no `=` but in `=>` included, and a generic `type NAME<T> = L<T>` that passes its bare parameters on in the same order), an `export default L` at column 0 (a rename of `L` to `default`) whose bare identifier `L` an import or a declaration of the same file binds, the statement ending on its line, a Python `from X import L as NAME` at column 0, one level in a module-level `try`, `except`, `else`, `if`, `elif`, `with` or `finally` block or as such a block header's one-line body (`try: from X import L as NAME`), or a Python `NAME = L` or `NAME: <type> = L` whose type holds no `;` or `=`), at that line, a multi-line list's item at its own line; when the rename's relative or in-package `from` module, or the import that binds `L`, resolves to a file that declares `L`, that file must be the cited one or one that renames `L` in turn, by one of these forms or by a Python `L = M.L2` whose module `M` a column-0 `from X import M` binds, a chain classify-stale follows file by file until it reaches the cited declaration, so a rename of another file's same-named declaration stays a fabricated signature, as do a chain that leaves the files classify-stale resolves, loops or ends at another declaration, a generic alias whose parameters are constrained, defaulted, swapped or instantiated, a type assertion, any other expression (`NAME = a.b` included) and an import inside a function or a class), found only in a Python or TS/JS source where classify-stale ran (the Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden` and a bound provenance map) and `surface.json` has a complete `extraction`: `Documented extra: {name}`, at that line, which update-skill never routes (coverage-check §2c). Also a partial signature: a mismatch the signature judge tags `omitted-optional` (defined in signature-comparison-rules.md, Difference tags) on a form the skill labels partial (simplified, key parameters, a `...` that stands in for omitted members, not rest or variadic syntax), which the inventory marks `signature_form: "partial"`, as score-signatures.py checks: `Partial signature: {name}`, at the declaration, counted as matching in Signature Accuracy (coverage-check §2 and §2b). Also a return annotation the body contradicts: a mismatch the signature judge tags `annotation-contradicted` (defined in signature-comparison-rules.md, Difference tags) whose documented signature states a return type and writes the source's parameters, and whose `evidence_line`, the body line that returns the documented type, comes after the declaration and before the next one and, with the local source, starts with `return` (or is a one-line declaration's own expression body), as score-signatures.py checks: `Annotation contradicted: {name}`, at the declaration, counted as matching in Signature Accuracy, with no change to the skill (the annotation is reported upstream) (coverage-check §2 and §2b) Also a documentation error a Deep-tier run sees that no script checks (an import path whose module does not export the name, a type member that differs from its declaration): `Not checked by script: {name}`, at the documenting line, written by hand (coverage-check §2 and §5b) |
49
49
  | Info | `provenance-unverified` | Provenance line not verified: the line-check rules found no definition line, or the recorded line is not a line number (`line-invalid`) (coverage-check §4c) |
50
50
  | Info | `migration-section` | Historical migration content in Section 4b with no T2-future annotation (Case 2 of `migration-section-rules.md`) |
51
51
  | Info | `multi-denominator` | Multi-denominator reporting: barrel and documented-surface clusters diverge by design (>10% cross-cluster) |
@@ -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