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,665 +0,0 @@
1
- /**
2
- * Validate documentation links, in the source AND in the built site.
3
- *
4
- * WHY THIS EXISTS
5
- *
6
- * `docs/` is read in two places: rendered on GitHub, and published as the
7
- * site. A link can be valid in one and dead in the other, and nothing fails
8
- * on it: an href that resolves nowhere is still valid HTML, so Astro builds
9
- * clean and markdownlint has no opinion. `validate-doc-links.js` covers the
10
- * source side (site-absolute targets and anchors) but never sees the built
11
- * output, so a link that is correct in the Markdown and wrong as a route
12
- * passes every check and ships.
13
- *
14
- * A source-only check is therefore NOT sufficient, and that is the lesson
15
- * this tool encodes. Rewriting happens between the Markdown and the page:
16
- * `website/src/rehype-markdown-links.js` turns `.md` targets into routes, so
17
- * a bug there produces hrefs that look right in source and 404 in a browser.
18
- * Only resolving the BUILT output against the file tree catches that class.
19
- *
20
- * So there are two passes:
21
- *
22
- * 1. SOURCE (docs/ *.md) - a relative `.md` target must exist on disk.
23
- * Cheap, and catches a typo before a build is needed.
24
- * 2. BUILT (build/site/ *.html) - every internal href must resolve to a
25
- * real file. This is the pass that catches plugin bugs, base-path
26
- * bugs, and anything else that only manifests as a route.
27
- *
28
- * The built pass is skipped with a warning when no build is present, so the
29
- * tool stays usable locally without a build step. CI builds the site, so the
30
- * pass runs there. Use --require-build to make a missing build an error.
31
- *
32
- * Usage:
33
- * node tools/validate-docs-links.js [--strict] [--verbose] [--require-build]
34
- */
35
-
36
- const fs = require('node:fs');
37
- const path = require('node:path');
38
-
39
- /** Hosts/schemes that are never local links. */
40
- const EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
41
-
42
- /**
43
- * Read a `--flag value` pair out of an argv array.
44
- *
45
- * @param {string[]} argv - Arguments to search
46
- * @param {string} name - Flag name, including leading dashes
47
- * @returns {string|undefined} The value, or undefined when absent
48
- */
49
- function flagValue(argv, name) {
50
- const at = argv.indexOf(name);
51
- return at === -1 ? undefined : argv[at + 1];
52
- }
53
-
54
- /**
55
- * Resolve where to look, from argv, defaulting to this repository.
56
- *
57
- * The directory overrides exist so the checks can be pointed at a fixture
58
- * tree. Without them every path derives from `__dirname` and the tool can
59
- * only ever inspect its own repo, which makes it untestable: the logic that
60
- * decides whether a merge is blocked would itself be unverified.
61
- *
62
- * @param {string[]} [argv] - Defaults to the real process arguments
63
- * @returns {{projectRoot: string, docsDir: string, buildDir: string, strict: boolean, verbose: boolean, requireBuild: boolean}} Config
64
- */
65
- function resolveConfig(argv = process.argv.slice(2)) {
66
- const projectRoot = flagValue(argv, '--project-root') ?? path.resolve(__dirname, '..');
67
-
68
- return {
69
- projectRoot,
70
- docsDir: flagValue(argv, '--docs-dir') ?? path.join(projectRoot, 'docs'),
71
- buildDir: flagValue(argv, '--build-dir') ?? path.join(projectRoot, 'build', 'site'),
72
- strict: argv.includes('--strict'),
73
- verbose: argv.includes('--verbose'),
74
- requireBuild: argv.includes('--require-build'),
75
- };
76
- }
77
-
78
- /**
79
- * Recursively collect files with one of the given extensions.
80
- *
81
- * @param {string} dir - Directory to walk
82
- * @param {Set<string>} exts - Extensions to keep, with leading dot
83
- * @param {string[]} [out] - Accumulator
84
- * @returns {string[]} Absolute paths
85
- */
86
- function walk(dir, exts, out = []) {
87
- let entries;
88
- try {
89
- entries = fs.readdirSync(dir, { withFileTypes: true });
90
- } catch {
91
- return out;
92
- }
93
-
94
- for (const entry of entries) {
95
- const full = path.join(dir, entry.name);
96
- if (entry.isDirectory()) {
97
- if (entry.name === 'node_modules' || entry.name === '.git') {
98
- continue;
99
- }
100
- walk(full, exts, out);
101
- } else if (exts.has(path.extname(entry.name))) {
102
- out.push(full);
103
- }
104
- }
105
-
106
- return out;
107
- }
108
-
109
- /** Strip the query and anchor from an href, returning the path portion. */
110
- function pathPortion(href) {
111
- const q = href.indexOf('?');
112
- const h = href.indexOf('#');
113
- const first = Math.min(q === -1 ? Infinity : q, h === -1 ? Infinity : h);
114
- return first === Infinity ? href : href.slice(0, first);
115
- }
116
-
117
- // ---------------------------------------------------------------------------
118
- // Pass 1: source
119
- // ---------------------------------------------------------------------------
120
-
121
- /** Markdown inline links: the target inside `](...)`. */
122
- const MD_LINK = /\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
123
-
124
- /**
125
- * Blank out code so a link that is merely *shown* is not read as a real one.
126
- *
127
- * `MD_LINK` is a regex over raw text, so it cannot tell a link from a picture
128
- * of a link. Without this, documenting the link convention itself, e.g. a
129
- * ```markdown block containing [label](./example.md), fails the build as a
130
- * broken link, and the only way to appease it is to create the placeholder
131
- * file. (`tools/validate-file-refs.js` does NOT do this, so it carries the
132
- * same latent false positive; worth fixing there too if it ever fires.)
133
- *
134
- * Fences are tracked line by line rather than matched as a pair, so an
135
- * unterminated fence blanks to end of file instead of silently reverting to
136
- * prose scanning. Inline spans are only stripped outside a fence, so a stray
137
- * backtick inside a block cannot pair with one outside it. Line count is
138
- * preserved, which keeps byte offsets usable if this ever reports positions.
139
- *
140
- * @param {string} text - Raw Markdown
141
- * @returns {string} The same text with fenced blocks and code spans blanked
142
- */
143
- function blankCode(text) {
144
- let fence = null;
145
-
146
- return text
147
- .split('\n')
148
- .map((line) => {
149
- const marker = line.match(/^\s{0,3}(`{3,}|~{3,})/);
150
-
151
- if (fence) {
152
- // A closing fence is the same character, and at least as long.
153
- if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length) {
154
- fence = null;
155
- }
156
- return '';
157
- }
158
- if (marker) {
159
- fence = marker[1];
160
- return '';
161
- }
162
-
163
- // Inline spans: a run of N backticks closes on a run of N.
164
- return line.replaceAll(/(`+)[^`]*\1/g, ' ');
165
- })
166
- .join('\n');
167
- }
168
-
169
- /**
170
- * Check that every relative `.md` target in docs/ exists on disk.
171
- *
172
- * Deliberately does NOT enforce a `./` prefix. The plugin accepts bare links,
173
- * and GitHub renders them correctly, so requiring the prefix would be style,
174
- * not correctness. What matters is that the target exists.
175
- */
176
- function checkSource({ projectRoot, docsDir }, report) {
177
- const files = walk(docsDir, new Set(['.md']));
178
-
179
- for (const file of files) {
180
- const rel = path.relative(projectRoot, file);
181
- const dir = path.dirname(file);
182
- const text = blankCode(fs.readFileSync(file, 'utf8'));
183
-
184
- for (const match of text.matchAll(MD_LINK)) {
185
- const target = pathPortion(match[1]);
186
-
187
- if (!target || target.startsWith('#') || EXTERNAL.test(target)) {
188
- continue;
189
- }
190
- if (!target.endsWith('.md')) {
191
- continue;
192
- }
193
- // Root-absolute targets mean different things on the two surfaces, and
194
- // this pass owns the GitHub one. GitHub resolves `/x.md` against the
195
- // REPOSITORY root (it rewrites the href to /<owner>/<repo>/blob/<ref>/x.md),
196
- // whereas the rewriter treats docs/ as the site root, so `/architecture.md`
197
- // becomes the valid route /architecture/ while pointing at a repo-root
198
- // file that does not exist. The built pass only ever sees the route, so
199
- // it cannot notice. Resolve against the repo root, which is the surface
200
- // this pass is responsible for.
201
- //
202
- // Note `/docs/architecture.md` is CORRECT on both: repo-root
203
- // docs/architecture.md exists, and the rewriter strips the `/docs`
204
- // prefix. Existence is the test, not the prefix.
205
- const resolved = target.startsWith('/') ? path.join(projectRoot, target) : path.resolve(dir, target);
206
-
207
- if (!fs.existsSync(resolved)) {
208
- report(
209
- rel,
210
- target.startsWith('/')
211
- ? `root-absolute link target does not exist at the repository root, so it is dead on GitHub: ${match[1]}`
212
- : `link target does not exist: ${match[1]}`,
213
- );
214
- }
215
- }
216
- }
217
-
218
- return files.length;
219
- }
220
-
221
- // ---------------------------------------------------------------------------
222
- // Pass 2: built output
223
- // ---------------------------------------------------------------------------
224
-
225
- /** Normalize a URL pathname into a base with exactly one trailing slash. */
226
- function asBase(pathname) {
227
- if (!pathname || pathname === '/') {
228
- return '/';
229
- }
230
- return pathname.endsWith('/') ? pathname : `${pathname}/`;
231
- }
232
-
233
- /**
234
- * Resolve the base path the build on disk was actually produced with.
235
- *
236
- * Read from the ARTIFACT, not the environment. The environment can disagree
237
- * with the build (a site built with SITE_URL set, then validated without it),
238
- * and that disagreement makes every absolute href look broken: a loud, and
239
- * entirely false, failure. The root page's canonical URL is written by the
240
- * build itself, so it cannot drift from the output it describes.
241
- *
242
- * Falls back to the environment, then to '/', when no canonical is present.
243
- *
244
- * @returns {Promise<string>} The base path, e.g. '/repo/' or '/'
245
- */
246
- async function resolveBase({ projectRoot, buildDir }) {
247
- const rootIndex = path.join(buildDir, 'index.html');
248
-
249
- if (fs.existsSync(rootIndex)) {
250
- const html = fs.readFileSync(rootIndex, 'utf8');
251
- const canonical =
252
- html.match(/<link[^>]+rel="canonical"[^>]+href="([^"]+)"/i) || html.match(/<meta[^>]+property="og:url"[^>]+content="([^"]+)"/i);
253
-
254
- if (canonical) {
255
- try {
256
- return asBase(new URL(canonical[1]).pathname);
257
- } catch {
258
- // Not an absolute URL; fall through to the environment.
259
- }
260
- }
261
- }
262
-
263
- try {
264
- const mod = await import(`file://${path.join(projectRoot, 'website', 'src', 'lib', 'site-url.js')}`);
265
- return asBase(new URL(mod.getSiteUrl()).pathname);
266
- } catch {
267
- return '/';
268
- }
269
- }
270
-
271
- /**
272
- * Every URL a page asks the browser to fetch: navigations AND subresources.
273
- *
274
- * `href` alone is not the whole surface. A missing `src` is the same defect
275
- * with a worse symptom: the browser fetches it without the user clicking, and
276
- * a 404 script or image degrades the page silently. This matters here because
277
- * assets under `website/public/` are copied verbatim and referenced by
278
- * hand-written string paths (see `mermaid-lightbox.js` in astro.config.mjs),
279
- * so renaming one leaves a green build and a dead reference on every page.
280
- *
281
- * `srcset` is a comma-separated candidate list where each entry is a URL
282
- * optionally followed by a width/density descriptor, so it is split apart.
283
- * The build emits no `srcset` today, only SVGs, but adding one raster image
284
- * to the docs would change that without touching this file.
285
- *
286
- * @param {string} html - Full page source
287
- * @returns {Array<{attr: string, url: string}>} References, in document order
288
- */
289
- function assetUrls(html) {
290
- const refs = [];
291
-
292
- // `src` here never matches `srcset=`, which continues with `set=`.
293
- for (const match of html.matchAll(/\s(href|src)="([^"]*)"/g)) {
294
- refs.push({ attr: match[1], url: match[2] });
295
- }
296
-
297
- for (const match of html.matchAll(/\ssrcset="([^"]*)"/g)) {
298
- for (const candidate of match[1].split(',')) {
299
- const url = candidate.trim().split(/\s+/)[0];
300
- if (url) {
301
- refs.push({ attr: 'srcset', url });
302
- }
303
- }
304
- }
305
-
306
- return refs;
307
- }
308
-
309
- /**
310
- * Verify every internal reference in the built site resolves to a real file.
311
- *
312
- * Covers navigations (`href`) and subresources (`src`, `srcset`) alike: both
313
- * are URLs the page promises the browser it can fetch.
314
- *
315
- * A route ending in `/` must have an `index.html`; anything else must exist
316
- * as a file. Relative URLs are resolved against the page's own directory,
317
- * which is precisely how a browser resolves them, and precisely the step that
318
- * the earlier page-relative bug failed.
319
- *
320
- * @param {string} base - Deployment base path
321
- * @returns {number} Number of HTML pages checked
322
- */
323
- function checkBuilt({ projectRoot, buildDir }, base, report) {
324
- const pages = walk(buildDir, new Set(['.html']));
325
-
326
- for (const page of pages) {
327
- const rel = path.relative(projectRoot, page);
328
- const html = fs.readFileSync(page, 'utf8');
329
- // The page's URL directory, e.g. build/site/troubleshooting/index.html
330
- // is served at /troubleshooting/, so relative links resolve from there.
331
- const pageDir = path.dirname(page);
332
-
333
- const seen = new Set();
334
-
335
- for (const { attr, url: href } of assetUrls(html)) {
336
- if (!href || href.startsWith('#') || EXTERNAL.test(href) || seen.has(href)) {
337
- continue;
338
- }
339
- seen.add(href);
340
-
341
- const target = pathPortion(href);
342
- if (!target) {
343
- continue;
344
- }
345
-
346
- let resolved;
347
- if (target.startsWith('/')) {
348
- // Root-absolute: strip the base, then read from the build root.
349
- let withoutBase = target;
350
- if (base !== '/' && target.startsWith(base)) {
351
- withoutBase = `/${target.slice(base.length)}`;
352
- } else if (base !== '/') {
353
- report(rel, `absolute ${attr} is missing the base path ${base}: ${href}`);
354
- continue;
355
- }
356
- resolved = path.join(buildDir, withoutBase);
357
- } else {
358
- resolved = path.resolve(pageDir, target);
359
- }
360
-
361
- // Never let a link escape the build root.
362
- if (!resolved.startsWith(buildDir + path.sep)) {
363
- report(rel, `${attr} escapes the site root: ${href}`);
364
- continue;
365
- }
366
-
367
- const candidate = target.endsWith('/') || !path.extname(resolved) ? path.join(resolved, 'index.html') : resolved;
368
-
369
- if (!fs.existsSync(candidate)) {
370
- report(rel, `${attr} resolves to nothing: ${href} (looked for ${path.relative(buildDir, candidate)})`);
371
- }
372
- }
373
- }
374
-
375
- return pages.length;
376
- }
377
-
378
- // ---------------------------------------------------------------------------
379
-
380
- const BUILD_INFRA_INPUTS = [
381
- 'website/src/rehype-markdown-links.js',
382
- 'website/src/rehype-base-paths.js',
383
- 'website/src/lib/site-url.js',
384
- 'website/astro.config.mjs',
385
- 'tools/build-docs.js',
386
- ];
387
-
388
- /**
389
- * One sha1 per build input: the flat docs/*.md set plus BUILD_INFRA_INPUTS,
390
- * the route-shaping infrastructure (BOTH rehype rewriters, the base/site
391
- * resolver, the Astro config, and the builder itself). Canonical for both the
392
- * manifest writer in tools/build-docs.js and the freshness check below - the
393
- * first cut duplicated the list in both tools and both copies missed the
394
- * second rehype rewriter, which is exactly the drift a single owner prevents.
395
- */
396
- function collectBuildInputs(projectRoot) {
397
- const crypto = require('node:crypto');
398
- const sha1 = (file) => crypto.createHash('sha1').update(fs.readFileSync(file)).digest('hex');
399
- const inputs = {};
400
- const docsDir = path.join(projectRoot, 'docs');
401
- if (fs.existsSync(docsDir)) {
402
- for (const name of fs.readdirSync(docsDir).sort()) {
403
- if (name.endsWith('.md')) {
404
- inputs[`docs/${name}`] = sha1(path.join(docsDir, name));
405
- }
406
- }
407
- }
408
- for (const rel of BUILD_INFRA_INPUTS) {
409
- const file = path.join(projectRoot, rel);
410
- if (fs.existsSync(file)) {
411
- inputs[rel] = sha1(file);
412
- }
413
- }
414
- return inputs;
415
- }
416
-
417
- /**
418
- * Compare the build-inputs manifest against the tree as it is NOW.
419
- *
420
- * Returns {state, changed}: state is 'missing' (no manifest - an older build,
421
- * nothing to compare), 'match', or 'stale' with the changed inputs listed.
422
- * A 'match' is deliberately NOT a freshness certificate: Astro's content cache
423
- * can replay a stale render against unchanged inputs, so a matching manifest
424
- * proves nothing - only a MISmatch proves staleness. That asymmetry is why the
425
- * stale branch speaks definitely and the match branch stays hedged.
426
- */
427
- function buildInputsStatus({ projectRoot, buildDir }) {
428
- const manifestPath = path.join(path.dirname(buildDir), '.build-inputs.json');
429
- let manifest;
430
- try {
431
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
432
- } catch {
433
- return { state: 'missing', changed: [] };
434
- }
435
- if (!manifest || typeof manifest.inputs !== 'object' || manifest.inputs === null) {
436
- return { state: 'missing', changed: [] };
437
- }
438
- const current = collectBuildInputs(projectRoot);
439
- const changed = [];
440
- for (const key of new Set([...Object.keys(manifest.inputs), ...Object.keys(current)])) {
441
- if (manifest.inputs[key] !== current[key]) {
442
- changed.push(key);
443
- }
444
- }
445
- changed.sort();
446
- return { state: changed.length > 0 ? 'stale' : 'match', changed };
447
- }
448
-
449
- /**
450
- * Resolve every site-internal link in the built llms.txt against the built
451
- * tree. This link class is invisible to the HTML pass (llms.txt is not HTML),
452
- * which is exactly how a deleted page's index entry survived every check.
453
- *
454
- * The site root is read off the file's own `Documentation: <url>` header line
455
- * rather than off the build base, so the check is independent of what base the
456
- * local render happened to use. No header, or no file: nothing to check -
457
- * reported as a count of 0, never as a pass over content that was not read.
458
- */
459
- function checkLlmsIndex({ buildDir }, report) {
460
- const llmsPath = path.join(buildDir, 'llms.txt');
461
- let text;
462
- try {
463
- text = fs.readFileSync(llmsPath, 'utf-8');
464
- } catch {
465
- return 0;
466
- }
467
- const rootMatch = text.match(/^Documentation:\s+(\S+)$/m);
468
- if (!rootMatch) {
469
- return 0;
470
- }
471
- let rootPath;
472
- try {
473
- rootPath = new URL(rootMatch[1]).pathname;
474
- } catch {
475
- return 0;
476
- }
477
- if (!rootPath.endsWith('/')) {
478
- rootPath += '/';
479
- }
480
- let checked = 0;
481
- for (const match of text.matchAll(/\]\((\S+?)\)/g)) {
482
- const target = match[1];
483
- let url;
484
- try {
485
- url = new URL(target);
486
- } catch {
487
- continue; // relative or malformed: llms.txt only carries absolute links
488
- }
489
- if (!url.pathname.startsWith(rootPath)) {
490
- continue; // external link, not a site route
491
- }
492
- checked += 1;
493
- const route = url.pathname.slice(rootPath.length);
494
- const candidates =
495
- route === ''
496
- ? [path.join(buildDir, 'index.html')]
497
- : [path.join(buildDir, route.replace(/\/$/, ''), 'index.html'), path.join(buildDir, route)];
498
- if (!candidates.some((candidate) => fs.existsSync(candidate))) {
499
- report(
500
- 'build/site/llms.txt',
501
- `links a route that resolves to nothing: ${target} (looked for ${candidates
502
- .map((candidate) => path.relative(buildDir, candidate))
503
- .join(' or ')})`,
504
- );
505
- }
506
- }
507
- return checked;
508
- }
509
-
510
- /**
511
- * Run both passes and return what happened, without touching the process.
512
- *
513
- * Separated from the CLI so tests can drive it against a fixture tree and
514
- * assert on the result rather than on stdout or an exit code.
515
- *
516
- * @param {object} cfg - From resolveConfig()
517
- * @param {(msg: string) => void} [log] - Output sink; defaults to console.log
518
- * @returns {Promise<{issues: Array<{file: string, detail: string}>, sourceCount: number, builtCount: number, haveBuild: boolean, missingRequiredBuild: boolean, ok: boolean}>} Outcome
519
- */
520
- async function run(cfg, log = console.log) {
521
- const issues = [];
522
- const report = (file, detail) => issues.push({ file, detail });
523
-
524
- log(`\nValidating documentation links in: ${cfg.projectRoot}`);
525
- log(`Mode: ${cfg.strict ? 'STRICT (exit 1 on issues)' : 'ADVISORY'}`);
526
- log('');
527
-
528
- if (!fs.existsSync(cfg.docsDir)) {
529
- throw new Error(`docs/ not found at ${cfg.docsDir}`);
530
- }
531
-
532
- const sourceCount = checkSource(cfg, report);
533
- log(`Source pass: ${sourceCount} markdown file(s) in docs/`);
534
-
535
- let builtCount = 0;
536
- const haveBuild = fs.existsSync(cfg.buildDir);
537
- let missingRequiredBuild = false;
538
-
539
- if (haveBuild) {
540
- const base = await resolveBase(cfg);
541
- builtCount = checkBuilt(cfg, base, report);
542
- log(`Built pass: ${builtCount} page(s) in build/site (base ${base})`);
543
-
544
- const llmsCount = checkLlmsIndex(cfg, report);
545
- log(`llms.txt: ${llmsCount} site link(s) checked`);
546
-
547
- // This pass is only as honest as the build it reads. The manifest turns
548
- // "may be stale" into a definite STALE where inputs provably changed; a
549
- // matching manifest is still no freshness certificate, because Astro
550
- // caches rendered content in website/node_modules/.astro and that cache
551
- // survives both `rm -rf website/.astro` and a rebuild - a stale render can
552
- // report a clean pass against unchanged inputs (this bit the author while
553
- // writing this tool). CI is immune because `npm ci` starts cold.
554
- // Only the hedged branch is suppressed under --require-build (the CI
555
- // path); a provable STALE always speaks, since a cold CI build that
556
- // still mismatches its inputs is worth saying out loud.
557
- const freshness = buildInputsStatus(cfg);
558
- if (freshness.state === 'stale') {
559
- log('');
560
- log(` STALE BUILD: ${freshness.changed.length} input(s) changed since this render:`);
561
- for (const name of freshness.changed.slice(0, 5)) {
562
- log(` - ${name}`);
563
- }
564
- if (freshness.changed.length > 5) {
565
- log(` - (and ${freshness.changed.length - 5} more)`);
566
- }
567
- log(' This pass validated the OLD render. Rebuild before trusting it:');
568
- log(' npm run docs:build');
569
- } else if (!cfg.requireBuild) {
570
- log('');
571
- log(' Reading an existing build. If results look impossible, the render');
572
- log(' may be cached. Clear it and rebuild:');
573
- log(' rm -rf website/node_modules/.astro website/node_modules/.vite website/.astro build/site');
574
- log(' npm run docs:build');
575
- }
576
- } else if (cfg.requireBuild) {
577
- missingRequiredBuild = true;
578
- report('build/site', 'no build found, and --require-build was given. Run `npm run docs:build` first.');
579
- } else {
580
- log('Built pass: SKIPPED (no build/site). This pass is what catches route bugs.');
581
- log(' Run `npm run docs:build` first, or pass --require-build to enforce it.');
582
- }
583
-
584
- log('');
585
- log('\u2500'.repeat(60));
586
- log('');
587
-
588
- if (issues.length === 0) {
589
- log('Summary:');
590
- log(` Markdown files: ${sourceCount}`);
591
- log(` Built pages: ${builtCount}${haveBuild ? '' : ' (skipped)'}`);
592
- log('');
593
-
594
- // Never report an unqualified pass when the load-bearing pass did not run.
595
- // A green line that means "half the checks were skipped" is the exact
596
- // failure mode this tool exists to prevent, and reading one as a full pass
597
- // is how the broken links reached production in the first place.
598
- if (haveBuild) {
599
- log(' All documentation links valid!');
600
- } else {
601
- log(' Source links valid. THE BUILT-OUTPUT PASS DID NOT RUN, so');
602
- log(' route bugs (the class that shipped 404s) are unchecked here.');
603
- log(' Run `npm run docs:build` first for the full check. CI always does.');
604
- }
605
- log('');
606
- } else {
607
- const byFile = new Map();
608
- for (const issue of issues) {
609
- if (!byFile.has(issue.file)) {
610
- byFile.set(issue.file, []);
611
- }
612
- byFile.get(issue.file).push(issue.detail);
613
- }
614
-
615
- log(`Broken documentation links: ${issues.length}\n`);
616
- for (const [file, details] of byFile) {
617
- log(` ${file}`);
618
- for (const detail of details) {
619
- log(` - ${detail}`);
620
- }
621
- }
622
- log('');
623
-
624
- if (cfg.verbose) {
625
- log('A route that resolves to nothing is still valid HTML, so neither');
626
- log('Astro nor markdownlint will fail on it. That is why this check exists.\n');
627
- }
628
- }
629
-
630
- // --require-build is a fail-closed contract in its own right: the flag exists
631
- // to turn the skipped built pass into an error, so honouring it must not
632
- // depend on --strict also being passed. Everything else stays advisory.
633
- const ok = !(issues.length > 0 && (cfg.strict || missingRequiredBuild));
634
-
635
- return { issues, sourceCount, builtCount, haveBuild, missingRequiredBuild, ok };
636
- }
637
-
638
- module.exports = {
639
- resolveConfig,
640
- run,
641
- checkSource,
642
- checkBuilt,
643
- resolveBase,
644
- blankCode,
645
- assetUrls,
646
- walk,
647
- buildInputsStatus,
648
- collectBuildInputs,
649
- checkLlmsIndex,
650
- };
651
-
652
- /* c8 ignore start -- CLI entry point */
653
- if (require.main === module) {
654
- run(resolveConfig())
655
- .then((result) => {
656
- if (!result.ok) {
657
- process.exit(1);
658
- }
659
- })
660
- .catch((error) => {
661
- console.error(error.message);
662
- process.exit(1);
663
- });
664
- }
665
- /* c8 ignore stop */