bmad-module-skill-forge 0.9.0 → 0.10.1-alpha.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 +39 -0
- package/.gitattributes +16 -0
- package/README.md +105 -38
- package/docs/404.md +13 -10
- package/docs/RELEASING.md +185 -0
- package/docs/STABILITY.md +123 -0
- package/docs/_data/pinned.yaml +98 -0
- package/docs/agents.md +16 -11
- package/docs/architecture.md +221 -0
- package/docs/bmad-synergy.md +11 -13
- package/docs/concepts.md +18 -108
- package/docs/examples.md +121 -80
- package/docs/getting-started.md +85 -147
- package/docs/how-it-works.md +25 -536
- package/docs/index.md +44 -27
- package/docs/skill-model.md +328 -0
- package/docs/troubleshooting.md +39 -0
- package/docs/verifying-a-skill.md +232 -0
- package/docs/why-skf.md +93 -0
- package/docs/workflows.md +119 -13
- package/package.json +25 -4
- package/src/README.md +23 -25
- package/src/forger/forge-tier.yaml +4 -2
- package/src/forger/preferences.yaml +7 -1
- package/src/knowledge/agentskills-spec.md +1 -1
- package/src/knowledge/architecture-verification.md +1 -1
- package/src/knowledge/ccc-bridge.md +16 -15
- package/src/knowledge/overview.md +11 -11
- package/src/knowledge/progressive-capability.md +3 -3
- package/src/knowledge/provenance-tracking.md +9 -3
- package/src/knowledge/qmd-registry.md +8 -8
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +16 -1
- package/src/knowledge/split-body-strategy.md +1 -1
- package/src/knowledge/version-paths.md +17 -12
- package/src/module-help.csv +18 -17
- package/src/module.yaml +4 -0
- package/src/shared/health-check.md +372 -0
- package/src/shared/references/feasibility-report-schema.md +86 -0
- package/src/shared/references/headless-gate-convention.md +62 -0
- package/src/shared/references/output-contract-schema.md +35 -0
- package/src/shared/references/pipeline-contracts.md +102 -0
- package/src/shared/scripts/skf-atomic-write.py +370 -0
- package/src/shared/scripts/skf-manifest-ops.py +236 -0
- package/src/shared/scripts/skf-preflight.py +164 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +201 -0
- package/src/shared/scripts/skf-severity-classify.py +163 -0
- package/src/shared/scripts/skf-skill-inventory.py +210 -0
- package/src/shared/scripts/skf-structural-diff.py +257 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +318 -0
- package/src/shared/scripts/skf-validate-output.py +247 -0
- package/src/skf-analyze-source/SKILL.md +57 -0
- package/src/skf-analyze-source/assets/skill-brief-schema.md +125 -0
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +124 -0
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-01-init.md +6 -66
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-01b-continue.md +4 -59
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-02-scan-project.md +7 -68
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-03-identify-units.md +6 -66
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-04-map-and-detect.md +7 -68
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-05-recommend.md +6 -66
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-06-generate-briefs.md +14 -66
- package/src/skf-analyze-source/steps-c/step-07-health-check.md +22 -0
- package/src/skf-audit-skill/SKILL.md +59 -0
- package/src/{workflows/audit-skill/data → skf-audit-skill/assets}/drift-report-template.md +7 -7
- package/src/{workflows/audit-skill/data → skf-audit-skill/references}/severity-rules.md +11 -11
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-01-init.md +24 -68
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-02-re-index.md +36 -71
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-03-structural-diff.md +21 -63
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-04-semantic-diff.md +6 -65
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-05-severity-classify.md +5 -63
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-06-report.md +10 -59
- package/src/skf-audit-skill/steps-c/step-07-health-check.md +22 -0
- package/src/skf-brief-skill/SKILL.md +53 -0
- package/src/{workflows/brief-skill/data → skf-brief-skill/assets}/scope-templates.md +23 -0
- package/src/skf-brief-skill/assets/skill-brief-schema.md +206 -0
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-01-gather-intent.md +6 -67
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-02-analyze-target.md +6 -66
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-03-scope-definition.md +12 -68
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-04-confirm-brief.md +5 -63
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-05-write-brief.md +10 -69
- package/src/skf-brief-skill/steps-c/step-06-health-check.md +22 -0
- package/src/skf-create-skill/SKILL.md +63 -0
- package/src/{workflows/create-skill/data → skf-create-skill/assets}/compile-assembly-rules.md +60 -0
- package/src/{workflows/create-skill/data → skf-create-skill/assets}/skill-sections.md +92 -23
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +102 -0
- package/src/{workflows/create-skill/data → skf-create-skill/references}/extraction-patterns-tracing.md +13 -13
- package/src/{workflows/create-skill/data → skf-create-skill/references}/extraction-patterns.md +8 -3
- package/src/skf-create-skill/references/source-resolution-protocols.md +239 -0
- package/src/{workflows/create-skill/data → skf-create-skill/references}/tier-degradation-rules.md +8 -7
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-01-load-brief.md +25 -63
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-02-ecosystem-check.md +8 -64
- package/src/skf-create-skill/steps-c/step-03-extract.md +379 -0
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-03d-component-extraction.md +18 -35
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-04-enrich.md +12 -69
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-05-compile.md +71 -75
- package/src/skf-create-skill/steps-c/step-06-validate.md +267 -0
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-07-generate-artifacts.md +30 -74
- package/src/skf-create-skill/steps-c/step-08-report.md +145 -0
- package/src/skf-create-skill/steps-c/step-09-health-check.md +23 -0
- package/src/{workflows/create-skill/steps-c → skf-create-skill/steps-c/sub}/step-02b-ccc-discover.md +8 -66
- package/src/skf-create-skill/steps-c/sub/step-03b-fetch-temporal.md +229 -0
- package/src/{workflows/create-skill/steps-c → skf-create-skill/steps-c/sub}/step-03c-fetch-docs.md +18 -42
- package/src/skf-create-stack-skill/SKILL.md +64 -0
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/assets}/stack-skill-template.md +2 -1
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/compose-mode-rules.md +19 -5
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/integration-patterns.md +1 -1
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/manifest-patterns.md +10 -10
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-01-init.md +14 -55
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-02-detect-manifests.md +29 -63
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-03-rank-and-confirm.md +9 -59
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-04-parallel-extract.md +29 -64
- package/src/skf-create-stack-skill/steps-c/step-05-detect-integrations.md +164 -0
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-06-compile-stack.md +7 -60
- package/src/skf-create-stack-skill/steps-c/step-07-generate-output.md +328 -0
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-08-validate.md +17 -63
- package/src/skf-create-stack-skill/steps-c/step-09-report.md +128 -0
- package/src/skf-create-stack-skill/steps-c/step-10-health-check.md +22 -0
- package/src/skf-drop-skill/SKILL.md +57 -0
- package/src/{workflows/drop-skill → skf-drop-skill}/steps-c/step-01-select.md +9 -69
- package/src/{workflows/drop-skill → skf-drop-skill}/steps-c/step-02-execute.md +30 -106
- package/src/skf-drop-skill/steps-c/step-03-report.md +83 -0
- package/src/skf-drop-skill/steps-c/step-04-health-check.md +22 -0
- package/src/skf-export-skill/SKILL.md +57 -0
- package/src/skf-export-skill/assets/managed-section-format.md +138 -0
- package/src/{workflows/export-skill/data → skf-export-skill/assets}/snippet-format.md +8 -12
- package/src/skf-export-skill/steps-c/step-01-load-skill.md +247 -0
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-02-package.md +5 -58
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-03-generate-snippet.md +15 -77
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-04-update-context.md +59 -133
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-05-token-report.md +4 -58
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-06-summary.md +20 -64
- package/src/skf-export-skill/steps-c/step-07-health-check.md +22 -0
- package/src/skf-forger/SKILL.md +110 -0
- package/src/skf-forger/bmad-skill-manifest.yaml +11 -0
- package/src/skf-quick-skill/SKILL.md +56 -0
- package/src/{workflows/quick-skill/data → skf-quick-skill/references}/registry-resolution.md +6 -6
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-01-resolve-target.md +9 -62
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-02-ecosystem-check.md +5 -59
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-03-quick-extract.md +18 -61
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-04-compile.md +6 -60
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-05-validate.md +58 -78
- package/src/skf-quick-skill/steps-c/step-06-write.md +73 -0
- package/src/skf-quick-skill/steps-c/step-07-health-check.md +22 -0
- package/src/skf-refine-architecture/SKILL.md +57 -0
- package/src/{workflows/refine-architecture/data → skf-refine-architecture/references}/refinement-rules.md +16 -16
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-01-init.md +6 -63
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-02-gap-analysis.md +29 -71
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-03-issue-detection.md +5 -65
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-04-improvements.md +5 -64
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-05-compile.md +5 -56
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-06-report.md +14 -64
- package/src/skf-refine-architecture/steps-c/step-07-health-check.md +22 -0
- package/src/skf-rename-skill/SKILL.md +58 -0
- package/src/{workflows/rename-skill → skf-rename-skill}/steps-c/step-01-select.md +12 -73
- package/src/{workflows/rename-skill → skf-rename-skill}/steps-c/step-02-execute.md +32 -113
- package/src/skf-rename-skill/steps-c/step-03-report.md +83 -0
- package/src/skf-rename-skill/steps-c/step-04-health-check.md +22 -0
- package/src/skf-setup/SKILL.md +55 -0
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-01-detect-and-tier.md +6 -62
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-01b-ccc-index.md +9 -65
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-02-write-config.md +15 -60
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-03-auto-index.md +7 -66
- package/src/skf-setup/steps-c/step-04-report.md +92 -0
- package/src/skf-setup/steps-c/step-05-health-check.md +22 -0
- package/src/skf-test-skill/SKILL.md +59 -0
- package/src/skf-test-skill/references/migration-section-rules.md +114 -0
- package/src/skf-test-skill/references/scoring-rules.md +207 -0
- package/src/skf-test-skill/references/source-access-protocol.md +109 -0
- package/src/skf-test-skill/scripts/compute-score.py +310 -0
- package/src/skf-test-skill/steps-c/step-01-init.md +238 -0
- package/src/{workflows/test-skill → skf-test-skill}/steps-c/step-02-detect-mode.md +4 -62
- package/src/skf-test-skill/steps-c/step-03-coverage-check.md +347 -0
- package/src/skf-test-skill/steps-c/step-04-coherence-check.md +239 -0
- package/src/{workflows/test-skill → skf-test-skill}/steps-c/step-04b-external-validators.md +42 -77
- package/src/skf-test-skill/steps-c/step-05-score.md +266 -0
- package/src/skf-test-skill/steps-c/step-06-report.md +290 -0
- package/src/skf-test-skill/steps-c/step-07-health-check.md +25 -0
- package/src/skf-test-skill/templates/test-report-template.md +58 -0
- package/src/skf-update-skill/SKILL.md +58 -0
- package/src/{workflows/update-skill/data → skf-update-skill/references}/manual-section-rules.md +6 -6
- package/src/{workflows/update-skill/data → skf-update-skill/references}/merge-conflict-rules.md +5 -5
- package/src/skf-update-skill/references/remote-source-resolution.md +94 -0
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-01-init.md +22 -69
- package/src/skf-update-skill/steps-c/step-02-detect-changes.md +255 -0
- package/src/skf-update-skill/steps-c/step-03-re-extract.md +336 -0
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-04-merge.md +60 -72
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-05-validate.md +5 -62
- package/src/skf-update-skill/steps-c/step-06-write.md +278 -0
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-07-report.md +11 -66
- package/src/skf-update-skill/steps-c/step-08-health-check.md +22 -0
- package/src/skf-verify-stack/SKILL.md +60 -0
- package/src/skf-verify-stack/assets/feasibility-report-template.md +76 -0
- package/src/{workflows/verify-stack/data → skf-verify-stack/references}/coverage-patterns.md +12 -4
- package/src/skf-verify-stack/references/integration-verification-rules.md +78 -0
- package/src/skf-verify-stack/steps-c/step-01-init.md +178 -0
- package/src/{workflows/verify-stack → skf-verify-stack}/steps-c/step-02-coverage.md +27 -72
- package/src/skf-verify-stack/steps-c/step-03-integrations.md +179 -0
- package/src/{workflows/verify-stack → skf-verify-stack}/steps-c/step-04-requirements.md +14 -67
- package/src/skf-verify-stack/steps-c/step-05-synthesize.md +147 -0
- package/src/skf-verify-stack/steps-c/step-06-report.md +156 -0
- package/src/skf-verify-stack/steps-c/step-07-health-check.md +25 -0
- package/tools/cli/commands/install.js +1 -1
- package/tools/cli/commands/status.js +21 -23
- package/tools/cli/commands/uninstall.js +11 -19
- package/tools/cli/commands/update.js +2 -2
- package/tools/cli/lib/ide-skills.js +216 -0
- package/tools/cli/lib/installer.js +29 -52
- package/tools/cli/lib/manifest.js +11 -5
- package/tools/cli/lib/platform-codes.yaml +223 -0
- package/tools/cli/lib/ui.js +76 -47
- package/tools/validate-docs-drift.js +193 -0
- package/src/agents/forger.agent.yaml +0 -114
- package/src/workflows/README.md +0 -174
- package/src/workflows/analyze-source/data/skill-brief-schema.md +0 -125
- package/src/workflows/analyze-source/data/unit-detection-heuristics.md +0 -124
- package/src/workflows/analyze-source/workflow.md +0 -61
- package/src/workflows/audit-skill/workflow.md +0 -64
- package/src/workflows/brief-skill/data/skill-brief-schema.md +0 -159
- package/src/workflows/brief-skill/workflow.md +0 -58
- package/src/workflows/create-skill/data/source-resolution-protocols.md +0 -200
- package/src/workflows/create-skill/steps-c/step-03-extract.md +0 -301
- package/src/workflows/create-skill/steps-c/step-03b-fetch-temporal.md +0 -253
- package/src/workflows/create-skill/steps-c/step-06-validate.md +0 -254
- package/src/workflows/create-skill/steps-c/step-08-report.md +0 -176
- package/src/workflows/create-skill/workflow.md +0 -53
- package/src/workflows/create-stack-skill/steps-c/step-05-detect-integrations.md +0 -196
- package/src/workflows/create-stack-skill/steps-c/step-07-generate-output.md +0 -270
- package/src/workflows/create-stack-skill/steps-c/step-09-report.md +0 -145
- package/src/workflows/create-stack-skill/workflow.md +0 -59
- package/src/workflows/drop-skill/steps-c/step-03-report.md +0 -135
- package/src/workflows/drop-skill/workflow.md +0 -63
- package/src/workflows/export-skill/data/managed-section-format.md +0 -103
- package/src/workflows/export-skill/steps-c/step-01-load-skill.md +0 -237
- package/src/workflows/export-skill/workflow.md +0 -58
- package/src/workflows/quick-skill/steps-c/step-06-write.md +0 -179
- package/src/workflows/quick-skill/workflow.md +0 -58
- package/src/workflows/refine-architecture/workflow.md +0 -61
- package/src/workflows/rename-skill/steps-c/step-03-report.md +0 -132
- package/src/workflows/rename-skill/workflow.md +0 -64
- package/src/workflows/setup-forge/steps-c/step-04-report.md +0 -156
- package/src/workflows/setup-forge/workflow.md +0 -51
- package/src/workflows/test-skill/data/scoring-rules.md +0 -123
- package/src/workflows/test-skill/data/source-access-protocol.md +0 -51
- package/src/workflows/test-skill/steps-c/step-01-init.md +0 -217
- package/src/workflows/test-skill/steps-c/step-03-coverage-check.md +0 -280
- package/src/workflows/test-skill/steps-c/step-04-coherence-check.md +0 -271
- package/src/workflows/test-skill/steps-c/step-05-score.md +0 -222
- package/src/workflows/test-skill/steps-c/step-06-report.md +0 -207
- package/src/workflows/test-skill/templates/test-report-template.md +0 -28
- package/src/workflows/test-skill/workflow.md +0 -57
- package/src/workflows/update-skill/data/remote-source-resolution.md +0 -57
- package/src/workflows/update-skill/steps-c/step-02-detect-changes.md +0 -221
- package/src/workflows/update-skill/steps-c/step-03-re-extract.md +0 -256
- package/src/workflows/update-skill/steps-c/step-06-write.md +0 -265
- package/src/workflows/update-skill/workflow.md +0 -56
- package/src/workflows/verify-stack/data/feasibility-report-template.md +0 -63
- package/src/workflows/verify-stack/data/integration-verification-rules.md +0 -73
- package/src/workflows/verify-stack/steps-c/step-01-init.md +0 -179
- package/src/workflows/verify-stack/steps-c/step-03-integrations.md +0 -195
- package/src/workflows/verify-stack/steps-c/step-05-synthesize.md +0 -191
- package/src/workflows/verify-stack/steps-c/step-06-report.md +0 -197
- package/src/workflows/verify-stack/workflow.md +0 -61
- package/tools/cli/lib/compiler.js +0 -306
- package/tools/cli/lib/ide-commands.js +0 -244
- /package/src/{workflows/analyze-source → skf-analyze-source}/templates/analysis-report-template.md +0 -0
- /package/src/{workflows/quick-skill/data → skf-quick-skill/assets}/skill-template.md +0 -0
- /package/src/{workflows/setup-forge/data → skf-setup/references}/tier-rules.md +0 -0
- /package/src/{workflows/test-skill/data → skf-test-skill/assets}/output-section-formats.md +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Skill Inventory — Scan skills directory and produce structured inventory.
|
|
6
|
+
|
|
7
|
+
Scans the skills output folder, reads manifests and metadata, resolves active
|
|
8
|
+
versions via symlinks, and outputs a JSON inventory. Reused by 9+ skills.
|
|
9
|
+
|
|
10
|
+
CLI: python3 skf-skill-inventory.py <skills-output-folder>
|
|
11
|
+
python3 skf-skill-inventory.py <skills-output-folder> --skill <name>
|
|
12
|
+
python3 skf-skill-inventory.py <skills-output-folder> --manifest-only
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def read_json_file(path):
|
|
24
|
+
"""Read a JSON file, returning (data, None) or (None, error)."""
|
|
25
|
+
try:
|
|
26
|
+
with open(path, encoding="utf-8") as f:
|
|
27
|
+
return json.load(f), None
|
|
28
|
+
except FileNotFoundError:
|
|
29
|
+
return None, f"Not found: {path}"
|
|
30
|
+
except json.JSONDecodeError as e:
|
|
31
|
+
return None, f"JSON parse error in {path}: {e}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_active_version(skill_group_dir):
|
|
35
|
+
"""Resolve the active version for a skill group directory.
|
|
36
|
+
|
|
37
|
+
Returns (version_string, resolved_path) or (None, None).
|
|
38
|
+
"""
|
|
39
|
+
active_link = skill_group_dir / "active"
|
|
40
|
+
if active_link.is_symlink() or active_link.is_dir():
|
|
41
|
+
target = active_link.resolve()
|
|
42
|
+
if target.is_dir():
|
|
43
|
+
return target.name, target
|
|
44
|
+
return None, None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def scan_skill_group(skill_group_dir, skill_name):
|
|
48
|
+
"""Scan a single skill group directory and return its inventory entry."""
|
|
49
|
+
entry = {
|
|
50
|
+
"name": skill_name,
|
|
51
|
+
"path": str(skill_group_dir),
|
|
52
|
+
"versions": [],
|
|
53
|
+
"active_version": None,
|
|
54
|
+
"active_path": None,
|
|
55
|
+
"metadata": None,
|
|
56
|
+
"has_skill_md": False,
|
|
57
|
+
"has_provenance_map": False,
|
|
58
|
+
"has_context_snippet": False,
|
|
59
|
+
"errors": [],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Check for version directories (contain a skill-name subdirectory)
|
|
63
|
+
for child in sorted(skill_group_dir.iterdir()):
|
|
64
|
+
if child.is_dir() and child.name != "active" and not child.name.startswith("."):
|
|
65
|
+
# Check if this is a version dir (contains skill-name subdir or SKILL.md)
|
|
66
|
+
skill_subdir = child / skill_name
|
|
67
|
+
if skill_subdir.is_dir():
|
|
68
|
+
entry["versions"].append(child.name)
|
|
69
|
+
elif (child / "SKILL.md").exists():
|
|
70
|
+
# Flat version dir without skill-name nesting
|
|
71
|
+
entry["versions"].append(child.name)
|
|
72
|
+
|
|
73
|
+
# Resolve active version
|
|
74
|
+
active_ver, active_path = resolve_active_version(skill_group_dir)
|
|
75
|
+
if active_ver:
|
|
76
|
+
entry["active_version"] = active_ver
|
|
77
|
+
# The active path points to the version dir; skill files are in version/skill-name/
|
|
78
|
+
skill_pkg = active_path / skill_name
|
|
79
|
+
if skill_pkg.is_dir():
|
|
80
|
+
entry["active_path"] = str(skill_pkg)
|
|
81
|
+
elif (active_path / "SKILL.md").exists():
|
|
82
|
+
entry["active_path"] = str(active_path)
|
|
83
|
+
else:
|
|
84
|
+
entry["active_path"] = str(active_path)
|
|
85
|
+
|
|
86
|
+
# If no versions found, check for flat layout (SKILL.md at group root)
|
|
87
|
+
if not entry["versions"]:
|
|
88
|
+
if (skill_group_dir / "SKILL.md").exists():
|
|
89
|
+
entry["versions"].append("flat")
|
|
90
|
+
entry["active_version"] = "flat"
|
|
91
|
+
entry["active_path"] = str(skill_group_dir)
|
|
92
|
+
|
|
93
|
+
# Load metadata from active path
|
|
94
|
+
active_dir = Path(entry["active_path"]) if entry["active_path"] else None
|
|
95
|
+
if active_dir and active_dir.is_dir():
|
|
96
|
+
entry["has_skill_md"] = (active_dir / "SKILL.md").exists()
|
|
97
|
+
entry["has_provenance_map"] = (active_dir / "provenance-map.json").exists()
|
|
98
|
+
entry["has_context_snippet"] = (active_dir / "context-snippet.md").exists()
|
|
99
|
+
|
|
100
|
+
metadata_path = active_dir / "metadata.json"
|
|
101
|
+
meta, meta_err = read_json_file(metadata_path)
|
|
102
|
+
if meta:
|
|
103
|
+
entry["metadata"] = {
|
|
104
|
+
"version": meta.get("version"),
|
|
105
|
+
"language": meta.get("language"),
|
|
106
|
+
"source_authority": meta.get("source_authority"),
|
|
107
|
+
"source_repo": meta.get("source_repo"),
|
|
108
|
+
"generated_by": meta.get("generated_by"),
|
|
109
|
+
"confidence_tier": meta.get("confidence_tier"),
|
|
110
|
+
"exports_total": meta.get("stats", {}).get("exports_total"),
|
|
111
|
+
}
|
|
112
|
+
elif meta_err and "Not found" not in meta_err:
|
|
113
|
+
entry["errors"].append(meta_err)
|
|
114
|
+
|
|
115
|
+
return entry
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def scan_inventory(skills_folder, skill_filter=None, manifest_only=False):
|
|
119
|
+
"""Scan the skills output folder and produce an inventory."""
|
|
120
|
+
skills_dir = Path(skills_folder)
|
|
121
|
+
|
|
122
|
+
if not skills_dir.is_dir():
|
|
123
|
+
return {
|
|
124
|
+
"status": "error",
|
|
125
|
+
"error": f"Skills directory not found: {skills_dir}",
|
|
126
|
+
"code": "DIR_NOT_FOUND",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
result = {
|
|
130
|
+
"status": "ok",
|
|
131
|
+
"skills_folder": str(skills_dir),
|
|
132
|
+
"manifest": None,
|
|
133
|
+
"manifest_error": None,
|
|
134
|
+
"skills": [],
|
|
135
|
+
"summary": {
|
|
136
|
+
"total_skills": 0,
|
|
137
|
+
"total_versions": 0,
|
|
138
|
+
"with_metadata": 0,
|
|
139
|
+
"with_provenance": 0,
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Load export manifest
|
|
144
|
+
manifest_path = skills_dir / ".export-manifest.json"
|
|
145
|
+
manifest, manifest_err = read_json_file(manifest_path)
|
|
146
|
+
if manifest:
|
|
147
|
+
result["manifest"] = manifest
|
|
148
|
+
else:
|
|
149
|
+
result["manifest_error"] = manifest_err
|
|
150
|
+
|
|
151
|
+
if manifest_only:
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
# Determine which skills to scan
|
|
155
|
+
skill_names = set()
|
|
156
|
+
|
|
157
|
+
# From manifest exports
|
|
158
|
+
if manifest and "exports" in manifest:
|
|
159
|
+
skill_names.update(manifest["exports"].keys())
|
|
160
|
+
|
|
161
|
+
# From directory listing
|
|
162
|
+
for child in skills_dir.iterdir():
|
|
163
|
+
if child.is_dir() and not child.name.startswith("."):
|
|
164
|
+
skill_names.add(child.name)
|
|
165
|
+
|
|
166
|
+
# Apply filter
|
|
167
|
+
if skill_filter:
|
|
168
|
+
if skill_filter in skill_names:
|
|
169
|
+
skill_names = {skill_filter}
|
|
170
|
+
else:
|
|
171
|
+
return {
|
|
172
|
+
"status": "error",
|
|
173
|
+
"error": f"Skill '{skill_filter}' not found in {skills_dir}",
|
|
174
|
+
"code": "SKILL_NOT_FOUND",
|
|
175
|
+
"available": sorted(skill_names),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Scan each skill group
|
|
179
|
+
for name in sorted(skill_names):
|
|
180
|
+
skill_group_dir = skills_dir / name
|
|
181
|
+
if skill_group_dir.is_dir():
|
|
182
|
+
entry = scan_skill_group(skill_group_dir, name)
|
|
183
|
+
result["skills"].append(entry)
|
|
184
|
+
|
|
185
|
+
# Compute summary
|
|
186
|
+
result["summary"]["total_skills"] = len(result["skills"])
|
|
187
|
+
result["summary"]["total_versions"] = sum(len(s["versions"]) for s in result["skills"])
|
|
188
|
+
result["summary"]["with_metadata"] = sum(1 for s in result["skills"] if s["metadata"])
|
|
189
|
+
result["summary"]["with_provenance"] = sum(1 for s in result["skills"] if s["has_provenance_map"])
|
|
190
|
+
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
if len(sys.argv) < 2:
|
|
196
|
+
print("Usage: python3 skf-skill-inventory.py <skills-output-folder> [--skill <name>] [--manifest-only]", file=sys.stderr)
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
|
|
199
|
+
folder = sys.argv[1]
|
|
200
|
+
skill = None
|
|
201
|
+
manifest_only = "--manifest-only" in sys.argv
|
|
202
|
+
|
|
203
|
+
if "--skill" in sys.argv:
|
|
204
|
+
idx = sys.argv.index("--skill")
|
|
205
|
+
if idx + 1 < len(sys.argv):
|
|
206
|
+
skill = sys.argv[idx + 1]
|
|
207
|
+
|
|
208
|
+
result = scan_inventory(folder, skill_filter=skill, manifest_only=manifest_only)
|
|
209
|
+
print(json.dumps(result, indent=2))
|
|
210
|
+
sys.exit(0 if result["status"] == "ok" else 1)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.9"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""Structural Diff — Deterministic comparison of skill export inventories.
|
|
7
|
+
|
|
8
|
+
Compares a baseline export inventory against a current export inventory and
|
|
9
|
+
produces a structured JSON diff showing added, removed, and changed entries.
|
|
10
|
+
Used by audit-skill and update-skill to replace LLM-based inventory comparison.
|
|
11
|
+
|
|
12
|
+
CLI:
|
|
13
|
+
python3 skf-structural-diff.py baseline.json current.json
|
|
14
|
+
python3 skf-structural-diff.py baseline.json current.json -o diff-result.json
|
|
15
|
+
|
|
16
|
+
Input:
|
|
17
|
+
Two JSON files. Each file must be either:
|
|
18
|
+
- An object with an "exports" array (provenance-map / extraction-snapshot format)
|
|
19
|
+
- A plain array of export entries
|
|
20
|
+
|
|
21
|
+
Each export entry must have at minimum:
|
|
22
|
+
- name: string (primary key for matching)
|
|
23
|
+
- type: string (function/class/type/const/interface/etc.)
|
|
24
|
+
|
|
25
|
+
Optional fields used for change detection:
|
|
26
|
+
- signature: string
|
|
27
|
+
- file: string
|
|
28
|
+
- line: number
|
|
29
|
+
- confidence: string
|
|
30
|
+
|
|
31
|
+
Output:
|
|
32
|
+
JSON object:
|
|
33
|
+
summary: { added, removed, changed, moved, unchanged }
|
|
34
|
+
added: list of entries present in current but not baseline
|
|
35
|
+
removed: list of entries present in baseline but not current
|
|
36
|
+
changed: list of { name, field, baseline_value, current_value }
|
|
37
|
+
moved: list of { name, previous_file, current_file }
|
|
38
|
+
unchanged_count: number of entries that matched exactly
|
|
39
|
+
|
|
40
|
+
Exit codes:
|
|
41
|
+
0 — inventories are identical (no diff)
|
|
42
|
+
1 — differences found (or error)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import argparse
|
|
48
|
+
import json
|
|
49
|
+
import sys
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Fields compared for change detection (in order).
|
|
54
|
+
# "name" is the primary key and is not diffed as a field.
|
|
55
|
+
# "file" is excluded — file moves are tracked separately in the "moved" list.
|
|
56
|
+
DIFF_FIELDS = ["type", "signature", "line", "confidence"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_inventory(path: Path) -> tuple[list[dict], str | None]:
|
|
60
|
+
"""Load an export inventory from a JSON file.
|
|
61
|
+
|
|
62
|
+
Accepts either:
|
|
63
|
+
- {"exports": [...]} (provenance-map / extraction-snapshot)
|
|
64
|
+
- [...] (plain array)
|
|
65
|
+
|
|
66
|
+
Returns (exports, error_message).
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
text = path.read_text(encoding="utf-8")
|
|
70
|
+
except OSError as exc:
|
|
71
|
+
return [], f"Cannot read file '{path}': {exc}"
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
data = json.loads(text)
|
|
75
|
+
except json.JSONDecodeError as exc:
|
|
76
|
+
return [], f"Invalid JSON in '{path}': {exc}"
|
|
77
|
+
|
|
78
|
+
if isinstance(data, list):
|
|
79
|
+
return data, None
|
|
80
|
+
if isinstance(data, dict):
|
|
81
|
+
if "exports" in data and isinstance(data["exports"], list):
|
|
82
|
+
return data["exports"], None
|
|
83
|
+
# If it's a plain object without "exports", treat values as entries
|
|
84
|
+
# (some inventory formats are name-keyed objects)
|
|
85
|
+
if all(isinstance(v, dict) for v in data.values()):
|
|
86
|
+
return list(data.values()), None
|
|
87
|
+
return [], f"Unrecognised inventory format in '{path}': expected array or object with 'exports' key"
|
|
88
|
+
|
|
89
|
+
return [], f"Unrecognised inventory format in '{path}': top-level value must be array or object"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def index_by_name(entries: list[dict]) -> dict[str, dict]:
|
|
93
|
+
"""Build a name-keyed dict from an export list, skipping nameless entries."""
|
|
94
|
+
result: dict[str, dict] = {}
|
|
95
|
+
for entry in entries:
|
|
96
|
+
name = entry.get("name", "").strip()
|
|
97
|
+
if name:
|
|
98
|
+
result[name] = entry
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def entry_matches(base: dict, curr: dict) -> bool:
|
|
103
|
+
"""Return True if two entries are identical across all DIFF_FIELDS."""
|
|
104
|
+
for field in DIFF_FIELDS:
|
|
105
|
+
if base.get(field) != curr.get(field):
|
|
106
|
+
return False
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def diff_inventories(
|
|
111
|
+
baseline_entries: list[dict],
|
|
112
|
+
current_entries: list[dict],
|
|
113
|
+
) -> dict:
|
|
114
|
+
"""Compute the structural diff between two export inventories.
|
|
115
|
+
|
|
116
|
+
Returns a dict with keys: summary, added, removed, changed, unchanged_count.
|
|
117
|
+
"""
|
|
118
|
+
baseline = index_by_name(baseline_entries)
|
|
119
|
+
current = index_by_name(current_entries)
|
|
120
|
+
|
|
121
|
+
baseline_names = set(baseline.keys())
|
|
122
|
+
current_names = set(current.keys())
|
|
123
|
+
|
|
124
|
+
added_names = current_names - baseline_names
|
|
125
|
+
removed_names = baseline_names - current_names
|
|
126
|
+
common_names = baseline_names & current_names
|
|
127
|
+
|
|
128
|
+
added = [current[n] for n in sorted(added_names)]
|
|
129
|
+
removed = [baseline[n] for n in sorted(removed_names)]
|
|
130
|
+
|
|
131
|
+
changed: list[dict] = []
|
|
132
|
+
moved: list[dict] = []
|
|
133
|
+
unchanged_count = 0
|
|
134
|
+
|
|
135
|
+
for name in sorted(common_names):
|
|
136
|
+
base_entry = baseline[name]
|
|
137
|
+
curr_entry = current[name]
|
|
138
|
+
|
|
139
|
+
# Detect file moves separately
|
|
140
|
+
base_file = base_entry.get("file")
|
|
141
|
+
curr_file = curr_entry.get("file")
|
|
142
|
+
if base_file and curr_file and base_file != curr_file:
|
|
143
|
+
moved.append({
|
|
144
|
+
"name": name,
|
|
145
|
+
"previous_file": base_file,
|
|
146
|
+
"current_file": curr_file,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
entry_changed = False
|
|
150
|
+
for field in DIFF_FIELDS:
|
|
151
|
+
base_val = base_entry.get(field)
|
|
152
|
+
curr_val = curr_entry.get(field)
|
|
153
|
+
if base_val != curr_val:
|
|
154
|
+
changed.append({
|
|
155
|
+
"name": name,
|
|
156
|
+
"field": field,
|
|
157
|
+
"baseline_value": base_val,
|
|
158
|
+
"current_value": curr_val,
|
|
159
|
+
})
|
|
160
|
+
entry_changed = True
|
|
161
|
+
|
|
162
|
+
if not entry_changed:
|
|
163
|
+
unchanged_count += 1
|
|
164
|
+
|
|
165
|
+
# Count distinct names that have at least one field change
|
|
166
|
+
changed_names = len({c["name"] for c in changed})
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"summary": {
|
|
170
|
+
"added": len(added),
|
|
171
|
+
"removed": len(removed),
|
|
172
|
+
"changed": changed_names,
|
|
173
|
+
"moved": len(moved),
|
|
174
|
+
"unchanged": unchanged_count,
|
|
175
|
+
},
|
|
176
|
+
"added": added,
|
|
177
|
+
"removed": removed,
|
|
178
|
+
"changed": changed,
|
|
179
|
+
"moved": moved,
|
|
180
|
+
"unchanged_count": unchanged_count,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def main() -> int:
|
|
185
|
+
parser = argparse.ArgumentParser(
|
|
186
|
+
prog="skf-structural-diff.py",
|
|
187
|
+
description=(
|
|
188
|
+
"Compare two JSON export inventories and produce a structured diff.\n"
|
|
189
|
+
"\n"
|
|
190
|
+
"Each inventory file must be either a JSON array of export entries\n"
|
|
191
|
+
"or a JSON object with an 'exports' key containing such an array.\n"
|
|
192
|
+
"Every entry must have at minimum 'name' and 'type' fields.\n"
|
|
193
|
+
"\n"
|
|
194
|
+
"Exit code 0 means no differences were found.\n"
|
|
195
|
+
"Exit code 1 means differences were found (or an error occurred)."
|
|
196
|
+
),
|
|
197
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
198
|
+
epilog=(
|
|
199
|
+
"examples:\n"
|
|
200
|
+
" python3 skf-structural-diff.py provenance-map.json current-snapshot.json\n"
|
|
201
|
+
" python3 skf-structural-diff.py baseline.json current.json -o diff.json\n"
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
"baseline",
|
|
206
|
+
metavar="baseline.json",
|
|
207
|
+
help="path to the baseline export inventory (e.g. provenance-map.json)",
|
|
208
|
+
)
|
|
209
|
+
parser.add_argument(
|
|
210
|
+
"current",
|
|
211
|
+
metavar="current.json",
|
|
212
|
+
help="path to the current export inventory (e.g. extraction-snapshot.json)",
|
|
213
|
+
)
|
|
214
|
+
parser.add_argument(
|
|
215
|
+
"-o",
|
|
216
|
+
"--output",
|
|
217
|
+
metavar="FILE",
|
|
218
|
+
help="write JSON output to FILE instead of stdout",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
args = parser.parse_args()
|
|
222
|
+
|
|
223
|
+
baseline_path = Path(args.baseline)
|
|
224
|
+
current_path = Path(args.current)
|
|
225
|
+
|
|
226
|
+
baseline_entries, err = load_inventory(baseline_path)
|
|
227
|
+
if err:
|
|
228
|
+
print(json.dumps({"status": "error", "error": err}, indent=2))
|
|
229
|
+
return 1
|
|
230
|
+
|
|
231
|
+
current_entries, err = load_inventory(current_path)
|
|
232
|
+
if err:
|
|
233
|
+
print(json.dumps({"status": "error", "error": err}, indent=2))
|
|
234
|
+
return 1
|
|
235
|
+
|
|
236
|
+
result = diff_inventories(baseline_entries, current_entries)
|
|
237
|
+
output_text = json.dumps(result, indent=2)
|
|
238
|
+
|
|
239
|
+
if args.output:
|
|
240
|
+
out_path = Path(args.output)
|
|
241
|
+
try:
|
|
242
|
+
out_path.write_text(output_text + "\n", encoding="utf-8")
|
|
243
|
+
except OSError as exc:
|
|
244
|
+
print(
|
|
245
|
+
json.dumps({"status": "error", "error": f"Cannot write output: {exc}"}, indent=2)
|
|
246
|
+
)
|
|
247
|
+
return 1
|
|
248
|
+
else:
|
|
249
|
+
print(output_text)
|
|
250
|
+
|
|
251
|
+
summary = result["summary"]
|
|
252
|
+
has_diff = summary["added"] or summary["removed"] or summary["changed"] or summary["moved"]
|
|
253
|
+
return 1 if has_diff else 0
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
if __name__ == "__main__":
|
|
257
|
+
sys.exit(main())
|