bmad-module-skill-forge 2.2.0 → 3.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 +135 -18
- package/docs/verifying-a-skill.md +83 -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 +2 -2
- 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 +32 -12
- 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 +30 -14
- package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
- 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 +694 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
- 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 +3 -2
- 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 +1 -1
- 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 +4084 -85
- package/src/shared/scripts/skf-extraction-inventory.py +746 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +539 -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 +444 -203
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +661 -0
- package/src/shared/scripts/skf-hash-content.py +166 -12
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +159 -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 +1 -1
- 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 +161 -27
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
- 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 +128 -13
- 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 +670 -119
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +5 -5
- 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 +4 -1
- 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 +2919 -54
- package/src/shared/scripts/skf-write-skill-brief.py +367 -19
- 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 +27 -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/init.md +148 -210
- package/src/skf-audit-skill/references/re-index.md +96 -108
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +27 -18
- 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 +75 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
- package/src/skf-brief-skill/SKILL.md +36 -25
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +17 -24
- package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +142 -125
- package/src/skf-brief-skill/references/confirm-brief.md +10 -8
- 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 +24 -80
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +3 -2
- 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 +6 -5
- package/src/skf-create-skill/assets/skill-sections.md +15 -5
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
- 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 +107 -29
- 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 +20 -0
- package/src/skf-create-skill/references/extract.md +200 -161
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
- package/src/skf-create-skill/references/extraction-patterns.md +584 -196
- 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 +28 -59
- package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
- 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 +79 -146
- 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 +118 -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 +217 -254
- 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 +163 -88
- 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 +162 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +41 -155
- package/src/skf-test-skill/references/source-access-protocol.md +19 -92
- 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 +287 -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 +1381 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
- package/src/skf-test-skill/scripts/score-signatures.py +459 -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 +127 -52
- 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 +233 -222
- 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 +250 -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 +180 -47
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +100 -35
- package/src/skf-update-skill/references/write.md +331 -149
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
- 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,32 +1,76 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# /// script
|
|
3
|
-
# requires-python = ">=3.
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
4
|
# dependencies = []
|
|
5
5
|
# ///
|
|
6
6
|
"""Deterministic previous-vs-current delta for skf-verify-stack (synthesize.md §3).
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
classifying how
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
Deciding each finding's verdict is judgment, made by the run that wrote the
|
|
9
|
+
report; classifying how two runs differ is not. "improved / regressed / new /
|
|
10
|
+
unchanged" is a set-diff plus a fixed verdict ranking, one correct answer per
|
|
11
|
+
input. Doing it in prose lets the counts drift (mismatched pair keys,
|
|
12
|
+
inconsistent ranking), so it lives here. So does reading the finding sets out
|
|
13
|
+
of the two reports (--previous-report, --current-report): each one sits in a
|
|
14
|
+
table with a pinned header, so reading it has one correct answer too.
|
|
13
15
|
|
|
14
16
|
Verdict ranking (higher = healthier):
|
|
15
17
|
* coverage — Missing(0) < Covered(1). Replaced is intentional removal, not a
|
|
16
18
|
gap; Replaced findings are bucketed separately and never scored improved/
|
|
17
19
|
regressed (mirrors their exclusion from the coverage denominator).
|
|
18
20
|
* integration — Blocked(0) < Risky(1) < Plausible(2) < Verified(3).
|
|
19
|
-
*
|
|
21
|
+
* evidence tier: T3(0) < T2(1) < T1-low(2) < T1(3); a drop is a regression.
|
|
22
|
+
|
|
23
|
+
Tiers use one scale, the confidence tiers T1, T1-low, T2 and T3 (case-sensitive),
|
|
24
|
+
for every skill: the caller maps each skill to it before calling, or passes
|
|
25
|
+
--inventory, whose skf-enumerate-stack-skills.py inventory gives each skill's
|
|
26
|
+
evidence_tier (verify-stack does). metadata.json
|
|
27
|
+
`confidence_tier` is no input here: a single skill records its forge tier there
|
|
28
|
+
(Quick, Forge, Forge+ or Deep) and a stack its confidence tier, so its values sit
|
|
29
|
+
on two scales that do not compare. Any other token, a forge tier included, is
|
|
30
|
+
rejected (exit 2, code UNKNOWN_TIER), never skipped: a skipped token would hide
|
|
31
|
+
a regression.
|
|
20
32
|
|
|
21
33
|
Matching keys are normalized so identity is stable across runs:
|
|
22
34
|
* coverage findings match on technology.lower()
|
|
23
35
|
* integration findings match on the unordered pair {libA, libB} (lowercased),
|
|
24
36
|
so "A↔B" in one run equals "B↔A" in the other.
|
|
25
37
|
|
|
38
|
+
Reading a report (--previous-report, --current-report):
|
|
39
|
+
* A report whose frontmatter schemaVersion is not "1.0" is not read.
|
|
40
|
+
* coverage: the Technology and Verdict columns of the first table under
|
|
41
|
+
`## Coverage Analysis` whose header row has both (coverage.md §6 writes
|
|
42
|
+
`| Technology | Source Section | Skill Match | Verdict |`).
|
|
43
|
+
* integration: the lib_a, lib_b and verdict columns of the canonical
|
|
44
|
+
`| lib_a | lib_b | verdict | rationale |` table under
|
|
45
|
+
`## Integration Verdicts`, read by read_verdict_table() of the shared
|
|
46
|
+
skf-validate-feasibility-report.py, the reader the report's consumers use.
|
|
47
|
+
A report with no such table, or with two, is not read.
|
|
48
|
+
* tiers, from the previous report only: the skill and evidence_tier columns
|
|
49
|
+
of the first table under `## Evidence Sources` whose header row has both
|
|
50
|
+
(synthesize.md §5 writes `| skill | evidence_tier | confidence_tier |
|
|
51
|
+
metadata_schema_version | skill_md |`). A report written before tiers were
|
|
52
|
+
recorded has no such table, or one with no row: previousTiersRecorded is
|
|
53
|
+
then false and no tier is compared.
|
|
54
|
+
Tables in fenced code or in an HTML comment are skipped. A verdict or tier
|
|
55
|
+
cell holds the token alone (case-sensitive), and every row names its
|
|
56
|
+
technology, its two libraries or its skill.
|
|
57
|
+
|
|
26
58
|
CLI usage:
|
|
27
59
|
uv run skf-report-delta.py '<JSON>' # JSON literal positional
|
|
28
60
|
uv run skf-report-delta.py --json-input '<JSON>' # explicit flag form
|
|
29
61
|
cat input.json | uv run skf-report-delta.py --stdin # piped input
|
|
62
|
+
echo '{"currentTiers": {...}}' | uv run skf-report-delta.py \\
|
|
63
|
+
--previous-report <earlier.md> --current-report <this-run.md> --stdin
|
|
64
|
+
uv run skf-report-delta.py --previous-report <earlier.md> \\
|
|
65
|
+
--current-report <this-run.md> --inventory <skill-inventory.json>
|
|
66
|
+
|
|
67
|
+
--previous-report fills `previous` and `previousTiers` from that report,
|
|
68
|
+
--current-report fills `current` from its report, and --inventory fills
|
|
69
|
+
`currentTiers` from an skf-enumerate-stack-skills.py inventory (each
|
|
70
|
+
skills[] entry's name and evidence_tier), so the JSON leaves them out; with
|
|
71
|
+
both report flags the JSON may be left out too. --no-tiers compares no
|
|
72
|
+
tier: both tier maps, the previous report's tier table and --inventory are
|
|
73
|
+
ignored.
|
|
30
74
|
|
|
31
75
|
Input schema (one object):
|
|
32
76
|
{
|
|
@@ -35,8 +79,8 @@ Input schema (one object):
|
|
|
35
79
|
"integration": [{"libA": "react", "libB": "express", "verdict": "Verified"}, ...]
|
|
36
80
|
},
|
|
37
81
|
"current": { ... same shape ... },
|
|
38
|
-
"previousTiers": {"react": "T1", ...}, # optional, skill_name ->
|
|
39
|
-
"currentTiers": {"react": "T2", ...} # optional
|
|
82
|
+
"previousTiers": {"react": "T1", ...}, # optional, skill_name -> T1|T1-low|T2|T3
|
|
83
|
+
"currentTiers": {"react": "T2", ...} # optional, same scale
|
|
40
84
|
}
|
|
41
85
|
|
|
42
86
|
Output (stdout, one object):
|
|
@@ -48,30 +92,88 @@ Output (stdout, one object):
|
|
|
48
92
|
"dropped": [labels], "droppedCount": <int>,
|
|
49
93
|
"replaced": [labels], "replacedCount": <int>,
|
|
50
94
|
"tierDowngrades": [{"skill": "...", "from": "T1", "to": "T2"}],
|
|
51
|
-
"tierDowngradeCount": <int
|
|
95
|
+
"tierDowngradeCount": <int>,
|
|
96
|
+
"tiersCompared": <bool>, # both tier maps were there to compare
|
|
97
|
+
"previousTiersRecorded": <bool> | null # --previous-report: the report records
|
|
98
|
+
# tiers; null without it or with --no-tiers
|
|
52
99
|
}
|
|
53
100
|
|
|
101
|
+
Errors (stdout, one object): {"error": "<why>", "code": "<code>"}, plus "report"
|
|
102
|
+
("previous" or "current") and "path" when the error comes from a report, and
|
|
103
|
+
"path" alone when it comes from --inventory:
|
|
104
|
+
INVALID_INPUT the JSON does not fit the input schema above, or the
|
|
105
|
+
--inventory file cannot be read or holds no skills[] list
|
|
106
|
+
UNKNOWN_TIER a tier token outside T1, T1-low, T2 and T3
|
|
107
|
+
INVALID_REPORT a report that cannot be read, or is not read (see above)
|
|
108
|
+
HELPER_MISSING the shared skf-validate-feasibility-report.py is not in the
|
|
109
|
+
shared scripts folder beside this skill's folder
|
|
110
|
+
|
|
54
111
|
Exit codes:
|
|
55
|
-
0
|
|
56
|
-
1
|
|
57
|
-
2
|
|
112
|
+
0 delta emitted successfully
|
|
113
|
+
1 no input, input that is not JSON, or HELPER_MISSING
|
|
114
|
+
2 input rejected: INVALID_INPUT, UNKNOWN_TIER or INVALID_REPORT
|
|
58
115
|
"""
|
|
59
116
|
|
|
60
117
|
from __future__ import annotations
|
|
61
118
|
|
|
62
119
|
import argparse
|
|
120
|
+
import importlib.util
|
|
63
121
|
import json
|
|
122
|
+
import re
|
|
64
123
|
import sys
|
|
124
|
+
from pathlib import Path
|
|
65
125
|
|
|
66
126
|
COVERAGE_RANK = {"Missing": 0, "Covered": 1}
|
|
67
127
|
COVERAGE_VERDICTS = ("Covered", "Missing", "Replaced")
|
|
68
128
|
INTEGRATION_RANK = {"Blocked": 0, "Risky": 1, "Plausible": 2, "Verified": 3}
|
|
69
129
|
INTEGRATION_VERDICTS = tuple(INTEGRATION_RANK)
|
|
70
|
-
TIER_RANK = {"T2": 1, "T1-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
130
|
+
TIER_RANK = {"T3": 0, "T2": 1, "T1-low": 2, "T1": 3}
|
|
131
|
+
TIERS = tuple(sorted(TIER_RANK, key=TIER_RANK.get, reverse=True)) # strongest first
|
|
132
|
+
|
|
133
|
+
INVALID_INPUT = "INVALID_INPUT"
|
|
134
|
+
UNKNOWN_TIER = "UNKNOWN_TIER"
|
|
135
|
+
INVALID_REPORT = "INVALID_REPORT"
|
|
136
|
+
HELPER_MISSING = "HELPER_MISSING"
|
|
137
|
+
|
|
138
|
+
# The shared feasibility-report reader. Installed (under _bmad/skf/ or an
|
|
139
|
+
# IDE's skills folder) and in a dev checkout (src/), shared/ sits beside
|
|
140
|
+
# this skill's folder.
|
|
141
|
+
SHARED_READER = (
|
|
142
|
+
Path(__file__).resolve().parent.parent.parent
|
|
143
|
+
/ "shared"
|
|
144
|
+
/ "scripts"
|
|
145
|
+
/ "skf-validate-feasibility-report.py"
|
|
146
|
+
)
|
|
147
|
+
SCHEMA_VERSION = "1.0"
|
|
148
|
+
|
|
149
|
+
# The two tables read here besides the canonical verdict table: the section
|
|
150
|
+
# each sits under and the columns read from it (see "Reading a report").
|
|
151
|
+
COVERAGE_SECTION = "Coverage Analysis"
|
|
152
|
+
COVERAGE_COLUMNS = ("Technology", "Verdict")
|
|
153
|
+
TIERS_SECTION = "Evidence Sources"
|
|
154
|
+
TIER_COLUMNS = ("skill", "evidence_tier")
|
|
155
|
+
|
|
156
|
+
# Table scanning, the way the shared reader scans for the verdict table.
|
|
157
|
+
_FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})")
|
|
158
|
+
_COMMENT_OPEN_RE = re.compile(r"^\s{0,3}<!--")
|
|
159
|
+
_COMMENT_CLOSE = "-->"
|
|
160
|
+
_SECTION_END_RE = re.compile(r"^#{1,2}\s")
|
|
161
|
+
_H2_RE = re.compile(r"^##\s+(.+?)\s*$")
|
|
162
|
+
_CELL_SEPARATOR_RE = re.compile(r"(?<!\\)\|")
|
|
163
|
+
_DELIMITER_CELL_RE = re.compile(r"^:?-+:?$")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ReportError(Exception):
|
|
167
|
+
"""A file the delta does not read; `code` is INVALID_REPORT or UNKNOWN_TIER
|
|
168
|
+
for a report, INVALID_INPUT for an --inventory file."""
|
|
169
|
+
|
|
170
|
+
def __init__(self, code, message):
|
|
171
|
+
super().__init__(message)
|
|
172
|
+
self.code = code
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def make_error(message, code=INVALID_INPUT):
|
|
176
|
+
return {"error": message, "code": code}
|
|
75
177
|
|
|
76
178
|
|
|
77
179
|
def _validate_side(side, name):
|
|
@@ -101,18 +203,29 @@ def _validate_side(side, name):
|
|
|
101
203
|
|
|
102
204
|
|
|
103
205
|
def _validate(inp):
|
|
206
|
+
"""Return (message, code) for an input compute() rejects, else None."""
|
|
104
207
|
if inp is None or not isinstance(inp, dict):
|
|
105
|
-
return "Input must be a JSON object"
|
|
208
|
+
return "Input must be a JSON object", INVALID_INPUT
|
|
106
209
|
if "previous" not in inp or "current" not in inp:
|
|
107
|
-
return "Input requires `previous` and `current` objects"
|
|
210
|
+
return "Input requires `previous` and `current` objects", INVALID_INPUT
|
|
108
211
|
for name in ("previous", "current"):
|
|
109
212
|
err = _validate_side(inp[name], name)
|
|
110
213
|
if err:
|
|
111
|
-
return err
|
|
214
|
+
return err, INVALID_INPUT
|
|
112
215
|
for key in ("previousTiers", "currentTiers"):
|
|
113
216
|
tiers = inp.get(key)
|
|
114
|
-
if tiers is
|
|
115
|
-
|
|
217
|
+
if tiers is None:
|
|
218
|
+
continue
|
|
219
|
+
if not isinstance(tiers, dict):
|
|
220
|
+
return f"`{key}` must be an object mapping skill_name -> tier", INVALID_INPUT
|
|
221
|
+
for skill, tier in tiers.items():
|
|
222
|
+
if tier not in TIERS:
|
|
223
|
+
return (
|
|
224
|
+
f"`{key}.{skill}` tier {tier!r} not one of: {', '.join(TIERS)} "
|
|
225
|
+
"(map each skill to its evidence tier before calling; a forge tier "
|
|
226
|
+
"such as Deep is on another scale)",
|
|
227
|
+
UNKNOWN_TIER,
|
|
228
|
+
)
|
|
116
229
|
return None
|
|
117
230
|
|
|
118
231
|
|
|
@@ -134,10 +247,10 @@ def _rank(domain, verdict):
|
|
|
134
247
|
|
|
135
248
|
|
|
136
249
|
def compute(inp):
|
|
137
|
-
"""Pure delta over the two
|
|
250
|
+
"""Pure delta over the two finding sets."""
|
|
138
251
|
err = _validate(inp)
|
|
139
252
|
if err:
|
|
140
|
-
return make_error(err)
|
|
253
|
+
return make_error(*err)
|
|
141
254
|
|
|
142
255
|
prev = _index(inp["previous"])
|
|
143
256
|
curr = _index(inp["current"])
|
|
@@ -171,14 +284,13 @@ def compute(inp):
|
|
|
171
284
|
else:
|
|
172
285
|
buckets["dropped"].append(label)
|
|
173
286
|
|
|
287
|
+
prev_tiers = inp.get("previousTiers")
|
|
288
|
+
curr_tiers = inp.get("currentTiers")
|
|
289
|
+
compared = prev_tiers is not None and curr_tiers is not None
|
|
174
290
|
tier_downgrades = []
|
|
175
|
-
prev_tiers
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
pr = TIER_RANK.get(str(prev_tiers[skill]).upper())
|
|
179
|
-
cr = TIER_RANK.get(str(curr_tiers[skill]).upper())
|
|
180
|
-
if pr is None or cr is None:
|
|
181
|
-
continue
|
|
291
|
+
for skill in sorted(set(prev_tiers or {}) & set(curr_tiers or {})):
|
|
292
|
+
pr = TIER_RANK[prev_tiers[skill]]
|
|
293
|
+
cr = TIER_RANK[curr_tiers[skill]]
|
|
182
294
|
if cr < pr:
|
|
183
295
|
tier_downgrades.append({"skill": skill, "from": prev_tiers[skill], "to": curr_tiers[skill]})
|
|
184
296
|
|
|
@@ -188,6 +300,249 @@ def compute(inp):
|
|
|
188
300
|
result[name + "Count"] = len(items)
|
|
189
301
|
result["tierDowngrades"] = tier_downgrades
|
|
190
302
|
result["tierDowngradeCount"] = len(tier_downgrades)
|
|
303
|
+
result["tiersCompared"] = compared
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# --- Reading a report ---------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def load_reader():
|
|
311
|
+
"""Import the shared feasibility-report reader, or None when it is missing."""
|
|
312
|
+
try:
|
|
313
|
+
if not SHARED_READER.is_file():
|
|
314
|
+
return None
|
|
315
|
+
except OSError:
|
|
316
|
+
return None # a folder on the way that cannot be searched
|
|
317
|
+
spec = importlib.util.spec_from_file_location("skf_validate_feasibility_report", SHARED_READER)
|
|
318
|
+
if spec is None or spec.loader is None:
|
|
319
|
+
return None
|
|
320
|
+
module = importlib.util.module_from_spec(spec)
|
|
321
|
+
spec.loader.exec_module(module)
|
|
322
|
+
return module
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _cells(line):
|
|
326
|
+
"""Split one table row into its trimmed cells; `\\|` is a pipe inside a cell."""
|
|
327
|
+
row = line.strip()
|
|
328
|
+
if row.startswith("|"):
|
|
329
|
+
row = row[1:]
|
|
330
|
+
if row.endswith("|") and not row.endswith("\\|"):
|
|
331
|
+
row = row[:-1]
|
|
332
|
+
return [cell.strip().replace("\\|", "|") for cell in _CELL_SEPARATOR_RE.split(row)]
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _section_tables(body, section, first_line):
|
|
336
|
+
"""Yield (header, rows) for each table under `## {section}`.
|
|
337
|
+
|
|
338
|
+
`header` holds the header row's cells, and `rows` each data row as
|
|
339
|
+
(file_line, cells), counting from `first_line`, the file line of the
|
|
340
|
+
body's first line. A table is a line that starts with a pipe, then a
|
|
341
|
+
delimiter row, then every following line that starts with a pipe.
|
|
342
|
+
Fenced code and HTML comments are skipped.
|
|
343
|
+
"""
|
|
344
|
+
lines = body.split("\n")
|
|
345
|
+
fence = None
|
|
346
|
+
in_comment = in_section = False
|
|
347
|
+
idx = 0
|
|
348
|
+
while idx < len(lines):
|
|
349
|
+
line = lines[idx]
|
|
350
|
+
idx += 1
|
|
351
|
+
fence_match = _FENCE_RE.match(line)
|
|
352
|
+
if fence is not None:
|
|
353
|
+
marker = fence_match.group(1) if fence_match else ""
|
|
354
|
+
if marker[:1] == fence[0] and len(marker) >= len(fence):
|
|
355
|
+
fence = None
|
|
356
|
+
continue
|
|
357
|
+
if in_comment:
|
|
358
|
+
in_comment = _COMMENT_CLOSE not in line
|
|
359
|
+
continue
|
|
360
|
+
if fence_match:
|
|
361
|
+
fence = fence_match.group(1)
|
|
362
|
+
continue
|
|
363
|
+
if _COMMENT_OPEN_RE.match(line):
|
|
364
|
+
in_comment = _COMMENT_CLOSE not in line
|
|
365
|
+
continue
|
|
366
|
+
if _SECTION_END_RE.match(line):
|
|
367
|
+
heading = _H2_RE.match(line)
|
|
368
|
+
in_section = heading is not None and heading.group(1).strip() == section
|
|
369
|
+
continue
|
|
370
|
+
if not in_section or not line.lstrip().startswith("|") or idx >= len(lines):
|
|
371
|
+
continue
|
|
372
|
+
delimiter = lines[idx]
|
|
373
|
+
if not delimiter.lstrip().startswith("|") or not all(
|
|
374
|
+
_DELIMITER_CELL_RE.match(cell) for cell in _cells(delimiter)
|
|
375
|
+
):
|
|
376
|
+
continue
|
|
377
|
+
header, rows = _cells(line), []
|
|
378
|
+
idx += 1
|
|
379
|
+
while idx < len(lines) and lines[idx].lstrip().startswith("|"):
|
|
380
|
+
rows.append((first_line + idx, _cells(lines[idx])))
|
|
381
|
+
idx += 1
|
|
382
|
+
yield header, rows
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _columns(tables, wanted):
|
|
386
|
+
"""The first table whose header has every wanted column, as (indexes, rows), else None."""
|
|
387
|
+
for header, rows in tables:
|
|
388
|
+
if all(name in header for name in wanted):
|
|
389
|
+
return [header.index(name) for name in wanted], rows
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _cell(cells, index):
|
|
394
|
+
return cells[index] if index < len(cells) else ""
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def read_report(path, reader, with_tiers):
|
|
398
|
+
"""Read one report's finding sets (see "Reading a report").
|
|
399
|
+
|
|
400
|
+
Returns (side, tiers): `side` is {"coverage": [...], "integration": [...]}
|
|
401
|
+
in compute()'s input shape, and `tiers` maps each skill to its evidence
|
|
402
|
+
tier, or is None when `with_tiers` is false or the report records no
|
|
403
|
+
tier. Raises ReportError for a report that is not read.
|
|
404
|
+
"""
|
|
405
|
+
shown = f"`{path}`"
|
|
406
|
+
try:
|
|
407
|
+
# utf-8-sig drops the byte order mark some editors add, as the shared
|
|
408
|
+
# reader does.
|
|
409
|
+
content = Path(path).read_text(encoding="utf-8-sig")
|
|
410
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
411
|
+
raise ReportError(INVALID_REPORT, f"{shown} cannot be read: {exc}") from exc
|
|
412
|
+
frontmatter, body = reader.split_frontmatter(content)
|
|
413
|
+
version = frontmatter.get("schemaVersion")
|
|
414
|
+
if version != SCHEMA_VERSION:
|
|
415
|
+
found = "no schemaVersion" if version is None else f"schemaVersion {version!r}"
|
|
416
|
+
raise ReportError(INVALID_REPORT, f"{shown} has {found}, not {SCHEMA_VERSION!r}")
|
|
417
|
+
# The body starts on the file line after the frontmatter's closing `---`.
|
|
418
|
+
first_line = len(content.split("\n")) - len(body.split("\n")) + 1
|
|
419
|
+
|
|
420
|
+
table = _columns(_section_tables(body, COVERAGE_SECTION, first_line), COVERAGE_COLUMNS)
|
|
421
|
+
if table is None:
|
|
422
|
+
raise ReportError(
|
|
423
|
+
INVALID_REPORT,
|
|
424
|
+
f"{shown} has no table with Technology and Verdict columns under `## {COVERAGE_SECTION}`",
|
|
425
|
+
)
|
|
426
|
+
(tech_at, verdict_at), rows = table
|
|
427
|
+
coverage = []
|
|
428
|
+
for line, cells in rows:
|
|
429
|
+
tech, verdict = _cell(cells, tech_at), _cell(cells, verdict_at)
|
|
430
|
+
if not tech:
|
|
431
|
+
raise ReportError(INVALID_REPORT, f"{shown} line {line}: a coverage row names no technology")
|
|
432
|
+
if verdict not in COVERAGE_VERDICTS:
|
|
433
|
+
raise ReportError(
|
|
434
|
+
INVALID_REPORT,
|
|
435
|
+
f"{shown} line {line}: coverage verdict {verdict!r} for `{tech}` is not one of: "
|
|
436
|
+
f"{', '.join(COVERAGE_VERDICTS)}",
|
|
437
|
+
)
|
|
438
|
+
coverage.append({"technology": tech, "verdict": verdict})
|
|
439
|
+
|
|
440
|
+
verdict_header = "| " + " | ".join(reader.VERDICT_TABLE_HEADER) + " |"
|
|
441
|
+
found, rows, duplicate_line = reader.read_verdict_table(body, first_line)
|
|
442
|
+
if not found:
|
|
443
|
+
raise ReportError(INVALID_REPORT, f"{shown} has no `{verdict_header}` table under `## Integration Verdicts`")
|
|
444
|
+
if duplicate_line is not None:
|
|
445
|
+
raise ReportError(
|
|
446
|
+
INVALID_REPORT, f"{shown} holds the `{verdict_header}` table twice (again at line {duplicate_line})"
|
|
447
|
+
)
|
|
448
|
+
integration = []
|
|
449
|
+
for line, (lib_a, lib_b, verdict, _rationale) in rows:
|
|
450
|
+
if not lib_a or not lib_b:
|
|
451
|
+
raise ReportError(INVALID_REPORT, f"{shown} line {line}: a verdict row names fewer than two libraries")
|
|
452
|
+
if verdict not in INTEGRATION_VERDICTS:
|
|
453
|
+
raise ReportError(
|
|
454
|
+
INVALID_REPORT,
|
|
455
|
+
f"{shown} line {line}: verdict {verdict!r} for `{lib_a}` and `{lib_b}` is not one of: "
|
|
456
|
+
f"{', '.join(INTEGRATION_VERDICTS)}",
|
|
457
|
+
)
|
|
458
|
+
integration.append({"libA": lib_a, "libB": lib_b, "verdict": verdict})
|
|
459
|
+
|
|
460
|
+
tiers = None
|
|
461
|
+
table = _columns(_section_tables(body, TIERS_SECTION, first_line), TIER_COLUMNS) if with_tiers else None
|
|
462
|
+
if table is not None and table[1]:
|
|
463
|
+
(skill_at, tier_at), rows = table
|
|
464
|
+
tiers = {}
|
|
465
|
+
for line, cells in rows:
|
|
466
|
+
skill, tier = _cell(cells, skill_at), _cell(cells, tier_at)
|
|
467
|
+
if not skill:
|
|
468
|
+
raise ReportError(INVALID_REPORT, f"{shown} line {line}: an `## {TIERS_SECTION}` row names no skill")
|
|
469
|
+
if tier not in TIERS:
|
|
470
|
+
raise ReportError(
|
|
471
|
+
UNKNOWN_TIER,
|
|
472
|
+
f"{shown} line {line}: skill `{skill}` has evidence_tier {tier!r}, not one of: "
|
|
473
|
+
f"{', '.join(TIERS)}",
|
|
474
|
+
)
|
|
475
|
+
tiers[skill] = tier
|
|
476
|
+
return {"coverage": coverage, "integration": integration}, tiers
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def read_inventory_tiers(path):
|
|
480
|
+
"""`currentTiers` from an skf-enumerate-stack-skills.py inventory file.
|
|
481
|
+
|
|
482
|
+
Maps each skills[] entry's name to its evidence_tier, which compute()
|
|
483
|
+
checks against the tier scale. Raises ReportError (INVALID_INPUT) for a
|
|
484
|
+
file that cannot be read, is not JSON or holds no skills[] list of named
|
|
485
|
+
entries.
|
|
486
|
+
"""
|
|
487
|
+
shown = f"--inventory `{path}`"
|
|
488
|
+
try:
|
|
489
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
490
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
491
|
+
raise ReportError(INVALID_INPUT, f"{shown} cannot be read: {exc}") from exc
|
|
492
|
+
except json.JSONDecodeError as exc:
|
|
493
|
+
raise ReportError(INVALID_INPUT, f"{shown} is not JSON: {exc.msg}") from exc
|
|
494
|
+
skills = data.get("skills") if isinstance(data, dict) else None
|
|
495
|
+
if not isinstance(skills, list):
|
|
496
|
+
raise ReportError(INVALID_INPUT, f"{shown} holds no `skills` list")
|
|
497
|
+
tiers = {}
|
|
498
|
+
for i, entry in enumerate(skills):
|
|
499
|
+
name = entry.get("name") if isinstance(entry, dict) else None
|
|
500
|
+
if not isinstance(name, str) or not name.strip():
|
|
501
|
+
raise ReportError(INVALID_INPUT, f"{shown} skills[{i}] has no `name`")
|
|
502
|
+
tiers[name.strip()] = entry.get("evidence_tier")
|
|
503
|
+
return tiers
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def run(data, previous_report=None, current_report=None, no_tiers=False, reader=None, inventory=None):
|
|
507
|
+
"""The delta for one call: the JSON input, each side a report flag names read from that report.
|
|
508
|
+
|
|
509
|
+
`reader` is the shared report reader (load_reader()), needed only with a
|
|
510
|
+
report path. `inventory`, an skf-enumerate-stack-skills.py inventory file,
|
|
511
|
+
gives `currentTiers`.
|
|
512
|
+
"""
|
|
513
|
+
if not isinstance(data, dict):
|
|
514
|
+
return make_error("Input must be a JSON object")
|
|
515
|
+
inp = dict(data)
|
|
516
|
+
recorded = None
|
|
517
|
+
for side, path in (("previous", previous_report), ("current", current_report)):
|
|
518
|
+
if path is None:
|
|
519
|
+
continue
|
|
520
|
+
taken = ("previous", "previousTiers") if side == "previous" else ("current",)
|
|
521
|
+
clash = [key for key in taken if key in inp]
|
|
522
|
+
if clash:
|
|
523
|
+
return make_error(f"`{clash[0]}` comes from --{side}-report: leave it out of the JSON")
|
|
524
|
+
with_tiers = side == "previous" and not no_tiers
|
|
525
|
+
try:
|
|
526
|
+
inp[side], tiers = read_report(path, reader, with_tiers)
|
|
527
|
+
except ReportError as exc:
|
|
528
|
+
return {**make_error(str(exc), exc.code), "report": side, "path": path}
|
|
529
|
+
if with_tiers:
|
|
530
|
+
recorded = tiers is not None
|
|
531
|
+
if tiers is not None:
|
|
532
|
+
inp["previousTiers"] = tiers
|
|
533
|
+
if inventory is not None and not no_tiers:
|
|
534
|
+
if "currentTiers" in inp:
|
|
535
|
+
return make_error("`currentTiers` comes from --inventory: leave it out of the JSON")
|
|
536
|
+
try:
|
|
537
|
+
inp["currentTiers"] = read_inventory_tiers(inventory)
|
|
538
|
+
except ReportError as exc:
|
|
539
|
+
return {**make_error(str(exc), exc.code), "path": inventory}
|
|
540
|
+
if no_tiers:
|
|
541
|
+
inp.pop("previousTiers", None)
|
|
542
|
+
inp.pop("currentTiers", None)
|
|
543
|
+
result = compute(inp)
|
|
544
|
+
if "code" not in result:
|
|
545
|
+
result["previousTiersRecorded"] = recorded
|
|
191
546
|
return result
|
|
192
547
|
|
|
193
548
|
|
|
@@ -197,23 +552,46 @@ def compute(inp):
|
|
|
197
552
|
def _build_parser():
|
|
198
553
|
parser = argparse.ArgumentParser(
|
|
199
554
|
prog="skf-report-delta",
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
),
|
|
555
|
+
# The module docstring is the contract synthesize.md points --help at:
|
|
556
|
+
# the rankings, the one tier scale, how a report is read, and the input,
|
|
557
|
+
# output and error schemas.
|
|
558
|
+
description=__doc__,
|
|
205
559
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
206
560
|
epilog=(
|
|
207
|
-
"
|
|
561
|
+
"Examples:\n"
|
|
208
562
|
" uv run skf-report-delta.py "
|
|
209
563
|
"'{\"previous\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Missing\"}]},"
|
|
210
|
-
"\"current\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Covered\"}]}}'"
|
|
564
|
+
"\"current\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Covered\"}]}}'\n"
|
|
565
|
+
" uv run skf-report-delta.py "
|
|
566
|
+
"--previous-report forge-data/feasibility-report-my-app-20260101-080000.md "
|
|
567
|
+
"--current-report forge-data/feasibility-report-my-app-20260930-120000.md "
|
|
568
|
+
"--inventory skill-inventory.json"
|
|
211
569
|
),
|
|
212
570
|
)
|
|
213
571
|
src = parser.add_mutually_exclusive_group()
|
|
214
572
|
src.add_argument("json_input", nargs="?", help="JSON object as a positional argument.")
|
|
215
573
|
src.add_argument("--json-input", dest="json_input_flag", help="JSON object passed via flag.")
|
|
216
574
|
src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
|
|
575
|
+
parser.add_argument(
|
|
576
|
+
"--previous-report",
|
|
577
|
+
metavar="PATH",
|
|
578
|
+
help="read `previous` and `previousTiers` from this earlier feasibility report",
|
|
579
|
+
)
|
|
580
|
+
parser.add_argument(
|
|
581
|
+
"--current-report",
|
|
582
|
+
metavar="PATH",
|
|
583
|
+
help="read `current` from this run's feasibility report",
|
|
584
|
+
)
|
|
585
|
+
parser.add_argument(
|
|
586
|
+
"--inventory",
|
|
587
|
+
metavar="PATH",
|
|
588
|
+
help="read `currentTiers` from this skf-enumerate-stack-skills.py inventory (each skill's evidence_tier)",
|
|
589
|
+
)
|
|
590
|
+
parser.add_argument(
|
|
591
|
+
"--no-tiers",
|
|
592
|
+
action="store_true",
|
|
593
|
+
help="compare no tier: ignore both tier maps, the previous report's tier table and --inventory",
|
|
594
|
+
)
|
|
217
595
|
return parser
|
|
218
596
|
|
|
219
597
|
|
|
@@ -232,12 +610,15 @@ def main(argv=None):
|
|
|
232
610
|
args = parser.parse_args(argv)
|
|
233
611
|
raw = _resolve_input(args)
|
|
234
612
|
if not raw.strip():
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
613
|
+
if args.previous_report is None or args.current_report is None:
|
|
614
|
+
parser.print_usage(file=sys.stderr)
|
|
615
|
+
print(
|
|
616
|
+
"error: no input provided (positional arg, --json-input, or --stdin; "
|
|
617
|
+
"it may be left out only with both --previous-report and --current-report)",
|
|
618
|
+
file=sys.stderr,
|
|
619
|
+
)
|
|
620
|
+
return 1
|
|
621
|
+
raw = "{}"
|
|
241
622
|
|
|
242
623
|
try:
|
|
243
624
|
data = json.loads(raw)
|
|
@@ -245,12 +626,35 @@ def main(argv=None):
|
|
|
245
626
|
print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
|
|
246
627
|
return 1
|
|
247
628
|
|
|
248
|
-
|
|
629
|
+
reader = None
|
|
630
|
+
if args.previous_report is not None or args.current_report is not None:
|
|
631
|
+
reader = load_reader()
|
|
632
|
+
if reader is None:
|
|
633
|
+
message = f"the shared report reader is missing: {SHARED_READER}"
|
|
634
|
+
print(json.dumps(make_error(message, HELPER_MISSING), indent=2))
|
|
635
|
+
return 1
|
|
636
|
+
|
|
637
|
+
result = run(data, args.previous_report, args.current_report, args.no_tiers, reader, args.inventory)
|
|
249
638
|
print(json.dumps(result, indent=2))
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
639
|
+
return 2 if "code" in result else 0
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _force_utf8(*streams) -> None:
|
|
643
|
+
"""Reconfigure the given streams to UTF-8, keeping each stream's error handler.
|
|
644
|
+
|
|
645
|
+
A Windows console pipes them as cp1252, which cannot print every character
|
|
646
|
+
of the --help text, so --help would stop with UnicodeEncodeError, and which
|
|
647
|
+
garbles UTF-8 JSON piped into --stdin.
|
|
648
|
+
"""
|
|
649
|
+
for stream in streams:
|
|
650
|
+
if hasattr(stream, "reconfigure"):
|
|
651
|
+
errors = getattr(stream, "errors", None)
|
|
652
|
+
if errors is None:
|
|
653
|
+
stream.reconfigure(encoding="utf-8")
|
|
654
|
+
else:
|
|
655
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
253
656
|
|
|
254
657
|
|
|
255
658
|
if __name__ == "__main__":
|
|
659
|
+
_force_utf8(sys.stdin, sys.stdout, sys.stderr)
|
|
256
660
|
raise SystemExit(main())
|