@specatlas/core 0.1.24 → 0.1.26

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/index.js CHANGED
@@ -794,14 +794,14 @@ function wordBoundary(text, term) {
794
794
  const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
795
795
  return re.test(text);
796
796
  }
797
- function lintText(text, code, terms, label, path21, line) {
797
+ function lintText(text, code, terms, label, path22, line) {
798
798
  const out = [];
799
799
  const lower = text.toLowerCase();
800
800
  for (const term of terms) {
801
801
  if (wordBoundary(lower, term)) {
802
802
  out.push(
803
803
  diag(code, "error", `${label}: "${term}"`, {
804
- path: path21,
804
+ path: path22,
805
805
  line,
806
806
  suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
807
807
  })
@@ -810,7 +810,7 @@ function lintText(text, code, terms, label, path21, line) {
810
810
  }
811
811
  return out;
812
812
  }
813
- function lintRequirement(req, path21, opts = {}) {
813
+ function lintRequirement(req, path22, opts = {}) {
814
814
  const out = [];
815
815
  const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
816
816
  const tech = opts.language === "en" ? TECH_EN : TECH_ES;
@@ -821,32 +821,32 @@ function lintRequirement(req, path21, opts = {}) {
821
821
  ...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
822
822
  ];
823
823
  for (const part of parts) {
824
- out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path21, part.line));
824
+ out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path22, part.line));
825
825
  if (opts.businessOnly !== false) {
826
- out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path21, part.line));
826
+ out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path22, part.line));
827
827
  }
828
828
  }
829
829
  if (req.scenarios.length === 0) {
830
- out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path21, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
830
+ out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path22, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
831
831
  }
832
832
  return out;
833
833
  }
834
- function lintDelta(delta, livingRequirements, path21, opts = {}) {
834
+ function lintDelta(delta, livingRequirements, path22, opts = {}) {
835
835
  const out = [...delta.diagnostics];
836
836
  for (const req of [...delta.added, ...delta.modified]) {
837
- out.push(...lintRequirement(req, path21, opts));
837
+ out.push(...lintRequirement(req, path22, opts));
838
838
  }
839
839
  for (const req of delta.modified) {
840
840
  const living = livingRequirements.get(req.id);
841
841
  if (!living) {
842
- out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path21, line: req.line }));
842
+ out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path22, line: req.line }));
843
843
  continue;
844
844
  }
845
845
  for (const existing of living.scenarios) {
846
846
  if (!req.scenarios.some((s) => s.id === existing.id)) {
847
847
  out.push(
848
848
  diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
849
- path: path21,
849
+ path: path22,
850
850
  line: req.line,
851
851
  suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
852
852
  })
@@ -857,15 +857,15 @@ function lintDelta(delta, livingRequirements, path21, opts = {}) {
857
857
  for (const req of delta.removed) {
858
858
  const living = livingRequirements.get(req.id);
859
859
  if (!living) {
860
- out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path21, line: req.line }));
860
+ out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path22, line: req.line }));
861
861
  }
862
862
  }
863
863
  for (const rename2 of delta.renamed) {
864
864
  if (rename2.from.id !== rename2.to.id) {
865
- out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path21, line: rename2.line }));
865
+ out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path22, line: rename2.line }));
866
866
  }
867
867
  if (!livingRequirements.has(rename2.from.id)) {
868
- out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path21, line: rename2.line }));
868
+ out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path22, line: rename2.line }));
869
869
  }
870
870
  }
871
871
  return out;
@@ -895,7 +895,7 @@ var MERMAID_KEYWORDS = [
895
895
  "architecture-beta"
896
896
  ];
897
897
  var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
898
- function lintPlan(planText, path21) {
898
+ function lintPlan(planText, path22) {
899
899
  const out = [];
900
900
  const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
901
901
  for (const [index, block] of blocks.entries()) {
@@ -905,7 +905,7 @@ function lintPlan(planText, path21) {
905
905
  if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
906
906
  out.push(
907
907
  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)}"`, {
908
- path: path21,
908
+ path: path22,
909
909
  suggestion: "Corrige el tipo del diagrama o elimina el bloque"
910
910
  })
911
911
  );
@@ -919,7 +919,7 @@ function lintPlan(planText, path21) {
919
919
  if (open !== 0) {
920
920
  out.push(
921
921
  diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
922
- path: path21,
922
+ path: path22,
923
923
  suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
924
924
  })
925
925
  );
@@ -931,7 +931,7 @@ function lintPlan(planText, path21) {
931
931
  if (message.includes(";")) {
932
932
  out.push(
933
933
  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`, {
934
- path: path21,
934
+ path: path22,
935
935
  suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
936
936
  })
937
937
  );
@@ -1088,6 +1088,16 @@ function checkTrace(input) {
1088
1088
  findings.push(diag("TRACE-006", "warning", `Evidencia hu\xE9rfana: ${ev.scenario} no existe`, { path: change.verify?.path, line: ev.line }));
1089
1089
  }
1090
1090
  }
1091
+ for (const cover of change.fixCovers ?? []) {
1092
+ if (!livingReqs.has(cover)) {
1093
+ findings.push(
1094
+ diag("TRACE-011", "warning", `El fix ${change.slug} declara ${cover}, que no existe en las specs vivas`, {
1095
+ path: change.fix?.path,
1096
+ suggestion: "Corrige el identificador o qu\xEDtalo: la declaraci\xF3n es opcional"
1097
+ })
1098
+ );
1099
+ }
1100
+ }
1091
1101
  const summary = countBySeverity(findings);
1092
1102
  return { graph: buildTraceGraph(input), findings, summary };
1093
1103
  }
@@ -1463,35 +1473,149 @@ function detectionToYaml(result, generatedAt) {
1463
1473
  }
1464
1474
 
1465
1475
  // src/workspace.ts
1476
+ import path5 from "path";
1477
+
1478
+ // src/fixes.ts
1466
1479
  import path4 from "path";
1480
+ import { stringify as stringifyYaml3 } from "yaml";
1481
+ var LIVING_FIXES_DIR = path4.join(".sdd", "fixes");
1482
+ var COMMENT_RE = /<!--[\s\S]*?-->/g;
1483
+ function parseFixCovers(content) {
1484
+ const clean = content.replace(COMMENT_RE, "");
1485
+ const covers = [];
1486
+ for (const line of clean.replace(/\r\n?/g, "\n").split("\n")) {
1487
+ const match = /^\s*-?\s*Cubre:\s*(.+?)\s*$/.exec(line);
1488
+ if (!match) continue;
1489
+ for (const raw of (match[1] ?? "").split(",")) {
1490
+ const id = raw.trim().toUpperCase();
1491
+ if (/^REQ-[A-Z0-9-]+$/.test(id) && !covers.includes(id)) covers.push(id);
1492
+ }
1493
+ }
1494
+ return covers;
1495
+ }
1496
+ function coversOf(value) {
1497
+ if (Array.isArray(value)) {
1498
+ return value.map((v) => String(v).trim().toUpperCase()).filter((v) => /^REQ-[A-Z0-9-]+$/.test(v));
1499
+ }
1500
+ if (typeof value === "string") {
1501
+ return value.split(",").map((v) => v.trim().toUpperCase()).filter((v) => /^REQ-[A-Z0-9-]+$/.test(v));
1502
+ }
1503
+ return [];
1504
+ }
1505
+ function parseLivingFix(content, file) {
1506
+ const fm = parseFrontmatter(content, file);
1507
+ const data = fm.data;
1508
+ const base = path4.basename(file).replace(/\.md$/i, "");
1509
+ const slug = typeof data["slug"] === "string" && data["slug"].trim() !== "" ? data["slug"].trim() : base.replace(/^\d{4}-\d{2}-/, "");
1510
+ const fix = {
1511
+ slug,
1512
+ file,
1513
+ date: typeof data["date"] === "string" ? data["date"] : "",
1514
+ result: typeof data["result"] === "string" ? data["result"] : "pass",
1515
+ covers: coversOf(data["covers"]),
1516
+ content: fm.body.trimStart(),
1517
+ source: "living"
1518
+ };
1519
+ if (typeof data["domain"] === "string") fix.domain = data["domain"];
1520
+ if (typeof data["title"] === "string") fix.title = data["title"];
1521
+ return fix;
1522
+ }
1523
+ async function archivedFixes(root, known) {
1524
+ const archiveDir = path4.join(root, ".sdd", "changes", "archive");
1525
+ const fixes = [];
1526
+ for (const entry of await listDirs(archiveDir)) {
1527
+ const dir = path4.join(archiveDir, entry);
1528
+ const fixFile = path4.join(dir, "fix.md");
1529
+ const fixRaw = await readTextIfExists(fixFile);
1530
+ if (fixRaw === void 0) continue;
1531
+ const metaFile = path4.join(dir, "meta.yaml");
1532
+ const metaRaw = await readTextIfExists(metaFile);
1533
+ const meta = metaRaw !== void 0 ? parseChangeMeta(metaRaw, metaFile).meta : void 0;
1534
+ if (meta?.lane !== "fix") continue;
1535
+ const slug = entry.replace(/^\d{4}-\d{2}-/, "");
1536
+ if (known.has(slug)) continue;
1537
+ const evidence = parseVerifyFile(fixRaw, fixFile).evidence;
1538
+ const fix = {
1539
+ slug,
1540
+ file: fixFile,
1541
+ date: /^(\d{4}-\d{2})/.exec(entry)?.[1] ?? "",
1542
+ result: evidence.some((e) => e.result === "pass") ? "pass" : evidence.length > 0 ? "fail" : "pending",
1543
+ covers: parseFixCovers(fixRaw),
1544
+ content: parseFrontmatter(fixRaw, fixFile).body.trimStart(),
1545
+ source: "archive"
1546
+ };
1547
+ if (meta.domain !== void 0) fix.domain = meta.domain;
1548
+ if (meta.title !== void 0) fix.title = meta.title;
1549
+ fixes.push(fix);
1550
+ }
1551
+ return fixes;
1552
+ }
1553
+ async function loadLivingFixes(root) {
1554
+ const dir = path4.join(root, LIVING_FIXES_DIR);
1555
+ const entries = (await listDir(dir)).filter((entry) => entry.toLowerCase().endsWith(".md")).sort();
1556
+ const fixes = [];
1557
+ for (const entry of entries) {
1558
+ const file = path4.join(dir, entry);
1559
+ const content = await readTextIfExists(file);
1560
+ if (content === void 0) continue;
1561
+ fixes.push(parseLivingFix(content, file));
1562
+ }
1563
+ fixes.push(...await archivedFixes(root, new Set(fixes.map((fix) => fix.slug))));
1564
+ return fixes.sort((a, b) => b.date.localeCompare(a.date) || b.slug.localeCompare(a.slug));
1565
+ }
1566
+ async function writeLivingFix(root, input) {
1567
+ const dir = path4.join(root, LIVING_FIXES_DIR);
1568
+ const month = input.date.slice(0, 7);
1569
+ const file = path4.join(dir, `${month}-${input.slug}.md`);
1570
+ const relativePath = toPosix(path4.relative(root, file));
1571
+ if (await exists(file)) return { file, relativePath, created: false };
1572
+ const data = {
1573
+ slug: input.slug,
1574
+ date: input.date,
1575
+ result: input.result ?? "pass"
1576
+ };
1577
+ if (input.domain) data["domain"] = input.domain;
1578
+ if (input.title) data["title"] = input.title;
1579
+ if (input.covers && input.covers.length > 0) data["covers"] = input.covers;
1580
+ const body = input.content.replace(/^[\r\n]+/, "").trimEnd();
1581
+ const text = `---
1582
+ ${stringifyYaml3(data, { lineWidth: 120 })}---
1583
+
1584
+ ${body}
1585
+ `;
1586
+ await writeText(file, text);
1587
+ return { file, relativePath, created: true };
1588
+ }
1589
+
1590
+ // src/workspace.ts
1467
1591
  var SDD_DIR = ".sdd";
1468
1592
  async function findWorkspaceRoot(start) {
1469
- let current = path4.resolve(start);
1593
+ let current = path5.resolve(start);
1470
1594
  for (let i = 0; i < 40; i += 1) {
1471
- if (await isDirectory(path4.join(current, SDD_DIR))) return current;
1472
- const parent = path4.dirname(current);
1595
+ if (await isDirectory(path5.join(current, SDD_DIR))) return current;
1596
+ const parent = path5.dirname(current);
1473
1597
  if (parent === current) return void 0;
1474
1598
  current = parent;
1475
1599
  }
1476
1600
  return void 0;
1477
1601
  }
1478
1602
  async function loadApprovals(sddDir) {
1479
- const file = path4.join(sddDir, "approvals.yaml");
1603
+ const file = path5.join(sddDir, "approvals.yaml");
1480
1604
  const raw = await readTextIfExists(file);
1481
1605
  if (raw === void 0) return { byArtifact: /* @__PURE__ */ new Map(), diagnostics: [] };
1482
1606
  const parsed = parseApprovals(raw, file);
1483
1607
  const map = /* @__PURE__ */ new Map();
1484
1608
  for (const a of parsed.approvals?.approvals ?? []) {
1485
- map.set(a.artifact.split(path4.sep).join("/"), { hash: a.artifactHash, by: a.approvedBy, at: a.approvedAt });
1609
+ map.set(a.artifact.split(path5.sep).join("/"), { hash: a.artifactHash, by: a.approvedBy, at: a.approvedAt });
1486
1610
  }
1487
1611
  return { byArtifact: map, diagnostics: parsed.diagnostics };
1488
1612
  }
1489
1613
  async function loadSpecs(sddDir) {
1490
- const specsDir = path4.join(sddDir, "specs");
1614
+ const specsDir = path5.join(sddDir, "specs");
1491
1615
  const domains = await listDirs(specsDir);
1492
1616
  const out = [];
1493
1617
  for (const domain of domains) {
1494
- const file = path4.join(specsDir, domain, "spec.md");
1618
+ const file = path5.join(specsDir, domain, "spec.md");
1495
1619
  if (!await exists(file)) continue;
1496
1620
  const content = await readText(file);
1497
1621
  out.push({ domain, path: file, spec: parseSpecFile(content, file) });
@@ -1499,10 +1623,10 @@ async function loadSpecs(sddDir) {
1499
1623
  return out;
1500
1624
  }
1501
1625
  async function loadChange(root, slug, relDir) {
1502
- const dir = path4.join(root, SDD_DIR, "changes", relDir ?? slug);
1626
+ const dir = path5.join(root, SDD_DIR, "changes", relDir ?? slug);
1503
1627
  const diagnostics = [];
1504
1628
  const change = { slug, dir, diagnostics };
1505
- const metaFile = path4.join(dir, "meta.yaml");
1629
+ const metaFile = path5.join(dir, "meta.yaml");
1506
1630
  const metaRaw = await readTextIfExists(metaFile);
1507
1631
  if (metaRaw === void 0) {
1508
1632
  diagnostics.push(diag("ATLAS-FILES-001", "error", `El cambio "${slug}" no tiene meta.yaml`, { path: metaFile, suggestion: "Crea meta.yaml con slug, lane y dominio" }));
@@ -1511,7 +1635,7 @@ async function loadChange(root, slug, relDir) {
1511
1635
  diagnostics.push(...parsed.diagnostics);
1512
1636
  if (parsed.meta) change.meta = parsed.meta;
1513
1637
  }
1514
- const deltaFile = path4.join(dir, "spec.md");
1638
+ const deltaFile = path5.join(dir, "spec.md");
1515
1639
  const deltaRaw = await readTextIfExists(deltaFile);
1516
1640
  if (deltaRaw !== void 0) {
1517
1641
  change.delta = parseDelta(deltaRaw, deltaFile);
@@ -1519,41 +1643,42 @@ async function loadChange(root, slug, relDir) {
1519
1643
  } else {
1520
1644
  diagnostics.push(diag("ATLAS-FILES-002", "warning", `El cambio "${slug}" no tiene spec.md (delta)`, { path: deltaFile }));
1521
1645
  }
1522
- const planFile = path4.join(dir, "plan.md");
1646
+ const planFile = path5.join(dir, "plan.md");
1523
1647
  if (await exists(planFile)) change.planPath = planFile;
1524
- const reviewFile = path4.join(dir, "review.md");
1648
+ const reviewFile = path5.join(dir, "review.md");
1525
1649
  if (await exists(reviewFile)) change.reviewPath = reviewFile;
1526
- const presentationFile = path4.join(dir, "presentation", "index.html");
1650
+ const presentationFile = path5.join(dir, "presentation", "index.html");
1527
1651
  if (await exists(presentationFile)) change.presentationPath = presentationFile;
1528
- const tasksFile = path4.join(dir, "tasks.md");
1652
+ const tasksFile = path5.join(dir, "tasks.md");
1529
1653
  const tasksRaw = await readTextIfExists(tasksFile);
1530
1654
  if (tasksRaw !== void 0) {
1531
1655
  change.tasks = parseTasksFile(tasksRaw, tasksFile);
1532
1656
  diagnostics.push(...change.tasks.diagnostics);
1533
1657
  }
1534
- const verifyFile = path4.join(dir, "verify.md");
1658
+ const verifyFile = path5.join(dir, "verify.md");
1535
1659
  const verifyRaw = await readTextIfExists(verifyFile);
1536
1660
  if (verifyRaw !== void 0) {
1537
1661
  change.verify = parseVerifyFile(verifyRaw, verifyFile);
1538
1662
  diagnostics.push(...change.verify.diagnostics);
1539
1663
  }
1540
- const fixFile = path4.join(dir, "fix.md");
1664
+ const fixFile = path5.join(dir, "fix.md");
1541
1665
  const fixRaw = await readTextIfExists(fixFile);
1542
1666
  if (fixRaw !== void 0) {
1543
1667
  change.fix = parseVerifyFile(fixRaw, fixFile);
1544
1668
  diagnostics.push(...change.fix.diagnostics);
1669
+ change.fixCovers = parseFixCovers(fixRaw);
1545
1670
  }
1546
- const mockupManifest = path4.join(dir, "mockups", "manifest.yaml");
1671
+ const mockupManifest = path5.join(dir, "mockups", "manifest.yaml");
1547
1672
  if (await exists(mockupManifest)) change.mockupManifestPath = mockupManifest;
1548
1673
  return change;
1549
1674
  }
1550
1675
  async function listChangeSlugs(root, opts = {}) {
1551
- const changesDir = path4.join(root, SDD_DIR, "changes");
1676
+ const changesDir = path5.join(root, SDD_DIR, "changes");
1552
1677
  const dirs = await listDirs(changesDir);
1553
1678
  return dirs.filter((d) => opts.includeArchived ? true : d !== "archive");
1554
1679
  }
1555
1680
  async function loadWorkspace(root) {
1556
- const sddDir = path4.join(root, SDD_DIR);
1681
+ const sddDir = path5.join(root, SDD_DIR);
1557
1682
  const { config, diagnostics: configDiags } = await loadConfig(sddDir);
1558
1683
  const diagnostics = [...configDiags];
1559
1684
  const specs = await loadSpecs(sddDir);
@@ -1566,20 +1691,20 @@ async function loadWorkspace(root) {
1566
1691
  diagnostics.push(...change.diagnostics);
1567
1692
  }
1568
1693
  const archived = [];
1569
- const archivedDir = path4.join(sddDir, "changes", "archive");
1694
+ const archivedDir = path5.join(sddDir, "changes", "archive");
1570
1695
  if (await isDirectory(archivedDir)) {
1571
1696
  for (const entry of await listDirs(archivedDir)) {
1572
- archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""), path4.join("archive", entry)));
1697
+ archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""), path5.join("archive", entry)));
1573
1698
  }
1574
1699
  }
1575
1700
  return { workspace: { root, sddDir, specs, changes, archived, diagnostics }, config };
1576
1701
  }
1577
1702
  async function ensureSddDirs(sddDir) {
1578
1703
  const created = [];
1579
- const dirs = ["specs", "changes", "runs", "metrics", path4.join("profiles", "custom")];
1704
+ const dirs = ["specs", "changes", "runs", "metrics", "fixes", path5.join("profiles", "custom")];
1580
1705
  const { ensureDir: ensureDir2 } = await import("./fsx-VF2P7ALA.js");
1581
1706
  for (const d of dirs) {
1582
- const full = path4.join(sddDir, d);
1707
+ const full = path5.join(sddDir, d);
1583
1708
  if (!await exists(full)) {
1584
1709
  await ensureDir2(full);
1585
1710
  created.push(full);
@@ -1589,7 +1714,7 @@ async function ensureSddDirs(sddDir) {
1589
1714
  }
1590
1715
 
1591
1716
  // src/templates.ts
1592
- import { stringify as stringifyYaml3 } from "yaml";
1717
+ import { stringify as stringifyYaml4 } from "yaml";
1593
1718
  var ES = {
1594
1719
  constitution: `# Constituci\xF3n del proyecto
1595
1720
 
@@ -1750,6 +1875,10 @@ notes: 12/12 casos
1750
1875
 
1751
1876
  ## Evidencia
1752
1877
 
1878
+ <!-- Opcional: declara los requisitos que este fix afecta (aparecen en su trazabilidad).
1879
+ Cubre: REQ-DOMINIO-001
1880
+ -->
1881
+
1753
1882
  <!-- Registra la evidencia real con:
1754
1883
  satlas verify <slug> --file fix --scenario REQ-DOMINIO-001-S1 --command "<comando>" --by "<nombre>"
1755
1884
  o, si es manual:
@@ -1873,6 +2002,10 @@ by: your-name
1873
2002
  ## Rollback
1874
2003
  ## Evidence
1875
2004
 
2005
+ <!-- Optional: declare the requirements this fix affects (they show up in its traceability).
2006
+ Cubre: REQ-DOMAIN-001
2007
+ -->
2008
+
1876
2009
  <!-- Register real evidence with:
1877
2010
  satlas verify <slug> --file fix --scenario REQ-DOMAIN-001-S1 --command "<command>" --by "<name>"
1878
2011
  -->
@@ -1897,7 +2030,7 @@ function changeMetaYaml(meta) {
1897
2030
  if (meta.owner) doc["owner"] = meta.owner;
1898
2031
  if (meta.mockups) doc["mockups"] = meta.mockups;
1899
2032
  return `# Estado del cambio. La fase se DERIVA de los artefactos; aqu\xED solo hechos.
1900
- ` + stringifyYaml3(doc, { lineWidth: 120 });
2033
+ ` + stringifyYaml4(doc, { lineWidth: 120 });
1901
2034
  }
1902
2035
  function indexMarkdown(input) {
1903
2036
  const es = input.language !== "en";
@@ -1911,6 +2044,15 @@ function indexMarkdown(input) {
1911
2044
  if (input.specs.length === 0) lines.push(es ? "_Sin specs todav\xEDa._" : "_No specs yet._");
1912
2045
  for (const spec of input.specs) lines.push(`- \`${spec.domain}\` \u2014 ${spec.requirements} ${es ? "requisitos" : "requirements"}`);
1913
2046
  lines.push("");
2047
+ lines.push(es ? "## Fixes vivos" : "## Living fixes");
2048
+ lines.push("");
2049
+ const fixes = input.fixes ?? [];
2050
+ if (fixes.length === 0) lines.push(es ? "_Sin fixes archivados todav\xEDa._" : "_No archived fixes yet._");
2051
+ for (const fix of fixes) {
2052
+ const parts = [fix.date, fix.domain ? `\`${fix.domain}\`` : "", `\`${fix.slug}\``, fix.result].filter((part) => part !== "");
2053
+ lines.push(`- ${parts.join(" \xB7 ")}`);
2054
+ }
2055
+ lines.push("");
1914
2056
  lines.push(es ? "## Cambios activos" : "## Active changes");
1915
2057
  lines.push("");
1916
2058
  if (input.changes.length === 0) lines.push(es ? "_Sin cambios activos._" : "_No active changes._");
@@ -1926,7 +2068,7 @@ ${input.archived} change(s) in \`changes/archive/\`.`);
1926
2068
  }
1927
2069
 
1928
2070
  // src/github.ts
1929
- import path5 from "path";
2071
+ import path6 from "path";
1930
2072
  import { parse as parseYaml5 } from "yaml";
1931
2073
 
1932
2074
  // src/exec.ts
@@ -2071,8 +2213,8 @@ function issueBody(input) {
2071
2213
  lines.push(`- Siguiente: \`${input.next}\` \u2014 ${input.nextDescription}`);
2072
2214
  lines.push("");
2073
2215
  lines.push("### Artefactos");
2074
- lines.push(`- Spec: \`${path5.posix.join(".sdd", "changes", change.slug, "spec.md")}\``);
2075
- if (change.planPath) lines.push(`- Plan: \`${path5.posix.join(".sdd", "changes", change.slug, "plan.md")}\``);
2216
+ lines.push(`- Spec: \`${path6.posix.join(".sdd", "changes", change.slug, "spec.md")}\``);
2217
+ if (change.planPath) lines.push(`- Plan: \`${path6.posix.join(".sdd", "changes", change.slug, "plan.md")}\``);
2076
2218
  if (change.tasks) lines.push(`- Tareas: ${change.tasks.counts.done}/${change.tasks.counts.total}`);
2077
2219
  if (change.verify) lines.push(`- Evidencia registrada: ${change.verify.evidence.length}`);
2078
2220
  lines.push("");
@@ -2140,10 +2282,10 @@ async function commentIssue(root, number, body, runner = defaultRunner) {
2140
2282
  return result.ok ? [] : [diag("ATLAS-GH-006", "error", `No se pudo comentar el issue #${number}: ${(result.stderr || result.stdout).trim().slice(0, 300)}`)];
2141
2283
  }
2142
2284
  async function patchChangeMeta(root, slug, patch) {
2143
- const file = path5.join(path5.resolve(root), ".sdd", "changes", slug, "meta.yaml");
2285
+ const file = path6.join(path6.resolve(root), ".sdd", "changes", slug, "meta.yaml");
2144
2286
  const raw = await readTextIfExists(file);
2145
2287
  if (raw === void 0) {
2146
- return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${path5.relative(root, file)}`)] };
2288
+ return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${path6.relative(root, file)}`)] };
2147
2289
  }
2148
2290
  let data;
2149
2291
  try {
@@ -2197,7 +2339,7 @@ async function approveFromGithub(opts) {
2197
2339
  const { signApproval: signApproval2 } = await import("./approvals-2PO6223I.js");
2198
2340
  const signed = await signApproval2({
2199
2341
  root: opts.root,
2200
- artifact: path5.posix.join("changes", opts.slug, "spec.md"),
2342
+ artifact: path6.posix.join("changes", opts.slug, "spec.md"),
2201
2343
  by,
2202
2344
  channel: "tracker",
2203
2345
  note: `Aprobado v\xEDa GitHub issue #${number} (etiqueta "${label}")`
@@ -2257,7 +2399,7 @@ async function syncGithubIssue(root, slug, opts = {}) {
2257
2399
  }
2258
2400
 
2259
2401
  // src/evidence.ts
2260
- import path6 from "path";
2402
+ import path7 from "path";
2261
2403
  var SCENARIO_HEAD_RE2 = /^###\s+(REQ-[A-Z0-9-]+-S\d+)\b/;
2262
2404
  function locateBlocks(content) {
2263
2405
  const normalized = content.replace(/\r\n?/g, "\n");
@@ -2295,10 +2437,10 @@ function buildEvidenceYaml(evidence) {
2295
2437
  return lines.join("\n");
2296
2438
  }
2297
2439
  async function recordEvidence(opts) {
2298
- const root = path6.resolve(opts.root);
2440
+ const root = path7.resolve(opts.root);
2299
2441
  const slug = opts.slug;
2300
2442
  const fileKind = opts.file ?? "verify";
2301
- const file = path6.join(root, ".sdd", "changes", slug, fileKind === "fix" ? "fix.md" : "verify.md");
2443
+ const file = path7.join(root, ".sdd", "changes", slug, fileKind === "fix" ? "fix.md" : "verify.md");
2302
2444
  const diagnostics = [];
2303
2445
  const scenario = opts.scenario.toUpperCase();
2304
2446
  if (!SCENARIO_ID_RE.test(scenario)) {
@@ -2395,7 +2537,7 @@ function evidenceSummary(content) {
2395
2537
  }
2396
2538
 
2397
2539
  // src/runs.ts
2398
- import path7 from "path";
2540
+ import path8 from "path";
2399
2541
  import { randomBytes } from "crypto";
2400
2542
  var RUN_EVENT_TYPES = [
2401
2543
  "run_started",
@@ -2421,7 +2563,7 @@ function generateRunId(now = /* @__PURE__ */ new Date()) {
2421
2563
  return `${iso.slice(0, 8)}-${iso.slice(8, 14)}-${randomBytes(3).toString("hex")}`;
2422
2564
  }
2423
2565
  function runsDir(root) {
2424
- return path7.join(path7.resolve(root), ".sdd", "runs");
2566
+ return path8.join(path8.resolve(root), ".sdd", "runs");
2425
2567
  }
2426
2568
  async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Date()) {
2427
2569
  const runId = generateRunId(now);
@@ -2434,25 +2576,25 @@ async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Da
2434
2576
  updatedAt: localStamp(now)
2435
2577
  };
2436
2578
  if (inputs) state.inputs = inputs;
2437
- const dir = path7.join(runsDir(root), runId);
2579
+ const dir = path8.join(runsDir(root), runId);
2438
2580
  await ensureDir(dir);
2439
- await writeText(path7.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
2581
+ await writeText(path8.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
2440
2582
  `);
2441
- await writeText(path7.join(dir, "events.jsonl"), "");
2583
+ await writeText(path8.join(dir, "events.jsonl"), "");
2442
2584
  await appendRunEvent(root, runId, "run_started", { slug, phase });
2443
2585
  return state;
2444
2586
  }
2445
2587
  async function appendRunEvent(root, runId, type, data, now = /* @__PURE__ */ new Date()) {
2446
2588
  const event = { eventId: randomBytes(4).toString("hex"), at: localStamp(now), type };
2447
2589
  if (data) event.data = data;
2448
- const file = path7.join(runsDir(root), runId, "events.jsonl");
2590
+ const file = path8.join(runsDir(root), runId, "events.jsonl");
2449
2591
  const previous = await readTextIfExists(file) ?? "";
2450
2592
  await writeText(file, `${previous}${JSON.stringify(event)}
2451
2593
  `);
2452
2594
  return event;
2453
2595
  }
2454
2596
  async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Date()) {
2455
- const file = path7.join(runsDir(root), runId, "state.json");
2597
+ const file = path8.join(runsDir(root), runId, "state.json");
2456
2598
  const raw = await readTextIfExists(file);
2457
2599
  if (raw === void 0) return;
2458
2600
  const state = JSON.parse(raw);
@@ -2462,13 +2604,13 @@ async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Da
2462
2604
  `);
2463
2605
  }
2464
2606
  async function readRun(root, runId) {
2465
- const dir = path7.join(runsDir(root), runId);
2466
- const stateRaw = await readTextIfExists(path7.join(dir, "state.json"));
2607
+ const dir = path8.join(runsDir(root), runId);
2608
+ const stateRaw = await readTextIfExists(path8.join(dir, "state.json"));
2467
2609
  if (stateRaw === void 0) {
2468
2610
  return { diagnostics: [diag("ATLAS-RUN-001", "error", `No existe el run ${runId}`, { path: dir })] };
2469
2611
  }
2470
2612
  const state = JSON.parse(stateRaw);
2471
- const eventsRaw = await readTextIfExists(path7.join(dir, "events.jsonl")) ?? "";
2613
+ const eventsRaw = await readTextIfExists(path8.join(dir, "events.jsonl")) ?? "";
2472
2614
  const events = [];
2473
2615
  for (const line of eventsRaw.split("\n")) {
2474
2616
  if (line.trim() === "") continue;
@@ -2485,7 +2627,7 @@ async function listRuns(root, filter = {}) {
2485
2627
  const ids = await listDirs(dir);
2486
2628
  const out = [];
2487
2629
  for (const id of ids) {
2488
- const raw = await readTextIfExists(path7.join(dir, id, "state.json"));
2630
+ const raw = await readTextIfExists(path8.join(dir, id, "state.json"));
2489
2631
  if (raw === void 0) continue;
2490
2632
  try {
2491
2633
  const state = JSON.parse(raw);
@@ -2499,11 +2641,11 @@ async function listRuns(root, filter = {}) {
2499
2641
  }
2500
2642
 
2501
2643
  // src/analyze.ts
2502
- import path10 from "path";
2644
+ import path11 from "path";
2503
2645
 
2504
2646
  // src/mockups.ts
2505
- import path8 from "path";
2506
- import { parse as parseYaml6, stringify as stringifyYaml4 } from "yaml";
2647
+ import path9 from "path";
2648
+ import { parse as parseYaml6, stringify as stringifyYaml5 } from "yaml";
2507
2649
  import { z as z4 } from "zod";
2508
2650
  var mockupManifestSchema = z4.object({
2509
2651
  schema_version: z4.number().int().positive().default(1),
@@ -2528,20 +2670,20 @@ var mockupManifestSchema = z4.object({
2528
2670
  });
2529
2671
  var TOKEN_CANDIDATES = ["design/tokens.json", "DESIGN.md", "design/DESIGN.md", ".sdd/design/tokens.json"];
2530
2672
  function mockupsDir(root, slug) {
2531
- return path8.join(path8.resolve(root), ".sdd", "changes", slug, "mockups");
2673
+ return path9.join(path9.resolve(root), ".sdd", "changes", slug, "mockups");
2532
2674
  }
2533
2675
  async function tokensFile(root) {
2534
2676
  for (const candidate of TOKEN_CANDIDATES) {
2535
- const abs = path8.join(root, candidate);
2677
+ const abs = path9.join(root, candidate);
2536
2678
  if (await exists(abs)) return candidate;
2537
2679
  }
2538
2680
  return void 0;
2539
2681
  }
2540
2682
  async function computeInputsHash(root, change) {
2541
- const deltaPath = path8.join(change.dir, "spec.md");
2683
+ const deltaPath = path9.join(change.dir, "spec.md");
2542
2684
  const delta = await readTextIfExists(deltaPath) ?? "";
2543
2685
  const tokens = await tokensFile(root);
2544
- const tokensContent = tokens ? await readTextIfExists(path8.join(root, tokens)) ?? "" : "";
2686
+ const tokensContent = tokens ? await readTextIfExists(path9.join(root, tokens)) ?? "" : "";
2545
2687
  return artifactHash(`${delta}
2546
2688
  ---tokens---
2547
2689
  ${tokensContent}`);
@@ -2566,7 +2708,7 @@ function planMockups(change, platform = "web") {
2566
2708
  return { platform, screens };
2567
2709
  }
2568
2710
  async function readMockupManifest(root, slug) {
2569
- const file = path8.join(mockupsDir(root, slug), "manifest.yaml");
2711
+ const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
2570
2712
  const raw = await readTextIfExists(file);
2571
2713
  if (raw === void 0) return { diagnostics: [] };
2572
2714
  let data;
@@ -2658,7 +2800,7 @@ async function checkMockups(root, slug, change) {
2658
2800
  const { manifest, path: manifestPath, diagnostics } = await readMockupManifest(root, slug);
2659
2801
  const findings = [...diagnostics];
2660
2802
  if (!manifest || !manifestPath) {
2661
- return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path: path8.join(dir, "manifest.yaml") })], stale: false };
2803
+ return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path: path9.join(dir, "manifest.yaml") })], stale: false };
2662
2804
  }
2663
2805
  const known = /* @__PURE__ */ new Set();
2664
2806
  for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
@@ -2666,12 +2808,12 @@ async function checkMockups(root, slug, change) {
2666
2808
  }
2667
2809
  findings.push(...lintMockupManifest(manifest, known, manifestPath));
2668
2810
  for (const screen of manifest.screens) {
2669
- const html = await readTextIfExists(path8.join(dir, screen.file));
2811
+ const html = await readTextIfExists(path9.join(dir, screen.file));
2670
2812
  if (html === void 0) {
2671
- findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path: path8.join(dir, screen.file) }));
2813
+ findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path: path9.join(dir, screen.file) }));
2672
2814
  continue;
2673
2815
  }
2674
- findings.push(...lintMockupHtml(html, path8.join(dir, screen.file)));
2816
+ findings.push(...lintMockupHtml(html, path9.join(dir, screen.file)));
2675
2817
  }
2676
2818
  const inputsHash = await computeInputsHash(root, change);
2677
2819
  const stale = manifest.inputsHash !== void 0 && manifest.inputsHash !== inputsHash;
@@ -2686,7 +2828,7 @@ async function checkMockups(root, slug, change) {
2686
2828
  return { findings, stale, manifest, manifestPath };
2687
2829
  }
2688
2830
  async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date()) {
2689
- const file = path8.join(mockupsDir(root, slug), "plan.yaml");
2831
+ const file = path9.join(mockupsDir(root, slug), "plan.yaml");
2690
2832
  if (await exists(file)) return file;
2691
2833
  const doc = {
2692
2834
  schema_version: 1,
@@ -2694,11 +2836,11 @@ async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date(
2694
2836
  platform: plan.platform,
2695
2837
  screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints }))
2696
2838
  };
2697
- await writeText(file, stringifyYaml4(doc, { lineWidth: 120 }));
2839
+ await writeText(file, stringifyYaml5(doc, { lineWidth: 120 }));
2698
2840
  return file;
2699
2841
  }
2700
2842
  async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PURE__ */ new Date()) {
2701
- const file = path8.join(mockupsDir(root, slug), "manifest.yaml");
2843
+ const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
2702
2844
  if (await exists(file)) return file;
2703
2845
  const doc = {
2704
2846
  schema_version: 1,
@@ -2710,16 +2852,16 @@ async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PUR
2710
2852
  screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints })),
2711
2853
  screenshots: []
2712
2854
  };
2713
- await writeText(file, stringifyYaml4(doc, { lineWidth: 120 }));
2855
+ await writeText(file, stringifyYaml5(doc, { lineWidth: 120 }));
2714
2856
  return file;
2715
2857
  }
2716
2858
  async function updateMockupScreenshots(root, slug, screenshots) {
2717
- const file = path8.join(mockupsDir(root, slug), "manifest.yaml");
2859
+ const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
2718
2860
  const raw = await readTextIfExists(file);
2719
2861
  if (raw === void 0) return void 0;
2720
2862
  const data = parseYaml6(raw) ?? {};
2721
2863
  data["screenshots"] = screenshots;
2722
- await writeText(file, stringifyYaml4(data, { lineWidth: 120 }));
2864
+ await writeText(file, stringifyYaml5(data, { lineWidth: 120 }));
2723
2865
  return file;
2724
2866
  }
2725
2867
  async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Date()) {
@@ -2744,9 +2886,9 @@ async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Da
2744
2886
  const page2 = await browser.newPage();
2745
2887
  for (const bp of screen.breakpoints && screen.breakpoints.length > 0 ? screen.breakpoints : [1440]) {
2746
2888
  await page2.setViewportSize({ width: bp, height: 900 });
2747
- await page2.goto(`file://${path8.join(dir, screen.file).split(path8.sep).join("/")}`);
2748
- const rel = path8.posix.join("screens", `${screen.id}-${bp}.png`);
2749
- await page2.screenshot({ path: path8.join(dir, rel) });
2889
+ await page2.goto(`file://${path9.join(dir, screen.file).split(path9.sep).join("/")}`);
2890
+ const rel = path9.posix.join("screens", `${screen.id}-${bp}.png`);
2891
+ await page2.screenshot({ path: path9.join(dir, rel) });
2750
2892
  screenshots.push(rel);
2751
2893
  }
2752
2894
  await page2.close();
@@ -2762,7 +2904,7 @@ async function mockupsReady(root, slug, change) {
2762
2904
  return Boolean(check.manifest && check.manifest.screens.length > 0 && !check.stale);
2763
2905
  }
2764
2906
  async function setMockupRequirement(root, slug, value) {
2765
- const file = path8.join(root, ".sdd", "changes", slug, "meta.yaml");
2907
+ const file = path9.join(root, ".sdd", "changes", slug, "meta.yaml");
2766
2908
  const raw = await readTextIfExists(file) ?? `schema_version: 1
2767
2909
  slug: ${slug}
2768
2910
  lane: standard
@@ -2781,7 +2923,7 @@ lane: standard
2781
2923
  }
2782
2924
 
2783
2925
  // src/packs.ts
2784
- import path9 from "path";
2926
+ import path10 from "path";
2785
2927
  import { parse as parseYaml7 } from "yaml";
2786
2928
  import { z as z5 } from "zod";
2787
2929
  var checkSchema = z5.object({
@@ -2997,12 +3139,12 @@ ${texts.scenarios}`);
2997
3139
  }
2998
3140
  }
2999
3141
  async function loadProjectPacks(sddDir) {
3000
- const dir = path9.join(sddDir, "packs");
3142
+ const dir = path10.join(sddDir, "packs");
3001
3143
  const diagnostics = [];
3002
3144
  const packs = [];
3003
3145
  for (const entry of await listDir(dir)) {
3004
3146
  if (!/\.ya?ml$/i.test(entry)) continue;
3005
- const file = path9.join(dir, entry);
3147
+ const file = path10.join(dir, entry);
3006
3148
  const raw = await readTextIfExists(file);
3007
3149
  if (raw === void 0) continue;
3008
3150
  let data;
@@ -3091,7 +3233,7 @@ function packFindings(evaluations, change) {
3091
3233
  // src/analyze.ts
3092
3234
  var UI_DOMAINS = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
3093
3235
  async function runAnalyze(opts) {
3094
- const root = path10.resolve(opts.root);
3236
+ const root = path11.resolve(opts.root);
3095
3237
  const { workspace, config } = await loadWorkspace(root);
3096
3238
  const change = workspace.changes.find((c) => c.slug === opts.slug);
3097
3239
  if (!change) {
@@ -3109,7 +3251,7 @@ async function runAnalyze(opts) {
3109
3251
  for (const req of spec.spec.requirements) living.set(req.id, req);
3110
3252
  }
3111
3253
  if (change.delta) {
3112
- findings.push(...lintDelta(change.delta, living, path10.join(change.dir, "spec.md"), { language: config.spec.language }));
3254
+ findings.push(...lintDelta(change.delta, living, path11.join(change.dir, "spec.md"), { language: config.spec.language }));
3113
3255
  }
3114
3256
  const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false });
3115
3257
  findings.push(...trace.findings);
@@ -3158,7 +3300,7 @@ async function runAnalyze(opts) {
3158
3300
  if (mockups) result.mockups = mockups;
3159
3301
  if (packs) result.packs = packs;
3160
3302
  if (opts.write !== false) {
3161
- const file = path10.join(change.dir, "analyze.md");
3303
+ const file = path11.join(change.dir, "analyze.md");
3162
3304
  await writeText(file, renderAnalyze(change, result, localStamp(opts.now)));
3163
3305
  result.path = file;
3164
3306
  }
@@ -3201,9 +3343,9 @@ function renderAnalyze(change, result, generatedAt) {
3201
3343
  }
3202
3344
 
3203
3345
  // src/metrics.ts
3204
- import path11 from "path";
3346
+ import path12 from "path";
3205
3347
  async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3206
- const resolved = path11.resolve(root);
3348
+ const resolved = path12.resolve(root);
3207
3349
  const { workspace, config } = await loadWorkspace(resolved);
3208
3350
  const approvals = await loadApprovals(workspace.sddDir);
3209
3351
  const living = /* @__PURE__ */ new Map();
@@ -3218,7 +3360,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3218
3360
  const deltaContent = await readDelta(change);
3219
3361
  const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
3220
3362
  const findings = [];
3221
- if (change.delta) findings.push(...lintDelta(change.delta, living, path11.join(change.dir, "spec.md"), { language: config.spec.language }));
3363
+ if (change.delta) findings.push(...lintDelta(change.delta, living, path12.join(change.dir, "spec.md"), { language: config.spec.language }));
3222
3364
  const trace = checkTrace({
3223
3365
  specs: workspace.specs,
3224
3366
  change,
@@ -3283,7 +3425,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3283
3425
  attention.push({ slug: change.slug, kind: "missing-evidence", detail: `evidencia ${change.scenariosPassed}/${change.scenariosTotal}` });
3284
3426
  }
3285
3427
  }
3286
- const archiveDir = path11.join(workspace.sddDir, "changes", "archive");
3428
+ const archiveDir = path12.join(workspace.sddDir, "changes", "archive");
3287
3429
  const archivedEntries = await listDirs(archiveDir);
3288
3430
  const throughputByMonth = {};
3289
3431
  for (const entry of archivedEntries) {
@@ -3321,15 +3463,15 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
3321
3463
  }
3322
3464
  async function readDelta(change) {
3323
3465
  const { readTextIfExists: readTextIfExists2 } = await import("./fsx-VF2P7ALA.js");
3324
- return readTextIfExists2(path11.join(change.dir, "spec.md"));
3466
+ return readTextIfExists2(path12.join(change.dir, "spec.md"));
3325
3467
  }
3326
3468
 
3327
3469
  // src/present.ts
3328
- import path12 from "path";
3470
+ import path13 from "path";
3329
3471
  import { escapeHtml, inlineMarkdown, renderMarkdown as renderRich } from "@specatlas/render";
3330
3472
  import { defaultTokens, renderDocument, renderStyles } from "@specatlas/render";
3331
3473
  async function generatePresentation(opts) {
3332
- const root = path12.resolve(opts.root);
3474
+ const root = path13.resolve(opts.root);
3333
3475
  const { workspace, config } = await loadWorkspace(root);
3334
3476
  const language = config.project.language;
3335
3477
  const change = workspace.changes.find((c) => c.slug === opts.slug);
@@ -3339,17 +3481,17 @@ async function generatePresentation(opts) {
3339
3481
  if (!change.delta) {
3340
3482
  return { slug: opts.slug, diagnostics: [diag("ATLAS-PRESENT-001", "error", `El cambio "${opts.slug}" no tiene spec.md (delta)`)] };
3341
3483
  }
3342
- const deltaPath = path12.join(change.dir, "spec.md");
3484
+ const deltaPath = path13.join(change.dir, "spec.md");
3343
3485
  const deltaContent = await readTextIfExists(deltaPath) ?? "";
3344
3486
  const hash = artifactHash(deltaContent);
3345
3487
  const now = opts.now ?? /* @__PURE__ */ new Date();
3346
- const presentationDir = path12.join(change.dir, "presentation");
3347
- const mockupsCopyDir = path12.join(presentationDir, "mockups");
3488
+ const presentationDir = path13.join(change.dir, "presentation");
3489
+ const mockupsCopyDir = path13.join(presentationDir, "mockups");
3348
3490
  await ensureDir(presentationDir);
3349
3491
  const mockupInfo = await copyMockups(root, change, mockupsCopyDir);
3350
- const proposalRaw = await readTextIfExists(path12.join(change.dir, "proposal.md"));
3492
+ const proposalRaw = await readTextIfExists(path13.join(change.dir, "proposal.md"));
3351
3493
  const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
3352
- const approvals = await loadApprovals(path12.join(root, ".sdd"));
3494
+ const approvals = await loadApprovals(path13.join(root, ".sdd"));
3353
3495
  const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
3354
3496
  const labels = labelsFor(language);
3355
3497
  const html = page({
@@ -3370,7 +3512,7 @@ async function generatePresentation(opts) {
3370
3512
  approveHint: `satlas approve ${change.slug} --by "<nombre>" --channel presentation`,
3371
3513
  ...approval.status === "valid" && approval.approvedBy ? { approval: { by: approval.approvedBy, at: approval.approvedAt ?? "" } } : {}
3372
3514
  });
3373
- const outFile = path12.join(presentationDir, "index.html");
3515
+ const outFile = path13.join(presentationDir, "index.html");
3374
3516
  await writeText(outFile, html);
3375
3517
  return { slug: change.slug, path: outFile, hash, diagnostics: mockupInfo.diagnostics };
3376
3518
  }
@@ -3382,12 +3524,12 @@ async function copyMockups(root, change, destDir) {
3382
3524
  await ensureDir(destDir);
3383
3525
  const screens = [];
3384
3526
  for (const screen of manifest.screens) {
3385
- const src = path12.join(dir, screen.file);
3527
+ const src = path13.join(dir, screen.file);
3386
3528
  if (!await exists(src)) {
3387
3529
  diagnostics.push(diag("ATLAS-PRESENT-002", "warning", `El mockup ${screen.file} no existe y no se incluir\xE1`, { path: src }));
3388
3530
  continue;
3389
3531
  }
3390
- await copyFile(src, path12.join(destDir, screen.file));
3532
+ await copyFile(src, path13.join(destDir, screen.file));
3391
3533
  screens.push({
3392
3534
  id: screen.id,
3393
3535
  title: screen.title ?? screen.id,
@@ -3397,13 +3539,13 @@ async function copyMockups(root, change, destDir) {
3397
3539
  });
3398
3540
  }
3399
3541
  const screenshots = [];
3400
- const shotsDir = path12.join(dir, "screens");
3542
+ const shotsDir = path13.join(dir, "screens");
3401
3543
  if (await exists(shotsDir)) {
3402
- await ensureDir(path12.join(destDir, "screens"));
3544
+ await ensureDir(path13.join(destDir, "screens"));
3403
3545
  const { listDir: listDir4 } = await import("./fsx-VF2P7ALA.js");
3404
3546
  for (const file of await listDir4(shotsDir)) {
3405
3547
  if (!/\.(png|jpe?g|webp)$/i.test(file)) continue;
3406
- await copyFile(path12.join(shotsDir, file), path12.join(destDir, "screens", file));
3548
+ await copyFile(path13.join(shotsDir, file), path13.join(destDir, "screens", file));
3407
3549
  screenshots.push(`mockups/screens/${file}`);
3408
3550
  }
3409
3551
  }
@@ -3578,7 +3720,7 @@ function renderMarkdown(markdown) {
3578
3720
  }
3579
3721
 
3580
3722
  // src/adopt.ts
3581
- import path13 from "path";
3723
+ import path14 from "path";
3582
3724
  var COMMON_ROOTS = ["src", "app", "lib", "modules", "services", "packages", "api", "features", "domain", "internal", "components"];
3583
3725
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", ".git", ".sdd", ".opencode", ".claude", "test", "tests", "__tests__", "e2e", "docs", "scripts", "assets", "public", "migrations", "vendor", "bin", "obj"]);
3584
3726
  var SOURCE_EXT = /* @__PURE__ */ new Set([
@@ -3622,11 +3764,11 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([
3622
3764
  ".groovy"
3623
3765
  ]);
3624
3766
  async function adoptWorkspace(opts) {
3625
- const root = path13.resolve(opts.root);
3767
+ const root = path14.resolve(opts.root);
3626
3768
  const dryRun = opts.dryRun ?? false;
3627
3769
  const diagnostics = [];
3628
- const sddDir = path13.join(root, ".sdd");
3629
- if (!await exists(path13.join(sddDir, "config.yaml"))) {
3770
+ const sddDir = path14.join(root, ".sdd");
3771
+ if (!await exists(path14.join(sddDir, "config.yaml"))) {
3630
3772
  return {
3631
3773
  root,
3632
3774
  domains: [],
@@ -3637,8 +3779,8 @@ async function adoptWorkspace(opts) {
3637
3779
  }
3638
3780
  const { config } = await loadConfig(sddDir);
3639
3781
  const files = await walkFiles(root, { skipDirs: [...SKIP_DIRS] });
3640
- const inventoried = files.map((f) => toPosix(path13.relative(root, f.path)));
3641
- const relative = inventoried.filter((f) => SOURCE_EXT.has(path13.extname(f).toLowerCase()));
3782
+ const inventoried = files.map((f) => toPosix(path14.relative(root, f.path)));
3783
+ const relative = inventoried.filter((f) => SOURCE_EXT.has(path14.extname(f).toLowerCase()));
3642
3784
  if (relative.length === 0) {
3643
3785
  diagnostics.push(diag("ATLAS-ADOPT-002", "warning", "No se encontraron archivos de c\xF3digo para inventariar", { path: root, suggestion: "A\xF1ade el dominio a mano con --domains <nombre>" }));
3644
3786
  }
@@ -3666,13 +3808,13 @@ async function adoptWorkspace(opts) {
3666
3808
  const createdSpecs = [];
3667
3809
  for (const name of domainNames.sort()) {
3668
3810
  const domainFiles = (counts.get(name) ?? []).sort();
3669
- const existingSpec = await exists(path13.join(sddDir, "specs", name, "spec.md"));
3811
+ const existingSpec = await exists(path14.join(sddDir, "specs", name, "spec.md"));
3670
3812
  domains.push({ name, files: domainFiles.length, samples: domainFiles.slice(0, 8), existingSpec });
3671
3813
  if (existingSpec) {
3672
- diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path: path13.join(sddDir, "specs", name, "spec.md") }));
3814
+ diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path: path14.join(sddDir, "specs", name, "spec.md") }));
3673
3815
  continue;
3674
3816
  }
3675
- const specPath = path13.join(sddDir, "specs", name, "spec.md");
3817
+ const specPath = path14.join(sddDir, "specs", name, "spec.md");
3676
3818
  if (!dryRun) {
3677
3819
  await writeText(specPath, baselineSpec(name, domainFiles));
3678
3820
  createdSpecs.push(specPath);
@@ -3680,7 +3822,7 @@ async function adoptWorkspace(opts) {
3680
3822
  createdSpecs.push(specPath);
3681
3823
  }
3682
3824
  }
3683
- const reportPath = path13.join(sddDir, "adopt-report.md");
3825
+ const reportPath = path14.join(sddDir, "adopt-report.md");
3684
3826
  if (!dryRun) {
3685
3827
  await writeText(reportPath, adoptReport({ root, config, domains, detection: detection?.best, language: config.project.language, files: relative.length, inventoried: inventoried.length, now: opts.now ?? /* @__PURE__ */ new Date() }));
3686
3828
  }
@@ -3700,12 +3842,12 @@ async function loadProjectProfiles(root, dirs) {
3700
3842
  if (!await exists(dir)) continue;
3701
3843
  out.push(...await loadProfilesFromDir(dir));
3702
3844
  }
3703
- const custom = path13.join(root, ".sdd", "profiles", "custom");
3845
+ const custom = path14.join(root, ".sdd", "profiles", "custom");
3704
3846
  if (await exists(custom)) out.push(...await loadProfilesFromDir(custom));
3705
3847
  return out;
3706
3848
  }
3707
3849
  async function fallbackDomainName(root, projectName) {
3708
- const pkg = await readTextIfExists(path13.join(root, "package.json"));
3850
+ const pkg = await readTextIfExists(path14.join(root, "package.json"));
3709
3851
  if (pkg) {
3710
3852
  try {
3711
3853
  const data = JSON.parse(pkg);
@@ -3793,13 +3935,13 @@ function adoptReport(input) {
3793
3935
  }
3794
3936
 
3795
3937
  // src/init.ts
3796
- import path14 from "path";
3938
+ import path15 from "path";
3797
3939
  async function initWorkspace(opts) {
3798
- const root = path14.resolve(opts.root);
3799
- const sddDir = path14.join(root, ".sdd");
3940
+ const root = path15.resolve(opts.root);
3941
+ const sddDir = path15.join(root, ".sdd");
3800
3942
  const diagnostics = [];
3801
3943
  const created = [];
3802
- if (await exists(path14.join(sddDir, "config.yaml"))) {
3944
+ if (await exists(path15.join(sddDir, "config.yaml"))) {
3803
3945
  return {
3804
3946
  sddDir,
3805
3947
  created,
@@ -3809,29 +3951,29 @@ async function initWorkspace(opts) {
3809
3951
  created.push(...await ensureSddDirs(sddDir));
3810
3952
  const language = opts.language ?? "es";
3811
3953
  const cfg = defaultConfig({ name: opts.name, language });
3812
- cfg.project.name = opts.name ?? path14.basename(root);
3954
+ cfg.project.name = opts.name ?? path15.basename(root);
3813
3955
  await writeConfig(sddDir, cfg);
3814
- created.push(path14.join(sddDir, "config.yaml"));
3956
+ created.push(path15.join(sddDir, "config.yaml"));
3815
3957
  const templates = templatesFor(language);
3816
- await writeText(path14.join(sddDir, "constitution.md"), templates.constitution);
3817
- created.push(path14.join(sddDir, "constitution.md"));
3818
- await writeText(path14.join(sddDir, "glossary.md"), templates.glossary);
3819
- created.push(path14.join(sddDir, "glossary.md"));
3958
+ await writeText(path15.join(sddDir, "constitution.md"), templates.constitution);
3959
+ created.push(path15.join(sddDir, "constitution.md"));
3960
+ await writeText(path15.join(sddDir, "glossary.md"), templates.glossary);
3961
+ created.push(path15.join(sddDir, "glossary.md"));
3820
3962
  const detected = await detectIfPossible(root, opts.profilesDir);
3821
3963
  if (detected) {
3822
3964
  const yaml = detectionToYaml(detected, localStamp(opts.now));
3823
- await writeText(path14.join(sddDir, "profiles", "detected.yaml"), yaml);
3824
- created.push(path14.join(sddDir, "profiles", "detected.yaml"));
3965
+ await writeText(path15.join(sddDir, "profiles", "detected.yaml"), yaml);
3966
+ created.push(path15.join(sddDir, "profiles", "detected.yaml"));
3825
3967
  if (detected.best) {
3826
3968
  diagnostics.push(diag("ATLAS-INIT-002", "info", `Stack detectado: ${detected.best.displayName} (${detected.best.score} puntos)`, { suggestion: "Revisa .sdd/profiles/detected.yaml" }));
3827
3969
  } else {
3828
3970
  diagnostics.push(diag("ATLAS-INIT-003", "info", "No se detect\xF3 un stack conocido: se usar\xE1 el perfil gen\xE9rico", { suggestion: "Crea un perfil propio con `satlas profile` (F2) o edita .sdd/profiles/custom/" }));
3829
3971
  }
3830
3972
  }
3831
- await writeText(path14.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
3832
- created.push(path14.join(sddDir, "INDEX.md"));
3973
+ await writeText(path15.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
3974
+ created.push(path15.join(sddDir, "INDEX.md"));
3833
3975
  if (opts.local) {
3834
- const gitignore = path14.join(root, ".gitignore");
3976
+ const gitignore = path15.join(root, ".gitignore");
3835
3977
  const current = await readTextIfExists(gitignore) ?? "";
3836
3978
  if (!/(^|\n)\.sdd\/?(\n|$)/.test(current)) {
3837
3979
  const next = current.endsWith("\n") || current === "" ? `${current}.sdd/
@@ -3866,13 +4008,13 @@ ${es ? "## Archivados\n\n0 cambio(s) en `changes/archive/`." : "## Archived\n\n0
3866
4008
  }
3867
4009
 
3868
4010
  // src/new.ts
3869
- import path15 from "path";
4011
+ import path16 from "path";
3870
4012
  var SLUG_RE = /^[a-z0-9][a-z0-9-]{1,49}$/;
3871
4013
  async function createChange(opts) {
3872
4014
  const diagnostics = [];
3873
4015
  const slug = opts.slug.trim().toLowerCase();
3874
4016
  const language = opts.language ?? opts.cfg?.project.language ?? "es";
3875
- const dir = path15.join(path15.resolve(opts.root), ".sdd", "changes", slug);
4017
+ const dir = path16.join(path16.resolve(opts.root), ".sdd", "changes", slug);
3876
4018
  if (!SLUG_RE.test(slug)) {
3877
4019
  return {
3878
4020
  slug,
@@ -3903,20 +4045,20 @@ async function createChange(opts) {
3903
4045
  await ensureDir(dir);
3904
4046
  const templates = templatesFor(language);
3905
4047
  const files = [];
3906
- const metaFile = path15.join(dir, "meta.yaml");
4048
+ const metaFile = path16.join(dir, "meta.yaml");
3907
4049
  await writeText(metaFile, changeMetaYaml(meta));
3908
4050
  files.push(metaFile);
3909
4051
  if (lane === "fix") {
3910
- const fixFile = path15.join(dir, "fix.md");
4052
+ const fixFile = path16.join(dir, "fix.md");
3911
4053
  await writeText(fixFile, renderTemplate(templates.fix, { TITLE: title }));
3912
4054
  files.push(fixFile);
3913
4055
  return { slug, dir, files, diagnostics };
3914
4056
  }
3915
- const proposalFile = path15.join(dir, "proposal.md");
4057
+ const proposalFile = path16.join(dir, "proposal.md");
3916
4058
  await writeText(proposalFile, renderTemplate(templates.proposal, { TITLE: title }));
3917
4059
  files.push(proposalFile);
3918
4060
  const domainUpper = domain.replace(/[^a-z0-9]/gi, "").toUpperCase() || "GEN";
3919
- const deltaFile = path15.join(dir, "spec.md");
4061
+ const deltaFile = path16.join(dir, "spec.md");
3920
4062
  await writeText(deltaFile, templates.specDelta({ title, domainUpper, domain }));
3921
4063
  files.push(deltaFile);
3922
4064
  return { slug, dir, files, diagnostics };
@@ -3925,7 +4067,7 @@ async function createChange(opts) {
3925
4067
  // src/archive.ts
3926
4068
  import { promises as fs } from "fs";
3927
4069
  import { cp, rename, rm } from "fs/promises";
3928
- import path16 from "path";
4070
+ import path17 from "path";
3929
4071
  var REQ_HEADER_RE = /^###\s+(?:Requisito|Requirement):\s+(REQ-[A-Z0-9-]+)\s*(?:—|-|–)\s*(.*)$/;
3930
4072
  function foldDelta(livingBody, delta, language = "es") {
3931
4073
  const diagnostics = [];
@@ -4034,7 +4176,7 @@ async function restoreFile(file, previous) {
4034
4176
  }
4035
4177
  }
4036
4178
  async function archiveChange(opts) {
4037
- const root = path16.resolve(opts.root);
4179
+ const root = path17.resolve(opts.root);
4038
4180
  const diagnostics = [];
4039
4181
  const { config } = await loadWorkspace(root);
4040
4182
  const language = opts.language ?? config.project.language;
@@ -4050,16 +4192,43 @@ async function archiveChange(opts) {
4050
4192
  return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
4051
4193
  }
4052
4194
  const nowFix = opts.now ?? /* @__PURE__ */ new Date();
4053
- const targetFix = path16.join(root, ".sdd", "changes", "archive", `${localMonth(nowFix)}-${opts.slug}`);
4195
+ const targetFix = path17.join(root, ".sdd", "changes", "archive", `${localMonth(nowFix)}-${opts.slug}`);
4054
4196
  if (await exists(targetFix)) {
4055
4197
  diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${targetFix}`, { path: targetFix }));
4056
4198
  return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
4057
4199
  }
4200
+ let livingFix;
4201
+ let livingCreated = false;
4202
+ if (!opts.dryRun) {
4203
+ const fixRaw = await readTextIfExists(path17.join(change.dir, "fix.md")) ?? "";
4204
+ try {
4205
+ const written = await writeLivingFix(root, {
4206
+ slug: opts.slug,
4207
+ date: localDate(nowFix),
4208
+ result: "pass",
4209
+ ...change.meta.domain !== void 0 ? { domain: change.meta.domain } : {},
4210
+ ...change.meta.title !== void 0 ? { title: change.meta.title } : {},
4211
+ covers: change.fixCovers ?? [],
4212
+ content: fixRaw
4213
+ });
4214
+ livingFix = written.relativePath;
4215
+ livingCreated = written.created;
4216
+ } catch (error) {
4217
+ diagnostics.push(
4218
+ diag("ATLAS-ARCH-006", "error", `No se pudo conservar el fix vivo: ${error.message}`, {
4219
+ path: path17.join(root, ".sdd", "fixes"),
4220
+ suggestion: "Revisa los permisos de .sdd/fixes y vuelve a intentar; el fix sigue sin archivar"
4221
+ })
4222
+ );
4223
+ return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: false };
4224
+ }
4225
+ }
4058
4226
  if (!opts.dryRun) {
4059
- await ensureDir(path16.dirname(targetFix));
4227
+ await ensureDir(path17.dirname(targetFix));
4060
4228
  try {
4061
4229
  await moveDirectory(change.dir, targetFix);
4062
4230
  } catch (error) {
4231
+ if (livingCreated && livingFix) await removeFile(path17.join(root, livingFix));
4063
4232
  diagnostics.push(
4064
4233
  diag("ATLAS-ARCH-004", "error", `No se pudo mover el cambio al hist\xF3rico: ${error.message}`, {
4065
4234
  path: change.dir,
@@ -4068,9 +4237,16 @@ async function archiveChange(opts) {
4068
4237
  );
4069
4238
  return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: false };
4070
4239
  }
4071
- await regenerateIndex(root, config);
4240
+ await regenerateIndex(root, config, nowFix);
4072
4241
  }
4073
- return { slug: opts.slug, archivedTo: targetFix, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
4242
+ return {
4243
+ slug: opts.slug,
4244
+ archivedTo: targetFix,
4245
+ ...livingFix !== void 0 ? { livingFix } : {},
4246
+ fold: emptyFold,
4247
+ diagnostics,
4248
+ dryRun: opts.dryRun ?? false
4249
+ };
4074
4250
  }
4075
4251
  if (!change.delta) {
4076
4252
  diagnostics.push(diag("ATLAS-ARCH-003", "error", `El cambio "${opts.slug}" no tiene spec.md (delta): no hay nada que plegar`, { path: change.dir }));
@@ -4078,10 +4254,10 @@ async function archiveChange(opts) {
4078
4254
  }
4079
4255
  const domain = change.meta.domain;
4080
4256
  if (!domain) {
4081
- diagnostics.push(diag("ATLAS-ARCH-001", "error", `El cambio "${opts.slug}" no declara domain en meta.yaml`, { path: path16.join(change.dir, "meta.yaml") }));
4257
+ diagnostics.push(diag("ATLAS-ARCH-001", "error", `El cambio "${opts.slug}" no declara domain en meta.yaml`, { path: path17.join(change.dir, "meta.yaml") }));
4082
4258
  return { slug: opts.slug, fold: { content: "", applied: { added: [], modified: [], removed: [], renamed: [] }, diagnostics }, diagnostics, dryRun: opts.dryRun ?? false };
4083
4259
  }
4084
- const specFile = path16.join(root, ".sdd", "specs", domain, "spec.md");
4260
+ const specFile = path17.join(root, ".sdd", "specs", domain, "spec.md");
4085
4261
  const existing = await readTextIfExists(specFile);
4086
4262
  const base = existing ?? `---
4087
4263
  domain: ${domain}
@@ -4111,8 +4287,8 @@ ${Object.entries(nextFm).map(([k, v]) => `${k}: ${String(v)}`).join("\n")}
4111
4287
 
4112
4288
  ${body}`;
4113
4289
  const month = localMonth(now);
4114
- const archiveDir = path16.join(root, ".sdd", "changes", "archive");
4115
- const target = path16.join(archiveDir, `${month}-${opts.slug}`);
4290
+ const archiveDir = path17.join(root, ".sdd", "changes", "archive");
4291
+ const target = path17.join(archiveDir, `${month}-${opts.slug}`);
4116
4292
  if (await exists(target)) {
4117
4293
  diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${target}`, { path: target }));
4118
4294
  return { slug: opts.slug, domain, fold, diagnostics, dryRun: opts.dryRun ?? false };
@@ -4138,23 +4314,31 @@ ${body}`;
4138
4314
  }
4139
4315
  async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
4140
4316
  const { workspace, config } = cfg ? { workspace: (await loadWorkspace(root)).workspace, config: cfg } : await loadWorkspace(root);
4141
- const archiveDir = path16.join(root, ".sdd", "changes", "archive");
4317
+ const archiveDir = path17.join(root, ".sdd", "changes", "archive");
4142
4318
  const archived = await exists(archiveDir) ? (await fs.readdir(archiveDir)).filter((e) => !e.startsWith(".")).length : 0;
4319
+ const fixes = await loadLivingFixes(root);
4143
4320
  const markdown = indexMarkdown({
4144
4321
  projectName: config.project.name,
4145
4322
  language: config.project.language,
4146
4323
  specs: workspace.specs.map((s) => ({ domain: s.domain, requirements: s.spec.requirements.length })),
4147
4324
  changes: workspace.changes.map((c) => ({ slug: c.slug, lane: c.meta?.lane ?? config.lanes.default })),
4325
+ fixes: fixes.map((fix) => ({ slug: fix.slug, date: fix.date, result: fix.result, ...fix.domain !== void 0 ? { domain: fix.domain } : {} })),
4148
4326
  archived
4149
4327
  });
4150
4328
  void now;
4151
- await writeText(path16.join(root, ".sdd", "INDEX.md"), markdown);
4329
+ await writeText(path17.join(root, ".sdd", "INDEX.md"), markdown);
4330
+ }
4331
+ async function removeFile(file) {
4332
+ try {
4333
+ await rm(file, { force: true });
4334
+ } catch {
4335
+ }
4152
4336
  }
4153
4337
 
4154
4338
  // src/migrations.ts
4155
4339
  import { promises as fs2 } from "fs";
4156
- import path17 from "path";
4157
- import { parse as parseYaml8, stringify as stringifyYaml5 } from "yaml";
4340
+ import path18 from "path";
4341
+ import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
4158
4342
  var SCHEMA_VERSION = 1;
4159
4343
  var BACKUP_DIRNAME = ".backup";
4160
4344
  var BACKUP_POINTER = ".latest";
@@ -4166,28 +4350,28 @@ var SCHEMA_MIGRATIONS = [
4166
4350
  to: SCHEMA_VERSION
4167
4351
  }
4168
4352
  ];
4169
- var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml", path17.join("profiles", "detected.yaml")];
4170
- var CHANGE_ARTIFACTS = ["meta.yaml", path17.join("mockups", "manifest.yaml")];
4353
+ var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml", path18.join("profiles", "detected.yaml")];
4354
+ var CHANGE_ARTIFACTS = ["meta.yaml", path18.join("mockups", "manifest.yaml")];
4171
4355
  async function pushChangeArtifacts(out, sddDir, relDir) {
4172
4356
  for (const rel of CHANGE_ARTIFACTS) {
4173
- const abs = path17.join(sddDir, relDir, rel);
4174
- if (await exists(abs)) out.push({ artifact: toPosix(path17.join(relDir, rel)), path: abs });
4357
+ const abs = path18.join(sddDir, relDir, rel);
4358
+ if (await exists(abs)) out.push({ artifact: toPosix(path18.join(relDir, rel)), path: abs });
4175
4359
  }
4176
4360
  }
4177
4361
  async function collectVersionedArtifacts(root) {
4178
- const sddDir = path17.join(root, ".sdd");
4362
+ const sddDir = path18.join(root, ".sdd");
4179
4363
  const out = [];
4180
4364
  for (const rel of FIXED_ARTIFACTS) {
4181
- const abs = path17.join(sddDir, rel);
4365
+ const abs = path18.join(sddDir, rel);
4182
4366
  if (await exists(abs)) out.push({ artifact: toPosix(rel), path: abs });
4183
4367
  }
4184
- const changesDir = path17.join(sddDir, "changes");
4368
+ const changesDir = path18.join(sddDir, "changes");
4185
4369
  for (const slug of await listDirs(changesDir)) {
4186
4370
  if (slug === "archive") continue;
4187
- await pushChangeArtifacts(out, sddDir, path17.join("changes", slug));
4371
+ await pushChangeArtifacts(out, sddDir, path18.join("changes", slug));
4188
4372
  }
4189
- for (const entry of await listDirs(path17.join(changesDir, "archive"))) {
4190
- await pushChangeArtifacts(out, sddDir, path17.join("changes", "archive", entry));
4373
+ for (const entry of await listDirs(path18.join(changesDir, "archive"))) {
4374
+ await pushChangeArtifacts(out, sddDir, path18.join("changes", "archive", entry));
4191
4375
  }
4192
4376
  return out;
4193
4377
  }
@@ -4272,7 +4456,7 @@ async function removeDir(dir) {
4272
4456
  } catch {
4273
4457
  }
4274
4458
  }
4275
- async function removeFile(file) {
4459
+ async function removeFile2(file) {
4276
4460
  try {
4277
4461
  await fs2.rm(file, { force: true });
4278
4462
  } catch {
@@ -4284,22 +4468,22 @@ function backupName(now) {
4284
4468
  async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
4285
4469
  const plan = await planUpgrade(root);
4286
4470
  if (plan.pending.length === 0) return { status: "up-to-date", plan, applied: [] };
4287
- const sddDir = path17.join(root, ".sdd");
4288
- const backupRoot = path17.join(sddDir, BACKUP_DIRNAME);
4471
+ const sddDir = path18.join(root, ".sdd");
4472
+ const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
4289
4473
  const name = backupName(now);
4290
- const backupDir = path17.join(backupRoot, name);
4291
- const pointerFile = path17.join(backupRoot, BACKUP_POINTER);
4474
+ const backupDir = path18.join(backupRoot, name);
4475
+ const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
4292
4476
  const previous = [];
4293
4477
  try {
4294
4478
  for (const item of plan.pending) {
4295
4479
  const content = await readTextIfExists(item.path);
4296
4480
  if (content === void 0) throw new Error("el elemento desapareci\xF3 mientras se respaldaba");
4297
4481
  previous.push({ item, contents: content });
4298
- await writeText(path17.join(backupDir, "files", ...item.artifact.split("/")), content);
4482
+ await writeText(path18.join(backupDir, "files", ...item.artifact.split("/")), content);
4299
4483
  }
4300
4484
  await writeText(
4301
- path17.join(backupDir, "backup.yaml"),
4302
- stringifyYaml5(
4485
+ path18.join(backupDir, "backup.yaml"),
4486
+ stringifyYaml6(
4303
4487
  {
4304
4488
  schema_version: SCHEMA_VERSION,
4305
4489
  created_at: localStamp(now),
@@ -4313,7 +4497,7 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
4313
4497
  `);
4314
4498
  } catch (err) {
4315
4499
  await removeDir(backupDir);
4316
- await removeFile(pointerFile);
4500
+ await removeFile2(pointerFile);
4317
4501
  return { status: "failed", plan, applied: [], failure: { artifact: "(respaldo)", message: err.message } };
4318
4502
  }
4319
4503
  const written = [];
@@ -4333,13 +4517,13 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
4333
4517
  }
4334
4518
  }
4335
4519
  }
4336
- await removeFile(pointerFile);
4520
+ await removeFile2(pointerFile);
4337
4521
  await removeDir(backupDir);
4338
4522
  return { status: "failed", plan, applied: [], failure: { artifact: item.artifact, message: err.message } };
4339
4523
  }
4340
4524
  }
4341
4525
  for (const dir of await listDirs(backupRoot)) {
4342
- if (dir !== name) await removeDir(path17.join(backupRoot, dir));
4526
+ if (dir !== name) await removeDir(path18.join(backupRoot, dir));
4343
4527
  }
4344
4528
  return {
4345
4529
  status: "applied",
@@ -4348,20 +4532,20 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
4348
4532
  backup: {
4349
4533
  name,
4350
4534
  dir: backupDir,
4351
- relativeDir: toPosix(path17.relative(root, backupDir)),
4535
+ relativeDir: toPosix(path18.relative(root, backupDir)),
4352
4536
  createdAt: localStamp(now),
4353
4537
  files: plan.pending.map((i) => i.artifact)
4354
4538
  }
4355
4539
  };
4356
4540
  }
4357
4541
  async function rollbackUpgrade(root) {
4358
- const sddDir = path17.join(root, ".sdd");
4359
- const backupRoot = path17.join(sddDir, BACKUP_DIRNAME);
4360
- const pointerFile = path17.join(backupRoot, BACKUP_POINTER);
4542
+ const sddDir = path18.join(root, ".sdd");
4543
+ const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
4544
+ const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
4361
4545
  const name = (await readTextIfExists(pointerFile))?.trim();
4362
4546
  if (!name) return { status: "no-backup", restored: [] };
4363
- const backupDir = path17.join(backupRoot, name);
4364
- const manifestRaw = await readTextIfExists(path17.join(backupDir, "backup.yaml"));
4547
+ const backupDir = path18.join(backupRoot, name);
4548
+ const manifestRaw = await readTextIfExists(path18.join(backupDir, "backup.yaml"));
4365
4549
  if (manifestRaw === void 0) return { status: "no-backup", restored: [] };
4366
4550
  let files = [];
4367
4551
  try {
@@ -4372,8 +4556,8 @@ async function rollbackUpgrade(root) {
4372
4556
  }
4373
4557
  const restored = [];
4374
4558
  for (const file of files) {
4375
- const from = path17.join(backupDir, "files", ...file.artifact.split("/"));
4376
- const to = path17.join(sddDir, ...file.artifact.split("/"));
4559
+ const from = path18.join(backupDir, "files", ...file.artifact.split("/"));
4560
+ const to = path18.join(sddDir, ...file.artifact.split("/"));
4377
4561
  const content = await readTextIfExists(from);
4378
4562
  if (content === void 0) continue;
4379
4563
  try {
@@ -4384,7 +4568,7 @@ async function rollbackUpgrade(root) {
4384
4568
  }
4385
4569
  }
4386
4570
  await removeDir(backupDir);
4387
- await removeFile(pointerFile);
4571
+ await removeFile2(pointerFile);
4388
4572
  return { status: "restored", restored, backup: name };
4389
4573
  }
4390
4574
  async function upgradeAdvisory(root) {
@@ -4417,7 +4601,7 @@ async function upgradeAdvisory(root) {
4417
4601
  }
4418
4602
 
4419
4603
  // src/sarif.ts
4420
- import path18 from "path";
4604
+ import path19 from "path";
4421
4605
  var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
4422
4606
  var SARIF_VERSION = "2.1.0";
4423
4607
  var TOOL_NAME = "SpecAtlas";
@@ -4455,7 +4639,7 @@ function resultOf(diagnostic, root) {
4455
4639
  message: { text: diagnostic.message }
4456
4640
  };
4457
4641
  if (diagnostic.path) {
4458
- const rel = path18.relative(root, diagnostic.path);
4642
+ const rel = path19.relative(root, diagnostic.path);
4459
4643
  if (rel !== "") {
4460
4644
  const physicalLocation = {
4461
4645
  artifactLocation: { uri: toPosix(rel) },
@@ -4505,7 +4689,7 @@ function toSarifText(opts) {
4505
4689
  }
4506
4690
 
4507
4691
  // src/doctor.ts
4508
- import path19 from "path";
4692
+ import path20 from "path";
4509
4693
  async function runDoctor(root) {
4510
4694
  const findings = [];
4511
4695
  const { workspace, config } = await loadWorkspace(root);
@@ -4513,7 +4697,7 @@ async function runDoctor(root) {
4513
4697
  const approvals = await loadApprovals(workspace.sddDir);
4514
4698
  findings.push(...approvals.diagnostics);
4515
4699
  for (const change of workspace.changes) {
4516
- const deltaPath = path19.join(change.dir, "spec.md");
4700
+ const deltaPath = path20.join(change.dir, "spec.md");
4517
4701
  const deltaContent = await readTextIfExists(deltaPath);
4518
4702
  const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
4519
4703
  if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
@@ -4535,7 +4719,7 @@ async function runDoctor(root) {
4535
4719
  }
4536
4720
  for (const override of change.meta?.overrides ?? []) {
4537
4721
  if (!override.reason.trim() || !override.by.trim()) {
4538
- findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path19.join(change.dir, "meta.yaml") }));
4722
+ findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path20.join(change.dir, "meta.yaml") }));
4539
4723
  }
4540
4724
  }
4541
4725
  }
@@ -4558,7 +4742,7 @@ async function specHashOf(filePath) {
4558
4742
  }
4559
4743
 
4560
4744
  // src/gate.ts
4561
- import path20 from "path";
4745
+ import path21 from "path";
4562
4746
  var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
4563
4747
  function count(name, diagnostics) {
4564
4748
  return {
@@ -4575,7 +4759,7 @@ function livingRequirementsMap(specs) {
4575
4759
  return map;
4576
4760
  }
4577
4761
  async function runCiGate(opts) {
4578
- const root = path20.resolve(opts.root);
4762
+ const root = path21.resolve(opts.root);
4579
4763
  const { workspace, config } = await loadWorkspace(root);
4580
4764
  const diagnostics = [];
4581
4765
  const checks = [];
@@ -4586,7 +4770,7 @@ async function runCiGate(opts) {
4586
4770
  let changesErrors = 0;
4587
4771
  let changesWarnings = 0;
4588
4772
  for (const change of workspace.changes) {
4589
- const lintFindings = change.delta ? lintDelta(change.delta, living, path20.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
4773
+ const lintFindings = change.delta ? lintDelta(change.delta, living, path21.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
4590
4774
  const trace = checkTrace({
4591
4775
  specs: workspace.specs,
4592
4776
  change,
@@ -4636,6 +4820,7 @@ export {
4636
4820
  BUILTIN_PACKS,
4637
4821
  CONFIG_FILE,
4638
4822
  CORE_VERSION,
4823
+ LIVING_FIXES_DIR,
4639
4824
  REQ_HEAD_RE,
4640
4825
  REQ_ID_RE,
4641
4826
  RULE_ID_RE,
@@ -4735,6 +4920,7 @@ export {
4735
4920
  loadChange,
4736
4921
  loadConfig,
4737
4922
  loadDetectedBest,
4923
+ loadLivingFixes,
4738
4924
  loadProfileFile,
4739
4925
  loadProfilesFromDir,
4740
4926
  loadProjectPacks,
@@ -4755,10 +4941,12 @@ export {
4755
4941
  parseChangeMeta,
4756
4942
  parseConfig,
4757
4943
  parseDelta,
4944
+ parseFixCovers,
4758
4945
  parseFrontmatter,
4759
4946
  parseGitHubRemote,
4760
4947
  parseGlossary,
4761
4948
  parseIssueUrl,
4949
+ parseLivingFix,
4762
4950
  parseRequirementBlocks,
4763
4951
  parseSpecFile,
4764
4952
  parseTasksFile,
@@ -4810,6 +4998,7 @@ export {
4810
4998
  verifyApproval,
4811
4999
  walkFiles,
4812
5000
  writeConfig,
5001
+ writeLivingFix,
4813
5002
  writeMockupManifest,
4814
5003
  writeMockupPlan,
4815
5004
  writeText