specatlas 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -8824,7 +8824,16 @@ function renderMarkdown(markdown2, opts = {}) {
8824
8824
  const source = code.join("\n");
8825
8825
  if (lang === "mermaid") {
8826
8826
  if (mermaidMode === "script" && opts.mermaidScriptUri) {
8827
- out.push(`<div class="mermaid-block"><pre class="mermaid">${escapeHtml(source)}</pre></div>`);
8827
+ out.push(`<div class="mermaid-block">
8828
+ <div class="diagram-tools" role="toolbar" aria-label="Herramientas del diagrama">
8829
+ <button type="button" data-diagram-zoom="out" title="Alejar" aria-label="Alejar">\u2212</button>
8830
+ <button type="button" data-diagram-zoom="in" title="Acercar" aria-label="Acercar">\uFF0B</button>
8831
+ <button type="button" data-diagram-zoom="reset" title="Tama\xF1o original" aria-label="Tama\xF1o original">100%</button>
8832
+ <button type="button" data-diagram-fullscreen title="Pantalla completa (Esc para salir)" aria-label="Pantalla completa">\u26F6</button>
8833
+ <button type="button" data-diagram-download title="Descargar SVG" aria-label="Descargar SVG">\u2913</button>
8834
+ </div>
8835
+ <div class="diagram-canvas"><pre class="mermaid">${escapeHtml(source)}</pre></div>
8836
+ </div>`);
8828
8837
  } else {
8829
8838
  out.push(
8830
8839
  `<div class="mermaid-block callout"><pre class="mermaid">${escapeHtml(source)}</pre><p><small>Diagrama mermaid \u2014 visible con la vista previa de Markdown del editor.</small></p></div>`
@@ -8871,34 +8880,49 @@ function renderMarkdown(markdown2, opts = {}) {
8871
8880
  quote.push((lines[i] ?? "").replace(/^>\s?/, ""));
8872
8881
  i += 1;
8873
8882
  }
8874
- const joined = quote.join(" ");
8875
- const labelRe = /(?:^|\s)([\p{L}][\p{L}\s/()-]{1,32}):\s/gu;
8876
- const hits = [];
8877
- let hitMatch;
8878
- while ((hitMatch = labelRe.exec(joined)) !== null) {
8879
- if (hitMatch[1].trim().split(/\s+/).length > 4) continue;
8880
- hits.push({ start: hitMatch.index, end: hitMatch.index + hitMatch[0].length, label: hitMatch[1] });
8881
- }
8882
8883
  const chips = [];
8883
8884
  const prose = [];
8884
- let cursor = 0;
8885
- for (let h = 0; h < hits.length; h += 1) {
8886
- const hit = hits[h];
8887
- const before = joined.slice(cursor, hit.start).replace(/[\s·]+$/u, "").trim();
8888
- if (before) prose.push(before);
8889
- const valueEnd = h + 1 < hits.length ? hits[h + 1].start : joined.length;
8890
- let value = joined.slice(hit.end, valueEnd).replace(/[\s·]+$/u, "").trim();
8891
- const sentenceBreak = /[·.]\s+(?=[\p{Lu}][\p{L}]+(?:\s+[\p{L}]+){3,})/u.exec(value);
8892
- if (sentenceBreak) {
8893
- const note = value.slice(sentenceBreak.index + 1).trim();
8894
- if (note) prose.push(note);
8895
- value = value.slice(0, sentenceBreak.index + 1);
8885
+ const chipFor = (label, value) => `<span class="meta-chip"><b>${inlineMarkdown(label)}:</b> ${inlineMarkdown(value)}</span>`;
8886
+ for (const rawLine of quote) {
8887
+ const line2 = rawLine.trim();
8888
+ if (!line2) continue;
8889
+ const whole = /^(?:\*{0,2})([\p{L}][\p{L}\s/()-]{1,32}?)(?:\*{0,2})\s*:\s*(.+)$/u.exec(line2);
8890
+ const hasInlineLabels = / · \s*\*{0,2}[\p{L}][\p{L}\s/()-]{1,32}\*{0,2}:\s/u.test(line2);
8891
+ if (whole && !hasInlineLabels && whole[1].trim().split(/\s+/).length <= 4) {
8892
+ chips.push(chipFor(whole[1], whole[2]));
8893
+ continue;
8894
+ }
8895
+ const labelRe = /(?:^|\s|\*{0,2})([\p{L}][\p{L}\s/()-]{1,32}?)\*{0,2}:\s/gu;
8896
+ const hits = [];
8897
+ let hitMatch;
8898
+ while ((hitMatch = labelRe.exec(line2)) !== null) {
8899
+ if (hitMatch[1].trim().split(/\s+/).length > 4) continue;
8900
+ const prefix = /^\*+/.exec(hitMatch[0])?.[0].length ?? 0;
8901
+ hits.push({ start: hitMatch.index + prefix, end: hitMatch.index + hitMatch[0].length, label: hitMatch[1] });
8902
+ }
8903
+ if (hits.length === 0) {
8904
+ prose.push(line2);
8905
+ continue;
8906
+ }
8907
+ let cursor = 0;
8908
+ for (let h = 0; h < hits.length; h += 1) {
8909
+ const hit = hits[h];
8910
+ const before = line2.slice(cursor, hit.start).replace(/[\s·*]+$/u, "").trim();
8911
+ if (before) prose.push(before);
8912
+ const valueEnd = h + 1 < hits.length ? hits[h + 1].start : line2.length;
8913
+ let value = line2.slice(hit.end, valueEnd).replace(/[\s·*]+$/u, "").trim();
8914
+ const sentenceBreak = /[·.]\s+(?=[\p{Lu}][\p{L}]+(?:\s+[\p{L}]+){3,})/u.exec(value);
8915
+ if (sentenceBreak) {
8916
+ const note = value.slice(sentenceBreak.index + 1).trim();
8917
+ if (note) prose.push(note);
8918
+ value = value.slice(0, sentenceBreak.index + 1);
8919
+ }
8920
+ chips.push(chipFor(hit.label, value));
8921
+ cursor = valueEnd;
8896
8922
  }
8897
- chips.push(`<span class="meta-chip"><b>${inlineMarkdown(hit.label)}:</b> ${inlineMarkdown(value)}</span>`);
8898
- cursor = valueEnd;
8923
+ const tail = line2.slice(cursor).replace(/^[\s·*]+/u, "").trim();
8924
+ if (tail) prose.push(tail);
8899
8925
  }
8900
- const tail = joined.slice(cursor).replace(/^[\s·]+/u, "").trim();
8901
- if (tail) prose.push(tail);
8902
8926
  const chipLine = chips.length > 0 ? `<div class="meta-line">${chips.join("")}</div>` : "";
8903
8927
  const proseHtml = prose.length > 0 ? `<p>${inlineMarkdown(prose.join(" \xB7 "))}</p>` : "";
8904
8928
  out.push(`<blockquote>${chipLine}${proseHtml}</blockquote>`);
@@ -8913,8 +8937,12 @@ function renderMarkdown(markdown2, opts = {}) {
8913
8937
  rows.push(splitRow(lines[i] ?? ""));
8914
8938
  i += 1;
8915
8939
  }
8940
+ const width = Math.max(header.length, ...rows.map((row) => row.length));
8941
+ const pad = (cells) => [...cells, ...Array.from({ length: Math.max(0, width - cells.length) }, () => "")];
8942
+ const paddedHeader = pad(header);
8943
+ const paddedRows = rows.map((row) => pad(row));
8916
8944
  out.push(
8917
- `<table><thead><tr>${header.map((cell) => `<th>${inlineMarkdown(cell)}</th>`).join("")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${inlineMarkdown(cell)}</td>`).join("")}</tr>`).join("")}</tbody></table>`
8945
+ `<div class="table-wrap"><table><thead><tr>${paddedHeader.map((cell) => `<th>${inlineMarkdown(cell)}</th>`).join("")}</tr></thead><tbody>${paddedRows.map((row) => `<tr>${row.map((cell) => `<td>${inlineMarkdown(cell)}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`
8918
8946
  );
8919
8947
  continue;
8920
8948
  }
@@ -9631,6 +9659,71 @@ function lintDelta(delta, livingRequirements, path192, opts = {}) {
9631
9659
  }
9632
9660
  return out;
9633
9661
  }
9662
+ var MERMAID_KEYWORDS = [
9663
+ "flowchart",
9664
+ "graph",
9665
+ "sequenceDiagram",
9666
+ "stateDiagram-v2",
9667
+ "stateDiagram",
9668
+ "classDiagram",
9669
+ "erDiagram",
9670
+ "journey",
9671
+ "gantt",
9672
+ "pie",
9673
+ "mindmap",
9674
+ "timeline",
9675
+ "quadrantChart",
9676
+ "xychart-beta",
9677
+ "block-beta",
9678
+ "architecture-beta"
9679
+ ];
9680
+ var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
9681
+ function lintPlan(planText, path192) {
9682
+ const out = [];
9683
+ const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
9684
+ for (const [index, block] of blocks.entries()) {
9685
+ const code = (block[1] ?? "").replace(/\r\n?/g, "\n");
9686
+ const lines = code.split("\n");
9687
+ const first = (lines.find((line) => line.trim().length > 0) ?? "").trim();
9688
+ if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
9689
+ out.push(
9690
+ diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: la primera l\xEDnea debe declarar el tipo (${MERMAID_KEYWORDS.slice(0, 5).join(", ")}\u2026) y empieza por "${first.slice(0, 30)}"`, {
9691
+ path: path192,
9692
+ suggestion: "Corrige el tipo del diagrama o elimina el bloque"
9693
+ })
9694
+ );
9695
+ continue;
9696
+ }
9697
+ let open = 0;
9698
+ for (const line of lines) {
9699
+ if (MERMAID_BLOCKS.test(line)) open += 1;
9700
+ else if (/^\s*end\b/.test(line)) open -= 1;
9701
+ }
9702
+ if (open !== 0) {
9703
+ out.push(
9704
+ diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
9705
+ path: path192,
9706
+ suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
9707
+ })
9708
+ );
9709
+ }
9710
+ if (first.startsWith("sequenceDiagram")) {
9711
+ for (const line of lines) {
9712
+ if (!/^\s*[^\s:]+-{1,2}>{1,2}[^\s:]*\s*:\s*/.test(line)) continue;
9713
+ const message = line.slice(line.indexOf(":") + 1);
9714
+ if (message.includes(";")) {
9715
+ out.push(
9716
+ diag("LINT-PLN-003", "error", `Diagrama mermaid ${index + 1}: el mensaje "${message.trim().slice(0, 40)}\u2026" usa \`;\` y mermaid lo interpreta como fin de sentencia`, {
9717
+ path: path192,
9718
+ suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
9719
+ })
9720
+ );
9721
+ }
9722
+ }
9723
+ }
9724
+ }
9725
+ return out;
9726
+ }
9634
9727
  function lintSpec(spec, opts = {}) {
9635
9728
  const out = [...spec.diagnostics];
9636
9729
  for (const req of spec.requirements) {
@@ -11720,6 +11813,10 @@ async function runAnalyze(opts) {
11720
11813
  if (lane !== "fix" && !change.planPath) {
11721
11814
  findings.push(diag("ATLAS-ANALYZE-001", "warning", "El cambio no tiene plan.md (plan t\xE9cnico)", { suggestion: "Ejecuta la fase /satlas-plan" }));
11722
11815
  }
11816
+ if (change.planPath) {
11817
+ const planText = await readTextIfExists(change.planPath) ?? "";
11818
+ findings.push(...lintPlan(planText, change.planPath));
11819
+ }
11723
11820
  const deltaScenarios = [...change.delta?.added ?? [], ...change.delta?.modified ?? []].flatMap((r) => r.scenarios);
11724
11821
  const passed = new Set((change.verify?.evidence ?? []).filter((e) => e.result === "pass").map((e) => e.scenario));
11725
11822
  const evidence = { done: deltaScenarios.filter((s) => passed.has(s.id)).length, total: deltaScenarios.length };
@@ -12800,6 +12897,10 @@ async function runCiGate(opts) {
12800
12897
  requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
12801
12898
  });
12802
12899
  const changeDiags = [...lintFindings, ...trace.findings];
12900
+ if (change.planPath) {
12901
+ const planText = await readTextIfExists(change.planPath) ?? "";
12902
+ changeDiags.push(...lintPlan(planText, change.planPath));
12903
+ }
12803
12904
  if (change.tasks && change.tasks.counts.total > 0) {
12804
12905
  const plan = planWaves(change.tasks, { maxParallel: config.waves.max_parallel });
12805
12906
  changeDiags.push(...plan.blocks.flatMap((block) => block.diagnostics));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specatlas",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "SpecAtlas: kernel determinista de Spec-Driven Development (CLI)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@specatlas/adapters": "0.1.1",
43
- "@specatlas/core": "0.1.13"
43
+ "@specatlas/core": "0.1.15"
44
44
  },
45
45
  "scripts": {
46
46
  "typecheck": "tsc --noEmit",
@@ -30,6 +30,7 @@ El contenido se escribe en **{{LANGUAGE_NAME}}**. El plan es interno: puede (y d
30
30
  1. Contexto AS-IS · 2. Enfoque técnico (con alternativas descartadas) · 3. **Diagramas** · 4. Diseño por capa/módulos · 5. Matriz de trazabilidad (REQ → tareas) · 6. Matriz de paridad AS-IS → TO-BE (solo refactors sustitutivos) · 7. Tareas (referencia) · 8. Riesgos y mitigaciones · 9. Rollback · 10. Dependencias y supuestos.
31
31
  - **Encabezado**: antes de la sección 1, una cita con los metadatos **una clave por línea** — **Cambio**, **Carril**, **Dominio**, **Spec aprobada** y **Contrato visual** (si existe) — y una última línea: «Los artefactos aprobados no se editan: este plan y `tasks.md` son los únicos artefactos que produce esta fase.»
32
32
  3. En `## 3. Diagramas` incluye los diagramas mermaid que el cambio necesite: `erDiagram` si toca datos, `sequenceDiagram` si hay integración/API/jobs, `flowchart` si hay proceso o validaciones, `stateDiagram-v2` si hay estados, `classDiagram` si el dominio no es trivial, diagrama de arquitectura si hay módulos nuevos.
33
+ - Reglas mermaid: cada bloque empieza con el tipo (`flowchart`, `sequenceDiagram`, `stateDiagram-v2`, `erDiagram`…), todo bloque (`alt`/`loop`/`opt`/`par`/`rect`/`subgraph`) se cierra con `end`, y **no uses `;` dentro de los mensajes o etiquetas** de `sequenceDiagram` (mermaid lo interpreta como fin de sentencia: usa `·` o `,`).
33
34
  4. Escribe `tasks.md` con la gramática canónica (separador ` · `):
34
35
  - `## Bloque N — Título` y `- [ ] T<N>.<seq> Acción · Archivos: ruta · Cubre: REQ-…-S1 · Depende de: T<N>.<seq> · Reversión: cómo revertir`
35
36
  - Tareas atómicas (una acción por tarea). Trabajo de infraestructura sin requisito: `· Infra`.