bmad-module-skill-forge 2.2.0 → 4.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 +49 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +143 -18
- package/docs/verifying-a-skill.md +87 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +3 -3
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +31 -59
- package/src/shared/references/description-guard-protocol.md +12 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +233 -64
- package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +772 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +21 -3
- package/src/shared/scripts/skf-description-guard.py +99 -11
- package/src/shared/scripts/skf-detect-docs.py +694 -2
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
- package/src/shared/scripts/skf-detect-tools.py +209 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +5247 -96
- package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +664 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +469 -205
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +700 -0
- package/src/shared/scripts/skf-hash-content.py +180 -20
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +161 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +143 -20
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +176 -28
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +155 -14
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +787 -121
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +120 -67
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +23 -2
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +3945 -152
- package/src/shared/scripts/skf-write-skill-brief.py +406 -25
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +28 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +149 -210
- package/src/skf-audit-skill/references/re-index.md +103 -110
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +34 -25
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +80 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
- package/src/skf-brief-skill/SKILL.md +37 -26
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +18 -25
- package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +144 -127
- package/src/skf-brief-skill/references/confirm-brief.md +11 -9
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +7 -10
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
- package/src/skf-create-skill/assets/skill-sections.md +21 -11
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +119 -70
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
- package/src/skf-create-skill/references/extract.md +204 -162
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
- package/src/skf-create-skill/references/extraction-patterns.md +588 -201
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +37 -57
- package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +28 -19
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +83 -146
- package/src/skf-drop-skill/references/health-check.md +2 -0
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +122 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -90
- package/src/skf-setup/references/detect-and-tier.md +58 -67
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +209 -264
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +164 -91
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +164 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +42 -155
- package/src/skf-test-skill/references/source-access-protocol.md +18 -94
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +380 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
- package/src/skf-test-skill/scripts/score-signatures.py +479 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +253 -223
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +254 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +182 -49
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +102 -35
- package/src/skf-update-skill/references/write.md +357 -155
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -679
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""Signature Accuracy and Type Coverage for SKF test-skill, from checked per-file results.
|
|
7
|
+
|
|
8
|
+
coverage-check.md section 2 compares each documented signature with the
|
|
9
|
+
source and section 2b scores two categories from the comparison: Signature
|
|
10
|
+
Accuracy and Type Coverage, 22% and 14% of a contextual score. Which
|
|
11
|
+
signature differs is a judgment, made by a subagent that reads the source;
|
|
12
|
+
everything after it has one answer per input and runs here: the list of
|
|
13
|
+
signatures to compare, the schema check of each subagent result, the two
|
|
14
|
+
ratios and the gap records of the wrong signatures.
|
|
15
|
+
|
|
16
|
+
plan the comparisons to make. For each documented export with a
|
|
17
|
+
signature (an inventory entry of any kind but `method` that carries
|
|
18
|
+
`params`, `return_type` or `usage_signature`) in the signature
|
|
19
|
+
universe, grouped by source file, with the line that defines it in
|
|
20
|
+
the source and the documented signature. The signature universe is
|
|
21
|
+
the surface's `all` set plus every name on its export records, so
|
|
22
|
+
the names of a Python sub-package load-coverage-inputs.py keeps out
|
|
23
|
+
of `all` (excluded.nestedEntries) keep their checks. Also the whole
|
|
24
|
+
documented-signature map, for the fallback scan that has no surface
|
|
25
|
+
lines yet.
|
|
26
|
+
score the categories. Reads every subagent result, checks its schema,
|
|
27
|
+
and computes:
|
|
28
|
+
Signature Accuracy = matching / compared * 100, where compared is
|
|
29
|
+
the documented signatures of the signature universe (with a
|
|
30
|
+
fallback scan's exports_found) and matching those no result
|
|
31
|
+
names in signature_mismatches[]: a wrong signature is wrong
|
|
32
|
+
whichever set the denominator counts
|
|
33
|
+
Type Coverage = documented types / total types * 100, where
|
|
34
|
+
total types counts the names of the --surface-set set (the
|
|
35
|
+
denominator set coverage-check section 2b picked) whose kind is
|
|
36
|
+
a type kind (TYPE_KINDS: interface, type alias, enum and class,
|
|
37
|
+
with a Rust struct counted as a class and a trait as an
|
|
38
|
+
interface) and documented types those the inventory documents
|
|
39
|
+
A category with nothing to compare scores 100 (nothing documented
|
|
40
|
+
is wrong) and says so in `warnings`. Each signature mismatch of a
|
|
41
|
+
compared name becomes a Critical `signature-mismatch` record in the
|
|
42
|
+
gap ledger's record format (gap-ledger.py append --input reads
|
|
43
|
+
--gaps-output); a mismatch a result reports for any other name (one
|
|
44
|
+
the skill does not document, or the surface lacks) is listed in
|
|
45
|
+
`warnings` instead, and no gap.
|
|
46
|
+
|
|
47
|
+
Inputs, all files in the run folder:
|
|
48
|
+
--inventory the validated inventory validate-inventory.py wrote (--output)
|
|
49
|
+
--surface load-coverage-inputs.py surface output: `exports[]` (name,
|
|
50
|
+
kind, file, line, signatureLine) and the name `sets`;
|
|
51
|
+
--surface-set picks the set (default "all") score's Type
|
|
52
|
+
Coverage counts, while plan and Signature Accuracy compare
|
|
53
|
+
the signature universe whichever set it picks
|
|
54
|
+
--results a subagent result, as the subagent returned it (a wrapping
|
|
55
|
+
markdown fence is stripped), repeatable. One object, or a
|
|
56
|
+
list of them:
|
|
57
|
+
{"file": "<source file>",
|
|
58
|
+
"exports_found": ["..."], (fallback scan only)
|
|
59
|
+
"types_found": ["..."], (fallback scan only)
|
|
60
|
+
"signature_mismatches": [
|
|
61
|
+
{"name", "line", "source_sig", "documented_sig", "issue"}]}
|
|
62
|
+
|
|
63
|
+
Rounding is compute-score.py's (JS-compatible half-up, two decimals).
|
|
64
|
+
|
|
65
|
+
Usage:
|
|
66
|
+
uv run score-signatures.py plan --inventory <file> [--surface <file>] [--output <file>]
|
|
67
|
+
uv run score-signatures.py score --inventory <file> --surface <file> [--surface-set <set>] \\
|
|
68
|
+
--results <file> [--results <file> ...] [--output <file>] [--gaps-output <file>]
|
|
69
|
+
|
|
70
|
+
Output (stdout; --output also writes it, --gaps-output writes the records):
|
|
71
|
+
plan: {"compared": N, "files": [{"file", "checks": [{"name", "line",
|
|
72
|
+
"signatureLine", "documented": {...}}]}], "documentedSignatures": {...}}
|
|
73
|
+
score: {"valid": true, "signatureAccuracy", "matchingSignatures",
|
|
74
|
+
"totalDocumented", "comparedNames": [the names compared],
|
|
75
|
+
"typeCoverage", "documentedTypes", "totalTypes",
|
|
76
|
+
"missingTypes": [...], "mismatches": [...], "gapRecords": [...],
|
|
77
|
+
"warnings": [...]}
|
|
78
|
+
or {"valid": false, "violations": [...]} when a result breaks the schema
|
|
79
|
+
|
|
80
|
+
Exit codes:
|
|
81
|
+
0 the result was printed
|
|
82
|
+
1 an input file cannot be read or is not the JSON it should be, or the
|
|
83
|
+
surface has no --surface-set set
|
|
84
|
+
2 a subagent result breaks the schema (violations[] names each breach;
|
|
85
|
+
nothing is written to --output or --gaps-output)
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
from __future__ import annotations
|
|
89
|
+
|
|
90
|
+
import argparse
|
|
91
|
+
import importlib.util
|
|
92
|
+
import json
|
|
93
|
+
import math
|
|
94
|
+
import sys
|
|
95
|
+
from pathlib import Path
|
|
96
|
+
|
|
97
|
+
# Surface kinds that count toward total_types. A Rust struct is a class and a
|
|
98
|
+
# trait an interface; `type` is a type alias.
|
|
99
|
+
TYPE_KINDS = frozenset({"interface", "type", "enum", "class", "struct", "trait"})
|
|
100
|
+
SIGNATURE_FIELDS = ("params", "return_type", "usage_signature")
|
|
101
|
+
MISMATCH_FIELDS = ("name", "line", "source_sig", "documented_sig", "issue")
|
|
102
|
+
|
|
103
|
+
_INVENTORY_MODULE = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _inventory():
|
|
107
|
+
"""validate-inventory.py beside this script: the fence rule lives there."""
|
|
108
|
+
global _INVENTORY_MODULE
|
|
109
|
+
if _INVENTORY_MODULE is None:
|
|
110
|
+
sibling = Path(__file__).resolve().parent / "validate-inventory.py"
|
|
111
|
+
spec = importlib.util.spec_from_file_location("skf_validate_inventory", sibling)
|
|
112
|
+
if spec is None or spec.loader is None or not sibling.is_file():
|
|
113
|
+
raise ImportError(f"validate-inventory.py not found beside {Path(__file__).name}")
|
|
114
|
+
module = importlib.util.module_from_spec(spec)
|
|
115
|
+
spec.loader.exec_module(module)
|
|
116
|
+
_INVENTORY_MODULE = module
|
|
117
|
+
return _INVENTORY_MODULE
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class InputError(Exception):
|
|
121
|
+
"""An input file that cannot be read, or is not the JSON it should be."""
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def round2(value: float) -> float:
|
|
125
|
+
"""Round to 2 decimals with JS-compatible half-up rounding (matches compute-score.py)."""
|
|
126
|
+
return math.floor(value * 100 + 0.5) / 100
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _read_text(path: str, flag: str) -> str:
|
|
130
|
+
try:
|
|
131
|
+
return Path(path).read_bytes().decode("utf-8-sig")
|
|
132
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
133
|
+
raise InputError(f"cannot read {flag} {path}: {exc}") from exc
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _read_json(path: str, flag: str):
|
|
137
|
+
try:
|
|
138
|
+
return json.loads(_read_text(path, flag))
|
|
139
|
+
except json.JSONDecodeError as exc:
|
|
140
|
+
raise InputError(f"{flag} {path} is not JSON: {exc.msg}") from exc
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# --------------------------------------------------------------------------
|
|
144
|
+
# Inputs
|
|
145
|
+
# --------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def load_inventory(path: str) -> list[dict]:
|
|
149
|
+
data = _read_json(path, "--inventory")
|
|
150
|
+
if isinstance(data, dict) and isinstance(data.get("inventory"), dict):
|
|
151
|
+
data = data["inventory"] # validate-inventory.py's whole result
|
|
152
|
+
if not isinstance(data, dict) or not isinstance(data.get("exports"), list):
|
|
153
|
+
raise InputError(f"--inventory {path} holds no `exports` list")
|
|
154
|
+
return [e for e in data["exports"] if isinstance(e, dict)]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def load_surface(path: str, surface_set: str) -> tuple[list[str], list[dict], list[str]]:
|
|
158
|
+
"""(the chosen name set, every surface export record, the `all` set)."""
|
|
159
|
+
data = _read_json(path, "--surface")
|
|
160
|
+
sets = data.get("sets") if isinstance(data, dict) else None
|
|
161
|
+
|
|
162
|
+
def name_set(key: str) -> list[str]:
|
|
163
|
+
names = sets.get(key) if isinstance(sets, dict) else None
|
|
164
|
+
if not isinstance(names, list):
|
|
165
|
+
raise InputError(f"--surface {path} has no name set `{key}`")
|
|
166
|
+
return [n for n in names if isinstance(n, str) and n]
|
|
167
|
+
|
|
168
|
+
exports = data.get("exports") if isinstance(data.get("exports"), list) else []
|
|
169
|
+
return name_set(surface_set), [e for e in exports if isinstance(e, dict)], name_set("all")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def check_result(item: object, where: str) -> list[str]:
|
|
173
|
+
"""The schema breaches of one per-file result object."""
|
|
174
|
+
if not isinstance(item, dict):
|
|
175
|
+
return [f"{where} is not a JSON object"]
|
|
176
|
+
errors = []
|
|
177
|
+
if not (isinstance(item.get("file"), str) and item["file"].strip()):
|
|
178
|
+
errors.append(f"{where}: `file` must be a non-empty string")
|
|
179
|
+
for key in ("exports_found", "types_found"):
|
|
180
|
+
value = item.get(key)
|
|
181
|
+
if value is not None and not (isinstance(value, list) and all(isinstance(v, str) for v in value)):
|
|
182
|
+
errors.append(f"{where}: `{key}` must be a list of strings when present")
|
|
183
|
+
mismatches = item.get("signature_mismatches")
|
|
184
|
+
if not isinstance(mismatches, list):
|
|
185
|
+
errors.append(f"{where}: `signature_mismatches` must be present and a list (may be empty)")
|
|
186
|
+
return errors
|
|
187
|
+
for i, entry in enumerate(mismatches):
|
|
188
|
+
at = f"{where}.signature_mismatches[{i}]"
|
|
189
|
+
if not isinstance(entry, dict):
|
|
190
|
+
errors.append(f"{at} is not an object")
|
|
191
|
+
continue
|
|
192
|
+
missing = [f for f in MISMATCH_FIELDS if f not in entry]
|
|
193
|
+
if missing:
|
|
194
|
+
errors.append(f"{at} missing field(s): {', '.join(missing)}")
|
|
195
|
+
continue
|
|
196
|
+
line = entry["line"]
|
|
197
|
+
if isinstance(line, bool) or not isinstance(line, int) or line < 1:
|
|
198
|
+
errors.append(f"{at}: `line` must be a line number (an integer from 1)")
|
|
199
|
+
for field in ("name", "source_sig", "documented_sig", "issue"):
|
|
200
|
+
if not (isinstance(entry[field], str) and entry[field].strip()):
|
|
201
|
+
errors.append(f"{at}: `{field}` must be a non-empty string")
|
|
202
|
+
return errors
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def parse_result_text(text: str, where: str) -> tuple[list[dict], list[str]]:
|
|
206
|
+
"""(the result objects, the schema breaches) of one subagent response."""
|
|
207
|
+
inner = _inventory().strip_fences(text)
|
|
208
|
+
try:
|
|
209
|
+
data = json.loads(inner)
|
|
210
|
+
except json.JSONDecodeError as exc:
|
|
211
|
+
return [], [f"{where} is not valid JSON: {exc.msg}"]
|
|
212
|
+
items = data if isinstance(data, list) else [data]
|
|
213
|
+
errors = []
|
|
214
|
+
for i, item in enumerate(items):
|
|
215
|
+
errors += check_result(item, where if len(items) == 1 else f"{where}[{i}]")
|
|
216
|
+
return ([] if errors else items), errors
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def load_results(paths: list[str]) -> tuple[list[dict], list[str]]:
|
|
220
|
+
"""Every result object of the --results files, and every schema breach."""
|
|
221
|
+
results, violations = [], []
|
|
222
|
+
for path in paths:
|
|
223
|
+
items, errors = parse_result_text(_read_text(path, "--results"), Path(path).name)
|
|
224
|
+
results += items
|
|
225
|
+
violations += errors
|
|
226
|
+
return results, violations
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# --------------------------------------------------------------------------
|
|
230
|
+
# Plan and score
|
|
231
|
+
# --------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _has_signature(entry: dict) -> bool:
|
|
235
|
+
return any(
|
|
236
|
+
isinstance(entry.get(f), list) or (isinstance(entry.get(f), str) and entry[f].strip())
|
|
237
|
+
for f in SIGNATURE_FIELDS
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def documented_signatures(inventory: list[dict]) -> dict[str, dict]:
|
|
242
|
+
"""{name: its documented signature fields} for each documented export with one."""
|
|
243
|
+
out: dict[str, dict] = {}
|
|
244
|
+
for entry in inventory:
|
|
245
|
+
name = entry.get("name")
|
|
246
|
+
if entry.get("kind") == "method" or not (isinstance(name, str) and name) or name in out:
|
|
247
|
+
continue
|
|
248
|
+
if _has_signature(entry):
|
|
249
|
+
out[name] = {f: entry[f] for f in SIGNATURE_FIELDS if f in entry}
|
|
250
|
+
return out
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def documented_names(inventory: list[dict]) -> set[str]:
|
|
254
|
+
return {e["name"] for e in inventory
|
|
255
|
+
if isinstance(e.get("name"), str) and e["name"] and e.get("kind") != "method"}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _records_by_name(exports: list[dict]) -> dict[str, list[dict]]:
|
|
259
|
+
by_name: dict[str, list[dict]] = {}
|
|
260
|
+
for record in sorted(exports, key=lambda r: (str(r.get("file") or ""), r.get("line") or 0)):
|
|
261
|
+
name = record.get("name")
|
|
262
|
+
if isinstance(name, str) and name:
|
|
263
|
+
by_name.setdefault(name, []).append(record)
|
|
264
|
+
return by_name
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def signature_universe(names: list[str], exports: list[dict]) -> set[str]:
|
|
268
|
+
"""The names whose documented signature is compared: the `all` set
|
|
269
|
+
(`names`) plus every name on the surface's export records. A nested
|
|
270
|
+
Python top's names leave `all` but keep their records, so a documented
|
|
271
|
+
one still has its signature checked."""
|
|
272
|
+
return set(names) | set(_records_by_name(exports))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def plan(inventory: list[dict], names: list[str] | None, exports: list[dict]) -> dict:
|
|
276
|
+
"""The comparisons over the signature universe of `names` (the `all`
|
|
277
|
+
set) and `exports`; none without a surface (`names` None)."""
|
|
278
|
+
signatures = documented_signatures(inventory)
|
|
279
|
+
files: dict[str, list[dict]] = {}
|
|
280
|
+
compared = 0
|
|
281
|
+
if names is not None:
|
|
282
|
+
by_name = _records_by_name(exports)
|
|
283
|
+
for name in sorted(signature_universe(names, exports) & set(signatures)):
|
|
284
|
+
record = (by_name.get(name) or [{}])[0]
|
|
285
|
+
compared += 1
|
|
286
|
+
files.setdefault(str(record.get("file") or ""), []).append({
|
|
287
|
+
"name": name,
|
|
288
|
+
"line": record.get("line"),
|
|
289
|
+
"signatureLine": record.get("signatureLine"),
|
|
290
|
+
"documented": signatures[name],
|
|
291
|
+
})
|
|
292
|
+
return {
|
|
293
|
+
"compared": compared,
|
|
294
|
+
"files": [{"file": f, "checks": checks} for f, checks in sorted(files.items())],
|
|
295
|
+
"documentedSignatures": signatures,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _gap_record(m: dict) -> dict:
|
|
300
|
+
name, where = m["name"], f"{m['file']}:{m['line']}"
|
|
301
|
+
return {
|
|
302
|
+
"severity": "Critical",
|
|
303
|
+
"category": "signature-mismatch",
|
|
304
|
+
"title": f"Signature mismatch: {name}",
|
|
305
|
+
"source": where,
|
|
306
|
+
"export": name,
|
|
307
|
+
"issue": f"documented `{m['documented_sig']}`, source `{m['source_sig']}`: {m['issue']}",
|
|
308
|
+
"remediation": (f"Update the documented signature of `{name}` from `{m['documented_sig']}` "
|
|
309
|
+
f"to `{m['source_sig']}`, the signature the source defines at `{where}`."),
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def score(inventory: list[dict], names: list[str], exports: list[dict], results: list[dict],
|
|
314
|
+
type_names: list[str] | None = None) -> dict:
|
|
315
|
+
"""The two categories: Signature Accuracy over the signature universe of
|
|
316
|
+
`names` (the `all` set) and `exports`, Type Coverage over `type_names`
|
|
317
|
+
when given, else over `names`. A fallback scan's exports_found join the
|
|
318
|
+
universe, and `names` when no `type_names` is given."""
|
|
319
|
+
by_name = _records_by_name(exports)
|
|
320
|
+
# A fallback scan reports its own names and types.
|
|
321
|
+
fallback_types = {n for r in results for n in r.get("types_found") or []}
|
|
322
|
+
surface = set(names) | {n for r in results for n in r.get("exports_found") or []}
|
|
323
|
+
documented = documented_names(inventory)
|
|
324
|
+
signatures = documented_signatures(inventory)
|
|
325
|
+
compared = (surface | signature_universe(names, exports)) & set(signatures)
|
|
326
|
+
warnings = []
|
|
327
|
+
|
|
328
|
+
mismatches = []
|
|
329
|
+
seen = set()
|
|
330
|
+
for result in results:
|
|
331
|
+
for entry in result["signature_mismatches"]:
|
|
332
|
+
key = (entry["name"], result["file"], entry["line"])
|
|
333
|
+
if key in seen:
|
|
334
|
+
continue
|
|
335
|
+
seen.add(key)
|
|
336
|
+
if entry["name"] not in compared:
|
|
337
|
+
warnings.append(f"{result['file']}:{entry['line']}: a signature mismatch for "
|
|
338
|
+
f"`{entry['name']}`, which is no documented signature on the source surface: "
|
|
339
|
+
"not scored, no gap")
|
|
340
|
+
continue
|
|
341
|
+
mismatches.append({"name": entry["name"], "file": result["file"], "line": entry["line"],
|
|
342
|
+
"source_sig": entry["source_sig"], "documented_sig": entry["documented_sig"],
|
|
343
|
+
"issue": entry["issue"]})
|
|
344
|
+
mismatches.sort(key=lambda m: (m["file"], m["line"], m["name"]))
|
|
345
|
+
|
|
346
|
+
wrong = {m["name"] for m in mismatches}
|
|
347
|
+
if compared:
|
|
348
|
+
signature_accuracy = round2((len(compared) - len(wrong)) / len(compared) * 100)
|
|
349
|
+
else:
|
|
350
|
+
signature_accuracy = 100.0
|
|
351
|
+
warnings.append("no documented signature is on the source surface: Signature Accuracy has "
|
|
352
|
+
"nothing to compare and scores 100")
|
|
353
|
+
|
|
354
|
+
def kind_of(name: str) -> str | None:
|
|
355
|
+
if name in fallback_types:
|
|
356
|
+
return "type"
|
|
357
|
+
return next((r.get("kind") for r in by_name.get(name, []) if r.get("kind")), None)
|
|
358
|
+
|
|
359
|
+
types = {n for n in (surface if type_names is None else set(type_names)) if kind_of(n) in TYPE_KINDS}
|
|
360
|
+
documented_types = types & documented
|
|
361
|
+
if types:
|
|
362
|
+
type_coverage = round2(len(documented_types) / len(types) * 100)
|
|
363
|
+
else:
|
|
364
|
+
type_coverage = 100.0
|
|
365
|
+
warnings.append("the source surface has no interface, type alias, enum or class: Type "
|
|
366
|
+
"Coverage has nothing to cover and scores 100")
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
"valid": True,
|
|
370
|
+
"signatureAccuracy": signature_accuracy,
|
|
371
|
+
"matchingSignatures": len(compared) - len(wrong),
|
|
372
|
+
"totalDocumented": len(compared),
|
|
373
|
+
"comparedNames": sorted(compared),
|
|
374
|
+
"typeCoverage": type_coverage,
|
|
375
|
+
"documentedTypes": len(documented_types),
|
|
376
|
+
"totalTypes": len(types),
|
|
377
|
+
"missingTypes": sorted(types - documented),
|
|
378
|
+
"mismatches": mismatches,
|
|
379
|
+
"gapRecords": [_gap_record(m) for m in mismatches],
|
|
380
|
+
"warnings": warnings,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# --------------------------------------------------------------------------
|
|
385
|
+
# CLI
|
|
386
|
+
# --------------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _write_json(path: str, payload) -> None:
|
|
390
|
+
target = Path(path)
|
|
391
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
392
|
+
target.write_bytes((json.dumps(payload, indent=2, ensure_ascii=False) + "\n").encode("utf-8"))
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _cmd_plan(args: argparse.Namespace) -> tuple[dict, int]:
|
|
396
|
+
inventory = load_inventory(args.inventory)
|
|
397
|
+
names, exports = (None, [])
|
|
398
|
+
if args.surface is not None:
|
|
399
|
+
_, exports, names = load_surface(args.surface, args.surface_set)
|
|
400
|
+
out = plan(inventory, names, exports)
|
|
401
|
+
if args.output:
|
|
402
|
+
_write_json(args.output, out)
|
|
403
|
+
return out, 0
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _cmd_score(args: argparse.Namespace) -> tuple[dict, int]:
|
|
407
|
+
inventory = load_inventory(args.inventory)
|
|
408
|
+
chosen, exports, names = load_surface(args.surface, args.surface_set)
|
|
409
|
+
results, violations = load_results(args.results or [])
|
|
410
|
+
if violations:
|
|
411
|
+
for path in (args.output, args.gaps_output):
|
|
412
|
+
if path:
|
|
413
|
+
Path(path).unlink(missing_ok=True)
|
|
414
|
+
return {"valid": False, "violations": violations}, 2
|
|
415
|
+
out = score(inventory, names, exports, results, None if args.surface_set == "all" else chosen)
|
|
416
|
+
if args.output:
|
|
417
|
+
_write_json(args.output, out)
|
|
418
|
+
if args.gaps_output:
|
|
419
|
+
_write_json(args.gaps_output, out["gapRecords"])
|
|
420
|
+
return out, 0
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
424
|
+
parser = argparse.ArgumentParser(
|
|
425
|
+
prog="score-signatures",
|
|
426
|
+
description="Signature Accuracy and Type Coverage from schema-checked per-file results.",
|
|
427
|
+
)
|
|
428
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
429
|
+
|
|
430
|
+
p = sub.add_parser("plan", help="the signature comparisons for the subagents to make")
|
|
431
|
+
p.add_argument("--inventory", required=True, help="the validated inventory file")
|
|
432
|
+
p.add_argument("--surface", help="load-coverage-inputs.py surface output")
|
|
433
|
+
p.add_argument("--surface-set", default="all",
|
|
434
|
+
help="a surface name set, which must exist (default: all); the plan compares the signature "
|
|
435
|
+
"universe whichever set it names")
|
|
436
|
+
p.add_argument("--output", help="also write the plan to this file")
|
|
437
|
+
p.set_defaults(func=_cmd_plan)
|
|
438
|
+
|
|
439
|
+
p = sub.add_parser("score", help="the two category scores and the mismatch gap records")
|
|
440
|
+
p.add_argument("--inventory", required=True, help="the validated inventory file")
|
|
441
|
+
p.add_argument("--surface", required=True, help="load-coverage-inputs.py surface output")
|
|
442
|
+
p.add_argument("--surface-set", default="all",
|
|
443
|
+
help="the surface name set Type Coverage counts (default: all)")
|
|
444
|
+
p.add_argument("--results", action="append", help="a subagent result file (repeatable)")
|
|
445
|
+
p.add_argument("--output", help="also write the scores to this file")
|
|
446
|
+
p.add_argument("--gaps-output", help="write the gap ledger records to this file")
|
|
447
|
+
p.set_defaults(func=_cmd_score)
|
|
448
|
+
return parser
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def main(argv: list[str] | None = None) -> int:
|
|
452
|
+
args = _build_parser().parse_args(argv)
|
|
453
|
+
try:
|
|
454
|
+
out, code = args.func(args)
|
|
455
|
+
except (InputError, ImportError) as exc:
|
|
456
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
457
|
+
return 1
|
|
458
|
+
print(json.dumps(out, indent=2))
|
|
459
|
+
return code
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _force_utf8(*streams) -> None:
|
|
463
|
+
"""Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
|
|
464
|
+
|
|
465
|
+
A Windows console pipes them as cp1252, which cannot print every character
|
|
466
|
+
of the --help text, so --help would stop with UnicodeEncodeError.
|
|
467
|
+
"""
|
|
468
|
+
for stream in streams:
|
|
469
|
+
if hasattr(stream, "reconfigure"):
|
|
470
|
+
errors = getattr(stream, "errors", None)
|
|
471
|
+
if errors is None:
|
|
472
|
+
stream.reconfigure(encoding="utf-8")
|
|
473
|
+
else:
|
|
474
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
if __name__ == "__main__":
|
|
478
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
479
|
+
sys.exit(main())
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""Print the stdin payload of a shared SKF helper, read from the files on disk.
|
|
7
|
+
|
|
8
|
+
coverage-check.md §0b runs skf-detect-workspaces.py (is the local source a
|
|
9
|
+
monorepo?), a shared helper that takes one JSON payload on stdin and reads
|
|
10
|
+
no file itself. A payload the model writes by hand can cut a file short or
|
|
11
|
+
escape it wrongly, and the helper then fails or answers for the wrong text.
|
|
12
|
+
This script reads the files and prints the payload, and the step pipes it
|
|
13
|
+
into the helper:
|
|
14
|
+
|
|
15
|
+
uv run stage-helper-payload.py detect-workspaces --source-root <dir> \\
|
|
16
|
+
| uv run skf-detect-workspaces.py
|
|
17
|
+
|
|
18
|
+
(The Quick-tier export scan needs no payload: skf-extract-public-api.py
|
|
19
|
+
--mode quick reads its files itself, with --manifest-file and --entry-file.)
|
|
20
|
+
|
|
21
|
+
detect-workspaces prints {"tree": [...], "manifests": {...}}:
|
|
22
|
+
|
|
23
|
+
tree every file below --source-root, as a POSIX path relative to
|
|
24
|
+
it, sorted. Folders named in SKIPPED_DIRS (installed
|
|
25
|
+
dependencies, virtual environments, build output, caches) and
|
|
26
|
+
hidden folders (a name that starts with ".") are not read, and a
|
|
27
|
+
linked folder is not followed.
|
|
28
|
+
manifests the text of each file directly in --source-root that is not
|
|
29
|
+
hidden, holds at most MAX_ROOT_FILE_BYTES and reads as UTF-8, by
|
|
30
|
+
its name. The detector reads the root manifests it knows
|
|
31
|
+
(package.json, pnpm-workspace.yaml, lerna.json, Cargo.toml ...)
|
|
32
|
+
and ignores the rest, so this script keeps no list of their
|
|
33
|
+
names to fall out of step with the detector's.
|
|
34
|
+
|
|
35
|
+
The payload is one line of ASCII JSON on stdout, so a pipe carries the same
|
|
36
|
+
bytes on every platform.
|
|
37
|
+
|
|
38
|
+
Exit codes:
|
|
39
|
+
0 the payload is printed
|
|
40
|
+
1 --source-root is not a folder; one JSON line {"status": "error",
|
|
41
|
+
"error": ...} goes to stderr and nothing to stdout, so the helper the
|
|
42
|
+
payload was piped into reads no input and fails too
|
|
43
|
+
2 bad arguments (argparse: a missing or unknown argument, usage on stderr)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import argparse
|
|
49
|
+
import json
|
|
50
|
+
import os
|
|
51
|
+
import sys
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
# Folders that hold no workspace member of the repository itself.
|
|
55
|
+
SKIPPED_DIRS = frozenset({
|
|
56
|
+
"node_modules", "bower_components", "jspm_packages", "vendor", "Pods",
|
|
57
|
+
"venv", "site-packages", "__pycache__", "target",
|
|
58
|
+
})
|
|
59
|
+
MAX_ROOT_FILE_BYTES = 1024 * 1024
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class StageError(Exception):
|
|
63
|
+
"""A path the payload cannot be built from."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _source_root(value: str) -> Path:
|
|
67
|
+
root = Path(value)
|
|
68
|
+
if not root.is_dir():
|
|
69
|
+
raise StageError(f"--source-root is not a folder: {value}")
|
|
70
|
+
return root
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --------------------------------------------------------------------------
|
|
74
|
+
# detect-workspaces
|
|
75
|
+
# --------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _skipped(name: str) -> bool:
|
|
79
|
+
return name.startswith(".") or name in SKIPPED_DIRS
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def list_tree(root: Path) -> list[str]:
|
|
83
|
+
"""Every file below `root` as a sorted POSIX path, outside skipped and linked folders."""
|
|
84
|
+
tree = []
|
|
85
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
86
|
+
here = Path(dirpath)
|
|
87
|
+
dirnames[:] = [d for d in dirnames if not _skipped(d) and not (here / d).is_symlink()]
|
|
88
|
+
for name in filenames:
|
|
89
|
+
tree.append((here / name).relative_to(root).as_posix())
|
|
90
|
+
return sorted(tree)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def root_texts(root: Path) -> dict[str, str]:
|
|
94
|
+
"""The text of each file directly in `root` that is not hidden, small enough, and UTF-8."""
|
|
95
|
+
texts = {}
|
|
96
|
+
for path in sorted(root.iterdir()):
|
|
97
|
+
if path.name.startswith(".") or path.is_symlink() or not path.is_file():
|
|
98
|
+
continue
|
|
99
|
+
if path.stat().st_size > MAX_ROOT_FILE_BYTES:
|
|
100
|
+
continue
|
|
101
|
+
try:
|
|
102
|
+
texts[path.name] = path.read_bytes().decode("utf-8-sig")
|
|
103
|
+
except UnicodeDecodeError:
|
|
104
|
+
continue
|
|
105
|
+
return texts
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def workspaces_payload(source_root: str) -> dict:
|
|
109
|
+
root = _source_root(source_root)
|
|
110
|
+
return {"tree": list_tree(root), "manifests": root_texts(root)}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# --------------------------------------------------------------------------
|
|
114
|
+
# CLI
|
|
115
|
+
# --------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _cmd_detect_workspaces(args: argparse.Namespace) -> dict:
|
|
119
|
+
return workspaces_payload(args.source_root)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
123
|
+
parser = argparse.ArgumentParser(
|
|
124
|
+
prog="stage-helper-payload",
|
|
125
|
+
description="Print a shared helper's stdin payload, read from the files on disk.",
|
|
126
|
+
)
|
|
127
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
128
|
+
|
|
129
|
+
p = sub.add_parser("detect-workspaces", help="the tree and root files for skf-detect-workspaces.py")
|
|
130
|
+
p.add_argument("--source-root", required=True, help="the local source folder")
|
|
131
|
+
p.set_defaults(func=_cmd_detect_workspaces)
|
|
132
|
+
|
|
133
|
+
return parser
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main(argv: list[str] | None = None) -> int:
|
|
137
|
+
args = _build_parser().parse_args(argv)
|
|
138
|
+
try:
|
|
139
|
+
payload = args.func(args)
|
|
140
|
+
except (StageError, OSError) as exc:
|
|
141
|
+
print(json.dumps({"status": "error", "error": str(exc)}), file=sys.stderr)
|
|
142
|
+
return 1
|
|
143
|
+
print(json.dumps(payload))
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
sys.exit(main())
|