bmad-module-skill-forge 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +49 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +135 -18
- package/docs/verifying-a-skill.md +83 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +2 -2
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +32 -12
- package/src/shared/references/description-guard-protocol.md +12 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +30 -14
- package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +694 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
- package/src/shared/scripts/skf-description-guard.py +99 -11
- package/src/shared/scripts/skf-detect-docs.py +694 -2
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
- package/src/shared/scripts/skf-detect-tools.py +209 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +4084 -85
- package/src/shared/scripts/skf-extraction-inventory.py +746 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +661 -0
- package/src/shared/scripts/skf-hash-content.py +166 -12
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +159 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +161 -27
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +670 -119
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +5 -5
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
- package/src/shared/scripts/skf-write-skill-brief.py +367 -19
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/init.md +148 -210
- package/src/skf-audit-skill/references/re-index.md +96 -108
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +27 -18
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +75 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
- package/src/skf-brief-skill/SKILL.md +36 -25
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +17 -24
- package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +142 -125
- package/src/skf-brief-skill/references/confirm-brief.md +10 -8
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +3 -2
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
- package/src/skf-create-skill/assets/skill-sections.md +15 -5
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +107 -29
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
- package/src/skf-create-skill/references/extract.md +200 -161
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
- package/src/skf-create-skill/references/extraction-patterns.md +584 -196
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +28 -59
- package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +28 -19
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +79 -146
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +118 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -90
- package/src/skf-setup/references/detect-and-tier.md +58 -67
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +217 -254
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +163 -88
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +162 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +41 -155
- package/src/skf-test-skill/references/source-access-protocol.md +19 -92
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +287 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
- package/src/skf-test-skill/scripts/score-signatures.py +459 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +233 -222
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +250 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +180 -47
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +100 -35
- package/src/skf-update-skill/references/write.md +331 -149
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -679
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = []
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Check Workspace Drift — pre-flight guard for gap-driven update-skill runs.
|
|
6
6
|
|
|
7
|
-
`skf-update-skill/references/
|
|
7
|
+
`skf-update-skill/references/gap-driven.md §3` defines a four-state guard:
|
|
8
8
|
the workspace at `source_root` must point at the commit the skill was pinned
|
|
9
9
|
against (`metadata.source_commit`), otherwise gap-driven spot-checks read
|
|
10
10
|
bytes that differ from the pinned tree and silently produce wrong results
|
|
@@ -14,13 +14,22 @@ code).
|
|
|
14
14
|
The guard's logic is deterministic — three git commands and a comparison —
|
|
15
15
|
but the prose form asked the LLM to chain them per run, with subtle short-
|
|
16
16
|
SHA prefix matching and skip-paths for non-git workspaces. This script bakes
|
|
17
|
-
the dispatch in.
|
|
17
|
+
the dispatch in. test-skill runs the same guard (`--workflow test-skill`,
|
|
18
|
+
one call per repository of a stack skill), and audit-skill asks the
|
|
19
|
+
`upstream` command below whether the remote moved past the skill's commit.
|
|
20
|
+
|
|
21
|
+
git runs with the git location variables (GIT_DIR, GIT_INDEX_FILE, ...)
|
|
22
|
+
removed from its environment. A git hook or `git rebase --exec` exports
|
|
23
|
+
them (GIT_DIR too when run in a linked worktree); inherited by
|
|
24
|
+
`git -C <source-root>`, they would make the check read another
|
|
25
|
+
repository's HEAD.
|
|
18
26
|
|
|
19
27
|
CLI:
|
|
20
28
|
uv run skf-check-workspace-drift.py <source-root> \\
|
|
21
29
|
--pinned-commit <SHA or empty> \\
|
|
22
30
|
[--source-ref <ref>] \\
|
|
23
|
-
[--allow-drift]
|
|
31
|
+
[--allow-drift] \\
|
|
32
|
+
[--workflow update-skill|test-skill]
|
|
24
33
|
|
|
25
34
|
Inputs:
|
|
26
35
|
source_root Filesystem path to the workspace under test.
|
|
@@ -34,6 +43,10 @@ Inputs:
|
|
|
34
43
|
--allow-drift Suppress the drift halt. Mismatches still return
|
|
35
44
|
status="overridden" so the caller can surface a
|
|
36
45
|
warning in the final report.
|
|
46
|
+
--workflow The workflow the halt message speaks for (WORKFLOWS):
|
|
47
|
+
update-skill (the default) or test-skill, which
|
|
48
|
+
checks each repository of a stack skill with its own
|
|
49
|
+
call. It changes halt_message only.
|
|
37
50
|
|
|
38
51
|
Output (JSON on stdout):
|
|
39
52
|
{
|
|
@@ -53,12 +66,91 @@ Exit codes:
|
|
|
53
66
|
2 — drift detected and --allow-drift was NOT passed; caller MUST halt
|
|
54
67
|
with status="halted-for-workspace-drift". The halt_message field
|
|
55
68
|
is pre-formatted with all substitutions applied.
|
|
69
|
+
|
|
70
|
+
upstream: has the remote moved past the baseline?
|
|
71
|
+
|
|
72
|
+
uv run skf-check-workspace-drift.py upstream --source-root <path> \\
|
|
73
|
+
--baseline-commit <SHA> --baseline-ref <ref> [--timeout <seconds>]
|
|
74
|
+
|
|
75
|
+
A first argument `upstream` selects this command; a source root that is
|
|
76
|
+
a folder named `upstream` in the current folder is passed as
|
|
77
|
+
`./upstream` to the check above.
|
|
78
|
+
|
|
79
|
+
One `git ls-remote --symref origin` call reads the remote's default
|
|
80
|
+
branch, its tags (an annotated tag resolves to its commit) and the
|
|
81
|
+
baseline branch. Nothing is fetched and nothing in the clone changes, so
|
|
82
|
+
a `--depth 1 --branch <tag>` clone, which has no `origin/HEAD`, is read
|
|
83
|
+
like any other. The baseline ref decides what "moved" means:
|
|
84
|
+
|
|
85
|
+
- A tag whose name ends in a version (`v1.2.3`, `1.2.3-rc.1`,
|
|
86
|
+
`pkg@1.2.3`, `tokio/v1.0.0`): the newest tag of its family, a release
|
|
87
|
+
unless the baseline is a pre-release, sorted by version. A monorepo
|
|
88
|
+
prefix (`pkg@`, `tokio/v`) is a family of its own. The release
|
|
89
|
+
prefixes, none, `v` and `V` (RELEASE_PREFIXES), are one family, since
|
|
90
|
+
a project may switch between them (express went from `4.22.0` to
|
|
91
|
+
`v4.22.1`); a tag under another of them than the baseline's counts
|
|
92
|
+
only when its version has a major and a minor number, so a date or
|
|
93
|
+
build number (`20240101`, `1234`) does not. The remote moved when that
|
|
94
|
+
tag names another commit and is newer than the baseline, or when no
|
|
95
|
+
newer tag exists and the baseline tag itself now names another commit,
|
|
96
|
+
unless `git merge-base --is-ancestor` finds that commit in the
|
|
97
|
+
baseline's history (it cannot in a shallow clone that lacks the
|
|
98
|
+
commit, so version order decides there). Commits on the default branch
|
|
99
|
+
after the tag are not a move.
|
|
100
|
+
- Any other tag: the commit it names now.
|
|
101
|
+
- A branch: the commit the remote branch names now.
|
|
102
|
+
- `HEAD` or a commit: the remote's default branch.
|
|
103
|
+
|
|
104
|
+
Output (JSON on stdout; every key is always present):
|
|
105
|
+
status "unchanged" | "moved" | "skipped" | "fetch-failed"
|
|
106
|
+
skip_reason null | "no-baseline-ref" | "no-baseline-commit" |
|
|
107
|
+
"git-unavailable" | "not-a-git-tree" |
|
|
108
|
+
"invalid-baseline-ref" | "baseline-ref-missing"
|
|
109
|
+
(UPSTREAM_SKIP_REASONS)
|
|
110
|
+
fetch_error git's last error line (fetch-failed), else null
|
|
111
|
+
basis null | "same-commit" | "no-newer-tag" |
|
|
112
|
+
"contained" | "newer-tag" | "tag-moved" |
|
|
113
|
+
"ref-moved" (UPSTREAM_BASES): what decided it
|
|
114
|
+
baseline_ref as given
|
|
115
|
+
ref_kind "tag" | "branch" | "head" | "commit" | null
|
|
116
|
+
baseline_commit the full commit when the clone holds it, else as given
|
|
117
|
+
baseline_commit_short
|
|
118
|
+
remote_head the commit of the remote's default branch, else null
|
|
119
|
+
remote_head_short
|
|
120
|
+
remote_default_branch its name ("main"), else null
|
|
121
|
+
latest_tag the newest tag of the baseline's family (for a
|
|
122
|
+
branch, HEAD or commit: of a release prefix), else null
|
|
123
|
+
latest_tag_commit and its commit, else null
|
|
124
|
+
latest_tag_commit_short
|
|
125
|
+
upstream_ref moved: the ref to read instead (the newer tag, the
|
|
126
|
+
moved tag or branch, or "HEAD"), else null
|
|
127
|
+
upstream_commit moved: its commit, else null
|
|
128
|
+
upstream_commit_short
|
|
129
|
+
audit_ref the values an audit that stays on the baseline
|
|
130
|
+
audit_ref_source records: baseline_ref or "(unknown)", "baseline"
|
|
131
|
+
audit_commit ("unavailable" without ref and commit), and
|
|
132
|
+
baseline_commit or "(unknown)"
|
|
133
|
+
log_message "upstream_check: <status> (...)"
|
|
134
|
+
warnings list of strings
|
|
135
|
+
|
|
136
|
+
A baseline commit that is not 7 to 40 hex digits counts as missing.
|
|
137
|
+
|
|
138
|
+
Exit codes: 0 once the JSON is printed (it never gates; the caller
|
|
139
|
+
owns the choice), 1 unexpected error (one JSON line on stderr), 2 usage
|
|
140
|
+
error. git runs through skf-source-tree.py (the sibling in this folder,
|
|
141
|
+
which must sit beside this script) with LC_ALL=C, no hooks, no terminal
|
|
142
|
+
or credential-manager prompt and the git location variables removed. A
|
|
143
|
+
remote query still running after --timeout seconds (60 by default) is
|
|
144
|
+
stopped with everything git started, and reported as fetch-failed.
|
|
56
145
|
"""
|
|
57
146
|
|
|
58
147
|
from __future__ import annotations
|
|
59
148
|
|
|
60
149
|
import argparse
|
|
150
|
+
import importlib.util
|
|
61
151
|
import json
|
|
152
|
+
import os
|
|
153
|
+
import re
|
|
62
154
|
import shutil
|
|
63
155
|
import subprocess
|
|
64
156
|
import sys
|
|
@@ -67,6 +159,17 @@ from pathlib import Path
|
|
|
67
159
|
|
|
68
160
|
SKIP_NO_PINNED = "no-pinned-commit"
|
|
69
161
|
SKIP_NOT_GIT = "not-a-git-tree"
|
|
162
|
+
# Keep identical to GIT_LOCATION_VARS in skf-merge-ccc-exclusions.py.
|
|
163
|
+
GIT_LOCATION_VARS = (
|
|
164
|
+
"GIT_DIR",
|
|
165
|
+
"GIT_WORK_TREE",
|
|
166
|
+
"GIT_INDEX_FILE",
|
|
167
|
+
"GIT_PREFIX",
|
|
168
|
+
"GIT_COMMON_DIR",
|
|
169
|
+
"GIT_OBJECT_DIRECTORY",
|
|
170
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
171
|
+
"GIT_NAMESPACE",
|
|
172
|
+
)
|
|
70
173
|
|
|
71
174
|
|
|
72
175
|
# --------------------------------------------------------------------------
|
|
@@ -75,12 +178,18 @@ SKIP_NOT_GIT = "not-a-git-tree"
|
|
|
75
178
|
|
|
76
179
|
|
|
77
180
|
def _git(args: list[str], *, cwd: Path) -> tuple[int, str, str]:
|
|
78
|
-
"""Run a git command; return (rc, stdout, stderr). Stdout/stderr stripped.
|
|
181
|
+
"""Run a git command; return (rc, stdout, stderr). Stdout/stderr stripped.
|
|
182
|
+
|
|
183
|
+
The git location variables are dropped from the child's environment
|
|
184
|
+
(see the module docstring).
|
|
185
|
+
"""
|
|
186
|
+
env = {k: v for k, v in os.environ.items() if k not in GIT_LOCATION_VARS}
|
|
79
187
|
proc = subprocess.run(
|
|
80
188
|
["git", "-C", str(cwd), *args],
|
|
81
189
|
capture_output=True,
|
|
82
190
|
text=True,
|
|
83
191
|
check=False,
|
|
192
|
+
env=env,
|
|
84
193
|
)
|
|
85
194
|
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
|
|
86
195
|
|
|
@@ -123,8 +232,8 @@ def classify_match(pinned: str, head: str) -> str | None:
|
|
|
123
232
|
return None
|
|
124
233
|
|
|
125
234
|
|
|
126
|
-
def short(sha: str) -> str:
|
|
127
|
-
return sha[:7]
|
|
235
|
+
def short(sha: str | None) -> str | None:
|
|
236
|
+
return sha[:7] if sha else None
|
|
128
237
|
|
|
129
238
|
|
|
130
239
|
# --------------------------------------------------------------------------
|
|
@@ -149,12 +258,39 @@ that spot-checks will read bytes that differ from the pinned commit),
|
|
|
149
258
|
re-run update-skill with `--allow-workspace-drift`."""
|
|
150
259
|
|
|
151
260
|
|
|
261
|
+
TEST_HALT_MESSAGE_TEMPLATE = """\
|
|
262
|
+
Workspace HEAD does not match the commit this skill was pinned against.
|
|
263
|
+
|
|
264
|
+
pinned (metadata.source_commit): {pinned_commit}
|
|
265
|
+
pinned ref (metadata.source_ref): {source_ref}
|
|
266
|
+
workspace HEAD ({source_root}): {head_sha}
|
|
267
|
+
|
|
268
|
+
Test-skill verifies against the source the skill was extracted from.
|
|
269
|
+
Testing against a drifted tree produces false gaps and mismatches. Re-sync:
|
|
270
|
+
|
|
271
|
+
git -C "{source_root}" checkout {checkout_target}
|
|
272
|
+
|
|
273
|
+
Or re-run test-skill with `--allow-workspace-drift` to test against the
|
|
274
|
+
current workspace (accepts that findings reflect HEAD, not the pin)."""
|
|
275
|
+
|
|
276
|
+
HALT_MESSAGES = {
|
|
277
|
+
"update-skill": HALT_MESSAGE_TEMPLATE,
|
|
278
|
+
"test-skill": TEST_HALT_MESSAGE_TEMPLATE,
|
|
279
|
+
}
|
|
280
|
+
WORKFLOWS = tuple(HALT_MESSAGES)
|
|
281
|
+
|
|
282
|
+
|
|
152
283
|
def build_halt_message(
|
|
153
|
-
*,
|
|
284
|
+
*,
|
|
285
|
+
source_root: Path,
|
|
286
|
+
pinned_commit: str,
|
|
287
|
+
head_sha_full: str,
|
|
288
|
+
source_ref: str | None,
|
|
289
|
+
workflow: str = "update-skill",
|
|
154
290
|
) -> str:
|
|
155
291
|
ref_display = source_ref if source_ref else "unset"
|
|
156
292
|
checkout_target = source_ref if source_ref else pinned_commit
|
|
157
|
-
return
|
|
293
|
+
return HALT_MESSAGES[workflow].format(
|
|
158
294
|
pinned_commit=pinned_commit,
|
|
159
295
|
source_ref=ref_display,
|
|
160
296
|
source_root=str(source_root),
|
|
@@ -174,6 +310,7 @@ def check(
|
|
|
174
310
|
pinned_commit: str,
|
|
175
311
|
source_ref: str | None,
|
|
176
312
|
allow_drift: bool,
|
|
313
|
+
workflow: str = "update-skill",
|
|
177
314
|
) -> dict:
|
|
178
315
|
"""Run the four-state guard and return a result envelope."""
|
|
179
316
|
if is_skippable_pinned(pinned_commit):
|
|
@@ -234,6 +371,7 @@ def check(
|
|
|
234
371
|
pinned_commit=pinned_commit,
|
|
235
372
|
head_sha_full=head,
|
|
236
373
|
source_ref=source_ref,
|
|
374
|
+
workflow=workflow,
|
|
237
375
|
)
|
|
238
376
|
if allow_drift:
|
|
239
377
|
return {
|
|
@@ -265,12 +403,396 @@ def check(
|
|
|
265
403
|
}
|
|
266
404
|
|
|
267
405
|
|
|
406
|
+
# --------------------------------------------------------------------------
|
|
407
|
+
# upstream
|
|
408
|
+
# --------------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
UPSTREAM_SKIP_REASONS = (
|
|
412
|
+
"no-baseline-ref",
|
|
413
|
+
"no-baseline-commit",
|
|
414
|
+
"git-unavailable",
|
|
415
|
+
"not-a-git-tree",
|
|
416
|
+
"invalid-baseline-ref",
|
|
417
|
+
"baseline-ref-missing",
|
|
418
|
+
)
|
|
419
|
+
UPSTREAM_BASES = (
|
|
420
|
+
"same-commit",
|
|
421
|
+
"no-newer-tag",
|
|
422
|
+
"contained",
|
|
423
|
+
"newer-tag",
|
|
424
|
+
"tag-moved",
|
|
425
|
+
"ref-moved",
|
|
426
|
+
)
|
|
427
|
+
DEFAULT_UPSTREAM_TIMEOUT_SEC = 60.0
|
|
428
|
+
LOCAL_TIMEOUT_SEC = 30.0
|
|
429
|
+
# The version prefixes of release tags (`4.22.0`, `v4.22.1`, `V2.0`): one
|
|
430
|
+
# family, since a project may switch from one to another.
|
|
431
|
+
RELEASE_PREFIXES = ("", "v", "V")
|
|
432
|
+
_MAJOR_MINOR_RE = re.compile(r"\d+\.\d+")
|
|
433
|
+
_UNSET = {"", "null", "none", "local"}
|
|
434
|
+
_HEAD_REFS = {"head"}
|
|
435
|
+
_SOURCE_TREE = None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _sibling():
|
|
439
|
+
"""skf-source-tree.py from this folder, loaded once: upstream runs git
|
|
440
|
+
through its runner (_run stops a call and all it started at the time
|
|
441
|
+
limit; _resolve_outside_cwd never takes a git planted in the current
|
|
442
|
+
folder), sorts tags with its split_version and shares its SHA40_RE,
|
|
443
|
+
REF_RE, _last_error and _stat_dir."""
|
|
444
|
+
global _SOURCE_TREE
|
|
445
|
+
if _SOURCE_TREE is None:
|
|
446
|
+
path = Path(__file__).resolve().parent / "skf-source-tree.py"
|
|
447
|
+
spec = importlib.util.spec_from_file_location("skf_source_tree", path)
|
|
448
|
+
if spec is None or spec.loader is None:
|
|
449
|
+
raise ImportError(f"cannot load {path}")
|
|
450
|
+
module = importlib.util.module_from_spec(spec)
|
|
451
|
+
spec.loader.exec_module(module)
|
|
452
|
+
_SOURCE_TREE = module
|
|
453
|
+
return _SOURCE_TREE
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _git_capped(cwd: Path, *args: str, timeout: float) -> tuple[int | None, str, str]:
|
|
457
|
+
"""`git -C cwd <args>` with no hooks, no prompts and a time limit.
|
|
458
|
+
|
|
459
|
+
(returncode, stdout, stderr): returncode None when git cannot start or
|
|
460
|
+
the time limit stopped it (stderr then says which).
|
|
461
|
+
"""
|
|
462
|
+
tree = _sibling()
|
|
463
|
+
exe = tree._resolve_outside_cwd("git")
|
|
464
|
+
if exe is None:
|
|
465
|
+
return None, "", "git is not installed"
|
|
466
|
+
argv = [exe, "-c", f"core.hooksPath={os.devnull}", "-C", str(cwd), *args]
|
|
467
|
+
try:
|
|
468
|
+
rc, out, err, _killed = tree._run(argv, max(0.1, timeout))
|
|
469
|
+
except (OSError, ValueError) as e:
|
|
470
|
+
return None, "", f"git could not run: {e}"
|
|
471
|
+
if rc is None:
|
|
472
|
+
return None, "", f"timed out after {timeout:g} seconds"
|
|
473
|
+
return (
|
|
474
|
+
rc,
|
|
475
|
+
out.decode("utf-8", errors="replace"),
|
|
476
|
+
err.decode("utf-8", errors="replace"),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _parse_ls_remote(text: str) -> tuple[dict, dict, dict]:
|
|
481
|
+
"""(refs, tags, symrefs) of `git ls-remote --symref` output.
|
|
482
|
+
|
|
483
|
+
tags maps a tag name to the commit it names: the peeled `^{}` line of
|
|
484
|
+
an annotated tag wins over the tag object.
|
|
485
|
+
"""
|
|
486
|
+
sha40 = _sibling().SHA40_RE
|
|
487
|
+
refs, tags, symrefs = {}, {}, {}
|
|
488
|
+
for line in text.splitlines():
|
|
489
|
+
left, _, name = line.partition("\t")
|
|
490
|
+
left, name = left.strip(), name.strip()
|
|
491
|
+
if not name:
|
|
492
|
+
continue
|
|
493
|
+
if left.startswith("ref: "):
|
|
494
|
+
symrefs[name] = left[len("ref: ") :].strip()
|
|
495
|
+
elif sha40.match(left):
|
|
496
|
+
refs[name] = left
|
|
497
|
+
if name.startswith("refs/tags/"):
|
|
498
|
+
tag = name[len("refs/tags/") :]
|
|
499
|
+
if tag.endswith("^{}"):
|
|
500
|
+
tags[tag[:-3]] = left
|
|
501
|
+
else:
|
|
502
|
+
tags.setdefault(tag, left)
|
|
503
|
+
return refs, tags, symrefs
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _family(prefix: str):
|
|
507
|
+
"""The test a tag passes, given its name and version prefix, to count
|
|
508
|
+
as a release of a baseline tag whose version prefix is `prefix`.
|
|
509
|
+
|
|
510
|
+
A monorepo prefix (`pkg@`, `tokio/v`) counts only itself. A release
|
|
511
|
+
prefix counts the other two as well, for a tag whose version has a
|
|
512
|
+
major and a minor number: a date or build number (`20240101`, `1234`)
|
|
513
|
+
under another release prefix never counts.
|
|
514
|
+
"""
|
|
515
|
+
if prefix not in RELEASE_PREFIXES:
|
|
516
|
+
return lambda name, tag_prefix: tag_prefix == prefix
|
|
517
|
+
return lambda name, tag_prefix: tag_prefix == prefix or (
|
|
518
|
+
tag_prefix in RELEASE_PREFIXES
|
|
519
|
+
and _MAJOR_MINOR_RE.match(name, len(tag_prefix)) is not None
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _newest(tags: dict, counts, releases_only: bool) -> tuple[str | None, tuple | None]:
|
|
524
|
+
"""(name, key) of the newest tag counts(name, prefix) takes, else (None, None)."""
|
|
525
|
+
best, best_key = None, None
|
|
526
|
+
for name in tags:
|
|
527
|
+
split = _sibling().split_version(name)
|
|
528
|
+
if split is None or not counts(name, split[0]):
|
|
529
|
+
continue
|
|
530
|
+
if releases_only and split[1][1] == 0:
|
|
531
|
+
continue
|
|
532
|
+
if best is None or (split[1], name) > (best_key, best):
|
|
533
|
+
best, best_key = name, split[1]
|
|
534
|
+
return best, best_key
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def upstream(
|
|
538
|
+
source_root: Path,
|
|
539
|
+
*,
|
|
540
|
+
baseline_commit: str,
|
|
541
|
+
baseline_ref: str,
|
|
542
|
+
timeout: float = DEFAULT_UPSTREAM_TIMEOUT_SEC,
|
|
543
|
+
) -> dict:
|
|
544
|
+
"""Compare the baseline with what the remote names now (module docstring)."""
|
|
545
|
+
ref = (baseline_ref or "").strip()
|
|
546
|
+
commit = (baseline_commit or "").strip()
|
|
547
|
+
ref_unset = ref.lower() in _UNSET
|
|
548
|
+
# "", "local", "null" and "none" are not 7 to 40 hex digits either.
|
|
549
|
+
commit_unset = not re.fullmatch(r"[0-9A-Fa-f]{7,40}", commit)
|
|
550
|
+
out = {
|
|
551
|
+
"status": "skipped",
|
|
552
|
+
"skip_reason": None,
|
|
553
|
+
"fetch_error": None,
|
|
554
|
+
"basis": None,
|
|
555
|
+
"baseline_ref": baseline_ref,
|
|
556
|
+
"ref_kind": None,
|
|
557
|
+
"baseline_commit": baseline_commit,
|
|
558
|
+
"baseline_commit_short": None if commit_unset else short(commit),
|
|
559
|
+
"remote_head": None,
|
|
560
|
+
"remote_head_short": None,
|
|
561
|
+
"remote_default_branch": None,
|
|
562
|
+
"latest_tag": None,
|
|
563
|
+
"latest_tag_commit": None,
|
|
564
|
+
"latest_tag_commit_short": None,
|
|
565
|
+
"upstream_ref": None,
|
|
566
|
+
"upstream_commit": None,
|
|
567
|
+
"upstream_commit_short": None,
|
|
568
|
+
"audit_ref": "(unknown)" if ref_unset else ref,
|
|
569
|
+
"audit_ref_source": "unavailable" if ref_unset and commit_unset else "baseline",
|
|
570
|
+
"audit_commit": "(unknown)" if commit_unset else commit,
|
|
571
|
+
"log_message": "",
|
|
572
|
+
"warnings": [],
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
def done(status: str, reason: str | None = None) -> dict:
|
|
576
|
+
out["status"] = status
|
|
577
|
+
out["skip_reason"] = reason if status == "skipped" else None
|
|
578
|
+
if status == "skipped":
|
|
579
|
+
detail = reason
|
|
580
|
+
elif status == "fetch-failed":
|
|
581
|
+
detail = out["fetch_error"]
|
|
582
|
+
elif status == "moved":
|
|
583
|
+
detail = (
|
|
584
|
+
f"{ref} {out['baseline_commit_short']} -> "
|
|
585
|
+
f"{out['upstream_ref']} {out['upstream_commit_short']}"
|
|
586
|
+
)
|
|
587
|
+
else:
|
|
588
|
+
detail = f"{ref} at {out['baseline_commit_short']}"
|
|
589
|
+
out["log_message"] = f"upstream_check: {status} ({detail})"
|
|
590
|
+
return out
|
|
591
|
+
|
|
592
|
+
if ref_unset:
|
|
593
|
+
return done("skipped", "no-baseline-ref")
|
|
594
|
+
if commit_unset:
|
|
595
|
+
return done("skipped", "no-baseline-commit")
|
|
596
|
+
tree = _sibling()
|
|
597
|
+
if tree._resolve_outside_cwd("git") is None:
|
|
598
|
+
return done("skipped", "git-unavailable")
|
|
599
|
+
# os.stat, not pathlib: before Python 3.14, is_dir() raises inside a
|
|
600
|
+
# folder the user cannot search.
|
|
601
|
+
if tree._stat_dir(source_root) is not True:
|
|
602
|
+
return done("skipped", "not-a-git-tree")
|
|
603
|
+
rc, text, _err = _git_capped(
|
|
604
|
+
source_root, "rev-parse", "--is-inside-work-tree", timeout=LOCAL_TIMEOUT_SEC
|
|
605
|
+
)
|
|
606
|
+
if rc != 0 or text.strip() != "true":
|
|
607
|
+
return done("skipped", "not-a-git-tree")
|
|
608
|
+
head_ref = ref.lower() in _HEAD_REFS
|
|
609
|
+
if not head_ref and (
|
|
610
|
+
not tree.REF_RE.fullmatch(ref) or ".." in ref or ref.endswith((".", "/"))
|
|
611
|
+
):
|
|
612
|
+
return done("skipped", "invalid-baseline-ref")
|
|
613
|
+
|
|
614
|
+
lowered = commit.lower()
|
|
615
|
+
rc, text, _err = _git_capped(
|
|
616
|
+
source_root,
|
|
617
|
+
"rev-parse",
|
|
618
|
+
"--verify",
|
|
619
|
+
"--quiet",
|
|
620
|
+
f"{lowered}^{{commit}}",
|
|
621
|
+
timeout=LOCAL_TIMEOUT_SEC,
|
|
622
|
+
)
|
|
623
|
+
full = text.strip() if rc == 0 and tree.SHA40_RE.match(text.strip()) else None
|
|
624
|
+
if full and classify_match(lowered, full):
|
|
625
|
+
out.update(
|
|
626
|
+
baseline_commit=full,
|
|
627
|
+
baseline_commit_short=short(full),
|
|
628
|
+
audit_commit=full,
|
|
629
|
+
)
|
|
630
|
+
base = full or lowered
|
|
631
|
+
|
|
632
|
+
patterns = ["HEAD", "refs/tags/*"]
|
|
633
|
+
if not head_ref:
|
|
634
|
+
patterns.append(f"refs/heads/{ref}")
|
|
635
|
+
rc, text, err = _git_capped(
|
|
636
|
+
source_root, "ls-remote", "--symref", "--", "origin", *patterns, timeout=timeout
|
|
637
|
+
)
|
|
638
|
+
if rc != 0:
|
|
639
|
+
out["fetch_error"] = tree._last_error(err) if rc is not None else err
|
|
640
|
+
return done("fetch-failed")
|
|
641
|
+
refs, tags, symrefs = _parse_ls_remote(text)
|
|
642
|
+
remote_head = refs.get("HEAD")
|
|
643
|
+
out.update(remote_head=remote_head, remote_head_short=short(remote_head))
|
|
644
|
+
head_target = symrefs.get("HEAD", "")
|
|
645
|
+
if head_target.startswith("refs/heads/"):
|
|
646
|
+
out["remote_default_branch"] = head_target[len("refs/heads/") :]
|
|
647
|
+
|
|
648
|
+
def newer(name: str | None, sha: str | None, basis: str) -> dict:
|
|
649
|
+
out.update(
|
|
650
|
+
upstream_ref=name,
|
|
651
|
+
upstream_commit=sha,
|
|
652
|
+
upstream_commit_short=short(sha),
|
|
653
|
+
basis=basis,
|
|
654
|
+
)
|
|
655
|
+
return done("moved")
|
|
656
|
+
|
|
657
|
+
def same(basis: str) -> dict:
|
|
658
|
+
out["basis"] = basis
|
|
659
|
+
return done("unchanged")
|
|
660
|
+
|
|
661
|
+
def show_latest(name: str) -> None:
|
|
662
|
+
out.update(
|
|
663
|
+
latest_tag=name,
|
|
664
|
+
latest_tag_commit=tags[name],
|
|
665
|
+
latest_tag_commit_short=short(tags[name]),
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
split = tree.split_version(ref)
|
|
669
|
+
if ref in tags:
|
|
670
|
+
kind = "tag"
|
|
671
|
+
elif f"refs/heads/{ref}" in refs:
|
|
672
|
+
kind = "branch"
|
|
673
|
+
elif head_ref:
|
|
674
|
+
kind = "head"
|
|
675
|
+
elif re.fullmatch(r"[0-9a-fA-F]{7,40}", ref):
|
|
676
|
+
kind = "commit"
|
|
677
|
+
elif split is not None:
|
|
678
|
+
kind = "tag" # deleted upstream: its newer siblings still count
|
|
679
|
+
out["warnings"].append(f"the baseline tag {ref} no longer exists upstream")
|
|
680
|
+
else:
|
|
681
|
+
return done("skipped", "baseline-ref-missing")
|
|
682
|
+
out["ref_kind"] = kind
|
|
683
|
+
|
|
684
|
+
if kind != "tag":
|
|
685
|
+
latest, _key = _newest(
|
|
686
|
+
tags, lambda _name, prefix: prefix in RELEASE_PREFIXES, releases_only=True
|
|
687
|
+
)
|
|
688
|
+
if latest:
|
|
689
|
+
show_latest(latest)
|
|
690
|
+
if kind == "branch":
|
|
691
|
+
name, tip = ref, refs[f"refs/heads/{ref}"]
|
|
692
|
+
else:
|
|
693
|
+
name, tip = "HEAD", remote_head
|
|
694
|
+
if tip is None:
|
|
695
|
+
return done("skipped", "baseline-ref-missing")
|
|
696
|
+
if classify_match(base, tip):
|
|
697
|
+
return same("same-commit")
|
|
698
|
+
return newer(name, tip, "ref-moved")
|
|
699
|
+
|
|
700
|
+
tip = tags.get(ref)
|
|
701
|
+
if split is None:
|
|
702
|
+
if classify_match(base, tip):
|
|
703
|
+
return same("same-commit")
|
|
704
|
+
return newer(ref, tip, "tag-moved")
|
|
705
|
+
prefix, key = split
|
|
706
|
+
latest, latest_key = _newest(tags, _family(prefix), releases_only=key[1] == 1)
|
|
707
|
+
if latest is None:
|
|
708
|
+
return done("skipped", "baseline-ref-missing")
|
|
709
|
+
latest_commit = tags[latest]
|
|
710
|
+
show_latest(latest)
|
|
711
|
+
if classify_match(base, latest_commit):
|
|
712
|
+
return same("same-commit")
|
|
713
|
+
|
|
714
|
+
def contained(sha: str) -> bool:
|
|
715
|
+
"""True when the clone holds sha in the baseline commit's history."""
|
|
716
|
+
if not full:
|
|
717
|
+
return False
|
|
718
|
+
rc, _text, _err = _git_capped(
|
|
719
|
+
source_root,
|
|
720
|
+
"merge-base",
|
|
721
|
+
"--is-ancestor",
|
|
722
|
+
sha,
|
|
723
|
+
full,
|
|
724
|
+
timeout=LOCAL_TIMEOUT_SEC,
|
|
725
|
+
)
|
|
726
|
+
return rc == 0
|
|
727
|
+
|
|
728
|
+
if latest_key <= key:
|
|
729
|
+
if tip is None or classify_match(base, tip):
|
|
730
|
+
return same("no-newer-tag")
|
|
731
|
+
return same("contained") if contained(tip) else newer(ref, tip, "tag-moved")
|
|
732
|
+
if contained(latest_commit):
|
|
733
|
+
return same("contained")
|
|
734
|
+
return newer(latest, latest_commit, "newer-tag")
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _build_upstream_parser() -> argparse.ArgumentParser:
|
|
738
|
+
parser = argparse.ArgumentParser(
|
|
739
|
+
prog="skf-check-workspace-drift upstream",
|
|
740
|
+
description=(
|
|
741
|
+
"Tell whether the remote moved past the commit and ref a skill was "
|
|
742
|
+
"built from."
|
|
743
|
+
),
|
|
744
|
+
)
|
|
745
|
+
parser.add_argument(
|
|
746
|
+
"--source-root",
|
|
747
|
+
required=True,
|
|
748
|
+
help="the skill's source clone",
|
|
749
|
+
)
|
|
750
|
+
parser.add_argument(
|
|
751
|
+
"--baseline-commit",
|
|
752
|
+
required=True,
|
|
753
|
+
help="the commit the skill was built from",
|
|
754
|
+
)
|
|
755
|
+
parser.add_argument(
|
|
756
|
+
"--baseline-ref",
|
|
757
|
+
required=True,
|
|
758
|
+
help="the tag, branch or HEAD it was built from",
|
|
759
|
+
)
|
|
760
|
+
parser.add_argument(
|
|
761
|
+
"--timeout",
|
|
762
|
+
type=float,
|
|
763
|
+
default=DEFAULT_UPSTREAM_TIMEOUT_SEC,
|
|
764
|
+
help="seconds the remote query may take (default 60)",
|
|
765
|
+
)
|
|
766
|
+
return parser
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def upstream_main(argv: list[str]) -> int:
|
|
770
|
+
args = _build_upstream_parser().parse_args(argv)
|
|
771
|
+
try:
|
|
772
|
+
result = upstream(
|
|
773
|
+
Path(os.path.expanduser(args.source_root)),
|
|
774
|
+
baseline_commit=args.baseline_commit,
|
|
775
|
+
baseline_ref=args.baseline_ref,
|
|
776
|
+
timeout=args.timeout,
|
|
777
|
+
)
|
|
778
|
+
except Exception as e: # noqa: BLE001 - one JSON error line, never a traceback
|
|
779
|
+
error = {"status": "error", "message": f"{type(e).__name__}: {e}"}
|
|
780
|
+
print(json.dumps(error), file=sys.stderr)
|
|
781
|
+
return 1
|
|
782
|
+
json.dump(result, sys.stdout, indent=2)
|
|
783
|
+
sys.stdout.write("\n")
|
|
784
|
+
return 0
|
|
785
|
+
|
|
786
|
+
|
|
268
787
|
# --------------------------------------------------------------------------
|
|
269
788
|
# CLI
|
|
270
789
|
# --------------------------------------------------------------------------
|
|
271
790
|
|
|
272
791
|
|
|
273
792
|
def main(argv: list[str] | None = None) -> int:
|
|
793
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
794
|
+
if argv[:1] == ["upstream"]:
|
|
795
|
+
return upstream_main(argv[1:])
|
|
274
796
|
parser = argparse.ArgumentParser(
|
|
275
797
|
prog="skf-check-workspace-drift",
|
|
276
798
|
description=(
|
|
@@ -294,6 +816,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
294
816
|
action="store_true",
|
|
295
817
|
help="suppress the drift halt; mismatch becomes status=overridden",
|
|
296
818
|
)
|
|
819
|
+
parser.add_argument(
|
|
820
|
+
"--workflow",
|
|
821
|
+
choices=WORKFLOWS,
|
|
822
|
+
default="update-skill",
|
|
823
|
+
help="the workflow the halt message speaks for (default update-skill)",
|
|
824
|
+
)
|
|
297
825
|
args = parser.parse_args(argv)
|
|
298
826
|
|
|
299
827
|
source_root = Path(args.source_root)
|
|
@@ -309,6 +837,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
309
837
|
pinned_commit=args.pinned_commit,
|
|
310
838
|
source_ref=args.source_ref,
|
|
311
839
|
allow_drift=args.allow_drift,
|
|
840
|
+
workflow=args.workflow,
|
|
312
841
|
)
|
|
313
842
|
json.dump(result, sys.stdout, indent=2)
|
|
314
843
|
sys.stdout.write("\n")
|