@dforce2055/dai 0.11.0 → 0.13.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.
@@ -56,17 +56,22 @@ export function trackerKeysIn(branch) {
56
56
  // chore/, docs/, ci/… → nunca
57
57
  // fix/ y el resto → solo si el nombre trae un ID con pinta de key de tracker
58
58
  // `main`/`develop`/sin branch → no (no es una branch de trabajo).
59
+ //
60
+ // `kind` distingue POR QUÉ no se exige, que no es lo mismo para todos los comandos:
61
+ // una `chore/` está exenta POR TIPO (el repo declara que ahí no hay producto), mientras
62
+ // que `mi-branch` simplemente no dice nada. `dai pr` usa esa diferencia para no colgarle
63
+ // la US viva del repo a una PR de archivado (issue #31).
59
64
  export function requiresLink(branch) {
60
65
  const t = branchType(branch);
61
- if (ALWAYS.has(t)) return { required: true, reason: `'${t}/' es trabajo de producto: requiere US` };
62
- if (EXEMPT.has(t)) return { required: false, reason: `'${t}/' está exenta de US (governance/branch-naming.md)` };
63
- if (t === "") return { required: false, reason: "sin prefijo tipo/, no es una branch de trabajo: sin gate de link" };
66
+ if (ALWAYS.has(t)) return { required: true, kind: "always", reason: `'${t}/' es trabajo de producto: requiere US` };
67
+ if (EXEMPT.has(t)) return { required: false, kind: "exempt", reason: `'${t}/' está exenta de US (governance/branch-naming.md)` };
68
+ if (t === "") return { required: false, kind: "untyped", reason: "sin prefijo tipo/, no es una branch de trabajo: sin gate de link" };
64
69
  // Tipo desconocido (fix/, spike/, lo que el repo use): si nombró un ID, lo tomamos
65
70
  // como intención de implementar una US y se lo exigimos. Si no, no inventamos.
66
71
  const keyish = trackerKeysIn(branch).length > 0;
67
72
  return keyish
68
- ? { required: true, reason: `'${t}/' con un ID en el nombre: se toma como trabajo de producto` }
69
- : { required: false, reason: `'${t}/' sin ID en el nombre: no exige US (governance/branch-naming.md)` };
73
+ ? { required: true, kind: "always", reason: `'${t}/' con un ID en el nombre: se toma como trabajo de producto` }
74
+ : { required: false, kind: "untyped", reason: `'${t}/' sin ID en el nombre: no exige US (governance/branch-naming.md)` };
70
75
  }
71
76
 
72
77
  // Aplana los implements descubiertos a filas { path, change, repo, id, version, ac_hash },
@@ -142,3 +147,54 @@ export function stampScope({ branch, rows, allRows = rows, ids = [], all = false
142
147
  reason: `hay ${hit.length > 1 ? hit.length : rows.length} US vivas y la branch '${branch}' no dice cuál`,
143
148
  };
144
149
  }
150
+
151
+ // La decisión de `dai pr`: ¿con qué US se titula y se linkea ESTA PR?
152
+ //
153
+ // { mode, target, candidates, reason, missing }
154
+ //
155
+ // mode "explicit" → el id que pidió el usuario (dai pr --us ABC-482)
156
+ // mode "branch" → la branch nombra una US viva del repo: esa
157
+ // mode "only" → hay una sola US viva y la branch no está exenta: esa
158
+ // mode "exempt" → branch exenta por tipo (chore/, docs/…) que no nombra US: PR SIN US
159
+ // mode "ambiguous" → varias candidatas y ninguna pista: NO elige, pregunta
160
+ // mode "none" → la branch pide US y el repo no tiene ninguna viva
161
+ //
162
+ // Es la misma pregunta que resuelve stampScope, con dos diferencias que importan
163
+ // (issues #31, #32, #33 — antes `dai pr` recorría TODOS los implements.yaml del repo,
164
+ // archivados incluidos, y se quedaba con el último):
165
+ //
166
+ // 1. Una PR implementa UNA US, no cuatro: no hay modo "all", y el resultado es un
167
+ // único `target`.
168
+ // 2. Una branch exenta NO hereda la US viva del repo. Una PR de `chore/archive-specs`
169
+ // titulada con la historia de un compañero es peor que una sin título lindo: la
170
+ // lista de PRs es la única superficie donde el equipo lee la trazabilidad.
171
+ //
172
+ // `rows` viene del discover SIN archivados; `allRows` incluye los archivados y solo se
173
+ // usa para resolver un id explícito (reabrir la PR de un change ya archivado) y para
174
+ // listar qué US conoce el repo cuando el id pedido no está.
175
+ export function prScope({ branch, rows, allRows = rows, ids = [] }) {
176
+ if (ids.length) {
177
+ const want = String(ids[0]).toLowerCase();
178
+ const target = allRows.find((r) => String(r.id).toLowerCase() === want) || null;
179
+ // `missing` conserva la grafía del usuario: devolverle su ABC-404 en minúsculas lo
180
+ // manda a dudar del case en vez de del id.
181
+ return { mode: "explicit", target, candidates: allRows, missing: target ? [] : [ids[0]], reason: `pediste --us ${ids[0]}` };
182
+ }
183
+
184
+ const hit = matchBranchToImplements(branch, rows);
185
+ if (hit.length === 1) {
186
+ return { mode: "branch", target: hit[0], candidates: rows, reason: `la branch '${branch}' nombra ${hit[0].id}` };
187
+ }
188
+ if (hit.length > 1) {
189
+ return { mode: "ambiguous", target: null, candidates: hit, reason: `la branch '${branch}' nombra ${hit.length} US vivas` };
190
+ }
191
+
192
+ // La branch no nombra ninguna US viva.
193
+ const req = requiresLink(branch);
194
+ if (req.kind === "exempt") {
195
+ return { mode: "exempt", target: null, candidates: rows, reason: `${req.reason} y su nombre no nombra ninguna US` };
196
+ }
197
+ if (rows.length === 0) return { mode: "none", target: null, candidates: [], reason: "no hay implements.yaml vivo en el repo" };
198
+ if (rows.length === 1) return { mode: "only", target: rows[0], candidates: rows, reason: "es la única US viva del repo" };
199
+ return { mode: "ambiguous", target: null, candidates: rows, reason: `hay ${rows.length} US vivas y la branch '${branch}' no dice cuál` };
200
+ }
@@ -41,7 +41,9 @@ export function extractTitle(md) {
41
41
 
42
42
  // Render del implements.yaml (schema ADR-0004).
43
43
  export function renderImplementsYaml({ change, repo, id, version = "v1", ac_hash, autor }) {
44
- return `# Link QUÉ↔CÓMO · scaffoldeado por dai link-us. El ÚNICO link autorado a mano.
44
+ return `# Link QUÉ↔CÓMO · scaffoldeado por dai link-us. El ÚNICO link AUTORADO del método:
45
+ # lo escribe alguien, no se deriva. Todo lo de abajo ya está resuelto salvo 'introduces',
46
+ # que se completa AL TERMINAR de implementar — recién ahí se sabe qué se introdujo.
45
47
  # Schema: docs/adr/0004-ubicacion-y-schema-implements.md
46
48
  change: ${change}
47
49
  repo: ${repo}
@@ -52,7 +54,8 @@ implements:
52
54
  ac_hash: ${ac_hash}
53
55
 
54
56
  introduces:
55
- - <capacidad-tecnica> # completar: specs técnicas nuevas de este change
57
+ - <capacidad-tecnica> # al cerrar la implementación: capacidades técnicas nuevas
58
+ # (o borrá el bloque si no hay ninguna)
56
59
 
57
60
  autor: ${autor}
58
61
  `;
package/cli/lib/pr.mjs CHANGED
@@ -18,6 +18,7 @@ export function replaceSection(body, heading, content) {
18
18
  // placeholders que encuentra y deja el resto para que el humano lo edite.
19
19
  export function composePrBody(template, d) {
20
20
  let b = template;
21
+ if (!d.id) return composePrBodyNoUs(b, d);
21
22
  b = b.replace(/`ABC-###`/g, `\`${d.id}\``);
22
23
  b = b.replace(/@ `vX`/g, `@ \`${d.version}\``);
23
24
  b = b.replace(/`<hash>`/g, `\`${d.ac_hash}\``);
@@ -32,6 +33,21 @@ export function composePrBody(template, d) {
32
33
  return upsertLinksBlock(b, d);
33
34
  }
34
35
 
36
+ // PR de una branch exenta (chore/, docs/, release/…): no implementa una US y no se le
37
+ // exige link. Lo que NO puede pasar es que salga con la US de otro ni con el placeholder
38
+ // `ABC-###` del template — las dos cosas pasaron en repos reales (issues #31, #33).
39
+ // Se dice explícitamente que no hay US, y por qué.
40
+ function composePrBodyNoUs(template, d) {
41
+ let b = replaceSection(template, "🔗 Implementa",
42
+ `- **Sin US:** esta PR no implementa una User Story.\n` +
43
+ (d.noUsReason ? `- **Motivo:** ${d.noUsReason}.\n` : "") +
44
+ `- No se le exige link (\`governance/branch-naming.md\`).`);
45
+ if (d.commits && d.commits.length) {
46
+ b = replaceSection(b, "Cambios realizados", d.commits.map((c) => `- [x] ${c}`).join("\n"));
47
+ }
48
+ return upsertLinksBlock(b, d);
49
+ }
50
+
35
51
  // ── Bloque de enlaces ────────────────────────────────────────────────────────
36
52
  // Delimitado y regenerable a propósito. Con el comentario suelto de antes, cualquier
37
53
  // agente que reescribiera "Enlaces relacionados" se lo llevaba puesto sin dejar rastro
@@ -70,8 +86,12 @@ export function upsertLinksBlock(body, d) {
70
86
  }
71
87
 
72
88
  // Título del PR: el pasado a mano, o "<ID>: <título de la US>", o solo el ID.
73
- export function prTitle(opts, id, usTitle) {
89
+ // Sin US (branch exenta) cae al `fallback` — el subject del último commit: describe
90
+ // lo que hay adentro. Inventar un título con la US de otro es el bug de los issues
91
+ // #31/#33, y el título es lo ÚNICO que se ve en la lista de PRs.
92
+ export function prTitle(opts, id, usTitle, fallback = "") {
74
93
  if (opts.title) return opts.title;
94
+ if (!id) return fallback;
75
95
  if (usTitle) return `${id}: ${usTitle}`;
76
96
  return id;
77
97
  }
@@ -157,7 +157,7 @@ $ dai link-us ABC-482 --us us.md --change finalizar-compra
157
157
  ```
158
158
 
159
159
  ```yaml
160
- # implements.yaml — el ÚNICO link autorado a mano (schema ADR-0004)
160
+ # implements.yaml — el ÚNICO link AUTORADO: se escribe, no se deriva (schema ADR-0004)
161
161
  change: finalizar-compra
162
162
  repo: frontend
163
163
 
package/docs/glosario.md CHANGED
@@ -20,7 +20,7 @@
20
20
  | Término | Qué es |
21
21
  |---|---|
22
22
  | **Link (QUÉ↔CÓMO)** | La relación entre un requerimiento y su implementación. |
23
- | **`implements`** | La declaración `implements: <id>@<version>` en el código. El **único** link autorado a mano. |
23
+ | **`implements`** | La declaración `implements: <id>@<version>` en el código. El **único** link autorado (se escribe, no se deriva): lo scaffoldea `dai link-us` y se cierra al terminar de implementar. |
24
24
  | **`implements.yaml`** | El archivo, en el change del repo, que contiene ese link. Lo genera `link-us`. Ejemplo lleno + árbol de dónde vive entre los artefactos de OpenSpec: [ADR-0004](adr/0004-ubicacion-y-schema-implements.md). |
25
25
  | **Trazabilidad inversa / cobertura** | El mapa "quién implementó este QUÉ". **Se genera, nunca se escribe.** |
26
26
  | **Índice / router** | La tabla central que dice qué ID vive en qué repos. Es un router, **no un almacén**: no guarda el detalle. |
package/docs/guias/dev.md CHANGED
@@ -7,7 +7,10 @@
7
7
 
8
8
  - El **CÓMO**: diseño técnico, modelo de datos, arquitectura de la solución.
9
9
  - Las **tareas técnicas** (las derivas tú, desde la US, con OpenSpec).
10
- - El **link** (`implements.yaml`): es el **único** que se autora a mano ([Art. 9](../MANIFIESTO.md#art-9)).
10
+ - El **link** (`implements.yaml`): es el **único** que se autora, no se deriva ([Art. 9](../MANIFIESTO.md#art-9)).
11
+ `dai link-us` lo scaffoldea con el `id`/`version`/`ac_hash` ya resueltos; lo único que queda
12
+ abierto es `introduces`, y se cierra **al terminar de implementar** —normalmente lo completa
13
+ el agente que implementó, y tú lo revisas en la PR—.
11
14
  - El **código** y su **spec técnica**.
12
15
 
13
16
  ## Lo que NO tocas
@@ -88,3 +91,10 @@ que está junto a tu `implements.yaml`.
88
91
  - `dai check` · `dai pr` · `dai stamp` · `dai done` (limpieza, opcional)
89
92
  - `dai update-us` — empuja al tracker una US que refinaste implementando
90
93
  - `definition-of-done.md`
94
+
95
+ > **Ojo con el nombre de los comandos `opsx`.** Acá se escriben en la forma de Claude Code
96
+ > (`/opsx:apply`). En **Copilot y Cursor** los mismos comandos van **con guion**:
97
+ > `/opsx-apply`. No es un alias: es el nombre del archivo que genera OpenSpec para cada
98
+ > asistente. Con la forma equivocada el agente no encuentra el comando, no carga el workflow
99
+ > y se pone a improvisar sin avisar. `dai init` te dice cuál te toca.
100
+
@@ -2,6 +2,15 @@
2
2
 
3
3
  Guías de **setup operativo** — lo que haces una vez por máquina para trabajar con dai.
4
4
 
5
+ ## De cero a publicar
6
+
7
+ - [**Setup para analistas funcionales y PMs (Windows)**](./setup-funcional) — el camino
8
+ completo sin git ni repositorio: Node, dai, las skills en Copilot, el token de Jira y una
9
+ épica + US de prueba publicadas de verdad.
10
+ - [**Setup para desarrolladores (Windows)**](./setup-dev) — el otro lado: Node, dai, git + SSH
11
+ + `glab`, las skills en Copilot, OpenSpec y el ciclo completo sobre una US real
12
+ (`link-us` → `check` → `mr` → `stamp`).
13
+
5
14
  ## Preparar el entorno
6
15
 
7
16
  - [**Configurar git**](./configurar-git) — tu identidad (nombre + correo) para que los