ADR-0020 — Belief Self-Assessment v1¶
Status¶
Accepted (2026-06-07).
Context¶
Todos los ADRs de análisis (0016 / 0017 / 0018 / 0019) producen artefactos que el operador lee para entender qué pasó con la creencia del agente. Pero ninguno responde a la pregunta más directa de la misión:
¿Qué afirma el agente que sabe — y no sabe — en tiempo de ejecución?
Hasta ADR-0019 el sistema mide errores y covarianzas, los audita
post-hoc, los compara entre runs. En ningún momento el agente
emite una afirmación estructurada sobre el estado de su propio
conocimiento. La covarianza 15x15 que el estimador adjunta es una
matriz: información cruda, no una claim interpretable.
ADR-0020 corrige ese gap añadiendo una capa de introspección runtime:
para cada VehicleState que la creencia produzca, el agente puede
publicar — vía un componente puro y determinista — un
BeliefSelfAssessment que clasifica por-eje y por-bloque qué
afirma saber, con qué calidad, frente a qué umbrales explícitos
escogidos por el operador.
La clasificación es discreta y descriptiva:
- KNOWN: per-axis std declarado ≤ umbral "known".
- UNCERTAIN: per-axis std entre umbrales "known" y "unknown".
- UNKNOWN: per-axis std ≥ umbral "unknown", o covarianza ausente.
No hay inferencia probabilística. No hay scoring. No hay verdict de "calibrado". El agente emite una clasificación rule-based contra umbrales que el operador elige y que viajan con el assessment para que toda lectura sea verificable y reproducible.
Este es el primer ADR que añade un productor runtime desde la línea de misión (vs. los ADRs previos que añaden consumidores offline).
Decision¶
Tres componentes nuevos, todos disciplinados al posture observacional del proyecto:
1. Núcleo runtime: core.uncertainty.self_assessment¶
SelfAssessmentLevel(StrEnum, cerrado):"known","uncertain","unknown".AssessmentThresholds(frozen dataclass): los seis umbrales que el caller declara ({position,velocity,orientation} × {known,unknown}_std), validados (> 0,known < unknown).BeliefSelfAssessment(frozen dataclass): por-eje std declarado, por-eje level, por-bloque overall level, overall_level global, hash SHA-256 de los thresholds usados, copia del propioAssessmentThresholdspara auto-contención auditable.assess_belief(state, thresholds) -> BeliefSelfAssessment: función pura. Cero clock, cero random, cero I/O. Sistate.nav.covariance_15x15esNone, todos los stds sonNoney todos los levels sonUNKNOWN— el agente reconoce abiertamente que su belief carece de covarianza.
2. Plumbing telemetría¶
CHANNEL_SELF_ASSESSMENT = "/self_assessment"entelemetry.channels.SelfAssessmentToTelemetryAdapterentelemetry.adapters— mismo patrón queModeEventToTelemetryAdapter. Publica cada assessment usandobelief_stamp_sim_nscomo log_time (ADR-0002, sin reloj de pared).- Decoder registrado en el catálogo cerrado de
telemetry.replay. Replay produce instanciasBeliefSelfAssessmentreconstruidas con re-validación de invariantes.
3. Análisis offline: analysis.self_assessment¶
SelfAssessmentSummary(frozen dataclass): conteo de records por level (KNOWN/UNCERTAIN/UNKNOWN) en cada bloque (position/velocity/ orientation) y overall. Timestamp first/last/span. Cobertura temporal por level (qué fracción del run estuvo en cada level).summarize_self_assessments(records) -> SelfAssessmentSummary: función pura.read_self_assessments_from_mcap(path) -> tuple[BeliefSelfAssessment, ...]: lectura determinista de un MCAP capturado.- CLI:
ghost analyze-self-assessment --mcap PATH [--output PATH]. Lee/self_assessmentdel MCAP, decodea y emite el summary JSON canónico.
Constantes versionadas¶
SELF_ASSESSMENT_PROTOCOL_VERSION: int = 1SELF_ASSESSMENT_SUMMARY_SCHEMA_VERSION: str = "1"
Lógica de clasificación (frozen)¶
Para cada eje cartesiano de cada bloque (position X/Y/Z, velocity X/Y/Z, orientation tangent X/Y/Z):
- Si
covariance_15x15 is Noneo el std calculado no es finito → level =UNKNOWN, std =None. - Else
std_axis = sqrt(covariance[i, i])para el índice de bloque correspondiente. - Si
std_axis ≤ threshold_known→KNOWN. - Elif
std_axis ≥ threshold_unknown→UNKNOWN. - Else →
UNCERTAIN.
Para block_overall_level (e.g. position_overall_level):
max(en el ordenamiento KNOWN(0) < UNCERTAIN(1) < UNKNOWN(2)) sobre los tres axes del bloque. El bloque hereda el peor nivel de sus axes.
Para overall_level global:
maxsobreposition_overall_level,velocity_overall_level,orientation_overall_level. Si cualquier bloque es UNKNOWN, el agente declara UNKNOWN en su conocimiento total.
Block-to-covariance-index mapping¶
covariance_15x15 tiene estructura por-bloque (cf. NavigationState
en ADR-0005):
[0:3]posición ENU.[3:6]velocidad world.[6:9]orientación tangent (axis-angle).[9:12]accel bias.[12:15]gyro bias.
V1 audita los tres primeros bloques (los que tienen umbrales explícitos
en AssessmentThresholds). Accel/gyro biases NO se evalúan en V1 — su
exposición como dimensión separada queda diferida. Esto está documentado
en el ADR y reflejado en las pruebas.
Self-contención auditable¶
Cada BeliefSelfAssessment contiene:
- Los stds por-axis (los números crudos que se usaron).
- Los levels por-axis y por-bloque (la clasificación que produjo).
thresholds_used: AssessmentThresholds(los umbrales completos inline).thresholds_sha256: str(hash content-addressed de laAssessmentThresholdscanónicamente serializada).
Un auditor que tenga sólo el assessment puede:
- Verificar bit-a-bit la clasificación re-aplicando las reglas.
- Verificar que
thresholds_sha256matchea SHA-256 del JSON canónico dethresholds_used— provenance content-addressed.
Inputs¶
- Runtime: un
VehicleState(del estimador o del aggregator) + unAssessmentThresholds(escogido por el operador del experimento). - Análisis: un MCAP que contenga records en el canal
/self_assessment.
Outputs¶
BeliefSelfAssessmentpor cadaVehicleStateque el caller asese.- Publicación opcional en
/self_assessmentvía adapter. SelfAssessmentSummarypost-hoc por run.
Limits¶
- No retrocompatibilidad ascendente del enum. Si un futuro ADR añade
niveles intermedios (e.g. "low_confidence"), bump
SELF_ASSESSMENT_PROTOCOL_VERSIONy crea decodificador específico. - Per-axis std es marginal, no principal. Para covarianzas con correlaciones cruzadas fuertes la per-axis std subestima la incertidumbre direccional. V1 acepta este trade-off por simplicidad y por alineación con thresholds per-axis del operador. Una extensión futura puede agregar análisis de eigenvalores.
- Bloques biases (accel, gyro) NO se evalúan en V1. Su umbral no
está en
AssessmentThresholds. Documentado y testeable. - Tie-breaking: KNOWN vs UNCERTAIN frontera (
std == known_threshold) resuelve hacia KNOWN. UNCERTAIN vs UNKNOWN frontera (std == unknown_threshold) resuelve hacia UNKNOWN. Estricta documentación de la convención. - Health/perception mode NO se integran en V1. Sensor health
degradado no fuerza UNKNOWN automáticamente. El operador debe
combinar con
/perception/modepor separado si quiere ese efecto. - Stale data NO se penaliza en V1. Si un belief se publica con timestamp viejo, el assessment sólo refleja covarianza, no edad. Un ADR futuro puede agregar staleness explícitamente.
Determinism¶
assess_beliefes función pura. Mismo(state, thresholds)→ mismoBeliefSelfAssessmentbyte-a-byte tras serialización.SelfAssessmentToTelemetryAdapter.publishno lee reloj de pared.- MCAP capture + replay →
BeliefSelfAssessmentreconstruido = original. summarize_self_assessmentses función pura.- SHA-256 de los thresholds usados es estable cross-CPython.
Exclusiones explícitas¶
- No ML / clustering / scoring. Sólo reglas determinísticas contra umbrales explícitos.
- No "el agente está bien calibrado". El assessment no habla de calibración; ADR-0019 sí lo hace, y son artefactos disjuntos.
- No detección de cambio de régimen. El assessment es por-record
estático. Cualquier comparación temporal vive en
analysis.*o en el operador. - No control / actuación. El assessment es un signal observacional. Nada en V1 lo conecta a comandos del actuador.
- No agregación tipo NEES/NIS. Mismo razonamiento que ADR-0019.
- No "uncertainty budget" o "remaining confidence". Solo levels discretos.
Determinism guarantees verificables¶
| # | Invariante | Verificación |
|---|---|---|
| 1 | assess_belief(s, t) == assess_belief(s, t) byte-a-byte tras encode |
Test directo. |
| 2 | covariance None → todos los stds None, todos los levels UNKNOWN | Test. |
| 3 | covariance diagonal con std = known_threshold → todos los levels KNOWN | Test (frontera). |
| 4 | covariance diagonal con std = unknown_threshold → todos los levels UNKNOWN | Test (frontera). |
| 5 | assess_belief(s, t).thresholds_sha256 == sha256(json(t)) |
Test. |
| 6 | assess_belief(s, t).thresholds_used == t |
Test. |
| 7 | block_overall_level = max(axis_x, axis_y, axis_z) en ordenamiento KNOWN < UNCERTAIN < UNKNOWN |
Test sobre casos mixtos. |
| 8 | overall_level = max(position_overall, velocity_overall, orientation_overall) |
Test. |
| 9 | AssessmentThresholds(known >= unknown) raises ValueError |
Test. |
| 10 | AssessmentThresholds(*_std <= 0) raises ValueError |
Test. |
| 11 | Adapter publica con belief_stamp_sim_ns como log_time |
Test sobre InMemorySink. |
| 12 | Round-trip MCAP: write N assessments → read → reconstrucción byte-equal | Test. |
| 13 | summarize_self_assessments([]) → counts all zero |
Test. |
| 14 | summarize cuenta levels correctamente |
Test. |
| 15 | CLI smoke: mcap con N records → JSON con N en total_records | Test. |
| 16 | Cross-process SHA-256 estabilidad sobre summary final | Test/Script. |
Consecuencias¶
Positivo.
- Por primera vez el agente emite una afirmación estructurada sobre su propio conocimiento. La línea entre "el operador analiza" y "el agente reporta" se cruza.
- Cualquier capa futura de decisión (autonomy tiers de ADR-0009)
puede consumir
/self_assessmentdirectamente sin re-implementar la lógica de clasificación. - El assessment es content-addressed vía
thresholds_sha256: un cambio de umbrales produce un assessment trivialmente distinguible. - El channel
/self_assessmentes un nuevo canal MCAP estándar reproducible / analizable / comparable con todo el toolchain ADR-0013 / 0017 / 0018. - Operadores pueden ejecutar experimentos del tipo "¿qué fracción del run el agente cree que es KNOWN bajo umbrales X?" sin escribir código.
Negativo.
- Aumenta el espacio MCAP por record (~200 bytes / assessment). Aceptable; los assessments se publican por step de belief, no por step de sensor.
- El operador ahora elige umbrales — una decisión que tiene consecuencias. La cláusula de honestidad del ADR exige que los umbrales viajen con el assessment para que la decisión sea reproducible.
- Acopla
core.uncertaintyal concepto de "level" que es nuevo. Mismo module ya teníaPerceptionMode(enum cerrado),Validity(ladder), etc. La adición es coherente.
Alternativas consideradas¶
- Levels continuos en
[0, 1](e.g. confidence score). Rechazado: re-introduce scoring/heurística. Niveles discretos son auditables y no exigen tolerancia. - Integrar perception mode / sensor health en el assessment. Rechazado v1: agrega dimensiones y dependencias antes de que esté probada la dimensión más simple (covarianza). Diferido.
- Publicar covarianza completa en el assessment. Rechazado: el
assessment es una afirmación clasificatoria, no una réplica de la
matriz. La matriz vive en
VehicleState.nav.covariance_15x15y se publica en/state/navaparte. - Un comparativo N-vía entre assessments de runs distintos. Mismo patrón que ADR-0018, pero diferido a v2 — primero ganamos experiencia con la primitiva.
- Hacer assess automático dentro de
NoisyGroundTruthEstimator. Rechazado: violaría ADR-0015 (no modificar) y acoplaría productores con introspección. El caller wiring decide qué componer.
Backward compatibility¶
- ADR-0013, 0014, 0015, 0016, 0017, 0018, 0019 sin tocar.
telemetry.channelsañade un canal nuevo (canal previo intactos).telemetry.adaptersañade una clase (clases previas intactas).telemetry.replayañade una entrada al catálogo cerrado del decoder (entradas previas intactas). Compatible con MCAPs ya escritos porque el decoder sólo se invoca cuando aparece el nuevoschema_name.core.uncertainty.__init__añade re-exports nuevos.cli.pyañade un subcomando nuevo.
Cero rotura.
Mission posture¶
Es el primer ADR que añade un productor runtime de introspección. Hasta ahora el agente no decía nada sobre sí mismo en tiempo de ejecución — sólo dejaba evidencia para que el operador la juzgara offline. Ahora el agente publica una afirmación clasificatoria verificable, contenida, comparable, replayable, auditable.
Este es el primer paso concreto hacia el agente que, en palabras de la misión, sabe cuándo no sabe — y, además, lo dice.