@imolko/ultra-agent 0.1.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/README.md +93 -0
- package/bin/cli.js +2 -0
- package/dist-node/bin.d.ts +2 -0
- package/dist-node/bin.d.ts.map +1 -0
- package/dist-node/bin.js +5 -0
- package/dist-node/bin.js.map +1 -0
- package/dist-node/cli.d.ts +41 -0
- package/dist-node/cli.d.ts.map +1 -0
- package/dist-node/cli.js +170 -0
- package/dist-node/cli.js.map +1 -0
- package/dist-node/index.d.ts +14 -0
- package/dist-node/index.d.ts.map +1 -0
- package/dist-node/index.js +40 -0
- package/dist-node/index.js.map +1 -0
- package/dist-node/init-lint.d.ts +43 -0
- package/dist-node/init-lint.d.ts.map +1 -0
- package/dist-node/init-lint.js +84 -0
- package/dist-node/init-lint.js.map +1 -0
- package/dist-node/manifest.d.ts +33 -0
- package/dist-node/manifest.d.ts.map +1 -0
- package/dist-node/manifest.js +59 -0
- package/dist-node/manifest.js.map +1 -0
- package/dist-node/materializer.d.ts +52 -0
- package/dist-node/materializer.d.ts.map +1 -0
- package/dist-node/materializer.js +194 -0
- package/dist-node/materializer.js.map +1 -0
- package/dist-node/package-root.d.ts +5 -0
- package/dist-node/package-root.d.ts.map +1 -0
- package/dist-node/package-root.js +17 -0
- package/dist-node/package-root.js.map +1 -0
- package/dist-node/payload.d.ts +22 -0
- package/dist-node/payload.d.ts.map +1 -0
- package/dist-node/payload.js +49 -0
- package/dist-node/payload.js.map +1 -0
- package/dist-node/pointer-hub.d.ts +22 -0
- package/dist-node/pointer-hub.d.ts.map +1 -0
- package/dist-node/pointer-hub.js +50 -0
- package/dist-node/pointer-hub.js.map +1 -0
- package/dist-node/publication.cli.d.ts +9 -0
- package/dist-node/publication.cli.d.ts.map +1 -0
- package/dist-node/publication.cli.js +17 -0
- package/dist-node/publication.cli.js.map +1 -0
- package/dist-node/publication.d.ts +23 -0
- package/dist-node/publication.d.ts.map +1 -0
- package/dist-node/publication.js +47 -0
- package/dist-node/publication.js.map +1 -0
- package/dist-node/reconcile.cli.d.ts +11 -0
- package/dist-node/reconcile.cli.d.ts.map +1 -0
- package/dist-node/reconcile.cli.js +39 -0
- package/dist-node/reconcile.cli.js.map +1 -0
- package/dist-node/reconcile.d.ts +30 -0
- package/dist-node/reconcile.d.ts.map +1 -0
- package/dist-node/reconcile.js +51 -0
- package/dist-node/reconcile.js.map +1 -0
- package/dist-node/ship/prompts/fix.md +22 -0
- package/dist-node/ship/prompts/implement.md +34 -0
- package/dist-node/ship/prompts/review.md +28 -0
- package/dist-node/ship/ship.agent.d.ts +22 -0
- package/dist-node/ship/ship.agent.d.ts.map +1 -0
- package/dist-node/ship/ship.agent.js +142 -0
- package/dist-node/ship/ship.agent.js.map +1 -0
- package/dist-node/ship/ship.args.d.ts +15 -0
- package/dist-node/ship/ship.args.d.ts.map +1 -0
- package/dist-node/ship/ship.args.js +93 -0
- package/dist-node/ship/ship.args.js.map +1 -0
- package/dist-node/ship/ship.context.d.ts +12 -0
- package/dist-node/ship/ship.context.d.ts.map +1 -0
- package/dist-node/ship/ship.context.js +3 -0
- package/dist-node/ship/ship.context.js.map +1 -0
- package/dist-node/ship/ship.d.ts +4 -0
- package/dist-node/ship/ship.d.ts.map +1 -0
- package/dist-node/ship/ship.js +221 -0
- package/dist-node/ship/ship.js.map +1 -0
- package/dist-node/ship/ship.mr.d.ts +9 -0
- package/dist-node/ship/ship.mr.d.ts.map +1 -0
- package/dist-node/ship/ship.mr.js +171 -0
- package/dist-node/ship/ship.mr.js.map +1 -0
- package/dist-node/ship/ship.output.d.ts +9 -0
- package/dist-node/ship/ship.output.d.ts.map +1 -0
- package/dist-node/ship/ship.output.js +21 -0
- package/dist-node/ship/ship.output.js.map +1 -0
- package/dist-node/ship/ship.tracker.d.ts +37 -0
- package/dist-node/ship/ship.tracker.d.ts.map +1 -0
- package/dist-node/ship/ship.tracker.js +220 -0
- package/dist-node/ship/ship.tracker.js.map +1 -0
- package/dist-node/version.cli.d.ts +9 -0
- package/dist-node/version.cli.d.ts.map +1 -0
- package/dist-node/version.cli.js +30 -0
- package/dist-node/version.cli.js.map +1 -0
- package/dist-node/version.d.ts +24 -0
- package/dist-node/version.d.ts.map +1 -0
- package/dist-node/version.js +76 -0
- package/dist-node/version.js.map +1 -0
- package/docs/adr/ADR-0000-proceso-y-formato-de-adrs.md +115 -0
- package/docs/adr/foundational/A-001-clean-architecture-4-capas.md +88 -0
- package/docs/adr/foundational/A-002-cqrs-semantico.md +58 -0
- package/docs/adr/foundational/A-003-tdd.md +97 -0
- package/docs/adr/foundational/A-005-either-return-custom-methods.md +80 -0
- package/docs/adr/foundational/A-006-feature-completion-criteria.md +47 -0
- package/docs/adr/foundational/A-007-data-contract-fail-fast.md +109 -0
- package/docs/adr/foundational/A-008-bounded-context-decomposition.md +130 -0
- package/docs/adr/foundational/A-009-use-case-taxonomy.md +99 -0
- package/docs/adr/foundational/A-010-propiedad-emision-eventos-dominio.md +95 -0
- package/docs/adr/foundational/A-011-characterization-first-refactoring.md +90 -0
- package/docs/adr/foundational/A-012-helm-mecanismo-primario-despliegue.md +67 -0
- package/docs/adr/foundational/A-013-contrato-settings-unica-fuente-configuracion.md +74 -0
- package/docs/adr/foundational/A-014-generacion-artefactos-despliegue-desde-contrato-settings.md +75 -0
- package/docs/adr/foundational/A-015-cloudevents-estandar-eventos.md +85 -0
- package/docs/adr/foundational/A-016-cicd-gitlab-pipeline-raiz-pipelines-modulares.md +92 -0
- package/docs/adr/foundational/A-017-taxonomia-repositorios-librerias-sistemas.md +93 -0
- package/docs/adr/foundational/A-018-practicas-build-imagenes-docker.md +85 -0
- package/docs/adr/foundational/A-019-convenciones-autoria-helm-charts.md +97 -0
- package/docs/adr/foundational/A-020-entorno-desarrollo-estandarizado-devcontainer.md +58 -0
- package/docs/adr/foundational/A-022-formato-reporte-tests-ctrf.md +55 -0
- package/docs/adr/foundational/A-023-composicion-bloques-prompt-agentes.md +65 -0
- package/docs/adr/foundational/A-024-features-slices-verticales-portables.md +41 -0
- package/docs/adr/foundational/A-025-anatomia-mecanica-feature-puertos-adaptadores-drivers.md +104 -0
- package/docs/adr/foundational/A-026-infraestructura-compartida.md +44 -0
- package/docs/adr/foundational/A-027-patron-wiring-entry-point.md +67 -0
- package/docs/adr/foundational/A-029-verbos-intencion-metodos-lado-negocio.md +54 -0
- package/docs/adr/foundational/A-030-global-development-tooling.md +52 -0
- package/docs/adr/foundational/A-031-ubiquitous-language.md +64 -0
- package/docs/adr/foundational/A-032-domain-artifact-documentation-format.md +128 -0
- package/docs/adr/foundational/A-033-estandar-mensajes-commit.md +127 -0
- package/docs/adr/foundational/A-034-canonical-skill-set.md +64 -0
- package/docs/adr/foundational/A-035-captura-ideas-reanclaje-candies.md +51 -0
- package/docs/adr/foundational/A-036-adopcion-wayfinder-ortogonalidad-labels.md +37 -0
- package/docs/adr/foundational/A-037-formalizacion-taxonomia-labels.md +98 -0
- package/docs/adr/foundational/A-038-issues-referencian-skills-nombre.md +54 -0
- package/docs/adr/foundational/A-039-pipeline-ship-issues-hijos-spec.md +64 -0
- package/docs/adr/foundational/A-040-punteros-configuracion-repo-skills.md +44 -0
- package/docs/adr/foundational/A-041-estrategia-de-ramas.md +45 -0
- package/docs/adr/foundational/A-042-formato-documentos-issue-tracker.md +78 -0
- package/docs/adr/foundational/A-043-vocabulario-etiquetas-adrs.md +42 -0
- package/docs/adr/foundational/A-044-composicion-funcional-procesos-operaciones.md +44 -0
- package/docs/adr/foundational/A-045-guardrails-aplicables-no-directrices.md +48 -0
- package/docs/adr/foundational/A-046-metricas-de-forma-y-legibilidad-del-codigo.md +50 -0
- package/docs/adr/foundational/A-047-dependencias-explicitas-sin-globales-ambientales.md +40 -0
- package/docs/adr/foundational/A-048-cobertura-minima-codigo.md +39 -0
- package/docs/adr/foundational/A-049-materializacion-bucle-guardrails-extension-nativa-pi.md +49 -0
- package/docs/adr/foundational/A-050-el-caso-de-uso-pide-al-dominio-no-lee-su-representacion.md +50 -0
- package/docs/adr/foundational/A-051-orden-imports-por-distancia.md +47 -0
- package/docs/adr/foundational/A-052-distribucion-metodologia-paquete-versionado.md +42 -0
- package/docs/adr/foundational/A-053-errores-compartidos-entre-features-dos-hogares-por-capa.md +43 -0
- package/docs/adr/foundational/A-054-contrato-costura-driver-port-raw-data-canal-error.md +65 -0
- package/docs/adr/foundational/index.md +98 -0
- package/docs/adr/index.md +19 -0
- package/docs/adr/python/P-001-dependencias-por-feature-fastapi-depends.md +63 -0
- package/docs/adr/python/P-002-inyeccion-token-por-request-sin-persistirlo.md +35 -0
- package/docs/adr/python/P-003-sqlalchemy-async-connection-pooling.md +59 -0
- package/docs/adr/python/P-004-serializer-json-custom-modelos-pydantic.md +47 -0
- package/docs/adr/python/P-005-patrones-pitfalls-nicegui.md +148 -0
- package/docs/adr/python/P-006-convenciones-pytest.md +96 -0
- package/docs/adr/python/P-007-ci-python-alineacion-poetry-version-unica.md +55 -0
- package/docs/adr/python/P-008-empaquetado-infraestructura-compartida-por-tecnologia.md +38 -0
- package/docs/adr/python/index.md +14 -0
- package/docs/adr/typescript/T-001-ddd-artifact-validation-zod-primitives.md +136 -0
- package/docs/adr/typescript/T-002-test-documentation-jsdoc.md +133 -0
- package/docs/adr/typescript/T-003-readme-formatting-domain-artifacts.md +51 -0
- package/docs/adr/typescript/T-005-input-optionality-use-case-not-entity.md +130 -0
- package/docs/adr/typescript/T-006-small-enums-inlined-zod-regex.md +77 -0
- package/docs/adr/typescript/T-007-controller-input-zod-schema.md +79 -0
- package/docs/adr/typescript/T-008-expressive-use-case-pipe.md +207 -0
- package/docs/adr/typescript/T-009-mdx-brace-escaping.md +80 -0
- package/docs/adr/typescript/T-010-stack-tooling-typescript-dominio.md +53 -0
- package/docs/adr/typescript/T-011-ultra-reporter-herramienta-canonica-documentacion.md +52 -0
- package/docs/adr/typescript/T-012-npm-workspaces-estructura-monorepo-clean-architecture.md +54 -0
- package/docs/adr/typescript/T-013-definicion-eventos-versionados-ultra-ddd.md +45 -0
- package/docs/adr/typescript/T-014-publicacion-distribucion-librerias-npm-semver.md +53 -0
- package/docs/adr/typescript/T-015-method-placement-definition.md +35 -0
- package/docs/adr/typescript/T-016-contrato-settings-zod.md +34 -0
- package/docs/adr/typescript/T-017-characterization-tests-golden-files.md +39 -0
- package/docs/adr/typescript/T-018-procedencia-campos-frontmatter.md +55 -0
- package/docs/adr/typescript/T-019-ultra-generator-herramienta-canonica-generacion.md +35 -0
- package/docs/adr/typescript/T-020-ultra-settings-herramienta-canonica-exportacion-despliegue.md +36 -0
- package/docs/adr/typescript/T-021-pipe-flujo-datos-operadores-semantica-contexto-inmutable.md +105 -0
- package/docs/adr/typescript/T-022-construction-is-execution-single-boundary.md +112 -0
- package/docs/adr/typescript/T-023-registry-cli-declara-dependencias-reales.md +84 -0
- package/docs/adr/typescript/T-024-paquete-ultra-agent-manifiesto-pi-peers-cli.md +39 -0
- package/docs/adr/typescript/T-025-publicacion-npm-trusted-publishing-procedencia.md +59 -0
- package/docs/adr/typescript/index.md +30 -0
- package/eslint/index.mjs +721 -0
- package/package.json +94 -0
- package/pi/extensions/handoff/README.md +92 -0
- package/pi/extensions/handoff/handoff-prompt.txt +9 -0
- package/pi/extensions/handoff/index.ts +296 -0
- package/pi/extensions/handoff/package.json +12 -0
- package/pi/extensions/qa-guardrail/README.md +108 -0
- package/pi/extensions/qa-guardrail/adapters/cli.ts +100 -0
- package/pi/extensions/qa-guardrail/adapters/eslint.ts +90 -0
- package/pi/extensions/qa-guardrail/adapters/registry.ts +45 -0
- package/pi/extensions/qa-guardrail/baseline.ts +74 -0
- package/pi/extensions/qa-guardrail/cerco.ts +134 -0
- package/pi/extensions/qa-guardrail/check.ts +185 -0
- package/pi/extensions/qa-guardrail/config.ts +189 -0
- package/pi/extensions/qa-guardrail/delta.ts +52 -0
- package/pi/extensions/qa-guardrail/feedback.ts +26 -0
- package/pi/extensions/qa-guardrail/index.ts +311 -0
- package/pi/extensions/qa-guardrail/nstop.ts +82 -0
- package/pi/extensions/qa-guardrail/package.json +12 -0
- package/pi/extensions/qa-guardrail/selection.ts +98 -0
- package/pi/extensions/qa-guardrail/types.ts +64 -0
- package/scripts/copy-assets.sh +10 -0
- package/scripts/reconcile-tags.sh +94 -0
- package/scripts/release-beta.sh +64 -0
- package/scripts/release-build.sh +53 -0
- package/scripts/release.sh +51 -0
- package/scripts/verify-adr-consistency.py +217 -0
- package/skills/code-review/SKILL.md +66 -0
- package/skills/codebase-design/SKILL.md +46 -0
- package/skills/configure-repo/SKILL.md +33 -0
- package/skills/diagnose-bug/SKILL.md +63 -0
- package/skills/domain-modeling/SKILL.md +56 -0
- package/skills/grill-me/SKILL.md +34 -0
- package/skills/i-have-an-idea/SKILL.md +166 -0
- package/skills/implement/SKILL.md +22 -0
- package/skills/research/SKILL.md +17 -0
- package/skills/ship/SKILL.md +26 -0
- package/skills/spike/SKILL.md +27 -0
- package/skills/tdd/SKILL.md +68 -0
- package/skills/tdd/references/deep-modules.md +53 -0
- package/skills/tdd/references/interface-design.md +74 -0
- package/skills/tdd/references/mocking.md +80 -0
- package/skills/tdd/references/refactoring.md +49 -0
- package/skills/tdd/references/tests.md +184 -0
- package/skills/to-specs/SKILL.md +141 -0
- package/skills/to-tasks/SKILL.md +143 -0
- package/skills/triage/SKILL.md +73 -0
- package/skills/ultra-generator/SKILL.md +263 -0
- package/skills/ultra-generator/references/aggregate-root.md +149 -0
- package/skills/ultra-generator/references/domain-event.md +174 -0
- package/skills/ultra-generator/references/entity.md +133 -0
- package/skills/ultra-generator/references/service.md +92 -0
- package/skills/ultra-generator/references/value-object.md +135 -0
- package/skills/wayfinder/SKILL.md +201 -0
- package/skills/workflow/SKILL.md +31 -0
- package/skills/writing-for-agents/SKILL-MECHANICS.md +32 -0
- package/skills/writing-for-agents/SKILL.md +86 -0
- package/skills/writing-for-agents/references/adrs.md +19 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps a file to the whitelisted adapter that covers it, using the config's
|
|
3
|
+
* `include`/`exclude` globs. Pure path logic: no filesystem access.
|
|
4
|
+
*
|
|
5
|
+
* `minimatch` is loaded lazily (and cached) on first use, so loading this
|
|
6
|
+
* module — and the extension that imports it — never requires `minimatch` to
|
|
7
|
+
* be installed. If it cannot be resolved, selection fails closed (no file
|
|
8
|
+
* matches, hence no adapter runs), mirroring the lazy-linter contract of
|
|
9
|
+
* A-049.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
import { relative, resolve } from "node:path";
|
|
14
|
+
import type { QaGuardrailConfig, ToolEntry } from "./types.js";
|
|
15
|
+
|
|
16
|
+
/** Normalize a path to forward slashes for glob matching. */
|
|
17
|
+
export function toPosixPath(p: string): string {
|
|
18
|
+
return p.split("\\").join("/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Minimatch = (path: string, pattern: string, options?: { dot?: boolean }) => boolean;
|
|
22
|
+
|
|
23
|
+
let cachedMatcher: Minimatch | null | undefined;
|
|
24
|
+
|
|
25
|
+
function loadMinimatch(): Minimatch | null {
|
|
26
|
+
try {
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const minimatch = require("minimatch") as Minimatch;
|
|
29
|
+
return (path, pattern) => minimatch(path, pattern, { dot: true });
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getMatcher(): Minimatch | null {
|
|
36
|
+
if (cachedMatcher === undefined) {
|
|
37
|
+
cachedMatcher = loadMinimatch();
|
|
38
|
+
}
|
|
39
|
+
return cachedMatcher;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Test seam: replace or clear the cached glob matcher (fail-closed when null). */
|
|
43
|
+
export function setGlobMatcher(matcher: Minimatch | null | undefined): void {
|
|
44
|
+
cachedMatcher = matcher;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True when `absPath` is selected by the tool entry's include/exclude globs.
|
|
49
|
+
*
|
|
50
|
+
* - Files outside `cwd` never match.
|
|
51
|
+
* - An empty `include` list matches nothing (fail-closed, like ESLint flat config).
|
|
52
|
+
* - `exclude` wins over `include`.
|
|
53
|
+
* - A missing glob matcher matches nothing (fail-closed, never a load error).
|
|
54
|
+
*/
|
|
55
|
+
export function isFileSelected(absPath: string, entry: ToolEntry, cwd: string): boolean {
|
|
56
|
+
const rel = toPosixPath(relative(resolve(cwd), resolve(absPath)));
|
|
57
|
+
if (rel === "" || rel.startsWith("..")) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const matcher = getMatcher();
|
|
62
|
+
if (!matcher) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (entry.include.length === 0) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const included = entry.include.some((pattern) => matcher(rel, pattern));
|
|
70
|
+
if (!included) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (entry.exclude.length === 0) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const excluded = entry.exclude.some((pattern) => matcher(rel, pattern));
|
|
77
|
+
return !excluded;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Return the id of the first enabled tool that covers `file`, or `undefined`.
|
|
82
|
+
*/
|
|
83
|
+
export function selectAdapterId(
|
|
84
|
+
file: string,
|
|
85
|
+
config: QaGuardrailConfig,
|
|
86
|
+
cwd: string,
|
|
87
|
+
): string | undefined {
|
|
88
|
+
const absPath = resolve(cwd, file);
|
|
89
|
+
for (const entry of config.tools) {
|
|
90
|
+
if (entry.enabled === false) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (isFileSelected(absPath, entry, cwd)) {
|
|
94
|
+
return entry.id;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the qa-guardrail extension.
|
|
3
|
+
*
|
|
4
|
+
* These are the public contracts the modules exchange. They are intentionally
|
|
5
|
+
* small: a violation is the only shape that crosses the adapter boundary, and
|
|
6
|
+
* the config shape is the only shape that crosses the config boundary.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A single lint violation, normalized from whatever the linter reports. */
|
|
10
|
+
export interface Violation {
|
|
11
|
+
/** Rule id (e.g. "max-params"). `null` for fatal parse errors. */
|
|
12
|
+
ruleId: string | null;
|
|
13
|
+
/** Human-readable description from the linter. */
|
|
14
|
+
message: string;
|
|
15
|
+
/** 1-based line number. `0` when the linter reports no location. */
|
|
16
|
+
line: number;
|
|
17
|
+
/** 1-based column number. */
|
|
18
|
+
column: number;
|
|
19
|
+
/** ESLint severity: 1 = warning, 2 = error. */
|
|
20
|
+
severity: number;
|
|
21
|
+
endLine?: number;
|
|
22
|
+
endColumn?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One tool entry in `.pi/qa-guardrail.json`. */
|
|
26
|
+
export interface ToolEntry {
|
|
27
|
+
/** Whitelisted adapter id, e.g. "eslint". */
|
|
28
|
+
id: string;
|
|
29
|
+
/** Glob patterns of files this tool covers. Empty = covers nothing. */
|
|
30
|
+
include: string[];
|
|
31
|
+
/** Glob patterns of files this tool must never cover. */
|
|
32
|
+
exclude: string[];
|
|
33
|
+
/** Marker file names used to locate the project root (e.g. "package.json"). */
|
|
34
|
+
rootMarkers: string[];
|
|
35
|
+
/** Optional kill switch. Defaults to true. */
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Root shape of `.pi/qa-guardrail.json`. */
|
|
40
|
+
export interface QaGuardrailConfig {
|
|
41
|
+
tools: ToolEntry[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Context handed to an adapter's `lint` call. */
|
|
45
|
+
export interface LintContext {
|
|
46
|
+
/** Project root (directory containing the linter config, found via rootMarkers). */
|
|
47
|
+
root: string;
|
|
48
|
+
/** pi's current working directory. */
|
|
49
|
+
cwd: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Uniform adapter interface: `lint(file) → Violation[]`. */
|
|
53
|
+
export interface LintAdapter {
|
|
54
|
+
/** Whitelisted id, e.g. "eslint". */
|
|
55
|
+
id: string;
|
|
56
|
+
/**
|
|
57
|
+
* Lint one file and return its violations. Returns `[]` for a clean or
|
|
58
|
+
* ignored file. Never fails silently: when linting is impossible (linter
|
|
59
|
+
* missing or its configuration broken), the implementation throws — the
|
|
60
|
+
* runner catches, warns, and skips feedback. Loading the extension never
|
|
61
|
+
* requires the linter.
|
|
62
|
+
*/
|
|
63
|
+
lint(file: string, ctx: LintContext): Promise<Violation[]>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Copia los prompts de fase del subcomando ship al árbol compilado (dist-node),
|
|
5
|
+
# de modo que se publiquen como payload del paquete (A-039) y se lean en runtime.
|
|
6
|
+
echo "Copiando prompts de ship..."
|
|
7
|
+
mkdir -p dist-node/ship/prompts
|
|
8
|
+
cp -r src/ship/prompts/. dist-node/ship/prompts/
|
|
9
|
+
|
|
10
|
+
echo "Assets copiados correctamente."
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Reconciliación de etiquetas git desde el registry (T-025): cada versión
|
|
5
|
+
# publicada en npm que no tenga etiqueta git se etiqueta en el commit que la
|
|
6
|
+
# produjo, que npm expone como `gitHead`. Idempotente: las etiquetas que ya
|
|
7
|
+
# existen no se recrean ni se mueven.
|
|
8
|
+
#
|
|
9
|
+
# El push usa el remote configurado por `scripts/release-build.sh`, que lleva
|
|
10
|
+
# incrustada la credencial GITLAB_TOKEN (https://oauth2:${GITLAB_TOKEN}@...).
|
|
11
|
+
# No se imprime la URL del remote ni el valor de la variable.
|
|
12
|
+
|
|
13
|
+
source ./ci-helpers.sh
|
|
14
|
+
|
|
15
|
+
PACKAGE_NAME="@imolko/ultra-agent"
|
|
16
|
+
|
|
17
|
+
section_start "reconcile_tags" "Reconciliar etiquetas git con el registry"
|
|
18
|
+
|
|
19
|
+
# 1. Leer las versiones publicadas. Un paquete aún no publicado no tiene nada
|
|
20
|
+
# que reconciliar: la primera versión se publica a mano con 2FA (T-025).
|
|
21
|
+
set +e
|
|
22
|
+
PUBLISHED_JSON="$(npm view "$PACKAGE_NAME" versions --json 2>/dev/null)"
|
|
23
|
+
NPM_VIEW_CODE=$?
|
|
24
|
+
set -e
|
|
25
|
+
|
|
26
|
+
if [ "$NPM_VIEW_CODE" -ne 0 ] || [ -z "$PUBLISHED_JSON" ]; then
|
|
27
|
+
log_info "El paquete $PACKAGE_NAME aún no tiene versiones publicadas; no hay nada que reconciliar."
|
|
28
|
+
section_end "reconcile_tags"
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
VERSIONS_FILE="$(mktemp)"
|
|
33
|
+
INPUT_FILE="$(mktemp)"
|
|
34
|
+
trap 'rm -f "$VERSIONS_FILE" "$INPUT_FILE"' EXIT
|
|
35
|
+
|
|
36
|
+
# Convierte el array JSON de versiones en una versión por línea.
|
|
37
|
+
printf '%s\n' "$PUBLISHED_JSON" | node -e '
|
|
38
|
+
let data = "";
|
|
39
|
+
process.stdin.on("data", (c) => { data += c; });
|
|
40
|
+
process.stdin.on("end", () => {
|
|
41
|
+
const versions = JSON.parse(data) ?? [];
|
|
42
|
+
for (const version of versions) process.stdout.write(version + "\n");
|
|
43
|
+
});
|
|
44
|
+
' > "$VERSIONS_FILE"
|
|
45
|
+
|
|
46
|
+
# 2. Construye la entrada JSON para el CLI de reconciliación: las versiones
|
|
47
|
+
# publicadas con su `gitHead` (del registry, no del estado local) y las
|
|
48
|
+
# etiquetas git ya existentes.
|
|
49
|
+
{
|
|
50
|
+
printf '{"published":['
|
|
51
|
+
first=1
|
|
52
|
+
while IFS= read -r version; do
|
|
53
|
+
[ -z "$version" ] && continue
|
|
54
|
+
git_head="$(npm view "${PACKAGE_NAME}@${version}" gitHead --json 2>/dev/null || true)"
|
|
55
|
+
if [ "$first" -eq 1 ]; then first=0; else printf ','; fi
|
|
56
|
+
if [ -n "$git_head" ]; then
|
|
57
|
+
# `npm view ... --json` devuelve el valor ya codificado como cadena JSON.
|
|
58
|
+
printf '{"version":"%s","gitHead":%s}' "$version" "$git_head"
|
|
59
|
+
else
|
|
60
|
+
printf '{"version":"%s","gitHead":null}' "$version"
|
|
61
|
+
fi
|
|
62
|
+
done < "$VERSIONS_FILE"
|
|
63
|
+
|
|
64
|
+
printf '],"existingTags":['
|
|
65
|
+
first=1
|
|
66
|
+
while IFS= read -r tag; do
|
|
67
|
+
[ -z "$tag" ] && continue
|
|
68
|
+
if [ "$first" -eq 1 ]; then first=0; else printf ','; fi
|
|
69
|
+
printf '"%s"' "$tag"
|
|
70
|
+
done < <(git tag --list)
|
|
71
|
+
printf ']}\n'
|
|
72
|
+
} > "$INPUT_FILE"
|
|
73
|
+
|
|
74
|
+
# 3. La decisión (qué versiones faltan) vive en el CLI; el shell solo crea y
|
|
75
|
+
# empuja las etiquetas planeadas.
|
|
76
|
+
PLANS="$(node dist-node/reconcile.cli.js < "$INPUT_FILE")"
|
|
77
|
+
|
|
78
|
+
if [ -z "$PLANS" ]; then
|
|
79
|
+
log_info "Todas las versiones publicadas ya tienen etiqueta; nada que reconciliar."
|
|
80
|
+
else
|
|
81
|
+
while IFS=$'\t' read -r version commit; do
|
|
82
|
+
[ -z "$version" ] && continue
|
|
83
|
+
# Defensa de idempotencia: no recrear una etiqueta ya existente.
|
|
84
|
+
if git rev-parse -q --verify "refs/tags/$version" >/dev/null 2>&1; then
|
|
85
|
+
log_info "La etiqueta $version ya existe; se omite."
|
|
86
|
+
continue
|
|
87
|
+
fi
|
|
88
|
+
git tag "$version" "$commit"
|
|
89
|
+
git push origin "refs/tags/$version"
|
|
90
|
+
log_success "Etiqueta $version creada en $commit y empujada."
|
|
91
|
+
done <<< "$PLANS"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
section_end "reconcile_tags"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Publica la versión beta: la versión se deriva de los tipos de Conventional
|
|
5
|
+
# Commits (A-033 §7) — feat→minor, fix→patch, breaking→major — vía
|
|
6
|
+
# `dist-node/version.cli.js`. Sustituye las keywords ad hoc UPGRADE_*_VERSION.
|
|
7
|
+
|
|
8
|
+
DEFAULT_BASE_VERSION="0.0.0"
|
|
9
|
+
|
|
10
|
+
source ./ci-helpers.sh
|
|
11
|
+
|
|
12
|
+
section_start "reconcile_tags" "Reconciliar etiquetas git publicadas"
|
|
13
|
+
# T-025: antes de derivar la versión, etiquetar en su `gitHead` toda versión
|
|
14
|
+
# ya publicada que no tenga etiqueta. Idempotente; empuja con GITLAB_TOKEN.
|
|
15
|
+
bash scripts/reconcile-tags.sh
|
|
16
|
+
section_end "reconcile_tags"
|
|
17
|
+
|
|
18
|
+
section_start "new_version_beta" "Calcular la nueva versión beta"
|
|
19
|
+
LATEST_BETA_TAG=$(git tag --list "*-beta" --sort=-version:refname | head -n 1)
|
|
20
|
+
if [ -z "$LATEST_BETA_TAG" ]; then
|
|
21
|
+
BASE_VERSION="$DEFAULT_BASE_VERSION"
|
|
22
|
+
COMMITS=$(git log --pretty=format:"%s")
|
|
23
|
+
else
|
|
24
|
+
BASE_VERSION="${LATEST_BETA_TAG%-beta}"
|
|
25
|
+
COMMITS=$(git log "${LATEST_BETA_TAG}..HEAD" --pretty=format:"%s")
|
|
26
|
+
fi
|
|
27
|
+
log_info "Último tag beta: ${LATEST_BETA_TAG:-<ninguno>}"
|
|
28
|
+
log_info "Versión base: $BASE_VERSION"
|
|
29
|
+
|
|
30
|
+
set +e
|
|
31
|
+
NEXT_BASE=$(printf '%s\n' "$COMMITS" | node dist-node/version.cli.js "$BASE_VERSION")
|
|
32
|
+
code=$?
|
|
33
|
+
set -e
|
|
34
|
+
|
|
35
|
+
if [ "$code" -ne 0 ]; then
|
|
36
|
+
log_info "No hay commits que muevan versión (feat/fix/breaking); se omite la publicación beta."
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
NEW_VERSION="${NEXT_BASE}-beta"
|
|
41
|
+
log_success "Versión a publicar: $NEW_VERSION"
|
|
42
|
+
section_end "new_version_beta"
|
|
43
|
+
|
|
44
|
+
section_start "publish_npm" "Publicando en npm"
|
|
45
|
+
npm version "$NEW_VERSION" --no-git-tag-version --allow-same-version || exit 1
|
|
46
|
+
echo "@imolko:registry=https://registry.npmjs.org/" > .npmrc
|
|
47
|
+
# Token stage-only (NPM_TOKEN), no el JWT OIDC: en el camino directo npm lo
|
|
48
|
+
# reemplaza por el token intercambiado vía OIDC (NPM_ID_TOKEN); en staging
|
|
49
|
+
# es la credencial que el mantenedor aprueba con 2FA.
|
|
50
|
+
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
|
|
51
|
+
|
|
52
|
+
# T-025: el camino sale del claim `runner_environment` del token OIDC, no de
|
|
53
|
+
# una heurística sobre el runner. direct → npm publish (OIDC); staged →
|
|
54
|
+
# npm stage publish (token stage-only + aprobación 2FA).
|
|
55
|
+
PUBLICATION_PATH="$(node dist-node/publication.cli.js "${NPM_ID_TOKEN:-}")"
|
|
56
|
+
|
|
57
|
+
if [ "$PUBLICATION_PATH" = "direct" ]; then
|
|
58
|
+
log_info "Camino de publicación: direct (Trusted Publishing OIDC)"
|
|
59
|
+
npm publish --tag beta --access public || exit 1
|
|
60
|
+
else
|
|
61
|
+
log_info "Camino de publicación: staged (stage-only + 2FA)"
|
|
62
|
+
npm stage publish --tag beta --access public || exit 1
|
|
63
|
+
fi
|
|
64
|
+
section_end "publish_npm"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
source ./ci-helpers.sh
|
|
5
|
+
|
|
6
|
+
# Pisos de toolchain para el camino OIDC de Trusted Publishing (T-025).
|
|
7
|
+
# - Node ≥ 22.14.0: piso de runtime del flujo OIDC y de npm 11.
|
|
8
|
+
# - npm ≥ 11.5.1: por debajo de esta versión `npm publish` no completa el
|
|
9
|
+
# intercambio OIDC y el camino directo fallaría de forma silenciosa.
|
|
10
|
+
# Se verifica en el job para evitar deriva.
|
|
11
|
+
# - npm ≥ 11.21.0: piso de dist-tags por OIDC. Este flujo no publica dist-tags
|
|
12
|
+
# por OIDC, así que no se exige; queda documentado para no endurecer el job
|
|
13
|
+
# sin necesidad (T-025, notas adicionales del PRD).
|
|
14
|
+
REQUIRED_NODE="22.14.0"
|
|
15
|
+
REQUIRED_NPM="11.5.1"
|
|
16
|
+
|
|
17
|
+
section_start "verify_toolchain" "Verificar pisos de versión de node y npm"
|
|
18
|
+
node -v
|
|
19
|
+
NODE_VERSION="$(node -v | sed 's/^v//')"
|
|
20
|
+
if ! printf '%s\n%s\n' "$REQUIRED_NODE" "$NODE_VERSION" | sort -V -C; then
|
|
21
|
+
log_error "node $NODE_VERSION no cumple el piso $REQUIRED_NODE requerido para Trusted Publishing (OIDC)."
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
log_success "node $NODE_VERSION cumple el piso $REQUIRED_NODE"
|
|
25
|
+
|
|
26
|
+
NPM_VERSION="$(npm -v)"
|
|
27
|
+
log_info "npm $NPM_VERSION"
|
|
28
|
+
if ! printf '%s\n%s\n' "$REQUIRED_NPM" "$NPM_VERSION" | sort -V -C; then
|
|
29
|
+
log_error "npm $NPM_VERSION no cumple el piso $REQUIRED_NPM requerido para Trusted Publishing (OIDC)."
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
log_success "npm $NPM_VERSION cumple el piso $REQUIRED_NPM"
|
|
33
|
+
section_end "verify_toolchain"
|
|
34
|
+
|
|
35
|
+
section_start "configure_git" "Configurar git y remote de push"
|
|
36
|
+
git config --global user.email "${GIT_USER_EMAIL}"
|
|
37
|
+
git config --global user.name "${GIT_USER_NAME}"
|
|
38
|
+
# La credencial GITLAB_TOKEN queda incrustada en el remote. No se imprime ni
|
|
39
|
+
# `git remote -v` ni el valor de la variable: la URL no debe aparecer en claro.
|
|
40
|
+
git remote set-url origin "https://oauth2:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
|
|
41
|
+
section_end "configure_git"
|
|
42
|
+
|
|
43
|
+
section_start "fetch_tags" "Traer historial y etiquetas completas"
|
|
44
|
+
# Tolerar el clon ya completo: `--unshallow` falla si el repositorio no es
|
|
45
|
+
# superficial, y ese caso no es un error.
|
|
46
|
+
git fetch --unshallow --tags || true
|
|
47
|
+
git fetch origin "+refs/tags/*:refs/tags/*"
|
|
48
|
+
section_end "fetch_tags"
|
|
49
|
+
|
|
50
|
+
section_start "build" "Construir el paquete"
|
|
51
|
+
npm install
|
|
52
|
+
npm run build
|
|
53
|
+
section_end "build"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Publica la versión estable: promueve el último tag beta quitando el sufijo
|
|
5
|
+
# `-beta` (A-033 §7, canal beta→estable). No usa keywords ad hoc.
|
|
6
|
+
|
|
7
|
+
source ./ci-helpers.sh
|
|
8
|
+
|
|
9
|
+
section_start "reconcile_tags" "Reconciliar etiquetas git publicadas"
|
|
10
|
+
# T-025: antes de derivar la versión, etiquetar en su `gitHead` toda versión
|
|
11
|
+
# ya publicada que no tenga etiqueta. Idempotente; empuja con GITLAB_TOKEN.
|
|
12
|
+
bash scripts/reconcile-tags.sh
|
|
13
|
+
section_end "reconcile_tags"
|
|
14
|
+
|
|
15
|
+
section_start "new_version" "Calcular la versión estable a publicar"
|
|
16
|
+
LATEST_BETA_TAG=$(git tag --list "*-beta" --sort=-version:refname | head -n 1)
|
|
17
|
+
if [ -z "$LATEST_BETA_TAG" ]; then
|
|
18
|
+
log_error "No se encontró ningún tag beta para publicar"
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
RELEASE_VERSION="${LATEST_BETA_TAG%-beta}"
|
|
23
|
+
if git rev-parse "$RELEASE_VERSION" >/dev/null 2>&1; then
|
|
24
|
+
log_error "La versión $RELEASE_VERSION ya existe como tag"
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
log_success "Versión a publicar: $RELEASE_VERSION"
|
|
29
|
+
section_end "new_version"
|
|
30
|
+
|
|
31
|
+
section_start "publish_npm" "Publicando en npm"
|
|
32
|
+
npm version "$RELEASE_VERSION" --no-git-tag-version --allow-same-version || exit 1
|
|
33
|
+
echo "@imolko:registry=https://registry.npmjs.org/" > .npmrc
|
|
34
|
+
# Token stage-only (NPM_TOKEN), no el JWT OIDC: en el camino directo npm lo
|
|
35
|
+
# reemplaza por el token intercambiado vía OIDC (NPM_ID_TOKEN); en staging
|
|
36
|
+
# es la credencial que el mantenedor aprueba con 2FA.
|
|
37
|
+
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
|
|
38
|
+
|
|
39
|
+
# T-025: el camino sale del claim `runner_environment` del token OIDC, no de
|
|
40
|
+
# una heurística sobre el runner. direct → npm publish (OIDC); staged →
|
|
41
|
+
# npm stage publish (token stage-only + aprobación 2FA).
|
|
42
|
+
PUBLICATION_PATH="$(node dist-node/publication.cli.js "${NPM_ID_TOKEN:-}")"
|
|
43
|
+
|
|
44
|
+
if [ "$PUBLICATION_PATH" = "direct" ]; then
|
|
45
|
+
log_info "Camino de publicación: direct (Trusted Publishing OIDC)"
|
|
46
|
+
npm publish --access public || exit 1
|
|
47
|
+
else
|
|
48
|
+
log_info "Camino de publicación: staged (stage-only + 2FA)"
|
|
49
|
+
npm stage publish --access public || exit 1
|
|
50
|
+
fi
|
|
51
|
+
section_end "publish_npm"
|
|
@@ -0,0 +1,217 @@
|
|
|
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 no referencia T/P/proyecto;
|
|
6
|
+
T/P no se referencian entre sí ni al tier de proyecto).
|
|
7
|
+
- enlaces todos los enlaces markdown internos (relativos, *.md) resuelven a un
|
|
8
|
+
archivo existente.
|
|
9
|
+
- frontmatter el título H1 sigue `# <TIER>-<NNN>: Título` y los campos `originally` /
|
|
10
|
+
`Enriquecido desde` usan la ortografía exacta.
|
|
11
|
+
- indices cada ADR aparece en el índice de su tier y el índice no lista ADRs
|
|
12
|
+
inexistentes.
|
|
13
|
+
- hardcoded los ADRs activos no duplican listas cuya fuente canónica vive en el
|
|
14
|
+
paquete (skills → @imolko/ultra-agent) ni en docs/agents/ (punteros).
|
|
15
|
+
|
|
16
|
+
Uso: python3 scripts/verify-adr-consistency.py
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
24
|
+
ADR_DIR = ROOT / "docs" / "adr"
|
|
25
|
+
|
|
26
|
+
TIER_DIRS = {
|
|
27
|
+
"A": ADR_DIR / "foundational",
|
|
28
|
+
"T": ADR_DIR / "typescript",
|
|
29
|
+
"P": ADR_DIR / "python",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
H1_RE = re.compile(r"^# (A|T|P)-\d{3}: .+")
|
|
33
|
+
ORIGINALLY_RE = re.compile(r"^\*\*originally\*\*:", flags=re.MULTILINE)
|
|
34
|
+
ORIGINALLY_WRONG_RE = re.compile(r"^\*\*[Oo]riginally\*\*:", flags=re.MULTILINE)
|
|
35
|
+
ENRICHED_RE = re.compile(r"^\*\*Enriquecido desde\*\*:", flags=re.MULTILINE)
|
|
36
|
+
ENRICHED_WRONG_RE = re.compile(r"^\*\*[Ee]nriquecido [Dd]esde\*\*:", flags=re.MULTILINE)
|
|
37
|
+
|
|
38
|
+
# Anchors caducados: la fuente canónica de los skills pasó al paquete y el hub de
|
|
39
|
+
# punteros pasó a docs/agents/ (A-052). Un ADR activo no debe repetirlos.
|
|
40
|
+
STALE_ANCHORS = (".pi/skills/", ".pi/repo-config.md")
|
|
41
|
+
|
|
42
|
+
violations = []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def report(rule: str, message: str) -> None:
|
|
46
|
+
violations.append(f"[{rule}] {message}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_active_adr(text: str) -> bool:
|
|
50
|
+
match = re.search(r"^\*\*Estado\*\*: ?(.+)$", text, flags=re.MULTILINE)
|
|
51
|
+
if not match:
|
|
52
|
+
return True
|
|
53
|
+
state = match.group(1).strip()
|
|
54
|
+
return state not in ("Reemplazado", "Deprecado")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def tier_adr_files(tier: str):
|
|
58
|
+
return sorted(TIER_DIRS[tier].glob(f"{tier}-*.md"))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def link_targets(text: str):
|
|
62
|
+
"""Devuelve los destinos de los enlaces markdown inline `](dest)`. """
|
|
63
|
+
return re.findall(r"\]\(([^)]+)\)", text)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_link(adir_file: Path, target: str):
|
|
67
|
+
"""Resuelve un target de enlace relativo contra el directorio del ADR."""
|
|
68
|
+
target = target.strip()
|
|
69
|
+
if target.startswith(("<", ">")) or "://" in target or target.startswith("#"):
|
|
70
|
+
return None # URL externa, autolink o ancla pura — fuera del gate
|
|
71
|
+
# Ignorar fragmentos (#ancla)
|
|
72
|
+
target = target.split("#", 1)[0]
|
|
73
|
+
if not target:
|
|
74
|
+
return None
|
|
75
|
+
if not target.endswith(".md"):
|
|
76
|
+
return None
|
|
77
|
+
return (adir_file.parent / target).resolve()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def target_tier(resolved: Path):
|
|
81
|
+
"""Clasifica un archivo resuelto en su tier (o None si está fuera del registro)."""
|
|
82
|
+
try:
|
|
83
|
+
rel = resolved.relative_to(ADR_DIR)
|
|
84
|
+
except ValueError:
|
|
85
|
+
return None
|
|
86
|
+
parts = rel.parts
|
|
87
|
+
if len(parts) >= 2:
|
|
88
|
+
if parts[0] == "foundational":
|
|
89
|
+
return "A"
|
|
90
|
+
if parts[0] == "typescript":
|
|
91
|
+
return "T"
|
|
92
|
+
if parts[0] == "python":
|
|
93
|
+
return "P"
|
|
94
|
+
if rel.name.startswith("ADR-0000") or rel.name == "index.md":
|
|
95
|
+
return "meta"
|
|
96
|
+
return "project"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
ALLOWED_TARGETS = {
|
|
100
|
+
"A": {"A", "meta"},
|
|
101
|
+
"T": {"A", "T", "meta"},
|
|
102
|
+
"P": {"A", "P", "meta"},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def rule_jerarquia() -> None:
|
|
107
|
+
for tier, allowed in ALLOWED_TARGETS.items():
|
|
108
|
+
for adr in tier_adr_files(tier):
|
|
109
|
+
text = adr.read_text(encoding="utf-8")
|
|
110
|
+
for target in link_targets(text):
|
|
111
|
+
resolved = resolve_link(adr, target)
|
|
112
|
+
if resolved is None:
|
|
113
|
+
continue
|
|
114
|
+
resolved_tier = target_tier(resolved)
|
|
115
|
+
if resolved_tier is None:
|
|
116
|
+
continue
|
|
117
|
+
if resolved_tier not in allowed:
|
|
118
|
+
report(
|
|
119
|
+
"jerarquia",
|
|
120
|
+
f"{adr.relative_to(ROOT)} referencia al tier {resolved_tier} ({target}), prohibido para el tier {tier}",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def rule_enlaces() -> None:
|
|
125
|
+
for md in sorted(ADR_DIR.rglob("*.md")):
|
|
126
|
+
text = md.read_text(encoding="utf-8")
|
|
127
|
+
for target in link_targets(text):
|
|
128
|
+
resolved = resolve_link(md, target)
|
|
129
|
+
if resolved is None:
|
|
130
|
+
continue
|
|
131
|
+
if not resolved.exists():
|
|
132
|
+
report("enlaces", f"{md.relative_to(ROOT)} enlaza a un archivo inexistente: {target}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def rule_frontmatter() -> None:
|
|
136
|
+
for tier in ("A", "T", "P"):
|
|
137
|
+
for adr in tier_adr_files(tier):
|
|
138
|
+
text = adr.read_text(encoding="utf-8")
|
|
139
|
+
rel = adr.relative_to(ROOT)
|
|
140
|
+
|
|
141
|
+
first_line = text.splitlines()[0] if text.splitlines() else ""
|
|
142
|
+
if not H1_RE.match(first_line):
|
|
143
|
+
report("frontmatter", f"{rel}: el título H1 no sigue `# <TIER>-<NNN>: Título`")
|
|
144
|
+
|
|
145
|
+
if not ORIGINALLY_RE.search(text):
|
|
146
|
+
report("frontmatter", f"{rel}: falta el campo `**originally**:` (minúsculas)")
|
|
147
|
+
|
|
148
|
+
# Detectar ortografía incorrecta del campo
|
|
149
|
+
for line in text.splitlines():
|
|
150
|
+
stripped = line.strip()
|
|
151
|
+
if ORIGINALLY_WRONG_RE.match(stripped) and not ORIGINALLY_RE.match(stripped):
|
|
152
|
+
report("frontmatter", f"{rel}: `originally` debe ir en minúsculas")
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
for line in text.splitlines():
|
|
156
|
+
stripped = line.strip()
|
|
157
|
+
if ENRICHED_WRONG_RE.match(stripped) and not ENRICHED_RE.match(stripped):
|
|
158
|
+
report("frontmatter", f"{rel}: el campo opcional debe escribirse `**Enriquecido desde**:`")
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def rule_indices() -> None:
|
|
163
|
+
for tier in ("A", "T", "P"):
|
|
164
|
+
index = TIER_DIRS[tier] / "index.md"
|
|
165
|
+
if not index.exists():
|
|
166
|
+
report("indices", f"falta el índice del tier {tier}: {index.relative_to(ROOT)}")
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
index_text = index.read_text(encoding="utf-8")
|
|
170
|
+
# Enlaces dentro del tier (`./<nombre>.md`)
|
|
171
|
+
index_targets = {t for t in link_targets(index_text) if t.startswith("./") and t.endswith(".md")}
|
|
172
|
+
|
|
173
|
+
adr_files = tier_adr_files(tier)
|
|
174
|
+
for adr in adr_files:
|
|
175
|
+
target = f"./{adr.name}"
|
|
176
|
+
if target not in index_targets:
|
|
177
|
+
report("indices", f"{adr.relative_to(ROOT)} no aparece en el índice de su tier")
|
|
178
|
+
|
|
179
|
+
for target in index_targets:
|
|
180
|
+
resolved = (TIER_DIRS[tier] / target[2:]).resolve()
|
|
181
|
+
if not resolved.exists():
|
|
182
|
+
report("indices", f"el índice {index.relative_to(ROOT)} lista un ADR inexistente: {target}")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def rule_hardcoded() -> None:
|
|
186
|
+
for tier in ("A", "T", "P"):
|
|
187
|
+
for adr in tier_adr_files(tier):
|
|
188
|
+
text = adr.read_text(encoding="utf-8")
|
|
189
|
+
if not is_active_adr(text):
|
|
190
|
+
continue
|
|
191
|
+
for anchor in STALE_ANCHORS:
|
|
192
|
+
if anchor in text:
|
|
193
|
+
report(
|
|
194
|
+
"hardcoded",
|
|
195
|
+
f"{adr.relative_to(ROOT)} duplica el ancla caducada `{anchor}` (fuente canónica: paquete/docs/agents)",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main() -> int:
|
|
200
|
+
rule_jerarquia()
|
|
201
|
+
rule_enlaces()
|
|
202
|
+
rule_frontmatter()
|
|
203
|
+
rule_indices()
|
|
204
|
+
rule_hardcoded()
|
|
205
|
+
|
|
206
|
+
if violations:
|
|
207
|
+
for v in violations:
|
|
208
|
+
print(v, file=sys.stderr)
|
|
209
|
+
print(f"{len(violations)} violación(es) en el registro ADR.", file=sys.stderr)
|
|
210
|
+
return 1
|
|
211
|
+
|
|
212
|
+
print("Registro ADR consistente: 0 violaciones.")
|
|
213
|
+
return 0
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
sys.exit(main())
|