@caddis/cli 0.2.6 → 0.4.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 (265) hide show
  1. package/bundles/antigravity-plugin/agents/claude-md-curator.md +1 -1
  2. package/bundles/antigravity-plugin/agents/knowledge-transfer.md +17 -32
  3. package/bundles/antigravity-plugin/caddis_statusline.py +825 -0
  4. package/bundles/antigravity-plugin/claude-md/folder-agents.md.tmpl +21 -0
  5. package/bundles/antigravity-plugin/guard_agy.py +60 -9
  6. package/bundles/antigravity-plugin/plugin.json +1 -1
  7. package/bundles/antigravity-plugin/scripts/caddis_gate.py +524 -0
  8. package/bundles/antigravity-plugin/scripts/caddis_inventory.py +439 -0
  9. package/bundles/antigravity-plugin/scripts/caddis_migrate_dir.py +270 -0
  10. package/bundles/antigravity-plugin/scripts/caddis_spawn.py +300 -0
  11. package/bundles/antigravity-plugin/scripts/caddis_statusline.py +825 -0
  12. package/bundles/antigravity-plugin/scripts/caddis_tidy.py +366 -0
  13. package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +924 -0
  14. package/bundles/antigravity-plugin/scripts/claude-deepseek.ps1 +4 -0
  15. package/bundles/antigravity-plugin/scripts/claude-deepseek.sh +3 -0
  16. package/bundles/antigravity-plugin/scripts/claude-glm.ps1 +4 -0
  17. package/bundles/antigravity-plugin/scripts/claude-glm.sh +3 -0
  18. package/bundles/antigravity-plugin/scripts/claude-oss.ps1 +64 -0
  19. package/bundles/antigravity-plugin/scripts/claude-oss.sh +60 -0
  20. package/bundles/antigravity-plugin/scripts/claudster_config.py +121 -0
  21. package/bundles/antigravity-plugin/scripts/claudster_doctor.py +356 -0
  22. package/bundles/antigravity-plugin/scripts/claudster_init.py +400 -0
  23. package/bundles/antigravity-plugin/scripts/claudster_migrate_rules.py +300 -0
  24. package/bundles/antigravity-plugin/scripts/enforcement_inventory.py +261 -0
  25. package/bundles/antigravity-plugin/scripts/hook_log.py +156 -0
  26. package/bundles/antigravity-plugin/scripts/new-machine.ps1 +170 -0
  27. package/bundles/antigravity-plugin/scripts/oss_ask.py +98 -0
  28. package/bundles/antigravity-plugin/scripts/oss_model.py +174 -0
  29. package/bundles/antigravity-plugin/scripts/oss_review.py +718 -0
  30. package/bundles/antigravity-plugin/scripts/setup_project_ai.py +1432 -0
  31. package/bundles/antigravity-plugin/scripts/usage_review.py +1323 -0
  32. package/bundles/antigravity-plugin/session_end_agy.py +52 -8
  33. package/bundles/antigravity-plugin/skills/_registry.md +1 -0
  34. package/bundles/antigravity-plugin/skills/advisory-hub/SKILL.md +44 -0
  35. package/bundles/antigravity-plugin/skills/code-review/SKILL.md +11 -0
  36. package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +1 -1
  37. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +21 -0
  38. package/bundles/antigravity-plugin/skills/gate-review/SKILL.md +11 -0
  39. package/bundles/antigravity-plugin/skills/implement/SKILL.md +36 -0
  40. package/bundles/antigravity-plugin/skills/security-review/SKILL.md +9 -0
  41. package/bundles/antigravity-plugin/skills/ship/SKILL.md +1 -1
  42. package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +1 -1
  43. package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +1 -1
  44. package/bundles/antigravity-plugin/skills/statusline/SKILL.md +105 -0
  45. package/bundles/antigravity-plugin/skills/use-model/SKILL.md +15 -3
  46. package/bundles/antigravity-plugin/skills/validate-phase/SKILL.md +31 -0
  47. package/bundles/antigravity-plugin/warm_start_agy.py +117 -11
  48. package/bundles/antigravity-plugin-extras/plugin.json +1 -1
  49. package/bundles/antigravity-plugin-extras/skills/architecture-design/SKILL.md +1 -1
  50. package/bundles/antigravity-plugin-extras/skills/caching-patterns/SKILL.md +1 -1
  51. package/bundles/antigravity-plugin-extras/skills/webapp-development/SKILL.md +1 -1
  52. package/bundles/codex/.codex/config.toml.example +36 -0
  53. package/bundles/codex/.codex/skills/coding/api-design/SKILL.md +249 -0
  54. package/bundles/codex/.codex/skills/coding/backend-development/SKILL.md +305 -0
  55. package/bundles/codex/.codex/skills/coding/code-review/SKILL.md +140 -0
  56. package/bundles/codex/.codex/skills/coding/codebase-audit/SKILL.md +204 -0
  57. package/bundles/codex/.codex/skills/coding/dashboard-perf-sweep/SKILL.md +132 -0
  58. package/bundles/codex/.codex/skills/coding/dashboard-perf-sweep/scripts/perf_sweep.py +249 -0
  59. package/bundles/codex/.codex/skills/coding/error-handling/SKILL.md +260 -0
  60. package/bundles/codex/.codex/skills/coding/fastapi-dev/SKILL.md +300 -0
  61. package/bundles/codex/.codex/skills/coding/javascript-typescript/SKILL.md +142 -0
  62. package/bundles/codex/.codex/skills/coding/python/SKILL.md +388 -0
  63. package/bundles/codex/.codex/skills/coding/refactoring/SKILL.md +486 -0
  64. package/bundles/codex/.codex/skills/coding/security-review/SKILL.md +205 -0
  65. package/bundles/codex/.codex/skills/coding/sql/SKILL.md +321 -0
  66. package/bundles/codex/.codex/skills/data/database-design/SKILL.md +177 -0
  67. package/bundles/codex/.codex/skills/data/db-diagram/SKILL.md +148 -0
  68. package/bundles/codex/.codex/skills/data/db-diagram/scripts/sql_to_graph.py +1212 -0
  69. package/bundles/codex/.codex/skills/devops/ci-cd-pipeline/SKILL.md +315 -0
  70. package/bundles/codex/.codex/skills/devops/gh-cli/SKILL.md +195 -0
  71. package/bundles/codex/.codex/skills/devops/git-commit/SKILL.md +285 -0
  72. package/bundles/codex/.codex/skills/devops/using-git-worktrees/SKILL.md +217 -0
  73. package/bundles/codex/.codex/skills/devops/windows-deployment/SKILL.md +880 -0
  74. package/bundles/codex/.codex/skills/docs/code-documentation/SKILL.md +271 -0
  75. package/bundles/codex/.codex/skills/docs/explainer-doc/SKILL.md +97 -0
  76. package/bundles/codex/.codex/skills/docs/technical-writing/SKILL.md +286 -0
  77. package/bundles/codex/.codex/skills/docs/writing-plans/SKILL.md +387 -0
  78. package/bundles/codex/.codex/skills/frontend/css-architecture/SKILL.md +305 -0
  79. package/bundles/codex/.codex/skills/frontend/css-architecture/references/RESPONSIVE-DESIGN.md +604 -0
  80. package/bundles/codex/.codex/skills/frontend/frontend-design/SKILL.md +193 -0
  81. package/bundles/codex/.codex/skills/frontend/frontend-design/references/my-tech-stack.md +127 -0
  82. package/bundles/codex/.codex/skills/frontend/mockup/SKILL.md +242 -0
  83. package/bundles/codex/.codex/skills/frontend/react-best-practices/SKILL.md +269 -0
  84. package/bundles/codex/.codex/skills/frontend/react-dev/README.md +404 -0
  85. package/bundles/codex/.codex/skills/frontend/react-dev/SKILL.md +459 -0
  86. package/bundles/codex/.codex/skills/frontend/react-dev/examples/generic-components.md +579 -0
  87. package/bundles/codex/.codex/skills/frontend/react-dev/examples/server-components.md +579 -0
  88. package/bundles/codex/.codex/skills/frontend/react-dev/references/event-handlers.md +574 -0
  89. package/bundles/codex/.codex/skills/frontend/react-dev/references/hooks.md +456 -0
  90. package/bundles/codex/.codex/skills/frontend/react-dev/references/react-19-patterns.md +638 -0
  91. package/bundles/codex/.codex/skills/frontend/react-dev/references/react-router.md +1002 -0
  92. package/bundles/codex/.codex/skills/frontend/react-dev/references/tanstack-router.md +587 -0
  93. package/bundles/codex/.codex/skills/frontend/ui-review/SKILL.md +176 -0
  94. package/bundles/codex/.codex/skills/frontend/ui-review/references/framework-fixes.md +471 -0
  95. package/bundles/codex/.codex/skills/frontend/ui-review/references/visual-checklist.md +236 -0
  96. package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/DESIGN_TOKENS.md +487 -0
  97. package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/IMPLEMENTATION_GUIDE.md +177 -0
  98. package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/SKILL.md +732 -0
  99. package/bundles/codex/.codex/skills/media/draw-io/SKILL.md +162 -0
  100. package/bundles/codex/.codex/skills/media/draw-io/references/aws-icons.md +677 -0
  101. package/bundles/codex/.codex/skills/media/draw-io/references/layout-guidelines.md +142 -0
  102. package/bundles/codex/.codex/skills/media/draw-io/references/troubleshooting.md +118 -0
  103. package/bundles/codex/.codex/skills/media/draw-io/references/workflows.md +103 -0
  104. package/bundles/codex/.codex/skills/media/draw-io/scripts/convert-drawio-to-png.sh +25 -0
  105. package/bundles/codex/.codex/skills/media/draw-io/scripts/find_aws_icon.py +79 -0
  106. package/bundles/codex/.codex/skills/media/mermaid-diagrams/SKILL.md +292 -0
  107. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/ENHANCEMENTS.md +264 -0
  108. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/MERMAID-SUMMARY.md +137 -0
  109. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/advanced-features.md +556 -0
  110. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/architecture-diagrams.md +192 -0
  111. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/c4-diagrams.md +410 -0
  112. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/class-diagrams.md +361 -0
  113. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/erd-diagrams.md +510 -0
  114. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/flowcharts.md +450 -0
  115. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/sequence-diagrams.md +394 -0
  116. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/troubleshooting.md +335 -0
  117. package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/workflows.md +418 -0
  118. package/bundles/codex/.codex/skills/media/particle-art/SKILL.md +243 -0
  119. package/bundles/codex/.codex/skills/media/particle-art/references/canvas-utils.ts +171 -0
  120. package/bundles/codex/.codex/skills/media/particle-art/references/dot-field.template.tsx +203 -0
  121. package/bundles/codex/.codex/skills/media/particle-art/references/flow-field.template.tsx +263 -0
  122. package/bundles/codex/.codex/skills/media/particle-art/references/node-shape.template.tsx +261 -0
  123. package/bundles/codex/.codex/skills/media/particle-art/references/shape-sampler.ts +281 -0
  124. package/bundles/codex/.codex/skills/media/particle-art/references/stipple-morph.template.tsx +167 -0
  125. package/bundles/codex/.codex/skills/media/particle-art/references/stipple.template.tsx +175 -0
  126. package/bundles/codex/.codex/skills/media/particle-art/references/trail-ghost.template.tsx +266 -0
  127. package/bundles/codex/.codex/skills/media/particle-art/references/usage-examples.md +320 -0
  128. package/bundles/codex/.codex/skills/testing/playwright/API_REFERENCE.md +653 -0
  129. package/bundles/codex/.codex/skills/testing/playwright/SKILL.md +454 -0
  130. package/bundles/codex/.codex/skills/testing/playwright/lib/helpers.js +441 -0
  131. package/bundles/codex/.codex/skills/testing/playwright/package.json +26 -0
  132. package/bundles/codex/.codex/skills/testing/playwright/run.js +228 -0
  133. package/bundles/codex/.codex/skills/testing/tdd-workflow/SKILL.md +188 -0
  134. package/bundles/codex/.codex/skills/testing/test-strategy/SKILL.md +155 -0
  135. package/bundles/codex/.codex/skills/testing/webapp-testing/LICENSE.txt +202 -0
  136. package/bundles/codex/.codex/skills/testing/webapp-testing/SKILL.md +97 -0
  137. package/bundles/codex/.codex/skills/testing/webapp-testing/examples/console_logging.py +35 -0
  138. package/bundles/codex/.codex/skills/testing/webapp-testing/examples/element_discovery.py +40 -0
  139. package/bundles/codex/.codex/skills/testing/webapp-testing/examples/static_html_automation.py +33 -0
  140. package/bundles/codex/.codex/skills/testing/webapp-testing/scripts/with_server.py +106 -0
  141. package/bundles/codex/.codex/skills/workflow/advisory-hub/SKILL.md +227 -0
  142. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/context-template.md +214 -0
  143. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/hub-handbook.md +233 -0
  144. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/report-contract.md +94 -0
  145. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/succession.md +118 -0
  146. package/bundles/codex/.codex/skills/workflow/best-practices/SKILL.md +500 -0
  147. package/bundles/codex/.codex/skills/workflow/best-practices/agents/best-practices-referencer.md +263 -0
  148. package/bundles/codex/.codex/skills/workflow/best-practices/agents/codebase-context-builder.md +326 -0
  149. package/bundles/codex/.codex/skills/workflow/best-practices/agents/task-intent-analyzer.md +245 -0
  150. package/bundles/codex/.codex/skills/workflow/best-practices/references/anti-patterns.md +571 -0
  151. package/bundles/codex/.codex/skills/workflow/best-practices/references/before-after-examples.md +1114 -0
  152. package/bundles/codex/.codex/skills/workflow/best-practices/references/best-practices-guide.md +513 -0
  153. package/bundles/codex/.codex/skills/workflow/best-practices/references/common-workflows.md +692 -0
  154. package/bundles/codex/.codex/skills/workflow/best-practices/references/prompt-patterns.md +547 -0
  155. package/bundles/codex/.codex/skills/workflow/brainstorming/SKILL.md +57 -0
  156. package/bundles/codex/.codex/skills/workflow/context-curator/SKILL.md +157 -0
  157. package/bundles/codex/.codex/skills/workflow/golden-plan/SKILL.md +586 -0
  158. package/bundles/codex/.codex/skills/workflow/preflight/SKILL.md +440 -0
  159. package/bundles/codex/.codex/skills/workflow/skill-creator/LICENSE.txt +202 -0
  160. package/bundles/codex/.codex/skills/workflow/skill-creator/SKILL.md +491 -0
  161. package/bundles/codex/.codex/skills/workflow/skill-creator/agents/analyzer.md +274 -0
  162. package/bundles/codex/.codex/skills/workflow/skill-creator/agents/comparator.md +202 -0
  163. package/bundles/codex/.codex/skills/workflow/skill-creator/agents/grader.md +223 -0
  164. package/bundles/codex/.codex/skills/workflow/skill-creator/assets/eval_review.html +146 -0
  165. package/bundles/codex/.codex/skills/workflow/skill-creator/eval-viewer/generate_review.py +471 -0
  166. package/bundles/codex/.codex/skills/workflow/skill-creator/eval-viewer/viewer.html +1325 -0
  167. package/bundles/codex/.codex/skills/workflow/skill-creator/references/schemas.md +430 -0
  168. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/__init__.py +0 -0
  169. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/aggregate_benchmark.py +401 -0
  170. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/generate_report.py +326 -0
  171. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/improve_description.py +247 -0
  172. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/package_skill.py +136 -0
  173. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/quick_validate.py +103 -0
  174. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/run_eval.py +310 -0
  175. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/run_loop.py +328 -0
  176. package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/utils.py +47 -0
  177. package/bundles/codex/AGENTS.md +146 -0
  178. package/bundles/manifest.json +4 -3
  179. package/dist/claude-deepseek.js +544 -0
  180. package/dist/claude-glm.js +544 -0
  181. package/dist/claude-oss.js +544 -0
  182. package/dist/cli.js +542 -90
  183. package/package.json +5 -2
  184. package/bundles/antigravity-plugin/statusline-command-agy.sh +0 -91
  185. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +0 -93
  186. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  187. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  188. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +0 -93
  189. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  190. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +0 -93
  191. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
  192. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  193. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +0 -93
  194. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  195. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  196. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  197. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +0 -93
  198. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  199. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/DMMono-OFL.txt +0 -93
  200. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
  201. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +0 -94
  202. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
  203. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
  204. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +0 -93
  205. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
  206. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Gloock-OFL.txt +0 -93
  207. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
  208. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  209. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +0 -93
  210. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  211. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  212. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  213. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  214. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  215. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  216. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  217. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  218. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +0 -93
  219. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  220. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  221. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  222. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Italiana-OFL.txt +0 -93
  223. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
  224. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  225. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +0 -93
  226. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  227. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
  228. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
  229. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-OFL.txt +0 -93
  230. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +0 -93
  231. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  232. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
  233. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  234. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
  235. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-OFL.txt +0 -93
  236. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
  237. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
  238. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +0 -93
  239. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
  240. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +0 -93
  241. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  242. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
  243. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +0 -93
  244. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
  245. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  246. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +0 -93
  247. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +0 -93
  248. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  249. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  250. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +0 -93
  251. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  252. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +0 -93
  253. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  254. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  255. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +0 -93
  256. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
  257. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +0 -93
  258. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
  259. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
  260. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  261. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
  262. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +0 -93
  263. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
  264. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +0 -93
  265. package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
@@ -0,0 +1,924 @@
1
+ """Generic doc-discipline checker for caddis-managed repos.
2
+
3
+ Ported from an internal project and made harness-generic: the same route-drift /
4
+ doc-map-integrity / rules-file-budget checks, but every check **auto-skips** when its inputs
5
+ are absent, so a backend-only or doc-less repo passes silently with no noise. Read-only; never writes.
6
+
7
+ Four checks (all relative to a passed-in repo root — no module-level ROOT):
8
+
9
+ 1. **Page-guide route coverage** — every live route in ``frontend/src/routeTree.gen.ts`` has an
10
+ entry in ``UI_PAGE_GUIDE.md``. A missing entry is a hard failure. Absent route tree OR page
11
+ guide → skip **silently** (a backend repo must not be nagged to "run npm run build").
12
+ 2. **Doc-map integrity** — every link in ``<artifact-dir>/kb/DOC-MAP.md`` resolves (dangling = hard
13
+ failure), and every KB note (``<artifact-dir>/kb/*.md``) is indexed in it (orphan = warning). Only the
14
+ curated KB is governed — the wider ``docs/`` folder is NOT policed. Absent doc-map → **skip**.
15
+ 3. **Rules-file budget** — the always-loaded rules file stays lean (warning only). Post-migration
16
+ that is ``AGENTS.md`` (canonical) plus its tiny ``CLAUDE.md`` @import shim; a pre-migration repo has
17
+ only ``CLAUDE.md``. Config key ``agents_md_budget`` (``claude_md_budget`` is the back-compat alias).
18
+ 4. **OKF v0.2 trust signals** — when an artifact-dir doc declares ``status`` / ``stale_after`` /
19
+ ``verified`` / ``generated``, they must be well-formed (warning only). **Opt-in by presence:** a
20
+ missing trust field is never reported, so every pre-v0.2 document passes silently. Nothing here
21
+ can fail the gate — the signals are recommended, not required. See
22
+ ``.github/instructions/document-frontmatter.instructions.md``.
23
+
24
+ Usage::
25
+
26
+ python scripts/check_doc_coverage.py # human report, exit 0
27
+ python scripts/check_doc_coverage.py --check # gate mode, exit 1 on a hard failure
28
+
29
+ Enforcement is tiered: only *missing routes* and *dangling doc-map links* fail the gate (they are
30
+ unambiguous and rare). Everything else warns. In --check mode a clean repo prints nothing.
31
+
32
+ Defaults are baked in as module constants; a per-repo ``<artifact-dir>/config.toml [doc_coverage]``
33
+ override is a deliberate fast-follow, not part of this version.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import argparse
39
+ import os
40
+ import re
41
+ import subprocess
42
+ import sys
43
+ from pathlib import Path
44
+
45
+ # Shared per-repo config reader + artifact-dir resolution (fail-open). Fall back to shims if it's
46
+ # ever absent, so the gate degrades to baked-in defaults rather than crashing.
47
+ try:
48
+ from claudster_config import (ARTIFACT_DIRS, artifact_read, artifact_root, get_int, get_str,
49
+ get_str_list, load_config)
50
+ except Exception: # pragma: no cover - defensive shim
51
+ ARTIFACT_DIRS = (".caddis",) # type: ignore
52
+
53
+ def artifact_root(root): # type: ignore
54
+ return Path(root) / ARTIFACT_DIRS[0]
55
+
56
+ def artifact_read(root, *parts): # type: ignore
57
+ return artifact_root(root).joinpath(*parts)
58
+
59
+ def load_config(root, section): # type: ignore
60
+ return {}
61
+
62
+ def get_int(cfg, key, default): # type: ignore
63
+ return default
64
+
65
+ def get_str(cfg, key, default): # type: ignore
66
+ return default
67
+
68
+ def get_str_list(cfg, key, default): # type: ignore
69
+ return default
70
+
71
+ # Directories never worth scanning for CLAUDE.md — pruned DURING the walk so we never descend into a
72
+ # huge/vendored tree (and never trip over a broken symlink inside node_modules, which crashes rglob).
73
+ _SKIP_DIRS = {".venv", "venv", "node_modules", ".git", "__pycache__", ".mypy_cache",
74
+ ".ruff_cache", ".pytest_cache", ".tanstack", "dist", "build"}
75
+
76
+ # Default locations, relative to the repo root passed into run(). Generic — no repo-specific names.
77
+ DEFAULT_ROUTE_TREE = "frontend/src/routeTree.gen.ts"
78
+ DEFAULT_PAGE_GUIDE = "UI_PAGE_GUIDE.md"
79
+ # Nominal doc-map path. The real one is resolved per-repo by doc_map_path().
80
+ DOC_MAP_REL = "kb/DOC-MAP.md"
81
+ DEFAULT_DOC_MAP = f".caddis/{DOC_MAP_REL}"
82
+
83
+ # Routes intentionally undocumented in the page guide (pure framework/internal paths). Keep tiny.
84
+ IGNORE_ROUTES: frozenset[str] = frozenset()
85
+
86
+ # Always-loaded rules file line budget; keep it lean. Warning only. Post-migration the canonical file
87
+ # is AGENTS.md (measured with its CLAUDE.md @import shim); pre-migration repos have only CLAUDE.md.
88
+ # Name kept as CLAUDE_MD_BUDGET for back-compat; AGENTS_MD_BUDGET is an alias.
89
+ CLAUDE_MD_BUDGET = 200
90
+ AGENTS_MD_BUDGET = CLAUDE_MD_BUDGET
91
+
92
+ # The doc-map governs ONLY the curated KB it indexes (<artifact-dir>/kb/). The wider docs/ folder is
93
+ # the project's own documentation and is intentionally NOT policed — the KB is the code-relevant set
94
+ # the team curates. Orphan = a KB note that isn't indexed in the doc-map (warning only). Scoping this
95
+ # to the KB means zero assumptions about any project's docs/ layout, and no noise.
96
+ GOVERNED_GLOBS: tuple[str, ...] = tuple(f"{name}/kb/*.md" for name in ARTIFACT_DIRS)
97
+
98
+
99
+ def doc_map_path(root) -> Path:
100
+ """This repo's DOC-MAP, under ``.caddis/kb/``. Both the read target for the gate and the write
101
+ target for ``reindex``."""
102
+ return artifact_read(root, *DOC_MAP_REL.split("/"))
103
+
104
+
105
+ def kb_dir(root) -> Path:
106
+ """This repo's KB folder — beside its DOC-MAP, so notes and index never split across dirs."""
107
+ return doc_map_path(root).parent
108
+
109
+
110
+ # --------------------------------------------------------------------------- #
111
+ # Pure functions (no filesystem) — the testable core. Ported verbatim.
112
+ # --------------------------------------------------------------------------- #
113
+ def extract_code_routes(route_tree_text: str) -> set[str]:
114
+ """Route paths registered in the generated TanStack route tree."""
115
+ return {
116
+ route.strip()
117
+ for route in re.findall(r"""path:\s*['"]([^'"]+)['"]""", route_tree_text)
118
+ if route.strip().startswith("/")
119
+ }
120
+
121
+
122
+ def extract_documented_routes(guide_text: str) -> set[str]:
123
+ """Backtick-wrapped ``/route`` tokens in the page guide, excluding API endpoints.
124
+
125
+ HTML comments are stripped first (symmetry with ``extract_docmap_entries``) so an example route
126
+ in a maintenance comment is not mistaken for a documented route (which would warn as a phantom).
127
+ """
128
+ guide_text = re.sub(r"<!--.*?-->", "", guide_text, flags=re.DOTALL)
129
+ found: set[str] = set()
130
+ for token in re.findall(r"`(/[^`]*)`", guide_text):
131
+ token = token.strip()
132
+ if token.startswith("/api/"):
133
+ continue
134
+ found.add(token)
135
+ return found
136
+
137
+
138
+ def route_coverage_gaps(
139
+ route_tree_text: str,
140
+ guide_text: str,
141
+ ignore: frozenset[str] | set[str] = frozenset(),
142
+ ) -> tuple[list[str], list[str]]:
143
+ """Return ``(missing, extra)`` — live routes absent from the guide, and vice versa."""
144
+ code = extract_code_routes(route_tree_text) - set(ignore)
145
+ documented = extract_documented_routes(guide_text)
146
+ missing = sorted(code - documented)
147
+ extra = sorted(documented - code)
148
+ return missing, extra
149
+
150
+
151
+ def oversize_files(lengths: dict[str, int], budget: int) -> list[tuple[str, int]]:
152
+ """Files whose line count exceeds ``budget``, sorted by name."""
153
+ return sorted((name, n) for name, n in lengths.items() if n > budget)
154
+
155
+
156
+ def extract_docmap_entries(docmap_text: str) -> set[str]:
157
+ """``*.md`` link targets the doc-map points at, as written (markdown link syntax).
158
+
159
+ Targets are returned verbatim except for a stripped leading ``./``; ``http(s)`` links are
160
+ excluded. Resolving them against the doc-map's directory is the caller's job (see ``run``).
161
+ Links inside HTML comments are ignored — a doc-map's maintenance guide may show example links
162
+ that don't exist yet, and those must not register as real (dangling) entries.
163
+ """
164
+ # OKF-lite: if the doc-map itself carries a leading frontmatter block, a `.md`
165
+ # link written inside it must not register as an entry (it would read as dangling).
166
+ docmap_text = re.sub(r"\A---\r?\n.*?\r?\n---\r?\n", "", docmap_text, flags=re.DOTALL)
167
+ docmap_text = re.sub(r"<!--.*?-->", "", docmap_text, flags=re.DOTALL)
168
+ entries: set[str] = set()
169
+ for target in re.findall(r"\]\(([^)]+\.md)\)", docmap_text):
170
+ target = target.strip()
171
+ if target.startswith(("http://", "https://")):
172
+ continue
173
+ if target.startswith("./"):
174
+ target = target[2:]
175
+ entries.add(target)
176
+ return entries
177
+
178
+
179
+ # --------------------------------------------------------------------------- #
180
+ # OKF v0.2 trust signals — well-formedness only, WARNING only.
181
+ #
182
+ # Contract (see .github/instructions/document-frontmatter.instructions.md):
183
+ # * `type` is the only required field. A MISSING trust field is never reported — not as a
184
+ # failure, not as a warning. These checks are opt-in by presence, so every pre-v0.2 document
185
+ # stays silent. Backward compatibility is the load-bearing property here.
186
+ # * A malformed field that IS present warns, so a typo surfaces without gating anything.
187
+ # * Deliberately hand-rolled, not PyYAML: the harness ships no third-party deps, and a doc-gate
188
+ # must never be the reason a repo needs one. We only need four top-level keys.
189
+ # --------------------------------------------------------------------------- #
190
+
191
+ # Both vocabularies are accepted — caddis's work-tracking values AND OKF v0.2's. Neither supersedes
192
+ # the other; the instruction file carries the mapping. Legacy synonyms included so old docs stay quiet.
193
+ TRUST_STATUS_VALUES: frozenset[str] = frozenset({
194
+ "draft", "stable", "deprecated", # OKF v0.2
195
+ "current", "done", "superseded", "ready", # caddis
196
+ "shipped", "implemented", # caddis legacy synonyms for `done`
197
+ "open", "doing", "dropped", # caddis parking-lot (`done` shared above)
198
+ })
199
+
200
+ _FRONTMATTER_RE = re.compile(r"\A---[ \t]*\r?\n(.*?)\r?\n---[ \t]*(?:\r?\n|\Z)", re.DOTALL)
201
+ _ISO_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
202
+ _ISO_DATETIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})?$")
203
+ _BY_RE = re.compile(r"(?:^|[\s{,])by:\s*([^,}\n]+)", re.MULTILINE)
204
+ _AT_RE = re.compile(r"(?:^|[\s{,])at:\s*([^,}\n]+)", re.MULTILINE)
205
+
206
+
207
+ def frontmatter_block(text: str) -> str | None:
208
+ """The raw body of a leading ``---`` YAML block, or None when the document has no frontmatter."""
209
+ match = _FRONTMATTER_RE.match(text.lstrip(""))
210
+ return match.group(1) if match else None
211
+
212
+
213
+ def _scalar(raw: str) -> str:
214
+ """A frontmatter scalar with its trailing ``# comment`` and surrounding quotes removed."""
215
+ return re.sub(r"\s+#.*$", "", raw).strip().strip("'\"").strip()
216
+
217
+
218
+ def _top_level_field(block: str, key: str) -> tuple[str, list[str]] | None:
219
+ """``(inline value, nested lines)`` for an unindented ``key:`` in ``block``; None if absent.
220
+
221
+ Nested lines are the indented continuation lines that follow — which is how both
222
+ ``verified:``-then-`` - {...}`` and a block-style mapping are captured without a YAML parser.
223
+ """
224
+ lines = block.splitlines()
225
+ for i, line in enumerate(lines):
226
+ match = re.match(rf"^{re.escape(key)}:[ \t]*(.*)$", line)
227
+ if not match:
228
+ continue
229
+ nested: list[str] = []
230
+ for follow in lines[i + 1:]:
231
+ if follow.strip() and not follow[:1].isspace():
232
+ break # next top-level key ends this field
233
+ if follow.strip():
234
+ nested.append(follow)
235
+ return match.group(1), nested
236
+ return None
237
+
238
+
239
+ def _actor_entry_issues(chunk: str, what: str) -> list[str]:
240
+ """Warnings for one ``{by, at}`` record, written inline (flow) or as indented block style."""
241
+ issues: list[str] = []
242
+ by = _BY_RE.search(chunk)
243
+ at = _AT_RE.search(chunk)
244
+ if not by or not _scalar(by.group(1)):
245
+ issues.append(f"{what} entry is missing `by:`")
246
+ if not at:
247
+ issues.append(f"{what} entry is missing `at:`")
248
+ else:
249
+ value = _scalar(at.group(1))
250
+ # A full ISO 8601 UTC timestamp is the standard; a bare date is tolerated (still unambiguous).
251
+ if not (_ISO_DATETIME_RE.match(value) or _ISO_DATE_RE.match(value)):
252
+ issues.append(f"{what} `at: {value}` is not an ISO 8601 timestamp (YYYY-MM-DDTHH:MM:SSZ)")
253
+ return issues
254
+
255
+
256
+ def _split_list_entries(nested: list[str]) -> list[str]:
257
+ """Group indented lines into ``- ``-introduced list entries (one chunk of text each)."""
258
+ entries: list[str] = []
259
+ for line in nested:
260
+ if line.strip().startswith("- "):
261
+ entries.append(line.strip()[2:])
262
+ elif entries:
263
+ entries[-1] += "\n" + line.strip()
264
+ return entries
265
+
266
+
267
+ HUB_REPORTS_DIR = ".caddis/advisory-hub-reports"
268
+
269
+
270
+ def misfiled_hub_spawns(root: Path) -> list[str]:
271
+ """Files whose FRONTMATTER declares `type: hub-spawn` but which live outside the reports dir.
272
+
273
+ `/caddis:spawn-hub` states the correct path, justifies it, and is gated — but the gate only
274
+ runs INSIDE the command. An agent asked for a handover "basically like what we do with caddis
275
+ spawn-hub" hand-wrote one without invoking it, never read the instruction, and filed the
276
+ artefact in `.caddis/prompts/` because that is where the project's generic prompts live. The
277
+ mechanism was self-enforcing only for those already using it.
278
+
279
+ The knock-on is the sharp part: spawn-hub's own trap-rotation step reads the last spawn prompt
280
+ from the reports dir to pick a fresh trap. A prompt filed elsewhere is invisible, so rotation
281
+ silently runs against an empty set — the exact failure the artefact file was introduced to
282
+ prevent, returning through a different door.
283
+
284
+ Frontmatter only, never a prose match: this repo's own documentation says `type: hub-spawn` in
285
+ running text, and flagging that would make the check permanently red where it is authored.
286
+ """
287
+ out: list[str] = []
288
+ try:
289
+ # --others --exclude-standard is load-bearing: a hand-rolled spawn prompt is UNTRACKED at
290
+ # the exact moment it is misfiled, and a plain `git ls-files` lists only tracked files. The
291
+ # first version of this check used one, so it passed cleanly on a live misfiled artefact —
292
+ # a check that cannot fail, guarding the case it was written for.
293
+ listed = subprocess.run(
294
+ ["git", "ls-files", "--cached", "--others", "--exclude-standard", "*.md"],
295
+ cwd=str(root), capture_output=True, text=True, timeout=30)
296
+ files = [root / p for p in listed.stdout.splitlines() if p.strip()] \
297
+ if listed.returncode == 0 else []
298
+ except Exception:
299
+ files = []
300
+ if not files: # no git, or an empty listing — fall back to the artefact tree
301
+ files = sorted((root / ".caddis").rglob("*.md")) if (root / ".caddis").is_dir() else []
302
+ for f in files:
303
+ rel = f.relative_to(root).as_posix() if f.is_absolute() else str(f)
304
+ if rel.startswith(HUB_REPORTS_DIR):
305
+ continue
306
+ try:
307
+ block = frontmatter_block(f.read_text(encoding="utf-8", errors="ignore"))
308
+ except OSError:
309
+ continue
310
+ if block is None:
311
+ continue
312
+ field = _top_level_field(block, "type")
313
+ if field and _scalar(field[0]).strip().lower() == "hub-spawn":
314
+ out.append(rel)
315
+ return out
316
+
317
+
318
+ def trust_signal_warnings(text: str) -> list[str]:
319
+ """Well-formedness warnings for the OKF v0.2 trust signals present in ``text``. Pure.
320
+
321
+ Returns ``[]`` for a document with no frontmatter, or with frontmatter carrying none of the
322
+ trust fields — silence is the correct answer for every pre-v0.2 document. Never raises, and
323
+ the caller must never treat a returned string as a gate failure.
324
+ """
325
+ block = frontmatter_block(text)
326
+ if block is None:
327
+ return []
328
+ warns: list[str] = []
329
+
330
+ status = _top_level_field(block, "status")
331
+ if status is not None:
332
+ value = _scalar(status[0])
333
+ # Only a single bare token is vetted. caddis has long-standing docs whose status is a
334
+ # sentence ("DRAFT — approved shape (Option 2), awaiting go"); vetting those would nag
335
+ # about prose that predates the field's vocabulary. A one-word typo still surfaces.
336
+ if value and " " not in value and value.lower() not in TRUST_STATUS_VALUES:
337
+ warns.append(
338
+ f"`status: {value}` is not a known value "
339
+ f"({', '.join(sorted(TRUST_STATUS_VALUES))})"
340
+ )
341
+
342
+ stale = _top_level_field(block, "stale_after")
343
+ if stale is not None:
344
+ value = _scalar(stale[0])
345
+ if not value:
346
+ warns.append("`stale_after:` is empty — give an ISO date (YYYY-MM-DD) or drop the field")
347
+ elif not (_ISO_DATE_RE.match(value) or _ISO_DATETIME_RE.match(value)):
348
+ warns.append(f"`stale_after: {value}` is not an ISO date (YYYY-MM-DD)")
349
+
350
+ verified = _top_level_field(block, "verified")
351
+ if verified is not None:
352
+ inline, nested = verified
353
+ entries = _split_list_entries(nested)
354
+ flow = _scalar(inline)
355
+ if not entries and flow.startswith("["):
356
+ # Inline flow-sequence form: verified: [ { by: x, at: y }, { ... } ]
357
+ entries = [e for e in flow.strip("[]").split("},") if e.strip()]
358
+ if entries:
359
+ for entry in entries:
360
+ warns.extend(_actor_entry_issues(entry, "`verified`"))
361
+ elif flow and not flow.startswith("["):
362
+ warns.append("`verified:` must be a list of `{ by, at }` entries, not a scalar")
363
+ else:
364
+ warns.append("`verified:` is present but lists no `{ by, at }` entries")
365
+
366
+ generated = _top_level_field(block, "generated")
367
+ if generated is not None:
368
+ inline, nested = generated
369
+ chunk = inline + "\n" + "\n".join(nested)
370
+ if not chunk.strip():
371
+ warns.append("`generated:` is present but empty — needs `{ by, at }`")
372
+ else:
373
+ warns.extend(_actor_entry_issues(chunk, "`generated`"))
374
+
375
+ return warns
376
+
377
+
378
+ def docmap_issues(
379
+ entries: set[str],
380
+ governed: set[str],
381
+ existing: set[str],
382
+ ) -> tuple[list[str], list[str]]:
383
+ """Return ``(orphans, dangling)``.
384
+
385
+ orphan = a governed reference doc that the map does not index.
386
+ dangling = a path the map links to that does not exist on disk.
387
+ """
388
+ orphans = sorted(governed - entries)
389
+ dangling = sorted(e for e in entries if e not in existing)
390
+ return orphans, dangling
391
+
392
+
393
+ # --------------------------------------------------------------------------- #
394
+ # Filesystem glue — all path resolution and skip logic lives here.
395
+ # --------------------------------------------------------------------------- #
396
+ def _to_root_relative(target: str, doc_map_file: Path, root: Path) -> str:
397
+ """Resolve a doc-map link target (written relative to the doc-map) to a repo-root path."""
398
+ resolved = (doc_map_file.parent / target).resolve()
399
+ try:
400
+ return resolved.relative_to(root.resolve()).as_posix()
401
+ except ValueError:
402
+ return target # outside the repo — leave as-is (will surface as dangling)
403
+
404
+
405
+ def _governed_docs(root: Path, globs: tuple[str, ...]) -> set[str]:
406
+ docs: set[str] = set()
407
+ for pattern in globs:
408
+ for path in root.glob(pattern):
409
+ if path.is_file():
410
+ docs.add(path.relative_to(root).as_posix())
411
+ return docs
412
+
413
+
414
+ def scan_trust_signals(root: Path) -> list[tuple[str, list[str]]]:
415
+ """``(repo-relative path, warnings)`` for every artifact-dir doc with a malformed trust field.
416
+
417
+ Scoped to the caddis artifact dir(s) — the docs this standard governs — not the whole repo, and
418
+ not ``docs/``. A repo with no artifact dir yields nothing. Files that produce no warnings are
419
+ omitted, so the common case (no trust fields anywhere) returns an empty list.
420
+ """
421
+ root = Path(root)
422
+ out: list[tuple[str, list[str]]] = []
423
+ for name in ARTIFACT_DIRS:
424
+ base = root / name
425
+ if not base.is_dir():
426
+ continue
427
+ for dirpath, dirnames, filenames in os.walk(base):
428
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
429
+ for fn in sorted(filenames):
430
+ if not fn.endswith(".md"):
431
+ continue
432
+ path = Path(dirpath) / fn
433
+ try:
434
+ text = path.read_text(encoding="utf-8", errors="replace")
435
+ except OSError:
436
+ continue # vanished/locked mid-scan — never crash the gate
437
+ issues = trust_signal_warnings(text)
438
+ if issues:
439
+ out.append((path.relative_to(root).as_posix(), issues))
440
+ return sorted(out)
441
+
442
+
443
+ def _count_lines(path: Path) -> int | None:
444
+ # errors="replace": scans arbitrary repo files; a non-UTF-8 rules file must not crash the gate.
445
+ try:
446
+ return len(path.read_text(encoding="utf-8", errors="replace").splitlines())
447
+ except OSError:
448
+ return None # vanished/locked file mid-scan — skip, never crash the gate
449
+
450
+
451
+ def _rules_md_lengths(root: Path) -> dict[str, int]:
452
+ """Line counts of the always-loaded rules file per folder.
453
+
454
+ Post-migration the canonical rules live in ``AGENTS.md`` and ``CLAUDE.md`` is a tiny ``@AGENTS.md``
455
+ import shim; the size Claude Code actually loads is AGENTS.md + the shim, so both are summed and the
456
+ entry is keyed by the AGENTS.md path. A pre-migration folder has only ``CLAUDE.md`` — measure that.
457
+ os.walk with in-place dir pruning skips vendored/generated trees DURING traversal so we never
458
+ descend into node_modules (slow, and a broken symlink there raises FileNotFoundError mid-rglob).
459
+ """
460
+ lengths: dict[str, int] = {}
461
+ for dirpath, dirnames, filenames in os.walk(root):
462
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
463
+ d = Path(dirpath)
464
+ if "AGENTS.md" in filenames:
465
+ n = _count_lines(d / "AGENTS.md")
466
+ if n is None:
467
+ continue
468
+ if "CLAUDE.md" in filenames: # + the @import shim it loads alongside
469
+ n += _count_lines(d / "CLAUDE.md") or 0
470
+ lengths[(d / "AGENTS.md").relative_to(root).as_posix()] = n
471
+ elif "CLAUDE.md" in filenames: # pre-migration repo: CLAUDE.md is the rules file
472
+ n = _count_lines(d / "CLAUDE.md")
473
+ if n is not None:
474
+ lengths[(d / "CLAUDE.md").relative_to(root).as_posix()] = n
475
+ return lengths
476
+
477
+
478
+ def _repo_root() -> Path:
479
+ """Resolve the repo root: git top-level if available, else cwd. Location-independent, so it
480
+ works wherever setup-project-ai copies the checker (and matches the caddis hooks)."""
481
+ try:
482
+ import subprocess
483
+ out = subprocess.run(
484
+ ["git", "rev-parse", "--show-toplevel"],
485
+ capture_output=True, text=True, timeout=5,
486
+ )
487
+ if out.returncode == 0 and out.stdout.strip():
488
+ return Path(out.stdout.strip())
489
+ except Exception:
490
+ pass
491
+ return Path.cwd()
492
+
493
+
494
+ # --------------------------------------------------------------------------- #
495
+ # Doc-map generation & reindex — deterministic scaffolding / backfill for repos
496
+ # that have the harness but no KB yet (the KB was introduced late). ADDITIVE: it
497
+ # creates a missing map and indexes un-indexed KB notes, but never deletes a
498
+ # human-written row (mirrors knowledge-transfer's "append, don't delete"); a
499
+ # dangling link is *reported*, not removed. Reused by setup_project_ai for the
500
+ # fresh-scaffold path so both share one implementation.
501
+ # --------------------------------------------------------------------------- #
502
+
503
+ # Root-level docs worth linking from a fresh map, each with a canned one-liner. Only the
504
+ # ones that actually exist get linked (a dangling link is a hard gate failure).
505
+ _KNOWN_ROOT_DOCS: tuple[tuple[str, str], ...] = (
506
+ ("README.md", "Repo overview — what it is, how to run it."),
507
+ ("ARCHITECTURE.md", "System architecture."),
508
+ ("DESIGN.md", "Design notes / decisions."),
509
+ ("MIGRATION.md", "Migration / cutover notes."),
510
+ ("CONTRIBUTING.md", "How to contribute / dev setup."),
511
+ ("CHANGELOG.md", "Release history."),
512
+ ("ROADMAP.md", "Roadmap."),
513
+ )
514
+ _MAX_DISCOVERED_DOCS = 8 # cap docs/ links so a large docs tree can't bloat the map
515
+
516
+ _DOCMAP_SCAFFOLD = """\
517
+ # DOC-MAP — {name} code knowledge index (the KB)
518
+
519
+ > **What this is:** the curated index of the docs that matter for understanding *this codebase* —
520
+ > one line each: what it is and when to read it. A router, not a summary; detail lives in the linked
521
+ > docs. KB notes live beside this file in `{artifact_dir}/kb/`.
522
+ >
523
+ > **Discipline:** kept honest by [`check_doc_coverage.py`](../../scripts/check_doc_coverage.py) —
524
+ > every link here must resolve (a link to a missing file is a **hard failure**), and a KB note
525
+ > (`{artifact_dir}/kb/*.md`) not indexed here **warns**. Only `{artifact_dir}/kb/*.md` is governed.
526
+
527
+ ## Read first
528
+ 1. `CLAUDE.md` — project laws + the dev harness (if the repo has one).
529
+ 2. The entries below, by area.
530
+
531
+ ## Knowledge base (`{artifact_dir}/kb/`)
532
+
533
+ | Doc | What / when to read |
534
+ |---|---|
535
+ | _(no KB notes yet — add them here as links)_ | |
536
+
537
+ ## Other key code-relevant docs
538
+
539
+ | Doc | What / when to read |
540
+ |---|---|
541
+ | _(optionally link a README, design doc, or runbook)_ | |
542
+
543
+ ---
544
+
545
+ *Governed = `{artifact_dir}/kb/*.md` only (must be indexed here). The wider repo docs are not policed.*
546
+ """
547
+
548
+
549
+ def discover_reference_docs(root: Path) -> list[tuple[str, str]]:
550
+ """Deterministically find code-relevant docs worth linking from a fresh map.
551
+
552
+ Returns ``(repo-root-relative posix path, one-line description)`` pairs. Only **existing** files
553
+ are returned (a dangling link would hard-fail the gate). Scans the known root docs, then
554
+ ``docs/**/*.md`` (sorted, capped); never the KB itself or vendored trees.
555
+ """
556
+ root = Path(root)
557
+ found: list[tuple[str, str]] = []
558
+ for name, desc in _KNOWN_ROOT_DOCS:
559
+ if (root / name).is_file():
560
+ found.append((name, desc))
561
+ docs_dir = root / "docs"
562
+ if docs_dir.is_dir():
563
+ # os.walk (not rglob) with in-place dir-pruning: skips vendored trees and does not follow
564
+ # symlinks (followlinks=False), so a symlinked directory loop can't send it spinning. The
565
+ # is_file() guard below then excludes broken file-symlinks — linking one would make the
566
+ # freshly-built map dangle against its own gate.
567
+ rels: list[str] = []
568
+ for dirpath, dirnames, filenames in os.walk(docs_dir):
569
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
570
+ for fn in filenames:
571
+ if not fn.endswith(".md"):
572
+ continue
573
+ fp = Path(dirpath) / fn
574
+ try:
575
+ if fp.is_file(): # follows symlinks → a broken link is False → excluded
576
+ rels.append(fp.relative_to(root).as_posix())
577
+ except (OSError, ValueError):
578
+ continue
579
+ found.extend((rel, "Reference doc.") for rel in sorted(rels)[:_MAX_DISCOVERED_DOCS])
580
+ return found
581
+
582
+
583
+ def _row(target: str, label: str, desc: str) -> str:
584
+ """One markdown table row: ``| [label](target) | desc |``."""
585
+ return f"| [{label}]({target}) | {desc} |"
586
+
587
+
588
+ # A placeholder row's LABEL cell is *entirely* an italic ``_(…)_`` stub. Matching the whole cell (not
589
+ # just a ``_(`` prefix) avoids dropping a real row whose label merely starts with ``_(`` — e.g.
590
+ # ``| _(deprecated)_ [old.md](old.md) | … |`` is a real linked doc, not a placeholder.
591
+ _PLACEHOLDER_LABEL = re.compile(r"^_\(.*\)_$")
592
+
593
+
594
+ def _is_placeholder_row(row: str) -> bool:
595
+ """True for a scaffold placeholder row — one whose *label* (first) cell is only an ``_(…)_`` stub.
596
+
597
+ Checks ONLY the label cell, and requires the WHOLE cell to be the italic stub. A real row's label
598
+ is a ``[link](…)``; its *description* cell may legitimately contain ``_(`` (e.g. "the `_(x)_` case")
599
+ — neither must be mistaken for a placeholder, or the row is silently dropped (data loss).
600
+ """
601
+ cells = [c.strip() for c in row.split("|")]
602
+ return len(cells) > 1 and bool(_PLACEHOLDER_LABEL.match(cells[1]))
603
+
604
+
605
+ def reference_rows(root: Path) -> list[str]:
606
+ """Table rows for every discovered reference doc, its link written relative to the doc-map dir.
607
+
608
+ The doc-map lives two levels deep (`<artifact-dir>/kb/`), so a repo-root path is reached via `../../`.
609
+ """
610
+ return [_row("../../" + rel, rel, desc) for rel, desc in discover_reference_docs(root)]
611
+
612
+
613
+ def kb_note_rows(root: Path, indexed: set[str] | None = None) -> list[str]:
614
+ """Rows for `<artifact-dir>/kb/*.md` notes (excluding DOC-MAP), skipping already-indexed ones.
615
+
616
+ ``indexed`` holds repo-root-relative paths already linked in the map. The description is a plain
617
+ nudge (NOT an ``_(…)_`` placeholder, so a later reindex won't drop it) for the agent/human to fill.
618
+ A half-migrated repo (both dirs present) has BOTH scanned, so a straggler note left in the legacy
619
+ dir is still indexed — its link is written relative to the doc-map, like any other row.
620
+
621
+ Defaults to ``None`` rather than ``frozenset()``: the annotation said ``set[str]`` while the
622
+ default was a frozenset, so the two disagreed and mypy failed the file. ``None`` is the idiomatic
623
+ way to get an empty default without a shared mutable one.
624
+ """
625
+ indexed = indexed or set()
626
+ root = Path(root)
627
+ home = kb_dir(root)
628
+ notes = {p.resolve(): p for name in ARTIFACT_DIRS for p in (root / name / "kb").glob("*.md")}
629
+ rows: list[str] = []
630
+ for p in sorted(notes.values()):
631
+ rel = p.relative_to(root).as_posix()
632
+ if p.name == "DOC-MAP.md" or rel in indexed:
633
+ continue
634
+ if p.parent.resolve() != home.resolve():
635
+ rows.append(_row("../../" + rel, rel, "auto-indexed — add a one-line 'when to read'."))
636
+ continue
637
+ rows.append(_row(p.name, p.name, "auto-indexed — add a one-line 'when to read'."))
638
+ return rows
639
+
640
+
641
+ def insert_table_rows(text: str, heading_contains: str, rows: list[str]) -> str:
642
+ """Append ``rows`` to the markdown table under the first ``## …heading_contains…`` heading. Pure.
643
+
644
+ Drops italic placeholder rows (a first cell containing ``_(``) when adding real rows. If the
645
+ heading or its table can't be located, returns ``text`` unchanged — fail-safe, never corrupts.
646
+ """
647
+ if not rows:
648
+ return text
649
+ lines = text.splitlines()
650
+ heading = next(
651
+ (i for i, ln in enumerate(lines)
652
+ if ln.strip().startswith("## ") and heading_contains.lower() in ln.strip().lower()),
653
+ None,
654
+ )
655
+ if heading is None:
656
+ return text
657
+ start = end = None
658
+ for i in range(heading + 1, len(lines)):
659
+ s = lines[i].strip()
660
+ if s.startswith("## "):
661
+ break
662
+ if s.startswith("|"):
663
+ start = i if start is None else start
664
+ end = i
665
+ elif start is not None and not s:
666
+ break
667
+ # Both or neither: `end` is only ever assigned in the same branch that sets `start`, so this
668
+ # cannot fire in practice — but the narrowing is what lets mypy see that `end + 1` below is
669
+ # int + int rather than None + int, and the type checker is right that nothing else proved it.
670
+ if start is None or end is None:
671
+ return text
672
+ head = lines[start:start + 2] # header row + |---| separator
673
+ body = [b for b in lines[start + 2:end + 1] if not _is_placeholder_row(b)] # keep real rows
674
+ out = "\n".join(lines[:start] + head + body + rows + lines[end + 1:])
675
+ return out + "\n" if text.endswith("\n") else out # preserve trailing newline (git-clean)
676
+
677
+
678
+ def build_docmap(root: Path, project_name: str = "project") -> str:
679
+ """Full DOC-MAP text for a fresh repo: scaffold + discovered reference docs + existing KB notes.
680
+
681
+ The scaffold names the artifact dir this repo actually uses, so an unmigrated repo's map doesn't
682
+ tell readers to look in a `.caddis/kb/` that isn't there."""
683
+ text = _DOCMAP_SCAFFOLD.replace("{name}", project_name)
684
+ text = text.replace("{artifact_dir}", artifact_root(root).name)
685
+ text = insert_table_rows(text, "Knowledge base", kb_note_rows(root))
686
+ text = insert_table_rows(text, "Other key", reference_rows(root))
687
+ return text
688
+
689
+
690
+ def dangling_as_written(text: str, doc_map_file: Path, root: Path) -> list[str]:
691
+ """As-written ``.md`` link targets in ``text`` whose resolved file is missing on disk (sorted).
692
+
693
+ Returned verbatim (not root-relative) so a row can be matched and removed as-written by
694
+ :func:`remove_rows_with_targets`.
695
+ """
696
+ return sorted(
697
+ t for t in extract_docmap_entries(text)
698
+ if not (root / _to_root_relative(t, doc_map_file, root)).exists()
699
+ )
700
+
701
+
702
+ def _dangling_rootrel(text: str, doc_map_file: Path, root: Path) -> list[str]:
703
+ """Repo-root-relative ``.md`` links in ``text`` whose file is missing on disk (sorted)."""
704
+ ents = {_to_root_relative(t, doc_map_file, root) for t in extract_docmap_entries(text)}
705
+ return sorted(e for e in ents if not (root / e).exists())
706
+
707
+
708
+ def remove_rows_with_targets(text: str, targets: list[str]) -> str:
709
+ """Drop markdown table rows whose link points at any of ``targets`` (as-written). Pure.
710
+
711
+ Only ``|``-delimited rows are removed (the index table itself) — prose links are left alone.
712
+ Tolerates a leading ``./`` in the written link, and preserves a trailing newline.
713
+ """
714
+ if not targets:
715
+ return text
716
+ tset = set(targets)
717
+
718
+ def _links_dangling(line: str) -> bool:
719
+ return line.lstrip().startswith("|") and any(
720
+ f"]({t})" in line or f"](./{t})" in line for t in tset
721
+ )
722
+
723
+ kept = [ln for ln in text.splitlines() if not _links_dangling(ln)]
724
+ out = "\n".join(kept)
725
+ return out + "\n" if text.endswith("\n") else out
726
+
727
+
728
+ def _atomic_write(path: Path, text: str) -> bool:
729
+ """Write ``text`` via a same-dir, per-process temp file + atomic replace. Returns True on success.
730
+
731
+ A crash mid-write can't leave a truncated DOC-MAP (mirrors dream_memory.save_facts). The temp name
732
+ is PID-tagged so two concurrent runs don't fight over one temp. Returns **False (never raises)** if
733
+ the target is locked — on Windows an open editor / AV can hold it — so a maintenance run degrades to
734
+ a clear message instead of a stack trace.
735
+ """
736
+ tmp = path.with_name(f"{path.name}.{os.getpid()}.tmp")
737
+ try:
738
+ path.parent.mkdir(parents=True, exist_ok=True)
739
+ tmp.write_text(text, encoding="utf-8", newline="\n")
740
+ tmp.replace(path)
741
+ return True
742
+ except OSError:
743
+ try:
744
+ tmp.unlink()
745
+ except OSError:
746
+ pass
747
+ return False
748
+
749
+
750
+ def reindex(
751
+ root: Path, project_name: str = "project", write: bool = True, prune: bool = False
752
+ ) -> tuple[bool, list[str]]:
753
+ """Create a missing DOC-MAP, index un-indexed KB notes, and (with ``prune``) drop dangling rows.
754
+
755
+ Returns ``(changed, summary_lines)``. Indexing is additive (never deletes). ``prune`` is the
756
+ explicit, destructive opt-in that removes index ROWS linking to now-missing files. The summary
757
+ reports **only what actually landed** — a prose dangling link (not a table row) or a missing
758
+ Knowledge-base table is reported as still-broken, never as done. Without ``prune``, dangling links
759
+ are only *reported*.
760
+ """
761
+ root = Path(root)
762
+ doc_map = doc_map_path(root)
763
+ doc_map_rel = doc_map.relative_to(root).as_posix()
764
+ if not doc_map.exists():
765
+ text = build_docmap(root, project_name)
766
+ if write and not _atomic_write(doc_map, text):
767
+ return False, [f"could not create {doc_map_rel} (target locked?) — no changes made"]
768
+ return True, [f"created {doc_map_rel} ({text.count('](')} link(s) pre-indexed)"]
769
+
770
+ original = doc_map.read_text(encoding="utf-8")
771
+ entries = {_to_root_relative(t, doc_map, root) for t in extract_docmap_entries(original)}
772
+ governed = _governed_docs(root, GOVERNED_GLOBS) - {doc_map.relative_to(root).as_posix()}
773
+ existing = {e for e in entries if (root / e).exists()}
774
+ orphans, dangling = docmap_issues(entries, governed, existing)
775
+
776
+ summary: list[str] = []
777
+ updated = original
778
+
779
+ # Index orphan KB notes (additive). Report success ONLY if the rows actually landed — insert is a
780
+ # no-op when the "Knowledge base" heading/table is absent, and we must not claim work we didn't do.
781
+ if orphans:
782
+ after = insert_table_rows(updated, "Knowledge base", kb_note_rows(root, entries))
783
+ if after != updated:
784
+ updated = after
785
+ summary.append(f"indexed {len(orphans)} KB note(s): " + ", ".join(orphans))
786
+ else:
787
+ summary.append(f"could not index {len(orphans)} note(s) — no 'Knowledge base' table found: "
788
+ + ", ".join(orphans))
789
+
790
+ # Dangling links. --prune removes only table ROWS; a dangling link in prose survives, so report
791
+ # what was actually pruned vs. what remains — never claim the gate is clean when it isn't.
792
+ if dangling:
793
+ if prune:
794
+ updated = remove_rows_with_targets(updated, dangling_as_written(updated, doc_map, root))
795
+ remaining = _dangling_rootrel(updated, doc_map, root)
796
+ pruned = [d for d in dangling if d not in remaining]
797
+ if pruned:
798
+ summary.append(f"pruned {len(pruned)} dangling link(s): " + ", ".join(pruned))
799
+ if remaining:
800
+ summary.append(f"{len(remaining)} dangling link(s) remain (in prose, not a table row) "
801
+ "— fix by hand: " + ", ".join(remaining))
802
+ else:
803
+ summary.append(f"{len(dangling)} dangling link(s) to fix (run --prune to remove): "
804
+ + ", ".join(dangling))
805
+
806
+ changed = updated != original
807
+ if changed and write and not _atomic_write(doc_map, updated):
808
+ return False, summary + ["WARNING: could not write DOC-MAP (target locked?) — changes NOT saved"]
809
+ if not summary:
810
+ summary.append("DOC-MAP.md already in sync — nothing to do.")
811
+ return changed, summary
812
+
813
+
814
+ def run(root: Path, check: bool) -> int:
815
+ root = Path(root)
816
+ # Optional per-repo overrides ([doc_coverage] in <artifact-dir>/config.toml); defaults otherwise.
817
+ cfg = load_config(root, "doc_coverage")
818
+ route_tree = root / get_str(cfg, "route_tree", DEFAULT_ROUTE_TREE)
819
+ page_guide = root / get_str(cfg, "page_guide", DEFAULT_PAGE_GUIDE)
820
+ doc_map = doc_map_path(root)
821
+ # agents_md_budget is the current key; claude_md_budget is the accepted back-compat alias.
822
+ budget = get_int(cfg, "agents_md_budget", get_int(cfg, "claude_md_budget", CLAUDE_MD_BUDGET))
823
+ ignore = frozenset(get_str_list(cfg, "ignore_routes", list(IGNORE_ROUTES)))
824
+
825
+ hard_failures: list[str] = []
826
+ warnings: list[str] = []
827
+
828
+ # 1. Page-guide route coverage. Skip SILENTLY when either input is absent — a backend-only
829
+ # repo has no route tree and must not be nagged (inversion vs the ported reference impl).
830
+ if route_tree.exists() and page_guide.exists():
831
+ missing, extra = route_coverage_gaps(
832
+ route_tree.read_text(encoding="utf-8"),
833
+ page_guide.read_text(encoding="utf-8"),
834
+ ignore=ignore,
835
+ )
836
+ if missing:
837
+ hard_failures.append("UI_PAGE_GUIDE.md missing route(s): " + ", ".join(missing))
838
+ if extra:
839
+ warnings.append("UI_PAGE_GUIDE.md documents route(s) not in code: " + ", ".join(extra))
840
+
841
+ # 2. Doc-map integrity. Skip when the doc-map is absent — a repo without a KB is fine, NOT a
842
+ # hard failure (inversion vs the ported reference impl, which failed here). Links are written
843
+ # relative to the doc-map's own location; normalise to repo-root-relative before comparing.
844
+ if doc_map.exists():
845
+ raw = extract_docmap_entries(doc_map.read_text(encoding="utf-8"))
846
+ entries = {_to_root_relative(target, doc_map, root) for target in raw}
847
+ governed = _governed_docs(root, GOVERNED_GLOBS) - {doc_map.relative_to(root).as_posix()}
848
+ existing = {e for e in entries if (root / e).exists()}
849
+ orphans, dangling = docmap_issues(entries, governed, existing)
850
+ if dangling:
851
+ hard_failures.append("DOC-MAP.md links to missing file(s): " + ", ".join(dangling))
852
+ if orphans:
853
+ warnings.append("DOC-MAP.md does not index governed doc(s): " + ", ".join(orphans))
854
+
855
+ # 3. Rules-file budget (warning only) — AGENTS.md (+ its CLAUDE.md shim), or CLAUDE.md pre-migration.
856
+ for name, n in oversize_files(_rules_md_lengths(root), budget):
857
+ warnings.append(f"{name} is {n} lines (> {budget} budget)")
858
+
859
+ # 4. OKF v0.2 trust signals (warning only, and only when a field is PRESENT and malformed).
860
+ # Never a hard failure and never reported for an absent field — the trust signals are
861
+ # recommended, not required, so a pre-v0.2 repo produces exactly zero output here.
862
+ for rel, issues in scan_trust_signals(root):
863
+ for issue in issues:
864
+ warnings.append(f"{rel}: {issue}")
865
+
866
+ # 5. Misfiled Hub artefacts (warning only).
867
+ for rel in misfiled_hub_spawns(root):
868
+ warnings.append(
869
+ f"{rel} is a hub-spawn artefact outside {HUB_REPORTS_DIR} — /caddis:spawn-hub's "
870
+ "trap rotation reads only that directory, so a prompt filed elsewhere is invisible "
871
+ "to it and the next succession rotates against an empty set")
872
+
873
+ for w in warnings:
874
+ print(f" warning: {w}")
875
+
876
+ if hard_failures:
877
+ print("\nDoc coverage check FAILED:")
878
+ for f in hard_failures:
879
+ print(f" - {f}")
880
+ print("\nFix: add the missing page(s) to UI_PAGE_GUIDE.md / index them in DOC-MAP.md.")
881
+ return 1 if check else 0
882
+
883
+ # Quiet on success in gate mode (no noise); give the human a confirmation otherwise.
884
+ if not check:
885
+ print("Doc coverage OK." if not warnings else "\nDoc coverage OK (warnings above).")
886
+ return 0
887
+
888
+
889
+ def main() -> None:
890
+ if hasattr(sys.stdout, "reconfigure"):
891
+ sys.stdout.reconfigure(encoding="utf-8") # Windows cp1252 can't encode some doc names
892
+ parser = argparse.ArgumentParser(
893
+ description="Verify reference docs cover the live app; exit non-zero on hard drift.",
894
+ )
895
+ parser.add_argument(
896
+ "--check",
897
+ action="store_true",
898
+ help="Gate mode: exit non-zero on a hard failure (missing route / dangling link).",
899
+ )
900
+ parser.add_argument(
901
+ "--reindex",
902
+ action="store_true",
903
+ help="Create a missing DOC-MAP or index un-indexed KB notes (additive; writes the file).",
904
+ )
905
+ parser.add_argument(
906
+ "--prune",
907
+ action="store_true",
908
+ help="With reindex: also REMOVE index rows that link to missing files (destructive; opt-in).",
909
+ )
910
+ args = parser.parse_args()
911
+ root = _repo_root()
912
+ # Reindex/prune are maintenance actions (the `/kb` command drives them), not a gate — run, exit 0.
913
+ if args.reindex or args.prune:
914
+ _changed, summary = reindex(root, root.name, prune=args.prune)
915
+ for line in summary:
916
+ print(f"[kb] {line}")
917
+ sys.exit(0)
918
+ # Resolve the repo root from git (cwd fallback) — the pre-push gate runs from the repo root,
919
+ # so this is correct wherever the checker is copied. Tests call run() with an explicit root.
920
+ sys.exit(run(root, args.check))
921
+
922
+
923
+ if __name__ == "__main__":
924
+ main()