sprint-0: fundaciones VMS-Sailor

Sprint 0 completo del producto VMS-Sailor (Vessel Management System
integrado para buques 30-40m). Brief de referencia en
VMS_Sailor_v2_Parte_*.md (intacto).

Core (vmssailor.core, 95.17% coverage, 99 tests verde):
- ShipCoord: sistema naval x_pp/y_cl/z_bl frozen
- Vessel, Deck, Bulkhead
- Equipment, EquipmentModel, Sensor, EquipmentSpec
- Tag, AlarmConfig, TagBinding, Scaling
- CardInstance, Bus, Topology con validacion 21 puntos I/O AR-NMEA-IO-v1.0
- Alarm, PermissiveRule, Condition
- Project agregado raiz con validacion cross-entity
- Persistencia portable .vmsproj (SQLite) con roundtrip verificable

Biblioteca curada seed (vmssailor.library):
- systems_catalog.json completo (catalogo maestro Parte 1 sec 7)
- 2 vessels: Sunseeker 76, Ferretti 850
- 2 motores: MTU 12V 2000 M96, Volvo D13-900
- 1 genset: Northern Lights M65C13
- yacht_motor_planeo.yaml (reglas heuristicas)
- TODO marcado data_source=seed_estimate - requiere validacion datasheets

Tools:
- vms-validate-library: CLI valida biblioteca completa
- vms-generate-test-project: CLI demo + verificacion roundtrip persistencia

Design System + 8 mockups HTML estaticos:
- docs/design_system.md (paleta Deep Ocean, gradientes, typography, motion)
- docs/brand/ (logo + variantes SVG)
- docs/mockups/splash, studio_main, runtime_overview,
  runtime_mimic_fuel (P&ID animado), runtime_alarms, runtime_trim (panel
  estrella con horizonte artificial), mobile_overview, mobile_trim
- docs/mockups/index.html (galeria)

Firmware (Sprint 12+ implementacion):
- firmware/ar_nmea_io_v1/src/config/pinout.h con macros GPIO

Decisiones autonomas documentadas en docs/decisions_sprint0.md.

Stack: Python 3.11 + uv + Pydantic v2 + SQLite stdlib + hatchling +
pytest 9 + ruff + mypy. Sin PySide6, FastAPI, Flutter ni firmware
funcional (entran en sprints siguientes).

Criterio de aceptacion Sprint 0: cumplido.
- uv sync: OK
- pytest: 99/99 verde
- cov vmssailor.core: 95.17% (objetivo >=80%)
- ruff: clean
- vms-validate-library: OK
- vms-generate-test-project: INTEGRIDAD OK

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 07:26:06 -04:00
commit deb04c9315
96 changed files with 15335 additions and 0 deletions
View File
+39
View File
@@ -0,0 +1,39 @@
"""Tests del library loader."""
from __future__ import annotations
import json
from pathlib import Path
from vmssailor.library.loader import load_library
def test_loader_returns_result_with_format() -> None:
r = load_library()
s = r.format()
assert "Library load summary" in s
assert "Vessels" in s
assert "EquipmentModels" in s
def test_loader_handles_corrupt_json(tmp_path: Path) -> None:
# Construimos una biblioteca falsa con un JSON malo
fake_lib = tmp_path / "lib"
(fake_lib / "vessels").mkdir(parents=True)
(fake_lib / "vessels" / "broken.json").write_text("{ not valid json", encoding="utf-8")
(fake_lib / "equipment").mkdir(parents=True)
(fake_lib / "rules").mkdir(parents=True)
# systems_catalog.json mínimo válido
(fake_lib / "systems_catalog.json").write_text(
json.dumps({"_meta": {"version": 1}, "categories": []}), encoding="utf-8"
)
r = load_library(fake_lib)
assert not r.ok()
assert any("broken.json" in i.path for i in r.errors)
def test_loader_handles_missing_systems_catalog(tmp_path: Path) -> None:
fake_lib = tmp_path / "lib"
fake_lib.mkdir()
r = load_library(fake_lib)
assert any("systems_catalog.json" in i.path for i in r.errors)
+80
View File
@@ -0,0 +1,80 @@
"""Tests de integridad de la biblioteca seed."""
from __future__ import annotations
from vmssailor.core.enums import EquipmentCategory, SystemId
from vmssailor.library import load_library, load_systems_catalog
def test_systems_catalog_loads() -> None:
cat = load_systems_catalog()
assert "categories" in cat
assert len(cat["categories"]) >= 10 # 11 categorías mínimo
def test_systems_catalog_covers_all_enum_values() -> None:
cat = load_systems_catalog()
json_ids = set()
for c in cat["categories"]:
for s in c["systems"]:
json_ids.add(s["id"])
enum_ids = {s.value for s in SystemId}
missing_in_json = enum_ids - json_ids
missing_in_enum = json_ids - enum_ids
assert not missing_in_json, f"SystemId enum tiene valores no en catálogo: {missing_in_json}"
assert not missing_in_enum, f"Catálogo tiene IDs no en SystemId enum: {missing_in_enum}"
def test_library_loads_without_errors() -> None:
result = load_library()
assert result.ok(), f"Biblioteca con errores: {result.errors}"
def test_library_has_minimum_seed() -> None:
result = load_library()
vessel_ids = {v.id for v in result.vessels}
assert "sunseeker_76" in vessel_ids
assert "ferretti_850" in vessel_ids
eq_ids = {e.id for e in result.equipment_models}
assert "mtu_12v_2000_m96" in eq_ids
assert "volvo_d13_900hp" in eq_ids
assert "northern_lights_m65c13" in eq_ids
def test_seed_engines_are_engine_main() -> None:
result = load_library()
for em in result.equipment_models:
if em.id.startswith("mtu_") or em.id.startswith("volvo_"):
assert em.category == EquipmentCategory.ENGINE_MAIN, em.id
def test_seed_genset_category() -> None:
result = load_library()
nl = next(e for e in result.equipment_models if e.id == "northern_lights_m65c13")
assert nl.category == EquipmentCategory.GENSET
def test_rules_yacht_motor_planeo_present() -> None:
result = load_library()
assert "yacht_motor_planeo" in result.rules
def test_rules_reference_existing_equipment_models() -> None:
result = load_library()
# Filtrar warnings que no son del cross-ref
cross_ref_warns = [
w
for w in result.warnings
if "no existe en equipment_models" in w.message
]
assert not cross_ref_warns, f"Rules con model_ref roto: {cross_ref_warns}"
def test_seed_marked_as_estimate() -> None:
"""Todos los archivos seed deben venir marcados como seed_estimate."""
result = load_library()
for v in result.vessels:
assert v.data_source == "seed_estimate", v.id
for em in result.equipment_models:
assert em.data_source == "seed_estimate", em.id