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,50 +1,261 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
3
|
-
# dependencies = []
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = ["pyyaml"]
|
|
4
4
|
# ///
|
|
5
|
-
"""Campaign Parse Manifest
|
|
5
|
+
"""Campaign Parse Manifest: validate every campaign target before the first compile.
|
|
6
6
|
|
|
7
|
-
step-01 (Setup)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
step-01 (Setup) takes a campaign's targets from a `--manifest` file, from a
|
|
8
|
+
`--brief` campaign-brief.yaml, or from the operator's answer (a pasted list or
|
|
9
|
+
repository URLs, rewritten as manifest lines and piped in). Every one of those
|
|
10
|
+
sources goes through this script, so a malformed target, a repository URL no
|
|
11
|
+
later stage can read, a name no sub-skill accepts, or two targets with one
|
|
12
|
+
name stop the campaign at Setup, never part-way through a run. step-01 section
|
|
13
|
+
1 documents the manifest format (one `name,repo_url,tier,pin[;dep1,dep2]`
|
|
14
|
+
target per line) and this parser is its single source of truth.
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
Manifest format (one target per line):
|
|
14
17
|
|
|
15
18
|
name,repo_url,tier,pin[;dep1,dep2,...]
|
|
16
19
|
|
|
20
|
+
- `name` may be empty: it is then taken from the repository's name
|
|
17
21
|
- `pin` may be empty (latest): `name,repo_url,tier,` or `name,repo_url,tier`
|
|
18
22
|
- a trailing `;`-segment lists depends_on names (comma-separated)
|
|
19
23
|
- blank lines and lines starting with `#` are skipped
|
|
20
24
|
- `tier` must be `A` or `B`
|
|
21
25
|
|
|
26
|
+
Brief format: a campaign-brief.yaml (templates/campaign-brief-template.yaml).
|
|
27
|
+
Each `targets[]` entry is checked the same way; `tier` defaults to `A`, `pin`
|
|
28
|
+
to null and `depends_on` to [], and every other field a target carries (a
|
|
29
|
+
language or scope hint) is kept. `pin` must be a string or null (quote a pin
|
|
30
|
+
such as 1.10, which YAML reads as a number), and `quality_gate.soft_target`
|
|
31
|
+
and `quality_gate.soft_fallback` must be numbers when the brief gives them.
|
|
32
|
+
|
|
33
|
+
Every source, each rule taken from the script of the stage that reads it:
|
|
34
|
+
- `repo_url` must name a GitHub repository: a URL, an SSH URL or `owner/repo`.
|
|
35
|
+
targets[] carries it as `https://github.com/<owner>/<repo>`, the one shape
|
|
36
|
+
the pin stage (skf-validate-pins.py) and every later stage read; another
|
|
37
|
+
host, or a path inside a repository, is an error
|
|
38
|
+
- `name` must be a skill name: lower-case letters, digits and hyphens,
|
|
39
|
+
starting and ending with a letter or digit, at most 64 characters
|
|
40
|
+
(brief-skill's KEBAB_RE, skf-validate-frontmatter.py's length)
|
|
41
|
+
- a Tier A `pin` must be an X.Y.Z version (brief-skill's `target_version`
|
|
42
|
+
rule): the pin reaches the Tier A build only as that input
|
|
43
|
+
- names are unique (a duplicate is an error, never a silent overwrite)
|
|
44
|
+
- `dangling_depends_on` lists each depends_on name that is no target, and
|
|
45
|
+
`tier_inversions` each Tier A target that depends on a Tier B target (the
|
|
46
|
+
strategy stage rejects both, through dependency_problems, which
|
|
47
|
+
campaign-deps.py imports); neither is an error here, so Setup can ask
|
|
48
|
+
about them before it writes state
|
|
49
|
+
|
|
50
|
+
`--stack-name <state-file>` instead prints the capstone's `stack_name`: the
|
|
51
|
+
state's campaign name by the rule that names a target from its repository,
|
|
52
|
+
cut so create-stack-skill's `-stack` suffix still fits in 64 characters, or
|
|
53
|
+
null when nothing is left of it.
|
|
54
|
+
|
|
22
55
|
CLI:
|
|
23
56
|
uv run campaign-parse-manifest.py <path/to/manifest.txt>
|
|
24
57
|
cat manifest.txt | uv run campaign-parse-manifest.py -
|
|
58
|
+
uv run campaign-parse-manifest.py --brief <path/to/campaign-brief.yaml>
|
|
59
|
+
uv run campaign-parse-manifest.py --stack-name <path/to/_campaign-state.yaml>
|
|
25
60
|
|
|
26
61
|
Output (JSON on stdout):
|
|
27
|
-
{"targets": [{"name","repo_url","tier","pin","depends_on"
|
|
62
|
+
{"targets": [{"name","repo_url","tier","pin","depends_on", ...}],
|
|
63
|
+
"errors": [{"line" | "target" | "field", "message"}],
|
|
64
|
+
"filled_names": [{"name","repo_url"}],
|
|
65
|
+
"dangling_depends_on": [{"skill","depends_on"}],
|
|
66
|
+
"tier_inversions": [{"skill","depends_on"}]}
|
|
67
|
+
--brief adds "brief": {"campaign_name","quality_gate","architecture_doc_path",
|
|
68
|
+
"notes"} with only the keys the brief gives.
|
|
69
|
+
--stack-name prints {"stack_name": "<name>" | null}.
|
|
28
70
|
|
|
29
71
|
Exit codes:
|
|
30
|
-
0 parsed cleanly (no errors)
|
|
31
|
-
1 one or more malformed lines (errors[] populated; targets[]
|
|
32
|
-
|
|
72
|
+
0 parsed cleanly (no errors), or the stack name printed
|
|
73
|
+
1 one or more malformed lines or targets (errors[] populated; targets[]
|
|
74
|
+
omits them)
|
|
75
|
+
2 file error (not found / unreadable / a brief or state that is not a
|
|
76
|
+
YAML mapping)
|
|
33
77
|
"""
|
|
34
78
|
|
|
35
79
|
from __future__ import annotations
|
|
36
80
|
|
|
37
81
|
import argparse
|
|
82
|
+
import importlib.util
|
|
38
83
|
import json
|
|
84
|
+
import re
|
|
39
85
|
import sys
|
|
40
86
|
from pathlib import Path
|
|
41
|
-
from typing import Any, Dict, List
|
|
87
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
88
|
+
|
|
89
|
+
import yaml
|
|
90
|
+
|
|
91
|
+
BRIEF_KEYS = ("campaign_name", "quality_gate", "architecture_doc_path", "notes")
|
|
92
|
+
TARGET_KEYS = ("name", "repo_url", "tier", "pin", "depends_on")
|
|
93
|
+
SHARED_SCRIPTS = Path(__file__).resolve().parent.parent.parent / "shared" / "scripts"
|
|
94
|
+
STACK_SUFFIX = "-stack"
|
|
95
|
+
# An owner or repository segment of a GitHub URL.
|
|
96
|
+
SEGMENT_RE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _shared(file_name: str) -> Any:
|
|
100
|
+
spec = importlib.util.spec_from_file_location(file_name[:-3].replace("-", "_"), SHARED_SCRIPTS / file_name)
|
|
101
|
+
module = importlib.util.module_from_spec(spec)
|
|
102
|
+
spec.loader.exec_module(module)
|
|
103
|
+
return module
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# The rules the later stages apply, read from their own scripts so Setup
|
|
107
|
+
# cannot accept a target they refuse.
|
|
108
|
+
_PINS = _shared("skf-validate-pins.py")
|
|
109
|
+
_BRIEF_INPUTS = _shared("skf-validate-brief-inputs.py")
|
|
110
|
+
GITHUB_URL_RE = _PINS._GITHUB_URL_RE
|
|
111
|
+
KEBAB_RE = _BRIEF_INPUTS.KEBAB_RE
|
|
112
|
+
VERSION_RE = _BRIEF_INPUTS.SEMVER_RE
|
|
113
|
+
MAX_NAME = _shared("skf-validate-frontmatter.py").MAX_SKILL_NAME_LENGTH
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def canonical_repo_url(repo_url: str) -> Tuple[Optional[str], Optional[str]]:
|
|
117
|
+
"""(`https://github.com/<owner>/<repo>`, None) for a GitHub repository, else (None, why not).
|
|
118
|
+
|
|
119
|
+
Reads a GitHub URL (http or https, `www.` and `.git` allowed), an SSH URL
|
|
120
|
+
(`git@github.com:o/r` or `ssh://git@github.com/o/r`) and `owner/repo`.
|
|
121
|
+
"""
|
|
122
|
+
url = repo_url.strip()
|
|
123
|
+
ssh = re.match(r"^(?:ssh://)?git@([^:/\s]+)[:/](.*)$", url)
|
|
124
|
+
if ssh:
|
|
125
|
+
host, path = ssh.group(1), ssh.group(2)
|
|
126
|
+
else:
|
|
127
|
+
rest = re.sub(r"^https?://", "", url, flags=re.I)
|
|
128
|
+
if "://" in rest:
|
|
129
|
+
return None, f"`repo_url` `{repo_url}` is no GitHub URL: use https://github.com/<owner>/<repo>"
|
|
130
|
+
first, _, path = rest.partition("/")
|
|
131
|
+
if rest != url or "." in first:
|
|
132
|
+
host = first
|
|
133
|
+
else:
|
|
134
|
+
host, path = "github.com", rest
|
|
135
|
+
host = host.lower()
|
|
136
|
+
if host.startswith("www."):
|
|
137
|
+
host = host[4:]
|
|
138
|
+
if host != "github.com":
|
|
139
|
+
return None, (
|
|
140
|
+
f"`repo_url` `{repo_url}` is not on GitHub: the pin and provenance stages "
|
|
141
|
+
"read GitHub repositories only"
|
|
142
|
+
)
|
|
143
|
+
parts = [p for p in path.strip().strip("/").split("/") if p]
|
|
144
|
+
if parts and parts[-1].lower().endswith(".git"):
|
|
145
|
+
parts[-1] = parts[-1][:-4]
|
|
146
|
+
if len(parts) < 2 or not all(SEGMENT_RE.match(p) and p.strip(".") for p in parts[:2]):
|
|
147
|
+
return None, f"`repo_url` `{repo_url}` names no repository (use a GitHub URL or `owner/repo`)"
|
|
148
|
+
canonical = f"https://github.com/{parts[0]}/{parts[1]}"
|
|
149
|
+
if len(parts) > 2:
|
|
150
|
+
return None, (
|
|
151
|
+
f"`repo_url` `{repo_url}` names a path inside a repository: give the repository "
|
|
152
|
+
f"itself, `{canonical}`"
|
|
153
|
+
)
|
|
154
|
+
if not GITHUB_URL_RE.match(canonical):
|
|
155
|
+
return None, f"`repo_url` `{repo_url}` names no repository (use a GitHub URL or `owner/repo`)"
|
|
156
|
+
return canonical, None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def name_from_repo(repo: str) -> str:
|
|
160
|
+
"""A skill name from a repository name: lower case, runs of other characters as `-`."""
|
|
161
|
+
return re.sub(r"[^a-z0-9]+", "-", repo.lower()).strip("-")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def is_skill_name(name: str) -> bool:
|
|
165
|
+
return bool(KEBAB_RE.match(name)) and len(name) <= MAX_NAME
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def stack_name(campaign_name: Any) -> Optional[str]:
|
|
169
|
+
"""The capstone's stack_name: the campaign name as a skill name, short enough
|
|
170
|
+
for create-stack-skill to append `-stack` within MAX_NAME; None when empty."""
|
|
171
|
+
name = name_from_repo(campaign_name if isinstance(campaign_name, str) else "")
|
|
172
|
+
room = MAX_NAME if name.endswith(STACK_SUFFIX) else MAX_NAME - len(STACK_SUFFIX)
|
|
173
|
+
return name[:room].rstrip("-") or None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def dependency_problems(skills: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, str]]]:
|
|
177
|
+
"""Each depends_on name that is no skill, and each Tier A skill that depends on a Tier B skill.
|
|
178
|
+
|
|
179
|
+
The one statement of both rules: campaign-deps.py imports it for the
|
|
180
|
+
strategy stage, so Setup and Strategy report the same entries. Tier B
|
|
181
|
+
skills are built in the batch stage, after the skill loop that builds
|
|
182
|
+
Tier A skills, so a Tier A skill could never pass that dependency gate.
|
|
183
|
+
"""
|
|
184
|
+
tiers = {s.get("name"): s.get("tier") for s in skills}
|
|
185
|
+
dangling: List[Dict[str, str]] = []
|
|
186
|
+
inversions: List[Dict[str, str]] = []
|
|
187
|
+
for skill in skills:
|
|
188
|
+
for dep in skill.get("depends_on") or []:
|
|
189
|
+
if dep not in tiers:
|
|
190
|
+
dangling.append({"skill": skill.get("name"), "depends_on": dep})
|
|
191
|
+
elif skill.get("tier") == "A" and tiers[dep] == "B":
|
|
192
|
+
inversions.append({"skill": skill.get("name"), "depends_on": dep})
|
|
193
|
+
return {"dangling_depends_on": dangling, "tier_inversions": inversions}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class _Collector:
|
|
197
|
+
"""Checks targets one at a time and keeps the results of the whole set."""
|
|
198
|
+
|
|
199
|
+
def __init__(self) -> None:
|
|
200
|
+
self.targets: List[Dict[str, Any]] = []
|
|
201
|
+
self.errors: List[Dict[str, Any]] = []
|
|
202
|
+
self.filled: List[Dict[str, str]] = []
|
|
203
|
+
self.seen: set = set()
|
|
204
|
+
|
|
205
|
+
def error(self, where: Dict[str, Any], message: str) -> None:
|
|
206
|
+
self.errors.append({**where, "message": message})
|
|
207
|
+
|
|
208
|
+
def add(self, where: Dict[str, int], target: Dict[str, Any]) -> None:
|
|
209
|
+
name, repo_url, tier, pin = target["name"], target["repo_url"], target["tier"], target["pin"]
|
|
210
|
+
if not repo_url:
|
|
211
|
+
label = f"`{name}`" if name else "target"
|
|
212
|
+
self.error(where, f"{label} has empty `repo_url`")
|
|
213
|
+
return
|
|
214
|
+
canonical, problem = canonical_repo_url(repo_url)
|
|
215
|
+
if canonical is None:
|
|
216
|
+
self.error(where, problem)
|
|
217
|
+
return
|
|
218
|
+
target["repo_url"] = canonical
|
|
219
|
+
if not name:
|
|
220
|
+
name = name_from_repo(canonical.rsplit("/", 1)[1])
|
|
221
|
+
if not name:
|
|
222
|
+
self.error(where, f"empty `name`, and none can be taken from `{repo_url}`")
|
|
223
|
+
return
|
|
224
|
+
target["name"] = name
|
|
225
|
+
self.filled.append({"name": name, "repo_url": canonical})
|
|
226
|
+
if not is_skill_name(name):
|
|
227
|
+
self.error(where, (
|
|
228
|
+
f"`{name}` is no skill name: use lower-case letters, digits and hyphens, starting "
|
|
229
|
+
f"and ending with a letter or digit, at most {MAX_NAME} characters"
|
|
230
|
+
))
|
|
231
|
+
return
|
|
232
|
+
if tier not in ("A", "B"):
|
|
233
|
+
self.error(where, f"`{name}` has invalid tier `{tier}` (must be A or B)")
|
|
234
|
+
return
|
|
235
|
+
if tier == "A" and pin is not None and not VERSION_RE.match(pin):
|
|
236
|
+
self.error(where, (
|
|
237
|
+
f"`{name}` is Tier A with pin `{pin}`, which is no X.Y.Z version: brief-skill builds "
|
|
238
|
+
"a Tier A skill from a release version only (write it in full, such as `1.2.0` or "
|
|
239
|
+
"`v1.2.0`, or leave the pin empty for the latest release)"
|
|
240
|
+
))
|
|
241
|
+
return
|
|
242
|
+
if name in self.seen:
|
|
243
|
+
self.error(where, f"duplicate target name `{name}`")
|
|
244
|
+
return
|
|
245
|
+
self.seen.add(name)
|
|
246
|
+
self.targets.append(target)
|
|
247
|
+
|
|
248
|
+
def result(self) -> Dict[str, Any]:
|
|
249
|
+
return {
|
|
250
|
+
"targets": self.targets,
|
|
251
|
+
"errors": self.errors,
|
|
252
|
+
"filled_names": self.filled,
|
|
253
|
+
**dependency_problems(self.targets),
|
|
254
|
+
}
|
|
42
255
|
|
|
43
256
|
|
|
44
257
|
def parse_manifest_text(text: str) -> Dict[str, Any]:
|
|
45
|
-
|
|
46
|
-
errors: List[Dict[str, Any]] = []
|
|
47
|
-
seen: set = set()
|
|
258
|
+
found = _Collector()
|
|
48
259
|
|
|
49
260
|
for lineno, raw in enumerate(text.splitlines(), start=1):
|
|
50
261
|
line = raw.strip()
|
|
@@ -54,64 +265,160 @@ def parse_manifest_text(text: str) -> Dict[str, Any]:
|
|
|
54
265
|
body, sep, deps_part = line.partition(";")
|
|
55
266
|
fields = [f.strip() for f in body.split(",")]
|
|
56
267
|
if len(fields) < 3:
|
|
57
|
-
|
|
268
|
+
found.error({"line": lineno}, f"expected `name,repo_url,tier[,pin]`, got {len(fields)} field(s)")
|
|
58
269
|
continue
|
|
59
270
|
|
|
60
271
|
name, repo_url, tier = fields[0], fields[1], fields[2]
|
|
61
272
|
pin = fields[3] if len(fields) >= 4 and fields[3] != "" else None
|
|
273
|
+
depends_on = [d.strip() for d in deps_part.split(",") if d.strip()] if sep else []
|
|
274
|
+
found.add(
|
|
275
|
+
{"line": lineno},
|
|
276
|
+
{"name": name, "repo_url": repo_url, "tier": tier, "pin": pin, "depends_on": depends_on},
|
|
277
|
+
)
|
|
62
278
|
|
|
63
|
-
|
|
64
|
-
errors.append({"line": lineno, "message": "empty `name`"})
|
|
65
|
-
continue
|
|
66
|
-
if not repo_url:
|
|
67
|
-
errors.append({"line": lineno, "message": f"`{name}` has empty `repo_url`"})
|
|
68
|
-
continue
|
|
69
|
-
if tier not in ("A", "B"):
|
|
70
|
-
errors.append({"line": lineno, "message": f"`{name}` has invalid tier `{tier}` (must be A or B)"})
|
|
71
|
-
continue
|
|
72
|
-
if name in seen:
|
|
73
|
-
errors.append({"line": lineno, "message": f"duplicate target name `{name}`"})
|
|
74
|
-
continue
|
|
75
|
-
seen.add(name)
|
|
279
|
+
return found.result()
|
|
76
280
|
|
|
77
|
-
depends_on = [d.strip() for d in deps_part.split(",") if d.strip()] if sep else []
|
|
78
281
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
282
|
+
def _brief_target(found: _Collector, index: int, entry: Any) -> None:
|
|
283
|
+
where = {"target": index}
|
|
284
|
+
if not isinstance(entry, dict):
|
|
285
|
+
found.error(where, "a target must be a mapping with at least `repo_url`")
|
|
286
|
+
return
|
|
287
|
+
name = entry.get("name")
|
|
288
|
+
repo_url = entry.get("repo_url")
|
|
289
|
+
tier = entry.get("tier", "A")
|
|
290
|
+
pin = entry.get("pin")
|
|
291
|
+
depends_on = entry.get("depends_on")
|
|
292
|
+
if name is not None and not isinstance(name, str):
|
|
293
|
+
found.error(where, f"`name` must be a string, got {name!r}")
|
|
294
|
+
return
|
|
295
|
+
if repo_url is not None and not isinstance(repo_url, str):
|
|
296
|
+
found.error(where, f"`repo_url` must be a string, got {repo_url!r}")
|
|
297
|
+
return
|
|
298
|
+
if pin is not None and not isinstance(pin, str):
|
|
299
|
+
found.error(where, f"`pin` must be a string or null, got {pin!r} (quote a version pin)")
|
|
300
|
+
return
|
|
301
|
+
if depends_on is None:
|
|
302
|
+
depends_on = []
|
|
303
|
+
if not isinstance(depends_on, list) or not all(isinstance(d, str) and d.strip() for d in depends_on):
|
|
304
|
+
found.error(where, "`depends_on` must be a list of target names")
|
|
305
|
+
return
|
|
306
|
+
target = {
|
|
307
|
+
"name": (name or "").strip(),
|
|
308
|
+
"repo_url": (repo_url or "").strip(),
|
|
309
|
+
"tier": tier,
|
|
310
|
+
"pin": (pin.strip() or None) if isinstance(pin, str) else None,
|
|
311
|
+
"depends_on": [d.strip() for d in depends_on],
|
|
312
|
+
}
|
|
313
|
+
target.update({k: v for k, v in entry.items() if k not in TARGET_KEYS})
|
|
314
|
+
found.add(where, target)
|
|
82
315
|
|
|
83
|
-
return {"targets": targets, "errors": errors}
|
|
84
316
|
|
|
317
|
+
def parse_brief(brief: Dict[str, Any]) -> Dict[str, Any]:
|
|
318
|
+
found = _Collector()
|
|
319
|
+
targets = brief.get("targets")
|
|
320
|
+
if targets is None:
|
|
321
|
+
targets = []
|
|
322
|
+
if not isinstance(targets, list):
|
|
323
|
+
found.error({"field": "targets"}, "`targets` must be a list")
|
|
324
|
+
targets = []
|
|
325
|
+
for index, entry in enumerate(targets, start=1):
|
|
326
|
+
_brief_target(found, index, entry)
|
|
85
327
|
|
|
86
|
-
|
|
328
|
+
fields = {k: brief[k] for k in BRIEF_KEYS if k in brief}
|
|
329
|
+
gate = fields.get("quality_gate")
|
|
330
|
+
if gate is not None and not isinstance(gate, dict):
|
|
331
|
+
found.error({"field": "quality_gate"}, "`quality_gate` must be a mapping")
|
|
332
|
+
fields.pop("quality_gate")
|
|
333
|
+
elif isinstance(gate, dict):
|
|
334
|
+
for key in ("soft_target", "soft_fallback"):
|
|
335
|
+
value = gate.get(key)
|
|
336
|
+
if key in gate and (isinstance(value, bool) or not isinstance(value, (int, float))):
|
|
337
|
+
found.error({"field": f"quality_gate.{key}"}, f"`quality_gate.{key}` must be a number, got {value!r}")
|
|
338
|
+
result = found.result()
|
|
339
|
+
result["brief"] = fields
|
|
340
|
+
return result
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _read(path: str) -> tuple:
|
|
344
|
+
p = Path(path)
|
|
345
|
+
if not p.is_file():
|
|
346
|
+
return None, ("MANIFEST_NOT_FOUND", f"Manifest not found: {path}")
|
|
347
|
+
try:
|
|
348
|
+
# utf-8-sig: a byte-order mark an editor wrote is no part of the first name.
|
|
349
|
+
return p.read_text(encoding="utf-8-sig"), None
|
|
350
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
351
|
+
return None, ("MANIFEST_READ_ERROR", f"Manifest unreadable: {exc}")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _fail(code: str, message: str) -> int:
|
|
355
|
+
json.dump({"error": message, "code": code}, sys.stderr)
|
|
356
|
+
sys.stderr.write("\n")
|
|
357
|
+
return 2
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _emit(payload: Dict[str, Any]) -> None:
|
|
361
|
+
json.dump(payload, sys.stdout, separators=(",", ":"))
|
|
362
|
+
sys.stdout.write("\n")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def run(path: str, brief: bool = False) -> int:
|
|
87
366
|
if path == "-":
|
|
88
|
-
|
|
367
|
+
# Bytes as UTF-8, whatever the console's code page (cp1252 on Windows).
|
|
368
|
+
text = sys.stdin.buffer.read().decode("utf-8", errors="replace").lstrip("")
|
|
89
369
|
else:
|
|
90
|
-
|
|
91
|
-
if
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
370
|
+
text, problem = _read(path)
|
|
371
|
+
if problem:
|
|
372
|
+
code, message = problem
|
|
373
|
+
if brief:
|
|
374
|
+
code, message = code.replace("MANIFEST", "BRIEF"), message.replace("Manifest", "Brief")
|
|
375
|
+
return _fail(code, message)
|
|
376
|
+
|
|
377
|
+
if brief:
|
|
95
378
|
try:
|
|
96
|
-
|
|
97
|
-
except
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
379
|
+
data = yaml.safe_load(text)
|
|
380
|
+
except yaml.YAMLError as exc:
|
|
381
|
+
return _fail("BRIEF_PARSE_ERROR", f"Brief is not valid YAML: {exc}")
|
|
382
|
+
if not isinstance(data, dict):
|
|
383
|
+
return _fail("BRIEF_PARSE_ERROR", "Brief is not a YAML mapping")
|
|
384
|
+
result = parse_brief(data)
|
|
385
|
+
else:
|
|
386
|
+
result = parse_manifest_text(text)
|
|
387
|
+
_emit(result)
|
|
105
388
|
return 1 if result["errors"] else 0
|
|
106
389
|
|
|
107
390
|
|
|
391
|
+
def run_stack_name(state_file: str) -> int:
|
|
392
|
+
path = Path(state_file)
|
|
393
|
+
if not path.is_file():
|
|
394
|
+
return _fail("STATE_NOT_FOUND", f"State file not found: {state_file}")
|
|
395
|
+
try:
|
|
396
|
+
state = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
397
|
+
except (OSError, UnicodeDecodeError, yaml.YAMLError) as exc:
|
|
398
|
+
return _fail("STATE_PARSE_ERROR", f"Failed to parse state file: {exc}")
|
|
399
|
+
if not isinstance(state, dict) or not isinstance(state.get("campaign"), dict):
|
|
400
|
+
return _fail("STATE_PARSE_ERROR", "State file has no campaign mapping")
|
|
401
|
+
_emit({"stack_name": stack_name(state["campaign"].get("name"))})
|
|
402
|
+
return 0
|
|
403
|
+
|
|
404
|
+
|
|
108
405
|
def main(argv: list[str] | None = None) -> int:
|
|
109
406
|
parser = argparse.ArgumentParser(
|
|
110
407
|
prog="campaign-parse-manifest",
|
|
111
|
-
description="
|
|
408
|
+
description="Validate campaign targets from a --manifest list or a campaign-brief.yaml, "
|
|
409
|
+
"or print the capstone's stack name from a campaign state.",
|
|
112
410
|
)
|
|
113
|
-
parser.add_argument("path", help="path to manifest text file, or `-` for stdin")
|
|
411
|
+
parser.add_argument("path", nargs="?", help="path to manifest text file, or `-` for stdin")
|
|
412
|
+
parser.add_argument("--brief", help="path to a campaign-brief.yaml to validate instead")
|
|
413
|
+
parser.add_argument("--stack-name", dest="stack_state", help="path to _campaign-state.yaml whose campaign name gives the stack name")
|
|
114
414
|
args = parser.parse_args(argv)
|
|
415
|
+
given = [value for value in (args.path, args.brief, args.stack_state) if value is not None]
|
|
416
|
+
if len(given) != 1:
|
|
417
|
+
parser.error("give one of: a manifest path (or `-` for stdin), --brief <file>, or --stack-name <state-file>")
|
|
418
|
+
if args.stack_state is not None:
|
|
419
|
+
return run_stack_name(args.stack_state)
|
|
420
|
+
if args.brief is not None:
|
|
421
|
+
return run(args.brief, brief=True)
|
|
115
422
|
return run(args.path)
|
|
116
423
|
|
|
117
424
|
|