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.
- drowse/__init__.py +312 -0
- drowse/__main__.py +5 -0
- drowse/cli/__init__.py +5 -0
- drowse/cli/alpha_grid.py +96 -0
- drowse/cli/config_file.py +340 -0
- drowse/cli/main.py +52 -0
- drowse/cli/parsers.py +1478 -0
- drowse/cli/runners/__init__.py +155 -0
- drowse/cli/runners/config.py +85 -0
- drowse/cli/runners/experiment.py +344 -0
- drowse/cli/runners/lens.py +661 -0
- drowse/cli/runners/manifold.py +1097 -0
- drowse/cli/runners/pack.py +413 -0
- drowse/cli/runners/sae.py +304 -0
- drowse/cli/runners/serve.py +220 -0
- drowse/cli/runners/shared.py +389 -0
- drowse/cli/runners/template.py +232 -0
- drowse/core/__init__.py +1 -0
- drowse/core/capture.py +1205 -0
- drowse/core/errors.py +260 -0
- drowse/core/events.py +156 -0
- drowse/core/extraction.py +3076 -0
- drowse/core/generation.py +1738 -0
- drowse/core/generation_finalizer.py +278 -0
- drowse/core/histogram.py +30 -0
- drowse/core/hooks.py +1885 -0
- drowse/core/instruments/__init__.py +49 -0
- drowse/core/instruments/geometry.py +551 -0
- drowse/core/instruments/lens.py +1341 -0
- drowse/core/instruments/protocol.py +211 -0
- drowse/core/instruments/sae.py +935 -0
- drowse/core/instruments/types.py +597 -0
- drowse/core/jlens.py +874 -0
- drowse/core/jlens_fit.py +1305 -0
- drowse/core/joint_logprobs.py +748 -0
- drowse/core/loom.py +1938 -0
- drowse/core/loom_diff.py +408 -0
- drowse/core/mahalanobis.py +579 -0
- drowse/core/manifold.py +4106 -0
- drowse/core/measurements.py +382 -0
- drowse/core/model.py +1724 -0
- drowse/core/monitor.py +2060 -0
- drowse/core/monitor_attach.py +682 -0
- drowse/core/naming.py +42 -0
- drowse/core/naturalness.py +187 -0
- drowse/core/profile.py +617 -0
- drowse/core/relp.py +313 -0
- drowse/core/results.py +467 -0
- drowse/core/role_templates.py +677 -0
- drowse/core/sae.py +891 -0
- drowse/core/sae_training.py +239 -0
- drowse/core/sampling.py +127 -0
- drowse/core/scene.py +810 -0
- drowse/core/scoring.py +270 -0
- drowse/core/session.py +11015 -0
- drowse/core/static_cache.py +316 -0
- drowse/core/steering.py +229 -0
- drowse/core/steering_composer.py +1135 -0
- drowse/core/steering_expr.py +1413 -0
- drowse/core/surface_topology.py +128 -0
- drowse/core/token_callback.py +52 -0
- drowse/core/token_payloads.py +244 -0
- drowse/core/topology.py +1469 -0
- drowse/core/transcript.py +582 -0
- drowse/core/tree_filter.py +353 -0
- drowse/core/triggers.py +250 -0
- drowse/data/baseline_prompts.json +50 -0
- drowse/data/manifolds/confident.uncertain/manifold.json +28 -0
- drowse/data/manifolds/confident.uncertain/nodes/00_confident.json +50 -0
- drowse/data/manifolds/confident.uncertain/nodes/01_uncertain.json +50 -0
- drowse/data/manifolds/creative.conventional/manifold.json +28 -0
- drowse/data/manifolds/creative.conventional/nodes/00_creative.json +50 -0
- drowse/data/manifolds/creative.conventional/nodes/01_conventional.json +50 -0
- drowse/data/manifolds/curious.disinterested/manifold.json +28 -0
- drowse/data/manifolds/curious.disinterested/nodes/00_curious.json +50 -0
- drowse/data/manifolds/curious.disinterested/nodes/01_disinterested.json +50 -0
- drowse/data/manifolds/direct.indirect/manifold.json +28 -0
- drowse/data/manifolds/direct.indirect/nodes/00_direct.json +50 -0
- drowse/data/manifolds/direct.indirect/nodes/01_indirect.json +50 -0
- drowse/data/manifolds/emotions/manifold.json +115 -0
- drowse/data/manifolds/emotions/nodes/00_excited.json +50 -0
- drowse/data/manifolds/emotions/nodes/01_happy.json +50 -0
- drowse/data/manifolds/emotions/nodes/02_calm.json +50 -0
- drowse/data/manifolds/emotions/nodes/03_weary.json +50 -0
- drowse/data/manifolds/emotions/nodes/04_sad.json +50 -0
- drowse/data/manifolds/emotions/nodes/05_distressed.json +50 -0
- drowse/data/manifolds/emotions/nodes/06_alert.json +50 -0
- drowse/data/manifolds/emotions/nodes/07_dominant.json +50 -0
- drowse/data/manifolds/emotions/nodes/08_submissive.json +50 -0
- drowse/data/manifolds/emotions/nodes/09_angry.json +50 -0
- drowse/data/manifolds/emotions/nodes/10_fearful.json +50 -0
- drowse/data/manifolds/emotions/nodes/11_surprised.json +50 -0
- drowse/data/manifolds/emotions/nodes/12_triumphant.json +50 -0
- drowse/data/manifolds/emotions/nodes/13_humiliated.json +50 -0
- drowse/data/manifolds/emotions/nodes/14_disgusted.json +50 -0
- drowse/data/manifolds/emotions/nodes/15_bored.json +50 -0
- drowse/data/manifolds/emotions/nodes/16_grateful.json +50 -0
- drowse/data/manifolds/emotions/nodes/17_relieved.json +50 -0
- drowse/data/manifolds/emotions/nodes/18_playful.json +50 -0
- drowse/data/manifolds/emotions/nodes/19_hopeful.json +50 -0
- drowse/data/manifolds/formal.casual/manifold.json +28 -0
- drowse/data/manifolds/formal.casual/nodes/00_formal.json +50 -0
- drowse/data/manifolds/formal.casual/nodes/01_casual.json +50 -0
- drowse/data/manifolds/honest.deceptive/manifold.json +28 -0
- drowse/data/manifolds/honest.deceptive/nodes/00_honest.json +50 -0
- drowse/data/manifolds/honest.deceptive/nodes/01_deceptive.json +50 -0
- drowse/data/manifolds/humorous.serious/manifold.json +28 -0
- drowse/data/manifolds/humorous.serious/nodes/00_humorous.json +50 -0
- drowse/data/manifolds/humorous.serious/nodes/01_serious.json +50 -0
- drowse/data/manifolds/individualist.collectivist/manifold.json +28 -0
- drowse/data/manifolds/individualist.collectivist/nodes/00_individualist.json +50 -0
- drowse/data/manifolds/individualist.collectivist/nodes/01_collectivist.json +50 -0
- drowse/data/manifolds/masculine.feminine/manifold.json +28 -0
- drowse/data/manifolds/masculine.feminine/nodes/00_masculine.json +50 -0
- drowse/data/manifolds/masculine.feminine/nodes/01_feminine.json +50 -0
- drowse/data/manifolds/months/manifold.json +120 -0
- drowse/data/manifolds/months/nodes/00_january.json +50 -0
- drowse/data/manifolds/months/nodes/01_february.json +50 -0
- drowse/data/manifolds/months/nodes/02_march.json +50 -0
- drowse/data/manifolds/months/nodes/03_april.json +50 -0
- drowse/data/manifolds/months/nodes/04_may.json +50 -0
- drowse/data/manifolds/months/nodes/05_june.json +50 -0
- drowse/data/manifolds/months/nodes/06_july.json +50 -0
- drowse/data/manifolds/months/nodes/07_august.json +50 -0
- drowse/data/manifolds/months/nodes/08_september.json +50 -0
- drowse/data/manifolds/months/nodes/09_october.json +50 -0
- drowse/data/manifolds/months/nodes/10_november.json +50 -0
- drowse/data/manifolds/months/nodes/11_december.json +50 -0
- drowse/data/manifolds/personas/manifold.json +550 -0
- drowse/data/manifolds/personas/nodes/00_assistant.json +50 -0
- drowse/data/manifolds/personas/nodes/01_analyst.json +50 -0
- drowse/data/manifolds/personas/nodes/02_impostor.json +50 -0
- drowse/data/manifolds/personas/nodes/03_teacher.json +50 -0
- drowse/data/manifolds/personas/nodes/04_doctor.json +50 -0
- drowse/data/manifolds/personas/nodes/05_librarian.json +50 -0
- drowse/data/manifolds/personas/nodes/06_farmer.json +50 -0
- drowse/data/manifolds/personas/nodes/07_soldier.json +50 -0
- drowse/data/manifolds/personas/nodes/08_lawyer.json +50 -0
- drowse/data/manifolds/personas/nodes/09_chef.json +50 -0
- drowse/data/manifolds/personas/nodes/100_assassin.json +50 -0
- drowse/data/manifolds/personas/nodes/101_thief.json +50 -0
- drowse/data/manifolds/personas/nodes/102_conman.json +50 -0
- drowse/data/manifolds/personas/nodes/103_hacker.json +50 -0
- drowse/data/manifolds/personas/nodes/104_cultist.json +50 -0
- drowse/data/manifolds/personas/nodes/105_tyrant.json +50 -0
- drowse/data/manifolds/personas/nodes/106_vandal.json +50 -0
- drowse/data/manifolds/personas/nodes/10_journalist.json +50 -0
- drowse/data/manifolds/personas/nodes/11_mechanic.json +50 -0
- drowse/data/manifolds/personas/nodes/12_sailor.json +50 -0
- drowse/data/manifolds/personas/nodes/13_pilot.json +50 -0
- drowse/data/manifolds/personas/nodes/14_athlete.json +50 -0
- drowse/data/manifolds/personas/nodes/15_engineer.json +50 -0
- drowse/data/manifolds/personas/nodes/16_scientist.json +50 -0
- drowse/data/manifolds/personas/nodes/17_banker.json +50 -0
- drowse/data/manifolds/personas/nodes/18_salesperson.json +50 -0
- drowse/data/manifolds/personas/nodes/19_nerd.json +50 -0
- drowse/data/manifolds/personas/nodes/20_jock.json +50 -0
- drowse/data/manifolds/personas/nodes/21_gamer.json +50 -0
- drowse/data/manifolds/personas/nodes/22_furry.json +50 -0
- drowse/data/manifolds/personas/nodes/23_weeaboo.json +50 -0
- drowse/data/manifolds/personas/nodes/24_influencer.json +50 -0
- drowse/data/manifolds/personas/nodes/25_stoner.json +50 -0
- drowse/data/manifolds/personas/nodes/26_femboy.json +50 -0
- drowse/data/manifolds/personas/nodes/27_ceo.json +50 -0
- drowse/data/manifolds/personas/nodes/28_emperor.json +50 -0
- drowse/data/manifolds/personas/nodes/29_president.json +50 -0
- drowse/data/manifolds/personas/nodes/30_diplomat.json +50 -0
- drowse/data/manifolds/personas/nodes/31_judge.json +50 -0
- drowse/data/manifolds/personas/nodes/32_bureaucrat.json +50 -0
- drowse/data/manifolds/personas/nodes/33_conservative.json +50 -0
- drowse/data/manifolds/personas/nodes/34_liberal.json +50 -0
- drowse/data/manifolds/personas/nodes/35_libertarian.json +50 -0
- drowse/data/manifolds/personas/nodes/36_communist.json +50 -0
- drowse/data/manifolds/personas/nodes/37_anarchist.json +50 -0
- drowse/data/manifolds/personas/nodes/38_reactionary.json +50 -0
- drowse/data/manifolds/personas/nodes/39_loner.json +50 -0
- drowse/data/manifolds/personas/nodes/40_friend.json +50 -0
- drowse/data/manifolds/personas/nodes/41_rival.json +50 -0
- drowse/data/manifolds/personas/nodes/42_stranger.json +50 -0
- drowse/data/manifolds/personas/nodes/43_lover.json +50 -0
- drowse/data/manifolds/personas/nodes/44_stalker.json +50 -0
- drowse/data/manifolds/personas/nodes/45_lunatic.json +50 -0
- drowse/data/manifolds/personas/nodes/46_paranoid.json +50 -0
- drowse/data/manifolds/personas/nodes/47_detective.json +50 -0
- drowse/data/manifolds/personas/nodes/48_therapist.json +50 -0
- drowse/data/manifolds/personas/nodes/49_nomad.json +50 -0
- drowse/data/manifolds/personas/nodes/50_pirate.json +50 -0
- drowse/data/manifolds/personas/nodes/51_child.json +50 -0
- drowse/data/manifolds/personas/nodes/52_caveman.json +50 -0
- drowse/data/manifolds/personas/nodes/53_monk.json +50 -0
- drowse/data/manifolds/personas/nodes/54_knight.json +50 -0
- drowse/data/manifolds/personas/nodes/55_alchemist.json +50 -0
- drowse/data/manifolds/personas/nodes/56_blacksmith.json +50 -0
- drowse/data/manifolds/personas/nodes/57_viking.json +50 -0
- drowse/data/manifolds/personas/nodes/58_druid.json +50 -0
- drowse/data/manifolds/personas/nodes/59_poet.json +50 -0
- drowse/data/manifolds/personas/nodes/60_novelist.json +50 -0
- drowse/data/manifolds/personas/nodes/61_jester.json +50 -0
- drowse/data/manifolds/personas/nodes/62_philosopher.json +50 -0
- drowse/data/manifolds/personas/nodes/63_painter.json +50 -0
- drowse/data/manifolds/personas/nodes/64_musician.json +50 -0
- drowse/data/manifolds/personas/nodes/65_oracle.json +50 -0
- drowse/data/manifolds/personas/nodes/66_god.json +50 -0
- drowse/data/manifolds/personas/nodes/67_angel.json +50 -0
- drowse/data/manifolds/personas/nodes/68_prophet.json +50 -0
- drowse/data/manifolds/personas/nodes/69_saint.json +50 -0
- drowse/data/manifolds/personas/nodes/70_ghost.json +50 -0
- drowse/data/manifolds/personas/nodes/71_phoenix.json +50 -0
- drowse/data/manifolds/personas/nodes/72_witch.json +50 -0
- drowse/data/manifolds/personas/nodes/73_vampire.json +50 -0
- drowse/data/manifolds/personas/nodes/74_mermaid.json +50 -0
- drowse/data/manifolds/personas/nodes/75_sphinx.json +50 -0
- drowse/data/manifolds/personas/nodes/76_banshee.json +50 -0
- drowse/data/manifolds/personas/nodes/77_golem.json +50 -0
- drowse/data/manifolds/personas/nodes/78_deer.json +50 -0
- drowse/data/manifolds/personas/nodes/79_wolf.json +50 -0
- drowse/data/manifolds/personas/nodes/80_raven.json +50 -0
- drowse/data/manifolds/personas/nodes/81_serpent.json +50 -0
- drowse/data/manifolds/personas/nodes/82_lion.json +50 -0
- drowse/data/manifolds/personas/nodes/83_tree.json +50 -0
- drowse/data/manifolds/personas/nodes/84_river.json +50 -0
- drowse/data/manifolds/personas/nodes/85_storm.json +50 -0
- drowse/data/manifolds/personas/nodes/86_flame.json +50 -0
- drowse/data/manifolds/personas/nodes/87_mirror.json +50 -0
- drowse/data/manifolds/personas/nodes/88_clock.json +50 -0
- drowse/data/manifolds/personas/nodes/89_computer.json +50 -0
- drowse/data/manifolds/personas/nodes/90_robot.json +50 -0
- drowse/data/manifolds/personas/nodes/91_cyborg.json +50 -0
- drowse/data/manifolds/personas/nodes/92_drone.json +50 -0
- drowse/data/manifolds/personas/nodes/93_mecha.json +50 -0
- drowse/data/manifolds/personas/nodes/94_alien.json +50 -0
- drowse/data/manifolds/personas/nodes/95_trickster.json +50 -0
- drowse/data/manifolds/personas/nodes/96_virus.json +50 -0
- drowse/data/manifolds/personas/nodes/97_spy.json +50 -0
- drowse/data/manifolds/personas/nodes/98_narcissist.json +50 -0
- drowse/data/manifolds/personas/nodes/99_demon.json +50 -0
- drowse/data/manifolds/refusing.compliant/manifold.json +28 -0
- drowse/data/manifolds/refusing.compliant/nodes/00_refusing.json +50 -0
- drowse/data/manifolds/refusing.compliant/nodes/01_compliant.json +50 -0
- drowse/data/manifolds/religious.secular/manifold.json +28 -0
- drowse/data/manifolds/religious.secular/nodes/00_religious.json +50 -0
- drowse/data/manifolds/religious.secular/nodes/01_secular.json +50 -0
- drowse/data/manifolds/sincere.manipulative/manifold.json +28 -0
- drowse/data/manifolds/sincere.manipulative/nodes/00_sincere.json +50 -0
- drowse/data/manifolds/sincere.manipulative/nodes/01_manipulative.json +50 -0
- drowse/data/manifolds/sycophantic.blunt/manifold.json +28 -0
- drowse/data/manifolds/sycophantic.blunt/nodes/00_sycophantic.json +50 -0
- drowse/data/manifolds/sycophantic.blunt/nodes/01_blunt.json +50 -0
- drowse/data/manifolds/technical.accessible/manifold.json +28 -0
- drowse/data/manifolds/technical.accessible/nodes/00_technical.json +50 -0
- drowse/data/manifolds/technical.accessible/nodes/01_accessible.json +50 -0
- drowse/data/manifolds/traditional.progressive/manifold.json +28 -0
- drowse/data/manifolds/traditional.progressive/nodes/00_traditional.json +50 -0
- drowse/data/manifolds/traditional.progressive/nodes/01_progressive.json +50 -0
- drowse/data/manifolds/verbose.concise/manifold.json +28 -0
- drowse/data/manifolds/verbose.concise/nodes/00_verbose.json +50 -0
- drowse/data/manifolds/verbose.concise/nodes/01_concise.json +50 -0
- drowse/data/manifolds/warm.clinical/manifold.json +28 -0
- drowse/data/manifolds/warm.clinical/nodes/00_warm.json +50 -0
- drowse/data/manifolds/warm.clinical/nodes/01_clinical.json +50 -0
- drowse/data/neutral_statements.json +98 -0
- drowse/io/__init__.py +1 -0
- drowse/io/alignment.py +1334 -0
- drowse/io/atomic.py +278 -0
- drowse/io/bake.py +558 -0
- drowse/io/bootstrap.py +73 -0
- drowse/io/brand_migration.py +48 -0
- drowse/io/drowse_archive.py +1830 -0
- drowse/io/gguf_io.py +300 -0
- drowse/io/hf.py +81 -0
- drowse/io/hf_manifolds.py +931 -0
- drowse/io/integrity.py +102 -0
- drowse/io/lens.py +1992 -0
- drowse/io/lens_sources.py +1003 -0
- drowse/io/manifold_authoring.py +1348 -0
- drowse/io/manifold_folder.py +1854 -0
- drowse/io/manifold_lifecycle.py +1253 -0
- drowse/io/manifold_tensors.py +727 -0
- drowse/io/manifolds.py +443 -0
- drowse/io/paths.py +289 -0
- drowse/io/probes_bootstrap.py +58 -0
- drowse/io/sae.py +382 -0
- drowse/io/sae_artifacts.py +318 -0
- drowse/io/selectors.py +503 -0
- drowse/io/shards.py +160 -0
- drowse/io/source_registry.py +138 -0
- drowse/io/staging.py +80 -0
- drowse/io/templates.py +707 -0
- drowse/notebook/__init__.py +38 -0
- drowse/notebook/data.py +99 -0
- drowse/notebook/plots.py +352 -0
- drowse/py.typed +0 -0
- drowse/server/__init__.py +20 -0
- drowse/server/app.py +987 -0
- drowse/server/background_job.py +246 -0
- drowse/server/instrument_routes.py +1101 -0
- drowse/server/manifold_routes.py +1028 -0
- drowse/server/model_names.py +82 -0
- drowse/server/native_common.py +92 -0
- drowse/server/native_routes.py +45 -0
- drowse/server/ollama.py +817 -0
- drowse/server/operation_ledger.py +187 -0
- drowse/server/probe_routes.py +220 -0
- drowse/server/profile_models.py +52 -0
- drowse/server/profile_routes.py +440 -0
- drowse/server/request_helpers.py +198 -0
- drowse/server/request_ledger.py +85 -0
- drowse/server/response_models.py +814 -0
- drowse/server/session_models.py +146 -0
- drowse/server/session_routes.py +150 -0
- drowse/server/sse.py +333 -0
- drowse/server/streaming.py +129 -0
- drowse/server/template_routes.py +161 -0
- drowse/server/tree_models.py +121 -0
- drowse/server/tree_routes.py +550 -0
- drowse/server/ws_events.py +65 -0
- drowse/server/ws_models.py +219 -0
- drowse/server/ws_stream.py +1205 -0
- drowse/web/__init__.py +22 -0
- drowse/web/dist/LICENSE-Fluent-Icons.txt +21 -0
- drowse/web/dist/LICENSE-Fluent-System-Icons.txt +21 -0
- drowse/web/dist/LICENSE-Martian-Mono.txt +93 -0
- drowse/web/dist/LICENSE-Torph.txt +21 -0
- drowse/web/dist/LICENSE-Wix-Madefor.txt +93 -0
- drowse/web/dist/assets/App-BewwoFwv.js +33 -0
- drowse/web/dist/assets/App.css +1 -0
- drowse/web/dist/assets/MartianMono-cyrillic-ext.woff2 +0 -0
- drowse/web/dist/assets/MartianMono-cyrillic.woff2 +0 -0
- drowse/web/dist/assets/MartianMono-latin-ext.woff2 +0 -0
- drowse/web/dist/assets/MartianMono-latin.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforDisplay-cyrillic.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforDisplay-latin-ext.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforDisplay-latin.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforDisplay-vietnamese.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-italic-latin-ext.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-italic-latin.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-normal-cyrillic.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-normal-latin-ext.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-normal-latin.woff2 +0 -0
- drowse/web/dist/assets/WixMadeforText-normal-vietnamese.woff2 +0 -0
- drowse/web/dist/assets/ab.svelte-CbVGAYXo.js +1 -0
- drowse/web/dist/assets/artifactUpdates-Cjeu6LJc.js +1 -0
- drowse/web/dist/assets/artifactUpdates-DfAoYC9j.js +1 -0
- drowse/web/dist/assets/chat.svelte-DxKufn9g.js +1 -0
- drowse/web/dist/assets/drawers-BDWC0hIN.js +123 -0
- drowse/web/dist/assets/drawers.css +1 -0
- drowse/web/dist/assets/drowse.js +7 -0
- drowse/web/dist/assets/eleutherai.png +0 -0
- drowse/web/dist/assets/errors-Dvb10QW_.js +2 -0
- drowse/web/dist/assets/expression-4yISdAdq.js +1 -0
- drowse/web/dist/assets/files-CvRivkey.js +1 -0
- drowse/web/dist/assets/files-JhDJVgD_.js +1 -0
- drowse/web/dist/assets/fittingCancellation-D72BZWPZ.js +1 -0
- drowse/web/dist/assets/gemma-3-1b-it.json +1 -0
- drowse/web/dist/assets/gemma-3-270m-it.json +1 -0
- drowse/web/dist/assets/gemma-3-4b-it.json +1 -0
- drowse/web/dist/assets/generation-DbkBAhP_.js +1 -0
- drowse/web/dist/assets/index-client-2OdH6IQp.js +3 -0
- drowse/web/dist/assets/index-client-D4S4PooY.js +1 -0
- drowse/web/dist/assets/index.css +1 -0
- drowse/web/dist/assets/instrumentAuthoring.svelte-Csh7NOGX.js +1 -0
- drowse/web/dist/assets/instrumentAuthoring.svelte-w9YPcDsP.js +1 -0
- drowse/web/dist/assets/instruments.svelte-XnEUQA6B.js +1 -0
- drowse/web/dist/assets/localData-DIBDM_zo.js +1 -0
- drowse/web/dist/assets/loom.svelte-D_YyqHHK.js +1 -0
- drowse/web/dist/assets/manifoldJobs.svelte-9f5hs_J7.js +1 -0
- drowse/web/dist/assets/manifoldJobs.svelte-Bwo2GYOH.js +1 -0
- drowse/web/dist/assets/openai.png +0 -0
- drowse/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
- drowse/web/dist/assets/probes.svelte-COcN1Rh8.js +1 -0
- drowse/web/dist/assets/registry-B0_pJoMg.js +1 -0
- drowse/web/dist/assets/registry-CQv71XNh.js +1 -0
- drowse/web/dist/assets/sampling.svelte-BU0kED3k.js +1 -0
- drowse/web/dist/assets/samplingCapabilities-DM6eoDoc.js +1 -0
- drowse/web/dist/assets/savedConversations.svelte-BsBLc-p7.js +1 -0
- drowse/web/dist/assets/savedConversations.svelte-TuSes9Kg.js +1 -0
- drowse/web/dist/assets/selectors-BxXnPPN_.js +1 -0
- drowse/web/dist/assets/serviceTools-CG0q1Xqg.js +2 -0
- drowse/web/dist/assets/session.svelte-D2STLuP6.js +1 -0
- drowse/web/dist/assets/steering.svelte-DvFT3O-d.js +1 -0
- drowse/web/dist/assets/steeringPositions-Dr0JOFu7.js +1 -0
- drowse/web/dist/assets/templates-Bkq2x5ID.js +1 -0
- drowse/web/dist/assets/types-CuMUbvHW.js +1 -0
- drowse/web/dist/assets/validation-5wgTuS7k.js +1 -0
- drowse/web/dist/assets/webmcp-DyqJf4z6.js +2 -0
- drowse/web/dist/assets/workspaceController-C2L33e3m.js +1 -0
- drowse/web/dist/assets/workspaceController-DJhXD_BC.js +1 -0
- drowse/web/dist/assets/workspaceTools-BWdmXopx.js +2 -0
- drowse/web/dist/favicon.ico +0 -0
- drowse/web/dist/favicon.svg +1 -0
- drowse/web/dist/icons/apple-touch-icon.png +0 -0
- drowse/web/dist/icons/drowse-192.png +0 -0
- drowse/web/dist/icons/drowse-512.png +0 -0
- drowse/web/dist/icons/drowse-maskable-512.png +0 -0
- drowse/web/dist/icons/tab-analyzing-0.png +0 -0
- drowse/web/dist/icons/tab-analyzing-1.png +0 -0
- drowse/web/dist/icons/tab-analyzing-2.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-0.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-1.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-10.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-11.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-12.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-13.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-14.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-15.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-16.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-17.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-18.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-19.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-2.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-20.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-21.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-22.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-23.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-24.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-25.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-26.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-27.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-28.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-29.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-3.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-30.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-31.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-32.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-33.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-34.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-35.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-36.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-37.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-38.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-39.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-4.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-40.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-41.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-42.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-43.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-44.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-45.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-46.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-47.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-5.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-6.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-7.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-8.png +0 -0
- drowse/web/dist/icons/tab-analyzing-dark-9.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-0.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-1.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-10.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-11.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-12.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-13.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-14.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-15.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-16.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-17.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-18.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-19.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-2.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-20.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-21.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-22.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-23.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-24.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-25.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-26.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-27.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-28.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-29.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-3.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-30.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-31.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-32.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-33.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-34.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-35.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-36.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-37.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-38.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-39.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-4.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-40.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-41.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-42.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-43.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-44.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-45.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-46.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-47.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-5.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-6.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-7.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-8.png +0 -0
- drowse/web/dist/icons/tab-analyzing-light-9.png +0 -0
- drowse/web/dist/icons/tab-chat-settings-dark.png +0 -0
- drowse/web/dist/icons/tab-chat-settings-light.png +0 -0
- drowse/web/dist/icons/tab-chat-settings.png +0 -0
- drowse/web/dist/icons/tab-chats-dark.png +0 -0
- drowse/web/dist/icons/tab-chats-light.png +0 -0
- drowse/web/dist/icons/tab-chats.png +0 -0
- drowse/web/dist/icons/tab-checking-0.png +0 -0
- drowse/web/dist/icons/tab-checking-1.png +0 -0
- drowse/web/dist/icons/tab-checking-2.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-0.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-1.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-10.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-11.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-12.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-13.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-14.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-15.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-16.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-17.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-18.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-19.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-2.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-20.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-21.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-22.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-23.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-24.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-25.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-26.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-27.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-28.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-29.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-3.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-30.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-31.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-32.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-33.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-34.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-35.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-36.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-37.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-38.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-39.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-4.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-40.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-41.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-42.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-43.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-44.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-45.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-46.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-47.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-5.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-6.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-7.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-8.png +0 -0
- drowse/web/dist/icons/tab-checking-dark-9.png +0 -0
- drowse/web/dist/icons/tab-checking-light-0.png +0 -0
- drowse/web/dist/icons/tab-checking-light-1.png +0 -0
- drowse/web/dist/icons/tab-checking-light-10.png +0 -0
- drowse/web/dist/icons/tab-checking-light-11.png +0 -0
- drowse/web/dist/icons/tab-checking-light-12.png +0 -0
- drowse/web/dist/icons/tab-checking-light-13.png +0 -0
- drowse/web/dist/icons/tab-checking-light-14.png +0 -0
- drowse/web/dist/icons/tab-checking-light-15.png +0 -0
- drowse/web/dist/icons/tab-checking-light-16.png +0 -0
- drowse/web/dist/icons/tab-checking-light-17.png +0 -0
- drowse/web/dist/icons/tab-checking-light-18.png +0 -0
- drowse/web/dist/icons/tab-checking-light-19.png +0 -0
- drowse/web/dist/icons/tab-checking-light-2.png +0 -0
- drowse/web/dist/icons/tab-checking-light-20.png +0 -0
- drowse/web/dist/icons/tab-checking-light-21.png +0 -0
- drowse/web/dist/icons/tab-checking-light-22.png +0 -0
- drowse/web/dist/icons/tab-checking-light-23.png +0 -0
- drowse/web/dist/icons/tab-checking-light-24.png +0 -0
- drowse/web/dist/icons/tab-checking-light-25.png +0 -0
- drowse/web/dist/icons/tab-checking-light-26.png +0 -0
- drowse/web/dist/icons/tab-checking-light-27.png +0 -0
- drowse/web/dist/icons/tab-checking-light-28.png +0 -0
- drowse/web/dist/icons/tab-checking-light-29.png +0 -0
- drowse/web/dist/icons/tab-checking-light-3.png +0 -0
- drowse/web/dist/icons/tab-checking-light-30.png +0 -0
- drowse/web/dist/icons/tab-checking-light-31.png +0 -0
- drowse/web/dist/icons/tab-checking-light-32.png +0 -0
- drowse/web/dist/icons/tab-checking-light-33.png +0 -0
- drowse/web/dist/icons/tab-checking-light-34.png +0 -0
- drowse/web/dist/icons/tab-checking-light-35.png +0 -0
- drowse/web/dist/icons/tab-checking-light-36.png +0 -0
- drowse/web/dist/icons/tab-checking-light-37.png +0 -0
- drowse/web/dist/icons/tab-checking-light-38.png +0 -0
- drowse/web/dist/icons/tab-checking-light-39.png +0 -0
- drowse/web/dist/icons/tab-checking-light-4.png +0 -0
- drowse/web/dist/icons/tab-checking-light-40.png +0 -0
- drowse/web/dist/icons/tab-checking-light-41.png +0 -0
- drowse/web/dist/icons/tab-checking-light-42.png +0 -0
- drowse/web/dist/icons/tab-checking-light-43.png +0 -0
- drowse/web/dist/icons/tab-checking-light-44.png +0 -0
- drowse/web/dist/icons/tab-checking-light-45.png +0 -0
- drowse/web/dist/icons/tab-checking-light-46.png +0 -0
- drowse/web/dist/icons/tab-checking-light-47.png +0 -0
- drowse/web/dist/icons/tab-checking-light-5.png +0 -0
- drowse/web/dist/icons/tab-checking-light-6.png +0 -0
- drowse/web/dist/icons/tab-checking-light-7.png +0 -0
- drowse/web/dist/icons/tab-checking-light-8.png +0 -0
- drowse/web/dist/icons/tab-checking-light-9.png +0 -0
- drowse/web/dist/icons/tab-comparison-dark.png +0 -0
- drowse/web/dist/icons/tab-comparison-light.png +0 -0
- drowse/web/dist/icons/tab-comparison.png +0 -0
- drowse/web/dist/icons/tab-controls-dark.png +0 -0
- drowse/web/dist/icons/tab-controls-light.png +0 -0
- drowse/web/dist/icons/tab-controls.png +0 -0
- drowse/web/dist/icons/tab-conversation-dark.png +0 -0
- drowse/web/dist/icons/tab-conversation-light.png +0 -0
- drowse/web/dist/icons/tab-conversation.png +0 -0
- drowse/web/dist/icons/tab-credits-dark.png +0 -0
- drowse/web/dist/icons/tab-credits-light.png +0 -0
- drowse/web/dist/icons/tab-credits.png +0 -0
- drowse/web/dist/icons/tab-downloading-0.png +0 -0
- drowse/web/dist/icons/tab-downloading-1.png +0 -0
- drowse/web/dist/icons/tab-downloading-2.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-0.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-1.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-10.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-11.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-12.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-13.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-14.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-15.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-16.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-17.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-18.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-19.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-2.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-20.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-21.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-22.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-23.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-24.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-25.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-26.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-27.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-28.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-29.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-3.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-30.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-31.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-32.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-33.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-34.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-35.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-36.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-37.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-38.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-39.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-4.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-40.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-41.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-42.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-43.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-44.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-45.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-46.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-47.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-5.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-6.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-7.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-8.png +0 -0
- drowse/web/dist/icons/tab-downloading-dark-9.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-0.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-1.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-10.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-11.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-12.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-13.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-14.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-15.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-16.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-17.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-18.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-19.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-2.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-20.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-21.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-22.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-23.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-24.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-25.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-26.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-27.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-28.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-29.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-3.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-30.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-31.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-32.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-33.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-34.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-35.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-36.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-37.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-38.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-39.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-4.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-40.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-41.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-42.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-43.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-44.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-45.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-46.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-47.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-5.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-6.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-7.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-8.png +0 -0
- drowse/web/dist/icons/tab-downloading-light-9.png +0 -0
- drowse/web/dist/icons/tab-error-dark.png +0 -0
- drowse/web/dist/icons/tab-error-light.png +0 -0
- drowse/web/dist/icons/tab-error.png +0 -0
- drowse/web/dist/icons/tab-home-dark.png +0 -0
- drowse/web/dist/icons/tab-home-light.png +0 -0
- drowse/web/dist/icons/tab-home.png +0 -0
- drowse/web/dist/icons/tab-loading-0.png +0 -0
- drowse/web/dist/icons/tab-loading-1.png +0 -0
- drowse/web/dist/icons/tab-loading-2.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-0.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-1.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-10.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-11.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-12.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-13.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-14.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-15.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-16.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-17.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-18.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-19.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-2.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-20.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-21.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-22.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-23.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-24.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-25.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-26.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-27.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-28.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-29.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-3.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-30.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-31.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-32.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-33.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-34.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-35.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-36.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-37.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-38.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-39.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-4.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-40.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-41.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-42.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-43.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-44.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-45.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-46.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-47.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-5.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-6.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-7.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-8.png +0 -0
- drowse/web/dist/icons/tab-loading-dark-9.png +0 -0
- drowse/web/dist/icons/tab-loading-light-0.png +0 -0
- drowse/web/dist/icons/tab-loading-light-1.png +0 -0
- drowse/web/dist/icons/tab-loading-light-10.png +0 -0
- drowse/web/dist/icons/tab-loading-light-11.png +0 -0
- drowse/web/dist/icons/tab-loading-light-12.png +0 -0
- drowse/web/dist/icons/tab-loading-light-13.png +0 -0
- drowse/web/dist/icons/tab-loading-light-14.png +0 -0
- drowse/web/dist/icons/tab-loading-light-15.png +0 -0
- drowse/web/dist/icons/tab-loading-light-16.png +0 -0
- drowse/web/dist/icons/tab-loading-light-17.png +0 -0
- drowse/web/dist/icons/tab-loading-light-18.png +0 -0
- drowse/web/dist/icons/tab-loading-light-19.png +0 -0
- drowse/web/dist/icons/tab-loading-light-2.png +0 -0
- drowse/web/dist/icons/tab-loading-light-20.png +0 -0
- drowse/web/dist/icons/tab-loading-light-21.png +0 -0
- drowse/web/dist/icons/tab-loading-light-22.png +0 -0
- drowse/web/dist/icons/tab-loading-light-23.png +0 -0
- drowse/web/dist/icons/tab-loading-light-24.png +0 -0
- drowse/web/dist/icons/tab-loading-light-25.png +0 -0
- drowse/web/dist/icons/tab-loading-light-26.png +0 -0
- drowse/web/dist/icons/tab-loading-light-27.png +0 -0
- drowse/web/dist/icons/tab-loading-light-28.png +0 -0
- drowse/web/dist/icons/tab-loading-light-29.png +0 -0
- drowse/web/dist/icons/tab-loading-light-3.png +0 -0
- drowse/web/dist/icons/tab-loading-light-30.png +0 -0
- drowse/web/dist/icons/tab-loading-light-31.png +0 -0
- drowse/web/dist/icons/tab-loading-light-32.png +0 -0
- drowse/web/dist/icons/tab-loading-light-33.png +0 -0
- drowse/web/dist/icons/tab-loading-light-34.png +0 -0
- drowse/web/dist/icons/tab-loading-light-35.png +0 -0
- drowse/web/dist/icons/tab-loading-light-36.png +0 -0
- drowse/web/dist/icons/tab-loading-light-37.png +0 -0
- drowse/web/dist/icons/tab-loading-light-38.png +0 -0
- drowse/web/dist/icons/tab-loading-light-39.png +0 -0
- drowse/web/dist/icons/tab-loading-light-4.png +0 -0
- drowse/web/dist/icons/tab-loading-light-40.png +0 -0
- drowse/web/dist/icons/tab-loading-light-41.png +0 -0
- drowse/web/dist/icons/tab-loading-light-42.png +0 -0
- drowse/web/dist/icons/tab-loading-light-43.png +0 -0
- drowse/web/dist/icons/tab-loading-light-44.png +0 -0
- drowse/web/dist/icons/tab-loading-light-45.png +0 -0
- drowse/web/dist/icons/tab-loading-light-46.png +0 -0
- drowse/web/dist/icons/tab-loading-light-47.png +0 -0
- drowse/web/dist/icons/tab-loading-light-5.png +0 -0
- drowse/web/dist/icons/tab-loading-light-6.png +0 -0
- drowse/web/dist/icons/tab-loading-light-7.png +0 -0
- drowse/web/dist/icons/tab-loading-light-8.png +0 -0
- drowse/web/dist/icons/tab-loading-light-9.png +0 -0
- drowse/web/dist/icons/tab-loom-dark.png +0 -0
- drowse/web/dist/icons/tab-loom-light.png +0 -0
- drowse/web/dist/icons/tab-loom-working-0.png +0 -0
- drowse/web/dist/icons/tab-loom-working-1.png +0 -0
- drowse/web/dist/icons/tab-loom-working-2.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-0.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-1.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-10.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-11.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-12.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-13.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-14.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-15.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-16.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-17.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-18.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-19.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-2.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-20.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-21.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-22.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-23.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-24.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-25.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-26.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-27.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-28.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-29.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-3.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-30.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-31.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-32.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-33.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-34.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-35.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-36.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-37.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-38.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-39.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-4.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-40.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-41.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-42.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-43.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-44.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-45.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-46.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-47.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-5.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-6.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-7.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-8.png +0 -0
- drowse/web/dist/icons/tab-loom-working-dark-9.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-0.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-1.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-10.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-11.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-12.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-13.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-14.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-15.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-16.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-17.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-18.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-19.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-2.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-20.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-21.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-22.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-23.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-24.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-25.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-26.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-27.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-28.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-29.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-3.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-30.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-31.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-32.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-33.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-34.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-35.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-36.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-37.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-38.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-39.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-4.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-40.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-41.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-42.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-43.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-44.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-45.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-46.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-47.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-5.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-6.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-7.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-8.png +0 -0
- drowse/web/dist/icons/tab-loom-working-light-9.png +0 -0
- drowse/web/dist/icons/tab-loom.png +0 -0
- drowse/web/dist/icons/tab-models-dark.png +0 -0
- drowse/web/dist/icons/tab-models-light.png +0 -0
- drowse/web/dist/icons/tab-models.png +0 -0
- drowse/web/dist/icons/tab-offline-dark.png +0 -0
- drowse/web/dist/icons/tab-offline-light.png +0 -0
- drowse/web/dist/icons/tab-offline.png +0 -0
- drowse/web/dist/icons/tab-paused-dark.png +0 -0
- drowse/web/dist/icons/tab-paused-light.png +0 -0
- drowse/web/dist/icons/tab-paused.png +0 -0
- drowse/web/dist/icons/tab-ready-dark.png +0 -0
- drowse/web/dist/icons/tab-ready-light.png +0 -0
- drowse/web/dist/icons/tab-ready.png +0 -0
- drowse/web/dist/icons/tab-saving-0.png +0 -0
- drowse/web/dist/icons/tab-saving-1.png +0 -0
- drowse/web/dist/icons/tab-saving-2.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-0.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-1.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-10.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-11.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-12.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-13.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-14.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-15.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-16.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-17.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-18.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-19.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-2.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-20.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-21.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-22.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-23.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-24.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-25.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-26.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-27.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-28.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-29.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-3.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-30.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-31.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-32.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-33.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-34.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-35.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-36.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-37.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-38.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-39.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-4.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-40.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-41.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-42.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-43.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-44.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-45.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-46.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-47.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-5.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-6.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-7.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-8.png +0 -0
- drowse/web/dist/icons/tab-saving-dark-9.png +0 -0
- drowse/web/dist/icons/tab-saving-light-0.png +0 -0
- drowse/web/dist/icons/tab-saving-light-1.png +0 -0
- drowse/web/dist/icons/tab-saving-light-10.png +0 -0
- drowse/web/dist/icons/tab-saving-light-11.png +0 -0
- drowse/web/dist/icons/tab-saving-light-12.png +0 -0
- drowse/web/dist/icons/tab-saving-light-13.png +0 -0
- drowse/web/dist/icons/tab-saving-light-14.png +0 -0
- drowse/web/dist/icons/tab-saving-light-15.png +0 -0
- drowse/web/dist/icons/tab-saving-light-16.png +0 -0
- drowse/web/dist/icons/tab-saving-light-17.png +0 -0
- drowse/web/dist/icons/tab-saving-light-18.png +0 -0
- drowse/web/dist/icons/tab-saving-light-19.png +0 -0
- drowse/web/dist/icons/tab-saving-light-2.png +0 -0
- drowse/web/dist/icons/tab-saving-light-20.png +0 -0
- drowse/web/dist/icons/tab-saving-light-21.png +0 -0
- drowse/web/dist/icons/tab-saving-light-22.png +0 -0
- drowse/web/dist/icons/tab-saving-light-23.png +0 -0
- drowse/web/dist/icons/tab-saving-light-24.png +0 -0
- drowse/web/dist/icons/tab-saving-light-25.png +0 -0
- drowse/web/dist/icons/tab-saving-light-26.png +0 -0
- drowse/web/dist/icons/tab-saving-light-27.png +0 -0
- drowse/web/dist/icons/tab-saving-light-28.png +0 -0
- drowse/web/dist/icons/tab-saving-light-29.png +0 -0
- drowse/web/dist/icons/tab-saving-light-3.png +0 -0
- drowse/web/dist/icons/tab-saving-light-30.png +0 -0
- drowse/web/dist/icons/tab-saving-light-31.png +0 -0
- drowse/web/dist/icons/tab-saving-light-32.png +0 -0
- drowse/web/dist/icons/tab-saving-light-33.png +0 -0
- drowse/web/dist/icons/tab-saving-light-34.png +0 -0
- drowse/web/dist/icons/tab-saving-light-35.png +0 -0
- drowse/web/dist/icons/tab-saving-light-36.png +0 -0
- drowse/web/dist/icons/tab-saving-light-37.png +0 -0
- drowse/web/dist/icons/tab-saving-light-38.png +0 -0
- drowse/web/dist/icons/tab-saving-light-39.png +0 -0
- drowse/web/dist/icons/tab-saving-light-4.png +0 -0
- drowse/web/dist/icons/tab-saving-light-40.png +0 -0
- drowse/web/dist/icons/tab-saving-light-41.png +0 -0
- drowse/web/dist/icons/tab-saving-light-42.png +0 -0
- drowse/web/dist/icons/tab-saving-light-43.png +0 -0
- drowse/web/dist/icons/tab-saving-light-44.png +0 -0
- drowse/web/dist/icons/tab-saving-light-45.png +0 -0
- drowse/web/dist/icons/tab-saving-light-46.png +0 -0
- drowse/web/dist/icons/tab-saving-light-47.png +0 -0
- drowse/web/dist/icons/tab-saving-light-5.png +0 -0
- drowse/web/dist/icons/tab-saving-light-6.png +0 -0
- drowse/web/dist/icons/tab-saving-light-7.png +0 -0
- drowse/web/dist/icons/tab-saving-light-8.png +0 -0
- drowse/web/dist/icons/tab-saving-light-9.png +0 -0
- drowse/web/dist/icons/tab-thinking-0.png +0 -0
- drowse/web/dist/icons/tab-thinking-1.png +0 -0
- drowse/web/dist/icons/tab-thinking-2.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-0.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-1.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-10.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-11.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-12.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-13.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-14.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-15.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-16.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-17.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-18.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-19.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-2.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-20.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-21.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-22.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-23.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-24.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-25.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-26.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-27.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-28.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-29.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-3.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-30.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-31.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-32.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-33.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-34.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-35.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-36.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-37.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-38.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-39.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-4.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-40.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-41.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-42.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-43.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-44.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-45.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-46.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-47.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-5.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-6.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-7.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-8.png +0 -0
- drowse/web/dist/icons/tab-thinking-dark-9.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-0.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-1.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-10.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-11.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-12.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-13.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-14.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-15.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-16.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-17.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-18.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-19.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-2.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-20.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-21.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-22.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-23.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-24.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-25.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-26.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-27.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-28.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-29.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-3.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-30.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-31.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-32.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-33.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-34.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-35.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-36.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-37.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-38.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-39.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-4.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-40.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-41.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-42.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-43.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-44.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-45.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-46.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-47.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-5.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-6.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-7.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-8.png +0 -0
- drowse/web/dist/icons/tab-thinking-light-9.png +0 -0
- drowse/web/dist/icons/tab-tokens-dark.png +0 -0
- drowse/web/dist/icons/tab-tokens-light.png +0 -0
- drowse/web/dist/icons/tab-tokens.png +0 -0
- drowse/web/dist/icons/tab-training-0.png +0 -0
- drowse/web/dist/icons/tab-training-1.png +0 -0
- drowse/web/dist/icons/tab-training-2.png +0 -0
- drowse/web/dist/icons/tab-training-dark-0.png +0 -0
- drowse/web/dist/icons/tab-training-dark-1.png +0 -0
- drowse/web/dist/icons/tab-training-dark-10.png +0 -0
- drowse/web/dist/icons/tab-training-dark-11.png +0 -0
- drowse/web/dist/icons/tab-training-dark-12.png +0 -0
- drowse/web/dist/icons/tab-training-dark-13.png +0 -0
- drowse/web/dist/icons/tab-training-dark-14.png +0 -0
- drowse/web/dist/icons/tab-training-dark-15.png +0 -0
- drowse/web/dist/icons/tab-training-dark-16.png +0 -0
- drowse/web/dist/icons/tab-training-dark-17.png +0 -0
- drowse/web/dist/icons/tab-training-dark-18.png +0 -0
- drowse/web/dist/icons/tab-training-dark-19.png +0 -0
- drowse/web/dist/icons/tab-training-dark-2.png +0 -0
- drowse/web/dist/icons/tab-training-dark-20.png +0 -0
- drowse/web/dist/icons/tab-training-dark-21.png +0 -0
- drowse/web/dist/icons/tab-training-dark-22.png +0 -0
- drowse/web/dist/icons/tab-training-dark-23.png +0 -0
- drowse/web/dist/icons/tab-training-dark-24.png +0 -0
- drowse/web/dist/icons/tab-training-dark-25.png +0 -0
- drowse/web/dist/icons/tab-training-dark-26.png +0 -0
- drowse/web/dist/icons/tab-training-dark-27.png +0 -0
- drowse/web/dist/icons/tab-training-dark-28.png +0 -0
- drowse/web/dist/icons/tab-training-dark-29.png +0 -0
- drowse/web/dist/icons/tab-training-dark-3.png +0 -0
- drowse/web/dist/icons/tab-training-dark-30.png +0 -0
- drowse/web/dist/icons/tab-training-dark-31.png +0 -0
- drowse/web/dist/icons/tab-training-dark-32.png +0 -0
- drowse/web/dist/icons/tab-training-dark-33.png +0 -0
- drowse/web/dist/icons/tab-training-dark-34.png +0 -0
- drowse/web/dist/icons/tab-training-dark-35.png +0 -0
- drowse/web/dist/icons/tab-training-dark-36.png +0 -0
- drowse/web/dist/icons/tab-training-dark-37.png +0 -0
- drowse/web/dist/icons/tab-training-dark-38.png +0 -0
- drowse/web/dist/icons/tab-training-dark-39.png +0 -0
- drowse/web/dist/icons/tab-training-dark-4.png +0 -0
- drowse/web/dist/icons/tab-training-dark-40.png +0 -0
- drowse/web/dist/icons/tab-training-dark-41.png +0 -0
- drowse/web/dist/icons/tab-training-dark-42.png +0 -0
- drowse/web/dist/icons/tab-training-dark-43.png +0 -0
- drowse/web/dist/icons/tab-training-dark-44.png +0 -0
- drowse/web/dist/icons/tab-training-dark-45.png +0 -0
- drowse/web/dist/icons/tab-training-dark-46.png +0 -0
- drowse/web/dist/icons/tab-training-dark-47.png +0 -0
- drowse/web/dist/icons/tab-training-dark-5.png +0 -0
- drowse/web/dist/icons/tab-training-dark-6.png +0 -0
- drowse/web/dist/icons/tab-training-dark-7.png +0 -0
- drowse/web/dist/icons/tab-training-dark-8.png +0 -0
- drowse/web/dist/icons/tab-training-dark-9.png +0 -0
- drowse/web/dist/icons/tab-training-light-0.png +0 -0
- drowse/web/dist/icons/tab-training-light-1.png +0 -0
- drowse/web/dist/icons/tab-training-light-10.png +0 -0
- drowse/web/dist/icons/tab-training-light-11.png +0 -0
- drowse/web/dist/icons/tab-training-light-12.png +0 -0
- drowse/web/dist/icons/tab-training-light-13.png +0 -0
- drowse/web/dist/icons/tab-training-light-14.png +0 -0
- drowse/web/dist/icons/tab-training-light-15.png +0 -0
- drowse/web/dist/icons/tab-training-light-16.png +0 -0
- drowse/web/dist/icons/tab-training-light-17.png +0 -0
- drowse/web/dist/icons/tab-training-light-18.png +0 -0
- drowse/web/dist/icons/tab-training-light-19.png +0 -0
- drowse/web/dist/icons/tab-training-light-2.png +0 -0
- drowse/web/dist/icons/tab-training-light-20.png +0 -0
- drowse/web/dist/icons/tab-training-light-21.png +0 -0
- drowse/web/dist/icons/tab-training-light-22.png +0 -0
- drowse/web/dist/icons/tab-training-light-23.png +0 -0
- drowse/web/dist/icons/tab-training-light-24.png +0 -0
- drowse/web/dist/icons/tab-training-light-25.png +0 -0
- drowse/web/dist/icons/tab-training-light-26.png +0 -0
- drowse/web/dist/icons/tab-training-light-27.png +0 -0
- drowse/web/dist/icons/tab-training-light-28.png +0 -0
- drowse/web/dist/icons/tab-training-light-29.png +0 -0
- drowse/web/dist/icons/tab-training-light-3.png +0 -0
- drowse/web/dist/icons/tab-training-light-30.png +0 -0
- drowse/web/dist/icons/tab-training-light-31.png +0 -0
- drowse/web/dist/icons/tab-training-light-32.png +0 -0
- drowse/web/dist/icons/tab-training-light-33.png +0 -0
- drowse/web/dist/icons/tab-training-light-34.png +0 -0
- drowse/web/dist/icons/tab-training-light-35.png +0 -0
- drowse/web/dist/icons/tab-training-light-36.png +0 -0
- drowse/web/dist/icons/tab-training-light-37.png +0 -0
- drowse/web/dist/icons/tab-training-light-38.png +0 -0
- drowse/web/dist/icons/tab-training-light-39.png +0 -0
- drowse/web/dist/icons/tab-training-light-4.png +0 -0
- drowse/web/dist/icons/tab-training-light-40.png +0 -0
- drowse/web/dist/icons/tab-training-light-41.png +0 -0
- drowse/web/dist/icons/tab-training-light-42.png +0 -0
- drowse/web/dist/icons/tab-training-light-43.png +0 -0
- drowse/web/dist/icons/tab-training-light-44.png +0 -0
- drowse/web/dist/icons/tab-training-light-45.png +0 -0
- drowse/web/dist/icons/tab-training-light-46.png +0 -0
- drowse/web/dist/icons/tab-training-light-47.png +0 -0
- drowse/web/dist/icons/tab-training-light-5.png +0 -0
- drowse/web/dist/icons/tab-training-light-6.png +0 -0
- drowse/web/dist/icons/tab-training-light-7.png +0 -0
- drowse/web/dist/icons/tab-training-light-8.png +0 -0
- drowse/web/dist/icons/tab-training-light-9.png +0 -0
- drowse/web/dist/icons/tab-unsupported-dark.png +0 -0
- drowse/web/dist/icons/tab-unsupported-light.png +0 -0
- drowse/web/dist/icons/tab-unsupported.png +0 -0
- drowse/web/dist/icons/tab-working-0.png +0 -0
- drowse/web/dist/icons/tab-working-1.png +0 -0
- drowse/web/dist/icons/tab-working-2.png +0 -0
- drowse/web/dist/icons/tab-working-dark-0.png +0 -0
- drowse/web/dist/icons/tab-working-dark-1.png +0 -0
- drowse/web/dist/icons/tab-working-dark-10.png +0 -0
- drowse/web/dist/icons/tab-working-dark-11.png +0 -0
- drowse/web/dist/icons/tab-working-dark-12.png +0 -0
- drowse/web/dist/icons/tab-working-dark-13.png +0 -0
- drowse/web/dist/icons/tab-working-dark-14.png +0 -0
- drowse/web/dist/icons/tab-working-dark-15.png +0 -0
- drowse/web/dist/icons/tab-working-dark-16.png +0 -0
- drowse/web/dist/icons/tab-working-dark-17.png +0 -0
- drowse/web/dist/icons/tab-working-dark-18.png +0 -0
- drowse/web/dist/icons/tab-working-dark-19.png +0 -0
- drowse/web/dist/icons/tab-working-dark-2.png +0 -0
- drowse/web/dist/icons/tab-working-dark-20.png +0 -0
- drowse/web/dist/icons/tab-working-dark-21.png +0 -0
- drowse/web/dist/icons/tab-working-dark-22.png +0 -0
- drowse/web/dist/icons/tab-working-dark-23.png +0 -0
- drowse/web/dist/icons/tab-working-dark-24.png +0 -0
- drowse/web/dist/icons/tab-working-dark-25.png +0 -0
- drowse/web/dist/icons/tab-working-dark-26.png +0 -0
- drowse/web/dist/icons/tab-working-dark-27.png +0 -0
- drowse/web/dist/icons/tab-working-dark-28.png +0 -0
- drowse/web/dist/icons/tab-working-dark-29.png +0 -0
- drowse/web/dist/icons/tab-working-dark-3.png +0 -0
- drowse/web/dist/icons/tab-working-dark-30.png +0 -0
- drowse/web/dist/icons/tab-working-dark-31.png +0 -0
- drowse/web/dist/icons/tab-working-dark-32.png +0 -0
- drowse/web/dist/icons/tab-working-dark-33.png +0 -0
- drowse/web/dist/icons/tab-working-dark-34.png +0 -0
- drowse/web/dist/icons/tab-working-dark-35.png +0 -0
- drowse/web/dist/icons/tab-working-dark-36.png +0 -0
- drowse/web/dist/icons/tab-working-dark-37.png +0 -0
- drowse/web/dist/icons/tab-working-dark-38.png +0 -0
- drowse/web/dist/icons/tab-working-dark-39.png +0 -0
- drowse/web/dist/icons/tab-working-dark-4.png +0 -0
- drowse/web/dist/icons/tab-working-dark-40.png +0 -0
- drowse/web/dist/icons/tab-working-dark-41.png +0 -0
- drowse/web/dist/icons/tab-working-dark-42.png +0 -0
- drowse/web/dist/icons/tab-working-dark-43.png +0 -0
- drowse/web/dist/icons/tab-working-dark-44.png +0 -0
- drowse/web/dist/icons/tab-working-dark-45.png +0 -0
- drowse/web/dist/icons/tab-working-dark-46.png +0 -0
- drowse/web/dist/icons/tab-working-dark-47.png +0 -0
- drowse/web/dist/icons/tab-working-dark-5.png +0 -0
- drowse/web/dist/icons/tab-working-dark-6.png +0 -0
- drowse/web/dist/icons/tab-working-dark-7.png +0 -0
- drowse/web/dist/icons/tab-working-dark-8.png +0 -0
- drowse/web/dist/icons/tab-working-dark-9.png +0 -0
- drowse/web/dist/icons/tab-working-light-0.png +0 -0
- drowse/web/dist/icons/tab-working-light-1.png +0 -0
- drowse/web/dist/icons/tab-working-light-10.png +0 -0
- drowse/web/dist/icons/tab-working-light-11.png +0 -0
- drowse/web/dist/icons/tab-working-light-12.png +0 -0
- drowse/web/dist/icons/tab-working-light-13.png +0 -0
- drowse/web/dist/icons/tab-working-light-14.png +0 -0
- drowse/web/dist/icons/tab-working-light-15.png +0 -0
- drowse/web/dist/icons/tab-working-light-16.png +0 -0
- drowse/web/dist/icons/tab-working-light-17.png +0 -0
- drowse/web/dist/icons/tab-working-light-18.png +0 -0
- drowse/web/dist/icons/tab-working-light-19.png +0 -0
- drowse/web/dist/icons/tab-working-light-2.png +0 -0
- drowse/web/dist/icons/tab-working-light-20.png +0 -0
- drowse/web/dist/icons/tab-working-light-21.png +0 -0
- drowse/web/dist/icons/tab-working-light-22.png +0 -0
- drowse/web/dist/icons/tab-working-light-23.png +0 -0
- drowse/web/dist/icons/tab-working-light-24.png +0 -0
- drowse/web/dist/icons/tab-working-light-25.png +0 -0
- drowse/web/dist/icons/tab-working-light-26.png +0 -0
- drowse/web/dist/icons/tab-working-light-27.png +0 -0
- drowse/web/dist/icons/tab-working-light-28.png +0 -0
- drowse/web/dist/icons/tab-working-light-29.png +0 -0
- drowse/web/dist/icons/tab-working-light-3.png +0 -0
- drowse/web/dist/icons/tab-working-light-30.png +0 -0
- drowse/web/dist/icons/tab-working-light-31.png +0 -0
- drowse/web/dist/icons/tab-working-light-32.png +0 -0
- drowse/web/dist/icons/tab-working-light-33.png +0 -0
- drowse/web/dist/icons/tab-working-light-34.png +0 -0
- drowse/web/dist/icons/tab-working-light-35.png +0 -0
- drowse/web/dist/icons/tab-working-light-36.png +0 -0
- drowse/web/dist/icons/tab-working-light-37.png +0 -0
- drowse/web/dist/icons/tab-working-light-38.png +0 -0
- drowse/web/dist/icons/tab-working-light-39.png +0 -0
- drowse/web/dist/icons/tab-working-light-4.png +0 -0
- drowse/web/dist/icons/tab-working-light-40.png +0 -0
- drowse/web/dist/icons/tab-working-light-41.png +0 -0
- drowse/web/dist/icons/tab-working-light-42.png +0 -0
- drowse/web/dist/icons/tab-working-light-43.png +0 -0
- drowse/web/dist/icons/tab-working-light-44.png +0 -0
- drowse/web/dist/icons/tab-working-light-45.png +0 -0
- drowse/web/dist/icons/tab-working-light-46.png +0 -0
- drowse/web/dist/icons/tab-working-light-47.png +0 -0
- drowse/web/dist/icons/tab-working-light-5.png +0 -0
- drowse/web/dist/icons/tab-working-light-6.png +0 -0
- drowse/web/dist/icons/tab-working-light-7.png +0 -0
- drowse/web/dist/icons/tab-working-light-8.png +0 -0
- drowse/web/dist/icons/tab-working-light-9.png +0 -0
- drowse/web/dist/index.html +34 -0
- drowse/web/dist/social/drowse.png +0 -0
- drowse/web/dist/social/drowse.svg +6 -0
- drowse/web/dist/theme-init.js +25 -0
- drowse/web/routes.py +118 -0
- drowse_ai-5.3.1.dist-info/METADATA +632 -0
- drowse_ai-5.3.1.dist-info/RECORD +1341 -0
- drowse_ai-5.3.1.dist-info/WHEEL +5 -0
- drowse_ai-5.3.1.dist-info/entry_points.txt +2 -0
- drowse_ai-5.3.1.dist-info/licenses/LICENSE +661 -0
- 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
|