specatlas 0.1.16 → 0.1.17

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
@@ -8871,34 +8871,49 @@ function renderMarkdown(markdown2, opts = {}) {
8871
8871
  quote.push((lines[i] ?? "").replace(/^>\s?/, ""));
8872
8872
  i += 1;
8873
8873
  }
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
8874
  const chips = [];
8883
8875
  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);
8876
+ const chipFor = (label, value) => `<span class="meta-chip"><b>${inlineMarkdown(label)}:</b> ${inlineMarkdown(value)}</span>`;
8877
+ for (const rawLine of quote) {
8878
+ const line2 = rawLine.trim();
8879
+ if (!line2) continue;
8880
+ const whole = /^(?:\*{0,2})([\p{L}][\p{L}\s/()-]{1,32}?)(?:\*{0,2})\s*:\s*(.+)$/u.exec(line2);
8881
+ const hasInlineLabels = / · \s*\*{0,2}[\p{L}][\p{L}\s/()-]{1,32}\*{0,2}:\s/u.test(line2);
8882
+ if (whole && !hasInlineLabels && whole[1].trim().split(/\s+/).length <= 4) {
8883
+ chips.push(chipFor(whole[1], whole[2]));
8884
+ continue;
8885
+ }
8886
+ const labelRe = /(?:^|\s|\*{0,2})([\p{L}][\p{L}\s/()-]{1,32}?)\*{0,2}:\s/gu;
8887
+ const hits = [];
8888
+ let hitMatch;
8889
+ while ((hitMatch = labelRe.exec(line2)) !== null) {
8890
+ if (hitMatch[1].trim().split(/\s+/).length > 4) continue;
8891
+ const prefix = /^\*+/.exec(hitMatch[0])?.[0].length ?? 0;
8892
+ hits.push({ start: hitMatch.index + prefix, end: hitMatch.index + hitMatch[0].length, label: hitMatch[1] });
8893
+ }
8894
+ if (hits.length === 0) {
8895
+ prose.push(line2);
8896
+ continue;
8897
+ }
8898
+ let cursor = 0;
8899
+ for (let h = 0; h < hits.length; h += 1) {
8900
+ const hit = hits[h];
8901
+ const before = line2.slice(cursor, hit.start).replace(/[\s·*]+$/u, "").trim();
8902
+ if (before) prose.push(before);
8903
+ const valueEnd = h + 1 < hits.length ? hits[h + 1].start : line2.length;
8904
+ let value = line2.slice(hit.end, valueEnd).replace(/[\s·*]+$/u, "").trim();
8905
+ const sentenceBreak = /[·.]\s+(?=[\p{Lu}][\p{L}]+(?:\s+[\p{L}]+){3,})/u.exec(value);
8906
+ if (sentenceBreak) {
8907
+ const note = value.slice(sentenceBreak.index + 1).trim();
8908
+ if (note) prose.push(note);
8909
+ value = value.slice(0, sentenceBreak.index + 1);
8910
+ }
8911
+ chips.push(chipFor(hit.label, value));
8912
+ cursor = valueEnd;
8896
8913
  }
8897
- chips.push(`<span class="meta-chip"><b>${inlineMarkdown(hit.label)}:</b> ${inlineMarkdown(value)}</span>`);
8898
- cursor = valueEnd;
8914
+ const tail = line2.slice(cursor).replace(/^[\s·*]+/u, "").trim();
8915
+ if (tail) prose.push(tail);
8899
8916
  }
8900
- const tail = joined.slice(cursor).replace(/^[\s·]+/u, "").trim();
8901
- if (tail) prose.push(tail);
8902
8917
  const chipLine = chips.length > 0 ? `<div class="meta-line">${chips.join("")}</div>` : "";
8903
8918
  const proseHtml = prose.length > 0 ? `<p>${inlineMarkdown(prose.join(" \xB7 "))}</p>` : "";
8904
8919
  out.push(`<blockquote>${chipLine}${proseHtml}</blockquote>`);
@@ -9631,6 +9646,71 @@ function lintDelta(delta, livingRequirements, path192, opts = {}) {
9631
9646
  }
9632
9647
  return out;
9633
9648
  }
9649
+ var MERMAID_KEYWORDS = [
9650
+ "flowchart",
9651
+ "graph",
9652
+ "sequenceDiagram",
9653
+ "stateDiagram-v2",
9654
+ "stateDiagram",
9655
+ "classDiagram",
9656
+ "erDiagram",
9657
+ "journey",
9658
+ "gantt",
9659
+ "pie",
9660
+ "mindmap",
9661
+ "timeline",
9662
+ "quadrantChart",
9663
+ "xychart-beta",
9664
+ "block-beta",
9665
+ "architecture-beta"
9666
+ ];
9667
+ var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
9668
+ function lintPlan(planText, path192) {
9669
+ const out = [];
9670
+ const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
9671
+ for (const [index, block] of blocks.entries()) {
9672
+ const code = (block[1] ?? "").replace(/\r\n?/g, "\n");
9673
+ const lines = code.split("\n");
9674
+ const first = (lines.find((line) => line.trim().length > 0) ?? "").trim();
9675
+ if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
9676
+ out.push(
9677
+ 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)}"`, {
9678
+ path: path192,
9679
+ suggestion: "Corrige el tipo del diagrama o elimina el bloque"
9680
+ })
9681
+ );
9682
+ continue;
9683
+ }
9684
+ let open = 0;
9685
+ for (const line of lines) {
9686
+ if (MERMAID_BLOCKS.test(line)) open += 1;
9687
+ else if (/^\s*end\b/.test(line)) open -= 1;
9688
+ }
9689
+ if (open !== 0) {
9690
+ out.push(
9691
+ diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
9692
+ path: path192,
9693
+ suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
9694
+ })
9695
+ );
9696
+ }
9697
+ if (first.startsWith("sequenceDiagram")) {
9698
+ for (const line of lines) {
9699
+ if (!/^\s*[^\s:]+-{1,2}>{1,2}[^\s:]*\s*:\s*/.test(line)) continue;
9700
+ const message = line.slice(line.indexOf(":") + 1);
9701
+ if (message.includes(";")) {
9702
+ out.push(
9703
+ 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`, {
9704
+ path: path192,
9705
+ suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
9706
+ })
9707
+ );
9708
+ }
9709
+ }
9710
+ }
9711
+ }
9712
+ return out;
9713
+ }
9634
9714
  function lintSpec(spec, opts = {}) {
9635
9715
  const out = [...spec.diagnostics];
9636
9716
  for (const req of spec.requirements) {
@@ -11720,6 +11800,10 @@ async function runAnalyze(opts) {
11720
11800
  if (lane !== "fix" && !change.planPath) {
11721
11801
  findings.push(diag("ATLAS-ANALYZE-001", "warning", "El cambio no tiene plan.md (plan t\xE9cnico)", { suggestion: "Ejecuta la fase /satlas-plan" }));
11722
11802
  }
11803
+ if (change.planPath) {
11804
+ const planText = await readTextIfExists(change.planPath) ?? "";
11805
+ findings.push(...lintPlan(planText, change.planPath));
11806
+ }
11723
11807
  const deltaScenarios = [...change.delta?.added ?? [], ...change.delta?.modified ?? []].flatMap((r) => r.scenarios);
11724
11808
  const passed = new Set((change.verify?.evidence ?? []).filter((e) => e.result === "pass").map((e) => e.scenario));
11725
11809
  const evidence = { done: deltaScenarios.filter((s) => passed.has(s.id)).length, total: deltaScenarios.length };
@@ -12800,6 +12884,10 @@ async function runCiGate(opts) {
12800
12884
  requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
12801
12885
  });
12802
12886
  const changeDiags = [...lintFindings, ...trace.findings];
12887
+ if (change.planPath) {
12888
+ const planText = await readTextIfExists(change.planPath) ?? "";
12889
+ changeDiags.push(...lintPlan(planText, change.planPath));
12890
+ }
12803
12891
  if (change.tasks && change.tasks.counts.total > 0) {
12804
12892
  const plan = planWaves(change.tasks, { maxParallel: config.waves.max_parallel });
12805
12893
  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.17",
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.14"
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`.