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,499 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = []
5
+ # ///
6
+ """SKF Render Metadata Stats — derive metadata.json's computed stats block and
7
+ confidence_distribution deterministically from the staged provenance-map.
8
+
9
+ Two places in `skf-create-skill` do the same pure arithmetic over the staged
10
+ staging JSON, by hand, every run:
11
+
12
+ 1. **compile.md §4 (Build metadata.json)** bins each provenance entry by its
13
+ `signature_source` tier into `confidence_distribution.{t1,t1_low,t2,t3}`,
14
+ counts documented exports, and computes the coverage ratios. This is the
15
+ step that documents the LLM's recurring miscount — binning ~8 T2
16
+ annotations + ~80 T3 doc items *on top of* 59 exports and getting a
17
+ distribution that sums to 147 ≠ 59.
18
+
19
+ 2. **validate.md §7 (Validate metadata.json)** re-derives the same sums,
20
+ ratios, and array-length counts to auto-fix any drift ("these are
21
+ computed values").
22
+
23
+ Both are pure projections over the provenance-map plus a small judgment
24
+ payload — the values the prompt genuinely decides (which count feeds
25
+ `exports_public_api`, the gated `effective_denominator`). This script owns the
26
+ arithmetic so the prompt stops doing it by hand; the judgment stays in the
27
+ prompt and is passed in.
28
+
29
+ Because the distribution is a `Counter` over `entries[]` — one entry counted
30
+ once by its tier — the script **cannot** reproduce the 147≠59 miscount: the
31
+ four bins always sum to the number of entries with a recognized
32
+ `signature_source`, never to an enrichment total.
33
+
34
+ Shape carve-outs (mirroring `skf-test-skill` coverage-check §4b): the
35
+ distribution ALWAYS bins `entries[]`, so it always sums to `len(entries)`. What
36
+ `exports_documented` equals is what differs by shape:
37
+
38
+ - **library** — `exports_documented = len(entries)`; the four bins sum to it.
39
+ - **reference-app** — `entries[]` are per-citation, so the distribution sums
40
+ to the *citation count* (= `len(entries)`), NOT to `exports_documented`
41
+ (which is the pattern-surface proxy `pattern_surfaces_documented`, a
42
+ judgment value). `effective_denominator` is never emitted.
43
+ - **stack** — `entries[]` are the cited constituent contracts, so the
44
+ distribution sums to the *constituent count* (= `len(entries)`), NOT to
45
+ `exports_documented` (a stack's own barrel is empty by design).
46
+
47
+ Modes
48
+ -----
49
+
50
+ Default (compute) — read the provenance-map path + a judgment payload on stdin;
51
+ emit the derived `stats` + `confidence_distribution` + a `coherence` report.
52
+ `compile.md §4` writes the returned values verbatim.
53
+
54
+ `--check <metadata.json>` — read the provenance-map path + the on-disk
55
+ metadata.json; re-derive the computed values (taking the judgment values —
56
+ `exports_public_api`, `exports_internal`, `effective_denominator` — from the
57
+ on-disk metadata itself) and compare against what is on disk. `coherence.ok`
58
+ is false with a `violations[]` list when the on-disk file drifted; the emitted
59
+ `stats`/`confidence_distribution` are the corrected values `validate.md §7`
60
+ writes back.
61
+
62
+ Judgment payload (stdin JSON, compute mode)::
63
+
64
+ {
65
+ "exports_public_api": 40, # int — public-entry-point export count (judgment)
66
+ "exports_internal": 19, # int — non-underscore internal export count (judgment)
67
+ "effective_denominator": 12, # int|null — optional, stratified-scope monorepo only
68
+ "pattern_surfaces_documented": 8, # int — reference-app only (exports_documented proxy)
69
+ "exports_documented": 0, # int — stack only (own-barrel count, usually 0)
70
+ "scripts": [ ... ], # list — scripts[] array (or "scripts_count": int)
71
+ "assets": [ ... ] # list — assets[] array (or "assets_count": int)
72
+ }
73
+
74
+ Output JSON (both modes)::
75
+
76
+ {
77
+ "stats": {
78
+ "exports_documented": 59,
79
+ "exports_public_api": 40,
80
+ "exports_internal": 19,
81
+ "exports_total": 59,
82
+ "public_api_coverage": 1.475, # documented/public_api, null if public_api == 0
83
+ "total_coverage": 1.0, # documented/total, null if total == 0
84
+ "scripts_count": 0,
85
+ "assets_count": 0
86
+ },
87
+ "confidence_distribution": {"t1": 40, "t1_low": 5, "t2": 10, "t3": 4},
88
+ "coherence": {"ok": true, "violations": []}
89
+ }
90
+
91
+ Exit codes:
92
+ 0 — coherence ok (no violations)
93
+ 1 — coherence violations present (drift to auto-fix / bad signature_source);
94
+ the JSON on stdout is still authoritative — rely on it, not the code
95
+ 2 — usage / IO / JSON-decode error
96
+ """
97
+
98
+ from __future__ import annotations
99
+
100
+ import argparse
101
+ import json
102
+ import sys
103
+ from pathlib import Path
104
+
105
+
106
+ # Canonical signature_source tiers → distribution bin keys. Compared
107
+ # case-insensitively so "T1-low" / "t1-low" both land in t1_low.
108
+ _SIG_MAP = {"T1": "t1", "T1-LOW": "t1_low", "T2": "t2", "T3": "t3"}
109
+ _VALID_SHAPES = ("library", "stack", "reference-app")
110
+ _DIST_KEYS = ("t1", "t1_low", "t2", "t3")
111
+
112
+
113
+ # --------------------------------------------------------------------------
114
+ # IO
115
+ # --------------------------------------------------------------------------
116
+
117
+
118
+ def load_json(path: Path) -> object:
119
+ """Read + parse a JSON file. Raises ValueError on any failure."""
120
+ try:
121
+ text = path.read_text(encoding="utf-8")
122
+ except OSError as exc:
123
+ raise ValueError(f"failed to read {path}: {exc}") from exc
124
+ try:
125
+ return json.loads(text)
126
+ except json.JSONDecodeError as exc:
127
+ raise ValueError(f"malformed JSON in {path}: {exc}") from exc
128
+
129
+
130
+ # --------------------------------------------------------------------------
131
+ # Pure derivations
132
+ # --------------------------------------------------------------------------
133
+
134
+
135
+ def bin_signature_sources(entries: list) -> tuple[dict[str, int], int]:
136
+ """Counter over provenance entries by signature_source tier.
137
+
138
+ Returns (distribution, unrecognized) where distribution has the four
139
+ canonical bin keys and `unrecognized` counts entries whose
140
+ `signature_source` is missing or not one of T1/T1-low/T2/T3. Counting
141
+ entries (each exactly once, by its own tier) is what makes the 147≠59
142
+ enrichment-fold miscount structurally impossible.
143
+ """
144
+ dist = {k: 0 for k in _DIST_KEYS}
145
+ unrecognized = 0
146
+ if not isinstance(entries, list):
147
+ return dist, unrecognized
148
+ for entry in entries:
149
+ raw = entry.get("signature_source") if isinstance(entry, dict) else None
150
+ key = _SIG_MAP.get(raw.strip().upper()) if isinstance(raw, str) else None
151
+ if key is not None:
152
+ dist[key] += 1
153
+ else:
154
+ unrecognized += 1
155
+ return dist, unrecognized
156
+
157
+
158
+ def ratio(numerator: int, denominator: int) -> float | None:
159
+ """Coverage ratio; None when the denominator is 0 (per the spec's
160
+ null-when-empty rule). Rounded to 4 dp for deterministic output."""
161
+ if denominator == 0:
162
+ return None
163
+ return round(numerator / denominator, 4)
164
+
165
+
166
+ def _int(value: object, default: int = 0) -> int:
167
+ """Coerce a judgment value to int, tolerating None / bad types."""
168
+ if isinstance(value, bool):
169
+ return int(value)
170
+ if isinstance(value, (int, float)):
171
+ return int(value)
172
+ if isinstance(value, str):
173
+ try:
174
+ return int(value.strip())
175
+ except ValueError:
176
+ return default
177
+ return default
178
+
179
+
180
+ def _count(judgment: dict, arr_key: str, count_key: str) -> int:
181
+ """Length of an array field if present, else the scalar count field."""
182
+ arr = judgment.get(arr_key)
183
+ if isinstance(arr, list):
184
+ return len(arr)
185
+ return _int(judgment.get(count_key), 0)
186
+
187
+
188
+ def count_file_entries_by_type(prov: dict) -> dict[str, int]:
189
+ """Count provenance-map file_entries[] by file_type."""
190
+ counts = {"script": 0, "asset": 0, "doc": 0, "other": 0}
191
+ fe = prov.get("file_entries")
192
+ if isinstance(fe, list):
193
+ for row in fe:
194
+ if not isinstance(row, dict):
195
+ counts["other"] += 1
196
+ continue
197
+ ft = row.get("file_type")
198
+ counts[ft if ft in ("script", "asset", "doc") else "other"] += 1
199
+ return counts
200
+
201
+
202
+ def derive_stats(prov: dict, judgment: dict, shape: str = "library") -> dict:
203
+ """Derive the stats block + confidence_distribution for one skill.
204
+
205
+ `prov` is the parsed provenance-map. `judgment` carries the values the
206
+ prompt decides. `shape` selects how `exports_documented` is set. The
207
+ distribution always bins `entries[]`.
208
+ """
209
+ entries = prov.get("entries")
210
+ entries = entries if isinstance(entries, list) else []
211
+ dist, unrecognized = bin_signature_sources(entries)
212
+ entry_count = len(entries)
213
+
214
+ if shape == "reference-app":
215
+ exports_documented = _int(judgment.get("pattern_surfaces_documented"), 0)
216
+ elif shape == "stack":
217
+ exports_documented = _int(judgment.get("exports_documented"), 0)
218
+ else: # library
219
+ exports_documented = entry_count
220
+
221
+ exports_public_api = _int(judgment.get("exports_public_api"), 0)
222
+ exports_internal = _int(judgment.get("exports_internal"), 0)
223
+ exports_total = exports_public_api + exports_internal
224
+
225
+ stats: dict[str, object] = {
226
+ "exports_documented": exports_documented,
227
+ "exports_public_api": exports_public_api,
228
+ "exports_internal": exports_internal,
229
+ "exports_total": exports_total,
230
+ "public_api_coverage": ratio(exports_documented, exports_public_api),
231
+ "total_coverage": ratio(exports_documented, exports_total),
232
+ "scripts_count": _count(judgment, "scripts", "scripts_count"),
233
+ "assets_count": _count(judgment, "assets", "assets_count"),
234
+ }
235
+
236
+ # reference-app: emit the pattern-surface proxy alongside exports_documented
237
+ if shape == "reference-app":
238
+ stats["pattern_surfaces_documented"] = exports_documented
239
+
240
+ # effective_denominator: optional, and never for a reference app (its
241
+ # coverage basis is pattern surfaces, not an export barrel).
242
+ eff = judgment.get("effective_denominator")
243
+ if eff is not None and shape != "reference-app":
244
+ stats["effective_denominator"] = _int(eff, 0)
245
+
246
+ return {
247
+ "stats": stats,
248
+ "confidence_distribution": dist,
249
+ "_derivation": {
250
+ "shape": shape,
251
+ "entry_count": entry_count,
252
+ "distribution_sum": sum(dist.values()),
253
+ "unrecognized_signature_source": unrecognized,
254
+ },
255
+ }
256
+
257
+
258
+ # --------------------------------------------------------------------------
259
+ # Coherence
260
+ # --------------------------------------------------------------------------
261
+
262
+
263
+ def _cmp_int(violations: list, field: str, expected, actual, note: str | None = None) -> None:
264
+ if expected != actual:
265
+ v = {"field": field, "expected": expected, "actual": actual}
266
+ if note:
267
+ v["note"] = note
268
+ violations.append(v)
269
+
270
+
271
+ def _cmp_ratio(violations: list, field: str, expected, actual) -> None:
272
+ norm = round(float(actual), 4) if isinstance(actual, (int, float)) and not isinstance(actual, bool) else actual
273
+ exp = round(expected, 4) if isinstance(expected, float) else expected
274
+ if exp != norm:
275
+ violations.append({"field": field, "expected": expected, "actual": actual})
276
+
277
+
278
+ def coherence_compute(derived: dict, prov: dict) -> dict:
279
+ """Internal-consistency checks for compute mode.
280
+
281
+ The distribution must account for every provenance entry (a bin-sum below
282
+ the entry count means some entries carry a missing/unrecognized
283
+ signature_source). When the provenance-map already tracks scripts/assets in
284
+ file_entries[], those counts must agree with the stats counts.
285
+ """
286
+ violations: list[dict] = []
287
+ entry_count = derived["_derivation"]["entry_count"]
288
+ dist_sum = derived["_derivation"]["distribution_sum"]
289
+ if dist_sum != entry_count:
290
+ missing = entry_count - dist_sum
291
+ violations.append({
292
+ "field": "confidence_distribution",
293
+ "expected": entry_count,
294
+ "actual": dist_sum,
295
+ "note": (
296
+ f"{missing} provenance entr{'y' if abs(missing) == 1 else 'ies'} "
297
+ "with missing or unrecognized signature_source"
298
+ ),
299
+ })
300
+
301
+ fe = count_file_entries_by_type(prov)
302
+ sc = derived["stats"]["scripts_count"]
303
+ ac = derived["stats"]["assets_count"]
304
+ # Conservative: only cross-check a type once the provenance-map actually
305
+ # carries rows of that type (file_entries are written in a later compile
306
+ # section, so an empty count is "not yet populated", not "zero").
307
+ if fe["script"] > 0:
308
+ _cmp_int(violations, "stats.scripts_count", fe["script"], sc,
309
+ "stats.scripts_count disagrees with provenance-map file_entries[file_type=script]")
310
+ if fe["asset"] > 0:
311
+ _cmp_int(violations, "stats.assets_count", fe["asset"], ac,
312
+ "stats.assets_count disagrees with provenance-map file_entries[file_type=asset]")
313
+
314
+ return {"ok": not violations, "violations": violations}
315
+
316
+
317
+ def check_stats(derived: dict, metadata: dict, prov: dict) -> dict:
318
+ """Compare re-derived values against the on-disk metadata.json.
319
+
320
+ Every mismatch is a `{field, expected, actual}` violation, where `expected`
321
+ is the re-derived (correct) value and `actual` is what is on disk.
322
+ validate.md §7 either writes `derived` verbatim or fixes each violation.
323
+ """
324
+ violations: list[dict] = []
325
+ m_stats = metadata.get("stats") if isinstance(metadata.get("stats"), dict) else {}
326
+ d_stats = derived["stats"]
327
+
328
+ _cmp_int(violations, "stats.exports_documented", d_stats["exports_documented"],
329
+ m_stats.get("exports_documented"))
330
+ _cmp_int(violations, "stats.exports_total", d_stats["exports_total"],
331
+ m_stats.get("exports_total"))
332
+ _cmp_ratio(violations, "stats.public_api_coverage", d_stats["public_api_coverage"],
333
+ m_stats.get("public_api_coverage"))
334
+ _cmp_ratio(violations, "stats.total_coverage", d_stats["total_coverage"],
335
+ m_stats.get("total_coverage"))
336
+
337
+ m_dist = metadata.get("confidence_distribution") if isinstance(metadata.get("confidence_distribution"), dict) else {}
338
+ for key in _DIST_KEYS:
339
+ _cmp_int(violations, f"confidence_distribution.{key}",
340
+ derived["confidence_distribution"][key], m_dist.get(key))
341
+
342
+ # array length vs its stats count (validate.md §7: "verify
343
+ # stats.scripts_count/stats.assets_count match array lengths")
344
+ scripts_arr = metadata.get("scripts")
345
+ if isinstance(scripts_arr, list):
346
+ _cmp_int(violations, "stats.scripts_count", len(scripts_arr),
347
+ m_stats.get("scripts_count"),
348
+ "stats.scripts_count does not match len(scripts[])")
349
+ assets_arr = metadata.get("assets")
350
+ if isinstance(assets_arr, list):
351
+ _cmp_int(violations, "stats.assets_count", len(assets_arr),
352
+ m_stats.get("assets_count"),
353
+ "stats.assets_count does not match len(assets[])")
354
+
355
+ # provenance-map file_entries count vs metadata scripts/assets counts
356
+ if isinstance(scripts_arr, list) or isinstance(assets_arr, list):
357
+ fe = count_file_entries_by_type(prov)
358
+ claimed_sc = _int(m_stats.get("scripts_count"), 0)
359
+ claimed_ac = _int(m_stats.get("assets_count"), 0)
360
+ if fe["script"] != claimed_sc and (fe["script"] > 0 or claimed_sc > 0):
361
+ _cmp_int(violations, "provenance.file_entries.script", claimed_sc, fe["script"],
362
+ "provenance-map file_entries[file_type=script] count disagrees with metadata scripts_count")
363
+ if fe["asset"] != claimed_ac and (fe["asset"] > 0 or claimed_ac > 0):
364
+ _cmp_int(violations, "provenance.file_entries.asset", claimed_ac, fe["asset"],
365
+ "provenance-map file_entries[file_type=asset] count disagrees with metadata assets_count")
366
+
367
+ return {"ok": not violations, "violations": violations}
368
+
369
+
370
+ def detect_shape(metadata: dict) -> str:
371
+ """Infer the shape from an on-disk metadata.json (used by --check when
372
+ --shape is not supplied)."""
373
+ if metadata.get("scope_type") == "reference-app":
374
+ return "reference-app"
375
+ if metadata.get("skill_type") == "stack":
376
+ return "stack"
377
+ return "library"
378
+
379
+
380
+ # --------------------------------------------------------------------------
381
+ # CLI
382
+ # --------------------------------------------------------------------------
383
+
384
+
385
+ def _read_stdin_judgment() -> dict:
386
+ raw = sys.stdin.read() if not sys.stdin.isatty() else ""
387
+ if not raw.strip():
388
+ return {}
389
+ payload = json.loads(raw)
390
+ if not isinstance(payload, dict):
391
+ raise ValueError("stdin judgment payload must be a JSON object")
392
+ return payload
393
+
394
+
395
+ def _emit(result: dict, out_path: str | None) -> None:
396
+ text = json.dumps(result, indent=2)
397
+ if out_path:
398
+ Path(out_path).write_text(text + "\n", encoding="utf-8")
399
+ else:
400
+ sys.stdout.write(text + "\n")
401
+
402
+
403
+ def build_parser() -> argparse.ArgumentParser:
404
+ parser = argparse.ArgumentParser(
405
+ prog="skf-render-metadata-stats",
406
+ description=(
407
+ "Derive metadata.json stats + confidence_distribution from the staged "
408
+ "provenance-map (compute mode), or re-verify an on-disk metadata.json "
409
+ "against the map (--check). Replaces the hand-binning/summing in "
410
+ "skf-create-skill compile.md §4 and validate.md §7."
411
+ ),
412
+ )
413
+ parser.add_argument("provenance_map", help="path to the staged provenance-map.json")
414
+ parser.add_argument(
415
+ "--check", metavar="METADATA_JSON", dest="check",
416
+ help="verify this on-disk metadata.json against the provenance-map instead of "
417
+ "reading judgment from stdin; emits corrected stats + a violations list",
418
+ )
419
+ parser.add_argument(
420
+ "--shape", choices=_VALID_SHAPES, default=None,
421
+ help="assembly shape (default: library in compute mode; inferred from "
422
+ "metadata.json in --check mode). Selects what exports_documented equals.",
423
+ )
424
+ parser.add_argument("-o", "--output", dest="output", help="write JSON here (default: stdout)")
425
+ parser.add_argument("--verbose", action="store_true", help="diagnostics to stderr")
426
+ return parser
427
+
428
+
429
+ def main(argv: list[str] | None = None) -> int:
430
+ args = build_parser().parse_args(argv)
431
+
432
+ prov_path = Path(args.provenance_map)
433
+ if not prov_path.is_file():
434
+ sys.stderr.write(f"error: provenance-map not found: {prov_path}\n")
435
+ return 2
436
+ try:
437
+ prov = load_json(prov_path)
438
+ except ValueError as exc:
439
+ sys.stderr.write(f"error: {exc}\n")
440
+ return 2
441
+ if not isinstance(prov, dict):
442
+ sys.stderr.write("error: provenance-map must be a JSON object at top level\n")
443
+ return 2
444
+
445
+ if args.check:
446
+ meta_path = Path(args.check)
447
+ if not meta_path.is_file():
448
+ sys.stderr.write(f"error: metadata.json not found: {meta_path}\n")
449
+ return 2
450
+ try:
451
+ metadata = load_json(meta_path)
452
+ except ValueError as exc:
453
+ sys.stderr.write(f"error: {exc}\n")
454
+ return 2
455
+ if not isinstance(metadata, dict):
456
+ sys.stderr.write("error: metadata.json must be a JSON object at top level\n")
457
+ return 2
458
+ shape = args.shape or detect_shape(metadata)
459
+ m_stats = metadata.get("stats") if isinstance(metadata.get("stats"), dict) else {}
460
+ judgment = {
461
+ "exports_public_api": m_stats.get("exports_public_api"),
462
+ "exports_internal": m_stats.get("exports_internal"),
463
+ "effective_denominator": m_stats.get("effective_denominator"),
464
+ "pattern_surfaces_documented": m_stats.get("pattern_surfaces_documented"),
465
+ "exports_documented": m_stats.get("exports_documented"),
466
+ "scripts": metadata.get("scripts"),
467
+ "assets": metadata.get("assets"),
468
+ }
469
+ derived = derive_stats(prov, judgment, shape)
470
+ coherence = check_stats(derived, metadata, prov)
471
+ else:
472
+ try:
473
+ judgment = _read_stdin_judgment()
474
+ except (ValueError, json.JSONDecodeError) as exc:
475
+ sys.stderr.write(f"error: {exc}\n")
476
+ return 2
477
+ shape = args.shape or "library"
478
+ derived = derive_stats(prov, judgment, shape)
479
+ coherence = coherence_compute(derived, prov)
480
+
481
+ if args.verbose:
482
+ sys.stderr.write(
483
+ f"shape={shape} entries={derived['_derivation']['entry_count']} "
484
+ f"dist_sum={derived['_derivation']['distribution_sum']} "
485
+ f"unrecognized={derived['_derivation']['unrecognized_signature_source']} "
486
+ f"violations={len(coherence['violations'])}\n"
487
+ )
488
+
489
+ result = {
490
+ "stats": derived["stats"],
491
+ "confidence_distribution": derived["confidence_distribution"],
492
+ "coherence": coherence,
493
+ }
494
+ _emit(result, args.output)
495
+ return 0 if coherence["ok"] else 1
496
+
497
+
498
+ if __name__ == "__main__":
499
+ raise SystemExit(main())