bmad-module-skill-forge 2.0.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/docs/_data/pinned.yaml +1 -1
- package/package.json +2 -2
- package/src/knowledge/skill-lifecycle.md +1 -1
- package/src/module-help.csv +2 -2
- 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-detect-docs.py +232 -1
- package/src/shared/scripts/skf-detect-language.py +77 -0
- package/src/shared/scripts/skf-detect-tools.py +105 -2
- package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
- 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-manifest-ops.py +67 -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-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 +39 -1
- 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 +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +214 -0
- package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
- package/src/skf-analyze-source/SKILL.md +17 -32
- 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 +42 -180
- 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/customize.toml +14 -0
- package/src/skf-brief-skill/references/analyze-target.md +6 -14
- package/src/skf-brief-skill/references/confirm-brief.md +2 -14
- package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
- package/src/skf-brief-skill/references/gather-intent.md +19 -22
- 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 +4 -14
- package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
- package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
- package/src/skf-brief-skill/references/version-resolution.md +1 -1
- package/src/skf-brief-skill/references/write-brief.md +17 -10
- package/src/skf-campaign/SKILL.md +15 -84
- package/src/skf-campaign/customize.toml +2 -0
- package/src/skf-campaign/manifest.yaml +4 -0
- package/src/skf-campaign/references/campaign-contracts.md +67 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
- package/src/skf-campaign/references/step-01-setup.md +4 -5
- package/src/skf-campaign/references/step-02-strategy.md +8 -3
- package/src/skf-campaign/references/step-03-pins.md +1 -1
- package/src/skf-campaign/references/step-04-provenance.md +1 -1
- package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
- package/src/skf-campaign/references/step-06-batch.md +7 -6
- package/src/skf-campaign/references/step-11-maintenance.md +2 -2
- package/src/skf-campaign/references/step-resume.md +9 -8
- package/src/skf-campaign/scripts/campaign-deps.py +10 -1
- package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
- package/src/skf-campaign/scripts/campaign-status.py +162 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
- package/src/skf-create-skill/SKILL.md +14 -3
- package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
- 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 +34 -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 +14 -29
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
- package/src/skf-create-skill/references/extraction-patterns.md +7 -7
- package/src/skf-create-skill/references/generate-artifacts.md +9 -23
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +16 -18
- package/src/skf-create-skill/references/report.md +12 -15
- package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
- package/src/skf-create-skill/references/step-auto-shard.md +52 -25
- package/src/skf-create-skill/references/step-doc-rot.md +30 -12
- package/src/skf-create-skill/references/step-doc-sources.md +2 -2
- package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
- package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
- 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 +59 -28
- 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 +51 -31
- 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 +42 -10
- package/src/skf-drop-skill/references/select.md +88 -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 +27 -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 +14 -7
- package/src/skf-refine-architecture/customize.toml +9 -0
- package/src/skf-refine-architecture/references/compile.md +9 -7
- 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 +13 -10
- 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 +107 -78
- 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 +20 -12
- package/src/skf-setup/customize.toml +10 -0
- package/src/skf-setup/references/auto-index.md +9 -10
- package/src/skf-setup/references/ccc-index.md +14 -15
- package/src/skf-setup/references/detect-and-tier.md +6 -8
- package/src/skf-setup/references/report.md +22 -31
- 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 +109 -75
- 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 +38 -20
- 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 +6 -6
- 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 +131 -1
- 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 +14 -0
- package/src/skf-update-skill/references/detect-changes.md +19 -37
- package/src/skf-update-skill/references/health-check.md +3 -9
- package/src/skf-update-skill/references/init.md +39 -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 +12 -24
- 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/src/skf-rename-skill/references/rebuild-context.md +0 -110
|
@@ -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())
|