codemble 0.4.0__tar.gz → 0.5.0__tar.gz

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 (220) hide show
  1. {codemble-0.4.0 → codemble-0.5.0}/CHANGELOG.md +76 -0
  2. {codemble-0.4.0 → codemble-0.5.0}/CLAUDE.md +60 -4
  3. {codemble-0.4.0 → codemble-0.5.0}/PKG-INFO +15 -4
  4. {codemble-0.4.0 → codemble-0.5.0}/README.md +14 -3
  5. {codemble-0.4.0 → codemble-0.5.0}/TESTING.md +2 -2
  6. {codemble-0.4.0 → codemble-0.5.0}/codemble/__init__.py +1 -1
  7. codemble-0.5.0/codemble/adapters/parse_progress.py +99 -0
  8. {codemble-0.4.0 → codemble-0.5.0}/codemble/adapters/project.py +50 -16
  9. {codemble-0.4.0 → codemble-0.5.0}/codemble/adapters/python_ast.py +23 -5
  10. {codemble-0.4.0 → codemble-0.5.0}/codemble/adapters/typescript_tree_sitter.py +4 -1
  11. {codemble-0.4.0 → codemble-0.5.0}/codemble/checks/service.py +130 -106
  12. {codemble-0.4.0 → codemble-0.5.0}/codemble/llm/study.py +3 -2
  13. codemble-0.5.0/codemble/paths.py +27 -0
  14. {codemble-0.4.0 → codemble-0.5.0}/codemble/progress/store.py +16 -10
  15. {codemble-0.4.0 → codemble-0.5.0}/codemble/server/app.py +171 -23
  16. codemble-0.5.0/codemble/server/parse_job.py +153 -0
  17. {codemble-0.4.0 → codemble-0.5.0}/codemble/server/runtime.py +66 -1
  18. codemble-0.4.0/codemble/web_dist/assets/index-DKTfLRbf.js → codemble-0.5.0/codemble/web_dist/assets/index-BlkEA_-c.js +1 -1
  19. codemble-0.4.0/codemble/web_dist/assets/index-BHcmQpjd.css → codemble-0.5.0/codemble/web_dist/assets/index-xOQxhaMB.css +1 -1
  20. {codemble-0.4.0 → codemble-0.5.0}/codemble/web_dist/index.html +2 -2
  21. codemble-0.5.0/docs/releases/v0.5.0.md +94 -0
  22. {codemble-0.4.0 → codemble-0.5.0}/docs-site/astro.config.mjs +1 -0
  23. {codemble-0.4.0 → codemble-0.5.0}/docs-site/package.json +1 -1
  24. codemble-0.5.0/docs-site/public/shots/loading.png +0 -0
  25. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/checks-and-lighting.md +7 -0
  26. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/installation.md +4 -3
  27. codemble-0.5.0/docs-site/src/content/docs/progress/m13-scale.md +53 -0
  28. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/quickstart.md +3 -3
  29. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/the-galaxy.md +12 -0
  30. {codemble-0.4.0 → codemble-0.5.0}/pyproject.toml +1 -1
  31. codemble-0.5.0/tests/conftest.py +57 -0
  32. codemble-0.5.0/tests/fixtures/check_suites.json +1240 -0
  33. codemble-0.5.0/tests/fixtures/impact/alpha.py +6 -0
  34. codemble-0.5.0/tests/fixtures/impact/beta.py +5 -0
  35. codemble-0.5.0/tests/fixtures/impact/gamma.py +5 -0
  36. codemble-0.5.0/tests/fixtures/impact/helpers.py +6 -0
  37. codemble-0.5.0/tests/fixtures/impact/noisy.py +9 -0
  38. {codemble-0.4.0 → codemble-0.5.0}/tests/test_checks.py +97 -20
  39. codemble-0.5.0/tests/test_parse_job.py +167 -0
  40. codemble-0.5.0/tests/test_parse_progress.py +134 -0
  41. {codemble-0.4.0 → codemble-0.5.0}/tests/test_progress.py +25 -0
  42. {codemble-0.4.0 → codemble-0.5.0}/tests/test_project_parser.py +29 -0
  43. {codemble-0.4.0 → codemble-0.5.0}/tests/test_python_ast.py +45 -0
  44. {codemble-0.4.0 → codemble-0.5.0}/tests/test_server.py +563 -17
  45. codemble-0.5.0/tests/test_smoke.py +214 -0
  46. {codemble-0.4.0 → codemble-0.5.0}/tests/test_study.py +52 -0
  47. {codemble-0.4.0 → codemble-0.5.0}/web/package-lock.json +2 -2
  48. {codemble-0.4.0 → codemble-0.5.0}/web/package.json +1 -1
  49. {codemble-0.4.0 → codemble-0.5.0}/web/scripts/check_graph_data.mjs +85 -0
  50. {codemble-0.4.0 → codemble-0.5.0}/web/scripts/check_learner_session.mjs +292 -8
  51. {codemble-0.4.0 → codemble-0.5.0}/web/src/App.jsx +368 -19
  52. {codemble-0.4.0 → codemble-0.5.0}/web/src/GalaxyCanvas.jsx +7 -0
  53. {codemble-0.4.0 → codemble-0.5.0}/web/src/GuidanceLayer.jsx +27 -2
  54. {codemble-0.4.0 → codemble-0.5.0}/web/src/MapView.jsx +32 -6
  55. {codemble-0.4.0 → codemble-0.5.0}/web/src/graphData.js +42 -0
  56. {codemble-0.4.0 → codemble-0.5.0}/web/src/learnerSession.js +202 -2
  57. {codemble-0.4.0 → codemble-0.5.0}/web/src/styles.css +282 -0
  58. codemble-0.4.0/tests/conftest.py +0 -30
  59. codemble-0.4.0/tests/test_smoke.py +0 -75
  60. {codemble-0.4.0 → codemble-0.5.0}/.env.example +0 -0
  61. {codemble-0.4.0 → codemble-0.5.0}/.gitattributes +0 -0
  62. {codemble-0.4.0 → codemble-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  63. {codemble-0.4.0 → codemble-0.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  64. {codemble-0.4.0 → codemble-0.5.0}/.github/ISSUE_TEMPLATE/early_tester.yml +0 -0
  65. {codemble-0.4.0 → codemble-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  66. {codemble-0.4.0 → codemble-0.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  67. {codemble-0.4.0 → codemble-0.5.0}/.github/dependabot.yml +0 -0
  68. {codemble-0.4.0 → codemble-0.5.0}/.github/workflows/ci.yml +0 -0
  69. {codemble-0.4.0 → codemble-0.5.0}/.github/workflows/pages.yml +0 -0
  70. {codemble-0.4.0 → codemble-0.5.0}/.github/workflows/publish-pypi.yml +0 -0
  71. {codemble-0.4.0 → codemble-0.5.0}/.gitignore +0 -0
  72. {codemble-0.4.0 → codemble-0.5.0}/AGENTS.md +0 -0
  73. {codemble-0.4.0 → codemble-0.5.0}/CODE_OF_CONDUCT.md +0 -0
  74. {codemble-0.4.0 → codemble-0.5.0}/CONTRIBUTING.md +0 -0
  75. {codemble-0.4.0 → codemble-0.5.0}/LICENSE +0 -0
  76. {codemble-0.4.0 → codemble-0.5.0}/NOTICE +0 -0
  77. {codemble-0.4.0 → codemble-0.5.0}/SECURITY.md +0 -0
  78. {codemble-0.4.0 → codemble-0.5.0}/assets/demo.gif +0 -0
  79. {codemble-0.4.0 → codemble-0.5.0}/codemble/adapters/__init__.py +0 -0
  80. {codemble-0.4.0 → codemble-0.5.0}/codemble/adapters/base.py +0 -0
  81. {codemble-0.4.0 → codemble-0.5.0}/codemble/adapters/discovery.py +0 -0
  82. {codemble-0.4.0 → codemble-0.5.0}/codemble/checks/__init__.py +0 -0
  83. {codemble-0.4.0 → codemble-0.5.0}/codemble/cli.py +0 -0
  84. {codemble-0.4.0 → codemble-0.5.0}/codemble/graph/__init__.py +0 -0
  85. {codemble-0.4.0 → codemble-0.5.0}/codemble/graph/finalize.py +0 -0
  86. {codemble-0.4.0 → codemble-0.5.0}/codemble/graph/layout.py +0 -0
  87. {codemble-0.4.0 → codemble-0.5.0}/codemble/graph/mapview.py +0 -0
  88. {codemble-0.4.0 → codemble-0.5.0}/codemble/lens/__init__.py +0 -0
  89. {codemble-0.4.0 → codemble-0.5.0}/codemble/lens/javascript_typescript.py +0 -0
  90. {codemble-0.4.0 → codemble-0.5.0}/codemble/lens/python.py +0 -0
  91. {codemble-0.4.0 → codemble-0.5.0}/codemble/llm/__init__.py +0 -0
  92. {codemble-0.4.0 → codemble-0.5.0}/codemble/llm/local_status.py +0 -0
  93. {codemble-0.4.0 → codemble-0.5.0}/codemble/llm/providers.py +0 -0
  94. {codemble-0.4.0 → codemble-0.5.0}/codemble/llm/structural.py +0 -0
  95. {codemble-0.4.0 → codemble-0.5.0}/codemble/progress/__init__.py +0 -0
  96. {codemble-0.4.0 → codemble-0.5.0}/codemble/server/__init__.py +0 -0
  97. {codemble-0.4.0 → codemble-0.5.0}/codemble/web_dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  98. {codemble-0.4.0 → codemble-0.5.0}/codemble/web_dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  99. {codemble-0.4.0 → codemble-0.5.0}/codemble/web_dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  100. {codemble-0.4.0 → codemble-0.5.0}/codemble/web_dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  101. {codemble-0.4.0 → codemble-0.5.0}/codemble/web_dist/assets/shippori-mincho-latin-500-normal-C-QwvIb3.woff +0 -0
  102. {codemble-0.4.0 → codemble-0.5.0}/codemble/web_dist/assets/shippori-mincho-latin-500-normal-XI1O8euf.woff2 +0 -0
  103. {codemble-0.4.0 → codemble-0.5.0}/codemble/web_dist/assets/shippori-mincho-latin-700-normal-CkoCYOiI.woff +0 -0
  104. {codemble-0.4.0 → codemble-0.5.0}/codemble/web_dist/assets/shippori-mincho-latin-700-normal-DHcmzUO5.woff2 +0 -0
  105. {codemble-0.4.0 → codemble-0.5.0}/codemble/web_dist/assets/zen-kaku-gothic-new-latin-400-normal-BEdayliK.woff2 +0 -0
  106. {codemble-0.4.0 → codemble-0.5.0}/codemble/web_dist/assets/zen-kaku-gothic-new-latin-400-normal-CPSmNJAU.woff +0 -0
  107. {codemble-0.4.0 → codemble-0.5.0}/docs/adr/README.md +0 -0
  108. {codemble-0.4.0 → codemble-0.5.0}/docs/plans/2026-07-19-audience-modes-and-local-narration-design.md +0 -0
  109. {codemble-0.4.0 → codemble-0.5.0}/docs/plans/2026-07-19-audience-modes-backend-plan.md +0 -0
  110. {codemble-0.4.0 → codemble-0.5.0}/docs/plans/2026-07-19-audience-modes-frontend-plan.md +0 -0
  111. {codemble-0.4.0 → codemble-0.5.0}/docs/plans/2026-07-19-install-ux-folder-picker-design.md +0 -0
  112. {codemble-0.4.0 → codemble-0.5.0}/docs/plans/2026-07-19-install-ux-folder-picker-plan.md +0 -0
  113. {codemble-0.4.0 → codemble-0.5.0}/docs/plans/README.md +0 -0
  114. {codemble-0.4.0 → codemble-0.5.0}/docs/plans/phase-1.md +0 -0
  115. {codemble-0.4.0 → codemble-0.5.0}/docs/releases/checklist.md +0 -0
  116. {codemble-0.4.0 → codemble-0.5.0}/docs/releases/v0.1.0.md +0 -0
  117. {codemble-0.4.0 → codemble-0.5.0}/docs/releases/v0.2.0.md +0 -0
  118. {codemble-0.4.0 → codemble-0.5.0}/docs/releases/v0.4.0.md +0 -0
  119. {codemble-0.4.0 → codemble-0.5.0}/docs/research/README.md +0 -0
  120. {codemble-0.4.0 → codemble-0.5.0}/docs/superpowers/plans/2026-07-19-galaxy-ux-phase-a.md +0 -0
  121. {codemble-0.4.0 → codemble-0.5.0}/docs/superpowers/plans/2026-07-19-galaxy-ux-phase-b.md +0 -0
  122. {codemble-0.4.0 → codemble-0.5.0}/docs/superpowers/plans/2026-07-19-galaxy-ux-phase-c.md +0 -0
  123. {codemble-0.4.0 → codemble-0.5.0}/docs/superpowers/plans/2026-07-19-galaxy-ux-shared-contract.md +0 -0
  124. {codemble-0.4.0 → codemble-0.5.0}/docs/superpowers/specs/2026-07-19-galaxy-ux-overhaul-design.md +0 -0
  125. {codemble-0.4.0 → codemble-0.5.0}/docs-site/.gitignore +0 -0
  126. {codemble-0.4.0 → codemble-0.5.0}/docs-site/design.md +0 -0
  127. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/favicon-16.png +0 -0
  128. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/favicon-32.png +0 -0
  129. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/icon-180.png +0 -0
  130. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/icon-192.png +0 -0
  131. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/icon-512.png +0 -0
  132. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/icon.svg +0 -0
  133. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/icons/asterism.svg +0 -0
  134. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/icons/install.svg +0 -0
  135. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/mark-animated.svg +0 -0
  136. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/mark.svg +0 -0
  137. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/plates/hero-chart.svg +0 -0
  138. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/plates/hero-field.svg +0 -0
  139. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/plates/hero-gold.svg +0 -0
  140. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/plates/kasumi-rule.svg +0 -0
  141. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/plates/plate-galaxy.svg +0 -0
  142. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/plates/plate-study.svg +0 -0
  143. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/plates/plate-system.svg +0 -0
  144. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/brand/plates/seal.svg +0 -0
  145. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/favicon.svg +0 -0
  146. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/shots/easy-mode.png +0 -0
  147. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/shots/galaxy-lit.png +0 -0
  148. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/shots/galaxy.png +0 -0
  149. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/shots/map-architecture.png +0 -0
  150. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/shots/map-workflow.png +0 -0
  151. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/shots/study-panel.png +0 -0
  152. {codemble-0.4.0 → codemble-0.5.0}/docs-site/public/shots/system.png +0 -0
  153. {codemble-0.4.0 → codemble-0.5.0}/docs-site/scripts/build-plates.mjs +0 -0
  154. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/assets/codemble-mark-dark.svg +0 -0
  155. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/assets/codemble-mark-light.svg +0 -0
  156. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/components/Search.astro +0 -0
  157. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/components/Tatebanko.astro +0 -0
  158. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/architecture.md +0 -0
  159. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/build-from-source.md +0 -0
  160. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/contributing.md +0 -0
  161. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/correctness.md +0 -0
  162. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/early-testing.md +0 -0
  163. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/introduction.md +0 -0
  164. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/progress/m1-parser.md +0 -0
  165. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/progress/m10-polyglot-release.md +0 -0
  166. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/progress/m12-galaxy-look.md +0 -0
  167. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/progress/m2-galaxy.md +0 -0
  168. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/progress/m3-study.md +0 -0
  169. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/progress/m4-lens.md +0 -0
  170. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/progress/m5-checks.md +0 -0
  171. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/progress/m6-release.md +0 -0
  172. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/progress/m8-typescript.md +0 -0
  173. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/progress/m9-typescript-lens.md +0 -0
  174. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/roadmap.md +0 -0
  175. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/star-chart.md +0 -0
  176. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content/docs/study-panel.md +0 -0
  177. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/content.config.ts +0 -0
  178. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/pages/index.astro +0 -0
  179. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/styles/custom.css +0 -0
  180. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/styles/landing.css +0 -0
  181. {codemble-0.4.0 → codemble-0.5.0}/docs-site/src/styles/tokens.css +0 -0
  182. {codemble-0.4.0 → codemble-0.5.0}/scripts/dev.sh +0 -0
  183. {codemble-0.4.0 → codemble-0.5.0}/scripts/record_demo.sh +0 -0
  184. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/concepts_sample.py +0 -0
  185. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/polyglot/python_worker.py +0 -0
  186. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/polyglot/src/broken.ts +0 -0
  187. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/polyglot/src/legacy.js +0 -0
  188. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/polyglot/src/local.js +0 -0
  189. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/polyglot/src/main.ts +0 -0
  190. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/polyglot/src/reexport.ts +0 -0
  191. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/polyglot/src/util.ts +0 -0
  192. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/polyglot/src/widget.tsx +0 -0
  193. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/repeated_calls.py +0 -0
  194. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/.gitignore +0 -0
  195. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/ambiguous.py +0 -0
  196. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/api.py +0 -0
  197. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/app.py +0 -0
  198. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/broken.py +0 -0
  199. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/cli.py +0 -0
  200. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/pkg/__init__.py +0 -0
  201. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/pkg/helpers.py +0 -0
  202. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/pkg/service.py +0 -0
  203. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/pkg/util.py +0 -0
  204. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/runner/__main__.py +0 -0
  205. {codemble-0.4.0 → codemble-0.5.0}/tests/fixtures/sampleproj/shared.py +0 -0
  206. {codemble-0.4.0 → codemble-0.5.0}/tests/test_graph_finalization.py +0 -0
  207. {codemble-0.4.0 → codemble-0.5.0}/tests/test_mapview.py +0 -0
  208. {codemble-0.4.0 → codemble-0.5.0}/tests/test_providers.py +0 -0
  209. {codemble-0.4.0 → codemble-0.5.0}/tests/test_structural.py +0 -0
  210. {codemble-0.4.0 → codemble-0.5.0}/tests/test_typescript_tree_sitter.py +0 -0
  211. {codemble-0.4.0 → codemble-0.5.0}/web/README.md +0 -0
  212. {codemble-0.4.0 → codemble-0.5.0}/web/index.html +0 -0
  213. {codemble-0.4.0 → codemble-0.5.0}/web/scripts/record_demo.mjs +0 -0
  214. {codemble-0.4.0 → codemble-0.5.0}/web/src/ModeControl.jsx +0 -0
  215. {codemble-0.4.0 → codemble-0.5.0}/web/src/StudyPanel.jsx +0 -0
  216. {codemble-0.4.0 → codemble-0.5.0}/web/src/galaxyEffects.js +0 -0
  217. {codemble-0.4.0 → codemble-0.5.0}/web/src/galaxyMaterials.js +0 -0
  218. {codemble-0.4.0 → codemble-0.5.0}/web/src/main.jsx +0 -0
  219. {codemble-0.4.0 → codemble-0.5.0}/web/src/tokens.css +0 -0
  220. {codemble-0.4.0 → codemble-0.5.0}/web/vite.config.js +0 -0
@@ -5,6 +5,82 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.5.0] - 2026-07-20
9
+
10
+ ### Added
11
+ - Large projects now show a staged loading screen instead of a frozen tab.
12
+ Parsing runs on a worker thread; the app polls `GET /api/picker/progress` and
13
+ names the stage it is in — discovering, parsing, resolving, checks, layout —
14
+ with a real file count while files are read, and a narrated sub-step
15
+ (resolving imports, resolving calls, building the galaxy map, composing the
16
+ project) once the parse moves past counting files. A failed parse reports
17
+ the parser's own message and offers an in-app retry; cancelling stops the
18
+ parse at the next file boundary. A realistic 1,000-file Python project
19
+ parses and renders an interactive galaxy end to end; the parse itself is
20
+ 1.56x faster after removing a parser hotspot (below), and the one-pass check
21
+ index builds a full 1,000-region suite in under a second.
22
+ - The over-cap prompt is actionable in-app: the busiest subdirectories are
23
+ buttons that navigate the picker, and a typed path field accepts any folder
24
+ inside your home directory. Outside-home paths are still refused.
25
+ - A **Clear this project's progress** control on the star chart, behind a
26
+ confirmation, scoped to the open project only.
27
+ - A non-interactive `codemble` run now prints the busiest-scope suggestions
28
+ with the scale refusal instead of a bare message.
29
+
30
+ ### Changed
31
+ - The scale cap moved from 300 to 1,000 supported source files. LOD and
32
+ clustering remain Phase 2 work.
33
+ - `POST /api/picker/select` returns `202 {"state": "parsing"}` instead of
34
+ blocking until the graph is ready.
35
+ - `CODEMBLE_DATA_DIR` now relocates **everything** Codemble keeps under your home
36
+ directory — saved progress, the narration cache, and the `config` file — rather
37
+ than progress alone. All three resolve through one helper, so pointing the
38
+ variable somewhere else moves them together. Unset, the default is unchanged:
39
+ `~/.codemble`.
40
+ - Language focus now also projects onto the **2D Map**, matching the galaxy: a
41
+ focused language keeps its boxes, rows, and edges while the others are dropped,
42
+ and the survivors keep their backend-computed coordinates. The projection
43
+ happens in the frontend and never re-lays-out or mutates the immutable graph,
44
+ so focus finally means the same thing on both layers.
45
+ - The no-WebGL message now points the learner to the Map/Diagram layer, which
46
+ draws the same parser evidence as plain SVG and is one switch away, instead of
47
+ only stating that the galaxy needs WebGL.
48
+
49
+ ### Fixed
50
+ - Check generation walked every graph edge up to four times per region, and
51
+ rebuilt the node lookup and the option pool per region — an O(regions × graph)
52
+ freeze at bind. One index is now built per bind in a single pass, measured
53
+ ~16x faster at 1,000 files. A committed golden fixture proves the generated
54
+ suites and answers are byte-identical for the Python and mixed fixtures.
55
+ - `GET /api/graph` and `GET /api/map` re-hydrated progress and re-sorted every
56
+ node and edge on every request. The serialized document is cached and
57
+ invalidated on region light-up, Home selection, and project bind or reset —
58
+ measured ~25-26x faster warm.
59
+ - The Python adapter resolved a node's owning module with an O(definitions ×
60
+ modules) scan; it now walks the node id's own dotted prefixes in O(depth),
61
+ a further 1.56x on total parse wall-clock. Output is byte-identical.
62
+ - The test suite no longer reads the developer's `~/.codemble/config` or their
63
+ `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`. Every server test that built an app
64
+ without an explicit study service inherited whatever the machine had
65
+ configured, so the same test could construct a live provider locally and none
66
+ in CI. The two tests that request `/explanation` would then make a real, billed
67
+ API call and cache the reply under the developer's home directory.
68
+ - First-run onboarding assumed the learner was on the galaxy, but Easy — the
69
+ default audience — opens on the 2D Map. The coach-marks and the footer control
70
+ hint now key on the active layer, so a learner on the Map reads map guidance
71
+ ("click a box or row to study, switch tabs") instead of galaxy scroll/camera
72
+ controls that are not on screen.
73
+ - The audience-mode gate and the first-run coach-marks no longer open as two
74
+ stacked modals on a genuine first run; the coach-marks wait until a mode has
75
+ been chosen, so the gate resolves first.
76
+ - With no parser-recognisable entrypoint the "Change Home" control is not
77
+ rendered, so both Map tabs now state that reason instead of pointing at a
78
+ button that is not there. The has-candidates-but-unselected copy is unchanged.
79
+ - A project bind slow enough to outlast a parse cancellation could commit after
80
+ the picker had already been reset, resurrecting the just-released project or
81
+ clobbering the next selection. The commit now re-checks cancellation under the
82
+ same lock that performs it, so a stale bind can never rebind a released project.
83
+
8
84
  ## [0.4.0] - 2026-07-20
9
85
 
10
86
  The galaxy read as flat spheres on a plain background, connections were hard to
@@ -194,9 +194,43 @@ switching, then the "living cosmos" visual overhaul with a 2D Map layer, then
194
194
  the approved Non-Goal and binding relaxations. Phase A (the narration/mode/
195
195
  connections surface and project switching) and Phase B (M12: call-depth
196
196
  orbits, the 2D Map layer, and the living-cosmos visual overhaul) have both
197
- since shipped. Phase C (~1,000-file scale with staged parse progress) has not
198
- started there is still no threaded parse or parse-progress screen, and the
199
- scale cap remains ~300 files.
197
+ since shipped, and were released together as **v0.4.0** (tag `v0.4.0`, published
198
+ to PyPI, verified end to end from a clean `uvx codemble==0.4.0` install: the
199
+ wheel's SPA bundle is byte-identical to the tag, all 27 regions draw unclipped,
200
+ and the galaxy renders deep space with no console errors). Phase C (M13:
201
+ ~1,000-file scale with staged parse progress) has since shipped from that
202
+ plan: parsing now runs on a worker thread behind a `202`-accepted picker
203
+ select and a polled `GET /api/picker/progress` through five honest stages,
204
+ cancellation is checked between files and a crashed worker reports as an
205
+ in-app error rather than a hung server, the scale cap moved 300 → 1,000 with
206
+ the over-cap prompt offering clickable busiest scopes plus a home-jailed typed
207
+ path, a one-pass check index replaced the per-region edge scans
208
+ (byte-identical suites, pinned by a golden fixture before the refactor),
209
+ `/api/graph` and `/api/map` responses are now cached with invalidation on
210
+ light-up, Home change, and binding, and a Clear this project's progress
211
+ control was added to the star chart. A dedicated verification pass at a
212
+ realistic ~1,000-file project then found the `resolving` stage — the slowest
213
+ one — showed no moving signal for most of the wait; the fix narrates its real
214
+ sub-steps instead of leaving the screen static, and a parser hotspot found
215
+ alongside it (an O(definitions × modules) module-resolution scan in the Python
216
+ adapter) was fixed too, together taking real parse wall-clock on a 1,000-file
217
+ Python project from roughly 11.5s to roughly 7.5s with byte-identical output.
218
+ Suite hermeticity was also closed on the read side: `CODEMBLE_DATA_DIR` now
219
+ relocates the narration cache and the `config` file as well as saved progress
220
+ through one `codemble/paths.py` helper, and the test suite clears every
221
+ provider variable `from_environment` reads, so a server test can no longer make
222
+ a real billed API call against a developer's exported key. A pre-release
223
+ re-audit then closed a cluster of first-run gaps that converged on the
224
+ Easy-default learner (who lands on the 2D Map): the coach-marks and footer now
225
+ teach the layer the learner is actually on, the audience gate and coach-marks
226
+ no longer stack as two modals, the no-entrypoint Map tabs stop pointing at a
227
+ Change Home button that isn't shown, language focus now filters the Map as a
228
+ frontend projection, and a parse `bind` that outlasts a cancel can no longer
229
+ rebind a released project. Phase C plus that gap-fix wave shipped as **v0.5.0**;
230
+ the parse work collided with an independent implementation of the same three
231
+ foundational commits on `main`, reconciled by taking the branch's verified
232
+ superset while preserving main's unique `CODEMBLE_DATA_DIR`/config-isolation
233
+ fix, which lived in files the branch never touched.
200
234
 
201
235
  ### M0 — Repo, docs & website scaffold ✅ (2026-07-19)
202
236
  - [x] Root: README, LICENSE (Apache-2.0), CoC, SECURITY, CONTRIBUTING,
@@ -344,6 +378,24 @@ hash file content, never coordinates, so the orbit relayout did not re-dim any
344
378
  region; reduced motion always yields the finished lit state with zero
345
379
  animation.
346
380
 
381
+ ### M13 — Galaxy UX Phase C: scale ✅ (2026-07-20)
382
+ - [x] Threaded parse behind `202` select, `GET /api/picker/progress`, and a
383
+ staged loading screen with real file counts
384
+ - [x] Cancellation checked between files; a crashed worker becomes an error
385
+ state, never a hung server
386
+ - [x] Scale cap 300 → 1,000; clickable busiest scopes plus a jailed path field;
387
+ suggestions in the non-TTY CLI refusal
388
+ - [x] One per-bind check index replacing the per-region edge scans, pinned by a
389
+ golden suite fixture
390
+ - [x] Cached `/api/graph` and `/api/map` documents invalidated on light-up,
391
+ Home, and binding
392
+ - [x] Terminal stage lines for `codemble <path>`; reset-progress control
393
+
394
+ **Acceptance:** a ~1,000-file project parses with live progress and reaches an
395
+ interactive galaxy; re-fetching the graph does not re-sort the world; the scale
396
+ prompt is actionable entirely in-app; generated check suites are byte-identical
397
+ to before the index change.
398
+
347
399
  ## Decision Log **[AGENT-MAINTAINED — append only]**
348
400
 
349
401
  | Date | Decision | Why |
@@ -406,6 +458,10 @@ animation.
406
458
  | 2026-07-20 | Bloom resolution is capped with `composer.setPixelRatio(1)`, not the `UnrealBloomPass` constructor | `EffectComposer.setSize` forwards the canvas size to every pass on resize, overwriting the constructor's `resolution`. The pixel ratio is the cap that survives |
407
459
  | 2026-07-20 | **Corrects the row above**: bloom is capped by wrapping the bloom pass's own `setSize`, and the composer keeps the renderer's pixel ratio | The pixel ratio *did* cap bloom, but `EffectComposer.setSize` multiplies it into `renderTarget1/2` and every pass, so the whole scene rendered at 1x and upscaled — measured 1280x611 scene passes on a 2560x1221 buffer at dpr 2. Wrapping the one pass caps the one expensive thing: scene now 2560x1221, bloom mip 0 800x382 (1280x611 uncapped), `?benchmark` at 951 nodes unchanged at 928.8 → 961.5 fps median |
408
460
  | 2026-07-20 | **Corrects "binding is one-shot"** (2026-07-19 picker row): binding is one-*at-a-time*. `serve_project` attaches `PickerConfig(browse_root=Path.home())` too, so a `codemble <path>` run also exposes the picker endpoints after a reset, and browse then enumerates non-hidden directories under `$HOME` | The Switch project control has to work without a process restart, which is what that config is for — but the earlier row still claimed a permanent 409 for the path-opened flow, and this file is the source of truth. The home jail and the Host-header allowlist are unchanged; only the "one-shot" claim was false. An app built with no `PickerConfig` at all remains genuinely one-shot and refuses reset |
461
+ | 2026-07-20 | `CODEMBLE_DATA_DIR` owns every home-directory path — progress, the narration cache, and the `config` file — through one `codemble/paths.py` helper; the test suite additionally clears every provider variable `StudyService.from_environment` reads | The variable redirected progress only, while `StudyService` hardcoded `Path.home()` for the other two, so `create_app`'s default study service read the developer's real config and `ANTHROPIC_API_KEY`. Two server tests GET `/explanation` and assert only that a `status` key came back — true of `no_key`, `ready`, and `error` alike — so on a machine with a key they made a real billed API call and cached the reply under the developer's home while still passing. Redirecting the directory does nothing about the process environment, which is why the suite must clear the keys as well. No new variable is introduced, the default stays `~/.codemble`, and explicit `environ`/`config_path`/`cache_root` arguments still win over both channels |
462
+ | 2026-07-20 | Progress reporting is a thread-scoped per-file hook (`note_file_parsed`) bound by `ProjectParser`, not a new `LanguageAdapter` parameter | The public adapter seam must stay unchanged for Phase 2 languages; one hook site per adapter also gives cancellation its exact "between files" meaning |
463
+ | 2026-07-20 | Phase C adds `DELETE /api/progress`, the `CLEAR_PROGRESS` session event, and a `clearProgress` adapter method beyond the shared contract's Phase C rows | The contract's Phase C rows covered parse progress only, while the no-reset-progress-control gap is mapped to Phase C by the spec; recorded here rather than silently widened |
464
+ | 2026-07-20 | Generated check suites are pinned by a committed golden fixture before any performance work touches `checks/service.py` | The Correctness Contract makes suite drift top-severity, and a refactor that changes an answer is invisible without a byte-level pin |
409
465
 
410
466
  ## Non-Goals — do NOT build (point here when asked)
411
467
 
@@ -446,7 +502,7 @@ animation.
446
502
 
447
503
  ## Edge cases & limits
448
504
 
449
- - >~300 supported source files → prompt to scope to a subdirectory (LOD arrives Phase 2)
505
+ - >~1,000 supported source files → prompt to scope to a subdirectory (LOD arrives Phase 2)
450
506
  - No clear entrypoint → ranked candidates; user picks Home
451
507
  - Syntax errors / partial parses → parse what you can, flag the rest, never crash
452
508
  - Missing/invalid key → galaxy + structure + checks work; explanations show "add your key"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codemble
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: A learning game that turns the code AI wrote for you into a galaxy you light up by understanding it.
5
5
  Project-URL: Homepage, https://udhawan97.github.io/Codemble/
6
6
  Project-URL: Repository, https://github.com/udhawan97/Codemble
@@ -164,6 +164,17 @@ of your own code becomes a sky you understand.
164
164
  and this one has no model configured at all.
165
165
  </sub></p>
166
166
 
167
+ <p align="center">
168
+ <img src="https://github.com/udhawan97/Codemble/raw/main/docs-site/public/shots/loading.png" alt="Codemble's staged loading screen mapping a large project, with five named stages — finding source files, reading each file, connecting imports and calls, building graph-only checks, placing your galaxy — a live count reading 13 of 900 files, and a cancel button" width="900">
169
+ </p>
170
+
171
+ <p align="center"><sub>
172
+ Large projects (up to roughly 1,000 source files) parse in the background with
173
+ visible staged progress — a real file count while reading, named steps while
174
+ resolving — so a big project never looks like a frozen tab. Cancel any time to
175
+ pick another.
176
+ </sub></p>
177
+
167
178
  ## Two layers over one graph
168
179
 
169
180
  Codemble draws the same parser evidence two ways, switchable in the header. The
@@ -259,9 +270,9 @@ game; only the optional prose narration is unavailable.
259
270
 
260
271
  - **Supported source:** Python 3.11+, JavaScript/JSX, TypeScript/TSX, and mixed
261
272
  projects. Unsupported languages stay outside the graph rather than being guessed.
262
- - **Scale:** above roughly 300 supported source files, choose a subdirectory —
263
- the in-app picker prompts for the scope, or pass
264
- `codemble --path ./project/subdirectory`.
273
+ - **Scale:** above roughly 1,000 supported source files, choose a subdirectory —
274
+ the in-app picker offers the busiest scopes as buttons and accepts a typed
275
+ path, or pass `codemble --path ./project/subdirectory`.
265
276
  - **Ambiguous Home:** choose a parser-ranked entrypoint in the app or pass
266
277
  `--entrypoint NODE_ID`.
267
278
  - **Broken source:** syntax errors remain visible; Codemble maps safe partial
@@ -132,6 +132,17 @@ of your own code becomes a sky you understand.
132
132
  and this one has no model configured at all.
133
133
  </sub></p>
134
134
 
135
+ <p align="center">
136
+ <img src="https://github.com/udhawan97/Codemble/raw/main/docs-site/public/shots/loading.png" alt="Codemble's staged loading screen mapping a large project, with five named stages — finding source files, reading each file, connecting imports and calls, building graph-only checks, placing your galaxy — a live count reading 13 of 900 files, and a cancel button" width="900">
137
+ </p>
138
+
139
+ <p align="center"><sub>
140
+ Large projects (up to roughly 1,000 source files) parse in the background with
141
+ visible staged progress — a real file count while reading, named steps while
142
+ resolving — so a big project never looks like a frozen tab. Cancel any time to
143
+ pick another.
144
+ </sub></p>
145
+
135
146
  ## Two layers over one graph
136
147
 
137
148
  Codemble draws the same parser evidence two ways, switchable in the header. The
@@ -227,9 +238,9 @@ game; only the optional prose narration is unavailable.
227
238
 
228
239
  - **Supported source:** Python 3.11+, JavaScript/JSX, TypeScript/TSX, and mixed
229
240
  projects. Unsupported languages stay outside the graph rather than being guessed.
230
- - **Scale:** above roughly 300 supported source files, choose a subdirectory —
231
- the in-app picker prompts for the scope, or pass
232
- `codemble --path ./project/subdirectory`.
241
+ - **Scale:** above roughly 1,000 supported source files, choose a subdirectory —
242
+ the in-app picker offers the busiest scopes as buttons and accepts a typed
243
+ path, or pass `codemble --path ./project/subdirectory`.
233
244
  - **Ambiguous Home:** choose a parser-ranked entrypoint in the app or pass
234
245
  `--entrypoint NODE_ID`.
235
246
  - **Broken source:** syntax errors remain visible; Codemble maps safe partial
@@ -8,8 +8,8 @@ happened.
8
8
 
9
9
  ## Protect your project
10
10
 
11
- - Use a local project with at most 300 supported source files, or choose a smaller
12
- subdirectory when Codemble prompts (`--path` from the CLI).
11
+ - Use a local project with at most 1,000 supported source files, or choose a
12
+ smaller subdirectory when Codemble prompts (`--path` from the CLI).
13
13
  - Do not paste source code, API keys, credentials, or private project names into
14
14
  feedback. Codemble itself runs locally and has no telemetry.
15
15
  - An LLM key is optional. The galaxy, source, Lens, checks, and lighting work
@@ -1,4 +1,4 @@
1
1
  """Codemble — a learning game that turns the code AI wrote for you into a galaxy
2
2
  you light up by understanding it. See CLAUDE.md for the agent brief and roadmap."""
3
3
 
4
- __version__ = "0.4.0"
4
+ __version__ = "0.5.0"
@@ -0,0 +1,99 @@
1
+ """Per-file parse progress and cancellation, without widening the adapter seam.
2
+
3
+ ``LanguageAdapter`` keeps its four public methods exactly as they are. A
4
+ parse that wants progress binds a hook for its own thread with
5
+ ``reporting_files``; each adapter's private per-file helper calls
6
+ ``note_file_parsed`` once per source file it finishes reading. That single
7
+ call site is also the only cancellation check point, so "between files" means
8
+ exactly one place in each adapter.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import threading
14
+ from collections.abc import Callable, Iterator
15
+ from contextlib import contextmanager
16
+ from typing import Protocol, runtime_checkable
17
+
18
+ _local = threading.local()
19
+
20
+
21
+ class ParseCancelled(Exception):
22
+ """A running parse was cancelled; no graph will be produced."""
23
+
24
+
25
+ @runtime_checkable
26
+ class ParseProgress(Protocol):
27
+ """The reporting surface ``ProjectParser`` writes one parse's state to."""
28
+
29
+ def stage(self, stage: str) -> None:
30
+ """Report the stage now running."""
31
+
32
+ def files_total(self, total: int) -> None:
33
+ """Report how many source files this parse will read."""
34
+
35
+ def file_parsed(self) -> None:
36
+ """Report one finished file; raise ``ParseCancelled`` to stop."""
37
+
38
+ def detail(self, detail: str) -> None:
39
+ """Report the sub-step now running within a stage.
40
+
41
+ The ``resolving`` stage is one label over several seconds of cross-file
42
+ work; a sub-step keeps the screen honestly changing instead of frozen.
43
+ It is advisory only: unlike ``file_parsed`` it never cancels, so a
44
+ reporter that ignores it loses nothing but the finer copy.
45
+ """
46
+
47
+
48
+ @contextmanager
49
+ def reporting_files(on_file: Callable[[], None] | None) -> Iterator[None]:
50
+ """Bind ``on_file`` for this thread for the duration of one parse."""
51
+
52
+ previous = getattr(_local, "on_file", None)
53
+ _local.on_file = on_file
54
+ try:
55
+ yield
56
+ finally:
57
+ _local.on_file = previous
58
+
59
+
60
+ def note_file_parsed() -> None:
61
+ """Report one finished source file; a no-op when nobody is listening."""
62
+
63
+ on_file = getattr(_local, "on_file", None)
64
+ if on_file is not None:
65
+ on_file()
66
+
67
+
68
+ @contextmanager
69
+ def reporting_detail(on_detail: Callable[[str], None] | None) -> Iterator[None]:
70
+ """Bind ``on_detail`` for this thread for the duration of one parse.
71
+
72
+ Separate from ``reporting_files`` because resolving sub-steps outlive the
73
+ file-reading loop: they run through cross-file resolution and composition.
74
+ """
75
+
76
+ previous = getattr(_local, "on_detail", None)
77
+ _local.on_detail = on_detail
78
+ try:
79
+ yield
80
+ finally:
81
+ _local.on_detail = previous
82
+
83
+
84
+ def note_detail(detail: str) -> None:
85
+ """Report the resolving sub-step now running; a no-op with no listener."""
86
+
87
+ on_detail = getattr(_local, "on_detail", None)
88
+ if on_detail is not None:
89
+ on_detail(detail)
90
+
91
+
92
+ __all__ = [
93
+ "ParseCancelled",
94
+ "ParseProgress",
95
+ "note_detail",
96
+ "note_file_parsed",
97
+ "reporting_detail",
98
+ "reporting_files",
99
+ ]
@@ -13,6 +13,12 @@ from codemble.adapters.discovery import (
13
13
  SourceOwnership,
14
14
  discover_project_sources,
15
15
  )
16
+ from codemble.adapters.parse_progress import (
17
+ ParseProgress,
18
+ note_detail,
19
+ reporting_detail,
20
+ reporting_files,
21
+ )
16
22
  from codemble.graph.finalize import GraphFinalizationError, finalize_graph
17
23
 
18
24
 
@@ -26,10 +32,14 @@ class ProjectScaleError(ProjectParseError):
26
32
  def __init__(self, intake: ProjectIntake, scale_cap: int) -> None:
27
33
  self.intake = intake
28
34
  self.scale_cap = scale_cap
35
+ scopes = ", ".join(
36
+ f"{directory} ({count})" for directory, count in intake.scope_counts()[:6]
37
+ )
38
+ suggestion = f" Busiest scopes: {scopes}." if scopes else ""
29
39
  super().__init__(
30
40
  f"found {len(intake.files)} supported source files; Codemble is capped at "
31
41
  f"{scale_cap}. Re-run with `codemble --path PATH` to choose a project "
32
- "subdirectory."
42
+ f"subdirectory.{suggestion}"
33
43
  )
34
44
 
35
45
 
@@ -85,7 +95,9 @@ class ProjectParser:
85
95
 
86
96
  return tuple(adapter.language for adapter in self._adapters)
87
97
 
88
- scale_cap = 300
98
+ # Raised from 300 with the Phase C threaded parse and staged loading
99
+ # screen; LOD and clustering remain Phase 2.
100
+ scale_cap = 1000
89
101
 
90
102
  def intake(self, path: Path, *, explicit: bool = False) -> ProjectIntake:
91
103
  """Resolve one project scope and every adapter's owned files."""
@@ -133,30 +145,52 @@ class ProjectParser:
133
145
  *,
134
146
  entrypoint: str | None = None,
135
147
  explicit: bool = False,
148
+ progress: ParseProgress | None = None,
136
149
  ) -> Graph:
137
150
  """Parse every detected language and return one deterministic graph."""
138
151
 
139
- intake = (
140
- source
141
- if isinstance(source, ProjectIntake)
142
- else self.intake(source, explicit=explicit)
143
- )
152
+ if isinstance(source, ProjectIntake):
153
+ intake = source
154
+ else:
155
+ if progress is not None:
156
+ progress.stage("discovering")
157
+ intake = self.intake(source, explicit=explicit)
158
+ owned = {
159
+ adapter.language: intake._files_for(adapter.language)
160
+ for adapter in self._adapters
161
+ }
162
+ if progress is not None:
163
+ # The counter totals the files adapters will actually read, which
164
+ # is what ``note_file_parsed`` counts. ``intake.files`` is the
165
+ # deduplicated union and would drift if two adapters ever shared
166
+ # an extension.
167
+ progress.files_total(sum(len(files) for files in owned.values()))
168
+ progress.stage("parsing")
144
169
  graphs: list[Graph] = []
145
- for adapter in self._adapters:
146
- files = intake._files_for(adapter.language)
147
- if not files:
148
- continue
149
- try:
150
- graphs.append(adapter.parse_files(intake.root, files))
151
- except AdapterParseError as error:
152
- raise ProjectParseError(str(error)) from error
153
- return _compose_graphs(tuple(graphs), intake.root, entrypoint)
170
+ on_file = progress.file_parsed if progress is not None else None
171
+ # ``detail`` outlives the file-read loop: the adapters narrate their
172
+ # cross-file passes and composition narrates the merge, all under the
173
+ # single ``resolving`` stage the design spec fixes.
174
+ on_detail = getattr(progress, "detail", None) if progress is not None else None
175
+ with reporting_detail(on_detail), reporting_files(on_file):
176
+ for adapter in self._adapters:
177
+ files = owned[adapter.language]
178
+ if not files:
179
+ continue
180
+ try:
181
+ graphs.append(adapter.parse_files(intake.root, files))
182
+ except AdapterParseError as error:
183
+ raise ProjectParseError(str(error)) from error
184
+ if progress is not None:
185
+ progress.stage("resolving")
186
+ return _compose_graphs(tuple(graphs), intake.root, entrypoint)
154
187
 
155
188
  def _compose_graphs(
156
189
  graphs: tuple[Graph, ...],
157
190
  project_root: Path,
158
191
  entrypoint: str | None,
159
192
  ) -> Graph:
193
+ note_detail("Composing your project")
160
194
  nodes: list[Node] = []
161
195
  edges = []
162
196
  annotations = []
@@ -12,6 +12,7 @@ from pathlib import Path
12
12
 
13
13
  from codemble.adapters.base import AdapterParseError, ConceptAnnotation, Edge, Graph, Node
14
14
  from codemble.adapters.discovery import SourceDiscoveryError, discover_source_files
15
+ from codemble.adapters.parse_progress import note_detail, note_file_parsed
15
16
  from codemble.graph.finalize import GraphFinalizationError, finalize_graph
16
17
 
17
18
  _APP_FACTORIES = {"FastAPI", "Flask", "Typer"}
@@ -350,6 +351,10 @@ class PythonAstAdapter:
350
351
  module_bindings: dict[str, list[_ImportBinding]] = defaultdict(list)
351
352
  scope_bindings: dict[str, list[_ImportBinding]] = defaultdict(list)
352
353
 
354
+ # The stage counter is full once the last file is read; from here the
355
+ # resolving stage does its cross-file work, so it narrates the real
356
+ # passes rather than sitting on one frozen label.
357
+ note_detail("Resolving imports")
353
358
  for parsed in parsed_files:
354
359
  if parsed.tree is None:
355
360
  continue
@@ -371,6 +376,7 @@ class PythonAstAdapter:
371
376
  _, bindings = _resolve_import(parsed, syntax, modules, node_by_id)
372
377
  scope_bindings[definition.node_id].extend(bindings)
373
378
 
379
+ note_detail("Resolving calls")
374
380
  call_edges: list[Edge] = []
375
381
  children_by_parent: dict[str, list[Node]] = defaultdict(list)
376
382
  for definition in definitions:
@@ -401,6 +407,7 @@ class PythonAstAdapter:
401
407
  )
402
408
  )
403
409
 
410
+ note_detail("Reading language concepts")
404
411
  all_edges = [*import_edges, *call_edges]
405
412
  annotations: list[ConceptAnnotation] = []
406
413
  for parsed in parsed_files:
@@ -430,6 +437,7 @@ class PythonAstAdapter:
430
437
  parsed.relative_path for parsed in parsed_files if parsed.tree is None
431
438
  ),
432
439
  )
440
+ note_detail("Building the galaxy map")
433
441
  try:
434
442
  return finalize_graph(draft, entrypoint=entrypoint)
435
443
  except GraphFinalizationError as error:
@@ -488,7 +496,7 @@ def _parse_file(path: Path, project_root: Path) -> _ParsedFile:
488
496
  except (SyntaxError, UnicodeDecodeError):
489
497
  source = raw.decode("utf-8", errors="replace")
490
498
  tree = None
491
- return _ParsedFile(
499
+ parsed = _ParsedFile(
492
500
  path=path,
493
501
  relative_path=relative.as_posix(),
494
502
  module=_module_name(relative, project_root),
@@ -496,6 +504,8 @@ def _parse_file(path: Path, project_root: Path) -> _ParsedFile:
496
504
  digest=digest,
497
505
  tree=tree,
498
506
  )
507
+ note_file_parsed()
508
+ return parsed
499
509
 
500
510
 
501
511
  def _module_name(relative: Path, project_root: Path) -> str:
@@ -757,10 +767,18 @@ def _call_leaf_name(expression: ast.expr) -> str | None:
757
767
 
758
768
 
759
769
  def _module_from_node_id(node_id: str, modules: set[str]) -> str:
760
- return max(
761
- (module for module in modules if node_id == module or node_id.startswith(f"{module}.")),
762
- key=len,
763
- )
770
+ # Longest module that is a dotted prefix of ``node_id``. Walking the id's
771
+ # own prefixes (longest first) is byte-identical to scanning every module,
772
+ # but O(id depth) set lookups instead of O(modules) startswith calls -- the
773
+ # difference between 47M string scans and a few thousand on a 1k-file parse.
774
+ candidate = node_id
775
+ while True:
776
+ if candidate in modules:
777
+ return candidate
778
+ cut = candidate.rfind(".")
779
+ if cut == -1:
780
+ raise ValueError(f"no module owns node id: {node_id}")
781
+ candidate = candidate[:cut]
764
782
 
765
783
 
766
784
  __all__ = ["PythonAstAdapter", "PythonParseError"]
@@ -20,6 +20,7 @@ from codemble.adapters.base import (
20
20
  Node,
21
21
  )
22
22
  from codemble.adapters.discovery import SourceDiscoveryError, discover_source_files
23
+ from codemble.adapters.parse_progress import note_file_parsed
23
24
  from codemble.graph.finalize import GraphFinalizationError, finalize_graph
24
25
 
25
26
  _JAVASCRIPT_EXTENSIONS = frozenset({".js", ".jsx", ".mjs", ".cjs"})
@@ -344,7 +345,7 @@ def _parse_file(path: Path, project_root: Path) -> _ParsedFile:
344
345
  relative = path.relative_to(project_root).as_posix()
345
346
  language = "javascript" if path.suffix.lower() in _JAVASCRIPT_EXTENSIONS else "typescript"
346
347
  parser = Parser(_language_for(path.suffix.lower()))
347
- return _ParsedFile(
348
+ parsed = _ParsedFile(
348
349
  path=path,
349
350
  project_root=project_root,
350
351
  relative_path=relative,
@@ -355,6 +356,8 @@ def _parse_file(path: Path, project_root: Path) -> _ParsedFile:
355
356
  digest=hashlib.sha256(raw).hexdigest(),
356
357
  tree=parser.parse(raw),
357
358
  )
359
+ note_file_parsed()
360
+ return parsed
358
361
 
359
362
 
360
363
  def _language_for(extension: str) -> Language: