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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# /// script
|
|
3
|
-
# requires-python = ">=3.
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
4
|
# ///
|
|
5
5
|
"""Deterministic Documented-vs-Source coverage reconciliation.
|
|
6
6
|
|
|
@@ -12,28 +12,35 @@ already-extracted name lists (the §1 documented inventory and the §2 source
|
|
|
12
12
|
barrel) into a reproducible {documented, missing, stale, exportCoverage}
|
|
13
13
|
result, so the numerator no longer swings between runs on split-body skills.
|
|
14
14
|
|
|
15
|
-
It covers the
|
|
15
|
+
It covers the four branches §2c enumerates:
|
|
16
16
|
|
|
17
17
|
* "barrel" (enumerated path) — intersect documented_set against the source
|
|
18
18
|
barrel_set: Documented = |documented_set ∩ barrel_set|,
|
|
19
19
|
Missing = barrel_set − documented_set, Stale = documented_set − barrel_set,
|
|
20
20
|
Export Coverage = |Documented| / |barrel_set| * 100.
|
|
21
21
|
|
|
22
|
-
* "scalar" (§
|
|
23
|
-
|
|
22
|
+
* "scalar" (§2b's scalar branch, stats.effective_denominator, no enumerated name set):
|
|
23
|
+
look each documented name up in SKILL.md ∪ references/*.md;
|
|
24
24
|
Documented = count present, denominator = effective_denominator,
|
|
25
25
|
Missing = max(0, denominator − Documented), Stale not enumerable (empty),
|
|
26
26
|
Export Coverage = min(100, Documented / denominator * 100).
|
|
27
27
|
|
|
28
|
-
* "stack" (skill_type == "stack", empty source barrel)
|
|
28
|
+
* "stack" (skill_type == "stack", empty source barrel): look each
|
|
29
29
|
composition-surface name (provenance-map cited contracts, ::-excluded, or
|
|
30
|
-
libraries + integration_pairs
|
|
30
|
+
libraries + integration_pairs, from load-coverage-inputs.py) up in
|
|
31
31
|
SKILL.md ∪ references/*.md; denominator = stack_denominator,
|
|
32
32
|
Missing = max(0, denominator − Documented), Stale not enumerable,
|
|
33
33
|
Export Coverage = min(100, Documented / stack_denominator * 100).
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
* "docsOnly" (a skill whose citations are all [EXT:...], or no source
|
|
36
|
+
access): no source to compare against, so Export Coverage measures
|
|
37
|
+
documentation completeness: complete items / documented items * 100,
|
|
38
|
+
one item per name and kind of the §1 inventory, complete when it has a
|
|
39
|
+
description and, for a function or method, params and a return type
|
|
40
|
+
(validate-inventory.py's rule).
|
|
41
|
+
|
|
42
|
+
The two lookup branches bound their outputs because the numerator and the
|
|
43
|
+
denominator are independent measures of different sets: the lookup count can
|
|
37
44
|
exceed a consumer-surface denominator without either being wrong. Unbounded,
|
|
38
45
|
that produced a negative Missing and a >100% coverage that compute-score.py
|
|
39
46
|
rejects as out of range. `documented` is left as the true count (it is reported
|
|
@@ -41,54 +48,139 @@ verbatim as "Documented in SKILL.md"); the derived ratio is what gets bounded,
|
|
|
41
48
|
and `numeratorSurplus` / `coverageUncapped` / `coverageCapped` report the
|
|
42
49
|
overshoot so a deflated denominator stays visible instead of being swallowed.
|
|
43
50
|
|
|
51
|
+
A name counts as written when SKILL.md or a .md file directly in references/
|
|
52
|
+
holds it as a fixed string, case-sensitive, never inside a longer identifier
|
|
53
|
+
(`get` is not found in `target` or `getAll`): the rule validate-inventory.py
|
|
54
|
+
defines, which this script loads from beside it.
|
|
55
|
+
|
|
44
56
|
CLI usage:
|
|
57
|
+
uv run reconcile-coverage.py --denominator-source <source> [file flags] --output <file>
|
|
45
58
|
uv run reconcile-coverage.py '<JSON>' # JSON literal positional
|
|
46
59
|
uv run reconcile-coverage.py --json-input '<JSON>' # explicit flag form
|
|
47
60
|
cat input.json | uv run reconcile-coverage.py --stdin # piped input
|
|
48
61
|
|
|
62
|
+
File flags read the inputs from the run folder instead of a JSON payload;
|
|
63
|
+
each sets the input field it names and wins over the same field in the JSON:
|
|
64
|
+
--denominator-source denominatorSource
|
|
65
|
+
--denominator-value denominatorValue (scalar), when no --coverage-inputs
|
|
66
|
+
file gives it
|
|
67
|
+
--skill-dir skillPackagePath (scalar, stack)
|
|
68
|
+
--inventory PATH exports: the validated inventory validate-inventory.py
|
|
69
|
+
wrote with --output
|
|
70
|
+
--surface PATH barrelSet: one name set of load-coverage-inputs.py
|
|
71
|
+
surface, chosen by --surface-set (default "all")
|
|
72
|
+
--coverage-inputs PATH load-coverage-inputs.py metadata output: for the
|
|
73
|
+
scalar branch, denominatorValue from its
|
|
74
|
+
`effectiveDenominator` (stats.effective_denominator);
|
|
75
|
+
for the stack branch, compositionNames and
|
|
76
|
+
denominatorValue from its `stack` object
|
|
77
|
+
--verified PATH scalar: verify-declared-numerator.py's result; when it
|
|
78
|
+
found the declared count inflated, its `verified`
|
|
79
|
+
count is the numerator
|
|
80
|
+
--output PATH also write the result to this file (UTF-8 JSON); a
|
|
81
|
+
refused input removes a file left there earlier
|
|
82
|
+
|
|
49
83
|
Input schema (one object):
|
|
50
84
|
{
|
|
51
|
-
"denominatorSource": "barrel" | "scalar" | "stack",
|
|
85
|
+
"denominatorSource": "barrel" | "scalar" | "stack" | "docsOnly", # required
|
|
52
86
|
"exports": [ {"name": "...", "kind": "..."}, ... ], # §1 inventory
|
|
53
87
|
"barrelSet": ["a", "b", ...], # barrel: resolved name set
|
|
54
88
|
"perFileResults": [ {"exports_found": ["a", ...]} ], # barrel: union'd if no barrelSet
|
|
55
89
|
"denominatorValue": <int>, # scalar/stack: resolved denominator
|
|
56
|
-
"compositionNames": ["lib::x", ...], # stack: names to
|
|
57
|
-
"skillPackagePath": "/path/to/skill"
|
|
90
|
+
"compositionNames": ["lib::x", ...], # stack: names to look up
|
|
91
|
+
"skillPackagePath": "/path/to/skill", # scalar/stack: SKILL.md ∪ references
|
|
92
|
+
"verifiedNumerator": <int> # scalar: overrides the lookup count
|
|
58
93
|
}
|
|
59
94
|
|
|
60
95
|
Output (stdout, one object):
|
|
61
96
|
{
|
|
62
|
-
"branch": "enumerated" | "scalar" | "stack",
|
|
63
|
-
"denominatorSource": "barrel" | "scalar" | "stack",
|
|
97
|
+
"branch": "enumerated" | "scalar" | "stack" | "docsOnly",
|
|
98
|
+
"denominatorSource": "barrel" | "scalar" | "stack" | "docsOnly",
|
|
64
99
|
"denominator": <int>,
|
|
65
100
|
"documented": <int>,
|
|
66
|
-
"
|
|
101
|
+
"documentedNames": [names], # enumerated only: the documented names the denominator counts
|
|
102
|
+
"missing": [names], # enumerated: source names not documented; others: []
|
|
67
103
|
"missingCount": <int>, # always present; never negative
|
|
68
|
-
"stale": [names], # enumerated: documented names not in source;
|
|
104
|
+
"stale": [names], # enumerated: documented names not in source; others: []
|
|
69
105
|
"staleCount": <int>, # always present
|
|
70
|
-
"staleApplicable": <bool>, # false for scalar/stack (no barrel to enumerate)
|
|
106
|
+
"staleApplicable": <bool>, # false for scalar/stack/docsOnly (no barrel to enumerate)
|
|
71
107
|
"exportCoverage": <float>, # scalar/stack: capped at 100
|
|
72
108
|
"numeratorSurplus": <int>, # scalar/stack only: max(0, documented − denominator)
|
|
73
109
|
"coverageUncapped": <float>, # scalar/stack only: the ratio before the cap
|
|
74
|
-
"coverageCapped": <bool
|
|
110
|
+
"coverageCapped": <bool>, # scalar/stack only: true when the cap bound the ratio
|
|
111
|
+
"numeratorSource": "lookup" | "verified", # scalar only
|
|
112
|
+
"incomplete": [{"name", "kind", "missing"}] # docsOnly only: the incomplete items
|
|
75
113
|
}
|
|
76
114
|
|
|
77
115
|
Exit codes:
|
|
78
|
-
0
|
|
79
|
-
1
|
|
80
|
-
2
|
|
116
|
+
0 reconciliation emitted successfully
|
|
117
|
+
1 no input, input that does not parse as JSON, or an input file that cannot be read
|
|
118
|
+
2 input parsed but schema/semantics invalid (error object emitted as JSON)
|
|
119
|
+
|
|
120
|
+
Subcommand render (coverage-check.md §5 writes its output unchanged):
|
|
121
|
+
uv run reconcile-coverage.py render --coverage <coverage.json>
|
|
122
|
+
[--surface <surface.json>] [--signatures <signatures.json>]
|
|
123
|
+
|
|
124
|
+
prints the Markdown of the Coverage Analysis section that follows its
|
|
125
|
+
header lines, from the run files:
|
|
126
|
+
**Files Analyzed:** the distinct files the --surface export records name
|
|
127
|
+
(N/A without --surface: a docs-only run has none)
|
|
128
|
+
### Export Coverage on the enumerated branch, one row per name the
|
|
129
|
+
denominator counts, per stale name and per name whose
|
|
130
|
+
signature mismatches, by name:
|
|
131
|
+
Type the first kind the surface records for the name, else -
|
|
132
|
+
Documented no for a missing name, else yes
|
|
133
|
+
Signature no for a mismatch, yes for a compared name that
|
|
134
|
+
matches, unverified for a documented name when no
|
|
135
|
+
--signatures was given, - otherwise
|
|
136
|
+
Source Location the file:line the surface records for the name, a
|
|
137
|
+
record with a line preferred (gap-ledger.py's rule
|
|
138
|
+
for a missing export's Source), else -
|
|
139
|
+
Status FAIL for a signature mismatch, WARN for a missing or
|
|
140
|
+
stale name, PASS otherwise
|
|
141
|
+
The scalar and stack branches count without naming
|
|
142
|
+
the exports, and a docs-only run has no source
|
|
143
|
+
surface: one line says so instead (NO_TABLE).
|
|
144
|
+
### Coverage Summary the --coverage counts, the --signatures mismatch count
|
|
145
|
+
and warnings, the numerator surplus when the ratio
|
|
146
|
+
was capped, and the names the --surface leaves out
|
|
147
|
+
or puts back: `excluded.outsideScope` (scoped out
|
|
148
|
+
by the brief), `excluded.nestedEntries` (a nested
|
|
149
|
+
Python entry point's names) and
|
|
150
|
+
`guards.staleScope.restored`, each line only when
|
|
151
|
+
it names one
|
|
152
|
+
### Category Scores Export Coverage, and Signature Accuracy and Type
|
|
153
|
+
Coverage from --signatures (N/A without it)
|
|
154
|
+
Exit 0 when it printed; 1 when an input cannot be read or is not the
|
|
155
|
+
result of the script that writes it (one line on stderr); 2 a usage error.
|
|
81
156
|
"""
|
|
82
157
|
|
|
83
158
|
from __future__ import annotations
|
|
84
159
|
|
|
85
160
|
import argparse
|
|
161
|
+
import importlib.util
|
|
86
162
|
import json
|
|
87
163
|
import math
|
|
88
164
|
import sys
|
|
89
165
|
from pathlib import Path
|
|
90
166
|
|
|
91
|
-
VALID_SOURCES = ("barrel", "scalar", "stack")
|
|
167
|
+
VALID_SOURCES = ("barrel", "scalar", "stack", "docsOnly")
|
|
168
|
+
|
|
169
|
+
_INVENTORY_MODULE = None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _inventory():
|
|
173
|
+
"""validate-inventory.py beside this script: the name match and completeness rules live there."""
|
|
174
|
+
global _INVENTORY_MODULE
|
|
175
|
+
if _INVENTORY_MODULE is None:
|
|
176
|
+
sibling = Path(__file__).resolve().parent / "validate-inventory.py"
|
|
177
|
+
spec = importlib.util.spec_from_file_location("skf_validate_inventory", sibling)
|
|
178
|
+
if spec is None or spec.loader is None or not sibling.is_file():
|
|
179
|
+
raise ImportError(f"validate-inventory.py not found beside {Path(__file__).name}")
|
|
180
|
+
module = importlib.util.module_from_spec(spec)
|
|
181
|
+
spec.loader.exec_module(module)
|
|
182
|
+
_INVENTORY_MODULE = module
|
|
183
|
+
return _INVENTORY_MODULE
|
|
92
184
|
|
|
93
185
|
|
|
94
186
|
def round2(value):
|
|
@@ -135,34 +227,17 @@ def build_barrel_set(inp):
|
|
|
135
227
|
return barrel
|
|
136
228
|
|
|
137
229
|
|
|
138
|
-
# ---
|
|
230
|
+
# --- Documented-name lookup ---------------------------------------------------
|
|
139
231
|
|
|
140
232
|
|
|
141
233
|
def load_doc_text(skill_package_path):
|
|
142
|
-
"""
|
|
143
|
-
|
|
144
|
-
Deterministic: references are read in sorted order. Reads as UTF-8 so
|
|
145
|
-
non-ASCII exports do not mojibake on Windows (cp1252 default).
|
|
146
|
-
"""
|
|
147
|
-
root = Path(skill_package_path)
|
|
148
|
-
parts = []
|
|
149
|
-
skill_md = root / "SKILL.md"
|
|
150
|
-
if skill_md.is_file():
|
|
151
|
-
parts.append(skill_md.read_text(encoding="utf-8"))
|
|
152
|
-
refs_dir = root / "references"
|
|
153
|
-
if refs_dir.is_dir():
|
|
154
|
-
for ref in sorted(refs_dir.glob("*.md")):
|
|
155
|
-
parts.append(ref.read_text(encoding="utf-8"))
|
|
156
|
-
return "\n".join(parts)
|
|
234
|
+
"""SKILL.md ∪ references/*.md text, as validate-inventory.py reads it."""
|
|
235
|
+
return _inventory().load_doc_text(skill_package_path)
|
|
157
236
|
|
|
158
237
|
|
|
159
238
|
def names_present(names, doc_text):
|
|
160
|
-
"""
|
|
161
|
-
|
|
162
|
-
Substring containment, case-sensitive — mirrors `grep "{name}"`.
|
|
163
|
-
"""
|
|
164
|
-
present = {n for n in names if isinstance(n, str) and n and n in doc_text}
|
|
165
|
-
return sorted(present)
|
|
239
|
+
"""The sorted, de-duplicated `names` the doc text writes (validate-inventory.py's match)."""
|
|
240
|
+
return _inventory().names_present(names, doc_text)
|
|
166
241
|
|
|
167
242
|
|
|
168
243
|
# --- Core reconciliation ----------------------------------------------------
|
|
@@ -180,17 +255,46 @@ def _validate(inp):
|
|
|
180
255
|
if source == "barrel":
|
|
181
256
|
if inp.get("barrelSet") is None and inp.get("perFileResults") is None:
|
|
182
257
|
return "barrel branch requires either `barrelSet` or `perFileResults`"
|
|
258
|
+
elif source == "docsOnly":
|
|
259
|
+
if not isinstance(inp.get("exports"), list):
|
|
260
|
+
return "docsOnly branch requires the §1 inventory `exports` (or --inventory)"
|
|
183
261
|
else: # scalar / stack
|
|
184
262
|
dv = inp.get("denominatorValue")
|
|
185
263
|
if not isinstance(dv, int) or isinstance(dv, bool) or dv < 0:
|
|
186
264
|
return f"{source} branch requires integer `denominatorValue` >= 0"
|
|
187
265
|
if not inp.get("skillPackagePath"):
|
|
188
|
-
return f"{source} branch requires `skillPackagePath` to
|
|
266
|
+
return f"{source} branch requires `skillPackagePath` to read SKILL.md ∪ references"
|
|
189
267
|
if source == "stack" and inp.get("compositionNames") is None:
|
|
190
|
-
return "stack branch requires `compositionNames` (composition-surface names to
|
|
268
|
+
return "stack branch requires `compositionNames` (composition-surface names to look up)"
|
|
269
|
+
verified = inp.get("verifiedNumerator")
|
|
270
|
+
if verified is not None and (not isinstance(verified, int) or isinstance(verified, bool) or verified < 0):
|
|
271
|
+
return "verifiedNumerator must be an integer >= 0"
|
|
191
272
|
return None
|
|
192
273
|
|
|
193
274
|
|
|
275
|
+
def _docs_only(inp):
|
|
276
|
+
tally = _inventory().completeness(inp["exports"])
|
|
277
|
+
total = tally["total"]
|
|
278
|
+
if total == 0:
|
|
279
|
+
return make_error(
|
|
280
|
+
"docsOnly branch: the inventory documents no item, so Export Coverage is "
|
|
281
|
+
"undefined; the §2b docs-only guard should HALT"
|
|
282
|
+
)
|
|
283
|
+
return {
|
|
284
|
+
"branch": "docsOnly",
|
|
285
|
+
"denominatorSource": "docsOnly",
|
|
286
|
+
"denominator": total,
|
|
287
|
+
"documented": tally["complete"],
|
|
288
|
+
"missing": [],
|
|
289
|
+
"missingCount": total - tally["complete"],
|
|
290
|
+
"stale": [],
|
|
291
|
+
"staleCount": 0,
|
|
292
|
+
"staleApplicable": False,
|
|
293
|
+
"exportCoverage": round2(tally["complete"] / total * 100),
|
|
294
|
+
"incomplete": tally["incomplete"],
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
194
298
|
def reconcile(inp, doc_text=None):
|
|
195
299
|
"""Pure reconciliation. `doc_text` may be injected (tests); else loaded from disk."""
|
|
196
300
|
err = _validate(inp)
|
|
@@ -199,6 +303,9 @@ def reconcile(inp, doc_text=None):
|
|
|
199
303
|
|
|
200
304
|
source = inp["denominatorSource"]
|
|
201
305
|
|
|
306
|
+
if source == "docsOnly":
|
|
307
|
+
return _docs_only(inp)
|
|
308
|
+
|
|
202
309
|
if source == "barrel":
|
|
203
310
|
documented_set = build_documented_set(inp.get("exports"))
|
|
204
311
|
barrel_set = build_barrel_set(inp)
|
|
@@ -217,6 +324,7 @@ def reconcile(inp, doc_text=None):
|
|
|
217
324
|
"denominatorSource": source,
|
|
218
325
|
"denominator": denominator,
|
|
219
326
|
"documented": documented,
|
|
327
|
+
"documentedNames": sorted(documented_names),
|
|
220
328
|
"missing": missing,
|
|
221
329
|
"missingCount": len(missing),
|
|
222
330
|
"stale": stale,
|
|
@@ -225,39 +333,43 @@ def reconcile(inp, doc_text=None):
|
|
|
225
333
|
"exportCoverage": round2(documented / denominator * 100),
|
|
226
334
|
}
|
|
227
335
|
|
|
228
|
-
# scalar / stack
|
|
336
|
+
# scalar / stack: looked-up numerator against SKILL.md ∪ references
|
|
229
337
|
denominator = inp["denominatorValue"]
|
|
230
|
-
if doc_text is None:
|
|
231
|
-
doc_text = load_doc_text(inp["skillPackagePath"])
|
|
232
|
-
|
|
233
|
-
if source == "scalar":
|
|
234
|
-
candidates = sorted(build_documented_set(inp.get("exports")))
|
|
235
|
-
else: # stack
|
|
236
|
-
candidates = [n for n in inp["compositionNames"] if isinstance(n, str) and n]
|
|
237
|
-
|
|
238
|
-
present = names_present(candidates, doc_text)
|
|
239
|
-
documented = len(present)
|
|
240
|
-
|
|
241
338
|
if denominator == 0:
|
|
242
339
|
return make_error(
|
|
243
340
|
f"{source} branch: denominatorValue is 0 — Export Coverage is undefined; "
|
|
244
341
|
"upstream §2b guard should HALT before reconciliation"
|
|
245
342
|
)
|
|
246
343
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
344
|
+
numerator_source = "lookup"
|
|
345
|
+
if source == "scalar" and inp.get("verifiedNumerator") is not None:
|
|
346
|
+
# §2b's numerator ground truth found the declared count padded: its
|
|
347
|
+
# verified count is authoritative and replaces the lookup.
|
|
348
|
+
documented = inp["verifiedNumerator"]
|
|
349
|
+
numerator_source = "verified"
|
|
350
|
+
else:
|
|
351
|
+
if doc_text is None:
|
|
352
|
+
doc_text = load_doc_text(inp["skillPackagePath"])
|
|
353
|
+
if source == "scalar":
|
|
354
|
+
candidates = sorted(build_documented_set(inp.get("exports")))
|
|
355
|
+
else: # stack
|
|
356
|
+
candidates = [n for n in inp["compositionNames"] if isinstance(n, str) and n]
|
|
357
|
+
documented = len(names_present(candidates, doc_text))
|
|
358
|
+
|
|
359
|
+
# The looked-up numerator and the resolved denominator are independent
|
|
360
|
+
# measures, so `documented` can legitimately exceed `denominator`: a
|
|
361
|
+
# consumer-surface denominator counts one surface while the documented body
|
|
362
|
+
# may also name migration aliases, re-exported sibling symbols, and other
|
|
363
|
+
# extras. Left unbounded that yields a negative Missing and >100% coverage
|
|
364
|
+
# (which compute-score.py then rejects outright as out of range).
|
|
253
365
|
#
|
|
254
|
-
# `documented` stays the true
|
|
366
|
+
# `documented` stays the true count: it is reported as "Documented in
|
|
255
367
|
# SKILL.md", so capping it would print a number that was never measured.
|
|
256
368
|
# The derived ratio is what gets bounded, and the surplus is surfaced rather
|
|
257
369
|
# than swallowed so a deflated denominator stays visible.
|
|
258
370
|
surplus = max(0, documented - denominator)
|
|
259
371
|
coverage_uncapped = round2(documented / denominator * 100)
|
|
260
|
-
|
|
372
|
+
result = {
|
|
261
373
|
"branch": source,
|
|
262
374
|
"denominatorSource": source,
|
|
263
375
|
"denominator": denominator,
|
|
@@ -272,6 +384,302 @@ def reconcile(inp, doc_text=None):
|
|
|
272
384
|
"coverageUncapped": coverage_uncapped,
|
|
273
385
|
"coverageCapped": surplus > 0,
|
|
274
386
|
}
|
|
387
|
+
if source == "scalar":
|
|
388
|
+
result["numeratorSource"] = numerator_source
|
|
389
|
+
return result
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# --- The Coverage Analysis section (render) -----------------------------------
|
|
393
|
+
|
|
394
|
+
BRANCHES = ("enumerated", "scalar", "stack", "docsOnly")
|
|
395
|
+
# Why a branch names no export: the line it writes in place of the table, and its stale count.
|
|
396
|
+
NO_NAMES = {
|
|
397
|
+
"scalar": "the scalar denominator (`stats.effective_denominator`) counts the exports without naming them",
|
|
398
|
+
"stack": "the stack denominator counts the composition surface without naming each export",
|
|
399
|
+
"docsOnly": "a docs-only run has no source surface",
|
|
400
|
+
}
|
|
401
|
+
NO_TABLE = {
|
|
402
|
+
"scalar": f"No per-export table: {NO_NAMES['scalar']}. The summary below gives the counts.",
|
|
403
|
+
"stack": f"No per-export table: {NO_NAMES['stack']}. The summary below gives the counts.",
|
|
404
|
+
"docsOnly": f"No per-export table: {NO_NAMES['docsOnly']} to compare against. The summary below gives its "
|
|
405
|
+
"documentation completeness.",
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
_GAP_LEDGER_MODULE = None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class RenderError(Exception):
|
|
412
|
+
"""A render input that is not the result of the script that writes it."""
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _gap_ledger():
|
|
416
|
+
"""gap-ledger.py beside this script: its rule for a missing export's Source gives the Source Location."""
|
|
417
|
+
global _GAP_LEDGER_MODULE
|
|
418
|
+
if _GAP_LEDGER_MODULE is None:
|
|
419
|
+
sibling = Path(__file__).resolve().parent / "gap-ledger.py"
|
|
420
|
+
spec = importlib.util.spec_from_file_location("skf_gap_ledger_render", sibling)
|
|
421
|
+
if spec is None or spec.loader is None or not sibling.is_file():
|
|
422
|
+
raise RenderError(f"gap-ledger.py not found beside {Path(__file__).name}")
|
|
423
|
+
module = importlib.util.module_from_spec(spec)
|
|
424
|
+
spec.loader.exec_module(module)
|
|
425
|
+
_GAP_LEDGER_MODULE = module
|
|
426
|
+
return _GAP_LEDGER_MODULE
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _pct(value):
|
|
430
|
+
"""A score as the report writes it.
|
|
431
|
+
|
|
432
|
+
>>> _pct(60.0), _pct(66.67), _pct(None)
|
|
433
|
+
('60%', '66.67%', 'N/A')
|
|
434
|
+
"""
|
|
435
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
436
|
+
return "N/A"
|
|
437
|
+
return f"{int(value)}%" if float(value).is_integer() else f"{value}%"
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _cell(text):
|
|
441
|
+
"""A table cell: a `|` would end it."""
|
|
442
|
+
return str(text).replace("|", "\\|")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _string_list(data, key, label):
|
|
446
|
+
value = data.get(key)
|
|
447
|
+
if value is None:
|
|
448
|
+
return []
|
|
449
|
+
if not isinstance(value, list):
|
|
450
|
+
raise RenderError(f"{label}: `{key}` is not a list")
|
|
451
|
+
return [v for v in value if isinstance(v, str) and v]
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _incomplete(item):
|
|
455
|
+
"""A docs-only item and the fields it lacks.
|
|
456
|
+
|
|
457
|
+
>>> _incomplete({"name": "load", "kind": "function", "missing": ["params", "return_type"]})
|
|
458
|
+
'`load` (function) lacks `params`, `return_type`'
|
|
459
|
+
"""
|
|
460
|
+
fields = ", ".join(f"`{field}`" for field in item.get("missing") or [])
|
|
461
|
+
return f"`{item.get('name')}` ({item.get('kind')}) lacks {fields}"
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _left_out(surface):
|
|
465
|
+
"""The Coverage Summary lines for the names the surface leaves out or puts back, each only when it names one.
|
|
466
|
+
|
|
467
|
+
>>> for line in _left_out({
|
|
468
|
+
... "excluded": {"outsideScope": [{"name": "gen", "file": "src/gen.ts"}],
|
|
469
|
+
... "nestedEntries": [{"entry": "pkg/api/__init__.py", "within": "pkg/__init__.py",
|
|
470
|
+
... "names": ["router"]}]},
|
|
471
|
+
... "guards": {"staleScope": {"restored": [{"name": "old", "file": "src/old.ts", "line": 4}]}}}):
|
|
472
|
+
... print(line)
|
|
473
|
+
- **Scoped Out by the Brief:** `gen` (src/gen.ts)
|
|
474
|
+
- **Nested Entry Points:** `pkg/api/__init__.py` inside `pkg/__init__.py`: `router`
|
|
475
|
+
- **Restored by the Stale-Scope Guard:** `old` (src/old.ts:4)
|
|
476
|
+
>>> _left_out({"excluded": {"outsideScope": [], "nestedEntries": []}, "guards": None})
|
|
477
|
+
[]
|
|
478
|
+
"""
|
|
479
|
+
def items(value):
|
|
480
|
+
return [i for i in value if isinstance(i, dict)] if isinstance(value, list) else []
|
|
481
|
+
|
|
482
|
+
def where(item):
|
|
483
|
+
file, line = item.get("file"), item.get("line")
|
|
484
|
+
if not isinstance(file, str) or not file:
|
|
485
|
+
return ""
|
|
486
|
+
return f" ({file}:{line})" if isinstance(line, int) and not isinstance(line, bool) else f" ({file})"
|
|
487
|
+
|
|
488
|
+
excluded = surface.get("excluded") if isinstance(surface.get("excluded"), dict) else {}
|
|
489
|
+
guards = surface.get("guards") if isinstance(surface.get("guards"), dict) else {}
|
|
490
|
+
stale_scope = guards.get("staleScope") if isinstance(guards.get("staleScope"), dict) else {}
|
|
491
|
+
lines = []
|
|
492
|
+
scoped = [f"`{i['name']}`{where(i)}" for i in items(excluded.get("outsideScope"))
|
|
493
|
+
if isinstance(i.get("name"), str)]
|
|
494
|
+
if scoped:
|
|
495
|
+
lines.append(f"- **Scoped Out by the Brief:** {', '.join(scoped)}")
|
|
496
|
+
nested = [f"`{i['entry']}` inside `{i.get('within')}`: "
|
|
497
|
+
+ ", ".join(f"`{n}`" for n in i.get("names") or [] if isinstance(n, str))
|
|
498
|
+
for i in items(excluded.get("nestedEntries")) if isinstance(i.get("entry"), str)]
|
|
499
|
+
if nested:
|
|
500
|
+
lines.append(f"- **Nested Entry Points:** {'; '.join(nested)}")
|
|
501
|
+
restored = [f"`{i['name']}`{where(i)}" for i in items(stale_scope.get("restored"))
|
|
502
|
+
if isinstance(i.get("name"), str)]
|
|
503
|
+
if restored:
|
|
504
|
+
lines.append(f"- **Restored by the Stale-Scope Guard:** {', '.join(restored)}")
|
|
505
|
+
return lines
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _rows(coverage, surface, signatures):
|
|
509
|
+
"""The Export Coverage table of the enumerated branch."""
|
|
510
|
+
if "documentedNames" not in coverage:
|
|
511
|
+
raise RenderError("--coverage has no `documentedNames`: write it again with this reconcile-coverage.py")
|
|
512
|
+
documented = set(_string_list(coverage, "documentedNames", "--coverage"))
|
|
513
|
+
missing = set(_string_list(coverage, "missing", "--coverage"))
|
|
514
|
+
stale = set(_string_list(coverage, "stale", "--coverage"))
|
|
515
|
+
mismatched, compared = set(), set()
|
|
516
|
+
if signatures is not None:
|
|
517
|
+
mismatched = {m["name"] for m in signatures.get("mismatches") or []
|
|
518
|
+
if isinstance(m, dict) and isinstance(m.get("name"), str)}
|
|
519
|
+
compared = set(_string_list(signatures, "comparedNames", "--signatures"))
|
|
520
|
+
kinds, sources = {}, {}
|
|
521
|
+
if surface is not None:
|
|
522
|
+
for record in surface["exports"]:
|
|
523
|
+
if isinstance(record, dict) and isinstance(record.get("name"), str) and record.get("kind"):
|
|
524
|
+
kinds.setdefault(record["name"], str(record["kind"]))
|
|
525
|
+
sources = _gap_ledger()._surface_sources(surface)
|
|
526
|
+
rows = ["| Export | Type | Documented | Signature | Source Location | Status |",
|
|
527
|
+
"|--------|------|------------|-----------|-----------------|--------|"]
|
|
528
|
+
for name in sorted(documented | missing | stale | mismatched):
|
|
529
|
+
if name in mismatched:
|
|
530
|
+
signature = "no"
|
|
531
|
+
elif name in compared:
|
|
532
|
+
signature = "yes"
|
|
533
|
+
elif signatures is None and name not in missing:
|
|
534
|
+
signature = "unverified"
|
|
535
|
+
else:
|
|
536
|
+
signature = "-"
|
|
537
|
+
status = "FAIL" if name in mismatched else "WARN" if name in missing or name in stale else "PASS"
|
|
538
|
+
rows.append(f"| `{_cell(name)}` | {_cell(kinds.get(name, '-'))} | {'no' if name in missing else 'yes'} "
|
|
539
|
+
f"| {signature} | {_cell(sources.get(name, '-'))} | {status} |")
|
|
540
|
+
return rows
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def render(coverage, surface=None, signatures=None):
|
|
544
|
+
"""The Coverage Analysis section after its header lines, as Markdown (see the module docstring)."""
|
|
545
|
+
if not isinstance(coverage, dict) or coverage.get("branch") not in BRANCHES:
|
|
546
|
+
raise RenderError("--coverage is not reconcile-coverage.py's result (no known `branch`)")
|
|
547
|
+
if surface is not None and not (isinstance(surface, dict) and isinstance(surface.get("exports"), list)):
|
|
548
|
+
raise RenderError("--surface is not load-coverage-inputs.py surface's result (no `exports` list)")
|
|
549
|
+
if signatures is not None and not (isinstance(signatures, dict) and signatures.get("valid") is True):
|
|
550
|
+
raise RenderError("--signatures is not score-signatures.py score's result (no `valid: true`)")
|
|
551
|
+
branch = coverage["branch"]
|
|
552
|
+
docs_only = branch == "docsOnly"
|
|
553
|
+
|
|
554
|
+
if surface is None:
|
|
555
|
+
files = "N/A: no source surface"
|
|
556
|
+
else:
|
|
557
|
+
files = len({r["file"] for r in surface["exports"]
|
|
558
|
+
if isinstance(r, dict) and isinstance(r.get("file"), str) and r["file"]})
|
|
559
|
+
lines = [f"**Files Analyzed:** {files}", "", "### Export Coverage", ""]
|
|
560
|
+
lines += _rows(coverage, surface, signatures) if branch == "enumerated" else [NO_TABLE[branch]]
|
|
561
|
+
|
|
562
|
+
denominator, documented = coverage.get("denominator"), coverage.get("documented")
|
|
563
|
+
missing_count, coverage_pct = coverage.get("missingCount"), _pct(coverage.get("exportCoverage"))
|
|
564
|
+
lines += ["", "### Coverage Summary", ""]
|
|
565
|
+
if docs_only:
|
|
566
|
+
incomplete = [_incomplete(item) for item in coverage.get("incomplete") or [] if isinstance(item, dict)]
|
|
567
|
+
lines += [f"- **Exports Found:** {denominator} documented items",
|
|
568
|
+
f"- **Documented:** {documented} complete ({coverage_pct})",
|
|
569
|
+
f"- **Missing Documentation:** {missing_count} incomplete"
|
|
570
|
+
+ (f": {'; '.join(incomplete)}" if incomplete else "")]
|
|
571
|
+
else:
|
|
572
|
+
capped = coverage.get("coverageCapped")
|
|
573
|
+
floored = " (a floored residual: the numerator exceeds the denominator)" if capped else ""
|
|
574
|
+
lines += [f"- **Exports Found:** {denominator}",
|
|
575
|
+
f"- **Documented:** {documented} ({coverage_pct})",
|
|
576
|
+
f"- **Missing Documentation:** {missing_count}{floored}"]
|
|
577
|
+
mismatches = signatures.get("mismatches") if signatures is not None else None
|
|
578
|
+
lines.append(f"- **Signature Mismatches:** {len(mismatches) if isinstance(mismatches, list) else 'N/A'}")
|
|
579
|
+
if coverage.get("staleApplicable"):
|
|
580
|
+
lines.append(f"- **Stale Documentation:** {coverage.get('staleCount')}, documented extras included")
|
|
581
|
+
else:
|
|
582
|
+
lines.append(f"- **Stale Documentation:** N/A: {NO_NAMES.get(branch, 'the denominator names no stale export')}")
|
|
583
|
+
warnings = _string_list(signatures, "warnings", "--signatures") if signatures is not None else []
|
|
584
|
+
if warnings:
|
|
585
|
+
lines.append("- **Scoring Warnings:**")
|
|
586
|
+
lines += [f" - {w}" for w in warnings]
|
|
587
|
+
if coverage.get("coverageCapped"):
|
|
588
|
+
lines.append(f"- **Numerator Surplus:** {coverage.get('numeratorSurplus')}, uncapped coverage "
|
|
589
|
+
f"{_pct(coverage.get('coverageUncapped'))}")
|
|
590
|
+
if surface is not None:
|
|
591
|
+
lines += _left_out(surface)
|
|
592
|
+
|
|
593
|
+
scores = signatures or {}
|
|
594
|
+
lines += ["", "### Category Scores", "", "| Category | Score |", "|----------|-------|",
|
|
595
|
+
f"| Export Coverage | {coverage_pct} |",
|
|
596
|
+
f"| Signature Accuracy | {_pct(scores.get('signatureAccuracy'))} |",
|
|
597
|
+
f"| Type Coverage | {_pct(scores.get('typeCoverage'))} |"]
|
|
598
|
+
return "\n".join(lines) + "\n"
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _render_main(argv):
|
|
602
|
+
parser = argparse.ArgumentParser(
|
|
603
|
+
prog="reconcile-coverage render",
|
|
604
|
+
description="Print the Coverage Analysis section's Markdown from the run files (coverage-check.md §5).",
|
|
605
|
+
)
|
|
606
|
+
parser.add_argument("--coverage", required=True, metavar="PATH", help="this script's result (coverage.json)")
|
|
607
|
+
parser.add_argument("--surface", metavar="PATH", help="load-coverage-inputs.py surface output (surface.json)")
|
|
608
|
+
parser.add_argument("--signatures", metavar="PATH", help="score-signatures.py score output (signatures.json)")
|
|
609
|
+
args = parser.parse_args(argv)
|
|
610
|
+
try:
|
|
611
|
+
text = render(_read_json_file(args.coverage, "--coverage"),
|
|
612
|
+
_read_json_file(args.surface, "--surface") if args.surface else None,
|
|
613
|
+
_read_json_file(args.signatures, "--signatures") if args.signatures else None)
|
|
614
|
+
except (InputFileError, RenderError) as exc:
|
|
615
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
616
|
+
return 1
|
|
617
|
+
sys.stdout.write(text)
|
|
618
|
+
return 0
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
# --- Run-folder inputs --------------------------------------------------------
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
class InputFileError(Exception):
|
|
625
|
+
"""A file flag names a file that cannot be read or does not hold what it should."""
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _read_json_file(path, flag):
|
|
629
|
+
try:
|
|
630
|
+
return json.loads(Path(path).read_bytes().decode("utf-8-sig"))
|
|
631
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
632
|
+
raise InputFileError(f"cannot read {flag} {path}: {exc}") from exc
|
|
633
|
+
except json.JSONDecodeError as exc:
|
|
634
|
+
raise InputFileError(f"{flag} {path} is not JSON: {exc.msg}") from exc
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def apply_file_inputs(inp, args):
|
|
638
|
+
"""Fill the input object from the file flags; each flag wins over the JSON field."""
|
|
639
|
+
if args.denominator_source is not None:
|
|
640
|
+
inp["denominatorSource"] = args.denominator_source
|
|
641
|
+
if args.denominator_value is not None:
|
|
642
|
+
inp["denominatorValue"] = args.denominator_value
|
|
643
|
+
if args.skill_dir is not None:
|
|
644
|
+
inp["skillPackagePath"] = args.skill_dir
|
|
645
|
+
if args.inventory is not None:
|
|
646
|
+
data = _read_json_file(args.inventory, "--inventory")
|
|
647
|
+
if isinstance(data, dict) and isinstance(data.get("inventory"), dict):
|
|
648
|
+
data = data["inventory"] # validate-inventory.py's whole result
|
|
649
|
+
if not isinstance(data, dict) or not isinstance(data.get("exports"), list):
|
|
650
|
+
raise InputFileError(f"--inventory {args.inventory} holds no `exports` list")
|
|
651
|
+
inp["exports"] = data["exports"]
|
|
652
|
+
if args.surface is not None:
|
|
653
|
+
data = _read_json_file(args.surface, "--surface")
|
|
654
|
+
sets = data.get("sets") if isinstance(data, dict) else None
|
|
655
|
+
names = sets.get(args.surface_set) if isinstance(sets, dict) else None
|
|
656
|
+
if not isinstance(names, list):
|
|
657
|
+
raise InputFileError(f"--surface {args.surface} has no name set `{args.surface_set}`")
|
|
658
|
+
inp["barrelSet"] = names
|
|
659
|
+
if args.coverage_inputs is not None:
|
|
660
|
+
data = _read_json_file(args.coverage_inputs, "--coverage-inputs")
|
|
661
|
+
if not isinstance(data, dict):
|
|
662
|
+
raise InputFileError(f"--coverage-inputs {args.coverage_inputs} is not a JSON object")
|
|
663
|
+
if inp.get("denominatorSource") == "scalar":
|
|
664
|
+
if args.denominator_value is None:
|
|
665
|
+
if data.get("effectiveDenominator") is None:
|
|
666
|
+
raise InputFileError(f"--coverage-inputs {args.coverage_inputs} has no "
|
|
667
|
+
"`effectiveDenominator`: metadata.json has no stats.effective_denominator")
|
|
668
|
+
inp["denominatorValue"] = data["effectiveDenominator"]
|
|
669
|
+
else:
|
|
670
|
+
stack = data.get("stack")
|
|
671
|
+
if not isinstance(stack, dict):
|
|
672
|
+
raise InputFileError(f"--coverage-inputs {args.coverage_inputs} has no `stack` object")
|
|
673
|
+
inp["compositionNames"] = stack.get("compositionNames")
|
|
674
|
+
if args.denominator_value is None:
|
|
675
|
+
inp["denominatorValue"] = stack.get("denominator")
|
|
676
|
+
if args.verified is not None:
|
|
677
|
+
data = _read_json_file(args.verified, "--verified")
|
|
678
|
+
if not isinstance(data, dict):
|
|
679
|
+
raise InputFileError(f"--verified {args.verified} is not a verify-declared-numerator.py result")
|
|
680
|
+
if data.get("inflated") is True:
|
|
681
|
+
inp["verifiedNumerator"] = data.get("verified")
|
|
682
|
+
return inp
|
|
275
683
|
|
|
276
684
|
|
|
277
685
|
# --- CLI --------------------------------------------------------------------
|
|
@@ -283,17 +691,19 @@ def _build_parser():
|
|
|
283
691
|
description=(
|
|
284
692
|
"Deterministic Documented-vs-Source coverage reconciliation "
|
|
285
693
|
"(coverage-check.md §2c). Consumes the §1 documented inventory and "
|
|
286
|
-
"the §2 source
|
|
287
|
-
"skill package path
|
|
288
|
-
"exportCoverage} so the coverage
|
|
694
|
+
"the §2 source surface (or a resolved scalar/stack denominator + the "
|
|
695
|
+
"skill package path, or the inventory alone for a docs-only skill) and "
|
|
696
|
+
"emits {documented, missing, stale, exportCoverage} so the coverage "
|
|
697
|
+
"numerator is reproducible."
|
|
289
698
|
),
|
|
290
699
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
291
700
|
epilog=(
|
|
292
701
|
"Example (barrel branch):\n"
|
|
293
|
-
" uv run reconcile-coverage.py "
|
|
294
|
-
"
|
|
295
|
-
"
|
|
296
|
-
"
|
|
702
|
+
" uv run reconcile-coverage.py --denominator-source barrel "
|
|
703
|
+
"--inventory run/inventory.json --surface run/surface.json --output run/coverage.json\n"
|
|
704
|
+
"Render the Coverage Analysis section (see `render --help`):\n"
|
|
705
|
+
" uv run reconcile-coverage.py render --coverage run/coverage.json "
|
|
706
|
+
"[--surface run/surface.json] [--signatures run/signatures.json]"
|
|
297
707
|
),
|
|
298
708
|
)
|
|
299
709
|
src = parser.add_mutually_exclusive_group()
|
|
@@ -312,9 +722,27 @@ def _build_parser():
|
|
|
312
722
|
action="store_true",
|
|
313
723
|
help="Read the JSON object from stdin.",
|
|
314
724
|
)
|
|
725
|
+
files = parser.add_argument_group("inputs read from files (each wins over the same JSON field)")
|
|
726
|
+
files.add_argument("--denominator-source", choices=VALID_SOURCES, help="the §2c branch")
|
|
727
|
+
files.add_argument("--denominator-value", type=int, metavar="N",
|
|
728
|
+
help="the resolved scalar denominator")
|
|
729
|
+
files.add_argument("--skill-dir", metavar="DIR", help="the skill package: SKILL.md and references/")
|
|
730
|
+
files.add_argument("--inventory", metavar="PATH", help="the validated inventory file")
|
|
731
|
+
files.add_argument("--surface", metavar="PATH", help="load-coverage-inputs.py surface output")
|
|
732
|
+
files.add_argument("--surface-set", default="all", metavar="NAME",
|
|
733
|
+
help="the name set of --surface to intersect against (default: all)")
|
|
734
|
+
files.add_argument("--coverage-inputs", metavar="PATH",
|
|
735
|
+
help="load-coverage-inputs.py metadata output (scalar and stack branches)")
|
|
736
|
+
files.add_argument("--verified", metavar="PATH",
|
|
737
|
+
help="verify-declared-numerator.py output (scalar branch)")
|
|
738
|
+
parser.add_argument("--output", metavar="PATH", help="also write the result to this file")
|
|
315
739
|
return parser
|
|
316
740
|
|
|
317
741
|
|
|
742
|
+
FILE_FLAGS = ("denominator_source", "denominator_value", "skill_dir", "inventory", "surface",
|
|
743
|
+
"coverage_inputs", "verified")
|
|
744
|
+
|
|
745
|
+
|
|
318
746
|
def _resolve_input(args):
|
|
319
747
|
if args.stdin:
|
|
320
748
|
return sys.stdin.read()
|
|
@@ -325,30 +753,70 @@ def _resolve_input(args):
|
|
|
325
753
|
return ""
|
|
326
754
|
|
|
327
755
|
|
|
756
|
+
def _write_output(path, result):
|
|
757
|
+
target = Path(path)
|
|
758
|
+
if result.get("code") == "INVALID_INPUT":
|
|
759
|
+
target.unlink(missing_ok=True)
|
|
760
|
+
return
|
|
761
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
762
|
+
target.write_bytes((json.dumps(result, indent=2, ensure_ascii=False) + "\n").encode("utf-8"))
|
|
763
|
+
|
|
764
|
+
|
|
328
765
|
def main(argv=None):
|
|
766
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
767
|
+
if argv[:1] == ["render"]:
|
|
768
|
+
return _render_main(argv[1:])
|
|
329
769
|
parser = _build_parser()
|
|
330
770
|
args = parser.parse_args(argv)
|
|
331
771
|
raw = _resolve_input(args)
|
|
332
|
-
|
|
772
|
+
from_files = any(getattr(args, flag) is not None for flag in FILE_FLAGS)
|
|
773
|
+
if not raw.strip() and not from_files:
|
|
333
774
|
parser.print_usage(file=sys.stderr)
|
|
334
775
|
print(
|
|
335
|
-
"error: no input provided (positional arg, --json-input, or
|
|
776
|
+
"error: no input provided (positional arg, --json-input, --stdin or the file flags)",
|
|
336
777
|
file=sys.stderr,
|
|
337
778
|
)
|
|
338
779
|
return 1
|
|
339
780
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
781
|
+
data = {}
|
|
782
|
+
if raw.strip():
|
|
783
|
+
try:
|
|
784
|
+
data = json.loads(raw)
|
|
785
|
+
except json.JSONDecodeError as exc:
|
|
786
|
+
print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
|
|
787
|
+
return 1
|
|
788
|
+
|
|
789
|
+
if isinstance(data, dict):
|
|
790
|
+
try:
|
|
791
|
+
data = apply_file_inputs(data, args)
|
|
792
|
+
except InputFileError as exc:
|
|
793
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
794
|
+
return 1
|
|
345
795
|
|
|
346
796
|
result = reconcile(data)
|
|
797
|
+
if args.output:
|
|
798
|
+
_write_output(args.output, result)
|
|
347
799
|
print(json.dumps(result, indent=2))
|
|
348
800
|
if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
|
|
349
801
|
return 2
|
|
350
802
|
return 0
|
|
351
803
|
|
|
352
804
|
|
|
805
|
+
def _force_utf8(*streams) -> None:
|
|
806
|
+
"""Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
|
|
807
|
+
|
|
808
|
+
A Windows console pipes them as cp1252, which cannot print every character
|
|
809
|
+
of the --help text, so --help would stop with UnicodeEncodeError.
|
|
810
|
+
"""
|
|
811
|
+
for stream in streams:
|
|
812
|
+
if hasattr(stream, "reconfigure"):
|
|
813
|
+
errors = getattr(stream, "errors", None)
|
|
814
|
+
if errors is None:
|
|
815
|
+
stream.reconfigure(encoding="utf-8")
|
|
816
|
+
else:
|
|
817
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
818
|
+
|
|
819
|
+
|
|
353
820
|
if __name__ == "__main__":
|
|
821
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
354
822
|
raise SystemExit(main())
|