bmad-module-skill-forge 1.9.0 → 2.0.1
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/.nvmrc +1 -1
- package/README.md +10 -5
- package/docs/_data/pinned.yaml +2 -2
- package/docs/agents.md +11 -2
- package/docs/architecture.md +5 -4
- package/docs/bmad-synergy.md +30 -7
- package/docs/campaign.md +172 -0
- package/docs/examples.md +7 -3
- package/docs/forge-auto.md +90 -0
- package/docs/getting-started.md +9 -0
- package/docs/how-it-works.md +6 -4
- package/docs/index.md +4 -3
- package/docs/skill-model.md +1 -1
- package/docs/troubleshooting.md +17 -1
- package/docs/verifying-a-skill.md +2 -2
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +64 -24
- package/package.json +2 -2
- package/src/knowledge/skill-lifecycle.md +1 -1
- package/src/module-help.csv +4 -3
- package/src/shared/data/language-corpora.json +32 -0
- package/src/shared/references/pipeline-contracts.md +6 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
- package/src/shared/scripts/skf-comention-pairs.py +350 -0
- package/src/shared/scripts/skf-count-tokens.py +222 -0
- package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
- package/src/shared/scripts/skf-detect-docs.py +240 -4
- package/src/shared/scripts/skf-detect-language.py +82 -3
- package/src/shared/scripts/skf-detect-tools.py +105 -2
- package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
- package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
- package/src/shared/scripts/skf-extract-public-api.py +53 -0
- package/src/shared/scripts/skf-find-cycles.py +246 -0
- package/src/shared/scripts/skf-hash-content.py +487 -2
- package/src/shared/scripts/skf-language-corpora.py +100 -0
- package/src/shared/scripts/skf-manifest-ops.py +67 -1
- package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
- package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
- package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
- package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
- package/src/shared/scripts/skf-shape-detect.py +576 -25
- package/src/shared/scripts/skf-shard-body.py +461 -0
- package/src/shared/scripts/skf-skill-inventory.py +139 -3
- package/src/shared/scripts/skf-structural-diff.py +307 -67
- package/src/shared/scripts/skf-validate-brief-schema.py +41 -6
- package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
- package/src/shared/scripts/skf-validate-output.py +443 -10
- package/src/shared/scripts/skf-validate-pins.py +3 -3
- package/src/shared/scripts/skf-verify-no-trace.py +214 -0
- package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
- package/src/shared/scripts/skf-write-skill-brief.py +21 -5
- package/src/skf-analyze-source/SKILL.md +17 -32
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/customize.toml +2 -0
- package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
- package/src/skf-analyze-source/references/continue.md +8 -13
- package/src/skf-analyze-source/references/generate-briefs.md +28 -12
- package/src/skf-analyze-source/references/headless-contract.md +33 -0
- package/src/skf-analyze-source/references/health-check.md +1 -2
- package/src/skf-analyze-source/references/identify-units.md +25 -36
- package/src/skf-analyze-source/references/init.md +12 -25
- package/src/skf-analyze-source/references/map-and-detect.md +8 -20
- package/src/skf-analyze-source/references/recommend.md +4 -14
- package/src/skf-analyze-source/references/scan-project.md +3 -11
- package/src/skf-analyze-source/references/step-auto-scope.md +211 -197
- package/src/skf-analyze-source/references/step-shape-detect.md +20 -13
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
- package/src/skf-audit-skill/SKILL.md +9 -6
- package/src/skf-audit-skill/customize.toml +2 -0
- package/src/skf-audit-skill/references/init.md +58 -32
- package/src/skf-audit-skill/references/re-index.md +15 -34
- package/src/skf-audit-skill/references/report.md +11 -58
- package/src/skf-audit-skill/references/semantic-diff.md +4 -22
- package/src/skf-audit-skill/references/severity-classify.md +53 -87
- package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
- package/src/skf-audit-skill/references/structural-diff.md +43 -68
- package/src/skf-brief-skill/SKILL.md +7 -36
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
- package/src/skf-brief-skill/customize.toml +14 -0
- package/src/skf-brief-skill/references/analyze-target.md +13 -18
- package/src/skf-brief-skill/references/confirm-brief.md +2 -15
- package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
- package/src/skf-brief-skill/references/gather-intent.md +27 -24
- package/src/skf-brief-skill/references/headless-args.md +1 -1
- package/src/skf-brief-skill/references/health-check.md +1 -22
- package/src/skf-brief-skill/references/invocation-contract.md +37 -0
- package/src/skf-brief-skill/references/scope-definition.md +6 -15
- package/src/skf-brief-skill/references/step-auto-brief.md +22 -9
- package/src/skf-brief-skill/references/step-auto-validate.md +3 -7
- package/src/skf-brief-skill/references/version-resolution.md +1 -1
- package/src/skf-brief-skill/references/write-brief.md +19 -13
- package/src/skf-campaign/SKILL.md +121 -0
- package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
- package/src/skf-campaign/customize.toml +75 -0
- package/src/skf-campaign/manifest.yaml +15 -0
- package/src/skf-campaign/references/campaign-contracts.md +67 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +32 -0
- package/src/skf-campaign/references/health-check.md +35 -0
- package/src/skf-campaign/references/step-01-setup.md +121 -0
- package/src/skf-campaign/references/step-02-strategy.md +102 -0
- package/src/skf-campaign/references/step-03-pins.md +56 -0
- package/src/skf-campaign/references/step-04-provenance.md +63 -0
- package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
- package/src/skf-campaign/references/step-06-batch.md +88 -0
- package/src/skf-campaign/references/step-07-capstone.md +63 -0
- package/src/skf-campaign/references/step-08-verify.md +75 -0
- package/src/skf-campaign/references/step-09-refine.md +83 -0
- package/src/skf-campaign/references/step-10-export.md +106 -0
- package/src/skf-campaign/references/step-11-maintenance.md +84 -0
- package/src/skf-campaign/references/step-resume.md +115 -0
- package/src/skf-campaign/scripts/.gitkeep +0 -0
- package/src/skf-campaign/scripts/campaign-deps.py +244 -0
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
- package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
- package/src/skf-campaign/scripts/campaign-report.py +249 -0
- package/src/skf-campaign/scripts/campaign-status.py +162 -0
- package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
- package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
- package/src/skf-campaign/templates/campaign-report-template.md +54 -0
- package/src/skf-campaign/templates/kickoff-template.md +48 -0
- package/src/skf-create-skill/SKILL.md +15 -4
- package/src/skf-create-skill/assets/compile-assembly-rules.md +35 -34
- package/src/skf-create-skill/assets/skill-sections.md +9 -100
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
- package/src/skf-create-skill/customize.toml +13 -0
- package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
- package/src/skf-create-skill/references/compile.md +37 -47
- package/src/skf-create-skill/references/component-extraction.md +8 -35
- package/src/skf-create-skill/references/ecosystem-check.md +9 -25
- package/src/skf-create-skill/references/enrich.md +4 -17
- package/src/skf-create-skill/references/extract.md +20 -27
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
- package/src/skf-create-skill/references/extraction-patterns.md +10 -8
- package/src/skf-create-skill/references/generate-artifacts.md +20 -25
- package/src/skf-create-skill/references/health-check.md +2 -2
- package/src/skf-create-skill/references/load-brief.md +16 -18
- package/src/skf-create-skill/references/report.md +27 -14
- package/src/skf-create-skill/references/source-resolution-protocols.md +4 -4
- package/src/skf-create-skill/references/step-auto-shard.md +52 -25
- package/src/skf-create-skill/references/step-doc-rot.md +33 -15
- package/src/skf-create-skill/references/step-doc-sources.md +13 -4
- package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
- package/src/skf-create-skill/references/sub/fetch-docs.md +33 -46
- package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
- package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
- package/src/skf-create-skill/references/validate.md +69 -29
- package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
- package/src/skf-create-stack-skill/SKILL.md +15 -9
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
- package/src/skf-create-stack-skill/customize.toml +10 -0
- package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
- package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
- package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
- package/src/skf-create-stack-skill/references/generate-output.md +24 -75
- package/src/skf-create-stack-skill/references/init.md +9 -81
- package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
- package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
- package/src/skf-create-stack-skill/references/report.md +27 -62
- package/src/skf-create-stack-skill/references/validate.md +35 -31
- package/src/skf-drop-skill/SKILL.md +20 -23
- package/src/skf-drop-skill/customize.toml +10 -0
- package/src/skf-drop-skill/references/execute.md +67 -36
- package/src/skf-drop-skill/references/headless-contract.md +18 -0
- package/src/skf-drop-skill/references/health-check.md +1 -13
- package/src/skf-drop-skill/references/report.md +44 -10
- package/src/skf-drop-skill/references/select.md +93 -48
- package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
- package/src/skf-export-skill/SKILL.md +17 -14
- package/src/skf-export-skill/assets/managed-section-format.md +2 -5
- package/src/skf-export-skill/customize.toml +10 -6
- package/src/skf-export-skill/references/generate-snippet.md +21 -35
- package/src/skf-export-skill/references/load-skill.md +44 -42
- package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
- package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
- package/src/skf-export-skill/references/package.md +27 -33
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
- package/src/skf-export-skill/references/result-envelope.md +28 -0
- package/src/skf-export-skill/references/summary.md +11 -5
- package/src/skf-export-skill/references/token-report.md +42 -24
- package/src/skf-export-skill/references/update-context.md +32 -33
- package/src/skf-forger/SKILL.md +32 -39
- package/src/skf-forger/references/pipeline-mode.md +34 -0
- package/src/skf-forger/scripts/parse-pipeline.py +270 -0
- package/src/skf-quick-skill/SKILL.md +10 -60
- package/src/skf-quick-skill/customize.toml +9 -0
- package/src/skf-quick-skill/references/batch-mode.md +6 -4
- package/src/skf-quick-skill/references/compile.md +15 -52
- package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
- package/src/skf-quick-skill/references/finalize.md +12 -6
- package/src/skf-quick-skill/references/halt-contract.md +54 -0
- package/src/skf-quick-skill/references/health-check.md +3 -4
- package/src/skf-quick-skill/references/quick-extract.md +7 -18
- package/src/skf-quick-skill/references/registry-resolution.md +1 -23
- package/src/skf-quick-skill/references/resolve-target.md +27 -34
- package/src/skf-quick-skill/references/write-and-validate.md +10 -25
- package/src/skf-refine-architecture/SKILL.md +16 -8
- package/src/skf-refine-architecture/customize.toml +9 -0
- package/src/skf-refine-architecture/references/compile.md +10 -8
- package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
- package/src/skf-refine-architecture/references/health-check.md +1 -1
- package/src/skf-refine-architecture/references/improvements.md +9 -25
- package/src/skf-refine-architecture/references/init.md +9 -16
- package/src/skf-refine-architecture/references/issue-detection.md +10 -25
- package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
- package/src/skf-refine-architecture/references/report.md +14 -11
- package/src/skf-rename-skill/SKILL.md +11 -21
- package/src/skf-rename-skill/customize.toml +10 -0
- package/src/skf-rename-skill/references/execute.md +112 -82
- package/src/skf-rename-skill/references/exit-codes.md +12 -0
- package/src/skf-rename-skill/references/report.md +16 -8
- package/src/skf-rename-skill/references/select.md +69 -42
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
- package/src/skf-setup/SKILL.md +21 -13
- package/src/skf-setup/customize.toml +10 -0
- package/src/skf-setup/references/auto-index.md +11 -10
- package/src/skf-setup/references/ccc-index.md +14 -15
- package/src/skf-setup/references/detect-and-tier.md +10 -8
- package/src/skf-setup/references/report.md +26 -32
- package/src/skf-setup/references/tier-rules.md +2 -32
- package/src/skf-setup/references/write-config.md +9 -10
- package/src/skf-test-skill/SKILL.md +10 -8
- package/src/skf-test-skill/customize.toml +2 -0
- package/src/skf-test-skill/references/coherence-check.md +19 -31
- package/src/skf-test-skill/references/coverage-check.md +111 -67
- package/src/skf-test-skill/references/detect-mode.md +10 -41
- package/src/skf-test-skill/references/external-validators.md +11 -19
- package/src/skf-test-skill/references/init.md +39 -21
- package/src/skf-test-skill/references/migration-section-rules.md +4 -4
- package/src/skf-test-skill/references/report.md +60 -40
- package/src/skf-test-skill/references/score.md +32 -64
- package/src/skf-test-skill/references/scoring-rules.md +6 -62
- package/src/skf-test-skill/references/source-access-protocol.md +15 -5
- package/src/skf-test-skill/references/step-hard-gate.md +1 -3
- package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
- package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
- package/src/skf-test-skill/scripts/compute-score.py +135 -4
- package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
- package/src/skf-update-skill/SKILL.md +17 -5
- package/src/skf-update-skill/customize.toml +11 -6
- package/src/skf-update-skill/references/detect-changes.md +48 -36
- package/src/skf-update-skill/references/health-check.md +5 -11
- package/src/skf-update-skill/references/init.md +40 -32
- package/src/skf-update-skill/references/manual-section-rules.md +1 -2
- package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
- package/src/skf-update-skill/references/merge.md +13 -57
- package/src/skf-update-skill/references/re-extract.md +27 -25
- package/src/skf-update-skill/references/report.md +14 -7
- package/src/skf-update-skill/references/validate.md +32 -30
- package/src/skf-update-skill/references/write.md +86 -51
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
- package/src/skf-verify-stack/SKILL.md +11 -21
- package/src/skf-verify-stack/customize.toml +12 -3
- package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
- package/src/skf-verify-stack/references/coverage.md +21 -10
- package/src/skf-verify-stack/references/exit-codes.md +14 -0
- package/src/skf-verify-stack/references/health-check.md +1 -1
- package/src/skf-verify-stack/references/init.md +20 -25
- package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
- package/src/skf-verify-stack/references/integrations.md +44 -49
- package/src/skf-verify-stack/references/report.md +28 -34
- package/src/skf-verify-stack/references/requirements.md +3 -3
- package/src/skf-verify-stack/references/synthesize.md +36 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
- package/tools/cli/lib/ui.js +3 -2
- package/docs/deepwiki.md +0 -89
- package/src/skf-rename-skill/references/rebuild-context.md +0 -110
|
@@ -22,7 +22,7 @@ SKILL.md, then extract usage patterns per skill (the part that does
|
|
|
22
22
|
benefit from LLM judgment).
|
|
23
23
|
|
|
24
24
|
Subcommand:
|
|
25
|
-
enumerate <skills-root>
|
|
25
|
+
enumerate <skills-root> [--pairs] [--reliability]
|
|
26
26
|
Emit JSON {"skills": [...], "cycles": [...], "warnings": [...]}
|
|
27
27
|
describing every subdirectory of <skills-root> that resolves to a
|
|
28
28
|
skill package. Both layouts from knowledge/version-paths.md are
|
|
@@ -84,18 +84,47 @@ Symlink handling:
|
|
|
84
84
|
Per-skill errors (malformed metadata.json, OSError, etc.) are captured
|
|
85
85
|
as warnings on the top-level result; they do not exit the process.
|
|
86
86
|
|
|
87
|
+
Optional derived output (additive flags — off by default, so existing
|
|
88
|
+
consumers that read only skills/cycles/warnings are unaffected):
|
|
89
|
+
|
|
90
|
+
--pairs
|
|
91
|
+
Attach the complete set of unique library pairs derived from the
|
|
92
|
+
inventory: `pairs = [{"library_a": a, "library_b": b}, ...]` over
|
|
93
|
+
`itertools.combinations` of the sorted, deduplicated skill names,
|
|
94
|
+
plus `pair_count == N*(N-1)/2`. This is the deterministic
|
|
95
|
+
combinatorics the refine-architecture gap-analysis prompt used to
|
|
96
|
+
do in-context (where a dropped pair is a silently missed
|
|
97
|
+
integration gap). Sorting the names first makes ordering
|
|
98
|
+
independent of directory-scan order and byte-stable across runs.
|
|
99
|
+
|
|
100
|
+
--reliability
|
|
101
|
+
Attach the inventory reliability verdict computed from the counts
|
|
102
|
+
the script already owns:
|
|
103
|
+
skill_count — len(skills)
|
|
104
|
+
warning_count — len(warnings)
|
|
105
|
+
unreliable_ratio — warning_count / (skill_count + warning_count),
|
|
106
|
+
or 0.0 when the inventory is empty
|
|
107
|
+
inventory_reliable — unreliable_ratio <= 0.20 (the RELIABILITY
|
|
108
|
+
THRESHOLD lives here, in one unit-tested place,
|
|
109
|
+
so consuming prompts read a boolean instead of
|
|
110
|
+
re-deriving a ratio + threshold comparison).
|
|
111
|
+
The raw counts are emitted alongside the boolean so a caller's halt
|
|
112
|
+
message can still render "{warning_count}/{skill_count+warning_count}
|
|
113
|
+
skills returned malformed metadata".
|
|
114
|
+
|
|
87
115
|
Exit codes:
|
|
88
116
|
0 enumeration succeeded (including zero skills found)
|
|
89
117
|
1 user error (bad skills-root path)
|
|
90
118
|
|
|
91
119
|
CLI:
|
|
92
|
-
uv run skf-enumerate-stack-skills.py enumerate <skills-root>
|
|
120
|
+
uv run skf-enumerate-stack-skills.py enumerate <skills-root> [--pairs] [--reliability]
|
|
93
121
|
"""
|
|
94
122
|
|
|
95
123
|
from __future__ import annotations
|
|
96
124
|
|
|
97
125
|
import argparse
|
|
98
126
|
import hashlib
|
|
127
|
+
import itertools
|
|
99
128
|
import json
|
|
100
129
|
import re
|
|
101
130
|
import sys
|
|
@@ -112,6 +141,13 @@ SOURCE_REFERENCES = "references"
|
|
|
112
141
|
SOURCE_SKILL_MD = "skill-md"
|
|
113
142
|
SOURCE_UNKNOWN = "unknown"
|
|
114
143
|
|
|
144
|
+
# Inventory reliability policy (single source of truth). If the fraction of
|
|
145
|
+
# skip/malformed warnings exceeds this threshold, the inventory is deemed
|
|
146
|
+
# unreliable. The gate is `ratio <= THRESHOLD` (i.e. a ratio of exactly 0.20
|
|
147
|
+
# is still reliable) so a single malformed skill in a small 3-5 skill
|
|
148
|
+
# inventory does not trip the halt.
|
|
149
|
+
RELIABILITY_THRESHOLD = 0.20
|
|
150
|
+
|
|
115
151
|
_CONFIDENCE_BY_SOURCE = {
|
|
116
152
|
SOURCE_METADATA: "T1",
|
|
117
153
|
SOURCE_REFERENCES: "T2",
|
|
@@ -552,6 +588,53 @@ def enumerate_stack_skills(skills_root: Path) -> dict:
|
|
|
552
588
|
return result
|
|
553
589
|
|
|
554
590
|
|
|
591
|
+
# --------------------------------------------------------------------------
|
|
592
|
+
# Derived output: unique library pairs (--pairs)
|
|
593
|
+
# --------------------------------------------------------------------------
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def compute_pairs(skills: list[dict]) -> list[dict]:
|
|
597
|
+
"""Deterministic unique library pairs from an inventory's skills[].
|
|
598
|
+
|
|
599
|
+
Returns `[{"library_a": a, "library_b": b}, ...]` over every
|
|
600
|
+
combination of the sorted, deduplicated skill names — exactly
|
|
601
|
+
N*(N-1)/2 entries for N distinct names. Sorting first makes the order
|
|
602
|
+
independent of directory-scan order and byte-stable across runs; the
|
|
603
|
+
dedup guards against a pathological repeated name yielding a duplicate
|
|
604
|
+
pair. Never drops or duplicates a pair, unlike in-context enumeration
|
|
605
|
+
at larger N.
|
|
606
|
+
"""
|
|
607
|
+
names = sorted({e["name"] for e in skills})
|
|
608
|
+
return [
|
|
609
|
+
{"library_a": a, "library_b": b}
|
|
610
|
+
for a, b in itertools.combinations(names, 2)
|
|
611
|
+
]
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
# --------------------------------------------------------------------------
|
|
615
|
+
# Derived output: inventory reliability verdict (--reliability)
|
|
616
|
+
# --------------------------------------------------------------------------
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def compute_reliability(skill_count: int, warning_count: int) -> dict:
|
|
620
|
+
"""Reliability verdict from the inventory's own counts.
|
|
621
|
+
|
|
622
|
+
`unreliable_ratio` = warning_count / (skill_count + warning_count),
|
|
623
|
+
or 0.0 for an empty inventory (no ZeroDivisionError). The inventory is
|
|
624
|
+
reliable when that ratio is <= RELIABILITY_THRESHOLD (boundary
|
|
625
|
+
inclusive). Emits the raw counts too so a caller's halt message can
|
|
626
|
+
render "{warning_count}/{skill_count+warning_count}".
|
|
627
|
+
"""
|
|
628
|
+
total = skill_count + warning_count
|
|
629
|
+
unreliable_ratio = (warning_count / total) if total else 0.0
|
|
630
|
+
return {
|
|
631
|
+
"inventory_reliable": unreliable_ratio <= RELIABILITY_THRESHOLD,
|
|
632
|
+
"unreliable_ratio": unreliable_ratio,
|
|
633
|
+
"skill_count": skill_count,
|
|
634
|
+
"warning_count": warning_count,
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
|
|
555
638
|
# --------------------------------------------------------------------------
|
|
556
639
|
# CLI
|
|
557
640
|
# --------------------------------------------------------------------------
|
|
@@ -566,6 +649,15 @@ def _cmd_enumerate(args: argparse.Namespace) -> int:
|
|
|
566
649
|
)
|
|
567
650
|
return 1
|
|
568
651
|
result = enumerate_stack_skills(skills_root)
|
|
652
|
+
# Additive derived output — attached only when the flag is set, so the
|
|
653
|
+
# default shape stays {skills, cycles, warnings} for existing consumers.
|
|
654
|
+
if args.pairs:
|
|
655
|
+
result["pairs"] = compute_pairs(result["skills"])
|
|
656
|
+
result["pair_count"] = len(result["pairs"])
|
|
657
|
+
if args.reliability:
|
|
658
|
+
result.update(
|
|
659
|
+
compute_reliability(len(result["skills"]), len(result["warnings"]))
|
|
660
|
+
)
|
|
569
661
|
json.dump(result, sys.stdout, indent=2)
|
|
570
662
|
sys.stdout.write("\n")
|
|
571
663
|
return 0
|
|
@@ -590,6 +682,23 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
590
682
|
"skills_root",
|
|
591
683
|
help="directory containing skill packages (each subdir has SKILL.md)",
|
|
592
684
|
)
|
|
685
|
+
p_enum.add_argument(
|
|
686
|
+
"--pairs",
|
|
687
|
+
action="store_true",
|
|
688
|
+
help=(
|
|
689
|
+
"additionally emit `pairs[]` (unique {library_a, library_b} "
|
|
690
|
+
"combinations over the sorted skill names) and `pair_count`"
|
|
691
|
+
),
|
|
692
|
+
)
|
|
693
|
+
p_enum.add_argument(
|
|
694
|
+
"--reliability",
|
|
695
|
+
action="store_true",
|
|
696
|
+
help=(
|
|
697
|
+
"additionally emit the reliability verdict "
|
|
698
|
+
"(`inventory_reliable`, `unreliable_ratio`, `skill_count`, "
|
|
699
|
+
f"`warning_count`; threshold {RELIABILITY_THRESHOLD})"
|
|
700
|
+
),
|
|
701
|
+
)
|
|
593
702
|
p_enum.set_defaults(func=_cmd_enumerate)
|
|
594
703
|
|
|
595
704
|
return parser
|
|
@@ -261,6 +261,29 @@ def parse_settings_gradle(content: str) -> list[str]:
|
|
|
261
261
|
return modules
|
|
262
262
|
|
|
263
263
|
|
|
264
|
+
def parse_package_swift(content: str) -> dict:
|
|
265
|
+
"""Best-effort parse of a SwiftPM Package.swift manifest.
|
|
266
|
+
|
|
267
|
+
SwiftPM has no version field in the manifest (versions come from git tags),
|
|
268
|
+
so `version` is always None; the brief falls back to target_version / default.
|
|
269
|
+
"""
|
|
270
|
+
name_m = re.search(r"\bPackage\s*\(\s*name:\s*['\"]([^'\"]+)['\"]", content, re.DOTALL)
|
|
271
|
+
deps: list[str] = []
|
|
272
|
+
for m in re.finditer(r"\.package\s*\(\s*url:\s*['\"]([^'\"]+)['\"]", content):
|
|
273
|
+
seg = m.group(1).rstrip("/").rsplit("/", 1)[-1]
|
|
274
|
+
if seg.endswith(".git"):
|
|
275
|
+
seg = seg[: -len(".git")]
|
|
276
|
+
if seg:
|
|
277
|
+
deps.append(seg)
|
|
278
|
+
return {
|
|
279
|
+
"name": name_m.group(1).strip() if name_m else None,
|
|
280
|
+
"version": None,
|
|
281
|
+
"description": None,
|
|
282
|
+
"dependencies": deps,
|
|
283
|
+
"modules": [],
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
264
287
|
# --------------------------------------------------------------------------
|
|
265
288
|
# Export scanners
|
|
266
289
|
# --------------------------------------------------------------------------
|
|
@@ -388,6 +411,35 @@ def scan_exports_kotlin(content: str, source_file: str) -> list[dict]:
|
|
|
388
411
|
return out
|
|
389
412
|
|
|
390
413
|
|
|
414
|
+
# Swift declarations are public only when explicitly marked `public`/`open`
|
|
415
|
+
# (the default is internal). The lazy `[^\n]*?` skips intervening modifiers
|
|
416
|
+
# (final/static/class-method/@attributes) between the access keyword and the
|
|
417
|
+
# declaration keyword.
|
|
418
|
+
_SWIFT_DECL_RE = re.compile(
|
|
419
|
+
r"^\s*(?:public|open)\b[^\n]*?\b"
|
|
420
|
+
r"(func|class|struct|enum|protocol|actor|typealias|var|let)\s+([A-Za-z_]\w*)",
|
|
421
|
+
re.MULTILINE,
|
|
422
|
+
)
|
|
423
|
+
_SWIFT_DECL_KEYWORDS = {
|
|
424
|
+
"func", "class", "struct", "enum", "protocol", "actor", "typealias", "var", "let",
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def scan_exports_swift(content: str, source_file: str) -> list[dict]:
|
|
429
|
+
"""Swift defaults to internal — emit only public/open declarations."""
|
|
430
|
+
out: list[dict] = []
|
|
431
|
+
seen: set[str] = set()
|
|
432
|
+
for m in _SWIFT_DECL_RE.finditer(content):
|
|
433
|
+
name = m.group(2)
|
|
434
|
+
# Guard the rare `public class func foo` case where the lazy skip stops
|
|
435
|
+
# on the `class` modifier and captures the next keyword as the name.
|
|
436
|
+
if name in _SWIFT_DECL_KEYWORDS or name in seen:
|
|
437
|
+
continue
|
|
438
|
+
seen.add(name)
|
|
439
|
+
out.append({"name": name, "type": m.group(1), "source_file": source_file})
|
|
440
|
+
return out
|
|
441
|
+
|
|
442
|
+
|
|
391
443
|
# --------------------------------------------------------------------------
|
|
392
444
|
# Orchestrator
|
|
393
445
|
# --------------------------------------------------------------------------
|
|
@@ -405,6 +457,7 @@ LANGUAGE_DISPATCH: dict[str, tuple[ManifestParser, ExportScanner]] = {
|
|
|
405
457
|
"go": (parse_go_mod, scan_exports_go),
|
|
406
458
|
"java": (parse_pom_xml, scan_exports_java),
|
|
407
459
|
"kotlin": (parse_gradle, scan_exports_kotlin),
|
|
460
|
+
"swift": (parse_package_swift, scan_exports_swift),
|
|
408
461
|
}
|
|
409
462
|
|
|
410
463
|
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""SKF Find Cycles — enumerate the simple directed cycles in a pair graph.
|
|
7
|
+
|
|
8
|
+
`skf-verify-stack`'s `integrations.md` §4 (Cross-Reference Each Integration
|
|
9
|
+
Pair → Cycle detection) asks the LLM to build a directed pair graph — an edge
|
|
10
|
+
`A → B` exists when skill A literally cites skill B via Check 4 — and then to
|
|
11
|
+
"run cycle detection (DFS with visited + recursion stack)", appending one
|
|
12
|
+
synthetic `Risky` verdict row per circular integration dependency found.
|
|
13
|
+
|
|
14
|
+
Deciding *which* edges exist is a semantic judgment (Check-4 literal-citation
|
|
15
|
+
analysis) and stays in the prompt. But once the edge set is fixed, the set of
|
|
16
|
+
directed cycles is fully determined — a graph traversal with exactly one
|
|
17
|
+
correct answer per edge set. In-prompt DFS silently misses real multi-hop
|
|
18
|
+
cycles or invents spurious ones as the pair count grows; this helper makes the
|
|
19
|
+
traversal deterministic and unit-testable. The prompt hands over the edges as
|
|
20
|
+
JSON and appends the synthetic rows from `cycles[]` — no traversal by hand.
|
|
21
|
+
|
|
22
|
+
Subcommand:
|
|
23
|
+
find --edges <json-file-or-'-'>
|
|
24
|
+
--edges path to the edges JSON, or '-' to read from stdin.
|
|
25
|
+
|
|
26
|
+
Input shape (the Check-4 citation edges, one [from, to] pair each):
|
|
27
|
+
{"edges": [["A", "B"], ["B", "C"], ["C", "A"], ...]}
|
|
28
|
+
|
|
29
|
+
Emit JSON:
|
|
30
|
+
{
|
|
31
|
+
"cycles": [["A", "B", "C", "A"], ...],
|
|
32
|
+
"cycle_count": N
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Each cycle is a closed node path: the first node is repeated at the end
|
|
36
|
+
(`A → B → C → A` renders as ["A", "B", "C", "A"]). Every simple directed
|
|
37
|
+
cycle is enumerated exactly once — de-duplicated across its rotations by
|
|
38
|
+
canonicalising to the lexicographically-minimal rotation. `cycles[]` is
|
|
39
|
+
sorted by length ASC then lexicographically for stable, reproducible
|
|
40
|
+
ordering. A self-loop edge (["A", "A"]) is a 1-node cycle ["A", "A"].
|
|
41
|
+
|
|
42
|
+
CLI examples:
|
|
43
|
+
uv run skf-find-cycles.py find --edges edges.json
|
|
44
|
+
echo '{"edges": [["A","B"],["B","A"]]}' | uv run skf-find-cycles.py find --edges -
|
|
45
|
+
|
|
46
|
+
Exit codes:
|
|
47
|
+
0 — operation succeeded (including: no cycles / empty edge set → empty list)
|
|
48
|
+
2 — malformed input (bad JSON, wrong shape, non-string node) or internal
|
|
49
|
+
error
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import argparse
|
|
55
|
+
import json
|
|
56
|
+
import sys
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class InputError(Exception):
|
|
61
|
+
"""A malformed-input error → exit code 2."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# --------------------------------------------------------------------------
|
|
65
|
+
# Input validation
|
|
66
|
+
# --------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_edges(payload: object) -> list[tuple[str, str]]:
|
|
70
|
+
"""Validate and normalise the edges payload.
|
|
71
|
+
|
|
72
|
+
Accepts `{"edges": [[from, to], ...]}`; each edge must be a two-element
|
|
73
|
+
array of non-empty strings. Raises InputError on any structural defect.
|
|
74
|
+
Returns a list of (from, to) tuples (order preserved; duplicates kept —
|
|
75
|
+
they collapse during adjacency construction).
|
|
76
|
+
"""
|
|
77
|
+
if not isinstance(payload, dict):
|
|
78
|
+
raise InputError(
|
|
79
|
+
f"input must be a JSON object with an `edges` key; "
|
|
80
|
+
f"got {type(payload).__name__}"
|
|
81
|
+
)
|
|
82
|
+
edges = payload.get("edges")
|
|
83
|
+
if not isinstance(edges, list):
|
|
84
|
+
raise InputError("input `edges` must be a JSON array of [from, to] pairs")
|
|
85
|
+
normalized: list[tuple[str, str]] = []
|
|
86
|
+
for idx, edge in enumerate(edges):
|
|
87
|
+
if not isinstance(edge, list) or len(edge) != 2:
|
|
88
|
+
raise InputError(
|
|
89
|
+
f"edges[{idx}] must be a two-element [from, to] array; got {edge!r}"
|
|
90
|
+
)
|
|
91
|
+
src, dst = edge
|
|
92
|
+
if not isinstance(src, str) or not src:
|
|
93
|
+
raise InputError(f"edges[{idx}][0] must be a non-empty string; got {src!r}")
|
|
94
|
+
if not isinstance(dst, str) or not dst:
|
|
95
|
+
raise InputError(f"edges[{idx}][1] must be a non-empty string; got {dst!r}")
|
|
96
|
+
normalized.append((src, dst))
|
|
97
|
+
return normalized
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --------------------------------------------------------------------------
|
|
101
|
+
# Cycle enumeration
|
|
102
|
+
# --------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _canonicalize(cycle_nodes: list[str]) -> tuple[str, ...]:
|
|
106
|
+
"""Return the lexicographically-minimal rotation of a cycle's node list.
|
|
107
|
+
|
|
108
|
+
`cycle_nodes` is the cycle without the closing repeat (e.g. ["B","C","A"]
|
|
109
|
+
for B → C → A → B). All rotations describe the same directed cycle; the
|
|
110
|
+
minimal rotation is the stable de-duplication key ((A, B, C) here).
|
|
111
|
+
"""
|
|
112
|
+
n = len(cycle_nodes)
|
|
113
|
+
rotations = [tuple(cycle_nodes[i:] + cycle_nodes[:i]) for i in range(n)]
|
|
114
|
+
return min(rotations)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def find_cycles(edges: list[tuple[str, str]]) -> dict:
|
|
118
|
+
"""Enumerate every simple directed cycle in the edge set.
|
|
119
|
+
|
|
120
|
+
Deterministic: same edge set → byte-identical output. Runs a DFS from each
|
|
121
|
+
node (in sorted order) tracking the current recursion path; whenever the
|
|
122
|
+
walk returns to its start node a cycle is recorded, canonicalised to its
|
|
123
|
+
minimal rotation, and de-duplicated. Graphs here are small pair graphs, so
|
|
124
|
+
the exhaustive enumeration is inexpensive.
|
|
125
|
+
"""
|
|
126
|
+
# Build sorted, de-duplicated adjacency for deterministic traversal.
|
|
127
|
+
adj: dict[str, list[str]] = {}
|
|
128
|
+
nodes: set[str] = set()
|
|
129
|
+
for src, dst in edges:
|
|
130
|
+
adj.setdefault(src, [])
|
|
131
|
+
if dst not in adj[src]:
|
|
132
|
+
adj[src].append(dst)
|
|
133
|
+
nodes.add(src)
|
|
134
|
+
nodes.add(dst)
|
|
135
|
+
for src in adj:
|
|
136
|
+
adj[src].sort()
|
|
137
|
+
|
|
138
|
+
seen: set[tuple[str, ...]] = set()
|
|
139
|
+
|
|
140
|
+
def dfs(start: str, current: str, path: list[str], on_stack: set[str]) -> None:
|
|
141
|
+
for nxt in adj.get(current, ()):
|
|
142
|
+
if nxt == start:
|
|
143
|
+
canon = _canonicalize(path)
|
|
144
|
+
seen.add(canon)
|
|
145
|
+
elif nxt not in on_stack:
|
|
146
|
+
on_stack.add(nxt)
|
|
147
|
+
path.append(nxt)
|
|
148
|
+
dfs(start, nxt, path, on_stack)
|
|
149
|
+
path.pop()
|
|
150
|
+
on_stack.discard(nxt)
|
|
151
|
+
|
|
152
|
+
for start in sorted(nodes):
|
|
153
|
+
dfs(start, start, [start], {start})
|
|
154
|
+
|
|
155
|
+
# Stable output order: by cycle length, then lexicographically.
|
|
156
|
+
ordered = sorted(seen, key=lambda c: (len(c), c))
|
|
157
|
+
cycles = [list(c) + [c[0]] for c in ordered]
|
|
158
|
+
return {"cycles": cycles, "cycle_count": len(cycles)}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --------------------------------------------------------------------------
|
|
162
|
+
# CLI
|
|
163
|
+
# --------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _read_source(source: str) -> str:
|
|
167
|
+
"""Read text from a file path or stdin (if source == '-')."""
|
|
168
|
+
if source == "-":
|
|
169
|
+
try:
|
|
170
|
+
return sys.stdin.read()
|
|
171
|
+
except OSError as exc:
|
|
172
|
+
raise InputError(f"failed to read --edges from stdin: {exc}") from exc
|
|
173
|
+
path = Path(source)
|
|
174
|
+
if not path.is_file():
|
|
175
|
+
raise InputError(f"--edges file not found: {path}")
|
|
176
|
+
try:
|
|
177
|
+
return path.read_text(encoding="utf-8")
|
|
178
|
+
except OSError as exc:
|
|
179
|
+
raise InputError(f"failed to read --edges file {path}: {exc}") from exc
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _cmd_find(args: argparse.Namespace) -> int:
|
|
183
|
+
text = _read_source(args.edges)
|
|
184
|
+
try:
|
|
185
|
+
payload = json.loads(text)
|
|
186
|
+
except json.JSONDecodeError as exc:
|
|
187
|
+
raise InputError(f"malformed JSON in --edges input: {exc}") from exc
|
|
188
|
+
edges = parse_edges(payload)
|
|
189
|
+
result = find_cycles(edges)
|
|
190
|
+
if args.verbose:
|
|
191
|
+
print(
|
|
192
|
+
f"analyzed {len(edges)} edge(s); found {result['cycle_count']} cycle(s)",
|
|
193
|
+
file=sys.stderr,
|
|
194
|
+
)
|
|
195
|
+
json.dump(result, sys.stdout, indent=2)
|
|
196
|
+
sys.stdout.write("\n")
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
201
|
+
parser = argparse.ArgumentParser(
|
|
202
|
+
prog="skf-find-cycles",
|
|
203
|
+
description=(
|
|
204
|
+
"Enumerate the simple directed cycles in an integration pair graph "
|
|
205
|
+
"(skf-verify-stack integrations.md §4 cycle detection)."
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
209
|
+
|
|
210
|
+
p_find = sub.add_parser(
|
|
211
|
+
"find",
|
|
212
|
+
help="emit the de-duplicated simple directed cycles as JSON",
|
|
213
|
+
)
|
|
214
|
+
p_find.add_argument(
|
|
215
|
+
"--edges",
|
|
216
|
+
required=True,
|
|
217
|
+
help=(
|
|
218
|
+
"path to the edges JSON, or '-' for stdin. "
|
|
219
|
+
'Shape: {"edges": [["<from>", "<to>"], ...]}'
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
p_find.add_argument(
|
|
223
|
+
"--verbose",
|
|
224
|
+
action="store_true",
|
|
225
|
+
help="print a one-line summary to stderr",
|
|
226
|
+
)
|
|
227
|
+
p_find.set_defaults(func=_cmd_find)
|
|
228
|
+
|
|
229
|
+
return parser
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def main(argv: list[str] | None = None) -> int:
|
|
233
|
+
parser = _build_parser()
|
|
234
|
+
args = parser.parse_args(argv)
|
|
235
|
+
try:
|
|
236
|
+
return args.func(args)
|
|
237
|
+
except InputError as exc:
|
|
238
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
239
|
+
return 2
|
|
240
|
+
except Exception as exc: # noqa: BLE001 — last-resort guard → exit 2
|
|
241
|
+
print(f"internal error: {exc}", file=sys.stderr)
|
|
242
|
+
return 2
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
raise SystemExit(main())
|