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/__init__.py +2 -0
- disensor/__main__.py +3 -0
- disensor/cli.py +62 -0
- disensor/gate.py +245 -0
- disensor/plantilla.py +86 -0
- disensor/reglas.py +197 -0
- disensor/render.py +123 -0
- disensor/residuo.schema.json +354 -0
- disensor/vectores.py +189 -0
- disensor-0.1.0.dist-info/METADATA +95 -0
- disensor-0.1.0.dist-info/RECORD +15 -0
- disensor-0.1.0.dist-info/WHEEL +5 -0
- disensor-0.1.0.dist-info/entry_points.txt +2 -0
- disensor-0.1.0.dist-info/licenses/LICENSE +21 -0
- disensor-0.1.0.dist-info/top_level.txt +1 -0
disensor/vectores.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Vectores de conformidad del esquema v0.1.
|
|
2
|
+
|
|
3
|
+
Los vectores son la fuente de verdad compartida entre implementaciones del
|
|
4
|
+
validador (Python de referencia, TypeScript del plano de evidencia, y las que
|
|
5
|
+
vengan). Cada vector es un artefacto mas el veredicto esperado: valido o no,
|
|
6
|
+
y el conjunto de etiquetas de regla que deben dispararse ("schema" para
|
|
7
|
+
errores de forma, "R0" a "R10" para reglas estructurales).
|
|
8
|
+
|
|
9
|
+
Se comparan etiquetas, no mensajes: los mensajes son libres por
|
|
10
|
+
implementacion; las etiquetas no pueden divergir.
|
|
11
|
+
|
|
12
|
+
Uso: python -m disensor.vectores <directorio_destino>
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import copy
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from .reglas import validar_artefacto
|
|
22
|
+
|
|
23
|
+
RAIZ = Path(__file__).resolve().parents[2]
|
|
24
|
+
EJEMPLOS = RAIZ / "spec" / "ejemplos"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _cargar(nombre: str) -> dict:
|
|
28
|
+
with open(EJEMPLOS / nombre, encoding="utf-8") as f:
|
|
29
|
+
return json.load(f)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _etiquetas(errores: list[str]) -> list[str]:
|
|
33
|
+
tags = set()
|
|
34
|
+
for e in errores:
|
|
35
|
+
if e.startswith("[") and "]" in e:
|
|
36
|
+
tags.add(e[1:e.index("]")])
|
|
37
|
+
return sorted(tags)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def casos() -> list[tuple[str, dict, bool, set[str]]]:
|
|
41
|
+
"""(nombre, artefacto, valido_esperado, reglas_que_deben_aparecer)."""
|
|
42
|
+
plan = _cargar("ejemplo_1_compuerta_plan.json")
|
|
43
|
+
diff = _cargar("ejemplo_2_compuerta_diff.json")
|
|
44
|
+
mini = _cargar("ejemplo_3_perfil_minimizado.json")
|
|
45
|
+
lista: list[tuple[str, dict, bool, set[str]]] = []
|
|
46
|
+
|
|
47
|
+
# Validos
|
|
48
|
+
lista.append(("valido_compuerta_plan", copy.deepcopy(plan), True, set()))
|
|
49
|
+
lista.append(("valido_compuerta_diff", copy.deepcopy(diff), True, set()))
|
|
50
|
+
lista.append(("valido_perfil_minimizado", copy.deepcopy(mini), True, set()))
|
|
51
|
+
|
|
52
|
+
m = copy.deepcopy(diff)
|
|
53
|
+
m["evento"]["nivel_criticidad"] = "A"
|
|
54
|
+
for i in m["residuo"]["items"]:
|
|
55
|
+
if i["clase"] == "gap_de_ejecucion":
|
|
56
|
+
i["aceptacion_referente"] = {
|
|
57
|
+
"referente": "referente-tecnico-01",
|
|
58
|
+
"fecha": "2026-07-15T19:00:00-03:00",
|
|
59
|
+
"registro": "actas/2026-07-15-aceptacion-gap.md",
|
|
60
|
+
}
|
|
61
|
+
lista.append(("valido_nivel_a_gap_con_referente", m, True, set()))
|
|
62
|
+
|
|
63
|
+
m = copy.deepcopy(mini)
|
|
64
|
+
m["hallazgos"] = []
|
|
65
|
+
m["residuo"] = {
|
|
66
|
+
"ausencia_declarada": True,
|
|
67
|
+
"declaracion": "El ciclo corrio completo sobre el diff y no quedo nada sin resolver: "
|
|
68
|
+
"todo hallazgo se incorporo y sus correcciones pasaron pruebas especificas.",
|
|
69
|
+
}
|
|
70
|
+
m["metricas"]["conteos"] = {
|
|
71
|
+
"total_hallazgos": 0,
|
|
72
|
+
"validos": {"incorporado": 0, "deuda_registrada": 0, "decision_del_dueno": 0},
|
|
73
|
+
"falsos_positivos": {"refutado_verificable": 0, "refutado_interpretativo": 0},
|
|
74
|
+
"escalados_abiertos": 0,
|
|
75
|
+
}
|
|
76
|
+
lista.append(("valido_minimizado_ausencia_declarada", m, True, set()))
|
|
77
|
+
|
|
78
|
+
# Invalidos: reglas estructurales
|
|
79
|
+
m = copy.deepcopy(diff)
|
|
80
|
+
m["actores"]["arbitro_humano"]["presente"] = False
|
|
81
|
+
lista.append(("r0_sin_arbitro_humano", m, False, {"R0"}))
|
|
82
|
+
|
|
83
|
+
m = copy.deepcopy(diff)
|
|
84
|
+
m["residuo"]["items"] = [i for i in m["residuo"]["items"] if i["id"] != "r1"]
|
|
85
|
+
lista.append(("r1_escalado_sin_item", m, False, {"R1"}))
|
|
86
|
+
|
|
87
|
+
m = copy.deepcopy(diff)
|
|
88
|
+
m["residuo"]["items"][0]["ref_hallazgo"] = "h99"
|
|
89
|
+
lista.append(("r1_referencia_inexistente", m, False, {"R1"}))
|
|
90
|
+
|
|
91
|
+
m = copy.deepcopy(diff)
|
|
92
|
+
m["residuo"]["items"][0]["descripcion"] = "ninguno"
|
|
93
|
+
lista.append(("r2_marcador_generico", m, False, {"R2"}))
|
|
94
|
+
|
|
95
|
+
m = copy.deepcopy(diff)
|
|
96
|
+
m["actores"]["revisores"][0]["modelo"] = "COMPLETAR_modelo_del_revisor"
|
|
97
|
+
lista.append(("r2_marcador_de_plantilla", m, False, {"R2"}))
|
|
98
|
+
|
|
99
|
+
m = copy.deepcopy(diff)
|
|
100
|
+
m["actores"]["revisores"][0]["familia"] = "anthropic"
|
|
101
|
+
lista.append(("r4_sin_decorrelacion", m, False, {"R4"}))
|
|
102
|
+
|
|
103
|
+
m = copy.deepcopy(diff)
|
|
104
|
+
m["evento"]["nivel_criticidad"] = "A"
|
|
105
|
+
lista.append(("r5_gap_nivel_a_sin_referente", m, False, {"R5"}))
|
|
106
|
+
|
|
107
|
+
m = copy.deepcopy(diff)
|
|
108
|
+
m["metricas"]["conteos"]["validos"]["incorporado"] = 3
|
|
109
|
+
lista.append(("r6_conteo_inflado", m, False, {"R6"}))
|
|
110
|
+
|
|
111
|
+
m = copy.deepcopy(diff)
|
|
112
|
+
m["hallazgos"][0]["verificacion_de_la_correccion"] = {"tipo": "pendiente_en_compuerta_diff"}
|
|
113
|
+
lista.append(("r7_correccion_pendiente_en_diff", m, False, {"R7"}))
|
|
114
|
+
|
|
115
|
+
m = copy.deepcopy(mini)
|
|
116
|
+
m["hallazgos"][0]["titulo"] = "titulo que no deberia estar"
|
|
117
|
+
lista.append(("r9_fuga_de_texto_en_minimizado", m, False, {"R9"}))
|
|
118
|
+
|
|
119
|
+
m = copy.deepcopy(mini)
|
|
120
|
+
m["evento"]["repositorio"] = "https://github.com/ejemplo/repo"
|
|
121
|
+
lista.append(("r9_url_en_claro_en_minimizado", m, False, {"R9"}))
|
|
122
|
+
|
|
123
|
+
m = copy.deepcopy(diff)
|
|
124
|
+
m["hallazgos"] = []
|
|
125
|
+
lista.append(("r10_completo_sin_hallazgos", m, False, {"R10"}))
|
|
126
|
+
|
|
127
|
+
# Invalidos: solo schema (las reglas no se evaluan si la forma falla)
|
|
128
|
+
m = copy.deepcopy(diff)
|
|
129
|
+
m["evento"]["ruta_abreviada"] = {
|
|
130
|
+
"usada": True,
|
|
131
|
+
"justificacion": "cambio trivial de una linea en un helper",
|
|
132
|
+
"casos_protegidos_tocados": ["migracion_datos"],
|
|
133
|
+
}
|
|
134
|
+
lista.append(("schema_ruta_abreviada_sobre_caso_protegido", m, False, {"schema"}))
|
|
135
|
+
|
|
136
|
+
m = copy.deepcopy(diff)
|
|
137
|
+
m["hallazgos"][0]["estado_final"] = "deuda_registrada"
|
|
138
|
+
lista.append(("schema_deuda_sin_id", m, False, {"schema"}))
|
|
139
|
+
|
|
140
|
+
m = copy.deepcopy(diff)
|
|
141
|
+
m["residuo"]["items"][1]["tipo_refutacion"] = "interpretativa"
|
|
142
|
+
m["residuo"]["items"][1]["requiere_atencion_humana"] = False
|
|
143
|
+
lista.append(("schema_interpretativa_sin_atencion_humana", m, False, {"schema"}))
|
|
144
|
+
|
|
145
|
+
m = copy.deepcopy(diff)
|
|
146
|
+
m["evento"]["commit_cabeza"] = "ZZZZ"
|
|
147
|
+
lista.append(("schema_commit_invalido", m, False, {"schema"}))
|
|
148
|
+
|
|
149
|
+
m = copy.deepcopy(diff)
|
|
150
|
+
del m["residuo"]["items"]
|
|
151
|
+
lista.append(("schema_residuo_vacio", m, False, {"schema"}))
|
|
152
|
+
|
|
153
|
+
return lista
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def generar(destino: Path) -> int:
|
|
157
|
+
"""Escribe los vectores con el veredicto de la implementacion de referencia.
|
|
158
|
+
|
|
159
|
+
El veredicto se registra desde la implementacion, pero cada caso lleva una
|
|
160
|
+
expectativa minima (valido si o no, y que reglas deben aparecer) que se
|
|
161
|
+
verifica antes de escribir: el generador no puede bendecir un bug propio.
|
|
162
|
+
"""
|
|
163
|
+
destino.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
indice = []
|
|
165
|
+
for nombre, artefacto, valido_esperado, reglas_minimas in casos():
|
|
166
|
+
errores = validar_artefacto(artefacto)
|
|
167
|
+
valido = not errores
|
|
168
|
+
etiquetas = _etiquetas(errores)
|
|
169
|
+
assert valido == valido_esperado, f"{nombre}: esperaba valido={valido_esperado}, dio {valido}: {errores}"
|
|
170
|
+
faltan = reglas_minimas - set(etiquetas)
|
|
171
|
+
assert not faltan, f"{nombre}: faltan reglas {faltan} en {etiquetas}"
|
|
172
|
+
vector = {
|
|
173
|
+
"nombre": nombre,
|
|
174
|
+
"esperado": {"valido": valido, "reglas": etiquetas},
|
|
175
|
+
"artefacto": artefacto,
|
|
176
|
+
}
|
|
177
|
+
with open(destino / f"{nombre}.json", "w", encoding="utf-8") as f:
|
|
178
|
+
json.dump(vector, f, ensure_ascii=False, indent=2)
|
|
179
|
+
f.write("\n")
|
|
180
|
+
indice.append(nombre)
|
|
181
|
+
with open(destino / "INDICE.json", "w", encoding="utf-8") as f:
|
|
182
|
+
json.dump({"esquema": "residuo/v0.1", "vectores": indice}, f, ensure_ascii=False, indent=2)
|
|
183
|
+
f.write("\n")
|
|
184
|
+
print(f"{len(indice)} vectores escritos en {destino}")
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if __name__ == "__main__":
|
|
189
|
+
sys.exit(generar(Path(sys.argv[1] if len(sys.argv) > 1 else "spec/vectores")))
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: disensor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: disensor: adversarial plan & code review with a declared residue. Emite, valida y hace cumplir en CI declaraciones de residuo (esquema residuo/v0.1).
|
|
5
|
+
Author-email: Nicolas Rocchia <nicolasrocchia@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: jsonschema>=4.18
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# disensor
|
|
16
|
+
|
|
17
|
+
Adversarial plan & code review with a declared residue.
|
|
18
|
+
|
|
19
|
+
Declaración de residuo de revisión adversarial, con validación y gate de CI. Implementación de referencia del artefacto definido a partir del método de **desacuerdo controlado**: un modelo genera, un modelo de otra familia ataca, el generador verifica cada hallazgo, y el ciclo termina cuando todo hallazgo quedó resuelto, refutado con evidencia o escalado a un humano.
|
|
20
|
+
|
|
21
|
+
El artefacto que este repo define y hace cumplir registra cómo terminó cada evento de revisión: los hallazgos con su estado terminal, y el **residuo**: lo que el ciclo no pudo cerrar por sí mismo y descansa sobre el juicio de alguien. La declaración lista residuo, no cobertura: dirige el escrutinio del revisor humano en lugar de leerse como sello de calidad.
|
|
22
|
+
|
|
23
|
+
Paper del método: Rocchia, N. (2026), *Desacuerdo controlado: revisión adversarial automatizada con un segundo asistente de código en el desarrollo de software*, DOI [10.5281/zenodo.21633495](https://doi.org/10.5281/zenodo.21633495).
|
|
24
|
+
|
|
25
|
+
## Qué hay acá
|
|
26
|
+
|
|
27
|
+
- `spec/residuo.schema.json`: el esquema del artefacto (JSON Schema 2020-12), versión v0.1.
|
|
28
|
+
- `spec/ejemplos/`: tres artefactos de ejemplo, incluido un evento real anonimizado y el perfil minimizado sin texto libre.
|
|
29
|
+
- `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR y el scaffolding de artefactos.
|
|
30
|
+
- `action.yml`: GitHub Action compuesta, lista para usar.
|
|
31
|
+
- `docs/integracion-claude-code.md`: cómo el flujo real (Claude Code + Codex) emite el artefacto al cierre de cada evento.
|
|
32
|
+
|
|
33
|
+
## Uso rápido
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install disensor # para desarrollo, desde el repo clonado: pip install -e .
|
|
37
|
+
|
|
38
|
+
disensor nuevo --compuerta diff --nivel B # plantilla prellenada en .residuo/
|
|
39
|
+
disensor validar .residuo/<id>.json # schema + reglas R0 a R10
|
|
40
|
+
disensor gate --sin-comentario # lo que va a correr CI, en local
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
En el repositorio consumidor, `disensor.config.json` en la raíz declara el nivel de criticidad (el nivel viaja con el código, en un archivo versionado):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"nivel_criticidad": "B",
|
|
48
|
+
"nivel_A_habilitado": false
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Y el workflow (ver `docs/ejemplo-workflow.yml`):
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
on: pull_request
|
|
56
|
+
permissions:
|
|
57
|
+
contents: read
|
|
58
|
+
pull-requests: write
|
|
59
|
+
jobs:
|
|
60
|
+
gate:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
with:
|
|
65
|
+
fetch-depth: 0
|
|
66
|
+
- uses: NicolasRocchia/disensor@v0.1.0
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
El gate valida todos los artefactos de `.residuo/` del PR, aplica la política y publica la declaración como comentario (se actualiza en el lugar en cada push).
|
|
70
|
+
|
|
71
|
+
## Qué hace cumplir el gate
|
|
72
|
+
|
|
73
|
+
Por artefacto (reglas R0 a R10): coherencia entre hallazgos y residuo, conteos que cierran, decorrelación de familias entre generador y revisor, evidencia obligatoria en refutaciones verificables, atención humana obligatoria en refutaciones interpretativas, corrección verificada antes de cerrar un hallazgo en compuerta de diff, rechazo de marcadores genéricos, y perfil minimizado sin fugas de texto.
|
|
74
|
+
|
|
75
|
+
Por PR (chequeos G1 a G5): al menos una declaración válida en el rango, nivel del artefacto igual al declarado del repositorio, Nivel A bloqueado mientras la gobernanza no esté validada, política de confinamiento del revisor por nivel, y commit revisado dentro del rango del PR.
|
|
76
|
+
|
|
77
|
+
Límite honesto, heredado del protocolo: la máquina detecta el campo vacío y el marcador genérico, no la declaración falsa. El muestreo humano de PR cerrados sigue siendo la única defensa real contra el cumplimiento cosmético.
|
|
78
|
+
|
|
79
|
+
## Qué no hace
|
|
80
|
+
|
|
81
|
+
No corre modelos, no pide claves de API en CI, y ningún código viaja a ningún servicio: valida un JSON que ya está versionado en el repo. La orquestación del loop vive donde el equipo ya trabaja; el perfil `minimizado` del artefacto permite ambientes donde ni siquiera el texto de los hallazgos puede salir del entorno.
|
|
82
|
+
|
|
83
|
+
## Conformidad entre implementaciones
|
|
84
|
+
|
|
85
|
+
`spec/vectores/` contiene los vectores de conformidad: 22 artefactos con su veredicto esperado (valido o no, y las etiquetas de regla que deben dispararse). Toda implementacion del validador tiene que pasarlos identicos: la referencia en Python los corre en la suite (`tests/test_vectores.py`) y el port TypeScript del plano de evidencia los corre con `npm run conformidad`. Se comparan etiquetas, no mensajes. Los vectores se regeneran con `python -m disensor.vectores spec/vectores`.
|
|
86
|
+
|
|
87
|
+
`plano-evidencia/` contiene el Worker de ingesta (Cloudflare Workers mas D1) con el port TypeScript del validador y el recibo de integridad de solo agregado. Ver su README para el estado de verificacion y el despliegue.
|
|
88
|
+
|
|
89
|
+
## Estado
|
|
90
|
+
|
|
91
|
+
v0.1, borrador en uso. El esquema puede cambiar hasta v1.0; los cambios se declaran en el propio esquema. Dos decisiones abiertas antes de v0.2: claves del esquema en español o inglés, y licencia definitiva (hoy MIT; Apache-2.0 está en consideración por la concesión de patentes antes del release público).
|
|
92
|
+
|
|
93
|
+
## Licencia
|
|
94
|
+
|
|
95
|
+
MIT.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
disensor/__init__.py,sha256=ZFrFd6fxbpAtqdhi5sj4RUH-lJSf3fxbVGaFvLixyig,110
|
|
2
|
+
disensor/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30
|
|
3
|
+
disensor/cli.py,sha256=na88_YQB9MhGHb4cn1JLjbrnY2xsAPsS8t2SuN6Pd18,2394
|
|
4
|
+
disensor/gate.py,sha256=NsP1hrzV1lxVplJJHmMxmzH5XmA6Y1xKu6zJE6Qnv20,9869
|
|
5
|
+
disensor/plantilla.py,sha256=R6oXu8FV1R99IBX9Yv8dnmdFB-FW_oPB-lRz0WjoGJo,3260
|
|
6
|
+
disensor/reglas.py,sha256=fO9aEKg5-xggDF34Qb0JiWjLXydR5-x6tHChtdSjBcg,9055
|
|
7
|
+
disensor/render.py,sha256=XlFOgRUn2Aj8dUB1CK1f34OLGOEZYkdVQaJsGlW1khM,5334
|
|
8
|
+
disensor/residuo.schema.json,sha256=mHWXZCQqFKlw0E2thaDo-sijq-JZFJVyFrtQql1cFV0,19204
|
|
9
|
+
disensor/vectores.py,sha256=_JjC2oV5IyIla3oi74fll35zyWckyGVOL3cGWo_9Bwg,7373
|
|
10
|
+
disensor-0.1.0.dist-info/licenses/LICENSE,sha256=t4qKwdMVRVCTTjV9XDHfnKKtw7yjc13NzGZl3UTFRdk,1072
|
|
11
|
+
disensor-0.1.0.dist-info/METADATA,sha256=8ZNJFnlm69A45UgyxPphyjVcv7odqsAvqJiAYYVzPk8,5552
|
|
12
|
+
disensor-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
13
|
+
disensor-0.1.0.dist-info/entry_points.txt,sha256=cbPFH8kO5rduMHYMxYgtG91W0ScAn7_bkqsQBWlOttU,47
|
|
14
|
+
disensor-0.1.0.dist-info/top_level.txt,sha256=HGsTycFBoef6Ov_p03H4FWufQWjP50AeIzDOCpE7rlQ,9
|
|
15
|
+
disensor-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nicolas Rocchia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
disensor
|