383 lines
24 KiB
Markdown
383 lines
24 KiB
Markdown
# 15 — Rebuild Adoption Plan
|
||
|
||
> **Status:** proposed. **Source of truth for intent:** the greenfield
|
||
> rebuild specs at `~/Sync/otko-development/specifications` (`00`–`14`).
|
||
> This document plans how to bring that rebuild's good ideas into **this**
|
||
> repo without breaking the working app.
|
||
>
|
||
> Requirement IDs are shown without brackets for brevity (e.g. `PKG-032` =
|
||
> `[PKG-032]` in the rebuild set) so they stay grep-able.
|
||
|
||
---
|
||
|
||
## 1. Context
|
||
|
||
| | This repo (`~/Sync/otko`) | Rebuild (`~/Sync/otko-development`) |
|
||
|---|---|---|
|
||
| Role | **Working app** — the primary deliverable | Greenfield spec-conformant attempt |
|
||
| VCS | Not a git repo in this checkout | `rebuild` branch, commit `725040e` |
|
||
| License | AGPL-3.0 | MIT (+ `NOTICE`) |
|
||
| Python | 3.10–3.12 | 3.12 only |
|
||
| Project file | `.osmodel` | `.otko` |
|
||
| Scope | Superset: static, modal, **transient, pushover, response-spectrum**, quad/shell-adjacent elements, HDF5 | Narrow v1: static + modal; reports, combinations, self-weight, units display |
|
||
| src LOC | ~37k | ~25k |
|
||
| Gate | deps not installed here; **57 collection errors** locally | `ruff` passes; `mypy`/`pytest` deps missing locally |
|
||
|
||
The rebuild is *ahead* on: **reporting/Typst, load combinations, self-weight,
|
||
display-unit conversion, closed-form diagrams, modal mass participation,
|
||
progress/cancel, local-axis editing, viewmodel separation, and quality
|
||
gates**. It is *behind* on everything this repo already ships. Therefore the
|
||
plan is a **selective harvest**, not a merge.
|
||
|
||
## 2. Goal and non-goals
|
||
|
||
**Goal.** Adopt the rebuild's good capabilities into this repo, incrementally,
|
||
each phase independently shippable, with the existing app and tests staying
|
||
green.
|
||
|
||
**Non-goals.**
|
||
- Do **not** rewrite this repo to the rebuild's architecture (`ProjectStore`,
|
||
frozen `Project`, `CommandFactory`, `services/scene.py`,
|
||
`services/diagram_data.py` substrates).
|
||
- Do **not** replace the runner/export/results surface — this repo's is a
|
||
strict superset.
|
||
- Do **not** change `.osmodel`, the AGPL license, or the Python 3.10–3.12
|
||
support range as part of this plan.
|
||
|
||
## 3. Principles
|
||
|
||
1. **Additive, adapt, don't copy.** Port algorithms/patterns; re-type to this
|
||
repo's APIs (`StaticResults`/`ModalResults`, mutable `Project`,
|
||
4-value `UnitSystem`, `element_forces.DiagramData`).
|
||
2. **Protect the working app.** Every phase keeps `pytest -m "not slow"` and
|
||
the GUI smoke tests green. New optional deps stay **lazy** so headless
|
||
installs are unaffected.
|
||
3. **One phase = one branch = one shippable PR**, with tests and a gate.
|
||
4. **Trace every item to a rebuild spec ID** so "is it done?" stays mechanical.
|
||
5. **License hygiene.** MIT→AGPL-3.0 is one-way compatible: preserve the MIT
|
||
copyright/permission notice for ported files (`NOTICE` + per-file header).
|
||
Never copy this repo's AGPL code back into the MIT rebuild.
|
||
6. **Establish version control first.** This checkout has **no `.git`**. Before
|
||
any work, create a repo/branch per `AGENTS.md` (`origin` is the self-hosted
|
||
Forgejo; work on `feat/<topic>`, PR against `main`).
|
||
|
||
## 4. Adoption matrix
|
||
|
||
Verdicts: **PORT** (near-verbatim), **ADAPT** (re-type/re-wire), **REIMPL**
|
||
(reimplement against this repo's APIs), **SKIP** (intentional divergence).
|
||
Size: **S** ≈ hours · **M** ≈ a few days · **L** ≈ 1–2+ weeks.
|
||
|
||
### 4.1 Quality / foundations
|
||
|
||
| # | Capability | Rebuild source | Verdict | Phase | Size | Spec IDs |
|
||
|---|---|---|---|---|---|---|
|
||
| Q1 | Single-source version + solver pin via `_const.py` | `src/otko/_const.py`, `pyproject.toml` `[tool.hatch.version]` | ADAPT | 0 | S | PKG-032/033, RUN-085 |
|
||
| Q2 | Layering/import-discipline AST tests | `tests/unit/test_architecture.py` (246) | ADAPT (paths) | 0 | M | ARC-001..003/006/011/060..064 |
|
||
| Q3 | CI split into 5 jobs incl. missing **`type` (mypy)** job; mark integration `slow` | `.github/workflows/ci.yml`, `tests/unit/test_ci_config.py`, `test_meta_gates.py` | ADAPT | 0 | M | TST-004/031/040/041 |
|
||
| Q4 | conftest hardening (offscreen default; `ops.wipe()` in teardown) | `tests/conftest.py` | ADAPT | 0 | S | TST §7 |
|
||
| Q5 | `tests/services/` split from `tests/unit/` | tests layout | ADAPT | 0 | M | TST-002 |
|
||
| Q6 | Standalone docs + docs test | `docs/QUICK_GUIDE.md`, `docs/README.md`, `tests/unit/test_docs.py` | ADAPT | 0 | M | — |
|
||
| Q7 | `NOTICE` + attribution for ported MIT code | `NOTICE` | REIMPL (AGPL wording) | 0 | S | PKG-042 |
|
||
| Q8 | Drop phantom `scipy`/`pandas` from `[gui]` | `pyproject.toml`, `tests/unit/test_packaging.py` | ADAPT | 0 | S | PKG-011 |
|
||
| Q9 | Scoped mypy overrides + coverage `fail_under` | `pyproject.toml` | ADAPT | 0 | S | TST-031/032 |
|
||
| Q10 | Packaging config tests subset (entrypoint, line length, single-source) | `tests/unit/test_packaging.py` (117) | ADAPT | 0 | S | PKG-030..033 |
|
||
| Q11 | Spec-ID traceability harness | `tests/unit/test_spec_traceability.py` (343) | REIMPL | 8 | L | TST-013/024/025 |
|
||
| Q12 | Performance probes | `tests/performance/*` | ADAPT | 8 | L | OVR-010..014 |
|
||
|
||
### 4.2 Services / engine
|
||
|
||
| # | Capability | Rebuild source | Verdict | Phase | Size | Spec IDs |
|
||
|---|---|---|---|---|---|---|
|
||
| S1 | `SolverSession` Protocol + `Real`/`Recording` sessions | `services/solver_session.py` (154) | PORT | 1 | S | RUN-003/010/011/012 |
|
||
| S2 | Persistence refuses to save an invalid model + resolved path | `services/persistence.py` (63) | ADAPT | 1 | S | PER-021/022 |
|
||
| S3 | Result fields: `StaticResults.time`, modal `ModeParticipation` record | `services/results.py` (138) | ADAPT | 1 | S | RES-001/003/005, ANL-012 |
|
||
| S4 | Component-less element forces = absent, not zeros | `runner/run.py` | ADAPT | 1 | S | RUN-053 |
|
||
| S5 | `progress(int)` + `cancelled` + `isInterruptionRequested()` + `cancel()`; real Cancel UX | `services/qt_workers.py`, `viewmodels/analysis_vm.py`, `views/dialogs/run.py` | ADAPT | 2 | M | ANL-031..036, ARC-032/033, UX-081 |
|
||
| S6 | Modal mass participation unified with response-spectrum path | `runner/modal.py` (466), `services/results.py` | ADAPT | 2 | M | RUN-062/070..072, ANL-013 |
|
||
| S7 | Display-unit layer + `GRAVITY` + `ProjectMeta.display` (adapter over 4-system enum) | `core/units.py` (298) | ADAPT | 3 | M | UNT-003/004/010/020..024/030..032 |
|
||
| S8 | Closed-form diagram math (`N/V/M/T` shapes + extrema) | `core/diagrams.py` (436) | PORT | 4 | S | RES-010/011/012 |
|
||
| S9 | Backend-neutral `diagram_data` service + `render_matplotlib` | `services/diagram_data.py` (400) | REIMPL | 4 | M | RES-020..024/030..032, CAN-084 |
|
||
| S10 | **Report pipeline** CSV / PNG(≥300 dpi) / SVG / Typst (+optional PDF) + case-report action | `services/report/*` (1141) | PORT + ADAPT deps | 5 | M | RES-040..047/050/051/061 |
|
||
| S11 | Local axis: `LocalAxis`, element field, geomTransf dedup by `(type,vecxz,roll)`, editor + triad | `core/geometry/local_axis.py`, `runner/emit.py` | ADAPT | 6 | M | GEO-060..064, DOM-040 |
|
||
| S12 | Load combinations entity + commands + runner materialisation | `core/combinations.py` (44), `commands/combinations.py` (146) | PORT + ADAPT | 7 | M | LOD-050..058, ANL-001, RUN-050 |
|
||
| S13 | Self-weight service + regenerate command | `services/self_weight.py` (364) | REIMPL | 7 | M | UNT-040..045, LOD-011/040..043 |
|
||
| S14 | Material `rho` coverage for self-weight | `core/materials` | ADAPT | 7 | S | UNT-041 |
|
||
|
||
### 4.3 UI / UX
|
||
|
||
| # | Capability | Rebuild source | Verdict | Phase | Size | Spec IDs |
|
||
|---|---|---|---|---|---|---|
|
||
| U1 | Pure formatting helpers (`DOF/MASS/RESTRAINT_LABELS`, number/float parse) | `views/formatting.py` (84) | PORT | 0 | S | UX-033/062 |
|
||
| U2 | Timestamped, severity-tagged console | `views/docks/console.py` (103) | PORT | 0 | S | UX-042 |
|
||
| U3 | `ValidatedDialog` base (help line, inline errors, OK-gating, unit suffix) | `views/dialogs/base.py` (191) | PORT + staged | 8 | M | UX-060..064 |
|
||
| U4 | `ResultsVM` per-case handle cache; `CanvasVM` selection ownership | `viewmodels/results_vm.py`, `canvas_vm.py` | REIMPL | 8 | M | RES-005, CAN-042/041 |
|
||
| U5 | Overlay helpers: ghost undeformed, unit-labelled diagram extremes, zero hint, pattern-filtered loads | `views/canvas/overlays.py` (684) | REIMPL | 8 | M | CAN-052/062/082/083, LOD-063 |
|
||
| U6 | `QSettings` layout persistence + `closeEvent` save prompt | `views/main_window.py` | PORT | 0 | S | UX-013, PER-032/033, ARC-050 |
|
||
| U7 | `ThemeManager` + dark icon set | `views/resources/theme.py`, `icons.py` | ADAPT (verify icon provenance) | 8 | S | UX-070/071/072 |
|
||
| U8 | Thicken `ProjectViewModel` (move `action_handlers` logic into VM methods) | `viewmodels/project_vm.py` (713) | REIMPL | 8 | L | UX-001/002/005, ARC-005/020..022 |
|
||
| U9 | Analysis VM status + Simple/Advanced control fields | `viewmodels/analysis_vm.py` | ADAPT | 8 | M | ANL-021/050..054, UX-037 |
|
||
|
||
### 4.4 Explicitly deferred / skipped
|
||
|
||
| Item | Verdict | Why |
|
||
|---|---|---|
|
||
| Polygon-first sections + `services/sections_mesh.py` (opstool GPLv3) | DEFER (Phase 9, conditional) | Schema-breaking redesign; adds GPLv3/runtime deps; not report-critical |
|
||
| Frozen `Project`/entities + `ProjectStore`/`CommandFactory` rewrite | DEFER (not recommended now) | Large coordinated rewrite of 43 commands + all call sites; low user-visible value vs risk |
|
||
| Canvas backend Protocol + `plotly_backend.py` + `services/scene.py` | DEFER (Phase 9) | Requires core + views surgery; plotly pulls QtWebEngine (known teardown SIGSEGV) |
|
||
| `.otko` file suffix / greenfield `Project` schema (PER-001/006) | SKIP | Breaks `.osmodel` corpus and compatibility; do only as an explicit user-approved migration |
|
||
| Rebuild `runner/export.py` | SKIP | This repo's `export.py` is a superset (5 case types, `.py`+`.tcl`) |
|
||
| Rebuild static/modal HDF5 | SKIP | Rebuild has none; this repo's transient HDF5 is already better |
|
||
| Rebuild slim `materials`/`analysis`/`loads` unions | SKIP | Dropping `HystereticSM`, Transient/Pushover/ResponseSpectrum, Path/Imposed patterns would regress shipped features |
|
||
| MIT license / py3.12-only / single-OS CI assertions | SKIP | Intentional differences (AGPL, 3.10–3.12, 3-OS matrix) |
|
||
|
||
## 5. Phases
|
||
|
||
### Phase 0 — Foundations, safety net, quick wins
|
||
**Why first:** makes later phases verifiable and cheap; all items are
|
||
additive and low risk. Also unblocks the mpy gate that currently does nothing.
|
||
|
||
- **Prep:** initialise git and a `feat/rebuild-adoption` branch (§3.6).
|
||
- **Q1** Create `src/otko/_const.py` (`__version__`, `OPENSEESPY_VERSION`),
|
||
switch `pyproject.toml` to `dynamic = ["version"]` + `[tool.hatch.version]`,
|
||
and import the pin in `services/export.py` (currently duplicated at
|
||
`export.py:52`). Spec: PKG-032/033, RUN-085.
|
||
- **Q2** Port `tests/unit/test_architecture.py`; adapt `CANVAS = views/canvas3d`
|
||
path and `services/{_emit,_run}.py` paths. Expect it to flag three existing
|
||
offenders — decide per item: `OSS_PICK_DEBUG` env var in
|
||
`views/canvas3d/model_canvas.py:29` (ARC-052), `QUndoStack` outside VMs, and
|
||
the canvas path. Either fix or add a documented, narrow allowlist.
|
||
- **Q3** Split CI: `lint` (+`ruff format --check`), **`type` (mypy scopes)**,
|
||
`test-headless` (`pytest tests/unit tests/services -m "not slow"`),
|
||
`test-gui` (`xvfb-run pytest tests/gui`), `test-integration`
|
||
(`pytest tests/integration -m slow`). Add `pytestmark = pytest.mark.slow` to
|
||
integration modules. Keep the existing 3-OS × 3.10–3.12 matrix and the long
|
||
Linux Qt apt list.
|
||
- **Q4** `tests/conftest.py`: `QT_QPA_PLATFORM=offscreen` default; move
|
||
`ops.wipe()` to teardown (this repo currently wipes before each test).
|
||
- **Q5** Move service-level tests (`runner_translation`, `persistence`,
|
||
`results`, `export`, …) into `tests/services/`; update CI and `AGENTS.md`.
|
||
- **Q6** Add `docs/QUICK_GUIDE.md` + `docs/README.md` (rewrite `.otko` →
|
||
`.osmodel`, rebuild-only APIs → this repo's), update `CONTRIBUTING.md` with
|
||
the 5-layer table + install/run/test commands; port `test_docs.py`.
|
||
- **Q7** Add `NOTICE` crediting MIT `otko-development` for ported files;
|
||
add a per-file header to ported files (`# Ported from otko-development
|
||
(MIT), (c) 2026 OTKO contributors`).
|
||
- **Q8/Q9/Q10** `pyproject.toml`: drop `scipy`/`pandas` from `[gui]`; convert
|
||
mypy to scoped `strict` overrides (core/services/viewmodels); add
|
||
`[tool.coverage.run] fail_under`; port the `test_packaging.py` subset that
|
||
applies (entrypoint, version single-source, line length).
|
||
- **U1/U2/U6** Port `views/formatting.py`; timestamp the console
|
||
(`views/docks/console.py` into the existing dock); add `save_layout`/
|
||
`restore_layout` (`QSettings`) and a `closeEvent` unsaved-changes prompt.
|
||
|
||
**Gate:** `ruff check src tests`, `ruff format --check src tests`,
|
||
`mypy src/otko/core src/otko/services src/otko/viewmodels`,
|
||
`pytest -m "not slow"`, arch test, and the new packaging/docs tests pass.
|
||
|
||
### Phase 1 — Cheap, safe services wins
|
||
- **S1** Port `services/solver_session.py` (`SolverSession`, `RealSolverSession`,
|
||
`RecordingSolverSession`). Inject into `OpenSeesRunner` while keeping the
|
||
existing `ops_module=` shim; consolidate the two wipe paths
|
||
(`_emit.py:105` → `session.reset()`). Tests:
|
||
`tests/services/test_solver_session.py`.
|
||
- **S2** In `services/persistence.py::save_project`, call
|
||
`validate_references()` first and refuse to write an invalid model,
|
||
reporting the problem list (PER-022); return the resolved path (PER-021).
|
||
- **S3** Extend `StaticResults` with `time` and add a `ModeParticipation`
|
||
record to `ModalResults` (keep the existing dataclass names — consumers in
|
||
`views/docks/results_panel.py` dispatch on them).
|
||
- **S4** `RUN-053`: return an absent (not zero-filled) force entry when a
|
||
component is missing; guard the static `eleForce` fallback.
|
||
|
||
**Gate:** new service tests + existing runner/persistence tests green; runner
|
||
integration suite unchanged.
|
||
|
||
### Phase 2 — Progress & cancellation
|
||
- **S5** Add to `services/qt_workers.py`: `progress(int)`, `cancelled`,
|
||
an `_interruption_requested()` check between steps, and a bounded teardown
|
||
wait. Add `AnalysisRunner.cancel()` (calls `QThread.requestInterruption()`).
|
||
Wire progress callbacks through static (`RUN-054`) and modal (`RUN-063`)
|
||
runs. Replace the indeterminate bar in `views/dialogs/run_analysis.py` with
|
||
a real progress bar + working Cancel (UX-081). Add per-case status to the
|
||
case manager (ANL-050).
|
||
- **S6** Unify modal mass participation: put the rebuild's participation
|
||
math into one service and have **both** modal results and the existing
|
||
response-spectrum path (`services/spectrum.py`) consume it. Add the ANL-013
|
||
no-mass warning.
|
||
|
||
**Gate:** cancel leaves no partial handle; progress reaches 100; participation
|
||
sums to 1.0 against a hand-checked example.
|
||
|
||
### Phase 3 — Units display layer + GRAVITY
|
||
- **S7** Port the display machinery from `core/units.py` **as an adapter**:
|
||
keep this repo's 4-value `UnitSystem` and map families
|
||
(`SI_M_N|SI_MM_N → METRIC`, `US_FT_KIP|US_IN_KIP → IMPERIAL`); add
|
||
`GRAVITY`, `display(value, system, quantity, pref)`, `DisplayPrefs`; add
|
||
`ProjectMeta.display` (persisted). Retarget the status-bar/menu unit picker
|
||
so it writes **display prefs**, not the stored `meta.units` (fixes UNT-031).
|
||
- Watch-outs: rebuild uses `StrEnum` (3.11+) — do **not** adopt it (this repo
|
||
targets 3.10). `export.py` currently embeds `meta.units.value`; keep that
|
||
but source labels via the new API.
|
||
|
||
**Gate:** switching display units changes every label and leaves
|
||
`project.model_dump()` byte-identical; old 4-system `.osmodel` files still load.
|
||
|
||
### Phase 4 — Closed-form diagrams
|
||
- **S8** Port `core/diagrams.py` near-verbatim (pure numpy, self-contained).
|
||
- **S9** Build a new `services/diagram_data.py` (REIMPL) on top of this repo's
|
||
`StaticResults` + load patterns, reusing `core/diagrams` for interior
|
||
shapes/extrema. Run it **alongside** `services/element_forces.py`, migrate
|
||
`views/canvas3d/diagram_renderer.py` and `views/dock_manager.py` behind a
|
||
flag, then retire the old extractor once the on-screen output matches.
|
||
|
||
**Gate:** port `test_diagram_data.py` (end-values, Vz/My plane, axial,
|
||
all-six components, shared data, auto-scale); on-screen diagrams unchanged.
|
||
|
||
### Phase 5 — Reporting pipeline ★ (the headline v1 capability)
|
||
- **S10** Port `services/report/{__init__,csv,figures,typst,pdf}.py`. Add a
|
||
`[reports]` extra (`matplotlib`, `imageio-ffmpeg`; Typst CLI optional, probed
|
||
via `shutil.which`, graceful degradation per RES-046). Expose a "case report"
|
||
action that produces the full set: CSV tables, PNG (≥300 dpi), SVG, a Typst
|
||
document (PDF optional), conditional static/modal sections.
|
||
- Dependencies: **Phase 3** (units) and **Phase 4** (diagrams) must land first;
|
||
reuse **Phase 1** result fields.
|
||
|
||
**Gate:** the reference test — cantilever moment **50 kN·m at the fixed end**
|
||
(RES-061) — plus report generation off the GUI thread and preservation of the
|
||
original model file. Port `test_report_{csv,figures,typst,modal}.py` and the
|
||
`test_full_report.py` / `test_report_reference.py` integration tests.
|
||
|
||
### Phase 6 — Local axis (user-editable)
|
||
- **S11** Port `core/geometry/local_axis.py`; add
|
||
`local_axis: LocalAxis` to the frame elements (`ElasticBeamColumn`,
|
||
`ForceBeamColumn`, `DispBeamColumn`, `BeamWithHinges`). Change
|
||
`services/_emit.py` geomTransf to read the explicit axis and dedup by
|
||
`(type, vecxz, roll_deg)` instead of auto-deriving. Update the renderer
|
||
triad (its current `getattr(el, "vecxz")` at `model_renderer.py:727` is dead
|
||
code) and add an editor (property dock / dialog).
|
||
- **Critical migration risk:** a blind default `vecxz=(0,0,1)` is parallel to
|
||
a vertical member's axis and would break existing vertical models. Use a
|
||
**safe default rule** (e.g. derive the default from geometry exactly as the
|
||
runner does today, then only override when the user sets it) so existing
|
||
`.osmodel` files keep loading and producing identical results.
|
||
|
||
**Gate:** existing frame/eigen integration tests unchanged; new
|
||
`test_local_axis_parallel_rejected` + `test_geom_transf_dedup_by_combination`.
|
||
|
||
### Phase 7 — Load combinations + self-weight
|
||
- **S12** Port `core/combinations.py` and `commands/combinations.py`; add
|
||
`Project.combinations`; extend `validate_references`; add
|
||
`StaticCase.combination_id` with the XOR invariant (combination **or**
|
||
patterns+factors, ANL-001); materialise scaled loads in the runner
|
||
(LOD-054) without regressing the existing per-pattern factor path.
|
||
- **S13** Reimplement `services/self_weight.py` against this repo's types:
|
||
`w = ρ·A·g` distributed local for frames, equivalent nodal loads for trusses
|
||
(UNT-042/043), a `ConstantTimeSeries` + `PlainLoadPattern`, regeneration that
|
||
replaces the prior pattern (LOD-041), no-density/unresolved reporting
|
||
(LOD-042), and the gravity constant recorded in the description (UNT-045).
|
||
Add a `RegenerateSelfWeightCommand` (undoable).
|
||
- **S14** `ElasticIsotropic` currently is the only material with `rho`
|
||
(`materials/__init__.py:28`); add `rho` to the other self-weight-capable
|
||
materials, or report them as missing density.
|
||
- Depends on **Phase 3** (`GRAVITY`) and **Phase 6** (correct local projection).
|
||
|
||
**Gate:** port `test_loads_combinations.py` and `test_self_weight.py` (minus
|
||
the combination assertion until S12 lands); verify superposition matches a
|
||
hand calculation.
|
||
|
||
### Phase 8 — UI/UX adoption (larger, optional but valuable)
|
||
- **U3** `ValidatedDialog` base + migrate dialogs incrementally (unit suffix
|
||
via the Phase 3 API).
|
||
- **U4** Extract `ResultsVM` (per-case handle cache) and `CanvasVM` (selection
|
||
ownership) as supersets of the current state holders so existing call sites
|
||
keep working.
|
||
- **U5** Reimplement overlay **pure helpers** against this repo's
|
||
`deformation`/`element_forces`: ghost undeformed (CAN-062), unit-labelled
|
||
diagram extremes (CAN-082), zero-component hint (CAN-083), and
|
||
pattern-filtered load display (CAN-052/LOD-063). Unit-test them display-free.
|
||
- **U7** Adopt `ThemeManager` + a dark icon set — **verify SVG provenance and
|
||
license first** before copying any icon assets.
|
||
- **U8/U9** Thicken `ProjectViewModel` (move `action_handlers.py` logic into VM
|
||
methods, one command family at a time) and add analysis-case status /
|
||
Simple-vs-Advanced controls.
|
||
- **Q11/Q12** Spec-ID traceability harness and performance probes (only if the
|
||
team commits to the `[AREA-NNN]` convention and a perf budget).
|
||
|
||
**Gate:** GUI tests under `xvfb-run`; VM logic unit-tested without a display.
|
||
|
||
### Phase 9 — Deferred, only on explicit decision
|
||
- Polygon-first sections + `sections_mesh` (opstool **GPLv3**, lazy import,
|
||
optional `[sections]` extra; ensure AGPL compatibility and attribution).
|
||
- Frozen `Project`/`ProjectStore`/`CommandFactory` migration.
|
||
- Canvas backend Protocol + plotly backend + `services/scene.py`.
|
||
|
||
Revisit only after Phases 0–8 are stable and if the value justifies the blast
|
||
radius documented in the dossiers.
|
||
|
||
## 6. Dependency graph
|
||
|
||
```
|
||
Phase 0 (foundations)
|
||
│
|
||
├─► Phase 1 (sessions, persistence, result fields)
|
||
│ │
|
||
│ ├─► Phase 2 (progress/cancel, mass participation)
|
||
│ │
|
||
│ └─► Phase 3 (units display + GRAVITY) ──┐
|
||
│ │
|
||
│ Phase 4 (diagrams) ────────────────┤
|
||
│ ▼
|
||
│ Phase 5 (REPORTING) ★
|
||
│
|
||
├─► Phase 6 (local axis) ──┐
|
||
│ ▼
|
||
└─► Phase 7 (combinations + self-weight)
|
||
│
|
||
▼
|
||
Phase 8 (UI/UX) ──► Phase 9 (deferred)
|
||
```
|
||
|
||
**Critical path to the v1 promise (a printable case report):**
|
||
`Phase 3 → Phase 4 → Phase 5`, with `Phase 1` as a cheap prerequisite and
|
||
`Phase 2` interleavable.
|
||
|
||
## 7. Licensing & attribution
|
||
|
||
- This repo is **AGPL-3.0**; the rebuild is **MIT**. MIT code may be
|
||
incorporated into an AGPL work — **one-way**. Preserve the MIT notice:
|
||
add a `NOTICE` (Phase 0) and a short provenance header to each ported file.
|
||
- **Never** copy this repo's AGPL code back into the MIT rebuild.
|
||
- `opstool` is **GPLv3** (only relevant to deferred `sections_mesh`); GPLv3 ↔
|
||
AGPLv3 are compatible. Keep it lazy/optional and do not vendor its source.
|
||
- Verify the provenance/license of any copied **icon/SVG assets** before
|
||
adopting the dark icon set (U7).
|
||
|
||
## 8. Risks and mitigations
|
||
|
||
| Risk | Mitigation |
|
||
|---|---|
|
||
| Big-bang port destabilises the working app | Strict additive phases; feature flags for diagram/report migration; keep old paths until parity is proven |
|
||
| Freezing/command rewrite stalls progress | Explicitly deferred (Phase 9); combinations/self-weight do **not** require it |
|
||
| Units enum change corrupts stored models | Keep 4-value enum; add a family mapping + `DisplayPrefs`; byte-identical dump test |
|
||
| Local-axis default breaks vertical members | Derive default per-element as today; override only on explicit user set; regression tests on vertical frames |
|
||
| opstool private-API coupling / GPL | Deferred; pin version + thin adapter; lazy import; attribution |
|
||
| Report pipeline pulls GUI deps into headless | Keep `matplotlib`/Typst optional and imported lazily; headless CI must pass without `[reports]` |
|
||
| `StrEnum` / 3.11-only syntax | Target py3.10: avoid `StrEnum`, `zip(strict=)`, etc. |
|
||
| No VCS in this checkout | Initialise git + branch before any code changes |
|
||
| Tests can't run locally (deps missing) | Phase 0 fixes env expectations; run gates in CI |
|
||
|
||
## 9. Tracking
|
||
|
||
Adopt the rebuild's `.otko-build/tasks.json` **pattern** (schema, gates,
|
||
`{id, phase, layer, specs, title, tests, status, deps}`), seeded from this
|
||
plan. Each task names the phase, the spec IDs it implements, and its tests
|
||
(the rebuild's `tasks.json` is a working reference). Keep build state out of
|
||
versioned source (`.gitignore` it) or commit it deliberately — team choice.
|
||
|
||
## 10. Recommended first step
|
||
|
||
Execute **Phase 0** in one branch. It is almost entirely additive, it turns
|
||
the currently-inert mypy/CI gate into a real one, and it establishes the
|
||
attribution and docs baseline the later ports depend on. Then proceed
|
||
`1 → 2 → 3 → 4 → 5`, pulling Phase 6/7 forward only if self-weight is needed
|
||
before reporting.
|