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,29 +1,40 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = ["pyyaml"]
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF QMD Classify Collections — Set arithmetic over QMD collection names.
|
|
6
6
|
|
|
7
7
|
Replaces the prose-driven classification logic in `src/skf-setup/references/
|
|
8
|
-
auto-index.md` §
|
|
8
|
+
auto-index.md` §2 with one Python invocation. Compares the live
|
|
9
9
|
QMD collections (from `qmd collection list`) against the forge registry
|
|
10
10
|
(`qmd_collections` array in forge-tier.yaml) and classifies each name as
|
|
11
11
|
Healthy / Orphaned / Stale, applying the forge-namespace suffix filter
|
|
12
12
|
added in PR #244 to silently exclude collections owned by unrelated
|
|
13
13
|
tools sharing the QMD daemon.
|
|
14
14
|
|
|
15
|
-
Classification rules (per step 3 §
|
|
15
|
+
Classification rules (per step 3 §2):
|
|
16
16
|
|
|
17
|
-
Healthy
|
|
17
|
+
Healthy: name in {forge-suffix-matched live} AND in registry.
|
|
18
18
|
No action needed.
|
|
19
|
-
Orphaned
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
Orphaned: name in {forge-suffix-matched live}, NOT in registry, and
|
|
20
|
+
`qmd collection show <name>` reports a Path inside
|
|
21
|
+
--project-root. Offered for removal in step 3 §3.
|
|
22
|
+
Stale: name in registry but NOT in {all live}. Registry entry
|
|
22
23
|
should be removed.
|
|
23
|
-
Foreign
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
Foreign: name in live that does NOT match a forge suffix, or a
|
|
25
|
+
forge-suffixed name missing from the registry whose Path
|
|
26
|
+
lies outside --project-root (or that qmd cannot show).
|
|
27
|
+
Silently excluded from every classification: never
|
|
28
|
+
displayed, never proposed for removal. Reported as a count
|
|
29
|
+
for telemetry only.
|
|
30
|
+
|
|
31
|
+
QMD's index is shared by every project on the machine, and every SKF
|
|
32
|
+
project names its collections with the same suffixes, so a suffix alone
|
|
33
|
+
cannot tell this project's orphan from another project's healthy
|
|
34
|
+
collection. SKF adds each collection from a folder inside its project
|
|
35
|
+
(the forge data folder, the skills folder or `_bmad-output/`), so the
|
|
36
|
+
collection's Path names its owner. A Path that resolves outside the
|
|
37
|
+
project root, or none at all, never makes a collection removable here.
|
|
27
38
|
|
|
28
39
|
Forge suffixes (the only suffixes a forge-managed collection can have,
|
|
29
40
|
set by producers `skf-brief-skill` and `skf-create-skill` per
|
|
@@ -31,33 +42,66 @@ src/knowledge/qmd-registry.md § Collection Types):
|
|
|
31
42
|
|
|
32
43
|
-brief, -temporal, -docs, -extraction
|
|
33
44
|
|
|
34
|
-
|
|
45
|
+
Subcommands:
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
in QMD (caller obtains this from `qmd collection list`
|
|
38
|
-
before invoking the script). Empty string → no live
|
|
39
|
-
collections, which is a valid first-run state.
|
|
47
|
+
classify Classify the live collections against the registry.
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
--registry-from-yaml <path>
|
|
42
50
|
Path to forge-tier.yaml. The script reads the file's
|
|
43
51
|
`qmd_collections` array and extracts the `name` field
|
|
44
52
|
from each entry. Missing file or missing array → empty
|
|
45
53
|
registry, which is a valid first-run state.
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
--project-root <path>
|
|
56
|
+
The project whose orphans may be offered for removal.
|
|
57
|
+
|
|
58
|
+
--live-names Comma-separated list of collection names currently
|
|
59
|
+
in QMD. If omitted, the script invokes `qmd collection
|
|
60
|
+
list` itself. Empty string → no live collections, which
|
|
61
|
+
is a valid first-run state.
|
|
62
|
+
|
|
63
|
+
remove-orphans Remove the orphans a classification found.
|
|
64
|
+
|
|
65
|
+
--classification-from <path>
|
|
66
|
+
The classify output step 3 staged (`qmd-classify.json`);
|
|
67
|
+
its `orphaned` list names the collections to remove.
|
|
68
|
+
|
|
69
|
+
--project-root <path>
|
|
70
|
+
Each collection's Path is read again with `qmd
|
|
71
|
+
collection show` just before `qmd collection remove`
|
|
72
|
+
runs, and a collection whose Path no longer lies inside
|
|
73
|
+
this root is not removed (it is listed under `failed`).
|
|
74
|
+
|
|
75
|
+
Output of classify (single JSON document on stdout):
|
|
48
76
|
|
|
49
77
|
{
|
|
50
78
|
"status": "ok",
|
|
51
79
|
"version": "v1",
|
|
80
|
+
"live_names": ["bar-extraction", "foo-brief", "memory-root-1"],
|
|
52
81
|
"healthy": ["foo-brief", "foo-extraction"],
|
|
53
82
|
"orphaned": ["bar-extraction"],
|
|
83
|
+
"orphaned_paths": {"bar-extraction": "/abs/project/skills/bar"},
|
|
54
84
|
"stale": ["baz-docs"],
|
|
55
85
|
"foreign_filtered_count": 4,
|
|
56
86
|
"foreign_filtered_sample": ["memory-root-1", "sessions-2"]
|
|
57
87
|
}
|
|
58
88
|
|
|
89
|
+
Output of remove-orphans (single JSON document on stdout):
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
"status": "ok",
|
|
93
|
+
"version": "v1",
|
|
94
|
+
"removed": ["bar-extraction"],
|
|
95
|
+
"failed": ["old-docs"],
|
|
96
|
+
"errors": {"old-docs": "its Path no longer lies inside the project root"}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
A collection that could not be removed is a hygiene note, not an error:
|
|
100
|
+
remove-orphans exits 0 whatever it removed.
|
|
101
|
+
|
|
59
102
|
`foreign_filtered_sample` is capped at 5 names (telemetry; the full list
|
|
60
|
-
is never useful
|
|
103
|
+
is never useful: if it were forge-relevant it would have a forge suffix
|
|
104
|
+
and a Path inside the project).
|
|
61
105
|
|
|
62
106
|
CLI — invoke via `uv run` so the PEP 723 PyYAML dependency declared
|
|
63
107
|
above is auto-resolved on first call and cached. `docs/getting-started.md`
|
|
@@ -65,14 +109,15 @@ documents uv as the runtime prerequisite for exactly this. Bare
|
|
|
65
109
|
`python3` will fail with `ModuleNotFoundError: No module named 'yaml'`
|
|
66
110
|
on a fresh interpreter:
|
|
67
111
|
|
|
68
|
-
uv run skf-qmd-classify-collections.py \\
|
|
69
|
-
--
|
|
70
|
-
|
|
112
|
+
uv run skf-qmd-classify-collections.py classify \\
|
|
113
|
+
--registry-from-yaml /path/forge-tier.yaml --project-root /path/project
|
|
114
|
+
uv run skf-qmd-classify-collections.py remove-orphans \\
|
|
115
|
+
--classification-from /run/qmd-classify.json --project-root /path/project
|
|
71
116
|
|
|
72
117
|
Exit codes:
|
|
73
118
|
0 success
|
|
74
|
-
1 user error (bad args, malformed registry file)
|
|
75
|
-
2 internal error
|
|
119
|
+
1 user error (bad args, malformed registry or classification file)
|
|
120
|
+
2 internal error (qmd collection list failed)
|
|
76
121
|
"""
|
|
77
122
|
|
|
78
123
|
from __future__ import annotations
|
|
@@ -163,11 +208,36 @@ def load_registry_names(path: Path) -> list[str]:
|
|
|
163
208
|
return names
|
|
164
209
|
|
|
165
210
|
|
|
166
|
-
def
|
|
211
|
+
def is_inside(path: str | None, root) -> bool:
|
|
212
|
+
"""True when `path` resolves to `root` or a folder below it.
|
|
213
|
+
|
|
214
|
+
Both sides go through realpath and normcase, so a symlinked project
|
|
215
|
+
root or a Windows drive letter in another case still matches. A path
|
|
216
|
+
on another drive, or none at all, is never inside.
|
|
217
|
+
"""
|
|
218
|
+
if not path or not root:
|
|
219
|
+
return False
|
|
220
|
+
try:
|
|
221
|
+
target = os.path.normcase(os.path.realpath(path))
|
|
222
|
+
base = os.path.normcase(os.path.realpath(str(root)))
|
|
223
|
+
return os.path.commonpath([target, base]) == base
|
|
224
|
+
except (OSError, ValueError):
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def classify(live: list[str], registry: list[str], project_root=None,
|
|
229
|
+
paths: dict | None = None) -> dict:
|
|
167
230
|
"""Pure function: classify live vs registry into healthy/orphaned/stale/foreign.
|
|
168
231
|
|
|
232
|
+
With `project_root`, a forge-suffixed live name missing from the
|
|
233
|
+
registry is orphaned only when `paths` maps it to a Path inside that
|
|
234
|
+
root; the rest count as foreign. Without it every such name is
|
|
235
|
+
orphaned: only a caller that already knows they all belong to this
|
|
236
|
+
project may leave it out (the CLI always passes it).
|
|
237
|
+
|
|
169
238
|
Returns the classification payload (without status/version envelope).
|
|
170
239
|
"""
|
|
240
|
+
paths = paths or {}
|
|
171
241
|
forge_live = [n for n in live if is_forge_owned(n)]
|
|
172
242
|
foreign_live = [n for n in live if not is_forge_owned(n)]
|
|
173
243
|
|
|
@@ -176,7 +246,14 @@ def classify(live: list[str], registry: list[str]) -> dict:
|
|
|
176
246
|
all_live_set = set(live)
|
|
177
247
|
|
|
178
248
|
healthy = sorted(forge_live_set & registry_set)
|
|
179
|
-
|
|
249
|
+
candidates = sorted(forge_live_set - registry_set)
|
|
250
|
+
if project_root is None:
|
|
251
|
+
orphaned = candidates
|
|
252
|
+
else:
|
|
253
|
+
orphaned = [n for n in candidates if is_inside(paths.get(n), project_root)]
|
|
254
|
+
# Another project's collection, or one qmd could not show, is no
|
|
255
|
+
# orphan of this one: it counts the way a name with no forge suffix does.
|
|
256
|
+
foreign_live += [n for n in candidates if n not in orphaned]
|
|
180
257
|
# Stale uses the full live set, NOT just the forge-filtered set, so a
|
|
181
258
|
# registry entry whose name happens to match a non-forge live collection
|
|
182
259
|
# would still count as stale. Registry entries always have forge suffixes
|
|
@@ -187,6 +264,7 @@ def classify(live: list[str], registry: list[str]) -> dict:
|
|
|
187
264
|
"live_names": sorted(all_live_set),
|
|
188
265
|
"healthy": healthy,
|
|
189
266
|
"orphaned": orphaned,
|
|
267
|
+
"orphaned_paths": {n: paths[n] for n in orphaned if paths.get(n)},
|
|
190
268
|
"stale": stale,
|
|
191
269
|
"foreign_filtered_count": len(foreign_live),
|
|
192
270
|
"foreign_filtered_sample": foreign_live[:FOREIGN_SAMPLE_CAP],
|
|
@@ -231,7 +309,10 @@ def _resolve_outside_cwd(command: str) -> str | None:
|
|
|
231
309
|
into CWD would execute a repo-planted shim (e.g. qmd.cmd). Such a
|
|
232
310
|
resolution is treated as not-found. Explicit paths supplied by callers
|
|
233
311
|
(containing a separator) are honored as-is. Keep identical to the
|
|
234
|
-
sibling
|
|
312
|
+
sibling guards in skf-detect-tools.py,
|
|
313
|
+
skf-merge-ccc-exclusions.py, skf-ccc-git-hygiene.py,
|
|
314
|
+
skf-source-tree.py, skf-tessl-review.py and
|
|
315
|
+
skf-verify-provenance-completeness.py.
|
|
235
316
|
"""
|
|
236
317
|
resolved = shutil.which(command)
|
|
237
318
|
if resolved is None:
|
|
@@ -246,25 +327,19 @@ def _resolve_outside_cwd(command: str) -> str | None:
|
|
|
246
327
|
return resolved
|
|
247
328
|
|
|
248
329
|
|
|
249
|
-
def
|
|
250
|
-
"""
|
|
251
|
-
|
|
252
|
-
Owns the CLI parsing so callers (and prompts) don't reinvent it.
|
|
253
|
-
Empty stdout / non-zero exit → ([], error_message). The classifier's
|
|
254
|
-
same-process invocation here is the single source of truth for what
|
|
255
|
-
counts as a "live collection name".
|
|
256
|
-
"""
|
|
330
|
+
def _run_qmd(*args: str) -> tuple[int | None, str, str]:
|
|
331
|
+
"""Run `qmd <args>`; return (exit code, stdout, stderr), exit code None when it could not run."""
|
|
257
332
|
import subprocess
|
|
258
333
|
# Resolve before spawning: on Windows qmd ships from npm as a .CMD
|
|
259
334
|
# shim, which a bare-name subprocess.run cannot launch (WinError 2).
|
|
260
335
|
qmd = _resolve_outside_cwd("qmd")
|
|
261
336
|
if qmd is None:
|
|
262
|
-
return
|
|
337
|
+
return None, "", "qmd not found on PATH"
|
|
263
338
|
try:
|
|
264
339
|
# qmd emits UTF-8; a locale-default decode (cp1252 on Windows)
|
|
265
340
|
# mojibakes names or raises UnicodeDecodeError on unmapped bytes.
|
|
266
341
|
result = subprocess.run(
|
|
267
|
-
[qmd,
|
|
342
|
+
[qmd, *args],
|
|
268
343
|
capture_output=True,
|
|
269
344
|
encoding="utf-8",
|
|
270
345
|
errors="replace",
|
|
@@ -272,33 +347,124 @@ def fetch_live_names_from_qmd() -> tuple[list[str], str | None]:
|
|
|
272
347
|
check=False,
|
|
273
348
|
)
|
|
274
349
|
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
|
275
|
-
return
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
350
|
+
return None, "", f"qmd {' '.join(args[:2])} failed: {e}"
|
|
351
|
+
return result.returncode, result.stdout or "", result.stderr or ""
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def fetch_live_names_from_qmd() -> tuple[list[str], str | None]:
|
|
355
|
+
"""Invoke `qmd collection list` and return (names, error).
|
|
356
|
+
|
|
357
|
+
Owns the CLI parsing so callers (and prompts) don't reinvent it.
|
|
358
|
+
Empty stdout / non-zero exit → ([], error_message). The classifier's
|
|
359
|
+
same-process invocation here is the single source of truth for what
|
|
360
|
+
counts as a "live collection name".
|
|
361
|
+
"""
|
|
362
|
+
code, out, err = _run_qmd("collection", "list")
|
|
363
|
+
if code is None:
|
|
364
|
+
return [], err
|
|
365
|
+
if code != 0:
|
|
366
|
+
return [], f"qmd collection list exited {code}: {err.strip() or '<no stderr>'}"
|
|
367
|
+
return parse_collection_list_output(out), None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def parse_collection_path(raw: str) -> str | None:
|
|
371
|
+
"""The folder a `qmd collection show` output names on its `Path:` line, or None."""
|
|
372
|
+
for line in raw.splitlines():
|
|
373
|
+
key, sep, value = line.strip().partition(":")
|
|
374
|
+
if sep and key == "Path" and value.strip():
|
|
375
|
+
return value.strip()
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def fetch_collection_path(name: str) -> tuple[str | None, str | None]:
|
|
380
|
+
"""Run `qmd collection show <name>`; return (its Path, error)."""
|
|
381
|
+
code, out, err = _run_qmd("collection", "show", name)
|
|
382
|
+
if code is None:
|
|
383
|
+
return None, err
|
|
384
|
+
if code != 0:
|
|
385
|
+
return None, f"qmd collection show exited {code}: {(err or out).strip() or '<no output>'}"
|
|
386
|
+
path = parse_collection_path(out)
|
|
387
|
+
return (path, None) if path else (None, "qmd collection show printed no Path line")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def remove_orphans(names: list[str], project_root) -> dict:
|
|
391
|
+
"""Remove each orphan whose Path still lies inside `project_root`.
|
|
392
|
+
|
|
393
|
+
The Path is read again just before each removal, so a collection that
|
|
394
|
+
another project re-created under the same name since the classification
|
|
395
|
+
is left alone. Returns {"removed", "failed", "errors"}.
|
|
396
|
+
"""
|
|
397
|
+
removed: list[str] = []
|
|
398
|
+
failed: list[str] = []
|
|
399
|
+
errors: dict[str, str] = {}
|
|
400
|
+
for name in names:
|
|
401
|
+
path, error = fetch_collection_path(name)
|
|
402
|
+
if error is None and not is_inside(path, project_root):
|
|
403
|
+
error = "its Path no longer lies inside the project root"
|
|
404
|
+
if error is None:
|
|
405
|
+
code, out, err = _run_qmd("collection", "remove", name)
|
|
406
|
+
if code is None:
|
|
407
|
+
error = err
|
|
408
|
+
elif code != 0:
|
|
409
|
+
error = f"qmd collection remove exited {code}: {(err or out).strip() or '<no output>'}"
|
|
410
|
+
if error is None:
|
|
411
|
+
removed.append(name)
|
|
412
|
+
else:
|
|
413
|
+
failed.append(name)
|
|
414
|
+
errors[name] = " ".join(error.split())
|
|
415
|
+
return {"removed": removed, "failed": failed, "errors": errors}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def load_orphaned_names(path: Path) -> list[str]:
|
|
419
|
+
"""The `orphaned` list of a staged classification, or exit 1 when there is none."""
|
|
420
|
+
try:
|
|
421
|
+
data = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
422
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as e:
|
|
423
|
+
_die(1, f"remove-orphans: cannot read the classification {path}: {e}")
|
|
424
|
+
orphaned = data.get("orphaned") if isinstance(data, dict) else None
|
|
425
|
+
if not isinstance(orphaned, list) or not all(isinstance(n, str) for n in orphaned):
|
|
426
|
+
_die(1, f"remove-orphans: {path} holds no `orphaned` list of names")
|
|
427
|
+
return orphaned
|
|
279
428
|
|
|
280
429
|
|
|
281
430
|
def main() -> None:
|
|
282
431
|
parser = argparse.ArgumentParser(
|
|
283
|
-
description="Classify QMD collections vs forge registry.",
|
|
432
|
+
description="Classify QMD collections vs forge registry, and remove the orphans.",
|
|
284
433
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
285
434
|
)
|
|
286
|
-
parser.
|
|
435
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
436
|
+
p_classify = sub.add_parser("classify", help="Classify live collections against the registry")
|
|
437
|
+
p_classify.add_argument(
|
|
287
438
|
"--live-names",
|
|
288
439
|
default=None,
|
|
289
440
|
help="Comma-separated list of collection names currently in QMD. "
|
|
290
441
|
"If omitted, the script invokes `qmd collection list` itself. "
|
|
291
442
|
"Empty string → no live collections.",
|
|
292
443
|
)
|
|
293
|
-
|
|
444
|
+
p_classify.add_argument(
|
|
294
445
|
"--registry-from-yaml",
|
|
295
446
|
type=Path,
|
|
296
447
|
required=True,
|
|
297
448
|
help="Path to forge-tier.yaml. Script reads qmd_collections array. "
|
|
298
449
|
"Missing file → empty registry (first-run state).",
|
|
299
450
|
)
|
|
451
|
+
p_classify.add_argument(
|
|
452
|
+
"--project-root",
|
|
453
|
+
required=True,
|
|
454
|
+
help="The project whose orphans may be offered for removal: a collection "
|
|
455
|
+
"outside the registry is orphaned only when its Path lies inside it.",
|
|
456
|
+
)
|
|
457
|
+
p_remove = sub.add_parser("remove-orphans",
|
|
458
|
+
help="Remove the orphans a staged classification lists")
|
|
459
|
+
p_remove.add_argument("--classification-from", type=Path, required=True,
|
|
460
|
+
help="The classify output (qmd-classify.json).")
|
|
461
|
+
p_remove.add_argument("--project-root", required=True,
|
|
462
|
+
help="Each Path is checked against this root again before its removal.")
|
|
300
463
|
args = parser.parse_args()
|
|
301
464
|
|
|
465
|
+
if args.cmd == "remove-orphans":
|
|
466
|
+
_ok(remove_orphans(load_orphaned_names(args.classification_from), args.project_root))
|
|
467
|
+
return
|
|
302
468
|
if args.live_names is None:
|
|
303
469
|
names, error = fetch_live_names_from_qmd()
|
|
304
470
|
if error is not None:
|
|
@@ -307,8 +473,26 @@ def main() -> None:
|
|
|
307
473
|
else:
|
|
308
474
|
live = parse_live_names(args.live_names)
|
|
309
475
|
registry = load_registry_names(args.registry_from_yaml)
|
|
310
|
-
|
|
476
|
+
candidates = sorted({n for n in live if is_forge_owned(n)} - set(registry))
|
|
477
|
+
paths = {name: fetch_collection_path(name)[0] for name in candidates}
|
|
478
|
+
_ok(classify(live, registry, args.project_root, paths))
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _force_utf8(*streams) -> None:
|
|
482
|
+
"""Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
|
|
483
|
+
|
|
484
|
+
A Windows console pipes them as cp1252, which cannot print every character
|
|
485
|
+
of the --help text, so --help would stop with UnicodeEncodeError.
|
|
486
|
+
"""
|
|
487
|
+
for stream in streams:
|
|
488
|
+
if hasattr(stream, "reconfigure"):
|
|
489
|
+
errors = getattr(stream, "errors", None)
|
|
490
|
+
if errors is None:
|
|
491
|
+
stream.reconfigure(encoding="utf-8")
|
|
492
|
+
else:
|
|
493
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
311
494
|
|
|
312
495
|
|
|
313
496
|
if __name__ == "__main__":
|
|
497
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
314
498
|
main()
|