Files
AR-VMS-Seaman/vmssailor/shared/ids.py
T
alro65 deb04c9315 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>
2026-05-17 07:26:06 -04:00

69 lines
2.4 KiB
Python

"""Generación de IDs deterministas para el modelo de datos.
Reglas:
- IDs de Tag son **deterministas**: `{equipment.tag_prefix}.{sensor.id_upper}`
para que un mismo proyecto produzca siempre el mismo grafo de tags.
- IDs aleatorios de proyecto/equipo usan UUID4 si el integrador no provee
uno.
- IDs de Alarm (instancia activa) son timestamps + tag_id en hash corto.
"""
from __future__ import annotations
import re
import uuid
_TAG_PREFIX_RE = re.compile(r"^[A-Z][A-Z0-9_]*$")
_SENSOR_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$")
def make_tag_id(equipment_tag_prefix: str, sensor_id: str) -> str:
"""Compone un Tag.id determinista: 'ME_PORT' + 'oil_press' → 'ME_PORT.OIL_PRESS'.
Reglas:
- `equipment_tag_prefix` debe ser MAYÚSCULAS y empezar con letra.
- `sensor_id` debe ser snake_case minúsculas.
- Resultado: prefix + "." + sensor.upper().
"""
if not _TAG_PREFIX_RE.match(equipment_tag_prefix):
raise ValueError(
f"equipment_tag_prefix '{equipment_tag_prefix}' debe ser MAYÚSCULAS "
"[A-Z][A-Z0-9_]*."
)
if not _SENSOR_ID_RE.match(sensor_id):
raise ValueError(
f"sensor_id '{sensor_id}' debe ser snake_case [a-z][a-z0-9_]*."
)
return f"{equipment_tag_prefix}.{sensor_id.upper()}"
def make_alarm_config_id(tag_id: str, level_name: str) -> str:
"""ID determinista para un AlarmConfig: 'ME_PORT.OIL_PRESS.LOW'."""
if not level_name.replace("_", "").isalnum():
raise ValueError(f"level_name '{level_name}' inválido.")
return f"{tag_id}.{level_name.upper()}"
def make_alarm_instance_id(tag_id: str, alarm_config_id: str, timestamp_unix: float) -> str:
"""ID determinista para una INSTANCIA activa de alarma.
Garantiza unicidad por timestamp y por config.
"""
short = hex(int(timestamp_unix * 1000))[2:][-10:]
return f"alm.{alarm_config_id}.{short}"
def new_uuid() -> str:
"""UUID4 para IDs de proyecto, equipos, cartas sin nombre semántico."""
return str(uuid.uuid4())
def make_project_id(customer_slug: str, vessel_id: str) -> str:
"""Compone un Project.id estable a partir de cliente + buque."""
customer = re.sub(r"[^a-z0-9]+", "_", customer_slug.lower()).strip("_")
vessel = re.sub(r"[^a-z0-9_-]+", "_", vessel_id.lower()).strip("_")
if not customer or not vessel:
raise ValueError("customer_slug y vessel_id no pueden quedar vacíos tras normalizar.")
return f"{customer}__{vessel}"