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,18 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# /// script
|
|
3
|
-
# requires-python = ">=3.
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
4
|
# dependencies = []
|
|
5
5
|
# ///
|
|
6
|
-
"""Deterministic
|
|
6
|
+
"""Deterministic verdict tallies for skf-verify-stack.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
Each verdict itself is LLM judgment: matching an architecture technology to a
|
|
9
|
+
generated skill involves aliases and prose (coverage.md §3), and so does rating
|
|
10
|
+
an integration pair (integrations.md §4) or a requirement (requirements.md §3).
|
|
11
|
+
This script takes the rows the prompt already decided and does only the part
|
|
12
|
+
with one correct answer: counting each verdict class. --kind picks the rows.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
coverage (the default; coverage.md §3, persisted in §6)
|
|
15
|
+
Counts Covered, Missing and Replaced and turns the counts into
|
|
16
|
+
`coveragePercentage`. Two gotchas make the in-prose version drift between
|
|
17
|
+
runs, so they live here instead:
|
|
16
18
|
|
|
17
19
|
* The denominator excludes Replaced. `live_count = Covered + Missing`;
|
|
18
20
|
technologies flagged Replaced (intentionally being removed) are not a gap and
|
|
@@ -21,34 +23,104 @@ instead:
|
|
|
21
23
|
always yields the same `coveragePercentage` (the shared schema declares
|
|
22
24
|
`coveragePercentage: <0..100 integer>` but not the rounding rule).
|
|
23
25
|
|
|
26
|
+
integrations (integrations.md §4, persisted in §6)
|
|
27
|
+
Counts the rows of the canonical `| lib_a | lib_b | verdict | rationale |`
|
|
28
|
+
table by verdict: one row per integration pair, plus one Risky row per
|
|
29
|
+
circular dependency. --cycles names the JSON skf-find-cycles.py wrote
|
|
30
|
+
({"cycles": [[...], ...], ...}); each cycle is one more Risky row, so it
|
|
31
|
+
counts toward `pairs_risky` and the frontmatter's `pairsRisky` matches the
|
|
32
|
+
table. Pass the pair rows only: a cycle row in `rows` is counted twice.
|
|
33
|
+
|
|
34
|
+
requirements (requirements.md §3, persisted in §5)
|
|
35
|
+
Counts Fulfilled, Partially Fulfilled and Not Addressed.
|
|
36
|
+
|
|
37
|
+
--render (coverage and integrations)
|
|
38
|
+
Prints the report table's rows from the same rows instead of the counts,
|
|
39
|
+
one Markdown table row per line and nothing else, so no stage types a row
|
|
40
|
+
by hand: coverage.md §5 shows the coverage rows and §6 writes them under
|
|
41
|
+
`| Technology | Source Section | Skill Match | Verdict |`, and
|
|
42
|
+
integrations.md §6 writes the integration rows under the template's
|
|
43
|
+
canonical `| lib_a | lib_b | verdict | rationale |` header. The input is
|
|
44
|
+
checked as for the counts first. A coverage row gives its Source Section
|
|
45
|
+
and Skill Match from `section` and `skill` (`none` when null).
|
|
46
|
+
An integration row needs its `rationale`; after the pair rows comes one row
|
|
47
|
+
per cycle --cycles lists: `cycle`, the cycle's arrow chain (`A → B → C →
|
|
48
|
+
A`), Risky and "circular integration dependency detected: `<chain>`", so
|
|
49
|
+
the delta, which keys rows on their two libraries, never merges a cycle
|
|
50
|
+
into a pair. A cell holds its text on one line, each `|` behind a backslash.
|
|
51
|
+
With no row, nothing is printed.
|
|
52
|
+
|
|
24
53
|
CLI usage:
|
|
25
54
|
uv run skf-coverage-tally.py '<JSON>' # JSON literal positional
|
|
26
55
|
uv run skf-coverage-tally.py --json-input '<JSON>' # explicit flag form
|
|
27
56
|
cat input.json | uv run skf-coverage-tally.py --stdin # piped input
|
|
57
|
+
uv run skf-coverage-tally.py --kind integrations --cycles cycles.json --stdin < rows.json
|
|
58
|
+
uv run skf-coverage-tally.py --kind requirements --stdin < rows.json
|
|
59
|
+
uv run skf-coverage-tally.py --render --stdin < coverage-rows.json
|
|
60
|
+
uv run skf-coverage-tally.py --kind integrations --render --cycles cycles.json --stdin < rows.json
|
|
61
|
+
|
|
62
|
+
Input schema (one object), by kind:
|
|
63
|
+
coverage:
|
|
64
|
+
{"rows": [{"technology": "react", "verdict": "Covered"},
|
|
65
|
+
{"technology": "postgres","verdict": "Missing"},
|
|
66
|
+
{"technology": "old-orm", "verdict": "Replaced"}]}
|
|
67
|
+
Each technology once (compared trimmed and case-insensitively). A row may
|
|
68
|
+
carry "section" and "skill", each a string or null (--render reads an
|
|
69
|
+
absent one as null). A Covered row names its skill, a Missing or Replaced
|
|
70
|
+
one none: --render checks that on every row and the counts on each row
|
|
71
|
+
that carries "skill", so a rows file the tally accepts also renders.
|
|
72
|
+
integrations:
|
|
73
|
+
{"rows": [{"lib_a": "react", "lib_b": "zod", "verdict": "Verified",
|
|
74
|
+
"rationale": "..."}, ...]}
|
|
75
|
+
verdict: Verified, Plausible, Risky or Blocked (case-sensitive). Each
|
|
76
|
+
pair once, in either order, and never a library paired with itself.
|
|
77
|
+
rationale: the table's rationale cell, which --render requires.
|
|
78
|
+
requirements:
|
|
79
|
+
{"rows": [{"requirement_id": "R1", "verdict": "Fulfilled"}, ...]}
|
|
80
|
+
verdict: Fulfilled, Partially Fulfilled or Not Addressed (case-sensitive).
|
|
81
|
+
Each requirement_id once.
|
|
82
|
+
|
|
83
|
+
Output (stdout, one object), by kind:
|
|
84
|
+
coverage:
|
|
85
|
+
{
|
|
86
|
+
"covered_count": <int>,
|
|
87
|
+
"missing_count": <int>,
|
|
88
|
+
"replaced_count": <int>,
|
|
89
|
+
"live_count": <int>, # Covered + Missing (denominator)
|
|
90
|
+
"total_referenced": <int>, # all rows after dedup
|
|
91
|
+
"coverage_percentage": <int> # round-half-up(covered/live*100); 0 when live==0
|
|
92
|
+
}
|
|
93
|
+
integrations:
|
|
94
|
+
{
|
|
95
|
+
"pairs_verified": <int>,
|
|
96
|
+
"pairs_plausible": <int>,
|
|
97
|
+
"pairs_risky": <int>, # Risky pair rows + cycle_count
|
|
98
|
+
"pairs_blocked": <int>,
|
|
99
|
+
"pair_count": <int>, # the pair rows
|
|
100
|
+
"cycle_count": <int>, # the cycles --cycles lists (0 without it)
|
|
101
|
+
"row_count": <int> # pair_count + cycle_count: the table's rows
|
|
102
|
+
}
|
|
103
|
+
requirements:
|
|
104
|
+
{
|
|
105
|
+
"requirements_fulfilled": <int>,
|
|
106
|
+
"requirements_partial": <int>,
|
|
107
|
+
"requirements_not_addressed": <int>,
|
|
108
|
+
"requirement_count": <int>
|
|
109
|
+
}
|
|
28
110
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{"technology": "postgres","verdict": "Missing"},
|
|
34
|
-
{"technology": "old-orm", "verdict": "Replaced"}
|
|
35
|
-
]
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
Output (stdout, one object):
|
|
39
|
-
{
|
|
40
|
-
"covered_count": <int>,
|
|
41
|
-
"missing_count": <int>,
|
|
42
|
-
"replaced_count": <int>,
|
|
43
|
-
"live_count": <int>, # Covered + Missing (denominator)
|
|
44
|
-
"total_referenced": <int>, # all rows after dedup
|
|
45
|
-
"coverage_percentage": <int> # round-half-up(covered/live*100); 0 when live==0
|
|
46
|
-
}
|
|
111
|
+
--render: the rows as Markdown, one per line, in the order given (the
|
|
112
|
+
cycle rows last), with no JSON.
|
|
113
|
+
|
|
114
|
+
Errors (stdout, one object): {"error": "<why>", "code": "INVALID_INPUT"}.
|
|
47
115
|
|
|
48
116
|
Exit codes:
|
|
49
|
-
0
|
|
50
|
-
1
|
|
51
|
-
|
|
117
|
+
0 tally (or, with --render, the rows) emitted successfully
|
|
118
|
+
1 no input, input that is not JSON, or a --cycles file that cannot be read
|
|
119
|
+
or is not JSON
|
|
120
|
+
2 input parsed but its schema or semantics are invalid (error object
|
|
121
|
+
emitted as JSON); argparse usage errors, such as --cycles with another
|
|
122
|
+
kind or --render with requirements, exit 2 too, with usage on stderr and
|
|
123
|
+
no JSON
|
|
52
124
|
"""
|
|
53
125
|
|
|
54
126
|
from __future__ import annotations
|
|
@@ -57,37 +129,104 @@ import argparse
|
|
|
57
129
|
import json
|
|
58
130
|
import math
|
|
59
131
|
import sys
|
|
132
|
+
from pathlib import Path
|
|
60
133
|
|
|
61
134
|
VALID_VERDICTS = ("Covered", "Missing", "Replaced")
|
|
135
|
+
INTEGRATION_VERDICTS = ("Verified", "Plausible", "Risky", "Blocked")
|
|
136
|
+
REQUIREMENT_VERDICTS = ("Fulfilled", "Partially Fulfilled", "Not Addressed")
|
|
137
|
+
KINDS = ("coverage", "integrations", "requirements")
|
|
138
|
+
# The kinds whose report table --render prints.
|
|
139
|
+
RENDER_KINDS = ("coverage", "integrations")
|
|
140
|
+
# The Source Section or Skill Match of a coverage row that names none.
|
|
141
|
+
NO_CELL = "none"
|
|
142
|
+
# A cycle row's lib_a, and how its lib_b chains the cycle's nodes.
|
|
143
|
+
CYCLE_LABEL = "cycle"
|
|
144
|
+
CYCLE_ARROW = " → "
|
|
145
|
+
CYCLE_RATIONALE = "circular integration dependency detected: `{chain}`"
|
|
62
146
|
|
|
63
147
|
|
|
64
148
|
def make_error(message):
|
|
65
149
|
return {"error": message, "code": "INVALID_INPUT"}
|
|
66
150
|
|
|
67
151
|
|
|
68
|
-
def
|
|
152
|
+
def _rows(inp):
|
|
153
|
+
"""The `rows` list of an input object, or an error message."""
|
|
69
154
|
if inp is None or not isinstance(inp, dict):
|
|
70
|
-
return "Input must be a JSON object"
|
|
155
|
+
return None, "Input must be a JSON object"
|
|
71
156
|
rows = inp.get("rows")
|
|
72
157
|
if not isinstance(rows, list):
|
|
73
|
-
return "Missing or invalid required field: rows (must be a list)"
|
|
74
|
-
seen = set()
|
|
158
|
+
return None, "Missing or invalid required field: rows (must be a list)"
|
|
75
159
|
for i, row in enumerate(rows):
|
|
76
160
|
if not isinstance(row, dict):
|
|
77
|
-
return f"rows[{i}] must be an object"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
161
|
+
return None, f"rows[{i}] must be an object"
|
|
162
|
+
return rows, None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _name(row, i, key):
|
|
166
|
+
"""A row's trimmed, non-empty string `key`, or an error message."""
|
|
167
|
+
value = row.get(key)
|
|
168
|
+
if not isinstance(value, str) or not value.strip():
|
|
169
|
+
return None, f"rows[{i}] requires a non-empty string `{key}`"
|
|
170
|
+
return value.strip(), None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _verdict(row, i, allowed):
|
|
174
|
+
verdict = row.get("verdict")
|
|
175
|
+
if verdict not in allowed:
|
|
176
|
+
return f"rows[{i}] verdict {verdict!r} is not one of: {', '.join(allowed)}"
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _optional_text(row, i, key):
|
|
181
|
+
"""A row's optional string `key`, trimmed (None when absent, null or blank), or an error message."""
|
|
182
|
+
value = row.get(key)
|
|
183
|
+
if value is None:
|
|
184
|
+
return None, None
|
|
185
|
+
if not isinstance(value, str):
|
|
186
|
+
return None, f"rows[{i}] `{key}` must be a string or null"
|
|
187
|
+
return value.strip() or None, None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _cells(row, i, render):
|
|
191
|
+
"""A coverage row's Source Section and Skill Match (None for none), or an error message.
|
|
192
|
+
|
|
193
|
+
A Covered row names its skill, a Missing or Replaced one none. The pairing
|
|
194
|
+
is checked whenever the row carries `skill` (the rows file coverage.md §3
|
|
195
|
+
writes always does), so the tally refuses a row --render would refuse.
|
|
196
|
+
"""
|
|
197
|
+
section, err = _optional_text(row, i, "section")
|
|
198
|
+
if err:
|
|
199
|
+
return None, None, err
|
|
200
|
+
skill, err = _optional_text(row, i, "skill")
|
|
201
|
+
if err:
|
|
202
|
+
return None, None, err
|
|
203
|
+
if (render or "skill" in row) and (row["verdict"] == "Covered") != (skill is not None):
|
|
204
|
+
return None, None, (
|
|
205
|
+
f"rows[{i}] is {row['verdict']} but names {'no' if skill is None else 'a'} skill: "
|
|
206
|
+
"a Covered row names its skill, a Missing or Replaced one none"
|
|
207
|
+
)
|
|
208
|
+
return section, skill, None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _validate(inp, render=False):
|
|
212
|
+
rows, err = _rows(inp)
|
|
213
|
+
if err:
|
|
214
|
+
return err
|
|
215
|
+
seen = set()
|
|
216
|
+
for i, row in enumerate(rows):
|
|
217
|
+
tech, err = _name(row, i, "technology")
|
|
218
|
+
if err:
|
|
219
|
+
return err
|
|
220
|
+
err = _verdict(row, i, VALID_VERDICTS)
|
|
221
|
+
if err:
|
|
222
|
+
return err
|
|
223
|
+
norm = tech.lower()
|
|
88
224
|
if norm in seen:
|
|
89
|
-
return f"duplicate technology {
|
|
225
|
+
return f"duplicate technology {row['technology']!r}: the coverage list must be deduplicated first"
|
|
90
226
|
seen.add(norm)
|
|
227
|
+
_, _, err = _cells(row, i, render)
|
|
228
|
+
if err:
|
|
229
|
+
return err
|
|
91
230
|
return None
|
|
92
231
|
|
|
93
232
|
|
|
@@ -119,6 +258,137 @@ def tally(inp):
|
|
|
119
258
|
}
|
|
120
259
|
|
|
121
260
|
|
|
261
|
+
def _cycle_count(cycles):
|
|
262
|
+
"""The number of cycles in a skf-find-cycles.py result, or an error message.
|
|
263
|
+
|
|
264
|
+
Each cycle is a closed node path: at least two non-empty names, the first
|
|
265
|
+
repeated at the end (a self-loop is ["A", "A"]).
|
|
266
|
+
"""
|
|
267
|
+
if not isinstance(cycles, dict) or not isinstance(cycles.get("cycles"), list):
|
|
268
|
+
return None, "--cycles must hold the skf-find-cycles.py result, an object with a `cycles` list"
|
|
269
|
+
for i, cycle in enumerate(cycles["cycles"]):
|
|
270
|
+
if (
|
|
271
|
+
not isinstance(cycle, list)
|
|
272
|
+
or len(cycle) < 2
|
|
273
|
+
or not all(isinstance(node, str) and node.strip() for node in cycle)
|
|
274
|
+
or cycle[0] != cycle[-1]
|
|
275
|
+
):
|
|
276
|
+
return None, f"--cycles cycles[{i}] is not a closed node path such as [\"A\", \"B\", \"A\"]: {cycle!r}"
|
|
277
|
+
return len(cycles["cycles"]), None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def tally_integrations(inp, cycles=None):
|
|
281
|
+
"""Count the canonical verdict table's rows: the pair rows, and one Risky row per cycle."""
|
|
282
|
+
rows, err = _rows(inp)
|
|
283
|
+
if err:
|
|
284
|
+
return make_error(err)
|
|
285
|
+
counts = {verdict: 0 for verdict in INTEGRATION_VERDICTS}
|
|
286
|
+
seen = set()
|
|
287
|
+
for i, row in enumerate(rows):
|
|
288
|
+
lib_a, err = _name(row, i, "lib_a")
|
|
289
|
+
if err:
|
|
290
|
+
return make_error(err)
|
|
291
|
+
lib_b, err = _name(row, i, "lib_b")
|
|
292
|
+
if err:
|
|
293
|
+
return make_error(err)
|
|
294
|
+
err = _verdict(row, i, INTEGRATION_VERDICTS)
|
|
295
|
+
if err:
|
|
296
|
+
return make_error(err)
|
|
297
|
+
pair = frozenset((lib_a.lower(), lib_b.lower()))
|
|
298
|
+
if len(pair) == 1:
|
|
299
|
+
return make_error(f"rows[{i}] pairs {lib_a!r} with itself")
|
|
300
|
+
if pair in seen:
|
|
301
|
+
return make_error(f"duplicate pair {lib_a!r} / {lib_b!r}: list each integration pair once")
|
|
302
|
+
seen.add(pair)
|
|
303
|
+
counts[row["verdict"]] += 1
|
|
304
|
+
|
|
305
|
+
cycle_count = 0
|
|
306
|
+
if cycles is not None:
|
|
307
|
+
cycle_count, err = _cycle_count(cycles)
|
|
308
|
+
if err:
|
|
309
|
+
return make_error(err)
|
|
310
|
+
return {
|
|
311
|
+
"pairs_verified": counts["Verified"],
|
|
312
|
+
"pairs_plausible": counts["Plausible"],
|
|
313
|
+
"pairs_risky": counts["Risky"] + cycle_count,
|
|
314
|
+
"pairs_blocked": counts["Blocked"],
|
|
315
|
+
"pair_count": len(rows),
|
|
316
|
+
"cycle_count": cycle_count,
|
|
317
|
+
"row_count": len(rows) + cycle_count,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def tally_requirements(inp):
|
|
322
|
+
"""Count Fulfilled, Partially Fulfilled and Not Addressed requirements."""
|
|
323
|
+
rows, err = _rows(inp)
|
|
324
|
+
if err:
|
|
325
|
+
return make_error(err)
|
|
326
|
+
counts = {verdict: 0 for verdict in REQUIREMENT_VERDICTS}
|
|
327
|
+
seen = set()
|
|
328
|
+
for i, row in enumerate(rows):
|
|
329
|
+
requirement, err = _name(row, i, "requirement_id")
|
|
330
|
+
if err:
|
|
331
|
+
return make_error(err)
|
|
332
|
+
err = _verdict(row, i, REQUIREMENT_VERDICTS)
|
|
333
|
+
if err:
|
|
334
|
+
return make_error(err)
|
|
335
|
+
if requirement.lower() in seen:
|
|
336
|
+
return make_error(f"duplicate requirement_id {requirement!r}: list each requirement once")
|
|
337
|
+
seen.add(requirement.lower())
|
|
338
|
+
counts[row["verdict"]] += 1
|
|
339
|
+
return {
|
|
340
|
+
"requirements_fulfilled": counts["Fulfilled"],
|
|
341
|
+
"requirements_partial": counts["Partially Fulfilled"],
|
|
342
|
+
"requirements_not_addressed": counts["Not Addressed"],
|
|
343
|
+
"requirement_count": len(rows),
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# --- Rendering the report rows ------------------------------------------------
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _cell(value):
|
|
351
|
+
"""One table cell: its text on one line, a pipe escaped so it stays in the cell."""
|
|
352
|
+
return " ".join(str(value).split()).replace("|", "\\|")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _table_row(cells):
|
|
356
|
+
return "| " + " | ".join(_cell(cell) for cell in cells) + " |"
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def render_coverage(inp):
|
|
360
|
+
"""The coverage table's rows (Technology, Source Section, Skill Match, Verdict), or an error object."""
|
|
361
|
+
err = _validate(inp, render=True)
|
|
362
|
+
if err:
|
|
363
|
+
return make_error(err)
|
|
364
|
+
lines = []
|
|
365
|
+
for i, row in enumerate(inp["rows"]):
|
|
366
|
+
section, skill, _ = _cells(row, i, render=True)
|
|
367
|
+
lines.append(_table_row([row["technology"].strip(), section or NO_CELL, skill or NO_CELL, row["verdict"]]))
|
|
368
|
+
return lines
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def render_integrations(inp, cycles=None):
|
|
372
|
+
"""The canonical verdict table's rows (lib_a, lib_b, verdict, rationale), then one per cycle.
|
|
373
|
+
|
|
374
|
+
The rows and --cycles are checked as tally_integrations() checks them;
|
|
375
|
+
an error object comes back on the first problem.
|
|
376
|
+
"""
|
|
377
|
+
counted = tally_integrations(inp, cycles)
|
|
378
|
+
if "error" in counted:
|
|
379
|
+
return counted
|
|
380
|
+
lines = []
|
|
381
|
+
for i, row in enumerate(inp["rows"]):
|
|
382
|
+
rationale, err = _name(row, i, "rationale")
|
|
383
|
+
if err:
|
|
384
|
+
return make_error(err)
|
|
385
|
+
lines.append(_table_row([row["lib_a"].strip(), row["lib_b"].strip(), row["verdict"], rationale]))
|
|
386
|
+
for cycle in (cycles or {}).get("cycles", []):
|
|
387
|
+
chain = CYCLE_ARROW.join(node.strip() for node in cycle)
|
|
388
|
+
lines.append(_table_row([CYCLE_LABEL, chain, "Risky", CYCLE_RATIONALE.format(chain=chain)]))
|
|
389
|
+
return lines
|
|
390
|
+
|
|
391
|
+
|
|
122
392
|
# --- CLI --------------------------------------------------------------------
|
|
123
393
|
|
|
124
394
|
|
|
@@ -126,16 +396,25 @@ def _build_parser():
|
|
|
126
396
|
parser = argparse.ArgumentParser(
|
|
127
397
|
prog="skf-coverage-tally",
|
|
128
398
|
description=(
|
|
129
|
-
"Deterministic
|
|
130
|
-
"Covered/Missing/Replaced verdicts
|
|
131
|
-
"with Replaced excluded from the
|
|
399
|
+
"Deterministic verdict tallies for skf-verify-stack. --kind coverage "
|
|
400
|
+
"(the default, coverage.md §3) counts Covered/Missing/Replaced verdicts "
|
|
401
|
+
"and computes coveragePercentage with Replaced excluded from the "
|
|
402
|
+
"denominator and half-up rounding. --kind integrations (integrations.md "
|
|
403
|
+
"§4, persisted in §6) counts the verdict table's pair rows, plus one "
|
|
404
|
+
"Risky row per cycle the --cycles file lists. --kind requirements "
|
|
405
|
+
"(requirements.md §3, persisted in §5) counts Fulfilled/Partially "
|
|
406
|
+
"Fulfilled/Not Addressed. --render prints the coverage or integration "
|
|
407
|
+
"report rows instead of the counts."
|
|
132
408
|
),
|
|
133
409
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
134
410
|
epilog=(
|
|
135
|
-
"
|
|
411
|
+
"Examples:\n"
|
|
136
412
|
" uv run skf-coverage-tally.py "
|
|
137
413
|
"'{\"rows\":[{\"technology\":\"react\",\"verdict\":\"Covered\"},"
|
|
138
|
-
"{\"technology\":\"postgres\",\"verdict\":\"Missing\"}]}'"
|
|
414
|
+
"{\"technology\":\"postgres\",\"verdict\":\"Missing\"}]}'\n"
|
|
415
|
+
" uv run skf-coverage-tally.py --kind integrations --cycles cycles.json "
|
|
416
|
+
"--stdin < rows.json\n"
|
|
417
|
+
" uv run skf-coverage-tally.py --render --stdin < coverage-rows.json"
|
|
139
418
|
),
|
|
140
419
|
)
|
|
141
420
|
src = parser.add_mutually_exclusive_group()
|
|
@@ -154,6 +433,28 @@ def _build_parser():
|
|
|
154
433
|
action="store_true",
|
|
155
434
|
help="Read the JSON object from stdin.",
|
|
156
435
|
)
|
|
436
|
+
parser.add_argument(
|
|
437
|
+
"--kind",
|
|
438
|
+
choices=KINDS,
|
|
439
|
+
default="coverage",
|
|
440
|
+
help="Which rows to count (default: coverage).",
|
|
441
|
+
)
|
|
442
|
+
parser.add_argument(
|
|
443
|
+
"--cycles",
|
|
444
|
+
metavar="FILE",
|
|
445
|
+
help=(
|
|
446
|
+
"With --kind integrations: the JSON skf-find-cycles.py wrote; each "
|
|
447
|
+
"cycle counts as one more Risky row (with --render, one more row)."
|
|
448
|
+
),
|
|
449
|
+
)
|
|
450
|
+
parser.add_argument(
|
|
451
|
+
"--render",
|
|
452
|
+
action="store_true",
|
|
453
|
+
help=(
|
|
454
|
+
"With --kind coverage or integrations: print the report table's rows, "
|
|
455
|
+
"one Markdown row per line, instead of the counts."
|
|
456
|
+
),
|
|
457
|
+
)
|
|
157
458
|
return parser
|
|
158
459
|
|
|
159
460
|
|
|
@@ -167,9 +468,23 @@ def _resolve_input(args):
|
|
|
167
468
|
return ""
|
|
168
469
|
|
|
169
470
|
|
|
471
|
+
def _read_cycles(path):
|
|
472
|
+
"""The parsed --cycles file, or (None, error) when it cannot be read or is not JSON."""
|
|
473
|
+
try:
|
|
474
|
+
return json.loads(Path(path).read_text(encoding="utf-8")), None
|
|
475
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
476
|
+
return None, f"cannot read the --cycles file {path}: {exc}"
|
|
477
|
+
except json.JSONDecodeError as exc:
|
|
478
|
+
return None, f"the --cycles file {path} is not JSON: {exc.msg}"
|
|
479
|
+
|
|
480
|
+
|
|
170
481
|
def main(argv=None):
|
|
171
482
|
parser = _build_parser()
|
|
172
483
|
args = parser.parse_args(argv)
|
|
484
|
+
if args.cycles is not None and args.kind != "integrations":
|
|
485
|
+
parser.error("--cycles applies only with --kind integrations")
|
|
486
|
+
if args.render and args.kind not in RENDER_KINDS:
|
|
487
|
+
parser.error("--render applies only with --kind coverage or integrations")
|
|
173
488
|
raw = _resolve_input(args)
|
|
174
489
|
if not raw.strip():
|
|
175
490
|
parser.print_usage(file=sys.stderr)
|
|
@@ -185,12 +500,43 @@ def main(argv=None):
|
|
|
185
500
|
print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
|
|
186
501
|
return 1
|
|
187
502
|
|
|
188
|
-
|
|
503
|
+
if args.kind == "integrations":
|
|
504
|
+
cycles = None
|
|
505
|
+
if args.cycles is not None:
|
|
506
|
+
cycles, err = _read_cycles(args.cycles)
|
|
507
|
+
if err:
|
|
508
|
+
print(json.dumps(make_error(err), indent=2))
|
|
509
|
+
return 1
|
|
510
|
+
result = (render_integrations if args.render else tally_integrations)(data, cycles)
|
|
511
|
+
elif args.kind == "requirements":
|
|
512
|
+
result = tally_requirements(data)
|
|
513
|
+
else:
|
|
514
|
+
result = (render_coverage if args.render else tally)(data)
|
|
515
|
+
if isinstance(result, list):
|
|
516
|
+
sys.stdout.write("".join(line + "\n" for line in result))
|
|
517
|
+
return 0
|
|
189
518
|
print(json.dumps(result, indent=2))
|
|
190
519
|
if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
|
|
191
520
|
return 2
|
|
192
521
|
return 0
|
|
193
522
|
|
|
194
523
|
|
|
524
|
+
def _force_utf8(*streams) -> None:
|
|
525
|
+
"""Reconfigure the given streams to UTF-8, keeping each stream's error handler.
|
|
526
|
+
|
|
527
|
+
A Windows console pipes them as cp1252, which cannot print every character
|
|
528
|
+
a --help text or an error message may hold, and which garbles the UTF-8 row
|
|
529
|
+
files the stages redirect into --stdin.
|
|
530
|
+
"""
|
|
531
|
+
for stream in streams:
|
|
532
|
+
if hasattr(stream, "reconfigure"):
|
|
533
|
+
errors = getattr(stream, "errors", None)
|
|
534
|
+
if errors is None:
|
|
535
|
+
stream.reconfigure(encoding="utf-8")
|
|
536
|
+
else:
|
|
537
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
538
|
+
|
|
539
|
+
|
|
195
540
|
if __name__ == "__main__":
|
|
541
|
+
_force_utf8(sys.stdin, sys.stdout, sys.stderr)
|
|
196
542
|
raise SystemExit(main())
|