bmad-module-skill-forge 2.2.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 +49 -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 +12 -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 +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  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 +209 -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 +15 -5
  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 +28 -19
  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 -59
  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 -90
  348. package/src/skf-setup/references/detect-and-tier.md +58 -67
  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 -679
  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,22 +1,50 @@
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 contextual-coherence aggregator.
7
7
 
8
8
  Pure-function tally + weighted mean for the SKF test-skill workflow
9
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
10
+ which skill, type-import and integration-pattern references are accurate
11
+ (§4) and which integration patterns are complete (§5). This script does the
12
+ counting and the arithmetic those judgments feed: it counts the valid
13
+ references from the per-reference results, then computes the
14
+ reference-validity ratio, the integration-completeness ratio, and their
13
15
  fixed 0.6 / 0.4 weighted mean. That way the 18%-weight `coherence` input to
14
16
  compute-score.py is computed once, deterministically, instead of by hand on
15
17
  every run (this skill grades other skills — a false PASS is catastrophic, so
16
18
  every scoring input is scripted for run-to-run reproducibility).
17
19
 
18
- Formula (mirrors scoring-rules.md "Coherence Score Aggregation (Contextual
19
- Mode)" — the prose there remains the documented contract):
20
+ Per-reference input (the files coherence-check.md §5c passes):
21
+ --references skf-scan-skill-md-structure.py reference-check output: its
22
+ `references[]` hold the file-path and script/asset
23
+ references, each valid when its `status` is "ok"
24
+ ("missing": no file inside an allowed root; "escapes":
25
+ outside every root)
26
+ --judged the §4 subagent results for the other references (skill,
27
+ type-import, integration-pattern), as the subagent returned
28
+ them (a wrapping markdown fence is stripped): a JSON array,
29
+ or an object with a `references` array, of
30
+ {"reference", "line", "target_exists", "type_match",
31
+ "signature_match", "issues"}; valid when the three
32
+ booleans are true and `issues` is empty. Optional: a skill
33
+ with no such reference has no file.
34
+ --integration the §5 integration JSON ({"patterns_documented",
35
+ "patterns_complete", ...}, fence stripped)
36
+ `total_references` is the number of entries in both, `valid_references`
37
+ the number of valid ones, and `invalidReferences[]` lists each invalid
38
+ one with its `status`: "missing" or "escapes" from the scan, and for a
39
+ judged one "missing" (target_exists false) or "inaccurate". Each entry
40
+ also carries what coherence-check.md §5c needs for its gap, so the
41
+ caller never matches it back to the per-reference files: a scanned
42
+ one's `canonical` (the realpath the scanner checked, which an escape
43
+ names) and `root`, and a judged one's `issues` (when the subagent gave
44
+ none, the flags that are false, such as "type_match: false").
45
+
46
+ Formula (this script is its one home: scoring-rules.md points here, and
47
+ coherence-check.md §5 defines what makes a pattern complete):
20
48
 
21
49
  reference_validity = (valid_references / total_references) * 100
22
50
  integration_completeness = (complete_patterns / total_patterns) * 100
@@ -27,7 +55,7 @@ Field-name mapping: the step-04 §5 integration JSON calls the pattern counts
27
55
  (= complete_patterns); this script's input uses those step-04 names directly
28
56
  so §5c passes them through unrenamed.
29
57
 
30
- Edge cases (both documented in scoring-rules.md — absence is never penalized):
58
+ Edge cases (absence is never penalized):
31
59
  * patterns_documented == 0 -> no integration patterns to weigh, so
32
60
  combined_coherence == reference_validity and integrationCompleteness is
33
61
  null. (Do not divide by zero.)
@@ -46,11 +74,18 @@ Input schema (one JSON object):
46
74
 
47
75
  Output (JSON):
48
76
  {
49
- "input": { ...echo... },
77
+ "input": { ...echo, or the counts the files gave... },
50
78
  "referenceValidity": <0-100>,
51
79
  "integrationCompleteness": <0-100 | null when patterns_documented == 0>,
52
80
  "combinedCoherence": <0-100>,
53
- "patternsScored": <bool>
81
+ "patternsScored": <bool>,
82
+ "invalidReferences": [{"source": "scan" | "judged", "line": N | null,
83
+ "target": "...", "status": "...",
84
+ "canonical": "..." | null, "root": "..." | null,
85
+ "issues": ["..."]}]
86
+ # with the per-reference files only; canonical
87
+ # and root are null for a judged entry, and
88
+ # issues is empty for a scanned one
54
89
  }
55
90
  or {"error": ..., "code": "INVALID_INPUT"} on a schema violation.
56
91
 
@@ -58,14 +93,20 @@ Percentages use the same JavaScript-compatible 2-decimal rounding as
58
93
  compute-score.py so the two scoring scripts agree to the last digit.
59
94
 
60
95
  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
96
+ uv run aggregate-coherence.py --references <scan.json> [--judged <file>] --integration <file>
97
+ uv run aggregate-coherence.py '<JSON>' # counts, positional arg
98
+ uv run aggregate-coherence.py --json-input '<JSON>' # counts, explicit flag form
99
+ cat input.json | uv run aggregate-coherence.py --stdin # counts, piped input
100
+
101
+ --output <file> also writes the result to a file (UTF-8 JSON), the one
102
+ compute-score.py --coherence reads; a refused input removes a file left
103
+ there earlier.
64
104
 
65
105
  Exit codes (same convention as compute-score.py / reconcile-coverage.py):
66
- 0 — a result object was emitted
67
- 1 — input could not be parsed at all (no input provided, or malformed JSON)
68
- 2 — input parsed but schema/semantics invalid
106
+ 0 a result object was emitted
107
+ 1 input could not be parsed at all (no input provided, malformed JSON,
108
+ or a file that cannot be read)
109
+ 2 input parsed but schema/semantics invalid
69
110
 
70
111
  Both 1 and 2 emit an {"error": ..., "code": "INVALID_INPUT"} envelope on stdout,
71
112
  so the envelope's presence — not the specific code — tells a caller the input was
@@ -75,11 +116,13 @@ refused rather than scored.
75
116
  from __future__ import annotations
76
117
 
77
118
  import argparse
119
+ import importlib.util
78
120
  import json
79
121
  import math
80
122
  import sys
123
+ from pathlib import Path
81
124
 
82
- # Weights for the combined-coherence mean (from scoring-rules.md). Kept as
125
+ # Weights for the combined-coherence mean. Kept as
83
126
  # named constants so the 0.6 / 0.4 split lives in exactly one place.
84
127
  REFERENCE_VALIDITY_WEIGHT = 0.6
85
128
  INTEGRATION_COMPLETENESS_WEIGHT = 0.4
@@ -91,6 +134,26 @@ COUNT_FIELDS = (
91
134
  "patterns_complete",
92
135
  )
93
136
 
137
+ # reference-check statuses (skf-scan-skill-md-structure.py); only "ok" is valid.
138
+ SCAN_STATUSES = ("ok", "missing", "escapes")
139
+ JUDGED_FLAGS = ("target_exists", "type_match", "signature_match")
140
+
141
+ _INVENTORY_MODULE = None
142
+
143
+
144
+ def _inventory_module():
145
+ """validate-inventory.py beside this script: the fence rule lives there."""
146
+ global _INVENTORY_MODULE
147
+ if _INVENTORY_MODULE is None:
148
+ sibling = Path(__file__).resolve().parent / "validate-inventory.py"
149
+ spec = importlib.util.spec_from_file_location("skf_validate_inventory", sibling)
150
+ if spec is None or spec.loader is None or not sibling.is_file():
151
+ raise ImportError(f"validate-inventory.py not found beside {Path(__file__).name}")
152
+ module = importlib.util.module_from_spec(spec)
153
+ spec.loader.exec_module(module)
154
+ _INVENTORY_MODULE = module
155
+ return _INVENTORY_MODULE
156
+
94
157
 
95
158
  def round2(value):
96
159
  """Round to 2 decimals with JavaScript-compatible half-up rounding.
@@ -207,6 +270,98 @@ def aggregate_coherence(inp):
207
270
  }
208
271
 
209
272
 
273
+ def count_references(scan, judged):
274
+ """Count the valid references from the per-reference results.
275
+
276
+ `scan` is the reference-check output (an object with `references[]`),
277
+ `judged` the list of subagent results (or None). Returns
278
+ (valid, total, invalid[]) or an error string.
279
+
280
+ >>> scan = {"references": [{"line": 3, "target": "a.md", "status": "ok"},
281
+ ... {"line": 5, "target": "../x", "status": "escapes"}]}
282
+ >>> judged = [{"reference": "react", "line": 9, "target_exists": True, "type_match": True,
283
+ ... "signature_match": True, "issues": []},
284
+ ... {"reference": "./types", "line": 12, "target_exists": True, "type_match": False,
285
+ ... "signature_match": True, "issues": []}]
286
+ >>> valid, total, invalid = count_references(scan, judged)
287
+ >>> valid, total, [(i["target"], i["status"]) for i in invalid]
288
+ (2, 4, [('../x', 'escapes'), ('./types', 'inaccurate')])
289
+ >>> invalid[1]["issues"], invalid[1]["canonical"]
290
+ (['type_match: false'], None)
291
+ """
292
+ refs = scan.get("references") if isinstance(scan, dict) else None
293
+ if not isinstance(refs, list):
294
+ return "--references holds no `references` array (pass the reference-check output)"
295
+ valid, invalid = 0, []
296
+ for index, ref in enumerate(refs):
297
+ if not isinstance(ref, dict) or ref.get("status") not in SCAN_STATUSES:
298
+ return f"--references references[{index}] has no status in {list(SCAN_STATUSES)}"
299
+ if ref["status"] == "ok":
300
+ valid += 1
301
+ else:
302
+ invalid.append({"source": "scan", "line": ref.get("line"),
303
+ "target": ref.get("target"), "status": ref["status"],
304
+ "canonical": ref.get("canonical"), "root": ref.get("root"),
305
+ "issues": []})
306
+ for index, ref in enumerate(judged or []):
307
+ if not isinstance(ref, dict):
308
+ return f"--judged [{index}] is not an object"
309
+ for flag in JUDGED_FLAGS:
310
+ if not isinstance(ref.get(flag), bool):
311
+ return f"--judged [{index}] `{flag}` must be true or false"
312
+ issues = ref.get("issues", [])
313
+ if not isinstance(issues, list):
314
+ return f"--judged [{index}] `issues` must be a list"
315
+ if all(ref[flag] for flag in JUDGED_FLAGS) and not issues:
316
+ valid += 1
317
+ else:
318
+ invalid.append({"source": "judged", "line": ref.get("line"), "target": ref.get("reference"),
319
+ "status": "missing" if not ref["target_exists"] else "inaccurate",
320
+ "canonical": None, "root": None,
321
+ "issues": [str(i) for i in issues]
322
+ or [f"{flag}: false" for flag in JUDGED_FLAGS if not ref[flag]]})
323
+ return valid, len(refs) + len(judged or []), invalid
324
+
325
+
326
+ def _read_json_file(path, label, fenced=False):
327
+ """Parse a JSON file; a subagent response may come wrapped in a markdown fence."""
328
+ text = Path(path).read_bytes().decode("utf-8-sig")
329
+ if fenced:
330
+ text = _inventory_module().strip_fences(text)
331
+ try:
332
+ return json.loads(text)
333
+ except json.JSONDecodeError as exc:
334
+ raise ValueError(f"{label} {path} is not valid JSON: {exc.msg}") from exc
335
+
336
+
337
+ def aggregate_from_files(references, judged=None, integration=None):
338
+ """Aggregate from the per-reference files and the §5 integration JSON."""
339
+ scan = _read_json_file(references, "--references")
340
+ results = None
341
+ if judged is not None:
342
+ results = _read_json_file(judged, "--judged", fenced=True)
343
+ if isinstance(results, dict):
344
+ results = results.get("references")
345
+ if not isinstance(results, list):
346
+ return make_error(f"--judged {judged} holds no array of reference results")
347
+ patterns = _read_json_file(integration, "--integration", fenced=True)
348
+ if not isinstance(patterns, dict):
349
+ return make_error(f"--integration {integration} is not a JSON object")
350
+ counted = count_references(scan, results)
351
+ if isinstance(counted, str):
352
+ return make_error(counted)
353
+ valid, total, invalid = counted
354
+ result = aggregate_coherence({
355
+ "valid_references": valid,
356
+ "total_references": total,
357
+ "patterns_documented": patterns.get("patterns_documented"),
358
+ "patterns_complete": patterns.get("patterns_complete"),
359
+ })
360
+ if result.get("code") != "INVALID_INPUT":
361
+ result["invalidReferences"] = invalid
362
+ return result
363
+
364
+
210
365
  def _build_parser() -> argparse.ArgumentParser:
211
366
  parser = argparse.ArgumentParser(
212
367
  prog="aggregate-coherence",
@@ -240,9 +395,25 @@ def _build_parser() -> argparse.ArgumentParser:
240
395
  action="store_true",
241
396
  help="Read the JSON object from stdin.",
242
397
  )
398
+ parser.add_argument("--references", metavar="PATH",
399
+ help="skf-scan-skill-md-structure.py reference-check output")
400
+ parser.add_argument("--judged", metavar="PATH",
401
+ help="the subagent results for the skill, type-import and integration references")
402
+ parser.add_argument("--integration", metavar="PATH",
403
+ help="the integration JSON (patterns_documented, patterns_complete)")
404
+ parser.add_argument("--output", metavar="PATH", help="also write the result to this file")
243
405
  return parser
244
406
 
245
407
 
408
+ def _write_output(path, result):
409
+ target = Path(path)
410
+ if result.get("code") == "INVALID_INPUT":
411
+ target.unlink(missing_ok=True)
412
+ return
413
+ target.parent.mkdir(parents=True, exist_ok=True)
414
+ target.write_bytes((json.dumps(result, indent=2) + "\n").encode("utf-8"))
415
+
416
+
246
417
  def _resolve_input(args: argparse.Namespace) -> str:
247
418
  if args.stdin:
248
419
  return sys.stdin.read()
@@ -256,22 +427,39 @@ def _resolve_input(args: argparse.Namespace) -> str:
256
427
  def main(argv: list[str] | None = None) -> int:
257
428
  parser = _build_parser()
258
429
  args = parser.parse_args(argv)
259
- raw = _resolve_input(args)
260
- if not raw.strip():
261
- parser.print_usage(file=sys.stderr)
262
- print(
263
- "error: no input provided (positional arg, --json-input, or --stdin)",
264
- file=sys.stderr,
265
- )
266
- return 1
430
+ files = (args.references, args.judged, args.integration)
431
+ if any(f is not None for f in files):
432
+ if args.stdin or args.json_input_flag is not None or args.json_input is not None:
433
+ parser.error("pass the counts or the files (--references, --judged, --integration), not both")
434
+ if args.references is None or args.integration is None:
435
+ parser.error("--references and --integration go together (--judged is optional)")
436
+ try:
437
+ result = aggregate_from_files(*files)
438
+ except (OSError, UnicodeDecodeError, ValueError) as exc:
439
+ print(json.dumps(make_error(str(exc)), indent=2))
440
+ if args.output:
441
+ Path(args.output).unlink(missing_ok=True)
442
+ return 1
443
+ else:
444
+ raw = _resolve_input(args)
445
+ if not raw.strip():
446
+ parser.print_usage(file=sys.stderr)
447
+ print(
448
+ "error: no input provided (--references and --integration, positional arg, "
449
+ "--json-input, or --stdin)",
450
+ file=sys.stderr,
451
+ )
452
+ return 1
267
453
 
268
- try:
269
- data = json.loads(raw)
270
- except json.JSONDecodeError as exc:
271
- print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
272
- return 1
454
+ try:
455
+ data = json.loads(raw)
456
+ except json.JSONDecodeError as exc:
457
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
458
+ return 1
273
459
 
274
- result = aggregate_coherence(data)
460
+ result = aggregate_coherence(data)
461
+ if args.output:
462
+ _write_output(args.output, result)
275
463
  print(json.dumps(result, indent=2))
276
464
  # Same convention as compute-score.py / reconcile-coverage.py. This script
277
465
  # produces the coherence percentage that score.md §3a feeds to
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = []
5
+ # ///
6
+ """SKF test-skill result context: the emitter payload of a finished run.
7
+
8
+ report.md §4c hands the shared emitter (`skf-emit-result-envelope.py emit
9
+ --workflow skf-test-skill`) one payload, which the emitter turns into the
10
+ SKF_TEST_RESULT_JSON line and the run's result files. Every value in it
11
+ follows from records the run already wrote, so this script builds it rather
12
+ than the model retyping them from memory:
13
+
14
+ - the report frontmatter: skillName, runId, testMode, hardGate,
15
+ testResult, score, threshold, thresholdFallback, originalThreshold and
16
+ evidenceReportPath (the steps write the verdict inputs there, so a
17
+ compacted context cannot change them);
18
+ - the gap ledger: the gap counts by severity (gap-ledger.py's summary);
19
+ - the scoring script's output, which score.md saves: activeCategories and
20
+ inconclusiveReasons. A run the hard gate blocked was never scored and
21
+ has none.
22
+
23
+ The derived fields live here, in one place:
24
+
25
+ testResult verdict summary.result exit_code next_workflow
26
+ pass PASS PASS 0 export-skill
27
+ pass-with-drift pass-with-drift PASS_WITH_DRIFT 4 update-skill
28
+ fail FAIL FAIL 2 update-skill
29
+ inconclusive INCONCLUSIVE INCONCLUSIVE 3 null
30
+
31
+ A run the hard gate blocked (frontmatter hardGate: 'blocked') takes the
32
+ hard gate's own payload (hard-gate.py `blocked_envelope`): status error,
33
+ halt_reason hard-gate-blocked, verdict FAIL, exit 2, score and threshold
34
+ null. threshold_fallback and original_threshold appear only when the
35
+ threshold fallback fired: absent, never false or null, otherwise.
36
+
37
+ Payload (written to --output, UTF-8 JSON):
38
+ {<the envelope fields: status, skill_name, verdict, score, threshold,
39
+ report_path, next_workflow, exit_code, halt_reason, and
40
+ threshold_fallback and original_threshold when the fallback fired>,
41
+ "warnings": [<each --warning, in order>] (only when one is given),
42
+ "result_contract": {"skill": "skf-test-skill", <the envelope fields>,
43
+ "outputs": [{"type": "report", "path": <report>}],
44
+ "summary": {"score", "threshold", "result", "testMode",
45
+ "activeCategories", "inconclusiveReasons" (INCONCLUSIVE),
46
+ "threshold_fallback", "original_threshold",
47
+ "evidence_report_path" (fallback only), "gapCounts",
48
+ "hardGate"},
49
+ "runId": <runId>, "healthCheckDispatched": <bool>}}
50
+
51
+ CLI:
52
+ uv run build-result-context.py --report <report.md> --ledger <ledger.json>
53
+ [--score <scoring output>] [--no-health-check] [--warning <text>]...
54
+ --output <result-context.json>
55
+
56
+ --warning hands the emitter a warning the run kept for its result (such as
57
+ `customization_resolver_unavailable: <reason>`); the emitter adds the ones
58
+ the run recorded in its sink, and stamps all of them into the envelope and
59
+ the result files.
60
+
61
+ stdout (one object): {"status", "verdict", "exit_code", "next_workflow",
62
+ "halt_reason", "target", "output"}; `target` is the --target the emit call
63
+ takes: "stderr" for a blocked run, else "stdout".
64
+
65
+ Exit codes:
66
+ 0 the payload was written
67
+ 1 an input is missing or unreadable, the report's frontmatter holds no
68
+ settled verdict, or --output cannot be written ({"status": "error",
69
+ "error"} on stdout)
70
+ """
71
+
72
+ from __future__ import annotations
73
+
74
+ import argparse
75
+ import importlib.util
76
+ import json
77
+ import re
78
+ import sys
79
+ from pathlib import Path
80
+
81
+ WORKFLOW = "skf-test-skill"
82
+ # testResult -> (verdict, summary.result, exit_code, next_workflow)
83
+ VERDICTS = {
84
+ "pass": ("PASS", "PASS", 0, "export-skill"),
85
+ "pass-with-drift": ("pass-with-drift", "PASS_WITH_DRIFT", 4, "update-skill"),
86
+ "fail": ("FAIL", "FAIL", 2, "update-skill"),
87
+ "inconclusive": ("INCONCLUSIVE", "INCONCLUSIVE", 3, None),
88
+ }
89
+ _KEY_RE = re.compile(r"^([A-Za-z_][\w-]*):\s*(.*?)\s*$")
90
+
91
+
92
+ class ContextError(Exception):
93
+ """An input the payload cannot be built from: exit 1."""
94
+
95
+
96
+ def _sibling(name: str, module: str):
97
+ """Import a script that sits beside this one (gap-ledger.py, hard-gate.py)."""
98
+ path = Path(__file__).resolve().parent / name
99
+ spec = importlib.util.spec_from_file_location(module, path)
100
+ if spec is None or spec.loader is None or not path.is_file():
101
+ raise ContextError(f"{name} not found beside {Path(__file__).name}")
102
+ loaded = importlib.util.module_from_spec(spec)
103
+ spec.loader.exec_module(loaded)
104
+ return loaded
105
+
106
+
107
+ def read_frontmatter(text: str) -> dict[str, str]:
108
+ """Top-level scalar keys of the leading `---` block, quotes removed.
109
+
110
+ >>> read_frontmatter("---\\ntestResult: 'pass'\\nscore: 91.5%\\nsteps: ['a']\\n---\\n# R")
111
+ {'testResult': 'pass', 'score': '91.5%', 'steps': "['a']"}
112
+ >>> read_frontmatter("# no frontmatter")
113
+ {}
114
+ """
115
+ lines = text.lstrip("").split("\n")
116
+ if not lines or lines[0].strip() != "---":
117
+ return {}
118
+ out: dict[str, str] = {}
119
+ for line in lines[1:]:
120
+ if line.strip() == "---":
121
+ return out
122
+ m = _KEY_RE.match(line.rstrip("\r"))
123
+ if not m:
124
+ continue
125
+ value = m.group(2)
126
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
127
+ value = value[1:-1]
128
+ out[m.group(1)] = value
129
+ return {}
130
+
131
+
132
+ def percent(value: str | None):
133
+ """'87.5%' -> 87.5, '90%' or '90' -> 90, '' or None -> None.
134
+
135
+ >>> percent("87.5%"), percent("90"), percent(""), percent(None)
136
+ (87.5, 90, None, None)
137
+ """
138
+ text = (value or "").strip().rstrip("%").strip()
139
+ if not text:
140
+ return None
141
+ try:
142
+ number = float(text)
143
+ except ValueError:
144
+ raise ContextError(f"not a percentage: {value!r}") from None
145
+ return int(number) if number.is_integer() else number
146
+
147
+
148
+ def _read_json(path: str, label: str):
149
+ try:
150
+ return json.loads(Path(path).read_bytes().decode("utf-8-sig"))
151
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
152
+ raise ContextError(f"{label} {path} cannot be read: {exc}") from exc
153
+
154
+
155
+ def build(report: str, ledger: str, score: str | None = None, health_check: bool = True,
156
+ warnings: list[str] | None = None) -> dict:
157
+ """The emitter payload for the run whose report and ledger are given."""
158
+ try:
159
+ front = read_frontmatter(Path(report).read_bytes().decode("utf-8-sig"))
160
+ except (OSError, UnicodeDecodeError) as exc:
161
+ raise ContextError(f"--report {report} cannot be read: {exc}") from exc
162
+ if not front:
163
+ raise ContextError(f"--report {report} has no frontmatter")
164
+ gl = _sibling("gap-ledger.py", "skf_gap_ledger")
165
+ try:
166
+ counts = gl.summarize(Path(ledger), gl.load_ledger(Path(ledger)))["counts"]
167
+ except gl.LedgerError as exc:
168
+ raise ContextError(f"--ledger {ledger}: {exc}") from exc
169
+
170
+ skill = front.get("skillName") or None
171
+ blocked = front.get("hardGate") == "blocked"
172
+ if blocked:
173
+ envelope = dict(_sibling("hard-gate.py", "skf_hard_gate").blocked_envelope(skill, report))
174
+ result, active, inconclusive = "FAIL", [], None
175
+ else:
176
+ test_result = front.get("testResult", "")
177
+ if test_result not in VERDICTS:
178
+ raise ContextError(f"--report {report} holds no settled verdict (testResult {test_result!r})")
179
+ verdict, result, exit_code, next_workflow = VERDICTS[test_result]
180
+ envelope = {
181
+ "status": "success",
182
+ "skill_name": skill,
183
+ "verdict": verdict,
184
+ "score": percent(front.get("score")),
185
+ "threshold": percent(front.get("threshold")),
186
+ "report_path": report,
187
+ "next_workflow": next_workflow,
188
+ "exit_code": exit_code,
189
+ "halt_reason": None,
190
+ }
191
+ scored = _read_json(score, "--score") if score else {}
192
+ if not isinstance(scored, dict):
193
+ raise ContextError(f"--score {score} is not a JSON object")
194
+ active = scored.get("activeCategories", [])
195
+ inconclusive = scored.get("inconclusiveReasons") if verdict == "INCONCLUSIVE" else None
196
+
197
+ fallback = front.get("thresholdFallback", "").lower() == "true" and not blocked
198
+ if fallback:
199
+ envelope["threshold_fallback"] = True
200
+ envelope["original_threshold"] = percent(front.get("originalThreshold"))
201
+
202
+ summary = {
203
+ "score": envelope["score"],
204
+ "threshold": envelope["threshold"],
205
+ "result": result,
206
+ "testMode": front.get("testMode") or None,
207
+ "activeCategories": active,
208
+ }
209
+ if inconclusive is not None:
210
+ summary["inconclusiveReasons"] = inconclusive
211
+ if fallback:
212
+ summary["threshold_fallback"] = True
213
+ summary["original_threshold"] = envelope["original_threshold"]
214
+ summary["evidence_report_path"] = front.get("evidenceReportPath") or None
215
+ summary["gapCounts"] = counts
216
+ summary["hardGate"] = front.get("hardGate") or None
217
+
218
+ contract = {"skill": WORKFLOW, **envelope,
219
+ "outputs": [{"type": "report", "path": report}],
220
+ "summary": summary,
221
+ "runId": front.get("runId") or None,
222
+ "healthCheckDispatched": health_check}
223
+ payload = {**envelope, "result_contract": contract}
224
+ kept = [w.strip() for w in warnings or [] if w and w.strip()]
225
+ if kept:
226
+ payload["warnings"] = kept
227
+ return payload
228
+
229
+
230
+ def _build_parser() -> argparse.ArgumentParser:
231
+ parser = argparse.ArgumentParser(
232
+ prog="build-result-context",
233
+ description=("Build the shared emitter's payload for a finished test-skill run from its "
234
+ "report frontmatter, its gap ledger and the scoring output."),
235
+ )
236
+ parser.add_argument("--report", required=True, help="the test report, at its published name")
237
+ parser.add_argument("--ledger", required=True, help="the run's gap ledger")
238
+ parser.add_argument("--score", help="the scoring script's output (absent for a blocked run)")
239
+ parser.add_argument("--no-health-check", action="store_true",
240
+ help="the run was given --no-health-check (healthCheckDispatched false)")
241
+ parser.add_argument("--warning", action="append", default=[],
242
+ help="a warning the run kept for its result (repeatable)")
243
+ parser.add_argument("--output", required=True, help="where to write the payload")
244
+ return parser
245
+
246
+
247
+ def main(argv: list[str] | None = None) -> int:
248
+ args = _build_parser().parse_args(argv)
249
+ try:
250
+ payload = build(args.report, args.ledger, args.score, not args.no_health_check, args.warning)
251
+ out = Path(args.output)
252
+ out.parent.mkdir(parents=True, exist_ok=True)
253
+ out.write_bytes((json.dumps(payload, indent=2, ensure_ascii=False) + "\n").encode("utf-8"))
254
+ except (ContextError, OSError) as exc:
255
+ print(json.dumps({"status": "error", "error": str(exc)}))
256
+ return 1
257
+ blocked = payload["halt_reason"] is not None
258
+ print(json.dumps({
259
+ "status": payload["status"],
260
+ "verdict": payload["verdict"],
261
+ "exit_code": payload["exit_code"],
262
+ "next_workflow": payload["next_workflow"],
263
+ "halt_reason": payload["halt_reason"],
264
+ "target": "stderr" if blocked else "stdout",
265
+ "output": out.as_posix(),
266
+ }, indent=2))
267
+ return 0
268
+
269
+
270
+ def _force_utf8(*streams) -> None:
271
+ """Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
272
+
273
+ A Windows console pipes them as cp1252, which cannot print every character
274
+ of the --help text, so --help would stop with UnicodeEncodeError.
275
+ """
276
+ for stream in streams:
277
+ if hasattr(stream, "reconfigure"):
278
+ errors = getattr(stream, "errors", None)
279
+ if errors is None:
280
+ stream.reconfigure(encoding="utf-8")
281
+ else:
282
+ stream.reconfigure(encoding="utf-8", errors=errors)
283
+
284
+
285
+ if __name__ == "__main__":
286
+ _force_utf8(sys.stdout, sys.stderr)
287
+ sys.exit(main())