@dforce2055/dai 0.13.2 → 0.13.3
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/CHANGELOG.md +50 -0
- package/VERSION +1 -1
- package/cli/dai.mjs +21 -6
- package/cli/lib/pm-adapter.mjs +19 -2
- package/cli/lib/pm-clickup.mjs +1 -0
- package/cli/lib/pm-jira.mjs +1 -0
- package/cli/lib/pr.mjs +20 -6
- package/cli/lib/us.mjs +34 -2
- package/package.json +1 -1
- package/templates/pull-request.md +13 -9
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,55 @@
|
|
|
3
3
|
Formato basado en [Keep a Changelog](https://keepachangelog.com/). Versionado semver
|
|
4
4
|
(ver `VERSION`).
|
|
5
5
|
|
|
6
|
+
## [0.13.3] — 2026-09-02
|
|
7
|
+
|
|
8
|
+
**Una PR de dai se abría diciendo, en el mismo párrafo, dos cosas que no encajaban: que el
|
|
9
|
+
spec estaba "verificado con dai check: ✅ al día", y a continuación una explicación general
|
|
10
|
+
del método. Tirando de ese hilo aparecieron dos bugs distintos, y los dos eran dai afirmando
|
|
11
|
+
cosas que no le constaban.**
|
|
12
|
+
|
|
13
|
+
### Arreglado
|
|
14
|
+
- **El relleno del estado reescribía la prosa del template.** `dai pr` hacía un replace
|
|
15
|
+
**global** de `verificado con `dai check` ✅`, y esa frase estaba dos veces en el molde: en
|
|
16
|
+
el dato (`## 🔗 Implementa`) y en la prosa que explica el método. Así que a una oración
|
|
17
|
+
general —igual en todas las PRs— dai le insertaba el estado de *esta* PR, y quedaba
|
|
18
|
+
publicado: *"verificado con `dai check`: ✅ al día. Sin esto, el código no sabe a qué QUÉ
|
|
19
|
+
responde…"*. Ni doctrina ni dato. Ahora el relleno se acota a la sección; sin la sección
|
|
20
|
+
(un template ajeno con otra forma) cae al body entero, porque rellenar de más es
|
|
21
|
+
recuperable y publicar `ABC-###` no.
|
|
22
|
+
- **`dai pr` decía "❓ sin US" cuando el tracker no contestaba** — dos líneas debajo del id de
|
|
23
|
+
la US que sí existe. `coverageStatus` colapsaba en un mismo `sin-us` dos cosas que no
|
|
24
|
+
significan lo mismo: *el tracker contestó y la US no está* y *no hubo respuesta* (sin red,
|
|
25
|
+
sin token, 5xx, certificado corporativo). Los adaptadores ya distinguían los dos casos
|
|
26
|
+
(`404 → null`, cualquier otro error → `throw`), y el gate de CI también con su try/catch
|
|
27
|
+
propio; lo que rompía la distinción era un `.catch(() => null)` en `dai pr`. Hay un estado
|
|
28
|
+
nuevo, **`sin-respuesta`** (`⚠️ no verificado`), y la PR dice *"no verificado (el tracker no
|
|
29
|
+
respondió)"*, que es lo único cierto ([#43](https://github.com/dforce2055/dai/issues/43)).
|
|
30
|
+
- **`dai: fetch failed` era todo lo que se llegaba a leer.** Es el mensaje pelado de undici
|
|
31
|
+
cuando no hay red, el host no resuelve o el certificado no valida: no dice qué se estaba
|
|
32
|
+
consultando, ni contra qué, ni qué mirar — indistinguible de un bug de dai, el mismo modo de
|
|
33
|
+
falla que el push por SSH de la 0.13.1. Ahora el error nombra la US, el backend y el host, y
|
|
34
|
+
distingue red / credencial / error del tracker, con el próximo paso en cada caso.
|
|
35
|
+
- **`dai check` ya no se detenía en la primera US** que no pudiera consultar: lo reporta, sigue
|
|
36
|
+
con las demás y sale ≠ 0. **`dai stamp`** explicita que no estampa un estado que no pudo
|
|
37
|
+
verificar — escribir en el tracker de todo el equipo no se deshace.
|
|
38
|
+
|
|
39
|
+
### Cambiado
|
|
40
|
+
- **El molde de PR adelgaza: la doctrina pasa a comentario HTML.** El encabezado tenía cuatro
|
|
41
|
+
bloques y un solo dato — la doctrina de los dos activos, una línea que repetía la de arriba
|
|
42
|
+
(*"este PR está atado a la US vía implements.yaml"*) y la instrucción de borrar la sección si
|
|
43
|
+
no hay US, que `dai pr` resuelve solo desde la 0.13.2. Nada de eso decía algo sobre *esa* PR,
|
|
44
|
+
y repetirlo a la vista en cada una entrena a saltear el principio del cuerpo, que es
|
|
45
|
+
justamente donde va la descripción. Sigue estando para quien edite el molde, invisible al
|
|
46
|
+
renderizar; y la doctrina vive donde se lee una vez y no quinientas: `docs/glosario.md`,
|
|
47
|
+
`docs/guias/dev.md`, `governance/ci-rules.md`. Los repos ya inicializados lo reciben con
|
|
48
|
+
`dai sync`.
|
|
49
|
+
|
|
50
|
+
### Interno
|
|
51
|
+
- **367 tests** (+9): los dos caminos de la consulta (la US que no está y la que no se pudo
|
|
52
|
+
consultar), el mensaje de error por tipo de falla, y que el relleno del estado no toque la
|
|
53
|
+
prosa que lo rodea.
|
|
54
|
+
|
|
6
55
|
## [0.13.2] — 2026-09-02
|
|
7
56
|
|
|
8
57
|
**Una PR se publicaba con la descripción vacía y la lista de cambios diciendo "Cambio 1,
|
|
@@ -808,6 +857,7 @@ ClickUp y Jira Cloud.
|
|
|
808
857
|
- Tests de las rutas de red (jira/clickup/forge) con `fetch` mockeado. Sin links rotos;
|
|
809
858
|
`files` de npm sin tests ni secretos.
|
|
810
859
|
|
|
860
|
+
[0.13.3]: https://github.com/dforce2055/dai/releases/tag/v0.13.3
|
|
811
861
|
[0.13.2]: https://github.com/dforce2055/dai/releases/tag/v0.13.2
|
|
812
862
|
[0.13.1]: https://github.com/dforce2055/dai/releases/tag/v0.13.1
|
|
813
863
|
[0.13.0]: https://github.com/dforce2055/dai/releases/tag/v0.13.0
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.13.
|
|
1
|
+
0.13.3
|
package/cli/dai.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { acHash } from "./lib/ac-hash.mjs";
|
|
|
22
22
|
import { discoverImplements, isPlaceholderId } from "./lib/implements.mjs";
|
|
23
23
|
import { isValidKey, slugify, branchName, extractTitle, renderImplementsYaml } from "./lib/link-us.mjs";
|
|
24
24
|
import { loadDaiEnv } from "./lib/env.mjs";
|
|
25
|
-
import { getAdapter, coverageStatus, statusLabel } from "./lib/pm-adapter.mjs";
|
|
25
|
+
import { getAdapter, coverageStatus, statusLabel, fetchLiveUS } from "./lib/pm-adapter.mjs";
|
|
26
26
|
import { branchUrl, commitUrl, parseRemote, detectForge } from "./lib/forge-url.mjs";
|
|
27
27
|
import { parsePrRef, getPR, postComment, postReview } from "./lib/forge-api.mjs";
|
|
28
28
|
import { trackerUrl } from "./lib/tracker-url.mjs";
|
|
@@ -332,13 +332,18 @@ async function cmdCheck() {
|
|
|
332
332
|
for (const f of found) for (const im of f.implements || []) {
|
|
333
333
|
if (isPlaceholderId(im.id)) continue; // plantilla sin completar, no es una US real
|
|
334
334
|
n++;
|
|
335
|
-
const live = await adapter
|
|
336
|
-
const status = coverageStatus(im.ac_hash, live?.ac_hash);
|
|
335
|
+
const { us: live, unreachable, reason } = await fetchLiveUS(adapter, im.id);
|
|
336
|
+
const status = coverageStatus(im.ac_hash, live?.ac_hash, { unreachable });
|
|
337
337
|
if (status === "al-dia") process.stdout.write(`✅ ${im.id} al día (${im.version})\n`);
|
|
338
338
|
else if (status === "atrasado") {
|
|
339
339
|
process.stdout.write(`⚠️ ${im.id} ATRASADO: implementaste ${im.ac_hash}, la US viva es ${live.ac_hash}${live.spec_version ? ` (${live.spec_version})` : ""}\n`);
|
|
340
340
|
atrasadas.push(im.id);
|
|
341
341
|
worst = Math.max(worst, 1);
|
|
342
|
+
} else if (status === "sin-respuesta") {
|
|
343
|
+
// No es "no hay US": es "no pude preguntar". Antes moría acá con `fetch failed` y sin
|
|
344
|
+
// chequear las demás; ahora lo dice, sigue, y sale ≠ 0 porque no pudo verificar nada.
|
|
345
|
+
process.stdout.write(`⚠️ ${reason}\n`);
|
|
346
|
+
worst = Math.max(worst, 2);
|
|
342
347
|
} else {
|
|
343
348
|
process.stdout.write(`❓ ${im.id}: no encontré la US (backend ${adapter.kind}). ¿Falta el .md o el token?\n`);
|
|
344
349
|
worst = Math.max(worst, 2);
|
|
@@ -395,7 +400,11 @@ async function cmdStamp(ids = [], opts = {}) {
|
|
|
395
400
|
}
|
|
396
401
|
|
|
397
402
|
for (const r of targets) {
|
|
398
|
-
|
|
403
|
+
// Estampar es ESCRIBIR en el tracker de todo el equipo, y no se deshace: si no se pudo
|
|
404
|
+
// verificar el estado, no se estampa. (Hoy ya frenaba, por la excepción sin atrapar; acá
|
|
405
|
+
// queda explícito, con el motivo, y cubierto por un test.)
|
|
406
|
+
const { us: live, unreachable, reason } = await fetchLiveUS(adapter, r.id);
|
|
407
|
+
if (unreachable) fail(`${reason}\n No estampo un estado que no pude verificar.`, 2);
|
|
399
408
|
const status = coverageStatus(r.ac_hash, live?.ac_hash);
|
|
400
409
|
const record = {
|
|
401
410
|
repo: r.repo, change: r.change, version: r.version, ac_hash: r.ac_hash, status,
|
|
@@ -960,8 +969,14 @@ async function cmdPr(opts) {
|
|
|
960
969
|
|
|
961
970
|
// 2. Estado de trazabilidad (dai check) contra la US viva.
|
|
962
971
|
const adapter = getAdapter(process.env);
|
|
963
|
-
|
|
964
|
-
|
|
972
|
+
// El `.catch(() => null)` que había acá convertía "el tracker no contestó" en "no hay US",
|
|
973
|
+
// y esa afirmación se PUBLICABA en el cuerpo de la PR, al lado del id de la US que sí está.
|
|
974
|
+
const { us: live, unreachable, reason } = await fetchLiveUS(adapter, id);
|
|
975
|
+
if (unreachable) {
|
|
976
|
+
warn(reason);
|
|
977
|
+
warn("la PR va a decir que no se pudo verificar contra el tracker — no que no hay US.");
|
|
978
|
+
}
|
|
979
|
+
const status = id ? coverageStatus(ac_hash, live?.ac_hash, { unreachable }) : null;
|
|
965
980
|
if (status === "atrasado") {
|
|
966
981
|
warn(`la US ${id} está ATRASADA respecto de tu implementación (${ac_hash} ≠ ${live?.ac_hash}).`);
|
|
967
982
|
warn(`resincroniza antes de abrir la PR: dai link-us ${id} --resync`);
|
package/cli/lib/pm-adapter.mjs
CHANGED
|
@@ -21,19 +21,36 @@
|
|
|
21
21
|
|
|
22
22
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
23
23
|
import { join, dirname } from "node:path";
|
|
24
|
-
import { parseUS, renderCoverage } from "./us.mjs";
|
|
24
|
+
import { parseUS, renderCoverage, explainFetchError } from "./us.mjs";
|
|
25
25
|
import { slugify } from "./link-us.mjs";
|
|
26
26
|
import { jiraAdapter } from "./pm-jira.mjs";
|
|
27
27
|
import { clickupAdapter } from "./pm-clickup.mjs";
|
|
28
28
|
|
|
29
29
|
// Re-export para compatibilidad (tests y CLI importan estos desde acá).
|
|
30
|
-
export { parseUS, coverageStatus, statusLabel, renderCoverage } from "./us.mjs";
|
|
30
|
+
export { parseUS, coverageStatus, statusLabel, renderCoverage, explainFetchError } from "./us.mjs";
|
|
31
|
+
|
|
32
|
+
// Consulta la US preservando la diferencia entre "no existe" y "no pude preguntar". Los
|
|
33
|
+
// adaptadores ya la hacen —404 → null, cualquier otro error → throw— pero se perdía en cada
|
|
34
|
+
// caller: `dai pr` la borraba con un `.catch(() => null)` y publicaba "sin US" en la PR, y
|
|
35
|
+
// los demás morían con el `fetch failed` pelado de undici. El gate de CI era el único que la
|
|
36
|
+
// respetaba, con su try/catch propio; esto es ese criterio, compartido.
|
|
37
|
+
//
|
|
38
|
+
// → { us, unreachable, reason } · unreachable: no hubo respuesta, no sabemos nada
|
|
39
|
+
export async function fetchLiveUS(adapter, id) {
|
|
40
|
+
if (!id) return { us: null, unreachable: false, reason: null };
|
|
41
|
+
try {
|
|
42
|
+
return { us: await adapter.fetchUS(id), unreachable: false, reason: null };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { us: null, unreachable: true, reason: explainFetchError(e, { kind: adapter.kind, endpoint: adapter.endpoint, id }) };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
31
47
|
|
|
32
48
|
// ── backend md (local, offline) ───────────────────────────────────────────────
|
|
33
49
|
function mdAdapter(env) {
|
|
34
50
|
const dir = env.DAI_MD_US_DIR || ".dai/us";
|
|
35
51
|
return {
|
|
36
52
|
kind: "md",
|
|
53
|
+
endpoint: dir,
|
|
37
54
|
fetchUS(id) {
|
|
38
55
|
const p = join(dir, `${id}.md`);
|
|
39
56
|
if (!existsSync(p)) return null;
|
package/cli/lib/pm-clickup.mjs
CHANGED
|
@@ -25,6 +25,7 @@ export function clickupAdapter(env) {
|
|
|
25
25
|
if (!env.DAI_CLICKUP_TOKEN) throw new Error("falta DAI_CLICKUP_TOKEN en el .env.dai (backend clickup).");
|
|
26
26
|
return {
|
|
27
27
|
kind: "clickup",
|
|
28
|
+
endpoint: "api.clickup.com",
|
|
28
29
|
async fetchUS(id) {
|
|
29
30
|
const res = await fetch(clickupTaskUrl(id), { headers: clickupAuthHeaders(env) });
|
|
30
31
|
if (res.status === 404) return null;
|
package/cli/lib/pm-jira.mjs
CHANGED
|
@@ -139,6 +139,7 @@ export function jiraAdapter(env) {
|
|
|
139
139
|
if (!base) throw new Error("falta DAI_JIRA_BASE_URL en el .env.dai (backend jira).");
|
|
140
140
|
return {
|
|
141
141
|
kind: "jira",
|
|
142
|
+
endpoint: trim(base),
|
|
142
143
|
async fetchUS(id) {
|
|
143
144
|
const res = await daiFetch(jiraIssueUrl(base, id), { headers: jiraAuthHeaders(env) });
|
|
144
145
|
if (res.status === 404) return null;
|
package/cli/lib/pr.mjs
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
// Parte pura y testeable: rellena el template con los datos del link + git + check.
|
|
3
3
|
// Los efectos (git push, gh/glab create) viven en dai.mjs.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Más explícito que el label del CLI a propósito: esto queda PUBLICADO en la PR, donde
|
|
6
|
+
// quien lee no tiene el contexto de la corrida que la creó.
|
|
7
|
+
const EMOJI = {
|
|
8
|
+
"al-dia": "✅ al día", atrasado: "⚠️ atrasado", "sin-us": "❓ sin US",
|
|
9
|
+
"sin-respuesta": "⚠️ no verificado (el tracker no respondió)",
|
|
10
|
+
};
|
|
6
11
|
|
|
7
12
|
// Las secciones que dai se compromete a entregar llenas. Si alguna sale con el molde
|
|
8
13
|
// del template, la PR se publica vacía y el review no tiene qué mirar (era el bug:
|
|
@@ -115,12 +120,21 @@ export function composePrBody(template, d) {
|
|
|
115
120
|
return upsertLinksBlock(b, d);
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
// Los placeholders se rellenan DENTRO de la sección, no en todo el body: la misma frase
|
|
124
|
+
// ("verificado con `dai check` ✅") aparecía en la prosa que explica el método, así que un
|
|
125
|
+
// replace global le metía el estado de ESTA PR a una oración general y quedaba
|
|
126
|
+
// "verificado con dai check: ✅ al día. Sin esto, el código no sabe…". dai reescribiendo
|
|
127
|
+
// la prosa de otro es justo lo que el bloque de links ya evitaba con sus marcadores.
|
|
128
|
+
// Sin la sección (un template ajeno con otra forma) se cae al body entero: rellenar de más
|
|
129
|
+
// es recuperable, no rellenar deja la PR con `ABC-###` publicado.
|
|
118
130
|
function fillUsHeader(template, d) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
const fill = (t) => t
|
|
132
|
+
.replace(/`ABC-###`/g, `\`${d.id}\``)
|
|
133
|
+
.replace(/@ `vX`/g, `@ \`${d.version}\``)
|
|
134
|
+
.replace(/`<hash>`/g, `\`${d.ac_hash}\``)
|
|
135
|
+
.replace(/verificado con `dai check` ✅/g, `verificado con \`dai check\`: ${EMOJI[d.status] || d.status}`);
|
|
136
|
+
const sec = sectionBody(template, "🔗 Implementa");
|
|
137
|
+
return sec == null ? fill(template) : replaceSection(template, "🔗 Implementa", fill(sec).trim());
|
|
124
138
|
}
|
|
125
139
|
|
|
126
140
|
// PR de una branch exenta (chore/, docs/, release/…): no implementa una US y no se le
|
package/cli/lib/us.mjs
CHANGED
|
@@ -12,12 +12,22 @@ export function parseUS(raw) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// Compara el hash estampado (implements.yaml) con el hash vivo de la US.
|
|
15
|
-
|
|
15
|
+
//
|
|
16
|
+
// `sin-us` y `sin-respuesta` NO son lo mismo, y confundirlos es caro: el primero es una
|
|
17
|
+
// RESPUESTA del tracker (preguntamos y la US no está), el segundo es la AUSENCIA de
|
|
18
|
+
// respuesta (sin red, sin token, 5xx, certificado corporativo). Colapsados en un mismo
|
|
19
|
+
// `null`, dai terminaba AFIRMANDO que no había US cuando lo único cierto era que no había
|
|
20
|
+
// podido preguntar — y eso se publicaba en el cuerpo de una PR, al lado del id de la US.
|
|
21
|
+
export function coverageStatus(stampedHash, liveHash, { unreachable = false } = {}) {
|
|
22
|
+
if (unreachable) return "sin-respuesta";
|
|
16
23
|
if (liveHash == null) return "sin-us";
|
|
17
24
|
return stampedHash === liveHash ? "al-dia" : "atrasado";
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
const STATUS_LABEL = {
|
|
27
|
+
const STATUS_LABEL = {
|
|
28
|
+
"al-dia": "✅ al día", atrasado: "⚠️ atrasado",
|
|
29
|
+
"sin-us": "❓ sin US", "sin-respuesta": "⚠️ no verificado",
|
|
30
|
+
};
|
|
21
31
|
export const statusLabel = (s) => STATUS_LABEL[s] || s;
|
|
22
32
|
|
|
23
33
|
// Render de la cobertura como markdown (lo que un backend "estampa").
|
|
@@ -34,3 +44,25 @@ export function renderCoverage(id, r) {
|
|
|
34
44
|
if (r.commitUrl) lines.push(`- commit: ${r.commit} → ${r.commitUrl} (ancla durable)`);
|
|
35
45
|
return lines.join("\n") + "\n";
|
|
36
46
|
}
|
|
47
|
+
|
|
48
|
+
// `fetch failed` es TODO lo que dice undici cuando no hay red, el host no resuelve, el DNS
|
|
49
|
+
// se cayó o el certificado no valida. Sin contexto es indistinguible de un bug de dai: el
|
|
50
|
+
// dev lee "dai: fetch failed" y no sabe ni qué se estaba consultando. Es el mismo modo de
|
|
51
|
+
// falla que el push por SSH de la 0.13.1 — el dato que resuelve el problema existe, y no
|
|
52
|
+
// llega. Acá se le pone alrededor qué, contra qué, y qué mirar.
|
|
53
|
+
export function explainFetchError(err, { kind, endpoint, id } = {}) {
|
|
54
|
+
const raw = String(err?.message ?? err ?? "").split("\n")[0] || "error desconocido";
|
|
55
|
+
const donde = [kind, endpoint].filter(Boolean).join(" · ");
|
|
56
|
+
const cabeza = `no pude consultar ${id ? `la US ${id}` : "el tracker"}${donde ? ` en ${donde}` : ""}: ${raw}`;
|
|
57
|
+
if (/fetch failed|ENOTFOUND|ECONNREFUSED|EAI_AGAIN|ETIMEDOUT|ECONNRESET|certificate|self.signed/i.test(raw)) {
|
|
58
|
+
return `${cabeza}\n No llegó a haber respuesta. Revisá la red y el host del backend; si estás detrás de un\n` +
|
|
59
|
+
` proxy corporativo, declará la CA con NODE_EXTRA_CA_CERTS (nunca NODE_TLS_REJECT_UNAUTHORIZED=0).\n` +
|
|
60
|
+
` Diagnóstico: dai doctor`;
|
|
61
|
+
}
|
|
62
|
+
if (/\b40[13]\b|unauthorized|forbidden/i.test(raw)) {
|
|
63
|
+
return `${cabeza}\n El tracker rechazó las credenciales: revisá el token del .env.dai (¿venció?) y sus permisos.\n` +
|
|
64
|
+
` Diagnóstico: dai doctor`;
|
|
65
|
+
}
|
|
66
|
+
if (/\b5\d\d\b/.test(raw)) return `${cabeza}\n El error es del tracker, no tuyo: probá de nuevo en un rato.`;
|
|
67
|
+
return cabeza;
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforce2055/dai",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.3",
|
|
4
4
|
"description": "Metodología de desarrollo asistido por IA — CLI de acciones deterministas (trazabilidad QUÉ↔CÓMO).",
|
|
5
5
|
"repository": { "type": "git", "url": "git+https://github.com/dforce2055/dai.git" },
|
|
6
6
|
"homepage": "https://dforce2055.github.io/dai/",
|
|
@@ -5,19 +5,23 @@
|
|
|
5
5
|
Lo puede pre-llenar `dai`/una skill a partir del implements.yaml y el diff.
|
|
6
6
|
-->
|
|
7
7
|
|
|
8
|
-
> **Un PR en dai entrega dos activos, y el review cubre los dos:**
|
|
9
|
-
> 1. **La implementación** — el código que resuelve la US.
|
|
10
|
-
> 2. **El spec trazable** — el `implements.yaml` con el link a la US y el `@version`
|
|
11
|
-
> (`ac_hash`) verificado con `dai check` ✅. Sin esto, el código no sabe *a qué QUÉ*
|
|
12
|
-
> responde, y el CI bloquea el PR (ver `governance/ci-rules.md`).
|
|
13
|
-
|
|
14
8
|
## 🔗 Implementa
|
|
15
9
|
|
|
16
10
|
- **US:** `ABC-###` @ `vX` · ac_hash: `<hash>` · verificado con `dai check` ✅
|
|
17
|
-
- **Link:** este PR está atado a la US vía `implements.yaml`.
|
|
18
11
|
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
<!--
|
|
13
|
+
Un PR en dai entrega DOS activos, y el review cubre los dos: la implementación (el código
|
|
14
|
+
que resuelve la US) y el spec trazable (el `implements.yaml` con el link a la US y el
|
|
15
|
+
`@version`/`ac_hash` verificado). Sin el link, el código no sabe a qué QUÉ responde y el
|
|
16
|
+
CI bloquea el PR — ver `governance/ci-rules.md`.
|
|
17
|
+
|
|
18
|
+
¿Chore o fix sin ticket? No hay nada que borrar: `dai pr` lo detecta por el nombre de la
|
|
19
|
+
branch y escribe "Sin US" con el motivo. No se le exige link.
|
|
20
|
+
|
|
21
|
+
Esto es un comentario a propósito: es doctrina del método, igual en las 500 PRs del repo.
|
|
22
|
+
Repetirla a la vista en cada una entrena a saltear el principio del cuerpo, que es
|
|
23
|
+
justamente donde va la descripción.
|
|
24
|
+
-->
|
|
21
25
|
|
|
22
26
|
## Descripción
|
|
23
27
|
|