bmad-module-skill-forge 2.2.0 → 4.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 (437) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +143 -18
  18. package/docs/verifying-a-skill.md +87 -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 +3 -3
  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 +31 -59
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +233 -64
  64. package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
  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 +772 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
  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 +21 -3
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +5247 -96
  86. package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +664 -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 +469 -205
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +700 -0
  94. package/src/shared/scripts/skf-hash-content.py +180 -20
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +161 -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 +143 -20
  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 +176 -28
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
  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 +155 -14
  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 +787 -121
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +120 -67
  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 +23 -2
  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 +3945 -152
  138. package/src/shared/scripts/skf-write-skill-brief.py +406 -25
  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 +28 -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/health-check.md +2 -0
  168. package/src/skf-audit-skill/references/init.md +149 -210
  169. package/src/skf-audit-skill/references/re-index.md +103 -110
  170. package/src/skf-audit-skill/references/report.md +96 -33
  171. package/src/skf-audit-skill/references/semantic-diff.md +34 -25
  172. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  173. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  174. package/src/skf-audit-skill/references/structural-diff.md +80 -90
  175. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  176. package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
  177. package/src/skf-brief-skill/SKILL.md +37 -26
  178. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  179. package/src/skf-brief-skill/assets/scope-templates.md +18 -25
  180. package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
  181. package/src/skf-brief-skill/customize.toml +48 -20
  182. package/src/skf-brief-skill/references/analyze-target.md +144 -127
  183. package/src/skf-brief-skill/references/confirm-brief.md +11 -9
  184. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  185. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  186. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  187. package/src/skf-brief-skill/references/headless-args.md +84 -16
  188. package/src/skf-brief-skill/references/health-check.md +1 -1
  189. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  190. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  191. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  192. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  193. package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
  194. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  195. package/src/skf-brief-skill/references/version-resolution.md +7 -10
  196. package/src/skf-brief-skill/references/write-brief.md +66 -57
  197. package/src/skf-campaign/SKILL.md +31 -34
  198. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  199. package/src/skf-campaign/customize.toml +47 -16
  200. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  201. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  202. package/src/skf-campaign/references/health-check.md +7 -16
  203. package/src/skf-campaign/references/step-01-setup.md +46 -84
  204. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  205. package/src/skf-campaign/references/step-03-pins.md +26 -18
  206. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  207. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  208. package/src/skf-campaign/references/step-06-batch.md +41 -34
  209. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  210. package/src/skf-campaign/references/step-08-verify.md +28 -29
  211. package/src/skf-campaign/references/step-09-refine.md +29 -30
  212. package/src/skf-campaign/references/step-10-export.md +48 -36
  213. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  214. package/src/skf-campaign/references/step-resume.md +37 -68
  215. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  216. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  217. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  218. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  219. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  220. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  221. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  222. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  223. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  224. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  225. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  226. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  227. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  228. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  229. package/src/skf-create-skill/SKILL.md +27 -20
  230. package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
  231. package/src/skf-create-skill/assets/skill-sections.md +21 -11
  232. package/src/skf-create-skill/customize.toml +20 -8
  233. package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
  234. package/src/skf-create-skill/references/batch-mode.md +81 -0
  235. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  236. package/src/skf-create-skill/references/compile.md +119 -70
  237. package/src/skf-create-skill/references/component-extraction.md +113 -91
  238. package/src/skf-create-skill/references/enrich.md +41 -23
  239. package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
  240. package/src/skf-create-skill/references/extract.md +204 -162
  241. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  242. package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
  243. package/src/skf-create-skill/references/extraction-patterns.md +588 -201
  244. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  245. package/src/skf-create-skill/references/health-check.md +1 -1
  246. package/src/skf-create-skill/references/load-brief.md +53 -36
  247. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  248. package/src/skf-create-skill/references/report.md +37 -57
  249. package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
  250. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  251. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  252. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  253. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  254. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  255. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  256. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  257. package/src/skf-create-skill/references/validate.md +173 -99
  258. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  259. package/src/skf-create-stack-skill/SKILL.md +29 -56
  260. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  261. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  262. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  263. package/src/skf-create-stack-skill/customize.toml +23 -12
  264. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  265. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  266. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  267. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  268. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  269. package/src/skf-create-stack-skill/references/init.md +90 -32
  270. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  271. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  272. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  273. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  274. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  275. package/src/skf-create-stack-skill/references/report.md +39 -33
  276. package/src/skf-create-stack-skill/references/validate.md +20 -39
  277. package/src/skf-drop-skill/SKILL.md +28 -51
  278. package/src/skf-drop-skill/customize.toml +44 -30
  279. package/src/skf-drop-skill/references/execute.md +83 -146
  280. package/src/skf-drop-skill/references/health-check.md +2 -0
  281. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  282. package/src/skf-drop-skill/references/report.md +46 -30
  283. package/src/skf-drop-skill/references/select.md +122 -124
  284. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  285. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  286. package/src/skf-export-skill/SKILL.md +40 -62
  287. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  288. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  289. package/src/skf-export-skill/customize.toml +42 -13
  290. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  291. package/src/skf-export-skill/references/health-check.md +2 -0
  292. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  293. package/src/skf-export-skill/references/load-skill.md +88 -62
  294. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  295. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  296. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  297. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  298. package/src/skf-export-skill/references/package.md +15 -73
  299. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  300. package/src/skf-export-skill/references/result-envelope.md +31 -16
  301. package/src/skf-export-skill/references/summary.md +41 -17
  302. package/src/skf-export-skill/references/token-report.md +4 -12
  303. package/src/skf-export-skill/references/update-context.md +118 -237
  304. package/src/skf-forger/SKILL.md +61 -37
  305. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  306. package/src/skf-forger/customize.toml +43 -0
  307. package/src/skf-forger/references/pipeline-mode.md +73 -18
  308. package/src/skf-forger/scripts/forge-status.py +299 -0
  309. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  310. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  311. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  312. package/src/skf-quick-skill/SKILL.md +54 -53
  313. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  314. package/src/skf-quick-skill/customize.toml +51 -20
  315. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  316. package/src/skf-quick-skill/references/compile.md +30 -31
  317. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  318. package/src/skf-quick-skill/references/finalize.md +69 -18
  319. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  320. package/src/skf-quick-skill/references/health-check.md +5 -3
  321. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  322. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  323. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  324. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  325. package/src/skf-refine-architecture/SKILL.md +43 -53
  326. package/src/skf-refine-architecture/customize.toml +49 -20
  327. package/src/skf-refine-architecture/references/compile.md +97 -54
  328. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  329. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  330. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  331. package/src/skf-refine-architecture/references/improvements.md +14 -20
  332. package/src/skf-refine-architecture/references/init.md +98 -57
  333. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  334. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  335. package/src/skf-refine-architecture/references/report.md +65 -54
  336. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  337. package/src/skf-rename-skill/SKILL.md +19 -54
  338. package/src/skf-rename-skill/customize.toml +32 -26
  339. package/src/skf-rename-skill/references/execute.md +230 -184
  340. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  341. package/src/skf-rename-skill/references/health-check.md +1 -7
  342. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  343. package/src/skf-rename-skill/references/report.md +63 -52
  344. package/src/skf-rename-skill/references/select.md +197 -115
  345. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  346. package/src/skf-setup/SKILL.md +26 -26
  347. package/src/skf-setup/customize.toml +14 -10
  348. package/src/skf-setup/references/auto-index.md +50 -63
  349. package/src/skf-setup/references/ccc-index.md +29 -90
  350. package/src/skf-setup/references/detect-and-tier.md +58 -67
  351. package/src/skf-setup/references/health-check.md +2 -0
  352. package/src/skf-setup/references/invocation-contract.md +14 -0
  353. package/src/skf-setup/references/report.md +43 -166
  354. package/src/skf-setup/references/tier-rules.md +5 -5
  355. package/src/skf-setup/references/write-config.md +34 -57
  356. package/src/skf-test-skill/SKILL.md +17 -57
  357. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  358. package/src/skf-test-skill/customize.toml +27 -13
  359. package/src/skf-test-skill/references/coherence-check.md +134 -62
  360. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  361. package/src/skf-test-skill/references/coverage-check.md +209 -264
  362. package/src/skf-test-skill/references/detect-mode.md +5 -5
  363. package/src/skf-test-skill/references/external-validators.md +94 -82
  364. package/src/skf-test-skill/references/health-check.md +6 -7
  365. package/src/skf-test-skill/references/init.md +164 -91
  366. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  367. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  368. package/src/skf-test-skill/references/report.md +164 -142
  369. package/src/skf-test-skill/references/score.md +80 -107
  370. package/src/skf-test-skill/references/scoring-rules.md +42 -155
  371. package/src/skf-test-skill/references/source-access-protocol.md +18 -94
  372. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  373. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  374. package/src/skf-test-skill/scripts/build-result-context.py +380 -0
  375. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  376. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  377. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  378. package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
  379. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  380. package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
  381. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  382. package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
  383. package/src/skf-test-skill/scripts/score-signatures.py +479 -0
  384. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  385. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  386. package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
  387. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  388. package/src/skf-update-skill/SKILL.md +38 -25
  389. package/src/skf-update-skill/customize.toml +27 -12
  390. package/src/skf-update-skill/references/detect-changes.md +253 -223
  391. package/src/skf-update-skill/references/gap-driven.md +336 -0
  392. package/src/skf-update-skill/references/health-check.md +31 -5
  393. package/src/skf-update-skill/references/init.md +254 -84
  394. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  395. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  396. package/src/skf-update-skill/references/merge.md +182 -49
  397. package/src/skf-update-skill/references/re-extract.md +53 -262
  398. package/src/skf-update-skill/references/report.md +102 -35
  399. package/src/skf-update-skill/references/write.md +357 -155
  400. package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
  401. package/src/skf-verify-stack/SKILL.md +23 -42
  402. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  403. package/src/skf-verify-stack/customize.toml +55 -22
  404. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  405. package/src/skf-verify-stack/references/coverage.md +67 -64
  406. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  407. package/src/skf-verify-stack/references/init.md +107 -62
  408. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  409. package/src/skf-verify-stack/references/integrations.md +106 -65
  410. package/src/skf-verify-stack/references/report.md +59 -42
  411. package/src/skf-verify-stack/references/requirements.md +32 -9
  412. package/src/skf-verify-stack/references/synthesize.md +53 -39
  413. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  414. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  415. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  416. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  417. package/tools/cli/commands/install.js +11 -4
  418. package/tools/cli/commands/status.js +15 -3
  419. package/tools/cli/commands/update.js +11 -4
  420. package/tools/cli/lib/tool-check.js +320 -0
  421. package/tools/cli/lib/version-check.js +22 -9
  422. package/tools/cli/skf-cli.js +4 -1
  423. package/.gitattributes +0 -16
  424. package/.nvmrc +0 -1
  425. package/docs/_internal/RELEASING.md +0 -679
  426. package/release-audits/v1.0.0-launch-audit.md +0 -728
  427. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  428. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  429. package/src/skf-campaign/manifest.yaml +0 -15
  430. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  431. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  432. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  433. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  434. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  435. package/src/skf-update-skill/references/validate.md +0 -124
  436. package/tools/validate-docs-drift.js +0 -233
  437. package/tools/validate-docs-links.js +0 -665
@@ -1,5 +1,5 @@
1
1
  # /// script
2
- # requires-python = ">=3.10"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = []
4
4
  # ///
5
5
  """SKF Validate Output — Validate skill package artifacts.
@@ -16,23 +16,50 @@ Two package shapes are supported via --skill-type (default: individual):
16
16
  checks and instead validates the stack count-equalities
17
17
  (library_count vs per-library reference files; integration_count
18
18
  vs integration pair files; confidence_distribution sum vs
19
- library_count), emitted under validation.stack_counts.
19
+ library_count), emitted under validation.stack_counts, and the
20
+ stack structure, emitted under validation.stack_structure (see
21
+ validate_stack_structure): the SKILL.md headings of the stack
22
+ template with the catalog inline or behind a pointer, every
23
+ relative link in SKILL.md and references/stack-catalog.md, the
24
+ metadata.json keys and values with the dominant tier recomputed
25
+ by skf-render-stack-metadata.py beside this script, the headings
26
+ of each reference and pair file and a file for each library and
27
+ pair metadata.json lists, and a tier label on each
28
+ per-library summary, pair entry and reference file. Each of
29
+ its issues names in `check` the create-stack-skill validate.md
30
+ section that records it. --forge-tier <tier> also checks
31
+ metadata.json's forge_tier against the run's tier.
20
32
 
21
33
  The --export-gate flag selects a third, self-contained mode used by
22
- skf-export-skill's publishing gate (load-skill.md §2 + package.md §1-3). It is
23
- additive and orthogonal to --skill-type: it does NOT touch the individual/stack
24
- code path above, so existing callers keep byte-identical output. Under the flag
25
- the script emits the deterministic verdict the export prompt previously derived
26
- by hand each run:
34
+ skf-export-skill's publishing gate (load-skill.md §2; package.md renders the
35
+ verdict). It is additive and orthogonal to --skill-type: it does NOT touch the
36
+ individual/stack code path above, so existing callers keep byte-identical
37
+ output. Under the flag the script emits the deterministic verdict the export
38
+ prompt previously derived by hand each run:
27
39
  - metadata.json required-field presence for the full agentskills.io set
28
40
  (name, version, skill_type, source_authority, exports, generation_date,
29
41
  confidence_tier) as high-severity issues under validation.metadata.issues;
30
42
  - enum-membership checks (skill_type, source_authority, confidence_tier) as
31
- high-severity issues under validation.metadata.enum_issues;
43
+ high-severity issues under validation.metadata.enum_issues. The
44
+ confidence_tier set depends on skill_type: a single skill records its
45
+ forge tier (Quick / Forge / Forge+ / Deep), a stack the dominant
46
+ confidence tier of its libraries (T1 / T1-low / T2 / T3). A stack that
47
+ still holds a forge tier there, as older stacks do, passes with a low
48
+ note under validation.metadata.issues;
49
+ - recommended-field presence, chosen by skill_type, as low-severity issues
50
+ under validation.metadata.recommended_missing: description, source_repo,
51
+ language and tool_versions for a single skill (plus ast_node_count when
52
+ confidence_distribution.t1 is above 0), language and tool_versions for a
53
+ stack;
32
54
  - SKILL.md Section 7b (Scripts & Assets) cross-reference against on-disk
33
55
  scripts/ and assets/ files, under validation.crossref_7b.{missing,orphans}
34
56
  (a §7b-named file absent on disk is high; an on-disk file not named in §7b
35
- is a low orphan warning);
57
+ is a low orphan warning). Only a path that starts with scripts/ or
58
+ assets/, optionally after ./, is read as a bundled file. The section is
59
+ the level-2 `## Scripts & Assets` heading create-skill writes (optionally
60
+ numbered, as in `## 7b. Scripts & Assets`), found outside fenced code, and
61
+ validation.crossref_7b.{heading,heading_line} name the heading it matched
62
+ and its line (both null when SKILL.md has no Section 7b);
36
63
  - a deterministic export_status ∈ READY / WARNINGS / NOT_READY alongside the
37
64
  existing PASS/FAIL result (NOT_READY on any high issue, WARNINGS when only
38
65
  medium/low issues remain, READY when clean).
@@ -41,15 +68,19 @@ CLI: python3 skf-validate-output.py <skill-package-dir>
41
68
  python3 skf-validate-output.py <skill-package-dir> --generated-by quick-skill
42
69
  python3 skf-validate-output.py <skill-package-dir> --skip-frontmatter
43
70
  python3 skf-validate-output.py <skill-package-dir> --skill-type stack
71
+ python3 skf-validate-output.py <skill-package-dir> --skill-type stack --forge-tier Deep
44
72
  python3 skf-validate-output.py <skill-package-dir> --export-gate
45
73
  """
46
74
 
47
75
  from __future__ import annotations
48
76
 
77
+ import importlib.util
49
78
  import json
79
+ import posixpath
50
80
  import re
51
81
  import sys
52
82
  from pathlib import Path
83
+ from urllib.parse import unquote
53
84
 
54
85
 
55
86
  def validate_frontmatter(content, skill_name=None):
@@ -197,26 +228,91 @@ def validate_metadata_json(data, generated_by=None):
197
228
  _EXPORT_GATE_ENUMS = {
198
229
  "skill_type": ("single", "stack"),
199
230
  "source_authority": ("official", "internal", "community"),
200
- "confidence_tier": ("Quick", "Forge", "Forge+", "Deep"),
201
231
  }
202
232
 
203
- # Matches a scripts/… or assets/… path token (e.g. `scripts/run.py`). The
204
- # leading filename char must be alphanumeric/dot/underscore so a bare
205
- # `scripts/` (the §7b directory note with nothing after the slash) is not
206
- # captured as a file reference.
207
- _SECTION_7B_PATH_RE = re.compile(r"(?:scripts|assets)/[A-Za-z0-9._][A-Za-z0-9._/\-]*")
233
+ # confidence_tier holds a different scale per skill_type: a single skill
234
+ # records the forge tier it was compiled at, a stack records the dominant
235
+ # confidence tier of its libraries (create-stack-skill keeps the forge tier in
236
+ # forge_tier). Older stacks hold their forge tier in confidence_tier (the
237
+ # v0.10.0 stack template wrote it there, and stacks on disk still carry it),
238
+ # and the gate accepted them before the stack scale existed, so a stack also
239
+ # accepts a forge tier: validate_metadata_export_gate adds a low note for it
240
+ # instead of failing the export. When skill_type is neither, the value is
241
+ # checked against both scales (skill_type itself already fails its enum check).
242
+ _FORGE_TIERS = ("Quick", "Forge", "Forge+", "Deep")
243
+ _EXPORT_GATE_CONFIDENCE_TIERS = {
244
+ "single": _FORGE_TIERS,
245
+ "stack": ("T1", "T1-low", "T2", "T3") + _FORGE_TIERS,
246
+ }
247
+
248
+ # Recommended (non-required) metadata fields, by skill_type: the fields the
249
+ # generator's metadata template always writes (quick-skill and create-skill
250
+ # for single, create-stack-skill for stack). A stack's template has no
251
+ # description or source_repo, so a stack is never warned about them.
252
+ # ast_node_count is written only when ast-grep ran, so a single skill is
253
+ # expected to carry it only when confidence_distribution counts T1
254
+ # (AST-verified) signatures. A stack is never asked for it: its T1 count is
255
+ # inherited from its libraries, not from an AST pass of its own.
256
+ _EXPORT_GATE_RECOMMENDED = {
257
+ "single": ("description", "source_repo", "language", "tool_versions"),
258
+ "stack": ("language", "tool_versions"),
259
+ }
260
+
261
+ # Matches a scripts/… or assets/… path token (e.g. `scripts/run.py`) and
262
+ # captures it without an optional leading `./`. The match starts only at the
263
+ # beginning of a path token, so an upstream path such as
264
+ # `bmad-module-builder/scripts/x.py` is not read as the package's own
265
+ # `scripts/x.py`. The leading filename char must be alphanumeric/dot/underscore
266
+ # so a bare `scripts/` (the §7b directory note with nothing after the slash)
267
+ # is not captured as a file reference.
268
+ _SECTION_7B_PATH_RE = re.compile(
269
+ r"(?<![A-Za-z0-9._/\-])(?:\./)?((?:scripts|assets)/[A-Za-z0-9._][A-Za-z0-9._/\-]*)"
270
+ )
271
+ # The title of Section 7b's level-2 heading (skf-create-skill/assets/
272
+ # skill-sections.md, compile-assembly-rules.md), with an optional section
273
+ # number such as `7b.` before it.
274
+ _SECTION_7B_TITLE_RE = re.compile(r"^(?:\d+[A-Za-z]?\.?[ \t]+)?Scripts & Assets$", re.IGNORECASE)
275
+
276
+
277
+ def _is_blank(val):
278
+ """True when a metadata value is absent or empty (None, "", [], {})."""
279
+ if val is None:
280
+ return True
281
+ if isinstance(val, str):
282
+ return not val.strip()
283
+ if isinstance(val, (list, dict)):
284
+ return not val
285
+ return False
286
+
287
+
288
+ def _counts_t1(data):
289
+ """True when confidence_distribution.t1 is a positive number."""
290
+ dist = data.get("confidence_distribution")
291
+ if not isinstance(dist, dict):
292
+ return False
293
+ t1 = dist.get("t1")
294
+ return isinstance(t1, (int, float)) and not isinstance(t1, bool) and t1 > 0
208
295
 
209
296
 
210
297
  def validate_metadata_export_gate(data):
211
298
  """agentskills.io export-gate metadata validation.
212
299
 
213
- Returns (required_issues, enum_issues). Required-field presence for the full
214
- agentskills.io set is high-severity; enum-membership mismatches are
215
- high-severity. An empty `exports` array is a low warning (matching
300
+ Returns (required_issues, enum_issues, recommended_missing). Required-field
301
+ presence for the full agentskills.io set is high-severity; enum-membership
302
+ mismatches are high-severity, with the confidence_tier scale chosen by
303
+ skill_type. An empty `exports` array is a low warning (matching
216
304
  load-skill.md §2's "warn if empty — graceful handling"), not a hard halt.
305
+ So is a forge tier in a stack's confidence_tier (the value older stacks
306
+ hold); both of these low notes go in required_issues.
307
+ A missing or empty recommended field for the skill_type is a low warning;
308
+ when skill_type is neither single nor stack no recommended set applies.
217
309
  """
218
310
  required_issues = []
219
311
  enum_issues = []
312
+ recommended_missing = []
313
+ skill_type = data.get("skill_type")
314
+ if not isinstance(skill_type, str):
315
+ skill_type = None
220
316
 
221
317
  # String required fields — high-severity presence checks.
222
318
  for field in ("name", "version", "skill_type", "source_authority",
@@ -255,7 +351,75 @@ def validate_metadata_export_gate(data):
255
351
  "message": f"{field} '{val}' not in {list(allowed)}",
256
352
  })
257
353
 
258
- return required_issues, enum_issues
354
+ tier = data.get("confidence_tier")
355
+ if isinstance(tier, str) and tier:
356
+ if skill_type in _EXPORT_GATE_CONFIDENCE_TIERS:
357
+ allowed = _EXPORT_GATE_CONFIDENCE_TIERS[skill_type]
358
+ scope = f" for skill_type '{skill_type}'"
359
+ else:
360
+ allowed = tuple(dict.fromkeys(
361
+ t for tiers in _EXPORT_GATE_CONFIDENCE_TIERS.values() for t in tiers
362
+ ))
363
+ scope = ""
364
+ if tier not in allowed:
365
+ enum_issues.append({
366
+ "severity": "high",
367
+ "field": "confidence_tier",
368
+ "message": f"confidence_tier '{tier}' not in {list(allowed)}{scope}",
369
+ })
370
+ elif skill_type == "stack" and tier in _FORGE_TIERS:
371
+ required_issues.append({
372
+ "severity": "low",
373
+ "field": "confidence_tier",
374
+ "message": (
375
+ f"confidence_tier '{tier}' is a forge tier, as older stacks recorded it; "
376
+ "re-run Stack Skill (@Ferris SS) to record the dominant confidence tier "
377
+ "of its libraries (T1, T1-low, T2 or T3)"
378
+ ),
379
+ })
380
+
381
+ # Recommended fields: low-severity presence checks, by skill_type.
382
+ recommended = list(_EXPORT_GATE_RECOMMENDED.get(skill_type, ()))
383
+ if skill_type == "single" and _counts_t1(data):
384
+ recommended.append("ast_node_count")
385
+ for field in recommended:
386
+ if _is_blank(data.get(field)):
387
+ recommended_missing.append({
388
+ "severity": "low",
389
+ "field": field,
390
+ "message": f"recommended field {field} missing or empty for a {skill_type} skill",
391
+ })
392
+
393
+ return required_issues, enum_issues, recommended_missing
394
+
395
+
396
+ def _section_7b_bounds(skill_md_text):
397
+ """(heading, heading_line, start, end) of SKILL.md's Section 7b, or None.
398
+
399
+ Section 7b is the first level-2 heading titled `Scripts & Assets` (the
400
+ heading create-skill writes, optionally numbered as in
401
+ `## 7b. Scripts & Assets`) outside fenced code, and runs until the next
402
+ level-1 or level-2 heading outside fenced code. A heading that only
403
+ mentions scripts and assets (`### Loading assets from JavaScript`) or a
404
+ shell comment inside a code fence is not it. `heading` is the heading
405
+ line as written, `heading_line` its 1-based line number, and `start` and
406
+ `end` slice the section's body out of the text's lines.
407
+ """
408
+ if not skill_md_text:
409
+ return None
410
+ headings = _headings(skill_md_text)
411
+ for k, (level, title, index) in enumerate(headings):
412
+ if level == 2 and _SECTION_7B_TITLE_RE.match(title):
413
+ lines = skill_md_text.split("\n")
414
+ end = next((i for lvl, _, i in headings[k + 1:] if lvl <= 2), len(lines))
415
+ return lines[index].strip(), index + 1, index + 1, end
416
+ return None
417
+
418
+
419
+ def section_7b_heading(skill_md_text):
420
+ """(heading, heading_line) of SKILL.md's Section 7b, or (None, None) when it has none."""
421
+ bounds = _section_7b_bounds(skill_md_text)
422
+ return (bounds[0], bounds[1]) if bounds else (None, None)
259
423
 
260
424
 
261
425
  def _extract_section_7b_refs(skill_md_text):
@@ -263,38 +427,17 @@ def _extract_section_7b_refs(skill_md_text):
263
427
  (Scripts & Assets). Returns a set of posix path strings.
264
428
 
265
429
  Section 7b is optional (create-skill emits it only when scripts or assets
266
- are detected), so an absent section yields an empty set. The section is
267
- located by its heading — a markdown heading whose text mentions both
268
- "script" and "asset" (the "Scripts & Assets" title) — and the region runs
269
- until the next heading of the same or higher level.
430
+ are detected), so an absent section yields an empty set. _section_7b_bounds
431
+ locates it by its exact heading.
270
432
  """
271
- if not skill_md_text:
272
- return set()
273
-
274
- lines = skill_md_text.split("\n")
275
- start = None
276
- heading_level = None
277
- for i, line in enumerate(lines):
278
- m = re.match(r"^(#{1,6})\s+(.*)$", line)
279
- if m:
280
- text = m.group(2).lower()
281
- if "script" in text and "asset" in text:
282
- start = i + 1
283
- heading_level = len(m.group(1))
284
- break
285
-
286
- if start is None:
433
+ bounds = _section_7b_bounds(skill_md_text)
434
+ if bounds is None:
287
435
  return set()
288
436
 
289
- end = len(lines)
290
- for j in range(start, len(lines)):
291
- m = re.match(r"^(#{1,6})\s+", lines[j])
292
- if m and len(m.group(1)) <= heading_level:
293
- end = j
294
- break
295
-
296
- region = "\n".join(lines[start:end])
437
+ _, _, start, end = bounds
438
+ region = "\n".join(skill_md_text.split("\n")[start:end])
297
439
  refs = set()
440
+ # findall returns the capture group: the path without a leading `./`.
298
441
  for match in _SECTION_7B_PATH_RE.findall(region):
299
442
  refs.add(match.rstrip("./-"))
300
443
  return refs
@@ -431,7 +574,467 @@ def validate_stack_counts(skill_dir, meta):
431
574
  return issues, observed
432
575
 
433
576
 
434
- def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False, skill_type="individual", export_gate=False):
577
+ # --- Stack structure pass (--skill-type stack) ------------------------------
578
+ #
579
+ # The checks create-stack-skill's validate.md §4 to §7 made by hand over every
580
+ # file of a committed stack, against src/skf-create-stack-skill/assets/
581
+ # stack-skill-template.md and, for metadata.json, assets/metadata-contract.md.
582
+ # Each issue carries `check`, the validate.md section
583
+ # that records it: skill_md (§4), metadata (§5), references (§6) and
584
+ # tier_labels (§7).
585
+
586
+ # The helper beside this script that holds the stack tier rules.
587
+ _STACK_RULES_HELPER = "skf-render-stack-metadata.py"
588
+
589
+ # The metadata.json keys the stack metadata contract writes, less ast_node_count
590
+ # (written only when ast-grep ran) and the three validate_stack_counts checks
591
+ # (library_count, integration_count, confidence_distribution).
592
+ _STACK_REQUIRED_KEYS = (
593
+ "skill_type", "name", "version", "generation_date", "forge_tier", "confidence_tier",
594
+ "spec_version", "source_authority", "generated_by", "exports", "libraries",
595
+ "integration_pairs", "language", "tool_versions", "stats", "dependencies", "compatibility",
596
+ )
597
+ _STACK_TIERS = ("T1", "T1-low", "T2", "T3")
598
+ _SOURCE_AUTHORITIES = ("official", "community", "internal")
599
+ _DISTRIBUTION_KEYS = ("t1", "t1_low", "t2", "t3")
600
+
601
+ # The SKILL.md and stack-catalog.md headings of the stack template.
602
+ _STACK_CATALOG = "references/stack-catalog.md"
603
+ _INTEGRATION_PATTERNS = "Integration Patterns"
604
+ _PAIR_INTEGRATIONS = "Library Pair Integrations"
605
+ _REFERENCE_INDEX = "Library Reference Index"
606
+ _PER_LIBRARY = "Per-Library Summaries"
607
+ _CATALOG_POINTER = "Library Catalog"
608
+ _CONVENTIONS = "Conventions"
609
+ # The headings of a per-library file and of an integration pair file.
610
+ _LIBRARY_FILE_HEADINGS = ("Key Exports", "Usage Patterns")
611
+ _PAIR_FILE_HEADINGS = ("Integration Pattern", "Key Files")
612
+
613
+ # The line under the SKILL.md title: `> 3 libraries | 2 integration patterns | Forge tier: Deep`.
614
+ _STACK_HEADER_RE = re.compile(
615
+ r"^>[ \t]*(\d+)[ \t]+librar(?:y|ies)[ \t]*\|[ \t]*(\d+)[ \t]+integration patterns?[ \t]*\|"
616
+ r"[ \t]*Forge tier:[ \t]*([A-Za-z+]+)",
617
+ re.MULTILINE,
618
+ )
619
+ # A tier label: `**Confidence:**` then a T-code, T1-low tried before T1. It,
620
+ # and the `**Version:**` and `**Type:**` lines, may sit in a list item.
621
+ _TIER_LABEL_RE = re.compile(r"^(?:[-*+][ \t]+)?\*\*Confidence:\*\*[ \t]*(?:T1-low|T1|T2|T3)(?![\w-])",
622
+ re.MULTILINE)
623
+ _VERSION_LINE_RE = re.compile(r"^(?:[-*+][ \t]+)?\*\*Version:\*\*", re.MULTILINE)
624
+ _TYPE_LINE_RE = re.compile(r"^(?:[-*+][ \t]+)?\*\*Type:\*\*", re.MULTILINE)
625
+ _ATX_HEADING_RE = re.compile(r"^(#{1,6})[ \t]+(.*)$")
626
+ _FENCE_LINE_RE = re.compile(r"^\s*(`{3,}|~{3,})")
627
+ _INLINE_CODE_RE = re.compile(r"(`+)(?:(?!\1).)+?\1")
628
+ # An inline link or image: the target, bare or in angle brackets, then an optional title.
629
+ _LINK_RE = re.compile(r"\]\(\s*(<[^>\n]*>|[^)\s]+)(?:\s+(?:\"[^\"]*\"|'[^']*'))?\s*\)")
630
+ _URL_SCHEME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
631
+
632
+
633
+ def _stack_rules():
634
+ """The tier rules of skf-render-stack-metadata.py beside this script, or None."""
635
+ path = Path(__file__).resolve().parent / _STACK_RULES_HELPER
636
+ if not path.is_file():
637
+ return None
638
+ try:
639
+ spec = importlib.util.spec_from_file_location("skf_render_stack_metadata_rules", path)
640
+ module = importlib.util.module_from_spec(spec)
641
+ spec.loader.exec_module(module)
642
+ except (ImportError, OSError, SyntaxError):
643
+ return None
644
+ return module if callable(getattr(module, "dominant_tier", None)) else None
645
+
646
+
647
+ def _markdown_lines(text):
648
+ """(line, is_code) for each line; fence lines and fenced lines are code."""
649
+ out, fence = [], None
650
+ for line in text.split("\n"):
651
+ m = _FENCE_LINE_RE.match(line)
652
+ if fence is None:
653
+ if m:
654
+ fence = m.group(1)
655
+ out.append((line, True))
656
+ continue
657
+ out.append((line, False))
658
+ else:
659
+ if m and m.group(1)[0] == fence[0] and len(m.group(1)) >= len(fence) and not line.strip()[len(m.group(1)):]:
660
+ fence = None
661
+ out.append((line, True))
662
+ return out
663
+
664
+
665
+ def _headings(text):
666
+ """[(level, text, line_index)] of the ATX headings outside fenced code, a
667
+ closing run of hashes (`## Conventions ##`) removed."""
668
+ found = []
669
+ for i, (line, code) in enumerate(_markdown_lines(text)):
670
+ m = None if code else _ATX_HEADING_RE.match(line)
671
+ if m:
672
+ body = m.group(2).rstrip(" \t")
673
+ bare = body.rstrip("#")
674
+ if bare != body and (not bare or bare[-1] in " \t"):
675
+ body = bare
676
+ found.append((len(m.group(1)), body.strip(), i))
677
+ return found
678
+
679
+
680
+ def _find_heading(headings, level, name):
681
+ """The first heading of `level` named `name` (case-insensitive), or None."""
682
+ for heading in headings:
683
+ if heading[0] == level and heading[1].lower() == name.lower():
684
+ return heading
685
+ return None
686
+
687
+
688
+ def _block(text, headings, heading):
689
+ """The lines under `heading` up to the next heading of its level or higher."""
690
+ lines = text.split("\n")
691
+ level, _, start = heading
692
+ end = len(lines)
693
+ for other in headings:
694
+ if other[2] > start and other[0] <= level:
695
+ end = other[2]
696
+ break
697
+ return "\n".join(lines[start + 1:end])
698
+
699
+
700
+ def _entries(text, headings, parent, level):
701
+ """[(name, block)] for each heading of `level` inside the `parent` heading's section."""
702
+ section_end = len(text.split("\n"))
703
+ for other in headings:
704
+ if other[2] > parent[2] and other[0] <= parent[0]:
705
+ section_end = other[2]
706
+ break
707
+ return [
708
+ (heading[1], _block(text, headings, heading))
709
+ for heading in headings
710
+ if heading[0] == level and parent[2] < heading[2] < section_end
711
+ ]
712
+
713
+
714
+ def _header_region(text):
715
+ """A reference file's header: the text before its first `## ` heading."""
716
+ for level, _, index in _headings(text):
717
+ if level == 2:
718
+ return "\n".join(text.split("\n")[:index])
719
+ return text
720
+
721
+
722
+ def _relative_links(text):
723
+ """Each relative link target outside fenced code and inline code: no URL,
724
+ no in-file anchor, no absolute path; `#fragment` and `?query` removed."""
725
+ targets = []
726
+ for line, code in _markdown_lines(text):
727
+ if code:
728
+ continue
729
+ for match in _LINK_RE.finditer(_INLINE_CODE_RE.sub("", line)):
730
+ target = match.group(1)
731
+ if target.startswith("<"):
732
+ target = target[1:-1].strip()
733
+ target = unquote(target.split("#", 1)[0].split("?", 1)[0])
734
+ if target and not target.startswith("/") and not _URL_SCHEME_RE.match(target):
735
+ targets.append(target)
736
+ return targets
737
+
738
+
739
+ def validate_stack_structure(skill_dir, meta, generated_by=None, forge_tier=None):
740
+ """Validate a stack package against the stack template's structure.
741
+
742
+ `meta` is the parsed metadata.json, or None when it is missing or does not
743
+ parse (the metadata checks are then skipped). Checks, by `check`:
744
+ - skill_md: a title (H1) and the `> N libraries | M integration
745
+ patterns | Forge tier: T` line under it, its numbers and tier equal
746
+ to metadata.json's; `## Integration Patterns` (unless
747
+ integration_count is 0) ahead of the per-library summaries;
748
+ `## Conventions`; the catalog inline (`## Library Reference Index`
749
+ and `## Per-Library Summaries`) or behind a `## Library Catalog`
750
+ pointer that links references/stack-catalog.md, which holds both
751
+ sections; every relative link in SKILL.md and stack-catalog.md
752
+ resolving from the file that holds it.
753
+ - metadata: the metadata contract's keys (less ast_node_count and the three
754
+ validate_stack_counts checks); skill_type `stack`; name equal to the
755
+ package folder's; version and generation_date non-empty strings;
756
+ forge_tier a forge tier (and `forge_tier` when given);
757
+ confidence_tier a T-code equal to the dominant tier
758
+ skf-render-stack-metadata.py recomputes from confidence_distribution;
759
+ source_authority an authority; generated_by equal to `generated_by`
760
+ when given (low); exports an array; libraries a non-empty array of
761
+ distinct names, as many as library_count; integration_pairs pairs of
762
+ two different listed libraries, each once, as many as
763
+ integration_count; confidence_distribution holding each of t1,
764
+ t1_low, t2 and t3 as a whole number.
765
+ - references: each references/{library}.md has a title, a
766
+ `**Version:**` line, `## Key Exports` and `## Usage Patterns`; each
767
+ references/integrations/{pair}.md a title, a `**Type:**` line,
768
+ `## Integration Pattern` and `## Key Files`; and, with a readable
769
+ metadata.json, each library `libraries` lists has its
770
+ references/{library}.md and each pair `integration_pairs` lists its
771
+ references/integrations/{a}-{b}.md (or {b}-{a}.md).
772
+ - tier_labels: a `**Confidence:**` T-code label on each entry of the
773
+ per-library summaries (SKILL.md or stack-catalog.md), on each entry
774
+ of `### Library Pair Integrations`, and in the header of each
775
+ reference and pair file.
776
+
777
+ Returns (issues, observed). Every issue is medium, except a generated_by
778
+ mismatch and a dominant tier that could not be recomputed (low).
779
+ """
780
+ skill_dir = Path(skill_dir)
781
+ references = skill_dir / "references"
782
+ issues = []
783
+ observed = {
784
+ "catalog": None,
785
+ "dominant_tier": None,
786
+ "tier_rule": None,
787
+ "reference_files": 0,
788
+ "pair_files": 0,
789
+ "links_checked": 0,
790
+ "tier_labels_checked": 0,
791
+ }
792
+
793
+ def add(check, field, message, severity="medium"):
794
+ issues.append({"severity": severity, "check": check, "field": field, "message": message})
795
+
796
+ def read(path, check):
797
+ try:
798
+ return path.read_text(encoding="utf-8")
799
+ except (OSError, UnicodeDecodeError) as exc:
800
+ rel = path.relative_to(skill_dir).as_posix()
801
+ add(check, rel, f"cannot read {rel}: {exc}")
802
+ return None
803
+
804
+ meta = meta if isinstance(meta, dict) else None
805
+
806
+ def count(key):
807
+ value = meta.get(key) if meta is not None else None
808
+ return value if isinstance(value, int) and not isinstance(value, bool) else None
809
+
810
+ def check_links(text, holder, name):
811
+ for target in _relative_links(text):
812
+ observed["links_checked"] += 1
813
+ try:
814
+ resolves = (holder / target).exists()
815
+ except (OSError, ValueError): # a name the file system cannot hold
816
+ resolves = False
817
+ if not resolves:
818
+ add("skill_md", "link", f"{name} links `{target}`, which does not resolve from {name}'s folder")
819
+
820
+ def has_file(folder, name):
821
+ try:
822
+ return (folder / name).is_file()
823
+ except (OSError, ValueError): # a name the file system cannot hold
824
+ return False
825
+
826
+ # --- SKILL.md (§4) and its tier labels (§7) ----------------------------
827
+ skill_md_path = skill_dir / "SKILL.md"
828
+ skill_md = read(skill_md_path, "skill_md") if skill_md_path.is_file() else None
829
+ catalog_text = None
830
+ if skill_md is not None:
831
+ headings = _headings(skill_md)
832
+ if not any(level == 1 for level, _, _ in headings):
833
+ add("skill_md", "heading", "SKILL.md has no title (`# {project_name} Stack Skill`)")
834
+ header = _STACK_HEADER_RE.search(skill_md)
835
+ if header is None:
836
+ add("skill_md", "header", "SKILL.md has no `> {lib_count} libraries | {integration_count} integration "
837
+ "patterns | Forge tier: {tier}` line under its title")
838
+ elif meta is not None:
839
+ for value, key in ((int(header.group(1)), "library_count"), (int(header.group(2)), "integration_count")):
840
+ if count(key) is not None and value != count(key):
841
+ add("skill_md", "header", f"the SKILL.md header line gives {value} for {key}; metadata.json "
842
+ f"has {count(key)}")
843
+ if isinstance(meta.get("forge_tier"), str) and header.group(3) != meta["forge_tier"]:
844
+ add("skill_md", "header", f"the SKILL.md header line gives forge tier {header.group(3)}; "
845
+ f"metadata.json has {meta['forge_tier']}")
846
+
847
+ patterns = _find_heading(headings, 2, _INTEGRATION_PATTERNS)
848
+ if patterns is None and count("integration_count") != 0:
849
+ add("skill_md", "heading", f"SKILL.md has no `## {_INTEGRATION_PATTERNS}` section")
850
+ if _find_heading(headings, 2, _CONVENTIONS) is None:
851
+ add("skill_md", "heading", f"SKILL.md has no `## {_CONVENTIONS}` section")
852
+
853
+ index = _find_heading(headings, 2, _REFERENCE_INDEX)
854
+ summaries = _find_heading(headings, 2, _PER_LIBRARY)
855
+ pointer = _find_heading(headings, 2, _CATALOG_POINTER)
856
+ summaries_holder = None # (text, headings, heading, file name)
857
+ if index and summaries:
858
+ observed["catalog"] = "inline"
859
+ summaries_holder = (skill_md, headings, summaries, "SKILL.md")
860
+ elif pointer:
861
+ observed["catalog"] = "pointer"
862
+ links = [posixpath.normpath(t) for t in _relative_links(_block(skill_md, headings, pointer))]
863
+ if _STACK_CATALOG not in links:
864
+ add("skill_md", "catalog", f"the `## {_CATALOG_POINTER}` pointer does not link `{_STACK_CATALOG}`")
865
+ catalog_path = skill_dir / _STACK_CATALOG
866
+ if not catalog_path.is_file():
867
+ add("skill_md", "catalog", f"the `## {_CATALOG_POINTER}` pointer's `{_STACK_CATALOG}` does not exist")
868
+ else:
869
+ catalog_text = read(catalog_path, "skill_md")
870
+ if catalog_text is not None:
871
+ catalog_headings = _headings(catalog_text)
872
+ for name in (_REFERENCE_INDEX, _PER_LIBRARY):
873
+ if _find_heading(catalog_headings, 2, name) is None:
874
+ add("skill_md", "catalog", f"`{_STACK_CATALOG}` has no `## {name}` section")
875
+ catalog_summaries = _find_heading(catalog_headings, 2, _PER_LIBRARY)
876
+ if catalog_summaries:
877
+ summaries_holder = (catalog_text, catalog_headings, catalog_summaries, "stack-catalog.md")
878
+ else:
879
+ missing = [f"`## {name}`" for name, found in ((_REFERENCE_INDEX, index), (_PER_LIBRARY, summaries))
880
+ if not found]
881
+ add("skill_md", "catalog", f"SKILL.md has neither the inline catalog (no {' or '.join(missing)}) nor a "
882
+ f"`## {_CATALOG_POINTER}` pointer to `{_STACK_CATALOG}`")
883
+
884
+ first_summary = summaries if observed["catalog"] == "inline" else pointer
885
+ if patterns and first_summary and first_summary[2] < patterns[2]:
886
+ add("skill_md", "heading", f"`## {_INTEGRATION_PATTERNS}` comes after the per-library summaries")
887
+
888
+ check_links(skill_md, skill_dir, "SKILL.md")
889
+ if catalog_text is not None:
890
+ check_links(catalog_text, references, "stack-catalog.md")
891
+
892
+ # Tier labels: each per-library summary and each pair entry.
893
+ if summaries_holder is not None:
894
+ text, holder_headings, heading, name = summaries_holder
895
+ for entry, block in _entries(text, holder_headings, heading, 3):
896
+ observed["tier_labels_checked"] += 1
897
+ if not _TIER_LABEL_RE.search(block):
898
+ add("tier_labels", "tier_label",
899
+ f"the per-library summary `### {entry}` in {name} has no `**Confidence:**` T-code label")
900
+ pairs_heading = _find_heading(headings, 3, _PAIR_INTEGRATIONS)
901
+ if patterns and pairs_heading is None and (count("integration_count") or 0) > 0:
902
+ add("skill_md", "heading", f"`## {_INTEGRATION_PATTERNS}` has no `### {_PAIR_INTEGRATIONS}` section")
903
+ if patterns and pairs_heading and pairs_heading[2] > patterns[2]:
904
+ for entry, block in _entries(skill_md, headings, pairs_heading, 4):
905
+ observed["tier_labels_checked"] += 1
906
+ if not _TIER_LABEL_RE.search(block):
907
+ add("tier_labels", "tier_label",
908
+ f"the pair entry `#### {entry}` in SKILL.md has no `**Confidence:**` T-code label")
909
+
910
+ # --- Reference and pair files (§6) and their tier labels (§7) -------------
911
+ ref_files = []
912
+ if references.is_dir():
913
+ ref_files = [p for p in sorted(references.glob("*.md")) if p.name != "stack-catalog.md"]
914
+ pair_dir = references / "integrations"
915
+ pair_files = sorted(pair_dir.glob("*.md")) if pair_dir.is_dir() else []
916
+ for files, kind, line_re, line_name, required in (
917
+ (ref_files, "reference_files", _VERSION_LINE_RE, "**Version:**", _LIBRARY_FILE_HEADINGS),
918
+ (pair_files, "pair_files", _TYPE_LINE_RE, "**Type:**", _PAIR_FILE_HEADINGS),
919
+ ):
920
+ for path in files:
921
+ observed[kind] += 1
922
+ rel = path.relative_to(skill_dir).as_posix()
923
+ text = read(path, "references")
924
+ if text is None:
925
+ continue
926
+ headings = _headings(text)
927
+ if not any(level == 1 for level, _, _ in headings):
928
+ add("references", rel, f"{rel} has no title (`# ...`)")
929
+ if not line_re.search(text):
930
+ add("references", rel, f"{rel} has no `{line_name}` line")
931
+ for name in required:
932
+ if _find_heading(headings, 2, name) is None:
933
+ add("references", rel, f"{rel} has no `## {name}` section")
934
+ observed["tier_labels_checked"] += 1
935
+ if not _TIER_LABEL_RE.search(_header_region(text)):
936
+ add("tier_labels", "tier_label", f"{rel} has no `**Confidence:**` T-code label in its header")
937
+
938
+ # --- metadata.json (§5) ---------------------------------------------------
939
+ if meta is not None:
940
+ for key in _STACK_REQUIRED_KEYS:
941
+ if key not in meta:
942
+ add("metadata", key, f"metadata.json has no `{key}`")
943
+ if "skill_type" in meta and meta["skill_type"] != "stack":
944
+ add("metadata", "skill_type", f"skill_type is {meta['skill_type']!r}, not 'stack'")
945
+ if "name" in meta and meta["name"] != skill_dir.name:
946
+ add("metadata", "name", f"name {meta['name']!r} does not match the package folder {skill_dir.name!r}")
947
+ for key in ("version", "generation_date"):
948
+ if key in meta and (not isinstance(meta[key], str) or not meta[key].strip()):
949
+ add("metadata", key, f"{key} must be a non-empty string")
950
+ tier = meta.get("forge_tier")
951
+ if "forge_tier" in meta and tier not in _FORGE_TIERS:
952
+ add("metadata", "forge_tier", f"forge_tier {tier!r} not in {list(_FORGE_TIERS)}")
953
+ elif forge_tier is not None and "forge_tier" in meta and tier != forge_tier:
954
+ add("metadata", "forge_tier", f"forge_tier {tier!r} is not this run's forge tier {forge_tier!r}")
955
+ confidence_tier = meta.get("confidence_tier")
956
+ if "confidence_tier" in meta and confidence_tier not in _STACK_TIERS:
957
+ add("metadata", "confidence_tier", f"confidence_tier {confidence_tier!r} not in {list(_STACK_TIERS)}")
958
+ rules = _stack_rules()
959
+ if rules is None:
960
+ add("metadata", "confidence_tier", f"the dominant tier was not recomputed: {_STACK_RULES_HELPER} is not "
961
+ "beside skf-validate-output.py (re-install SKF)", severity="low")
962
+ elif isinstance(meta.get("confidence_distribution"), dict):
963
+ observed["tier_rule"] = _STACK_RULES_HELPER
964
+ observed["dominant_tier"] = rules.dominant_tier(meta["confidence_distribution"])
965
+ if confidence_tier in _STACK_TIERS and confidence_tier != observed["dominant_tier"]:
966
+ add("metadata", "confidence_tier", f"confidence_tier {confidence_tier!r} is not the dominant tier of "
967
+ f"confidence_distribution ({observed['dominant_tier']!r})")
968
+ authority = meta.get("source_authority")
969
+ if "source_authority" in meta and authority not in _SOURCE_AUTHORITIES:
970
+ add("metadata", "source_authority", f"source_authority {authority!r} not in {list(_SOURCE_AUTHORITIES)}")
971
+ if generated_by and "generated_by" in meta and meta["generated_by"] != generated_by:
972
+ add("metadata", "generated_by", f"generated_by is {meta['generated_by']!r}, expected {generated_by!r}",
973
+ severity="low")
974
+ if "exports" in meta and not isinstance(meta["exports"], list):
975
+ add("metadata", "exports", "exports must be an array (a stack's is empty)")
976
+
977
+ libraries = meta.get("libraries")
978
+ names = []
979
+ if "libraries" in meta:
980
+ if (not isinstance(libraries, list) or not libraries
981
+ or not all(isinstance(n, str) and n.strip() for n in libraries)):
982
+ add("metadata", "libraries", "libraries must be a non-empty array of library names")
983
+ else:
984
+ names = libraries
985
+ if len(set(names)) != len(names):
986
+ add("metadata", "libraries", "libraries names a library twice")
987
+ if count("library_count") is not None and len(names) != count("library_count"):
988
+ add("metadata", "libraries", f"libraries lists {len(names)} libraries; library_count is "
989
+ f"{count('library_count')}")
990
+ pairs = meta.get("integration_pairs")
991
+ listed_pairs = [] # each well-formed pair once, for the file check below
992
+ if "integration_pairs" in meta:
993
+ if not isinstance(pairs, list):
994
+ add("metadata", "integration_pairs", "integration_pairs must be an array of [library, library] pairs")
995
+ else:
996
+ seen = set()
997
+ for i, pair in enumerate(pairs):
998
+ if (not isinstance(pair, list) or len(pair) != 2
999
+ or not all(isinstance(n, str) for n in pair) or pair[0] == pair[1]):
1000
+ add("metadata", "integration_pairs",
1001
+ f"integration_pairs[{i}] must be two different library names; got {pair!r}")
1002
+ continue
1003
+ if names and not set(pair) <= set(names):
1004
+ add("metadata", "integration_pairs",
1005
+ f"integration_pairs[{i}] names a library libraries does not list: {pair!r}")
1006
+ if frozenset(pair) in seen:
1007
+ add("metadata", "integration_pairs", f"integration_pairs lists {pair!r} twice")
1008
+ else:
1009
+ listed_pairs.append(pair)
1010
+ seen.add(frozenset(pair))
1011
+ if count("integration_count") is not None and len(pairs) != count("integration_count"):
1012
+ add("metadata", "integration_pairs", f"integration_pairs lists {len(pairs)} pairs; "
1013
+ f"integration_count is {count('integration_count')}")
1014
+ distribution = meta.get("confidence_distribution")
1015
+ if isinstance(distribution, dict):
1016
+ for key in _DISTRIBUTION_KEYS:
1017
+ value = distribution.get(key)
1018
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
1019
+ add("metadata", "confidence_distribution",
1020
+ f"confidence_distribution.{key} must be a whole number; got {value!r}")
1021
+
1022
+ # Each library and pair metadata.json lists has a file of its own
1023
+ # (§6), so a misnamed file is caught even when the counts agree.
1024
+ for name in dict.fromkeys(names):
1025
+ if not has_file(references, f"{name}.md"):
1026
+ add("references", f"references/{name}.md",
1027
+ f"libraries lists {name!r}, but references/{name}.md does not exist")
1028
+ for a, b in listed_pairs:
1029
+ if not (has_file(pair_dir, f"{a}-{b}.md") or has_file(pair_dir, f"{b}-{a}.md")):
1030
+ add("references", f"references/integrations/{a}-{b}.md",
1031
+ f"integration_pairs lists {a!r} + {b!r}, but references/integrations/{a}-{b}.md does not exist")
1032
+
1033
+ return issues, observed
1034
+
1035
+
1036
+ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False, skill_type="individual", export_gate=False,
1037
+ forge_tier=None):
435
1038
  """Validate a complete skill package directory.
436
1039
 
437
1040
  When `skip_frontmatter` is True, the SKILL.md frontmatter pass is omitted —
@@ -441,7 +1044,9 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False,
441
1044
  `skill_type` selects the package schema (default "individual", unchanged for
442
1045
  existing callers). "stack" skips the individual-skill body-structure and
443
1046
  metadata-schema passes (which assume the single-skill shape) and instead
444
- runs validate_stack_counts, emitting validation.stack_counts.{issues,observed}.
1047
+ runs validate_stack_counts, emitting validation.stack_counts.{issues,observed},
1048
+ and validate_stack_structure, emitting validation.stack_structure.{issues,observed}
1049
+ (`forge_tier`, when given, is the run's tier metadata.json must record).
445
1050
  Frontmatter and context-snippet checks are shape-agnostic and run for both.
446
1051
 
447
1052
  When `export_gate` is True, a self-contained export-gate validation runs
@@ -515,14 +1120,22 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False,
515
1120
 
516
1121
  # Validate metadata.json
517
1122
  meta_path = files["metadata.json"]
1123
+ stack_meta = None # the parsed metadata.json object the stack structure pass reads
518
1124
  if meta_path.exists():
519
1125
  try:
520
1126
  with open(meta_path, encoding="utf-8") as f:
521
1127
  meta = json.load(f)
522
- if skill_type == "stack":
1128
+ if skill_type == "stack" and not isinstance(meta, dict):
1129
+ # Valid JSON that is not an object (e.g. `[]`) has no fields to read.
1130
+ result["validation"]["metadata"] = {"error": "metadata.json is not a JSON object"}
1131
+ result["validation"]["stack_counts"] = {"skipped": "metadata.json is not a JSON object"}
1132
+ result["summary"]["total_issues"] += 1
1133
+ result["summary"]["by_severity"]["high"] += 1
1134
+ elif skill_type == "stack":
523
1135
  # Individual-skill metadata schema (source_repo, stats.*) does not
524
1136
  # apply to a stack capstone; validate the stack count-equalities.
525
1137
  result["validation"]["metadata"] = {"skipped": "stack-type: individual metadata schema not checked (see validation.stack_counts)"}
1138
+ stack_meta = meta
526
1139
  stack_issues, observed = validate_stack_counts(skill_dir, meta)
527
1140
  result["validation"]["stack_counts"] = {"issues": stack_issues, "observed": observed}
528
1141
  for issue in stack_issues:
@@ -545,6 +1158,15 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False,
545
1158
  if skill_type == "stack":
546
1159
  result["validation"]["stack_counts"] = {"skipped": "metadata.json not found"}
547
1160
 
1161
+ # Stack structure: SKILL.md, reference files and tier labels run without
1162
+ # a readable metadata.json; its own checks need one.
1163
+ if skill_type == "stack":
1164
+ structure_issues, observed = validate_stack_structure(skill_dir, stack_meta, generated_by, forge_tier)
1165
+ result["validation"]["stack_structure"] = {"issues": structure_issues, "observed": observed}
1166
+ for issue in structure_issues:
1167
+ result["summary"]["total_issues"] += 1
1168
+ result["summary"]["by_severity"][issue["severity"]] += 1
1169
+
548
1170
  # Overall pass/fail
549
1171
  result["result"] = "PASS" if result["summary"]["by_severity"]["high"] == 0 else "FAIL"
550
1172
 
@@ -555,10 +1177,12 @@ def _validate_export_gate(skill_dir):
555
1177
  """Export-gate validation for skf-export-skill's publishing gate.
556
1178
 
557
1179
  Self-contained: does not run the individual/stack passes. Emits the
558
- deterministic verdict load-skill.md §2 and package.md §1-3 previously
559
- derived in-prompt — SKILL.md presence/non-emptiness, metadata.json valid
560
- JSON + required-field presence + enum membership, and the SKILL.md
561
- Section 7b <-> on-disk scripts/assets cross-reference — plus a deterministic
1180
+ deterministic verdict load-skill.md §2 and package.md previously
1181
+ derived in-prompt: SKILL.md presence/non-emptiness; metadata.json as a
1182
+ valid JSON object, with required-field presence and enum membership;
1183
+ recommended-field presence by skill_type (low,
1184
+ validation.metadata.recommended_missing); and the SKILL.md Section 7b <->
1185
+ on-disk scripts/assets cross-reference. It adds a deterministic
562
1186
  export_status ∈ READY / WARNINGS / NOT_READY.
563
1187
  """
564
1188
  skill_dir = Path(skill_dir)
@@ -596,11 +1220,12 @@ def _validate_export_gate(skill_dir):
596
1220
  result["validation"]["skill_md"] = {"issues": skill_md_issues}
597
1221
  _record(skill_md_issues)
598
1222
 
599
- # 2. metadata.json must exist, parse as JSON, and satisfy the required-field
600
- # presence + enum-membership contract.
1223
+ # 2. metadata.json must exist, parse as a JSON object, and satisfy the
1224
+ # required-field presence + enum-membership contract; missing
1225
+ # recommended fields for its skill_type are low warnings.
601
1226
  if not meta_path.exists():
602
1227
  meta_issues = [{"severity": "high", "field": "metadata.json", "message": "metadata.json not found"}]
603
- result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": []}
1228
+ result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": [], "recommended_missing": []}
604
1229
  _record(meta_issues)
605
1230
  else:
606
1231
  try:
@@ -608,22 +1233,36 @@ def _validate_export_gate(skill_dir):
608
1233
  meta = json.load(f)
609
1234
  except json.JSONDecodeError as e:
610
1235
  meta_issues = [{"severity": "high", "field": "metadata.json", "message": f"JSON parse error: {e}"}]
611
- result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": []}
1236
+ result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": [], "recommended_missing": []}
612
1237
  _record(meta_issues)
613
1238
  else:
614
- required_issues, enum_issues = validate_metadata_export_gate(meta)
615
- result["validation"]["metadata"] = {"issues": required_issues, "enum_issues": enum_issues}
1239
+ if isinstance(meta, dict):
1240
+ required_issues, enum_issues, recommended_missing = validate_metadata_export_gate(meta)
1241
+ else:
1242
+ # Valid JSON that is not an object (e.g. `[]`) has no fields to read.
1243
+ required_issues = [{"severity": "high", "field": "metadata.json", "message": "metadata.json is not a JSON object"}]
1244
+ enum_issues, recommended_missing = [], []
1245
+ result["validation"]["metadata"] = {
1246
+ "issues": required_issues,
1247
+ "enum_issues": enum_issues,
1248
+ "recommended_missing": recommended_missing,
1249
+ }
616
1250
  _record(required_issues)
617
1251
  _record(enum_issues)
1252
+ _record(recommended_missing)
618
1253
 
619
- # 3. SKILL.md Section 7b <-> on-disk scripts/assets cross-reference.
1254
+ # 3. SKILL.md Section 7b <-> on-disk scripts/assets cross-reference. The
1255
+ # heading the section was found by, and its line, go in the verdict, so
1256
+ # a halt on a missing file shows which heading named it.
620
1257
  missing, orphans = crossref_section_7b(skill_md_text, skill_dir)
1258
+ heading, heading_line = section_7b_heading(skill_md_text)
621
1259
  crossref_issues = []
622
1260
  for path in missing:
623
1261
  crossref_issues.append({
624
1262
  "severity": "high",
625
1263
  "field": "crossref_7b",
626
- "message": f"SKILL.md Section 7b references '{path}' but it is absent on disk",
1264
+ "message": (f"SKILL.md Section 7b ('{heading}' at line {heading_line}) references '{path}' "
1265
+ "but it is absent on disk"),
627
1266
  })
628
1267
  for path in orphans:
629
1268
  crossref_issues.append({
@@ -632,6 +1271,8 @@ def _validate_export_gate(skill_dir):
632
1271
  "message": f"'{path}' present on disk but not referenced in SKILL.md Section 7b (orphan)",
633
1272
  })
634
1273
  result["validation"]["crossref_7b"] = {
1274
+ "heading": heading,
1275
+ "heading_line": heading_line,
635
1276
  "missing": missing,
636
1277
  "orphans": orphans,
637
1278
  "issues": crossref_issues,
@@ -656,7 +1297,7 @@ if __name__ == "__main__":
656
1297
  print(
657
1298
  "Usage: python3 skf-validate-output.py <skill-package-dir> "
658
1299
  "[--generated-by <generator>] [--skip-frontmatter] "
659
- "[--skill-type {individual,stack}] [--export-gate]",
1300
+ "[--skill-type {individual,stack}] [--forge-tier <tier>] [--export-gate]",
660
1301
  file=sys.stderr,
661
1302
  )
662
1303
  sys.exit(1)
@@ -690,8 +1331,15 @@ if __name__ == "__main__":
690
1331
  )
691
1332
  sys.exit(2)
692
1333
 
1334
+ run_forge_tier = None
1335
+ if "--forge-tier" in sys.argv:
1336
+ idx = sys.argv.index("--forge-tier")
1337
+ if idx + 1 < len(sys.argv):
1338
+ run_forge_tier = sys.argv[idx + 1]
1339
+
693
1340
  result = validate_skill_package(
694
- pkg_dir, generated_by=gen_by, skip_frontmatter=skip_fm, skill_type=skill_type
1341
+ pkg_dir, generated_by=gen_by, skip_frontmatter=skip_fm, skill_type=skill_type,
1342
+ forge_tier=run_forge_tier,
695
1343
  )
696
1344
  print(json.dumps(result, indent=2))
697
1345
  sys.exit(0 if result["result"] == "PASS" else 1)