@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.
- package/bundles/antigravity-plugin/agents/claude-md-curator.md +1 -1
- package/bundles/antigravity-plugin/agents/knowledge-transfer.md +17 -32
- package/bundles/antigravity-plugin/caddis_statusline.py +825 -0
- package/bundles/antigravity-plugin/claude-md/folder-agents.md.tmpl +21 -0
- package/bundles/antigravity-plugin/guard_agy.py +60 -9
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +524 -0
- package/bundles/antigravity-plugin/scripts/caddis_inventory.py +439 -0
- package/bundles/antigravity-plugin/scripts/caddis_migrate_dir.py +270 -0
- package/bundles/antigravity-plugin/scripts/caddis_spawn.py +300 -0
- package/bundles/antigravity-plugin/scripts/caddis_statusline.py +825 -0
- package/bundles/antigravity-plugin/scripts/caddis_tidy.py +366 -0
- package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +924 -0
- package/bundles/antigravity-plugin/scripts/claude-deepseek.ps1 +4 -0
- package/bundles/antigravity-plugin/scripts/claude-deepseek.sh +3 -0
- package/bundles/antigravity-plugin/scripts/claude-glm.ps1 +4 -0
- package/bundles/antigravity-plugin/scripts/claude-glm.sh +3 -0
- package/bundles/antigravity-plugin/scripts/claude-oss.ps1 +64 -0
- package/bundles/antigravity-plugin/scripts/claude-oss.sh +60 -0
- package/bundles/antigravity-plugin/scripts/claudster_config.py +121 -0
- package/bundles/antigravity-plugin/scripts/claudster_doctor.py +356 -0
- package/bundles/antigravity-plugin/scripts/claudster_init.py +400 -0
- package/bundles/antigravity-plugin/scripts/claudster_migrate_rules.py +300 -0
- package/bundles/antigravity-plugin/scripts/enforcement_inventory.py +261 -0
- package/bundles/antigravity-plugin/scripts/hook_log.py +156 -0
- package/bundles/antigravity-plugin/scripts/new-machine.ps1 +170 -0
- package/bundles/antigravity-plugin/scripts/oss_ask.py +98 -0
- package/bundles/antigravity-plugin/scripts/oss_model.py +174 -0
- package/bundles/antigravity-plugin/scripts/oss_review.py +718 -0
- package/bundles/antigravity-plugin/scripts/setup_project_ai.py +1432 -0
- package/bundles/antigravity-plugin/scripts/usage_review.py +1323 -0
- package/bundles/antigravity-plugin/session_end_agy.py +52 -8
- package/bundles/antigravity-plugin/skills/_registry.md +1 -0
- package/bundles/antigravity-plugin/skills/advisory-hub/SKILL.md +44 -0
- package/bundles/antigravity-plugin/skills/code-review/SKILL.md +11 -0
- package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +21 -0
- package/bundles/antigravity-plugin/skills/gate-review/SKILL.md +11 -0
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +36 -0
- package/bundles/antigravity-plugin/skills/security-review/SKILL.md +9 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/statusline/SKILL.md +105 -0
- package/bundles/antigravity-plugin/skills/use-model/SKILL.md +15 -3
- package/bundles/antigravity-plugin/skills/validate-phase/SKILL.md +31 -0
- package/bundles/antigravity-plugin/warm_start_agy.py +117 -11
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/architecture-design/SKILL.md +1 -1
- package/bundles/antigravity-plugin-extras/skills/caching-patterns/SKILL.md +1 -1
- package/bundles/antigravity-plugin-extras/skills/webapp-development/SKILL.md +1 -1
- package/bundles/codex/.codex/config.toml.example +36 -0
- package/bundles/codex/.codex/skills/coding/api-design/SKILL.md +249 -0
- package/bundles/codex/.codex/skills/coding/backend-development/SKILL.md +305 -0
- package/bundles/codex/.codex/skills/coding/code-review/SKILL.md +140 -0
- package/bundles/codex/.codex/skills/coding/codebase-audit/SKILL.md +204 -0
- package/bundles/codex/.codex/skills/coding/dashboard-perf-sweep/SKILL.md +132 -0
- package/bundles/codex/.codex/skills/coding/dashboard-perf-sweep/scripts/perf_sweep.py +249 -0
- package/bundles/codex/.codex/skills/coding/error-handling/SKILL.md +260 -0
- package/bundles/codex/.codex/skills/coding/fastapi-dev/SKILL.md +300 -0
- package/bundles/codex/.codex/skills/coding/javascript-typescript/SKILL.md +142 -0
- package/bundles/codex/.codex/skills/coding/python/SKILL.md +388 -0
- package/bundles/codex/.codex/skills/coding/refactoring/SKILL.md +486 -0
- package/bundles/codex/.codex/skills/coding/security-review/SKILL.md +205 -0
- package/bundles/codex/.codex/skills/coding/sql/SKILL.md +321 -0
- package/bundles/codex/.codex/skills/data/database-design/SKILL.md +177 -0
- package/bundles/codex/.codex/skills/data/db-diagram/SKILL.md +148 -0
- package/bundles/codex/.codex/skills/data/db-diagram/scripts/sql_to_graph.py +1212 -0
- package/bundles/codex/.codex/skills/devops/ci-cd-pipeline/SKILL.md +315 -0
- package/bundles/codex/.codex/skills/devops/gh-cli/SKILL.md +195 -0
- package/bundles/codex/.codex/skills/devops/git-commit/SKILL.md +285 -0
- package/bundles/codex/.codex/skills/devops/using-git-worktrees/SKILL.md +217 -0
- package/bundles/codex/.codex/skills/devops/windows-deployment/SKILL.md +880 -0
- package/bundles/codex/.codex/skills/docs/code-documentation/SKILL.md +271 -0
- package/bundles/codex/.codex/skills/docs/explainer-doc/SKILL.md +97 -0
- package/bundles/codex/.codex/skills/docs/technical-writing/SKILL.md +286 -0
- package/bundles/codex/.codex/skills/docs/writing-plans/SKILL.md +387 -0
- package/bundles/codex/.codex/skills/frontend/css-architecture/SKILL.md +305 -0
- package/bundles/codex/.codex/skills/frontend/css-architecture/references/RESPONSIVE-DESIGN.md +604 -0
- package/bundles/codex/.codex/skills/frontend/frontend-design/SKILL.md +193 -0
- package/bundles/codex/.codex/skills/frontend/frontend-design/references/my-tech-stack.md +127 -0
- package/bundles/codex/.codex/skills/frontend/mockup/SKILL.md +242 -0
- package/bundles/codex/.codex/skills/frontend/react-best-practices/SKILL.md +269 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/README.md +404 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/SKILL.md +459 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/examples/generic-components.md +579 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/examples/server-components.md +579 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/references/event-handlers.md +574 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/references/hooks.md +456 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/references/react-19-patterns.md +638 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/references/react-router.md +1002 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/references/tanstack-router.md +587 -0
- package/bundles/codex/.codex/skills/frontend/ui-review/SKILL.md +176 -0
- package/bundles/codex/.codex/skills/frontend/ui-review/references/framework-fixes.md +471 -0
- package/bundles/codex/.codex/skills/frontend/ui-review/references/visual-checklist.md +236 -0
- package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/DESIGN_TOKENS.md +487 -0
- package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/IMPLEMENTATION_GUIDE.md +177 -0
- package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/SKILL.md +732 -0
- package/bundles/codex/.codex/skills/media/draw-io/SKILL.md +162 -0
- package/bundles/codex/.codex/skills/media/draw-io/references/aws-icons.md +677 -0
- package/bundles/codex/.codex/skills/media/draw-io/references/layout-guidelines.md +142 -0
- package/bundles/codex/.codex/skills/media/draw-io/references/troubleshooting.md +118 -0
- package/bundles/codex/.codex/skills/media/draw-io/references/workflows.md +103 -0
- package/bundles/codex/.codex/skills/media/draw-io/scripts/convert-drawio-to-png.sh +25 -0
- package/bundles/codex/.codex/skills/media/draw-io/scripts/find_aws_icon.py +79 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/SKILL.md +292 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/ENHANCEMENTS.md +264 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/MERMAID-SUMMARY.md +137 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/advanced-features.md +556 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/architecture-diagrams.md +192 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/c4-diagrams.md +410 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/class-diagrams.md +361 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/erd-diagrams.md +510 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/flowcharts.md +450 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/sequence-diagrams.md +394 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/troubleshooting.md +335 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/workflows.md +418 -0
- package/bundles/codex/.codex/skills/media/particle-art/SKILL.md +243 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/canvas-utils.ts +171 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/dot-field.template.tsx +203 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/flow-field.template.tsx +263 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/node-shape.template.tsx +261 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/shape-sampler.ts +281 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/stipple-morph.template.tsx +167 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/stipple.template.tsx +175 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/trail-ghost.template.tsx +266 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/usage-examples.md +320 -0
- package/bundles/codex/.codex/skills/testing/playwright/API_REFERENCE.md +653 -0
- package/bundles/codex/.codex/skills/testing/playwright/SKILL.md +454 -0
- package/bundles/codex/.codex/skills/testing/playwright/lib/helpers.js +441 -0
- package/bundles/codex/.codex/skills/testing/playwright/package.json +26 -0
- package/bundles/codex/.codex/skills/testing/playwright/run.js +228 -0
- package/bundles/codex/.codex/skills/testing/tdd-workflow/SKILL.md +188 -0
- package/bundles/codex/.codex/skills/testing/test-strategy/SKILL.md +155 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/LICENSE.txt +202 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/SKILL.md +97 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/examples/console_logging.py +35 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/examples/element_discovery.py +40 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/examples/static_html_automation.py +33 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/scripts/with_server.py +106 -0
- package/bundles/codex/.codex/skills/workflow/advisory-hub/SKILL.md +227 -0
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/context-template.md +214 -0
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/hub-handbook.md +233 -0
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/report-contract.md +94 -0
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/succession.md +118 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/SKILL.md +500 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/best-practices-referencer.md +263 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/codebase-context-builder.md +326 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/task-intent-analyzer.md +245 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/references/anti-patterns.md +571 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/references/before-after-examples.md +1114 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/references/best-practices-guide.md +513 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/references/common-workflows.md +692 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/references/prompt-patterns.md +547 -0
- package/bundles/codex/.codex/skills/workflow/brainstorming/SKILL.md +57 -0
- package/bundles/codex/.codex/skills/workflow/context-curator/SKILL.md +157 -0
- package/bundles/codex/.codex/skills/workflow/golden-plan/SKILL.md +586 -0
- package/bundles/codex/.codex/skills/workflow/preflight/SKILL.md +440 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/LICENSE.txt +202 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/SKILL.md +491 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/agents/analyzer.md +274 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/agents/comparator.md +202 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/agents/grader.md +223 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/assets/eval_review.html +146 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/eval-viewer/generate_review.py +471 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/eval-viewer/viewer.html +1325 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/references/schemas.md +430 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/__init__.py +0 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/generate_report.py +326 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/improve_description.py +247 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/package_skill.py +136 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/quick_validate.py +103 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/run_eval.py +310 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/run_loop.py +328 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/utils.py +47 -0
- package/bundles/codex/AGENTS.md +146 -0
- package/bundles/manifest.json +4 -3
- package/dist/claude-deepseek.js +544 -0
- package/dist/claude-glm.js +544 -0
- package/dist/claude-oss.js +544 -0
- package/dist/cli.js +542 -90
- package/package.json +5 -2
- package/bundles/antigravity-plugin/statusline-command-agy.sh +0 -91
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/DMMono-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +0 -94
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Gloock-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Italiana-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +0 -93
- 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()
|