bmad-module-skill-forge 2.1.0 → 3.0.0

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 (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +51 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +14 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +117 -9
  75. package/src/shared/scripts/skf-detect-docs.py +892 -4
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
  79. package/src/shared/scripts/skf-detect-tools.py +215 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +17 -6
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +55 -18
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -57
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -89
  348. package/src/skf-setup/references/detect-and-tier.md +58 -66
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -645
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
- # requires-python = ">=3.10"
3
+ # requires-python = ">=3.11"
4
4
  # ///
5
5
  """Deterministic metadata export-count coherence cross-check.
6
6
 
@@ -39,10 +39,15 @@ Stack skills (`skill_type == "stack"`) and reference apps
39
39
  intentionally *different* surfaces, so comparing them yields only false drift.
40
40
 
41
41
  CLI usage:
42
+ uv run check-metadata-coherence.py --inputs <coverage-inputs.json> # a file
42
43
  uv run check-metadata-coherence.py '<JSON>' # positional
43
44
  uv run check-metadata-coherence.py --json-input '<JSON>' # explicit flag
44
45
  cat input.json | uv run check-metadata-coherence.py --stdin # piped
45
46
 
47
+ --inputs reads the input object from a file: load-coverage-inputs.py
48
+ metadata writes these keys from metadata.json and the provenance map (any
49
+ other key in the file is ignored), so no count is copied by hand.
50
+
46
51
  Input schema (one object; omit any count that is absent for the skill):
47
52
  {
48
53
  "skillType": "stack" | ... | null, # metadata.json.skill_type
@@ -83,11 +88,12 @@ from __future__ import annotations
83
88
  import argparse
84
89
  import json
85
90
  import sys
91
+ from pathlib import Path
86
92
 
87
93
  DEFAULT_DRIFT_PCT = 10
88
94
  CATEGORY = "structural/metadata coherence"
89
95
 
90
- # Stable source labels used in finding bodies (match coverage-check.md §4b examples).
96
+ # Stable source labels used in finding bodies.
91
97
  LABEL_EXPORTS_PUBLIC_API = "stats.exports_public_api"
92
98
  LABEL_EXPORTS_LENGTH = "exports[].length"
93
99
  LABEL_EXPORTS_DOCUMENTED = "stats.exports_documented"
@@ -317,12 +323,19 @@ def _build_parser():
317
323
  action="store_true",
318
324
  help="Read the JSON object from stdin.",
319
325
  )
326
+ src.add_argument(
327
+ "--inputs",
328
+ metavar="PATH",
329
+ help="Read the JSON object from this file (load-coverage-inputs.py metadata output).",
330
+ )
320
331
  return parser
321
332
 
322
333
 
323
334
  def _resolve_input(args):
324
335
  if args.stdin:
325
336
  return sys.stdin.read()
337
+ if args.inputs is not None:
338
+ return Path(args.inputs).read_bytes().decode("utf-8-sig")
326
339
  if args.json_input_flag is not None:
327
340
  return args.json_input_flag
328
341
  if args.json_input is not None:
@@ -333,11 +346,15 @@ def _resolve_input(args):
333
346
  def main(argv=None):
334
347
  parser = _build_parser()
335
348
  args = parser.parse_args(argv)
336
- raw = _resolve_input(args)
349
+ try:
350
+ raw = _resolve_input(args)
351
+ except (OSError, UnicodeDecodeError) as exc:
352
+ print(f"error: cannot read --inputs {args.inputs}: {exc}", file=sys.stderr)
353
+ return 1
337
354
  if not raw.strip():
338
355
  parser.print_usage(file=sys.stderr)
339
356
  print(
340
- "error: no input provided (positional arg, --json-input, or --stdin)",
357
+ "error: no input provided (positional arg, --json-input, --stdin or --inputs)",
341
358
  file=sys.stderr,
342
359
  )
343
360
  return 1
@@ -355,5 +372,21 @@ def main(argv=None):
355
372
  return 0
356
373
 
357
374
 
375
+ def _force_utf8(*streams) -> None:
376
+ """Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
377
+
378
+ A Windows console pipes them as cp1252, which cannot print every character
379
+ of the --help text, so --help would stop with UnicodeEncodeError.
380
+ """
381
+ for stream in streams:
382
+ if hasattr(stream, "reconfigure"):
383
+ errors = getattr(stream, "errors", None)
384
+ if errors is None:
385
+ stream.reconfigure(encoding="utf-8")
386
+ else:
387
+ stream.reconfigure(encoding="utf-8", errors=errors)
388
+
389
+
358
390
  if __name__ == "__main__":
391
+ _force_utf8(sys.stdout, sys.stderr)
359
392
  raise SystemExit(main())
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
- # requires-python = ">=3.10"
3
+ # requires-python = ">=3.11"
4
4
  # dependencies = []
5
5
  # ///
6
6
  """Deterministic combined external-validation score.
@@ -13,12 +13,12 @@ run-to-run (an odd-sum average such as (80 + 73) / 2 = 76.5 is exactly where an
13
13
  in-prompt round swings).
14
14
 
15
15
  Rule (external-validators.md §4):
16
- * both tools ran -> mean of skill-check + tessl review scores
16
+ * both tools ran -> mean of the skill-check and Tessl Review scores
17
17
  * one tool ran -> that tool's score
18
18
  * neither ran -> null (scoring step redistributes the external weight)
19
19
 
20
- Both scores are on the same 0-100 scale (skill-check quality score; tessl review
21
- percentage). Rounding matches compute-score.py (JS-compatible half-up) so the
20
+ Both scores are on the same 0-100 scale (skill-check's quality score; the Tessl
21
+ Review score, `review_score` from skf-tessl-review.py, when the user opted in). Rounding matches compute-score.py (JS-compatible half-up) so the
22
22
  number this script emits and the one compute-score.py weights agree to the digit.
23
23
 
24
24
  Input schema (one JSON object; a tool that did not run is null or omitted):
@@ -40,6 +40,10 @@ CLI usage (mirrors compute-score.py):
40
40
  uv run combine-external-scores.py --json-input '<JSON>' # explicit flag
41
41
  cat input.json | uv run combine-external-scores.py --stdin # piped input
42
42
 
43
+ --output <file> also writes the result to a file (UTF-8 JSON), the one
44
+ compute-score.py --external reads; a refused input removes a file left
45
+ there earlier.
46
+
43
47
  Exit codes:
44
48
  0 — score emitted successfully
45
49
  1 — no input / input could not be parsed as JSON
@@ -52,6 +56,7 @@ import argparse
52
56
  import json
53
57
  import math
54
58
  import sys
59
+ from pathlib import Path
55
60
 
56
61
  SKILL_CHECK = "skill-check"
57
62
  TESSL = "tessl"
@@ -121,8 +126,8 @@ def _build_parser():
121
126
  prog="combine-external-scores",
122
127
  description=(
123
128
  "Deterministic combined external-validation score "
124
- "(external-validators.md §4). Averages the skill-check and tessl "
125
- "review scores (or passes a single available score through) into the "
129
+ "(external-validators.md §4). Averages the skill-check and Tessl "
130
+ "Review scores (or passes a single available score through) into the "
126
131
  "`externalValidation` scoring input."
127
132
  ),
128
133
  formatter_class=argparse.RawDescriptionHelpFormatter,
@@ -148,9 +153,19 @@ def _build_parser():
148
153
  action="store_true",
149
154
  help="Read the JSON object from stdin.",
150
155
  )
156
+ parser.add_argument("--output", metavar="PATH", help="also write the result to this file")
151
157
  return parser
152
158
 
153
159
 
160
+ def _write_output(path, result):
161
+ target = Path(path)
162
+ if result.get("code") == "INVALID_INPUT":
163
+ target.unlink(missing_ok=True)
164
+ return
165
+ target.parent.mkdir(parents=True, exist_ok=True)
166
+ target.write_bytes((json.dumps(result, indent=2) + "\n").encode("utf-8"))
167
+
168
+
154
169
  def _resolve_input(args):
155
170
  if args.stdin:
156
171
  return sys.stdin.read()
@@ -180,6 +195,8 @@ def main(argv=None):
180
195
  return 1
181
196
 
182
197
  result = combine(data)
198
+ if args.output:
199
+ _write_output(args.output, result)
183
200
  print(json.dumps(result, indent=2))
184
201
  if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
185
202
  return 2
@@ -1,19 +1,31 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
- # requires-python = ">=3.10"
3
+ # requires-python = ">=3.11"
4
4
  # dependencies = []
5
5
  # ///
6
6
  """Deterministic Completeness Score Calculator.
7
7
 
8
8
  Pure-function scoring script for the SKF test-skill workflow (step-05).
9
- Implements the weight tables, skip conditions, and proportional redistribution
10
- defined in scoring-rules.md.
9
+ It is the one home of the scoring rules: the weight tables, the skip
10
+ conditions and the proportional redistribution, the State 2 deduction, the
11
+ minimum-evidence floor, both caps and the threshold fallback. scoring-rules.md
12
+ points here and keeps only the Gap Severity table.
11
13
 
12
14
  CLI usage:
15
+ uv run compute-score.py --json-input '<JSON>' [score files] # explicit flag form
13
16
  uv run compute-score.py '<JSON>' # JSON literal as positional arg
14
- uv run compute-score.py --json-input '<JSON>' # explicit flag form
15
17
  cat input.json | uv run compute-score.py --stdin # piped input
16
18
 
19
+ Score files. The category scores come from the scripts that computed them,
20
+ read from the run folder by path, so no score is copied out of the report:
21
+ --coverage reconcile-coverage.py --output: exportCoverage
22
+ --signatures score-signatures.py score --output: signatureAccuracy, typeCoverage
23
+ --coherence aggregate-coherence.py --output: combinedCoherence -> coherence
24
+ --external combine-external-scores.py --output: externalScore -> externalValidation
25
+ --surface load-coverage-inputs.py surface --output: its `state2` counts
26
+ Each file sets its fields and wins over the same field in the JSON; a file
27
+ that holds an error envelope or lacks its field is refused (exit 2).
28
+
17
29
  Input schema (one object):
18
30
  {
19
31
  "mode": "contextual" | "naive",
@@ -27,11 +39,30 @@ Input schema (one object):
27
39
  },
28
40
  "threshold": <0-100, optional, default 80>,
29
41
  "evidenceCount": <int, optional, used for INCONCLUSIVE floor>,
30
- "analysisConfidence": "<optional string; 'degraded' fires the tooling cap>",
31
- "toolingStatus": "<optional string; a '*-missing' marker fires the tooling cap>"
42
+ "analysisConfidence": "<optional: the source access, one of ANALYSIS_CONFIDENCE;
43
+ metadata-only and remote-only skip Signature Accuracy
44
+ and Type Coverage, as Quick tier does>",
45
+ "toolingStatus": "<optional: 'ok', or the tooling problem, e.g. 'frontmatter-validator-timeout'>",
46
+ "state2Counts": {"provenance": N, "metadata": N, "union": N} # optional, State 2
32
47
  }
33
48
 
34
- Verdict override (post-score caps + threshold fallback — see compute_score):
49
+ Verdict order (each step reads the one before it):
50
+ 1. Score: redistribute the weights, weigh the scores, total them. On a
51
+ State 2 run of a skill that is not a stack, with state2Counts, the
52
+ State 2 undercount deduction comes first: when the provenance and
53
+ metadata union exceeds either source by more than STATE2_DIVERGENCE_PCT
54
+ percent, Export Coverage loses STATE2_DEDUCTION points (never below 0),
55
+ and `state2Deduction` and `scoringNotes` say so.
56
+ 2. Minimum-evidence floor: too few active categories make the result
57
+ INCONCLUSIVE, which nothing below overrides.
58
+ 3. PASS or FAIL: totalScore against the threshold (`result`).
59
+ 4. Caps: Cap 1 (any toolingStatus other than "ok") and Cap 2 (docs-only
60
+ with no external validation score) turn a PASS into FAIL.
61
+ 5. Threshold fallback: an uncapped FAIL whose totalScore is 80 or more,
62
+ against a threshold above 80, passes at the 80 floor. A run a cap
63
+ turned to FAIL stays FAIL, whatever the threshold.
64
+
65
+ Verdict override (post-score caps and threshold fallback, steps 4 and 5):
35
66
  When a cap or the threshold fallback engages, the output additionally carries
36
67
  `effectiveResult` (the final verdict after caps/fallback), `capReason`
37
68
  (string or null), `thresholdFallback` (bool), and `originalThreshold` (the
@@ -40,16 +71,16 @@ Verdict override (post-score caps + threshold fallback — see compute_score):
40
71
  always the pre-cap/pre-fallback score-vs-threshold (or evidence-floor) verdict.
41
72
 
42
73
  Exit codes (same convention as reconcile-coverage.py):
43
- 0 — a score was computed; the verdict may be PASS/FAIL/INCONCLUSIVE
44
- 1 — input could not be parsed at all (no input provided, or malformed JSON)
45
- 2 — input parsed but schema/semantics invalid
74
+ 0 a score was computed; the verdict may be PASS/FAIL/INCONCLUSIVE
75
+ 1 input could not be parsed at all (no input provided, or malformed JSON)
76
+ 2 input parsed but schema/semantics invalid, or a score file refused
46
77
 
47
78
  Both 1 and 2 emit an {"error": ..., "code": "INVALID_INPUT"} envelope on stdout,
48
79
  so the presence of that envelope — not the specific code — is what tells a caller
49
80
  the input was refused. A refused input is distinct from an unavailable script:
50
81
  the numbers handed in are wrong and must be fixed, NOT hand-computed into a total
51
- from the values this script declined to score. score.md §3c keys its
52
- manual-redistribution fallback on that distinction (no envelope at all).
82
+ from the values this script declined to score. score.md §3c keys on that
83
+ distinction: a refused input is corrected, a script that does not run halts.
53
84
  """
54
85
 
55
86
  from __future__ import annotations
@@ -58,8 +89,9 @@ import argparse
58
89
  import json
59
90
  import math
60
91
  import sys
92
+ from pathlib import Path
61
93
 
62
- # --- Weight Tables (from scoring-rules.md) ---
94
+ # --- Weight Tables ---
63
95
 
64
96
  CONTEXTUAL_WEIGHTS = {
65
97
  "exportCoverage": 36,
@@ -86,6 +118,21 @@ CATEGORIES = [
86
118
  ]
87
119
 
88
120
  DEFAULT_THRESHOLD = 80
121
+ # The threshold fallback's floor: an uncapped FAIL scoring this much passes
122
+ # when the threshold is above it.
123
+ FALLBACK_FLOOR = 80
124
+
125
+ # The source access levels a run reports (source-access-protocol.md). Tooling
126
+ # health is not one of them: it is toolingStatus.
127
+ ANALYSIS_CONFIDENCE = ("full", "provenance-map", "metadata-only", "remote-only", "docs-only")
128
+ # States 3 and 4: no local source, so no signature or type to compare.
129
+ NO_LOCAL_SOURCE = ("metadata-only", "remote-only")
130
+ TOOLING_OK = "ok"
131
+
132
+ # State 2 undercount risk: the provenance map is a frozen snapshot, so when it
133
+ # and metadata.json disagree by more than this, Export Coverage is reduced.
134
+ STATE2_DIVERGENCE_PCT = 5
135
+ STATE2_DEDUCTION = 10
89
136
 
90
137
 
91
138
  # --- Redistribution equivalence classes (moved here from scoring-rules.md) ---
@@ -94,8 +141,9 @@ DEFAULT_THRESHOLD = 80
94
141
  # `weights` / `activeCategories` / `skippedCategories` output depends ONLY on
95
142
  # three things: the base table (contextual vs naive), whether Signature Accuracy
96
143
  # + Type Coverage are skipped (Quick tier OR docsOnly OR state2 OR stackSkill OR
97
- # referenceApp), and whether External Validation is skipped (externalValidation
98
- # is null). Every (mode × tier × docsOnly × state2 × stackSkill × referenceApp)
144
+ # referenceApp OR State 3/4), and whether External Validation is skipped
145
+ # (externalValidation is null). Every (mode × tier × docsOnly × state2 ×
146
+ # stackSkill × referenceApp × analysisConfidence)
99
147
  # cell with an identical skip set + base table therefore reduces to the same
100
148
  # equivalence class below and emits identical final weights for identical input
101
149
  # scores — only the `skipReasons` string differs. The prompt (score.md) never
@@ -133,12 +181,14 @@ DEFAULT_THRESHOLD = 80
133
181
  # | 25 | naive | Quick | F | T | naive | Quick + State 2 | if null | D | (equiv.) |
134
182
  # | 26 | naive | * | T | T | naive | docs-only + State 2 | if null | D | (equiv.) |
135
183
  #
136
- # Stack skills (stackSkill) and reference-app skills (referenceApp) share the
137
- # skip set of the docsOnly/state2 rows: contextual → class B, naive → class D;
138
- # only skipReasons differs (fixtures suite_u_stack_skill_deep,
139
- # suite_v_reference_app_deep). Quick-tier rows (4, 8, 12, 17, 21, 25) then feed
140
- # the minimum-evidence floor in the result block below (§7 of compute_score),
141
- # which can force INCONCLUSIVE after this pre-floor redistribution.
184
+ # Stack skills (stackSkill), reference-app skills (referenceApp) and State 3/4
185
+ # runs (analysisConfidence metadata-only or remote-only) share the skip set of
186
+ # the docsOnly/state2 rows: contextual → class B, naive → class D; only
187
+ # skipReasons differs (fixtures suite_u_stack_skill_deep,
188
+ # suite_v_reference_app_deep, suite_ab_naive_forge_metadata_only). Quick-tier
189
+ # rows (4, 8, 12, 17, 21, 25) then feed the minimum-evidence floor in the
190
+ # result block below (§7 of compute_score), which can force INCONCLUSIVE after
191
+ # this pre-floor redistribution.
142
192
 
143
193
 
144
194
  # --- Helpers ---
@@ -205,6 +255,22 @@ def validate_input(inp):
205
255
  if val is not None and not isinstance(val, str):
206
256
  return f"{str_field} must be a string or null, got: {type(val).__name__}"
207
257
 
258
+ confidence = inp.get("analysisConfidence")
259
+ if confidence == "degraded":
260
+ return ("analysisConfidence no longer takes 'degraded': it names the source access only. "
261
+ "Pass the tooling problem as toolingStatus (for example 'frontmatter-validator-timeout').")
262
+ if confidence is not None and confidence not in ANALYSIS_CONFIDENCE:
263
+ return f"analysisConfidence must be one of: {', '.join(ANALYSIS_CONFIDENCE)}"
264
+
265
+ counts = inp.get("state2Counts")
266
+ if counts is not None:
267
+ if not isinstance(counts, dict):
268
+ return "state2Counts must be an object {provenance, metadata, union}"
269
+ for key in ("provenance", "metadata", "union"):
270
+ val = counts.get(key)
271
+ if isinstance(val, bool) or not isinstance(val, int) or val < 0:
272
+ return f"state2Counts.{key} must be an integer >= 0"
273
+
208
274
  for cat in CATEGORIES:
209
275
  score = inp["scores"].get(cat)
210
276
  if score is not None:
@@ -231,6 +297,7 @@ def compute_score(inp):
231
297
  state2 = inp.get("state2") is True
232
298
  stack_skill = inp.get("stackSkill") is True
233
299
  reference_app = inp.get("referenceApp") is True
300
+ no_local_source = inp.get("analysisConfidence") in NO_LOCAL_SOURCE
234
301
  threshold = inp.get("threshold") if inp.get("threshold") is not None else DEFAULT_THRESHOLD
235
302
  scores = inp["scores"]
236
303
 
@@ -239,7 +306,7 @@ def compute_score(inp):
239
306
 
240
307
  # 3. Determine skip set
241
308
  skip_reasons = {}
242
- skip_sig_type = tier == "Quick" or docs_only or state2 or stack_skill or reference_app
309
+ skip_sig_type = tier == "Quick" or docs_only or state2 or stack_skill or reference_app or no_local_source
243
310
 
244
311
  if skip_sig_type:
245
312
  reasons = []
@@ -253,6 +320,8 @@ def compute_score(inp):
253
320
  reasons.append("stack skill (external type surface)")
254
321
  if reference_app:
255
322
  reasons.append("reference-app (no library export signatures)")
323
+ if no_local_source:
324
+ reasons.append("no local source (State 3/4)")
256
325
  reason = " + ".join(reasons)
257
326
  skip_reasons["signatureAccuracy"] = reason
258
327
  skip_reasons["typeCoverage"] = reason
@@ -293,13 +362,41 @@ def compute_score(inp):
293
362
  else:
294
363
  final_weights[cat] = round2((adjusted_weights[cat] / sum_active_weights) * 100)
295
364
 
365
+ # 4b. State 2 undercount deduction: a frozen provenance map that disagrees
366
+ # with its own metadata may hide source exports, so prefer understating.
367
+ effective_scores = dict(scores)
368
+ scoring_notes = []
369
+ state2_deduction = None
370
+ counts = inp.get("state2Counts")
371
+ if state2 and not stack_skill and counts is not None:
372
+ sources = [counts["provenance"], counts["metadata"]]
373
+ divergence = 0.0
374
+ if all(sources):
375
+ divergence = round2(max((counts["union"] - n) / n * 100 for n in sources))
376
+ applied = divergence > STATE2_DIVERGENCE_PCT
377
+ raw = scores["exportCoverage"]
378
+ adjusted = max(0, raw - STATE2_DEDUCTION) if applied else raw
379
+ effective_scores["exportCoverage"] = adjusted
380
+ state2_deduction = {
381
+ "divergencePct": divergence,
382
+ "applied": applied,
383
+ "rawExportCoverage": raw,
384
+ "exportCoverage": adjusted,
385
+ }
386
+ if applied:
387
+ scoring_notes.append(
388
+ f"State 2 undercount risk acknowledged: {STATE2_DEDUCTION}-point deduction applied to "
389
+ f"Export Coverage (raw: {raw}%, adjusted: {adjusted}%; provenance and metadata "
390
+ f"diverge by {divergence}%)"
391
+ )
392
+
296
393
  # 5. Compute weighted scores
297
394
  weighted_scores = {}
298
395
  for cat in CATEGORIES:
299
396
  if final_weights[cat] == 0:
300
397
  weighted_scores[cat] = 0
301
398
  else:
302
- weighted_scores[cat] = round2((final_weights[cat] / 100) * scores[cat])
399
+ weighted_scores[cat] = round2((final_weights[cat] / 100) * effective_scores[cat])
303
400
 
304
401
  # 6. Compute total
305
402
  total_score = round2(sum(weighted_scores[cat] for cat in CATEGORIES))
@@ -310,7 +407,7 @@ def compute_score(inp):
310
407
  # 7. Determine result — MINIMUM-EVIDENCE FLOOR first, then PASS/FAIL.
311
408
  # skf-test-skill grades other skills; a false PASS is catastrophic.
312
409
  # If the evidence base is too thin to cross-validate itself, force
313
- # INCONCLUSIVE (a gate, not a pass/fail). See scoring-rules.md.
410
+ # INCONCLUSIVE (a gate, not a pass/fail).
314
411
  active_categories = [cat for cat in CATEGORIES if final_weights[cat] > 0]
315
412
  skipped_categories = [
316
413
  cat for cat in CATEGORIES if cat in skipped_set or base_weights[cat] == 0
@@ -352,12 +449,10 @@ def compute_score(inp):
352
449
  else:
353
450
  result = "PASS" if total_score >= threshold else "FAIL"
354
451
 
355
- # 7b. Post-score caps + threshold fallback — deterministic verdict override.
356
- # Lifted from score.md §3d/§4b so the cap<->fallback interaction is centralized
357
- # and unit-tested here rather than re-derived in the prompt. The minimum-
452
+ # 7b. Post-score caps, then the threshold fallback: the deterministic verdict
453
+ # override, kept here so the prompt never re-derives it. The minimum-
358
454
  # evidence floor (INCONCLUSIVE) is a gate that NO cap or fallback may override,
359
455
  # so all of this is skipped when result == "INCONCLUSIVE".
360
- analysis_confidence = inp.get("analysisConfidence")
361
456
  tooling_status = inp.get("toolingStatus")
362
457
 
363
458
  effective_result = result
@@ -368,15 +463,13 @@ def compute_score(inp):
368
463
 
369
464
  if result != "INCONCLUSIVE":
370
465
  cap_reasons = []
371
- # Cap 1 — tooling degraded: analysis confidence is "degraded", or a
372
- # missing-helper marker (e.g. python3-missing, frontmatter-validator-missing).
373
- tooling_degraded = analysis_confidence == "degraded" or (
374
- isinstance(tooling_status, str) and "missing" in tooling_status
375
- )
376
- if tooling_degraded:
466
+ # Cap 1: tooling degraded, any toolingStatus other than "ok" (a missing
467
+ # helper, or a validator that timed out). Absent means not reported.
468
+ if tooling_status is not None and tooling_status != TOOLING_OK:
377
469
  cap_fired = True
378
470
  cap_reasons.append(
379
- "tooling degraded — capped below threshold until helper restored"
471
+ f"tooling degraded (toolingStatus: {tooling_status}): capped below threshold "
472
+ "until the tooling is restored"
380
473
  )
381
474
  # Cap 2 — docs-only mode with no external validators available.
382
475
  if docs_only and scores.get("externalValidation") is None:
@@ -390,14 +483,12 @@ def compute_score(inp):
390
483
  if result == "PASS":
391
484
  effective_result = "FAIL"
392
485
 
393
- # Threshold fallback — convert a FAIL into PASS at the 80 floor when the
394
- # RAW totalScore clears 80 and the effective threshold was above 80. The
395
- # fallback reads the raw totalScore (matching score.md §4b's documented
396
- # `totalScore >= 80`), not the capped value; because it also requires
397
- # threshold > 80 (so threshold - 1 >= 80), min(totalScore, threshold - 1)
398
- # is itself >= 80 whenever totalScore >= 80, so raw-vs-capped is the same
399
- # decision — raw is used for spec fidelity and testability.
400
- if effective_result == "FAIL" and total_score >= 80 and threshold > 80:
486
+ # Threshold fallback: an uncapped FAIL passes at the 80 floor when
487
+ # totalScore clears 80 and the threshold was above 80. A cap means the
488
+ # evidence cannot be trusted at any threshold, so a capped run stays
489
+ # FAIL: a threshold above 80 is a target, never a way past a cap.
490
+ if (effective_result == "FAIL" and not cap_fired and total_score >= FALLBACK_FLOOR
491
+ and threshold > FALLBACK_FLOOR):
401
492
  threshold_fallback = True
402
493
  original_threshold = threshold
403
494
  effective_result = "PASS"
@@ -439,6 +530,12 @@ def compute_score(inp):
439
530
  output["thresholdFallback"] = threshold_fallback
440
531
  output["originalThreshold"] = original_threshold
441
532
 
533
+ if state2_deduction is not None:
534
+ output["state2Deduction"] = state2_deduction
535
+
536
+ if scoring_notes:
537
+ output["scoringNotes"] = scoring_notes
538
+
442
539
  if warnings:
443
540
  output["warnings"] = warnings
444
541
 
@@ -484,9 +581,67 @@ def _build_parser() -> argparse.ArgumentParser:
484
581
  action="store_true",
485
582
  help="Read the JSON object from stdin.",
486
583
  )
584
+ files = parser.add_argument_group("score files (each wins over the same JSON field)")
585
+ files.add_argument("--coverage", metavar="PATH", help="reconcile-coverage.py output: exportCoverage")
586
+ files.add_argument("--signatures", metavar="PATH",
587
+ help="score-signatures.py score output: signatureAccuracy and typeCoverage")
588
+ files.add_argument("--coherence", metavar="PATH", help="aggregate-coherence.py output: combinedCoherence")
589
+ files.add_argument("--external", metavar="PATH", help="combine-external-scores.py output: externalScore")
590
+ files.add_argument("--surface", metavar="PATH", help="load-coverage-inputs.py surface output: state2")
487
591
  return parser
488
592
 
489
593
 
594
+ # (flag, [(field in the file, category it fills)]); a null field leaves the category null.
595
+ SCORE_FILES = (
596
+ ("coverage", [("exportCoverage", "exportCoverage")]),
597
+ ("signatures", [("signatureAccuracy", "signatureAccuracy"), ("typeCoverage", "typeCoverage")]),
598
+ ("coherence", [("combinedCoherence", "coherence")]),
599
+ ("external", [("externalScore", "externalValidation")]),
600
+ )
601
+
602
+
603
+ class ScoreFileError(Exception):
604
+ """A score file that cannot be read or does not hold its score."""
605
+
606
+
607
+ def _read_score_file(path, flag):
608
+ try:
609
+ data = json.loads(Path(path).read_bytes().decode("utf-8-sig"))
610
+ except (OSError, UnicodeDecodeError) as exc:
611
+ raise ScoreFileError(f"cannot read --{flag} {path}: {exc}") from exc
612
+ except json.JSONDecodeError as exc:
613
+ raise ScoreFileError(f"--{flag} {path} is not JSON: {exc.msg}") from exc
614
+ if not isinstance(data, dict):
615
+ raise ScoreFileError(f"--{flag} {path} is not a JSON object")
616
+ if "error" in data or data.get("valid") is False:
617
+ raise ScoreFileError(f"--{flag} {path} holds a refused result, not a score")
618
+ return data
619
+
620
+
621
+ def apply_score_files(inp, args):
622
+ """Fill `scores` and `state2Counts` from the score files the flags name."""
623
+ for flag, fields in SCORE_FILES:
624
+ path = getattr(args, flag)
625
+ if path is None:
626
+ continue
627
+ data = _read_score_file(path, flag)
628
+ scores = inp.setdefault("scores", {})
629
+ if not isinstance(scores, dict):
630
+ raise ScoreFileError("scores must be an object")
631
+ for field, category in fields:
632
+ if field not in data:
633
+ raise ScoreFileError(f"--{flag} {path} has no `{field}`")
634
+ scores[category] = data[field]
635
+ if args.surface is not None:
636
+ data = _read_score_file(args.surface, "surface")
637
+ state2 = data.get("state2")
638
+ if isinstance(state2, dict):
639
+ inp["state2Counts"] = {"provenance": state2.get("provenanceCount"),
640
+ "metadata": state2.get("metadataCount"),
641
+ "union": state2.get("unionCount")}
642
+ return inp
643
+
644
+
490
645
  def _resolve_input(args: argparse.Namespace) -> str:
491
646
  if args.stdin:
492
647
  return sys.stdin.read()
@@ -512,17 +667,40 @@ def main(argv: list[str] | None = None) -> int:
512
667
  print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
513
668
  return 1
514
669
 
670
+ if isinstance(data, dict):
671
+ try:
672
+ data = apply_score_files(data, args)
673
+ except ScoreFileError as exc:
674
+ print(json.dumps(make_error(str(exc)), indent=2))
675
+ return 2
676
+
515
677
  result = compute_score(data)
516
678
  print(json.dumps(result, indent=2))
517
679
  # A rejected input exits 2, matching reconcile-coverage.py. Exiting 0 here
518
680
  # made a malformed scoring input indistinguishable from a scored run, so the
519
681
  # error envelope could be skimmed past and the score hand-computed from the
520
- # very numbers the script refused. score.md §3c keys the manual-redistribution
521
- # fallback on this distinction.
682
+ # very numbers the script refused. score.md §3c keys on this distinction: a
683
+ # refused input is corrected, a script that does not run halts.
522
684
  if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
523
685
  return 2
524
686
  return 0
525
687
 
526
688
 
689
+ def _force_utf8(*streams) -> None:
690
+ """Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
691
+
692
+ A Windows console pipes them as cp1252, which cannot print every character
693
+ of the --help text, so --help would stop with UnicodeEncodeError.
694
+ """
695
+ for stream in streams:
696
+ if hasattr(stream, "reconfigure"):
697
+ errors = getattr(stream, "errors", None)
698
+ if errors is None:
699
+ stream.reconfigure(encoding="utf-8")
700
+ else:
701
+ stream.reconfigure(encoding="utf-8", errors=errors)
702
+
703
+
527
704
  if __name__ == "__main__":
705
+ _force_utf8(sys.stdout, sys.stderr)
528
706
  raise SystemExit(main())