bmad-module-skill-forge 2.1.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 +51 -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 +14 -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 +117 -9
- package/src/shared/scripts/skf-detect-docs.py +892 -4
- 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 +215 -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 +17 -6
- 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 +55 -18
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -57
- 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 -89
- package/src/skf-setup/references/detect-and-tier.md +58 -66
- 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 -645
- 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,5 +1,5 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = []
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Validate Output — Validate skill package artifacts.
|
|
@@ -16,23 +16,50 @@ Two package shapes are supported via --skill-type (default: individual):
|
|
|
16
16
|
checks and instead validates the stack count-equalities
|
|
17
17
|
(library_count vs per-library reference files; integration_count
|
|
18
18
|
vs integration pair files; confidence_distribution sum vs
|
|
19
|
-
library_count), emitted under validation.stack_counts
|
|
19
|
+
library_count), emitted under validation.stack_counts, and the
|
|
20
|
+
stack structure, emitted under validation.stack_structure (see
|
|
21
|
+
validate_stack_structure): the SKILL.md headings of the stack
|
|
22
|
+
template with the catalog inline or behind a pointer, every
|
|
23
|
+
relative link in SKILL.md and references/stack-catalog.md, the
|
|
24
|
+
metadata.json keys and values with the dominant tier recomputed
|
|
25
|
+
by skf-render-stack-metadata.py beside this script, the headings
|
|
26
|
+
of each reference and pair file and a file for each library and
|
|
27
|
+
pair metadata.json lists, and a tier label on each
|
|
28
|
+
per-library summary, pair entry and reference file. Each of
|
|
29
|
+
its issues names in `check` the create-stack-skill validate.md
|
|
30
|
+
section that records it. --forge-tier <tier> also checks
|
|
31
|
+
metadata.json's forge_tier against the run's tier.
|
|
20
32
|
|
|
21
33
|
The --export-gate flag selects a third, self-contained mode used by
|
|
22
|
-
skf-export-skill's publishing gate (load-skill.md §2
|
|
23
|
-
additive and orthogonal to --skill-type: it does NOT touch the
|
|
24
|
-
code path above, so existing callers keep byte-identical
|
|
25
|
-
the script emits the deterministic verdict the export
|
|
26
|
-
by hand each run:
|
|
34
|
+
skf-export-skill's publishing gate (load-skill.md §2; package.md renders the
|
|
35
|
+
verdict). It is additive and orthogonal to --skill-type: it does NOT touch the
|
|
36
|
+
individual/stack code path above, so existing callers keep byte-identical
|
|
37
|
+
output. Under the flag the script emits the deterministic verdict the export
|
|
38
|
+
prompt previously derived by hand each run:
|
|
27
39
|
- metadata.json required-field presence for the full agentskills.io set
|
|
28
40
|
(name, version, skill_type, source_authority, exports, generation_date,
|
|
29
41
|
confidence_tier) as high-severity issues under validation.metadata.issues;
|
|
30
42
|
- enum-membership checks (skill_type, source_authority, confidence_tier) as
|
|
31
|
-
high-severity issues under validation.metadata.enum_issues
|
|
43
|
+
high-severity issues under validation.metadata.enum_issues. The
|
|
44
|
+
confidence_tier set depends on skill_type: a single skill records its
|
|
45
|
+
forge tier (Quick / Forge / Forge+ / Deep), a stack the dominant
|
|
46
|
+
confidence tier of its libraries (T1 / T1-low / T2 / T3). A stack that
|
|
47
|
+
still holds a forge tier there, as older stacks do, passes with a low
|
|
48
|
+
note under validation.metadata.issues;
|
|
49
|
+
- recommended-field presence, chosen by skill_type, as low-severity issues
|
|
50
|
+
under validation.metadata.recommended_missing: description, source_repo,
|
|
51
|
+
language and tool_versions for a single skill (plus ast_node_count when
|
|
52
|
+
confidence_distribution.t1 is above 0), language and tool_versions for a
|
|
53
|
+
stack;
|
|
32
54
|
- SKILL.md Section 7b (Scripts & Assets) cross-reference against on-disk
|
|
33
55
|
scripts/ and assets/ files, under validation.crossref_7b.{missing,orphans}
|
|
34
56
|
(a §7b-named file absent on disk is high; an on-disk file not named in §7b
|
|
35
|
-
is a low orphan warning)
|
|
57
|
+
is a low orphan warning). Only a path that starts with scripts/ or
|
|
58
|
+
assets/, optionally after ./, is read as a bundled file. The section is
|
|
59
|
+
the level-2 `## Scripts & Assets` heading create-skill writes (optionally
|
|
60
|
+
numbered, as in `## 7b. Scripts & Assets`), found outside fenced code, and
|
|
61
|
+
validation.crossref_7b.{heading,heading_line} name the heading it matched
|
|
62
|
+
and its line (both null when SKILL.md has no Section 7b);
|
|
36
63
|
- a deterministic export_status ∈ READY / WARNINGS / NOT_READY alongside the
|
|
37
64
|
existing PASS/FAIL result (NOT_READY on any high issue, WARNINGS when only
|
|
38
65
|
medium/low issues remain, READY when clean).
|
|
@@ -41,15 +68,19 @@ CLI: python3 skf-validate-output.py <skill-package-dir>
|
|
|
41
68
|
python3 skf-validate-output.py <skill-package-dir> --generated-by quick-skill
|
|
42
69
|
python3 skf-validate-output.py <skill-package-dir> --skip-frontmatter
|
|
43
70
|
python3 skf-validate-output.py <skill-package-dir> --skill-type stack
|
|
71
|
+
python3 skf-validate-output.py <skill-package-dir> --skill-type stack --forge-tier Deep
|
|
44
72
|
python3 skf-validate-output.py <skill-package-dir> --export-gate
|
|
45
73
|
"""
|
|
46
74
|
|
|
47
75
|
from __future__ import annotations
|
|
48
76
|
|
|
77
|
+
import importlib.util
|
|
49
78
|
import json
|
|
79
|
+
import posixpath
|
|
50
80
|
import re
|
|
51
81
|
import sys
|
|
52
82
|
from pathlib import Path
|
|
83
|
+
from urllib.parse import unquote
|
|
53
84
|
|
|
54
85
|
|
|
55
86
|
def validate_frontmatter(content, skill_name=None):
|
|
@@ -197,26 +228,91 @@ def validate_metadata_json(data, generated_by=None):
|
|
|
197
228
|
_EXPORT_GATE_ENUMS = {
|
|
198
229
|
"skill_type": ("single", "stack"),
|
|
199
230
|
"source_authority": ("official", "internal", "community"),
|
|
200
|
-
"confidence_tier": ("Quick", "Forge", "Forge+", "Deep"),
|
|
201
231
|
}
|
|
202
232
|
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
|
|
233
|
+
# confidence_tier holds a different scale per skill_type: a single skill
|
|
234
|
+
# records the forge tier it was compiled at, a stack records the dominant
|
|
235
|
+
# confidence tier of its libraries (create-stack-skill keeps the forge tier in
|
|
236
|
+
# forge_tier). Older stacks hold their forge tier in confidence_tier (the
|
|
237
|
+
# v0.10.0 stack template wrote it there, and stacks on disk still carry it),
|
|
238
|
+
# and the gate accepted them before the stack scale existed, so a stack also
|
|
239
|
+
# accepts a forge tier: validate_metadata_export_gate adds a low note for it
|
|
240
|
+
# instead of failing the export. When skill_type is neither, the value is
|
|
241
|
+
# checked against both scales (skill_type itself already fails its enum check).
|
|
242
|
+
_FORGE_TIERS = ("Quick", "Forge", "Forge+", "Deep")
|
|
243
|
+
_EXPORT_GATE_CONFIDENCE_TIERS = {
|
|
244
|
+
"single": _FORGE_TIERS,
|
|
245
|
+
"stack": ("T1", "T1-low", "T2", "T3") + _FORGE_TIERS,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# Recommended (non-required) metadata fields, by skill_type: the fields the
|
|
249
|
+
# generator's metadata template always writes (quick-skill and create-skill
|
|
250
|
+
# for single, create-stack-skill for stack). A stack's template has no
|
|
251
|
+
# description or source_repo, so a stack is never warned about them.
|
|
252
|
+
# ast_node_count is written only when ast-grep ran, so a single skill is
|
|
253
|
+
# expected to carry it only when confidence_distribution counts T1
|
|
254
|
+
# (AST-verified) signatures. A stack is never asked for it: its T1 count is
|
|
255
|
+
# inherited from its libraries, not from an AST pass of its own.
|
|
256
|
+
_EXPORT_GATE_RECOMMENDED = {
|
|
257
|
+
"single": ("description", "source_repo", "language", "tool_versions"),
|
|
258
|
+
"stack": ("language", "tool_versions"),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# Matches a scripts/… or assets/… path token (e.g. `scripts/run.py`) and
|
|
262
|
+
# captures it without an optional leading `./`. The match starts only at the
|
|
263
|
+
# beginning of a path token, so an upstream path such as
|
|
264
|
+
# `bmad-module-builder/scripts/x.py` is not read as the package's own
|
|
265
|
+
# `scripts/x.py`. The leading filename char must be alphanumeric/dot/underscore
|
|
266
|
+
# so a bare `scripts/` (the §7b directory note with nothing after the slash)
|
|
267
|
+
# is not captured as a file reference.
|
|
268
|
+
_SECTION_7B_PATH_RE = re.compile(
|
|
269
|
+
r"(?<![A-Za-z0-9._/\-])(?:\./)?((?:scripts|assets)/[A-Za-z0-9._][A-Za-z0-9._/\-]*)"
|
|
270
|
+
)
|
|
271
|
+
# The title of Section 7b's level-2 heading (skf-create-skill/assets/
|
|
272
|
+
# skill-sections.md, compile-assembly-rules.md), with an optional section
|
|
273
|
+
# number such as `7b.` before it.
|
|
274
|
+
_SECTION_7B_TITLE_RE = re.compile(r"^(?:\d+[A-Za-z]?\.?[ \t]+)?Scripts & Assets$", re.IGNORECASE)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _is_blank(val):
|
|
278
|
+
"""True when a metadata value is absent or empty (None, "", [], {})."""
|
|
279
|
+
if val is None:
|
|
280
|
+
return True
|
|
281
|
+
if isinstance(val, str):
|
|
282
|
+
return not val.strip()
|
|
283
|
+
if isinstance(val, (list, dict)):
|
|
284
|
+
return not val
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _counts_t1(data):
|
|
289
|
+
"""True when confidence_distribution.t1 is a positive number."""
|
|
290
|
+
dist = data.get("confidence_distribution")
|
|
291
|
+
if not isinstance(dist, dict):
|
|
292
|
+
return False
|
|
293
|
+
t1 = dist.get("t1")
|
|
294
|
+
return isinstance(t1, (int, float)) and not isinstance(t1, bool) and t1 > 0
|
|
208
295
|
|
|
209
296
|
|
|
210
297
|
def validate_metadata_export_gate(data):
|
|
211
298
|
"""agentskills.io export-gate metadata validation.
|
|
212
299
|
|
|
213
|
-
Returns (required_issues, enum_issues). Required-field
|
|
214
|
-
agentskills.io set is high-severity; enum-membership
|
|
215
|
-
high-severity
|
|
300
|
+
Returns (required_issues, enum_issues, recommended_missing). Required-field
|
|
301
|
+
presence for the full agentskills.io set is high-severity; enum-membership
|
|
302
|
+
mismatches are high-severity, with the confidence_tier scale chosen by
|
|
303
|
+
skill_type. An empty `exports` array is a low warning (matching
|
|
216
304
|
load-skill.md §2's "warn if empty — graceful handling"), not a hard halt.
|
|
305
|
+
So is a forge tier in a stack's confidence_tier (the value older stacks
|
|
306
|
+
hold); both of these low notes go in required_issues.
|
|
307
|
+
A missing or empty recommended field for the skill_type is a low warning;
|
|
308
|
+
when skill_type is neither single nor stack no recommended set applies.
|
|
217
309
|
"""
|
|
218
310
|
required_issues = []
|
|
219
311
|
enum_issues = []
|
|
312
|
+
recommended_missing = []
|
|
313
|
+
skill_type = data.get("skill_type")
|
|
314
|
+
if not isinstance(skill_type, str):
|
|
315
|
+
skill_type = None
|
|
220
316
|
|
|
221
317
|
# String required fields — high-severity presence checks.
|
|
222
318
|
for field in ("name", "version", "skill_type", "source_authority",
|
|
@@ -255,7 +351,75 @@ def validate_metadata_export_gate(data):
|
|
|
255
351
|
"message": f"{field} '{val}' not in {list(allowed)}",
|
|
256
352
|
})
|
|
257
353
|
|
|
258
|
-
|
|
354
|
+
tier = data.get("confidence_tier")
|
|
355
|
+
if isinstance(tier, str) and tier:
|
|
356
|
+
if skill_type in _EXPORT_GATE_CONFIDENCE_TIERS:
|
|
357
|
+
allowed = _EXPORT_GATE_CONFIDENCE_TIERS[skill_type]
|
|
358
|
+
scope = f" for skill_type '{skill_type}'"
|
|
359
|
+
else:
|
|
360
|
+
allowed = tuple(dict.fromkeys(
|
|
361
|
+
t for tiers in _EXPORT_GATE_CONFIDENCE_TIERS.values() for t in tiers
|
|
362
|
+
))
|
|
363
|
+
scope = ""
|
|
364
|
+
if tier not in allowed:
|
|
365
|
+
enum_issues.append({
|
|
366
|
+
"severity": "high",
|
|
367
|
+
"field": "confidence_tier",
|
|
368
|
+
"message": f"confidence_tier '{tier}' not in {list(allowed)}{scope}",
|
|
369
|
+
})
|
|
370
|
+
elif skill_type == "stack" and tier in _FORGE_TIERS:
|
|
371
|
+
required_issues.append({
|
|
372
|
+
"severity": "low",
|
|
373
|
+
"field": "confidence_tier",
|
|
374
|
+
"message": (
|
|
375
|
+
f"confidence_tier '{tier}' is a forge tier, as older stacks recorded it; "
|
|
376
|
+
"re-run Stack Skill (@Ferris SS) to record the dominant confidence tier "
|
|
377
|
+
"of its libraries (T1, T1-low, T2 or T3)"
|
|
378
|
+
),
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
# Recommended fields: low-severity presence checks, by skill_type.
|
|
382
|
+
recommended = list(_EXPORT_GATE_RECOMMENDED.get(skill_type, ()))
|
|
383
|
+
if skill_type == "single" and _counts_t1(data):
|
|
384
|
+
recommended.append("ast_node_count")
|
|
385
|
+
for field in recommended:
|
|
386
|
+
if _is_blank(data.get(field)):
|
|
387
|
+
recommended_missing.append({
|
|
388
|
+
"severity": "low",
|
|
389
|
+
"field": field,
|
|
390
|
+
"message": f"recommended field {field} missing or empty for a {skill_type} skill",
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
return required_issues, enum_issues, recommended_missing
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _section_7b_bounds(skill_md_text):
|
|
397
|
+
"""(heading, heading_line, start, end) of SKILL.md's Section 7b, or None.
|
|
398
|
+
|
|
399
|
+
Section 7b is the first level-2 heading titled `Scripts & Assets` (the
|
|
400
|
+
heading create-skill writes, optionally numbered as in
|
|
401
|
+
`## 7b. Scripts & Assets`) outside fenced code, and runs until the next
|
|
402
|
+
level-1 or level-2 heading outside fenced code. A heading that only
|
|
403
|
+
mentions scripts and assets (`### Loading assets from JavaScript`) or a
|
|
404
|
+
shell comment inside a code fence is not it. `heading` is the heading
|
|
405
|
+
line as written, `heading_line` its 1-based line number, and `start` and
|
|
406
|
+
`end` slice the section's body out of the text's lines.
|
|
407
|
+
"""
|
|
408
|
+
if not skill_md_text:
|
|
409
|
+
return None
|
|
410
|
+
headings = _headings(skill_md_text)
|
|
411
|
+
for k, (level, title, index) in enumerate(headings):
|
|
412
|
+
if level == 2 and _SECTION_7B_TITLE_RE.match(title):
|
|
413
|
+
lines = skill_md_text.split("\n")
|
|
414
|
+
end = next((i for lvl, _, i in headings[k + 1:] if lvl <= 2), len(lines))
|
|
415
|
+
return lines[index].strip(), index + 1, index + 1, end
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def section_7b_heading(skill_md_text):
|
|
420
|
+
"""(heading, heading_line) of SKILL.md's Section 7b, or (None, None) when it has none."""
|
|
421
|
+
bounds = _section_7b_bounds(skill_md_text)
|
|
422
|
+
return (bounds[0], bounds[1]) if bounds else (None, None)
|
|
259
423
|
|
|
260
424
|
|
|
261
425
|
def _extract_section_7b_refs(skill_md_text):
|
|
@@ -263,38 +427,17 @@ def _extract_section_7b_refs(skill_md_text):
|
|
|
263
427
|
(Scripts & Assets). Returns a set of posix path strings.
|
|
264
428
|
|
|
265
429
|
Section 7b is optional (create-skill emits it only when scripts or assets
|
|
266
|
-
are detected), so an absent section yields an empty set.
|
|
267
|
-
|
|
268
|
-
"script" and "asset" (the "Scripts & Assets" title) — and the region runs
|
|
269
|
-
until the next heading of the same or higher level.
|
|
430
|
+
are detected), so an absent section yields an empty set. _section_7b_bounds
|
|
431
|
+
locates it by its exact heading.
|
|
270
432
|
"""
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
lines = skill_md_text.split("\n")
|
|
275
|
-
start = None
|
|
276
|
-
heading_level = None
|
|
277
|
-
for i, line in enumerate(lines):
|
|
278
|
-
m = re.match(r"^(#{1,6})\s+(.*)$", line)
|
|
279
|
-
if m:
|
|
280
|
-
text = m.group(2).lower()
|
|
281
|
-
if "script" in text and "asset" in text:
|
|
282
|
-
start = i + 1
|
|
283
|
-
heading_level = len(m.group(1))
|
|
284
|
-
break
|
|
285
|
-
|
|
286
|
-
if start is None:
|
|
433
|
+
bounds = _section_7b_bounds(skill_md_text)
|
|
434
|
+
if bounds is None:
|
|
287
435
|
return set()
|
|
288
436
|
|
|
289
|
-
end =
|
|
290
|
-
|
|
291
|
-
m = re.match(r"^(#{1,6})\s+", lines[j])
|
|
292
|
-
if m and len(m.group(1)) <= heading_level:
|
|
293
|
-
end = j
|
|
294
|
-
break
|
|
295
|
-
|
|
296
|
-
region = "\n".join(lines[start:end])
|
|
437
|
+
_, _, start, end = bounds
|
|
438
|
+
region = "\n".join(skill_md_text.split("\n")[start:end])
|
|
297
439
|
refs = set()
|
|
440
|
+
# findall returns the capture group: the path without a leading `./`.
|
|
298
441
|
for match in _SECTION_7B_PATH_RE.findall(region):
|
|
299
442
|
refs.add(match.rstrip("./-"))
|
|
300
443
|
return refs
|
|
@@ -431,7 +574,467 @@ def validate_stack_counts(skill_dir, meta):
|
|
|
431
574
|
return issues, observed
|
|
432
575
|
|
|
433
576
|
|
|
434
|
-
|
|
577
|
+
# --- Stack structure pass (--skill-type stack) ------------------------------
|
|
578
|
+
#
|
|
579
|
+
# The checks create-stack-skill's validate.md §4 to §7 made by hand over every
|
|
580
|
+
# file of a committed stack, against src/skf-create-stack-skill/assets/
|
|
581
|
+
# stack-skill-template.md and, for metadata.json, assets/metadata-contract.md.
|
|
582
|
+
# Each issue carries `check`, the validate.md section
|
|
583
|
+
# that records it: skill_md (§4), metadata (§5), references (§6) and
|
|
584
|
+
# tier_labels (§7).
|
|
585
|
+
|
|
586
|
+
# The helper beside this script that holds the stack tier rules.
|
|
587
|
+
_STACK_RULES_HELPER = "skf-render-stack-metadata.py"
|
|
588
|
+
|
|
589
|
+
# The metadata.json keys the stack metadata contract writes, less ast_node_count
|
|
590
|
+
# (written only when ast-grep ran) and the three validate_stack_counts checks
|
|
591
|
+
# (library_count, integration_count, confidence_distribution).
|
|
592
|
+
_STACK_REQUIRED_KEYS = (
|
|
593
|
+
"skill_type", "name", "version", "generation_date", "forge_tier", "confidence_tier",
|
|
594
|
+
"spec_version", "source_authority", "generated_by", "exports", "libraries",
|
|
595
|
+
"integration_pairs", "language", "tool_versions", "stats", "dependencies", "compatibility",
|
|
596
|
+
)
|
|
597
|
+
_STACK_TIERS = ("T1", "T1-low", "T2", "T3")
|
|
598
|
+
_SOURCE_AUTHORITIES = ("official", "community", "internal")
|
|
599
|
+
_DISTRIBUTION_KEYS = ("t1", "t1_low", "t2", "t3")
|
|
600
|
+
|
|
601
|
+
# The SKILL.md and stack-catalog.md headings of the stack template.
|
|
602
|
+
_STACK_CATALOG = "references/stack-catalog.md"
|
|
603
|
+
_INTEGRATION_PATTERNS = "Integration Patterns"
|
|
604
|
+
_PAIR_INTEGRATIONS = "Library Pair Integrations"
|
|
605
|
+
_REFERENCE_INDEX = "Library Reference Index"
|
|
606
|
+
_PER_LIBRARY = "Per-Library Summaries"
|
|
607
|
+
_CATALOG_POINTER = "Library Catalog"
|
|
608
|
+
_CONVENTIONS = "Conventions"
|
|
609
|
+
# The headings of a per-library file and of an integration pair file.
|
|
610
|
+
_LIBRARY_FILE_HEADINGS = ("Key Exports", "Usage Patterns")
|
|
611
|
+
_PAIR_FILE_HEADINGS = ("Integration Pattern", "Key Files")
|
|
612
|
+
|
|
613
|
+
# The line under the SKILL.md title: `> 3 libraries | 2 integration patterns | Forge tier: Deep`.
|
|
614
|
+
_STACK_HEADER_RE = re.compile(
|
|
615
|
+
r"^>[ \t]*(\d+)[ \t]+librar(?:y|ies)[ \t]*\|[ \t]*(\d+)[ \t]+integration patterns?[ \t]*\|"
|
|
616
|
+
r"[ \t]*Forge tier:[ \t]*([A-Za-z+]+)",
|
|
617
|
+
re.MULTILINE,
|
|
618
|
+
)
|
|
619
|
+
# A tier label: `**Confidence:**` then a T-code, T1-low tried before T1. It,
|
|
620
|
+
# and the `**Version:**` and `**Type:**` lines, may sit in a list item.
|
|
621
|
+
_TIER_LABEL_RE = re.compile(r"^(?:[-*+][ \t]+)?\*\*Confidence:\*\*[ \t]*(?:T1-low|T1|T2|T3)(?![\w-])",
|
|
622
|
+
re.MULTILINE)
|
|
623
|
+
_VERSION_LINE_RE = re.compile(r"^(?:[-*+][ \t]+)?\*\*Version:\*\*", re.MULTILINE)
|
|
624
|
+
_TYPE_LINE_RE = re.compile(r"^(?:[-*+][ \t]+)?\*\*Type:\*\*", re.MULTILINE)
|
|
625
|
+
_ATX_HEADING_RE = re.compile(r"^(#{1,6})[ \t]+(.*)$")
|
|
626
|
+
_FENCE_LINE_RE = re.compile(r"^\s*(`{3,}|~{3,})")
|
|
627
|
+
_INLINE_CODE_RE = re.compile(r"(`+)(?:(?!\1).)+?\1")
|
|
628
|
+
# An inline link or image: the target, bare or in angle brackets, then an optional title.
|
|
629
|
+
_LINK_RE = re.compile(r"\]\(\s*(<[^>\n]*>|[^)\s]+)(?:\s+(?:\"[^\"]*\"|'[^']*'))?\s*\)")
|
|
630
|
+
_URL_SCHEME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _stack_rules():
|
|
634
|
+
"""The tier rules of skf-render-stack-metadata.py beside this script, or None."""
|
|
635
|
+
path = Path(__file__).resolve().parent / _STACK_RULES_HELPER
|
|
636
|
+
if not path.is_file():
|
|
637
|
+
return None
|
|
638
|
+
try:
|
|
639
|
+
spec = importlib.util.spec_from_file_location("skf_render_stack_metadata_rules", path)
|
|
640
|
+
module = importlib.util.module_from_spec(spec)
|
|
641
|
+
spec.loader.exec_module(module)
|
|
642
|
+
except (ImportError, OSError, SyntaxError):
|
|
643
|
+
return None
|
|
644
|
+
return module if callable(getattr(module, "dominant_tier", None)) else None
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _markdown_lines(text):
|
|
648
|
+
"""(line, is_code) for each line; fence lines and fenced lines are code."""
|
|
649
|
+
out, fence = [], None
|
|
650
|
+
for line in text.split("\n"):
|
|
651
|
+
m = _FENCE_LINE_RE.match(line)
|
|
652
|
+
if fence is None:
|
|
653
|
+
if m:
|
|
654
|
+
fence = m.group(1)
|
|
655
|
+
out.append((line, True))
|
|
656
|
+
continue
|
|
657
|
+
out.append((line, False))
|
|
658
|
+
else:
|
|
659
|
+
if m and m.group(1)[0] == fence[0] and len(m.group(1)) >= len(fence) and not line.strip()[len(m.group(1)):]:
|
|
660
|
+
fence = None
|
|
661
|
+
out.append((line, True))
|
|
662
|
+
return out
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _headings(text):
|
|
666
|
+
"""[(level, text, line_index)] of the ATX headings outside fenced code, a
|
|
667
|
+
closing run of hashes (`## Conventions ##`) removed."""
|
|
668
|
+
found = []
|
|
669
|
+
for i, (line, code) in enumerate(_markdown_lines(text)):
|
|
670
|
+
m = None if code else _ATX_HEADING_RE.match(line)
|
|
671
|
+
if m:
|
|
672
|
+
body = m.group(2).rstrip(" \t")
|
|
673
|
+
bare = body.rstrip("#")
|
|
674
|
+
if bare != body and (not bare or bare[-1] in " \t"):
|
|
675
|
+
body = bare
|
|
676
|
+
found.append((len(m.group(1)), body.strip(), i))
|
|
677
|
+
return found
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _find_heading(headings, level, name):
|
|
681
|
+
"""The first heading of `level` named `name` (case-insensitive), or None."""
|
|
682
|
+
for heading in headings:
|
|
683
|
+
if heading[0] == level and heading[1].lower() == name.lower():
|
|
684
|
+
return heading
|
|
685
|
+
return None
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _block(text, headings, heading):
|
|
689
|
+
"""The lines under `heading` up to the next heading of its level or higher."""
|
|
690
|
+
lines = text.split("\n")
|
|
691
|
+
level, _, start = heading
|
|
692
|
+
end = len(lines)
|
|
693
|
+
for other in headings:
|
|
694
|
+
if other[2] > start and other[0] <= level:
|
|
695
|
+
end = other[2]
|
|
696
|
+
break
|
|
697
|
+
return "\n".join(lines[start + 1:end])
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _entries(text, headings, parent, level):
|
|
701
|
+
"""[(name, block)] for each heading of `level` inside the `parent` heading's section."""
|
|
702
|
+
section_end = len(text.split("\n"))
|
|
703
|
+
for other in headings:
|
|
704
|
+
if other[2] > parent[2] and other[0] <= parent[0]:
|
|
705
|
+
section_end = other[2]
|
|
706
|
+
break
|
|
707
|
+
return [
|
|
708
|
+
(heading[1], _block(text, headings, heading))
|
|
709
|
+
for heading in headings
|
|
710
|
+
if heading[0] == level and parent[2] < heading[2] < section_end
|
|
711
|
+
]
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _header_region(text):
|
|
715
|
+
"""A reference file's header: the text before its first `## ` heading."""
|
|
716
|
+
for level, _, index in _headings(text):
|
|
717
|
+
if level == 2:
|
|
718
|
+
return "\n".join(text.split("\n")[:index])
|
|
719
|
+
return text
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _relative_links(text):
|
|
723
|
+
"""Each relative link target outside fenced code and inline code: no URL,
|
|
724
|
+
no in-file anchor, no absolute path; `#fragment` and `?query` removed."""
|
|
725
|
+
targets = []
|
|
726
|
+
for line, code in _markdown_lines(text):
|
|
727
|
+
if code:
|
|
728
|
+
continue
|
|
729
|
+
for match in _LINK_RE.finditer(_INLINE_CODE_RE.sub("", line)):
|
|
730
|
+
target = match.group(1)
|
|
731
|
+
if target.startswith("<"):
|
|
732
|
+
target = target[1:-1].strip()
|
|
733
|
+
target = unquote(target.split("#", 1)[0].split("?", 1)[0])
|
|
734
|
+
if target and not target.startswith("/") and not _URL_SCHEME_RE.match(target):
|
|
735
|
+
targets.append(target)
|
|
736
|
+
return targets
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def validate_stack_structure(skill_dir, meta, generated_by=None, forge_tier=None):
|
|
740
|
+
"""Validate a stack package against the stack template's structure.
|
|
741
|
+
|
|
742
|
+
`meta` is the parsed metadata.json, or None when it is missing or does not
|
|
743
|
+
parse (the metadata checks are then skipped). Checks, by `check`:
|
|
744
|
+
- skill_md: a title (H1) and the `> N libraries | M integration
|
|
745
|
+
patterns | Forge tier: T` line under it, its numbers and tier equal
|
|
746
|
+
to metadata.json's; `## Integration Patterns` (unless
|
|
747
|
+
integration_count is 0) ahead of the per-library summaries;
|
|
748
|
+
`## Conventions`; the catalog inline (`## Library Reference Index`
|
|
749
|
+
and `## Per-Library Summaries`) or behind a `## Library Catalog`
|
|
750
|
+
pointer that links references/stack-catalog.md, which holds both
|
|
751
|
+
sections; every relative link in SKILL.md and stack-catalog.md
|
|
752
|
+
resolving from the file that holds it.
|
|
753
|
+
- metadata: the metadata contract's keys (less ast_node_count and the three
|
|
754
|
+
validate_stack_counts checks); skill_type `stack`; name equal to the
|
|
755
|
+
package folder's; version and generation_date non-empty strings;
|
|
756
|
+
forge_tier a forge tier (and `forge_tier` when given);
|
|
757
|
+
confidence_tier a T-code equal to the dominant tier
|
|
758
|
+
skf-render-stack-metadata.py recomputes from confidence_distribution;
|
|
759
|
+
source_authority an authority; generated_by equal to `generated_by`
|
|
760
|
+
when given (low); exports an array; libraries a non-empty array of
|
|
761
|
+
distinct names, as many as library_count; integration_pairs pairs of
|
|
762
|
+
two different listed libraries, each once, as many as
|
|
763
|
+
integration_count; confidence_distribution holding each of t1,
|
|
764
|
+
t1_low, t2 and t3 as a whole number.
|
|
765
|
+
- references: each references/{library}.md has a title, a
|
|
766
|
+
`**Version:**` line, `## Key Exports` and `## Usage Patterns`; each
|
|
767
|
+
references/integrations/{pair}.md a title, a `**Type:**` line,
|
|
768
|
+
`## Integration Pattern` and `## Key Files`; and, with a readable
|
|
769
|
+
metadata.json, each library `libraries` lists has its
|
|
770
|
+
references/{library}.md and each pair `integration_pairs` lists its
|
|
771
|
+
references/integrations/{a}-{b}.md (or {b}-{a}.md).
|
|
772
|
+
- tier_labels: a `**Confidence:**` T-code label on each entry of the
|
|
773
|
+
per-library summaries (SKILL.md or stack-catalog.md), on each entry
|
|
774
|
+
of `### Library Pair Integrations`, and in the header of each
|
|
775
|
+
reference and pair file.
|
|
776
|
+
|
|
777
|
+
Returns (issues, observed). Every issue is medium, except a generated_by
|
|
778
|
+
mismatch and a dominant tier that could not be recomputed (low).
|
|
779
|
+
"""
|
|
780
|
+
skill_dir = Path(skill_dir)
|
|
781
|
+
references = skill_dir / "references"
|
|
782
|
+
issues = []
|
|
783
|
+
observed = {
|
|
784
|
+
"catalog": None,
|
|
785
|
+
"dominant_tier": None,
|
|
786
|
+
"tier_rule": None,
|
|
787
|
+
"reference_files": 0,
|
|
788
|
+
"pair_files": 0,
|
|
789
|
+
"links_checked": 0,
|
|
790
|
+
"tier_labels_checked": 0,
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
def add(check, field, message, severity="medium"):
|
|
794
|
+
issues.append({"severity": severity, "check": check, "field": field, "message": message})
|
|
795
|
+
|
|
796
|
+
def read(path, check):
|
|
797
|
+
try:
|
|
798
|
+
return path.read_text(encoding="utf-8")
|
|
799
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
800
|
+
rel = path.relative_to(skill_dir).as_posix()
|
|
801
|
+
add(check, rel, f"cannot read {rel}: {exc}")
|
|
802
|
+
return None
|
|
803
|
+
|
|
804
|
+
meta = meta if isinstance(meta, dict) else None
|
|
805
|
+
|
|
806
|
+
def count(key):
|
|
807
|
+
value = meta.get(key) if meta is not None else None
|
|
808
|
+
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
809
|
+
|
|
810
|
+
def check_links(text, holder, name):
|
|
811
|
+
for target in _relative_links(text):
|
|
812
|
+
observed["links_checked"] += 1
|
|
813
|
+
try:
|
|
814
|
+
resolves = (holder / target).exists()
|
|
815
|
+
except (OSError, ValueError): # a name the file system cannot hold
|
|
816
|
+
resolves = False
|
|
817
|
+
if not resolves:
|
|
818
|
+
add("skill_md", "link", f"{name} links `{target}`, which does not resolve from {name}'s folder")
|
|
819
|
+
|
|
820
|
+
def has_file(folder, name):
|
|
821
|
+
try:
|
|
822
|
+
return (folder / name).is_file()
|
|
823
|
+
except (OSError, ValueError): # a name the file system cannot hold
|
|
824
|
+
return False
|
|
825
|
+
|
|
826
|
+
# --- SKILL.md (§4) and its tier labels (§7) ----------------------------
|
|
827
|
+
skill_md_path = skill_dir / "SKILL.md"
|
|
828
|
+
skill_md = read(skill_md_path, "skill_md") if skill_md_path.is_file() else None
|
|
829
|
+
catalog_text = None
|
|
830
|
+
if skill_md is not None:
|
|
831
|
+
headings = _headings(skill_md)
|
|
832
|
+
if not any(level == 1 for level, _, _ in headings):
|
|
833
|
+
add("skill_md", "heading", "SKILL.md has no title (`# {project_name} Stack Skill`)")
|
|
834
|
+
header = _STACK_HEADER_RE.search(skill_md)
|
|
835
|
+
if header is None:
|
|
836
|
+
add("skill_md", "header", "SKILL.md has no `> {lib_count} libraries | {integration_count} integration "
|
|
837
|
+
"patterns | Forge tier: {tier}` line under its title")
|
|
838
|
+
elif meta is not None:
|
|
839
|
+
for value, key in ((int(header.group(1)), "library_count"), (int(header.group(2)), "integration_count")):
|
|
840
|
+
if count(key) is not None and value != count(key):
|
|
841
|
+
add("skill_md", "header", f"the SKILL.md header line gives {value} for {key}; metadata.json "
|
|
842
|
+
f"has {count(key)}")
|
|
843
|
+
if isinstance(meta.get("forge_tier"), str) and header.group(3) != meta["forge_tier"]:
|
|
844
|
+
add("skill_md", "header", f"the SKILL.md header line gives forge tier {header.group(3)}; "
|
|
845
|
+
f"metadata.json has {meta['forge_tier']}")
|
|
846
|
+
|
|
847
|
+
patterns = _find_heading(headings, 2, _INTEGRATION_PATTERNS)
|
|
848
|
+
if patterns is None and count("integration_count") != 0:
|
|
849
|
+
add("skill_md", "heading", f"SKILL.md has no `## {_INTEGRATION_PATTERNS}` section")
|
|
850
|
+
if _find_heading(headings, 2, _CONVENTIONS) is None:
|
|
851
|
+
add("skill_md", "heading", f"SKILL.md has no `## {_CONVENTIONS}` section")
|
|
852
|
+
|
|
853
|
+
index = _find_heading(headings, 2, _REFERENCE_INDEX)
|
|
854
|
+
summaries = _find_heading(headings, 2, _PER_LIBRARY)
|
|
855
|
+
pointer = _find_heading(headings, 2, _CATALOG_POINTER)
|
|
856
|
+
summaries_holder = None # (text, headings, heading, file name)
|
|
857
|
+
if index and summaries:
|
|
858
|
+
observed["catalog"] = "inline"
|
|
859
|
+
summaries_holder = (skill_md, headings, summaries, "SKILL.md")
|
|
860
|
+
elif pointer:
|
|
861
|
+
observed["catalog"] = "pointer"
|
|
862
|
+
links = [posixpath.normpath(t) for t in _relative_links(_block(skill_md, headings, pointer))]
|
|
863
|
+
if _STACK_CATALOG not in links:
|
|
864
|
+
add("skill_md", "catalog", f"the `## {_CATALOG_POINTER}` pointer does not link `{_STACK_CATALOG}`")
|
|
865
|
+
catalog_path = skill_dir / _STACK_CATALOG
|
|
866
|
+
if not catalog_path.is_file():
|
|
867
|
+
add("skill_md", "catalog", f"the `## {_CATALOG_POINTER}` pointer's `{_STACK_CATALOG}` does not exist")
|
|
868
|
+
else:
|
|
869
|
+
catalog_text = read(catalog_path, "skill_md")
|
|
870
|
+
if catalog_text is not None:
|
|
871
|
+
catalog_headings = _headings(catalog_text)
|
|
872
|
+
for name in (_REFERENCE_INDEX, _PER_LIBRARY):
|
|
873
|
+
if _find_heading(catalog_headings, 2, name) is None:
|
|
874
|
+
add("skill_md", "catalog", f"`{_STACK_CATALOG}` has no `## {name}` section")
|
|
875
|
+
catalog_summaries = _find_heading(catalog_headings, 2, _PER_LIBRARY)
|
|
876
|
+
if catalog_summaries:
|
|
877
|
+
summaries_holder = (catalog_text, catalog_headings, catalog_summaries, "stack-catalog.md")
|
|
878
|
+
else:
|
|
879
|
+
missing = [f"`## {name}`" for name, found in ((_REFERENCE_INDEX, index), (_PER_LIBRARY, summaries))
|
|
880
|
+
if not found]
|
|
881
|
+
add("skill_md", "catalog", f"SKILL.md has neither the inline catalog (no {' or '.join(missing)}) nor a "
|
|
882
|
+
f"`## {_CATALOG_POINTER}` pointer to `{_STACK_CATALOG}`")
|
|
883
|
+
|
|
884
|
+
first_summary = summaries if observed["catalog"] == "inline" else pointer
|
|
885
|
+
if patterns and first_summary and first_summary[2] < patterns[2]:
|
|
886
|
+
add("skill_md", "heading", f"`## {_INTEGRATION_PATTERNS}` comes after the per-library summaries")
|
|
887
|
+
|
|
888
|
+
check_links(skill_md, skill_dir, "SKILL.md")
|
|
889
|
+
if catalog_text is not None:
|
|
890
|
+
check_links(catalog_text, references, "stack-catalog.md")
|
|
891
|
+
|
|
892
|
+
# Tier labels: each per-library summary and each pair entry.
|
|
893
|
+
if summaries_holder is not None:
|
|
894
|
+
text, holder_headings, heading, name = summaries_holder
|
|
895
|
+
for entry, block in _entries(text, holder_headings, heading, 3):
|
|
896
|
+
observed["tier_labels_checked"] += 1
|
|
897
|
+
if not _TIER_LABEL_RE.search(block):
|
|
898
|
+
add("tier_labels", "tier_label",
|
|
899
|
+
f"the per-library summary `### {entry}` in {name} has no `**Confidence:**` T-code label")
|
|
900
|
+
pairs_heading = _find_heading(headings, 3, _PAIR_INTEGRATIONS)
|
|
901
|
+
if patterns and pairs_heading is None and (count("integration_count") or 0) > 0:
|
|
902
|
+
add("skill_md", "heading", f"`## {_INTEGRATION_PATTERNS}` has no `### {_PAIR_INTEGRATIONS}` section")
|
|
903
|
+
if patterns and pairs_heading and pairs_heading[2] > patterns[2]:
|
|
904
|
+
for entry, block in _entries(skill_md, headings, pairs_heading, 4):
|
|
905
|
+
observed["tier_labels_checked"] += 1
|
|
906
|
+
if not _TIER_LABEL_RE.search(block):
|
|
907
|
+
add("tier_labels", "tier_label",
|
|
908
|
+
f"the pair entry `#### {entry}` in SKILL.md has no `**Confidence:**` T-code label")
|
|
909
|
+
|
|
910
|
+
# --- Reference and pair files (§6) and their tier labels (§7) -------------
|
|
911
|
+
ref_files = []
|
|
912
|
+
if references.is_dir():
|
|
913
|
+
ref_files = [p for p in sorted(references.glob("*.md")) if p.name != "stack-catalog.md"]
|
|
914
|
+
pair_dir = references / "integrations"
|
|
915
|
+
pair_files = sorted(pair_dir.glob("*.md")) if pair_dir.is_dir() else []
|
|
916
|
+
for files, kind, line_re, line_name, required in (
|
|
917
|
+
(ref_files, "reference_files", _VERSION_LINE_RE, "**Version:**", _LIBRARY_FILE_HEADINGS),
|
|
918
|
+
(pair_files, "pair_files", _TYPE_LINE_RE, "**Type:**", _PAIR_FILE_HEADINGS),
|
|
919
|
+
):
|
|
920
|
+
for path in files:
|
|
921
|
+
observed[kind] += 1
|
|
922
|
+
rel = path.relative_to(skill_dir).as_posix()
|
|
923
|
+
text = read(path, "references")
|
|
924
|
+
if text is None:
|
|
925
|
+
continue
|
|
926
|
+
headings = _headings(text)
|
|
927
|
+
if not any(level == 1 for level, _, _ in headings):
|
|
928
|
+
add("references", rel, f"{rel} has no title (`# ...`)")
|
|
929
|
+
if not line_re.search(text):
|
|
930
|
+
add("references", rel, f"{rel} has no `{line_name}` line")
|
|
931
|
+
for name in required:
|
|
932
|
+
if _find_heading(headings, 2, name) is None:
|
|
933
|
+
add("references", rel, f"{rel} has no `## {name}` section")
|
|
934
|
+
observed["tier_labels_checked"] += 1
|
|
935
|
+
if not _TIER_LABEL_RE.search(_header_region(text)):
|
|
936
|
+
add("tier_labels", "tier_label", f"{rel} has no `**Confidence:**` T-code label in its header")
|
|
937
|
+
|
|
938
|
+
# --- metadata.json (§5) ---------------------------------------------------
|
|
939
|
+
if meta is not None:
|
|
940
|
+
for key in _STACK_REQUIRED_KEYS:
|
|
941
|
+
if key not in meta:
|
|
942
|
+
add("metadata", key, f"metadata.json has no `{key}`")
|
|
943
|
+
if "skill_type" in meta and meta["skill_type"] != "stack":
|
|
944
|
+
add("metadata", "skill_type", f"skill_type is {meta['skill_type']!r}, not 'stack'")
|
|
945
|
+
if "name" in meta and meta["name"] != skill_dir.name:
|
|
946
|
+
add("metadata", "name", f"name {meta['name']!r} does not match the package folder {skill_dir.name!r}")
|
|
947
|
+
for key in ("version", "generation_date"):
|
|
948
|
+
if key in meta and (not isinstance(meta[key], str) or not meta[key].strip()):
|
|
949
|
+
add("metadata", key, f"{key} must be a non-empty string")
|
|
950
|
+
tier = meta.get("forge_tier")
|
|
951
|
+
if "forge_tier" in meta and tier not in _FORGE_TIERS:
|
|
952
|
+
add("metadata", "forge_tier", f"forge_tier {tier!r} not in {list(_FORGE_TIERS)}")
|
|
953
|
+
elif forge_tier is not None and "forge_tier" in meta and tier != forge_tier:
|
|
954
|
+
add("metadata", "forge_tier", f"forge_tier {tier!r} is not this run's forge tier {forge_tier!r}")
|
|
955
|
+
confidence_tier = meta.get("confidence_tier")
|
|
956
|
+
if "confidence_tier" in meta and confidence_tier not in _STACK_TIERS:
|
|
957
|
+
add("metadata", "confidence_tier", f"confidence_tier {confidence_tier!r} not in {list(_STACK_TIERS)}")
|
|
958
|
+
rules = _stack_rules()
|
|
959
|
+
if rules is None:
|
|
960
|
+
add("metadata", "confidence_tier", f"the dominant tier was not recomputed: {_STACK_RULES_HELPER} is not "
|
|
961
|
+
"beside skf-validate-output.py (re-install SKF)", severity="low")
|
|
962
|
+
elif isinstance(meta.get("confidence_distribution"), dict):
|
|
963
|
+
observed["tier_rule"] = _STACK_RULES_HELPER
|
|
964
|
+
observed["dominant_tier"] = rules.dominant_tier(meta["confidence_distribution"])
|
|
965
|
+
if confidence_tier in _STACK_TIERS and confidence_tier != observed["dominant_tier"]:
|
|
966
|
+
add("metadata", "confidence_tier", f"confidence_tier {confidence_tier!r} is not the dominant tier of "
|
|
967
|
+
f"confidence_distribution ({observed['dominant_tier']!r})")
|
|
968
|
+
authority = meta.get("source_authority")
|
|
969
|
+
if "source_authority" in meta and authority not in _SOURCE_AUTHORITIES:
|
|
970
|
+
add("metadata", "source_authority", f"source_authority {authority!r} not in {list(_SOURCE_AUTHORITIES)}")
|
|
971
|
+
if generated_by and "generated_by" in meta and meta["generated_by"] != generated_by:
|
|
972
|
+
add("metadata", "generated_by", f"generated_by is {meta['generated_by']!r}, expected {generated_by!r}",
|
|
973
|
+
severity="low")
|
|
974
|
+
if "exports" in meta and not isinstance(meta["exports"], list):
|
|
975
|
+
add("metadata", "exports", "exports must be an array (a stack's is empty)")
|
|
976
|
+
|
|
977
|
+
libraries = meta.get("libraries")
|
|
978
|
+
names = []
|
|
979
|
+
if "libraries" in meta:
|
|
980
|
+
if (not isinstance(libraries, list) or not libraries
|
|
981
|
+
or not all(isinstance(n, str) and n.strip() for n in libraries)):
|
|
982
|
+
add("metadata", "libraries", "libraries must be a non-empty array of library names")
|
|
983
|
+
else:
|
|
984
|
+
names = libraries
|
|
985
|
+
if len(set(names)) != len(names):
|
|
986
|
+
add("metadata", "libraries", "libraries names a library twice")
|
|
987
|
+
if count("library_count") is not None and len(names) != count("library_count"):
|
|
988
|
+
add("metadata", "libraries", f"libraries lists {len(names)} libraries; library_count is "
|
|
989
|
+
f"{count('library_count')}")
|
|
990
|
+
pairs = meta.get("integration_pairs")
|
|
991
|
+
listed_pairs = [] # each well-formed pair once, for the file check below
|
|
992
|
+
if "integration_pairs" in meta:
|
|
993
|
+
if not isinstance(pairs, list):
|
|
994
|
+
add("metadata", "integration_pairs", "integration_pairs must be an array of [library, library] pairs")
|
|
995
|
+
else:
|
|
996
|
+
seen = set()
|
|
997
|
+
for i, pair in enumerate(pairs):
|
|
998
|
+
if (not isinstance(pair, list) or len(pair) != 2
|
|
999
|
+
or not all(isinstance(n, str) for n in pair) or pair[0] == pair[1]):
|
|
1000
|
+
add("metadata", "integration_pairs",
|
|
1001
|
+
f"integration_pairs[{i}] must be two different library names; got {pair!r}")
|
|
1002
|
+
continue
|
|
1003
|
+
if names and not set(pair) <= set(names):
|
|
1004
|
+
add("metadata", "integration_pairs",
|
|
1005
|
+
f"integration_pairs[{i}] names a library libraries does not list: {pair!r}")
|
|
1006
|
+
if frozenset(pair) in seen:
|
|
1007
|
+
add("metadata", "integration_pairs", f"integration_pairs lists {pair!r} twice")
|
|
1008
|
+
else:
|
|
1009
|
+
listed_pairs.append(pair)
|
|
1010
|
+
seen.add(frozenset(pair))
|
|
1011
|
+
if count("integration_count") is not None and len(pairs) != count("integration_count"):
|
|
1012
|
+
add("metadata", "integration_pairs", f"integration_pairs lists {len(pairs)} pairs; "
|
|
1013
|
+
f"integration_count is {count('integration_count')}")
|
|
1014
|
+
distribution = meta.get("confidence_distribution")
|
|
1015
|
+
if isinstance(distribution, dict):
|
|
1016
|
+
for key in _DISTRIBUTION_KEYS:
|
|
1017
|
+
value = distribution.get(key)
|
|
1018
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
1019
|
+
add("metadata", "confidence_distribution",
|
|
1020
|
+
f"confidence_distribution.{key} must be a whole number; got {value!r}")
|
|
1021
|
+
|
|
1022
|
+
# Each library and pair metadata.json lists has a file of its own
|
|
1023
|
+
# (§6), so a misnamed file is caught even when the counts agree.
|
|
1024
|
+
for name in dict.fromkeys(names):
|
|
1025
|
+
if not has_file(references, f"{name}.md"):
|
|
1026
|
+
add("references", f"references/{name}.md",
|
|
1027
|
+
f"libraries lists {name!r}, but references/{name}.md does not exist")
|
|
1028
|
+
for a, b in listed_pairs:
|
|
1029
|
+
if not (has_file(pair_dir, f"{a}-{b}.md") or has_file(pair_dir, f"{b}-{a}.md")):
|
|
1030
|
+
add("references", f"references/integrations/{a}-{b}.md",
|
|
1031
|
+
f"integration_pairs lists {a!r} + {b!r}, but references/integrations/{a}-{b}.md does not exist")
|
|
1032
|
+
|
|
1033
|
+
return issues, observed
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False, skill_type="individual", export_gate=False,
|
|
1037
|
+
forge_tier=None):
|
|
435
1038
|
"""Validate a complete skill package directory.
|
|
436
1039
|
|
|
437
1040
|
When `skip_frontmatter` is True, the SKILL.md frontmatter pass is omitted —
|
|
@@ -441,7 +1044,9 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False,
|
|
|
441
1044
|
`skill_type` selects the package schema (default "individual", unchanged for
|
|
442
1045
|
existing callers). "stack" skips the individual-skill body-structure and
|
|
443
1046
|
metadata-schema passes (which assume the single-skill shape) and instead
|
|
444
|
-
runs validate_stack_counts, emitting validation.stack_counts.{issues,observed}
|
|
1047
|
+
runs validate_stack_counts, emitting validation.stack_counts.{issues,observed},
|
|
1048
|
+
and validate_stack_structure, emitting validation.stack_structure.{issues,observed}
|
|
1049
|
+
(`forge_tier`, when given, is the run's tier metadata.json must record).
|
|
445
1050
|
Frontmatter and context-snippet checks are shape-agnostic and run for both.
|
|
446
1051
|
|
|
447
1052
|
When `export_gate` is True, a self-contained export-gate validation runs
|
|
@@ -515,14 +1120,22 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False,
|
|
|
515
1120
|
|
|
516
1121
|
# Validate metadata.json
|
|
517
1122
|
meta_path = files["metadata.json"]
|
|
1123
|
+
stack_meta = None # the parsed metadata.json object the stack structure pass reads
|
|
518
1124
|
if meta_path.exists():
|
|
519
1125
|
try:
|
|
520
1126
|
with open(meta_path, encoding="utf-8") as f:
|
|
521
1127
|
meta = json.load(f)
|
|
522
|
-
if skill_type == "stack":
|
|
1128
|
+
if skill_type == "stack" and not isinstance(meta, dict):
|
|
1129
|
+
# Valid JSON that is not an object (e.g. `[]`) has no fields to read.
|
|
1130
|
+
result["validation"]["metadata"] = {"error": "metadata.json is not a JSON object"}
|
|
1131
|
+
result["validation"]["stack_counts"] = {"skipped": "metadata.json is not a JSON object"}
|
|
1132
|
+
result["summary"]["total_issues"] += 1
|
|
1133
|
+
result["summary"]["by_severity"]["high"] += 1
|
|
1134
|
+
elif skill_type == "stack":
|
|
523
1135
|
# Individual-skill metadata schema (source_repo, stats.*) does not
|
|
524
1136
|
# apply to a stack capstone; validate the stack count-equalities.
|
|
525
1137
|
result["validation"]["metadata"] = {"skipped": "stack-type: individual metadata schema not checked (see validation.stack_counts)"}
|
|
1138
|
+
stack_meta = meta
|
|
526
1139
|
stack_issues, observed = validate_stack_counts(skill_dir, meta)
|
|
527
1140
|
result["validation"]["stack_counts"] = {"issues": stack_issues, "observed": observed}
|
|
528
1141
|
for issue in stack_issues:
|
|
@@ -545,6 +1158,15 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False,
|
|
|
545
1158
|
if skill_type == "stack":
|
|
546
1159
|
result["validation"]["stack_counts"] = {"skipped": "metadata.json not found"}
|
|
547
1160
|
|
|
1161
|
+
# Stack structure: SKILL.md, reference files and tier labels run without
|
|
1162
|
+
# a readable metadata.json; its own checks need one.
|
|
1163
|
+
if skill_type == "stack":
|
|
1164
|
+
structure_issues, observed = validate_stack_structure(skill_dir, stack_meta, generated_by, forge_tier)
|
|
1165
|
+
result["validation"]["stack_structure"] = {"issues": structure_issues, "observed": observed}
|
|
1166
|
+
for issue in structure_issues:
|
|
1167
|
+
result["summary"]["total_issues"] += 1
|
|
1168
|
+
result["summary"]["by_severity"][issue["severity"]] += 1
|
|
1169
|
+
|
|
548
1170
|
# Overall pass/fail
|
|
549
1171
|
result["result"] = "PASS" if result["summary"]["by_severity"]["high"] == 0 else "FAIL"
|
|
550
1172
|
|
|
@@ -555,10 +1177,12 @@ def _validate_export_gate(skill_dir):
|
|
|
555
1177
|
"""Export-gate validation for skf-export-skill's publishing gate.
|
|
556
1178
|
|
|
557
1179
|
Self-contained: does not run the individual/stack passes. Emits the
|
|
558
|
-
deterministic verdict load-skill.md §2 and package.md
|
|
559
|
-
derived in-prompt
|
|
560
|
-
JSON
|
|
561
|
-
|
|
1180
|
+
deterministic verdict load-skill.md §2 and package.md previously
|
|
1181
|
+
derived in-prompt: SKILL.md presence/non-emptiness; metadata.json as a
|
|
1182
|
+
valid JSON object, with required-field presence and enum membership;
|
|
1183
|
+
recommended-field presence by skill_type (low,
|
|
1184
|
+
validation.metadata.recommended_missing); and the SKILL.md Section 7b <->
|
|
1185
|
+
on-disk scripts/assets cross-reference. It adds a deterministic
|
|
562
1186
|
export_status ∈ READY / WARNINGS / NOT_READY.
|
|
563
1187
|
"""
|
|
564
1188
|
skill_dir = Path(skill_dir)
|
|
@@ -596,11 +1220,12 @@ def _validate_export_gate(skill_dir):
|
|
|
596
1220
|
result["validation"]["skill_md"] = {"issues": skill_md_issues}
|
|
597
1221
|
_record(skill_md_issues)
|
|
598
1222
|
|
|
599
|
-
# 2. metadata.json must exist, parse as JSON, and satisfy the
|
|
600
|
-
# presence + enum-membership contract
|
|
1223
|
+
# 2. metadata.json must exist, parse as a JSON object, and satisfy the
|
|
1224
|
+
# required-field presence + enum-membership contract; missing
|
|
1225
|
+
# recommended fields for its skill_type are low warnings.
|
|
601
1226
|
if not meta_path.exists():
|
|
602
1227
|
meta_issues = [{"severity": "high", "field": "metadata.json", "message": "metadata.json not found"}]
|
|
603
|
-
result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": []}
|
|
1228
|
+
result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": [], "recommended_missing": []}
|
|
604
1229
|
_record(meta_issues)
|
|
605
1230
|
else:
|
|
606
1231
|
try:
|
|
@@ -608,22 +1233,36 @@ def _validate_export_gate(skill_dir):
|
|
|
608
1233
|
meta = json.load(f)
|
|
609
1234
|
except json.JSONDecodeError as e:
|
|
610
1235
|
meta_issues = [{"severity": "high", "field": "metadata.json", "message": f"JSON parse error: {e}"}]
|
|
611
|
-
result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": []}
|
|
1236
|
+
result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": [], "recommended_missing": []}
|
|
612
1237
|
_record(meta_issues)
|
|
613
1238
|
else:
|
|
614
|
-
|
|
615
|
-
|
|
1239
|
+
if isinstance(meta, dict):
|
|
1240
|
+
required_issues, enum_issues, recommended_missing = validate_metadata_export_gate(meta)
|
|
1241
|
+
else:
|
|
1242
|
+
# Valid JSON that is not an object (e.g. `[]`) has no fields to read.
|
|
1243
|
+
required_issues = [{"severity": "high", "field": "metadata.json", "message": "metadata.json is not a JSON object"}]
|
|
1244
|
+
enum_issues, recommended_missing = [], []
|
|
1245
|
+
result["validation"]["metadata"] = {
|
|
1246
|
+
"issues": required_issues,
|
|
1247
|
+
"enum_issues": enum_issues,
|
|
1248
|
+
"recommended_missing": recommended_missing,
|
|
1249
|
+
}
|
|
616
1250
|
_record(required_issues)
|
|
617
1251
|
_record(enum_issues)
|
|
1252
|
+
_record(recommended_missing)
|
|
618
1253
|
|
|
619
|
-
# 3. SKILL.md Section 7b <-> on-disk scripts/assets cross-reference.
|
|
1254
|
+
# 3. SKILL.md Section 7b <-> on-disk scripts/assets cross-reference. The
|
|
1255
|
+
# heading the section was found by, and its line, go in the verdict, so
|
|
1256
|
+
# a halt on a missing file shows which heading named it.
|
|
620
1257
|
missing, orphans = crossref_section_7b(skill_md_text, skill_dir)
|
|
1258
|
+
heading, heading_line = section_7b_heading(skill_md_text)
|
|
621
1259
|
crossref_issues = []
|
|
622
1260
|
for path in missing:
|
|
623
1261
|
crossref_issues.append({
|
|
624
1262
|
"severity": "high",
|
|
625
1263
|
"field": "crossref_7b",
|
|
626
|
-
"message": f"SKILL.md Section 7b
|
|
1264
|
+
"message": (f"SKILL.md Section 7b ('{heading}' at line {heading_line}) references '{path}' "
|
|
1265
|
+
"but it is absent on disk"),
|
|
627
1266
|
})
|
|
628
1267
|
for path in orphans:
|
|
629
1268
|
crossref_issues.append({
|
|
@@ -632,6 +1271,8 @@ def _validate_export_gate(skill_dir):
|
|
|
632
1271
|
"message": f"'{path}' present on disk but not referenced in SKILL.md Section 7b (orphan)",
|
|
633
1272
|
})
|
|
634
1273
|
result["validation"]["crossref_7b"] = {
|
|
1274
|
+
"heading": heading,
|
|
1275
|
+
"heading_line": heading_line,
|
|
635
1276
|
"missing": missing,
|
|
636
1277
|
"orphans": orphans,
|
|
637
1278
|
"issues": crossref_issues,
|
|
@@ -656,7 +1297,7 @@ if __name__ == "__main__":
|
|
|
656
1297
|
print(
|
|
657
1298
|
"Usage: python3 skf-validate-output.py <skill-package-dir> "
|
|
658
1299
|
"[--generated-by <generator>] [--skip-frontmatter] "
|
|
659
|
-
"[--skill-type {individual,stack}] [--export-gate]",
|
|
1300
|
+
"[--skill-type {individual,stack}] [--forge-tier <tier>] [--export-gate]",
|
|
660
1301
|
file=sys.stderr,
|
|
661
1302
|
)
|
|
662
1303
|
sys.exit(1)
|
|
@@ -690,8 +1331,15 @@ if __name__ == "__main__":
|
|
|
690
1331
|
)
|
|
691
1332
|
sys.exit(2)
|
|
692
1333
|
|
|
1334
|
+
run_forge_tier = None
|
|
1335
|
+
if "--forge-tier" in sys.argv:
|
|
1336
|
+
idx = sys.argv.index("--forge-tier")
|
|
1337
|
+
if idx + 1 < len(sys.argv):
|
|
1338
|
+
run_forge_tier = sys.argv[idx + 1]
|
|
1339
|
+
|
|
693
1340
|
result = validate_skill_package(
|
|
694
|
-
pkg_dir, generated_by=gen_by, skip_frontmatter=skip_fm, skill_type=skill_type
|
|
1341
|
+
pkg_dir, generated_by=gen_by, skip_frontmatter=skip_fm, skill_type=skill_type,
|
|
1342
|
+
forge_tier=run_forge_tier,
|
|
695
1343
|
)
|
|
696
1344
|
print(json.dumps(result, indent=2))
|
|
697
1345
|
sys.exit(0 if result["result"] == "PASS" else 1)
|