bmad-module-skill-forge 1.9.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.nvmrc +1 -1
  3. package/README.md +10 -5
  4. package/docs/_data/pinned.yaml +2 -2
  5. package/docs/agents.md +11 -2
  6. package/docs/architecture.md +5 -4
  7. package/docs/bmad-synergy.md +30 -7
  8. package/docs/campaign.md +172 -0
  9. package/docs/examples.md +7 -3
  10. package/docs/forge-auto.md +90 -0
  11. package/docs/getting-started.md +9 -0
  12. package/docs/how-it-works.md +6 -4
  13. package/docs/index.md +4 -3
  14. package/docs/skill-model.md +1 -1
  15. package/docs/troubleshooting.md +17 -1
  16. package/docs/verifying-a-skill.md +2 -2
  17. package/docs/why-skf.md +1 -1
  18. package/docs/workflows.md +64 -24
  19. package/package.json +2 -2
  20. package/src/knowledge/skill-lifecycle.md +1 -1
  21. package/src/module-help.csv +4 -3
  22. package/src/shared/data/language-corpora.json +32 -0
  23. package/src/shared/references/pipeline-contracts.md +6 -3
  24. package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
  25. package/src/shared/scripts/skf-comention-pairs.py +350 -0
  26. package/src/shared/scripts/skf-count-tokens.py +222 -0
  27. package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
  28. package/src/shared/scripts/skf-detect-docs.py +240 -4
  29. package/src/shared/scripts/skf-detect-language.py +82 -3
  30. package/src/shared/scripts/skf-detect-tools.py +105 -2
  31. package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
  32. package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
  33. package/src/shared/scripts/skf-extract-public-api.py +53 -0
  34. package/src/shared/scripts/skf-find-cycles.py +246 -0
  35. package/src/shared/scripts/skf-hash-content.py +487 -2
  36. package/src/shared/scripts/skf-language-corpora.py +100 -0
  37. package/src/shared/scripts/skf-manifest-ops.py +67 -1
  38. package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
  39. package/src/shared/scripts/skf-preapply.py +1 -1
  40. package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
  41. package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
  42. package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
  43. package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
  44. package/src/shared/scripts/skf-shape-detect.py +576 -25
  45. package/src/shared/scripts/skf-shard-body.py +461 -0
  46. package/src/shared/scripts/skf-skill-inventory.py +139 -3
  47. package/src/shared/scripts/skf-structural-diff.py +307 -67
  48. package/src/shared/scripts/skf-validate-brief-schema.py +41 -6
  49. package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
  50. package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
  51. package/src/shared/scripts/skf-validate-output.py +443 -10
  52. package/src/shared/scripts/skf-validate-pins.py +3 -3
  53. package/src/shared/scripts/skf-verify-no-trace.py +214 -0
  54. package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
  55. package/src/shared/scripts/skf-write-skill-brief.py +21 -5
  56. package/src/skf-analyze-source/SKILL.md +17 -32
  57. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  58. package/src/skf-analyze-source/customize.toml +2 -0
  59. package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
  60. package/src/skf-analyze-source/references/continue.md +8 -13
  61. package/src/skf-analyze-source/references/generate-briefs.md +28 -12
  62. package/src/skf-analyze-source/references/headless-contract.md +33 -0
  63. package/src/skf-analyze-source/references/health-check.md +1 -2
  64. package/src/skf-analyze-source/references/identify-units.md +25 -36
  65. package/src/skf-analyze-source/references/init.md +12 -25
  66. package/src/skf-analyze-source/references/map-and-detect.md +8 -20
  67. package/src/skf-analyze-source/references/recommend.md +4 -14
  68. package/src/skf-analyze-source/references/scan-project.md +3 -11
  69. package/src/skf-analyze-source/references/step-auto-scope.md +211 -197
  70. package/src/skf-analyze-source/references/step-shape-detect.md +20 -13
  71. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
  72. package/src/skf-audit-skill/SKILL.md +9 -6
  73. package/src/skf-audit-skill/customize.toml +2 -0
  74. package/src/skf-audit-skill/references/init.md +58 -32
  75. package/src/skf-audit-skill/references/re-index.md +15 -34
  76. package/src/skf-audit-skill/references/report.md +11 -58
  77. package/src/skf-audit-skill/references/semantic-diff.md +4 -22
  78. package/src/skf-audit-skill/references/severity-classify.md +53 -87
  79. package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
  80. package/src/skf-audit-skill/references/structural-diff.md +43 -68
  81. package/src/skf-brief-skill/SKILL.md +7 -36
  82. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  83. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
  84. package/src/skf-brief-skill/customize.toml +14 -0
  85. package/src/skf-brief-skill/references/analyze-target.md +13 -18
  86. package/src/skf-brief-skill/references/confirm-brief.md +2 -15
  87. package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
  88. package/src/skf-brief-skill/references/gather-intent.md +27 -24
  89. package/src/skf-brief-skill/references/headless-args.md +1 -1
  90. package/src/skf-brief-skill/references/health-check.md +1 -22
  91. package/src/skf-brief-skill/references/invocation-contract.md +37 -0
  92. package/src/skf-brief-skill/references/scope-definition.md +6 -15
  93. package/src/skf-brief-skill/references/step-auto-brief.md +22 -9
  94. package/src/skf-brief-skill/references/step-auto-validate.md +3 -7
  95. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  96. package/src/skf-brief-skill/references/write-brief.md +19 -13
  97. package/src/skf-campaign/SKILL.md +121 -0
  98. package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
  99. package/src/skf-campaign/customize.toml +75 -0
  100. package/src/skf-campaign/manifest.yaml +15 -0
  101. package/src/skf-campaign/references/campaign-contracts.md +67 -0
  102. package/src/skf-campaign/references/campaign-directive-spec.md +32 -0
  103. package/src/skf-campaign/references/health-check.md +35 -0
  104. package/src/skf-campaign/references/step-01-setup.md +121 -0
  105. package/src/skf-campaign/references/step-02-strategy.md +102 -0
  106. package/src/skf-campaign/references/step-03-pins.md +56 -0
  107. package/src/skf-campaign/references/step-04-provenance.md +63 -0
  108. package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
  109. package/src/skf-campaign/references/step-06-batch.md +88 -0
  110. package/src/skf-campaign/references/step-07-capstone.md +63 -0
  111. package/src/skf-campaign/references/step-08-verify.md +75 -0
  112. package/src/skf-campaign/references/step-09-refine.md +83 -0
  113. package/src/skf-campaign/references/step-10-export.md +106 -0
  114. package/src/skf-campaign/references/step-11-maintenance.md +84 -0
  115. package/src/skf-campaign/references/step-resume.md +115 -0
  116. package/src/skf-campaign/scripts/.gitkeep +0 -0
  117. package/src/skf-campaign/scripts/campaign-deps.py +244 -0
  118. package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
  119. package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
  120. package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
  121. package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
  122. package/src/skf-campaign/scripts/campaign-report.py +249 -0
  123. package/src/skf-campaign/scripts/campaign-status.py +162 -0
  124. package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
  125. package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
  126. package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
  127. package/src/skf-campaign/templates/campaign-report-template.md +54 -0
  128. package/src/skf-campaign/templates/kickoff-template.md +48 -0
  129. package/src/skf-create-skill/SKILL.md +15 -4
  130. package/src/skf-create-skill/assets/compile-assembly-rules.md +35 -34
  131. package/src/skf-create-skill/assets/skill-sections.md +9 -100
  132. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
  133. package/src/skf-create-skill/customize.toml +13 -0
  134. package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
  135. package/src/skf-create-skill/references/compile.md +37 -47
  136. package/src/skf-create-skill/references/component-extraction.md +8 -35
  137. package/src/skf-create-skill/references/ecosystem-check.md +9 -25
  138. package/src/skf-create-skill/references/enrich.md +4 -17
  139. package/src/skf-create-skill/references/extract.md +20 -27
  140. package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
  141. package/src/skf-create-skill/references/extraction-patterns.md +10 -8
  142. package/src/skf-create-skill/references/generate-artifacts.md +20 -25
  143. package/src/skf-create-skill/references/health-check.md +2 -2
  144. package/src/skf-create-skill/references/load-brief.md +16 -18
  145. package/src/skf-create-skill/references/report.md +27 -14
  146. package/src/skf-create-skill/references/source-resolution-protocols.md +4 -4
  147. package/src/skf-create-skill/references/step-auto-shard.md +52 -25
  148. package/src/skf-create-skill/references/step-doc-rot.md +33 -15
  149. package/src/skf-create-skill/references/step-doc-sources.md +13 -4
  150. package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
  151. package/src/skf-create-skill/references/sub/fetch-docs.md +33 -46
  152. package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
  153. package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
  154. package/src/skf-create-skill/references/validate.md +69 -29
  155. package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
  156. package/src/skf-create-stack-skill/SKILL.md +15 -9
  157. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
  158. package/src/skf-create-stack-skill/customize.toml +10 -0
  159. package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
  160. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
  161. package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
  162. package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
  163. package/src/skf-create-stack-skill/references/generate-output.md +24 -75
  164. package/src/skf-create-stack-skill/references/init.md +9 -81
  165. package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
  166. package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
  167. package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
  168. package/src/skf-create-stack-skill/references/report.md +27 -62
  169. package/src/skf-create-stack-skill/references/validate.md +35 -31
  170. package/src/skf-drop-skill/SKILL.md +20 -23
  171. package/src/skf-drop-skill/customize.toml +10 -0
  172. package/src/skf-drop-skill/references/execute.md +67 -36
  173. package/src/skf-drop-skill/references/headless-contract.md +18 -0
  174. package/src/skf-drop-skill/references/health-check.md +1 -13
  175. package/src/skf-drop-skill/references/report.md +44 -10
  176. package/src/skf-drop-skill/references/select.md +93 -48
  177. package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
  178. package/src/skf-export-skill/SKILL.md +17 -14
  179. package/src/skf-export-skill/assets/managed-section-format.md +2 -5
  180. package/src/skf-export-skill/customize.toml +10 -6
  181. package/src/skf-export-skill/references/generate-snippet.md +21 -35
  182. package/src/skf-export-skill/references/load-skill.md +44 -42
  183. package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
  184. package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
  185. package/src/skf-export-skill/references/package.md +27 -33
  186. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
  187. package/src/skf-export-skill/references/result-envelope.md +28 -0
  188. package/src/skf-export-skill/references/summary.md +11 -5
  189. package/src/skf-export-skill/references/token-report.md +42 -24
  190. package/src/skf-export-skill/references/update-context.md +32 -33
  191. package/src/skf-forger/SKILL.md +32 -39
  192. package/src/skf-forger/references/pipeline-mode.md +34 -0
  193. package/src/skf-forger/scripts/parse-pipeline.py +270 -0
  194. package/src/skf-quick-skill/SKILL.md +10 -60
  195. package/src/skf-quick-skill/customize.toml +9 -0
  196. package/src/skf-quick-skill/references/batch-mode.md +6 -4
  197. package/src/skf-quick-skill/references/compile.md +15 -52
  198. package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
  199. package/src/skf-quick-skill/references/finalize.md +12 -6
  200. package/src/skf-quick-skill/references/halt-contract.md +54 -0
  201. package/src/skf-quick-skill/references/health-check.md +3 -4
  202. package/src/skf-quick-skill/references/quick-extract.md +7 -18
  203. package/src/skf-quick-skill/references/registry-resolution.md +1 -23
  204. package/src/skf-quick-skill/references/resolve-target.md +27 -34
  205. package/src/skf-quick-skill/references/write-and-validate.md +10 -25
  206. package/src/skf-refine-architecture/SKILL.md +16 -8
  207. package/src/skf-refine-architecture/customize.toml +9 -0
  208. package/src/skf-refine-architecture/references/compile.md +10 -8
  209. package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
  210. package/src/skf-refine-architecture/references/health-check.md +1 -1
  211. package/src/skf-refine-architecture/references/improvements.md +9 -25
  212. package/src/skf-refine-architecture/references/init.md +9 -16
  213. package/src/skf-refine-architecture/references/issue-detection.md +10 -25
  214. package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
  215. package/src/skf-refine-architecture/references/report.md +14 -11
  216. package/src/skf-rename-skill/SKILL.md +11 -21
  217. package/src/skf-rename-skill/customize.toml +10 -0
  218. package/src/skf-rename-skill/references/execute.md +112 -82
  219. package/src/skf-rename-skill/references/exit-codes.md +12 -0
  220. package/src/skf-rename-skill/references/report.md +16 -8
  221. package/src/skf-rename-skill/references/select.md +69 -42
  222. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
  223. package/src/skf-setup/SKILL.md +21 -13
  224. package/src/skf-setup/customize.toml +10 -0
  225. package/src/skf-setup/references/auto-index.md +11 -10
  226. package/src/skf-setup/references/ccc-index.md +14 -15
  227. package/src/skf-setup/references/detect-and-tier.md +10 -8
  228. package/src/skf-setup/references/report.md +26 -32
  229. package/src/skf-setup/references/tier-rules.md +2 -32
  230. package/src/skf-setup/references/write-config.md +9 -10
  231. package/src/skf-test-skill/SKILL.md +10 -8
  232. package/src/skf-test-skill/customize.toml +2 -0
  233. package/src/skf-test-skill/references/coherence-check.md +19 -31
  234. package/src/skf-test-skill/references/coverage-check.md +111 -67
  235. package/src/skf-test-skill/references/detect-mode.md +10 -41
  236. package/src/skf-test-skill/references/external-validators.md +11 -19
  237. package/src/skf-test-skill/references/init.md +39 -21
  238. package/src/skf-test-skill/references/migration-section-rules.md +4 -4
  239. package/src/skf-test-skill/references/report.md +60 -40
  240. package/src/skf-test-skill/references/score.md +32 -64
  241. package/src/skf-test-skill/references/scoring-rules.md +6 -62
  242. package/src/skf-test-skill/references/source-access-protocol.md +15 -5
  243. package/src/skf-test-skill/references/step-hard-gate.md +1 -3
  244. package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
  245. package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
  246. package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
  247. package/src/skf-test-skill/scripts/compute-score.py +135 -4
  248. package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
  249. package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
  250. package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
  251. package/src/skf-update-skill/SKILL.md +17 -5
  252. package/src/skf-update-skill/customize.toml +11 -6
  253. package/src/skf-update-skill/references/detect-changes.md +48 -36
  254. package/src/skf-update-skill/references/health-check.md +5 -11
  255. package/src/skf-update-skill/references/init.md +40 -32
  256. package/src/skf-update-skill/references/manual-section-rules.md +1 -2
  257. package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
  258. package/src/skf-update-skill/references/merge.md +13 -57
  259. package/src/skf-update-skill/references/re-extract.md +27 -25
  260. package/src/skf-update-skill/references/report.md +14 -7
  261. package/src/skf-update-skill/references/validate.md +32 -30
  262. package/src/skf-update-skill/references/write.md +86 -51
  263. package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
  264. package/src/skf-verify-stack/SKILL.md +11 -21
  265. package/src/skf-verify-stack/customize.toml +12 -3
  266. package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
  267. package/src/skf-verify-stack/references/coverage.md +21 -10
  268. package/src/skf-verify-stack/references/exit-codes.md +14 -0
  269. package/src/skf-verify-stack/references/health-check.md +1 -1
  270. package/src/skf-verify-stack/references/init.md +20 -25
  271. package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
  272. package/src/skf-verify-stack/references/integrations.md +44 -49
  273. package/src/skf-verify-stack/references/report.md +28 -34
  274. package/src/skf-verify-stack/references/requirements.md +3 -3
  275. package/src/skf-verify-stack/references/synthesize.md +36 -39
  276. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
  277. package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
  278. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
  279. package/tools/cli/lib/ui.js +3 -2
  280. package/docs/deepwiki.md +0 -89
  281. package/src/skf-rename-skill/references/rebuild-context.md +0 -110
@@ -56,7 +56,7 @@ When `docs_only_mode: true` is set by step 3 (indicating a skill where all SKILL
56
56
 
57
57
  This is functionally identical to Quick tier weight redistribution but with a different coverage denominator (self-consistency instead of source comparison).
58
58
 
59
- **External-validator requirement for docs-only:** docs-only mode removes two categories (Signature Accuracy, Type Coverage) from scoring. If External Validation is ALSO unavailable, the evidence base collapses to Coverage alone (naive) or Coverage + Coherence (contextual) — which in the naive/Quick case trips the minimum-evidence floor (INCONCLUSIVE). To keep docs-only skills gradable when external validators are present but still deterministic when they are missing: **when `docsOnly: true` AND `externalValidation is null`, step 5 MUST cap `totalScore` at `threshold - 1` (forcing FAIL) before the INCONCLUSIVE floor is evaluated.** This prevents a docs-only skill from PASSing with only one or two redistributed categories carrying all the weight. Implement in step 5 §4 as a pre-compare cap, recorded in the report as `scoring_notes: docs-only without external validators — capped below threshold`.
59
+ **External-validator requirement for docs-only:** docs-only mode removes two categories (Signature Accuracy, Type Coverage) from scoring. If External Validation is also unavailable, the evidence base collapses to Coverage alone (naive) or Coverage + Coherence (contextual) — which in the naive/Quick case trips the minimum-evidence floor (INCONCLUSIVE). To keep docs-only skills gradable when external validators are present but still deterministic when they are missing: **when `docsOnly: true` AND `externalValidation is null`, step 5 must cap `totalScore` at `threshold - 1` (forcing FAIL) before the INCONCLUSIVE floor is evaluated.** This prevents a docs-only skill from PASSing with only one or two redistributed categories carrying all the weight. Implement in step 5 §4 as a pre-compare cap, recorded in the report as `scoring_notes: docs-only without external validators — capped below threshold`.
60
60
 
61
61
  ### Stack Skills (Any Tier)
62
62
 
@@ -68,8 +68,6 @@ When `metadata.json.skill_type == "stack"` (set `stackSkill: true` in the scorin
68
68
  - **Applies regardless of detected tier** (Quick, Forge, Forge+, Deep) and is independent of `docsOnly` and `state2`. A stack skill can also be docs-only or State 2; the skip reasons combine additively (e.g. `"stack skill (external type surface) + State 2 (provenance-map)"`).
69
69
  - **Detection:** step 5 reads `metadata.json.skill_type` from the skill package. If the value is `"stack"`, set `stackSkill: true` in the scoring input JSON.
70
70
 
71
- Equivalence-class note: stack skills with `docsOnly:false` / `state2:false` map to the same equivalence class as State 2 contextual rows (class B) or State 2 naive rows (class D) — the redistribution math is identical; only the `skipReasons` string changes.
72
-
73
71
  ### Reference-App Skills (Any Tier)
74
72
 
75
73
  When `metadata.json.scope_type == "reference-app"` (set `referenceApp: true` in the scoring input):
@@ -80,8 +78,6 @@ When `metadata.json.scope_type == "reference-app"` (set `referenceApp: true` in
80
78
  - **Applies regardless of detected tier** (Quick, Forge, Forge+, Deep) and is independent of `docsOnly` and `state2`; skip reasons combine additively. `referenceApp` and `stackSkill` are distinct scope/type signals and should not both be set for the same skill.
81
79
  - **Detection:** step 5 reads `metadata.json.scope_type` from the skill package. If the value is `"reference-app"`, set `referenceApp: true` in the scoring input JSON. The skip reason recorded is `"reference-app (no library export signatures)"`.
82
80
 
83
- Equivalence-class note: reference-app skills map to the same redistribution equivalence class as stack skills — the math is identical; only the `skipReasons` string changes.
84
-
85
81
  ### State 2 Source Access (Any Tier, Provenance-Map Only)
86
82
 
87
83
  When source is not locally available and analysis resolves to State 2 (provenance-map baseline per source-access-protocol.md):
@@ -94,7 +90,7 @@ When source is not locally available and analysis resolves to State 2 (provenanc
94
90
 
95
91
  Note: When provenance-map entries are predominantly T1 (AST-verified at compilation time), the coverage and name-matching data is already at highest confidence. The N/A categories reflect the inability to re-verify at test time, not low-quality extraction data.
96
92
 
97
- **State 2 undercount risk acknowledgement:** provenance-map is a cached extraction snapshot — if the source has evolved since extraction, public API adds/removes will NOT surface in Export Coverage (denominator is frozen to the provenance-map union). When `state2: true` AND step 3 records any provenance vs metadata divergence (e.g. union > either source by >5%), apply a flat **10% deduction** to `exportCoverage` before calling the scoring script, AND set `analysis_confidence: provenance-map` (already set) with a report note: `scoring_notes: State 2 undercount risk acknowledged — 10% deduction applied to Export Coverage`. Rationale: the skill cannot be reliably scored on a frozen denominator when the cache is known to disagree with its own metadata; prefer understating over overstating.
93
+ **State 2 undercount risk acknowledgement:** provenance-map is a cached extraction snapshot — if the source has evolved since extraction, public API adds/removes will not surface in Export Coverage (denominator is frozen to the provenance-map union). When `state2: true` AND step 3 records any provenance vs metadata divergence (e.g. union > either source by >5%), apply a flat **10% deduction** to `exportCoverage` before calling the scoring script, AND set `analysis_confidence: provenance-map` (already set) with a report note: `scoring_notes: State 2 undercount risk acknowledged — 10% deduction applied to Export Coverage`. Rationale: the skill cannot be reliably scored on a frozen denominator when the cache is known to disagree with its own metadata; prefer understating over overstating.
98
94
 
99
95
  ### Forge Tier (ast-grep)
100
96
  - Export Coverage: AST-backed export comparison
@@ -114,60 +110,6 @@ Note: When provenance-map entries are predominantly T1 (AST-verified at compilat
114
110
  - Full scoring formula with maximum depth
115
111
  - **Migration & Deprecation Warnings section:** If T2-future annotations exist in the enrichment data, verify that Section 4b is present in SKILL.md Tier 1 and that each warning traces to a T2 provenance citation. If no T2-future annotations exist, Section 4b should normally be absent (not empty). Presence/absence mismatch is a Medium severity gap — with one Info-severity exception for historical-migration content (completed package renames, consolidated import paths, shipped API cutovers that remain load-bearing for training-data drift remediation). See `references/coherence-check.md` §2b/§5b for the three-case rule.
116
112
 
117
- ## Redistribution Combinations Matrix (M3 — terminology + determinism)
118
-
119
- The table below enumerates every `(mode × tier × docsOnly × state2)` cell and
120
- the redistribution outcome. Two cells map to the same **equivalence class**
121
- whenever their skip set and base-weight table are identical — the scoring
122
- script then emits the same `weights` / `activeCategories` / `skippedCategories`
123
- output regardless of which specific cell produced it. The rightmost column
124
- cites the representative fixture in
125
- `test/fixtures/compute-score-contract.json` used to pin the class's math.
126
-
127
- | # | mode | tier | docsOnly | state2 | Base table | Skipped (sig/type) | Skipped (ext if null) | Equiv. class | Representative fixture |
128
- |---|------------|----------|----------|--------|----------------|-----------------------|-----------------------|--------------|-------------------------------|
129
- | 1 | contextual | Deep | F | F | contextual | — | if null | **A** | `suite_a_all_active` |
130
- | 2 | contextual | Forge+ | F | F | contextual | — | if null | A | `suite_k_forge_plus` |
131
- | 3 | contextual | Forge | F | F | contextual | — | if null | A | `suite_p_contextual_forge` |
132
- | 4 | contextual | Quick | F | F | contextual | Quick tier | if null | **B** | `suite_c_quick_tier` |
133
- | 5 | contextual | Deep | T | F | contextual | docs-only | if null | B | `suite_r_contextual_deep_docsonly` |
134
- | 6 | contextual | Forge+ | T | F | contextual | docs-only | if null | B | (equiv. — see `suite_r_*`) |
135
- | 7 | contextual | Forge | T | F | contextual | docs-only | if null | B | (equiv.) |
136
- | 8 | contextual | Quick | T | F | contextual | Quick tier + docs-only| if null | B | `suite_f_docs_only` |
137
- | 9 | contextual | Deep | F | T | contextual | State 2 | if null | B | `suite_g_state2` |
138
- | 10| contextual | Forge+ | F | T | contextual | State 2 | if null | B | (equiv.) |
139
- | 11| contextual | Forge | F | T | contextual | State 2 | if null | B | (equiv.) |
140
- | 12| contextual | Quick | F | T | contextual | Quick + State 2 | if null | B | (equiv. — Quick+state2) |
141
- | 13| contextual | * | T | T | contextual | docs-only + State 2 | if null | B | (equiv.) |
142
- | 14| naive | Deep | F | F | naive | — | if null | **C** | `suite_q_naive_deep` |
143
- | 15| naive | Forge+ | F | F | naive | — | if null | C | (equiv.) |
144
- | 16| naive | Forge | F | F | naive | — | if null | C | `suite_b_naive` |
145
- | 17| naive | Quick | F | F | naive | Quick tier | if null | **D** | `suite_e_triple_skip` |
146
- | 18| naive | Deep | T | F | naive | docs-only | if null | D | `suite_s_naive_deep_docsonly` |
147
- | 19| naive | Forge+ | T | F | naive | docs-only | if null | D | (equiv.) |
148
- | 20| naive | Forge | T | F | naive | docs-only | if null | D | (equiv.) |
149
- | 21| naive | Quick | T | F | naive | Quick + docs-only | if null | D | `suite_o_input_echo` |
150
- | 22| naive | Deep | F | T | naive | State 2 | if null | D | `suite_t_naive_state2` |
151
- | 23| naive | Forge+ | F | T | naive | State 2 | if null | D | (equiv.) |
152
- | 24| naive | Forge | F | T | naive | State 2 | if null | D | (equiv.) |
153
- | 25| naive | Quick | F | T | naive | Quick + State 2 | if null | D | (equiv.) |
154
- | 26| naive | * | T | T | naive | docs-only + State 2 | if null | D | (equiv.) |
155
-
156
- **How to read the matrix.** The four equivalence classes A/B/C/D are
157
- exhaustive: the scoring script's weight-redistribution output depends ONLY on
158
- (base table, sig/type skip, ext skip), so every row mapping to class X emits
159
- the same final weights for identical input scores. "(equiv.)" rows are *not*
160
- missing coverage — they reduce algebraically to a listed representative. The
161
- M3 fixture set ships at least one representative per equivalence class and one
162
- representative per unique skip-reason combination so the `skipReasons` string
163
- is also pinned (e.g. "Quick tier", "docs-only mode", "State 2
164
- (provenance-map)", "Quick tier + docs-only mode").
165
-
166
- **Quick-tier INCONCLUSIVE interaction.** Rows 4, 8, 12, 17, 21, 25 all force
167
- the minimum-evidence floor to evaluate "Quick tier + Export Coverage alone"
168
- cases separately — see `Result Determination` below. The matrix above is the
169
- pre-floor redistribution only; INCONCLUSIVE verdict determination runs after.
170
-
171
113
  ## Score Calculation
172
114
 
173
115
  ```
@@ -185,11 +127,13 @@ combined_coherence = (reference_validity * 0.6) + (integration_completeness * 0.
185
127
 
186
128
  If no integration patterns exist, combined coherence equals reference validity.
187
129
 
130
+ This is the documented contract. The tally + weighted mean is computed by `scripts/aggregate-coherence.py` (invoked from coherence-check.md §5c), not by hand; the per-reference validity judgment (§4) and pattern-completeness judgment (§5) stay in the prompt, only the arithmetic is scripted.
131
+
188
132
  ## Result Determination
189
133
 
190
- Three-state gate — **PASS / FAIL / INCONCLUSIVE**. `INCONCLUSIVE` is not PASS and not FAIL; it signals insufficient evidence to grade the skill. Downstream workflows MUST treat `INCONCLUSIVE` as a hard gate — do not export, do not auto-retry, surface to the human.
134
+ Three-state gate — **PASS / FAIL / INCONCLUSIVE**. `INCONCLUSIVE` is not PASS and not FAIL; it signals insufficient evidence to grade the skill. Downstream workflows must treat `INCONCLUSIVE` as a hard gate — do not export, do not auto-retry, surface to the human.
191
135
 
192
- - **Minimum-Evidence Floor (MANDATORY — applies before PASS/FAIL comparison):**
136
+ - **Minimum-Evidence Floor (applies before PASS/FAIL comparison):**
193
137
  - `active_categories` = count of categories with a non-zero final weight *after* all redistribution (Quick tier, docs-only, State 2, external-validator-unavailable). Categories with a redistributed weight of 0 do not count as active, even if they received a score.
194
138
  - **If `active_categories < 2`** → force `result: INCONCLUSIVE` with rationale `"insufficient evidence: only {N} active category"`. A single active category cannot cross-validate itself and a PASS would be a false signal.
195
139
  - **If `tier == "Quick"` AND the sole active contributor is Export Coverage** → force `result: INCONCLUSIVE` with rationale `"Quick tier: Export Coverage alone is insufficient evidence — add a second active category by upgrading tier or enabling external validators"`. This catches the degenerate case where every signature/type/coherence/external category gets redistributed to 0 and Export Coverage is doing all the work.
@@ -41,14 +41,14 @@
41
41
 
42
42
  Leave `analysis_confidence` unchanged (still `full` or `provenance-map` per the waterfall) — stratified scope does not degrade confidence, only the denominator. Annotate the coverage report with: `Stratified scope — denominator: {effective_denominator | tier_a_include union | scope.include union} ({N} files matched, {M} exports union)`.
43
43
 
44
- **When this clause does NOT apply:** `scope.type: "full-library"` skills, single-package repositories, or stratified briefs where the full monorepo is intentionally in scope. For those, use the standard barrel-based denominator — **unless** the single-package repo is a pattern-reference app (see next bullet).
44
+ **When this clause does not apply:** `scope.type: "full-library"` skills, single-package repositories, or stratified briefs where the full monorepo is intentionally in scope. For those, use the standard barrel-based denominator — **unless** the single-package repo is a pattern-reference app (see next bullet) or publishes a multi-subpath `exports` map (use the multi-entry clause below).
45
45
 
46
46
  - **Pattern-reference apps (non-library source):** If the source is a single-package repo whose purpose is demonstrating an integration pattern rather than distributing a library API — typical markers are `scope.type: "full-library"` **without** a barrel file at any recognized entry-point path (`__init__.py`, `index.ts`/`index.js`, `lib.rs`, `mod.rs`) AND without a monorepo layout — the skill's value lives in wiring patterns, not exports. None of the preceding three clauses fits: there is no barrel to count from, no empty-barrel `scope.include` to consult, and no monorepo stratification to re-derive.
47
47
 
48
48
  **Trigger (either fires):**
49
49
 
50
50
  1. `scope.notes` in `forge-data/{skill_name}/skill-brief.yaml` flags pattern-reference intent (phrases such as "Reference app, not a library", "pattern-reference", "embedded-pattern skill", or "skill value is the … pattern"). The `scope.notes` field is authoritative when the author wrote it.
51
- 2. Source tree lacks a barrel file at every recognized entry-point path AND the repo is not a monorepo (no `packages/`, `workspaces`, `lerna.json`, `rush.json`, `nx.json`, or Cargo `[workspace]`). Detected at test time by filesystem inspection of `{source_path}`.
51
+ 2. Source tree lacks a barrel file at every recognized entry-point path AND the repo is not a monorepo (no `packages/`, `workspaces`, `lerna.json`, `rush.json`, `nx.json`, or Cargo `[workspace]`) AND the package does not declare a multi-subpath `exports` map (those route to the multi-entry clause below). Detected at test time by filesystem inspection of `{source_path}`.
52
52
 
53
53
  **Denominator:** canonicalized provenance-map entry count (same canonicalization as the "Provenance-map canonicalization" section below). `skf-create-skill`'s extraction pass has already curated the provenance-map to the authored pattern surface; treat it as the authoritative enumeration of the skill's documented reach.
54
54
 
@@ -56,7 +56,7 @@
56
56
 
57
57
  **Confidence:** leave `analysis_confidence` unchanged (still `full` or `provenance-map` per the waterfall). Pattern-reference does not degrade confidence — the surface is smaller than a library barrel, not lower quality. Annotate the coverage report with: `Pattern-reference — denominator: {tier_a_include union | canonicalized provenance-map count} ({N} pattern surfaces)`.
58
58
 
59
- **When this clause does NOT apply:** any repo with a non-empty barrel file, any monorepo (use the stratified-scope clause), or any single-package repo whose `scope.type` is explicitly `public-api` / `specific-modules` / `component-library` / `docs-only` (those scope types have their own denominator semantics). Also does NOT apply when `scope.type: "reference-app"` — that scope type carries its own pattern-surface denominator semantics (the brief speaks for itself), so this clause's filesystem trigger is moot.
59
+ **When this clause does not apply:** any repo with a non-empty barrel file, any monorepo (use the stratified-scope clause), or any single-package repo whose `scope.type` is explicitly `specific-modules` (use the specific-modules clause), `public-api` with a multi-subpath `exports` map (use the multi-entry clause below a `public-api` package WITHOUT such a map keeps the standard root-barrel rule), `component-library`, or `docs-only`. Also does not apply when `scope.type: "reference-app"` — that scope type carries its own pattern-surface denominator semantics (the brief speaks for itself), so this clause's filesystem trigger is moot.
60
60
 
61
61
  - **Single-crate curated subset (`scope.type: "specific-modules"`):** If the source is a single-package (non-monorepo) repo whose skill brief sets `scope.type: "specific-modules"` and uses `scope.include`/`scope.exclude` to carve a subset of the crate's public surface, the coverage denominator is the **in-scope reachable barrel** — not the full barrel.
62
62
 
@@ -70,7 +70,17 @@
70
70
 
71
71
  **When `effective_denominator` is present:** prefer `metadata.json.stats.effective_denominator` (same priority-1 rule as the stratified-scope clause), subject to the same deflation guard.
72
72
 
73
- **When this clause does NOT apply:** monorepo packages (use the stratified-scope clause), `scope.type: "full-library"` (use the standard barrel), or empty-barrel packages (use the empty-barrel clause). This clause is specifically for single-crate repos where the brief intentionally documents a curated subset rather than the full public surface.
73
+ **When this clause does not apply:** monorepo packages (use the stratified-scope clause), `scope.type: "full-library"` (use the standard barrel), or empty-barrel packages (use the empty-barrel clause). This clause is specifically for single-crate repos where the brief intentionally documents a curated subset rather than the full public surface.
74
+
75
+ - **Multi-entry (exports-map) packages (single-package libraries publishing via a `package.json` `exports` map):** If the in-scope `package.json` declares an `exports` map with **multiple non-root subpath entries** (more than just `"."`) and the repo carries **no** monorepo markers (`packages/` layout, `workspaces` field, `lerna.json`, `rush.json`, `nx.json`, Cargo `[workspace]`), the package's public surface spans every published subpath, not just the root barrel. The standard "named exports from `index.ts`" rule undercounts: it measures only the `"."` barrel while installers reach the full subpath set. This clause covers both `scope.type: "full-library"` AND `scope.type: "public-api"` for such packages.
76
+
77
+ **Denominator:** the **union of named exports across the files each NON-WILDCARD `exports` subpath resolves to**. Resolve each subpath to its target file (or its committed `.d.ts` / `.d.mts` declaration), then apply the same multi-line brace-accumulation and `export *` star-resolution rules documented for barrel re-derivation earlier in this file ("TypeScript / JavaScript barrel re-derivation"). **Explicitly exclude wildcard subpaths** (`"./*"` forms — they map to an open-ended file set whose surface is unbounded and uncountable). If the `exports` map has only a root `"."` entry, or only wildcard subpaths, fall back to the standard root-barrel rule.
78
+
79
+ **Curation/priority (same ladder the specific-modules clause uses):** prefer `metadata.json.stats.effective_denominator` first (subject to the existing deflation guard), then `scope.tier_a_include` globs (filtered by `scope.exclude`, the umbrella-barrel note applies) when the brief supplies it, else the full subpath union.
80
+
81
+ **Audit:** the root-barrel named-export count must be reported as a **secondary candidate** in the Denominator Candidates audit block (coverage-check.md §4) so the root-barrel-vs-subpath-union choice is auditable. Annotate the coverage report with: `Multi-entry (exports-map) — denominator: {effective_denominator | tier_a_include union | subpath union} ({N} subpaths resolved, {M} exports union; root barrel: {R})`.
82
+
83
+ **When this clause does not apply:** monorepo packages (use the stratified-scope clause), empty-barrel packages (use the empty-barrel clause), pattern-reference apps (use the pattern-reference clause), `scope.type: "specific-modules"` (use the specific-modules clause), or single-entry / wildcard-only `exports` maps (use the standard root-barrel rule).
74
84
 
75
85
  Internal module symbols are **excluded** from the coverage denominator unless they are explicitly documented in SKILL.md (in which case they count as documented extras, not missing coverage).
76
86
 
@@ -136,6 +146,6 @@ Set `analysis_confidence` in context for use in Section 2 analysis depth, step 5
136
146
 
137
147
  **Degradation notice rules:** When `analysis_confidence` is `provenance-map`, check the `confidence` field of provenance-map entries before emitting a degradation recommendation:
138
148
 
139
- - **All/most entries T1 (AST-verified):** The provenance-map data is already at highest confidence. Do NOT recommend re-running with a local clone — it would produce identical results. Use: "Resolved via: provenance-map (T1 AST-verified at compilation time). Local clone not required — provenance data is already at highest confidence."
149
+ - **All/most entries T1 (AST-verified):** The provenance-map data is already at highest confidence. Do not recommend re-running with a local clone — it would produce identical results. Use: "Resolved via: provenance-map (T1 AST-verified at compilation time). Local clone not required — provenance data is already at highest confidence."
140
150
  - **Mixed T1/T1-low entries:** Report the breakdown. Recommend local clone only for the T1-low entries: "Resolved via: provenance-map ({n} T1, {m} T1-low). Re-run with local clone to upgrade T1-low entries to full AST verification."
141
151
  - **All/most entries T1-low or lower:** Keep the standard recommendation: "Re-run with local clone for full AST-backed verification."
@@ -66,8 +66,6 @@ The hard gate passes. Medium, low, and info findings are documented in the gap r
66
66
  Update `{outputFile}` frontmatter:
67
67
  - Append `'hard-gate'` to `stepsCompleted`
68
68
 
69
- Report:
70
-
71
- "**Hard gate passed** — {N} medium/low/info finding(s) noted, proceeding to scoring."
69
+ Report that the hard gate passed, noting the count of non-blocking medium/low/info finding(s), then proceed to scoring.
72
70
 
73
71
  Update stepsCompleted, then load and execute `{nextStepFile}`.
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = []
5
+ # ///
6
+ """Deterministic contextual-coherence aggregator.
7
+
8
+ Pure-function tally + weighted mean for the SKF test-skill workflow
9
+ (step-04, coherence-check.md §5c). The prompt keeps the judgment — deciding
10
+ which cross-references are valid (§4) and which integration patterns are
11
+ complete (§5). This script does ONLY the arithmetic those judgments feed:
12
+ the reference-validity ratio, the integration-completeness ratio, and their
13
+ fixed 0.6 / 0.4 weighted mean. That way the 18%-weight `coherence` input to
14
+ compute-score.py is computed once, deterministically, instead of by hand on
15
+ every run (this skill grades other skills — a false PASS is catastrophic, so
16
+ every scoring input is scripted for run-to-run reproducibility).
17
+
18
+ Formula (mirrors scoring-rules.md "Coherence Score Aggregation (Contextual
19
+ Mode)" — the prose there remains the documented contract):
20
+
21
+ reference_validity = (valid_references / total_references) * 100
22
+ integration_completeness = (complete_patterns / total_patterns) * 100
23
+ combined_coherence = reference_validity * 0.6 + integration_completeness * 0.4
24
+
25
+ Field-name mapping: the step-04 §5 integration JSON calls the pattern counts
26
+ `patterns_documented` (= total_patterns) and `patterns_complete`
27
+ (= complete_patterns); this script's input uses those step-04 names directly
28
+ so §5c passes them through unrenamed.
29
+
30
+ Edge cases (both documented in scoring-rules.md — absence is never penalized):
31
+ * patterns_documented == 0 -> no integration patterns to weigh, so
32
+ combined_coherence == reference_validity and integrationCompleteness is
33
+ null. (Do not divide by zero.)
34
+ * total_references == 0 -> no references means no broken references, so
35
+ reference_validity == 100.0 (vacuously coherent). Unreachable on the normal
36
+ contextual path — §3 always extracts at least one reference — but handled so
37
+ the arithmetic never divides by zero.
38
+
39
+ Input schema (one JSON object):
40
+ {
41
+ "valid_references": <int >= 0, <= total_references>,
42
+ "total_references": <int >= 0>,
43
+ "patterns_documented": <int >= 0>,
44
+ "patterns_complete": <int >= 0, <= patterns_documented>
45
+ }
46
+
47
+ Output (JSON):
48
+ {
49
+ "input": { ...echo... },
50
+ "referenceValidity": <0-100>,
51
+ "integrationCompleteness": <0-100 | null when patterns_documented == 0>,
52
+ "combinedCoherence": <0-100>,
53
+ "patternsScored": <bool>
54
+ }
55
+ or {"error": ..., "code": "INVALID_INPUT"} on a schema violation.
56
+
57
+ Percentages use the same JavaScript-compatible 2-decimal rounding as
58
+ compute-score.py so the two scoring scripts agree to the last digit.
59
+
60
+ CLI usage (mirrors compute-score.py):
61
+ uv run aggregate-coherence.py '<JSON>' # positional arg
62
+ uv run aggregate-coherence.py --json-input '<JSON>' # explicit flag form
63
+ cat input.json | uv run aggregate-coherence.py --stdin # piped input
64
+
65
+ Exit codes:
66
+ 0 — input was parsed; either a result object or a schema-validation error
67
+ object ({"error": ..., "code": "INVALID_INPUT"}) was emitted as JSON
68
+ 1 — input could not be parsed at all (no input provided, or malformed JSON)
69
+ """
70
+
71
+ from __future__ import annotations
72
+
73
+ import argparse
74
+ import json
75
+ import math
76
+ import sys
77
+
78
+ # Weights for the combined-coherence mean (from scoring-rules.md). Kept as
79
+ # named constants so the 0.6 / 0.4 split lives in exactly one place.
80
+ REFERENCE_VALIDITY_WEIGHT = 0.6
81
+ INTEGRATION_COMPLETENESS_WEIGHT = 0.4
82
+
83
+ COUNT_FIELDS = (
84
+ "valid_references",
85
+ "total_references",
86
+ "patterns_documented",
87
+ "patterns_complete",
88
+ )
89
+
90
+
91
+ def round2(value):
92
+ """Round to 2 decimals with JavaScript-compatible half-up rounding.
93
+
94
+ Matches compute-score.py.round2 so both scoring scripts produce identical
95
+ numbers. JS Math.round rounds .5 away from zero for positives; Python's
96
+ built-in round uses banker's rounding. We replicate JS: floor(x*100+0.5)/100.
97
+ """
98
+ return math.floor(value * 100 + 0.5) / 100
99
+
100
+
101
+ def make_error(message):
102
+ return {"error": message, "code": "INVALID_INPUT"}
103
+
104
+
105
+ def validate_input(inp):
106
+ if inp is None or not isinstance(inp, dict):
107
+ return "Input must be a JSON object"
108
+
109
+ for field in COUNT_FIELDS:
110
+ if field not in inp or inp[field] is None:
111
+ return f"Missing required field: {field} (non-negative integer)"
112
+ value = inp[field]
113
+ # bool is a subclass of int; reject it so true/false can't pose as 1/0.
114
+ if isinstance(value, bool) or not isinstance(value, int):
115
+ return (
116
+ f"Field `{field}` must be a non-negative integer, "
117
+ f"got {type(value).__name__}: {value!r}"
118
+ )
119
+ if value < 0:
120
+ return f"Field `{field}` must be >= 0, got {value}"
121
+
122
+ if inp["valid_references"] > inp["total_references"]:
123
+ return (
124
+ "valid_references cannot exceed total_references "
125
+ f"({inp['valid_references']} > {inp['total_references']})"
126
+ )
127
+
128
+ if inp["patterns_complete"] > inp["patterns_documented"]:
129
+ return (
130
+ "patterns_complete cannot exceed patterns_documented "
131
+ f"({inp['patterns_complete']} > {inp['patterns_documented']})"
132
+ )
133
+
134
+ return None
135
+
136
+
137
+ def aggregate_coherence(inp):
138
+ """Compute reference validity, integration completeness, and combined coherence.
139
+
140
+ Weighted mean of the two ratios (0.6 / 0.4), JS-compatible 2-decimal rounding:
141
+ >>> out = aggregate_coherence({"valid_references": 6, "total_references": 7,
142
+ ... "patterns_documented": 5, "patterns_complete": 4})
143
+ >>> out["referenceValidity"], out["integrationCompleteness"], out["combinedCoherence"]
144
+ (85.71, 80.0, 83.43)
145
+ >>> out["patternsScored"]
146
+ True
147
+
148
+ Zero integration patterns -> combined equals reference validity, no divide-by-zero:
149
+ >>> out = aggregate_coherence({"valid_references": 9, "total_references": 10,
150
+ ... "patterns_documented": 0, "patterns_complete": 0})
151
+ >>> out["integrationCompleteness"] is None
152
+ True
153
+ >>> out["patternsScored"]
154
+ False
155
+ >>> out["referenceValidity"], out["combinedCoherence"]
156
+ (90.0, 90.0)
157
+
158
+ Zero references -> vacuously perfect reference validity, never a ZeroDivision:
159
+ >>> aggregate_coherence({"valid_references": 0, "total_references": 0,
160
+ ... "patterns_documented": 2, "patterns_complete": 1})["referenceValidity"]
161
+ 100.0
162
+
163
+ A schema violation returns an error object, not an exception:
164
+ >>> aggregate_coherence({"valid_references": 5, "total_references": 3,
165
+ ... "patterns_documented": 0, "patterns_complete": 0})["code"]
166
+ 'INVALID_INPUT'
167
+ """
168
+ validation_error = validate_input(inp)
169
+ if validation_error:
170
+ return make_error(validation_error)
171
+
172
+ valid_references = inp["valid_references"]
173
+ total_references = inp["total_references"]
174
+ patterns_documented = inp["patterns_documented"]
175
+ patterns_complete = inp["patterns_complete"]
176
+
177
+ # Reference validity — vacuously 100 when there are no references at all.
178
+ if total_references == 0:
179
+ reference_validity = 100.0
180
+ else:
181
+ reference_validity = round2((valid_references / total_references) * 100)
182
+
183
+ # Integration completeness — absent when no patterns are documented.
184
+ patterns_scored = patterns_documented > 0
185
+ if patterns_scored:
186
+ integration_completeness = round2(
187
+ (patterns_complete / patterns_documented) * 100
188
+ )
189
+ combined_coherence = round2(
190
+ reference_validity * REFERENCE_VALIDITY_WEIGHT
191
+ + integration_completeness * INTEGRATION_COMPLETENESS_WEIGHT
192
+ )
193
+ else:
194
+ integration_completeness = None
195
+ combined_coherence = reference_validity
196
+
197
+ return {
198
+ "input": {field: inp[field] for field in COUNT_FIELDS},
199
+ "referenceValidity": reference_validity,
200
+ "integrationCompleteness": integration_completeness,
201
+ "combinedCoherence": combined_coherence,
202
+ "patternsScored": patterns_scored,
203
+ }
204
+
205
+
206
+ def _build_parser() -> argparse.ArgumentParser:
207
+ parser = argparse.ArgumentParser(
208
+ prog="aggregate-coherence",
209
+ description=(
210
+ "Deterministic contextual-coherence aggregator. Input is a single "
211
+ "JSON object of reference + integration-pattern counts; output is the "
212
+ "reference-validity, integration-completeness, and combined-coherence "
213
+ "percentages that feed the `coherence` scoring input."
214
+ ),
215
+ epilog=(
216
+ "Example:\n"
217
+ " uv run aggregate-coherence.py "
218
+ "'{\"valid_references\":6,\"total_references\":7,"
219
+ "\"patterns_documented\":5,\"patterns_complete\":4}'"
220
+ ),
221
+ formatter_class=argparse.RawDescriptionHelpFormatter,
222
+ )
223
+ src = parser.add_mutually_exclusive_group()
224
+ src.add_argument(
225
+ "json_input",
226
+ nargs="?",
227
+ help="JSON object as a positional argument (single-quote it on the shell).",
228
+ )
229
+ src.add_argument(
230
+ "--json-input",
231
+ dest="json_input_flag",
232
+ help="JSON object passed via flag (overrides positional).",
233
+ )
234
+ src.add_argument(
235
+ "--stdin",
236
+ action="store_true",
237
+ help="Read the JSON object from stdin.",
238
+ )
239
+ return parser
240
+
241
+
242
+ def _resolve_input(args: argparse.Namespace) -> str:
243
+ if args.stdin:
244
+ return sys.stdin.read()
245
+ if args.json_input_flag is not None:
246
+ return args.json_input_flag
247
+ if args.json_input is not None:
248
+ return args.json_input
249
+ return ""
250
+
251
+
252
+ def main(argv: list[str] | None = None) -> int:
253
+ parser = _build_parser()
254
+ args = parser.parse_args(argv)
255
+ raw = _resolve_input(args)
256
+ if not raw.strip():
257
+ parser.print_usage(file=sys.stderr)
258
+ print(
259
+ "error: no input provided (positional arg, --json-input, or --stdin)",
260
+ file=sys.stderr,
261
+ )
262
+ return 1
263
+
264
+ try:
265
+ data = json.loads(raw)
266
+ except json.JSONDecodeError as exc:
267
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
268
+ return 1
269
+
270
+ result = aggregate_coherence(data)
271
+ print(json.dumps(result, indent=2))
272
+ return 0
273
+
274
+
275
+ if __name__ == "__main__":
276
+ raise SystemExit(main())