You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit b7c6bdb
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: docs/ARCHITECTURE.md
+23-1Lines changed: 23 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -49,6 +49,8 @@ one to open.
49
49
| Reference name shape |`dw/reference_names.py`: `reference_name_errors`| The shape of every `asset:`, `prompt:` and `output:` name is checked before the queue, and existence is checked later. |`tests/test_reference_names.py::TestTheValidationPass::test_a_malformed_reference_is_refused_before_the_queue`|
50
50
| Library reads |`dw/library.py`: `LibraryPath`| Reads go through the roots front to back, so an earlier name shadows a later one. |`tests/test_library_path.py::TestResolution::test_the_front_of_the_path_shadows_the_rest`|
51
51
| Library writes |`dw/library.py`: `LibraryPath`| Writes go only to the front root, so saving something opened from a read-only root writes a copy. |`tests/test_library_path.py::TestConstruction::test_the_writable_root_comes_first`, `tests/test_library_sources.py::TestServer::test_saving_an_example_prompt_writes_a_copy`|
52
+
| LoRA catalog matching |`dw/lora_catalog.py`: `matches`, `workflow_bases`| An entry fits a base only by exact repo id (and H3 partition); no alias or family match. |`tests/test_lora_catalog.py::TestMatching::test_the_base_must_match_exactly`|
53
+
| Hub search |`dw/lora_hub.py`: `search_hub`| The Hub is searched only by `GET /api/loras/recommend`, never raises, and offers no pickle-only repo. |`tests/test_lora_hub.py::TestCandidates::test_a_pickle_only_repo_is_dropped`, `tests/test_lora_hub.py::TestFailure::test_a_hub_error_is_returned_not_raised`|
52
54
| Packaged builtins |`dw/library.py`: `builtin_root`, `resolve_sub_workflow_reference`|`builtin:` names the packaged `dw/workflows/`, not the top-level `workflows/` examples. |`tests/test_sub_workflow_resolver.py::TestTheResolver::test_a_builtin_resolves_in_the_packaged_root`|
53
55
| Sub-workflow paths |`dw/library.py`: `resolve_sub_workflow_reference`, `resolve_sub_workflow`| A sub-workflow path is confined to the root it resolves in. The search order is in the docstring of `resolve_sub_workflow`. |`tests/test_sub_workflow_resolver.py::TestTheResolver::test_a_path_escaping_its_root_is_refused`|
54
56
| Workspace |`dw/workspace.py`: `resolve_workspace`, `set_workspace`|`--workspace` beats `DW_WORKSPACE`, which beats the setting, which beats a working directory that looks like a workspace, which beats `~/diffusers-workspace`. |`tests/test_workspace.py::TestResolution::test_a_flag_wins_over_everything`, `tests/test_workspace.py::TestResolution::test_a_bare_working_directory_falls_back_to_the_home_workspace`|
@@ -99,6 +101,7 @@ one to open.
99
101
| Worker protocol |`dw/worker_protocol.py`: `parse_reply`| Every command and reply is a frozen dataclass that travels as a wire dict. |`tests/test_worker_messages.py::test_from_wire_inverts_to_wire`, `tests/test_worker_messages.py::test_an_unknown_reply_type_is_kept_whole_rather_than_raised`|
100
102
| Persistent worker |`dw/worker.py`, `dw/worker_manager.py`, `dw/serve.py`| Jobs run in one spawned worker that keeps models loaded, so a change to engine code needs a server restart. | — |
101
103
| Failed-run reporting |`dw/worker.py`, `dw/worker_protocol.py`: `Failed`, `Cancelled`| A failed or cancelled run's reply still carries the manifest of the steps that ran. |`tests/test_worker_execute.py::test_failure_carries_the_manifest_of_the_steps_that_ran`, `tests/test_worker_execute.py::test_cancellation_carries_the_manifest_too`|
104
+
| Failure path redaction |`dw/path_redaction.py`: `redact_paths`, called by `dw/worker.py`| A failed run's message and traceback name a file under the job's asset roots or output directory by its `asset:`/`output:` reference, never its absolute server path. |`tests/test_worker_execute.py::test_a_failure_names_an_asset_by_reference_not_by_server_path`, `tests/test_path_redaction.py`|
102
105
| Run-time warnings |`dw/events.py`: `emit_warning`| A warning found at run time is emitted as an event, so it reaches the caller and not just the log. |`tests/test_concat_videos.py::TestWarningsReachTheCaller::test_the_level_spread_warning_is_emitted_as_an_event`|
103
106
104
107
## Media and DSP
@@ -140,11 +143,30 @@ one to open.
140
143
|`dw_mcp` stays torch-free |`dw_mcp/`|`dw_mcp` reaches `dw.serve` over HTTP and imports no `dw` module, because `dw/__init__.py` pulls in torch. |`tests/test_mcp_server.py::TestStartupWeight::test_the_server_starts_without_importing_the_engine`|
141
144
| Tool surface |`dw_mcp/server.py`, `dw_mcp/tools_*.py`| Only these modules import the MCP SDK, and each tool body is a one-line call into a handler. |`tests/test_mcp_server.py::test_the_wiring_table_covers_every_registered_tool`, `tests/test_mcp_server.py::test_the_stated_tool_count_is_the_registered_one`|
142
145
| Surface text budget | the tool docstrings in `dw_mcp/tools_*.py`, the instructions in `dw_mcp/server.py`| The instructions and each tool description stay at or under 2,048 characters, and the whole surface stays within its token budget. |`tests/test_mcp_server.py`: `SURFACE_BUDGET`, `CLIENT_TEXT_LIMIT`; `tests/test_mcp_server.py::test_no_text_the_agent_reads_is_cut_off_by_the_client`, `tests/test_mcp_server.py::test_the_tool_surface_fits_the_budget`|
143
-
| Spending needs consent |`dw_mcp/diagnose.py`|`run_workflow` and `rerun_job` refuse until `acknowledged_cost` is set. |`tests/test_mcp_diagnose.py::test_run_refuses_without_an_acknowledged_cost`, `tests/test_mcp_diagnose.py::test_rerun_refuses_without_an_acknowledged_cost`|
146
+
| Spending needs consent | each handler behind a tool that takes `acknowledged_cost` (`dw_mcp/diagnose.py`, `models.py`, `prompts.py`, `workspaces.py`) | Every tool that takes `acknowledged_cost` refuses until it is set; `delete_workspace`'s refusal is the server's 409. |`tests/test_mcp_twins.py::test_every_tool_that_takes_acknowledged_cost_refuses_without_it`|
147
+
| Copies of engine rules | the constants and word lists in `dw_mcp/` and `dw/run.py`|`dw_mcp` cannot import its owners, so each copy equals its owner; a copy that needs no twin is deleted, not pinned. |`tests/test_mcp_twins.py`|
148
+
| The CLI is a client of the server |`dw/run.py`|`dw.run` queues on `dw.serve` through `dw_mcp.client`, so `dw` imports `dw_mcp` and never the reverse. |`tests/test_mcp_server.py::TestStartupWeight::test_the_server_starts_without_importing_the_engine`, `tests/test_mcp_twins.py`|
149
+
| Inline images |`dw/server/inline_media.py`| Fitting an image to a longest side and halving it under a base64 budget happens on the server, for `/image` and `/frames` alike; `dw_mcp` forwards the size and budget and imports no Pillow. |`tests/test_server_gallery_image.py`, `tests/test_mcp_server.py::TestStartupWeight::test_the_server_starts_without_importing_the_engine`|
150
+
| Workflow patch |`dw/library.py`: `merge_patch`; `dw/server/routes/library.py`: `patch_workflow`| A merge patch is applied on the server under the lock `PUT` takes; no client reads, merges and writes back. |`tests/test_server_library_path.py::TestPatchWorkflow::test_patch_merges_onto_the_stored_definition`|
151
+
| Deleting a job's run |`dw/server/jobs.py`: `JobManager.run_location`; `dw/server/outputs.py`: `delete_run_directory`| The server resolves a job's run directory against the root the job ran in, and refuses a job still queued or running. |`tests/test_server_jobs.py::TestDeleteAJobsRun::test_a_running_job_is_409`|
152
+
| Level findings |`dw/server/assess.py`: `level_findings`| Gallery metadata reports level problems from `dw/audio_qc.py`'s thresholds; no client restates a threshold. |`tests/test_server_assess.py::TestMetadataLevelFindings::test_level_findings_read_audio_qc_thresholds`|
144
153
| API errors |`dw_mcp/client.py`| An API failure becomes a message a person can act on, here and nowhere else. |`tests/test_mcp_client.py::test_a_400_surfaces_the_servers_detail_verbatim`|
145
154
146
155
## UI
147
156
148
157
| Concept | Owner | Rule | Enforced by |
149
158
| --- | --- | --- | --- |
150
159
| The UI reads engine fields |`ui/src/lib/plan.ts`: `describePlan`, `ui/src/lib/results.ts`: `sectionBySubfolder`| The UI reads `plan`, `version` and `subfolder` as fields the server sends and derives nothing of its own, and it never sends `acknowledged_cost`. |`ui/src/lib/plan.test.ts`, `ui/src/lib/results.test.ts`|
160
+
| Reference prefixes in the UI |`ui/src/lib/references.ts`| The only module in `ui/src` that spells a reference prefix; each equals `dw/references.py`'s. |`tests/test_ui_twins.py::test_the_ui_spells_every_reference_prefix_the_engine_does`; `prefix_literals` in `ui/scripts/arch-metrics.mjs`|
161
+
| Output kinds on the job page |`dw/server/outputs.py`: `MEDIA_KINDS`, `output_kinds`| The page renders an output by the `output_kinds` the job detail carries, never by its extension. |`tests/test_server.py::test_a_job_reports_each_output_files_media_kind`, `ui/src/lib/pages/JobPage.test.ts`|
162
+
| The editor's live reference check |`ui/src/lib/flow.ts`: `danglingReferenceDetails`, owned by `dw/previous_results.py` and `dw/for_each.py`| The editor warns as the author types, so it keeps a copy of the reference rules; one case file is run through both, the engine deciding each case. |`tests/test_ui_twins.py::test_the_engine_decides_each_shared_reference_case`, `ui/src/lib/flow.test.ts`|
163
+
| Name segments in the UI |`ui/src/lib/names.ts`: `isNameSegment`| Workspaces, folders and stored names follow `dw/security.py`'s `WORKSPACE_NAME_PATTERN` and length cap, counted in code points. |`tests/test_ui_twins.py::test_the_engine_decides_each_shared_workspace_name_case`, `ui/src/lib/names.test.ts`|
164
+
| UI copies of engine rules | the constants `tests/test_ui_twins.py` reads | A list the UI must hold equals its owner, read from the TS source; a rule tested on both sides reads one case file in `tests/fixtures/`. |`tests/test_ui_twins.py`|
165
+
| UI overlays and suggestions |`ui/src/lib/ui/`: `ConfirmDialog`, `Modal`, `Popover`, `Suggest`| Bits UI is imported only here; pages and components use the wrappers, which own focus, Escape, outside presses and scroll lock. Overlay styles and the stacking scale (`--layer-*`) live in `ui/src/app.css`. |`ui/eslint.config.js` (`no-restricted-imports`), `ui/src/lib/ui/*.test.ts`, `ui/e2e/chrome.spec.ts`|
166
+
| Escape on a page under an overlay |`ui/src/lib/ui/layers.svelte.ts`: `holdLayer`, `overlayOpen`| Every open overlay holds a layer; a page's own Escape handling acts only when none is open. |`ui/src/lib/pages/GalleryPage.test.ts` (an Escape that closes a confirm leaves the selection) |
167
+
| The editors' document |`ui/src/lib/editorShell.svelte.ts`: `DocumentEditor`| The workflow and prompt editors keep one rule each for the view (remembered per editor), the JSON draft (a failed parse pins it), dirty against the last load or save, the save path, and the tab-close guard. |`ui/src/lib/editorShell.test.ts`, `ui/src/lib/pages/PromptEditorPage.test.ts`, `ui/src/lib/pages/EditorPage.test.ts`|
168
+
| UI polling |`ui/src/lib/poll.ts`: `poll`, `sleep`| A page that refreshes on a timer starts it through `poll` and stops it with the function `poll` returns. |`ui/src/lib/poll.test.ts`|
169
+
| Workspace state in the UI |`ui/src/lib/workspaceState.svelte.ts`: `workspace`| The current workspace is one `$state`; `api.ts` reads it from here, so the request layer imports no page state. |`import_cycles` in `ui/scripts/arch-metrics.mjs`|
170
+
| The UI's response contract |`dw/server/api_models.py`: `ApiModel`, `send_rejected_responses`; `scripts/dump_openapi.py`| Every JSON route `api.ts` calls declares a response model, and `types.ts` re-exports the types generated from it (`ui/src/lib/generated/`). The payload does not change: an absent key stays absent (`response_model_exclude_unset`), a sometimes-sent key is `sometimes()`. Runtime is lenient - an undeclared key passes and a rejected response is logged and sent as built; tests, the e2e server and the dump run strict (`DW_STRICT_RESPONSES=1`). The document is generated under the FastAPI and Pydantic `constraints-openapi.txt` pins: CI installs them, and the dump refuses to write under others. |`tests/test_api_contract.py`, `tests/test_ui_twins.py::test_every_json_route_the_ui_calls_declares_its_response`, CI's "Response contract" step |
171
+
| Where `--live` appears |`ui/src/app.css` (its header) | The state colour marks machine state only, in the files `ui/scripts/design-rules.test.ts` lists. |`ui/scripts/design-rules.test.ts`|
172
+
| UI architecture ratchet |`ui/scripts/arch-metrics.mjs`| No metric rises above `docs/stabilization/ui/baseline.json`. | the `ui` job in `.github/workflows/ci.yml`; `npm run preflight`|
0 commit comments