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,12 +1,15 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
- # requires-python = ">=3.10"
3
+ # requires-python = ">=3.11"
4
4
  # ///
5
- """SKF Validate Feasibility Report — deterministic structure/schema gate.
5
+ """SKF Validate Feasibility Report: deterministic structure/schema gate and locator.
6
6
 
7
- Validates a skf-verify-stack feasibility report (.md) against the two
8
- producer↔consumer contract invariants the report.md §1 prompt previously
9
- eyeballed by re-reading the file each run:
7
+ Checks and reads a skf-verify-stack feasibility report (.md) against the
8
+ shared feasibility-report schema, so neither the producer nor a consumer
9
+ re-reads the file in prose. Given a report path (validate mode) it checks
10
+ that report; given --locate it first finds the project's report. Both modes
11
+ check and read a report the same way (read_report()), so an explicit path
12
+ and a located report give the same keys:
10
13
 
11
14
  1. The five required body sections are all present and appear in the
12
15
  canonical order defined by the shared feasibility-report schema:
@@ -16,37 +19,106 @@ eyeballed by re-reading the file each run:
16
19
  ## Recommendations
17
20
  ## Evidence Sources
18
21
  2. Frontmatter `schemaVersion` equals the literal producer version "1.0".
19
-
20
- Both are pure structure/schema checks — identical input always yields
21
- identical pass/fail — so this belongs in a deterministic script, not an LLM
22
- re-read. The verdict is emitted as JSON on stdout:
22
+ 3. A report whose schemaVersion is not "1.0" is not interpreted: nothing
23
+ below is read from it. Otherwise overallVerdict, generatedAt and
24
+ coveragePercentage come from the frontmatter, and pairVerdicts from the
25
+ canonical `| lib_a | lib_b | verdict | rationale |` table under
26
+ `## Integration Verdicts`, and only that table (the display table with
27
+ more columns below it is not read). Fenced code and HTML comments are
28
+ skipped, so an example table in either is not read. The report holds
29
+ the canonical table once: a second one is a schema violation
30
+ (duplicateVerdictTableLine), since reading only the first, say the
31
+ template's empty table left above the filled one, would drop every
32
+ pair. Verdict tokens are case-sensitive: a token outside the schema's
33
+ set is listed in unknownTokens, never dropped or mapped, and its row
34
+ stays in pairVerdicts as written.
35
+ 4. coveragePercentage is the frontmatter value when it is a whole number
36
+ from 0 to 100, else null. coverageMeasured is true only when the
37
+ frontmatter's stepsCompleted list names `coverage` and
38
+ coveragePercentage is not null: the producer writes
39
+ `coveragePercentage: 0` when a run starts, so a run that stopped before
40
+ its coverage step holds a 0 that was never measured.
41
+
42
+ Every check has one right answer for a given file, so it belongs in a
43
+ script, not in an LLM re-read. Frontmatter is parsed with the standard
44
+ library only (no pyyaml dep) using the same delimiter handling as
45
+ skf-validate-output.py: a `---`-delimited block at the top of the file,
46
+ parsed as simple `key: value` scalars with surrounding quotes stripped, and
47
+ stepsCompleted read as a flow (`[a, b]`) or block (`- a`) list.
48
+
49
+ Locate mode (--locate <folder> --project-name <name>) finds the report for
50
+ a consumer, so no workflow builds the file name in prose:
51
+
52
+ 1. The project name becomes the producer's `{project_slug}` (slugify()):
53
+ NFKD-decomposed with combining marks dropped, so `Café` gives `cafe`;
54
+ letters and digits with no ASCII form dropped; lowercased; every run of
55
+ other characters (spaces, punctuation, symbols) turned into one hyphen;
56
+ hyphens trimmed from both ends; `project` when nothing is left, which
57
+ includes an empty name.
58
+ 2. Only <folder>/feasibility-report-<slug>-latest.md is read, the stable
59
+ copy: a timestamped report can be a halted run's partial report.
60
+
61
+ The JSON on stdout (validate mode leaves out the three locate keys, and
62
+ only validate mode carries schemaVersionFound, which earlier callers read):
23
63
 
24
64
  {
25
- "status": "ok" | "error",
26
- "path": "<report path>",
27
- "schemaVersionOk": bool,
28
- "schemaVersionFound": "<value>" | null,
29
- "headingsOk": bool,
65
+ "status": "ok" | "not-found" | "error", # not-found: locate mode only
66
+ "projectName": "<name>", # locate mode only
67
+ "projectSlug": "<slug>", # locate mode only
68
+ "latestPath": "<folder>/feasibility-report-<slug>-latest.md", # locate mode only
69
+ "path": "<report path>" | null, # null when locate found no report
70
+ "schemaVersion": "<value>" | null,
71
+ "schemaVersionFound": "<value>" | null, # validate mode only
72
+ "schemaVersionOk": bool | null, # null when the report was not read
73
+ "headingsOk": bool | null,
30
74
  "missingHeadings": [...],
31
75
  "orderViolations": [...],
32
- "violation": "schema-violation" | null
76
+ "generatedAt": "<value>" | null, # null when the report was not interpreted
77
+ "overallVerdict": "<token>" | null,
78
+ "coveragePercentage": int | null,
79
+ "coverageMeasured": bool | null,
80
+ "verdictTableFound": bool | null,
81
+ "duplicateVerdictTableLine": int | null,
82
+ "pairVerdicts": [{"lib_a", "lib_b", "verdict", "rationale"}, ...],
83
+ "unknownTokens": [{"field": "overallVerdict", "token"},
84
+ {"field": "verdict", "line", "lib_a", "lib_b", "token"}],
85
+ "violation": "schema-violation" | "io-error" | null,
86
+ "error": "<message>" # on io-error only
33
87
  }
34
88
 
35
- Frontmatter is parsed with the standard library only (no pyyaml dep) using the
36
- same delimiter handling as skf-validate-output.py: a `---`-delimited block at
37
- the top of the file, parsed as simple `key: value` scalars with surrounding
38
- quotes stripped. This is sufficient for the single `schemaVersion` scalar and
39
- keeps the script stdlib-only.
40
-
41
- Exit codes (per script-standards):
42
- 0 valid — all sections present + in order AND schemaVersion == "1.0"
43
- 1 schema-violation — missing/mis-ordered section, or schemaVersion mismatch
44
- (the JSON `violation` field + detail lists carry why)
45
- 2 IO/parse error — report file could not be read
89
+ `line` is the 1-based line of the table row in the report file, and
90
+ duplicateVerdictTableLine the 1-based line of the second canonical table's
91
+ header row (null when there is none). A report validate mode cannot read
92
+ keeps the values earlier callers read (schemaVersionOk and headingsOk
93
+ false). The producer can take `projectSlug` from any locate result,
94
+ whatever the status.
95
+
96
+ Exit codes, validate mode (per script-standards):
97
+ 0 valid: every check above passes (the five sections present and in
98
+ order, schemaVersion "1.0", the canonical table there once and every
99
+ verdict token known)
100
+ 1 schema-violation: a section is missing or out of order, schemaVersion
101
+ is not "1.0", the canonical table is missing or there twice, or a
102
+ verdict token is unknown (the JSON `violation` field and the detail
103
+ keys carry why)
104
+ 2 IO/parse error: the report file could not be read (violation
105
+ "io-error"). Also a usage error, which prints no JSON, such as a
106
+ second path (an unquoted report path that holds a space)
107
+
108
+ Exit codes, locate mode:
109
+ 0 status "ok": the report exists and passes every check above. Also
110
+ status "not-found": there is no -latest report for this project, and
111
+ the caller goes on without one
112
+ 1 schema-violation, as in validate mode
113
+ 2 the report exists but could not be read (violation "io-error"). Also a
114
+ usage error, which prints no JSON: --locate without --project-name,
115
+ --project-name without --locate, an empty folder, or a stray argument
116
+ (an unquoted folder path that holds a space)
46
117
 
47
118
  CLI:
48
119
  python3 skf-validate-feasibility-report.py <report.md>
49
120
  python3 skf-validate-feasibility-report.py <report.md> -o result.json
121
+ python3 skf-validate-feasibility-report.py --locate <folder> --project-name <name>
50
122
  """
51
123
 
52
124
  from __future__ import annotations
@@ -55,6 +127,7 @@ import argparse
55
127
  import json
56
128
  import re
57
129
  import sys
130
+ import unicodedata
58
131
  from pathlib import Path
59
132
 
60
133
  # Canonical body-section order per the shared feasibility-report schema
@@ -71,29 +144,84 @@ CANONICAL_HEADINGS = [
71
144
  # Producer schema version the consumer/producer both pin to.
72
145
  EXPECTED_SCHEMA_VERSION = "1.0"
73
146
 
147
+ # Verdict tokens per the shared schema; both sets are case-sensitive.
148
+ OVERALL_VERDICTS = ("FEASIBLE", "CONDITIONALLY_FEASIBLE", "NOT_FEASIBLE")
149
+ PAIR_VERDICTS = ("Verified", "Plausible", "Risky", "Blocked")
150
+
151
+ # The section that holds the pair table, and the table's fixed header row.
152
+ VERDICTS_HEADING = "Integration Verdicts"
153
+ VERDICT_TABLE_HEADER = ["lib_a", "lib_b", "verdict", "rationale"]
154
+
155
+ # The frontmatter list of the producer steps a run finished, and the step
156
+ # that measures coveragePercentage: a run that stopped before it holds the 0
157
+ # the producer writes when it starts.
158
+ STEPS_COMPLETED_KEY = "stepsCompleted"
159
+ COVERAGE_STEP = "coverage"
160
+
161
+ # The stable copy the producer writes next to each timestamped report. --locate
162
+ # reads only this file: a timestamped report can be a halted run's partial one.
163
+ LATEST_REPORT_NAME = "feasibility-report-{slug}-latest.md"
164
+
165
+ # The slug of a project name that leaves no ASCII letter or digit (a name
166
+ # written only in a script with no ASCII form, such as Japanese).
167
+ FALLBACK_SLUG = "project"
168
+
74
169
  # A level-2 markdown heading: exactly two '#', then whitespace, then the text.
75
170
  # `### Foo` does not match (the 3rd char is '#', not whitespace).
76
171
  _H2_RE = re.compile(r"^##\s+(.+?)\s*$")
77
172
 
173
+ # A level 1 or 2 heading: it ends the section before it, and starts an
174
+ # Integration Verdicts section when its text is VERDICTS_HEADING.
175
+ _SECTION_END_RE = re.compile(r"^#{1,2}\s")
78
176
 
79
- def split_frontmatter(content):
80
- """Return (frontmatter_dict, body_text).
177
+ # The opening or closing line of a fenced code block.
178
+ _FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})")
179
+
180
+ # The first line of an HTML comment block (CommonMark HTML block type 2). The
181
+ # block ends on the first line that holds _COMMENT_CLOSE, which can be this one.
182
+ _COMMENT_OPEN_RE = re.compile(r"^\s{0,3}<!--")
183
+ _COMMENT_CLOSE = "-->"
184
+
185
+ # A pipe that separates table cells; `\|` is a pipe inside a cell (GFM).
186
+ _CELL_SEPARATOR_RE = re.compile(r"(?<!\\)\|")
187
+
188
+ # One cell of a table's delimiter row: `---`, `:--`, `--:` or `:-:`.
189
+ _DELIMITER_CELL_RE = re.compile(r"^:?-+:?$")
190
+
191
+ _HYPHEN_RUN_RE = re.compile(r"-+")
81
192
 
82
- Stdlib-only, no YAML dependency — mirrors skf-validate-output.py's delimiter
83
- handling. The opening delimiter must be a line that is exactly `---`; the
84
- closing delimiter is the next line that is exactly `---`. The block between
85
- them is parsed as simple `key: value` scalars with surrounding quotes
86
- stripped. If no valid frontmatter block is present, returns ({}, content).
193
+ # coveragePercentage as the schema writes it: a whole number (0 to 100).
194
+ _WHOLE_NUMBER_RE = re.compile(r"[0-9]+")
195
+
196
+ # One item of a YAML block sequence: `- item`, or a lone `-` (an empty item).
197
+ _BLOCK_ITEM_RE = re.compile(r"^-(?:\s+(.*))?$")
198
+
199
+
200
+ def _frontmatter_end(lines):
201
+ """Return the index of the frontmatter's closing `---` line, or None.
202
+
203
+ The opening delimiter must be the first line, exactly `---`; the closing
204
+ delimiter is the next line that is exactly `---`. None when `lines` opens
205
+ no frontmatter block or never closes it.
87
206
  """
88
- lines = content.split("\n")
89
207
  if not lines or lines[0].rstrip() != "---":
90
- return {}, content
91
-
92
- end_idx = None
208
+ return None
93
209
  for i in range(1, len(lines)):
94
210
  if lines[i].rstrip() == "---":
95
- end_idx = i
96
- break
211
+ return i
212
+ return None
213
+
214
+
215
+ def split_frontmatter(content):
216
+ """Return (frontmatter_dict, body_text).
217
+
218
+ Stdlib-only, no YAML dependency: mirrors skf-validate-output.py's delimiter
219
+ handling (_frontmatter_end()). The block between the delimiters is parsed
220
+ as simple `key: value` scalars with surrounding quotes stripped. If no
221
+ valid frontmatter block is present, returns ({}, content).
222
+ """
223
+ lines = content.split("\n")
224
+ end_idx = _frontmatter_end(lines)
97
225
  if end_idx is None:
98
226
  return {}, content
99
227
 
@@ -107,6 +235,90 @@ def split_frontmatter(content):
107
235
  return fm, body
108
236
 
109
237
 
238
+ def _strip_comment(text):
239
+ """Drop a YAML comment: a `#` at the start or after whitespace, outside quotes."""
240
+ quote = None
241
+ for i, ch in enumerate(text):
242
+ if quote is not None:
243
+ if ch == quote:
244
+ quote = None
245
+ elif ch in "'\"":
246
+ quote = ch
247
+ elif ch == "#" and (i == 0 or text[i - 1].isspace()):
248
+ return text[:i]
249
+ return text
250
+
251
+
252
+ def _unquote(item):
253
+ """Return a trimmed item without its surrounding pair of quotes, if it has one."""
254
+ item = item.strip()
255
+ if len(item) >= 2 and item[0] == item[-1] and item[0] in "'\"":
256
+ return item[1:-1]
257
+ return item
258
+
259
+
260
+ def frontmatter_list(content, key):
261
+ """Return the items of a top-level frontmatter list, or None.
262
+
263
+ Reads the two YAML forms a producer writes for stepsCompleted: a flow
264
+ sequence that starts on the key's line (`key: ['init', 'coverage']`, and
265
+ runs on to its `]`) and a block sequence on the lines below the key
266
+ (`- init`). Items are trimmed, a pair of quotes around one is dropped and
267
+ a trailing comment is ignored; an item holding a comma or a bracket is
268
+ not supported. None when the key is absent, holds a scalar, or opens a
269
+ block sequence with no item. When the key appears more than once, the
270
+ last one counts, as in split_frontmatter().
271
+ """
272
+ lines = content.split("\n")
273
+ end = _frontmatter_end(lines)
274
+ if end is None:
275
+ return None
276
+ block = lines[1:end]
277
+ items = None
278
+ for idx, line in enumerate(block):
279
+ name, sep, value = line.partition(":")
280
+ if not sep or line[:1].isspace() or name.strip() != key:
281
+ continue
282
+ value = _strip_comment(value).strip()
283
+ if value.startswith("["):
284
+ text, nxt = value, idx + 1
285
+ while "]" not in text and nxt < len(block):
286
+ text += " " + _strip_comment(block[nxt]).strip()
287
+ nxt += 1
288
+ inner = text[1:].split("]", 1)[0]
289
+ items = [_unquote(part) for part in inner.split(",") if part.strip()]
290
+ elif value:
291
+ items = None # a scalar, not a list
292
+ else:
293
+ found = []
294
+ for following in block[idx + 1:]:
295
+ stripped = _strip_comment(following).strip()
296
+ if not stripped:
297
+ continue
298
+ match = _BLOCK_ITEM_RE.match(stripped)
299
+ if match is None:
300
+ break
301
+ found.append(_unquote(match.group(1) or ""))
302
+ items = found or None
303
+ return items
304
+
305
+
306
+ def _whole_percentage(value):
307
+ """Return a frontmatter percentage as an int from 0 to 100, or None.
308
+
309
+ `value` is the scalar split_frontmatter() read (quotes already dropped); a
310
+ trailing comment is ignored. Anything else, a fraction, a sign, a number
311
+ above 100 or no value at all, gives None.
312
+ """
313
+ if value is None:
314
+ return None
315
+ value = _strip_comment(value).strip()
316
+ if not _WHOLE_NUMBER_RE.fullmatch(value):
317
+ return None
318
+ number = int(value)
319
+ return number if number <= 100 else None
320
+
321
+
110
322
  def scan_headings(body):
111
323
  """Scan the body for the canonical level-2 sections.
112
324
 
@@ -150,60 +362,352 @@ def scan_headings(body):
150
362
  return missing, order_violations
151
363
 
152
364
 
365
+ def _uninterpreted():
366
+ """The keys read_report() fills only for a report whose schemaVersion is "1.0"."""
367
+ return {
368
+ "generatedAt": None,
369
+ "overallVerdict": None,
370
+ "coveragePercentage": None,
371
+ "coverageMeasured": None,
372
+ "verdictTableFound": None,
373
+ "duplicateVerdictTableLine": None,
374
+ "pairVerdicts": [],
375
+ "unknownTokens": [],
376
+ }
377
+
378
+
379
+ def read_report(content):
380
+ """Check and read one report's text: the part both modes share.
381
+
382
+ Returns (fields, ok). `fields` holds schemaVersion, schemaVersionOk,
383
+ headingsOk, missingHeadings and orderViolations, plus the keys
384
+ _uninterpreted() lists (see the module docstring for each). The sections
385
+ are checked whatever the version; a report whose schemaVersion is not
386
+ "1.0" is not interpreted, so its other keys keep their _uninterpreted()
387
+ values. `ok` is True when every check passes: the sections present and in
388
+ order, schemaVersion "1.0", the canonical table there once and every
389
+ verdict token known.
390
+ """
391
+ fm, body = split_frontmatter(content)
392
+ schema_version = fm.get("schemaVersion") # None if the key is absent
393
+ missing, order_violations = scan_headings(body)
394
+ fields = {
395
+ "schemaVersion": schema_version,
396
+ "schemaVersionOk": schema_version == EXPECTED_SCHEMA_VERSION,
397
+ "headingsOk": not missing and not order_violations,
398
+ "missingHeadings": missing,
399
+ "orderViolations": order_violations,
400
+ **_uninterpreted(),
401
+ }
402
+ if not fields["schemaVersionOk"]:
403
+ # An unknown version is never interpreted: nothing is read from it.
404
+ return fields, False
405
+
406
+ overall = fm.get("overallVerdict")
407
+ unknown = []
408
+ if overall not in OVERALL_VERDICTS:
409
+ unknown.append({"field": "overallVerdict", "token": overall})
410
+
411
+ # The body starts on the file line after the frontmatter's closing `---`.
412
+ first_line = len(content.split("\n")) - len(body.split("\n")) + 1
413
+ found, rows, duplicate_line = read_verdict_table(body, first_line)
414
+ pairs = []
415
+ for line_number, cells in rows:
416
+ pair = dict(zip(VERDICT_TABLE_HEADER, cells))
417
+ pairs.append(pair)
418
+ if pair["verdict"] not in PAIR_VERDICTS:
419
+ unknown.append(
420
+ {
421
+ "field": "verdict",
422
+ "line": line_number,
423
+ "lib_a": pair["lib_a"],
424
+ "lib_b": pair["lib_b"],
425
+ "token": pair["verdict"],
426
+ }
427
+ )
428
+
429
+ coverage = _whole_percentage(fm.get("coveragePercentage"))
430
+ steps = frontmatter_list(content, STEPS_COMPLETED_KEY) or []
431
+ fields.update(
432
+ generatedAt=fm.get("generatedAt") or None,
433
+ overallVerdict=overall,
434
+ coveragePercentage=coverage,
435
+ coverageMeasured=coverage is not None and COVERAGE_STEP in steps,
436
+ verdictTableFound=found,
437
+ duplicateVerdictTableLine=duplicate_line,
438
+ pairVerdicts=pairs,
439
+ unknownTokens=unknown,
440
+ )
441
+ ok = fields["headingsOk"] and found and duplicate_line is None and not unknown
442
+ return fields, ok
443
+
444
+
153
445
  def validate_report(path):
154
- """Validate one feasibility report. Returns (result_dict, exit_code)."""
446
+ """Check and read one feasibility report. Returns (result_dict, exit_code)."""
155
447
  p = Path(path)
156
448
  try:
157
- content = p.read_text(encoding="utf-8")
449
+ # utf-8-sig drops the byte order mark some editors add, which would
450
+ # otherwise hide the frontmatter's opening `---`.
451
+ content = p.read_text(encoding="utf-8-sig")
158
452
  except (OSError, UnicodeDecodeError) as e:
159
453
  return (
160
454
  {
161
455
  "status": "error",
162
456
  "path": str(p),
163
457
  "error": f"could not read report file: {e}",
164
- "schemaVersionOk": False,
458
+ "schemaVersion": None,
165
459
  "schemaVersionFound": None,
460
+ # The values earlier callers read for a report that cannot be read.
461
+ "schemaVersionOk": False,
166
462
  "headingsOk": False,
167
463
  "missingHeadings": [],
168
464
  "orderViolations": [],
465
+ **_uninterpreted(),
169
466
  "violation": "io-error",
170
467
  },
171
468
  2,
172
469
  )
173
470
 
174
- fm, body = split_frontmatter(content)
175
- schema_version = fm.get("schemaVersion") # None if the key is absent
176
- schema_version_ok = schema_version == EXPECTED_SCHEMA_VERSION
177
-
178
- missing, order_violations = scan_headings(body)
179
- headings_ok = not missing and not order_violations
180
-
181
- ok = schema_version_ok and headings_ok
471
+ fields, ok = read_report(content)
182
472
  result = {
183
473
  "status": "ok" if ok else "error",
184
474
  "path": str(p),
185
- "schemaVersionOk": schema_version_ok,
186
- "schemaVersionFound": schema_version,
187
- "headingsOk": headings_ok,
188
- "missingHeadings": missing,
189
- "orderViolations": order_violations,
475
+ "schemaVersionFound": fields["schemaVersion"], # the name earlier callers read
476
+ **fields,
190
477
  "violation": None if ok else "schema-violation",
191
478
  }
192
479
  return result, (0 if ok else 1)
193
480
 
194
481
 
195
- def main(argv=None):
196
- parser = argparse.ArgumentParser(
482
+ def slugify(project_name):
483
+ """Return the `{project_slug}` of a project name (the producer's rule).
484
+
485
+ NFKD-decompose the name and drop its combining marks, so an accented Latin
486
+ letter keeps its base letter (`Café` -> `cafe`) and compatibility forms
487
+ fold to ASCII (full-width letters, ligatures, superscripts). A letter or
488
+ digit that still has no ASCII form is dropped (`Straße` -> `strae`).
489
+ Lowercase the rest, turn every run of other characters (spaces,
490
+ punctuation and symbols of any script) into one hyphen, and trim hyphens
491
+ from both ends. A name that leaves nothing gets FALLBACK_SLUG.
492
+
493
+ Deterministic and idempotent: slugify(slugify(name)) == slugify(name).
494
+ """
495
+ kept = []
496
+ for ch in unicodedata.normalize("NFKD", project_name):
497
+ if unicodedata.category(ch).startswith("M"):
498
+ continue # a combining mark; the letter it sat on stays
499
+ if ch.isascii() and ch.isalnum():
500
+ kept.append(ch.lower())
501
+ elif ch.isalnum():
502
+ continue # a letter or digit with no ASCII form
503
+ else:
504
+ kept.append("-")
505
+ return _HYPHEN_RUN_RE.sub("-", "".join(kept)).strip("-") or FALLBACK_SLUG
506
+
507
+
508
+ def _table_cells(line):
509
+ """Split one markdown table row into its trimmed cells.
510
+
511
+ The leading pipe is stripped and so is a trailing one; `\\|` is a pipe
512
+ inside a cell (GFM) and comes back as `|`.
513
+ """
514
+ row = line.strip()
515
+ if row.startswith("|"):
516
+ row = row[1:]
517
+ if row.endswith("|") and not row.endswith("\\|"):
518
+ row = row[:-1]
519
+ return [cell.strip().replace("\\|", "|") for cell in _CELL_SEPARATOR_RE.split(row)]
520
+
521
+
522
+ def read_verdict_table(body, first_line=1):
523
+ """Read the canonical pair table under `## Integration Verdicts`.
524
+
525
+ Returns (found, rows, duplicate_line). `found` is True when that section
526
+ holds a table whose header row is exactly VERDICT_TABLE_HEADER, followed
527
+ by a delimiter row. `rows` lists each data row of the first such table as
528
+ (line_number, [lib_a, lib_b, verdict, rationale]), counting lines from
529
+ `first_line`, the file line of the body's first line. Table lines start
530
+ with a pipe, and a table ends at the first line that does not. Fenced
531
+ code and HTML comments (from a line that starts with `<!--` to the first
532
+ line that holds `-->`) are skipped, so an example table in either is not
533
+ read. The display table the producer writes below the canonical one has
534
+ more columns, so its header never matches.
535
+
536
+ The report holds the canonical table once. `duplicate_line` is the file
537
+ line of the header row of a second one, in the same section or in a
538
+ repeated `## Integration Verdicts` section, and None when there is none:
539
+ a producer that appends its table below the template's empty one leaves
540
+ two, and reading only the first would drop every pair.
541
+
542
+ A row with fewer than four cells gets empty ones; a row with more keeps
543
+ the extra cells in its rationale (an unescaped `|` in the text).
544
+
545
+ Deterministic: same body, same result.
546
+ """
547
+ width = len(VERDICT_TABLE_HEADER)
548
+ lines = body.split("\n")
549
+ found, rows = False, []
550
+ fence = None
551
+ in_comment = False
552
+ in_section = False
553
+ resume = 0 # the line after the first canonical table's last row
554
+ for idx, line in enumerate(lines):
555
+ if idx < resume:
556
+ continue
557
+ fence_match = _FENCE_RE.match(line)
558
+ if fence is not None:
559
+ marker = fence_match.group(1) if fence_match else ""
560
+ if marker[:1] == fence[0] and len(marker) >= len(fence):
561
+ fence = None
562
+ continue
563
+ if in_comment:
564
+ in_comment = _COMMENT_CLOSE not in line
565
+ continue
566
+ if fence_match:
567
+ fence = fence_match.group(1)
568
+ continue
569
+ if _COMMENT_OPEN_RE.match(line):
570
+ in_comment = _COMMENT_CLOSE not in line
571
+ continue
572
+ if _SECTION_END_RE.match(line):
573
+ heading = _H2_RE.match(line)
574
+ in_section = heading is not None and heading.group(1).strip() == VERDICTS_HEADING
575
+ continue
576
+ if not in_section or not line.lstrip().startswith("|"):
577
+ continue
578
+ if _table_cells(line) != VERDICT_TABLE_HEADER:
579
+ continue
580
+ delimiter = lines[idx + 1] if idx + 1 < len(lines) else ""
581
+ delimiter_cells = _table_cells(delimiter)
582
+ if not (
583
+ delimiter.lstrip().startswith("|")
584
+ and len(delimiter_cells) == width
585
+ and all(_DELIMITER_CELL_RE.match(cell) for cell in delimiter_cells)
586
+ ):
587
+ continue
588
+ if found:
589
+ return True, rows, first_line + idx
590
+ found = True
591
+ resume = idx + 2
592
+ while resume < len(lines) and lines[resume].lstrip().startswith("|"):
593
+ cells = _table_cells(lines[resume])
594
+ if len(cells) > width:
595
+ cells = cells[: width - 1] + [" | ".join(cells[width - 1:])]
596
+ cells += [""] * (width - len(cells))
597
+ rows.append((first_line + resume, cells))
598
+ resume += 1
599
+ return found, rows, None
600
+
601
+
602
+ def locate_report(folder, project_name):
603
+ """Find a project's -latest feasibility report, then check and read it.
604
+
605
+ Returns (result_dict, exit_code): see "Locate mode" in the module
606
+ docstring for the keys and the exit codes. The report found is checked
607
+ and read by read_report(), as validate mode reads a report path.
608
+ """
609
+ slug = slugify(project_name)
610
+ latest = Path(folder) / LATEST_REPORT_NAME.format(slug=slug)
611
+ result = {
612
+ "status": "not-found",
613
+ "projectName": project_name,
614
+ "projectSlug": slug,
615
+ "latestPath": str(latest),
616
+ "path": None,
617
+ "schemaVersion": None,
618
+ "schemaVersionOk": None,
619
+ "headingsOk": None,
620
+ "missingHeadings": [],
621
+ "orderViolations": [],
622
+ **_uninterpreted(),
623
+ "violation": None,
624
+ }
625
+ try:
626
+ if not latest.is_file():
627
+ return result, 0
628
+ content = latest.read_text(encoding="utf-8-sig") # see validate_report
629
+ except (OSError, UnicodeDecodeError) as e:
630
+ result.update(
631
+ status="error",
632
+ path=str(latest),
633
+ violation="io-error",
634
+ error=f"could not read report file: {e}",
635
+ )
636
+ return result, 2
637
+
638
+ fields, ok = read_report(content)
639
+ result.update(fields)
640
+ result.update(
641
+ status="ok" if ok else "error",
642
+ path=str(latest),
643
+ violation=None if ok else "schema-violation",
644
+ )
645
+ return result, (0 if ok else 1)
646
+
647
+
648
+ class _PairedFlagsParser(argparse.ArgumentParser):
649
+ """Check the flags that only work together as part of parsing.
650
+
651
+ The checks run in parse_known_args, which parse_args calls, so a caller
652
+ that only parses with the parser _build_parser() returns (the helper-call
653
+ contract test does) refuses `--locate` without `--project-name` as main()
654
+ does.
655
+ """
656
+
657
+ def parse_known_args(self, args=None, namespace=None):
658
+ parsed, extras = super().parse_known_args(args, namespace)
659
+ if parsed.locate is not None:
660
+ if parsed.project_name is None:
661
+ self.error("--project-name is required with --locate")
662
+ if not parsed.locate.strip():
663
+ self.error("--locate needs a folder path")
664
+ elif parsed.project_name is not None:
665
+ self.error("--project-name only applies with --locate")
666
+ return parsed, extras
667
+
668
+
669
+ def _build_parser():
670
+ parser = _PairedFlagsParser(
197
671
  description=(
198
- "Validate a feasibility report's section order and schemaVersion. "
199
- "Confirms the five required body sections (Executive Summary, "
200
- "Coverage Analysis, Integration Verdicts, Recommendations, "
201
- "Evidence Sources) are present and in canonical order, and that "
202
- "frontmatter schemaVersion == \"1.0\". Exit 0 valid, 1 "
203
- "schema-violation, 2 IO/parse error."
672
+ "Check and read a feasibility report, given its path or found with "
673
+ "--locate. Both modes confirm the five required body sections "
674
+ "(Executive Summary, Coverage Analysis, Integration Verdicts, "
675
+ "Recommendations, Evidence Sources) are present and in canonical "
676
+ "order and that frontmatter schemaVersion == \"1.0\"; for a 1.0 "
677
+ "report they also require the one canonical Integration Verdicts "
678
+ "table and known verdict tokens, and return generatedAt, "
679
+ "overallVerdict, coveragePercentage, coverageMeasured, "
680
+ "pairVerdicts and unknownTokens. Given a report path: exit 0 "
681
+ "valid, 1 schema-violation, 2 IO or usage error. Given --locate "
682
+ "FOLDER --project-name NAME: reads "
683
+ "FOLDER/feasibility-report-<slug>-latest.md, the slug made from "
684
+ "NAME by the producer's rule; exit 0 ok or not-found, 1 "
685
+ "schema-violation, 2 IO or usage error."
204
686
  )
205
687
  )
206
- parser.add_argument("report", help="path to the feasibility report .md file")
688
+ target = parser.add_mutually_exclusive_group(required=True)
689
+ target.add_argument(
690
+ "report",
691
+ nargs="?",
692
+ help="path to the feasibility report .md file to check and read",
693
+ )
694
+ target.add_argument(
695
+ "--locate",
696
+ metavar="FOLDER",
697
+ help=(
698
+ "find and read the project's feasibility-report-<slug>-latest.md in "
699
+ "FOLDER (the forge data folder); needs --project-name"
700
+ ),
701
+ )
702
+ parser.add_argument(
703
+ "--project-name",
704
+ metavar="NAME",
705
+ help=(
706
+ "the project_name the report was written for (with --locate); a "
707
+ "name that leaves no slug, an empty one included, gets the slug "
708
+ "'project'"
709
+ ),
710
+ )
207
711
  parser.add_argument(
208
712
  "-o",
209
713
  "--output",
@@ -214,9 +718,16 @@ def main(argv=None):
214
718
  action="store_true",
215
719
  help="print the resolved exit code to stderr",
216
720
  )
217
- args = parser.parse_args(argv)
721
+ return parser
722
+
218
723
 
219
- result, exit_code = validate_report(args.report)
724
+ def main(argv=None):
725
+ args = _build_parser().parse_args(argv)
726
+
727
+ if args.locate is not None:
728
+ result, exit_code = locate_report(args.locate, args.project_name)
729
+ else:
730
+ result, exit_code = validate_report(args.report)
220
731
  text = json.dumps(result, indent=2)
221
732
 
222
733
  if args.output: