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
@@ -0,0 +1,674 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Fetch Temporal: a GitHub repository's issues, pull requests, release
6
+ notes and changelog, written as create-skill's temporal feeder.
7
+
8
+ create-skill step 3b (references/sub/fetch-temporal.md) keeps what it fetched
9
+ from GitHub in the skill's `.skf-temporal/` folder under forge_data_folder:
10
+ step 4 searches it through a QMD collection, and step 5c's doc-rot scan reads
11
+ it. The same upstream data must give the same files on every run, so this
12
+ helper owns the fetches, the release loop and the markdown each file holds.
13
+ The step keeps the eligibility checks and what it tells the user.
14
+
15
+ CLI:
16
+ uv run skf-fetch-temporal.py repo --source-repo <repo or local path>
17
+ uv run skf-fetch-temporal.py fetch --repo <owner/repo> --feeder <folder> \\
18
+ [--exports <file>] [--timeout <seconds>]
19
+
20
+ repo
21
+ ----
22
+ Which GitHub repository a brief's source_repo names: an https, ssh or git
23
+ URL, `git@github.com:owner/repo` or the `owner/repo` shorthand, read by
24
+ skf-source-tree.py's parse_remote (the sibling in this folder), or a local
25
+ path whose git repository has a GitHub `origin` remote. When it names one,
26
+ the helper also asks whether gh is installed and logged in (`gh auth
27
+ status`, 10 seconds at most).
28
+
29
+ {
30
+ "repo": "owner/repo" | null,
31
+ "via": "url" | "origin" | null,
32
+ "gh": "ok" | "missing" | "unauthenticated" | "failed" | "not-run",
33
+ "skip_reason": null | "not-github" | "not-a-git-repo" | "no-origin"
34
+ | "gh-missing" | "gh-unauthenticated" | "gh-failed"
35
+ }
36
+
37
+ skip_reason is null when the repository can be fetched: a GitHub
38
+ repository, and a gh that answers.
39
+
40
+ fetch
41
+ -----
42
+ Fetches into a fresh folder beside the feeder, `<feeder>.new` (one an
43
+ interrupted fetch left behind is removed first), which holds a `.gitignore`
44
+ of `*` so the feeder keeps itself out of git:
45
+
46
+ issues.md the last ISSUE_LIMIT issues, open and closed
47
+ (`gh issue list`)
48
+ prs.md the last PR_LIMIT merged pull requests (`gh pr list`)
49
+ releases.md the notes of the last RELEASE_LIMIT releases, one
50
+ `gh release view` per tag, run one after another; a
51
+ tag whose fetch fails gets a one-line placeholder, and
52
+ a rate limit stops the loop with the releases so far
53
+ changelog.md CHANGELOG.md, else RELEASES.md, at the repository
54
+ root, in any letter case
55
+ targeted-issues.md up to ISSUE_SEARCH_LIMIT issues for each of the first
56
+ TARGETED_NAMES --exports names (`gh search issues`,
57
+ SEARCH_WORKERS at a time); a name keeps only
58
+ [A-Za-z0-9_], and one with nothing left is skipped
59
+
60
+ The four list fetches run at the same time, then the release loop, then
61
+ the searches. A file is written only when its fetch returned something: a
62
+ placeholder is not, so releases.md needs the notes of one release at
63
+ least, and targeted-issues.md an issue one search found. When at least one
64
+ file was written, the fetch folder replaces the feeder; otherwise it is
65
+ removed and the feeder, if any, stays as the last good fetch left it.
66
+ --feeder must be a folder named `.skf-temporal`: the helper deletes it and
67
+ `.skf-temporal.new` beside it, and nothing else.
68
+
69
+ Each file but changelog.md (the file as the repository holds it) is
70
+ markdown with one `##` section per item, items in a fixed order (issues
71
+ and pull requests by number, newest first; releases as GitHub lists them;
72
+ search names by name), with no timestamp of the run: the same upstream data
73
+ gives byte-identical files. Bodies keep their text, with line endings made
74
+ `\\n`.
75
+
76
+ --exports names the extraction inventory's top_exports: a JSON list of
77
+ names, or an object with a `top_exports` list, in a file or on stdin (`-`).
78
+ Without it, or with an empty list, no search runs and no
79
+ targeted-issues.md is written. --timeout caps each gh call (default
80
+ DEFAULT_TIMEOUT_SEC seconds).
81
+
82
+ {
83
+ "status": "replaced" | "kept",
84
+ "repo": "owner/repo",
85
+ "feeder": "<folder>",
86
+ "files": ["<name>.md", ...], # the feeder's .md files after the run
87
+ "fetches": {
88
+ "issues": {"status": "ok" | "empty" | "failed", "count": N,
89
+ "detail": null | "..."},
90
+ "prs": {...same...},
91
+ "releases": {"status": "ok" | "empty" | "failed" | "stopped",
92
+ "count": N, "fetched": N, "failed": N,
93
+ "stopped_at": null | N, "detail": null | "..."},
94
+ "changelog": {"status": "ok" | "none" | "failed",
95
+ "file": "<name>" | null, "detail": null | "..."},
96
+ "targeted": {"status": "ok" | "none" | "unsupported" | "failed"
97
+ | "stopped",
98
+ "names": N, "searched": N, "failed": N,
99
+ "skipped": ["<name>", ...], "stopped_at": null | N,
100
+ "detail": null | "..."}
101
+ },
102
+ "warnings": ["..."]
103
+ }
104
+
105
+ gh and git run through skf-source-tree.py (the sibling in this folder): a
106
+ call the time limit stops is stopped with everything it started, and a gh
107
+ or git in the current folder is never run. Every gh call but the search
108
+ names github.com (HOST), the host `repo` checks the login for, whatever
109
+ GH_HOST says.
110
+
111
+ Exit codes:
112
+ 0 repo: JSON printed; fetch: the feeder now holds this fetch
113
+ 3 fetch: nothing was fetched, the feeder is as it was
114
+ 2 usage error: a malformed --repo, a --feeder not named .skf-temporal,
115
+ an unreadable --exports
116
+ 1 unexpected error: {"status": "error", "message": ...} on stderr
117
+ """
118
+
119
+ from __future__ import annotations
120
+
121
+ import argparse
122
+ import concurrent.futures
123
+ import importlib.util
124
+ import json
125
+ import os
126
+ import re
127
+ import sys
128
+ from pathlib import Path
129
+
130
+ HOST = "github.com"
131
+ FEEDER_NAME = ".skf-temporal"
132
+ FETCH_SUFFIX = ".new"
133
+ ISSUE_LIMIT = 100
134
+ PR_LIMIT = 100
135
+ RELEASE_LIMIT = 10
136
+ TARGETED_NAMES = 10
137
+ ISSUE_SEARCH_LIMIT = 5
138
+ SEARCH_WORKERS = 5
139
+ DEFAULT_TIMEOUT_SEC = 60.0
140
+ AUTH_TIMEOUT_SEC = 10.0
141
+ CHANGELOG_NAMES = ("CHANGELOG.md", "RELEASES.md")
142
+ FEEDER_FILES = ("issues.md", "prs.md", "releases.md", "changelog.md", "targeted-issues.md")
143
+
144
+ _REPO_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9._-]+$")
145
+ _HTTP_CODE_RE = re.compile(r"HTTP (\d{3})")
146
+ _SIBLING = None
147
+
148
+
149
+ class UsageError(Exception):
150
+ """An input the helper cannot use (exit 2)."""
151
+
152
+
153
+ def _sibling():
154
+ """skf-source-tree.py from this folder, loaded once: gh and git run
155
+ through its runner (_run stops a call and all it started at the time
156
+ limit; _resolve_outside_cwd never takes a gh or git planted in the
157
+ current folder), and parse_remote reads a repository URL."""
158
+ global _SIBLING
159
+ if _SIBLING is None:
160
+ path = Path(__file__).resolve().parent / "skf-source-tree.py"
161
+ spec = importlib.util.spec_from_file_location("skf_source_tree", path)
162
+ if spec is None or spec.loader is None:
163
+ raise ImportError(f"cannot load {path}")
164
+ module = importlib.util.module_from_spec(spec)
165
+ spec.loader.exec_module(module)
166
+ _SIBLING = module
167
+ return _SIBLING
168
+
169
+
170
+ # --------------------------------------------------------------------------
171
+ # gh and git
172
+ # --------------------------------------------------------------------------
173
+
174
+
175
+ def _last_line(text: str) -> str:
176
+ lines = [line.strip() for line in (text or "").splitlines() if line.strip()]
177
+ return lines[-1] if lines else ""
178
+
179
+
180
+ def _gh(args: list[str], timeout: float) -> tuple[str, str, str]:
181
+ """`gh <args>`: (result, stdout, detail). result is ok, missing,
182
+ unauthenticated, not-found, rate-limited, unsupported, timeout or failed."""
183
+ tree = _sibling()
184
+ exe = tree._resolve_outside_cwd("gh")
185
+ if exe is None:
186
+ return "missing", "", "gh is not installed"
187
+ try:
188
+ rc, out, err, _killed = tree._run([exe, *args], timeout)
189
+ except (OSError, ValueError) as exc:
190
+ return "missing", "", f"gh could not run: {exc}"
191
+ if rc is None:
192
+ return "timeout", "", f"gh did not answer within {timeout:g} seconds"
193
+ stdout = out.decode("utf-8", errors="replace")
194
+ stderr = err.decode("utf-8", errors="replace")
195
+ if rc == 0:
196
+ return "ok", stdout, ""
197
+ detail = _last_line(stderr) or f"gh exited {rc}"
198
+ text = (stderr + stdout).lower()
199
+ m = _HTTP_CODE_RE.search(stderr)
200
+ code = int(m.group(1)) if m else None
201
+ if rc == 4:
202
+ return "unauthenticated", "", "gh is not logged in"
203
+ if code == 429 or "rate limit" in text:
204
+ return "rate-limited", "", detail
205
+ if code == 404:
206
+ return "not-found", "", detail
207
+ if "unknown command" in text or "unknown flag" in text:
208
+ return "unsupported", "", detail
209
+ return "failed", "", detail
210
+
211
+
212
+ def _git(cwd: str, *args: str) -> tuple[int | None, str]:
213
+ """`git -C cwd <args>`: (returncode, stdout); returncode None when git cannot run."""
214
+ tree = _sibling()
215
+ exe = tree._resolve_outside_cwd("git")
216
+ if exe is None:
217
+ return None, ""
218
+ try:
219
+ rc, out, _err, _killed = tree._run([exe, "-c", f"core.hooksPath={os.devnull}", "-C", cwd, *args],
220
+ AUTH_TIMEOUT_SEC)
221
+ except (OSError, ValueError):
222
+ return None, ""
223
+ return rc, out.decode("utf-8", errors="replace").strip()
224
+
225
+
226
+ # --------------------------------------------------------------------------
227
+ # repo
228
+ # --------------------------------------------------------------------------
229
+
230
+
231
+ def github_repo(value: str | None) -> str | None:
232
+ """`owner/repo` of the github.com repository a URL or shorthand names, else None."""
233
+ parsed = _sibling().parse_remote(value)
234
+ if parsed is None:
235
+ return None
236
+ host, owners, repo = parsed
237
+ if host != "github.com" or len(owners) != 1:
238
+ return None
239
+ return f"{owners[0]}/{repo}"
240
+
241
+
242
+ def resolve_repo(source_repo: str) -> dict:
243
+ """The repo result (see the repo section of the module docstring)."""
244
+ result = {"repo": None, "via": None, "gh": "not-run", "skip_reason": None}
245
+ value = (source_repo or "").strip()
246
+ if _sibling().parse_remote(value) is not None:
247
+ repo, via = github_repo(value), "url"
248
+ if repo is None:
249
+ result["skip_reason"] = "not-github"
250
+ return result
251
+ else:
252
+ path = os.path.expanduser(value) if value else ""
253
+ rc, inside = _git(path, "rev-parse", "--is-inside-work-tree") if path and os.path.isdir(path) else (1, "")
254
+ if rc != 0 or inside != "true":
255
+ result["skip_reason"] = "not-a-git-repo"
256
+ return result
257
+ rc, origin = _git(path, "remote", "get-url", "origin")
258
+ if rc != 0 or not origin:
259
+ result["skip_reason"] = "no-origin"
260
+ return result
261
+ repo, via = github_repo(origin), "origin"
262
+ if repo is None:
263
+ result["skip_reason"] = "not-github"
264
+ return result
265
+ result["repo"], result["via"] = repo, via
266
+ status, _out, _detail = _gh(["auth", "status", "--hostname", HOST], AUTH_TIMEOUT_SEC)
267
+ # gh auth status exits 1 when no account is logged in or its token is rejected.
268
+ result["gh"] = {"ok": "ok", "missing": "missing", "unauthenticated": "unauthenticated",
269
+ "failed": "unauthenticated"}.get(status, "failed")
270
+ if result["gh"] != "ok":
271
+ result["skip_reason"] = f"gh-{result['gh']}"
272
+ return result
273
+
274
+
275
+ # --------------------------------------------------------------------------
276
+ # Markdown
277
+ # --------------------------------------------------------------------------
278
+
279
+
280
+ def _text(value) -> str:
281
+ """A body or title as the feeder holds it: `\\n` line endings, no trailing space."""
282
+ text = value if isinstance(value, str) else ""
283
+ return text.replace("\r\n", "\n").replace("\r", "\n").strip()
284
+
285
+
286
+ def _labels(item: dict) -> str:
287
+ names = sorted(label.get("name", "") for label in item.get("labels") or [] if isinstance(label, dict))
288
+ names = [name for name in names if name]
289
+ return ", ".join(names) if names else "none"
290
+
291
+
292
+ def _by_number(items: list) -> list[dict]:
293
+ items = [item for item in items if isinstance(item, dict)]
294
+ return sorted(items, key=lambda item: -int(item.get("number") or 0))
295
+
296
+
297
+ def _document(title: str, sections: list[str]) -> str:
298
+ return "\n\n".join([f"# {title}", *sections]) + "\n"
299
+
300
+
301
+ def render_issues(repo: str, issues: list) -> str:
302
+ sections = []
303
+ for item in _by_number(issues):
304
+ lines = [f"## #{item.get('number')}: {_text(item.get('title'))}", "",
305
+ f"- State: {_text(item.get('state')).lower()}",
306
+ f"- Labels: {_labels(item)}",
307
+ f"- Created: {_text(item.get('createdAt'))}"]
308
+ if item.get("closedAt"):
309
+ lines.append(f"- Closed: {_text(item.get('closedAt'))}")
310
+ body = _text(item.get("body"))
311
+ sections.append("\n".join(lines) + (f"\n\n{body}" if body else ""))
312
+ return _document(f"Issues: {repo}", sections)
313
+
314
+
315
+ def render_prs(repo: str, prs: list) -> str:
316
+ sections = []
317
+ for item in _by_number(prs):
318
+ lines = [f"## #{item.get('number')}: {_text(item.get('title'))}", "",
319
+ f"- Merged: {_text(item.get('mergedAt'))}",
320
+ f"- Labels: {_labels(item)}"]
321
+ body = _text(item.get("body"))
322
+ sections.append("\n".join(lines) + (f"\n\n{body}" if body else ""))
323
+ return _document(f"Merged pull requests: {repo}", sections)
324
+
325
+
326
+ def render_release(release: dict) -> str:
327
+ tag = _text(release.get("tagName"))
328
+ name = _text(release.get("name"))
329
+ heading = f"## {tag}: {name}" if name and name != tag else f"## {tag}"
330
+ lines = [heading, "", f"- Published: {_text(release.get('publishedAt'))}"]
331
+ body = _text(release.get("body"))
332
+ return "\n".join(lines) + (f"\n\n{body}" if body else "")
333
+
334
+
335
+ def render_releases(repo: str, sections: list[str]) -> str:
336
+ return _document(f"Releases: {repo}", sections)
337
+
338
+
339
+ def render_targeted(repo: str, results: dict[str, list | None]) -> str:
340
+ sections = []
341
+ for name in sorted(results):
342
+ found = results[name]
343
+ if found is None:
344
+ sections.append(f"## {name} (fetch failed)")
345
+ continue
346
+ if not found:
347
+ sections.append(f"## {name} (no issues found)")
348
+ continue
349
+ parts = [f"## {name}"]
350
+ for item in _by_number(found):
351
+ heading = f"### #{item.get('number')}: {_text(item.get('title'))} ({_text(item.get('state')).lower()})"
352
+ body = _text(item.get("body"))
353
+ parts.append(heading + (f"\n\n{body}" if body else ""))
354
+ sections.append("\n\n".join(parts))
355
+ return _document(f"Issues naming exported functions: {repo}", sections)
356
+
357
+
358
+ def render_changelog(text: str) -> str:
359
+ return _text(text) + "\n"
360
+
361
+
362
+ # --------------------------------------------------------------------------
363
+ # fetch
364
+ # --------------------------------------------------------------------------
365
+
366
+
367
+ def _json_list(stdout: str) -> list | None:
368
+ try:
369
+ data = json.loads(stdout or "[]")
370
+ except ValueError:
371
+ return None
372
+ return data if isinstance(data, list) else None
373
+
374
+
375
+ def safe_names(names: list) -> tuple[list[str], list[str]]:
376
+ """(the first TARGETED_NAMES search terms, the names before the last of
377
+ them that nothing was left of)."""
378
+ kept: list[str] = []
379
+ skipped: list[str] = []
380
+ for name in names:
381
+ if len(kept) == TARGETED_NAMES:
382
+ break
383
+ if not isinstance(name, str):
384
+ continue
385
+ safe = re.sub(r"[^A-Za-z0-9_]", "", name)
386
+ if not safe:
387
+ skipped.append(name)
388
+ elif safe not in kept:
389
+ kept.append(safe)
390
+ return kept, skipped
391
+
392
+
393
+ def _list_fetch(args: list[str], timeout: float) -> tuple[dict, list]:
394
+ status, out, detail = _gh(args, timeout)
395
+ if status != "ok":
396
+ return {"status": "failed", "count": 0, "detail": detail or status}, []
397
+ items = _json_list(out)
398
+ if items is None:
399
+ return {"status": "failed", "count": 0, "detail": "gh printed no JSON list"}, []
400
+ return {"status": "ok" if items else "empty", "count": len(items), "detail": None}, items
401
+
402
+
403
+ def _changelog(repo: str, timeout: float) -> tuple[dict, str | None]:
404
+ status, out, detail = _gh(["api", "--hostname", HOST, f"repos/{repo}/contents"], timeout)
405
+ if status != "ok":
406
+ return {"status": "failed", "file": None, "detail": detail or status}, None
407
+ entries = _json_list(out) or []
408
+ names = {e.get("name", "").lower(): e.get("name") for e in entries
409
+ if isinstance(e, dict) and e.get("type") == "file" and isinstance(e.get("name"), str)}
410
+ for wanted in CHANGELOG_NAMES:
411
+ name = names.get(wanted.lower())
412
+ if name is None:
413
+ continue
414
+ status, out, detail = _gh(["api", "--hostname", HOST, "-H", "Accept: application/vnd.github.raw",
415
+ f"repos/{repo}/contents/{name}"], timeout)
416
+ if status != "ok":
417
+ return {"status": "failed", "file": name, "detail": detail or status}, None
418
+ return {"status": "ok", "file": name, "detail": None}, out
419
+ return {"status": "none", "file": None, "detail": None}, None
420
+
421
+
422
+ def _release_loop(repo: str, tags: list[dict], timeout: float, warnings: list[str]) -> tuple[dict, list[str]]:
423
+ sections: list[str] = []
424
+ record = {"status": "ok" if tags else "empty", "count": len(tags), "fetched": 0, "failed": 0,
425
+ "stopped_at": None, "detail": None}
426
+ for index, tag_item in enumerate(tags, start=1):
427
+ tag = _text(tag_item.get("tagName")) if isinstance(tag_item, dict) else ""
428
+ if not tag:
429
+ continue
430
+ status, out, detail = _gh(["release", "view", tag, "-R", f"{HOST}/{repo}", "--json",
431
+ "tagName,name,publishedAt,body"], timeout)
432
+ if status == "rate-limited":
433
+ record["status"], record["stopped_at"] = "stopped", index
434
+ break
435
+ try:
436
+ release = json.loads(out) if status == "ok" else None
437
+ except ValueError:
438
+ release = None
439
+ if isinstance(release, dict):
440
+ sections.append(render_release(release))
441
+ record["fetched"] += 1
442
+ else:
443
+ sections.append(f"## {tag} (fetch failed: {detail or 'gh printed no JSON'})")
444
+ record["failed"] += 1
445
+ if record["stopped_at"] is not None:
446
+ kept = "partial releases.md kept" if record["fetched"] else "no release notes fetched"
447
+ warnings.append(f"Release fetch stopped at tag {record['stopped_at']}/{len(tags)} due to rate limiting: "
448
+ f"{kept}.")
449
+ if not record["fetched"]:
450
+ # Placeholders alone are no fetched content: they never make a releases.md.
451
+ if record["failed"] and record["status"] == "ok":
452
+ record["status"], record["detail"] = "failed", "every release fetch failed"
453
+ sections = []
454
+ return record, sections
455
+
456
+
457
+ def _searches(repo: str, names: list[str], timeout: float, warnings: list[str]) -> tuple[dict, dict]:
458
+ record = {"status": "ok", "names": len(names), "searched": 0, "failed": 0, "skipped": [],
459
+ "stopped_at": None, "detail": None}
460
+ results: dict[str, list | None] = {}
461
+ stop = False
462
+
463
+ def search(name: str) -> tuple[str, str, str]:
464
+ return _gh(["search", "issues", "--repo", repo, name, "--limit", str(ISSUE_SEARCH_LIMIT),
465
+ "--json", "number,title,state,body"], timeout)
466
+
467
+ with concurrent.futures.ThreadPoolExecutor(max_workers=SEARCH_WORKERS) as pool:
468
+ pending = {}
469
+ queue = list(names)
470
+ while (queue or pending) and not stop:
471
+ while queue and len(pending) < SEARCH_WORKERS:
472
+ name = queue.pop(0)
473
+ pending[pool.submit(search, name)] = name
474
+ done, _ = concurrent.futures.wait(pending, return_when=concurrent.futures.FIRST_COMPLETED)
475
+ for future in sorted(done, key=lambda f: names.index(pending[f])):
476
+ name = pending.pop(future)
477
+ status, out, detail = future.result()
478
+ if status == "unsupported":
479
+ record["status"], record["detail"] = "unsupported", detail
480
+ stop = True
481
+ elif status == "rate-limited":
482
+ if record["stopped_at"] is None:
483
+ record["status"], record["stopped_at"] = "stopped", names.index(name) + 1
484
+ warnings.append(f"Targeted search stopped at function {names.index(name) + 1}/{len(names)} "
485
+ "due to rate limiting.")
486
+ stop = True
487
+ else:
488
+ items = _json_list(out) if status == "ok" else None
489
+ results[name] = items
490
+ record["searched"] += 1
491
+ if items is None:
492
+ record["failed"] += 1
493
+ for future, name in pending.items(): # finished or cancelled once a stop was seen
494
+ future.cancel()
495
+ if record["status"] == "unsupported":
496
+ return record, {}
497
+ if record["searched"] and record["failed"] == record["searched"] and record["status"] == "ok":
498
+ record["status"], record["detail"] = "failed", "every search failed"
499
+ if not any(results.values()):
500
+ # Only an issue a search found is fetched content: "(fetch failed)" and
501
+ # "(no issues found)" sections alone never make a targeted-issues.md.
502
+ return record, {}
503
+ return record, results
504
+
505
+
506
+ def _remove(path: Path) -> None:
507
+ """Delete a folder the helper owns; a link or junction there is removed, never followed."""
508
+ tree = _sibling()
509
+ if tree._is_link_or_junction(path):
510
+ if path.is_dir() and not path.is_symlink():
511
+ path.rmdir() # a Windows junction: rmdir drops the link, not its target
512
+ else:
513
+ path.unlink()
514
+ elif path.is_dir():
515
+ tree._rmtree(path)
516
+ elif path.exists():
517
+ path.unlink()
518
+
519
+
520
+ def fetch(repo: str, feeder: Path, exports: list | None, timeout: float) -> tuple[dict, int]:
521
+ """Fetch into `<feeder>.new` and replace the feeder when a file was written: (result, exit code)."""
522
+ staging = feeder.with_name(FEEDER_NAME + FETCH_SUFFIX)
523
+ warnings: list[str] = []
524
+ _remove(staging)
525
+ staging.mkdir(parents=True)
526
+ (staging / ".gitignore").write_bytes(b"*\n")
527
+
528
+ lists = {
529
+ "issues": ["issue", "list", "-R", f"{HOST}/{repo}", "--state", "all", "--limit", str(ISSUE_LIMIT),
530
+ "--json", "number,title,state,labels,createdAt,closedAt,body"],
531
+ "prs": ["pr", "list", "-R", f"{HOST}/{repo}", "--state", "merged", "--limit", str(PR_LIMIT),
532
+ "--json", "number,title,mergedAt,labels,body"],
533
+ "releases": ["release", "list", "-R", f"{HOST}/{repo}", "--limit", str(RELEASE_LIMIT),
534
+ "--json", "tagName,name,publishedAt"],
535
+ }
536
+ with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
537
+ listed = {key: pool.submit(_list_fetch, args, timeout) for key, args in lists.items()}
538
+ changelog_future = pool.submit(_changelog, repo, timeout)
539
+ results = {key: future.result() for key, future in listed.items()}
540
+ changelog_record, changelog_text = changelog_future.result()
541
+
542
+ files: dict[str, str] = {}
543
+ fetches: dict[str, dict] = {}
544
+ for key, render in (("issues", render_issues), ("prs", render_prs)):
545
+ record, items = results[key]
546
+ fetches[key] = record
547
+ if items:
548
+ files[f"{key}.md"] = render(repo, items)
549
+
550
+ tags_record, tags = results["releases"]
551
+ if tags_record["status"] == "failed":
552
+ fetches["releases"] = {**tags_record, "fetched": 0, "failed": 0, "stopped_at": None}
553
+ else:
554
+ fetches["releases"], sections = _release_loop(repo, tags, timeout, warnings)
555
+ if sections:
556
+ files["releases.md"] = render_releases(repo, sections)
557
+
558
+ fetches["changelog"] = changelog_record
559
+ if changelog_text is not None and _text(changelog_text):
560
+ files["changelog.md"] = render_changelog(changelog_text)
561
+
562
+ names, skipped = safe_names(exports or [])
563
+ if names:
564
+ fetches["targeted"], searched = _searches(repo, names, timeout, warnings)
565
+ if searched:
566
+ files["targeted-issues.md"] = render_targeted(repo, searched)
567
+ else:
568
+ fetches["targeted"] = {"status": "none", "names": 0, "searched": 0, "failed": 0, "skipped": [],
569
+ "stopped_at": None, "detail": None}
570
+ fetches["targeted"]["skipped"] = skipped
571
+ for name in skipped:
572
+ warnings.append(f"Targeted search skipped {name!r}: no letter, digit or underscore to search for.")
573
+
574
+ for name, text in files.items():
575
+ (staging / name).write_bytes(text.encode("utf-8"))
576
+
577
+ if files:
578
+ _remove(feeder)
579
+ os.replace(staging, feeder)
580
+ status, code = "replaced", 0
581
+ else:
582
+ _remove(staging)
583
+ warnings.append("No temporal context could be fetched: the feeder keeps what the last good fetch left.")
584
+ status, code = "kept", 3
585
+ present = sorted(p.name for p in feeder.glob("*.md")) if feeder.is_dir() else []
586
+ return {"status": status, "repo": repo, "feeder": str(feeder), "files": present,
587
+ "fetches": fetches, "warnings": warnings}, code
588
+
589
+
590
+ # --------------------------------------------------------------------------
591
+ # CLI
592
+ # --------------------------------------------------------------------------
593
+
594
+
595
+ def read_exports(source: str) -> list:
596
+ """The --exports names: a JSON list, or an object with a top_exports list."""
597
+ try:
598
+ text = sys.stdin.read() if source == "-" else Path(source).read_text(encoding="utf-8")
599
+ except (OSError, UnicodeDecodeError) as exc:
600
+ raise UsageError(f"cannot read --exports {source}: {exc}") from exc
601
+ try:
602
+ data = json.loads(text) if text.strip() else []
603
+ except ValueError as exc:
604
+ raise UsageError(f"--exports {source} is not JSON: {exc}") from exc
605
+ if isinstance(data, dict):
606
+ data = data.get("top_exports") or []
607
+ if not isinstance(data, list):
608
+ raise UsageError(f"--exports {source} must be a JSON list of names or hold a top_exports list")
609
+ return data
610
+
611
+
612
+ def _build_parser() -> argparse.ArgumentParser:
613
+ parser = argparse.ArgumentParser(
614
+ prog="skf-fetch-temporal.py",
615
+ description=__doc__,
616
+ formatter_class=argparse.RawDescriptionHelpFormatter,
617
+ )
618
+ sub = parser.add_subparsers(dest="command", required=True)
619
+ repo = sub.add_parser("repo", help="which GitHub repository a source names, and whether gh can read it")
620
+ repo.add_argument("--source-repo", required=True, help="the brief's source_repo: a URL, owner/repo or a local path")
621
+ fetch_cmd = sub.add_parser("fetch", help="fetch issues, pull requests, releases and the changelog")
622
+ fetch_cmd.add_argument("--repo", required=True, help="owner/repo, as the repo command printed it")
623
+ fetch_cmd.add_argument("--feeder", required=True, help="the feeder folder, named .skf-temporal")
624
+ fetch_cmd.add_argument("--exports", metavar="FILE",
625
+ help="the top_exports names as JSON (a file, or - for stdin)")
626
+ fetch_cmd.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT_SEC,
627
+ help=f"seconds each gh call may take (default {DEFAULT_TIMEOUT_SEC:g})")
628
+ return parser
629
+
630
+
631
+ def main(argv: list[str] | None = None) -> int:
632
+ args = _build_parser().parse_args(argv)
633
+ try:
634
+ if args.command == "repo":
635
+ result, code = resolve_repo(args.source_repo), 0
636
+ else:
637
+ if not _REPO_RE.match(args.repo or ""):
638
+ raise UsageError(f"--repo must be owner/repo, got {args.repo!r}")
639
+ feeder = Path(args.feeder)
640
+ if feeder.name != FEEDER_NAME:
641
+ raise UsageError(f"--feeder must be a folder named {FEEDER_NAME}, got {args.feeder!r}")
642
+ if args.timeout <= 0:
643
+ raise UsageError("--timeout must be more than 0")
644
+ exports = read_exports(args.exports) if args.exports else None
645
+ result, code = fetch(args.repo, feeder, exports, args.timeout)
646
+ except UsageError as exc:
647
+ sys.stderr.write(f"skf-fetch-temporal: {exc}\n")
648
+ return 2
649
+ except Exception as exc: # never a traceback in place of the JSON the step reads
650
+ sys.stderr.write(json.dumps({"status": "error", "message": f"{type(exc).__name__}: {exc}"}) + "\n")
651
+ return 1
652
+ print(json.dumps(result, ensure_ascii=False))
653
+ return code
654
+
655
+
656
+ def _force_utf8(*streams) -> None:
657
+ """Reconfigure stdin, stdout and stderr to UTF-8, keeping each stream's error handler.
658
+
659
+ A Windows console pipes them as cp1252, which cannot carry every
660
+ character of an issue title in the JSON, nor read UTF-8 export names
661
+ piped to stdin (reconfigured here, before the first read).
662
+ """
663
+ for stream in streams:
664
+ if hasattr(stream, "reconfigure"):
665
+ errors = getattr(stream, "errors", None)
666
+ if errors is None:
667
+ stream.reconfigure(encoding="utf-8")
668
+ else:
669
+ stream.reconfigure(encoding="utf-8", errors=errors)
670
+
671
+
672
+ if __name__ == "__main__":
673
+ _force_utf8(sys.stdin, sys.stdout, sys.stderr)
674
+ sys.exit(main())