bmad-module-skill-forge 7.0.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +1 -1
- package/docs/_data/pinned.yaml +9 -10
- package/docs/campaign.md +2 -2
- package/docs/skill-model.md +3 -3
- package/docs/verifying-a-skill.md +17 -18
- package/docs/workflows.md +7 -7
- package/package.json +1 -1
- package/src/shared/references/description-guard-protocol.md +12 -10
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +38 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +5 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +8 -3
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +6 -1
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +7 -2
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +1 -1
- package/src/shared/scripts/skf-parse-gaps.py +4 -1
- package/src/shared/scripts/skf-quick-batch.py +8 -4
- package/src/shared/scripts/skf-verify-provenance-completeness.py +727 -179
- package/src/skf-audit-skill/customize.toml +4 -3
- package/src/skf-audit-skill/references/report.md +1 -1
- package/src/skf-campaign/assets/campaign-state-schema.json +58 -0
- package/src/skf-campaign/references/campaign-contracts.md +1 -1
- package/src/skf-campaign/references/step-06-batch.md +2 -2
- package/src/skf-campaign/references/step-07-capstone.md +1 -1
- package/src/skf-campaign/references/step-08-verify.md +1 -1
- package/src/skf-campaign/references/step-09-refine.md +1 -1
- package/src/skf-campaign/references/step-11-maintenance.md +1 -1
- package/src/skf-campaign/scripts/campaign-render-batch.py +43 -10
- package/src/skf-campaign/scripts/campaign-report.py +60 -4
- package/src/skf-campaign/scripts/campaign-state.py +67 -19
- package/src/skf-campaign/templates/campaign-report-template.md +4 -0
- package/src/skf-create-skill/assets/skill-sections.md +2 -2
- package/src/skf-create-skill/references/report.md +2 -2
- package/src/skf-create-skill/references/validate.md +49 -29
- package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
- package/src/skf-create-stack-skill/references/report.md +3 -3
- package/src/skf-create-stack-skill/references/validate.md +9 -4
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-export-skill/references/summary.md +1 -1
- package/src/skf-quick-skill/references/batch-mode.md +28 -2
- package/src/skf-quick-skill/references/compile.md +2 -0
- package/src/skf-quick-skill/references/finalize.md +3 -2
- package/src/skf-quick-skill/references/halt-contract.md +3 -3
- package/src/skf-quick-skill/references/quick-extract.md +2 -1
- package/src/skf-quick-skill/references/write-and-validate.md +11 -5
- package/src/skf-refine-architecture/customize.toml +1 -1
- package/src/skf-refine-architecture/references/report.md +2 -2
- package/src/skf-rename-skill/references/report.md +1 -1
- package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
- package/src/skf-test-skill/references/coverage-check.md +2 -2
- package/src/skf-test-skill/references/invocation-contract.md +1 -1
- package/src/skf-test-skill/references/report.md +1 -1
- package/src/skf-test-skill/references/scoring-rules.md +1 -1
- package/src/skf-test-skill/references/signature-comparison-rules.md +4 -1
- package/src/skf-test-skill/scripts/gap-ledger.py +196 -46
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +12 -6
- package/src/skf-update-skill/references/gap-driven.md +1 -1
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +1 -1
- package/src/skf-update-skill/references/report.md +18 -7
- package/src/skf-update-skill/references/write.md +24 -12
- package/src/skf-verify-stack/references/report.md +1 -1
|
@@ -4,13 +4,13 @@ Load this file only from coverage-check.md §2, and follow the one section §2 n
|
|
|
4
4
|
|
|
5
5
|
## Quick Tier
|
|
6
6
|
|
|
7
|
-
- **A skill quick-skill built** (`metadata.json` `generated_by` is `quick-skill`), or **any skill in Python, JavaScript or TypeScript**, **from local source** (State 1): parse the entry points with `{extractPublicApiHelper}` `--mode quick`. quick-skill built its export list with that parser, so both sides count one surface, and in those three languages it gives each name the file that defines it (`defined_in`), so the brief's globs place the name as the recipe runner does. Resolve it ← first existing path in `{extractPublicApiProbeOrder}` and run it once per package in scope, with its manifest and one `--entry-file` for each entry-point file the Source API Surface Definition makes its surface, each path relative to `{source_path}` (`<n>` counts the packages from 1):
|
|
7
|
+
- **A skill quick-skill built** (`metadata.json` `generated_by` is `quick-skill`), or **any skill in Python, JavaScript or TypeScript**, **from local source** (State 1): parse the entry points with `{extractPublicApiHelper}` `--mode quick`. quick-skill built its export list with that parser, so both sides count one surface (except names quick-skill took from a README for a star re-export from another package: the parser gives that star no name, so those names read as stale), and in those three languages it gives each name the file that defines it (`defined_in`), so the brief's globs place the name as the recipe runner does. Resolve it ← first existing path in `{extractPublicApiProbeOrder}` and run it once per package in scope, with its manifest and one `--entry-file` for each entry-point file the Source API Surface Definition makes its surface, each path relative to `{source_path}` (`<n>` counts the packages from 1):
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
uv run {extractPublicApiHelper} --mode quick --language <language> --source-root "{source_path}" --manifest-file <manifest path> --entry-file <entry path> > "{run_dir}/quick-<n>.json"
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Its `warnings` name each statement whose names the entry file alone cannot give. Read by eye only the statements its warnings name, save the names they give as a per-file result for the entry file that holds the statement, `{"file": "<entry path>", "exports_found": [<each name>], "signature_mismatches": []}`, and list those names in the Coverage Analysis section as read by eye. Give `surface` one `--quick` per package, one `--per-file` per saved result, the brief when the skill has one (its globs give the `scope.include` and `tier_a_include` sets), the metadata and, when init.md §2 bound one, the provenance map (the baselines of the §2b candidates and guards; they add no name):
|
|
13
|
+
Its `warnings` name each statement whose names the entry file alone cannot give. Read by eye only the statements its warnings name, save the names they give as a per-file result for the entry file that holds the statement, `{"file": "<entry path>", "exports_found": [<each name>], "signature_mismatches": []}`, and list those names in the Coverage Analysis section as read by eye. A warning on an `export *` or `export type *` from another package (a specifier that is no relative path; the warning writes either as `export *`) gives no name, as the recipe runner leaves such a star unresolved: list the statement as the source writes it, `<line>: <statement>`, in the `unresolved_stars` of that entry file's per-file result, and in the Coverage Analysis section as unresolved. Give `surface` one `--quick` per package, one `--per-file` per saved result that lists a name, the brief when the skill has one (its globs give the `scope.include` and `tier_a_include` sets), the metadata and, when init.md §2 bound one, the provenance map (the baselines of the §2b candidates and guards; they add no name):
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
uv run {coverageInputsScript} surface --quick "{run_dir}/quick-<n>.json" [--per-file "{run_dir}/per-file-<n>.json"] [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] [--monorepo] [--pattern-reference-intent] [--curated-subset] [--barrel <state>] --output "{run_dir}/surface.json"
|
|
@@ -26,7 +26,7 @@ The recipe runner cannot be used, so subagents read the files in scope one at a
|
|
|
26
26
|
uv run {scoreSignaturesScript} plan --inventory "{run_dir}/inventory.json" --output "{run_dir}/signature-plan.json"
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
For each source file that defines public API exports, delegate a subagent that gets that map with the file and the absolute path of `{signatureRulesFile}` (under `{skill-root}`). It gets the `documentedAlternatives` map too, whose forms it compares as that file says. It extracts the symbols the file exports, with their full signatures, with ast-grep (or reads the file when `extraction.fallback.reason` says no ast-grep can run; list those files in the Coverage Analysis section as read by eye), leaves out a name the file only passes on from another module of the source being tested (`export { x } from './x'`, `export * from`, an `__init__.py` import, a Rust `pub use`), which that module's own result counts, keeps a name it passes on from another package (`export { z } from 'zod'`), which no other result counts, compares each with the map's entry by the rules of that file (overload sets and `difference` tags included), gives each mismatch the `line` that defines the export, and returns only `{"file", "exports_found": [...], "types_found": [<the exported interfaces, type aliases, enums and classes>], "signature_mismatches": [...]}`, each mismatch in the shape §2's signature comparison returns. Save each response to `{run_dir}/per-file-<n>.txt` with the Write tool and build the surface from them, one `--per-file` each and `--fallback-scan` (each names the file that defines its exports, so a `scope.tier_a_include` list that counts none of them does not turn the guards off), with the same baselines as at Quick tier, and `--extraction` when the runner's JSON sent you here (`extraction.fallback.needed`): it adds no name, only its scope and the globs it lists as matching no file, so the guards still run:
|
|
29
|
+
For each source file that defines public API exports, delegate a subagent that gets that map with the file and the absolute path of `{signatureRulesFile}` (under `{skill-root}`). It gets the `documentedAlternatives` map too, whose forms it compares as that file says. It extracts the symbols the file exports, with their full signatures, with ast-grep (or reads the file when `extraction.fallback.reason` says no ast-grep can run; list those files in the Coverage Analysis section as read by eye), leaves out a name the file only passes on from another module of the source being tested (`export { x } from './x'`, `export * from`, an `__init__.py` import, a Rust `pub use`), which that module's own result counts, keeps a name it passes on from another package (`export { z } from 'zod'`), which no other result counts, gives no name for an `export * from` or `export type * from` another package (a specifier that is no relative path; `export * as ns` still gives `ns`), which the recipe runner leaves unresolved, listing each such statement as `<line>: <statement>` in `unresolved_stars`, compares each with the map's entry by the rules of that file (overload sets and `difference` tags included), gives each mismatch the `line` that defines the export, and returns only `{"file", "exports_found": [...], "types_found": [<the exported interfaces, type aliases, enums and classes>], "signature_mismatches": [...], "unresolved_stars": [...]}`, each mismatch in the shape §2's signature comparison returns. List each response's `unresolved_stars` in the Coverage Analysis section as unresolved. Save each response to `{run_dir}/per-file-<n>.txt` with the Write tool and build the surface from them, one `--per-file` each and `--fallback-scan` (each names the file that defines its exports, so a `scope.tier_a_include` list that counts none of them does not turn the guards off), with the same baselines as at Quick tier, and `--extraction` when the runner's JSON sent you here (`extraction.fallback.needed`): it adds no name, only its scope and the globs it lists as matching no file, so the guards still run:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
uv run {coverageInputsScript} surface --per-file "{run_dir}/per-file-<n>.txt" --fallback-scan [--extraction "{run_dir}/extract-full.json"] [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] [--monorepo] [--pattern-reference-intent] [--curated-subset] [--barrel <state>] --output "{run_dir}/surface.json"
|
|
@@ -107,7 +107,7 @@ Only when the validated inventory's `references` is non-empty. Each `cross_check
|
|
|
107
107
|
|
|
108
108
|
### 2. Analyze Source Code (Tier-Dependent)
|
|
109
109
|
|
|
110
|
-
Identify the public API surface from the package entry points (0b), as deep as the tier allows. Whichever branch runs, `{coverageInputsScript} surface` writes it to `{run_dir}/surface.json`, which §2b, §2c, §4 and step 5 read. Names read by eye reach it as per-file results, `{"file": "<path>", "exports_found": [<each name>], "signature_mismatches": []}` for each file, saved to `{run_dir}/per-file-<n>.json` with the Write tool and given to `surface` with one `--per-file` each.
|
|
110
|
+
Identify the public API surface from the package entry points (0b), as deep as the tier allows. Whichever branch runs, `{coverageInputsScript} surface` writes it to `{run_dir}/surface.json`, which §2b, §2c, §4 and step 5 read. Names read by eye reach it as per-file results, `{"file": "<path>", "exports_found": [<each name>], "signature_mismatches": []}` for each file, saved to `{run_dir}/per-file-<n>.json` with the Write tool and given to `surface` with one `--per-file` each. An `export * from` or `export type * from` another package adds no name: list `<line>: <statement>` in `unresolved_stars` and Coverage Analysis.
|
|
111
111
|
|
|
112
112
|
**Exits.** Every `{coverageInputsScript}` and `{scoreSignaturesScript}` command in §2 and §2b exits 0, or exits 1 with its error on stderr: HALT with it (`halt_reason: "helper-failed"`, phase `coverage-check:surface`). `surface` with a `--per-file`, and `score`, also exit 2 when a subagent response breaks the schema: re-dispatch that subagent once with its `violations[]` appended (or correct a result the main thread wrote), save the new response over the old one and run the command again; a second failure HALTs the same way.
|
|
113
113
|
|
|
@@ -298,7 +298,7 @@ uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check -
|
|
|
298
298
|
uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check --from guards --input "{run_dir}/surface.json" --metadata "{resolved_skill_package}/metadata.json"
|
|
299
299
|
uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check --from numerator --input "{run_dir}/numerator.json" --metadata "{resolved_skill_package}/metadata.json"
|
|
300
300
|
uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check --from metadata-coherence --input "{run_dir}/metadata-coherence.json" --metadata "{resolved_skill_package}/metadata.json"
|
|
301
|
-
uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check --from provenance-line --input "{run_dir}/provenance-verify.json"
|
|
301
|
+
uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coverage-check --from provenance-line --input "{run_dir}/provenance-verify.json" [--stale "{run_dir}/stale.json"]
|
|
302
302
|
```
|
|
303
303
|
|
|
304
304
|
**A split-body mismatch's Source** is a line inside the skill package: when §1b found one, load `{coverageBranchesFile}` and follow its **Split-Body Sources** section.
|
|
@@ -59,7 +59,7 @@ When the emitter itself is not installed (init.md §0), no line can be printed:
|
|
|
59
59
|
|
|
60
60
|
## Result Files
|
|
61
61
|
|
|
62
|
-
report.md §4c runs the emitter with `--result-dir "{forge_version}"`. It writes the per-run record `skf-test-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name, `-2`, `-3` appended when a run already took that second's name) and then its copy `skf-test-skill-result-latest.json`, the record of the last run whose emitter wrote this copy, at a stable path (a copy, not a symlink), each atomically. The record holds the envelope's fields, plus `skill`, `outputs[]` (the published report), `summary` (`score`, `threshold`, `result` (`PASS`, `PASS_WITH_DRIFT`, `FAIL` or `INCONCLUSIVE`), `testMode`, `activeCategories[]`, `inconclusiveReasons[]` when present, `threshold_fallback`, `original_threshold` and `evidence_report_path` when the threshold fallback fired, `gapCounts` with the gap ledger's gaps by severity, and `hardGate`), `runId` and `healthCheckDispatched`, and the emitter stamps `timestamp`, `run_id`, `headless_decisions` and `warnings` into it. A run the hard gate blocked writes it too, as a FAIL record, so its `skf-test-skill-result-latest.json` holds that FAIL, not an earlier run's verdict. A HALT writes no result file, and neither does a run cut off before report.md §4c: the copy then still holds an earlier run's verdict.
|
|
62
|
+
report.md §4c runs the emitter with `--result-dir "{forge_version}"`. It writes the per-run record `skf-test-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name, `-2`, `-3` appended when a run already took that second's name) and then its copy `skf-test-skill-result-latest.json`, the record of the last run whose emitter wrote this copy, at a stable path (a copy, not a symlink), each atomically. The record holds the envelope's fields, plus `skill`, `outputs[]` (the published report), `summary` (`score`, `threshold`, `result` (`PASS`, `PASS_WITH_DRIFT`, `FAIL` or `INCONCLUSIVE`), `testMode`, `activeCategories[]`, `inconclusiveReasons[]` when present, `threshold_fallback`, `original_threshold` and `evidence_report_path` when the threshold fallback fired, `gapCounts` with the gap ledger's gaps by severity, and `hardGate`), `runId` and `healthCheckDispatched`, and the emitter stamps `timestamp`, `run_id`, `headless_decisions` and `warnings` into it. A run the hard gate blocked writes it too, as a FAIL record, so its `skf-test-skill-result-latest.json` holds that FAIL, not an earlier run's verdict, unless the run's line holds a `result_file_write_failed` warning (the emitter stops at the first write that fails). A HALT writes no result file, and neither does a run cut off before report.md §4c: the copy then still holds an earlier run's verdict.
|
|
63
63
|
|
|
64
64
|
## Emitting a Halt
|
|
65
65
|
|
|
@@ -235,7 +235,7 @@ and bind `{report_file}` ← `{publishedReportFile}`. If the move fails, HALT (`
|
|
|
235
235
|
uv run {emitEnvelopeHelper} emit --workflow skf-test-skill --run-dir "{run_dir}" --result-dir "{forge_version}" --target {emit_target} < "{run_dir}/result-context.json"
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
-
It writes `{forge_version}/skf-test-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name) and then its `skf-test-skill-result-latest.json` copy (the record of the last run whose emitter wrote this copy, at a stable path: a copy, not a symlink; a caller reads its own run's `SKF_TEST_RESULT_JSON` line), each atomically, so a partial write is never observable; stamps the timestamp, `run_id`, `headless_decisions` and `warnings` into both; and prints the `SKF_TEST_RESULT_JSON:` line. Bind `{result_line}` ← that line and `{result_path}` ← its `result_path`. A run the hard gate blocked writes the record too, as a FAIL (`status: "error"`, `verdict: "FAIL"`, `halt_reason: "hard-gate-blocked"`, `exit_code` 2), so after a blocked run `skf-test-skill-result-latest.json` holds its FAIL, never an earlier run's verdict. A
|
|
238
|
+
It writes `{forge_version}/skf-test-skill-result-{YYYYMMDD-HHmmss}.json` (UTC; it picks the name) and then its `skf-test-skill-result-latest.json` copy (the record of the last run whose emitter wrote this copy, at a stable path: a copy, not a symlink; a caller reads its own run's `SKF_TEST_RESULT_JSON` line), each atomically, so a partial write is never observable; stamps the timestamp, `run_id`, `headless_decisions` and `warnings` into both; and prints the `SKF_TEST_RESULT_JSON:` line. Bind `{result_line}` ← that line and `{result_path}` ← its `result_path`. A run the hard gate blocked writes the record too, as a FAIL (`status: "error"`, `verdict: "FAIL"`, `halt_reason: "hard-gate-blocked"`, `exit_code` 2), so after a blocked run `skf-test-skill-result-latest.json` holds its FAIL, never an earlier run's verdict, unless the run's line holds a `result_file_write_failed` warning (the emitter stops at the first write that fails). A per-run record that could not be written leaves `result_path` null, and a `-latest` copy that could not be written leaves it set; either adds a `result_file_write_failed` warning to the line, and the run still finishes. If the emitter exits non-zero, run it once more; if it fails again, HALT (`halt_reason: "write-failed"`, phase `report:result-contract`) with its stderr: the run's result contract could not be written. `references/invocation-contract.md` lists the record's fields.
|
|
239
239
|
|
|
240
240
|
Once the result files are written, append `'report'` to `stepsCompleted` in the `{outputFile}` frontmatter (it stays out of the expected set above).
|
|
241
241
|
|
|
@@ -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 an export-list item or a Python `from X import`
|
|
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) |
|
|
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) |
|
|
@@ -26,6 +26,8 @@ The names of Python's `*args` and `**kwargs` are never compared (`**kwargs` agai
|
|
|
26
26
|
|
|
27
27
|
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
28
|
|
|
29
|
+
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
|
+
|
|
29
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.
|
|
30
32
|
|
|
31
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.
|
|
@@ -38,7 +40,7 @@ A component is an export the source declares as one: a function that takes one p
|
|
|
38
40
|
|
|
39
41
|
A component's own props are the members written in its package: its interface or object type, object literals intersected into it, and members computed from its own code (`VariantProps<typeof buttonVariants>`). Members of a type from another package (`React.ComponentProps<"button">`, `HTMLAttributes<...>`, `ComponentProps<typeof Link>`) are inherited, as are the `ref` a `forwardRef` result takes and the optional `children` of `PropsWithChildren<P>`. A prop the form writes must exist on `P`, own or inherited. A component whose props type lives in a package outside the source tree (`const Tabs = TabsPrimitive.Root`) has its props left uncompared: only that it is a component is checked.
|
|
40
42
|
|
|
41
|
-
A prop the form writes that `P` lacks, or a type or an optionality it writes that the source contradicts, is a mismatch. So is a required own prop it leaves out, and a required inherited one (a `Link`'s `href`) left out of a full form. An optional own prop a typed props list or a table leaves out is an `omitted-optional` difference; one a list of bare names or a usage leaves out, and an optional inherited prop, is no difference. `| null` and `| undefined` on an optional prop are no difference (`VariantProps` adds both). A props type name the form writes that the source does not declare (`Button(props: ButtonProps)` against a source with no `ButtonProps`) is not compared by name, only the props the form lists. Values in a usage example (`variant="subtle"`) are not compared.
|
|
43
|
+
A prop the form writes that `P` lacks, or a type or an optionality it writes that the source contradicts, is a mismatch. So is a required own prop it leaves out, and a required inherited one (a `Link`'s `href`) left out of a full form. An optional own prop a typed props list or a table leaves out is an `omitted-optional` difference; one a list of bare names or a usage leaves out, and an optional inherited prop, is no difference. A rest element in a component's destructured props form (`({ className, variant = "default", ...props }: NativeButtonProps)`) stands for every prop of `P` the form does not name, own or inherited, required ones included, so the form leaves none of them out, and the rest's own name is not compared as a prop. `| null` and `| undefined` on an optional prop are no difference (`VariantProps` adds both). A props type name the form writes that the source does not declare (`Button(props: ButtonProps)` against a source with no `ButtonProps`) is not compared by name, only the props the form lists. Values in a usage example (`variant="subtle"`) are not compared.
|
|
42
44
|
|
|
43
45
|
A props list states optionality as Reading a form says: `({ of, meta, story })`, or a list of names, states none, and in an object type literal (`{ of?: X; meta: M }`) that states it for any member, each member written with no mark claims to be required. A table states optionality by a Required column, a `?` on a prop's name or a Default cell that gives a value; its Type column states types, never optionality. A Default cell with a value marks only its own row optional and an empty one states nothing, so Create Skill's `| Prop | Type | Default | Description |` table never reads a prop with an empty Default cell as required. A destructuring default (`({ of, meta = {} })`) likewise marks only its own member, and the others state no optionality.
|
|
44
46
|
|
|
@@ -46,6 +48,7 @@ A props list states optionality as Reading a form says: `({ of, meta, story })`,
|
|
|
46
48
|
|
|
47
49
|
- 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`.
|
|
48
50
|
- 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.
|
|
49
52
|
- 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.
|
|
50
53
|
- 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.
|
|
51
54
|
- 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.
|
|
@@ -115,25 +115,46 @@ cites (never read; default `metadata.json`):
|
|
|
115
115
|
name it (outside the citations), or on the next non-blank line
|
|
116
116
|
after one, cite exactly one of those files: the gap
|
|
117
117
|
is at that file's `declared_in` item, and two cited or none
|
|
118
|
-
leave it Medium. The documented extra's
|
|
119
|
-
that documents it (with --skill-dir) and its remediation asks no
|
|
118
|
+
leave it Medium. The documented extra's remediation asks no
|
|
120
119
|
change (update-skill never routes it) and says whether signature
|
|
121
120
|
scoring compared its documented signature (a name the
|
|
122
121
|
--signatures result lists in `comparedNames`). `excluded` is
|
|
123
122
|
read once a stale name that is not fabricated needs it, and a
|
|
124
123
|
malformed one is then INVALID_INPUT. Any other is a Medium `stale-documentation` gap
|
|
125
|
-
`Stale documentation: {name}` at the
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
`Stale documentation: {name}` at the line that documents the
|
|
125
|
+
name (with --skill-dir), else `SKILL.md`. That line is the
|
|
126
|
+
first that writes it as an export, the first tier that holds
|
|
127
|
+
the name winning: (1) its first origin in the documented
|
|
128
|
+
inventory's floor (load-coverage-inputs.py floor); (2) a
|
|
129
|
+
column-0 declaration of it in a fenced block of a language
|
|
130
|
+
floor's `_declared_names` reads (JS/TS, Python, Rust, Go),
|
|
131
|
+
cited or not, outside an HTML comment and a data block (json, yaml...);
|
|
132
|
+
(3) a heading, or the first cell of a table whose header's
|
|
133
|
+
first cell names exports (floor's table headers), whose text is
|
|
134
|
+
the name or holds a code span floor reads as the name; (4) a
|
|
135
|
+
line whose inline code span is the name, or the name directly
|
|
136
|
+
followed by `(`. Tiers 2 to 4 read SKILL.md, then every .md file
|
|
137
|
+
under references/, subfolders included, in path order, outside
|
|
138
|
+
the frontmatter (3 and 4 outside fenced code and HTML comments
|
|
139
|
+
too); within a tier the first file, then its lowest line, wins.
|
|
140
|
+
A bare word, or a span that holds the name among other words
|
|
141
|
+
(`uitripled add`), documents nothing; with no such line, or
|
|
142
|
+
without --skill-dir, the Source is `SKILL.md`. Floor's
|
|
143
|
+
InputError (no SKILL.md, or a top-level file it cannot read or
|
|
144
|
+
decode) leaves tier 1 empty, and a skill file that cannot be
|
|
145
|
+
read leaves tiers 2 to 4 empty. One whose
|
|
146
|
+
`excluded.outsideScope` row says the brief
|
|
129
147
|
scopes its file out (`excludeGlob`, the `scope.exclude` glob
|
|
130
148
|
that matches it, or `outsideInclude`, no `scope.include` glob
|
|
131
149
|
covering it) has its own issue, naming the entry point and the
|
|
132
150
|
file and line, and remediation: to keep the documentation,
|
|
133
151
|
narrow that exclude glob (a removal needs a `demoted-exclude`
|
|
134
152
|
amendment) or widen `scope.include`, else remove it. A row the
|
|
135
|
-
brief does not explain keeps the wording above.
|
|
136
|
-
|
|
153
|
+
brief does not explain keeps the wording above. Every stale
|
|
154
|
+
name's issue (fabricated, documented extra, stale documentation
|
|
155
|
+
or scoped out) opens with the line that documents it, as
|
|
156
|
+
`file:line` (with --skill-dir), else with `the skill`, whatever
|
|
157
|
+
its Source. Scalar and stack branches: a
|
|
137
158
|
`missingCount` above 0 is one Medium `missing-export` gap
|
|
138
159
|
`{missingCount} of {denominator} exports not documented` at
|
|
139
160
|
`{meta}`, except that a scalar count taken from the verified
|
|
@@ -194,15 +215,26 @@ cites (never read; default `metadata.json`):
|
|
|
194
215
|
entries its counts differ by; a barrel one sends the fix to
|
|
195
216
|
Create Skill, which rebuilds `exports[]` from the source.
|
|
196
217
|
provenance-line
|
|
197
|
-
--input skf-verify-provenance-completeness.py verify's result
|
|
218
|
+
--input skf-verify-provenance-completeness.py verify's result;
|
|
219
|
+
--stale.
|
|
198
220
|
Each `stale[]` item, at its `{source_file}:{source_line}` with
|
|
199
221
|
`export`, by `reason`: `line-not-definition` is a Low
|
|
200
222
|
`provenance-line` gap `Provenance line is not the definition of
|
|
201
223
|
{export_name}` when `definition_lines` holds a line, else an Info
|
|
202
224
|
`provenance-unverified` gap `Provenance line not verified for
|
|
203
225
|
{export_name}`, and its issue names the base of a `folded_to`
|
|
204
|
-
variant
|
|
205
|
-
`
|
|
226
|
+
variant. When the --stale result (classify-stale) marks
|
|
227
|
+
`{export_name}` `aliased` with a `file:line` `defined_at`, that
|
|
228
|
+
Info gap's issue and remediation name the alias at `defined_at`,
|
|
229
|
+
and the remediation asks first to record the rename if no other
|
|
230
|
+
entry already has its `export_name` (`export_name` the item's
|
|
231
|
+
`renames`, else "a name a cited file declares", `reexported_as`
|
|
232
|
+
the export), else to cite the alias, moving the citations either
|
|
233
|
+
way; when `renames` is `default`, which the map keys on
|
|
234
|
+
`export_name` and so holds once, it asks only to cite the alias.
|
|
235
|
+
A --stale result that is not a list is INVALID_INPUT.
|
|
236
|
+
`file-missing` is a Low `provenance-line`
|
|
237
|
+
gap `Provenance file is missing for {export_name}`;
|
|
206
238
|
`line-out-of-bounds` a Low `provenance-line` gap `Provenance line
|
|
207
239
|
is out of range for {export_name}`; `line-invalid` an Info
|
|
208
240
|
`provenance-unverified` gap `Provenance line not verified for
|
|
@@ -747,7 +779,7 @@ ADAPTER_OPTIONS: dict[str, frozenset[str]] = {
|
|
|
747
779
|
"guards": frozenset({"metadata"}),
|
|
748
780
|
"numerator": frozenset({"metadata"}),
|
|
749
781
|
"metadata-coherence": frozenset({"metadata"}),
|
|
750
|
-
"provenance-line": frozenset(),
|
|
782
|
+
"provenance-line": frozenset({"stale"}),
|
|
751
783
|
"coherence": frozenset(),
|
|
752
784
|
"structure": frozenset({"served"}),
|
|
753
785
|
}
|
|
@@ -776,8 +808,10 @@ _SIBLINGS: dict[str, object] = {}
|
|
|
776
808
|
|
|
777
809
|
def _sibling(filename: str):
|
|
778
810
|
"""A script beside this one, loaded once: validate-inventory.py (the
|
|
779
|
-
|
|
780
|
-
|
|
811
|
+
skill-citation scan the homonym pick reads), load-coverage-inputs.py
|
|
812
|
+
(the documented inventory's floor, and its fence and table readers)
|
|
813
|
+
and locate-export-segments.py (the skill's files and line model), the
|
|
814
|
+
last two for the line that documents a stale name."""
|
|
781
815
|
module = _SIBLINGS.get(filename)
|
|
782
816
|
if module is None:
|
|
783
817
|
path = Path(__file__).resolve().parent / filename
|
|
@@ -872,21 +906,90 @@ def _provenance_sources(provenance: dict | None) -> dict[str, str]:
|
|
|
872
906
|
return sources
|
|
873
907
|
|
|
874
908
|
|
|
909
|
+
def _documenting_lines(skill_dir: Path) -> dict[str, str]:
|
|
910
|
+
"""name -> the `file:line` that documents it as an export, the first tier
|
|
911
|
+
that holds the name winning:
|
|
912
|
+
|
|
913
|
+
1. its first origin in the documented inventory's floor
|
|
914
|
+
(load-coverage-inputs.py `floor`: SKILL.md and the top-level
|
|
915
|
+
references/*.md);
|
|
916
|
+
2. a column-0 public declaration of it in a fenced code block (floor's
|
|
917
|
+
`_declared_names`), cited or not, outside an HTML comment and a data
|
|
918
|
+
block;
|
|
919
|
+
3. a heading, or the first cell of a table whose header's first cell is
|
|
920
|
+
one of floor's `FLOOR_TABLE_HEADERS`, whose stripped text is the name
|
|
921
|
+
or holds a code span floor's `_floor_name` reads as the name;
|
|
922
|
+
4. a line whose inline code span, stripped, is the name, or the name
|
|
923
|
+
directly followed by `(`.
|
|
924
|
+
|
|
925
|
+
Tiers 2 to 4 read SKILL.md, then every .md file under references/,
|
|
926
|
+
subfolders included, in path order (locate-export-segments.py
|
|
927
|
+
`skill_files`), never the frontmatter, tiers 3 and 4 outside fenced code
|
|
928
|
+
and HTML comments; within a tier the first file, then its lowest line,
|
|
929
|
+
wins. A bare word, or a span that holds the name among other words
|
|
930
|
+
(`uitripled add`), documents nothing. An input a tier cannot read is
|
|
931
|
+
skipped: floor's InputError (no SKILL.md, a file that is not UTF-8)
|
|
932
|
+
leaves tier 1 empty, an unreadable file tiers 2 to 4."""
|
|
933
|
+
coverage = _sibling("load-coverage-inputs.py")
|
|
934
|
+
segments = _sibling("locate-export-segments.py")
|
|
935
|
+
tiers: list[dict[str, str]] = [{}, {}, {}, {}]
|
|
936
|
+
try:
|
|
937
|
+
for entry in coverage.floor(str(skill_dir))["names"]:
|
|
938
|
+
origin = entry["origins"][0]
|
|
939
|
+
tiers[0].setdefault(entry["name"], f"{origin['file']}:{origin['line']}")
|
|
940
|
+
except coverage.InputError:
|
|
941
|
+
pass
|
|
942
|
+
try:
|
|
943
|
+
files = segments.skill_files(skill_dir)
|
|
944
|
+
except OSError:
|
|
945
|
+
files = []
|
|
946
|
+
# floor's fence filter: a fence in an HTML comment or a data block declares nothing, and a Svelte block's
|
|
947
|
+
# `export let` is a component prop. Unlike floor, an unheaded fence counts.
|
|
948
|
+
js_family = segments.JS_FAMILY - {"svelte"}
|
|
949
|
+
for rel, lines in files:
|
|
950
|
+
start = segments._frontmatter_end(lines)
|
|
951
|
+
blocks, code = segments._fenced_blocks(lines, start)
|
|
952
|
+
comments = coverage._html_comment_lines(lines, start, code)
|
|
953
|
+
for opening, closing, info in blocks:
|
|
954
|
+
language = segments._language(info)
|
|
955
|
+
if opening in comments or language in segments.DATA_LANGUAGES:
|
|
956
|
+
continue
|
|
957
|
+
masked = segments.mask_code("\n".join(lines[opening + 1:closing]), language).split("\n")
|
|
958
|
+
for offset, declared in coverage._declared_names(masked, language, js_family):
|
|
959
|
+
tiers[1].setdefault(declared, f"{rel}:{opening + offset + 2}")
|
|
960
|
+
headings, prose = segments._prose(lines, start, code)
|
|
961
|
+
shaped = [(index, text) for index, _level, text, _above in headings]
|
|
962
|
+
written = list(shaped)
|
|
963
|
+
for block in prose:
|
|
964
|
+
written += [(i, lines[i]) for i in range(block["start"], block["end"] + 1)]
|
|
965
|
+
if block["kind"] == "table-row" and block["header"] is not None:
|
|
966
|
+
if coverage._header_word(coverage._cells(block["header"])[0]) in coverage.FLOOR_TABLE_HEADERS:
|
|
967
|
+
shaped.append((block["start"], coverage._cells(lines[block["start"]])[0]))
|
|
968
|
+
for index, text in sorted(shaped):
|
|
969
|
+
for key in [text.strip()] + [coverage._floor_name(span) for span in segments._inline_code(text)]:
|
|
970
|
+
if key:
|
|
971
|
+
tiers[2].setdefault(key, f"{rel}:{index + 1}")
|
|
972
|
+
for index, text in sorted(written):
|
|
973
|
+
for span in map(str.strip, segments._inline_code(text)):
|
|
974
|
+
for key in (span, span.split("(", 1)[0] if "(" in span else None):
|
|
975
|
+
if key:
|
|
976
|
+
tiers[3].setdefault(key, f"{rel}:{index + 1}")
|
|
977
|
+
found: dict[str, str] = {}
|
|
978
|
+
for tier in tiers:
|
|
979
|
+
for key, line in tier.items():
|
|
980
|
+
found.setdefault(key, line)
|
|
981
|
+
return found
|
|
982
|
+
|
|
983
|
+
|
|
875
984
|
def _documenting_line(skill_dir: Path, name: str) -> str | None:
|
|
876
|
-
"""The
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
except OSError:
|
|
885
|
-
continue
|
|
886
|
-
for number, line in enumerate(text.split("\n"), start=1):
|
|
887
|
-
if pattern.search(line):
|
|
888
|
-
return f"{path.relative_to(skill_dir).as_posix()}:{number}"
|
|
889
|
-
return None
|
|
985
|
+
"""The `file:line` that documents `name` as an export in the skill at
|
|
986
|
+
`skill_dir` (`_documenting_lines`), else None."""
|
|
987
|
+
return _documenting_lines(skill_dir).get(name)
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def _documents(doc: str | None, name: str) -> str:
|
|
991
|
+
"""The subject of a stale name's Issue: the line that documents it, else the skill."""
|
|
992
|
+
return f"{f'`{doc}`' if doc else 'the skill'} documents `{name}`"
|
|
890
993
|
|
|
891
994
|
|
|
892
995
|
def _scoped_out_by(row: dict) -> bool:
|
|
@@ -1069,8 +1172,8 @@ def _scoped_out_gap(name: str, row: dict, doc: str | None, brief: str) -> dict:
|
|
|
1069
1172
|
f"Decide whether `{name}` belongs on the surface: `{brief}` scopes out `{at}`, where it is defined, since "
|
|
1070
1173
|
f"{' and '.join(causes)}. To keep its documentation, {' and '.join(fixes)}; else remove the documentation "
|
|
1071
1174
|
f"of `{name}` at {doc or 'SKILL.md'}.",
|
|
1072
|
-
issue=f"{
|
|
1073
|
-
|
|
1175
|
+
issue=f"{_documents(doc, name)}, which {who} exports and `{at}` defines, a file `{brief}` scopes out "
|
|
1176
|
+
"(`excluded.outsideScope`)",
|
|
1074
1177
|
export=name)
|
|
1075
1178
|
|
|
1076
1179
|
|
|
@@ -1113,9 +1216,13 @@ def _stale_gaps(cov: dict, surface: dict | None, stale: object, scored: set[str]
|
|
|
1113
1216
|
if not isinstance(stale, list):
|
|
1114
1217
|
raise AdapterError("--stale is not classify-stale JSON (a list)")
|
|
1115
1218
|
classified = {i["name"]: i for i in stale if isinstance(i, dict) and isinstance(i.get("name"), str)}
|
|
1116
|
-
|
|
1219
|
+
names = _names(cov, "stale", "--input")
|
|
1220
|
+
# the lines that document each name, read once for every stale name
|
|
1221
|
+
documenting = _documenting_lines(Path(skill_dir)) if skill_dir and names else {}
|
|
1222
|
+
for name in names:
|
|
1117
1223
|
item = classified.get(name) or {}
|
|
1118
1224
|
cited = item.get("source")
|
|
1225
|
+
doc = documenting.get(name)
|
|
1119
1226
|
if item.get("fabricated") is True:
|
|
1120
1227
|
if not isinstance(cited, str) or not cited:
|
|
1121
1228
|
# a Critical gap needs the line it cites: never downgrade it
|
|
@@ -1124,14 +1231,13 @@ def _stale_gaps(cov: dict, surface: dict | None, stale: object, scored: set[str]
|
|
|
1124
1231
|
"Critical", "fabricated-signature", f"Fabricated signature: {name}", cited,
|
|
1125
1232
|
f"Remove `{name}` from SKILL.md and references/, or correct it to the export the source "
|
|
1126
1233
|
f"defines: no file its provenance cites defines it (`{cited}`).",
|
|
1127
|
-
issue=f"
|
|
1128
|
-
"
|
|
1234
|
+
issue=f"{_documents(doc, name)}, which the source API surface lacks and the cited file does "
|
|
1235
|
+
"not define",
|
|
1129
1236
|
export=name))
|
|
1130
1237
|
continue
|
|
1131
1238
|
if not outside_read:
|
|
1132
1239
|
outside, outside_read = _outside_scope(surface), True
|
|
1133
1240
|
extra = _documented_extra_at(name, item, outside, skill_dir)
|
|
1134
|
-
doc = _documenting_line(Path(skill_dir), name) if skill_dir else None
|
|
1135
1241
|
if extra:
|
|
1136
1242
|
declared, among = extra
|
|
1137
1243
|
# a homonym: the one declaring file the skill's own citations name
|
|
@@ -1146,8 +1252,8 @@ def _stale_gaps(cov: dict, surface: dict | None, stale: object, scored: set[str]
|
|
|
1146
1252
|
"Info", "observation", f"Documented extra: {name}", declared,
|
|
1147
1253
|
f"No change: `{name}` is a documented extra, a name the source still declares at "
|
|
1148
1254
|
f"`{declared}` outside the enumerated surface, and its documentation stays. {checked}",
|
|
1149
|
-
issue=f"{
|
|
1150
|
-
f"
|
|
1255
|
+
issue=f"{_documents(doc, name)}, which the enumerated source API surface lacks and the source "
|
|
1256
|
+
f"still declares{picked}",
|
|
1151
1257
|
export=name))
|
|
1152
1258
|
continue
|
|
1153
1259
|
if outside is not None and name in outside and _scoped_out_by(outside[name]):
|
|
@@ -1158,7 +1264,7 @@ def _stale_gaps(cov: dict, surface: dict | None, stale: object, scored: set[str]
|
|
|
1158
1264
|
"Medium", "stale-documentation", f"Stale documentation: {name}", doc or "SKILL.md",
|
|
1159
1265
|
f"Check `{name}` against the source and update or remove its documentation at "
|
|
1160
1266
|
f"{doc or 'SKILL.md'}: the enumerated source surface does not export it.",
|
|
1161
|
-
issue=f"
|
|
1267
|
+
issue=f"{_documents(doc, name)}, which the enumerated source API surface lacks",
|
|
1162
1268
|
export=name))
|
|
1163
1269
|
return records
|
|
1164
1270
|
|
|
@@ -1448,10 +1554,48 @@ def from_metadata_coherence(result: object, metadata: str | None = None) -> list
|
|
|
1448
1554
|
return records
|
|
1449
1555
|
|
|
1450
1556
|
|
|
1451
|
-
def
|
|
1557
|
+
def _aliased_items(stale: object) -> dict[str, dict]:
|
|
1558
|
+
"""name -> its classify-stale item (`--stale`) when it is `aliased` with a `file:line` `defined_at`: a rename
|
|
1559
|
+
elsewhere declares the name, so a provenance line gap of it names the alias. Raises AdapterError, as the
|
|
1560
|
+
coverage adapter does, when the result is not a list."""
|
|
1561
|
+
if stale is None:
|
|
1562
|
+
return {}
|
|
1563
|
+
if not isinstance(stale, list):
|
|
1564
|
+
raise AdapterError("--stale is not classify-stale JSON (a list)")
|
|
1565
|
+
return {i["name"]: i for i in stale
|
|
1566
|
+
if isinstance(i, dict) and isinstance(i.get("name"), str) and i.get("reason") == "aliased"
|
|
1567
|
+
and isinstance(i.get("defined_at"), str) and i["defined_at"].rpartition(":")[0]
|
|
1568
|
+
and i["defined_at"].rpartition(":")[2].isdigit()}
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
def _alias_fix(name: str, file: str, item: dict) -> str:
|
|
1572
|
+
"""The remediation of an unverified provenance line of a name classify-stale found `aliased` (`item`): record
|
|
1573
|
+
the rename if no other entry already has its `export_name`, else cite the alias. The map keys on
|
|
1574
|
+
`export_name`, so it holds one `default` entry at most (a barrel of `export { default as X }` lines renames
|
|
1575
|
+
several): a rename of `default` takes the alias alone. It never says which cited file declares the renamed
|
|
1576
|
+
name: any checked entry's may."""
|
|
1577
|
+
alias, local = item["defined_at"], item.get("renames")
|
|
1578
|
+
alias_file, _, alias_line = alias.rpartition(":")
|
|
1579
|
+
named = isinstance(local, str) and bool(local)
|
|
1580
|
+
renamed = f"`{local}`" if named else "a name a cited file declares"
|
|
1581
|
+
head = f"`{name}` is an alias: `{alias}` renames {renamed} to it, and no line of `{file}` defines `{name}`. "
|
|
1582
|
+
cite = (f"the alias: set `source_file` to `{alias_file}` and `source_line` to `{alias_line}`, and move the "
|
|
1583
|
+
f"citations of `{name}` there.")
|
|
1584
|
+
if local == "default":
|
|
1585
|
+
return f"{head}Cite {cite}"
|
|
1586
|
+
guard = f"`export_name` {renamed}" if named else "that name as its `export_name`"
|
|
1587
|
+
return (f"{head}If no other entry already has {guard}, record the rename in this provenance entry: set "
|
|
1588
|
+
f"`export_name` to {renamed}, add `reexported_as` `{name}`, cite the line that declares {renamed} and "
|
|
1589
|
+
f"move the citations of `{name}` to that line. Or cite {cite}")
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
def from_provenance_line(result: object, stale: object = None) -> list[dict]:
|
|
1452
1593
|
"""verify's `stale[]` items: provenance line gaps, one per citation that does not resolve or is not the
|
|
1453
|
-
definition line.
|
|
1594
|
+
definition line. `stale` (classify-stale's result, `--stale`, or None) gives the names a rename declares: an
|
|
1595
|
+
unverified line of one names the alias and, unless the name it renames is `default`, how to record the rename
|
|
1596
|
+
(#911)."""
|
|
1454
1597
|
result = _object(result, "--input (skf-verify-provenance-completeness.py verify)", "stale")
|
|
1598
|
+
aliased = _aliased_items(stale)
|
|
1455
1599
|
records: list[dict] = []
|
|
1456
1600
|
for item in _list(result, "stale", "--input"):
|
|
1457
1601
|
if not isinstance(item, dict):
|
|
@@ -1506,12 +1650,16 @@ def from_provenance_line(result: object) -> list[dict]:
|
|
|
1506
1650
|
f"it; the lines that define it are {lines}.{folded}",
|
|
1507
1651
|
export=name))
|
|
1508
1652
|
else:
|
|
1509
|
-
|
|
1510
|
-
|
|
1653
|
+
alias = aliased[name] if isinstance(name, str) and name in aliased else None
|
|
1654
|
+
fix = _alias_fix(name, file, alias) if alias else (
|
|
1511
1655
|
f"Open `{file}` and confirm that line {line} defines `{base or name}`; if another line does, set "
|
|
1512
|
-
"the provenance `source_line` to it."
|
|
1513
|
-
|
|
1514
|
-
|
|
1656
|
+
"the provenance `source_line` to it.")
|
|
1657
|
+
issue = (f"no line of `{file}` defines `{name}`: the source declares it only as the alias at "
|
|
1658
|
+
f"`{alias['defined_at']}`" if alias else
|
|
1659
|
+
f"the line-check rules found no definition line for `{name}` in `{file}`; check by hand")
|
|
1660
|
+
records.append(_gap(
|
|
1661
|
+
"Info", "provenance-unverified", f"Provenance line not verified for {name}", where, fix,
|
|
1662
|
+
issue=issue + (f".{folded}" if folded else ""),
|
|
1515
1663
|
export=name))
|
|
1516
1664
|
return records
|
|
1517
1665
|
|
|
@@ -1680,7 +1828,8 @@ def build_records(args: argparse.Namespace) -> list[dict]:
|
|
|
1680
1828
|
if kind == "metadata-coherence":
|
|
1681
1829
|
return from_metadata_coherence(data, args.metadata)
|
|
1682
1830
|
if kind == "provenance-line":
|
|
1683
|
-
|
|
1831
|
+
stale = read_result(args.stale, "--stale") if args.stale is not None else None
|
|
1832
|
+
return from_provenance_line(data, stale)
|
|
1684
1833
|
return from_coherence(data)
|
|
1685
1834
|
|
|
1686
1835
|
|
|
@@ -1897,7 +2046,8 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1897
2046
|
"the names outside scope)")
|
|
1898
2047
|
p.add_argument("--signatures", help="coverage: score-signatures.py score's result (missingTypes)")
|
|
1899
2048
|
p.add_argument("--numerator", help="coverage: verify-declared-numerator.py's result (absent names)")
|
|
1900
|
-
p.add_argument("--stale", help="coverage: classify-stale's result (fabricated signatures, documented extras)"
|
|
2049
|
+
p.add_argument("--stale", help="coverage: classify-stale's result (fabricated signatures, documented extras); "
|
|
2050
|
+
"provenance-line: the same (the names a rename declares)")
|
|
1901
2051
|
p.add_argument("--provenance", help="coverage: the provenance map (an absent name's Source)")
|
|
1902
2052
|
p.add_argument("--skill-dir", help="coverage: the skill package (a stale name's documenting line)")
|
|
1903
2053
|
p.add_argument("--metadata", help="the metadata.json path a record cites as its Source (not read)")
|
|
@@ -25,9 +25,11 @@ reading files and writing one JSON object:
|
|
|
25
25
|
signature is read only where the place writes it in a fixed
|
|
26
26
|
form (Signatures, below). A name is a plain
|
|
27
27
|
identifier (`[A-Za-z_$][\\w$]*`) once a leading `new ` and
|
|
28
|
-
everything from the first `(
|
|
29
|
-
`fetch(url): Promise<T>` and
|
|
30
|
-
`
|
|
28
|
+
everything from the first `(`, `<` or `[` are cut, so
|
|
29
|
+
`Meta<T>`, `App[P, R]`, `fetch(url): Promise<T>` and
|
|
30
|
+
`new Client(opts)` give `Meta`, `App`, `fetch` and `Client`;
|
|
31
|
+
an empty `[]` at the cut (a TS array type, `Foo[]`) gives
|
|
32
|
+
none. A dotted or `::` name gives none, so a
|
|
31
33
|
namespace-style skill such as zod (`z.object`) gets no table
|
|
32
34
|
floor. The forms, read outside the frontmatter and HTML
|
|
33
35
|
comments (a table row or heading inside fenced code gives none):
|
|
@@ -614,12 +616,16 @@ def census(skill_dir: str) -> dict:
|
|
|
614
616
|
|
|
615
617
|
|
|
616
618
|
def _floor_name(text: str) -> str | None:
|
|
617
|
-
"""The plain identifier `text` writes once a leading `new ` and everything from the first `(
|
|
618
|
-
cut; None for any other text, a dotted or `::` name
|
|
619
|
+
"""The plain identifier `text` writes once a leading `new ` and everything from the first `(`, `<` or `[` are
|
|
620
|
+
cut; None for any other text, a dotted or `::` name and an array type (`Foo[]`, an empty `[]` at the cut)
|
|
621
|
+
among them."""
|
|
619
622
|
text = text.strip()
|
|
620
623
|
if text.startswith("new "):
|
|
621
624
|
text = text[4:]
|
|
622
|
-
|
|
625
|
+
cut = re.match(r"([^(<\[]*)(\[\s*\])?", text)
|
|
626
|
+
if cut.group(2):
|
|
627
|
+
return None
|
|
628
|
+
text = cut.group(1).strip()
|
|
623
629
|
return text if PLAIN_IDENTIFIER_RE.fullmatch(text) else None
|
|
624
630
|
|
|
625
631
|
|
|
@@ -99,7 +99,7 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
|
|
|
99
99
|
|
|
100
100
|
- `stale-documentation` (the skill documents a name the source's enumerated surface lacks and that is not a documented extra, an Info `observation`): the source has no line to repair the name from, so a person removes its documentation when the name is gone from the source, fixes the brief's scope when the surface missed it, or, when the source still declares the name, keeps the documentation and re-runs Test Skill at a tier that classifies it.
|
|
101
101
|
- `metadata-drift` (divergent export counts in the metadata): update-skill recounts `stats.exports_public_api`, `stats.exports_documented` and `stats.effective_denominator` each time it writes metadata.json, so a metadata patch to them does not hold (under `--allow-workspace-drift` and for a docs-only skill it keeps the recorded public API counts and `stats.effective_denominator` instead); Create Skill rebuilds diverging barrel counts from the source and recounts a `stats.effective_denominator` the brief's `scope.tier_a_include` set disagrees with (denominator drift), a person fixes denominator deflation on a brief with no tier A list by adding `scope.tier_a_include` to it, and documented-surface counts that differ by the `Type::method` entries the named-export count leaves out need no fix.
|
|
102
|
-
- `provenance-unverified`: the line-check rules could not verify the line, or the recorded line is not a number, so a person checks by hand which line defines the export and, when it is not the recorded one, sets its provenance `source_line` to that line.
|
|
102
|
+
- `provenance-unverified`: the line-check rules could not verify the line, or the recorded line is not a number, so a person checks by hand which line defines the export and, when it is not the recorded one, sets its provenance `source_line` to that line; when the gap names an alias, no line of the cited file defines the export, so a person records the rename in its provenance entry (`export_name` the name the alias renames, `reexported_as` the export) only when no other entry has that `export_name` and the alias does not rename a default export, and otherwise cites the alias line, as the remediation says.
|
|
103
103
|
- `denominator-inflation`: it concerns the brief's scope and the count design, which a person decides in the skill brief, as the gap's remediation says.
|
|
104
104
|
- `brief-scope-stale`: a person edits the brief's scope globs by hand so each matches the files it meant, since update-skill does not rewrite them.
|
|
105
105
|
- `multi-denominator`: no fix is needed, since the barrel and the documented surface measure different sets by design and the report records both counts.
|
|
@@ -9,4 +9,4 @@ The argument set, gate map, outputs and headless result line of `skf-update-skil
|
|
|
9
9
|
| **Gates** | step 1: init.md §4's degraded-rebuild gate [D]/[X] when no provenance map is found (headless: halts, or takes [D] under `--allow-degraded`; a gap-driven run halts `blocked` there instead); init.md §4b's [G]/[S] test-report offer, interactive only (headless warns `unconsumed-test-report`); Confirm Gate [C] (init.md §8) | step 2, each only when triggered: detect-changes.md §1b's promote prompt and §1c's scope-expansion prompt [P]/[S]/[U] (headless: defers each candidate to a person), §2.2's deletion-ratio gate [C]/[B]/[A] (headless: [C]) | step 2g (gap-driven): gap-driven.md §1 rule R1's [D]/[R] for each gap whose remediation names removal (headless: [D], or [R] only when the export is internal or `#[doc(hidden)]`) | step 4: Confirm Gate [C] only after the user resolved [MANUAL] conflicts (merge.md §4); a clean merge auto-proceeds, and a headless run with conflicts halts `halted-for-manual-mismatch` before anything is written |
|
|
10
10
|
| **Outputs** | A new version folder `{skill_group}/{new_version}/` and its forge folder, beside the unchanged previous version (a gap-driven repair writes the current version in place), and a finished run's `update-skill-result-{YYYYMMDD-HHmmss}.json` and `update-skill-result-latest.json` in that version's forge folder (a run that found no change: the current one's). `--detect-only` and `--dry-run` write none of these, nor the skill brief. Run state goes to a run folder under `{project-root}/_bmad-output/.skf-run/` that step 7 removes; a halt keeps it |
|
|
11
11
|
| **Concurrency** | A second real update of the same skill halts `halted-for-concurrent-run` while the first holds its run lock (init.md §1b); `--detect-only` and `--dry-run` take no lock. write.md §8 halts with the same status when another flip of the `active` link holds its lock for 5 seconds. A remote skill is read from a private tree, and no mode writes to the shared workspace clone before write.md §9. |
|
|
12
|
-
| **Headless** | No gate prompts: each resolves as the Gates row says, and the run records the decision. One `SKF_UPDATE_RESULT_JSON` line (report.md §5b), at step 6 or at a HALT, printed by the shared emitter and checked against `shared/scripts/schemas/skf-update-result-envelope.v1.json`. It is the run's last line: the shared health check displays it after everything else; a HALT displays its line and stops. Pipelines branch on `skf_update.status`: `success`, `no-changes`, `detect-only` and `dry-run` are successful exits. A caller reads the run's own `SKF_UPDATE_RESULT_JSON` line. A run that printed no envelope line has no result to read, whatever the process exit code: it was cut off, or could not write its result, and `update-skill-result-latest.json` may then describe an earlier run, so it is never read in its place. |
|
|
12
|
+
| **Headless** | No gate prompts: each resolves as the Gates row says, and the run records the decision. One `SKF_UPDATE_RESULT_JSON` line (report.md §5b), at step 6 or at a HALT, printed by the shared emitter and checked against `shared/scripts/schemas/skf-update-result-envelope.v1.json`. It is the run's last line: the shared health check displays it after everything else; a HALT displays its line and stops. Pipelines branch on `skf_update.status`: `success`, `no-changes`, `detect-only` and `dry-run` are successful exits. A headless run whose emitter refused its result line for good ends on a `halted-for-write-failure` line instead, with `error.phase` `report:result-contract` and an `error.reason` that starts `result-contract-not-written`, and nothing it wrote is undone. A caller reads the run's own `SKF_UPDATE_RESULT_JSON` line. A run that printed no envelope line has no result to read, whatever the process exit code: it was cut off, or could not write its result, and `update-skill-result-latest.json` may then describe an earlier run, so it is never read in its place. |
|
|
@@ -299,7 +299,7 @@ uv run {runLockHelper} acquire \
|
|
|
299
299
|
```
|
|
300
300
|
|
|
301
301
|
It renames the staged folder to `{skill_group}/{new_version}/` in one step, so no reader ever sees a half-copied version.
|
|
302
|
-
6. **Create the forge folder** `{forge_data_folder}/{skill_name}/{new_version}/` and copy `provenance-map.json`, `evidence-report.md` and `extraction-rules.yaml
|
|
302
|
+
6. **Create the forge folder** `{forge_data_folder}/{skill_name}/{new_version}/` and copy into it each of `provenance-map.json`, `evidence-report.md` and `extraction-rules.yaml`: from `{forge_version}` when it is there, else from `{forge_data_folder}/{skill_name}/` when it is there and init.md §4 bound `{provenance_map_path}` to that flat folder's map (an older skill's flat forge data, `knowledge/version-paths.md`). Copy, never move: the previous version's files and the flat ones stay as they are. Step 5 updates the provenance map and appends this update to the evidence report there; the extraction rules record how the skill was extracted. Test reports, drift reports and result files belong to the version they were made for and stay where they are.
|
|
303
303
|
7. **Rebind** `{skill_package}` ← `{skill_group}/{new_version}/{skill_name}` and `{forge_version}` ← `{forge_data_folder}/{skill_name}/{new_version}`. Every later step reads and writes the new version there; `{manual_inventory}` keeps the amended path §4 bound.
|
|
304
304
|
|
|
305
305
|
If no `{atomicWriteProbeOrder}` candidate resolves, or the stage, copy, write, publish or forge-folder step fails, HALT with status `halted-for-write-failure`, naming the failed step (for a missing helper: "skf-atomic-write.py is missing; re-install SKF"). The halt procedure takes `phase: "merge:new-version-folder"`, `path: "{skill_group}/{new_version}"`, `reason: "<the failed step>: <its error>"`; its rollback removes what this section created, `{version_staging}`, `{skill_group}/{new_version}/` and `{forge_data_folder}/{skill_name}/{new_version}/`, none of which existed before `begin` recorded them. The previous version is unchanged.
|