disensor 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
disensor/render.py ADDED
@@ -0,0 +1,123 @@
1
+ """Render de la declaracion de residuo como comentario de PR (Markdown).
2
+
3
+ Principio de la seccion 9 del protocolo: la declaracion lista residuo, no
4
+ cobertura. El comentario dirige el escrutinio del revisor humano hacia lo que
5
+ el ciclo no pudo cerrar, en lugar de leerse como sello de calidad.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ MARCADOR = "<!-- disensor-gate -->"
10
+
11
+ NOMBRE_CLASE = {
12
+ "escalado_sin_decision": "Escalado sin decision",
13
+ "refutacion_del_principal": "Refutacion del modelo principal",
14
+ "gap_de_ejecucion": "Gap de ejecucion",
15
+ }
16
+
17
+ NOMBRE_ESTADO = {
18
+ "incorporado": "incorporados",
19
+ "deuda_registrada": "deuda registrada",
20
+ "decision_del_dueno": "decision del dueno",
21
+ "refutado_verificable": "refutados con evidencia",
22
+ "refutado_interpretativo": "refutados por criterio",
23
+ "escalado_abierto": "escalados abiertos",
24
+ }
25
+
26
+
27
+ def _linea_item(item: dict, perfil: str) -> str:
28
+ clase = NOMBRE_CLASE[item["clase"]]
29
+ atencion = " **(requiere atencion humana)**" if item.get("requiere_atencion_humana") else ""
30
+ ref = f" (hallazgo {item['ref_hallazgo']})" if item.get("ref_hallazgo") else ""
31
+ detalle = ""
32
+ if perfil == "completo" and item.get("descripcion"):
33
+ detalle = f": {item['descripcion']}"
34
+ elif item["clase"] == "refutacion_del_principal" and item.get("tipo_refutacion"):
35
+ detalle = f" ({item['tipo_refutacion']})"
36
+ elif item["clase"] == "gap_de_ejecucion" and item.get("motivo_gap"):
37
+ detalle = f" ({item['motivo_gap'].replace('_', ' ')})"
38
+ evidencia = item.get("evidencia", {})
39
+ enlace = f" [evidencia]({evidencia['enlace']})" if evidencia.get("enlace") else ""
40
+ aceptacion = ""
41
+ if item.get("aceptacion_referente"):
42
+ ar = item["aceptacion_referente"]
43
+ aceptacion = f" Aceptado por escrito por {ar['referente']} ({ar['registro']})."
44
+ return f"- **{clase}**{ref}{detalle}{atencion}{enlace}{aceptacion}"
45
+
46
+
47
+ def _resumen_conteos(a: dict) -> str:
48
+ c = a["metricas"]["conteos"]
49
+ v = c["validos"]
50
+ fp = c["falsos_positivos"]
51
+ partes = []
52
+ if v["incorporado"]:
53
+ partes.append(f"{v['incorporado']} incorporados")
54
+ if v["deuda_registrada"]:
55
+ partes.append(f"{v['deuda_registrada']} a deuda registrada")
56
+ if v["decision_del_dueno"]:
57
+ partes.append(f"{v['decision_del_dueno']} con decision del dueno")
58
+ if fp["refutado_verificable"]:
59
+ partes.append(f"{fp['refutado_verificable']} refutados con evidencia")
60
+ if fp["refutado_interpretativo"]:
61
+ partes.append(f"{fp['refutado_interpretativo']} refutados por criterio")
62
+ if c["escalados_abiertos"]:
63
+ partes.append(f"{c['escalados_abiertos']} escalados abiertos")
64
+ detalle = ", ".join(partes) if partes else "sin hallazgos"
65
+ return f"{c['total_hallazgos']} hallazgos: {detalle}."
66
+
67
+
68
+ def render_artefacto(a: dict) -> str:
69
+ ev = a["evento"]
70
+ ac = a["actores"]
71
+ lineas = []
72
+ revisores = ", ".join(f"{r['modelo']} ({r['familia']})" for r in ac["revisores"])
73
+ confinamientos = ", ".join(
74
+ f"{r['confinamiento']['modo'].replace('_', ' ')}"
75
+ + (" verificado" if r["confinamiento"]["verificado"] else " SIN VERIFICAR")
76
+ for r in ac["revisores"]
77
+ )
78
+ lineas.append(
79
+ f"### Evento `{a['evento']['id_evento'][:8]}` "
80
+ f"(compuerta {ev['compuerta']}, Nivel {ev['nivel_criticidad']}, commit `{ev['commit_cabeza'][:7]}`)"
81
+ )
82
+ lineas.append("")
83
+ lineas.append(
84
+ f"Generador {ac['generador']['modelo']} ({ac['generador']['familia']}); "
85
+ f"revisor {revisores}; confinamiento: {confinamientos}. "
86
+ f"{_resumen_conteos(a)}"
87
+ )
88
+ if ev["ruta_abreviada"].get("usada"):
89
+ lineas.append(f"Ruta abreviada usada. Justificacion: {ev['ruta_abreviada'].get('justificacion', '')}")
90
+ lineas.append("")
91
+ residuo = a["residuo"]
92
+ if residuo.get("ausencia_declarada"):
93
+ lineas.append(f"**Sin residuo.** Declaracion expresa: {residuo['declaracion']}")
94
+ else:
95
+ lineas.append("**Residuo declarado** (lo que el ciclo no pudo cerrar por si mismo):")
96
+ for item in residuo["items"]:
97
+ lineas.append(_linea_item(item, a["perfil"]))
98
+ return "\n".join(lineas)
99
+
100
+
101
+ def render_comentario(validos: list[dict], errores_por_archivo: dict[str, list[str]],
102
+ errores_gate: list[str]) -> str:
103
+ """Comentario completo del PR, con marcador para actualizarlo en el lugar."""
104
+ lineas = [MARCADOR, "## Declaracion de residuo", ""]
105
+ if errores_gate or errores_por_archivo:
106
+ lineas.append("**El gate fallo.** El PR no declara residuo en forma valida:")
107
+ for msg in errores_gate:
108
+ lineas.append(f"- {msg}")
109
+ for archivo, errs in errores_por_archivo.items():
110
+ for msg in errs:
111
+ lineas.append(f"- `{archivo}`: {msg}")
112
+ lineas.append("")
113
+ for a in validos:
114
+ lineas.append(render_artefacto(a))
115
+ lineas.append("")
116
+ lineas.append("---")
117
+ lineas.append(
118
+ "Esta declaracion lista residuo, no cobertura: dirige el escrutinio del revisor humano "
119
+ "hacia lo que quedo abierto en lugar de leerse como sello de calidad. "
120
+ "La maquina valida forma y coherencia; que el residuo declarado sea el residuo real "
121
+ "se controla por muestreo humano, no por este gate."
122
+ )
123
+ return "\n".join(lineas)
@@ -0,0 +1,354 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://www.pelatech.com.ar/specs/residuo/v0.1/residuo.schema.json",
4
+ "title": "Declaracion de residuo de un evento de revision adversarial (desacuerdo controlado)",
5
+ "description": "Artefacto que un evento de revision adversarial emite al cierre del ciclo. Declara residuo, no cobertura (protocolo, seccion 9): lista lo que el ciclo no pudo cerrar por si mismo, o declara expresamente su ausencia. Los hallazgos resueltos y verificados no son residuo; su evidencia es el diff. Version provisional v0.1, sujeta a ratificacion.",
6
+ "type": "object",
7
+ "required": ["esquema", "perfil", "evento", "actores", "residuo", "metricas"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "esquema": {
11
+ "const": "residuo/v0.1",
12
+ "description": "Identificador de version del esquema."
13
+ },
14
+ "perfil": {
15
+ "enum": ["completo", "minimizado"],
16
+ "description": "completo: incluye textos de hallazgos y evidencia (uso interno o SaaS con confianza plena). minimizado: solo clases, estados, conteos y hashes; ningun texto libre sale del entorno del cliente. Las prohibiciones de fuga del perfil minimizado las verifica el validador estructural (regla R9)."
17
+ },
18
+ "evento": {
19
+ "type": "object",
20
+ "required": ["id_evento", "creado_en", "repositorio", "commit_cabeza", "compuerta", "nivel_criticidad", "ruta_abreviada"],
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "id_evento": { "type": "string", "format": "uuid" },
24
+ "creado_en": { "type": "string", "format": "date-time" },
25
+ "repositorio": { "type": "string", "minLength": 1, "description": "URL o identificador estable del repo. En perfil minimizado puede ser un hash." },
26
+ "pr": { "type": "string", "description": "Numero o URL del pull request, si el evento cierra sobre un PR." },
27
+ "commit_base": { "type": "string", "pattern": "^[0-9a-f]{7,40}$" },
28
+ "commit_cabeza": { "type": "string", "pattern": "^[0-9a-f]{7,40}$" },
29
+ "compuerta": {
30
+ "enum": ["plan", "diff", "arquitectura"],
31
+ "description": "Que se sometio a revision: el plan antes de implementar, el diff antes de commitear, o una comparacion de arquitectura."
32
+ },
33
+ "nivel_criticidad": {
34
+ "enum": ["A", "B", "C"],
35
+ "description": "Nivel del protocolo (seccion 3.1). El nivel viaja con el codigo, no con la intencion original."
36
+ },
37
+ "ruta_abreviada": {
38
+ "type": "object",
39
+ "required": ["usada"],
40
+ "additionalProperties": false,
41
+ "properties": {
42
+ "usada": { "type": "boolean" },
43
+ "justificacion": { "type": "string", "minLength": 15, "description": "Obligatoria si usada = true. Una linea con la justificacion asentada en el PR (seccion 3.2)." },
44
+ "casos_protegidos_tocados": {
45
+ "type": "array",
46
+ "uniqueItems": true,
47
+ "items": {
48
+ "enum": ["autenticacion_autorizacion", "datos_personales", "calculo_economico", "migracion_datos", "integracion_core"]
49
+ },
50
+ "description": "Los cinco casos de la seccion 3.2 que el cambio toca. Si la lista no esta vacia, la ruta abreviada no se admite: lo verifica la regla R3 del validador y lo refuerza el if/then de este esquema."
51
+ }
52
+ },
53
+ "allOf": [
54
+ {
55
+ "if": { "properties": { "usada": { "const": true } }, "required": ["usada"] },
56
+ "then": { "required": ["justificacion"] }
57
+ },
58
+ {
59
+ "if": {
60
+ "properties": {
61
+ "usada": { "const": true },
62
+ "casos_protegidos_tocados": { "type": "array", "minItems": 1 }
63
+ },
64
+ "required": ["usada", "casos_protegidos_tocados"]
65
+ },
66
+ "then": { "properties": { "usada": { "const": false } } }
67
+ }
68
+ ]
69
+ }
70
+ }
71
+ },
72
+ "actores": {
73
+ "type": "object",
74
+ "required": ["generador", "revisores", "arbitro_humano"],
75
+ "additionalProperties": false,
76
+ "properties": {
77
+ "generador": {
78
+ "type": "object",
79
+ "required": ["familia", "modelo"],
80
+ "additionalProperties": false,
81
+ "properties": {
82
+ "familia": { "$ref": "#/$defs/familia" },
83
+ "modelo": { "type": "string", "minLength": 1 },
84
+ "version": { "type": "string" }
85
+ }
86
+ },
87
+ "revisores": {
88
+ "type": "array",
89
+ "minItems": 1,
90
+ "items": {
91
+ "type": "object",
92
+ "required": ["id_revisor", "familia", "modelo", "confinamiento"],
93
+ "additionalProperties": false,
94
+ "properties": {
95
+ "id_revisor": { "type": "string", "minLength": 1, "description": "Identificador local del revisor dentro del evento (r1, r2...). Los hallazgos lo referencian en origen." },
96
+ "familia": { "$ref": "#/$defs/familia" },
97
+ "modelo": { "type": "string", "minLength": 1 },
98
+ "id_sesion": { "type": "string", "description": "Identificador de sesion que reporta el CLI del revisor, si existe. Permite reconstruir la corrida." },
99
+ "consigna_hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$", "description": "Hash de la consigna adversarial usada. Ancla la corrida a una version concreta de la consigna sin exponer su texto." },
100
+ "confinamiento": {
101
+ "type": "object",
102
+ "required": ["modo", "verificado"],
103
+ "additionalProperties": false,
104
+ "properties": {
105
+ "modo": {
106
+ "enum": ["permisos", "sandbox", "solo_lectura_por_instruccion", "sin_confinamiento"],
107
+ "description": "Como se garantizo que el revisor solo lee. El protocolo (seccion 10) exige permisos y no consigna; declarar el modo real hace visible la brecha cuando existe."
108
+ },
109
+ "verificado": { "type": "boolean", "description": "Si se verifico despues de la corrida que el revisor no modifico el repositorio." },
110
+ "metodo_verificacion": { "enum": ["git_status_limpio", "hash_del_arbol", "otro"] }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ },
116
+ "tercera_revision": {
117
+ "type": "object",
118
+ "description": "Revision independiente adicional en cambios de alto riesgo (seccion 6): humana o de otra familia de modelo.",
119
+ "required": ["tipo"],
120
+ "additionalProperties": false,
121
+ "properties": {
122
+ "tipo": { "enum": ["humana", "modelo_tercera_familia"] },
123
+ "referencia": { "type": "string" }
124
+ }
125
+ },
126
+ "arbitro_humano": {
127
+ "type": "object",
128
+ "required": ["presente"],
129
+ "additionalProperties": false,
130
+ "properties": {
131
+ "presente": { "type": "boolean", "description": "El desarrollador entra al final del ciclo o antes si el loop escala. Un evento sin arbitro humano presente no cumple el protocolo." },
132
+ "id": { "type": "string", "description": "Identificador del dueno de la feature. Anonimizable (hash o alias) segun politica de la organizacion." }
133
+ }
134
+ }
135
+ }
136
+ },
137
+ "hallazgos": {
138
+ "type": "array",
139
+ "description": "Todos los hallazgos del evento con su estado final. Obligatorio en perfil completo (regla R10); en minimizado puede omitirse y valen los conteos de metricas.",
140
+ "items": { "$ref": "#/$defs/hallazgo" }
141
+ },
142
+ "residuo": {
143
+ "type": "object",
144
+ "description": "El corazon de la declaracion (seccion 6): lo que el ciclo no pudo cerrar por si mismo, en tres clases, o la declaracion expresa de ausencia. Lo comun a las tres clases: ninguna quedo respaldada por ejecucion; descansan sobre el juicio de alguien.",
145
+ "additionalProperties": false,
146
+ "oneOf": [
147
+ { "required": ["ausencia_declarada", "declaracion"] },
148
+ { "required": ["items"] }
149
+ ],
150
+ "properties": {
151
+ "ausencia_declarada": {
152
+ "const": true,
153
+ "description": "Solo si no quedo residuo. La ausencia se declara expresamente, nunca queda implicita en un campo vacio (seccion 12.2, falla 2)."
154
+ },
155
+ "declaracion": {
156
+ "type": "string",
157
+ "minLength": 30,
158
+ "description": "Texto de la declaracion expresa de ausencia. La regla R2 del validador rechaza marcadores genericos (N/A, ninguno, todo resuelto, aplicado). Limite honesto del control: la maquina detecta el campo vacio y el marcador generico, no la declaracion falsa; el muestreo humano sigue siendo la unica defensa real contra el cumplimiento cosmetico."
159
+ },
160
+ "items": {
161
+ "type": "array",
162
+ "minItems": 1,
163
+ "items": { "$ref": "#/$defs/item_residuo" }
164
+ }
165
+ }
166
+ },
167
+ "metricas": {
168
+ "type": "object",
169
+ "description": "Agregados para el panel de calibracion, definidos en la seccion 11 del protocolo. Presentes en ambos perfiles: no contienen texto libre.",
170
+ "required": ["conteos"],
171
+ "additionalProperties": false,
172
+ "properties": {
173
+ "conteos": {
174
+ "type": "object",
175
+ "required": ["total_hallazgos", "validos", "falsos_positivos", "escalados_abiertos"],
176
+ "additionalProperties": false,
177
+ "properties": {
178
+ "total_hallazgos": { "type": "integer", "minimum": 0 },
179
+ "validos": {
180
+ "type": "object",
181
+ "description": "Hallazgo valido: produjo cambio en plan o codigo, deuda tecnica registrada con identificador, o decision del dueno que modifica alcance, comportamiento o riesgo aceptado (seccion 11). Descartar nunca cuenta como valido.",
182
+ "required": ["incorporado", "deuda_registrada", "decision_del_dueno"],
183
+ "additionalProperties": false,
184
+ "properties": {
185
+ "incorporado": { "type": "integer", "minimum": 0 },
186
+ "deuda_registrada": { "type": "integer", "minimum": 0 },
187
+ "decision_del_dueno": { "type": "integer", "minimum": 0 }
188
+ }
189
+ },
190
+ "falsos_positivos": {
191
+ "type": "object",
192
+ "description": "Descartes. Con evidencia verificable: falso positivo. Interpretativo: falso positivo que ademas va al residuo.",
193
+ "required": ["refutado_verificable", "refutado_interpretativo"],
194
+ "additionalProperties": false,
195
+ "properties": {
196
+ "refutado_verificable": { "type": "integer", "minimum": 0 },
197
+ "refutado_interpretativo": { "type": "integer", "minimum": 0 }
198
+ }
199
+ },
200
+ "escalados_abiertos": {
201
+ "type": "integer",
202
+ "minimum": 0,
203
+ "description": "No entran en ninguno de los dos numeradores: se cuentan aparte como residuo hasta que se resuelvan."
204
+ }
205
+ }
206
+ },
207
+ "tiempo_extra_seg": {
208
+ "type": "integer",
209
+ "minimum": 0,
210
+ "description": "Cronometro de punta a punta del ciclo: corridas del revisor, verificacion de cada hallazgo, reescritura del plan y tiempo de refutacion. No incluye la implementacion (seccion 11)."
211
+ },
212
+ "costo_tokens": {
213
+ "type": "object",
214
+ "additionalProperties": false,
215
+ "properties": {
216
+ "revisores": { "type": "integer", "minimum": 0 },
217
+ "generador": { "type": "integer", "minimum": 0 }
218
+ }
219
+ }
220
+ }
221
+ },
222
+ "extensiones": {
223
+ "type": "object",
224
+ "description": "Espacio para campos propios de la organizacion, con nombre de dominio invertido como prefijo. No participa de las reglas del protocolo."
225
+ }
226
+ },
227
+ "$defs": {
228
+ "familia": {
229
+ "enum": ["anthropic", "openai", "google", "meta", "mistral", "otra"],
230
+ "description": "Familia de modelo. La decorrelacion exige que la familia de cada revisor difiera de la del generador (regla R4). Sumar una familia baja la correlacion, no la elimina: por eso en Nivel A la diversidad de modelos nunca es el unico control."
231
+ },
232
+ "evidencia": {
233
+ "type": "object",
234
+ "additionalProperties": false,
235
+ "properties": {
236
+ "texto": { "type": "string", "minLength": 10, "description": "Cita de codigo, dato contractual o comportamiento observable. Solo en perfil completo." },
237
+ "enlace": { "type": "string", "description": "Referencia estable (permalink a linea, test, log)." },
238
+ "hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$", "description": "Hash de la evidencia. Unico contenido admitido en perfil minimizado." }
239
+ }
240
+ },
241
+ "hallazgo": {
242
+ "type": "object",
243
+ "required": ["id", "origen", "severidad", "estado_final", "verificacion"],
244
+ "additionalProperties": false,
245
+ "properties": {
246
+ "id": { "type": "string", "pattern": "^h[0-9]+$" },
247
+ "origen": { "type": "string", "minLength": 1, "description": "id_revisor del revisor que lo produjo." },
248
+ "severidad": { "enum": ["critica", "mayor", "menor", "info"] },
249
+ "titulo": { "type": "string", "minLength": 5, "description": "Solo perfil completo." },
250
+ "descripcion": { "type": "string", "description": "Solo perfil completo." },
251
+ "ubicacion": { "type": "string", "description": "Ruta y lineas afectadas. Solo perfil completo." },
252
+ "estado_final": {
253
+ "enum": ["incorporado", "deuda_registrada", "decision_del_dueno", "refutado_verificable", "refutado_interpretativo", "escalado_abierto"],
254
+ "description": "Salida terminal del hallazgo al cierre del evento. Los tres primeros son las tres formas de hallazgo valido (seccion 11). Los dos refutados son falsos positivos; el interpretativo ademas integra el residuo. escalado_abierto integra el residuo y no cuenta en ningun numerador."
255
+ },
256
+ "verificacion": {
257
+ "type": "object",
258
+ "required": ["contra"],
259
+ "additionalProperties": false,
260
+ "properties": {
261
+ "contra": {
262
+ "enum": ["repositorio", "ejecucion", "ninguna"],
263
+ "description": "Contra que verifico el generador el hallazgo antes de aceptarlo o refutarlo (no creerle a ciegas al revisor; decorrelacion en ambas direcciones)."
264
+ },
265
+ "detalle": { "type": "string" }
266
+ }
267
+ },
268
+ "ajuste_al_remedio": {
269
+ "type": "string",
270
+ "description": "Cuando el hallazgo es valido pero el remedio propuesto por el revisor era incorrecto y se adopto con modificacion. El hallazgo cuenta como incorporado; el ajuste queda registrado."
271
+ },
272
+ "verificacion_de_la_correccion": {
273
+ "type": "object",
274
+ "description": "Un hallazgo incorporado no se cierra al aplicar la correccion sino cuando la correccion paso su propia verificacion: compuerta de diff sobre ese cambio o prueba especifica (seccion 6). Obligatoria en compuerta diff para estado incorporado (regla R7).",
275
+ "required": ["tipo"],
276
+ "additionalProperties": false,
277
+ "properties": {
278
+ "tipo": { "enum": ["compuerta_diff", "prueba_especifica", "pendiente_en_compuerta_diff"] },
279
+ "referencia": { "type": "string" }
280
+ }
281
+ },
282
+ "id_deuda": { "type": "string", "description": "Obligatorio si estado_final = deuda_registrada: identificador en el sistema de deuda tecnica." },
283
+ "registro_riesgo": { "type": "string", "description": "Obligatorio si estado_final = decision_del_dueno: registro del riesgo aceptado o del cambio de alcance." },
284
+ "evidencia": { "$ref": "#/$defs/evidencia" }
285
+ },
286
+ "allOf": [
287
+ {
288
+ "if": { "properties": { "estado_final": { "const": "deuda_registrada" } }, "required": ["estado_final"] },
289
+ "then": { "required": ["id_deuda"] }
290
+ },
291
+ {
292
+ "if": { "properties": { "estado_final": { "const": "decision_del_dueno" } }, "required": ["estado_final"] },
293
+ "then": { "required": ["registro_riesgo"] }
294
+ },
295
+ {
296
+ "if": { "properties": { "estado_final": { "const": "refutado_verificable" } }, "required": ["estado_final"] },
297
+ "then": { "required": ["evidencia"] }
298
+ }
299
+ ]
300
+ },
301
+ "item_residuo": {
302
+ "type": "object",
303
+ "required": ["id", "clase", "requiere_atencion_humana"],
304
+ "additionalProperties": false,
305
+ "properties": {
306
+ "id": { "type": "string", "pattern": "^r[0-9]+$" },
307
+ "clase": {
308
+ "enum": ["escalado_sin_decision", "refutacion_del_principal", "gap_de_ejecucion"],
309
+ "description": "Las tres clases de la seccion 6: hallazgos escalados sin decision, hallazgos que el modelo principal refuto, y comportamiento que la ejecucion no pudo arbitrar."
310
+ },
311
+ "ref_hallazgo": { "type": "string", "pattern": "^h[0-9]+$", "description": "Hallazgo de origen, si aplica. Los gap_de_ejecucion pueden no nacer de un hallazgo." },
312
+ "requiere_atencion_humana": {
313
+ "type": "boolean",
314
+ "description": "true como minimo para toda refutacion interpretativa: es la que pide atencion humana (seccion 6)."
315
+ },
316
+ "tipo_refutacion": {
317
+ "enum": ["verificable", "interpretativa"],
318
+ "description": "Obligatorio si clase = refutacion_del_principal. Verificable: cita de codigo, dato contractual o comportamiento observable. Interpretativa: criterio de dominio o arquitectura. Las dos se declaran para que el revisor humano pueda no darlas por buenas."
319
+ },
320
+ "descripcion": { "type": "string", "description": "Solo perfil completo." },
321
+ "evidencia": { "$ref": "#/$defs/evidencia" },
322
+ "motivo_gap": {
323
+ "enum": ["entorno_no_reproduce", "tercero_sin_ambiente_prueba", "otro"],
324
+ "description": "Obligatorio si clase = gap_de_ejecucion."
325
+ },
326
+ "aceptacion_referente": {
327
+ "type": "object",
328
+ "description": "En Nivel A, un gap de ejecucion bloquea el merge hasta que un referente tecnico lo acepte por escrito (seccion 7). La regla R5 lo exige.",
329
+ "required": ["referente", "fecha", "registro"],
330
+ "additionalProperties": false,
331
+ "properties": {
332
+ "referente": { "type": "string" },
333
+ "fecha": { "type": "string", "format": "date-time" },
334
+ "registro": { "type": "string", "description": "Enlace o identificador del registro escrito de la aceptacion." }
335
+ }
336
+ }
337
+ },
338
+ "allOf": [
339
+ {
340
+ "if": { "properties": { "clase": { "const": "refutacion_del_principal" } }, "required": ["clase"] },
341
+ "then": { "required": ["tipo_refutacion"] }
342
+ },
343
+ {
344
+ "if": { "properties": { "clase": { "const": "gap_de_ejecucion" } }, "required": ["clase"] },
345
+ "then": { "required": ["motivo_gap"] }
346
+ },
347
+ {
348
+ "if": { "properties": { "tipo_refutacion": { "const": "interpretativa" } }, "required": ["tipo_refutacion"] },
349
+ "then": { "properties": { "requiere_atencion_humana": { "const": true } } }
350
+ }
351
+ ]
352
+ }
353
+ }
354
+ }