incitaciones 0.1.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 (318) hide show
  1. package/LICENSE +395 -0
  2. package/README.md +410 -0
  3. package/content/distilled/abstraction-miner.md +32 -0
  4. package/content/distilled/adversarial-stakeholder-evaluation.md +121 -0
  5. package/content/distilled/anti-slop-prose.md +46 -0
  6. package/content/distilled/bias-audit.md +39 -0
  7. package/content/distilled/code-review/SKILL.md +53 -0
  8. package/content/distilled/code-review/references/criteria.md +50 -0
  9. package/content/distilled/code-review/references/templates.md +75 -0
  10. package/content/distilled/code-review/references/variants.md +32 -0
  11. package/content/distilled/commit/SKILL.md +41 -0
  12. package/content/distilled/commit/references/criteria.md +21 -0
  13. package/content/distilled/commit/references/templates.md +46 -0
  14. package/content/distilled/composability-diagnostician.md +67 -0
  15. package/content/distilled/context-guardian.md +20 -0
  16. package/content/distilled/create-handoff/SKILL.md +16 -0
  17. package/content/distilled/create-handoff/references/handoff-template.md +65 -0
  18. package/content/distilled/create-handoff/references/naming.md +7 -0
  19. package/content/distilled/create-issues/SKILL.md +72 -0
  20. package/content/distilled/create-issues/references/criteria.md +44 -0
  21. package/content/distilled/create-issues/references/templates.md +75 -0
  22. package/content/distilled/create-plan/SKILL.md +19 -0
  23. package/content/distilled/create-plan/references/plan-template.md +87 -0
  24. package/content/distilled/create-plan/references/scenarios.md +19 -0
  25. package/content/distilled/debug/SKILL.md +42 -0
  26. package/content/distilled/debug/references/criteria.md +27 -0
  27. package/content/distilled/debug/references/templates.md +39 -0
  28. package/content/distilled/describe-pr/SKILL.md +17 -0
  29. package/content/distilled/describe-pr/references/pr-template.md +62 -0
  30. package/content/distilled/design-practice.md +213 -0
  31. package/content/distilled/design-review/SKILL.md +29 -0
  32. package/content/distilled/design-review/references/combined-review.md +27 -0
  33. package/content/distilled/design-review/references/decision-matrix.md +122 -0
  34. package/content/distilled/design-review/references/problem-statement.md +141 -0
  35. package/content/distilled/design-review/references/report-template.md +36 -0
  36. package/content/distilled/design-review/references/scope-document.md +51 -0
  37. package/content/distilled/distill-prompt/SKILL.md +41 -0
  38. package/content/distilled/distill-prompt/references/criteria.md +26 -0
  39. package/content/distilled/distill-prompt/references/templates.md +30 -0
  40. package/content/distilled/doc-link-verifier/SKILL.md +212 -0
  41. package/content/distilled/doc-link-verifier/scripts/check_links.py +453 -0
  42. package/content/distilled/doc-link-verifier/scripts/extract_links.py +521 -0
  43. package/content/distilled/edge-case-discovery.md +123 -0
  44. package/content/distilled/error-handling-diagnostician.md +90 -0
  45. package/content/distilled/extract-prompt.md +220 -0
  46. package/content/distilled/formal-verification-evaluator.md +158 -0
  47. package/content/distilled/grill-me.md +24 -0
  48. package/content/distilled/guided-review.md +68 -0
  49. package/content/distilled/implement-documentation.md +68 -0
  50. package/content/distilled/implement-plan/SKILL.md +23 -0
  51. package/content/distilled/implement-plan/references/common-situations.md +36 -0
  52. package/content/distilled/implement-plan/references/verification-template.md +18 -0
  53. package/content/distilled/invalid-states-diagnostician.md +63 -0
  54. package/content/distilled/issue-review/SKILL.md +37 -0
  55. package/content/distilled/issue-review/references/convergence.md +19 -0
  56. package/content/distilled/issue-review/references/final-report.md +60 -0
  57. package/content/distilled/issue-review/references/pass-1-clarity.md +28 -0
  58. package/content/distilled/issue-review/references/pass-2-scope.md +22 -0
  59. package/content/distilled/issue-review/references/pass-3-dependencies.md +23 -0
  60. package/content/distilled/issue-review/references/pass-4-alignment.md +17 -0
  61. package/content/distilled/issue-review/references/pass-5-executability.md +17 -0
  62. package/content/distilled/issue-review/references/setup.md +25 -0
  63. package/content/distilled/iterate-plan/SKILL.md +24 -0
  64. package/content/distilled/iterate-plan/references/change-template.md +15 -0
  65. package/content/distilled/iterate-plan/references/common-scenarios.md +14 -0
  66. package/content/distilled/julia-performance-diagnostician.md +71 -0
  67. package/content/distilled/modularity-diagnostician.md +68 -0
  68. package/content/distilled/multi-agent-review.md +196 -0
  69. package/content/distilled/mutability-diagnostician.md +63 -0
  70. package/content/distilled/narrative-article.md +60 -0
  71. package/content/distilled/optionality-review.md +190 -0
  72. package/content/distilled/parallel-review/SKILL.md +49 -0
  73. package/content/distilled/parallel-review/references/criteria.md +34 -0
  74. package/content/distilled/parallel-review/references/templates.md +59 -0
  75. package/content/distilled/plan-review/SKILL.md +42 -0
  76. package/content/distilled/plan-review/references/criteria.md +51 -0
  77. package/content/distilled/plan-review/references/templates.md +67 -0
  78. package/content/distilled/pre-mortem.md +10 -0
  79. package/content/distilled/project-status-report.md +124 -0
  80. package/content/distilled/pwa-accessibility-review.md +81 -0
  81. package/content/distilled/rca-diagnostician/SKILL.md +30 -0
  82. package/content/distilled/rca-diagnostician/references/action-hierarchy.md +77 -0
  83. package/content/distilled/rca-diagnostician/references/bias-countermeasures.md +64 -0
  84. package/content/distilled/rca-diagnostician/references/evidence-timeline.md +49 -0
  85. package/content/distilled/rca-diagnostician/references/hypothesis-methods.md +53 -0
  86. package/content/distilled/rca-diagnostician/references/problem-definition.md +34 -0
  87. package/content/distilled/rca-diagnostician/references/report-template.md +75 -0
  88. package/content/distilled/rca-diagnostician/references/rigor-checklist.md +66 -0
  89. package/content/distilled/red-team-review.md +133 -0
  90. package/content/distilled/research-codebase/SKILL.md +22 -0
  91. package/content/distilled/research-codebase/references/report-template.md +75 -0
  92. package/content/distilled/research-codebase/references/search-patterns.md +27 -0
  93. package/content/distilled/research-documentation.md +79 -0
  94. package/content/distilled/research-review/SKILL.md +42 -0
  95. package/content/distilled/research-review/references/criteria.md +51 -0
  96. package/content/distilled/research-review/references/templates.md +73 -0
  97. package/content/distilled/resonant-refactor.md +45 -0
  98. package/content/distilled/resume-handoff/SKILL.md +19 -0
  99. package/content/distilled/resume-handoff/references/analysis-template.md +40 -0
  100. package/content/distilled/resume-handoff/references/locating.md +12 -0
  101. package/content/distilled/review-documentation.md +44 -0
  102. package/content/distilled/rigidity-diagnostician.md +70 -0
  103. package/content/distilled/rule-of-5-universal/SKILL.md +26 -0
  104. package/content/distilled/rule-of-5-universal/references/criteria.md +34 -0
  105. package/content/distilled/rule-of-5-universal/references/examples.md +69 -0
  106. package/content/distilled/rule-of-5-universal/references/templates.md +136 -0
  107. package/content/distilled/specification-evaluation-diagnostician.md +106 -0
  108. package/content/distilled/specification-review/SKILL.md +43 -0
  109. package/content/distilled/specification-review/references/criteria.md +51 -0
  110. package/content/distilled/specification-review/references/templates.md +67 -0
  111. package/content/distilled/systematic-housekeeping.md +27 -0
  112. package/content/distilled/tdd/SKILL.md +38 -0
  113. package/content/distilled/tdd/references/criteria.md +26 -0
  114. package/content/distilled/tdd/references/templates.md +53 -0
  115. package/content/distilled/test-abstraction-miner.md +43 -0
  116. package/content/distilled/test-friction.md +51 -0
  117. package/content/distilled/testability-implementability-evaluator.md +144 -0
  118. package/content/distilled/ui-align.md +32 -0
  119. package/content/distilled/ux-dx-evaluation-diagnostician/SKILL.md +21 -0
  120. package/content/distilled/ux-dx-evaluation-diagnostician/references/layer-1-product.md +20 -0
  121. package/content/distilled/ux-dx-evaluation-diagnostician/references/layer-2-engineering.md +20 -0
  122. package/content/distilled/ux-dx-evaluation-diagnostician/references/layer-3-interface.md +23 -0
  123. package/content/distilled/ux-dx-evaluation-diagnostician/references/layer-4-ecosystem.md +20 -0
  124. package/content/distilled/ux-dx-evaluation-diagnostician/references/layer-5-documentation.md +19 -0
  125. package/content/distilled/ux-dx-evaluation-diagnostician/references/report-template.md +36 -0
  126. package/content/distilled/verification-diagnostician.md +90 -0
  127. package/content/distilled/verify-prompt/SKILL.md +32 -0
  128. package/content/distilled/verify-prompt/references/criteria.md +21 -0
  129. package/content/manifest.json +3220 -0
  130. package/package.json +50 -0
  131. package/pi-package/README.md +9 -0
  132. package/pi-package/prompts/abstraction-miner.md +10 -0
  133. package/pi-package/prompts/adversarial-stakeholder-evaluation.md +10 -0
  134. package/pi-package/prompts/anti-slop-prose.md +10 -0
  135. package/pi-package/prompts/bias-audit.md +10 -0
  136. package/pi-package/prompts/code-review.md +10 -0
  137. package/pi-package/prompts/commit.md +10 -0
  138. package/pi-package/prompts/composability-diagnostician.md +10 -0
  139. package/pi-package/prompts/context-guardian.md +10 -0
  140. package/pi-package/prompts/create-handoff.md +10 -0
  141. package/pi-package/prompts/create-issues.md +10 -0
  142. package/pi-package/prompts/create-plan.md +10 -0
  143. package/pi-package/prompts/debug.md +10 -0
  144. package/pi-package/prompts/describe-pr.md +10 -0
  145. package/pi-package/prompts/design-practice.md +10 -0
  146. package/pi-package/prompts/design-review.md +10 -0
  147. package/pi-package/prompts/distill-prompt.md +10 -0
  148. package/pi-package/prompts/doc-link-verifier.md +10 -0
  149. package/pi-package/prompts/edge-case-discovery.md +10 -0
  150. package/pi-package/prompts/error-handling-diagnostician.md +10 -0
  151. package/pi-package/prompts/extract-prompt.md +10 -0
  152. package/pi-package/prompts/formal-verification-evaluator.md +10 -0
  153. package/pi-package/prompts/grill-me.md +10 -0
  154. package/pi-package/prompts/guided-review.md +10 -0
  155. package/pi-package/prompts/implement-documentation.md +10 -0
  156. package/pi-package/prompts/implement-plan.md +10 -0
  157. package/pi-package/prompts/invalid-states-diagnostician.md +10 -0
  158. package/pi-package/prompts/issue-review.md +10 -0
  159. package/pi-package/prompts/iterate-plan.md +10 -0
  160. package/pi-package/prompts/julia-performance-diagnostician.md +10 -0
  161. package/pi-package/prompts/modularity-diagnostician.md +10 -0
  162. package/pi-package/prompts/multi-agent-review.md +10 -0
  163. package/pi-package/prompts/mutability-diagnostician.md +10 -0
  164. package/pi-package/prompts/narrative-article.md +10 -0
  165. package/pi-package/prompts/optionality-review.md +10 -0
  166. package/pi-package/prompts/parallel-review.md +10 -0
  167. package/pi-package/prompts/plan-review.md +10 -0
  168. package/pi-package/prompts/pre-mortem.md +10 -0
  169. package/pi-package/prompts/project-status-report.md +10 -0
  170. package/pi-package/prompts/pwa-accessibility-review.md +10 -0
  171. package/pi-package/prompts/rca-diagnostician.md +10 -0
  172. package/pi-package/prompts/red-team-review.md +10 -0
  173. package/pi-package/prompts/research-codebase.md +10 -0
  174. package/pi-package/prompts/research-documentation.md +10 -0
  175. package/pi-package/prompts/research-review.md +10 -0
  176. package/pi-package/prompts/resonant-refactor.md +10 -0
  177. package/pi-package/prompts/resume-handoff.md +10 -0
  178. package/pi-package/prompts/review-documentation.md +10 -0
  179. package/pi-package/prompts/rigidity-diagnostician.md +10 -0
  180. package/pi-package/prompts/rule-of-5-universal.md +10 -0
  181. package/pi-package/prompts/specification-evaluation-diagnostician.md +10 -0
  182. package/pi-package/prompts/specification-review.md +10 -0
  183. package/pi-package/prompts/systematic-housekeeping.md +10 -0
  184. package/pi-package/prompts/tdd.md +10 -0
  185. package/pi-package/prompts/test-abstraction-miner.md +10 -0
  186. package/pi-package/prompts/test-friction.md +10 -0
  187. package/pi-package/prompts/testability-implementability-evaluator.md +10 -0
  188. package/pi-package/prompts/ui-align.md +10 -0
  189. package/pi-package/prompts/ux-dx-evaluation-diagnostician.md +10 -0
  190. package/pi-package/prompts/verification-diagnostician.md +10 -0
  191. package/pi-package/prompts/verify-prompt.md +10 -0
  192. package/pi-package/skills/abstraction-miner/SKILL.md +38 -0
  193. package/pi-package/skills/adversarial-stakeholder-evaluation/SKILL.md +127 -0
  194. package/pi-package/skills/anti-slop-prose/SKILL.md +52 -0
  195. package/pi-package/skills/bias-audit/SKILL.md +45 -0
  196. package/pi-package/skills/code-review/SKILL.md +59 -0
  197. package/pi-package/skills/code-review/references/criteria.md +50 -0
  198. package/pi-package/skills/code-review/references/templates.md +75 -0
  199. package/pi-package/skills/code-review/references/variants.md +32 -0
  200. package/pi-package/skills/commit/SKILL.md +47 -0
  201. package/pi-package/skills/commit/references/criteria.md +21 -0
  202. package/pi-package/skills/commit/references/templates.md +46 -0
  203. package/pi-package/skills/composability-diagnostician/SKILL.md +73 -0
  204. package/pi-package/skills/context-guardian/SKILL.md +26 -0
  205. package/pi-package/skills/create-handoff/SKILL.md +22 -0
  206. package/pi-package/skills/create-handoff/references/handoff-template.md +65 -0
  207. package/pi-package/skills/create-handoff/references/naming.md +7 -0
  208. package/pi-package/skills/create-issues/SKILL.md +78 -0
  209. package/pi-package/skills/create-issues/references/criteria.md +44 -0
  210. package/pi-package/skills/create-issues/references/templates.md +75 -0
  211. package/pi-package/skills/create-plan/SKILL.md +25 -0
  212. package/pi-package/skills/create-plan/references/plan-template.md +87 -0
  213. package/pi-package/skills/create-plan/references/scenarios.md +19 -0
  214. package/pi-package/skills/debug/SKILL.md +48 -0
  215. package/pi-package/skills/debug/references/criteria.md +27 -0
  216. package/pi-package/skills/debug/references/templates.md +39 -0
  217. package/pi-package/skills/describe-pr/SKILL.md +23 -0
  218. package/pi-package/skills/describe-pr/references/pr-template.md +62 -0
  219. package/pi-package/skills/design-practice/SKILL.md +219 -0
  220. package/pi-package/skills/design-review/SKILL.md +35 -0
  221. package/pi-package/skills/design-review/references/combined-review.md +27 -0
  222. package/pi-package/skills/design-review/references/decision-matrix.md +122 -0
  223. package/pi-package/skills/design-review/references/problem-statement.md +141 -0
  224. package/pi-package/skills/design-review/references/report-template.md +36 -0
  225. package/pi-package/skills/design-review/references/scope-document.md +51 -0
  226. package/pi-package/skills/distill-prompt/SKILL.md +47 -0
  227. package/pi-package/skills/distill-prompt/references/criteria.md +26 -0
  228. package/pi-package/skills/distill-prompt/references/templates.md +30 -0
  229. package/pi-package/skills/doc-link-verifier/SKILL.md +218 -0
  230. package/pi-package/skills/doc-link-verifier/scripts/check_links.py +453 -0
  231. package/pi-package/skills/doc-link-verifier/scripts/extract_links.py +521 -0
  232. package/pi-package/skills/edge-case-discovery/SKILL.md +129 -0
  233. package/pi-package/skills/error-handling-diagnostician/SKILL.md +96 -0
  234. package/pi-package/skills/extract-prompt/SKILL.md +226 -0
  235. package/pi-package/skills/formal-verification-evaluator/SKILL.md +164 -0
  236. package/pi-package/skills/grill-me/SKILL.md +30 -0
  237. package/pi-package/skills/guided-review/SKILL.md +74 -0
  238. package/pi-package/skills/implement-documentation/SKILL.md +74 -0
  239. package/pi-package/skills/implement-plan/SKILL.md +29 -0
  240. package/pi-package/skills/implement-plan/references/common-situations.md +36 -0
  241. package/pi-package/skills/implement-plan/references/verification-template.md +18 -0
  242. package/pi-package/skills/invalid-states-diagnostician/SKILL.md +69 -0
  243. package/pi-package/skills/issue-review/SKILL.md +43 -0
  244. package/pi-package/skills/issue-review/references/convergence.md +19 -0
  245. package/pi-package/skills/issue-review/references/final-report.md +60 -0
  246. package/pi-package/skills/issue-review/references/pass-1-clarity.md +28 -0
  247. package/pi-package/skills/issue-review/references/pass-2-scope.md +22 -0
  248. package/pi-package/skills/issue-review/references/pass-3-dependencies.md +23 -0
  249. package/pi-package/skills/issue-review/references/pass-4-alignment.md +17 -0
  250. package/pi-package/skills/issue-review/references/pass-5-executability.md +17 -0
  251. package/pi-package/skills/issue-review/references/setup.md +25 -0
  252. package/pi-package/skills/iterate-plan/SKILL.md +30 -0
  253. package/pi-package/skills/iterate-plan/references/change-template.md +15 -0
  254. package/pi-package/skills/iterate-plan/references/common-scenarios.md +14 -0
  255. package/pi-package/skills/julia-performance-diagnostician/SKILL.md +77 -0
  256. package/pi-package/skills/modularity-diagnostician/SKILL.md +74 -0
  257. package/pi-package/skills/multi-agent-review/SKILL.md +202 -0
  258. package/pi-package/skills/mutability-diagnostician/SKILL.md +69 -0
  259. package/pi-package/skills/narrative-article/SKILL.md +66 -0
  260. package/pi-package/skills/optionality-review/SKILL.md +196 -0
  261. package/pi-package/skills/parallel-review/SKILL.md +55 -0
  262. package/pi-package/skills/parallel-review/references/criteria.md +34 -0
  263. package/pi-package/skills/parallel-review/references/templates.md +59 -0
  264. package/pi-package/skills/plan-review/SKILL.md +48 -0
  265. package/pi-package/skills/plan-review/references/criteria.md +51 -0
  266. package/pi-package/skills/plan-review/references/templates.md +67 -0
  267. package/pi-package/skills/pre-mortem/SKILL.md +16 -0
  268. package/pi-package/skills/project-status-report/SKILL.md +130 -0
  269. package/pi-package/skills/pwa-accessibility-review/SKILL.md +87 -0
  270. package/pi-package/skills/rca-diagnostician/SKILL.md +36 -0
  271. package/pi-package/skills/rca-diagnostician/references/action-hierarchy.md +77 -0
  272. package/pi-package/skills/rca-diagnostician/references/bias-countermeasures.md +64 -0
  273. package/pi-package/skills/rca-diagnostician/references/evidence-timeline.md +49 -0
  274. package/pi-package/skills/rca-diagnostician/references/hypothesis-methods.md +53 -0
  275. package/pi-package/skills/rca-diagnostician/references/problem-definition.md +34 -0
  276. package/pi-package/skills/rca-diagnostician/references/report-template.md +75 -0
  277. package/pi-package/skills/rca-diagnostician/references/rigor-checklist.md +66 -0
  278. package/pi-package/skills/red-team-review/SKILL.md +139 -0
  279. package/pi-package/skills/research-codebase/SKILL.md +28 -0
  280. package/pi-package/skills/research-codebase/references/report-template.md +75 -0
  281. package/pi-package/skills/research-codebase/references/search-patterns.md +27 -0
  282. package/pi-package/skills/research-documentation/SKILL.md +85 -0
  283. package/pi-package/skills/research-review/SKILL.md +48 -0
  284. package/pi-package/skills/research-review/references/criteria.md +51 -0
  285. package/pi-package/skills/research-review/references/templates.md +73 -0
  286. package/pi-package/skills/resonant-refactor/SKILL.md +51 -0
  287. package/pi-package/skills/resume-handoff/SKILL.md +25 -0
  288. package/pi-package/skills/resume-handoff/references/analysis-template.md +40 -0
  289. package/pi-package/skills/resume-handoff/references/locating.md +12 -0
  290. package/pi-package/skills/review-documentation/SKILL.md +50 -0
  291. package/pi-package/skills/rigidity-diagnostician/SKILL.md +76 -0
  292. package/pi-package/skills/rule-of-5-universal/SKILL.md +32 -0
  293. package/pi-package/skills/rule-of-5-universal/references/criteria.md +34 -0
  294. package/pi-package/skills/rule-of-5-universal/references/examples.md +69 -0
  295. package/pi-package/skills/rule-of-5-universal/references/templates.md +136 -0
  296. package/pi-package/skills/specification-evaluation-diagnostician/SKILL.md +112 -0
  297. package/pi-package/skills/specification-review/SKILL.md +49 -0
  298. package/pi-package/skills/specification-review/references/criteria.md +51 -0
  299. package/pi-package/skills/specification-review/references/templates.md +67 -0
  300. package/pi-package/skills/systematic-housekeeping/SKILL.md +33 -0
  301. package/pi-package/skills/tdd/SKILL.md +44 -0
  302. package/pi-package/skills/tdd/references/criteria.md +26 -0
  303. package/pi-package/skills/tdd/references/templates.md +53 -0
  304. package/pi-package/skills/test-abstraction-miner/SKILL.md +49 -0
  305. package/pi-package/skills/test-friction/SKILL.md +57 -0
  306. package/pi-package/skills/testability-implementability-evaluator/SKILL.md +150 -0
  307. package/pi-package/skills/ui-align/SKILL.md +38 -0
  308. package/pi-package/skills/ux-dx-evaluation-diagnostician/SKILL.md +27 -0
  309. package/pi-package/skills/ux-dx-evaluation-diagnostician/references/layer-1-product.md +20 -0
  310. package/pi-package/skills/ux-dx-evaluation-diagnostician/references/layer-2-engineering.md +20 -0
  311. package/pi-package/skills/ux-dx-evaluation-diagnostician/references/layer-3-interface.md +23 -0
  312. package/pi-package/skills/ux-dx-evaluation-diagnostician/references/layer-4-ecosystem.md +20 -0
  313. package/pi-package/skills/ux-dx-evaluation-diagnostician/references/layer-5-documentation.md +19 -0
  314. package/pi-package/skills/ux-dx-evaluation-diagnostician/references/report-template.md +36 -0
  315. package/pi-package/skills/verification-diagnostician/SKILL.md +96 -0
  316. package/pi-package/skills/verify-prompt/SKILL.md +38 -0
  317. package/pi-package/skills/verify-prompt/references/criteria.md +21 -0
  318. package/scripts/cli.mjs +453 -0
@@ -0,0 +1,453 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.12"
4
+ # dependencies = ["aiohttp>=3.9"]
5
+ # ///
6
+ """
7
+ Check links extracted by extract_links.py.
8
+
9
+ For each link:
10
+ - external: async HEAD request (fall back to GET), follow redirects, capture final URL
11
+ - relative: resolve against the file that contains it, confirm the target exists.
12
+ If a fragment is present, verify it matches a heading in the target.
13
+ - anchor: verify the fragment matches a heading in the same file.
14
+ - mailto: pass through (no verification).
15
+
16
+ Reads the JSON output of extract_links.py. Writes a JSON report that augments
17
+ each link entry with status info. Designed to be re-run safely — results are
18
+ cached on disk, keyed by URL.
19
+
20
+ Usage:
21
+ uv run check_links.py <links.json> [--out report.json]
22
+ [--concurrency 8] [--cache .link_cache.json]
23
+ [--repo-root <path>] [--timeout 15]
24
+ [--user-agent "..."] [--no-cache]
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import asyncio
31
+ import json
32
+ import re
33
+ import sys
34
+ import time
35
+ import urllib.parse
36
+ from collections import defaultdict
37
+ from dataclasses import dataclass, asdict, field
38
+ from pathlib import Path
39
+
40
+ import aiohttp
41
+
42
+
43
+ DEFAULT_UA = "Mozilla/5.0 (compatible; doc-link-verifier/1.0)"
44
+
45
+ # Conservative per-host delay so we don't hammer any single site.
46
+ PER_HOST_DELAY = 0.5 # seconds
47
+
48
+
49
+ @dataclass
50
+ class CheckResult:
51
+ status: str # ok | broken | redirected | anchor-missing | unknown | skipped
52
+ http_status: int | None = None
53
+ final_url: str | None = None
54
+ reason: str | None = None
55
+ title: str | None = None # <title> or first H1, for shallow context check
56
+ headings: list[str] = field(default_factory=list) # H1-H3 from target page (markdown/html)
57
+ redirect_chain: list[str] = field(default_factory=list)
58
+ elapsed_ms: int | None = None
59
+
60
+ def to_dict(self) -> dict:
61
+ return asdict(self)
62
+
63
+
64
+ # -------- Caching --------
65
+
66
+ def load_cache(path: Path) -> dict[str, dict]:
67
+ if not path.exists():
68
+ return {}
69
+ try:
70
+ return json.loads(path.read_text(encoding='utf-8'))
71
+ except Exception:
72
+ return {}
73
+
74
+
75
+ def save_cache(path: Path, cache: dict[str, dict]) -> None:
76
+ path.write_text(json.dumps(cache, indent=2, ensure_ascii=False), encoding='utf-8')
77
+
78
+
79
+ # -------- External URL fetching --------
80
+
81
+ TITLE_RE = re.compile(r'<title[^>]*>(.*?)</title>', re.IGNORECASE | re.DOTALL)
82
+ H1_HTML_RE = re.compile(r'<h1[^>]*>(.*?)</h1>', re.IGNORECASE | re.DOTALL)
83
+ HEADING_HTML_RE = re.compile(r'<h([1-3])[^>]*>(.*?)</h\1>', re.IGNORECASE | re.DOTALL)
84
+ TAG_STRIP = re.compile(r'<[^>]+>')
85
+
86
+
87
+ def strip_html(s: str) -> str:
88
+ return re.sub(r'\s+', ' ', TAG_STRIP.sub('', s)).strip()
89
+
90
+
91
+ def _parse_body(body: bytes) -> tuple[str | None, list[str]]:
92
+ """Extract title and headings from an HTML body."""
93
+ try:
94
+ text = body.decode('utf-8', errors='replace')
95
+ except Exception:
96
+ return None, []
97
+ title = None
98
+ m = TITLE_RE.search(text)
99
+ if m:
100
+ title = strip_html(m.group(1))
101
+ else:
102
+ h1 = H1_HTML_RE.search(text)
103
+ if h1:
104
+ title = strip_html(h1.group(1))
105
+ headings = [strip_html(hm.group(2)) for hm in HEADING_HTML_RE.finditer(text)]
106
+ return title, headings[:50]
107
+
108
+
109
+ async def fetch_external(session: aiohttp.ClientSession, url: str,
110
+ timeout: aiohttp.ClientTimeout,
111
+ deep: bool = False) -> CheckResult:
112
+ """
113
+ Async HEAD request; fall back to GET if the server returns 403/405/501.
114
+ When deep=True, always GET and parse title/headings.
115
+ """
116
+ start = time.monotonic()
117
+ redirect_chain: list[str] = []
118
+
119
+ async def _request(method: str) -> tuple[int, str, bytes]:
120
+ async with session.request(method, url, timeout=timeout,
121
+ allow_redirects=True,
122
+ max_redirects=10) as resp:
123
+ # Capture redirect history
124
+ for h in resp.history:
125
+ redirect_chain.append(str(h.url))
126
+ body = b''
127
+ if method == 'GET':
128
+ body = await resp.content.read(200_000)
129
+ return resp.status, str(resp.url), body
130
+
131
+ try:
132
+ method = 'GET' if deep else 'HEAD'
133
+ status, final_url, body = await _request(method)
134
+
135
+ # Some servers reject HEAD — fall back to GET
136
+ if method == 'HEAD' and status in (403, 405, 501):
137
+ redirect_chain.clear()
138
+ status, final_url, body = await _request('GET')
139
+
140
+ if status >= 400:
141
+ return CheckResult(
142
+ status='broken', http_status=status,
143
+ reason=f"HTTP {status}",
144
+ final_url=final_url, redirect_chain=redirect_chain,
145
+ elapsed_ms=int((time.monotonic() - start) * 1000),
146
+ )
147
+
148
+ result_status = 'ok'
149
+ if redirect_chain or final_url != url:
150
+ result_status = 'redirected'
151
+
152
+ title = None
153
+ headings: list[str] = []
154
+ if deep and body:
155
+ title, headings = _parse_body(body)
156
+
157
+ return CheckResult(
158
+ status=result_status,
159
+ http_status=status,
160
+ final_url=final_url,
161
+ title=title,
162
+ headings=headings,
163
+ redirect_chain=redirect_chain,
164
+ elapsed_ms=int((time.monotonic() - start) * 1000),
165
+ )
166
+ except asyncio.TimeoutError:
167
+ return CheckResult(
168
+ status='unknown', reason='timeout',
169
+ redirect_chain=redirect_chain,
170
+ elapsed_ms=int((time.monotonic() - start) * 1000),
171
+ )
172
+ except aiohttp.ClientError as e:
173
+ return CheckResult(
174
+ status='broken', reason=f"{type(e).__name__}: {e}",
175
+ redirect_chain=redirect_chain,
176
+ elapsed_ms=int((time.monotonic() - start) * 1000),
177
+ )
178
+ except Exception as e:
179
+ return CheckResult(
180
+ status='unknown', reason=f"{type(e).__name__}: {e}",
181
+ redirect_chain=redirect_chain,
182
+ elapsed_ms=int((time.monotonic() - start) * 1000),
183
+ )
184
+
185
+
186
+ # -------- Relative/anchor checking --------
187
+
188
+ MD_HEADING_RE = re.compile(r'^(#{1,6})\s+(.+?)\s*#*\s*$', re.MULTILINE)
189
+ RST_HEADING_CHARS = set('=-~^"\'`*+#<>_:.')
190
+
191
+
192
+ _MD_LINK_RE = re.compile(r'\[([^\]]*)\]\([^)]*\)')
193
+ _MD_IMAGE_RE = re.compile(r'!\[([^\]]*)\]\([^)]*\)')
194
+ _MD_INLINE_CODE_RE = re.compile(r'`([^`]+)`')
195
+
196
+
197
+ def _strip_md_inline(text: str) -> str:
198
+ """Strip inline markdown formatting so heading text matches GitHub slugs."""
199
+ text = _MD_IMAGE_RE.sub(r'\1', text) # ![alt](url) → alt
200
+ text = _MD_LINK_RE.sub(r'\1', text) # [text](url) → text
201
+ text = _MD_INLINE_CODE_RE.sub(r'\1', text) # `code` → code
202
+ text = re.sub(r'[*_]{1,3}', '', text) # bold/italic markers
203
+ return text
204
+
205
+
206
+ def slugify_github(heading: str) -> str:
207
+ """
208
+ GitHub's algorithm for turning a heading into an anchor slug:
209
+ strip inline formatting, lowercase, strip punctuation (keep hyphens,
210
+ underscores, spaces), replace spaces with hyphens.
211
+ """
212
+ s = _strip_md_inline(heading).strip().lower()
213
+ s = re.sub(r'[^\w\s-]', '', s)
214
+ s = re.sub(r'\s+', '-', s)
215
+ return s
216
+
217
+
218
+ def collect_headings(content: str, ext: str) -> list[str]:
219
+ """Return heading slugs for the given file content, with GitHub-style disambiguation."""
220
+ raw_slugs: list[str] = []
221
+ if ext in {'.md', '.mdx', '.markdown'}:
222
+ for m in MD_HEADING_RE.finditer(content):
223
+ raw_slugs.append(slugify_github(m.group(2)))
224
+ elif ext == '.rst':
225
+ lines = content.splitlines()
226
+ i = 0
227
+ while i < len(lines):
228
+ line = lines[i].strip()
229
+ if line and set(line) <= RST_HEADING_CHARS and len(line) >= 2:
230
+ # Check for overline pattern: decoration, title, decoration
231
+ if (i + 2 < len(lines)
232
+ and lines[i + 1].strip()
233
+ and lines[i + 2].strip()
234
+ and set(lines[i + 2].strip()) <= RST_HEADING_CHARS
235
+ and lines[i + 2].strip()[0] == line[0]):
236
+ raw_slugs.append(slugify_github(lines[i + 1].strip()))
237
+ i += 3
238
+ continue
239
+ # Underline pattern: previous line is the title
240
+ elif i > 0 and lines[i - 1].strip():
241
+ raw_slugs.append(slugify_github(lines[i - 1].strip()))
242
+ i += 1
243
+
244
+ # Disambiguate duplicates: GitHub appends -1, -2, etc.
245
+ seen: dict[str, int] = {}
246
+ slugs: list[str] = []
247
+ for s in raw_slugs:
248
+ if s in seen:
249
+ slugs.append(f"{s}-{seen[s]}")
250
+ seen[s] += 1
251
+ else:
252
+ slugs.append(s)
253
+ seen[s] = 1
254
+ return slugs
255
+
256
+
257
+ def check_relative(link: dict, repo_root: Path) -> CheckResult:
258
+ url = link['url']
259
+ source_file = repo_root / link['file']
260
+ # Split off fragment
261
+ if '#' in url:
262
+ path_part, _, fragment = url.partition('#')
263
+ else:
264
+ path_part, fragment = url, ''
265
+
266
+ # Resolve path relative to the source file's directory, or repo root if absolute
267
+ if path_part == '':
268
+ target = source_file # same-file anchor
269
+ elif path_part.startswith('/'):
270
+ target = (repo_root / path_part.lstrip('/')).resolve()
271
+ else:
272
+ target = (source_file.parent / path_part).resolve()
273
+
274
+ if not target.exists():
275
+ return CheckResult(status='broken', reason=f"file not found: {target}")
276
+
277
+ if fragment:
278
+ if target.is_dir():
279
+ return CheckResult(status='broken',
280
+ reason="fragment on a directory link")
281
+ ext = target.suffix.lower()
282
+ try:
283
+ content = target.read_text(encoding='utf-8', errors='replace')
284
+ except Exception as e:
285
+ return CheckResult(status='unknown', reason=str(e))
286
+ slugs = collect_headings(content, ext)
287
+ frag_slug = slugify_github(urllib.parse.unquote(fragment))
288
+ if frag_slug in slugs:
289
+ return CheckResult(status='ok', headings=slugs[:50])
290
+ return CheckResult(
291
+ status='anchor-missing',
292
+ reason=f"fragment '#{fragment}' not found in {target.name}",
293
+ headings=slugs[:50],
294
+ )
295
+
296
+ return CheckResult(status='ok')
297
+
298
+
299
+ def check_anchor(link: dict, repo_root: Path) -> CheckResult:
300
+ source_file = repo_root / link['file']
301
+ fragment = link['url'].lstrip('#')
302
+ ext = source_file.suffix.lower()
303
+ try:
304
+ content = source_file.read_text(encoding='utf-8', errors='replace')
305
+ except Exception as e:
306
+ return CheckResult(status='unknown', reason=str(e))
307
+ slugs = collect_headings(content, ext)
308
+ frag_slug = slugify_github(urllib.parse.unquote(fragment))
309
+ if frag_slug in slugs:
310
+ return CheckResult(status='ok', headings=slugs[:50])
311
+ return CheckResult(
312
+ status='anchor-missing',
313
+ reason=f"#{fragment} not found in {source_file.name}",
314
+ headings=slugs[:50],
315
+ )
316
+
317
+
318
+ # -------- Async main driver --------
319
+
320
+ async def check_external_urls(
321
+ to_fetch: dict[str, list[int]],
322
+ links: list[dict],
323
+ results: list[dict | None],
324
+ cache: dict[str, dict],
325
+ cache_key_suffix: str,
326
+ concurrency: int,
327
+ timeout_s: int,
328
+ user_agent: str,
329
+ deep: bool,
330
+ ) -> None:
331
+ """Fetch all external URLs concurrently with per-host rate limiting."""
332
+ semaphore = asyncio.Semaphore(concurrency)
333
+ # Per-host lock + last-hit time for polite rate limiting
334
+ host_locks: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
335
+ host_last_hit: dict[str, float] = {}
336
+ timeout = aiohttp.ClientTimeout(total=timeout_s)
337
+
338
+ async with aiohttp.ClientSession(
339
+ headers={'User-Agent': user_agent,
340
+ 'Accept': 'text/html,application/xhtml+xml,*/*;q=0.8'},
341
+ ) as session:
342
+
343
+ async def _fetch_one(url: str) -> tuple[str, dict]:
344
+ cache_key = url + cache_key_suffix
345
+ if cache_key in cache:
346
+ return url, cache[cache_key]
347
+
348
+ host = urllib.parse.urlparse(url).netloc
349
+ # Host lock serializes requests per host (delay + fetch together).
350
+ # Semaphore only wraps the fetch so delays don't waste slots.
351
+ async with host_locks[host]:
352
+ now = time.monotonic()
353
+ wait = PER_HOST_DELAY - (now - host_last_hit.get(host, 0))
354
+ if wait > 0:
355
+ await asyncio.sleep(wait)
356
+ async with semaphore:
357
+ host_last_hit[host] = time.monotonic()
358
+ r = await fetch_external(session, url, timeout, deep=deep)
359
+ d = r.to_dict()
360
+ cache[cache_key] = d
361
+ return url, d
362
+
363
+ tasks = [_fetch_one(url) for url in to_fetch]
364
+ for coro in asyncio.as_completed(tasks):
365
+ url, check = await coro
366
+ for idx in to_fetch[url]:
367
+ results[idx] = {**links[idx], 'check': check}
368
+
369
+
370
+ def main() -> int:
371
+ p = argparse.ArgumentParser(description=__doc__)
372
+ p.add_argument('links_json', help='Output of extract_links.py')
373
+ p.add_argument('--out', help='Write report JSON here (default: stdout)')
374
+ p.add_argument('--repo-root', help='Repository root (default: from links_json)')
375
+ p.add_argument('--concurrency', type=int, default=8)
376
+ p.add_argument('--timeout', type=int, default=15)
377
+ p.add_argument('--cache', default='.link_cache.json',
378
+ help='Cache file for external URL results')
379
+ p.add_argument('--no-cache', action='store_true', help='Disable cache')
380
+ p.add_argument('--deep', action='store_true',
381
+ help='Fetch full page bodies (enables title/heading extraction)')
382
+ p.add_argument('--user-agent', default=DEFAULT_UA)
383
+ args = p.parse_args()
384
+
385
+ data = json.loads(Path(args.links_json).read_text(encoding='utf-8'))
386
+ links: list[dict] = data['links']
387
+ repo_root = Path(args.repo_root or data['repo']).resolve()
388
+
389
+ cache_path = Path(args.cache)
390
+ cache = {} if args.no_cache else load_cache(cache_path)
391
+ cache_key_suffix = ':deep' if args.deep else ':shallow'
392
+
393
+ results: list[dict | None] = [None] * len(links)
394
+
395
+ # Relative, anchor, mailto — fast, no I/O, done synchronously
396
+ external_indices = [i for i, l in enumerate(links) if l.get('link_type') == 'external']
397
+ other_indices = [i for i, l in enumerate(links) if l.get('link_type') != 'external']
398
+
399
+ for i in other_indices:
400
+ link = links[i]
401
+ lt = link.get('link_type', '')
402
+ if lt == 'relative':
403
+ result = check_relative(link, repo_root)
404
+ elif lt == 'anchor':
405
+ result = check_anchor(link, repo_root)
406
+ elif lt == 'mailto':
407
+ result = CheckResult(status='skipped', reason='mailto link')
408
+ else:
409
+ result = CheckResult(status='skipped', reason=f'unhandled link type: {lt}')
410
+ results[i] = {**link, 'check': result.to_dict()}
411
+
412
+ # External: dedupe by URL so we don't hit the same URL multiple times
413
+ to_fetch: dict[str, list[int]] = {}
414
+ for i in external_indices:
415
+ url = links[i]['url']
416
+ to_fetch.setdefault(url, []).append(i)
417
+
418
+ print(f"checking {len(to_fetch)} unique external URLs "
419
+ f"(across {len(external_indices)} occurrences)...", file=sys.stderr)
420
+
421
+ asyncio.run(check_external_urls(
422
+ to_fetch, links, results, cache, cache_key_suffix,
423
+ args.concurrency, args.timeout, args.user_agent, args.deep,
424
+ ))
425
+
426
+ if not args.no_cache:
427
+ save_cache(cache_path, cache)
428
+
429
+ # Summary counts
430
+ summary: dict[str, int] = {}
431
+ for r in results:
432
+ s = r['check']['status'] # type: ignore[index]
433
+ summary[s] = summary.get(s, 0) + 1
434
+
435
+ report = {
436
+ 'repo': str(repo_root),
437
+ 'link_count': len(results),
438
+ 'summary': summary,
439
+ 'links': results,
440
+ }
441
+
442
+ out_text = json.dumps(report, indent=2, ensure_ascii=False)
443
+ if args.out:
444
+ Path(args.out).write_text(out_text, encoding='utf-8')
445
+ print(f"wrote report to {args.out}", file=sys.stderr)
446
+ print(f"summary: {summary}", file=sys.stderr)
447
+ else:
448
+ print(out_text)
449
+ return 0
450
+
451
+
452
+ if __name__ == '__main__':
453
+ sys.exit(main())