modelroom 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. modelroom-0.1.0/.gitignore +26 -0
  2. modelroom-0.1.0/AGENTS.md +180 -0
  3. modelroom-0.1.0/CHANGELOG.md +991 -0
  4. modelroom-0.1.0/CONTRACTS.md +4367 -0
  5. modelroom-0.1.0/LICENSE +21 -0
  6. modelroom-0.1.0/PKG-INFO +390 -0
  7. modelroom-0.1.0/README.md +365 -0
  8. modelroom-0.1.0/docs/README.md +28 -0
  9. modelroom-0.1.0/docs/assets/banner.png +0 -0
  10. modelroom-0.1.0/docs/assets/banner.svg +41 -0
  11. modelroom-0.1.0/docs/assets/social-preview.png +0 -0
  12. modelroom-0.1.0/docs/assets/social-preview.svg +41 -0
  13. modelroom-0.1.0/modelroom/__init__.py +9 -0
  14. modelroom-0.1.0/modelroom/answers.py +55 -0
  15. modelroom-0.1.0/modelroom/binding.py +209 -0
  16. modelroom-0.1.0/modelroom/catalog.py +185 -0
  17. modelroom-0.1.0/modelroom/catalog.toml +569 -0
  18. modelroom-0.1.0/modelroom/cli.py +724 -0
  19. modelroom-0.1.0/modelroom/config.py +668 -0
  20. modelroom-0.1.0/modelroom/contracts.py +798 -0
  21. modelroom-0.1.0/modelroom/daemon.py +205 -0
  22. modelroom-0.1.0/modelroom/dialog.py +463 -0
  23. modelroom-0.1.0/modelroom/document.py +169 -0
  24. modelroom-0.1.0/modelroom/examples.py +551 -0
  25. modelroom-0.1.0/modelroom/fetch.py +190 -0
  26. modelroom-0.1.0/modelroom/fetch_types.py +40 -0
  27. modelroom-0.1.0/modelroom/fit.py +334 -0
  28. modelroom-0.1.0/modelroom/guided.py +783 -0
  29. modelroom-0.1.0/modelroom/guided_context.py +379 -0
  30. modelroom-0.1.0/modelroom/guided_contracts.py +127 -0
  31. modelroom-0.1.0/modelroom/guided_loadtest.py +295 -0
  32. modelroom-0.1.0/modelroom/guided_models.py +556 -0
  33. modelroom-0.1.0/modelroom/guided_search.py +266 -0
  34. modelroom-0.1.0/modelroom/hf.py +489 -0
  35. modelroom-0.1.0/modelroom/http.py +321 -0
  36. modelroom-0.1.0/modelroom/importer.py +495 -0
  37. modelroom-0.1.0/modelroom/intro.py +456 -0
  38. modelroom-0.1.0/modelroom/llmfit.py +344 -0
  39. modelroom-0.1.0/modelroom/loadtest.py +554 -0
  40. modelroom-0.1.0/modelroom/measure.py +676 -0
  41. modelroom-0.1.0/modelroom/measurements.py +494 -0
  42. modelroom-0.1.0/modelroom/migrate.py +234 -0
  43. modelroom-0.1.0/modelroom/ollama.py +388 -0
  44. modelroom-0.1.0/modelroom/ollama_local.py +63 -0
  45. modelroom-0.1.0/modelroom/profile.py +278 -0
  46. modelroom-0.1.0/modelroom/protocol_v1.toml +31 -0
  47. modelroom-0.1.0/modelroom/provenance.py +170 -0
  48. modelroom-0.1.0/modelroom/quantization.py +308 -0
  49. modelroom-0.1.0/modelroom/ranking.py +134 -0
  50. modelroom-0.1.0/modelroom/relation.py +113 -0
  51. modelroom-0.1.0/modelroom/render.py +454 -0
  52. modelroom-0.1.0/modelroom/render_cmd.py +243 -0
  53. modelroom-0.1.0/modelroom/screen.py +500 -0
  54. modelroom-0.1.0/modelroom/search.py +794 -0
  55. modelroom-0.1.0/modelroom/search_age.py +130 -0
  56. modelroom-0.1.0/modelroom/search_apply.py +158 -0
  57. modelroom-0.1.0/modelroom/search_pages.py +353 -0
  58. modelroom-0.1.0/modelroom/search_word.py +238 -0
  59. modelroom-0.1.0/modelroom/state.py +519 -0
  60. modelroom-0.1.0/modelroom/toml_writer.py +71 -0
  61. modelroom-0.1.0/modelroom/views.py +775 -0
  62. modelroom-0.1.0/pyproject.toml +68 -0
  63. modelroom-0.1.0/tests/conftest.py +20 -0
  64. modelroom-0.1.0/tests/fixture_support.py +544 -0
  65. modelroom-0.1.0/tests/fixtures/README.md +293 -0
  66. modelroom-0.1.0/tests/fixtures/catalog_excerpt.toml +27 -0
  67. modelroom-0.1.0/tests/fixtures/config_v1/config.toml +32 -0
  68. modelroom-0.1.0/tests/fixtures/config_v2/config.toml +32 -0
  69. modelroom-0.1.0/tests/fixtures/export_v1.json +127 -0
  70. modelroom-0.1.0/tests/fixtures/hf_deepseek_r1_qwen3_8b_config.json +35 -0
  71. modelroom-0.1.0/tests/fixtures/hf_deepseek_r1_qwen3_8b_model.json +80 -0
  72. modelroom-0.1.0/tests/fixtures/hf_qwen_qwen35_9b_config.json +103 -0
  73. modelroom-0.1.0/tests/fixtures/hf_qwen_qwen35_9b_model.json +1 -0
  74. modelroom-0.1.0/tests/fixtures/hf_search_mistral_publisher.json +51 -0
  75. modelroom-0.1.0/tests/fixtures/hf_search_mistral_unsloth.json +55 -0
  76. modelroom-0.1.0/tests/fixtures/hf_search_none.json +1 -0
  77. modelroom-0.1.0/tests/fixtures/hf_search_qwen38_27b_catalog_page.json +155 -0
  78. modelroom-0.1.0/tests/fixtures/hf_search_qwen_most_downloaded.json +70 -0
  79. modelroom-0.1.0/tests/fixtures/hf_search_qwen_newest.json +53 -0
  80. modelroom-0.1.0/tests/fixtures/hf_search_qwen_page_full.json +402 -0
  81. modelroom-0.1.0/tests/fixtures/hf_search_qwen_publisher.json +31 -0
  82. modelroom-0.1.0/tests/fixtures/hf_search_qwen_unsloth.json +71 -0
  83. modelroom-0.1.0/tests/fixtures/hf_search_qwen_unsloth_downloads.json +62 -0
  84. modelroom-0.1.0/tests/fixtures/hf_synthetic_dense_config.json +12 -0
  85. modelroom-0.1.0/tests/fixtures/hf_synthetic_paginated_tree_page1.json +20 -0
  86. modelroom-0.1.0/tests/fixtures/hf_synthetic_paginated_tree_page2.json +18 -0
  87. modelroom-0.1.0/tests/fixtures/hf_typed_qwen_qwen35_9b.json +37 -0
  88. modelroom-0.1.0/tests/fixtures/hf_typed_unsloth_qwen35_9b_gguf.json +30 -0
  89. modelroom-0.1.0/tests/fixtures/hf_unsloth_deepseek_r1_qwen3_8b_gguf_model.json +66 -0
  90. modelroom-0.1.0/tests/fixtures/hf_unsloth_deepseek_r1_qwen3_8b_gguf_tree.json +56 -0
  91. modelroom-0.1.0/tests/fixtures/hf_unsloth_does_not_exist_model.json +10 -0
  92. modelroom-0.1.0/tests/fixtures/hf_unsloth_qwen35_9b_gguf_model.json +1 -0
  93. modelroom-0.1.0/tests/fixtures/hf_unsloth_qwen35_9b_gguf_tree.json +1 -0
  94. modelroom-0.1.0/tests/fixtures/llmfit_system_laptop.json +39 -0
  95. modelroom-0.1.0/tests/fixtures/llmfit_version.txt +1 -0
  96. modelroom-0.1.0/tests/fixtures/lspci_nn_cpu_server.txt +6 -0
  97. modelroom-0.1.0/tests/fixtures/lspci_nn_headless.txt +3 -0
  98. modelroom-0.1.0/tests/fixtures/lspci_nn_intel_laptop.txt +3 -0
  99. modelroom-0.1.0/tests/fixtures/lspci_nn_nvidia_laptop.txt +2 -0
  100. modelroom-0.1.0/tests/fixtures/markdown_view_ap9_c2.md +37 -0
  101. modelroom-0.1.0/tests/fixtures/measurement_v2_invalid.json +60 -0
  102. modelroom-0.1.0/tests/fixtures/nvidia_smi_one_gpu.csv +1 -0
  103. modelroom-0.1.0/tests/fixtures/nvidia_smi_two_gpus.csv +2 -0
  104. modelroom-0.1.0/tests/fixtures/ollama_generate_valid.json +14 -0
  105. modelroom-0.1.0/tests/fixtures/ollama_manifest_head_9b.json +7 -0
  106. modelroom-0.1.0/tests/fixtures/ollama_ps_deepseek_loaded.json +23 -0
  107. modelroom-0.1.0/tests/fixtures/ollama_qwen35_9b-mlx-bf16.json +1 -0
  108. modelroom-0.1.0/tests/fixtures/ollama_qwen35_9b-q4_K_M.json +1 -0
  109. modelroom-0.1.0/tests/fixtures/ollama_qwen35_9b.json +1 -0
  110. modelroom-0.1.0/tests/fixtures/ollama_show_hf_gguf.json +19 -0
  111. modelroom-0.1.0/tests/fixtures/ollama_tags_local.json +58 -0
  112. modelroom-0.1.0/tests/fixtures/ollama_tags_local_loadtest.json +127 -0
  113. modelroom-0.1.0/tests/fixtures/ollama_tags_qwen35.html +4829 -0
  114. modelroom-0.1.0/tests/fixtures/ollama_version_local.json +3 -0
  115. modelroom-0.1.0/tests/fixtures/proc_meminfo_linux.txt +5 -0
  116. modelroom-0.1.0/tests/fixtures/profiles_v1/linux-cpu-server.json +15 -0
  117. modelroom-0.1.0/tests/fixtures/profiles_v1/unified-memory.json +15 -0
  118. modelroom-0.1.0/tests/fixtures/profiles_v1/vram-zero.json +15 -0
  119. modelroom-0.1.0/tests/fixtures/profiles_v1/windows-nvidia-laptop.json +49 -0
  120. modelroom-0.1.0/tests/golden/guided-screen-measured.txt +70 -0
  121. modelroom-0.1.0/tests/golden/guided-screen-nothing.txt +45 -0
  122. modelroom-0.1.0/tests/golden/guided-screen.txt +65 -0
  123. modelroom-0.1.0/tests/search_testset.toml +62 -0
  124. modelroom-0.1.0/tests/test_binding.py +175 -0
  125. modelroom-0.1.0/tests/test_catalog.py +245 -0
  126. modelroom-0.1.0/tests/test_cli.py +798 -0
  127. modelroom-0.1.0/tests/test_config.py +922 -0
  128. modelroom-0.1.0/tests/test_contracts.py +987 -0
  129. modelroom-0.1.0/tests/test_daemon.py +268 -0
  130. modelroom-0.1.0/tests/test_dialog.py +510 -0
  131. modelroom-0.1.0/tests/test_document.py +108 -0
  132. modelroom-0.1.0/tests/test_fetch.py +608 -0
  133. modelroom-0.1.0/tests/test_fit.py +322 -0
  134. modelroom-0.1.0/tests/test_fit_size.py +198 -0
  135. modelroom-0.1.0/tests/test_fit_v2.py +182 -0
  136. modelroom-0.1.0/tests/test_guided.py +1918 -0
  137. modelroom-0.1.0/tests/test_guided_context.py +309 -0
  138. modelroom-0.1.0/tests/test_guided_contracts.py +156 -0
  139. modelroom-0.1.0/tests/test_guided_models.py +663 -0
  140. modelroom-0.1.0/tests/test_guided_screen.py +104 -0
  141. modelroom-0.1.0/tests/test_guided_search.py +173 -0
  142. modelroom-0.1.0/tests/test_hardware_cmd.py +763 -0
  143. modelroom-0.1.0/tests/test_hf.py +1224 -0
  144. modelroom-0.1.0/tests/test_http.py +463 -0
  145. modelroom-0.1.0/tests/test_importer.py +1065 -0
  146. modelroom-0.1.0/tests/test_intro.py +369 -0
  147. modelroom-0.1.0/tests/test_language_standard.py +276 -0
  148. modelroom-0.1.0/tests/test_llmfit.py +475 -0
  149. modelroom-0.1.0/tests/test_loadtest.py +571 -0
  150. modelroom-0.1.0/tests/test_measure.py +776 -0
  151. modelroom-0.1.0/tests/test_measurements.py +381 -0
  152. modelroom-0.1.0/tests/test_migrate.py +405 -0
  153. modelroom-0.1.0/tests/test_ollama.py +638 -0
  154. modelroom-0.1.0/tests/test_ollama_local.py +94 -0
  155. modelroom-0.1.0/tests/test_profile.py +263 -0
  156. modelroom-0.1.0/tests/test_provenance.py +708 -0
  157. modelroom-0.1.0/tests/test_public_hygiene.py +73 -0
  158. modelroom-0.1.0/tests/test_quantization.py +551 -0
  159. modelroom-0.1.0/tests/test_ranking.py +209 -0
  160. modelroom-0.1.0/tests/test_relation.py +88 -0
  161. modelroom-0.1.0/tests/test_release_check.py +500 -0
  162. modelroom-0.1.0/tests/test_release_smoke.py +157 -0
  163. modelroom-0.1.0/tests/test_render.py +884 -0
  164. modelroom-0.1.0/tests/test_screen.py +425 -0
  165. modelroom-0.1.0/tests/test_search.py +1752 -0
  166. modelroom-0.1.0/tests/test_search_pages.py +356 -0
  167. modelroom-0.1.0/tests/test_search_word.py +252 -0
  168. modelroom-0.1.0/tests/test_selftest.py +649 -0
  169. modelroom-0.1.0/tests/test_state.py +886 -0
  170. modelroom-0.1.0/tests/test_version_single_source.py +41 -0
@@ -0,0 +1,26 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .venv/
9
+
10
+ # Local environment and secrets never enter the repository
11
+ .env
12
+ .env.*
13
+ !.env.example
14
+
15
+ # Generated artefacts
16
+ _derived/
17
+ _graph_code_modelroom/
18
+
19
+ # Editor and OS noise
20
+ .idea/
21
+ .vscode/
22
+ .DS_Store
23
+ Thumbs.db
24
+
25
+ # Local safety-net for deletes (a developer-machine hook, not part of the repository)
26
+ .trash/
@@ -0,0 +1,180 @@
1
+ # AGENTS.md
2
+
3
+ Rules for every tool and every person working on this repository. Tool-specific files (if any
4
+ ever appear) import this one and stay thin; the truth lives here.
5
+
6
+ ## Purpose
7
+
8
+ `modelroom` finds the local packages (GGUF and tensor builds) that exist for an allow-list of
9
+ model families on Hugging Face and in the Ollama registry, and computes whether each package
10
+ fits a measured machine. It is a command-line tool and a small library, published under MIT.
11
+ Status: alpha, first release 0.1.0; the data shapes are versioned contracts (`CONTRACTS.md`),
12
+ layout and fit rules may still change.
13
+
14
+ The commands:
15
+
16
+ | Command | What it does |
17
+ |---|---|
18
+ | `modelroom` | the guided mode: asks, writes `modelroom.toml`, then runs the three commands below, and between the fetch and the render offers the load test of the packages this machine already has (`CONTRACTS.md`, "Guided mode", "Load test (stage 1)") |
19
+ | `modelroom --answers <file>` | the same run with the dialog's answers from a TOML file, for a self-test or CI |
20
+ | `modelroom --config <file>` | the guided mode on that configuration, rather than the folder it last used |
21
+ | `modelroom hardware --config <file> [--machine <name>] [--cpu-only] [--new-identity \| --same-machine]` | measure this machine and write its schema-2 profile; `--new-identity` writes a new one, `--same-machine` writes the bound profile again under its own id (`CONTRACTS.md`, "Profile binding") |
22
+ | `modelroom fetch --config <file> --machine <name>` | fetch package metadata for every configured base model |
23
+ | `modelroom render --config <file>` | write the ranking per machine: Markdown, and the JSON view next to it |
24
+ | `modelroom migrate --config <file>` | move schema-1 profiles and configuration to schema 2 |
25
+ | `modelroom export-profile --config <file> [--profile <id>] --out <file>` | write one machine's profile and measurements to a file |
26
+ | `modelroom import-profile <file> --config <file>` | read such a file into this results folder |
27
+
28
+ Only the guided mode ever asks a question; every subcommand runs without a terminal.
29
+
30
+ ## Stack and language choice
31
+
32
+ Python (3.11 or newer), because the catalog logic and Pydantic contracts it integrates with are
33
+ Python and the run has to be platform neutral. Dependencies are kept to the standard library,
34
+ `pydantic` and `questionary`; `llmfit` is an external tool called as a subprocess, never
35
+ vendored. Everything in this repository is English; the next section says which English, and
36
+ which words.
37
+
38
+ ### Terminal dialog library
39
+
40
+ The guided mode's selection lists with check marks are `questionary` (MIT) on `prompt_toolkit`
41
+ (BSD), imported by `modelroom/dialog.py` and by nothing else -- every other module and all six
42
+ subcommands run without it. No dialog layer is written by hand.
43
+
44
+ The pin follows a gate, never convenience: the candidate has to render and read real keys in
45
+ every terminal a user of this package may sit at. Measured 2026-09-24 with `questionary` 2.1.1
46
+ on `prompt_toolkit` 3.0.53, PowerShell 5.1, key events written into the console input buffer
47
+ (`WriteConsoleInputW` on `CONIN$`, the same buffer a keyboard fills) and read back through
48
+ prompt_toolkit's own Win32 input: Windows console host (`conhost`) **pass**, Windows Terminal
49
+ **pass**. A Linux SSH session with a PTY is not part of that measurement and is open. Without a
50
+ passed gate there is no pin.
51
+
52
+ Package manager is `uv`. `uv.lock` is committed and is the truth for dependency versions;
53
+ install with `uv sync --frozen`. To raise a dependency: `uv lock --upgrade-package <name>`,
54
+ run the tests, commit the lock.
55
+
56
+ ## Language standard
57
+
58
+ **US English**, everywhere: identifiers, messages, docstrings, comments, commit messages,
59
+ documentation. A British spelling is a finding, not a style choice -- write `color`,
60
+ `behavior`, `initialize`, `optimize`, `normalize`, `center`, `catalog`, `analyze`, `favor`,
61
+ `license` (noun and verb alike), `modeling`, `labeled`. No German ever reaches a user of the
62
+ package, so none of it is written here either.
63
+
64
+ One word per idea, so that a message, a field value and a paragraph of documentation say the
65
+ same thing in the same way:
66
+
67
+ | Write | For | Never |
68
+ |---|---|---|
69
+ | `latest`, `legacy` (with its `successor`), `unknown` | where a model stands in its family | `older`, `newer`, `outdated` |
70
+ | `measured`, `entered`, `computed` | where a value came from | "detected", "estimated", "real" |
71
+ | `publisher`, `listed packager`, `other` | who owns a repository | `untrusted`, `trustworthy`, or any other verdict on a packager; only the class is stated |
72
+ | `unknown` | a fact with no evidence behind it | an empty cell, `n/a`, or a plausible guess |
73
+ | `–` | the same fact in a **column of a list or a table**, where the word would read as a statement about the row (decided 2026-09-25) | an empty cell, `n/a`, `-` where the console can encode `–` |
74
+ | `not comparable` | two measurements from different scenarios | "slower", "worse" |
75
+ | `display adapter only` | a GPU that is present but cannot carry a fit | "no GPU" |
76
+ | `none known` | no Ollama name is mapped to this package, in prose and in a log | "missing", "not available"; in a column of a list it is `–`, see above |
77
+
78
+ An age statement is always positive evidence about one model (a successor named at the
79
+ publisher's repository, or the shipped catalog), never a comparison of two version numbers.
80
+
81
+ `tests/test_language_standard.py` is this section in executable form: it reads every module
82
+ under `modelroom/` and the prose files, and names file, line, word and replacement. Its
83
+ exception list carries a reason per entry and has to shrink, never grow quietly.
84
+
85
+ ## Working rules
86
+
87
+ - Test first (red, green, refactor). Real dependencies or simple in-memory fakes; no mocking
88
+ frameworks. Network-facing code is tested against recorded fixtures, never against the live
89
+ API in the default test run.
90
+ - Conventional Commits in English: `feat(scope): ...`, `fix(scope): ...`, `docs: ...`,
91
+ `chore: ...`, `test: ...`, `refactor: ...`.
92
+ - YAGNI. No placeholder code, no unimplemented TODOs, no dead code. A function does one thing.
93
+ - Explicit error handling. Every failure that a user can act on becomes a message and an exit
94
+ code; nothing is swallowed.
95
+ - A cross-process lock is a kernel file lock held on a stable file (`msvcrt.locking` on Windows,
96
+ `fcntl.flock` elsewhere, non-blocking), never a create-rename-delete choreography with an age
97
+ rule: the kernel releases a crashed holder's lock, a live holder keeps it regardless of age, and
98
+ the lock file is never renamed or deleted. Three review rounds showed that no file-name
99
+ choreography closes every race; see the "Lock file" section of `CONTRACTS.md`.
100
+ - Exit codes are part of the contract: `0` success (`hardware`: measured and written, even when
101
+ `llmfit` was not there to cross-check the readings), `1` at least one `fetch` area incomplete
102
+ (or a command stopped at another process's lock, or this run is not newer than the stored
103
+ snapshot -- both leave the state directory untouched -- or `hardware` wrote the profile but
104
+ could not bind this machine to it), `2` the configuration is missing or
105
+ invalid, the named machine is not a writer (`fetch`) or not configured at all (`hardware`),
106
+ `hardware`'s bound profile belongs to another machine, or an external tool a command requires
107
+ is missing or too old, `3` an input file has an unsupported schema version. The guided mode
108
+ adds two of its own: `2` as well when there is no terminal and no `--answers`, when the input
109
+ ends, or when an answer is missing or unusable, and `130` when the user presses Ctrl-C.
110
+ Details: `CONTRACTS.md`.
111
+
112
+ ## Hard boundaries
113
+
114
+ This repository is public. Nothing operator-specific belongs in it:
115
+
116
+ - No secrets, ever. Not in code, not in tests, not in fixtures, not in the history.
117
+ - No personal names, no e-mail addresses, no absolute paths from anyone's machine. The only
118
+ organization named is the maintainer, in `README.md`, `LICENSE` and `pyproject.toml`.
119
+ - No references to an internal workspace, its folder layout, its personas or its chronicle.
120
+ `tests/test_public_hygiene.py` enforces a negative list over every tracked file and runs
121
+ from the git hooks in `.githooks/`.
122
+ - Configuration is data the user brings (`modelroom.toml` or a `Configuration` object). The
123
+ package never guesses paths relative to the current working directory: paths in a
124
+ configuration file resolve relative to that file, programmatic callers pass absolute paths.
125
+
126
+ ## Contracts
127
+
128
+ Shared data shapes are defined once, as Pydantic models in `modelroom/contracts.py`
129
+ (`extra="forbid"`, value ranges, coupled fields validated), and described for readers in
130
+ `CONTRACTS.md` with the same examples. Contract tests keep the two in step. Every persisted
131
+ file (configuration, snapshot, hardware profile) carries a `schema_version`; readers declare
132
+ the range they accept and refuse anything else before they write. See `CONTRACTS.md`.
133
+
134
+ ## Documentation chain
135
+
136
+ `docs/` holds the repository's own record, one folder per genre, created with its first real
137
+ entry and never as an empty shell:
138
+
139
+ ```
140
+ idea -> decision -> execution -> measurement -> operation -> incident
141
+ docs/rfc/ docs/adr/ docs/plans/ docs/slo/ docs/runbooks/ docs/postmortems/
142
+ docs/playbooks/
143
+ ```
144
+
145
+ An ADR is immutable; a changed situation gets a new ADR that supersedes the old one. Each
146
+ genre folder carries a `README.md` index with one line per entry. `docs/README.md` is the map.
147
+
148
+ ## Security, definition of done
149
+
150
+ Attack surface of this tool: outbound HTTPS to `huggingface.co`, `ollama.com` and `registry.ollama.ai`,
151
+ HTTP to the Ollama daemon on `127.0.0.1:11434` -- five read and generate calls, never `/api/pull`
152
+ or `/api/delete` -- subprocess calls to `llmfit` and `nvidia-smi` with fixed argument lists, and
153
+ file writes under the configured state directory. No user-facing web surface, no uploads, no HTML
154
+ rendering of whatever a registry returns. The tests therefore have to prove, with a deliberately
155
+ broken input each:
156
+
157
+ - a configuration that points the state directory outside its own folder tree is rejected
158
+ before anything is written;
159
+ - responses from the two registries are validated against the contracts and an unexpected
160
+ shape ends the area as incomplete instead of corrupting the snapshot;
161
+ - a snapshot or hardware file with an unsupported `schema_version` is refused before any
162
+ write (exit `3`);
163
+ - a second command against the same state directory stops at the lock instead of waiting;
164
+ - the `llmfit` subprocess is called with a fixed argument list, never with user-controlled
165
+ strings, and a missing or too old `llmfit` ends with exit `2` and an install hint;
166
+ - the daemon transport refuses every origin but `http://127.0.0.1:11434` and every call outside
167
+ its five, carries no proxy handler and follows no redirect, and no module of the package names
168
+ `/api/pull` or `/api/delete`.
169
+
170
+ ## Versioning and releases
171
+
172
+ One version source: `[project] version` in `pyproject.toml`, Semver, starting at `0.1.0`.
173
+ `modelroom.__version__` reads it from package metadata; a test fails if a second version
174
+ literal appears anywhere. Releases are annotated tags `vX.Y.Z` on `main`. Release procedure:
175
+
176
+ 1. Gate: `uv run --frozen python scripts/release-check.py` ends with `RELEASE-CHECK: OK <version>`.
177
+ 2. Smoke: `uv run --frozen python scripts/release-smoke.py` ends with `RELEASE-SMOKE: OK`.
178
+ 3. Tag: `git tag -a vX.Y.Z -m "modelroom X.Y.Z"`, then the gate again with `--tag vX.Y.Z`.
179
+ 4. Review: an independent second-model review of the release diff, findings closed or answered.
180
+ 5. Upload: `uv build` into an empty folder and `uv publish` by the maintainer, then push the tag.