@dforce2055/dai 0.1.0
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/.env.example +30 -0
- package/CHANGELOG.md +46 -0
- package/CODE_OF_CONDUCT.md +37 -0
- package/CONTRIBUTING.md +66 -0
- package/LICENSE +674 -0
- package/README.md +288 -0
- package/SECURITY.md +37 -0
- package/VERSION +1 -0
- package/cli/dai.mjs +692 -0
- package/cli/lib/ac-hash.mjs +74 -0
- package/cli/lib/args.mjs +23 -0
- package/cli/lib/bootstrap.mjs +74 -0
- package/cli/lib/env.mjs +23 -0
- package/cli/lib/forge-api.mjs +96 -0
- package/cli/lib/forge-url.mjs +61 -0
- package/cli/lib/fsutil.mjs +24 -0
- package/cli/lib/implements.mjs +94 -0
- package/cli/lib/link-us.mjs +59 -0
- package/cli/lib/pm-adapter.mjs +59 -0
- package/cli/lib/pm-clickup.mjs +54 -0
- package/cli/lib/pm-jira.mjs +123 -0
- package/cli/lib/pr.mjs +53 -0
- package/cli/lib/us.mjs +36 -0
- package/docs/EJEMPLO-END-TO-END.md +330 -0
- package/docs/MANIFIESTO.md +114 -0
- package/docs/METODOLOGIA.md +254 -0
- package/docs/PROBAR.md +91 -0
- package/docs/SCRUM-CON-IA.md +190 -0
- package/docs/adr/0001-contrato-ac-hash.md +86 -0
- package/docs/adr/0002-agnostico-del-asistente.md +87 -0
- package/docs/adr/0003-deteccion-y-estampado-son-comandos.md +73 -0
- package/docs/adr/0004-ubicacion-y-schema-implements.md +94 -0
- package/docs/adr/0005-superficie-comandos-y-stamp.md +65 -0
- package/docs/adr/0006-distribucion-y-licencia.md +59 -0
- package/docs/adr/0007-modelo-de-autenticacion.md +63 -0
- package/docs/adr/README.md +19 -0
- package/docs/detalle/01-refinamiento.md +33 -0
- package/docs/detalle/02-planning.md +27 -0
- package/docs/detalle/03-ramas.md +32 -0
- package/docs/detalle/04-tdd.md +35 -0
- package/docs/detalle/05-smoke.md +32 -0
- package/docs/detalle/06-code-review.md +34 -0
- package/docs/detalle/07-merge-trazabilidad.md +33 -0
- package/docs/detalle/08-daily.md +29 -0
- package/docs/detalle/09-review.md +25 -0
- package/docs/detalle/10-retro.md +27 -0
- package/docs/detalle/README.md +20 -0
- package/docs/glosario.md +79 -0
- package/docs/guias/dev.md +66 -0
- package/docs/guias/lead.md +53 -0
- package/docs/guias/po.md +50 -0
- package/governance/branch-naming.md +36 -0
- package/governance/ci-rules.md +57 -0
- package/governance/commit-convention.md +76 -0
- package/index.html +479 -0
- package/install.sh +19 -0
- package/manifest.yaml +76 -0
- package/package.json +55 -0
- package/skills/dai-review/SKILL.md +78 -0
- package/skills/doc-to-backlog/SKILL.md +70 -0
- package/skills/doc-to-backlog/templates/backlog-candidato.md +49 -0
- package/skills/grill-epic/SKILL.md +76 -0
- package/skills/grill-intent/SKILL.md +43 -0
- package/skills/grill-intent/templates/intent.md +36 -0
- package/skills/grill-user-story/SKILL.md +76 -0
- package/skills/grill-user-story/templates/user-story.md +61 -0
- package/skills/link-us/SKILL.md +42 -0
- package/skills/link-us/templates/implements.yaml +16 -0
- package/skills/tdd/SKILL.md +109 -0
- package/skills/tdd/deep-modules.md +33 -0
- package/skills/tdd/interface-design.md +31 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/refactoring.md +10 -0
- package/skills/tdd/tests.md +61 -0
- package/templates/adr.md +43 -0
- package/templates/commit-msg +48 -0
- package/templates/definition-of-done.md +50 -0
- package/templates/definition-of-ready.md +51 -0
- package/templates/epica.md +62 -0
- package/templates/formato-us.md +129 -0
- package/templates/pull-request.md +62 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// dai · backend Jira del adaptador de PM (REST v3 / Jira Cloud, cara CLI).
|
|
2
|
+
// Auth: Basic (email + api_token) — token scopeado, no contraseña (ADR-0007).
|
|
3
|
+
// Config: DAI_JIRA_BASE_URL, DAI_JIRA_EMAIL, DAI_JIRA_TOKEN.
|
|
4
|
+
//
|
|
5
|
+
// Jira Cloud usa ADF (Atlassian Document Format, JSON) para descripción y comentarios,
|
|
6
|
+
// no markdown ni string. Así que: al LEER convertimos ADF → markdown (para que el
|
|
7
|
+
// hasher encuentre "## Criterios de aceptación"); al ESCRIBIR el stamp, componemos ADF.
|
|
8
|
+
|
|
9
|
+
import { parseUS } from "./us.mjs";
|
|
10
|
+
|
|
11
|
+
const trim = (b) => String(b || "").replace(/\/+$/, "");
|
|
12
|
+
|
|
13
|
+
export function jiraIssueUrl(base, id) {
|
|
14
|
+
return `${trim(base)}/rest/api/3/issue/${encodeURIComponent(id)}?fields=summary,description`;
|
|
15
|
+
}
|
|
16
|
+
export function jiraCommentUrl(base, id) {
|
|
17
|
+
return `${trim(base)}/rest/api/3/issue/${encodeURIComponent(id)}/comment`;
|
|
18
|
+
}
|
|
19
|
+
export function jiraAuthHeaders(env) {
|
|
20
|
+
const cred = Buffer.from(`${env.DAI_JIRA_EMAIL || ""}:${env.DAI_JIRA_TOKEN || ""}`).toString("base64");
|
|
21
|
+
return { Authorization: `Basic ${cred}`, Accept: "application/json", "Content-Type": "application/json" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── ADF → markdown (para leer la descripción) ────────────────────────────────
|
|
25
|
+
export function adfToMarkdown(node) {
|
|
26
|
+
if (node == null) return "";
|
|
27
|
+
if (Array.isArray(node)) return node.map(adfToMarkdown).join("");
|
|
28
|
+
switch (node.type) {
|
|
29
|
+
case "doc": return (node.content || []).map(adfToMarkdown).join("\n");
|
|
30
|
+
case "heading": return "#".repeat(node.attrs?.level || 1) + " " + (node.content || []).map(adfToMarkdown).join("") + "\n";
|
|
31
|
+
case "paragraph": return (node.content || []).map(adfToMarkdown).join("").trim() + "\n";
|
|
32
|
+
case "bulletList":
|
|
33
|
+
case "orderedList": return (node.content || []).map(adfToMarkdown).join("");
|
|
34
|
+
case "listItem": return "- " + (node.content || []).map(adfToMarkdown).join("").trim() + "\n";
|
|
35
|
+
case "text": return node.text || "";
|
|
36
|
+
case "hardBreak": return "\n";
|
|
37
|
+
default: return (node.content || []).map(adfToMarkdown).join("");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── markdown → ADF (para CREAR el issue: la descripción va en ADF) ────────────
|
|
42
|
+
// Parser de bloques: headings, párrafos y bullets. Suficiente para el formato de US.
|
|
43
|
+
export function markdownToAdf(md) {
|
|
44
|
+
const clean = (s) => s.replace(/[*_`]+/g, "").trim();
|
|
45
|
+
const content = [];
|
|
46
|
+
let para = [], bullets = null;
|
|
47
|
+
const flushPara = () => { if (para.length) { const t = clean(para.join(" ")); if (t) content.push({ type: "paragraph", content: [{ type: "text", text: t }] }); para = []; } };
|
|
48
|
+
const flushBullets = () => { if (bullets) { if (bullets.length) content.push({ type: "bulletList", content: bullets }); bullets = null; } };
|
|
49
|
+
for (const raw of String(md || "").split(/\r?\n/)) {
|
|
50
|
+
const line = raw.replace(/\s+$/, "");
|
|
51
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
52
|
+
const b = line.match(/^\s*[-*+]\s+(?:\[[ xX]\]\s+)?(.*)$/);
|
|
53
|
+
if (h) { flushPara(); flushBullets(); const t = clean(h[2]); if (t) content.push({ type: "heading", attrs: { level: h[1].length }, content: [{ type: "text", text: t }] }); }
|
|
54
|
+
else if (b) { flushPara(); if (!bullets) bullets = []; const t = clean(b[1]); if (t) bullets.push({ type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: t }] }] }); }
|
|
55
|
+
else if (line.trim() === "") { flushPara(); flushBullets(); }
|
|
56
|
+
else { flushBullets(); para.push(line.trim()); }
|
|
57
|
+
}
|
|
58
|
+
flushPara(); flushBullets();
|
|
59
|
+
if (content.length === 0) content.push({ type: "paragraph", content: [{ type: "text", text: " " }] });
|
|
60
|
+
return { type: "doc", version: 1, content };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Arma el texto de la US: summary (campo) + descripción (ADF→md o string).
|
|
64
|
+
export function jiraIssueToText(json) {
|
|
65
|
+
const f = json.fields || {};
|
|
66
|
+
const desc = typeof f.description === "string" ? f.description
|
|
67
|
+
: f.description ? adfToMarkdown(f.description) : "";
|
|
68
|
+
return `# ${f.summary || json.key || ""}\n\n${desc}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── cobertura → ADF (para escribir el comentario del stamp) ───────────────────
|
|
72
|
+
const STATUS = { "al-dia": "✅ al día", atrasado: "⚠️ atrasado", "sin-us": "❓ sin US" };
|
|
73
|
+
const txt = (s) => ({ type: "text", text: String(s) });
|
|
74
|
+
const link = (s, href) => ({ type: "text", text: String(s), marks: [{ type: "link", attrs: { href } }] });
|
|
75
|
+
|
|
76
|
+
export function renderCoverageAdf(id, r) {
|
|
77
|
+
const content = [
|
|
78
|
+
{ type: "heading", attrs: { level: 4 }, content: [txt(`Cobertura de ${id} — generado por dai stamp`)] },
|
|
79
|
+
{ type: "paragraph", content: [txt(`${r.repo} / ${r.change} @ ${r.version} (${r.ac_hash}) — ${STATUS[r.status] || r.status}`)] },
|
|
80
|
+
];
|
|
81
|
+
const items = [];
|
|
82
|
+
if (r.branchUrl) items.push({ type: "listItem", content: [{ type: "paragraph", content: [txt("branch: "), link(r.branch, r.branchUrl)] }] });
|
|
83
|
+
if (r.commitUrl) items.push({ type: "listItem", content: [{ type: "paragraph", content: [txt("commit: "), link((r.commit || "").slice(0, 8), r.commitUrl), txt(" (ancla durable)")] }] });
|
|
84
|
+
if (items.length) content.push({ type: "bulletList", content: items });
|
|
85
|
+
return { type: "doc", version: 1, content };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function jiraAdapter(env) {
|
|
89
|
+
const base = env.DAI_JIRA_BASE_URL;
|
|
90
|
+
if (!base) throw new Error("falta DAI_JIRA_BASE_URL en el .env (backend jira).");
|
|
91
|
+
return {
|
|
92
|
+
kind: "jira",
|
|
93
|
+
async fetchUS(id) {
|
|
94
|
+
const res = await fetch(jiraIssueUrl(base, id), { headers: jiraAuthHeaders(env) });
|
|
95
|
+
if (res.status === 404) return null;
|
|
96
|
+
if (!res.ok) throw new Error(`jira ${res.status}: ${await res.text()}`);
|
|
97
|
+
return { id, ...parseUS(jiraIssueToText(await res.json())) };
|
|
98
|
+
},
|
|
99
|
+
async stamp(id, record) {
|
|
100
|
+
const res = await fetch(jiraCommentUrl(base, id), {
|
|
101
|
+
method: "POST", headers: jiraAuthHeaders(env),
|
|
102
|
+
body: JSON.stringify({ body: renderCoverageAdf(id, record) }),
|
|
103
|
+
});
|
|
104
|
+
if (!res.ok) throw new Error(`jira ${res.status}: ${await res.text()}`);
|
|
105
|
+
return `${trim(base)}/browse/${id}`;
|
|
106
|
+
},
|
|
107
|
+
async createUS({ title, descriptionMarkdown }) {
|
|
108
|
+
const project = env.DAI_JIRA_PROJECT;
|
|
109
|
+
const issuetype = env.DAI_JIRA_ISSUETYPE || "Story";
|
|
110
|
+
if (!project) throw new Error("falta DAI_JIRA_PROJECT en el .env (la clave del proyecto donde crear el issue).");
|
|
111
|
+
const res = await fetch(`${trim(base)}/rest/api/3/issue`, {
|
|
112
|
+
method: "POST", headers: jiraAuthHeaders(env),
|
|
113
|
+
body: JSON.stringify({ fields: {
|
|
114
|
+
project: { key: project }, issuetype: { name: issuetype },
|
|
115
|
+
summary: title, description: markdownToAdf(descriptionMarkdown),
|
|
116
|
+
} }),
|
|
117
|
+
});
|
|
118
|
+
if (!res.ok) throw new Error(`jira ${res.status}: ${await res.text()}`);
|
|
119
|
+
const j = await res.json();
|
|
120
|
+
return { id: j.key, url: `${trim(base)}/browse/${j.key}` };
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
package/cli/lib/pr.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// dai · componer el cuerpo de una Pull/Merge Request desde el template (ADR-0005).
|
|
2
|
+
// Parte pura y testeable: rellena el template con los datos del link + git + check.
|
|
3
|
+
// Los efectos (git push, gh/glab create) viven en dai.mjs.
|
|
4
|
+
|
|
5
|
+
const EMOJI = { "al-dia": "✅ al día", atrasado: "⚠️ atrasado", "sin-us": "❓ sin US" };
|
|
6
|
+
|
|
7
|
+
// Reemplaza el cuerpo de una sección (## Heading … hasta el próximo ## o el final)
|
|
8
|
+
// por content. Tolerante: si no encuentra la sección, devuelve el body igual.
|
|
9
|
+
// Sin flag `m`: `$` = fin de string (no fin de línea), así no corta antes de tiempo.
|
|
10
|
+
export function replaceSection(body, heading, content) {
|
|
11
|
+
const esc = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
|
+
const re = new RegExp(`(^|\\n)(##[ \\t]*${esc}[ \\t]*\\n)([\\s\\S]*?)(\\n##\\s|$)`, "i");
|
|
13
|
+
if (!re.test(body)) return body;
|
|
14
|
+
return body.replace(re, (m, pre, h, _b, tail) => `${pre}${h}\n${content}\n${tail}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Rellena el template del PR con los datos precargados. Tolerante: reemplaza los
|
|
18
|
+
// placeholders que encuentra y deja el resto para que el humano lo edite.
|
|
19
|
+
export function composePrBody(template, d) {
|
|
20
|
+
let b = template;
|
|
21
|
+
b = b.replace(/`ABC-###`/g, `\`${d.id}\``);
|
|
22
|
+
b = b.replace(/@ `vX`/g, `@ \`${d.version}\``);
|
|
23
|
+
b = b.replace(/`<hash>`/g, `\`${d.ac_hash}\``);
|
|
24
|
+
b = b.replace(/verificado con `dai check` ✅/g, `verificado con \`dai check\`: ${EMOJI[d.status] || d.status}`);
|
|
25
|
+
|
|
26
|
+
// Descripción: default desde la US (el humano lo pule).
|
|
27
|
+
if (d.usTitle) b = replaceSection(b, "Descripción", `Implementa la US **${d.usTitle}** (\`${d.id}\`). Ver los criterios de aceptación en el tracker.`);
|
|
28
|
+
// Cambios realizados: de los commits de la branch (como gh pr create --fill).
|
|
29
|
+
if (d.commits && d.commits.length) {
|
|
30
|
+
b = replaceSection(b, "Cambios realizados", d.commits.map((c) => `- [x] ${c}`).join("\n"));
|
|
31
|
+
}
|
|
32
|
+
// Bloque de enlaces (lo agrega dai; el humano completa el resto del template).
|
|
33
|
+
const links = ["", "<!-- Enlaces precargados por `dai pr` -->"];
|
|
34
|
+
if (d.usUrl) links.push(`- US: ${d.usUrl}`);
|
|
35
|
+
if (d.branchUrl) links.push(`- branch \`${d.branch}\`: ${d.branchUrl}`);
|
|
36
|
+
if (d.commitUrl) links.push(`- commit \`${(d.commit || "").slice(0, 8)}\`: ${d.commitUrl}`);
|
|
37
|
+
return b.replace(/(##\s*Enlaces relacionados\s*\n)(<!--[\s\S]*?-->)?/i,
|
|
38
|
+
(m, h) => `${h}${links.join("\n")}\n`) === b
|
|
39
|
+
? b + "\n" + links.join("\n") + "\n" // si no había sección Enlaces, apéndela
|
|
40
|
+
: b.replace(/(##\s*Enlaces relacionados\s*\n)(<!--[\s\S]*?-->)?/i, (m, h) => `${h}${links.join("\n")}\n`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Título del PR: el pasado a mano, o "<ID>: <título de la US>", o solo el ID.
|
|
44
|
+
export function prTitle(opts, id, usTitle) {
|
|
45
|
+
if (opts.title) return opts.title;
|
|
46
|
+
if (usTitle) return `${id}: ${usTitle}`;
|
|
47
|
+
return id;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Herramienta de CLI del forge según el host del remoto.
|
|
51
|
+
export function forgeTool(forge) {
|
|
52
|
+
return forge === "gitlab" ? "glab" : "gh";
|
|
53
|
+
}
|
package/cli/lib/us.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// dai · primitivos de US y cobertura, compartidos por todos los backends de PM.
|
|
2
|
+
// Vive aparte de pm-adapter.mjs para que jira/clickup lo importen sin ciclos.
|
|
3
|
+
|
|
4
|
+
import { acHash } from "./ac-hash.mjs";
|
|
5
|
+
import { extractTitle } from "./link-us.mjs";
|
|
6
|
+
|
|
7
|
+
// Parseo puro de una US (markdown o texto) → identidad + hash vivo.
|
|
8
|
+
export function parseUS(raw) {
|
|
9
|
+
const title = extractTitle(raw);
|
|
10
|
+
const m = raw.match(/spec[_ ]version[^\n]*?\b(v\d+)\b/i);
|
|
11
|
+
return { title, spec_version: m ? m[1] : null, ac_hash: acHash(raw) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Compara el hash estampado (implements.yaml) con el hash vivo de la US.
|
|
15
|
+
export function coverageStatus(stampedHash, liveHash) {
|
|
16
|
+
if (liveHash == null) return "sin-us";
|
|
17
|
+
return stampedHash === liveHash ? "al-dia" : "atrasado";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const STATUS_LABEL = { "al-dia": "✅ al día", atrasado: "⚠️ atrasado", "sin-us": "❓ sin US" };
|
|
21
|
+
export const statusLabel = (s) => STATUS_LABEL[s] || s;
|
|
22
|
+
|
|
23
|
+
// Render de la cobertura como markdown (lo que un backend "estampa").
|
|
24
|
+
export function renderCoverage(id, r) {
|
|
25
|
+
const lines = [
|
|
26
|
+
`# Cobertura de ${id} · generado por dai stamp`,
|
|
27
|
+
"",
|
|
28
|
+
"| repo | change | versión | ac_hash | estado |",
|
|
29
|
+
"|------|--------|---------|---------|--------|",
|
|
30
|
+
`| ${r.repo} | ${r.change} | ${r.version} | ${r.ac_hash} | ${statusLabel(r.status)} |`,
|
|
31
|
+
"",
|
|
32
|
+
];
|
|
33
|
+
if (r.branchUrl) lines.push(`- branch: ${r.branch} → ${r.branchUrl}`);
|
|
34
|
+
if (r.commitUrl) lines.push(`- commit: ${r.commit} → ${r.commitUrl} (ancla durable)`);
|
|
35
|
+
return lines.join("\n") + "\n";
|
|
36
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Ejemplo end-to-end — El "golden path"
|
|
2
|
+
|
|
3
|
+
> **Para qué sirve.** Es la pieza que la gente copia. Una User Story real viajando
|
|
4
|
+
> de *idea vaga* a *desplegada*, mostrando **el artefacto concreto que produce cada
|
|
5
|
+
> uno de los 10 pasos** de [`SCRUM-CON-IA.md`](SCRUM-CON-IA.md). Si tienes que
|
|
6
|
+
> mostrar la metodología en una sola lectura, es esta.
|
|
7
|
+
>
|
|
8
|
+
> Dominio del ejemplo: **un carrito de compras** (checkout). Los nombres
|
|
9
|
+
> (`ABC-482`, `frontend`) son ilustrativos.
|
|
10
|
+
|
|
11
|
+
## El mapa: qué artefacto sale de cada paso
|
|
12
|
+
|
|
13
|
+
| Paso | Skill / evento | Artefacto que produce |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| 1 Refinamiento | `grill-intent` → `grill-user-story` | `intent.md` + la **US** (`ABC-482`) |
|
|
16
|
+
| 2 Planning | `opsx:propose` | `proposal.md` + `design.md` + `tasks.md` + `specs/` |
|
|
17
|
+
| 3 Rama | `link-us` | branch + `implements.yaml` |
|
|
18
|
+
| 4 TDD | `tdd` | test (RED) → código (GREEN) |
|
|
19
|
+
| 5 Smoke | skill de smoke | escenario end-to-end verde |
|
|
20
|
+
| 6 Code review | review propio del dev → `dai pr` → partner (con `/dai-review`) | PR (código + spec) + aprobación |
|
|
21
|
+
| 7 Merge | `dai stamp` | cobertura estampada en el tracker |
|
|
22
|
+
| 8 Daily | — (humano) | sincronización verbal |
|
|
23
|
+
| 9 Review/Demo | criterios Gherkin | US aceptada por el PO |
|
|
24
|
+
| 10 Retro | — (humano) | 1–2 mejoras |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Paso 1 — Refinamiento: de la idea vaga a la US testeable
|
|
29
|
+
|
|
30
|
+
### 1a · Cómo empieza (el ticket vago)
|
|
31
|
+
|
|
32
|
+
El PO crea un ticket en el gestor. Nace con **identidad** (`ABC-482`) y poco más:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
ABC-482 · "Arreglar lo del carrito que a veces deja comprar de más"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Impresentable para implementar. Acá entra la IA.
|
|
39
|
+
|
|
40
|
+
### 1b · Gate 0 — `grill-intent` desafía el problema
|
|
41
|
+
|
|
42
|
+
La skill no acepta el ticket: interroga el *problema* detrás (contra el
|
|
43
|
+
[`MANIFIESTO.md`](MANIFIESTO.md)).
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
IA: ¿Qué duele HOY, concretamente? "Comprar de más" es un síntoma.
|
|
47
|
+
PO: Un cliente finalizó la compra de un producto que ya no tenía stock, y quedó
|
|
48
|
+
una orden que no podemos cumplir. También pasa que finalizan con el carrito
|
|
49
|
+
vacío y se genera una orden fantasma.
|
|
50
|
+
IA: ¿Quién siente el dolor? "El sistema" no es un usuario.
|
|
51
|
+
PO: El comprador (recibe un error después de comprar) y depósito, que recibe
|
|
52
|
+
órdenes que no puede despachar.
|
|
53
|
+
IA: ¿Y si no lo hacemos? (costo de no actuar)
|
|
54
|
+
PO: Órdenes que se cancelan a mano, clientes enojados. Pasó bastante el último mes.
|
|
55
|
+
IA: La solución implícita es "validar el carrito al finalizar". ¿Es el salto correcto,
|
|
56
|
+
o el problema real es que el stock que muestra el carrito no es confiable?
|
|
57
|
+
PO: No, validar al finalizar es lo correcto: el stock ya existe, falta chequearlo
|
|
58
|
+
en el momento de la compra.
|
|
59
|
+
|
|
60
|
+
Veredicto: a-spec ✅ (problema real, usuario concreto, costo claro, sin solution-lock)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
→ produce `openspec/intents/20260703-checkout-invalido/intent.md` con ese veredicto.
|
|
64
|
+
|
|
65
|
+
### 1c · `grill-user-story` produce la US
|
|
66
|
+
|
|
67
|
+
Con el problema validado, la skill interroga hasta que la US es **testeable por
|
|
68
|
+
construcción** ([`formato-us.md`](../templates/formato-us.md)) y la publica en el gestor:
|
|
69
|
+
|
|
70
|
+
```markdown
|
|
71
|
+
# 🔗 Metadata de trazabilidad
|
|
72
|
+
| Campo | Valor |
|
|
73
|
+
|--------------|------------------------------|
|
|
74
|
+
| ID | ABC-482 |
|
|
75
|
+
| spec_version | v1 |
|
|
76
|
+
| Autor | J. Pérez (PO) |
|
|
77
|
+
| Estado | pulida |
|
|
78
|
+
| Repos esperados | frontend |
|
|
79
|
+
|
|
80
|
+
# Finalizar la compra del carrito
|
|
81
|
+
|
|
82
|
+
## Historia
|
|
83
|
+
Como **comprador**
|
|
84
|
+
quiero **finalizar la compra de mi carrito**
|
|
85
|
+
para **recibir los productos que elijo sin sorpresas**.
|
|
86
|
+
|
|
87
|
+
## Casos de uso
|
|
88
|
+
- **Happy path** — el comprador finaliza un carrito con productos con stock → se crea la orden.
|
|
89
|
+
- **Alternativo** — intenta finalizar con el carrito vacío → el sistema lo frena.
|
|
90
|
+
- **Excepción** — un producto del carrito no tiene stock → se rechaza indicando cuál.
|
|
91
|
+
|
|
92
|
+
## 🔗 Criterios de aceptación
|
|
93
|
+
- [ ] **AC-1** —
|
|
94
|
+
- **Dado** un carrito con productos que tienen stock
|
|
95
|
+
- **Cuando** el comprador finaliza la compra
|
|
96
|
+
- **Entonces** se crea la orden y el carrito queda vacío
|
|
97
|
+
- [ ] **AC-2** —
|
|
98
|
+
- **Dado** un carrito vacío
|
|
99
|
+
- **Cuando** se intenta finalizar la compra
|
|
100
|
+
- **Entonces** el sistema lo rechaza y NO crea ninguna orden
|
|
101
|
+
- [ ] **AC-3** —
|
|
102
|
+
- **Dado** un carrito con un producto sin stock
|
|
103
|
+
- **Cuando** se intenta finalizar la compra
|
|
104
|
+
- **Entonces** se rechaza indicando qué producto no tiene stock
|
|
105
|
+
|
|
106
|
+
## Fuera de scope
|
|
107
|
+
- El pago (pasarela, tarjetas) es otra US.
|
|
108
|
+
|
|
109
|
+
## Reglas de negocio
|
|
110
|
+
- Una orden creada descuenta el stock de cada producto.
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> Nota que la US **no dice** tablas, endpoints ni framework — solo el QUÉ. Y cada
|
|
114
|
+
> AC es un test en potencia ([Art. 3](./MANIFIESTO.md#art-3) del manifiesto).
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Paso 2 — Planning: `opsx:propose` deriva el CÓMO
|
|
119
|
+
|
|
120
|
+
El dev corre `opsx:explore` → `opsx:propose` sobre la US. OpenSpec genera el design
|
|
121
|
+
y las tareas. El dev valida y ajusta:
|
|
122
|
+
|
|
123
|
+
```markdown
|
|
124
|
+
# design.md (extracto)
|
|
125
|
+
## Enfoque
|
|
126
|
+
Guard de validación en el caso de uso `FinalizarCompra`. El carrito se valida ANTES
|
|
127
|
+
de crear la orden: no vacío, y todos sus productos con stock.
|
|
128
|
+
|
|
129
|
+
## Reglas de la transición
|
|
130
|
+
carrito con stock ──finalizar──► orden creada, carrito vacío (permitido)
|
|
131
|
+
carrito vacío ──finalizar──► ✗ CarritoVacioError
|
|
132
|
+
producto sin stock ──finalizar──► ✗ SinStockError(producto)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```markdown
|
|
136
|
+
# tasks.md (extracto)
|
|
137
|
+
- [ ] T1. Test: finalizar carrito con stock → orden creada, carrito vacío (AC-1)
|
|
138
|
+
- [ ] T2. Test: finalizar carrito vacío → CarritoVacioError, sin orden (AC-2)
|
|
139
|
+
- [ ] T3. Test: producto sin stock → SinStockError con el producto (AC-3)
|
|
140
|
+
- [ ] T4. Guard de validación en FinalizarCompra
|
|
141
|
+
- [ ] T5. Smoke end-to-end del flujo
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
> Las tareas **nacen del cómo**, definidas por quien va a implementar — no bajadas
|
|
145
|
+
> desde arriba (Art. 1).
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Paso 3 — Rama: `link-us` ata el código al QUÉ
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
$ dai link-us ABC-482 --us us.md --change finalizar-compra
|
|
153
|
+
✓ branch: feature/ABC-482-finalizar-la-compra-del-carrito
|
|
154
|
+
✓ archivo: openspec/changes/finalizar-compra/implements.yaml (ac_hash 7f3a9c2e)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
# implements.yaml — el ÚNICO link autorado a mano (schema ADR-0004)
|
|
159
|
+
change: finalizar-compra
|
|
160
|
+
repo: frontend
|
|
161
|
+
|
|
162
|
+
implements:
|
|
163
|
+
- id: ABC-482
|
|
164
|
+
version: v1
|
|
165
|
+
ac_hash: 7f3a9c2e # lo calculó `dai ac-hash` sobre los criterios de la US v1
|
|
166
|
+
|
|
167
|
+
introduces:
|
|
168
|
+
- guard-carrito-vacio
|
|
169
|
+
|
|
170
|
+
autor: D. Force (dev)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
> El key `ABC-482` **no se tipeó**: salió del argumento. La rama y el link son
|
|
174
|
+
> correctos por construcción (Art. 8, Art. 9).
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Paso 4 — TDD: un test a la vez (RED → GREEN)
|
|
179
|
+
|
|
180
|
+
Vertical slice del AC-2 (el guard del carrito vacío). **Primero el test (RED):**
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
test("un carrito vacío no se puede finalizar", async () => {
|
|
184
|
+
const carrito = await nuevoCarrito({ items: [] });
|
|
185
|
+
|
|
186
|
+
const accion = finalizarCompra(carrito.id);
|
|
187
|
+
|
|
188
|
+
await expect(accion).rejects.toThrow(CarritoVacioError);
|
|
189
|
+
expect(await ordenesDe(carrito.id)).toHaveLength(0); // NO se creó ninguna orden
|
|
190
|
+
});
|
|
191
|
+
// ▶ FALLA: finalizarCompra todavía no valida el carrito.
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Después el código mínimo (GREEN):**
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
export async function finalizarCompra(id: CarritoId) {
|
|
198
|
+
const carrito = await repo.obtener(id);
|
|
199
|
+
if (carrito.items.length === 0) throw new CarritoVacioError(id);
|
|
200
|
+
const sinStock = carrito.items.filter((i) => !hayStock(i));
|
|
201
|
+
if (sinStock.length > 0) throw new SinStockError(sinStock);
|
|
202
|
+
const orden = await crearOrden(carrito);
|
|
203
|
+
return repo.vaciar(carrito, orden);
|
|
204
|
+
}
|
|
205
|
+
// ▶ VERDE. Repetir el ciclo para AC-1 y AC-3.
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
> El test verifica por la **interfaz pública** (`finalizarCompra`, `ordenesDe`), no
|
|
209
|
+
> espía lo interno. Sobrevive a un refactor (Art. 7 + skill `tdd`).
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Paso 5 — Smoke: el flujo entero, verde
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
$ smoke checkout
|
|
217
|
+
✓ carrito con stock → orden creada, carrito vacío
|
|
218
|
+
✓ carrito vacío → rechazado, sin orden
|
|
219
|
+
✓ producto sin stock → rechazado indicando el producto
|
|
220
|
+
SMOKE OK (3/3)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Paso 6 — Code review: el dev primero, después un partner
|
|
226
|
+
|
|
227
|
+
**Primero el dev revisa la implementación de la IA** — minucioso y con criterio
|
|
228
|
+
(correctitud, casos borde, seguridad, calidad). El dev es responsable del código, no la IA
|
|
229
|
+
(anti vibe-coding). Ajusta y **commitea** lo que haga falta.
|
|
230
|
+
|
|
231
|
+
Con el smoke verde y **todo commiteado** (lo que quede suelto no entra en la PR), crea la
|
|
232
|
+
PR con `dai pr` — precargada con la US, el estado del check y los links (los **dos activos**:
|
|
233
|
+
código + spec trazable) — y la asigna a un partner:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
$ dai check
|
|
237
|
+
✅ ABC-482 al día (v1)
|
|
238
|
+
$ dai pr --assignee mgomez
|
|
239
|
+
✓ PR #123 creada → …/pull/123 (base: main · US: ABC-482 @ v1 · dai check ✅)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
El **partner** revisa la PR. Se apoya en la skill `/dai-review` para un primer pase con
|
|
243
|
+
comentario estándar, y **firma** aprobación o rechazo:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
🤖 /dai-review (primer pase, ayuda al partner)
|
|
247
|
+
· AC-2 cubierto y verificado (no se crea orden con carrito vacío). ✓
|
|
248
|
+
· Sugerencia: CarritoVacioError y SinStockError deberían extender un DomainError
|
|
249
|
+
común, como el resto del módulo.
|
|
250
|
+
· Sin problemas de correctitud.
|
|
251
|
+
|
|
252
|
+
👤 M. Gómez (partner): de acuerdo con el DomainError. Aprobado tras el ajuste.
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
> El dev revisa su propio código; un partner distinto revisa la PR y **firma** (Art. 5).
|
|
256
|
+
> La skill `/dai-review` le saca el ruido al partner, pero la aprobación la firma la persona.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Paso 7 — Merge: la trazabilidad se estampa sola
|
|
261
|
+
|
|
262
|
+
Al mergear, se corre `dai stamp` (el dev, o el CI si está automatizado — ADR-0003).
|
|
263
|
+
Lee el `implements.yaml` y **estampa la cobertura inversa** en el ticket `ABC-482`,
|
|
264
|
+
con links a la implementación:
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
ABC-482 · implementado por (lo estampó dai stamp)
|
|
268
|
+
┌──────────┬───────────────────┬─────────┬──────────┬───────────┐
|
|
269
|
+
│ repo │ change │ versión │ ac_hash │ estado │
|
|
270
|
+
├──────────┼───────────────────┼─────────┼──────────┼───────────┤
|
|
271
|
+
│ frontend │ finalizar-compra │ v1 │ 7f3a9c2e │ ✅ al día │
|
|
272
|
+
└──────────┴───────────────────┴─────────┴──────────┴───────────┘
|
|
273
|
+
branch → …/tree/feature/ABC-482-finalizar-la-compra-del-carrito
|
|
274
|
+
commit → …/commit/abc123 (ancla durable)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
> El estado se **deriva**, no se reporta (Art. 10). El link (branch + commit) hace
|
|
278
|
+
> que el ticket sea un router hacia la implementación real (§2.5).
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Paso 8 — Daily *(humano, a propósito)*
|
|
283
|
+
|
|
284
|
+
> *"Ayer cerré ABC-482, la validación del checkout con el guard de carrito. Hoy
|
|
285
|
+
> agarro ABC-490. Sin trabas."* — La IA no genera esto; el equipo se sincroniza (Art. 6).
|
|
286
|
+
|
|
287
|
+
## Paso 9 — Review / Demo
|
|
288
|
+
|
|
289
|
+
El PO valida contra los **mismos criterios que ya eran tests**. Finaliza un carrito,
|
|
290
|
+
prueba con uno vacío, prueba con un producto sin stock, ve los rechazos. AC-1, AC-2,
|
|
291
|
+
AC-3 verdes → **US aceptada**. Cero sorpresas: si el QUÉ hubiera cambiado, el
|
|
292
|
+
`@version` lo habría gritado antes.
|
|
293
|
+
|
|
294
|
+
## Paso 10 — Retro *(humano)*
|
|
295
|
+
|
|
296
|
+
> *"El Gate 0 nos ahorró rediseñar el stock, que no hacía falta. Mejora para el
|
|
297
|
+
> próximo sprint: sumar el smoke al pipeline y no correrlo a mano."* La matriz de
|
|
298
|
+
> trazabilidad aportó el dato; la decisión la tomó el equipo (Art. 6).
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Epílogo — Y cuando el QUÉ cambia (el `@version` gritando solo)
|
|
303
|
+
|
|
304
|
+
Dos sprints después, el PO agrega un criterio a `ABC-482` (ahora exige **avisar al
|
|
305
|
+
comprador qué productos quedaron sin stock, sin cancelar el resto del carrito**).
|
|
306
|
+
Sube la US a **v2** → cambia el `ac_hash`.
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
ABC-482 · implementado por
|
|
310
|
+
┌──────────┬───────────────────┬─────────┬──────────┬────────────────────────────┐
|
|
311
|
+
│ repo │ change │ versión │ ac_hash │ estado │
|
|
312
|
+
├──────────┼───────────────────┼─────────┼──────────┼────────────────────────────┤
|
|
313
|
+
│ frontend │ finalizar-compra │ v1 │ 7f3a9c2e │ ⚠️ ATRASADO (la US es v2) │
|
|
314
|
+
└──────────┴───────────────────┴─────────┴──────────┴────────────────────────────┘
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Nadie le avisó al dev: `dai check` lo marcó solo al re-derivar el hash de la US viva
|
|
318
|
+
y compararlo con el estampado (Art. 11). El dev abre una nueva iteración —mismo
|
|
319
|
+
`ABC-482`, ahora contra v2— y el ciclo vuelve a empezar desde el paso 3.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Qué demuestra este recorrido
|
|
324
|
+
|
|
325
|
+
- **Los 10 pasos son tu Scrum de siempre** — solo que en cada uno hay una skill.
|
|
326
|
+
- **El link nunca se escribió dos veces**: `implements.yaml` una vez, la cobertura
|
|
327
|
+
se derivó.
|
|
328
|
+
- **Nada llegó a producción sin ser testeable y trazable** (Arts. 3, 9, 10).
|
|
329
|
+
- **Los rituales humanos siguieron siendo humanos** (Art. 6).
|
|
330
|
+
- Y cuando el negocio cambió, **el desajuste se hizo visible solo** (Art. 11).
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Manifiesto — Desarrollo Asistido por IA
|
|
2
|
+
|
|
3
|
+
> **Qué es esto.** La *constitución* de la metodología: los principios inmutables
|
|
4
|
+
> contra los que se mide toda decisión. Cuando `grill-intent` desafía un problema,
|
|
5
|
+
> lo hace contra estos artículos. Cuando dudas si algo "va con el método", la
|
|
6
|
+
> respuesta está acá. Es corto a propósito — un manifiesto que no se puede citar de
|
|
7
|
+
> memoria no gobierna nada.
|
|
8
|
+
>
|
|
9
|
+
> **Cómo se usa.** Es el input de constitución que las skills asumen (junto con
|
|
10
|
+
> `openspec/project.md` y `CLAUDE.md`). No se negocia por conveniencia de un
|
|
11
|
+
> sprint. Se cambia por decisión explícita del equipo, registrada como ADR.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Los cuatro valores
|
|
16
|
+
|
|
17
|
+
1. **El QUÉ y el CÓMO son cosas distintas, con dueños distintos.**
|
|
18
|
+
2. **La IA asiste; la persona decide.**
|
|
19
|
+
3. **Nada existe si no se puede testear ni trazar.**
|
|
20
|
+
4. **La ceremonia se agrega cuando duele, no antes.**
|
|
21
|
+
|
|
22
|
+
Todo lo que sigue son artículos que hacen operativos esos cuatro valores.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## I. Sobre el QUÉ y el CÓMO
|
|
27
|
+
|
|
28
|
+
<a id="art-1"></a>**Art. 1 — Separación de responsabilidades.**
|
|
29
|
+
El *funcional* define **qué** hay que hacer y **por qué**. El *técnico* define
|
|
30
|
+
**cómo**. Nadie invade el terreno del otro: la US no dice endpoints ni tablas; el
|
|
31
|
+
design no re-discute el valor de negocio.
|
|
32
|
+
|
|
33
|
+
<a id="art-2"></a>**Art. 2 — El QUÉ es tool-agnóstico.**
|
|
34
|
+
El requerimiento vive con una identidad y una forma mínima que **no dependen de la
|
|
35
|
+
herramienta de abajo** (OpenSpec, Swagger, YAML, Markdown). Cambiar la herramienta
|
|
36
|
+
técnica no rompe el link.
|
|
37
|
+
|
|
38
|
+
<a id="art-3"></a>**Art. 3 — Testeable o no existe.**
|
|
39
|
+
Un criterio de aceptación que no se puede volver un test, no es un criterio. *"El
|
|
40
|
+
usuario tiene una buena experiencia"* se rechaza. *"Un carrito vacío no se puede
|
|
41
|
+
finalizar"* se acepta. Si no es verificable, no entra.
|
|
42
|
+
|
|
43
|
+
## II. Sobre la IA y el humano (HITL)
|
|
44
|
+
|
|
45
|
+
<a id="art-4"></a>**Art. 4 — La IA saca a preguntas, no inventa.**
|
|
46
|
+
El QUÉ se produce **por interrogación** (`grill-*`), no por generación. La IA no
|
|
47
|
+
adivina requerimientos: presiona hasta que la persona los explicita. Un QUÉ que la
|
|
48
|
+
IA "completó sola" es una alucinación con formato lindo.
|
|
49
|
+
|
|
50
|
+
<a id="art-5"></a>**Art. 5 — La persona firma.**
|
|
51
|
+
Toda decisión irreversible o de negocio —aceptar una US, aprobar un PR, descartar
|
|
52
|
+
un problema— la toma y la firma un humano. La IA propone; nunca autoriza.
|
|
53
|
+
|
|
54
|
+
<a id="art-6"></a>**Art. 6 — Los rituales de coordinación son humanos.**
|
|
55
|
+
El *daily* y la *retro* se hacen a mano, a propósito. Son donde el equipo se
|
|
56
|
+
apropia del proceso y lo entiende. La IA puede darles datos; no los reemplaza.
|
|
57
|
+
|
|
58
|
+
<a id="art-7"></a>**Art. 7 — No vibe coding.**
|
|
59
|
+
No se improvisa código sobre una idea vaga. Toda implementación arranca de una US
|
|
60
|
+
bien definida, pasa por un design, y se construye con tests. La disciplina no es
|
|
61
|
+
opcional: es lo que separa este método de "pedirle cosas a un chat".
|
|
62
|
+
|
|
63
|
+
## III. Sobre la trazabilidad
|
|
64
|
+
|
|
65
|
+
<a id="art-8"></a>**Art. 8 — Identidad estable.**
|
|
66
|
+
Todo QUÉ nace con un ID único, independiente del path y del formato. Es lo único
|
|
67
|
+
que lo hace linkeable. No se inventa un esquema nuevo: es el ticket del gestor (o
|
|
68
|
+
el change en el nivel más chico).
|
|
69
|
+
|
|
70
|
+
<a id="art-9"></a>**Art. 9 — El link se autora una sola vez.**
|
|
71
|
+
El CÓMO declara `implements: <id>@<version>`. Es el **único** link escrito a mano.
|
|
72
|
+
La dirección inversa (quién implementó qué) **siempre se deriva, nunca se escribe**.
|
|
73
|
+
Escribirlo en los dos lados es firmar la desincronización.
|
|
74
|
+
|
|
75
|
+
<a id="art-10"></a>**Art. 10 — El estado se deriva, no se reporta.**
|
|
76
|
+
La matriz de trazabilidad —quién implementó qué, contra qué versión, quién quedó
|
|
77
|
+
atrasado— no la mantiene ninguna persona. La calcula la máquina a partir de los
|
|
78
|
+
links. Si alguien la actualiza a mano, algo está mal diseñado.
|
|
79
|
+
|
|
80
|
+
<a id="art-11"></a>**Art. 11 — El versionado avisa solo.**
|
|
81
|
+
El `@version` (número legible + hash de criterios) hace que un cambio del QUÉ marque
|
|
82
|
+
solo a los CÓMO atrasados. Nadie avisa a nadie: el link versionado lo grita. El
|
|
83
|
+
número comunica; el hash detecta.
|
|
84
|
+
|
|
85
|
+
<a id="art-12"></a>**Art. 12 — Capacidad entera, detalle on-demand.**
|
|
86
|
+
El link es a nivel de capacidad/US entera, no criterio-por-criterio. Si el QUÉ sabe
|
|
87
|
+
*quién* lo implementó, el detalle fino se resuelve leyendo el repo por ID. El índice
|
|
88
|
+
central es un **router, no un almacén**.
|
|
89
|
+
|
|
90
|
+
## IV. Sobre la escala
|
|
91
|
+
|
|
92
|
+
<a id="art-13"></a>**Art. 13 — Un protocolo, varios niveles de ceremonia.**
|
|
93
|
+
No hay una metodología para equipos chicos y otra para grandes. Hay **un** protocolo
|
|
94
|
+
invariante (Arts. 1–12) y un dial de ceremonia que sube o baja según la escala. El
|
|
95
|
+
que aprende el nivel chico ya sabe el grande.
|
|
96
|
+
|
|
97
|
+
<a id="art-14"></a>**Art. 14 — No adelantar complejidad.**
|
|
98
|
+
Cada capa de plomería (tracker externo, CI que estampa, matriz de ambientes) se
|
|
99
|
+
agrega **cuando duele, no antes**. Empezar con la maquinaria completa "por las
|
|
100
|
+
dudas" es tan malo como no tener método.
|
|
101
|
+
|
|
102
|
+
<a id="art-15"></a>**Art. 15 — Los roles se colapsan, el link no.**
|
|
103
|
+
Cuando una misma persona es autor del QUÉ y del CÓMO, los gates se aligeran (un
|
|
104
|
+
auto-check honesto en vez de la firma de otro), pero el link `implements` **sigue
|
|
105
|
+
existiendo**. La ceremonia se achica; la trazabilidad no se negocia.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Cómo se enmienda
|
|
110
|
+
|
|
111
|
+
Estos artículos se cambian solo por **decisión explícita del equipo**, registrada
|
|
112
|
+
como un ADR con fecha y motivo. Ningún sprint, deadline ni "esta vez es distinto"
|
|
113
|
+
alcanza para saltárselos en silencio. Si un artículo estorba seguido, esa es la
|
|
114
|
+
señal de que hay que debatirlo y enmendarlo — no de ignorarlo.
|