@imolko/ultra-agent 0.2.0-beta → 1.0.0-beta
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.
- package/dist-node/materializer.d.ts.map +1 -1
- package/dist-node/materializer.js +2 -1
- package/dist-node/materializer.js.map +1 -1
- package/dist-node/payload.d.ts +18 -4
- package/dist-node/payload.d.ts.map +1 -1
- package/dist-node/payload.js +27 -6
- package/dist-node/payload.js.map +1 -1
- package/docs/adr/ADR-0000-proceso-y-formato-de-adrs.md +2 -2
- package/docs/adr/foundational/A-052-distribucion-metodologia-paquete-versionado.md +1 -1
- package/package.json +8 -2
- package/scripts/adr-gate/links.mjs +59 -0
- package/scripts/adr-gate/registry.mjs +155 -0
- package/scripts/adr-gate/report.mjs +21 -0
- package/scripts/adr-gate/rules/enlaces.mjs +33 -0
- package/scripts/adr-gate/rules/frontmatter.mjs +126 -0
- package/scripts/adr-gate/rules/hardcoded.mjs +63 -0
- package/scripts/adr-gate/rules/indices.mjs +100 -0
- package/scripts/adr-gate/rules/jerarquia.mjs +102 -0
- package/scripts/adr-gate/text.mjs +263 -0
- package/scripts/ensure-bin-exec.mjs +8 -3
- package/scripts/verify-adr-consistency.mjs +71 -0
- package/skills/writing-for-agents/references/adrs.md +2 -2
- package/scripts/verify-adr-consistency.py +0 -250
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Gate de consistencia del registro ADR (ADR-0000).
|
|
3
|
-
|
|
4
|
-
Valida cinco reglas y sale con código 0 solo si no hay violaciones:
|
|
5
|
-
- jerarquia los ADRs no referencian tiers prohibidos (A solo cita A; T cita A y T;
|
|
6
|
-
P cita A y P). Detecta tanto enlaces markdown como citas en texto plano a
|
|
7
|
-
IDs `<PREFIJO>-NNN`: una cita con un prefijo no permitido para el tier —o
|
|
8
|
-
con cualquier otro prefijo, incluidos G y PJ— es una violación, porque
|
|
9
|
-
ningún ADR A/T/P referencia el tier de proyecto. Una misma referencia
|
|
10
|
-
prohibida se reporta una sola vez aunque aparezca como enlace y como ID.
|
|
11
|
-
- enlaces todos los enlaces markdown internos (relativos, *.md) resuelven a un
|
|
12
|
-
archivo existente.
|
|
13
|
-
- frontmatter el título H1 sigue `# <TIER>-<NNN>: Título` y los campos `originally` /
|
|
14
|
-
`Enriquecido desde` usan la ortografía exacta.
|
|
15
|
-
- indices cada ADR aparece en el índice de su tier y el índice no lista ADRs
|
|
16
|
-
inexistentes.
|
|
17
|
-
- hardcoded los ADRs activos no duplican listas cuya fuente canónica vive en el
|
|
18
|
-
paquete (skills → @imolko/ultra-agent) ni en docs/agents/ (punteros).
|
|
19
|
-
|
|
20
|
-
Uso: python3 scripts/verify-adr-consistency.py
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
import re
|
|
24
|
-
import sys
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
|
|
27
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
28
|
-
ADR_DIR = ROOT / "docs" / "adr"
|
|
29
|
-
|
|
30
|
-
TIER_DIRS = {
|
|
31
|
-
"A": ADR_DIR / "foundational",
|
|
32
|
-
"T": ADR_DIR / "typescript",
|
|
33
|
-
"P": ADR_DIR / "python",
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
H1_RE = re.compile(r"^# (A|T|P)-\d{3}: .+")
|
|
37
|
-
ORIGINALLY_RE = re.compile(r"^\*\*originally\*\*:", flags=re.MULTILINE)
|
|
38
|
-
ORIGINALLY_WRONG_RE = re.compile(r"^\*\*[Oo]riginally\*\*:", flags=re.MULTILINE)
|
|
39
|
-
ENRICHED_RE = re.compile(r"^\*\*Enriquecido desde\*\*:", flags=re.MULTILINE)
|
|
40
|
-
ENRICHED_WRONG_RE = re.compile(r"^\*\*[Ee]nriquecido [Dd]esde\*\*:", flags=re.MULTILINE)
|
|
41
|
-
|
|
42
|
-
# Anchors caducados: la fuente canónica de los skills pasó al paquete y el hub de
|
|
43
|
-
# punteros pasó a docs/agents/ (A-052). Un ADR activo no debe repetirlos.
|
|
44
|
-
STALE_ANCHORS = (".pi/skills/", ".pi/repo-config.md")
|
|
45
|
-
|
|
46
|
-
violations = []
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def report(rule: str, message: str) -> None:
|
|
50
|
-
violations.append(f"[{rule}] {message}")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def is_active_adr(text: str) -> bool:
|
|
54
|
-
match = re.search(r"^\*\*Estado\*\*: ?(.+)$", text, flags=re.MULTILINE)
|
|
55
|
-
if not match:
|
|
56
|
-
return True
|
|
57
|
-
state = match.group(1).strip()
|
|
58
|
-
return state not in ("Reemplazado", "Deprecado")
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def tier_adr_files(tier: str):
|
|
62
|
-
return sorted(TIER_DIRS[tier].glob(f"{tier}-*.md"))
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def link_targets(text: str):
|
|
66
|
-
"""Devuelve los destinos de los enlaces markdown inline `](dest)`. """
|
|
67
|
-
return re.findall(r"\]\(([^)]+)\)", text)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def resolve_link(adir_file: Path, target: str):
|
|
71
|
-
"""Resuelve un target de enlace relativo contra el directorio del ADR."""
|
|
72
|
-
target = target.strip()
|
|
73
|
-
if target.startswith(("<", ">")) or "://" in target or target.startswith("#"):
|
|
74
|
-
return None # URL externa, autolink o ancla pura — fuera del gate
|
|
75
|
-
# Ignorar fragmentos (#ancla)
|
|
76
|
-
target = target.split("#", 1)[0]
|
|
77
|
-
if not target:
|
|
78
|
-
return None
|
|
79
|
-
if not target.endswith(".md"):
|
|
80
|
-
return None
|
|
81
|
-
return (adir_file.parent / target).resolve()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def target_tier(resolved: Path):
|
|
85
|
-
"""Clasifica un archivo resuelto en su tier (o None si está fuera del registro)."""
|
|
86
|
-
try:
|
|
87
|
-
rel = resolved.relative_to(ADR_DIR)
|
|
88
|
-
except ValueError:
|
|
89
|
-
return None
|
|
90
|
-
parts = rel.parts
|
|
91
|
-
if len(parts) >= 2:
|
|
92
|
-
if parts[0] == "foundational":
|
|
93
|
-
return "A"
|
|
94
|
-
if parts[0] == "typescript":
|
|
95
|
-
return "T"
|
|
96
|
-
if parts[0] == "python":
|
|
97
|
-
return "P"
|
|
98
|
-
if rel.name.startswith("ADR-0000") or rel.name == "index.md":
|
|
99
|
-
return "meta"
|
|
100
|
-
return "project"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
ALLOWED_TARGETS = {
|
|
104
|
-
"A": {"A", "meta"},
|
|
105
|
-
"T": {"A", "T", "meta"},
|
|
106
|
-
"P": {"A", "P", "meta"},
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
# Citas en texto plano a un ID de ADR. Una a dos letras mayúsculas evita falsos
|
|
110
|
-
# positivos con identificadores de tres letras (ISO-8601, RFC-2119, SHA-256).
|
|
111
|
-
PLAIN_ID_RE = re.compile(r"\b([A-Z]{1,2})-\d{3}\b")
|
|
112
|
-
|
|
113
|
-
# Destino de un enlace markdown inline `](destino)`. Se elimina antes del escaneo
|
|
114
|
-
# de texto plano para que una referencia no se reporte también como cita.
|
|
115
|
-
LINK_TARGET_RE = re.compile(r"\]\([^)]*\)")
|
|
116
|
-
|
|
117
|
-
# Enlace markdown inline completo: etiqueta y destino. La etiqueta puede citar el
|
|
118
|
-
# ID cuando el destino no lo contiene (p. ej. `[T-001](../typescript/index.md)`).
|
|
119
|
-
LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)]*)\)")
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def rule_jerarquia() -> None:
|
|
123
|
-
for tier, allowed in ALLOWED_TARGETS.items():
|
|
124
|
-
allowed_prefixes = {t for t in allowed if t in TIER_DIRS}
|
|
125
|
-
for adr in tier_adr_files(tier):
|
|
126
|
-
text = adr.read_text(encoding="utf-8")
|
|
127
|
-
reported_ids = set()
|
|
128
|
-
|
|
129
|
-
for label, target in LINK_RE.findall(text):
|
|
130
|
-
resolved = resolve_link(adr, target)
|
|
131
|
-
if resolved is None:
|
|
132
|
-
continue
|
|
133
|
-
resolved_tier = target_tier(resolved)
|
|
134
|
-
if resolved_tier is None or resolved_tier in allowed:
|
|
135
|
-
continue
|
|
136
|
-
# Los IDs citados por el enlace —en la etiqueta o en el destino—
|
|
137
|
-
# quedan cubiertos por esta violación y no se reportan otra vez.
|
|
138
|
-
for match in PLAIN_ID_RE.finditer(f"{label} {target}"):
|
|
139
|
-
reported_ids.add(match.group(0))
|
|
140
|
-
report(
|
|
141
|
-
"jerarquia",
|
|
142
|
-
f"{adr.relative_to(ROOT)} referencia al tier {resolved_tier} ({target}), prohibido para el tier {tier}",
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
plain_text = LINK_TARGET_RE.sub("", text)
|
|
146
|
-
for match in PLAIN_ID_RE.finditer(plain_text):
|
|
147
|
-
prefix, ident = match.group(1), match.group(0)
|
|
148
|
-
if prefix in allowed_prefixes or ident in reported_ids:
|
|
149
|
-
continue
|
|
150
|
-
reported_ids.add(ident)
|
|
151
|
-
report(
|
|
152
|
-
"jerarquia",
|
|
153
|
-
f"{adr.relative_to(ROOT)} cita en texto plano {ident} (prefijo {prefix}), prohibido para el tier {tier}",
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def rule_enlaces() -> None:
|
|
158
|
-
for md in sorted(ADR_DIR.rglob("*.md")):
|
|
159
|
-
text = md.read_text(encoding="utf-8")
|
|
160
|
-
for target in link_targets(text):
|
|
161
|
-
resolved = resolve_link(md, target)
|
|
162
|
-
if resolved is None:
|
|
163
|
-
continue
|
|
164
|
-
if not resolved.exists():
|
|
165
|
-
report("enlaces", f"{md.relative_to(ROOT)} enlaza a un archivo inexistente: {target}")
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def rule_frontmatter() -> None:
|
|
169
|
-
for tier in ("A", "T", "P"):
|
|
170
|
-
for adr in tier_adr_files(tier):
|
|
171
|
-
text = adr.read_text(encoding="utf-8")
|
|
172
|
-
rel = adr.relative_to(ROOT)
|
|
173
|
-
|
|
174
|
-
first_line = text.splitlines()[0] if text.splitlines() else ""
|
|
175
|
-
if not H1_RE.match(first_line):
|
|
176
|
-
report("frontmatter", f"{rel}: el título H1 no sigue `# <TIER>-<NNN>: Título`")
|
|
177
|
-
|
|
178
|
-
if not ORIGINALLY_RE.search(text):
|
|
179
|
-
report("frontmatter", f"{rel}: falta el campo `**originally**:` (minúsculas)")
|
|
180
|
-
|
|
181
|
-
# Detectar ortografía incorrecta del campo
|
|
182
|
-
for line in text.splitlines():
|
|
183
|
-
stripped = line.strip()
|
|
184
|
-
if ORIGINALLY_WRONG_RE.match(stripped) and not ORIGINALLY_RE.match(stripped):
|
|
185
|
-
report("frontmatter", f"{rel}: `originally` debe ir en minúsculas")
|
|
186
|
-
break
|
|
187
|
-
|
|
188
|
-
for line in text.splitlines():
|
|
189
|
-
stripped = line.strip()
|
|
190
|
-
if ENRICHED_WRONG_RE.match(stripped) and not ENRICHED_RE.match(stripped):
|
|
191
|
-
report("frontmatter", f"{rel}: el campo opcional debe escribirse `**Enriquecido desde**:`")
|
|
192
|
-
break
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def rule_indices() -> None:
|
|
196
|
-
for tier in ("A", "T", "P"):
|
|
197
|
-
index = TIER_DIRS[tier] / "index.md"
|
|
198
|
-
if not index.exists():
|
|
199
|
-
report("indices", f"falta el índice del tier {tier}: {index.relative_to(ROOT)}")
|
|
200
|
-
continue
|
|
201
|
-
|
|
202
|
-
index_text = index.read_text(encoding="utf-8")
|
|
203
|
-
# Enlaces dentro del tier (`./<nombre>.md`)
|
|
204
|
-
index_targets = {t for t in link_targets(index_text) if t.startswith("./") and t.endswith(".md")}
|
|
205
|
-
|
|
206
|
-
adr_files = tier_adr_files(tier)
|
|
207
|
-
for adr in adr_files:
|
|
208
|
-
target = f"./{adr.name}"
|
|
209
|
-
if target not in index_targets:
|
|
210
|
-
report("indices", f"{adr.relative_to(ROOT)} no aparece en el índice de su tier")
|
|
211
|
-
|
|
212
|
-
for target in index_targets:
|
|
213
|
-
resolved = (TIER_DIRS[tier] / target[2:]).resolve()
|
|
214
|
-
if not resolved.exists():
|
|
215
|
-
report("indices", f"el índice {index.relative_to(ROOT)} lista un ADR inexistente: {target}")
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def rule_hardcoded() -> None:
|
|
219
|
-
for tier in ("A", "T", "P"):
|
|
220
|
-
for adr in tier_adr_files(tier):
|
|
221
|
-
text = adr.read_text(encoding="utf-8")
|
|
222
|
-
if not is_active_adr(text):
|
|
223
|
-
continue
|
|
224
|
-
for anchor in STALE_ANCHORS:
|
|
225
|
-
if anchor in text:
|
|
226
|
-
report(
|
|
227
|
-
"hardcoded",
|
|
228
|
-
f"{adr.relative_to(ROOT)} duplica el ancla caducada `{anchor}` (fuente canónica: paquete/docs/agents)",
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def main() -> int:
|
|
233
|
-
rule_jerarquia()
|
|
234
|
-
rule_enlaces()
|
|
235
|
-
rule_frontmatter()
|
|
236
|
-
rule_indices()
|
|
237
|
-
rule_hardcoded()
|
|
238
|
-
|
|
239
|
-
if violations:
|
|
240
|
-
for v in violations:
|
|
241
|
-
print(v, file=sys.stderr)
|
|
242
|
-
print(f"{len(violations)} violación(es) en el registro ADR.", file=sys.stderr)
|
|
243
|
-
return 1
|
|
244
|
-
|
|
245
|
-
print("Registro ADR consistente: 0 violaciones.")
|
|
246
|
-
return 0
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if __name__ == "__main__":
|
|
250
|
-
sys.exit(main())
|