specatlas 0.1.15 → 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 +186 -36
- package/package.json +2 -2
- package/workflow/phases/plan.md +1 -0
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
|
-
|
|
8885
|
-
for (
|
|
8886
|
-
const
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
const
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
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
|
-
|
|
8898
|
-
|
|
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>`);
|
|
@@ -8976,6 +8991,7 @@ import path13 from "path";
|
|
|
8976
8991
|
import path14 from "path";
|
|
8977
8992
|
import path15 from "path";
|
|
8978
8993
|
import { promises as fs } from "fs";
|
|
8994
|
+
import { cp, rename, rm } from "fs/promises";
|
|
8979
8995
|
import path16 from "path";
|
|
8980
8996
|
import path17 from "path";
|
|
8981
8997
|
import path18 from "path";
|
|
@@ -9620,12 +9636,77 @@ function lintDelta(delta, livingRequirements, path192, opts = {}) {
|
|
|
9620
9636
|
out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path192, line: req.line }));
|
|
9621
9637
|
}
|
|
9622
9638
|
}
|
|
9623
|
-
for (const
|
|
9624
|
-
if (
|
|
9625
|
-
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${
|
|
9639
|
+
for (const rename2 of delta.renamed) {
|
|
9640
|
+
if (rename2.from.id !== rename2.to.id) {
|
|
9641
|
+
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path192, line: rename2.line }));
|
|
9626
9642
|
}
|
|
9627
|
-
if (!livingRequirements.has(
|
|
9628
|
-
out.push(diag("TRACE-007", "error", `RENAMED ${
|
|
9643
|
+
if (!livingRequirements.has(rename2.from.id)) {
|
|
9644
|
+
out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path192, line: rename2.line }));
|
|
9645
|
+
}
|
|
9646
|
+
}
|
|
9647
|
+
return out;
|
|
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
|
+
}
|
|
9629
9710
|
}
|
|
9630
9711
|
}
|
|
9631
9712
|
return out;
|
|
@@ -11719,6 +11800,10 @@ async function runAnalyze(opts) {
|
|
|
11719
11800
|
if (lane !== "fix" && !change.planPath) {
|
|
11720
11801
|
findings.push(diag("ATLAS-ANALYZE-001", "warning", "El cambio no tiene plan.md (plan t\xE9cnico)", { suggestion: "Ejecuta la fase /satlas-plan" }));
|
|
11721
11802
|
}
|
|
11803
|
+
if (change.planPath) {
|
|
11804
|
+
const planText = await readTextIfExists(change.planPath) ?? "";
|
|
11805
|
+
findings.push(...lintPlan(planText, change.planPath));
|
|
11806
|
+
}
|
|
11722
11807
|
const deltaScenarios = [...change.delta?.added ?? [], ...change.delta?.modified ?? []].flatMap((r) => r.scenarios);
|
|
11723
11808
|
const passed = new Set((change.verify?.evidence ?? []).filter((e) => e.result === "pass").map((e) => e.scenario));
|
|
11724
11809
|
const evidence = { done: deltaScenarios.filter((s) => passed.has(s.id)).length, total: deltaScenarios.length };
|
|
@@ -12539,16 +12624,16 @@ function foldDelta(livingBody, delta, language = "es") {
|
|
|
12539
12624
|
lines = [...lines.slice(0, range.start), ...lines.slice(range.end)];
|
|
12540
12625
|
applied.removed.push(req.id);
|
|
12541
12626
|
}
|
|
12542
|
-
for (const
|
|
12543
|
-
const range = findRange(
|
|
12627
|
+
for (const rename2 of delta.renamed) {
|
|
12628
|
+
const range = findRange(rename2.from.id);
|
|
12544
12629
|
if (!range) {
|
|
12545
|
-
diagnostics.push(diag("TRACE-007", "error", `No se puede renombrar ${
|
|
12630
|
+
diagnostics.push(diag("TRACE-007", "error", `No se puede renombrar ${rename2.from.id}: no existe en la spec viva`, { path: delta.path, line: rename2.line }));
|
|
12546
12631
|
continue;
|
|
12547
12632
|
}
|
|
12548
12633
|
const header = lines[range.start] ?? "";
|
|
12549
12634
|
const label = /Requirement/i.test(header) ? "Requirement" : "Requisito";
|
|
12550
|
-
lines[range.start] = header.replace(REQ_HEADER_RE, (_match, id) => `### ${label}: ${id} \u2014 ${
|
|
12551
|
-
applied.renamed.push(
|
|
12635
|
+
lines[range.start] = header.replace(REQ_HEADER_RE, (_match, id) => `### ${label}: ${id} \u2014 ${rename2.to.title}`);
|
|
12636
|
+
applied.renamed.push(rename2.from.id);
|
|
12552
12637
|
}
|
|
12553
12638
|
for (const req of delta.added) {
|
|
12554
12639
|
const rendered = renderRequirement(req, language);
|
|
@@ -12563,6 +12648,46 @@ function trimTrailingBlanks(lines) {
|
|
|
12563
12648
|
while (out.length > 0 && (out[out.length - 1] ?? "").trim() === "") out.pop();
|
|
12564
12649
|
return out;
|
|
12565
12650
|
}
|
|
12651
|
+
var TRANSIENT_MOVE_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "ENOTEMPTY", "EACCES"]);
|
|
12652
|
+
function delay(ms) {
|
|
12653
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12654
|
+
}
|
|
12655
|
+
async function moveDirectory(from, to, ops = {}) {
|
|
12656
|
+
const doRename = ops.rename ?? rename;
|
|
12657
|
+
const doCopy = ops.cp ?? cp;
|
|
12658
|
+
const doRemove = ops.rm ?? rm;
|
|
12659
|
+
let lastError;
|
|
12660
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
12661
|
+
try {
|
|
12662
|
+
await doRename(from, to);
|
|
12663
|
+
return { strategy: "rename" };
|
|
12664
|
+
} catch (error) {
|
|
12665
|
+
lastError = error;
|
|
12666
|
+
const code = error.code ?? "";
|
|
12667
|
+
if (!TRANSIENT_MOVE_CODES.has(code)) throw error;
|
|
12668
|
+
await delay(250 * attempt);
|
|
12669
|
+
}
|
|
12670
|
+
}
|
|
12671
|
+
await doCopy(from, to, { recursive: true, force: true });
|
|
12672
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
12673
|
+
try {
|
|
12674
|
+
await doRemove(from, { recursive: true, force: true, maxRetries: 2 });
|
|
12675
|
+
return { strategy: "copy" };
|
|
12676
|
+
} catch (error) {
|
|
12677
|
+
lastError = error;
|
|
12678
|
+
await delay(350 * attempt);
|
|
12679
|
+
}
|
|
12680
|
+
}
|
|
12681
|
+
await doRemove(to, { recursive: true, force: true }).catch(() => void 0);
|
|
12682
|
+
throw lastError;
|
|
12683
|
+
}
|
|
12684
|
+
async function restoreFile(file, previous) {
|
|
12685
|
+
try {
|
|
12686
|
+
if (previous === void 0) await fs.rm(file, { force: true });
|
|
12687
|
+
else await writeText(file, previous);
|
|
12688
|
+
} catch {
|
|
12689
|
+
}
|
|
12690
|
+
}
|
|
12566
12691
|
async function archiveChange(opts) {
|
|
12567
12692
|
const root = path16.resolve(opts.root);
|
|
12568
12693
|
const diagnostics = [];
|
|
@@ -12587,7 +12712,17 @@ async function archiveChange(opts) {
|
|
|
12587
12712
|
}
|
|
12588
12713
|
if (!opts.dryRun) {
|
|
12589
12714
|
await ensureDir(path16.dirname(targetFix));
|
|
12590
|
-
|
|
12715
|
+
try {
|
|
12716
|
+
await moveDirectory(change.dir, targetFix);
|
|
12717
|
+
} catch (error) {
|
|
12718
|
+
diagnostics.push(
|
|
12719
|
+
diag("ATLAS-ARCH-004", "error", `No se pudo mover el cambio al hist\xF3rico: ${error.message}`, {
|
|
12720
|
+
path: change.dir,
|
|
12721
|
+
suggestion: "Cierra las pesta\xF1as con archivos de este cambio (y espera unos segundos si OneDrive est\xE1 sincronizando) y vuelve a intentar"
|
|
12722
|
+
})
|
|
12723
|
+
);
|
|
12724
|
+
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: false };
|
|
12725
|
+
}
|
|
12591
12726
|
await regenerateIndex(root, config);
|
|
12592
12727
|
}
|
|
12593
12728
|
return { slug: opts.slug, archivedTo: targetFix, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -12640,7 +12775,18 @@ ${body}`;
|
|
|
12640
12775
|
if (!opts.dryRun) {
|
|
12641
12776
|
await writeText(specFile, nextContent);
|
|
12642
12777
|
await ensureDir(archiveDir);
|
|
12643
|
-
|
|
12778
|
+
try {
|
|
12779
|
+
await moveDirectory(change.dir, target);
|
|
12780
|
+
} catch (error) {
|
|
12781
|
+
await restoreFile(specFile, existing);
|
|
12782
|
+
diagnostics.push(
|
|
12783
|
+
diag("ATLAS-ARCH-004", "error", `No se pudo mover el cambio al hist\xF3rico: ${error.message}`, {
|
|
12784
|
+
path: change.dir,
|
|
12785
|
+
suggestion: "Cierra las pesta\xF1as con archivos de este cambio (y espera unos segundos si OneDrive est\xE1 sincronizando) y vuelve a intentar; el cambio y la spec viva quedaron como estaban"
|
|
12786
|
+
})
|
|
12787
|
+
);
|
|
12788
|
+
return { slug: opts.slug, domain, fold, diagnostics, dryRun: false };
|
|
12789
|
+
}
|
|
12644
12790
|
await regenerateIndex(root, config, now);
|
|
12645
12791
|
}
|
|
12646
12792
|
return { slug: opts.slug, domain, archivedTo: target, fold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -12738,6 +12884,10 @@ async function runCiGate(opts) {
|
|
|
12738
12884
|
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
12739
12885
|
});
|
|
12740
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
|
+
}
|
|
12741
12891
|
if (change.tasks && change.tasks.counts.total > 0) {
|
|
12742
12892
|
const plan = planWaves(change.tasks, { maxParallel: config.waves.max_parallel });
|
|
12743
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.
|
|
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.
|
|
43
|
+
"@specatlas/core": "0.1.14"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"typecheck": "tsc --noEmit",
|
package/workflow/phases/plan.md
CHANGED
|
@@ -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`.
|