otko/specifications/15-rebuild-adoption-plan.md

24 KiB
Raw Blame History

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.

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.