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
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = ["pyyaml"]
|
|
4
|
+
# ///
|
|
5
|
+
"""Campaign Quality Gate: the campaign's quality bar, applied by one script.
|
|
6
|
+
|
|
7
|
+
The campaign holds a quality gate (`campaign.quality_gate` in state: `hard`,
|
|
8
|
+
`soft_target`, `soft_fallback`), and the directive's `## Quality Overrides`
|
|
9
|
+
and `## Skip List` sections adjust it while the campaign runs. Which gate a
|
|
10
|
+
skill gets, whether a test result settles it, and which completed skills
|
|
11
|
+
clear the gate each have one right answer, so they are computed here and the
|
|
12
|
+
step files read the result.
|
|
13
|
+
|
|
14
|
+
Precedence, highest first: the directive's Quality Overrides (a line for one
|
|
15
|
+
skill beats a campaign-wide line), then the gate in state, which `check`
|
|
16
|
+
settled at step-01 from a campaign brief's `quality_gate` and the
|
|
17
|
+
customize.toml values.
|
|
18
|
+
|
|
19
|
+
Subcommands:
|
|
20
|
+
|
|
21
|
+
check Settle and validate the gate step-01 writes to state: each of
|
|
22
|
+
`hard`, `soft_target` and `soft_fallback` from the brief's
|
|
23
|
+
`quality_gate` (--brief-file) when it gives one, else from the
|
|
24
|
+
customize.toml values passed in. `hard` must be
|
|
25
|
+
`zero-critical-high` (test-skill's own hard gate, the only one
|
|
26
|
+
there is); `soft_target` and `soft_fallback` are numbers from 0
|
|
27
|
+
to 100, the fallback not above the target.
|
|
28
|
+
resolve One Tier A skill's run at step-05: whether the directive's Skip
|
|
29
|
+
List names it, the threshold its test run gets (`threshold`, the
|
|
30
|
+
effective `soft_target`) and, with --brief-file, the inputs
|
|
31
|
+
brief-skill takes for it (`brief_skill`: `target_repo` from the
|
|
32
|
+
brief, `target_version` the skill's pin, the brief target's
|
|
33
|
+
language and scope hints), so no step reads them out of prose.
|
|
34
|
+
record Settle a Tier A skill from the SKF_TEST_RESULT_JSON line its
|
|
35
|
+
test run printed (stdin, or the file --result names). Only the
|
|
36
|
+
verdict PASS completes the skill; FAIL, INCONCLUSIVE,
|
|
37
|
+
pass-with-drift, an error envelope and a missing or unreadable
|
|
38
|
+
line fail it, with the reason. test-skill already applied the
|
|
39
|
+
threshold, its caps and its 80% floor fallback, so no score is
|
|
40
|
+
compared here.
|
|
41
|
+
classify Classify every completed skill against its effective gate for
|
|
42
|
+
the step-10 export: `pass` (score at or above soft_target),
|
|
43
|
+
`fallback` (at or above soft_fallback) or `fail` (below it, or
|
|
44
|
+
no score at all). Only pass and fallback skills export. A Tier A
|
|
45
|
+
score is test-skill's; a Tier B score is the skill-check score
|
|
46
|
+
quick-skill records, not a test-skill score. Each row's
|
|
47
|
+
`export_name` is the skill folder skf-export-skill resolves: the
|
|
48
|
+
last part of the `skill_path` the build recorded
|
|
49
|
+
(`{skills_output_folder}/{skill-name}/{version}/{skill-name}`),
|
|
50
|
+
which quick-skill names after the library for a Tier B skill,
|
|
51
|
+
else the campaign name.
|
|
52
|
+
|
|
53
|
+
Directive format (a section runs to the next heading of level 1 or 2; each
|
|
54
|
+
entry is one list item, backticks around a name are dropped):
|
|
55
|
+
|
|
56
|
+
## Quality Overrides
|
|
57
|
+
- soft_target: 85 campaign-wide
|
|
58
|
+
- soft_fallback: 75 campaign-wide
|
|
59
|
+
- cognee: soft_target 80, soft_fallback 70 for one skill
|
|
60
|
+
|
|
61
|
+
## Skip List
|
|
62
|
+
- cognee: upstream is mid-rewrite a reason after a colon, a dash
|
|
63
|
+
- legacy-lib or in parentheses is optional
|
|
64
|
+
|
|
65
|
+
A list item in these sections that does not read this way is returned in
|
|
66
|
+
`unparsed[]`, never dropped silently: a Quality Overrides item with any other
|
|
67
|
+
text, or that names a key twice, is unparsed and none of its numbers apply. A
|
|
68
|
+
directive path that names no file means no directive.
|
|
69
|
+
|
|
70
|
+
CLI:
|
|
71
|
+
uv run campaign-quality-gate.py check --hard <value> --soft-target <N> --soft-fallback <N> [--brief-file <p>]
|
|
72
|
+
uv run campaign-quality-gate.py resolve --state-file <p> --skill <name> [--directive-file <p>] [--brief-file <p>]
|
|
73
|
+
uv run campaign-quality-gate.py record --skill <name> [--result <p>] (stdin otherwise)
|
|
74
|
+
uv run campaign-quality-gate.py classify --state-file <p> [--directive-file <p>]
|
|
75
|
+
|
|
76
|
+
Output (one JSON object on stdout):
|
|
77
|
+
check: {"hard", "soft_target", "soft_fallback"}
|
|
78
|
+
resolve: {"skill", "skip", "skip_reason", "hard", "soft_target",
|
|
79
|
+
"soft_fallback", "threshold", "overrides": [...],
|
|
80
|
+
"unparsed": [...], "warnings": [...]}, and with --brief-file
|
|
81
|
+
"brief_skill": {"target_repo", "skill_name", "target_version",
|
|
82
|
+
"language_hint", "scope_hint"} (null when the skill has none)
|
|
83
|
+
record: {"skill", "status": "completed|failed", "verdict",
|
|
84
|
+
"quality_score", "threshold", "threshold_fallback", "reason"}
|
|
85
|
+
classify: {"gate": {...}, "skills": [{"name", "tier", "skill_path",
|
|
86
|
+
"export_name", "quality_score", "soft_target", "soft_fallback",
|
|
87
|
+
"verdict", "reason"}],
|
|
88
|
+
"export": [...], "excluded": [{"name", "tier", "quality_score",
|
|
89
|
+
"verdict", "reason"}], "counts": {"pass", "fallback", "fail"},
|
|
90
|
+
"unparsed": [...], "warnings": [...]}
|
|
91
|
+
|
|
92
|
+
Exit codes:
|
|
93
|
+
0 result printed (for record, read `status`)
|
|
94
|
+
2 error, as {"error", "code"} on stderr: STATE_NOT_FOUND,
|
|
95
|
+
STATE_PARSE_ERROR, SKILL_NOT_FOUND, DIRECTIVE_UNREADABLE,
|
|
96
|
+
RESULT_NOT_FOUND, BRIEF_NOT_FOUND, BRIEF_UNREADABLE (missing, or not a
|
|
97
|
+
YAML mapping), TARGET_NOT_FOUND (the brief has no target with a
|
|
98
|
+
repo_url for the skill), or INVALID_GATE (with `errors[]`: an
|
|
99
|
+
unsupported hard gate, a soft value outside 0 to 100, a fallback above
|
|
100
|
+
the target, from the brief, in state or after the directive's overrides)
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
from __future__ import annotations
|
|
104
|
+
|
|
105
|
+
import argparse
|
|
106
|
+
import json
|
|
107
|
+
import re
|
|
108
|
+
import sys
|
|
109
|
+
from pathlib import Path, PurePosixPath
|
|
110
|
+
from typing import Any
|
|
111
|
+
|
|
112
|
+
import yaml
|
|
113
|
+
|
|
114
|
+
HARD_GATES = ("zero-critical-high",)
|
|
115
|
+
SOFT_KEYS = ("soft_target", "soft_fallback")
|
|
116
|
+
GATE_KEYS = ("hard", *SOFT_KEYS)
|
|
117
|
+
# Each brief-skill hint and the brief target fields it is read from, in order.
|
|
118
|
+
HINT_FIELDS = {"language_hint": ("language_hint", "language"), "scope_hint": ("scope_hint", "scope")}
|
|
119
|
+
|
|
120
|
+
HEADING_RE = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$")
|
|
121
|
+
ITEM_RE = re.compile(r"^\s*(?:[-*+]|\d+[.)])\s+(.*\S)\s*$")
|
|
122
|
+
PAIR_RE = re.compile(r"\b(soft_target|soft_fallback)\b\s*[:=]?\s*(-?\d+(?:\.\d+)?)\s*%?", re.I)
|
|
123
|
+
# The whole override text: one or more PAIR_RE pairs separated by commas, nothing else.
|
|
124
|
+
PAIRS_RE = re.compile(
|
|
125
|
+
r"(?:soft_target|soft_fallback)\b\s*[:=]?\s*-?\d+(?:\.\d+)?\s*%?"
|
|
126
|
+
r"(?:\s*,\s*(?:soft_target|soft_fallback)\b\s*[:=]?\s*-?\d+(?:\.\d+)?\s*%?)*",
|
|
127
|
+
re.I,
|
|
128
|
+
)
|
|
129
|
+
SKILL_LINE_RE = re.compile(r"^([^\s:]+)\s*:\s*(.+)$")
|
|
130
|
+
SKIP_NAME_RE = re.compile(r"^([^\s:(]+)(.*)$")
|
|
131
|
+
TS_PREFIX = "SKF_TEST_RESULT_JSON:"
|
|
132
|
+
ANY_ENVELOPE_RE = re.compile(r"SKF_[A-Z_]+_RESULT_JSON:")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class GateError(Exception):
|
|
136
|
+
"""A failure the CLI reports as {"error", "code"} on stderr with exit 2."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, message: str, code: str, **extra: Any) -> None:
|
|
139
|
+
super().__init__(message)
|
|
140
|
+
self.code = code
|
|
141
|
+
self.extra = extra
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _number(text: str) -> int | float:
|
|
145
|
+
value = float(text)
|
|
146
|
+
return int(value) if value.is_integer() else value
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _plain(value: Any) -> Any:
|
|
150
|
+
"""A whole float as an int, so a threshold of 90.0 prints as 90."""
|
|
151
|
+
if isinstance(value, float) and value.is_integer():
|
|
152
|
+
return int(value)
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# --------------------------------------------------------------------------
|
|
157
|
+
# Directive
|
|
158
|
+
# --------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _sections(text: str) -> dict[str, list[tuple[int, str]]]:
|
|
162
|
+
"""The list items of each level-2 section, keyed by lower-case title."""
|
|
163
|
+
sections: dict[str, list[tuple[int, str]]] = {}
|
|
164
|
+
current: list[tuple[int, str]] | None = None
|
|
165
|
+
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
166
|
+
heading = HEADING_RE.match(line)
|
|
167
|
+
if heading:
|
|
168
|
+
level = len(heading.group(1))
|
|
169
|
+
if level <= 2:
|
|
170
|
+
title = " ".join(heading.group(2).split()).lower()
|
|
171
|
+
current = sections.setdefault(title, []) if level == 2 else None
|
|
172
|
+
continue
|
|
173
|
+
if current is not None:
|
|
174
|
+
item = ITEM_RE.match(line)
|
|
175
|
+
if item:
|
|
176
|
+
current.append((lineno, item.group(1).replace("`", "").strip()))
|
|
177
|
+
return sections
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def parse_directive(text: str) -> dict[str, Any]:
|
|
181
|
+
"""Read the Skip List and Quality Overrides sections of a directive."""
|
|
182
|
+
sections = _sections(text)
|
|
183
|
+
skip: dict[str, str | None] = {}
|
|
184
|
+
campaign: dict[str, int | float] = {}
|
|
185
|
+
skills: dict[str, dict[str, int | float]] = {}
|
|
186
|
+
unparsed: list[dict[str, Any]] = []
|
|
187
|
+
|
|
188
|
+
for lineno, entry in sections.get("skip list", []):
|
|
189
|
+
match = SKIP_NAME_RE.match(entry)
|
|
190
|
+
if not match:
|
|
191
|
+
unparsed.append({"section": "Skip List", "line": lineno, "text": entry})
|
|
192
|
+
continue
|
|
193
|
+
rest = match.group(2).strip()
|
|
194
|
+
if rest.startswith("(") and rest.endswith(")"):
|
|
195
|
+
rest = rest[1:-1]
|
|
196
|
+
skip[match.group(1)] = re.sub(r"^\W+", "", rest).strip() or None
|
|
197
|
+
|
|
198
|
+
for lineno, entry in sections.get("quality overrides", []):
|
|
199
|
+
first = entry.split(None, 1)[0].rstrip(":=").lower()
|
|
200
|
+
per_skill = SKILL_LINE_RE.match(entry)
|
|
201
|
+
if first in SOFT_KEYS:
|
|
202
|
+
name, text = None, entry
|
|
203
|
+
elif per_skill:
|
|
204
|
+
name, text = per_skill.group(1), per_skill.group(2)
|
|
205
|
+
else:
|
|
206
|
+
name, text = None, ""
|
|
207
|
+
# Only the documented grammar applies: other text or a repeated key
|
|
208
|
+
# leaves the whole item unparsed, so no stray number becomes the gate.
|
|
209
|
+
pairs = PAIR_RE.findall(text) if PAIRS_RE.fullmatch(text) else []
|
|
210
|
+
if len({key.lower() for key, _ in pairs}) < len(pairs):
|
|
211
|
+
pairs = []
|
|
212
|
+
if not pairs:
|
|
213
|
+
unparsed.append({"section": "Quality Overrides", "line": lineno, "text": entry})
|
|
214
|
+
continue
|
|
215
|
+
target = campaign if name is None else skills.setdefault(name, {})
|
|
216
|
+
for key, value in pairs:
|
|
217
|
+
target[key.lower()] = _number(value)
|
|
218
|
+
|
|
219
|
+
return {"skip": skip, "campaign": campaign, "skills": skills, "unparsed": unparsed}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def read_directive(path: str | None) -> dict[str, Any]:
|
|
223
|
+
"""The parsed directive, or an empty one when no path is given or no file is there."""
|
|
224
|
+
if not path or not Path(path).is_file():
|
|
225
|
+
return parse_directive("")
|
|
226
|
+
try:
|
|
227
|
+
text = Path(path).read_bytes().decode("utf-8", errors="replace").lstrip("\ufeff")
|
|
228
|
+
except OSError as exc:
|
|
229
|
+
raise GateError(f"Directive unreadable: {exc}", "DIRECTIVE_UNREADABLE") from exc
|
|
230
|
+
return parse_directive(text)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _unknown_name_warnings(directive: dict[str, Any], names: set[str]) -> list[str]:
|
|
234
|
+
warnings = []
|
|
235
|
+
for name in directive["skip"]:
|
|
236
|
+
if name not in names:
|
|
237
|
+
warnings.append(f"Skip List names `{name}`, which is no skill in this campaign")
|
|
238
|
+
for name in directive["skills"]:
|
|
239
|
+
if name not in names:
|
|
240
|
+
warnings.append(f"Quality Overrides names `{name}`, which is no skill in this campaign")
|
|
241
|
+
return warnings
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# --------------------------------------------------------------------------
|
|
245
|
+
# Gate
|
|
246
|
+
# --------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def gate_errors(gate: dict[str, Any]) -> list[str]:
|
|
250
|
+
"""Why a gate cannot be applied, or [] when it can."""
|
|
251
|
+
errors = []
|
|
252
|
+
hard = gate.get("hard")
|
|
253
|
+
if hard not in HARD_GATES:
|
|
254
|
+
errors.append(
|
|
255
|
+
f"hard gate `{hard}` is not supported: the only hard gate is `zero-critical-high`, "
|
|
256
|
+
"the Critical and High gap check test-skill runs on every skill"
|
|
257
|
+
)
|
|
258
|
+
in_range = []
|
|
259
|
+
for key in SOFT_KEYS:
|
|
260
|
+
value = gate.get(key)
|
|
261
|
+
if _is_number(value) and 0 <= value <= 100:
|
|
262
|
+
in_range.append(key)
|
|
263
|
+
else:
|
|
264
|
+
errors.append(f"`{key}` must be a number from 0 to 100, got {value!r}")
|
|
265
|
+
if len(in_range) == 2 and gate["soft_fallback"] > gate["soft_target"]:
|
|
266
|
+
errors.append(
|
|
267
|
+
f"`soft_fallback` {_plain(gate['soft_fallback'])} is above "
|
|
268
|
+
f"`soft_target` {_plain(gate['soft_target'])}"
|
|
269
|
+
)
|
|
270
|
+
return errors
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _is_number(value: Any) -> bool:
|
|
274
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def effective_gate(
|
|
278
|
+
base: dict[str, Any], directive: dict[str, Any], skill: str | None = None
|
|
279
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
280
|
+
"""The gate after the directive's overrides, and one line per override applied."""
|
|
281
|
+
gate = {"hard": base.get("hard"), **{key: base.get(key) for key in SOFT_KEYS}}
|
|
282
|
+
applied = []
|
|
283
|
+
for key, value in directive["campaign"].items():
|
|
284
|
+
gate[key] = value
|
|
285
|
+
applied.append(f"directive Quality Overrides: {key} {value}")
|
|
286
|
+
if skill is not None:
|
|
287
|
+
for key, value in directive["skills"].get(skill, {}).items():
|
|
288
|
+
gate[key] = value
|
|
289
|
+
applied.append(f"directive Quality Overrides for {skill}: {key} {value}")
|
|
290
|
+
return gate, applied
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _checked(gate: dict[str, Any], applied: list[str]) -> dict[str, Any]:
|
|
294
|
+
errors = gate_errors(gate)
|
|
295
|
+
if errors:
|
|
296
|
+
where = f" (after {'; '.join(applied)})" if applied else ""
|
|
297
|
+
raise GateError(f"The quality gate{where} cannot be applied: " + "; ".join(errors),
|
|
298
|
+
"INVALID_GATE", errors=errors)
|
|
299
|
+
return {key: _plain(value) for key, value in gate.items()}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _load_brief(path: str) -> dict[str, Any]:
|
|
303
|
+
if not Path(path).is_file():
|
|
304
|
+
raise GateError(f"Brief not found: {path}", "BRIEF_NOT_FOUND")
|
|
305
|
+
try:
|
|
306
|
+
brief = yaml.safe_load(Path(path).read_text(encoding="utf-8-sig"))
|
|
307
|
+
except (OSError, UnicodeDecodeError, yaml.YAMLError) as exc:
|
|
308
|
+
raise GateError(f"Brief unreadable: {exc}", "BRIEF_UNREADABLE") from exc
|
|
309
|
+
if not isinstance(brief, dict):
|
|
310
|
+
raise GateError(f"Brief is not a YAML mapping: {path}", "BRIEF_UNREADABLE")
|
|
311
|
+
return brief
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _load_state(path: str) -> dict[str, Any]:
|
|
315
|
+
if not Path(path).is_file():
|
|
316
|
+
raise GateError(f"State file not found: {path}", "STATE_NOT_FOUND")
|
|
317
|
+
try:
|
|
318
|
+
state = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
|
|
319
|
+
except (OSError, UnicodeDecodeError, yaml.YAMLError) as exc:
|
|
320
|
+
raise GateError(f"Failed to parse state file: {exc}", "STATE_PARSE_ERROR") from exc
|
|
321
|
+
if not isinstance(state, dict) or not isinstance(state.get("skills"), list):
|
|
322
|
+
raise GateError("State file is not a campaign state (no skills[] list)", "STATE_PARSE_ERROR")
|
|
323
|
+
campaign = state.get("campaign")
|
|
324
|
+
if not isinstance(campaign, dict) or not isinstance(campaign.get("quality_gate"), dict):
|
|
325
|
+
raise GateError("State file has no campaign.quality_gate", "INVALID_GATE",
|
|
326
|
+
errors=["campaign.quality_gate is missing"])
|
|
327
|
+
return state
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# --------------------------------------------------------------------------
|
|
331
|
+
# Subcommands
|
|
332
|
+
# --------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def check(hard: str, soft_target: str, soft_fallback: str, brief: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
336
|
+
gate: dict[str, Any] = {"hard": hard}
|
|
337
|
+
for key, raw in (("soft_target", soft_target), ("soft_fallback", soft_fallback)):
|
|
338
|
+
try:
|
|
339
|
+
gate[key] = _number(raw)
|
|
340
|
+
except ValueError:
|
|
341
|
+
gate[key] = raw
|
|
342
|
+
applied = []
|
|
343
|
+
brief_gate = (brief or {}).get("quality_gate")
|
|
344
|
+
if brief_gate is not None and not isinstance(brief_gate, dict):
|
|
345
|
+
raise GateError("The brief's quality_gate is not a mapping", "INVALID_GATE",
|
|
346
|
+
errors=["the brief's quality_gate must be a mapping"])
|
|
347
|
+
for key in GATE_KEYS:
|
|
348
|
+
if (brief_gate or {}).get(key) is not None:
|
|
349
|
+
gate[key] = brief_gate[key]
|
|
350
|
+
applied.append(f"campaign brief: {key} {brief_gate[key]}")
|
|
351
|
+
return _checked(gate, applied)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _hint(target: dict[str, Any], fields: tuple[str, ...]) -> str | None:
|
|
355
|
+
for field in fields:
|
|
356
|
+
value = target.get(field)
|
|
357
|
+
if isinstance(value, str) and value.strip():
|
|
358
|
+
return value.strip()
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def brief_skill_inputs(state: dict[str, Any], brief: dict[str, Any], skill: str) -> dict[str, Any]:
|
|
363
|
+
"""The inputs brief-skill takes for one skill: its repository and hints from the
|
|
364
|
+
brief, its pin from state (the step-01 parser accepts only an X.Y.Z Tier A pin)."""
|
|
365
|
+
target = next(
|
|
366
|
+
(t for t in brief.get("targets") or [] if isinstance(t, dict) and t.get("name") == skill), None
|
|
367
|
+
)
|
|
368
|
+
repo_url = (target or {}).get("repo_url")
|
|
369
|
+
if not isinstance(repo_url, str) or not repo_url.strip():
|
|
370
|
+
raise GateError(f"The campaign brief has no target with a repo_url for skill '{skill}'",
|
|
371
|
+
"TARGET_NOT_FOUND")
|
|
372
|
+
entry = next(s for s in state["skills"] if isinstance(s, dict) and s.get("name") == skill)
|
|
373
|
+
pin = entry.get("pin")
|
|
374
|
+
return {
|
|
375
|
+
"target_repo": repo_url.strip(),
|
|
376
|
+
"skill_name": skill,
|
|
377
|
+
"target_version": str(pin) if pin not in (None, "") else None,
|
|
378
|
+
**{hint: _hint(target, fields) for hint, fields in HINT_FIELDS.items()},
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def resolve(
|
|
383
|
+
state: dict[str, Any], directive: dict[str, Any], skill: str, brief: dict[str, Any] | None = None
|
|
384
|
+
) -> dict[str, Any]:
|
|
385
|
+
names = {s.get("name") for s in state["skills"] if isinstance(s, dict)}
|
|
386
|
+
if skill not in names:
|
|
387
|
+
raise GateError(f"Skill '{skill}' not found in state", "SKILL_NOT_FOUND")
|
|
388
|
+
gate, applied = effective_gate(state["campaign"]["quality_gate"], directive, skill)
|
|
389
|
+
gate = _checked(gate, applied)
|
|
390
|
+
skipped = skill in directive["skip"]
|
|
391
|
+
extra = {} if brief is None else {"brief_skill": brief_skill_inputs(state, brief, skill)}
|
|
392
|
+
return {
|
|
393
|
+
"skill": skill,
|
|
394
|
+
"skip": skipped,
|
|
395
|
+
"skip_reason": directive["skip"][skill] if skipped else None,
|
|
396
|
+
**gate,
|
|
397
|
+
"threshold": gate["soft_target"],
|
|
398
|
+
"overrides": applied,
|
|
399
|
+
"unparsed": directive["unparsed"],
|
|
400
|
+
"warnings": _unknown_name_warnings(directive, names),
|
|
401
|
+
**extra,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _envelope(text: str) -> tuple[dict[str, Any] | None, str | None]:
|
|
406
|
+
"""The last test-skill envelope in `text`, or None and why there is none."""
|
|
407
|
+
text = text.lstrip("\ufeff")
|
|
408
|
+
lines = [line for line in text.splitlines() if TS_PREFIX in line]
|
|
409
|
+
if lines:
|
|
410
|
+
payload = lines[-1].split(TS_PREFIX, 1)[1].strip()
|
|
411
|
+
elif not text.strip():
|
|
412
|
+
return None, "no-result"
|
|
413
|
+
elif ANY_ENVELOPE_RE.search(text):
|
|
414
|
+
return None, "not-a-test-result"
|
|
415
|
+
else:
|
|
416
|
+
payload = text.strip()
|
|
417
|
+
try:
|
|
418
|
+
envelope = json.loads(payload)
|
|
419
|
+
except json.JSONDecodeError:
|
|
420
|
+
return None, "result-unreadable"
|
|
421
|
+
if not isinstance(envelope, dict):
|
|
422
|
+
return None, "result-unreadable"
|
|
423
|
+
return envelope, None
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def record(skill: str, text: str) -> dict[str, Any]:
|
|
427
|
+
envelope, problem = _envelope(text)
|
|
428
|
+
out: dict[str, Any] = {
|
|
429
|
+
"skill": skill,
|
|
430
|
+
"status": "failed",
|
|
431
|
+
"verdict": None,
|
|
432
|
+
"quality_score": None,
|
|
433
|
+
"threshold": None,
|
|
434
|
+
"threshold_fallback": False,
|
|
435
|
+
"reason": problem,
|
|
436
|
+
}
|
|
437
|
+
if envelope is None:
|
|
438
|
+
return out
|
|
439
|
+
|
|
440
|
+
verdict = envelope.get("verdict")
|
|
441
|
+
if isinstance(verdict, str):
|
|
442
|
+
verdict = {"PASS_WITH_DRIFT": "pass-with-drift"}.get(verdict.upper(), verdict)
|
|
443
|
+
if verdict.upper() in ("PASS", "FAIL", "INCONCLUSIVE"):
|
|
444
|
+
verdict = verdict.upper()
|
|
445
|
+
score = envelope.get("score")
|
|
446
|
+
out.update(
|
|
447
|
+
verdict=verdict,
|
|
448
|
+
quality_score=_plain(score) if _is_number(score) else None,
|
|
449
|
+
threshold=_plain(envelope["threshold"]) if _is_number(envelope.get("threshold")) else None,
|
|
450
|
+
threshold_fallback=envelope.get("threshold_fallback") is True,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
named = envelope.get("skill_name")
|
|
454
|
+
if isinstance(named, str) and named and named != skill:
|
|
455
|
+
out["reason"] = f"result-for-another-skill: {named}"
|
|
456
|
+
elif envelope.get("status") == "error":
|
|
457
|
+
out["reason"] = envelope.get("halt_reason") or verdict or "error"
|
|
458
|
+
elif verdict == "PASS":
|
|
459
|
+
route = envelope.get("next_workflow")
|
|
460
|
+
if route in (None, "export-skill"):
|
|
461
|
+
out.update(status="completed", reason=None)
|
|
462
|
+
else:
|
|
463
|
+
out["reason"] = f"PASS routed to {route}"
|
|
464
|
+
elif verdict in ("FAIL", "INCONCLUSIVE", "pass-with-drift"):
|
|
465
|
+
out["reason"] = verdict
|
|
466
|
+
else:
|
|
467
|
+
out["reason"] = f"unknown-verdict: {verdict}"
|
|
468
|
+
return out
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def export_name(skill: dict[str, Any]) -> Any:
|
|
472
|
+
"""The skill folder skf-export-skill resolves for a completed skill: the
|
|
473
|
+
last part of the package path its build recorded, else its campaign name."""
|
|
474
|
+
path = skill.get("skill_path")
|
|
475
|
+
if isinstance(path, str) and path.strip():
|
|
476
|
+
return PurePosixPath(path.strip().replace("\\", "/")).name or skill.get("name")
|
|
477
|
+
return skill.get("name")
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def classify(state: dict[str, Any], directive: dict[str, Any]) -> dict[str, Any]:
|
|
481
|
+
base = state["campaign"]["quality_gate"]
|
|
482
|
+
names = {s.get("name") for s in state["skills"] if isinstance(s, dict)}
|
|
483
|
+
rows: list[dict[str, Any]] = []
|
|
484
|
+
for skill in state["skills"]:
|
|
485
|
+
if not isinstance(skill, dict) or skill.get("status") != "completed":
|
|
486
|
+
continue
|
|
487
|
+
name = skill.get("name")
|
|
488
|
+
gate = _checked(*effective_gate(base, directive, name))
|
|
489
|
+
score = skill.get("quality_score")
|
|
490
|
+
if not _is_number(score):
|
|
491
|
+
verdict, reason = "fail", "no quality score to check against the gate"
|
|
492
|
+
elif score >= gate["soft_target"]:
|
|
493
|
+
verdict, reason = "pass", None
|
|
494
|
+
elif score >= gate["soft_fallback"]:
|
|
495
|
+
verdict, reason = "fallback", f"below soft_target {gate['soft_target']}"
|
|
496
|
+
else:
|
|
497
|
+
verdict, reason = "fail", f"below soft_fallback {gate['soft_fallback']}"
|
|
498
|
+
rows.append({
|
|
499
|
+
"name": name,
|
|
500
|
+
"tier": skill.get("tier"),
|
|
501
|
+
"skill_path": skill.get("skill_path"),
|
|
502
|
+
"export_name": export_name(skill),
|
|
503
|
+
"quality_score": _plain(score) if _is_number(score) else None,
|
|
504
|
+
"soft_target": gate["soft_target"],
|
|
505
|
+
"soft_fallback": gate["soft_fallback"],
|
|
506
|
+
"verdict": verdict,
|
|
507
|
+
"reason": reason,
|
|
508
|
+
})
|
|
509
|
+
return {
|
|
510
|
+
"gate": _checked(*effective_gate(base, directive)),
|
|
511
|
+
"skills": rows,
|
|
512
|
+
"export": [r["name"] for r in rows if r["verdict"] in ("pass", "fallback")],
|
|
513
|
+
"excluded": [
|
|
514
|
+
{key: r[key] for key in ("name", "tier", "quality_score", "verdict", "reason")}
|
|
515
|
+
for r in rows if r["verdict"] == "fail"
|
|
516
|
+
],
|
|
517
|
+
"counts": {v: sum(1 for r in rows if r["verdict"] == v) for v in ("pass", "fallback", "fail")},
|
|
518
|
+
"unparsed": directive["unparsed"],
|
|
519
|
+
"warnings": _unknown_name_warnings(directive, names),
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# --------------------------------------------------------------------------
|
|
524
|
+
# CLI
|
|
525
|
+
# --------------------------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _emit(payload: dict[str, Any], stream: Any) -> None:
|
|
529
|
+
stream.write(json.dumps(payload, separators=(",", ":")) + "\n")
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _read_result(path: str | None) -> str:
|
|
533
|
+
if path is None:
|
|
534
|
+
return sys.stdin.buffer.read().decode("utf-8", errors="replace")
|
|
535
|
+
if not Path(path).is_file():
|
|
536
|
+
raise GateError(f"Result file not found: {path}", "RESULT_NOT_FOUND")
|
|
537
|
+
return Path(path).read_bytes().decode("utf-8", errors="replace")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
541
|
+
parser = argparse.ArgumentParser(
|
|
542
|
+
prog="campaign-quality-gate",
|
|
543
|
+
description="Apply the campaign quality gate: check it, resolve it per skill, "
|
|
544
|
+
"settle a test result, classify completed skills for export.",
|
|
545
|
+
)
|
|
546
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
547
|
+
|
|
548
|
+
p_check = sub.add_parser("check", help="validate a quality gate before it is written to state")
|
|
549
|
+
p_check.add_argument("--hard", required=True)
|
|
550
|
+
p_check.add_argument("--soft-target", required=True)
|
|
551
|
+
p_check.add_argument("--soft-fallback", required=True)
|
|
552
|
+
p_check.add_argument("--brief-file", help="campaign brief whose quality_gate wins field by field")
|
|
553
|
+
|
|
554
|
+
p_resolve = sub.add_parser("resolve", help="one skill's gate and Skip List entry")
|
|
555
|
+
p_resolve.add_argument("--state-file", required=True)
|
|
556
|
+
p_resolve.add_argument("--skill", required=True)
|
|
557
|
+
p_resolve.add_argument("--directive-file")
|
|
558
|
+
p_resolve.add_argument("--brief-file", help="campaign brief: adds the brief-skill inputs (brief_skill)")
|
|
559
|
+
|
|
560
|
+
p_record = sub.add_parser("record", help="settle a skill from its SKF_TEST_RESULT_JSON line")
|
|
561
|
+
p_record.add_argument("--skill", required=True)
|
|
562
|
+
p_record.add_argument("--result", help="file holding the envelope line (default: stdin)")
|
|
563
|
+
|
|
564
|
+
p_classify = sub.add_parser("classify", help="classify completed skills for export")
|
|
565
|
+
p_classify.add_argument("--state-file", required=True)
|
|
566
|
+
p_classify.add_argument("--directive-file")
|
|
567
|
+
return parser
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def main(argv: list[str] | None = None) -> int:
|
|
571
|
+
args = build_parser().parse_args(argv)
|
|
572
|
+
try:
|
|
573
|
+
if args.command == "check":
|
|
574
|
+
brief = _load_brief(args.brief_file) if args.brief_file else None
|
|
575
|
+
result = check(args.hard, args.soft_target, args.soft_fallback, brief)
|
|
576
|
+
elif args.command == "resolve":
|
|
577
|
+
state = _load_state(args.state_file)
|
|
578
|
+
brief = _load_brief(args.brief_file) if args.brief_file else None
|
|
579
|
+
result = resolve(state, read_directive(args.directive_file), args.skill, brief)
|
|
580
|
+
elif args.command == "record":
|
|
581
|
+
result = record(args.skill, _read_result(args.result))
|
|
582
|
+
else:
|
|
583
|
+
result = classify(_load_state(args.state_file), read_directive(args.directive_file))
|
|
584
|
+
except GateError as exc:
|
|
585
|
+
_emit({"error": str(exc), "code": exc.code, **exc.extra}, sys.stderr)
|
|
586
|
+
return 2
|
|
587
|
+
_emit(result, sys.stdout)
|
|
588
|
+
return 0
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
if __name__ == "__main__":
|
|
592
|
+
sys.exit(main())
|