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.
Files changed (235) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.nvmrc +1 -1
  3. package/docs/_data/pinned.yaml +1 -1
  4. package/package.json +2 -2
  5. package/src/knowledge/skill-lifecycle.md +1 -1
  6. package/src/module-help.csv +2 -2
  7. package/src/shared/scripts/skf-comention-pairs.py +350 -0
  8. package/src/shared/scripts/skf-count-tokens.py +222 -0
  9. package/src/shared/scripts/skf-detect-docs.py +232 -1
  10. package/src/shared/scripts/skf-detect-language.py +77 -0
  11. package/src/shared/scripts/skf-detect-tools.py +105 -2
  12. package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
  13. package/src/shared/scripts/skf-find-cycles.py +246 -0
  14. package/src/shared/scripts/skf-hash-content.py +487 -2
  15. package/src/shared/scripts/skf-manifest-ops.py +67 -1
  16. package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
  17. package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
  18. package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
  19. package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
  20. package/src/shared/scripts/skf-shard-body.py +461 -0
  21. package/src/shared/scripts/skf-skill-inventory.py +139 -3
  22. package/src/shared/scripts/skf-structural-diff.py +307 -67
  23. package/src/shared/scripts/skf-validate-brief-schema.py +39 -1
  24. package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
  25. package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
  26. package/src/shared/scripts/skf-validate-output.py +443 -10
  27. package/src/shared/scripts/skf-validate-pins.py +1 -1
  28. package/src/shared/scripts/skf-verify-no-trace.py +214 -0
  29. package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
  30. package/src/skf-analyze-source/SKILL.md +17 -32
  31. package/src/skf-analyze-source/customize.toml +2 -0
  32. package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
  33. package/src/skf-analyze-source/references/continue.md +8 -13
  34. package/src/skf-analyze-source/references/generate-briefs.md +28 -12
  35. package/src/skf-analyze-source/references/headless-contract.md +33 -0
  36. package/src/skf-analyze-source/references/health-check.md +1 -2
  37. package/src/skf-analyze-source/references/identify-units.md +25 -36
  38. package/src/skf-analyze-source/references/init.md +12 -25
  39. package/src/skf-analyze-source/references/map-and-detect.md +8 -20
  40. package/src/skf-analyze-source/references/recommend.md +4 -14
  41. package/src/skf-analyze-source/references/scan-project.md +3 -11
  42. package/src/skf-analyze-source/references/step-auto-scope.md +42 -180
  43. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
  44. package/src/skf-audit-skill/SKILL.md +9 -6
  45. package/src/skf-audit-skill/customize.toml +2 -0
  46. package/src/skf-audit-skill/references/init.md +58 -32
  47. package/src/skf-audit-skill/references/re-index.md +15 -34
  48. package/src/skf-audit-skill/references/report.md +11 -58
  49. package/src/skf-audit-skill/references/semantic-diff.md +4 -22
  50. package/src/skf-audit-skill/references/severity-classify.md +53 -87
  51. package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
  52. package/src/skf-audit-skill/references/structural-diff.md +43 -68
  53. package/src/skf-brief-skill/SKILL.md +7 -36
  54. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  55. package/src/skf-brief-skill/customize.toml +14 -0
  56. package/src/skf-brief-skill/references/analyze-target.md +6 -14
  57. package/src/skf-brief-skill/references/confirm-brief.md +2 -14
  58. package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
  59. package/src/skf-brief-skill/references/gather-intent.md +19 -22
  60. package/src/skf-brief-skill/references/headless-args.md +1 -1
  61. package/src/skf-brief-skill/references/health-check.md +1 -22
  62. package/src/skf-brief-skill/references/invocation-contract.md +37 -0
  63. package/src/skf-brief-skill/references/scope-definition.md +4 -14
  64. package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
  65. package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
  66. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  67. package/src/skf-brief-skill/references/write-brief.md +17 -10
  68. package/src/skf-campaign/SKILL.md +15 -84
  69. package/src/skf-campaign/customize.toml +2 -0
  70. package/src/skf-campaign/manifest.yaml +4 -0
  71. package/src/skf-campaign/references/campaign-contracts.md +67 -0
  72. package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
  73. package/src/skf-campaign/references/step-01-setup.md +4 -5
  74. package/src/skf-campaign/references/step-02-strategy.md +8 -3
  75. package/src/skf-campaign/references/step-03-pins.md +1 -1
  76. package/src/skf-campaign/references/step-04-provenance.md +1 -1
  77. package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
  78. package/src/skf-campaign/references/step-06-batch.md +7 -6
  79. package/src/skf-campaign/references/step-11-maintenance.md +2 -2
  80. package/src/skf-campaign/references/step-resume.md +9 -8
  81. package/src/skf-campaign/scripts/campaign-deps.py +10 -1
  82. package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
  83. package/src/skf-campaign/scripts/campaign-status.py +162 -0
  84. package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
  85. package/src/skf-create-skill/SKILL.md +14 -3
  86. package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
  87. package/src/skf-create-skill/assets/skill-sections.md +9 -100
  88. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
  89. package/src/skf-create-skill/customize.toml +13 -0
  90. package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
  91. package/src/skf-create-skill/references/compile.md +34 -47
  92. package/src/skf-create-skill/references/component-extraction.md +8 -35
  93. package/src/skf-create-skill/references/ecosystem-check.md +9 -25
  94. package/src/skf-create-skill/references/enrich.md +4 -17
  95. package/src/skf-create-skill/references/extract.md +14 -29
  96. package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
  97. package/src/skf-create-skill/references/extraction-patterns.md +7 -7
  98. package/src/skf-create-skill/references/generate-artifacts.md +9 -23
  99. package/src/skf-create-skill/references/health-check.md +1 -1
  100. package/src/skf-create-skill/references/load-brief.md +16 -18
  101. package/src/skf-create-skill/references/report.md +12 -15
  102. package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
  103. package/src/skf-create-skill/references/step-auto-shard.md +52 -25
  104. package/src/skf-create-skill/references/step-doc-rot.md +30 -12
  105. package/src/skf-create-skill/references/step-doc-sources.md +2 -2
  106. package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
  107. package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
  108. package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
  109. package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
  110. package/src/skf-create-skill/references/validate.md +59 -28
  111. package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
  112. package/src/skf-create-stack-skill/SKILL.md +15 -9
  113. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
  114. package/src/skf-create-stack-skill/customize.toml +10 -0
  115. package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
  116. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
  117. package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
  118. package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
  119. package/src/skf-create-stack-skill/references/generate-output.md +24 -75
  120. package/src/skf-create-stack-skill/references/init.md +9 -81
  121. package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
  122. package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
  123. package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
  124. package/src/skf-create-stack-skill/references/report.md +27 -62
  125. package/src/skf-create-stack-skill/references/validate.md +35 -31
  126. package/src/skf-drop-skill/SKILL.md +20 -23
  127. package/src/skf-drop-skill/customize.toml +10 -0
  128. package/src/skf-drop-skill/references/execute.md +51 -31
  129. package/src/skf-drop-skill/references/headless-contract.md +18 -0
  130. package/src/skf-drop-skill/references/health-check.md +1 -13
  131. package/src/skf-drop-skill/references/report.md +42 -10
  132. package/src/skf-drop-skill/references/select.md +88 -48
  133. package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
  134. package/src/skf-export-skill/SKILL.md +17 -14
  135. package/src/skf-export-skill/assets/managed-section-format.md +2 -5
  136. package/src/skf-export-skill/customize.toml +10 -6
  137. package/src/skf-export-skill/references/generate-snippet.md +21 -35
  138. package/src/skf-export-skill/references/load-skill.md +44 -42
  139. package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
  140. package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
  141. package/src/skf-export-skill/references/package.md +27 -33
  142. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
  143. package/src/skf-export-skill/references/result-envelope.md +28 -0
  144. package/src/skf-export-skill/references/summary.md +11 -5
  145. package/src/skf-export-skill/references/token-report.md +42 -24
  146. package/src/skf-export-skill/references/update-context.md +32 -33
  147. package/src/skf-forger/SKILL.md +27 -39
  148. package/src/skf-forger/references/pipeline-mode.md +34 -0
  149. package/src/skf-forger/scripts/parse-pipeline.py +270 -0
  150. package/src/skf-quick-skill/SKILL.md +10 -60
  151. package/src/skf-quick-skill/customize.toml +9 -0
  152. package/src/skf-quick-skill/references/batch-mode.md +6 -4
  153. package/src/skf-quick-skill/references/compile.md +15 -52
  154. package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
  155. package/src/skf-quick-skill/references/finalize.md +12 -6
  156. package/src/skf-quick-skill/references/halt-contract.md +54 -0
  157. package/src/skf-quick-skill/references/health-check.md +3 -4
  158. package/src/skf-quick-skill/references/quick-extract.md +7 -18
  159. package/src/skf-quick-skill/references/registry-resolution.md +1 -23
  160. package/src/skf-quick-skill/references/resolve-target.md +27 -34
  161. package/src/skf-quick-skill/references/write-and-validate.md +10 -25
  162. package/src/skf-refine-architecture/SKILL.md +14 -7
  163. package/src/skf-refine-architecture/customize.toml +9 -0
  164. package/src/skf-refine-architecture/references/compile.md +9 -7
  165. package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
  166. package/src/skf-refine-architecture/references/health-check.md +1 -1
  167. package/src/skf-refine-architecture/references/improvements.md +9 -25
  168. package/src/skf-refine-architecture/references/init.md +9 -16
  169. package/src/skf-refine-architecture/references/issue-detection.md +10 -25
  170. package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
  171. package/src/skf-refine-architecture/references/report.md +13 -10
  172. package/src/skf-rename-skill/SKILL.md +11 -21
  173. package/src/skf-rename-skill/customize.toml +10 -0
  174. package/src/skf-rename-skill/references/execute.md +107 -78
  175. package/src/skf-rename-skill/references/exit-codes.md +12 -0
  176. package/src/skf-rename-skill/references/report.md +16 -8
  177. package/src/skf-rename-skill/references/select.md +69 -42
  178. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
  179. package/src/skf-setup/SKILL.md +20 -12
  180. package/src/skf-setup/customize.toml +10 -0
  181. package/src/skf-setup/references/auto-index.md +9 -10
  182. package/src/skf-setup/references/ccc-index.md +14 -15
  183. package/src/skf-setup/references/detect-and-tier.md +6 -8
  184. package/src/skf-setup/references/report.md +22 -31
  185. package/src/skf-setup/references/tier-rules.md +2 -32
  186. package/src/skf-setup/references/write-config.md +9 -10
  187. package/src/skf-test-skill/SKILL.md +10 -8
  188. package/src/skf-test-skill/customize.toml +2 -0
  189. package/src/skf-test-skill/references/coherence-check.md +19 -31
  190. package/src/skf-test-skill/references/coverage-check.md +109 -75
  191. package/src/skf-test-skill/references/detect-mode.md +10 -41
  192. package/src/skf-test-skill/references/external-validators.md +11 -19
  193. package/src/skf-test-skill/references/init.md +38 -20
  194. package/src/skf-test-skill/references/migration-section-rules.md +4 -4
  195. package/src/skf-test-skill/references/report.md +60 -40
  196. package/src/skf-test-skill/references/score.md +32 -64
  197. package/src/skf-test-skill/references/scoring-rules.md +6 -62
  198. package/src/skf-test-skill/references/source-access-protocol.md +6 -6
  199. package/src/skf-test-skill/references/step-hard-gate.md +1 -3
  200. package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
  201. package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
  202. package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
  203. package/src/skf-test-skill/scripts/compute-score.py +131 -1
  204. package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
  205. package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
  206. package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
  207. package/src/skf-update-skill/SKILL.md +17 -5
  208. package/src/skf-update-skill/customize.toml +14 -0
  209. package/src/skf-update-skill/references/detect-changes.md +19 -37
  210. package/src/skf-update-skill/references/health-check.md +3 -9
  211. package/src/skf-update-skill/references/init.md +39 -32
  212. package/src/skf-update-skill/references/manual-section-rules.md +1 -2
  213. package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
  214. package/src/skf-update-skill/references/merge.md +13 -57
  215. package/src/skf-update-skill/references/re-extract.md +12 -24
  216. package/src/skf-update-skill/references/report.md +14 -7
  217. package/src/skf-update-skill/references/validate.md +32 -30
  218. package/src/skf-update-skill/references/write.md +86 -51
  219. package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
  220. package/src/skf-verify-stack/SKILL.md +11 -21
  221. package/src/skf-verify-stack/customize.toml +12 -3
  222. package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
  223. package/src/skf-verify-stack/references/coverage.md +21 -10
  224. package/src/skf-verify-stack/references/exit-codes.md +14 -0
  225. package/src/skf-verify-stack/references/health-check.md +1 -1
  226. package/src/skf-verify-stack/references/init.md +20 -25
  227. package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
  228. package/src/skf-verify-stack/references/integrations.md +44 -49
  229. package/src/skf-verify-stack/references/report.md +28 -34
  230. package/src/skf-verify-stack/references/requirements.md +3 -3
  231. package/src/skf-verify-stack/references/synthesize.md +36 -39
  232. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
  233. package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
  234. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
  235. package/src/skf-rename-skill/references/rebuild-context.md +0 -110
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = []
5
+ # ///
6
+ """Deterministic previous-vs-current delta for skf-verify-stack (synthesize.md §3).
7
+
8
+ Reading two feasibility reports and deciding each finding's verdict is judgment;
9
+ classifying how the two runs differ is not. Given the two already-extracted
10
+ finding sets, "improved / regressed / new / unchanged" is a set-diff plus a
11
+ fixed verdict ranking — one correct answer per input. Doing it in prose lets the
12
+ counts drift (mismatched pair keys, inconsistent ranking), so it lives here.
13
+
14
+ Verdict ranking (higher = healthier):
15
+ * coverage — Missing(0) < Covered(1). Replaced is intentional removal, not a
16
+ gap; Replaced findings are bucketed separately and never scored improved/
17
+ regressed (mirrors their exclusion from the coverage denominator).
18
+ * integration — Blocked(0) < Risky(1) < Plausible(2) < Verified(3).
19
+ * confidence tier — T2(1) < T1-low(2) < T1(3); a drop is a regression.
20
+
21
+ Matching keys are normalized so identity is stable across runs:
22
+ * coverage findings match on technology.lower()
23
+ * integration findings match on the unordered pair {libA, libB} (lowercased),
24
+ so "A↔B" in one run equals "B↔A" in the other.
25
+
26
+ CLI usage:
27
+ uv run skf-report-delta.py '<JSON>' # JSON literal positional
28
+ uv run skf-report-delta.py --json-input '<JSON>' # explicit flag form
29
+ cat input.json | uv run skf-report-delta.py --stdin # piped input
30
+
31
+ Input schema (one object):
32
+ {
33
+ "previous": {
34
+ "coverage": [{"technology": "react", "verdict": "Covered"}, ...],
35
+ "integration": [{"libA": "react", "libB": "express", "verdict": "Verified"}, ...]
36
+ },
37
+ "current": { ... same shape ... },
38
+ "previousTiers": {"react": "T1", ...}, # optional, skill_name -> tier
39
+ "currentTiers": {"react": "T2", ...} # optional
40
+ }
41
+
42
+ Output (stdout, one object):
43
+ {
44
+ "improved": [labels], "improvedCount": <int>,
45
+ "regressed": [labels], "regressedCount": <int>,
46
+ "unchanged": [labels], "unchangedCount": <int>,
47
+ "new": [labels], "newCount": <int>,
48
+ "dropped": [labels], "droppedCount": <int>,
49
+ "replaced": [labels], "replacedCount": <int>,
50
+ "tierDowngrades": [{"skill": "...", "from": "T1", "to": "T2"}],
51
+ "tierDowngradeCount": <int>
52
+ }
53
+
54
+ Exit codes:
55
+ 0 — delta emitted successfully
56
+ 1 — no input / input could not be parsed as JSON
57
+ 2 — input parsed but schema/semantics invalid (error object emitted as JSON)
58
+ """
59
+
60
+ from __future__ import annotations
61
+
62
+ import argparse
63
+ import json
64
+ import sys
65
+
66
+ COVERAGE_RANK = {"Missing": 0, "Covered": 1}
67
+ COVERAGE_VERDICTS = ("Covered", "Missing", "Replaced")
68
+ INTEGRATION_RANK = {"Blocked": 0, "Risky": 1, "Plausible": 2, "Verified": 3}
69
+ INTEGRATION_VERDICTS = tuple(INTEGRATION_RANK)
70
+ TIER_RANK = {"T2": 1, "T1-LOW": 2, "T1": 3, "T3": 0}
71
+
72
+
73
+ def make_error(message):
74
+ return {"error": message, "code": "INVALID_INPUT"}
75
+
76
+
77
+ def _validate_side(side, name):
78
+ if not isinstance(side, dict):
79
+ return f"`{name}` must be an object with `coverage`/`integration` lists"
80
+ cov = side.get("coverage", [])
81
+ integ = side.get("integration", [])
82
+ if not isinstance(cov, list) or not isinstance(integ, list):
83
+ return f"`{name}.coverage` and `{name}.integration` must be lists"
84
+ for i, row in enumerate(cov):
85
+ if not isinstance(row, dict) or not isinstance(row.get("technology"), str):
86
+ return f"`{name}.coverage[{i}]` requires a string `technology`"
87
+ if row.get("verdict") not in COVERAGE_VERDICTS:
88
+ return (
89
+ f"`{name}.coverage[{i}]` verdict {row.get('verdict')!r} not one of: "
90
+ f"{', '.join(COVERAGE_VERDICTS)}"
91
+ )
92
+ for i, row in enumerate(integ):
93
+ if not isinstance(row, dict) or not isinstance(row.get("libA"), str) or not isinstance(row.get("libB"), str):
94
+ return f"`{name}.integration[{i}]` requires string `libA` and `libB`"
95
+ if row.get("verdict") not in INTEGRATION_VERDICTS:
96
+ return (
97
+ f"`{name}.integration[{i}]` verdict {row.get('verdict')!r} not one of: "
98
+ f"{', '.join(INTEGRATION_VERDICTS)}"
99
+ )
100
+ return None
101
+
102
+
103
+ def _validate(inp):
104
+ if inp is None or not isinstance(inp, dict):
105
+ return "Input must be a JSON object"
106
+ if "previous" not in inp or "current" not in inp:
107
+ return "Input requires `previous` and `current` objects"
108
+ for name in ("previous", "current"):
109
+ err = _validate_side(inp[name], name)
110
+ if err:
111
+ return err
112
+ for key in ("previousTiers", "currentTiers"):
113
+ tiers = inp.get(key)
114
+ if tiers is not None and not isinstance(tiers, dict):
115
+ return f"`{key}` must be an object mapping skill_name -> tier"
116
+ return None
117
+
118
+
119
+ def _index(side):
120
+ """Return {normalized_key: (label, domain, verdict)} for one report side."""
121
+ out = {}
122
+ for row in side.get("coverage", []):
123
+ tech = row["technology"].strip()
124
+ out[("cov", tech.lower())] = (tech, "coverage", row["verdict"])
125
+ for row in side.get("integration", []):
126
+ a, b = row["libA"].strip(), row["libB"].strip()
127
+ pair = tuple(sorted([a.lower(), b.lower()]))
128
+ out[("int", pair)] = (f"{a} ↔ {b}", "integration", row["verdict"])
129
+ return out
130
+
131
+
132
+ def _rank(domain, verdict):
133
+ return COVERAGE_RANK.get(verdict) if domain == "coverage" else INTEGRATION_RANK.get(verdict)
134
+
135
+
136
+ def compute(inp):
137
+ """Pure delta over the two extracted finding sets."""
138
+ err = _validate(inp)
139
+ if err:
140
+ return make_error(err)
141
+
142
+ prev = _index(inp["previous"])
143
+ curr = _index(inp["current"])
144
+
145
+ buckets = {k: [] for k in ("improved", "regressed", "unchanged", "new", "dropped", "replaced")}
146
+
147
+ for key, (label, domain, verdict) in curr.items():
148
+ if key not in prev:
149
+ # Replaced-on-arrival is informational, not a regression/new gap.
150
+ if verdict == "Replaced":
151
+ buckets["replaced"].append(label)
152
+ else:
153
+ buckets["new"].append(label)
154
+ continue
155
+ _, _, prev_verdict = prev[key]
156
+ if verdict == "Replaced" or prev_verdict == "Replaced":
157
+ buckets["replaced"].append(label)
158
+ continue
159
+ pr, cr = _rank(domain, prev_verdict), _rank(domain, verdict)
160
+ if cr > pr:
161
+ buckets["improved"].append(label)
162
+ elif cr < pr:
163
+ buckets["regressed"].append(label)
164
+ else:
165
+ buckets["unchanged"].append(label)
166
+
167
+ for key, (label, _domain, verdict) in prev.items():
168
+ if key not in curr:
169
+ if verdict == "Replaced":
170
+ buckets["replaced"].append(label)
171
+ else:
172
+ buckets["dropped"].append(label)
173
+
174
+ tier_downgrades = []
175
+ prev_tiers = inp.get("previousTiers") or {}
176
+ curr_tiers = inp.get("currentTiers") or {}
177
+ for skill in sorted(set(prev_tiers) & set(curr_tiers)):
178
+ pr = TIER_RANK.get(str(prev_tiers[skill]).upper())
179
+ cr = TIER_RANK.get(str(curr_tiers[skill]).upper())
180
+ if pr is None or cr is None:
181
+ continue
182
+ if cr < pr:
183
+ tier_downgrades.append({"skill": skill, "from": prev_tiers[skill], "to": curr_tiers[skill]})
184
+
185
+ result = {}
186
+ for name, items in buckets.items():
187
+ result[name] = sorted(items)
188
+ result[name + "Count"] = len(items)
189
+ result["tierDowngrades"] = tier_downgrades
190
+ result["tierDowngradeCount"] = len(tier_downgrades)
191
+ return result
192
+
193
+
194
+ # --- CLI --------------------------------------------------------------------
195
+
196
+
197
+ def _build_parser():
198
+ parser = argparse.ArgumentParser(
199
+ prog="skf-report-delta",
200
+ description=(
201
+ "Deterministic previous-vs-current feasibility delta (synthesize.md "
202
+ "§3). Consumes the two extracted finding sets and emits improved / "
203
+ "regressed / unchanged / new / dropped counts plus tier downgrades."
204
+ ),
205
+ formatter_class=argparse.RawDescriptionHelpFormatter,
206
+ epilog=(
207
+ "Example:\n"
208
+ " uv run skf-report-delta.py "
209
+ "'{\"previous\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Missing\"}]},"
210
+ "\"current\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Covered\"}]}}'"
211
+ ),
212
+ )
213
+ src = parser.add_mutually_exclusive_group()
214
+ src.add_argument("json_input", nargs="?", help="JSON object as a positional argument.")
215
+ src.add_argument("--json-input", dest="json_input_flag", help="JSON object passed via flag.")
216
+ src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
217
+ return parser
218
+
219
+
220
+ def _resolve_input(args):
221
+ if args.stdin:
222
+ return sys.stdin.read()
223
+ if args.json_input_flag is not None:
224
+ return args.json_input_flag
225
+ if args.json_input is not None:
226
+ return args.json_input
227
+ return ""
228
+
229
+
230
+ def main(argv=None):
231
+ parser = _build_parser()
232
+ args = parser.parse_args(argv)
233
+ raw = _resolve_input(args)
234
+ if not raw.strip():
235
+ parser.print_usage(file=sys.stderr)
236
+ print(
237
+ "error: no input provided (positional arg, --json-input, or --stdin)",
238
+ file=sys.stderr,
239
+ )
240
+ return 1
241
+
242
+ try:
243
+ data = json.loads(raw)
244
+ except json.JSONDecodeError as exc:
245
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
246
+ return 1
247
+
248
+ result = compute(data)
249
+ print(json.dumps(result, indent=2))
250
+ if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
251
+ return 2
252
+ return 0
253
+
254
+
255
+ if __name__ == "__main__":
256
+ raise SystemExit(main())
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = []
5
+ # ///
6
+ """Deterministic overall-feasibility verdict rollup for skf-verify-stack (synthesize.md §1).
7
+
8
+ Deciding each individual finding — is this integration Blocked, is that technology
9
+ Missing, is a requirement Not Addressed — is judgment and happens upstream in
10
+ Steps 02-04, persisted to the report frontmatter/tables. Rolling those already-decided
11
+ counts up into the single overall verdict is *not* judgment: it is a fixed threshold
12
+ ladder with one correct answer per input. Walking a five-tier ordered ladder (with a
13
+ short-circuit, a downgrade rule, and a post-verdict guard) in-prompt lets the headline
14
+ verdict drift between runs, so the token computation lives here. The rationale prose —
15
+ which co-occurring problems to name, how to phrase the recommendation — stays in the
16
+ prompt; this script emits only the token plus the stable condition codes the prompt
17
+ cites when it writes that rationale.
18
+
19
+ The ladder (exactly mirrors synthesize.md §1; evaluate top-to-bottom, first match wins):
20
+
21
+ 1. Zero-coverage short-circuit — coveragePercentage == 0 -> NOT_FEASIBLE
22
+ (no live coverage: analysis is vacuous; the remainder of the ladder is skipped).
23
+ 2. NOT_FEASIBLE — any integration Blocked (pairsBlocked > 0).
24
+ 3. CONDITIONALLY_FEASIBLE — ANY of: a Missing technology (missingCount > 0),
25
+ a Risky integration (pairsRisky > 0), or — only when the requirements pass ran —
26
+ a Not Addressed or Partially Fulfilled requirement.
27
+ 4. FEASIBLE — none of the above AND zero pairs capped at
28
+ Plausible (pairsPlausible == 0). If any pair sits at Plausible, downgrade to
29
+ CONDITIONALLY_FEASIBLE.
30
+
31
+ Post-verdict zero-integration-pairs guard (applied after ANY verdict): when all four
32
+ integration counts are 0 AND the user continued past a step-2 zero-state [C] gate,
33
+ the guard fires — a FEASIBLE verdict is overridden to CONDITIONALLY_FEASIBLE, and
34
+ regardless of verdict the prompt appends the "no integration claims found" note.
35
+
36
+ Note that coveragePercentage and missingCount are independent inputs on purpose: half-up
37
+ rounding means a stack with covered=199, missing=1 rounds to coveragePercentage == 100
38
+ while missingCount is still > 0, so the Missing trigger reads missingCount directly.
39
+
40
+ CLI usage:
41
+ uv run skf-verdict-rollup.py '<JSON>' # JSON literal positional
42
+ uv run skf-verdict-rollup.py --json-input '<JSON>' # explicit flag form
43
+ cat input.json | uv run skf-verdict-rollup.py --stdin # piped input
44
+
45
+ Input schema (one object; counts come straight from the report frontmatter/tables):
46
+ {
47
+ "coveragePercentage": <int 0..100>, # from coverage.md (coverage-tally)
48
+ "missingCount": <int>, # Missing technologies (Replaced excluded)
49
+ "pairsBlocked": <int>, # from integrations.md
50
+ "pairsRisky": <int>,
51
+ "pairsPlausible": <int>, # includes Check-4-missing caps
52
+ "pairsVerified": <int>,
53
+ "requirementsEvaluated": <bool>, # optional (default false): requirementsPass == "completed"
54
+ "requirementsNotAddressed": <int>, # optional (default 0); ignored unless evaluated
55
+ "requirementsPartial": <int>, # optional (default 0); ignored unless evaluated
56
+ "continuedPastZeroState": <bool> # optional (default false): user pressed [C] past a step-2 zero-state gate
57
+ }
58
+
59
+ Output (stdout, one object):
60
+ {
61
+ "overallVerdict": "FEASIBLE" | "CONDITIONALLY_FEASIBLE" | "NOT_FEASIBLE",
62
+ "matchedConditions": [<condition codes, in ladder order>],
63
+ "zeroPairsGuardFired": <bool>
64
+ }
65
+
66
+ Condition codes (stable; the prompt cites these when synthesizing the rationale):
67
+ zero-coverage · blocked-integration · missing-coverage · risky-integration ·
68
+ requirements-not-addressed · requirements-partial · plausible-cap · zero-integration-pairs
69
+
70
+ Exit codes:
71
+ 0 — verdict emitted successfully
72
+ 1 — no input / input could not be parsed as JSON
73
+ 2 — input parsed but schema/semantics invalid (error object emitted as JSON)
74
+ """
75
+
76
+ from __future__ import annotations
77
+
78
+ import argparse
79
+ import json
80
+ import sys
81
+
82
+ VERDICTS = ("FEASIBLE", "CONDITIONALLY_FEASIBLE", "NOT_FEASIBLE")
83
+ REQUIRED_COUNTS = (
84
+ "missingCount",
85
+ "pairsBlocked",
86
+ "pairsRisky",
87
+ "pairsPlausible",
88
+ "pairsVerified",
89
+ )
90
+ OPTIONAL_COUNTS = ("requirementsNotAddressed", "requirementsPartial")
91
+
92
+
93
+ def make_error(message):
94
+ return {"error": message, "code": "INVALID_INPUT"}
95
+
96
+
97
+ def _is_nonneg_int(value):
98
+ # bool is a subclass of int; reject it so a stray true/false can't pose as a count.
99
+ return isinstance(value, int) and not isinstance(value, bool) and value >= 0
100
+
101
+
102
+ def _validate(inp):
103
+ if inp is None or not isinstance(inp, dict):
104
+ return "Input must be a JSON object"
105
+
106
+ pct = inp.get("coveragePercentage")
107
+ if not isinstance(pct, int) or isinstance(pct, bool) or not (0 <= pct <= 100):
108
+ return "coveragePercentage must be an integer in 0..100"
109
+
110
+ for field in REQUIRED_COUNTS:
111
+ if field not in inp:
112
+ return f"missing required field: {field}"
113
+ if not _is_nonneg_int(inp[field]):
114
+ return f"{field} must be a non-negative integer"
115
+
116
+ for field in OPTIONAL_COUNTS:
117
+ if field in inp and inp[field] is not None and not _is_nonneg_int(inp[field]):
118
+ return f"{field} must be a non-negative integer when present"
119
+
120
+ for field in ("requirementsEvaluated", "continuedPastZeroState"):
121
+ if field in inp and not isinstance(inp[field], bool):
122
+ return f"{field} must be a boolean when present"
123
+
124
+ return None
125
+
126
+
127
+ def rollup(inp):
128
+ """Pure verdict rollup over the persisted counts. See module docstring for the ladder."""
129
+ err = _validate(inp)
130
+ if err:
131
+ return make_error(err)
132
+
133
+ pct = inp["coveragePercentage"]
134
+ missing = inp["missingCount"]
135
+ blocked = inp["pairsBlocked"]
136
+ risky = inp["pairsRisky"]
137
+ plausible = inp["pairsPlausible"]
138
+ verified = inp["pairsVerified"]
139
+ req_eval = bool(inp.get("requirementsEvaluated", False))
140
+ not_addressed = inp.get("requirementsNotAddressed") or 0
141
+ partial = inp.get("requirementsPartial") or 0
142
+ continued = bool(inp.get("continuedPastZeroState", False))
143
+
144
+ matched: list[str] = []
145
+
146
+ # 1. Zero-coverage short-circuit — wins over everything else.
147
+ if pct == 0:
148
+ verdict = "NOT_FEASIBLE"
149
+ matched.append("zero-coverage")
150
+ # 2. Any Blocked integration is a fundamental incompatibility.
151
+ elif blocked > 0:
152
+ verdict = "NOT_FEASIBLE"
153
+ matched.append("blocked-integration")
154
+ # Co-occurring problems the rationale should also name (§1).
155
+ if missing > 0:
156
+ matched.append("missing-coverage")
157
+ if risky > 0:
158
+ matched.append("risky-integration")
159
+ else:
160
+ # 3. Any gap / risk / unmet requirement -> conditional.
161
+ conditional: list[str] = []
162
+ if missing > 0:
163
+ conditional.append("missing-coverage")
164
+ if risky > 0:
165
+ conditional.append("risky-integration")
166
+ if req_eval and not_addressed > 0:
167
+ conditional.append("requirements-not-addressed")
168
+ if req_eval and partial > 0:
169
+ conditional.append("requirements-partial")
170
+ if conditional:
171
+ verdict = "CONDITIONALLY_FEASIBLE"
172
+ matched.extend(conditional)
173
+ elif plausible > 0:
174
+ # 4. Clean bar except for Check-4-missing caps -> downgrade.
175
+ verdict = "CONDITIONALLY_FEASIBLE"
176
+ matched.append("plausible-cap")
177
+ else:
178
+ verdict = "FEASIBLE"
179
+
180
+ # Post-verdict zero-integration-pairs guard.
181
+ zero_pairs = blocked == 0 and risky == 0 and plausible == 0 and verified == 0
182
+ guard_fired = zero_pairs and continued
183
+ if guard_fired:
184
+ if verdict == "FEASIBLE":
185
+ verdict = "CONDITIONALLY_FEASIBLE"
186
+ matched.append("zero-integration-pairs")
187
+
188
+ return {
189
+ "overallVerdict": verdict,
190
+ "matchedConditions": matched,
191
+ "zeroPairsGuardFired": guard_fired,
192
+ }
193
+
194
+
195
+ # --- CLI --------------------------------------------------------------------
196
+
197
+
198
+ def _build_parser():
199
+ parser = argparse.ArgumentParser(
200
+ prog="skf-verdict-rollup",
201
+ description=(
202
+ "Deterministic overall-feasibility verdict rollup (synthesize.md §1). "
203
+ "Consumes the persisted coverage / integration / requirements counts and "
204
+ "emits the FEASIBLE / CONDITIONALLY_FEASIBLE / NOT_FEASIBLE token plus the "
205
+ "condition codes the prompt cites in its rationale."
206
+ ),
207
+ formatter_class=argparse.RawDescriptionHelpFormatter,
208
+ epilog=(
209
+ "Example:\n"
210
+ " uv run skf-verdict-rollup.py "
211
+ "'{\"coveragePercentage\":100,\"missingCount\":0,\"pairsBlocked\":0,"
212
+ "\"pairsRisky\":0,\"pairsPlausible\":0,\"pairsVerified\":3}'"
213
+ ),
214
+ )
215
+ src = parser.add_mutually_exclusive_group()
216
+ src.add_argument("json_input", nargs="?", help="JSON object as a positional argument.")
217
+ src.add_argument("--json-input", dest="json_input_flag", help="JSON object passed via flag.")
218
+ src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
219
+ return parser
220
+
221
+
222
+ def _resolve_input(args):
223
+ if args.stdin:
224
+ return sys.stdin.read()
225
+ if args.json_input_flag is not None:
226
+ return args.json_input_flag
227
+ if args.json_input is not None:
228
+ return args.json_input
229
+ return ""
230
+
231
+
232
+ def main(argv=None):
233
+ parser = _build_parser()
234
+ args = parser.parse_args(argv)
235
+ raw = _resolve_input(args)
236
+ if not raw.strip():
237
+ parser.print_usage(file=sys.stderr)
238
+ print(
239
+ "error: no input provided (positional arg, --json-input, or --stdin)",
240
+ file=sys.stderr,
241
+ )
242
+ return 1
243
+
244
+ try:
245
+ data = json.loads(raw)
246
+ except json.JSONDecodeError as exc:
247
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
248
+ return 1
249
+
250
+ result = rollup(data)
251
+ print(json.dumps(result, indent=2))
252
+ if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
253
+ return 2
254
+ return 0
255
+
256
+
257
+ if __name__ == "__main__":
258
+ raise SystemExit(main())
@@ -1,110 +0,0 @@
1
- ---
2
- # Static reference loaded by execute.md §7. The carve removes ~70
3
- # lines of context-rebuild loop logic from the main execute.md so
4
- # the §1–§6 + §8–§10 critical-path stays tight; §7 itself is always
5
- # reached on the success path so this file loads on every successful
6
- # rename, but the rollback paths (§4–§6 failure jumps) skip §7
7
- # entirely and never load this reference.
8
- ---
9
-
10
- <!-- Config: communicate in {communication_language}. Render the rebuilt-files report in {document_output_language}. -->
11
-
12
- # Rename: Rebuild Context Files (per-IDE managed-section sweep)
13
-
14
- ## Purpose
15
-
16
- After §6 re-keys the manifest from `{old_name}` to `{new_name}`, every IDE's context file (`CLAUDE.md`, `.cursorrules`, `AGENTS.md`, etc.) still contains the old name in its managed-section snippet rows. This step rewrites each one in-place via the surgical between-marker swap so the on-disk managed sections reflect the new name.
17
-
18
- Loaded by `execute.md` §7 once `manifest_updated == true` (§6 succeeded) and the rollback paths in §4–§6 were not taken.
19
-
20
- ## Resolve target_context_files
21
-
22
- Load the `ides` list from `config.yaml`. The installer writes IDE identifiers — these must be mapped to context files and skill roots using the "IDE → Context File Mapping" table in `{managedSectionLogic}`:
23
-
24
- 1. For each entry in `config.yaml.ides`, look up its `context_file` and `skill_root` from the mapping table.
25
- 2. For any entry not found in the table, default to `{unknownIdeDefaultContextFile}` / `{unknownIdeDefaultSkillRoot}` (resolved by SKILL.md On-Activation §3 from `customize.toml`; bundled defaults `AGENTS.md` / `.agents/skills/`) and emit a warning: `Unknown IDE '{value}' in config.yaml — defaulting to {unknownIdeDefaultContextFile}`.
26
- 3. Deduplicate by `context_file` — when multiple IDEs map to the same context file, use the first configured IDE's `skill_root`.
27
- 4. If `config.yaml.ides` is absent or the mapping yields an empty list, fall back to `[{context_file: "{unknownIdeDefaultContextFile}", skill_root: "{unknownIdeDefaultSkillRoot}"}]` and emit a note: `No IDEs configured in config.yaml — defaulting to {unknownIdeDefaultContextFile}`.
28
-
29
- Store the result as `target_context_files`.
30
-
31
- ## Per-file loop
32
-
33
- For each entry in `target_context_files`:
34
-
35
- ### 1. Resolve target file
36
-
37
- Resolve the absolute path at `{context_file}`.
38
-
39
- ### 2. Read the current file
40
-
41
- - If the file does not exist, skip this context file (nothing to rebuild — the file will be re-created the next time export-skill runs).
42
- - If the file exists but contains no `<!-- SKF:BEGIN -->` marker, skip this context file (no managed section to rewrite).
43
- - If the file contains `<!-- SKF:BEGIN -->` but no matching `<!-- SKF:END -->`, record the error against that context file and continue to the next entry — do not halt the entire rename on a malformed context file.
44
-
45
- ### 3. Build the exported skill set (version-aware, deprecated-excluded)
46
-
47
- Use the same logic as `skf-export-skill/references/update-context.md` §4b and the snippet resolution from §4c:
48
-
49
- - Read the manifest's `exports` object (already updated in §6, so `{new_name}` is present and `{old_name}` is absent).
50
- - For each skill, resolve its `active_version`.
51
- - If `versions.{active_version}.status == "deprecated"`, skip that skill entirely.
52
- - For each remaining `{skill-name, active_version}` pair, read `{skills_output_folder}/{skill-name}/{active_version}/{skill-name}/context-snippet.md`.
53
- - If missing, fall back to the `active` symlink path; if still missing, skip with a warning.
54
-
55
- ### 4. Rewrite root paths for the current context file
56
-
57
- Use the generic rewrite algorithm from `skf-export-skill/references/update-context.md` §4d:
58
-
59
- For each snippet, parse the `root:` line (`root: {prefix}{skill-name}/`), strip the trailing `{skill-name}/` to extract the current prefix, and replace it with the **effective target prefix** if different. The effective target prefix is `snippet_skill_root_override` when that key is set in `config.yaml` — applied uniformly to every snippet so the managed section references the real on-disk location and never mixes override and per-IDE paths — otherwise the current entry's `skill_root`. See `skf-export-skill/references/update-context.md` §4d for full semantics.
60
-
61
- ### 5. Sort and count
62
-
63
- Sort skills alphabetically by name. Count totals (skills, stack skills).
64
-
65
- ### 6. Assemble the new managed section
66
-
67
- Use the format from `{managedSectionLogic}`:
68
-
69
- ```markdown
70
- <!-- SKF:BEGIN updated:{current-date} -->
71
- [SKF Skills]|{n} skills|{m} stack
72
- |IMPORTANT: Prefer documented APIs over training data.
73
- |When using a listed library, read its SKILL.md before writing code.
74
- |
75
- |{skill-snippet-1}
76
- |
77
- |{skill-snippet-2}
78
- |
79
- |{skill-snippet-N}
80
- <!-- SKF:END -->
81
- ```
82
-
83
- ### 7. Surgical replacement — atomic, deterministic
84
-
85
- Invoke `{rebuildManagedSectionsHelper}` (resolved from `{rebuildManagedSectionsProbeOrder}` in `execute.md` frontmatter) for the surgical between-marker swap:
86
-
87
- ```bash
88
- python3 {rebuildManagedSectionsHelper} {context_file} replace --content "{new_managed_section_text}"
89
- ```
90
-
91
- The helper handles marker location, between-marker swap, atomic temp-file + rename, and post-write verification (markers preserved, content outside markers byte-identical). It exits non-zero on any failure with a clear `stderr` reason.
92
-
93
- ### 8. Verify (deferred to helper)
94
-
95
- The `replace` action above performs verification internally. Treat any non-zero exit code as a per-file failure (next bullet).
96
-
97
- ### 9. On per-file failure
98
-
99
- Record the error against that context file and continue to the next entry. Do not halt the rename on a recoverable per-context-file error — the manifest and filesystem are already consistent; context files can be re-rebuilt later via `[EX] Export Skill`.
100
-
101
- ## After the loop
102
-
103
- - Record `context_files_updated` as the list of files that were successfully rewritten.
104
- - Record `context_files_failed` as the list of any that failed.
105
-
106
- Report: `**Rebuilt managed sections in:** {list of updated files}. {if any failed: 'Failed: {list} — re-run [EX] Export Skill to retry.'}`
107
-
108
- ## Rollback semantics
109
-
110
- §7 failures **do not** trigger a rollback. The manifest and filesystem are the canonical state — context files are derived artifacts that can be regenerated at any time via `[EX] Export Skill`. The rename is considered successful as soon as §6 lands, even if §7 partially or fully fails to rewrite context files.