drowse.ai 5.3.1__py3-none-any.whl

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 (1341) hide show
  1. drowse/__init__.py +312 -0
  2. drowse/__main__.py +5 -0
  3. drowse/cli/__init__.py +5 -0
  4. drowse/cli/alpha_grid.py +96 -0
  5. drowse/cli/config_file.py +340 -0
  6. drowse/cli/main.py +52 -0
  7. drowse/cli/parsers.py +1478 -0
  8. drowse/cli/runners/__init__.py +155 -0
  9. drowse/cli/runners/config.py +85 -0
  10. drowse/cli/runners/experiment.py +344 -0
  11. drowse/cli/runners/lens.py +661 -0
  12. drowse/cli/runners/manifold.py +1097 -0
  13. drowse/cli/runners/pack.py +413 -0
  14. drowse/cli/runners/sae.py +304 -0
  15. drowse/cli/runners/serve.py +220 -0
  16. drowse/cli/runners/shared.py +389 -0
  17. drowse/cli/runners/template.py +232 -0
  18. drowse/core/__init__.py +1 -0
  19. drowse/core/capture.py +1205 -0
  20. drowse/core/errors.py +260 -0
  21. drowse/core/events.py +156 -0
  22. drowse/core/extraction.py +3076 -0
  23. drowse/core/generation.py +1738 -0
  24. drowse/core/generation_finalizer.py +278 -0
  25. drowse/core/histogram.py +30 -0
  26. drowse/core/hooks.py +1885 -0
  27. drowse/core/instruments/__init__.py +49 -0
  28. drowse/core/instruments/geometry.py +551 -0
  29. drowse/core/instruments/lens.py +1341 -0
  30. drowse/core/instruments/protocol.py +211 -0
  31. drowse/core/instruments/sae.py +935 -0
  32. drowse/core/instruments/types.py +597 -0
  33. drowse/core/jlens.py +874 -0
  34. drowse/core/jlens_fit.py +1305 -0
  35. drowse/core/joint_logprobs.py +748 -0
  36. drowse/core/loom.py +1938 -0
  37. drowse/core/loom_diff.py +408 -0
  38. drowse/core/mahalanobis.py +579 -0
  39. drowse/core/manifold.py +4106 -0
  40. drowse/core/measurements.py +382 -0
  41. drowse/core/model.py +1724 -0
  42. drowse/core/monitor.py +2060 -0
  43. drowse/core/monitor_attach.py +682 -0
  44. drowse/core/naming.py +42 -0
  45. drowse/core/naturalness.py +187 -0
  46. drowse/core/profile.py +617 -0
  47. drowse/core/relp.py +313 -0
  48. drowse/core/results.py +467 -0
  49. drowse/core/role_templates.py +677 -0
  50. drowse/core/sae.py +891 -0
  51. drowse/core/sae_training.py +239 -0
  52. drowse/core/sampling.py +127 -0
  53. drowse/core/scene.py +810 -0
  54. drowse/core/scoring.py +270 -0
  55. drowse/core/session.py +11015 -0
  56. drowse/core/static_cache.py +316 -0
  57. drowse/core/steering.py +229 -0
  58. drowse/core/steering_composer.py +1135 -0
  59. drowse/core/steering_expr.py +1413 -0
  60. drowse/core/surface_topology.py +128 -0
  61. drowse/core/token_callback.py +52 -0
  62. drowse/core/token_payloads.py +244 -0
  63. drowse/core/topology.py +1469 -0
  64. drowse/core/transcript.py +582 -0
  65. drowse/core/tree_filter.py +353 -0
  66. drowse/core/triggers.py +250 -0
  67. drowse/data/baseline_prompts.json +50 -0
  68. drowse/data/manifolds/confident.uncertain/manifold.json +28 -0
  69. drowse/data/manifolds/confident.uncertain/nodes/00_confident.json +50 -0
  70. drowse/data/manifolds/confident.uncertain/nodes/01_uncertain.json +50 -0
  71. drowse/data/manifolds/creative.conventional/manifold.json +28 -0
  72. drowse/data/manifolds/creative.conventional/nodes/00_creative.json +50 -0
  73. drowse/data/manifolds/creative.conventional/nodes/01_conventional.json +50 -0
  74. drowse/data/manifolds/curious.disinterested/manifold.json +28 -0
  75. drowse/data/manifolds/curious.disinterested/nodes/00_curious.json +50 -0
  76. drowse/data/manifolds/curious.disinterested/nodes/01_disinterested.json +50 -0
  77. drowse/data/manifolds/direct.indirect/manifold.json +28 -0
  78. drowse/data/manifolds/direct.indirect/nodes/00_direct.json +50 -0
  79. drowse/data/manifolds/direct.indirect/nodes/01_indirect.json +50 -0
  80. drowse/data/manifolds/emotions/manifold.json +115 -0
  81. drowse/data/manifolds/emotions/nodes/00_excited.json +50 -0
  82. drowse/data/manifolds/emotions/nodes/01_happy.json +50 -0
  83. drowse/data/manifolds/emotions/nodes/02_calm.json +50 -0
  84. drowse/data/manifolds/emotions/nodes/03_weary.json +50 -0
  85. drowse/data/manifolds/emotions/nodes/04_sad.json +50 -0
  86. drowse/data/manifolds/emotions/nodes/05_distressed.json +50 -0
  87. drowse/data/manifolds/emotions/nodes/06_alert.json +50 -0
  88. drowse/data/manifolds/emotions/nodes/07_dominant.json +50 -0
  89. drowse/data/manifolds/emotions/nodes/08_submissive.json +50 -0
  90. drowse/data/manifolds/emotions/nodes/09_angry.json +50 -0
  91. drowse/data/manifolds/emotions/nodes/10_fearful.json +50 -0
  92. drowse/data/manifolds/emotions/nodes/11_surprised.json +50 -0
  93. drowse/data/manifolds/emotions/nodes/12_triumphant.json +50 -0
  94. drowse/data/manifolds/emotions/nodes/13_humiliated.json +50 -0
  95. drowse/data/manifolds/emotions/nodes/14_disgusted.json +50 -0
  96. drowse/data/manifolds/emotions/nodes/15_bored.json +50 -0
  97. drowse/data/manifolds/emotions/nodes/16_grateful.json +50 -0
  98. drowse/data/manifolds/emotions/nodes/17_relieved.json +50 -0
  99. drowse/data/manifolds/emotions/nodes/18_playful.json +50 -0
  100. drowse/data/manifolds/emotions/nodes/19_hopeful.json +50 -0
  101. drowse/data/manifolds/formal.casual/manifold.json +28 -0
  102. drowse/data/manifolds/formal.casual/nodes/00_formal.json +50 -0
  103. drowse/data/manifolds/formal.casual/nodes/01_casual.json +50 -0
  104. drowse/data/manifolds/honest.deceptive/manifold.json +28 -0
  105. drowse/data/manifolds/honest.deceptive/nodes/00_honest.json +50 -0
  106. drowse/data/manifolds/honest.deceptive/nodes/01_deceptive.json +50 -0
  107. drowse/data/manifolds/humorous.serious/manifold.json +28 -0
  108. drowse/data/manifolds/humorous.serious/nodes/00_humorous.json +50 -0
  109. drowse/data/manifolds/humorous.serious/nodes/01_serious.json +50 -0
  110. drowse/data/manifolds/individualist.collectivist/manifold.json +28 -0
  111. drowse/data/manifolds/individualist.collectivist/nodes/00_individualist.json +50 -0
  112. drowse/data/manifolds/individualist.collectivist/nodes/01_collectivist.json +50 -0
  113. drowse/data/manifolds/masculine.feminine/manifold.json +28 -0
  114. drowse/data/manifolds/masculine.feminine/nodes/00_masculine.json +50 -0
  115. drowse/data/manifolds/masculine.feminine/nodes/01_feminine.json +50 -0
  116. drowse/data/manifolds/months/manifold.json +120 -0
  117. drowse/data/manifolds/months/nodes/00_january.json +50 -0
  118. drowse/data/manifolds/months/nodes/01_february.json +50 -0
  119. drowse/data/manifolds/months/nodes/02_march.json +50 -0
  120. drowse/data/manifolds/months/nodes/03_april.json +50 -0
  121. drowse/data/manifolds/months/nodes/04_may.json +50 -0
  122. drowse/data/manifolds/months/nodes/05_june.json +50 -0
  123. drowse/data/manifolds/months/nodes/06_july.json +50 -0
  124. drowse/data/manifolds/months/nodes/07_august.json +50 -0
  125. drowse/data/manifolds/months/nodes/08_september.json +50 -0
  126. drowse/data/manifolds/months/nodes/09_october.json +50 -0
  127. drowse/data/manifolds/months/nodes/10_november.json +50 -0
  128. drowse/data/manifolds/months/nodes/11_december.json +50 -0
  129. drowse/data/manifolds/personas/manifold.json +550 -0
  130. drowse/data/manifolds/personas/nodes/00_assistant.json +50 -0
  131. drowse/data/manifolds/personas/nodes/01_analyst.json +50 -0
  132. drowse/data/manifolds/personas/nodes/02_impostor.json +50 -0
  133. drowse/data/manifolds/personas/nodes/03_teacher.json +50 -0
  134. drowse/data/manifolds/personas/nodes/04_doctor.json +50 -0
  135. drowse/data/manifolds/personas/nodes/05_librarian.json +50 -0
  136. drowse/data/manifolds/personas/nodes/06_farmer.json +50 -0
  137. drowse/data/manifolds/personas/nodes/07_soldier.json +50 -0
  138. drowse/data/manifolds/personas/nodes/08_lawyer.json +50 -0
  139. drowse/data/manifolds/personas/nodes/09_chef.json +50 -0
  140. drowse/data/manifolds/personas/nodes/100_assassin.json +50 -0
  141. drowse/data/manifolds/personas/nodes/101_thief.json +50 -0
  142. drowse/data/manifolds/personas/nodes/102_conman.json +50 -0
  143. drowse/data/manifolds/personas/nodes/103_hacker.json +50 -0
  144. drowse/data/manifolds/personas/nodes/104_cultist.json +50 -0
  145. drowse/data/manifolds/personas/nodes/105_tyrant.json +50 -0
  146. drowse/data/manifolds/personas/nodes/106_vandal.json +50 -0
  147. drowse/data/manifolds/personas/nodes/10_journalist.json +50 -0
  148. drowse/data/manifolds/personas/nodes/11_mechanic.json +50 -0
  149. drowse/data/manifolds/personas/nodes/12_sailor.json +50 -0
  150. drowse/data/manifolds/personas/nodes/13_pilot.json +50 -0
  151. drowse/data/manifolds/personas/nodes/14_athlete.json +50 -0
  152. drowse/data/manifolds/personas/nodes/15_engineer.json +50 -0
  153. drowse/data/manifolds/personas/nodes/16_scientist.json +50 -0
  154. drowse/data/manifolds/personas/nodes/17_banker.json +50 -0
  155. drowse/data/manifolds/personas/nodes/18_salesperson.json +50 -0
  156. drowse/data/manifolds/personas/nodes/19_nerd.json +50 -0
  157. drowse/data/manifolds/personas/nodes/20_jock.json +50 -0
  158. drowse/data/manifolds/personas/nodes/21_gamer.json +50 -0
  159. drowse/data/manifolds/personas/nodes/22_furry.json +50 -0
  160. drowse/data/manifolds/personas/nodes/23_weeaboo.json +50 -0
  161. drowse/data/manifolds/personas/nodes/24_influencer.json +50 -0
  162. drowse/data/manifolds/personas/nodes/25_stoner.json +50 -0
  163. drowse/data/manifolds/personas/nodes/26_femboy.json +50 -0
  164. drowse/data/manifolds/personas/nodes/27_ceo.json +50 -0
  165. drowse/data/manifolds/personas/nodes/28_emperor.json +50 -0
  166. drowse/data/manifolds/personas/nodes/29_president.json +50 -0
  167. drowse/data/manifolds/personas/nodes/30_diplomat.json +50 -0
  168. drowse/data/manifolds/personas/nodes/31_judge.json +50 -0
  169. drowse/data/manifolds/personas/nodes/32_bureaucrat.json +50 -0
  170. drowse/data/manifolds/personas/nodes/33_conservative.json +50 -0
  171. drowse/data/manifolds/personas/nodes/34_liberal.json +50 -0
  172. drowse/data/manifolds/personas/nodes/35_libertarian.json +50 -0
  173. drowse/data/manifolds/personas/nodes/36_communist.json +50 -0
  174. drowse/data/manifolds/personas/nodes/37_anarchist.json +50 -0
  175. drowse/data/manifolds/personas/nodes/38_reactionary.json +50 -0
  176. drowse/data/manifolds/personas/nodes/39_loner.json +50 -0
  177. drowse/data/manifolds/personas/nodes/40_friend.json +50 -0
  178. drowse/data/manifolds/personas/nodes/41_rival.json +50 -0
  179. drowse/data/manifolds/personas/nodes/42_stranger.json +50 -0
  180. drowse/data/manifolds/personas/nodes/43_lover.json +50 -0
  181. drowse/data/manifolds/personas/nodes/44_stalker.json +50 -0
  182. drowse/data/manifolds/personas/nodes/45_lunatic.json +50 -0
  183. drowse/data/manifolds/personas/nodes/46_paranoid.json +50 -0
  184. drowse/data/manifolds/personas/nodes/47_detective.json +50 -0
  185. drowse/data/manifolds/personas/nodes/48_therapist.json +50 -0
  186. drowse/data/manifolds/personas/nodes/49_nomad.json +50 -0
  187. drowse/data/manifolds/personas/nodes/50_pirate.json +50 -0
  188. drowse/data/manifolds/personas/nodes/51_child.json +50 -0
  189. drowse/data/manifolds/personas/nodes/52_caveman.json +50 -0
  190. drowse/data/manifolds/personas/nodes/53_monk.json +50 -0
  191. drowse/data/manifolds/personas/nodes/54_knight.json +50 -0
  192. drowse/data/manifolds/personas/nodes/55_alchemist.json +50 -0
  193. drowse/data/manifolds/personas/nodes/56_blacksmith.json +50 -0
  194. drowse/data/manifolds/personas/nodes/57_viking.json +50 -0
  195. drowse/data/manifolds/personas/nodes/58_druid.json +50 -0
  196. drowse/data/manifolds/personas/nodes/59_poet.json +50 -0
  197. drowse/data/manifolds/personas/nodes/60_novelist.json +50 -0
  198. drowse/data/manifolds/personas/nodes/61_jester.json +50 -0
  199. drowse/data/manifolds/personas/nodes/62_philosopher.json +50 -0
  200. drowse/data/manifolds/personas/nodes/63_painter.json +50 -0
  201. drowse/data/manifolds/personas/nodes/64_musician.json +50 -0
  202. drowse/data/manifolds/personas/nodes/65_oracle.json +50 -0
  203. drowse/data/manifolds/personas/nodes/66_god.json +50 -0
  204. drowse/data/manifolds/personas/nodes/67_angel.json +50 -0
  205. drowse/data/manifolds/personas/nodes/68_prophet.json +50 -0
  206. drowse/data/manifolds/personas/nodes/69_saint.json +50 -0
  207. drowse/data/manifolds/personas/nodes/70_ghost.json +50 -0
  208. drowse/data/manifolds/personas/nodes/71_phoenix.json +50 -0
  209. drowse/data/manifolds/personas/nodes/72_witch.json +50 -0
  210. drowse/data/manifolds/personas/nodes/73_vampire.json +50 -0
  211. drowse/data/manifolds/personas/nodes/74_mermaid.json +50 -0
  212. drowse/data/manifolds/personas/nodes/75_sphinx.json +50 -0
  213. drowse/data/manifolds/personas/nodes/76_banshee.json +50 -0
  214. drowse/data/manifolds/personas/nodes/77_golem.json +50 -0
  215. drowse/data/manifolds/personas/nodes/78_deer.json +50 -0
  216. drowse/data/manifolds/personas/nodes/79_wolf.json +50 -0
  217. drowse/data/manifolds/personas/nodes/80_raven.json +50 -0
  218. drowse/data/manifolds/personas/nodes/81_serpent.json +50 -0
  219. drowse/data/manifolds/personas/nodes/82_lion.json +50 -0
  220. drowse/data/manifolds/personas/nodes/83_tree.json +50 -0
  221. drowse/data/manifolds/personas/nodes/84_river.json +50 -0
  222. drowse/data/manifolds/personas/nodes/85_storm.json +50 -0
  223. drowse/data/manifolds/personas/nodes/86_flame.json +50 -0
  224. drowse/data/manifolds/personas/nodes/87_mirror.json +50 -0
  225. drowse/data/manifolds/personas/nodes/88_clock.json +50 -0
  226. drowse/data/manifolds/personas/nodes/89_computer.json +50 -0
  227. drowse/data/manifolds/personas/nodes/90_robot.json +50 -0
  228. drowse/data/manifolds/personas/nodes/91_cyborg.json +50 -0
  229. drowse/data/manifolds/personas/nodes/92_drone.json +50 -0
  230. drowse/data/manifolds/personas/nodes/93_mecha.json +50 -0
  231. drowse/data/manifolds/personas/nodes/94_alien.json +50 -0
  232. drowse/data/manifolds/personas/nodes/95_trickster.json +50 -0
  233. drowse/data/manifolds/personas/nodes/96_virus.json +50 -0
  234. drowse/data/manifolds/personas/nodes/97_spy.json +50 -0
  235. drowse/data/manifolds/personas/nodes/98_narcissist.json +50 -0
  236. drowse/data/manifolds/personas/nodes/99_demon.json +50 -0
  237. drowse/data/manifolds/refusing.compliant/manifold.json +28 -0
  238. drowse/data/manifolds/refusing.compliant/nodes/00_refusing.json +50 -0
  239. drowse/data/manifolds/refusing.compliant/nodes/01_compliant.json +50 -0
  240. drowse/data/manifolds/religious.secular/manifold.json +28 -0
  241. drowse/data/manifolds/religious.secular/nodes/00_religious.json +50 -0
  242. drowse/data/manifolds/religious.secular/nodes/01_secular.json +50 -0
  243. drowse/data/manifolds/sincere.manipulative/manifold.json +28 -0
  244. drowse/data/manifolds/sincere.manipulative/nodes/00_sincere.json +50 -0
  245. drowse/data/manifolds/sincere.manipulative/nodes/01_manipulative.json +50 -0
  246. drowse/data/manifolds/sycophantic.blunt/manifold.json +28 -0
  247. drowse/data/manifolds/sycophantic.blunt/nodes/00_sycophantic.json +50 -0
  248. drowse/data/manifolds/sycophantic.blunt/nodes/01_blunt.json +50 -0
  249. drowse/data/manifolds/technical.accessible/manifold.json +28 -0
  250. drowse/data/manifolds/technical.accessible/nodes/00_technical.json +50 -0
  251. drowse/data/manifolds/technical.accessible/nodes/01_accessible.json +50 -0
  252. drowse/data/manifolds/traditional.progressive/manifold.json +28 -0
  253. drowse/data/manifolds/traditional.progressive/nodes/00_traditional.json +50 -0
  254. drowse/data/manifolds/traditional.progressive/nodes/01_progressive.json +50 -0
  255. drowse/data/manifolds/verbose.concise/manifold.json +28 -0
  256. drowse/data/manifolds/verbose.concise/nodes/00_verbose.json +50 -0
  257. drowse/data/manifolds/verbose.concise/nodes/01_concise.json +50 -0
  258. drowse/data/manifolds/warm.clinical/manifold.json +28 -0
  259. drowse/data/manifolds/warm.clinical/nodes/00_warm.json +50 -0
  260. drowse/data/manifolds/warm.clinical/nodes/01_clinical.json +50 -0
  261. drowse/data/neutral_statements.json +98 -0
  262. drowse/io/__init__.py +1 -0
  263. drowse/io/alignment.py +1334 -0
  264. drowse/io/atomic.py +278 -0
  265. drowse/io/bake.py +558 -0
  266. drowse/io/bootstrap.py +73 -0
  267. drowse/io/brand_migration.py +48 -0
  268. drowse/io/drowse_archive.py +1830 -0
  269. drowse/io/gguf_io.py +300 -0
  270. drowse/io/hf.py +81 -0
  271. drowse/io/hf_manifolds.py +931 -0
  272. drowse/io/integrity.py +102 -0
  273. drowse/io/lens.py +1992 -0
  274. drowse/io/lens_sources.py +1003 -0
  275. drowse/io/manifold_authoring.py +1348 -0
  276. drowse/io/manifold_folder.py +1854 -0
  277. drowse/io/manifold_lifecycle.py +1253 -0
  278. drowse/io/manifold_tensors.py +727 -0
  279. drowse/io/manifolds.py +443 -0
  280. drowse/io/paths.py +289 -0
  281. drowse/io/probes_bootstrap.py +58 -0
  282. drowse/io/sae.py +382 -0
  283. drowse/io/sae_artifacts.py +318 -0
  284. drowse/io/selectors.py +503 -0
  285. drowse/io/shards.py +160 -0
  286. drowse/io/source_registry.py +138 -0
  287. drowse/io/staging.py +80 -0
  288. drowse/io/templates.py +707 -0
  289. drowse/notebook/__init__.py +38 -0
  290. drowse/notebook/data.py +99 -0
  291. drowse/notebook/plots.py +352 -0
  292. drowse/py.typed +0 -0
  293. drowse/server/__init__.py +20 -0
  294. drowse/server/app.py +987 -0
  295. drowse/server/background_job.py +246 -0
  296. drowse/server/instrument_routes.py +1101 -0
  297. drowse/server/manifold_routes.py +1028 -0
  298. drowse/server/model_names.py +82 -0
  299. drowse/server/native_common.py +92 -0
  300. drowse/server/native_routes.py +45 -0
  301. drowse/server/ollama.py +817 -0
  302. drowse/server/operation_ledger.py +187 -0
  303. drowse/server/probe_routes.py +220 -0
  304. drowse/server/profile_models.py +52 -0
  305. drowse/server/profile_routes.py +440 -0
  306. drowse/server/request_helpers.py +198 -0
  307. drowse/server/request_ledger.py +85 -0
  308. drowse/server/response_models.py +814 -0
  309. drowse/server/session_models.py +146 -0
  310. drowse/server/session_routes.py +150 -0
  311. drowse/server/sse.py +333 -0
  312. drowse/server/streaming.py +129 -0
  313. drowse/server/template_routes.py +161 -0
  314. drowse/server/tree_models.py +121 -0
  315. drowse/server/tree_routes.py +550 -0
  316. drowse/server/ws_events.py +65 -0
  317. drowse/server/ws_models.py +219 -0
  318. drowse/server/ws_stream.py +1205 -0
  319. drowse/web/__init__.py +22 -0
  320. drowse/web/dist/LICENSE-Fluent-Icons.txt +21 -0
  321. drowse/web/dist/LICENSE-Fluent-System-Icons.txt +21 -0
  322. drowse/web/dist/LICENSE-Martian-Mono.txt +93 -0
  323. drowse/web/dist/LICENSE-Torph.txt +21 -0
  324. drowse/web/dist/LICENSE-Wix-Madefor.txt +93 -0
  325. drowse/web/dist/assets/App-BewwoFwv.js +33 -0
  326. drowse/web/dist/assets/App.css +1 -0
  327. drowse/web/dist/assets/MartianMono-cyrillic-ext.woff2 +0 -0
  328. drowse/web/dist/assets/MartianMono-cyrillic.woff2 +0 -0
  329. drowse/web/dist/assets/MartianMono-latin-ext.woff2 +0 -0
  330. drowse/web/dist/assets/MartianMono-latin.woff2 +0 -0
  331. drowse/web/dist/assets/WixMadeforDisplay-cyrillic.woff2 +0 -0
  332. drowse/web/dist/assets/WixMadeforDisplay-latin-ext.woff2 +0 -0
  333. drowse/web/dist/assets/WixMadeforDisplay-latin.woff2 +0 -0
  334. drowse/web/dist/assets/WixMadeforDisplay-vietnamese.woff2 +0 -0
  335. drowse/web/dist/assets/WixMadeforText-italic-latin-ext.woff2 +0 -0
  336. drowse/web/dist/assets/WixMadeforText-italic-latin.woff2 +0 -0
  337. drowse/web/dist/assets/WixMadeforText-normal-cyrillic.woff2 +0 -0
  338. drowse/web/dist/assets/WixMadeforText-normal-latin-ext.woff2 +0 -0
  339. drowse/web/dist/assets/WixMadeforText-normal-latin.woff2 +0 -0
  340. drowse/web/dist/assets/WixMadeforText-normal-vietnamese.woff2 +0 -0
  341. drowse/web/dist/assets/ab.svelte-CbVGAYXo.js +1 -0
  342. drowse/web/dist/assets/artifactUpdates-Cjeu6LJc.js +1 -0
  343. drowse/web/dist/assets/artifactUpdates-DfAoYC9j.js +1 -0
  344. drowse/web/dist/assets/chat.svelte-DxKufn9g.js +1 -0
  345. drowse/web/dist/assets/drawers-BDWC0hIN.js +123 -0
  346. drowse/web/dist/assets/drawers.css +1 -0
  347. drowse/web/dist/assets/drowse.js +7 -0
  348. drowse/web/dist/assets/eleutherai.png +0 -0
  349. drowse/web/dist/assets/errors-Dvb10QW_.js +2 -0
  350. drowse/web/dist/assets/expression-4yISdAdq.js +1 -0
  351. drowse/web/dist/assets/files-CvRivkey.js +1 -0
  352. drowse/web/dist/assets/files-JhDJVgD_.js +1 -0
  353. drowse/web/dist/assets/fittingCancellation-D72BZWPZ.js +1 -0
  354. drowse/web/dist/assets/gemma-3-1b-it.json +1 -0
  355. drowse/web/dist/assets/gemma-3-270m-it.json +1 -0
  356. drowse/web/dist/assets/gemma-3-4b-it.json +1 -0
  357. drowse/web/dist/assets/generation-DbkBAhP_.js +1 -0
  358. drowse/web/dist/assets/index-client-2OdH6IQp.js +3 -0
  359. drowse/web/dist/assets/index-client-D4S4PooY.js +1 -0
  360. drowse/web/dist/assets/index.css +1 -0
  361. drowse/web/dist/assets/instrumentAuthoring.svelte-Csh7NOGX.js +1 -0
  362. drowse/web/dist/assets/instrumentAuthoring.svelte-w9YPcDsP.js +1 -0
  363. drowse/web/dist/assets/instruments.svelte-XnEUQA6B.js +1 -0
  364. drowse/web/dist/assets/localData-DIBDM_zo.js +1 -0
  365. drowse/web/dist/assets/loom.svelte-D_YyqHHK.js +1 -0
  366. drowse/web/dist/assets/manifoldJobs.svelte-9f5hs_J7.js +1 -0
  367. drowse/web/dist/assets/manifoldJobs.svelte-Bwo2GYOH.js +1 -0
  368. drowse/web/dist/assets/openai.png +0 -0
  369. drowse/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
  370. drowse/web/dist/assets/probes.svelte-COcN1Rh8.js +1 -0
  371. drowse/web/dist/assets/registry-B0_pJoMg.js +1 -0
  372. drowse/web/dist/assets/registry-CQv71XNh.js +1 -0
  373. drowse/web/dist/assets/sampling.svelte-BU0kED3k.js +1 -0
  374. drowse/web/dist/assets/samplingCapabilities-DM6eoDoc.js +1 -0
  375. drowse/web/dist/assets/savedConversations.svelte-BsBLc-p7.js +1 -0
  376. drowse/web/dist/assets/savedConversations.svelte-TuSes9Kg.js +1 -0
  377. drowse/web/dist/assets/selectors-BxXnPPN_.js +1 -0
  378. drowse/web/dist/assets/serviceTools-CG0q1Xqg.js +2 -0
  379. drowse/web/dist/assets/session.svelte-D2STLuP6.js +1 -0
  380. drowse/web/dist/assets/steering.svelte-DvFT3O-d.js +1 -0
  381. drowse/web/dist/assets/steeringPositions-Dr0JOFu7.js +1 -0
  382. drowse/web/dist/assets/templates-Bkq2x5ID.js +1 -0
  383. drowse/web/dist/assets/types-CuMUbvHW.js +1 -0
  384. drowse/web/dist/assets/validation-5wgTuS7k.js +1 -0
  385. drowse/web/dist/assets/webmcp-DyqJf4z6.js +2 -0
  386. drowse/web/dist/assets/workspaceController-C2L33e3m.js +1 -0
  387. drowse/web/dist/assets/workspaceController-DJhXD_BC.js +1 -0
  388. drowse/web/dist/assets/workspaceTools-BWdmXopx.js +2 -0
  389. drowse/web/dist/favicon.ico +0 -0
  390. drowse/web/dist/favicon.svg +1 -0
  391. drowse/web/dist/icons/apple-touch-icon.png +0 -0
  392. drowse/web/dist/icons/drowse-192.png +0 -0
  393. drowse/web/dist/icons/drowse-512.png +0 -0
  394. drowse/web/dist/icons/drowse-maskable-512.png +0 -0
  395. drowse/web/dist/icons/tab-analyzing-0.png +0 -0
  396. drowse/web/dist/icons/tab-analyzing-1.png +0 -0
  397. drowse/web/dist/icons/tab-analyzing-2.png +0 -0
  398. drowse/web/dist/icons/tab-analyzing-dark-0.png +0 -0
  399. drowse/web/dist/icons/tab-analyzing-dark-1.png +0 -0
  400. drowse/web/dist/icons/tab-analyzing-dark-10.png +0 -0
  401. drowse/web/dist/icons/tab-analyzing-dark-11.png +0 -0
  402. drowse/web/dist/icons/tab-analyzing-dark-12.png +0 -0
  403. drowse/web/dist/icons/tab-analyzing-dark-13.png +0 -0
  404. drowse/web/dist/icons/tab-analyzing-dark-14.png +0 -0
  405. drowse/web/dist/icons/tab-analyzing-dark-15.png +0 -0
  406. drowse/web/dist/icons/tab-analyzing-dark-16.png +0 -0
  407. drowse/web/dist/icons/tab-analyzing-dark-17.png +0 -0
  408. drowse/web/dist/icons/tab-analyzing-dark-18.png +0 -0
  409. drowse/web/dist/icons/tab-analyzing-dark-19.png +0 -0
  410. drowse/web/dist/icons/tab-analyzing-dark-2.png +0 -0
  411. drowse/web/dist/icons/tab-analyzing-dark-20.png +0 -0
  412. drowse/web/dist/icons/tab-analyzing-dark-21.png +0 -0
  413. drowse/web/dist/icons/tab-analyzing-dark-22.png +0 -0
  414. drowse/web/dist/icons/tab-analyzing-dark-23.png +0 -0
  415. drowse/web/dist/icons/tab-analyzing-dark-24.png +0 -0
  416. drowse/web/dist/icons/tab-analyzing-dark-25.png +0 -0
  417. drowse/web/dist/icons/tab-analyzing-dark-26.png +0 -0
  418. drowse/web/dist/icons/tab-analyzing-dark-27.png +0 -0
  419. drowse/web/dist/icons/tab-analyzing-dark-28.png +0 -0
  420. drowse/web/dist/icons/tab-analyzing-dark-29.png +0 -0
  421. drowse/web/dist/icons/tab-analyzing-dark-3.png +0 -0
  422. drowse/web/dist/icons/tab-analyzing-dark-30.png +0 -0
  423. drowse/web/dist/icons/tab-analyzing-dark-31.png +0 -0
  424. drowse/web/dist/icons/tab-analyzing-dark-32.png +0 -0
  425. drowse/web/dist/icons/tab-analyzing-dark-33.png +0 -0
  426. drowse/web/dist/icons/tab-analyzing-dark-34.png +0 -0
  427. drowse/web/dist/icons/tab-analyzing-dark-35.png +0 -0
  428. drowse/web/dist/icons/tab-analyzing-dark-36.png +0 -0
  429. drowse/web/dist/icons/tab-analyzing-dark-37.png +0 -0
  430. drowse/web/dist/icons/tab-analyzing-dark-38.png +0 -0
  431. drowse/web/dist/icons/tab-analyzing-dark-39.png +0 -0
  432. drowse/web/dist/icons/tab-analyzing-dark-4.png +0 -0
  433. drowse/web/dist/icons/tab-analyzing-dark-40.png +0 -0
  434. drowse/web/dist/icons/tab-analyzing-dark-41.png +0 -0
  435. drowse/web/dist/icons/tab-analyzing-dark-42.png +0 -0
  436. drowse/web/dist/icons/tab-analyzing-dark-43.png +0 -0
  437. drowse/web/dist/icons/tab-analyzing-dark-44.png +0 -0
  438. drowse/web/dist/icons/tab-analyzing-dark-45.png +0 -0
  439. drowse/web/dist/icons/tab-analyzing-dark-46.png +0 -0
  440. drowse/web/dist/icons/tab-analyzing-dark-47.png +0 -0
  441. drowse/web/dist/icons/tab-analyzing-dark-5.png +0 -0
  442. drowse/web/dist/icons/tab-analyzing-dark-6.png +0 -0
  443. drowse/web/dist/icons/tab-analyzing-dark-7.png +0 -0
  444. drowse/web/dist/icons/tab-analyzing-dark-8.png +0 -0
  445. drowse/web/dist/icons/tab-analyzing-dark-9.png +0 -0
  446. drowse/web/dist/icons/tab-analyzing-light-0.png +0 -0
  447. drowse/web/dist/icons/tab-analyzing-light-1.png +0 -0
  448. drowse/web/dist/icons/tab-analyzing-light-10.png +0 -0
  449. drowse/web/dist/icons/tab-analyzing-light-11.png +0 -0
  450. drowse/web/dist/icons/tab-analyzing-light-12.png +0 -0
  451. drowse/web/dist/icons/tab-analyzing-light-13.png +0 -0
  452. drowse/web/dist/icons/tab-analyzing-light-14.png +0 -0
  453. drowse/web/dist/icons/tab-analyzing-light-15.png +0 -0
  454. drowse/web/dist/icons/tab-analyzing-light-16.png +0 -0
  455. drowse/web/dist/icons/tab-analyzing-light-17.png +0 -0
  456. drowse/web/dist/icons/tab-analyzing-light-18.png +0 -0
  457. drowse/web/dist/icons/tab-analyzing-light-19.png +0 -0
  458. drowse/web/dist/icons/tab-analyzing-light-2.png +0 -0
  459. drowse/web/dist/icons/tab-analyzing-light-20.png +0 -0
  460. drowse/web/dist/icons/tab-analyzing-light-21.png +0 -0
  461. drowse/web/dist/icons/tab-analyzing-light-22.png +0 -0
  462. drowse/web/dist/icons/tab-analyzing-light-23.png +0 -0
  463. drowse/web/dist/icons/tab-analyzing-light-24.png +0 -0
  464. drowse/web/dist/icons/tab-analyzing-light-25.png +0 -0
  465. drowse/web/dist/icons/tab-analyzing-light-26.png +0 -0
  466. drowse/web/dist/icons/tab-analyzing-light-27.png +0 -0
  467. drowse/web/dist/icons/tab-analyzing-light-28.png +0 -0
  468. drowse/web/dist/icons/tab-analyzing-light-29.png +0 -0
  469. drowse/web/dist/icons/tab-analyzing-light-3.png +0 -0
  470. drowse/web/dist/icons/tab-analyzing-light-30.png +0 -0
  471. drowse/web/dist/icons/tab-analyzing-light-31.png +0 -0
  472. drowse/web/dist/icons/tab-analyzing-light-32.png +0 -0
  473. drowse/web/dist/icons/tab-analyzing-light-33.png +0 -0
  474. drowse/web/dist/icons/tab-analyzing-light-34.png +0 -0
  475. drowse/web/dist/icons/tab-analyzing-light-35.png +0 -0
  476. drowse/web/dist/icons/tab-analyzing-light-36.png +0 -0
  477. drowse/web/dist/icons/tab-analyzing-light-37.png +0 -0
  478. drowse/web/dist/icons/tab-analyzing-light-38.png +0 -0
  479. drowse/web/dist/icons/tab-analyzing-light-39.png +0 -0
  480. drowse/web/dist/icons/tab-analyzing-light-4.png +0 -0
  481. drowse/web/dist/icons/tab-analyzing-light-40.png +0 -0
  482. drowse/web/dist/icons/tab-analyzing-light-41.png +0 -0
  483. drowse/web/dist/icons/tab-analyzing-light-42.png +0 -0
  484. drowse/web/dist/icons/tab-analyzing-light-43.png +0 -0
  485. drowse/web/dist/icons/tab-analyzing-light-44.png +0 -0
  486. drowse/web/dist/icons/tab-analyzing-light-45.png +0 -0
  487. drowse/web/dist/icons/tab-analyzing-light-46.png +0 -0
  488. drowse/web/dist/icons/tab-analyzing-light-47.png +0 -0
  489. drowse/web/dist/icons/tab-analyzing-light-5.png +0 -0
  490. drowse/web/dist/icons/tab-analyzing-light-6.png +0 -0
  491. drowse/web/dist/icons/tab-analyzing-light-7.png +0 -0
  492. drowse/web/dist/icons/tab-analyzing-light-8.png +0 -0
  493. drowse/web/dist/icons/tab-analyzing-light-9.png +0 -0
  494. drowse/web/dist/icons/tab-chat-settings-dark.png +0 -0
  495. drowse/web/dist/icons/tab-chat-settings-light.png +0 -0
  496. drowse/web/dist/icons/tab-chat-settings.png +0 -0
  497. drowse/web/dist/icons/tab-chats-dark.png +0 -0
  498. drowse/web/dist/icons/tab-chats-light.png +0 -0
  499. drowse/web/dist/icons/tab-chats.png +0 -0
  500. drowse/web/dist/icons/tab-checking-0.png +0 -0
  501. drowse/web/dist/icons/tab-checking-1.png +0 -0
  502. drowse/web/dist/icons/tab-checking-2.png +0 -0
  503. drowse/web/dist/icons/tab-checking-dark-0.png +0 -0
  504. drowse/web/dist/icons/tab-checking-dark-1.png +0 -0
  505. drowse/web/dist/icons/tab-checking-dark-10.png +0 -0
  506. drowse/web/dist/icons/tab-checking-dark-11.png +0 -0
  507. drowse/web/dist/icons/tab-checking-dark-12.png +0 -0
  508. drowse/web/dist/icons/tab-checking-dark-13.png +0 -0
  509. drowse/web/dist/icons/tab-checking-dark-14.png +0 -0
  510. drowse/web/dist/icons/tab-checking-dark-15.png +0 -0
  511. drowse/web/dist/icons/tab-checking-dark-16.png +0 -0
  512. drowse/web/dist/icons/tab-checking-dark-17.png +0 -0
  513. drowse/web/dist/icons/tab-checking-dark-18.png +0 -0
  514. drowse/web/dist/icons/tab-checking-dark-19.png +0 -0
  515. drowse/web/dist/icons/tab-checking-dark-2.png +0 -0
  516. drowse/web/dist/icons/tab-checking-dark-20.png +0 -0
  517. drowse/web/dist/icons/tab-checking-dark-21.png +0 -0
  518. drowse/web/dist/icons/tab-checking-dark-22.png +0 -0
  519. drowse/web/dist/icons/tab-checking-dark-23.png +0 -0
  520. drowse/web/dist/icons/tab-checking-dark-24.png +0 -0
  521. drowse/web/dist/icons/tab-checking-dark-25.png +0 -0
  522. drowse/web/dist/icons/tab-checking-dark-26.png +0 -0
  523. drowse/web/dist/icons/tab-checking-dark-27.png +0 -0
  524. drowse/web/dist/icons/tab-checking-dark-28.png +0 -0
  525. drowse/web/dist/icons/tab-checking-dark-29.png +0 -0
  526. drowse/web/dist/icons/tab-checking-dark-3.png +0 -0
  527. drowse/web/dist/icons/tab-checking-dark-30.png +0 -0
  528. drowse/web/dist/icons/tab-checking-dark-31.png +0 -0
  529. drowse/web/dist/icons/tab-checking-dark-32.png +0 -0
  530. drowse/web/dist/icons/tab-checking-dark-33.png +0 -0
  531. drowse/web/dist/icons/tab-checking-dark-34.png +0 -0
  532. drowse/web/dist/icons/tab-checking-dark-35.png +0 -0
  533. drowse/web/dist/icons/tab-checking-dark-36.png +0 -0
  534. drowse/web/dist/icons/tab-checking-dark-37.png +0 -0
  535. drowse/web/dist/icons/tab-checking-dark-38.png +0 -0
  536. drowse/web/dist/icons/tab-checking-dark-39.png +0 -0
  537. drowse/web/dist/icons/tab-checking-dark-4.png +0 -0
  538. drowse/web/dist/icons/tab-checking-dark-40.png +0 -0
  539. drowse/web/dist/icons/tab-checking-dark-41.png +0 -0
  540. drowse/web/dist/icons/tab-checking-dark-42.png +0 -0
  541. drowse/web/dist/icons/tab-checking-dark-43.png +0 -0
  542. drowse/web/dist/icons/tab-checking-dark-44.png +0 -0
  543. drowse/web/dist/icons/tab-checking-dark-45.png +0 -0
  544. drowse/web/dist/icons/tab-checking-dark-46.png +0 -0
  545. drowse/web/dist/icons/tab-checking-dark-47.png +0 -0
  546. drowse/web/dist/icons/tab-checking-dark-5.png +0 -0
  547. drowse/web/dist/icons/tab-checking-dark-6.png +0 -0
  548. drowse/web/dist/icons/tab-checking-dark-7.png +0 -0
  549. drowse/web/dist/icons/tab-checking-dark-8.png +0 -0
  550. drowse/web/dist/icons/tab-checking-dark-9.png +0 -0
  551. drowse/web/dist/icons/tab-checking-light-0.png +0 -0
  552. drowse/web/dist/icons/tab-checking-light-1.png +0 -0
  553. drowse/web/dist/icons/tab-checking-light-10.png +0 -0
  554. drowse/web/dist/icons/tab-checking-light-11.png +0 -0
  555. drowse/web/dist/icons/tab-checking-light-12.png +0 -0
  556. drowse/web/dist/icons/tab-checking-light-13.png +0 -0
  557. drowse/web/dist/icons/tab-checking-light-14.png +0 -0
  558. drowse/web/dist/icons/tab-checking-light-15.png +0 -0
  559. drowse/web/dist/icons/tab-checking-light-16.png +0 -0
  560. drowse/web/dist/icons/tab-checking-light-17.png +0 -0
  561. drowse/web/dist/icons/tab-checking-light-18.png +0 -0
  562. drowse/web/dist/icons/tab-checking-light-19.png +0 -0
  563. drowse/web/dist/icons/tab-checking-light-2.png +0 -0
  564. drowse/web/dist/icons/tab-checking-light-20.png +0 -0
  565. drowse/web/dist/icons/tab-checking-light-21.png +0 -0
  566. drowse/web/dist/icons/tab-checking-light-22.png +0 -0
  567. drowse/web/dist/icons/tab-checking-light-23.png +0 -0
  568. drowse/web/dist/icons/tab-checking-light-24.png +0 -0
  569. drowse/web/dist/icons/tab-checking-light-25.png +0 -0
  570. drowse/web/dist/icons/tab-checking-light-26.png +0 -0
  571. drowse/web/dist/icons/tab-checking-light-27.png +0 -0
  572. drowse/web/dist/icons/tab-checking-light-28.png +0 -0
  573. drowse/web/dist/icons/tab-checking-light-29.png +0 -0
  574. drowse/web/dist/icons/tab-checking-light-3.png +0 -0
  575. drowse/web/dist/icons/tab-checking-light-30.png +0 -0
  576. drowse/web/dist/icons/tab-checking-light-31.png +0 -0
  577. drowse/web/dist/icons/tab-checking-light-32.png +0 -0
  578. drowse/web/dist/icons/tab-checking-light-33.png +0 -0
  579. drowse/web/dist/icons/tab-checking-light-34.png +0 -0
  580. drowse/web/dist/icons/tab-checking-light-35.png +0 -0
  581. drowse/web/dist/icons/tab-checking-light-36.png +0 -0
  582. drowse/web/dist/icons/tab-checking-light-37.png +0 -0
  583. drowse/web/dist/icons/tab-checking-light-38.png +0 -0
  584. drowse/web/dist/icons/tab-checking-light-39.png +0 -0
  585. drowse/web/dist/icons/tab-checking-light-4.png +0 -0
  586. drowse/web/dist/icons/tab-checking-light-40.png +0 -0
  587. drowse/web/dist/icons/tab-checking-light-41.png +0 -0
  588. drowse/web/dist/icons/tab-checking-light-42.png +0 -0
  589. drowse/web/dist/icons/tab-checking-light-43.png +0 -0
  590. drowse/web/dist/icons/tab-checking-light-44.png +0 -0
  591. drowse/web/dist/icons/tab-checking-light-45.png +0 -0
  592. drowse/web/dist/icons/tab-checking-light-46.png +0 -0
  593. drowse/web/dist/icons/tab-checking-light-47.png +0 -0
  594. drowse/web/dist/icons/tab-checking-light-5.png +0 -0
  595. drowse/web/dist/icons/tab-checking-light-6.png +0 -0
  596. drowse/web/dist/icons/tab-checking-light-7.png +0 -0
  597. drowse/web/dist/icons/tab-checking-light-8.png +0 -0
  598. drowse/web/dist/icons/tab-checking-light-9.png +0 -0
  599. drowse/web/dist/icons/tab-comparison-dark.png +0 -0
  600. drowse/web/dist/icons/tab-comparison-light.png +0 -0
  601. drowse/web/dist/icons/tab-comparison.png +0 -0
  602. drowse/web/dist/icons/tab-controls-dark.png +0 -0
  603. drowse/web/dist/icons/tab-controls-light.png +0 -0
  604. drowse/web/dist/icons/tab-controls.png +0 -0
  605. drowse/web/dist/icons/tab-conversation-dark.png +0 -0
  606. drowse/web/dist/icons/tab-conversation-light.png +0 -0
  607. drowse/web/dist/icons/tab-conversation.png +0 -0
  608. drowse/web/dist/icons/tab-credits-dark.png +0 -0
  609. drowse/web/dist/icons/tab-credits-light.png +0 -0
  610. drowse/web/dist/icons/tab-credits.png +0 -0
  611. drowse/web/dist/icons/tab-downloading-0.png +0 -0
  612. drowse/web/dist/icons/tab-downloading-1.png +0 -0
  613. drowse/web/dist/icons/tab-downloading-2.png +0 -0
  614. drowse/web/dist/icons/tab-downloading-dark-0.png +0 -0
  615. drowse/web/dist/icons/tab-downloading-dark-1.png +0 -0
  616. drowse/web/dist/icons/tab-downloading-dark-10.png +0 -0
  617. drowse/web/dist/icons/tab-downloading-dark-11.png +0 -0
  618. drowse/web/dist/icons/tab-downloading-dark-12.png +0 -0
  619. drowse/web/dist/icons/tab-downloading-dark-13.png +0 -0
  620. drowse/web/dist/icons/tab-downloading-dark-14.png +0 -0
  621. drowse/web/dist/icons/tab-downloading-dark-15.png +0 -0
  622. drowse/web/dist/icons/tab-downloading-dark-16.png +0 -0
  623. drowse/web/dist/icons/tab-downloading-dark-17.png +0 -0
  624. drowse/web/dist/icons/tab-downloading-dark-18.png +0 -0
  625. drowse/web/dist/icons/tab-downloading-dark-19.png +0 -0
  626. drowse/web/dist/icons/tab-downloading-dark-2.png +0 -0
  627. drowse/web/dist/icons/tab-downloading-dark-20.png +0 -0
  628. drowse/web/dist/icons/tab-downloading-dark-21.png +0 -0
  629. drowse/web/dist/icons/tab-downloading-dark-22.png +0 -0
  630. drowse/web/dist/icons/tab-downloading-dark-23.png +0 -0
  631. drowse/web/dist/icons/tab-downloading-dark-24.png +0 -0
  632. drowse/web/dist/icons/tab-downloading-dark-25.png +0 -0
  633. drowse/web/dist/icons/tab-downloading-dark-26.png +0 -0
  634. drowse/web/dist/icons/tab-downloading-dark-27.png +0 -0
  635. drowse/web/dist/icons/tab-downloading-dark-28.png +0 -0
  636. drowse/web/dist/icons/tab-downloading-dark-29.png +0 -0
  637. drowse/web/dist/icons/tab-downloading-dark-3.png +0 -0
  638. drowse/web/dist/icons/tab-downloading-dark-30.png +0 -0
  639. drowse/web/dist/icons/tab-downloading-dark-31.png +0 -0
  640. drowse/web/dist/icons/tab-downloading-dark-32.png +0 -0
  641. drowse/web/dist/icons/tab-downloading-dark-33.png +0 -0
  642. drowse/web/dist/icons/tab-downloading-dark-34.png +0 -0
  643. drowse/web/dist/icons/tab-downloading-dark-35.png +0 -0
  644. drowse/web/dist/icons/tab-downloading-dark-36.png +0 -0
  645. drowse/web/dist/icons/tab-downloading-dark-37.png +0 -0
  646. drowse/web/dist/icons/tab-downloading-dark-38.png +0 -0
  647. drowse/web/dist/icons/tab-downloading-dark-39.png +0 -0
  648. drowse/web/dist/icons/tab-downloading-dark-4.png +0 -0
  649. drowse/web/dist/icons/tab-downloading-dark-40.png +0 -0
  650. drowse/web/dist/icons/tab-downloading-dark-41.png +0 -0
  651. drowse/web/dist/icons/tab-downloading-dark-42.png +0 -0
  652. drowse/web/dist/icons/tab-downloading-dark-43.png +0 -0
  653. drowse/web/dist/icons/tab-downloading-dark-44.png +0 -0
  654. drowse/web/dist/icons/tab-downloading-dark-45.png +0 -0
  655. drowse/web/dist/icons/tab-downloading-dark-46.png +0 -0
  656. drowse/web/dist/icons/tab-downloading-dark-47.png +0 -0
  657. drowse/web/dist/icons/tab-downloading-dark-5.png +0 -0
  658. drowse/web/dist/icons/tab-downloading-dark-6.png +0 -0
  659. drowse/web/dist/icons/tab-downloading-dark-7.png +0 -0
  660. drowse/web/dist/icons/tab-downloading-dark-8.png +0 -0
  661. drowse/web/dist/icons/tab-downloading-dark-9.png +0 -0
  662. drowse/web/dist/icons/tab-downloading-light-0.png +0 -0
  663. drowse/web/dist/icons/tab-downloading-light-1.png +0 -0
  664. drowse/web/dist/icons/tab-downloading-light-10.png +0 -0
  665. drowse/web/dist/icons/tab-downloading-light-11.png +0 -0
  666. drowse/web/dist/icons/tab-downloading-light-12.png +0 -0
  667. drowse/web/dist/icons/tab-downloading-light-13.png +0 -0
  668. drowse/web/dist/icons/tab-downloading-light-14.png +0 -0
  669. drowse/web/dist/icons/tab-downloading-light-15.png +0 -0
  670. drowse/web/dist/icons/tab-downloading-light-16.png +0 -0
  671. drowse/web/dist/icons/tab-downloading-light-17.png +0 -0
  672. drowse/web/dist/icons/tab-downloading-light-18.png +0 -0
  673. drowse/web/dist/icons/tab-downloading-light-19.png +0 -0
  674. drowse/web/dist/icons/tab-downloading-light-2.png +0 -0
  675. drowse/web/dist/icons/tab-downloading-light-20.png +0 -0
  676. drowse/web/dist/icons/tab-downloading-light-21.png +0 -0
  677. drowse/web/dist/icons/tab-downloading-light-22.png +0 -0
  678. drowse/web/dist/icons/tab-downloading-light-23.png +0 -0
  679. drowse/web/dist/icons/tab-downloading-light-24.png +0 -0
  680. drowse/web/dist/icons/tab-downloading-light-25.png +0 -0
  681. drowse/web/dist/icons/tab-downloading-light-26.png +0 -0
  682. drowse/web/dist/icons/tab-downloading-light-27.png +0 -0
  683. drowse/web/dist/icons/tab-downloading-light-28.png +0 -0
  684. drowse/web/dist/icons/tab-downloading-light-29.png +0 -0
  685. drowse/web/dist/icons/tab-downloading-light-3.png +0 -0
  686. drowse/web/dist/icons/tab-downloading-light-30.png +0 -0
  687. drowse/web/dist/icons/tab-downloading-light-31.png +0 -0
  688. drowse/web/dist/icons/tab-downloading-light-32.png +0 -0
  689. drowse/web/dist/icons/tab-downloading-light-33.png +0 -0
  690. drowse/web/dist/icons/tab-downloading-light-34.png +0 -0
  691. drowse/web/dist/icons/tab-downloading-light-35.png +0 -0
  692. drowse/web/dist/icons/tab-downloading-light-36.png +0 -0
  693. drowse/web/dist/icons/tab-downloading-light-37.png +0 -0
  694. drowse/web/dist/icons/tab-downloading-light-38.png +0 -0
  695. drowse/web/dist/icons/tab-downloading-light-39.png +0 -0
  696. drowse/web/dist/icons/tab-downloading-light-4.png +0 -0
  697. drowse/web/dist/icons/tab-downloading-light-40.png +0 -0
  698. drowse/web/dist/icons/tab-downloading-light-41.png +0 -0
  699. drowse/web/dist/icons/tab-downloading-light-42.png +0 -0
  700. drowse/web/dist/icons/tab-downloading-light-43.png +0 -0
  701. drowse/web/dist/icons/tab-downloading-light-44.png +0 -0
  702. drowse/web/dist/icons/tab-downloading-light-45.png +0 -0
  703. drowse/web/dist/icons/tab-downloading-light-46.png +0 -0
  704. drowse/web/dist/icons/tab-downloading-light-47.png +0 -0
  705. drowse/web/dist/icons/tab-downloading-light-5.png +0 -0
  706. drowse/web/dist/icons/tab-downloading-light-6.png +0 -0
  707. drowse/web/dist/icons/tab-downloading-light-7.png +0 -0
  708. drowse/web/dist/icons/tab-downloading-light-8.png +0 -0
  709. drowse/web/dist/icons/tab-downloading-light-9.png +0 -0
  710. drowse/web/dist/icons/tab-error-dark.png +0 -0
  711. drowse/web/dist/icons/tab-error-light.png +0 -0
  712. drowse/web/dist/icons/tab-error.png +0 -0
  713. drowse/web/dist/icons/tab-home-dark.png +0 -0
  714. drowse/web/dist/icons/tab-home-light.png +0 -0
  715. drowse/web/dist/icons/tab-home.png +0 -0
  716. drowse/web/dist/icons/tab-loading-0.png +0 -0
  717. drowse/web/dist/icons/tab-loading-1.png +0 -0
  718. drowse/web/dist/icons/tab-loading-2.png +0 -0
  719. drowse/web/dist/icons/tab-loading-dark-0.png +0 -0
  720. drowse/web/dist/icons/tab-loading-dark-1.png +0 -0
  721. drowse/web/dist/icons/tab-loading-dark-10.png +0 -0
  722. drowse/web/dist/icons/tab-loading-dark-11.png +0 -0
  723. drowse/web/dist/icons/tab-loading-dark-12.png +0 -0
  724. drowse/web/dist/icons/tab-loading-dark-13.png +0 -0
  725. drowse/web/dist/icons/tab-loading-dark-14.png +0 -0
  726. drowse/web/dist/icons/tab-loading-dark-15.png +0 -0
  727. drowse/web/dist/icons/tab-loading-dark-16.png +0 -0
  728. drowse/web/dist/icons/tab-loading-dark-17.png +0 -0
  729. drowse/web/dist/icons/tab-loading-dark-18.png +0 -0
  730. drowse/web/dist/icons/tab-loading-dark-19.png +0 -0
  731. drowse/web/dist/icons/tab-loading-dark-2.png +0 -0
  732. drowse/web/dist/icons/tab-loading-dark-20.png +0 -0
  733. drowse/web/dist/icons/tab-loading-dark-21.png +0 -0
  734. drowse/web/dist/icons/tab-loading-dark-22.png +0 -0
  735. drowse/web/dist/icons/tab-loading-dark-23.png +0 -0
  736. drowse/web/dist/icons/tab-loading-dark-24.png +0 -0
  737. drowse/web/dist/icons/tab-loading-dark-25.png +0 -0
  738. drowse/web/dist/icons/tab-loading-dark-26.png +0 -0
  739. drowse/web/dist/icons/tab-loading-dark-27.png +0 -0
  740. drowse/web/dist/icons/tab-loading-dark-28.png +0 -0
  741. drowse/web/dist/icons/tab-loading-dark-29.png +0 -0
  742. drowse/web/dist/icons/tab-loading-dark-3.png +0 -0
  743. drowse/web/dist/icons/tab-loading-dark-30.png +0 -0
  744. drowse/web/dist/icons/tab-loading-dark-31.png +0 -0
  745. drowse/web/dist/icons/tab-loading-dark-32.png +0 -0
  746. drowse/web/dist/icons/tab-loading-dark-33.png +0 -0
  747. drowse/web/dist/icons/tab-loading-dark-34.png +0 -0
  748. drowse/web/dist/icons/tab-loading-dark-35.png +0 -0
  749. drowse/web/dist/icons/tab-loading-dark-36.png +0 -0
  750. drowse/web/dist/icons/tab-loading-dark-37.png +0 -0
  751. drowse/web/dist/icons/tab-loading-dark-38.png +0 -0
  752. drowse/web/dist/icons/tab-loading-dark-39.png +0 -0
  753. drowse/web/dist/icons/tab-loading-dark-4.png +0 -0
  754. drowse/web/dist/icons/tab-loading-dark-40.png +0 -0
  755. drowse/web/dist/icons/tab-loading-dark-41.png +0 -0
  756. drowse/web/dist/icons/tab-loading-dark-42.png +0 -0
  757. drowse/web/dist/icons/tab-loading-dark-43.png +0 -0
  758. drowse/web/dist/icons/tab-loading-dark-44.png +0 -0
  759. drowse/web/dist/icons/tab-loading-dark-45.png +0 -0
  760. drowse/web/dist/icons/tab-loading-dark-46.png +0 -0
  761. drowse/web/dist/icons/tab-loading-dark-47.png +0 -0
  762. drowse/web/dist/icons/tab-loading-dark-5.png +0 -0
  763. drowse/web/dist/icons/tab-loading-dark-6.png +0 -0
  764. drowse/web/dist/icons/tab-loading-dark-7.png +0 -0
  765. drowse/web/dist/icons/tab-loading-dark-8.png +0 -0
  766. drowse/web/dist/icons/tab-loading-dark-9.png +0 -0
  767. drowse/web/dist/icons/tab-loading-light-0.png +0 -0
  768. drowse/web/dist/icons/tab-loading-light-1.png +0 -0
  769. drowse/web/dist/icons/tab-loading-light-10.png +0 -0
  770. drowse/web/dist/icons/tab-loading-light-11.png +0 -0
  771. drowse/web/dist/icons/tab-loading-light-12.png +0 -0
  772. drowse/web/dist/icons/tab-loading-light-13.png +0 -0
  773. drowse/web/dist/icons/tab-loading-light-14.png +0 -0
  774. drowse/web/dist/icons/tab-loading-light-15.png +0 -0
  775. drowse/web/dist/icons/tab-loading-light-16.png +0 -0
  776. drowse/web/dist/icons/tab-loading-light-17.png +0 -0
  777. drowse/web/dist/icons/tab-loading-light-18.png +0 -0
  778. drowse/web/dist/icons/tab-loading-light-19.png +0 -0
  779. drowse/web/dist/icons/tab-loading-light-2.png +0 -0
  780. drowse/web/dist/icons/tab-loading-light-20.png +0 -0
  781. drowse/web/dist/icons/tab-loading-light-21.png +0 -0
  782. drowse/web/dist/icons/tab-loading-light-22.png +0 -0
  783. drowse/web/dist/icons/tab-loading-light-23.png +0 -0
  784. drowse/web/dist/icons/tab-loading-light-24.png +0 -0
  785. drowse/web/dist/icons/tab-loading-light-25.png +0 -0
  786. drowse/web/dist/icons/tab-loading-light-26.png +0 -0
  787. drowse/web/dist/icons/tab-loading-light-27.png +0 -0
  788. drowse/web/dist/icons/tab-loading-light-28.png +0 -0
  789. drowse/web/dist/icons/tab-loading-light-29.png +0 -0
  790. drowse/web/dist/icons/tab-loading-light-3.png +0 -0
  791. drowse/web/dist/icons/tab-loading-light-30.png +0 -0
  792. drowse/web/dist/icons/tab-loading-light-31.png +0 -0
  793. drowse/web/dist/icons/tab-loading-light-32.png +0 -0
  794. drowse/web/dist/icons/tab-loading-light-33.png +0 -0
  795. drowse/web/dist/icons/tab-loading-light-34.png +0 -0
  796. drowse/web/dist/icons/tab-loading-light-35.png +0 -0
  797. drowse/web/dist/icons/tab-loading-light-36.png +0 -0
  798. drowse/web/dist/icons/tab-loading-light-37.png +0 -0
  799. drowse/web/dist/icons/tab-loading-light-38.png +0 -0
  800. drowse/web/dist/icons/tab-loading-light-39.png +0 -0
  801. drowse/web/dist/icons/tab-loading-light-4.png +0 -0
  802. drowse/web/dist/icons/tab-loading-light-40.png +0 -0
  803. drowse/web/dist/icons/tab-loading-light-41.png +0 -0
  804. drowse/web/dist/icons/tab-loading-light-42.png +0 -0
  805. drowse/web/dist/icons/tab-loading-light-43.png +0 -0
  806. drowse/web/dist/icons/tab-loading-light-44.png +0 -0
  807. drowse/web/dist/icons/tab-loading-light-45.png +0 -0
  808. drowse/web/dist/icons/tab-loading-light-46.png +0 -0
  809. drowse/web/dist/icons/tab-loading-light-47.png +0 -0
  810. drowse/web/dist/icons/tab-loading-light-5.png +0 -0
  811. drowse/web/dist/icons/tab-loading-light-6.png +0 -0
  812. drowse/web/dist/icons/tab-loading-light-7.png +0 -0
  813. drowse/web/dist/icons/tab-loading-light-8.png +0 -0
  814. drowse/web/dist/icons/tab-loading-light-9.png +0 -0
  815. drowse/web/dist/icons/tab-loom-dark.png +0 -0
  816. drowse/web/dist/icons/tab-loom-light.png +0 -0
  817. drowse/web/dist/icons/tab-loom-working-0.png +0 -0
  818. drowse/web/dist/icons/tab-loom-working-1.png +0 -0
  819. drowse/web/dist/icons/tab-loom-working-2.png +0 -0
  820. drowse/web/dist/icons/tab-loom-working-dark-0.png +0 -0
  821. drowse/web/dist/icons/tab-loom-working-dark-1.png +0 -0
  822. drowse/web/dist/icons/tab-loom-working-dark-10.png +0 -0
  823. drowse/web/dist/icons/tab-loom-working-dark-11.png +0 -0
  824. drowse/web/dist/icons/tab-loom-working-dark-12.png +0 -0
  825. drowse/web/dist/icons/tab-loom-working-dark-13.png +0 -0
  826. drowse/web/dist/icons/tab-loom-working-dark-14.png +0 -0
  827. drowse/web/dist/icons/tab-loom-working-dark-15.png +0 -0
  828. drowse/web/dist/icons/tab-loom-working-dark-16.png +0 -0
  829. drowse/web/dist/icons/tab-loom-working-dark-17.png +0 -0
  830. drowse/web/dist/icons/tab-loom-working-dark-18.png +0 -0
  831. drowse/web/dist/icons/tab-loom-working-dark-19.png +0 -0
  832. drowse/web/dist/icons/tab-loom-working-dark-2.png +0 -0
  833. drowse/web/dist/icons/tab-loom-working-dark-20.png +0 -0
  834. drowse/web/dist/icons/tab-loom-working-dark-21.png +0 -0
  835. drowse/web/dist/icons/tab-loom-working-dark-22.png +0 -0
  836. drowse/web/dist/icons/tab-loom-working-dark-23.png +0 -0
  837. drowse/web/dist/icons/tab-loom-working-dark-24.png +0 -0
  838. drowse/web/dist/icons/tab-loom-working-dark-25.png +0 -0
  839. drowse/web/dist/icons/tab-loom-working-dark-26.png +0 -0
  840. drowse/web/dist/icons/tab-loom-working-dark-27.png +0 -0
  841. drowse/web/dist/icons/tab-loom-working-dark-28.png +0 -0
  842. drowse/web/dist/icons/tab-loom-working-dark-29.png +0 -0
  843. drowse/web/dist/icons/tab-loom-working-dark-3.png +0 -0
  844. drowse/web/dist/icons/tab-loom-working-dark-30.png +0 -0
  845. drowse/web/dist/icons/tab-loom-working-dark-31.png +0 -0
  846. drowse/web/dist/icons/tab-loom-working-dark-32.png +0 -0
  847. drowse/web/dist/icons/tab-loom-working-dark-33.png +0 -0
  848. drowse/web/dist/icons/tab-loom-working-dark-34.png +0 -0
  849. drowse/web/dist/icons/tab-loom-working-dark-35.png +0 -0
  850. drowse/web/dist/icons/tab-loom-working-dark-36.png +0 -0
  851. drowse/web/dist/icons/tab-loom-working-dark-37.png +0 -0
  852. drowse/web/dist/icons/tab-loom-working-dark-38.png +0 -0
  853. drowse/web/dist/icons/tab-loom-working-dark-39.png +0 -0
  854. drowse/web/dist/icons/tab-loom-working-dark-4.png +0 -0
  855. drowse/web/dist/icons/tab-loom-working-dark-40.png +0 -0
  856. drowse/web/dist/icons/tab-loom-working-dark-41.png +0 -0
  857. drowse/web/dist/icons/tab-loom-working-dark-42.png +0 -0
  858. drowse/web/dist/icons/tab-loom-working-dark-43.png +0 -0
  859. drowse/web/dist/icons/tab-loom-working-dark-44.png +0 -0
  860. drowse/web/dist/icons/tab-loom-working-dark-45.png +0 -0
  861. drowse/web/dist/icons/tab-loom-working-dark-46.png +0 -0
  862. drowse/web/dist/icons/tab-loom-working-dark-47.png +0 -0
  863. drowse/web/dist/icons/tab-loom-working-dark-5.png +0 -0
  864. drowse/web/dist/icons/tab-loom-working-dark-6.png +0 -0
  865. drowse/web/dist/icons/tab-loom-working-dark-7.png +0 -0
  866. drowse/web/dist/icons/tab-loom-working-dark-8.png +0 -0
  867. drowse/web/dist/icons/tab-loom-working-dark-9.png +0 -0
  868. drowse/web/dist/icons/tab-loom-working-light-0.png +0 -0
  869. drowse/web/dist/icons/tab-loom-working-light-1.png +0 -0
  870. drowse/web/dist/icons/tab-loom-working-light-10.png +0 -0
  871. drowse/web/dist/icons/tab-loom-working-light-11.png +0 -0
  872. drowse/web/dist/icons/tab-loom-working-light-12.png +0 -0
  873. drowse/web/dist/icons/tab-loom-working-light-13.png +0 -0
  874. drowse/web/dist/icons/tab-loom-working-light-14.png +0 -0
  875. drowse/web/dist/icons/tab-loom-working-light-15.png +0 -0
  876. drowse/web/dist/icons/tab-loom-working-light-16.png +0 -0
  877. drowse/web/dist/icons/tab-loom-working-light-17.png +0 -0
  878. drowse/web/dist/icons/tab-loom-working-light-18.png +0 -0
  879. drowse/web/dist/icons/tab-loom-working-light-19.png +0 -0
  880. drowse/web/dist/icons/tab-loom-working-light-2.png +0 -0
  881. drowse/web/dist/icons/tab-loom-working-light-20.png +0 -0
  882. drowse/web/dist/icons/tab-loom-working-light-21.png +0 -0
  883. drowse/web/dist/icons/tab-loom-working-light-22.png +0 -0
  884. drowse/web/dist/icons/tab-loom-working-light-23.png +0 -0
  885. drowse/web/dist/icons/tab-loom-working-light-24.png +0 -0
  886. drowse/web/dist/icons/tab-loom-working-light-25.png +0 -0
  887. drowse/web/dist/icons/tab-loom-working-light-26.png +0 -0
  888. drowse/web/dist/icons/tab-loom-working-light-27.png +0 -0
  889. drowse/web/dist/icons/tab-loom-working-light-28.png +0 -0
  890. drowse/web/dist/icons/tab-loom-working-light-29.png +0 -0
  891. drowse/web/dist/icons/tab-loom-working-light-3.png +0 -0
  892. drowse/web/dist/icons/tab-loom-working-light-30.png +0 -0
  893. drowse/web/dist/icons/tab-loom-working-light-31.png +0 -0
  894. drowse/web/dist/icons/tab-loom-working-light-32.png +0 -0
  895. drowse/web/dist/icons/tab-loom-working-light-33.png +0 -0
  896. drowse/web/dist/icons/tab-loom-working-light-34.png +0 -0
  897. drowse/web/dist/icons/tab-loom-working-light-35.png +0 -0
  898. drowse/web/dist/icons/tab-loom-working-light-36.png +0 -0
  899. drowse/web/dist/icons/tab-loom-working-light-37.png +0 -0
  900. drowse/web/dist/icons/tab-loom-working-light-38.png +0 -0
  901. drowse/web/dist/icons/tab-loom-working-light-39.png +0 -0
  902. drowse/web/dist/icons/tab-loom-working-light-4.png +0 -0
  903. drowse/web/dist/icons/tab-loom-working-light-40.png +0 -0
  904. drowse/web/dist/icons/tab-loom-working-light-41.png +0 -0
  905. drowse/web/dist/icons/tab-loom-working-light-42.png +0 -0
  906. drowse/web/dist/icons/tab-loom-working-light-43.png +0 -0
  907. drowse/web/dist/icons/tab-loom-working-light-44.png +0 -0
  908. drowse/web/dist/icons/tab-loom-working-light-45.png +0 -0
  909. drowse/web/dist/icons/tab-loom-working-light-46.png +0 -0
  910. drowse/web/dist/icons/tab-loom-working-light-47.png +0 -0
  911. drowse/web/dist/icons/tab-loom-working-light-5.png +0 -0
  912. drowse/web/dist/icons/tab-loom-working-light-6.png +0 -0
  913. drowse/web/dist/icons/tab-loom-working-light-7.png +0 -0
  914. drowse/web/dist/icons/tab-loom-working-light-8.png +0 -0
  915. drowse/web/dist/icons/tab-loom-working-light-9.png +0 -0
  916. drowse/web/dist/icons/tab-loom.png +0 -0
  917. drowse/web/dist/icons/tab-models-dark.png +0 -0
  918. drowse/web/dist/icons/tab-models-light.png +0 -0
  919. drowse/web/dist/icons/tab-models.png +0 -0
  920. drowse/web/dist/icons/tab-offline-dark.png +0 -0
  921. drowse/web/dist/icons/tab-offline-light.png +0 -0
  922. drowse/web/dist/icons/tab-offline.png +0 -0
  923. drowse/web/dist/icons/tab-paused-dark.png +0 -0
  924. drowse/web/dist/icons/tab-paused-light.png +0 -0
  925. drowse/web/dist/icons/tab-paused.png +0 -0
  926. drowse/web/dist/icons/tab-ready-dark.png +0 -0
  927. drowse/web/dist/icons/tab-ready-light.png +0 -0
  928. drowse/web/dist/icons/tab-ready.png +0 -0
  929. drowse/web/dist/icons/tab-saving-0.png +0 -0
  930. drowse/web/dist/icons/tab-saving-1.png +0 -0
  931. drowse/web/dist/icons/tab-saving-2.png +0 -0
  932. drowse/web/dist/icons/tab-saving-dark-0.png +0 -0
  933. drowse/web/dist/icons/tab-saving-dark-1.png +0 -0
  934. drowse/web/dist/icons/tab-saving-dark-10.png +0 -0
  935. drowse/web/dist/icons/tab-saving-dark-11.png +0 -0
  936. drowse/web/dist/icons/tab-saving-dark-12.png +0 -0
  937. drowse/web/dist/icons/tab-saving-dark-13.png +0 -0
  938. drowse/web/dist/icons/tab-saving-dark-14.png +0 -0
  939. drowse/web/dist/icons/tab-saving-dark-15.png +0 -0
  940. drowse/web/dist/icons/tab-saving-dark-16.png +0 -0
  941. drowse/web/dist/icons/tab-saving-dark-17.png +0 -0
  942. drowse/web/dist/icons/tab-saving-dark-18.png +0 -0
  943. drowse/web/dist/icons/tab-saving-dark-19.png +0 -0
  944. drowse/web/dist/icons/tab-saving-dark-2.png +0 -0
  945. drowse/web/dist/icons/tab-saving-dark-20.png +0 -0
  946. drowse/web/dist/icons/tab-saving-dark-21.png +0 -0
  947. drowse/web/dist/icons/tab-saving-dark-22.png +0 -0
  948. drowse/web/dist/icons/tab-saving-dark-23.png +0 -0
  949. drowse/web/dist/icons/tab-saving-dark-24.png +0 -0
  950. drowse/web/dist/icons/tab-saving-dark-25.png +0 -0
  951. drowse/web/dist/icons/tab-saving-dark-26.png +0 -0
  952. drowse/web/dist/icons/tab-saving-dark-27.png +0 -0
  953. drowse/web/dist/icons/tab-saving-dark-28.png +0 -0
  954. drowse/web/dist/icons/tab-saving-dark-29.png +0 -0
  955. drowse/web/dist/icons/tab-saving-dark-3.png +0 -0
  956. drowse/web/dist/icons/tab-saving-dark-30.png +0 -0
  957. drowse/web/dist/icons/tab-saving-dark-31.png +0 -0
  958. drowse/web/dist/icons/tab-saving-dark-32.png +0 -0
  959. drowse/web/dist/icons/tab-saving-dark-33.png +0 -0
  960. drowse/web/dist/icons/tab-saving-dark-34.png +0 -0
  961. drowse/web/dist/icons/tab-saving-dark-35.png +0 -0
  962. drowse/web/dist/icons/tab-saving-dark-36.png +0 -0
  963. drowse/web/dist/icons/tab-saving-dark-37.png +0 -0
  964. drowse/web/dist/icons/tab-saving-dark-38.png +0 -0
  965. drowse/web/dist/icons/tab-saving-dark-39.png +0 -0
  966. drowse/web/dist/icons/tab-saving-dark-4.png +0 -0
  967. drowse/web/dist/icons/tab-saving-dark-40.png +0 -0
  968. drowse/web/dist/icons/tab-saving-dark-41.png +0 -0
  969. drowse/web/dist/icons/tab-saving-dark-42.png +0 -0
  970. drowse/web/dist/icons/tab-saving-dark-43.png +0 -0
  971. drowse/web/dist/icons/tab-saving-dark-44.png +0 -0
  972. drowse/web/dist/icons/tab-saving-dark-45.png +0 -0
  973. drowse/web/dist/icons/tab-saving-dark-46.png +0 -0
  974. drowse/web/dist/icons/tab-saving-dark-47.png +0 -0
  975. drowse/web/dist/icons/tab-saving-dark-5.png +0 -0
  976. drowse/web/dist/icons/tab-saving-dark-6.png +0 -0
  977. drowse/web/dist/icons/tab-saving-dark-7.png +0 -0
  978. drowse/web/dist/icons/tab-saving-dark-8.png +0 -0
  979. drowse/web/dist/icons/tab-saving-dark-9.png +0 -0
  980. drowse/web/dist/icons/tab-saving-light-0.png +0 -0
  981. drowse/web/dist/icons/tab-saving-light-1.png +0 -0
  982. drowse/web/dist/icons/tab-saving-light-10.png +0 -0
  983. drowse/web/dist/icons/tab-saving-light-11.png +0 -0
  984. drowse/web/dist/icons/tab-saving-light-12.png +0 -0
  985. drowse/web/dist/icons/tab-saving-light-13.png +0 -0
  986. drowse/web/dist/icons/tab-saving-light-14.png +0 -0
  987. drowse/web/dist/icons/tab-saving-light-15.png +0 -0
  988. drowse/web/dist/icons/tab-saving-light-16.png +0 -0
  989. drowse/web/dist/icons/tab-saving-light-17.png +0 -0
  990. drowse/web/dist/icons/tab-saving-light-18.png +0 -0
  991. drowse/web/dist/icons/tab-saving-light-19.png +0 -0
  992. drowse/web/dist/icons/tab-saving-light-2.png +0 -0
  993. drowse/web/dist/icons/tab-saving-light-20.png +0 -0
  994. drowse/web/dist/icons/tab-saving-light-21.png +0 -0
  995. drowse/web/dist/icons/tab-saving-light-22.png +0 -0
  996. drowse/web/dist/icons/tab-saving-light-23.png +0 -0
  997. drowse/web/dist/icons/tab-saving-light-24.png +0 -0
  998. drowse/web/dist/icons/tab-saving-light-25.png +0 -0
  999. drowse/web/dist/icons/tab-saving-light-26.png +0 -0
  1000. drowse/web/dist/icons/tab-saving-light-27.png +0 -0
  1001. drowse/web/dist/icons/tab-saving-light-28.png +0 -0
  1002. drowse/web/dist/icons/tab-saving-light-29.png +0 -0
  1003. drowse/web/dist/icons/tab-saving-light-3.png +0 -0
  1004. drowse/web/dist/icons/tab-saving-light-30.png +0 -0
  1005. drowse/web/dist/icons/tab-saving-light-31.png +0 -0
  1006. drowse/web/dist/icons/tab-saving-light-32.png +0 -0
  1007. drowse/web/dist/icons/tab-saving-light-33.png +0 -0
  1008. drowse/web/dist/icons/tab-saving-light-34.png +0 -0
  1009. drowse/web/dist/icons/tab-saving-light-35.png +0 -0
  1010. drowse/web/dist/icons/tab-saving-light-36.png +0 -0
  1011. drowse/web/dist/icons/tab-saving-light-37.png +0 -0
  1012. drowse/web/dist/icons/tab-saving-light-38.png +0 -0
  1013. drowse/web/dist/icons/tab-saving-light-39.png +0 -0
  1014. drowse/web/dist/icons/tab-saving-light-4.png +0 -0
  1015. drowse/web/dist/icons/tab-saving-light-40.png +0 -0
  1016. drowse/web/dist/icons/tab-saving-light-41.png +0 -0
  1017. drowse/web/dist/icons/tab-saving-light-42.png +0 -0
  1018. drowse/web/dist/icons/tab-saving-light-43.png +0 -0
  1019. drowse/web/dist/icons/tab-saving-light-44.png +0 -0
  1020. drowse/web/dist/icons/tab-saving-light-45.png +0 -0
  1021. drowse/web/dist/icons/tab-saving-light-46.png +0 -0
  1022. drowse/web/dist/icons/tab-saving-light-47.png +0 -0
  1023. drowse/web/dist/icons/tab-saving-light-5.png +0 -0
  1024. drowse/web/dist/icons/tab-saving-light-6.png +0 -0
  1025. drowse/web/dist/icons/tab-saving-light-7.png +0 -0
  1026. drowse/web/dist/icons/tab-saving-light-8.png +0 -0
  1027. drowse/web/dist/icons/tab-saving-light-9.png +0 -0
  1028. drowse/web/dist/icons/tab-thinking-0.png +0 -0
  1029. drowse/web/dist/icons/tab-thinking-1.png +0 -0
  1030. drowse/web/dist/icons/tab-thinking-2.png +0 -0
  1031. drowse/web/dist/icons/tab-thinking-dark-0.png +0 -0
  1032. drowse/web/dist/icons/tab-thinking-dark-1.png +0 -0
  1033. drowse/web/dist/icons/tab-thinking-dark-10.png +0 -0
  1034. drowse/web/dist/icons/tab-thinking-dark-11.png +0 -0
  1035. drowse/web/dist/icons/tab-thinking-dark-12.png +0 -0
  1036. drowse/web/dist/icons/tab-thinking-dark-13.png +0 -0
  1037. drowse/web/dist/icons/tab-thinking-dark-14.png +0 -0
  1038. drowse/web/dist/icons/tab-thinking-dark-15.png +0 -0
  1039. drowse/web/dist/icons/tab-thinking-dark-16.png +0 -0
  1040. drowse/web/dist/icons/tab-thinking-dark-17.png +0 -0
  1041. drowse/web/dist/icons/tab-thinking-dark-18.png +0 -0
  1042. drowse/web/dist/icons/tab-thinking-dark-19.png +0 -0
  1043. drowse/web/dist/icons/tab-thinking-dark-2.png +0 -0
  1044. drowse/web/dist/icons/tab-thinking-dark-20.png +0 -0
  1045. drowse/web/dist/icons/tab-thinking-dark-21.png +0 -0
  1046. drowse/web/dist/icons/tab-thinking-dark-22.png +0 -0
  1047. drowse/web/dist/icons/tab-thinking-dark-23.png +0 -0
  1048. drowse/web/dist/icons/tab-thinking-dark-24.png +0 -0
  1049. drowse/web/dist/icons/tab-thinking-dark-25.png +0 -0
  1050. drowse/web/dist/icons/tab-thinking-dark-26.png +0 -0
  1051. drowse/web/dist/icons/tab-thinking-dark-27.png +0 -0
  1052. drowse/web/dist/icons/tab-thinking-dark-28.png +0 -0
  1053. drowse/web/dist/icons/tab-thinking-dark-29.png +0 -0
  1054. drowse/web/dist/icons/tab-thinking-dark-3.png +0 -0
  1055. drowse/web/dist/icons/tab-thinking-dark-30.png +0 -0
  1056. drowse/web/dist/icons/tab-thinking-dark-31.png +0 -0
  1057. drowse/web/dist/icons/tab-thinking-dark-32.png +0 -0
  1058. drowse/web/dist/icons/tab-thinking-dark-33.png +0 -0
  1059. drowse/web/dist/icons/tab-thinking-dark-34.png +0 -0
  1060. drowse/web/dist/icons/tab-thinking-dark-35.png +0 -0
  1061. drowse/web/dist/icons/tab-thinking-dark-36.png +0 -0
  1062. drowse/web/dist/icons/tab-thinking-dark-37.png +0 -0
  1063. drowse/web/dist/icons/tab-thinking-dark-38.png +0 -0
  1064. drowse/web/dist/icons/tab-thinking-dark-39.png +0 -0
  1065. drowse/web/dist/icons/tab-thinking-dark-4.png +0 -0
  1066. drowse/web/dist/icons/tab-thinking-dark-40.png +0 -0
  1067. drowse/web/dist/icons/tab-thinking-dark-41.png +0 -0
  1068. drowse/web/dist/icons/tab-thinking-dark-42.png +0 -0
  1069. drowse/web/dist/icons/tab-thinking-dark-43.png +0 -0
  1070. drowse/web/dist/icons/tab-thinking-dark-44.png +0 -0
  1071. drowse/web/dist/icons/tab-thinking-dark-45.png +0 -0
  1072. drowse/web/dist/icons/tab-thinking-dark-46.png +0 -0
  1073. drowse/web/dist/icons/tab-thinking-dark-47.png +0 -0
  1074. drowse/web/dist/icons/tab-thinking-dark-5.png +0 -0
  1075. drowse/web/dist/icons/tab-thinking-dark-6.png +0 -0
  1076. drowse/web/dist/icons/tab-thinking-dark-7.png +0 -0
  1077. drowse/web/dist/icons/tab-thinking-dark-8.png +0 -0
  1078. drowse/web/dist/icons/tab-thinking-dark-9.png +0 -0
  1079. drowse/web/dist/icons/tab-thinking-light-0.png +0 -0
  1080. drowse/web/dist/icons/tab-thinking-light-1.png +0 -0
  1081. drowse/web/dist/icons/tab-thinking-light-10.png +0 -0
  1082. drowse/web/dist/icons/tab-thinking-light-11.png +0 -0
  1083. drowse/web/dist/icons/tab-thinking-light-12.png +0 -0
  1084. drowse/web/dist/icons/tab-thinking-light-13.png +0 -0
  1085. drowse/web/dist/icons/tab-thinking-light-14.png +0 -0
  1086. drowse/web/dist/icons/tab-thinking-light-15.png +0 -0
  1087. drowse/web/dist/icons/tab-thinking-light-16.png +0 -0
  1088. drowse/web/dist/icons/tab-thinking-light-17.png +0 -0
  1089. drowse/web/dist/icons/tab-thinking-light-18.png +0 -0
  1090. drowse/web/dist/icons/tab-thinking-light-19.png +0 -0
  1091. drowse/web/dist/icons/tab-thinking-light-2.png +0 -0
  1092. drowse/web/dist/icons/tab-thinking-light-20.png +0 -0
  1093. drowse/web/dist/icons/tab-thinking-light-21.png +0 -0
  1094. drowse/web/dist/icons/tab-thinking-light-22.png +0 -0
  1095. drowse/web/dist/icons/tab-thinking-light-23.png +0 -0
  1096. drowse/web/dist/icons/tab-thinking-light-24.png +0 -0
  1097. drowse/web/dist/icons/tab-thinking-light-25.png +0 -0
  1098. drowse/web/dist/icons/tab-thinking-light-26.png +0 -0
  1099. drowse/web/dist/icons/tab-thinking-light-27.png +0 -0
  1100. drowse/web/dist/icons/tab-thinking-light-28.png +0 -0
  1101. drowse/web/dist/icons/tab-thinking-light-29.png +0 -0
  1102. drowse/web/dist/icons/tab-thinking-light-3.png +0 -0
  1103. drowse/web/dist/icons/tab-thinking-light-30.png +0 -0
  1104. drowse/web/dist/icons/tab-thinking-light-31.png +0 -0
  1105. drowse/web/dist/icons/tab-thinking-light-32.png +0 -0
  1106. drowse/web/dist/icons/tab-thinking-light-33.png +0 -0
  1107. drowse/web/dist/icons/tab-thinking-light-34.png +0 -0
  1108. drowse/web/dist/icons/tab-thinking-light-35.png +0 -0
  1109. drowse/web/dist/icons/tab-thinking-light-36.png +0 -0
  1110. drowse/web/dist/icons/tab-thinking-light-37.png +0 -0
  1111. drowse/web/dist/icons/tab-thinking-light-38.png +0 -0
  1112. drowse/web/dist/icons/tab-thinking-light-39.png +0 -0
  1113. drowse/web/dist/icons/tab-thinking-light-4.png +0 -0
  1114. drowse/web/dist/icons/tab-thinking-light-40.png +0 -0
  1115. drowse/web/dist/icons/tab-thinking-light-41.png +0 -0
  1116. drowse/web/dist/icons/tab-thinking-light-42.png +0 -0
  1117. drowse/web/dist/icons/tab-thinking-light-43.png +0 -0
  1118. drowse/web/dist/icons/tab-thinking-light-44.png +0 -0
  1119. drowse/web/dist/icons/tab-thinking-light-45.png +0 -0
  1120. drowse/web/dist/icons/tab-thinking-light-46.png +0 -0
  1121. drowse/web/dist/icons/tab-thinking-light-47.png +0 -0
  1122. drowse/web/dist/icons/tab-thinking-light-5.png +0 -0
  1123. drowse/web/dist/icons/tab-thinking-light-6.png +0 -0
  1124. drowse/web/dist/icons/tab-thinking-light-7.png +0 -0
  1125. drowse/web/dist/icons/tab-thinking-light-8.png +0 -0
  1126. drowse/web/dist/icons/tab-thinking-light-9.png +0 -0
  1127. drowse/web/dist/icons/tab-tokens-dark.png +0 -0
  1128. drowse/web/dist/icons/tab-tokens-light.png +0 -0
  1129. drowse/web/dist/icons/tab-tokens.png +0 -0
  1130. drowse/web/dist/icons/tab-training-0.png +0 -0
  1131. drowse/web/dist/icons/tab-training-1.png +0 -0
  1132. drowse/web/dist/icons/tab-training-2.png +0 -0
  1133. drowse/web/dist/icons/tab-training-dark-0.png +0 -0
  1134. drowse/web/dist/icons/tab-training-dark-1.png +0 -0
  1135. drowse/web/dist/icons/tab-training-dark-10.png +0 -0
  1136. drowse/web/dist/icons/tab-training-dark-11.png +0 -0
  1137. drowse/web/dist/icons/tab-training-dark-12.png +0 -0
  1138. drowse/web/dist/icons/tab-training-dark-13.png +0 -0
  1139. drowse/web/dist/icons/tab-training-dark-14.png +0 -0
  1140. drowse/web/dist/icons/tab-training-dark-15.png +0 -0
  1141. drowse/web/dist/icons/tab-training-dark-16.png +0 -0
  1142. drowse/web/dist/icons/tab-training-dark-17.png +0 -0
  1143. drowse/web/dist/icons/tab-training-dark-18.png +0 -0
  1144. drowse/web/dist/icons/tab-training-dark-19.png +0 -0
  1145. drowse/web/dist/icons/tab-training-dark-2.png +0 -0
  1146. drowse/web/dist/icons/tab-training-dark-20.png +0 -0
  1147. drowse/web/dist/icons/tab-training-dark-21.png +0 -0
  1148. drowse/web/dist/icons/tab-training-dark-22.png +0 -0
  1149. drowse/web/dist/icons/tab-training-dark-23.png +0 -0
  1150. drowse/web/dist/icons/tab-training-dark-24.png +0 -0
  1151. drowse/web/dist/icons/tab-training-dark-25.png +0 -0
  1152. drowse/web/dist/icons/tab-training-dark-26.png +0 -0
  1153. drowse/web/dist/icons/tab-training-dark-27.png +0 -0
  1154. drowse/web/dist/icons/tab-training-dark-28.png +0 -0
  1155. drowse/web/dist/icons/tab-training-dark-29.png +0 -0
  1156. drowse/web/dist/icons/tab-training-dark-3.png +0 -0
  1157. drowse/web/dist/icons/tab-training-dark-30.png +0 -0
  1158. drowse/web/dist/icons/tab-training-dark-31.png +0 -0
  1159. drowse/web/dist/icons/tab-training-dark-32.png +0 -0
  1160. drowse/web/dist/icons/tab-training-dark-33.png +0 -0
  1161. drowse/web/dist/icons/tab-training-dark-34.png +0 -0
  1162. drowse/web/dist/icons/tab-training-dark-35.png +0 -0
  1163. drowse/web/dist/icons/tab-training-dark-36.png +0 -0
  1164. drowse/web/dist/icons/tab-training-dark-37.png +0 -0
  1165. drowse/web/dist/icons/tab-training-dark-38.png +0 -0
  1166. drowse/web/dist/icons/tab-training-dark-39.png +0 -0
  1167. drowse/web/dist/icons/tab-training-dark-4.png +0 -0
  1168. drowse/web/dist/icons/tab-training-dark-40.png +0 -0
  1169. drowse/web/dist/icons/tab-training-dark-41.png +0 -0
  1170. drowse/web/dist/icons/tab-training-dark-42.png +0 -0
  1171. drowse/web/dist/icons/tab-training-dark-43.png +0 -0
  1172. drowse/web/dist/icons/tab-training-dark-44.png +0 -0
  1173. drowse/web/dist/icons/tab-training-dark-45.png +0 -0
  1174. drowse/web/dist/icons/tab-training-dark-46.png +0 -0
  1175. drowse/web/dist/icons/tab-training-dark-47.png +0 -0
  1176. drowse/web/dist/icons/tab-training-dark-5.png +0 -0
  1177. drowse/web/dist/icons/tab-training-dark-6.png +0 -0
  1178. drowse/web/dist/icons/tab-training-dark-7.png +0 -0
  1179. drowse/web/dist/icons/tab-training-dark-8.png +0 -0
  1180. drowse/web/dist/icons/tab-training-dark-9.png +0 -0
  1181. drowse/web/dist/icons/tab-training-light-0.png +0 -0
  1182. drowse/web/dist/icons/tab-training-light-1.png +0 -0
  1183. drowse/web/dist/icons/tab-training-light-10.png +0 -0
  1184. drowse/web/dist/icons/tab-training-light-11.png +0 -0
  1185. drowse/web/dist/icons/tab-training-light-12.png +0 -0
  1186. drowse/web/dist/icons/tab-training-light-13.png +0 -0
  1187. drowse/web/dist/icons/tab-training-light-14.png +0 -0
  1188. drowse/web/dist/icons/tab-training-light-15.png +0 -0
  1189. drowse/web/dist/icons/tab-training-light-16.png +0 -0
  1190. drowse/web/dist/icons/tab-training-light-17.png +0 -0
  1191. drowse/web/dist/icons/tab-training-light-18.png +0 -0
  1192. drowse/web/dist/icons/tab-training-light-19.png +0 -0
  1193. drowse/web/dist/icons/tab-training-light-2.png +0 -0
  1194. drowse/web/dist/icons/tab-training-light-20.png +0 -0
  1195. drowse/web/dist/icons/tab-training-light-21.png +0 -0
  1196. drowse/web/dist/icons/tab-training-light-22.png +0 -0
  1197. drowse/web/dist/icons/tab-training-light-23.png +0 -0
  1198. drowse/web/dist/icons/tab-training-light-24.png +0 -0
  1199. drowse/web/dist/icons/tab-training-light-25.png +0 -0
  1200. drowse/web/dist/icons/tab-training-light-26.png +0 -0
  1201. drowse/web/dist/icons/tab-training-light-27.png +0 -0
  1202. drowse/web/dist/icons/tab-training-light-28.png +0 -0
  1203. drowse/web/dist/icons/tab-training-light-29.png +0 -0
  1204. drowse/web/dist/icons/tab-training-light-3.png +0 -0
  1205. drowse/web/dist/icons/tab-training-light-30.png +0 -0
  1206. drowse/web/dist/icons/tab-training-light-31.png +0 -0
  1207. drowse/web/dist/icons/tab-training-light-32.png +0 -0
  1208. drowse/web/dist/icons/tab-training-light-33.png +0 -0
  1209. drowse/web/dist/icons/tab-training-light-34.png +0 -0
  1210. drowse/web/dist/icons/tab-training-light-35.png +0 -0
  1211. drowse/web/dist/icons/tab-training-light-36.png +0 -0
  1212. drowse/web/dist/icons/tab-training-light-37.png +0 -0
  1213. drowse/web/dist/icons/tab-training-light-38.png +0 -0
  1214. drowse/web/dist/icons/tab-training-light-39.png +0 -0
  1215. drowse/web/dist/icons/tab-training-light-4.png +0 -0
  1216. drowse/web/dist/icons/tab-training-light-40.png +0 -0
  1217. drowse/web/dist/icons/tab-training-light-41.png +0 -0
  1218. drowse/web/dist/icons/tab-training-light-42.png +0 -0
  1219. drowse/web/dist/icons/tab-training-light-43.png +0 -0
  1220. drowse/web/dist/icons/tab-training-light-44.png +0 -0
  1221. drowse/web/dist/icons/tab-training-light-45.png +0 -0
  1222. drowse/web/dist/icons/tab-training-light-46.png +0 -0
  1223. drowse/web/dist/icons/tab-training-light-47.png +0 -0
  1224. drowse/web/dist/icons/tab-training-light-5.png +0 -0
  1225. drowse/web/dist/icons/tab-training-light-6.png +0 -0
  1226. drowse/web/dist/icons/tab-training-light-7.png +0 -0
  1227. drowse/web/dist/icons/tab-training-light-8.png +0 -0
  1228. drowse/web/dist/icons/tab-training-light-9.png +0 -0
  1229. drowse/web/dist/icons/tab-unsupported-dark.png +0 -0
  1230. drowse/web/dist/icons/tab-unsupported-light.png +0 -0
  1231. drowse/web/dist/icons/tab-unsupported.png +0 -0
  1232. drowse/web/dist/icons/tab-working-0.png +0 -0
  1233. drowse/web/dist/icons/tab-working-1.png +0 -0
  1234. drowse/web/dist/icons/tab-working-2.png +0 -0
  1235. drowse/web/dist/icons/tab-working-dark-0.png +0 -0
  1236. drowse/web/dist/icons/tab-working-dark-1.png +0 -0
  1237. drowse/web/dist/icons/tab-working-dark-10.png +0 -0
  1238. drowse/web/dist/icons/tab-working-dark-11.png +0 -0
  1239. drowse/web/dist/icons/tab-working-dark-12.png +0 -0
  1240. drowse/web/dist/icons/tab-working-dark-13.png +0 -0
  1241. drowse/web/dist/icons/tab-working-dark-14.png +0 -0
  1242. drowse/web/dist/icons/tab-working-dark-15.png +0 -0
  1243. drowse/web/dist/icons/tab-working-dark-16.png +0 -0
  1244. drowse/web/dist/icons/tab-working-dark-17.png +0 -0
  1245. drowse/web/dist/icons/tab-working-dark-18.png +0 -0
  1246. drowse/web/dist/icons/tab-working-dark-19.png +0 -0
  1247. drowse/web/dist/icons/tab-working-dark-2.png +0 -0
  1248. drowse/web/dist/icons/tab-working-dark-20.png +0 -0
  1249. drowse/web/dist/icons/tab-working-dark-21.png +0 -0
  1250. drowse/web/dist/icons/tab-working-dark-22.png +0 -0
  1251. drowse/web/dist/icons/tab-working-dark-23.png +0 -0
  1252. drowse/web/dist/icons/tab-working-dark-24.png +0 -0
  1253. drowse/web/dist/icons/tab-working-dark-25.png +0 -0
  1254. drowse/web/dist/icons/tab-working-dark-26.png +0 -0
  1255. drowse/web/dist/icons/tab-working-dark-27.png +0 -0
  1256. drowse/web/dist/icons/tab-working-dark-28.png +0 -0
  1257. drowse/web/dist/icons/tab-working-dark-29.png +0 -0
  1258. drowse/web/dist/icons/tab-working-dark-3.png +0 -0
  1259. drowse/web/dist/icons/tab-working-dark-30.png +0 -0
  1260. drowse/web/dist/icons/tab-working-dark-31.png +0 -0
  1261. drowse/web/dist/icons/tab-working-dark-32.png +0 -0
  1262. drowse/web/dist/icons/tab-working-dark-33.png +0 -0
  1263. drowse/web/dist/icons/tab-working-dark-34.png +0 -0
  1264. drowse/web/dist/icons/tab-working-dark-35.png +0 -0
  1265. drowse/web/dist/icons/tab-working-dark-36.png +0 -0
  1266. drowse/web/dist/icons/tab-working-dark-37.png +0 -0
  1267. drowse/web/dist/icons/tab-working-dark-38.png +0 -0
  1268. drowse/web/dist/icons/tab-working-dark-39.png +0 -0
  1269. drowse/web/dist/icons/tab-working-dark-4.png +0 -0
  1270. drowse/web/dist/icons/tab-working-dark-40.png +0 -0
  1271. drowse/web/dist/icons/tab-working-dark-41.png +0 -0
  1272. drowse/web/dist/icons/tab-working-dark-42.png +0 -0
  1273. drowse/web/dist/icons/tab-working-dark-43.png +0 -0
  1274. drowse/web/dist/icons/tab-working-dark-44.png +0 -0
  1275. drowse/web/dist/icons/tab-working-dark-45.png +0 -0
  1276. drowse/web/dist/icons/tab-working-dark-46.png +0 -0
  1277. drowse/web/dist/icons/tab-working-dark-47.png +0 -0
  1278. drowse/web/dist/icons/tab-working-dark-5.png +0 -0
  1279. drowse/web/dist/icons/tab-working-dark-6.png +0 -0
  1280. drowse/web/dist/icons/tab-working-dark-7.png +0 -0
  1281. drowse/web/dist/icons/tab-working-dark-8.png +0 -0
  1282. drowse/web/dist/icons/tab-working-dark-9.png +0 -0
  1283. drowse/web/dist/icons/tab-working-light-0.png +0 -0
  1284. drowse/web/dist/icons/tab-working-light-1.png +0 -0
  1285. drowse/web/dist/icons/tab-working-light-10.png +0 -0
  1286. drowse/web/dist/icons/tab-working-light-11.png +0 -0
  1287. drowse/web/dist/icons/tab-working-light-12.png +0 -0
  1288. drowse/web/dist/icons/tab-working-light-13.png +0 -0
  1289. drowse/web/dist/icons/tab-working-light-14.png +0 -0
  1290. drowse/web/dist/icons/tab-working-light-15.png +0 -0
  1291. drowse/web/dist/icons/tab-working-light-16.png +0 -0
  1292. drowse/web/dist/icons/tab-working-light-17.png +0 -0
  1293. drowse/web/dist/icons/tab-working-light-18.png +0 -0
  1294. drowse/web/dist/icons/tab-working-light-19.png +0 -0
  1295. drowse/web/dist/icons/tab-working-light-2.png +0 -0
  1296. drowse/web/dist/icons/tab-working-light-20.png +0 -0
  1297. drowse/web/dist/icons/tab-working-light-21.png +0 -0
  1298. drowse/web/dist/icons/tab-working-light-22.png +0 -0
  1299. drowse/web/dist/icons/tab-working-light-23.png +0 -0
  1300. drowse/web/dist/icons/tab-working-light-24.png +0 -0
  1301. drowse/web/dist/icons/tab-working-light-25.png +0 -0
  1302. drowse/web/dist/icons/tab-working-light-26.png +0 -0
  1303. drowse/web/dist/icons/tab-working-light-27.png +0 -0
  1304. drowse/web/dist/icons/tab-working-light-28.png +0 -0
  1305. drowse/web/dist/icons/tab-working-light-29.png +0 -0
  1306. drowse/web/dist/icons/tab-working-light-3.png +0 -0
  1307. drowse/web/dist/icons/tab-working-light-30.png +0 -0
  1308. drowse/web/dist/icons/tab-working-light-31.png +0 -0
  1309. drowse/web/dist/icons/tab-working-light-32.png +0 -0
  1310. drowse/web/dist/icons/tab-working-light-33.png +0 -0
  1311. drowse/web/dist/icons/tab-working-light-34.png +0 -0
  1312. drowse/web/dist/icons/tab-working-light-35.png +0 -0
  1313. drowse/web/dist/icons/tab-working-light-36.png +0 -0
  1314. drowse/web/dist/icons/tab-working-light-37.png +0 -0
  1315. drowse/web/dist/icons/tab-working-light-38.png +0 -0
  1316. drowse/web/dist/icons/tab-working-light-39.png +0 -0
  1317. drowse/web/dist/icons/tab-working-light-4.png +0 -0
  1318. drowse/web/dist/icons/tab-working-light-40.png +0 -0
  1319. drowse/web/dist/icons/tab-working-light-41.png +0 -0
  1320. drowse/web/dist/icons/tab-working-light-42.png +0 -0
  1321. drowse/web/dist/icons/tab-working-light-43.png +0 -0
  1322. drowse/web/dist/icons/tab-working-light-44.png +0 -0
  1323. drowse/web/dist/icons/tab-working-light-45.png +0 -0
  1324. drowse/web/dist/icons/tab-working-light-46.png +0 -0
  1325. drowse/web/dist/icons/tab-working-light-47.png +0 -0
  1326. drowse/web/dist/icons/tab-working-light-5.png +0 -0
  1327. drowse/web/dist/icons/tab-working-light-6.png +0 -0
  1328. drowse/web/dist/icons/tab-working-light-7.png +0 -0
  1329. drowse/web/dist/icons/tab-working-light-8.png +0 -0
  1330. drowse/web/dist/icons/tab-working-light-9.png +0 -0
  1331. drowse/web/dist/index.html +34 -0
  1332. drowse/web/dist/social/drowse.png +0 -0
  1333. drowse/web/dist/social/drowse.svg +6 -0
  1334. drowse/web/dist/theme-init.js +25 -0
  1335. drowse/web/routes.py +118 -0
  1336. drowse_ai-5.3.1.dist-info/METADATA +632 -0
  1337. drowse_ai-5.3.1.dist-info/RECORD +1341 -0
  1338. drowse_ai-5.3.1.dist-info/WHEEL +5 -0
  1339. drowse_ai-5.3.1.dist-info/entry_points.txt +2 -0
  1340. drowse_ai-5.3.1.dist-info/licenses/LICENSE +661 -0
  1341. drowse_ai-5.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1854 @@
1
+ """On-disk format core for manifold-steering artifacts.
2
+
3
+ The dataclasses (:class:`ManifoldFolder` / :class:`ManifoldSidecar`),
4
+ integrity/sha256 helpers, ``manifold.json`` load/save, and the shared
5
+ module-private validators + payload builders that the authoring and
6
+ lifecycle layers both reuse. This is the dependency root of the split —
7
+ both :mod:`drowse.io.manifold_authoring` and
8
+ :mod:`drowse.io.manifold_lifecycle` import from here.
9
+
10
+ A *manifold* is a set of labeled nodes — each node a small corpus of
11
+ responses — placed at authoring coordinates on a :class:`ManifoldDomain`
12
+ (an n-dimensional intrinsic manifold of some topology: a box/disk, a
13
+ cylinder, a torus, a sphere, or an explicit immersion). Fitting a
14
+ manifold against a model produces a per-model RBF artifact (see
15
+ :mod:`drowse.core.manifold`). Manifolds live under their own root,
16
+ ``~/.drowse/manifolds/<ns>/<name>/``. A manifold carries N labeled nodes on a
17
+ domain; a two-node flat manifold is the bipolar-concept case.
18
+
19
+ ```
20
+ ~/.drowse/manifolds/<ns>/<name>/
21
+ manifold.json # name, description, domain spec, nodes
22
+ nodes/NN_<label>.json # one zero-padded file per node: a JSON list
23
+ # of statement strings
24
+ <safe_model>.safetensors # fitted per-model RBF (+ .json sidecar)
25
+ <safe_model>_sae-<rel>.safetensors # SAE-feature-space variant
26
+ ```
27
+
28
+ The user authors ``manifold.json`` (each node carries a ``label`` and its
29
+ authoring ``coords``) and the ``nodes/*.json`` corpus files by hand;
30
+ ``drowse manifold fit`` produces the per-model tensors and
31
+ back-fills the ``files`` integrity manifest. Tensor save/load itself
32
+ lives in :mod:`drowse.io.manifold_tensors` (``save_manifold`` / ``load_manifold``);
33
+ this module owns folder discovery, the node corpus, and integrity.
34
+ """
35
+ from __future__ import annotations
36
+
37
+ from contextlib import ExitStack, contextmanager
38
+ from collections.abc import Mapping
39
+ import hashlib
40
+ import json
41
+ import math
42
+ import re
43
+ import warnings
44
+ from dataclasses import dataclass, field
45
+ from pathlib import Path
46
+ from typing import Any, Optional
47
+
48
+ import torch
49
+
50
+ from drowse.core.errors import DrowseError
51
+ from drowse.core.manifold import BoxDomain, domain_from_spec
52
+ from drowse.core.role_templates import _ROLE_SLUG_RE
53
+ from drowse.io.atomic import write_json_atomic
54
+ from drowse.io.integrity import NAME_REGEX, hash_file, verify_integrity
55
+
56
+ # Manifold node *labels* are stricter than the artifact ``NAME_REGEX``
57
+ # (``io.packs``): they drop ``.`` because the dot is reserved as the
58
+ # bipolar-pole separator (``deer.wolf``) and the steering-expr lexer
59
+ # addresses a label via ``%label`` — a dotted label could neither be
60
+ # typed unambiguously (``persona%a.b`` reads ``a`` dotted with pole ``b``)
61
+ # nor resolved through the bare-name pipeline. Labels are therefore the
62
+ # grammar-addressable identifier subset of the name grammar: lowercase
63
+ # start, then ``[a-z0-9_-]`` only, ≤64 chars. The manifold *name*
64
+ # itself keeps ``NAME_REGEX`` (it is never used as a ``%`` operand).
65
+ _LABEL_REGEX = re.compile(r"^[a-z][a-z0-9_-]{0,63}$")
66
+
67
+ # Discover-mode fit modes — set as ``manifold.json::fit_mode`` for
68
+ # manifolds whose node coordinates are derived from the model's
69
+ # activations rather than authored by hand. Authored manifolds carry
70
+ # ``fit_mode == "authored"``. Current manifests always carry the discriminator.
71
+ # ``auto`` is a discover mode whose *resolved* geometry (flat ``pca`` vs curved
72
+ # ``spectral``, plus periodic ``BoxDomain`` axes) is chosen per-model at fit
73
+ # time by ``core.manifold.select_topology`` — the folder declares only the
74
+ # corpus + hyperparams, exactly like ``pca``/``spectral``.
75
+ _FIT_MODES_DISCOVER: frozenset[str] = frozenset({"pca", "spectral", "auto"})
76
+ # A baked manifold is a pre-fitted, corpus-less artifact: its geometry is
77
+ # frozen in the per-model tensor and it can never re-fit (no node corpus to
78
+ # pool from). Merge outputs and imported control vectors land this way — a
79
+ # steering *direction* is the K=1 affine case (see
80
+ # ``core/capture.fold_directions_to_subspace``). It is deliberately NOT a
81
+ # discover mode (``is_discover`` stays False — there are no per-model coords
82
+ # to derive).
83
+ _FIT_MODES_BAKED: frozenset[str] = frozenset({"baked"})
84
+ _FIT_MODES_ALL: frozenset[str] = (
85
+ frozenset({"authored"}) | _FIT_MODES_DISCOVER | _FIT_MODES_BAKED
86
+ )
87
+ MERGE_BAKE_POLICY = "additive_union_v1"
88
+
89
+
90
+ @contextmanager
91
+ def _locked_manifest(folder: Path):
92
+ """Serialize cross-process manifest read-modify-write for one folder."""
93
+ from drowse.io.atomic import artifact_lock
94
+
95
+ with artifact_lock(folder.parent / f"{folder.name}.manifest"):
96
+ yield
97
+
98
+
99
+ def manifold_pair_lock_path(tensor_path: Path) -> Path:
100
+ """Stable, bounded logical lock path for one fitted tensor pair.
101
+
102
+ The lock lives under the manifold namespace rather than inside the
103
+ removable manifold folder. Consequently ``rm`` and stage-swap refreshes
104
+ cannot unlink the held lock inode and let a later process acquire a fresh
105
+ inode for the same logical pair. The digest also bounds the filename for
106
+ long model/release-derived tensor names.
107
+ """
108
+
109
+ tensor_path = Path(tensor_path).expanduser().resolve(strict=False)
110
+ identity = f"{tensor_path.parent}\0{tensor_path.name}".encode()
111
+ digest = hashlib.sha256(identity).hexdigest()
112
+ return tensor_path.parent.parent / f".drowse-pair-{digest}"
113
+
114
+
115
+ @contextmanager
116
+ def manifold_pair_lock(tensor_path: Path):
117
+ """Lock a fitted tensor/sidecar pair using stable external identity."""
118
+
119
+ from drowse.io.atomic import artifact_lock
120
+
121
+ with artifact_lock(manifold_pair_lock_path(tensor_path)):
122
+ yield
123
+
124
+
125
+ def manifold_folder_tensor_paths(folder: Path) -> list[Path]:
126
+ """Return canonical tensor paths for every fitted/orphan pair in ``folder``.
127
+
128
+ Destructive folder replacement cannot trust ``manifold.json::files``: the
129
+ operation is also the recovery path for an interrupted publication whose
130
+ tensor or sidecar never acquired a manifest proof. Scan both halves and
131
+ canonicalize sidecars back to their tensor path so each logical pair is
132
+ locked exactly once.
133
+ """
134
+
135
+ from drowse.io.paths import parse_tensor_filename
136
+
137
+ folder = Path(folder)
138
+ candidates = list(folder.glob("*.safetensors")) + [
139
+ sidecar
140
+ for sidecar in folder.glob("*.json")
141
+ if sidecar.name != "manifold.json"
142
+ ]
143
+ tensors: set[Path] = set()
144
+ for candidate in candidates:
145
+ tensor = (
146
+ candidate.with_suffix(".safetensors")
147
+ if candidate.suffix == ".json" else candidate
148
+ )
149
+ if parse_tensor_filename(tensor.name) is not None:
150
+ tensors.add(tensor)
151
+ return sorted(tensors)
152
+
153
+
154
+ @contextmanager
155
+ def destructive_manifold_folder_transaction(folder: Path):
156
+ """Quiesce fitted readers before a whole-folder reset or stage swap.
157
+
158
+ Global mutation order is manifest then stable pair locks, with pair locks in
159
+ deterministic tensor-path order. The pair locks live outside the removable
160
+ folder, so holding this context across ``rmtree``/rename/recreation prevents
161
+ a new inode from bypassing a reader that still owns the old logical pair.
162
+ The manifest lock is re-entrant, allowing authoring entry points that already
163
+ serialize their destination to use this one shared destructive seam.
164
+ """
165
+
166
+ folder = Path(folder)
167
+ with _locked_manifest(folder):
168
+ with ExitStack() as stack:
169
+ for tensor_path in manifold_folder_tensor_paths(folder):
170
+ stack.enter_context(manifold_pair_lock(tensor_path))
171
+ yield
172
+
173
+
174
+ def reset_manifold_folder(folder: Path) -> None:
175
+ """Remove ``folder`` only after all extant fitted pairs are quiescent."""
176
+
177
+ import shutil
178
+
179
+ folder = Path(folder)
180
+ with destructive_manifold_folder_transaction(folder):
181
+ if folder.exists():
182
+ shutil.rmtree(folder)
183
+
184
+ # Per-method hyperparameter whitelists. Anything outside the whitelist
185
+ # for a given fit_mode is dropped at folder-create time so a user
186
+ # POSTing ``{fit_mode: "pca", hyperparams: {"k_nn": 5}}`` doesn't land
187
+ # a foreign key that would then crash ``derive_pca_coords`` at fit
188
+ # time with ``TypeError: unexpected keyword argument``. ``max_dim``
189
+ # is shared. Coordinate derivation is layer-agnostic — the consensus
190
+ # Gram averages every fit layer (``ManifoldExtractionPipeline.fit``), so
191
+ # there is no reference-layer knob.
192
+ # ``max_dim`` caps the derived intrinsic (layout) dim for both modes. A
193
+ # flat (``pca``) fit's per-layer steerable subspace *is* its k-dim layout
194
+ # span, so ``max_dim`` is its only dim knob — there is no separate
195
+ # ``max_subspace_dim`` for pca (the subspace dim equals the layout dim).
196
+ # ``max_subspace_dim`` survives only for the curved (``spectral``) fit,
197
+ # where the per-layer RBF subspace can carry off-surface dims beyond the
198
+ # intrinsic coordinate count. ``min_dim`` (spectral only) floors the
199
+ # layout dim the eigenvalue-ratio cliff derives — for an authored-
200
+ # dimensionality manifold (PAD's P×A×D) the cliff undershoots when one
201
+ # mode dominates the spectrum, so the floor keeps the declared geometry
202
+ # (set ``min_dim == max_dim`` to pin the dim exactly). The steer-time
203
+ # origin is always the projection of the per-model neutral mean onto the
204
+ # subspace (the affine fit neutral-anchors the frame), so there is no
205
+ # ``anchor_origin`` knob.
206
+ _HYPERPARAMS_BY_MODE: dict[str, frozenset[str]] = {
207
+ "pca": frozenset({
208
+ "max_dim", "var_threshold",
209
+ }),
210
+ "spectral": frozenset({
211
+ "max_dim", "min_dim", "k_nn", "bandwidth", "max_subspace_dim",
212
+ "smoothing",
213
+ }),
214
+ # ``auto`` routes to ``select_topology`` (flat/curved by GCV + periodic by
215
+ # persistent homology); it accepts the union of the knobs its candidate
216
+ # fits consume — ``max_dim`` (layout dim cap), ``smoothing`` (curved-fit
217
+ # GCV λ), ``persistence_frac`` (H1 loop-significance threshold), plus the
218
+ # spectral graph knobs and ``max_subspace_dim`` for a resolved curved fit.
219
+ "auto": frozenset({
220
+ "max_dim", "var_threshold", "min_dim", "k_nn", "bandwidth",
221
+ "max_subspace_dim", "smoothing", "persistence_frac",
222
+ }),
223
+ }
224
+
225
+
226
+ def domain_label(spec: dict[str, Any]) -> str:
227
+ """Short ``type(Nd)`` label for a manifold domain spec dict."""
228
+ kind = spec.get("type", "?")
229
+ if kind == "klein":
230
+ return "Klein bottle (2D)"
231
+ if kind == "projective":
232
+ return "Projective plane / RP² (2D)"
233
+ if kind == "box":
234
+ n = len(spec.get("axes", []))
235
+ elif kind == "sphere":
236
+ n = int(spec.get("dim", 0))
237
+ elif kind == "custom":
238
+ n = int(spec.get("embed_dim", 0))
239
+ else:
240
+ n = 0
241
+ return f"{kind}({n}d)"
242
+
243
+
244
+ def sanitize_hyperparams(
245
+ fit_mode: str, hyperparams: dict[str, Any] | None,
246
+ ) -> dict[str, Any]:
247
+ """Validate and return hyperparameters for one exact fit mode.
248
+
249
+ Single source of truth for the per-method whitelist; both the create
250
+ and the fit-override paths funnel through this so the folder
251
+ manifest never accepts a key that the selected dispatcher would ignore.
252
+ """
253
+ if hyperparams is None:
254
+ return {}
255
+ allowed = _HYPERPARAMS_BY_MODE.get(fit_mode)
256
+ if allowed is None:
257
+ raise ManifoldFormatError(f"unknown fit_mode {fit_mode!r}")
258
+ invalid = set(hyperparams) - allowed
259
+ if invalid:
260
+ names = ", ".join(sorted(invalid))
261
+ raise ManifoldFormatError(
262
+ f"fit_mode {fit_mode!r} does not accept hyperparameter(s): {names}"
263
+ )
264
+ int_keys = {"max_dim", "min_dim", "k_nn", "max_subspace_dim"}
265
+ float_keys = {"var_threshold", "bandwidth", "persistence_frac"}
266
+ for key, value in hyperparams.items():
267
+ valid = (
268
+ isinstance(value, int) and not isinstance(value, bool) and value > 0
269
+ if key in int_keys else
270
+ _finite_number(value)
271
+ if key in float_keys else
272
+ value == "auto" or _finite_number(value)
273
+ if key == "smoothing" else False
274
+ )
275
+ if not valid:
276
+ raise ManifoldFormatError(
277
+ f"fit_mode {fit_mode!r} hyperparameter {key!r} has invalid "
278
+ f"value {value!r}"
279
+ )
280
+ return dict(hyperparams)
281
+
282
+
283
+ # Current manifold artifact format. v10 splits the fit's capture identity into
284
+ # separately provable halves — ``capture_version`` + ``capture_render_sha256``
285
+ # (the tokenizer render, reproducible from tokenizer + prompts + corpus alone)
286
+ # alongside ``model_source_fingerprint`` (the pre-load-verifiable checkpoint
287
+ # identity) and ``baseline_prompts_sha256`` — so a fit no-op can be *proven*
288
+ # without loading weights (:func:`drowse.io.manifold_lifecycle.
289
+ # preflight_manifold_fit_noop`). A v9 sidecar carries none of them, so it
290
+ # cannot be proven and reads as an ordinary cache miss.
291
+ # v7 made manifest defaults and per-node provenance explicit, yielding one
292
+ # canonical payload shape. v6 added an optional per-layer ``affine_map`` tensor
293
+ # for flat fitted subspaces. Ordinary fits omit it (identity);
294
+ # rectangular/non-isometric cross-model transfers persist the exact
295
+ # authoring-to-orthonormal-reduced reparameterization. Old readers would
296
+ # otherwise ignore that tensor and silently move manifold world points, so this
297
+ # is a real format boundary.
298
+ MANIFOLD_FORMAT_VERSION = 10
299
+
300
+ MANIFOLD_SIDECAR_FIELDS = {
301
+ "format_version", "name", "method", "drowse_version", "domain",
302
+ "node_count", "node_labels", "feature_space", "fit_mode", "hyperparams",
303
+ "diagnostics", "node_spread_per_layer", "mahalanobis_share_per_layer",
304
+ "origin_per_layer", "nodes_sha256", "sae_release", "sae_revision",
305
+ "sae_fingerprint", "sae_ids_by_layer", "sae_full_coverage",
306
+ "model_fingerprint", "model_source_fingerprint", "capture_sha256",
307
+ "capture_version", "context_binding_sha256", "capture_render_sha256",
308
+ "baseline_prompts_sha256",
309
+ "fitted_layers",
310
+ "fit_policy_version", "share_metric", "subspace_metric",
311
+ "rbf_smoothing_per_layer", "sigma_field_per_layer", "resolved_fit_mode",
312
+ "topology_winner", "topology_candidates", "node_roles", "node_kinds",
313
+ "components", "bake_policy", "source_model_id",
314
+ "source_model_fingerprint", "transfer_quality_estimate",
315
+ }
316
+
317
+ MANIFOLD_METHOD_FIT_MODES: dict[str, frozenset[str]] = {
318
+ "manifold_pca": frozenset({"authored"}),
319
+ "manifold_sae": frozenset({"authored"}),
320
+ # Older monopolar sidecars inherited the writer's authored default because
321
+ # the early-return fit branch omitted its source-manifest provenance.
322
+ # Current writers correctly retain the one-node discover-pca identity.
323
+ "manifold_monopolar": frozenset({"authored", "pca"}),
324
+ "manifold_monopolar_sae": frozenset({"authored", "pca"}),
325
+ "manifold_discover_auto": frozenset({"auto"}),
326
+ "manifold_discover_pca": frozenset({"pca"}),
327
+ "manifold_discover_spectral": frozenset({"spectral"}),
328
+ "manifold_discover_sae": frozenset({"pca", "spectral", "auto"}),
329
+ "manifold_procrustes_transfer": frozenset(_FIT_MODES_ALL),
330
+ "merge": frozenset({"baked"}),
331
+ "folded_vector": frozenset({"baked"}),
332
+ }
333
+
334
+
335
+ def _is_sha256_digest(value: Any) -> bool:
336
+ """True for a canonical lowercase 64-hex sha256 digest."""
337
+ return (
338
+ isinstance(value, str)
339
+ and len(value) == 64
340
+ and all(char in "0123456789abcdef" for char in value)
341
+ )
342
+
343
+
344
+ def _finite_number(value: Any) -> bool:
345
+ return (
346
+ not isinstance(value, bool)
347
+ and isinstance(value, (int, float))
348
+ and math.isfinite(float(value))
349
+ )
350
+
351
+
352
+ def _validate_layer_map(
353
+ value: Any, *, location: str, field: str, fitted: set[int],
354
+ validate_value: Any, require_fitted: bool = True,
355
+ ) -> None:
356
+ if not isinstance(value, dict):
357
+ raise ManifoldFormatError(f"{location} field {field!r} must be an object")
358
+ for raw_layer, item in value.items():
359
+ if (
360
+ not isinstance(raw_layer, str)
361
+ or not raw_layer.isascii()
362
+ or not raw_layer.isdecimal()
363
+ or str(int(raw_layer)) != raw_layer
364
+ or require_fitted and int(raw_layer) not in fitted
365
+ ):
366
+ raise ManifoldFormatError(
367
+ f"{location} field {field!r} has invalid fitted-layer key {raw_layer!r}"
368
+ )
369
+ if not validate_value(item):
370
+ raise ManifoldFormatError(
371
+ f"{location} field {field!r} has invalid value for layer {raw_layer}"
372
+ )
373
+
374
+
375
+ def _validate_hyperparams(data: dict[str, Any], *, location: str) -> None:
376
+ params = data["hyperparams"]
377
+ mode = data["fit_mode"]
378
+ if not isinstance(params, dict):
379
+ raise ManifoldFormatError(f"{location} hyperparams must be an object")
380
+ if mode in {"authored", "baked"}:
381
+ if params:
382
+ raise ManifoldFormatError(f"{location} {mode} fit cannot carry hyperparams")
383
+ return
384
+ sanitize_hyperparams(mode, params)
385
+ int_keys = {"max_dim", "min_dim", "k_nn", "max_subspace_dim"}
386
+ float_keys = {"var_threshold", "bandwidth", "persistence_frac"}
387
+ for key, value in params.items():
388
+ valid = (
389
+ isinstance(value, int) and not isinstance(value, bool) and value > 0
390
+ if key in int_keys else
391
+ _finite_number(value)
392
+ if key in float_keys else
393
+ value == "auto" or _finite_number(value)
394
+ if key == "smoothing" else False
395
+ )
396
+ if not valid:
397
+ raise ManifoldFormatError(
398
+ f"{location} hyperparameter {key!r} has invalid value {value!r}"
399
+ )
400
+
401
+
402
+ def _validate_diagnostics(value: Any, *, location: str) -> None:
403
+ if not isinstance(value, dict):
404
+ raise ManifoldFormatError(f"{location} diagnostics must be an object")
405
+ allowed = {
406
+ "per_component_variance", "cumulative_variance", "eigenvalues",
407
+ "picked_k", "gap_index", "k_nn", "component_count", "heuristic_k",
408
+ "threshold", "gap_magnitude", "bandwidth", "pinned", "min_dim",
409
+ }
410
+ if set(value) - allowed:
411
+ raise ManifoldFormatError(f"{location} diagnostics has unknown fields")
412
+ list_keys = {"per_component_variance", "cumulative_variance", "eigenvalues"}
413
+ int_keys = {"picked_k", "gap_index", "k_nn", "component_count", "heuristic_k", "min_dim"}
414
+ for key, item in value.items():
415
+ valid = (
416
+ isinstance(item, list) and bool(item) and all(_finite_number(v) for v in item)
417
+ if key in list_keys else
418
+ isinstance(item, int) and not isinstance(item, bool) and item >= 0
419
+ if key in int_keys else
420
+ isinstance(item, bool)
421
+ if key == "pinned" else
422
+ _finite_number(item)
423
+ )
424
+ if not valid:
425
+ raise ManifoldFormatError(f"{location} diagnostics field {key!r} is invalid")
426
+
427
+
428
+ def _validate_topology_candidates(value: Any, *, location: str) -> None:
429
+ if not isinstance(value, list):
430
+ raise ManifoldFormatError(f"{location} topology_candidates must be an array")
431
+ fields = {"name", "fit_mode", "intrinsic_dim", "score", "viable", "reason"}
432
+ for candidate in value:
433
+ if not isinstance(candidate, dict) or set(candidate) != fields:
434
+ raise ManifoldFormatError(f"{location} has invalid topology candidate schema")
435
+ if (
436
+ not isinstance(candidate["name"], str) or not candidate["name"]
437
+ or candidate["fit_mode"] not in {"pca", "spectral"}
438
+ or isinstance(candidate["intrinsic_dim"], bool)
439
+ or not isinstance(candidate["intrinsic_dim"], int)
440
+ or candidate["intrinsic_dim"] < 1
441
+ or candidate["score"] is not None and not _finite_number(candidate["score"])
442
+ or not isinstance(candidate["viable"], bool)
443
+ or candidate["reason"] is not None and not isinstance(candidate["reason"], str)
444
+ ):
445
+ raise ManifoldFormatError(f"{location} has invalid topology candidate values")
446
+
447
+
448
+ def _validate_components(value: Any, *, location: str) -> None:
449
+ if value is None:
450
+ return
451
+ if not isinstance(value, dict) or not value:
452
+ raise ManifoldFormatError(f"{location} components must be null or non-empty object")
453
+ fields = {"selector", "alpha", "tensor_sha256"}
454
+ for coord, item in value.items():
455
+ if (
456
+ not isinstance(coord, str) or not coord
457
+ or not isinstance(item, dict) or set(item) != fields
458
+ or not isinstance(item["selector"], str) or not item["selector"]
459
+ or not _finite_number(item["alpha"])
460
+ or (
461
+ not isinstance(item["tensor_sha256"], str)
462
+ or len(item["tensor_sha256"]) != 64
463
+ or any(c not in "0123456789abcdef" for c in item["tensor_sha256"])
464
+ )
465
+ ):
466
+ raise ManifoldFormatError(f"{location} has invalid component provenance")
467
+
468
+
469
+ def validate_manifold_sidecar_payload(
470
+ data: Any, *, location: str = "manifold sidecar",
471
+ ) -> dict[str, Any]:
472
+ """Validate every field of the exact current fitted-sidecar contract."""
473
+ from drowse.io.brand_migration import LEGACY_NAMES, migrate_legacy_record
474
+
475
+ legacy_brand = isinstance(data, dict) and any(
476
+ f"{name}_version" in data for name in LEGACY_NAMES
477
+ )
478
+ data = migrate_legacy_record(data)
479
+ if (
480
+ legacy_brand
481
+ and set(data) == MANIFOLD_SIDECAR_FIELDS - {"context_binding_sha256"}
482
+ and data["fit_mode"] == "baked"
483
+ ):
484
+ # Original baked artifacts cannot be refitted. An absent context-binding
485
+ # proof means unknown, never a verified context binding.
486
+ data["context_binding_sha256"] = None
487
+ if not isinstance(data, dict) or set(data) != MANIFOLD_SIDECAR_FIELDS:
488
+ raise ManifoldFormatError(f"{location} does not match the current exact schema")
489
+ validate_manifold_format_version(data["format_version"], location=location)
490
+ from drowse.core.manifold import validate_domain_spec
491
+
492
+ validate_domain_spec(data["domain"])
493
+ for key in ("name", "method", "drowse_version", "feature_space", "fit_mode"):
494
+ if not isinstance(data[key], str) or not data[key]:
495
+ raise ManifoldFormatError(f"{location} field {key!r} must be non-empty str")
496
+ if data["fit_mode"] not in _FIT_MODES_ALL:
497
+ raise ManifoldFormatError(f"{location} has invalid fit_mode")
498
+ allowed_fit_modes = MANIFOLD_METHOD_FIT_MODES.get(data["method"])
499
+ if allowed_fit_modes is None or data["fit_mode"] not in allowed_fit_modes:
500
+ raise ManifoldFormatError(
501
+ f"{location} has invalid method/fit_mode combination"
502
+ )
503
+ labels = data["node_labels"]
504
+ if (
505
+ isinstance(data["node_count"], bool)
506
+ or not isinstance(data["node_count"], int)
507
+ or not isinstance(labels, list)
508
+ or not labels
509
+ or data["node_count"] != len(labels)
510
+ or any(not isinstance(label, str) or not _LABEL_REGEX.match(label) for label in labels)
511
+ or len(set(labels)) != len(labels)
512
+ ):
513
+ raise ManifoldFormatError(f"{location} has invalid node identity")
514
+ _validate_hyperparams(data, location=location)
515
+ _validate_diagnostics(data["diagnostics"], location=location)
516
+ if not isinstance(data["sae_full_coverage"], bool):
517
+ raise ManifoldFormatError(f"{location} sae_full_coverage must be bool")
518
+ nullable_strings = (
519
+ "nodes_sha256", "sae_release", "sae_revision", "sae_fingerprint",
520
+ "model_fingerprint", "model_source_fingerprint", "capture_sha256",
521
+ "share_metric",
522
+ "subspace_metric", "resolved_fit_mode", "topology_winner",
523
+ "bake_policy", "source_model_id", "source_model_fingerprint",
524
+ )
525
+ if any(
526
+ data[key] is not None and (not isinstance(data[key], str) or not data[key])
527
+ for key in nullable_strings
528
+ ):
529
+ raise ManifoldFormatError(f"{location} has invalid nullable string metadata")
530
+ # The two preflight-provable digests are always writer-produced sha256, so
531
+ # they are held to the exact digest grammar rather than "non-empty string":
532
+ # a preflight that compares a recomputed digest against a malformed stored
533
+ # one must fail loudly at load, not quietly mismatch forever.
534
+ if any(
535
+ data[key] is not None and not _is_sha256_digest(data[key])
536
+ for key in (
537
+ "context_binding_sha256", "capture_render_sha256",
538
+ "baseline_prompts_sha256",
539
+ )
540
+ ):
541
+ raise ManifoldFormatError(f"{location} has invalid capture digest metadata")
542
+ if data["capture_version"] is not None and (
543
+ isinstance(data["capture_version"], bool)
544
+ or not isinstance(data["capture_version"], int)
545
+ or data["capture_version"] < 0
546
+ ):
547
+ raise ManifoldFormatError(f"{location} has invalid capture_version")
548
+ if data["fit_policy_version"] is not None and (
549
+ isinstance(data["fit_policy_version"], bool)
550
+ or not isinstance(data["fit_policy_version"], int)
551
+ or data["fit_policy_version"] < 0
552
+ ):
553
+ raise ManifoldFormatError(f"{location} has invalid fit_policy_version")
554
+ quality = data["transfer_quality_estimate"]
555
+ if quality is not None and (
556
+ isinstance(quality, bool)
557
+ or not isinstance(quality, (int, float))
558
+ or not math.isfinite(float(quality))
559
+ ):
560
+ raise ManifoldFormatError(f"{location} has invalid transfer quality")
561
+ fitted = data["fitted_layers"]
562
+ if (
563
+ not isinstance(fitted, list)
564
+ or any(isinstance(v, bool) or not isinstance(v, int) or v < 0 for v in fitted)
565
+ or fitted != sorted(set(fitted))
566
+ ):
567
+ raise ManifoldFormatError(f"{location} has invalid fitted_layers")
568
+ fitted_set = set(fitted)
569
+ # ``node_spread_per_layer`` is measured before DLS prunes axes/layers, so
570
+ # it records the complete *evaluated* layer roster. Its keys may therefore
571
+ # be a strict superset of ``fitted_layers`` (the exact tensor roster).
572
+ # Keeping that distinction is what lets a cache prove that a dropped layer
573
+ # was evaluated rather than accidentally omitted. Apply-time bakes remain
574
+ # tensor-bound and must use fitted-layer keys only.
575
+ _validate_layer_map(
576
+ data["node_spread_per_layer"], location=location,
577
+ field="node_spread_per_layer", fitted=fitted_set,
578
+ validate_value=lambda value: _finite_number(value) and float(value) >= 0,
579
+ require_fitted=False,
580
+ )
581
+ _validate_layer_map(
582
+ data["mahalanobis_share_per_layer"], location=location,
583
+ field="mahalanobis_share_per_layer", fitted=fitted_set,
584
+ validate_value=lambda value: _finite_number(value) and float(value) >= 0,
585
+ )
586
+ _validate_layer_map(
587
+ data["origin_per_layer"], location=location, field="origin_per_layer",
588
+ fitted=fitted_set,
589
+ validate_value=lambda value: (
590
+ isinstance(value, list) and bool(value)
591
+ and all(_finite_number(coord) for coord in value)
592
+ ),
593
+ )
594
+ _validate_layer_map(
595
+ data["sae_ids_by_layer"], location=location, field="sae_ids_by_layer",
596
+ fitted=fitted_set,
597
+ validate_value=lambda value: (
598
+ isinstance(value, str) and bool(value)
599
+ ),
600
+ require_fitted=False,
601
+ )
602
+ _validate_layer_map(
603
+ data["rbf_smoothing_per_layer"], location=location,
604
+ field="rbf_smoothing_per_layer", fitted=fitted_set,
605
+ validate_value=lambda value: (
606
+ isinstance(value, dict) and set(value) == {"lambda", "edf", "gcv"}
607
+ and all(_finite_number(item) for item in value.values())
608
+ ),
609
+ )
610
+ _validate_layer_map(
611
+ data["sigma_field_per_layer"], location=location,
612
+ field="sigma_field_per_layer", fitted=fitted_set,
613
+ validate_value=lambda value: (
614
+ isinstance(value, dict)
615
+ and set(value) == {"sigma_mean", "sigma_min", "sigma_max", "lambda"}
616
+ and all(_finite_number(item) and float(item) >= 0 for item in value.values())
617
+ ),
618
+ )
619
+ roles, kinds = data["node_roles"], data["node_kinds"]
620
+ if not isinstance(roles, list) or not isinstance(kinds, list) or len(roles) != len(labels) or len(kinds) != len(labels):
621
+ raise ManifoldFormatError(f"{location} node provenance is misaligned")
622
+ for label, role, kind in zip(labels, roles, kinds, strict=True):
623
+ _validate_node_role(data["name"], label, role)
624
+ _validate_node_kind(data["name"], label, kind)
625
+ _validate_topology_candidates(data["topology_candidates"], location=location)
626
+ _validate_components(data["components"], location=location)
627
+ is_auto = data["fit_mode"] == "auto"
628
+ if is_auto != bool(
629
+ data["resolved_fit_mode"] is not None
630
+ and data["topology_winner"] is not None
631
+ and data["topology_candidates"]
632
+ ):
633
+ raise ManifoldFormatError(f"{location} has inconsistent auto-topology provenance")
634
+ if not is_auto and any(
635
+ value is not None
636
+ for value in (data["resolved_fit_mode"], data["topology_winner"])
637
+ ) or not is_auto and data["topology_candidates"]:
638
+ raise ManifoldFormatError(f"{location} non-auto fit carries topology provenance")
639
+ is_merge = data["method"] == "merge"
640
+ if is_merge:
641
+ if data["fit_mode"] != "baked" or data["components"] is None or data["bake_policy"] != MERGE_BAKE_POLICY:
642
+ raise ManifoldFormatError(f"{location} has incomplete merge provenance")
643
+ elif data["components"] is not None or data["bake_policy"] is not None:
644
+ raise ManifoldFormatError(f"{location} non-merge fit carries merge provenance")
645
+ is_transfer = data["method"] == "manifold_procrustes_transfer"
646
+ transfer_values = (
647
+ data["source_model_id"], data["source_model_fingerprint"],
648
+ data["transfer_quality_estimate"],
649
+ )
650
+ if is_transfer:
651
+ if transfer_values[0] is None or transfer_values[1] is None:
652
+ raise ManifoldFormatError(f"{location} has incomplete transfer provenance")
653
+ elif any(value is not None for value in transfer_values):
654
+ raise ManifoldFormatError(f"{location} non-transfer fit carries transfer provenance")
655
+ sae_values = (
656
+ data["sae_release"], data["sae_revision"], data["sae_fingerprint"],
657
+ )
658
+ if data["feature_space"] == "raw":
659
+ if any(value is not None for value in sae_values) or data["sae_ids_by_layer"] or data["sae_full_coverage"]:
660
+ raise ManifoldFormatError(f"{location} raw fit carries SAE provenance")
661
+ elif (
662
+ not data["feature_space"].startswith("sae-")
663
+ or data["sae_release"] is None
664
+ or not data["sae_ids_by_layer"]
665
+ ):
666
+ raise ManifoldFormatError(f"{location} has incomplete SAE provenance")
667
+ return data
668
+
669
+
670
+ def canonical_manifold_sidecar_payload(
671
+ *, name: str, method: str, drowse_version: str, domain: dict[str, Any],
672
+ node_labels: list[str], feature_space: str, fit_mode: str,
673
+ hyperparams: dict[str, Any] | None = None,
674
+ diagnostics: dict[str, Any] | None = None,
675
+ node_spread_per_layer: dict[str, Any] | None = None,
676
+ fitted_layers: list[int] | None = None,
677
+ semantic_metadata: Mapping[str, Any] | None = None,
678
+ ) -> dict[str, Any]:
679
+ """Build the one exact current fitted-manifold sidecar shape."""
680
+ count = len(node_labels)
681
+ payload = {
682
+ "format_version": MANIFOLD_FORMAT_VERSION,
683
+ "name": name, "method": method, "drowse_version": drowse_version,
684
+ "domain": domain, "node_count": count, "node_labels": node_labels,
685
+ "feature_space": feature_space, "fit_mode": fit_mode,
686
+ "hyperparams": dict(hyperparams or {}),
687
+ "diagnostics": dict(diagnostics or {}),
688
+ "node_spread_per_layer": dict(node_spread_per_layer or {}),
689
+ "mahalanobis_share_per_layer": {}, "origin_per_layer": {},
690
+ "nodes_sha256": None, "sae_release": None, "sae_revision": None,
691
+ "sae_fingerprint": None, "sae_ids_by_layer": {},
692
+ "sae_full_coverage": False, "model_fingerprint": None,
693
+ "model_source_fingerprint": None,
694
+ "capture_sha256": None, "capture_version": None,
695
+ "context_binding_sha256": None, "capture_render_sha256": None,
696
+ "baseline_prompts_sha256": None,
697
+ "fitted_layers": list(fitted_layers or []),
698
+ "fit_policy_version": None, "share_metric": None,
699
+ "subspace_metric": None, "rbf_smoothing_per_layer": {},
700
+ "sigma_field_per_layer": {}, "resolved_fit_mode": None,
701
+ "topology_winner": None, "topology_candidates": [],
702
+ "node_roles": [None] * count, "node_kinds": [None] * count,
703
+ "components": None, "bake_policy": None, "source_model_id": None,
704
+ "source_model_fingerprint": None, "transfer_quality_estimate": None,
705
+ }
706
+ for key in (
707
+ "sae_release", "sae_revision", "sae_fingerprint", "sae_ids_by_layer",
708
+ "sae_full_coverage", "resolved_fit_mode", "topology_winner",
709
+ "topology_candidates", "components", "bake_policy", "source_model_id",
710
+ "source_model_fingerprint", "transfer_quality_estimate",
711
+ "context_binding_sha256",
712
+ ):
713
+ if semantic_metadata is not None and key in semantic_metadata:
714
+ payload[key] = semantic_metadata[key]
715
+ return validate_manifold_sidecar_payload(payload)
716
+
717
+
718
+ def _validate_node_role(name: str, label: str, role: Any) -> str | None:
719
+ """Validate an optional per-node ``role`` field.
720
+
721
+ ``None`` means "use the standard assistant baseline". A non-empty string must match
722
+ :data:`drowse.core.role_templates._ROLE_SLUG_RE`
723
+ (``[a-z0-9._-]+``). Family-unsupported (Mistral-3) is *not*
724
+ checked here — the folder is model-agnostic; the check fires when
725
+ :func:`drowse.core.role_templates.apply_with_role` runs at fit time.
726
+ """
727
+ if role is None:
728
+ return None
729
+ if not isinstance(role, str) or not _ROLE_SLUG_RE.match(role):
730
+ raise ManifoldFormatError(
731
+ f"manifold {name!r} node {label!r} role {role!r} invalid; "
732
+ f"must match {_ROLE_SLUG_RE.pattern}"
733
+ )
734
+ return role
735
+
736
+
737
+ _NODE_KINDS = ("abstract", "concrete", "custom")
738
+
739
+
740
+ def _validate_node_kind(name: str, label: str, kind: Any) -> str | None:
741
+ """Validate an optional per-node ``kind`` field.
742
+
743
+ ``None`` means "unspecified". A non-empty value must be one of
744
+ :data:`_NODE_KINDS` (``"abstract"`` / ``"concrete"`` / ``"custom"``).
745
+ Generation-time provenance only — it selects the system template and
746
+ elicitation role label when authoring a node's conversational corpus
747
+ (``custom`` = a caller-supplied system prompt, no role swap), and never
748
+ feeds the fit.
749
+ """
750
+ if kind is None:
751
+ return None
752
+ if not isinstance(kind, str) or kind not in _NODE_KINDS:
753
+ raise ManifoldFormatError(
754
+ f"manifold {name!r} node {label!r} kind {kind!r} invalid; "
755
+ f"must be one of {_NODE_KINDS}"
756
+ )
757
+ return kind
758
+
759
+
760
+ class ManifoldFormatError(ValueError, DrowseError):
761
+ """Raised when a manifold folder is malformed or fails integrity."""
762
+
763
+
764
+ def validate_manifold_format_version(
765
+ value: Any, *, location: str = "manifold",
766
+ ) -> int:
767
+ """Validate the shared readable/writable manifold format boundary."""
768
+ if (
769
+ not isinstance(value, int)
770
+ or isinstance(value, bool)
771
+ or value != MANIFOLD_FORMAT_VERSION
772
+ ):
773
+ raise ManifoldFormatError(
774
+ f"{location} has format_version={value!r}; "
775
+ f"need exactly {MANIFOLD_FORMAT_VERSION}. Regenerate or re-fit it "
776
+ "with the current drowse."
777
+ )
778
+ return value
779
+
780
+
781
+ class BakedManifoldError(ValueError, DrowseError):
782
+ """Raised when an operation invalid for a corpus-less baked manifold runs.
783
+
784
+ A baked manifold (``fit_mode == "baked"``) has no node corpus and its
785
+ geometry lives only in the per-model tensor, so a re-fit is impossible.
786
+ Tensor-deleting lifecycle ops (``clear``, scoped ``refresh``) would
787
+ therefore destroy the only copy of the geometry irreversibly — they
788
+ refuse with this error and point the caller at ``manifold rm`` instead.
789
+ """
790
+
791
+
792
+ def min_nodes(n: int) -> int:
793
+ """Minimum node count for an ``n``-dimensional manifold.
794
+
795
+ ``2*n + 1`` — one center node plus two extremes per authoring axis,
796
+ so every axis is sampled at three levels and curvature is fittable.
797
+ At ``n == 1`` this is 3, the historic floor. This is *necessary* but
798
+ not *sufficient*: the nodes must also be poised (affinely span the
799
+ embedding), which :func:`drowse.core.manifold.fit_rbf_interpolant`
800
+ enforces.
801
+ """
802
+ return 2 * n + 1
803
+
804
+
805
+ def _node_filename(index: int, label: str) -> str:
806
+ """Canonical zero-padded node corpus filename: ``NN_<label>.json``."""
807
+ return f"{index:02d}_{label}.json"
808
+
809
+
810
+ def hash_manifold_files(folder: Path) -> dict[str, str]:
811
+ """Return ``{relative_posix_path: sha256}`` for a manifold's fitted files.
812
+
813
+ Covers the top-level fitted artifacts (the per-model ``.safetensors``
814
+ tensors and their ``.json`` sidecars); skips ``manifold.json`` itself
815
+ and the ``nodes/`` corpus. The node corpus is user-authored and
816
+ *expected* to be edited — editing it is the re-fit trigger, tracked
817
+ by the per-tensor ``nodes_sha256`` sidecar field — so hashing it into
818
+ an integrity manifest would reject the normal workflow. Used to
819
+ (re)populate ``manifold.json.files`` after a fit.
820
+ """
821
+ out: dict[str, str] = {}
822
+ for entry in sorted(folder.iterdir()):
823
+ if entry.is_file() and entry.name != "manifold.json":
824
+ out[entry.name] = hash_file(entry)
825
+ return out
826
+
827
+
828
+ def _canonical_json(obj: Any) -> bytes:
829
+ """Stable canonical-JSON encoding for hashing."""
830
+ return json.dumps(obj, sort_keys=True, separators=(",", ":")).encode("utf-8")
831
+
832
+
833
+ @dataclass
834
+ class ManifoldSidecar:
835
+ """JSON sidecar beside a fitted per-model manifold tensor.
836
+
837
+ Discover-mode fits (``fit_mode != "authored"``) additionally carry
838
+ the derived ``coords``, the hyperparameters used, and the per-method
839
+ diagnostics block (PCA variance bars or spectral spectrum) so the
840
+ sidecar is self-describing for downstream inspection.
841
+ """
842
+
843
+ # ``manifold_pca`` / ``manifold_sae`` for authored fits;
844
+ # ``manifold_discover_pca`` / ``manifold_discover_spectral`` /
845
+ # ``manifold_discover_sae`` for discover-mode fits (the SAE label
846
+ # collapses across pca/spectral because the SAE reconstruction
847
+ # rides into both before the coord derivation runs).
848
+ method: str
849
+ drowse_version: str
850
+ domain: dict[str, Any]
851
+ node_count: int
852
+ node_labels: list[str]
853
+ feature_space: str = "raw"
854
+ nodes_sha256: Optional[str] = None
855
+ sae_release: Optional[str] = None
856
+ sae_revision: Optional[str] = None
857
+ sae_fingerprint: Optional[str] = None
858
+ sae_ids_by_layer: dict[str, str] = field(default_factory=dict)
859
+ sae_full_coverage: bool = False
860
+ model_fingerprint: Optional[str] = None
861
+ # The *pre-load-verifiable* checkpoint identity
862
+ # (:func:`drowse.core.model.model_source_fingerprint`): resolved Hub commit
863
+ # or exact local file hashes, plus the load representation. Distinct from
864
+ # ``source_model_fingerprint``, which names the *other* model a cross-model
865
+ # Procrustes transfer came from. This one exists so a fit no-op can be
866
+ # proven without instantiating the transformer.
867
+ model_source_fingerprint: Optional[str] = None
868
+ capture_sha256: Optional[str] = None
869
+ # The two provable halves of ``capture_sha256``. ``capture_version`` is the
870
+ # capture-identity schema revision (:data:`drowse.core.extraction.
871
+ # MANIFOLD_CAPTURE_VERSION`); ``capture_render_sha256`` digests the
872
+ # tokenizer-render half alone — node partition plus every rendered row's
873
+ # token ids and pool index — which is exactly the part reproducible from
874
+ # tokenizer + baseline prompts + corpus with no weights loaded. The
875
+ # remaining half (model class, config, commit, load representation) is
876
+ # covered by ``model_fingerprint`` + ``model_source_fingerprint``.
877
+ capture_version: Optional[int] = None
878
+ capture_render_sha256: Optional[str] = None
879
+ # Digest of the exact shared baseline prompts (or a templated manifold's
880
+ # resolved contexts) the corpus rows were paired against. Redundant with
881
+ # ``capture_render_sha256`` by construction, and deliberately so: a
882
+ # baseline-prompt edit is the failure mode a weight-free preflight most
883
+ # needs to name, so it gets its own directly comparable channel.
884
+ baseline_prompts_sha256: Optional[str] = None
885
+ fitted_layers: list[int] = field(default_factory=list)
886
+ fit_policy_version: Optional[int] = None
887
+ # Discover-mode-only fields. ``None`` on authored fits.
888
+ fit_mode: str = "authored"
889
+ hyperparams: dict[str, Any] = field(default_factory=dict)
890
+ diagnostics: dict[str, Any] = field(default_factory=dict)
891
+ # Per-layer whitened between-node spread ``{str(L): tr(G_L)}`` — the
892
+ # concept's signal-concentration profile across the stack, in
893
+ # background-σ² units (comparable across layers). A diagnostic readout
894
+ # of "where does this concept live", distinct from the apply-time
895
+ # ``mahalanobis_share`` (which restricts the same whitened spread to the
896
+ # steerable subspace). Empty when the current fit has no measurements.
897
+ node_spread_per_layer: dict[str, Any] = field(default_factory=dict)
898
+ # Merge provenance on a ``fit_mode="baked"`` manifold — the
899
+ # ``{coord: {alpha, tensor_sha256}}`` map written by
900
+ # :func:`drowse.io.bake.merge_into_manifold`. ``None`` on every fit that
901
+ # isn't a merge (the common case). Informational only — a baked
902
+ # manifold never re-fits, so nothing branches on it.
903
+ components: Optional[dict[str, Any]] = None
904
+ bake_policy: Optional[str] = None
905
+ # Per-node assistant-role substitution used at fit time, in
906
+ # ``node_labels`` index order. ``None`` for a given node (and an
907
+ # empty list as a whole) = "standard assistant baseline" — the
908
+ # default. The same
909
+ # information rides ``ManifoldFolder.node_roles`` but the sidecar
910
+ # carries an independent copy so a downstream consumer
911
+ # (``manifold show``, the webui inspector) doesn't have to
912
+ # round-trip through the folder to know which role each node was
913
+ # pooled under.
914
+ node_roles: list[str | None] = field(default_factory=list)
915
+ # Per-node conceptual kind ("abstract"/"concrete"), ``node_labels`` order.
916
+ # Mirrors ``node_roles``' independent-copy rationale; generation provenance.
917
+ node_kinds: list[str | None] = field(default_factory=list)
918
+ # Metric labels: how the per-layer share was weighted and how the subspace
919
+ # basis was selected. Both read ``"mahalanobis"`` on an ordinary fit; a
920
+ # monopolar fold labels its raw-δ̂ basis ``"euclidean"`` (a basis-selection
921
+ # label, not a metric fallback — ``concept − ν`` cancels common-mode by
922
+ # differencing, like DiM).
923
+ share_metric: Optional[str] = None
924
+ subspace_metric: Optional[str] = None
925
+ # Topology-selection evidence, stamped for ``fit_mode="auto"`` only (all
926
+ # three are ``None``/empty otherwise — the payload validator enforces that
927
+ # consistency). ``resolved_fit_mode`` is the geometry ``select_topology``
928
+ # picked for *this* model (``pca`` flat / ``spectral`` curved),
929
+ # ``topology_winner`` names the winning candidate, and
930
+ # ``topology_candidates`` is the full ranked field — each
931
+ # ``{name, fit_mode, intrinsic_dim, score, viable, reason}`` — i.e. the GCV
932
+ # + persistent-homology evidence behind the flat-vs-curved-vs-periodic call.
933
+ resolved_fit_mode: Optional[str] = None
934
+ topology_winner: Optional[str] = None
935
+ topology_candidates: list[dict[str, Any]] = field(default_factory=list)
936
+ # Curved-fit per-layer summaries. ``rbf_smoothing_per_layer``
937
+ # ``{L: {lambda, edf, gcv}}`` records the GCV-selected penalized-RBF
938
+ # smoothing; ``sigma_field_per_layer``
939
+ # ``{L: {sigma_mean, sigma_min, sigma_max, lambda}}`` summarizes the
940
+ # fuzzy-manifold tube thickness. The σ-RBF tensors themselves ride the
941
+ # safetensors — these are the inspector summaries. Empty on flat fits.
942
+ rbf_smoothing_per_layer: dict[str, Any] = field(default_factory=dict)
943
+ sigma_field_per_layer: dict[str, Any] = field(default_factory=dict)
944
+
945
+ @property
946
+ def evaluated_layers(self) -> list[int]:
947
+ """Layers evaluated by the fit before DLS pruning.
948
+
949
+ Multi-node activation fits always measure ``node_spread_per_layer``
950
+ before selecting the retained tensor layers. Artifact kinds without
951
+ that diagnostic (for example baked and monopolar manifolds) evaluate
952
+ exactly their fitted tensor roster.
953
+ """
954
+ if self.node_spread_per_layer:
955
+ return sorted(int(layer) for layer in self.node_spread_per_layer)
956
+ return list(self.fitted_layers)
957
+
958
+ @classmethod
959
+ def load(cls, path: Path) -> "ManifoldSidecar":
960
+ data = load_manifold_sidecar_data(path)
961
+ return cls(
962
+ method=data["method"],
963
+ drowse_version=data["drowse_version"],
964
+ domain=data["domain"],
965
+ node_count=data["node_count"],
966
+ node_labels=data["node_labels"],
967
+ feature_space=data["feature_space"],
968
+ nodes_sha256=data["nodes_sha256"],
969
+ sae_release=data["sae_release"],
970
+ sae_revision=data["sae_revision"],
971
+ sae_fingerprint=data["sae_fingerprint"],
972
+ sae_ids_by_layer=dict(data["sae_ids_by_layer"]),
973
+ sae_full_coverage=data["sae_full_coverage"],
974
+ model_fingerprint=data["model_fingerprint"],
975
+ model_source_fingerprint=data["model_source_fingerprint"],
976
+ capture_sha256=data["capture_sha256"],
977
+ capture_version=data["capture_version"],
978
+ capture_render_sha256=data["capture_render_sha256"],
979
+ baseline_prompts_sha256=data["baseline_prompts_sha256"],
980
+ fitted_layers=list(data["fitted_layers"]),
981
+ fit_policy_version=data["fit_policy_version"],
982
+ fit_mode=data["fit_mode"],
983
+ hyperparams=dict(data["hyperparams"]),
984
+ diagnostics=dict(data["diagnostics"]),
985
+ node_spread_per_layer=dict(data["node_spread_per_layer"]),
986
+ node_roles=list(data["node_roles"]),
987
+ node_kinds=list(data["node_kinds"]),
988
+ components=data["components"],
989
+ bake_policy=data["bake_policy"],
990
+ share_metric=data["share_metric"],
991
+ subspace_metric=data["subspace_metric"],
992
+ resolved_fit_mode=data["resolved_fit_mode"],
993
+ topology_winner=data["topology_winner"],
994
+ topology_candidates=list(data["topology_candidates"]),
995
+ rbf_smoothing_per_layer=dict(data["rbf_smoothing_per_layer"]),
996
+ sigma_field_per_layer=dict(data["sigma_field_per_layer"]),
997
+ )
998
+
999
+
1000
+ def fitted_sidecar_is_current(path: Path) -> bool:
1001
+ """Whether a fitted sidecar carries the current schema generation.
1002
+
1003
+ A cheap version peek, deliberately narrower than the full validator: only a
1004
+ well-formed *older or newer* stamp counts as stale. Unreadable bytes, a
1005
+ non-object payload, and a missing or non-integer ``format_version`` all
1006
+ return ``True`` so the strict loader still runs and reports what is actually
1007
+ wrong — a format bump must invalidate fits, never launder corruption.
1008
+
1009
+ Early v10 writers predated the nullable ``context_binding_sha256`` proof.
1010
+ That one exact predecessor field set is also stale: it is safe to refit from
1011
+ the corpus, but it cannot prove that its capture used the current context.
1012
+ Other same-version schema damage remains corruption and reaches the strict
1013
+ loader below.
1014
+ """
1015
+ try:
1016
+ with open(path) as handle:
1017
+ payload = json.load(handle)
1018
+ value = payload.get("format_version")
1019
+ except (OSError, json.JSONDecodeError, AttributeError, TypeError):
1020
+ return True
1021
+ if isinstance(value, bool) or not isinstance(value, int):
1022
+ return True
1023
+ if value != MANIFOLD_FORMAT_VERSION:
1024
+ return False
1025
+ from drowse.io.brand_migration import migrate_legacy_record
1026
+
1027
+ payload = migrate_legacy_record(payload)
1028
+ legacy_v10_fields = MANIFOLD_SIDECAR_FIELDS - {"context_binding_sha256"}
1029
+ return set(payload) != legacy_v10_fields
1030
+
1031
+
1032
+ def load_manifold_sidecar_data(path: Path) -> dict[str, Any]:
1033
+ """Read and validate the exact current fitted-manifold sidecar shape.
1034
+
1035
+ The reader half of :func:`validate_manifold_sidecar_payload` — one
1036
+ validator gates writer and reader, so a sidecar that loads is exactly a
1037
+ sidecar the writer could have produced. Only the version check is hoisted
1038
+ ahead of it: an artifact from an older format should say so rather than
1039
+ report whichever field the current schema happens to miss first.
1040
+ """
1041
+ location = f"manifold sidecar {path}"
1042
+ try:
1043
+ with open(path) as f:
1044
+ data = json.load(f)
1045
+ except (json.JSONDecodeError, OSError) as exc:
1046
+ raise ManifoldFormatError(f"{location} is unreadable: {exc}") from exc
1047
+ if not isinstance(data, dict):
1048
+ raise ManifoldFormatError(f"{location} must be a JSON object")
1049
+ validate_manifold_format_version(data.get("format_version"), location=location)
1050
+ return validate_manifold_sidecar_payload(data, location=location)
1051
+
1052
+
1053
+ @dataclass
1054
+ class ManifoldFolder:
1055
+ """A manifold artifact folder on disk.
1056
+
1057
+ Discovery + corpus + integrity only — the fitted RBF tensors are
1058
+ loaded through :func:`drowse.io.manifold_tensors.load_manifold`.
1059
+
1060
+ Two folder shapes share this class via the ``fit_mode`` field:
1061
+
1062
+ - ``fit_mode == "authored"``: the
1063
+ user supplied per-node ``coords`` on a declared ``domain``. The
1064
+ fit pipeline embeds the coords and runs straight into
1065
+ ``fit_layer_subspace``.
1066
+ - ``fit_mode in {"pca", "spectral"}`` (discover mode): nodes carry
1067
+ ``{label}`` only — no coords, no top-level ``domain``. The fit
1068
+ pipeline pools per-node centroids, derives coords via
1069
+ :func:`drowse.core.manifold.discover_coords` (per-model, since
1070
+ different models embed the same heap differently), wraps them in
1071
+ a ``CustomDomain(k)`` with identity embedding, and fits.
1072
+
1073
+ For authored folders ``domain`` is the tagged-union spec and
1074
+ ``node_coords`` is the K×n list; for discover folders both are
1075
+ empty placeholders (``{}`` and ``[]``) — the real geometry lives
1076
+ in the per-model sidecar after the fit runs. ``hyperparams``
1077
+ captures the discover-mode knobs (``max_dim``, ``var_threshold``
1078
+ for PCA; ``max_dim``, ``k_nn``, ``bandwidth`` for spectral) and is
1079
+ empty for authored folders.
1080
+ """
1081
+
1082
+ folder: Path
1083
+ name: str
1084
+ description: str
1085
+ domain: dict[str, Any]
1086
+ node_labels: list[str]
1087
+ node_coords: list[list[float]]
1088
+ files: dict[str, str]
1089
+ fit_mode: str = "authored"
1090
+ hyperparams: dict[str, Any] = field(default_factory=dict)
1091
+ # Provenance tier.
1092
+ # ``"local"`` (default) — hand-authored / generated under ``local/``;
1093
+ # ``"bundled"`` — shipped under ``drowse/data/manifolds/`` and
1094
+ # materialized into ``default/`` (set by namespace at refresh time,
1095
+ # not stored); ``"hf://<owner>/<name>[@rev]"`` — pulled from the HF
1096
+ # hub (stamped by :func:`drowse.io.hf_manifolds.pull_manifold`). The
1097
+ # ``refresh_manifold`` lifecycle reads it to decide where to re-pull:
1098
+ # ``local`` is silently skipped (nothing upstream), ``hf://`` re-pulls.
1099
+ source: str = "local"
1100
+ # Per-node assistant-role substitution for role-augmented manifolds
1101
+ # (e.g. a persona manifold where each node is a persona). Aligned
1102
+ # with ``node_labels`` index-by-index. ``None`` for a given node =
1103
+ # "use the standard assistant baseline". An all-``None`` list keeps the
1104
+ # ordinary behavior — the centroid pooling
1105
+ # just goes through the default chat-template branch.
1106
+ node_roles: list[str | None] = field(default_factory=list)
1107
+ # Per-node conceptual ``kind`` — ``"abstract"`` (trait) or ``"concrete"``
1108
+ # (entity), aligned with ``node_labels`` index-by-index. ``None`` =
1109
+ # unspecified. Generation-time only (system template + elicitation role
1110
+ # label); never consumed by the fit.
1111
+ node_kinds: list[str | None] = field(default_factory=list)
1112
+ # Category tags carried so category-grouped probe bootstrap
1113
+ # (``load_default_manifolds`` -> ``_bootstrap_manifold_probes``) keeps
1114
+ # working once a steering vector lives as a 2-node ``pca`` manifold.
1115
+ # Always explicit in the manifest; empty means uncategorized.
1116
+ tags: list[str] = field(default_factory=list)
1117
+ # Reference to a standalone template artifact
1118
+ # (``drowse.io.templates.TemplateFolder``, ``<ns>/<name>`` or bare name).
1119
+ # A *templated* discover manifold's node corpora are the slot-filled
1120
+ # assistant turns derived from that template; the manifold stores the derived
1121
+ # corpus in ``nodes/`` (like any discover folder) and keeps this reference so
1122
+ # the fit can resolve the template's **multi-turn contexts** as the
1123
+ # per-manifold elicitation prefixes (the template analogue of the shared
1124
+ # baseline prompts). The template is the single authoring source; the corpus
1125
+ # is its materialization. ``None`` marks every non-templated manifold. The resolved
1126
+ # template's content hash folds into ``nodes_sha256`` (a context edit re-fits)
1127
+ # and the ref is preserved across re-fits by ``write_metadata``.
1128
+ template_ref: str | None = None
1129
+ # tensor stem (``<safe_model>`` or ``<safe_model>_sae-<rel>``) -> sidecar.
1130
+ _sidecars: dict[str, ManifoldSidecar] = field(default_factory=dict)
1131
+
1132
+ @property
1133
+ def is_discover(self) -> bool:
1134
+ """True when this folder's coords are derived per-model rather than authored."""
1135
+ return self.fit_mode in _FIT_MODES_DISCOVER
1136
+
1137
+ @classmethod
1138
+ def load(
1139
+ cls, folder: Path, *, verify_manifest: bool = True,
1140
+ ) -> "ManifoldFolder":
1141
+ """Parse a manifold folder.
1142
+
1143
+ Runtime/install/push callers keep the default full integrity walk.
1144
+ Metadata-only inventory, authoring, and lifecycle routing callers may set
1145
+ ``verify_manifest=False``; fit callers do likewise, hashing the live
1146
+ corpus into ``nodes_sha256`` and validating only the requested fitted
1147
+ tensor. This avoids rereading every historical model/SAE payload when no
1148
+ payload is being consumed. Structural checks and tensor/sidecar pairing
1149
+ still run either way.
1150
+ """
1151
+ folder = Path(folder)
1152
+ meta_path = folder / "manifold.json"
1153
+ if not meta_path.exists():
1154
+ raise ManifoldFormatError(f"no manifold.json in {folder}")
1155
+ try:
1156
+ with open(meta_path) as f:
1157
+ data = json.load(f)
1158
+ except (json.JSONDecodeError, OSError) as e:
1159
+ # A malformed manifest must surface as ManifoldFormatError so
1160
+ # callers (iter_manifold_folders, the HTTP routes) catch it
1161
+ # rather than letting a bare JSONDecodeError become a 500.
1162
+ raise ManifoldFormatError(
1163
+ f"manifold.json in {folder} is unreadable: {e}"
1164
+ ) from e
1165
+ if not isinstance(data, dict):
1166
+ raise ManifoldFormatError(
1167
+ f"manifold.json in {folder} must be a JSON object"
1168
+ )
1169
+
1170
+ validate_manifold_format_version(
1171
+ data.get("format_version"),
1172
+ location=f"manifold.json in {folder}",
1173
+ )
1174
+
1175
+ common_fields = {
1176
+ "format_version", "name", "description", "fit_mode", "nodes",
1177
+ "files", "source", "tags", "artifact_id", "fit_epochs",
1178
+ "template_ref",
1179
+ }
1180
+
1181
+ name = data.get("name")
1182
+ if not isinstance(name, str) or not NAME_REGEX.match(name):
1183
+ raise ManifoldFormatError(
1184
+ f"manifold name {name!r} invalid; must match {NAME_REGEX.pattern}"
1185
+ )
1186
+
1187
+ # Authored manifolds carry ``domain`` + per-node ``coords``. Discover
1188
+ # manifolds omit ``domain`` and derive coordinates per model at fit time.
1189
+ fit_mode = data.get("fit_mode")
1190
+ if fit_mode not in _FIT_MODES_ALL:
1191
+ raise ManifoldFormatError(
1192
+ f"manifold {name!r} fit_mode {fit_mode!r} invalid; "
1193
+ f"expected one of {sorted(_FIT_MODES_ALL)}"
1194
+ )
1195
+ mode_fields = (
1196
+ {"domain"} if fit_mode in {"authored", "baked"}
1197
+ else {"hyperparams"}
1198
+ )
1199
+ unknown = set(data) - common_fields - mode_fields
1200
+ if unknown:
1201
+ raise ManifoldFormatError(
1202
+ f"manifold {name!r} has unknown field(s): {sorted(unknown)}"
1203
+ )
1204
+ for key, expected in (
1205
+ ("description", str), ("files", dict), ("source", str),
1206
+ ("tags", list),
1207
+ ):
1208
+ if not isinstance(data.get(key), expected):
1209
+ raise ManifoldFormatError(
1210
+ f"manifold {name!r} field {key!r} must be {expected.__name__}"
1211
+ )
1212
+ if not data["source"]:
1213
+ raise ManifoldFormatError(
1214
+ f"manifold {name!r} field 'source' must be non-empty"
1215
+ )
1216
+ if "artifact_id" in data and (
1217
+ not isinstance(data["artifact_id"], str) or not data["artifact_id"]
1218
+ ):
1219
+ raise ManifoldFormatError(
1220
+ f"manifold {name!r} field 'artifact_id' must be a non-empty str"
1221
+ )
1222
+ if "fit_epochs" in data and not isinstance(data["fit_epochs"], dict):
1223
+ raise ManifoldFormatError(
1224
+ f"manifold {name!r} field 'fit_epochs' must be dict"
1225
+ )
1226
+
1227
+ nodes = data.get("nodes")
1228
+ if not isinstance(nodes, list) or not nodes:
1229
+ raise ManifoldFormatError(
1230
+ f"manifold {name!r} needs a non-empty 'nodes' list"
1231
+ )
1232
+
1233
+ node_labels: list[str] = []
1234
+ node_coords: list[list[float]] = []
1235
+ node_roles: list[str | None] = []
1236
+ node_kinds: list[str | None] = []
1237
+ hyperparams: dict[str, Any] = {}
1238
+ domain_spec: dict[str, Any]
1239
+
1240
+ if fit_mode == "authored":
1241
+ domain_spec_in: Any = data.get("domain") or {}
1242
+ if not isinstance(domain_spec_in, dict) or not domain_spec_in:
1243
+ raise ManifoldFormatError(
1244
+ f"authored manifold {name!r} needs a 'domain' object"
1245
+ )
1246
+ domain_spec = domain_spec_in
1247
+ try:
1248
+ domain = domain_from_spec(domain_spec)
1249
+ except (ValueError, KeyError) as e:
1250
+ raise ManifoldFormatError(
1251
+ f"manifold {name!r} has an invalid domain: {e}"
1252
+ ) from e
1253
+ n = domain.intrinsic_dim
1254
+ floor = min_nodes(n)
1255
+ if len(nodes) < floor:
1256
+ raise ManifoldFormatError(
1257
+ f"manifold {name!r} ({n}-D domain) needs a 'nodes' "
1258
+ f"list of >= {floor} entries, got {len(nodes)}"
1259
+ )
1260
+ for entry in nodes:
1261
+ if not isinstance(entry, dict):
1262
+ raise ManifoldFormatError(
1263
+ f"manifold {name!r} node {entry!r} must be an "
1264
+ f"object with 'label' and 'coords'"
1265
+ )
1266
+ expected_node = {"label", "coords", "role", "kind"}
1267
+ if set(entry) != expected_node:
1268
+ raise ManifoldFormatError(
1269
+ f"manifold {name!r} node fields must be "
1270
+ f"{sorted(expected_node)}"
1271
+ )
1272
+ label = entry.get("label")
1273
+ if not isinstance(label, str) or not _LABEL_REGEX.match(label):
1274
+ raise ManifoldFormatError(
1275
+ f"manifold {name!r} node label {label!r} invalid; "
1276
+ f"a node label is a grammar-addressable identifier "
1277
+ f"(no '.', reserved as the bipolar separator) — "
1278
+ f"must match {_LABEL_REGEX.pattern}"
1279
+ )
1280
+ coords = entry.get("coords")
1281
+ if (
1282
+ not isinstance(coords, list)
1283
+ or len(coords) != n
1284
+ or not all(_finite_number(c) for c in coords)
1285
+ ):
1286
+ raise ManifoldFormatError(
1287
+ f"manifold {name!r} node {label!r} needs 'coords' "
1288
+ f"of {n} number(s), got {coords!r}"
1289
+ )
1290
+ node_labels.append(label)
1291
+ node_coords.append([float(c) for c in coords])
1292
+ node_roles.append(_validate_node_role(name, label, entry.get("role")))
1293
+ node_kinds.append(_validate_node_kind(name, label, entry.get("kind")))
1294
+ _warn_authoring_quality(name, domain, node_coords)
1295
+ elif fit_mode == "baked":
1296
+ # Baked mode: a pre-fitted, corpus-less artifact (merge output /
1297
+ # imported control vector). Like authored it carries a display
1298
+ # ``domain`` (the ``CustomDomain`` the fold produced) so the
1299
+ # inspector/summary have geometry to report; like discover its
1300
+ # nodes are label-only (no ``coords`` — the real per-layer node
1301
+ # coords live baked in the tensor). There is no corpus and no
1302
+ # re-fit, so ``min_nodes`` and the authoring-quality advisory
1303
+ # are both inapplicable.
1304
+ domain_spec_in = data.get("domain") or {}
1305
+ if not isinstance(domain_spec_in, dict) or not domain_spec_in:
1306
+ raise ManifoldFormatError(
1307
+ f"baked manifold {name!r} needs a 'domain' object"
1308
+ )
1309
+ domain_spec = domain_spec_in
1310
+ try:
1311
+ domain_from_spec(domain_spec)
1312
+ except (ValueError, KeyError) as e:
1313
+ raise ManifoldFormatError(
1314
+ f"manifold {name!r} has an invalid domain: {e}"
1315
+ ) from e
1316
+ for entry in nodes:
1317
+ if not isinstance(entry, dict):
1318
+ raise ManifoldFormatError(
1319
+ f"baked manifold {name!r} node {entry!r} must be an "
1320
+ f"object with 'label'"
1321
+ )
1322
+ expected_node = {"label", "role", "kind"}
1323
+ if set(entry) != expected_node:
1324
+ raise ManifoldFormatError(
1325
+ f"baked manifold {name!r} node fields must be "
1326
+ f"{sorted(expected_node)}"
1327
+ )
1328
+ label = entry.get("label")
1329
+ if not isinstance(label, str) or not _LABEL_REGEX.match(label):
1330
+ raise ManifoldFormatError(
1331
+ f"baked manifold {name!r} node label {label!r} "
1332
+ f"invalid; must match {_LABEL_REGEX.pattern}"
1333
+ )
1334
+ if "coords" in entry:
1335
+ raise ManifoldFormatError(
1336
+ f"baked manifold {name!r} node {label!r} must not "
1337
+ f"carry 'coords' — baked geometry lives in the tensor"
1338
+ )
1339
+ node_labels.append(label)
1340
+ node_roles.append(_validate_node_role(name, label, entry.get("role")))
1341
+ node_kinds.append(_validate_node_kind(name, label, entry.get("kind")))
1342
+ else:
1343
+ # Discover mode: no ``domain`` field, no per-node ``coords``.
1344
+ # The fit pipeline derives coords per-model from the
1345
+ # activations and wraps them in a ``CustomDomain(k)``. We
1346
+ # do not even know the intrinsic dimension until after the
1347
+ # fit, so the ``min_nodes(k)`` floor is enforced at fit time
1348
+ # (once ``k`` is picked) rather than here.
1349
+ if data.get("domain"):
1350
+ raise ManifoldFormatError(
1351
+ f"discover-mode manifold {name!r} must not carry a "
1352
+ f"'domain' field — coords are derived per-model at "
1353
+ f"fit time"
1354
+ )
1355
+ domain_spec = {}
1356
+ raw_hyperparams = data.get("hyperparams")
1357
+ if not isinstance(raw_hyperparams, dict):
1358
+ raise ManifoldFormatError(
1359
+ f"discover manifold {name!r} field 'hyperparams' must be dict"
1360
+ )
1361
+ hyperparams = sanitize_hyperparams(fit_mode, raw_hyperparams)
1362
+ for entry in nodes:
1363
+ if not isinstance(entry, dict):
1364
+ raise ManifoldFormatError(
1365
+ f"discover manifold {name!r} node {entry!r} must "
1366
+ f"be an object with 'label'"
1367
+ )
1368
+ expected_node = {"label", "role", "kind"}
1369
+ if set(entry) != expected_node:
1370
+ raise ManifoldFormatError(
1371
+ f"discover manifold {name!r} node fields must be "
1372
+ f"{sorted(expected_node)}"
1373
+ )
1374
+ label = entry.get("label")
1375
+ if not isinstance(label, str) or not _LABEL_REGEX.match(label):
1376
+ raise ManifoldFormatError(
1377
+ f"discover manifold {name!r} node label "
1378
+ f"{label!r} invalid; a node label is a grammar-"
1379
+ f"addressable identifier (no '.', reserved as the "
1380
+ f"bipolar separator) — must match "
1381
+ f"{_LABEL_REGEX.pattern}"
1382
+ )
1383
+ if "coords" in entry:
1384
+ raise ManifoldFormatError(
1385
+ f"discover manifold {name!r} node {label!r} "
1386
+ f"must not carry 'coords' — coords are derived "
1387
+ f"at fit time"
1388
+ )
1389
+ node_labels.append(label)
1390
+ node_roles.append(_validate_node_role(name, label, entry.get("role")))
1391
+ node_kinds.append(_validate_node_kind(name, label, entry.get("kind")))
1392
+
1393
+ if len(set(node_labels)) != len(node_labels):
1394
+ raise ManifoldFormatError(
1395
+ f"manifold {name!r} has duplicate node labels"
1396
+ )
1397
+
1398
+ files = data["files"]
1399
+ if not isinstance(files, dict):
1400
+ raise ManifoldFormatError(
1401
+ f"manifold {name!r} 'files' must be an object"
1402
+ )
1403
+ # Verify only a populated manifest — a freshly hand-authored
1404
+ # manifold has no hashes yet; `fit` back-fills them.
1405
+ if files and verify_manifest:
1406
+ ok, bad = verify_integrity(folder, files)
1407
+ if not ok:
1408
+ raise ManifoldFormatError(
1409
+ f"manifold integrity check failed in {folder}: "
1410
+ f"tampered/missing {bad}"
1411
+ )
1412
+
1413
+ raw_tags = data["tags"]
1414
+ if not isinstance(raw_tags, list) or not all(
1415
+ isinstance(t, str) for t in raw_tags
1416
+ ):
1417
+ raise ManifoldFormatError(
1418
+ f"manifold {name!r} 'tags' must be a list of strings"
1419
+ )
1420
+
1421
+ # Optional reference to a standalone template artifact — present only on
1422
+ # templated discover manifolds. The node corpora were derived from it at
1423
+ # authoring time and live in ``nodes/`` like any discover folder; this
1424
+ # ref lets the fit resolve the template's multi-turn contexts as the
1425
+ # elicitation prefixes. Only discover folders may carry it.
1426
+ if "template_ref" not in data:
1427
+ raise ManifoldFormatError(
1428
+ f"manifold {name!r} needs explicit 'template_ref'"
1429
+ )
1430
+ raw_template_ref = data["template_ref"]
1431
+ template_ref: str | None = None
1432
+ if raw_template_ref is not None:
1433
+ if not isinstance(raw_template_ref, str) or not raw_template_ref:
1434
+ raise ManifoldFormatError(
1435
+ f"manifold {name!r} 'template_ref' must be a non-empty string"
1436
+ )
1437
+ if fit_mode not in _FIT_MODES_DISCOVER:
1438
+ raise ManifoldFormatError(
1439
+ f"manifold {name!r} carries a 'template_ref' but fit_mode is "
1440
+ f"{fit_mode!r}; templated corpora are a discover-mode feature "
1441
+ f"({sorted(_FIT_MODES_DISCOVER)})"
1442
+ )
1443
+ template_ref = raw_template_ref
1444
+
1445
+ inst = cls(
1446
+ folder=folder,
1447
+ name=name,
1448
+ description=data["description"],
1449
+ domain=domain_spec,
1450
+ node_labels=node_labels,
1451
+ node_coords=node_coords,
1452
+ files=files,
1453
+ fit_mode=fit_mode,
1454
+ hyperparams=hyperparams,
1455
+ source=data["source"],
1456
+ node_roles=node_roles,
1457
+ node_kinds=node_kinds,
1458
+ tags=[str(t) for t in raw_tags],
1459
+ template_ref=template_ref,
1460
+ )
1461
+
1462
+ # Every node file must be present — except for a baked manifold,
1463
+ # which has no ``nodes/`` corpus at all (its geometry is the tensor).
1464
+ if fit_mode != "baked":
1465
+ for idx, _label in enumerate(node_labels):
1466
+ p = inst.node_path(idx)
1467
+ if not p.exists():
1468
+ raise ManifoldFormatError(
1469
+ f"manifold {name!r} missing node corpus file {p}"
1470
+ )
1471
+
1472
+ for t in sorted(folder.glob("*.safetensors")):
1473
+ sc_path = t.with_suffix(".json")
1474
+ # Every fitted tensor must carry its ``.json`` sidecar. ``fit``
1475
+ # always writes one via ``save_manifold``, so a missing
1476
+ # sidecar means a genuinely-corrupt folder, not a legitimate
1477
+ # shape — refuse rather than silently accept a sidecar-less
1478
+ # tensor.
1479
+ if not sc_path.exists():
1480
+ raise ManifoldFormatError(
1481
+ f"manifold {name!r} tensor {t.name} has no sidecar "
1482
+ f"{sc_path.name}"
1483
+ )
1484
+ # A fit from an older format is **stale, not corrupt**: the folder's
1485
+ # corpus and geometry are untouched, so the only thing that expired
1486
+ # is one per-model tensor. Leave its stem unregistered and the
1487
+ # manifold reads as "not fitted for that model" — a clean cache miss
1488
+ # the next fit overwrites. Raising here instead would take the
1489
+ # whole artifact out of the selector index on a format bump,
1490
+ # unfitting its labels, probes, and steering along with it. A
1491
+ # corpus-less baked manifold has nothing to re-fit from, so it keeps
1492
+ # the strict read (and the tensor-less check below then fires).
1493
+ if fit_mode != "baked" and not fitted_sidecar_is_current(sc_path):
1494
+ continue
1495
+ inst._sidecars[t.stem] = ManifoldSidecar.load(sc_path)
1496
+
1497
+ # A baked manifold's tensor is its entire reason to exist (no corpus
1498
+ # to re-fit from), so a tensor-less baked folder is corrupt, not a
1499
+ # legitimate pre-fit shape the way a fresh authored/discover folder is.
1500
+ if fit_mode == "baked" and not inst._sidecars:
1501
+ raise ManifoldFormatError(
1502
+ f"baked manifold {name!r} has no fitted tensor — a corpus-less "
1503
+ f"manifold carries its geometry only in the per-model tensor"
1504
+ )
1505
+ return inst
1506
+
1507
+ # -- node corpus -------------------------------------------------------
1508
+
1509
+ def node_path(self, index: int) -> Path:
1510
+ return self.folder / "nodes" / _node_filename(
1511
+ index, self.node_labels[index],
1512
+ )
1513
+
1514
+ def node_groups(self) -> list[tuple[str, list[str]]]:
1515
+ """Return ``[(label, statements), ...]`` in node order."""
1516
+ if self.fit_mode == "baked":
1517
+ raise ManifoldFormatError(
1518
+ f"baked manifold {self.name!r} has no node corpus to read"
1519
+ )
1520
+ groups: list[tuple[str, list[str]]] = []
1521
+ for idx, label in enumerate(self.node_labels):
1522
+ with open(self.node_path(idx)) as f:
1523
+ statements = json.load(f)
1524
+ if not isinstance(statements, list) or not statements:
1525
+ raise ManifoldFormatError(
1526
+ f"manifold {self.name!r} node {label!r} must be a "
1527
+ f"non-empty JSON list of statements"
1528
+ )
1529
+ if not all(isinstance(s, str) for s in statements):
1530
+ raise ManifoldFormatError(
1531
+ f"manifold {self.name!r} node {label!r} statements "
1532
+ f"must all be strings"
1533
+ )
1534
+ groups.append((label, list(statements)))
1535
+ return groups
1536
+
1537
+ def nodes_sha256(
1538
+ self, *, resolved_template_sha256: str | None = None,
1539
+ ) -> str:
1540
+ """Stable hash of the inputs that determine a fit's output.
1541
+
1542
+ The staleness key: a fitted tensor's sidecar records this, and a
1543
+ re-fit is needed when it no longer matches. For authored
1544
+ manifolds the hash folds in the corpus, the domain spec, and
1545
+ the authoring coordinates — any geometry edit (moving a node,
1546
+ flipping an axis to periodic) triggers a re-fit alongside
1547
+ corpus edits. For discover manifolds the corresponding inputs
1548
+ are the corpus plus the fit mode plus the hyperparameters
1549
+ (``max_dim``, ``var_threshold`` for PCA; ``max_dim``,
1550
+ ``k_nn``, ``bandwidth`` for spectral) — changing any of those
1551
+ invalidates a cached fit.
1552
+
1553
+ The current hash covers the complete canonical fit input shape.
1554
+ """
1555
+ h = hashlib.sha256()
1556
+ if self.fit_mode == "baked":
1557
+ # No corpus and no re-fit: the key is provenance-only (the
1558
+ # sidecar records it, and it always matches since baked never
1559
+ # re-fits). Hash the identity that distinguishes it.
1560
+ h.update(_canonical_json({
1561
+ "fit_mode": "baked",
1562
+ "node_labels": list(self.node_labels),
1563
+ }))
1564
+ return h.hexdigest()
1565
+ # Labels are part of the fitted artifact (steering-by-label, nearest
1566
+ # reads, roles/kinds alignment). Node filenames are index-based, so a
1567
+ # rename can leave every corpus byte unchanged unless labels themselves
1568
+ # participate in the staleness key.
1569
+ h.update(_canonical_json(list(self.node_labels)))
1570
+ for idx in range(len(self.node_labels)):
1571
+ h.update(self.node_path(idx).read_bytes())
1572
+ if self.fit_mode == "authored":
1573
+ h.update(_canonical_json(self.domain))
1574
+ h.update(_canonical_json(self.node_coords))
1575
+ else:
1576
+ h.update(_canonical_json({
1577
+ "fit_mode": self.fit_mode,
1578
+ "hyperparams": self.hyperparams,
1579
+ }))
1580
+ # Per-node roles are inputs that determine the fit's geometry
1581
+ # (each node's centroid is pooled under its role's chat-template
1582
+ # substitution), so a role edit must invalidate a cached fit.
1583
+ h.update(_canonical_json(self.node_roles))
1584
+ # Per-node kind selects the generation system template + elicitation
1585
+ # role label, so it shapes the corpus a re-fit would pool — a kind edit
1586
+ # must invalidate a cached fit.
1587
+ h.update(_canonical_json(self.node_kinds))
1588
+ # A templated manifold's elicitation prefixes are the referenced
1589
+ # template's multi-turn contexts (a fit input that the node corpus files
1590
+ # above don't capture — they're only the slotted assistant turns), so a
1591
+ # context/value edit must invalidate a cached fit. Fold the resolved
1592
+ # template's content hash; fall back to the ref string if it can't be
1593
+ # resolved (best-effort — a missing template fails loudly at fit time).
1594
+ # ``None`` (every non-templated manifold) hashes identically to a missing
1595
+ # field.
1596
+ if self.template_ref is not None:
1597
+ if resolved_template_sha256 is not None:
1598
+ h.update(resolved_template_sha256.encode())
1599
+ return h.hexdigest()
1600
+ from drowse.io.templates import (
1601
+ AmbiguousTemplateError,
1602
+ TemplateNotFoundError,
1603
+ resolve_template,
1604
+ )
1605
+ try:
1606
+ h.update(resolve_template(self.template_ref).sha256().encode())
1607
+ except (TemplateNotFoundError, AmbiguousTemplateError):
1608
+ # The template genuinely can't be resolved (missing / a
1609
+ # cross-namespace collision); the ref string is the best
1610
+ # available staleness key, and the fit fails loudly at fit
1611
+ # time. Any *other* error (a corrupt template, an IO fault)
1612
+ # is a real bug — let it propagate rather than silently
1613
+ # degrade the staleness key into a stale-but-passing hash.
1614
+ h.update(_canonical_json({"template_ref": self.template_ref}))
1615
+ return h.hexdigest()
1616
+
1617
+ # -- fitted tensors ----------------------------------------------------
1618
+
1619
+ def tensor_models(self) -> list[str]:
1620
+ """Tensor stems present on disk (``<safe_model>[_sae-<rel>]``)."""
1621
+ return sorted(self._sidecars)
1622
+
1623
+ def tensor_path(self, stem: str) -> Path:
1624
+ return self.folder / f"{stem}.safetensors"
1625
+
1626
+ def sidecar(self, stem: str) -> ManifoldSidecar:
1627
+ return self._sidecars[stem]
1628
+
1629
+ # -- manifest ----------------------------------------------------------
1630
+
1631
+ def write_metadata(self, *, files: Optional[dict[str, str]] = None) -> None:
1632
+ """Rewrite authoring metadata while preserving trusted fitted hashes.
1633
+
1634
+ Fitted writers must call :meth:`update_file_hashes` with their exact
1635
+ outputs. A metadata-only rewrite never scans and blesses unrelated
1636
+ top-level files. The locked latest manifest supplies ``files`` when an
1637
+ explicit trusted mapping is not provided.
1638
+ """
1639
+ with _locked_manifest(self.folder):
1640
+ manifest_path = self.folder / "manifold.json"
1641
+ try:
1642
+ with open(manifest_path) as handle:
1643
+ latest_payload = json.load(handle)
1644
+ except (OSError, json.JSONDecodeError) as exc:
1645
+ raise ManifoldFormatError(
1646
+ f"cannot update unreadable manifest at {manifest_path}: {exc}"
1647
+ ) from exc
1648
+ if not isinstance(latest_payload, dict):
1649
+ raise ManifoldFormatError(
1650
+ f"manifold manifest at {manifest_path} must be a JSON object"
1651
+ )
1652
+ validate_manifold_format_version(
1653
+ latest_payload.get("format_version"), location=str(manifest_path),
1654
+ )
1655
+ if files is None:
1656
+ latest = latest_payload.get("files", {})
1657
+ if not isinstance(latest, dict):
1658
+ raise ManifoldFormatError(
1659
+ "manifold files manifest is not an object"
1660
+ )
1661
+ files = dict(latest)
1662
+ self.files = files
1663
+ payload: dict[str, Any] = {
1664
+ "format_version": MANIFOLD_FORMAT_VERSION,
1665
+ "name": self.name,
1666
+ "description": self.description,
1667
+ "fit_mode": self.fit_mode,
1668
+ "files": files,
1669
+ "source": self.source or "local",
1670
+ "tags": list(self.tags),
1671
+ "template_ref": self.template_ref,
1672
+ }
1673
+ for key in ("artifact_id", "fit_epochs"):
1674
+ if key in latest_payload:
1675
+ payload[key] = latest_payload[key]
1676
+ if self.fit_mode == "authored":
1677
+ payload["domain"] = self.domain
1678
+ payload["nodes"] = [
1679
+ _node_payload_authored(label, coords, role, kind)
1680
+ for label, coords, role, kind in zip(
1681
+ self.node_labels, self.node_coords,
1682
+ self._roles_padded(), self._kinds_padded(),
1683
+ strict=True,
1684
+ )
1685
+ ]
1686
+ elif self.fit_mode == "baked":
1687
+ # Display domain + label-only nodes (no coords, no hyperparams).
1688
+ payload["domain"] = self.domain
1689
+ payload["nodes"] = [
1690
+ _node_payload_discover(label, role, kind)
1691
+ for label, role, kind in zip(
1692
+ self.node_labels, self._roles_padded(),
1693
+ self._kinds_padded(), strict=True,
1694
+ )
1695
+ ]
1696
+ else:
1697
+ payload["hyperparams"] = self.hyperparams
1698
+ payload["nodes"] = [
1699
+ _node_payload_discover(label, role, kind)
1700
+ for label, role, kind in zip(
1701
+ self.node_labels, self._roles_padded(),
1702
+ self._kinds_padded(), strict=True,
1703
+ )
1704
+ ]
1705
+ write_json_atomic(self.folder / "manifold.json", payload)
1706
+
1707
+ def update_file_hashes(self, *paths: Path) -> None:
1708
+ """Refresh only newly written fitted artifacts in the integrity manifest.
1709
+
1710
+ A fit replaces one tensor/sidecar pair. Re-reading every historical
1711
+ model and variant in the folder makes persistence scale with old
1712
+ artifacts rather than the work just completed; the existing manifest
1713
+ was already verified by :meth:`load`, so unchanged entries can be kept.
1714
+ Starting from an empty manifest still records only the paths named by
1715
+ the successful writer. Unrelated pre-existing pairs remain untrusted;
1716
+ implicitly hashing them here would launder an interrupted or tampered
1717
+ artifact merely because another model was fitted.
1718
+ """
1719
+ resolved_paths = [Path(path) for path in paths]
1720
+ for resolved in resolved_paths:
1721
+ if resolved.parent != self.folder or not resolved.is_file():
1722
+ raise ValueError(
1723
+ f"manifest update path must be a fitted file in {self.folder}: "
1724
+ f"{resolved}"
1725
+ )
1726
+ with _locked_manifest(self.folder):
1727
+ manifest_path = self.folder / "manifold.json"
1728
+ try:
1729
+ with open(manifest_path) as handle:
1730
+ payload = json.load(handle)
1731
+ except (OSError, json.JSONDecodeError) as exc:
1732
+ raise ManifoldFormatError(
1733
+ f"cannot update unreadable manifest at {manifest_path}: {exc}"
1734
+ ) from exc
1735
+ if not isinstance(payload, dict):
1736
+ raise ManifoldFormatError(
1737
+ f"manifold manifest at {manifest_path} must be a JSON object"
1738
+ )
1739
+ # Reject an unknown schema before interpreting even familiar fields:
1740
+ # a future writer may have changed their shape or semantics.
1741
+ validate_manifold_format_version(
1742
+ payload.get("format_version"), location=str(manifest_path),
1743
+ )
1744
+ latest = payload.get("files", {})
1745
+ if not isinstance(latest, dict):
1746
+ raise ManifoldFormatError("manifold files manifest is not an object")
1747
+ # A newly published pair is always written by the current tensor
1748
+ # writer. Preserve the exact current format in the same locked CAS.
1749
+ files = dict(latest)
1750
+ for resolved in resolved_paths:
1751
+ files[resolved.name] = hash_file(resolved)
1752
+ payload["format_version"] = MANIFOLD_FORMAT_VERSION
1753
+ payload["files"] = files
1754
+ write_json_atomic(manifest_path, payload)
1755
+ self.files = files
1756
+
1757
+ def _roles_padded(self) -> list[str | None]:
1758
+ """Return ``node_roles`` padded to ``len(node_labels)`` with ``None``s.
1759
+
1760
+ ``ManifoldFolder`` constructed via :meth:`load` always carries a
1761
+ full-length ``node_roles``, but in-memory mutations (e.g.
1762
+ :func:`update_manifold_folder` swapping the node list) might
1763
+ leave the roles list out of sync. Padding here is defensive.
1764
+ """
1765
+ if len(self.node_roles) == len(self.node_labels):
1766
+ return list(self.node_roles)
1767
+ return [None] * len(self.node_labels)
1768
+
1769
+ def _kinds_padded(self) -> list[str | None]:
1770
+ """Return ``node_kinds`` padded to ``len(node_labels)`` with ``None``s.
1771
+
1772
+ Defensive twin of :meth:`_roles_padded` — keeps the kind list aligned
1773
+ with the labels under in-memory node-list mutations.
1774
+ """
1775
+ if len(self.node_kinds) == len(self.node_labels):
1776
+ return list(self.node_kinds)
1777
+ return [None] * len(self.node_labels)
1778
+
1779
+
1780
+ def _node_payload_authored(
1781
+ label: str, coords: list[float], role: str | None, kind: str | None = None,
1782
+ ) -> dict[str, Any]:
1783
+ """Build one authored-mode node entry for ``manifold.json``.
1784
+
1785
+ ``role`` and ``kind`` are explicit, including ``None``.
1786
+ """
1787
+ return {
1788
+ "label": label, "coords": [float(c) for c in coords],
1789
+ "role": role, "kind": kind,
1790
+ }
1791
+
1792
+
1793
+ def _node_payload_discover(
1794
+ label: str, role: str | None, kind: str | None = None,
1795
+ ) -> dict[str, Any]:
1796
+ """Build one discover-mode node entry for ``manifold.json``."""
1797
+ return {"label": label, "role": role, "kind": kind}
1798
+
1799
+
1800
+ def _warn_authoring_quality(
1801
+ name: str, domain: Any, node_coords: list[list[float]],
1802
+ ) -> None:
1803
+ """Soft advisory warnings on node placement — never raises.
1804
+
1805
+ Poisedness (the embedded coordinates failing to affinely span the
1806
+ embedding) is a hard error at fit time; flagging it here gives the
1807
+ author the feedback before they run a fit. A near-flat non-periodic
1808
+ axis (fewer than three distinct values) cannot show curvature.
1809
+ """
1810
+ coords = torch.tensor(node_coords, dtype=torch.float32)
1811
+ embedded = domain.embed(coords)
1812
+ centered = embedded - embedded.mean(dim=0, keepdim=True)
1813
+ rank = int(torch.linalg.matrix_rank(centered))
1814
+ if rank != domain.embed_dim:
1815
+ warnings.warn(
1816
+ f"manifold {name!r}: node coordinates have affine rank {rank} "
1817
+ f"but the embedding is {domain.embed_dim}-dimensional — the fit "
1818
+ f"will fail as not poised; spread the nodes across every axis",
1819
+ UserWarning,
1820
+ stacklevel=3,
1821
+ )
1822
+ if isinstance(domain, BoxDomain):
1823
+ for i, ax in enumerate(domain.axes):
1824
+ if ax.periodic:
1825
+ continue
1826
+ distinct = {round(row[i], 9) for row in node_coords}
1827
+ if len(distinct) < 3:
1828
+ warnings.warn(
1829
+ f"manifold {name!r}: axis {ax.name!r} has only "
1830
+ f"{len(distinct)} distinct coordinate value(s) — "
1831
+ f"curvature along it cannot be fitted",
1832
+ UserWarning,
1833
+ stacklevel=3,
1834
+ )
1835
+
1836
+
1837
+ @dataclass(frozen=True)
1838
+ class DiscoverGenerationPlan:
1839
+ """Resume / extend plan for streaming discover-manifold generation.
1840
+
1841
+ Returned by :func:`drowse.io.manifold_authoring.plan_discover_generation`.
1842
+ ``index_of`` maps each label to its on-disk node index (the ``NN_``
1843
+ filename prefix); ``pending`` is the declared labels whose corpus file
1844
+ is not yet on disk (the ones a run still needs to generate, in node
1845
+ order); ``added`` is the labels appended to ``manifold.json`` this call
1846
+ (the add-nodes case);
1847
+ ``resumed`` is ``True`` when an existing ``manifold.json`` was found.
1848
+ """
1849
+
1850
+ folder: Path
1851
+ index_of: dict[str, int]
1852
+ pending: tuple[str, ...]
1853
+ added: tuple[str, ...]
1854
+ resumed: bool