@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.d.ts +40 -1
- package/dist/index.js +382 -193
- package/package.json +1 -1
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,
|
|
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:
|
|
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,
|
|
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",
|
|
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",
|
|
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:
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
1593
|
+
let current = path5.resolve(start);
|
|
1470
1594
|
for (let i = 0; i < 40; i += 1) {
|
|
1471
|
-
if (await isDirectory(
|
|
1472
|
-
const parent =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1646
|
+
const planFile = path5.join(dir, "plan.md");
|
|
1523
1647
|
if (await exists(planFile)) change.planPath = planFile;
|
|
1524
|
-
const reviewFile =
|
|
1648
|
+
const reviewFile = path5.join(dir, "review.md");
|
|
1525
1649
|
if (await exists(reviewFile)) change.reviewPath = reviewFile;
|
|
1526
|
-
const presentationFile =
|
|
1650
|
+
const presentationFile = path5.join(dir, "presentation", "index.html");
|
|
1527
1651
|
if (await exists(presentationFile)) change.presentationPath = presentationFile;
|
|
1528
|
-
const tasksFile =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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}-/, ""),
|
|
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",
|
|
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 =
|
|
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
|
|
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
|
-
` +
|
|
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
|
|
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: \`${
|
|
2075
|
-
if (change.planPath) lines.push(`- Plan: \`${
|
|
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 =
|
|
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 ${
|
|
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:
|
|
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
|
|
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 =
|
|
2440
|
+
const root = path7.resolve(opts.root);
|
|
2299
2441
|
const slug = opts.slug;
|
|
2300
2442
|
const fileKind = opts.file ?? "verify";
|
|
2301
|
-
const file =
|
|
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
|
|
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
|
|
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 =
|
|
2579
|
+
const dir = path8.join(runsDir(root), runId);
|
|
2438
2580
|
await ensureDir(dir);
|
|
2439
|
-
await writeText(
|
|
2581
|
+
await writeText(path8.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
|
|
2440
2582
|
`);
|
|
2441
|
-
await writeText(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2466
|
-
const stateRaw = await readTextIfExists(
|
|
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(
|
|
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(
|
|
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
|
|
2644
|
+
import path11 from "path";
|
|
2503
2645
|
|
|
2504
2646
|
// src/mockups.ts
|
|
2505
|
-
import
|
|
2506
|
-
import { parse as parseYaml6, stringify as
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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:
|
|
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(
|
|
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:
|
|
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,
|
|
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 =
|
|
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,
|
|
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 =
|
|
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,
|
|
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 =
|
|
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,
|
|
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://${
|
|
2748
|
-
const rel =
|
|
2749
|
-
await page2.screenshot({ path:
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
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
|
|
3346
|
+
import path12 from "path";
|
|
3205
3347
|
async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
3206
|
-
const resolved =
|
|
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,
|
|
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 =
|
|
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(
|
|
3466
|
+
return readTextIfExists2(path12.join(change.dir, "spec.md"));
|
|
3325
3467
|
}
|
|
3326
3468
|
|
|
3327
3469
|
// src/present.ts
|
|
3328
|
-
import
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3347
|
-
const mockupsCopyDir =
|
|
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(
|
|
3492
|
+
const proposalRaw = await readTextIfExists(path13.join(change.dir, "proposal.md"));
|
|
3351
3493
|
const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
|
|
3352
|
-
const approvals = await loadApprovals(
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
3542
|
+
const shotsDir = path13.join(dir, "screens");
|
|
3401
3543
|
if (await exists(shotsDir)) {
|
|
3402
|
-
await ensureDir(
|
|
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(
|
|
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
|
|
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 =
|
|
3767
|
+
const root = path14.resolve(opts.root);
|
|
3626
3768
|
const dryRun = opts.dryRun ?? false;
|
|
3627
3769
|
const diagnostics = [];
|
|
3628
|
-
const sddDir =
|
|
3629
|
-
if (!await exists(
|
|
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(
|
|
3641
|
-
const relative = inventoried.filter((f) => SOURCE_EXT.has(
|
|
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(
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
3938
|
+
import path15 from "path";
|
|
3797
3939
|
async function initWorkspace(opts) {
|
|
3798
|
-
const root =
|
|
3799
|
-
const sddDir =
|
|
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(
|
|
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 ??
|
|
3954
|
+
cfg.project.name = opts.name ?? path15.basename(root);
|
|
3813
3955
|
await writeConfig(sddDir, cfg);
|
|
3814
|
-
created.push(
|
|
3956
|
+
created.push(path15.join(sddDir, "config.yaml"));
|
|
3815
3957
|
const templates = templatesFor(language);
|
|
3816
|
-
await writeText(
|
|
3817
|
-
created.push(
|
|
3818
|
-
await writeText(
|
|
3819
|
-
created.push(
|
|
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(
|
|
3824
|
-
created.push(
|
|
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(
|
|
3832
|
-
created.push(
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 {
|
|
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:
|
|
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 =
|
|
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 =
|
|
4115
|
-
const target =
|
|
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 =
|
|
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(
|
|
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
|
|
4157
|
-
import { parse as parseYaml8, stringify as
|
|
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",
|
|
4170
|
-
var CHANGE_ARTIFACTS = ["meta.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 =
|
|
4174
|
-
if (await exists(abs)) out.push({ artifact: toPosix(
|
|
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 =
|
|
4362
|
+
const sddDir = path18.join(root, ".sdd");
|
|
4179
4363
|
const out = [];
|
|
4180
4364
|
for (const rel of FIXED_ARTIFACTS) {
|
|
4181
|
-
const abs =
|
|
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 =
|
|
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,
|
|
4371
|
+
await pushChangeArtifacts(out, sddDir, path18.join("changes", slug));
|
|
4188
4372
|
}
|
|
4189
|
-
for (const entry of await listDirs(
|
|
4190
|
-
await pushChangeArtifacts(out, sddDir,
|
|
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
|
|
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 =
|
|
4288
|
-
const backupRoot =
|
|
4471
|
+
const sddDir = path18.join(root, ".sdd");
|
|
4472
|
+
const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
|
|
4289
4473
|
const name = backupName(now);
|
|
4290
|
-
const backupDir =
|
|
4291
|
-
const pointerFile =
|
|
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(
|
|
4482
|
+
await writeText(path18.join(backupDir, "files", ...item.artifact.split("/")), content);
|
|
4299
4483
|
}
|
|
4300
4484
|
await writeText(
|
|
4301
|
-
|
|
4302
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
4359
|
-
const backupRoot =
|
|
4360
|
-
const pointerFile =
|
|
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 =
|
|
4364
|
-
const manifestRaw = await readTextIfExists(
|
|
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 =
|
|
4376
|
-
const to =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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 =
|
|
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,
|
|
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
|