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,835 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = []
5
+ # ///
6
+ """Locate where a skill describes each export, each example call to it, and whether the call awaits it.
7
+
8
+ coherence-check.md §2.5 (naive mode) raises a High finding when a skill
9
+ describes an export as async and its examples never await it, or describes
10
+ it as sync and an example awaits it. What a description asserts about an
11
+ export is the model's call, never a keyword's: "`readFileSync` is the sync
12
+ counterpart of the async `readFile`" holds the word `async` and says that
13
+ `readFileSync` is sync. This script does only the part with one correct
14
+ answer per input and judges no meaning. For each export name it lists:
15
+
16
+ - descriptions[]: where SKILL.md and references/**/*.md describe the
17
+ export, outside the frontmatter, fenced code and HTML comments: a heading
18
+ that names it (bare or in code), with the prose of its section, and each
19
+ table row, list item or paragraph that names it in an inline code span.
20
+ Within a section whose heading names the export, nothing else is listed
21
+ for that export: the section text holds it. Each line of fenced code
22
+ that declares the export (`def`, `function`, `fn`, a typed declaration,
23
+ a signature line) is listed too, as kind "signature". An entry carries
24
+ the headings above it (`section`), a table row its table's header row
25
+ (`header`), and its text, cut at a fixed length (`truncated`).
26
+ - calls[]: each call to the export in fenced code: the name, as a whole
27
+ identifier, followed by `(` (a generic `<...>` or a `::` may stand
28
+ between), with any receiver before it (`client.fetchData(`), and not a
29
+ declaration. Comments and string literals are blanked first in the
30
+ languages this script knows by the fence's info string; a block tagged
31
+ as data or output (`json`, `yaml`, `text`, `console` ...) holds none.
32
+
33
+ A call is `awaited` when `await` stands before it (through its receiver:
34
+ `await client.fetchData(...)`), `.await` follows it (Rust), an asynchronous
35
+ loop or context manager consumes it (`async for row in stream(...)`, `async
36
+ with session(...)`, `for await (const row of stream(...))`), or it sits in
37
+ the arguments of a call that is itself awaited or is an event-loop runner
38
+ (`await Promise.all([fetchData(a)])`, `asyncio.run(fetch_data())`), without
39
+ crossing into a function body. `chainedMember` names the member read or
40
+ called on its result right after it (`then`, `catch`, `join`, `get`), else
41
+ null: whether that waits for the result is the caller's call.
42
+
43
+ Name match: the name as a fixed string, case-sensitive, never inside a
44
+ longer identifier (the Export match rule of skf-scan-skill-md-structure.py
45
+ usage-scope). A qualified name (`client.fetch`, `Store::load`) matches only
46
+ as written, except that a declaration also matches its last part
47
+ (`def fetch(`): a declaration never carries the qualifier.
48
+
49
+ Line model: UTF-8 with a leading byte order mark dropped, split on `\\n`
50
+ (a `\\r\\n` pair loses its `\\r`), so each `line` matches `grep -n`. The
51
+ frontmatter is a leading `---` ... `---` block. A fence opens on three or
52
+ more backticks or tildes (any indent) and closes on the same character
53
+ repeated at least as often with nothing after it; an unclosed fence runs to
54
+ the end of the file. A heading is an ATX heading (at most three spaces of
55
+ indent); its section runs to the next heading of the same or a higher level.
56
+
57
+ Input (one JSON object):
58
+ {
59
+ "names": ["fetchData", "Client.fetch", ...], # the §1 inventory's function and method names
60
+ "skillPackagePath": "/path/to/skill" # holds SKILL.md and references/
61
+ }
62
+
63
+ or the files: --inventory <file> --skill-dir <dir>. --inventory reads the
64
+ validated inventory validate-inventory.py wrote (its --output) and takes the
65
+ name of each entry whose kind is `function` or `method`, so no name is
66
+ copied by hand.
67
+
68
+ Output (stdout, one object):
69
+ {
70
+ "skillPackagePath": "...",
71
+ "files": ["SKILL.md", "references/api.md"], # read, in this order
72
+ "exports": [ # each name with at least one call, in input order
73
+ {
74
+ "name": "fetchData",
75
+ "descriptionCount": 3, # all found; descriptions[] holds the first 12
76
+ "descriptions": [{"file", "line", "endLine", "kind", "section", "header", "text", "truncated"}],
77
+ "calls": [{"file", "line", "fence", "language", "awaited", "chainedMember", "text"}],
78
+ "callCount": 2,
79
+ "awaitedCount": 1
80
+ }
81
+ ],
82
+ "notCalled": ["parseConfig"], # names no example calls
83
+ "summary": {"names": N, "called": N, "notCalled": N}
84
+ }
85
+ or {"error": ..., "code": "INVALID_INPUT"} on a schema violation.
86
+
87
+ `kind` is "heading", "table-row", "list-item", "paragraph" or "signature".
88
+ Lines are 1-based; `fence` is the line of the call's opening fence.
89
+
90
+ CLI usage (mirrors verify-declared-numerator.py):
91
+ uv run locate-export-segments.py --inventory <file> --skill-dir <dir>
92
+ uv run locate-export-segments.py '<JSON>' # positional
93
+ uv run locate-export-segments.py --json-input '<JSON>' # explicit flag
94
+ cat input.json | uv run locate-export-segments.py --stdin # piped input
95
+
96
+ Exit codes:
97
+ 0 segments emitted
98
+ 1 no input / input could not be parsed as JSON, or an --inventory file that cannot be read
99
+ 2 input parsed but schema invalid (error object emitted as JSON)
100
+ """
101
+
102
+ from __future__ import annotations
103
+
104
+ import argparse
105
+ import json
106
+ import re
107
+ import sys
108
+ from pathlib import Path
109
+
110
+ MAX_DESCRIPTIONS = 12
111
+ TEXT_CAP = 600
112
+ SECTION_TEXT_CAP = 1200
113
+ LINE_TEXT_CAP = 200
114
+ SECTION_PATH_CAP = 200
115
+
116
+ # The identifier characters of skf-scan-skill-md-structure.py's Export match
117
+ # (its _IDENT_CHARS): any Unicode letter, digit or `_`, and `$`.
118
+ IDENT_CHARS = r"\w$"
119
+ IDENT_RE = re.compile(f"[{IDENT_CHARS}]")
120
+ WORD_BEFORE_RE = re.compile(f"([{IDENT_CHARS}]+)$")
121
+
122
+ FENCE_RE = re.compile(r"^\s*(`{3,}|~{3,})(.*)$")
123
+ HEADING_RE = re.compile(r"^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$")
124
+ CLOSING_HASHES_RE = re.compile(r"(?:^|[ \t]+)#+$")
125
+ LIST_ITEM_RE = re.compile(r"^\s*(?:[-*+]|\d{1,9}[.)])(?:[ \t]+|$)")
126
+ TABLE_ROW_RE = re.compile(r"^\s*\|")
127
+ TABLE_SEPARATOR_RE = re.compile(r"^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$")
128
+ GENERIC_OR_PATH = r"(?:[ \t]*::)?(?:[ \t]*<[^<>()\n]*(?:<[^<>()\n]*>[^<>()\n]*)*>)?"
129
+ AFTER_CALL_RE = re.compile(f"\\s*\\??\\.\\s*((?:[^\\W\\d]|\\$)[{IDENT_CHARS}]*)")
130
+ AWAIT_BEFORE_RE = re.compile(f"(?<![{IDENT_CHARS}])await$")
131
+ AWAIT_AFTER_RE = re.compile(f"\\s*\\.\\s*await(?![{IDENT_CHARS}])")
132
+ # An expression an asynchronous loop or context manager consumes: `async for
133
+ # x in NAME(...)`, `async with NAME(...)`, `for await (const x of NAME(...))`,
134
+ # `await foreach (var x in NAME(...))`. Matched against its line up to it.
135
+ ASYNC_CONSUMER_RE = re.compile(
136
+ f"(?<![{IDENT_CHARS}])(?:async[ \\t]+for[ \\t].*[ \\t]in|async[ \\t]+with(?:[ \\t].*,)?"
137
+ f"|for[ \\t]+await[ \\t]*\\(.*[ \\t]of|await[ \\t]+foreach[ \\t]*\\(.*[ \\t]in)[ \\t]*$")
138
+
139
+ # Info strings of blocks that hold data or output, never calls.
140
+ DATA_LANGUAGES = frozenset({
141
+ "json", "jsonc", "json5", "yaml", "yml", "toml", "ini", "xml", "csv", "tsv", "text", "txt",
142
+ "plaintext", "plain", "mermaid", "markdown", "md", "log", "output", "diff", "console",
143
+ })
144
+ # Comment and string syntax by info string. Code under an unknown or missing
145
+ # info string is read as written.
146
+ HASH_COMMENTS = frozenset({
147
+ "python", "py", "python3", "ruby", "rb", "sh", "bash", "shell", "zsh", "fish", "r", "perl", "pl",
148
+ "elixir", "ex", "exs", "coffee", "coffeescript", "julia", "jl", "powershell", "ps1", "pwsh",
149
+ })
150
+ JS_FAMILY = frozenset({"js", "javascript", "jsx", "mjs", "cjs", "ts", "typescript", "tsx", "mts", "cts",
151
+ "vue", "svelte"})
152
+ C_FAMILY = frozenset({"java", "cs", "csharp", "c#", "c", "h", "cpp", "c++", "cc", "hpp", "dart", "php"})
153
+ SLASH_COMMENTS = JS_FAMILY | C_FAMILY | frozenset({
154
+ "rust", "rs", "go", "golang", "kotlin", "kt", "kts", "swift", "scala", "groovy", "zig",
155
+ })
156
+ # `'` opens a string here; elsewhere it opens a character literal or a Rust
157
+ # lifetime, both read as written.
158
+ SINGLE_QUOTE_STRINGS = HASH_COMMENTS | JS_FAMILY | frozenset({"php", "dart", "groovy"})
159
+ TEMPLATE_STRINGS = JS_FAMILY | frozenset({"go", "golang"})
160
+ TRIPLE_QUOTES = {
161
+ "python": ('"""', "'''"), "py": ('"""', "'''"), "python3": ('"""', "'''"),
162
+ "kotlin": ('"""',), "kt": ('"""',), "kts": ('"""',), "swift": ('"""',), "scala": ('"""',),
163
+ "java": ('"""',), "groovy": ('"""', "'''"), "dart": ('"""', "'''"), "julia": ('"""',), "jl": ('"""',),
164
+ }
165
+ # Languages where `Type name(...)` declares `name`: a word other than an
166
+ # expression word right before the name is a return type there. Untagged
167
+ # code reads the same way, since it is most often a signature.
168
+ TYPED_DECLARATIONS = C_FAMILY | frozenset({"groovy", ""})
169
+ # A `{` after a call at the start of a line opens a method body here; in
170
+ # Kotlin, Swift or Ruby it can be a trailing closure of a call.
171
+ BRACE_DECLARATIONS = JS_FAMILY | C_FAMILY
172
+
173
+ # A word right before the name that declares it.
174
+ DECLARATION_WORDS = frozenset({
175
+ "def", "function", "fn", "func", "fun", "sub", "macro", "class", "interface", "struct", "enum",
176
+ "trait", "type", "record", "object", "protocol", "actor", "typealias", "impl",
177
+ "async", "static", "public", "private", "protected", "internal", "override", "abstract", "virtual",
178
+ "readonly", "get", "set", "export", "default", "pub", "unsafe", "extern", "inline", "final", "open",
179
+ "suspend", "operator", "infix", "tailrec", "external", "native", "synchronized", "sealed", "data",
180
+ })
181
+ # A word that can stand right before a call, and before a `(` that only groups.
182
+ EXPRESSION_WORDS = frozenset({
183
+ "await", "return", "yield", "throw", "raise", "new", "else", "in", "of", "case", "typeof", "void",
184
+ "delete", "not", "and", "or", "is", "if", "elif", "while", "until", "unless", "assert", "print",
185
+ "from", "do", "then", "go", "defer", "try", "when", "with", "lambda", "puts", "echo", "instanceof",
186
+ "as", "match", "loop", "del", "move", "ref", "box", "using", "lock",
187
+ })
188
+ # Callees that run the coroutine they are given until it completes: any
189
+ # `run_until_complete` or `block_on`, and `run` of these event loops.
190
+ RUNNERS = frozenset({"run_until_complete", "block_on"})
191
+ RUN_OWNERS = frozenset({"asyncio", "trio", "anyio"})
192
+ BRACKETS = {"(": ")", "[": "]", "{": "}"}
193
+ CLOSERS = {v: k for k, v in BRACKETS.items()}
194
+
195
+
196
+ def make_error(message):
197
+ return {"error": message, "code": "INVALID_INPUT"}
198
+
199
+
200
+ def _cap(text, cap):
201
+ return (text[:cap], True) if len(text) > cap else (text, False)
202
+
203
+
204
+ # --------------------------------------------------------------------------
205
+ # Reading the skill
206
+ # --------------------------------------------------------------------------
207
+
208
+
209
+ def _split_lines(text):
210
+ """Split on newlines only, so line numbers match `grep -n`."""
211
+ lines = text.replace("\r\n", "\n").split("\n")
212
+ if lines and lines[-1] == "":
213
+ lines.pop()
214
+ return lines
215
+
216
+
217
+ def skill_files(root):
218
+ """(relative POSIX path, lines) of SKILL.md, then of each references/**/*.md in path order."""
219
+ root = Path(root)
220
+ paths = [root / "SKILL.md"] if (root / "SKILL.md").is_file() else []
221
+ refs = root / "references"
222
+ if refs.is_dir():
223
+ paths += sorted((p for p in refs.rglob("*.md") if p.is_file()),
224
+ key=lambda p: p.relative_to(root).as_posix())
225
+ return [(p.relative_to(root).as_posix(), _split_lines(p.read_bytes().decode("utf-8-sig", errors="replace")))
226
+ for p in paths]
227
+
228
+
229
+ def _frontmatter_end(lines):
230
+ """Index of the first body line, past a leading `---` ... `---` block."""
231
+ if lines and lines[0].strip() == "---":
232
+ for i in range(1, len(lines)):
233
+ if lines[i].strip() == "---":
234
+ return i + 1
235
+ return 0
236
+
237
+
238
+ def _fenced_blocks(lines, start):
239
+ """(opening fence, closing fence, info string) of each fenced block, and a per-line code flag.
240
+
241
+ An unclosed fence runs to the end of the file (its closing index is
242
+ len(lines)). A backtick line whose info string holds a backtick is
243
+ inline code, not a fence.
244
+ """
245
+ blocks, code = [], [False] * len(lines)
246
+ fence = None
247
+ for i in range(start, len(lines)):
248
+ m = FENCE_RE.match(lines[i])
249
+ if fence is None:
250
+ if m and not (m.group(1)[0] == "`" and "`" in m.group(2)):
251
+ fence = (m.group(1)[0], len(m.group(1)), i, m.group(2).strip())
252
+ code[i] = True
253
+ continue
254
+ code[i] = True
255
+ if m and m.group(1)[0] == fence[0] and len(m.group(1)) >= fence[1] and not m.group(2).strip():
256
+ blocks.append((fence[2], i, fence[3]))
257
+ fence = None
258
+ if fence is not None:
259
+ blocks.append((fence[2], len(lines), fence[3]))
260
+ return blocks, code
261
+
262
+
263
+ def _language(info):
264
+ """The info string's first word, lower case: `ts title="x"` and `{.ts}` give `ts`."""
265
+ words = info.split()
266
+ return words[0].strip("{}").lstrip(".").lower() if words else ""
267
+
268
+
269
+ def _inline_code(line):
270
+ """The text of each inline code span: a backtick run closed by an equal run."""
271
+ spans, i, n = [], 0, len(line)
272
+ while i < n:
273
+ if line[i] != "`":
274
+ i += 1
275
+ continue
276
+ j = i
277
+ while j < n and line[j] == "`":
278
+ j += 1
279
+ run, k, close = j - i, j, -1
280
+ while k < n:
281
+ if line[k] != "`":
282
+ k += 1
283
+ continue
284
+ m = k
285
+ while m < n and line[m] == "`":
286
+ m += 1
287
+ if m - k == run:
288
+ close = k
289
+ break
290
+ k = m
291
+ if close == -1:
292
+ i = j
293
+ continue
294
+ spans.append(line[j:close])
295
+ i = close + run
296
+ return spans
297
+
298
+
299
+ def _bounded(name):
300
+ """Regex for `name` as a fixed string never inside a longer identifier."""
301
+ body = re.escape(name)
302
+ if IDENT_RE.match(name[0]):
303
+ body = f"(?<![{IDENT_CHARS}])" + body
304
+ if IDENT_RE.match(name[-1]):
305
+ body += f"(?![{IDENT_CHARS}])"
306
+ return re.compile(body)
307
+
308
+
309
+ def _call_pattern(name):
310
+ return re.compile(_bounded(name).pattern + GENERIC_OR_PATH + r"[ \t]*\(")
311
+
312
+
313
+ def _last_part(name):
314
+ """`fetch` for `client.fetch` or `Store::fetch`; None for a plain name."""
315
+ part = re.split(r"\.|::|#|->", name)[-1]
316
+ return part if part and part != name else None
317
+
318
+
319
+ # --------------------------------------------------------------------------
320
+ # Descriptions in prose
321
+ # --------------------------------------------------------------------------
322
+
323
+
324
+ def _prose(lines, start, code):
325
+ """Headings and prose blocks outside the frontmatter, fenced code and HTML comments.
326
+
327
+ A heading is (index, level, text, headings above it). A block is a
328
+ table row, a list item (with its continuation lines) or a paragraph:
329
+ {kind, start, end (inclusive), path (headings above it), header (a
330
+ table row's header row), code (the text of its inline code spans)}.
331
+ """
332
+ headings, blocks = [], []
333
+ stack = []
334
+ current = None
335
+ header = None
336
+ in_comment = False
337
+
338
+ def close():
339
+ nonlocal current
340
+ if current is not None:
341
+ blocks.append(current)
342
+ current = None
343
+
344
+ def block(kind, i, path, table_header=None):
345
+ return {"kind": kind, "start": i, "end": i, "path": path, "header": table_header,
346
+ "code": "\n".join(_inline_code(lines[i]))}
347
+
348
+ for i in range(start, len(lines)):
349
+ line, stripped = lines[i], lines[i].strip()
350
+ if code[i] or not stripped:
351
+ close()
352
+ header = None
353
+ continue
354
+ if in_comment:
355
+ in_comment = "-->" not in line
356
+ continue
357
+ if stripped.startswith("<!--"):
358
+ close()
359
+ in_comment = "-->" not in stripped[4:]
360
+ continue
361
+ m = HEADING_RE.match(line)
362
+ if m:
363
+ close()
364
+ header = None
365
+ level = len(m.group(1))
366
+ text = CLOSING_HASHES_RE.sub("", m.group(2) or "").strip()
367
+ while stack and stack[-1][0] >= level:
368
+ stack.pop()
369
+ headings.append((i, level, text, [t for _, t in stack]))
370
+ stack.append((level, text))
371
+ continue
372
+ path = [t for _, t in stack]
373
+ if TABLE_ROW_RE.match(line):
374
+ close()
375
+ if TABLE_SEPARATOR_RE.match(line):
376
+ continue
377
+ blocks.append(block("table-row", i, path, header))
378
+ header = header or stripped
379
+ continue
380
+ header = None
381
+ if LIST_ITEM_RE.match(line):
382
+ close()
383
+ current = block("list-item", i, path)
384
+ elif current is None:
385
+ current = block("paragraph", i, path)
386
+ else:
387
+ current["end"] = i
388
+ current["code"] += "\n" + "\n".join(_inline_code(line))
389
+ close()
390
+ return headings, blocks
391
+
392
+
393
+ def _heading_path(headings, index):
394
+ """The headings above line `index`, outermost first."""
395
+ path = []
396
+ for idx, _, text, above in headings:
397
+ if idx >= index:
398
+ break
399
+ path = above + [text]
400
+ return path
401
+
402
+
403
+ def _section_end(headings, pos, total):
404
+ """Exclusive end of the section of headings[pos]."""
405
+ level = headings[pos][1]
406
+ for idx, lvl, _, _ in headings[pos + 1:]:
407
+ if lvl <= level:
408
+ return idx
409
+ return total
410
+
411
+
412
+ def _entry(rel, line, end_line, kind, path, header, text, cap):
413
+ text, truncated = _cap(text, cap)
414
+ return {"file": rel, "line": line, "endLine": end_line, "kind": kind,
415
+ "section": _cap(" > ".join(path), SECTION_PATH_CAP)[0], "header": header,
416
+ "text": text, "truncated": truncated}
417
+
418
+
419
+ def describe(rel, lines, code, headings, blocks, pattern):
420
+ """The heading, table-row, list-item and paragraph entries of one file for one export."""
421
+ found, covered = [], []
422
+ for pos, (idx, _, text, above) in enumerate(headings):
423
+ if any(a <= idx < b for a, b in covered) or not pattern.search(text):
424
+ continue
425
+ end = _section_end(headings, pos, len(lines))
426
+ covered.append((idx, end))
427
+ body = [lines[i].strip() for i in range(idx + 1, end)
428
+ if not code[i] and lines[i].strip() and not HEADING_RE.match(lines[i])
429
+ and not lines[i].strip().startswith("<!--") and not TABLE_SEPARATOR_RE.match(lines[i])]
430
+ found.append(_entry(rel, idx + 1, end, "heading", above, None, " ".join([text] + body), SECTION_TEXT_CAP))
431
+ for block in blocks:
432
+ if any(a <= block["start"] < b for a, b in covered) or not pattern.search(block["code"]):
433
+ continue
434
+ text = " ".join(lines[i].strip() for i in range(block["start"], block["end"] + 1))
435
+ found.append(_entry(rel, block["start"] + 1, block["end"] + 1, block["kind"], block["path"],
436
+ block["header"], text, TEXT_CAP))
437
+ return found
438
+
439
+
440
+ # --------------------------------------------------------------------------
441
+ # Calls and declarations in fenced code
442
+ # --------------------------------------------------------------------------
443
+
444
+
445
+ def mask_code(code, language):
446
+ """The code with comments and string literals blanked, every offset and newline kept.
447
+
448
+ Only a language this script knows is masked: code under an unknown or
449
+ missing info string is returned as written.
450
+ """
451
+ hash_comments = language in HASH_COMMENTS
452
+ slash_comments = language in SLASH_COMMENTS
453
+ if not (hash_comments or slash_comments):
454
+ return code
455
+ quotes = {'"'}
456
+ if language in SINGLE_QUOTE_STRINGS:
457
+ quotes.add("'")
458
+ if language in TEMPLATE_STRINGS:
459
+ quotes.add("`")
460
+ triples = TRIPLE_QUOTES.get(language, ())
461
+ out, n, i = list(code), len(code), 0
462
+ while i < n:
463
+ c = code[i]
464
+ triple = next((t for t in triples if code.startswith(t, i)), None)
465
+ if (hash_comments and c == "#") or (slash_comments and code.startswith("//", i)):
466
+ j = code.find("\n", i)
467
+ j = n if j == -1 else j
468
+ elif slash_comments and code.startswith("/*", i):
469
+ j = code.find("*/", i + 2)
470
+ j = n if j == -1 else j + 2
471
+ elif triple:
472
+ j = code.find(triple, i + 3)
473
+ j = n if j == -1 else j + 3
474
+ elif c in quotes:
475
+ j = i + 1
476
+ while j < n and code[j] != c and (c == "`" or code[j] != "\n"):
477
+ j += 2 if code[j] == "\\" else 1
478
+ j = min(j + 1, n)
479
+ else:
480
+ i += 1
481
+ continue
482
+ for k in range(i, j):
483
+ if out[k] != "\n":
484
+ out[k] = " "
485
+ i = j
486
+ return "".join(out)
487
+
488
+
489
+ def _matching_close(text, open_idx):
490
+ stack = []
491
+ for i in range(open_idx, len(text)):
492
+ c = text[i]
493
+ if c in BRACKETS:
494
+ stack.append(c)
495
+ elif c in CLOSERS:
496
+ if not stack or stack.pop() != CLOSERS[c]:
497
+ return None
498
+ if not stack:
499
+ return i
500
+ return None
501
+
502
+
503
+ def _matching_open(text, close_idx):
504
+ stack = []
505
+ for i in range(close_idx, -1, -1):
506
+ c = text[i]
507
+ if c in CLOSERS:
508
+ stack.append(c)
509
+ elif c in BRACKETS:
510
+ if not stack or stack.pop() != BRACKETS[c]:
511
+ return None
512
+ if not stack:
513
+ return i
514
+ return None
515
+
516
+
517
+ def _skip_space_back(text, i):
518
+ while i > 0 and text[i - 1] in " \t\n":
519
+ i -= 1
520
+ return i
521
+
522
+
523
+ def _chain_start(text, pos):
524
+ """Start of the expression whose last member starts at `pos`: `client.api.` in `client.api.name`."""
525
+ i = pos
526
+ while True:
527
+ k = _skip_space_back(text, i)
528
+ accessor = next((a for a in ("?.", "!.", "::", "->", ".") if text.endswith(a, 0, k)), None)
529
+ if accessor is None:
530
+ return i
531
+ k = _skip_space_back(text, k - len(accessor))
532
+ start = k
533
+ while start > 0 and text[start - 1] in ")]":
534
+ opened = _matching_open(text, start - 1)
535
+ if opened is None:
536
+ break
537
+ start = opened
538
+ word = WORD_BEFORE_RE.search(text, 0, start)
539
+ if word:
540
+ start = word.start()
541
+ if start == k:
542
+ return i
543
+ i = start
544
+
545
+
546
+ def _awaited_at(text, start):
547
+ """True when `await` stands right before the expression that starts at `start`, or an
548
+ asynchronous loop or context manager consumes it."""
549
+ if AWAIT_BEFORE_RE.search(text, 0, _skip_space_back(text, start)):
550
+ return True
551
+ return bool(ASYNC_CONSUMER_RE.search(text[text.rfind("\n", 0, start) + 1:start]))
552
+
553
+
554
+ def _is_runner(callee):
555
+ """True for `asyncio.run`, `loop.run_until_complete`, `rt.block_on` and the like."""
556
+ parts = [p for p in re.split(r"\.|::", callee) if p]
557
+ if not parts:
558
+ return False
559
+ if parts[-1] == "run":
560
+ return len(parts) >= 2 and parts[-2] in RUN_OWNERS
561
+ return parts[-1] in RUNNERS
562
+
563
+
564
+ def _opens_body(text, brace_idx):
565
+ """True when the `{` at brace_idx opens a function or statement body, not an object literal."""
566
+ k = _skip_space_back(text, brace_idx)
567
+ return k > 0 and (text[k - 1] in ")>" or bool(IDENT_RE.match(text[k - 1])))
568
+
569
+
570
+ def _enclosing_awaits(text, start):
571
+ """True when a call around the expression at `start` is awaited or runs it, within one function body."""
572
+ i, depth = start, 0
573
+ while i > 0:
574
+ i -= 1
575
+ c = text[i]
576
+ if c in CLOSERS:
577
+ depth += 1
578
+ elif c in BRACKETS:
579
+ if depth:
580
+ depth -= 1
581
+ elif c == "{" and _opens_body(text, i):
582
+ return False
583
+ elif c == "(":
584
+ k = _skip_space_back(text, i)
585
+ word = WORD_BEFORE_RE.search(text, 0, k)
586
+ if word and word.group(1) not in EXPRESSION_WORDS:
587
+ expr = _chain_start(text, word.start())
588
+ if _is_runner(re.sub(r"\s+", "", text[expr:k])):
589
+ return True
590
+ else:
591
+ expr = i # a grouping `(`: `await (fetch())`
592
+ close = _matching_close(text, i)
593
+ if _awaited_at(text, expr) or (close is not None and AWAIT_AFTER_RE.match(text, close + 1)):
594
+ return True
595
+ i = expr
596
+ return False
597
+
598
+
599
+ def _closes_generic(tail):
600
+ """True when `tail` ends in a generic type such as `Task<Data>`."""
601
+ depth = 0
602
+ for i in range(len(tail) - 1, -1, -1):
603
+ c = tail[i]
604
+ if c == ">":
605
+ depth += 1
606
+ elif c == "<":
607
+ depth -= 1
608
+ if depth == 0:
609
+ return i > 0 and bool(IDENT_RE.match(tail[i - 1]))
610
+ elif c in "()=;{}":
611
+ return False
612
+ return False
613
+
614
+
615
+ def _is_declaration(text, line_start, chain, name_start, close, language):
616
+ """True when the name at name_start is declared there, not called."""
617
+ tail = text[line_start:chain].rstrip(" \t")
618
+ if chain == name_start and tail:
619
+ word = WORD_BEFORE_RE.search(tail)
620
+ w = word.group(1) if word else ""
621
+ if w in DECLARATION_WORDS:
622
+ return True
623
+ if tail.endswith("*") and tail[:-1].rstrip(" \t").endswith("function"):
624
+ return True # `function* name(`
625
+ if language in TYPED_DECLARATIONS:
626
+ if w and (w not in EXPRESSION_WORDS or w == "void"):
627
+ return True # a return type: `Data name(`, `void name(`
628
+ if tail.endswith("[]") or (tail.endswith(">") and not tail.endswith(("=>", "->"))
629
+ and _closes_generic(tail)):
630
+ return True # `int[] name(`, `Task<Data> name(`
631
+ if tail.endswith(")"):
632
+ opened = _matching_open(text, line_start + len(tail) - 1)
633
+ if opened is not None and text[line_start:opened].rstrip().endswith("func"):
634
+ return True # Go: `func (c *Client) Name(`
635
+ if not tail and close is not None:
636
+ after = text[close + 1:].lstrip(" \t")
637
+ if after.startswith("->") or (after.startswith(":") and not after.startswith(("::", ":="))):
638
+ return True # a signature: `name(url: string): Data`, `name(url) -> Data`
639
+ if after.startswith("{") and language in BRACE_DECLARATIONS:
640
+ return True # a method body: `name(url) {`
641
+ return False
642
+
643
+
644
+ def scan_block(rel, lines, opening, closing, info, patterns):
645
+ """{name: {"calls": [...], "signatures": [...]}} for one fenced block.
646
+
647
+ `patterns` maps each name to (its call regex, the regexes of the other
648
+ forms a declaration of it may take).
649
+ """
650
+ language = _language(info)
651
+ found = {name: {"calls": [], "signatures": []} for name in patterns}
652
+ if language in DATA_LANGUAGES:
653
+ return found
654
+ body = lines[opening + 1:closing]
655
+ text = mask_code("\n".join(body), language)
656
+ for name, (call_re, declaration_res) in patterns.items():
657
+ seen = set()
658
+ for regex in (call_re, *declaration_res):
659
+ for m in regex.finditer(text):
660
+ name_start, paren = m.start(), m.end() - 1
661
+ if name_start in seen:
662
+ continue
663
+ seen.add(name_start)
664
+ close = _matching_close(text, paren)
665
+ chain = _chain_start(text, name_start)
666
+ line_start = text.rfind("\n", 0, name_start) + 1
667
+ row = text.count("\n", 0, name_start)
668
+ if _is_declaration(text, line_start, chain, name_start, close, language):
669
+ found[name]["signatures"].append({"line": opening + row + 2, "text": body[row].strip()})
670
+ continue
671
+ if regex is not call_re:
672
+ continue # the last part of a qualified name counts only as a declaration
673
+ awaited, chained = _awaited_at(text, chain), None
674
+ if close is not None:
675
+ if AWAIT_AFTER_RE.match(text, close + 1):
676
+ awaited = True
677
+ elif member := AFTER_CALL_RE.match(text, close + 1):
678
+ chained = member.group(1)
679
+ found[name]["calls"].append({
680
+ "file": rel, "line": opening + row + 2, "fence": opening + 1, "language": language or None,
681
+ "awaited": awaited or _enclosing_awaits(text, chain), "chainedMember": chained,
682
+ "text": _cap(body[row].strip(), LINE_TEXT_CAP)[0],
683
+ })
684
+ return found
685
+
686
+
687
+ # --------------------------------------------------------------------------
688
+ # Locate
689
+ # --------------------------------------------------------------------------
690
+
691
+
692
+ def _validate(inp):
693
+ if not isinstance(inp, dict):
694
+ return "Input must be a JSON object"
695
+ if not isinstance(inp.get("names"), list):
696
+ return "names must be a list of export names"
697
+ path = inp.get("skillPackagePath")
698
+ if not isinstance(path, str) or not path:
699
+ return "skillPackagePath is required: the folder that holds SKILL.md and references/"
700
+ if not Path(path).is_dir():
701
+ return f"skillPackagePath is not a folder: {path}"
702
+ return None
703
+
704
+
705
+ def locate(inp):
706
+ """Pure location over SKILL.md and references/**/*.md under skillPackagePath."""
707
+ err = _validate(inp)
708
+ if err:
709
+ return make_error(err)
710
+ names = list(dict.fromkeys(n for n in inp["names"] if isinstance(n, str) and n.strip()))
711
+ bounded = {name: _bounded(name) for name in names}
712
+ patterns = {name: (_call_pattern(name), [_call_pattern(p)] if (p := _last_part(name)) else [])
713
+ for name in names}
714
+ files = skill_files(inp["skillPackagePath"])
715
+ descriptions = {name: [] for name in names}
716
+ calls = {name: [] for name in names}
717
+ for rel, lines in files:
718
+ start = _frontmatter_end(lines)
719
+ fenced, code = _fenced_blocks(lines, start)
720
+ headings, blocks = _prose(lines, start, code)
721
+ per_file = {name: describe(rel, lines, code, headings, blocks, bounded[name]) for name in names}
722
+ for opening, closing, info in fenced:
723
+ for name, got in scan_block(rel, lines, opening, closing, info, patterns).items():
724
+ calls[name] += got["calls"]
725
+ per_file[name] += [_entry(rel, s["line"], s["line"], "signature",
726
+ _heading_path(headings, s["line"] - 1), None, s["text"], LINE_TEXT_CAP)
727
+ for s in got["signatures"]]
728
+ for name in names:
729
+ descriptions[name] += sorted(per_file[name], key=lambda e: e["line"])
730
+ exports = [{
731
+ "name": name,
732
+ "descriptionCount": len(descriptions[name]),
733
+ "descriptions": descriptions[name][:MAX_DESCRIPTIONS],
734
+ "calls": calls[name],
735
+ "callCount": len(calls[name]),
736
+ "awaitedCount": sum(1 for c in calls[name] if c["awaited"]),
737
+ } for name in names if calls[name]]
738
+ not_called = [name for name in names if not calls[name]]
739
+ return {
740
+ "skillPackagePath": inp["skillPackagePath"],
741
+ "files": [rel for rel, _ in files],
742
+ "exports": exports,
743
+ "notCalled": not_called,
744
+ "summary": {"names": len(names), "called": len(exports), "notCalled": len(not_called)},
745
+ }
746
+
747
+
748
+ # --- CLI --------------------------------------------------------------------
749
+
750
+
751
+ def _build_parser():
752
+ parser = argparse.ArgumentParser(
753
+ prog="locate-export-segments",
754
+ description=(
755
+ "Locate where a skill describes each export and each example call to it, and "
756
+ "whether the call awaits it (coherence-check.md §2.5). It judges no meaning: the "
757
+ "caller decides what each description asserts."
758
+ ),
759
+ formatter_class=argparse.RawDescriptionHelpFormatter,
760
+ epilog=(
761
+ "Example:\n"
762
+ " uv run locate-export-segments.py "
763
+ "'{\"names\":[\"fetchData\"],\"skillPackagePath\":\"/path/to/skill\"}'"
764
+ ),
765
+ )
766
+ src = parser.add_mutually_exclusive_group()
767
+ src.add_argument("json_input", nargs="?",
768
+ help="JSON object as a positional argument (single-quote it on the shell).")
769
+ src.add_argument("--json-input", dest="json_input_flag",
770
+ help="JSON object passed via flag (overrides positional).")
771
+ src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
772
+ src.add_argument("--inventory", metavar="PATH",
773
+ help="the validated inventory file: its function and method names (needs --skill-dir)")
774
+ parser.add_argument("--skill-dir", metavar="DIR", help="with --inventory: the skill package")
775
+ return parser
776
+
777
+
778
+ # The inventory kinds §2.5 checks: the ones a caller awaits or calls.
779
+ CALLABLE_KINDS = ("function", "method")
780
+
781
+
782
+ def inventory_input(path, skill_dir):
783
+ """The JSON input for the function and method names of a validated inventory file."""
784
+ data = json.loads(Path(path).read_bytes().decode("utf-8-sig"))
785
+ if isinstance(data, dict) and isinstance(data.get("inventory"), dict):
786
+ data = data["inventory"] # validate-inventory.py's whole result
787
+ exports = data.get("exports") if isinstance(data, dict) else None
788
+ if not isinstance(exports, list):
789
+ raise ValueError(f"--inventory {path} holds no `exports` list")
790
+ names = [e["name"] for e in exports
791
+ if isinstance(e, dict) and e.get("kind") in CALLABLE_KINDS and isinstance(e.get("name"), str)]
792
+ return {"names": names, "skillPackagePath": skill_dir}
793
+
794
+
795
+ def _resolve_input(args):
796
+ if args.stdin:
797
+ return sys.stdin.read()
798
+ if args.json_input_flag is not None:
799
+ return args.json_input_flag
800
+ if args.json_input is not None:
801
+ return args.json_input
802
+ return ""
803
+
804
+
805
+ def main(argv=None):
806
+ parser = _build_parser()
807
+ args = parser.parse_args(argv)
808
+ if args.inventory is not None:
809
+ if not args.skill_dir:
810
+ parser.error("--inventory needs --skill-dir")
811
+ try:
812
+ data = inventory_input(args.inventory, args.skill_dir)
813
+ except (OSError, UnicodeDecodeError, ValueError) as exc:
814
+ print(f"error: cannot read --inventory {args.inventory}: {exc}", file=sys.stderr)
815
+ return 1
816
+ result = locate(data)
817
+ print(json.dumps(result, indent=2))
818
+ return 2 if result.get("code") == "INVALID_INPUT" else 0
819
+ raw = _resolve_input(args)
820
+ if not raw.strip():
821
+ parser.print_usage(file=sys.stderr)
822
+ print("error: no input provided (positional arg, --json-input, or --stdin)", file=sys.stderr)
823
+ return 1
824
+ try:
825
+ data = json.loads(raw)
826
+ except json.JSONDecodeError as exc:
827
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
828
+ return 1
829
+ result = locate(data)
830
+ print(json.dumps(result, indent=2))
831
+ return 2 if result.get("code") == "INVALID_INPUT" else 0
832
+
833
+
834
+ if __name__ == "__main__":
835
+ sys.exit(main())