@specatlas/core 0.1.22 → 0.1.25
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 +181 -1
- package/dist/index.js +688 -159
- 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, path19, 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, path19, 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, path19, 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, path19) {
|
|
|
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, path19) {
|
|
|
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, path19) {
|
|
|
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,117 @@ 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
|
+
};
|
|
1518
|
+
if (typeof data["domain"] === "string") fix.domain = data["domain"];
|
|
1519
|
+
if (typeof data["title"] === "string") fix.title = data["title"];
|
|
1520
|
+
return fix;
|
|
1521
|
+
}
|
|
1522
|
+
async function loadLivingFixes(root) {
|
|
1523
|
+
const dir = path4.join(root, LIVING_FIXES_DIR);
|
|
1524
|
+
const entries = (await listDir(dir)).filter((entry) => entry.toLowerCase().endsWith(".md")).sort();
|
|
1525
|
+
const fixes = [];
|
|
1526
|
+
for (const entry of entries) {
|
|
1527
|
+
const file = path4.join(dir, entry);
|
|
1528
|
+
const content = await readTextIfExists(file);
|
|
1529
|
+
if (content === void 0) continue;
|
|
1530
|
+
fixes.push(parseLivingFix(content, file));
|
|
1531
|
+
}
|
|
1532
|
+
return fixes.sort((a, b) => b.date.localeCompare(a.date) || b.slug.localeCompare(a.slug));
|
|
1533
|
+
}
|
|
1534
|
+
async function writeLivingFix(root, input) {
|
|
1535
|
+
const dir = path4.join(root, LIVING_FIXES_DIR);
|
|
1536
|
+
const month = input.date.slice(0, 7);
|
|
1537
|
+
const file = path4.join(dir, `${month}-${input.slug}.md`);
|
|
1538
|
+
const relativePath = toPosix(path4.relative(root, file));
|
|
1539
|
+
if (await exists(file)) return { file, relativePath, created: false };
|
|
1540
|
+
const data = {
|
|
1541
|
+
slug: input.slug,
|
|
1542
|
+
date: input.date,
|
|
1543
|
+
result: input.result ?? "pass"
|
|
1544
|
+
};
|
|
1545
|
+
if (input.domain) data["domain"] = input.domain;
|
|
1546
|
+
if (input.title) data["title"] = input.title;
|
|
1547
|
+
if (input.covers && input.covers.length > 0) data["covers"] = input.covers;
|
|
1548
|
+
const body = input.content.replace(/^[\r\n]+/, "").trimEnd();
|
|
1549
|
+
const text = `---
|
|
1550
|
+
${stringifyYaml3(data, { lineWidth: 120 })}---
|
|
1551
|
+
|
|
1552
|
+
${body}
|
|
1553
|
+
`;
|
|
1554
|
+
await writeText(file, text);
|
|
1555
|
+
return { file, relativePath, created: true };
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/workspace.ts
|
|
1467
1559
|
var SDD_DIR = ".sdd";
|
|
1468
1560
|
async function findWorkspaceRoot(start) {
|
|
1469
|
-
let current =
|
|
1561
|
+
let current = path5.resolve(start);
|
|
1470
1562
|
for (let i = 0; i < 40; i += 1) {
|
|
1471
|
-
if (await isDirectory(
|
|
1472
|
-
const parent =
|
|
1563
|
+
if (await isDirectory(path5.join(current, SDD_DIR))) return current;
|
|
1564
|
+
const parent = path5.dirname(current);
|
|
1473
1565
|
if (parent === current) return void 0;
|
|
1474
1566
|
current = parent;
|
|
1475
1567
|
}
|
|
1476
1568
|
return void 0;
|
|
1477
1569
|
}
|
|
1478
1570
|
async function loadApprovals(sddDir) {
|
|
1479
|
-
const file =
|
|
1571
|
+
const file = path5.join(sddDir, "approvals.yaml");
|
|
1480
1572
|
const raw = await readTextIfExists(file);
|
|
1481
1573
|
if (raw === void 0) return { byArtifact: /* @__PURE__ */ new Map(), diagnostics: [] };
|
|
1482
1574
|
const parsed = parseApprovals(raw, file);
|
|
1483
1575
|
const map = /* @__PURE__ */ new Map();
|
|
1484
1576
|
for (const a of parsed.approvals?.approvals ?? []) {
|
|
1485
|
-
map.set(a.artifact.split(
|
|
1577
|
+
map.set(a.artifact.split(path5.sep).join("/"), { hash: a.artifactHash, by: a.approvedBy, at: a.approvedAt });
|
|
1486
1578
|
}
|
|
1487
1579
|
return { byArtifact: map, diagnostics: parsed.diagnostics };
|
|
1488
1580
|
}
|
|
1489
1581
|
async function loadSpecs(sddDir) {
|
|
1490
|
-
const specsDir =
|
|
1582
|
+
const specsDir = path5.join(sddDir, "specs");
|
|
1491
1583
|
const domains = await listDirs(specsDir);
|
|
1492
1584
|
const out = [];
|
|
1493
1585
|
for (const domain of domains) {
|
|
1494
|
-
const file =
|
|
1586
|
+
const file = path5.join(specsDir, domain, "spec.md");
|
|
1495
1587
|
if (!await exists(file)) continue;
|
|
1496
1588
|
const content = await readText(file);
|
|
1497
1589
|
out.push({ domain, path: file, spec: parseSpecFile(content, file) });
|
|
@@ -1499,10 +1591,10 @@ async function loadSpecs(sddDir) {
|
|
|
1499
1591
|
return out;
|
|
1500
1592
|
}
|
|
1501
1593
|
async function loadChange(root, slug, relDir) {
|
|
1502
|
-
const dir =
|
|
1594
|
+
const dir = path5.join(root, SDD_DIR, "changes", relDir ?? slug);
|
|
1503
1595
|
const diagnostics = [];
|
|
1504
1596
|
const change = { slug, dir, diagnostics };
|
|
1505
|
-
const metaFile =
|
|
1597
|
+
const metaFile = path5.join(dir, "meta.yaml");
|
|
1506
1598
|
const metaRaw = await readTextIfExists(metaFile);
|
|
1507
1599
|
if (metaRaw === void 0) {
|
|
1508
1600
|
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 +1603,7 @@ async function loadChange(root, slug, relDir) {
|
|
|
1511
1603
|
diagnostics.push(...parsed.diagnostics);
|
|
1512
1604
|
if (parsed.meta) change.meta = parsed.meta;
|
|
1513
1605
|
}
|
|
1514
|
-
const deltaFile =
|
|
1606
|
+
const deltaFile = path5.join(dir, "spec.md");
|
|
1515
1607
|
const deltaRaw = await readTextIfExists(deltaFile);
|
|
1516
1608
|
if (deltaRaw !== void 0) {
|
|
1517
1609
|
change.delta = parseDelta(deltaRaw, deltaFile);
|
|
@@ -1519,41 +1611,42 @@ async function loadChange(root, slug, relDir) {
|
|
|
1519
1611
|
} else {
|
|
1520
1612
|
diagnostics.push(diag("ATLAS-FILES-002", "warning", `El cambio "${slug}" no tiene spec.md (delta)`, { path: deltaFile }));
|
|
1521
1613
|
}
|
|
1522
|
-
const planFile =
|
|
1614
|
+
const planFile = path5.join(dir, "plan.md");
|
|
1523
1615
|
if (await exists(planFile)) change.planPath = planFile;
|
|
1524
|
-
const reviewFile =
|
|
1616
|
+
const reviewFile = path5.join(dir, "review.md");
|
|
1525
1617
|
if (await exists(reviewFile)) change.reviewPath = reviewFile;
|
|
1526
|
-
const presentationFile =
|
|
1618
|
+
const presentationFile = path5.join(dir, "presentation", "index.html");
|
|
1527
1619
|
if (await exists(presentationFile)) change.presentationPath = presentationFile;
|
|
1528
|
-
const tasksFile =
|
|
1620
|
+
const tasksFile = path5.join(dir, "tasks.md");
|
|
1529
1621
|
const tasksRaw = await readTextIfExists(tasksFile);
|
|
1530
1622
|
if (tasksRaw !== void 0) {
|
|
1531
1623
|
change.tasks = parseTasksFile(tasksRaw, tasksFile);
|
|
1532
1624
|
diagnostics.push(...change.tasks.diagnostics);
|
|
1533
1625
|
}
|
|
1534
|
-
const verifyFile =
|
|
1626
|
+
const verifyFile = path5.join(dir, "verify.md");
|
|
1535
1627
|
const verifyRaw = await readTextIfExists(verifyFile);
|
|
1536
1628
|
if (verifyRaw !== void 0) {
|
|
1537
1629
|
change.verify = parseVerifyFile(verifyRaw, verifyFile);
|
|
1538
1630
|
diagnostics.push(...change.verify.diagnostics);
|
|
1539
1631
|
}
|
|
1540
|
-
const fixFile =
|
|
1632
|
+
const fixFile = path5.join(dir, "fix.md");
|
|
1541
1633
|
const fixRaw = await readTextIfExists(fixFile);
|
|
1542
1634
|
if (fixRaw !== void 0) {
|
|
1543
1635
|
change.fix = parseVerifyFile(fixRaw, fixFile);
|
|
1544
1636
|
diagnostics.push(...change.fix.diagnostics);
|
|
1637
|
+
change.fixCovers = parseFixCovers(fixRaw);
|
|
1545
1638
|
}
|
|
1546
|
-
const mockupManifest =
|
|
1639
|
+
const mockupManifest = path5.join(dir, "mockups", "manifest.yaml");
|
|
1547
1640
|
if (await exists(mockupManifest)) change.mockupManifestPath = mockupManifest;
|
|
1548
1641
|
return change;
|
|
1549
1642
|
}
|
|
1550
1643
|
async function listChangeSlugs(root, opts = {}) {
|
|
1551
|
-
const changesDir =
|
|
1644
|
+
const changesDir = path5.join(root, SDD_DIR, "changes");
|
|
1552
1645
|
const dirs = await listDirs(changesDir);
|
|
1553
1646
|
return dirs.filter((d) => opts.includeArchived ? true : d !== "archive");
|
|
1554
1647
|
}
|
|
1555
1648
|
async function loadWorkspace(root) {
|
|
1556
|
-
const sddDir =
|
|
1649
|
+
const sddDir = path5.join(root, SDD_DIR);
|
|
1557
1650
|
const { config, diagnostics: configDiags } = await loadConfig(sddDir);
|
|
1558
1651
|
const diagnostics = [...configDiags];
|
|
1559
1652
|
const specs = await loadSpecs(sddDir);
|
|
@@ -1566,20 +1659,20 @@ async function loadWorkspace(root) {
|
|
|
1566
1659
|
diagnostics.push(...change.diagnostics);
|
|
1567
1660
|
}
|
|
1568
1661
|
const archived = [];
|
|
1569
|
-
const archivedDir =
|
|
1662
|
+
const archivedDir = path5.join(sddDir, "changes", "archive");
|
|
1570
1663
|
if (await isDirectory(archivedDir)) {
|
|
1571
1664
|
for (const entry of await listDirs(archivedDir)) {
|
|
1572
|
-
archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""),
|
|
1665
|
+
archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""), path5.join("archive", entry)));
|
|
1573
1666
|
}
|
|
1574
1667
|
}
|
|
1575
1668
|
return { workspace: { root, sddDir, specs, changes, archived, diagnostics }, config };
|
|
1576
1669
|
}
|
|
1577
1670
|
async function ensureSddDirs(sddDir) {
|
|
1578
1671
|
const created = [];
|
|
1579
|
-
const dirs = ["specs", "changes", "runs", "metrics",
|
|
1672
|
+
const dirs = ["specs", "changes", "runs", "metrics", "fixes", path5.join("profiles", "custom")];
|
|
1580
1673
|
const { ensureDir: ensureDir2 } = await import("./fsx-VF2P7ALA.js");
|
|
1581
1674
|
for (const d of dirs) {
|
|
1582
|
-
const full =
|
|
1675
|
+
const full = path5.join(sddDir, d);
|
|
1583
1676
|
if (!await exists(full)) {
|
|
1584
1677
|
await ensureDir2(full);
|
|
1585
1678
|
created.push(full);
|
|
@@ -1589,7 +1682,7 @@ async function ensureSddDirs(sddDir) {
|
|
|
1589
1682
|
}
|
|
1590
1683
|
|
|
1591
1684
|
// src/templates.ts
|
|
1592
|
-
import { stringify as
|
|
1685
|
+
import { stringify as stringifyYaml4 } from "yaml";
|
|
1593
1686
|
var ES = {
|
|
1594
1687
|
constitution: `# Constituci\xF3n del proyecto
|
|
1595
1688
|
|
|
@@ -1750,6 +1843,10 @@ notes: 12/12 casos
|
|
|
1750
1843
|
|
|
1751
1844
|
## Evidencia
|
|
1752
1845
|
|
|
1846
|
+
<!-- Opcional: declara los requisitos que este fix afecta (aparecen en su trazabilidad).
|
|
1847
|
+
Cubre: REQ-DOMINIO-001
|
|
1848
|
+
-->
|
|
1849
|
+
|
|
1753
1850
|
<!-- Registra la evidencia real con:
|
|
1754
1851
|
satlas verify <slug> --file fix --scenario REQ-DOMINIO-001-S1 --command "<comando>" --by "<nombre>"
|
|
1755
1852
|
o, si es manual:
|
|
@@ -1873,6 +1970,10 @@ by: your-name
|
|
|
1873
1970
|
## Rollback
|
|
1874
1971
|
## Evidence
|
|
1875
1972
|
|
|
1973
|
+
<!-- Optional: declare the requirements this fix affects (they show up in its traceability).
|
|
1974
|
+
Cubre: REQ-DOMAIN-001
|
|
1975
|
+
-->
|
|
1976
|
+
|
|
1876
1977
|
<!-- Register real evidence with:
|
|
1877
1978
|
satlas verify <slug> --file fix --scenario REQ-DOMAIN-001-S1 --command "<command>" --by "<name>"
|
|
1878
1979
|
-->
|
|
@@ -1897,7 +1998,7 @@ function changeMetaYaml(meta) {
|
|
|
1897
1998
|
if (meta.owner) doc["owner"] = meta.owner;
|
|
1898
1999
|
if (meta.mockups) doc["mockups"] = meta.mockups;
|
|
1899
2000
|
return `# Estado del cambio. La fase se DERIVA de los artefactos; aqu\xED solo hechos.
|
|
1900
|
-
` +
|
|
2001
|
+
` + stringifyYaml4(doc, { lineWidth: 120 });
|
|
1901
2002
|
}
|
|
1902
2003
|
function indexMarkdown(input) {
|
|
1903
2004
|
const es = input.language !== "en";
|
|
@@ -1911,6 +2012,15 @@ function indexMarkdown(input) {
|
|
|
1911
2012
|
if (input.specs.length === 0) lines.push(es ? "_Sin specs todav\xEDa._" : "_No specs yet._");
|
|
1912
2013
|
for (const spec of input.specs) lines.push(`- \`${spec.domain}\` \u2014 ${spec.requirements} ${es ? "requisitos" : "requirements"}`);
|
|
1913
2014
|
lines.push("");
|
|
2015
|
+
lines.push(es ? "## Fixes vivos" : "## Living fixes");
|
|
2016
|
+
lines.push("");
|
|
2017
|
+
const fixes = input.fixes ?? [];
|
|
2018
|
+
if (fixes.length === 0) lines.push(es ? "_Sin fixes archivados todav\xEDa._" : "_No archived fixes yet._");
|
|
2019
|
+
for (const fix of fixes) {
|
|
2020
|
+
const parts = [fix.date, fix.domain ? `\`${fix.domain}\`` : "", `\`${fix.slug}\``, fix.result].filter((part) => part !== "");
|
|
2021
|
+
lines.push(`- ${parts.join(" \xB7 ")}`);
|
|
2022
|
+
}
|
|
2023
|
+
lines.push("");
|
|
1914
2024
|
lines.push(es ? "## Cambios activos" : "## Active changes");
|
|
1915
2025
|
lines.push("");
|
|
1916
2026
|
if (input.changes.length === 0) lines.push(es ? "_Sin cambios activos._" : "_No active changes._");
|
|
@@ -1926,7 +2036,7 @@ ${input.archived} change(s) in \`changes/archive/\`.`);
|
|
|
1926
2036
|
}
|
|
1927
2037
|
|
|
1928
2038
|
// src/github.ts
|
|
1929
|
-
import
|
|
2039
|
+
import path6 from "path";
|
|
1930
2040
|
import { parse as parseYaml5 } from "yaml";
|
|
1931
2041
|
|
|
1932
2042
|
// src/exec.ts
|
|
@@ -2071,8 +2181,8 @@ function issueBody(input) {
|
|
|
2071
2181
|
lines.push(`- Siguiente: \`${input.next}\` \u2014 ${input.nextDescription}`);
|
|
2072
2182
|
lines.push("");
|
|
2073
2183
|
lines.push("### Artefactos");
|
|
2074
|
-
lines.push(`- Spec: \`${
|
|
2075
|
-
if (change.planPath) lines.push(`- Plan: \`${
|
|
2184
|
+
lines.push(`- Spec: \`${path6.posix.join(".sdd", "changes", change.slug, "spec.md")}\``);
|
|
2185
|
+
if (change.planPath) lines.push(`- Plan: \`${path6.posix.join(".sdd", "changes", change.slug, "plan.md")}\``);
|
|
2076
2186
|
if (change.tasks) lines.push(`- Tareas: ${change.tasks.counts.done}/${change.tasks.counts.total}`);
|
|
2077
2187
|
if (change.verify) lines.push(`- Evidencia registrada: ${change.verify.evidence.length}`);
|
|
2078
2188
|
lines.push("");
|
|
@@ -2140,10 +2250,10 @@ async function commentIssue(root, number, body, runner = defaultRunner) {
|
|
|
2140
2250
|
return result.ok ? [] : [diag("ATLAS-GH-006", "error", `No se pudo comentar el issue #${number}: ${(result.stderr || result.stdout).trim().slice(0, 300)}`)];
|
|
2141
2251
|
}
|
|
2142
2252
|
async function patchChangeMeta(root, slug, patch) {
|
|
2143
|
-
const file =
|
|
2253
|
+
const file = path6.join(path6.resolve(root), ".sdd", "changes", slug, "meta.yaml");
|
|
2144
2254
|
const raw = await readTextIfExists(file);
|
|
2145
2255
|
if (raw === void 0) {
|
|
2146
|
-
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${
|
|
2256
|
+
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${path6.relative(root, file)}`)] };
|
|
2147
2257
|
}
|
|
2148
2258
|
let data;
|
|
2149
2259
|
try {
|
|
@@ -2197,7 +2307,7 @@ async function approveFromGithub(opts) {
|
|
|
2197
2307
|
const { signApproval: signApproval2 } = await import("./approvals-2PO6223I.js");
|
|
2198
2308
|
const signed = await signApproval2({
|
|
2199
2309
|
root: opts.root,
|
|
2200
|
-
artifact:
|
|
2310
|
+
artifact: path6.posix.join("changes", opts.slug, "spec.md"),
|
|
2201
2311
|
by,
|
|
2202
2312
|
channel: "tracker",
|
|
2203
2313
|
note: `Aprobado v\xEDa GitHub issue #${number} (etiqueta "${label}")`
|
|
@@ -2257,7 +2367,7 @@ async function syncGithubIssue(root, slug, opts = {}) {
|
|
|
2257
2367
|
}
|
|
2258
2368
|
|
|
2259
2369
|
// src/evidence.ts
|
|
2260
|
-
import
|
|
2370
|
+
import path7 from "path";
|
|
2261
2371
|
var SCENARIO_HEAD_RE2 = /^###\s+(REQ-[A-Z0-9-]+-S\d+)\b/;
|
|
2262
2372
|
function locateBlocks(content) {
|
|
2263
2373
|
const normalized = content.replace(/\r\n?/g, "\n");
|
|
@@ -2295,10 +2405,10 @@ function buildEvidenceYaml(evidence) {
|
|
|
2295
2405
|
return lines.join("\n");
|
|
2296
2406
|
}
|
|
2297
2407
|
async function recordEvidence(opts) {
|
|
2298
|
-
const root =
|
|
2408
|
+
const root = path7.resolve(opts.root);
|
|
2299
2409
|
const slug = opts.slug;
|
|
2300
2410
|
const fileKind = opts.file ?? "verify";
|
|
2301
|
-
const file =
|
|
2411
|
+
const file = path7.join(root, ".sdd", "changes", slug, fileKind === "fix" ? "fix.md" : "verify.md");
|
|
2302
2412
|
const diagnostics = [];
|
|
2303
2413
|
const scenario = opts.scenario.toUpperCase();
|
|
2304
2414
|
if (!SCENARIO_ID_RE.test(scenario)) {
|
|
@@ -2395,7 +2505,7 @@ function evidenceSummary(content) {
|
|
|
2395
2505
|
}
|
|
2396
2506
|
|
|
2397
2507
|
// src/runs.ts
|
|
2398
|
-
import
|
|
2508
|
+
import path8 from "path";
|
|
2399
2509
|
import { randomBytes } from "crypto";
|
|
2400
2510
|
var RUN_EVENT_TYPES = [
|
|
2401
2511
|
"run_started",
|
|
@@ -2421,7 +2531,7 @@ function generateRunId(now = /* @__PURE__ */ new Date()) {
|
|
|
2421
2531
|
return `${iso.slice(0, 8)}-${iso.slice(8, 14)}-${randomBytes(3).toString("hex")}`;
|
|
2422
2532
|
}
|
|
2423
2533
|
function runsDir(root) {
|
|
2424
|
-
return
|
|
2534
|
+
return path8.join(path8.resolve(root), ".sdd", "runs");
|
|
2425
2535
|
}
|
|
2426
2536
|
async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Date()) {
|
|
2427
2537
|
const runId = generateRunId(now);
|
|
@@ -2434,25 +2544,25 @@ async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Da
|
|
|
2434
2544
|
updatedAt: localStamp(now)
|
|
2435
2545
|
};
|
|
2436
2546
|
if (inputs) state.inputs = inputs;
|
|
2437
|
-
const dir =
|
|
2547
|
+
const dir = path8.join(runsDir(root), runId);
|
|
2438
2548
|
await ensureDir(dir);
|
|
2439
|
-
await writeText(
|
|
2549
|
+
await writeText(path8.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
|
|
2440
2550
|
`);
|
|
2441
|
-
await writeText(
|
|
2551
|
+
await writeText(path8.join(dir, "events.jsonl"), "");
|
|
2442
2552
|
await appendRunEvent(root, runId, "run_started", { slug, phase });
|
|
2443
2553
|
return state;
|
|
2444
2554
|
}
|
|
2445
2555
|
async function appendRunEvent(root, runId, type, data, now = /* @__PURE__ */ new Date()) {
|
|
2446
2556
|
const event = { eventId: randomBytes(4).toString("hex"), at: localStamp(now), type };
|
|
2447
2557
|
if (data) event.data = data;
|
|
2448
|
-
const file =
|
|
2558
|
+
const file = path8.join(runsDir(root), runId, "events.jsonl");
|
|
2449
2559
|
const previous = await readTextIfExists(file) ?? "";
|
|
2450
2560
|
await writeText(file, `${previous}${JSON.stringify(event)}
|
|
2451
2561
|
`);
|
|
2452
2562
|
return event;
|
|
2453
2563
|
}
|
|
2454
2564
|
async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Date()) {
|
|
2455
|
-
const file =
|
|
2565
|
+
const file = path8.join(runsDir(root), runId, "state.json");
|
|
2456
2566
|
const raw = await readTextIfExists(file);
|
|
2457
2567
|
if (raw === void 0) return;
|
|
2458
2568
|
const state = JSON.parse(raw);
|
|
@@ -2462,13 +2572,13 @@ async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Da
|
|
|
2462
2572
|
`);
|
|
2463
2573
|
}
|
|
2464
2574
|
async function readRun(root, runId) {
|
|
2465
|
-
const dir =
|
|
2466
|
-
const stateRaw = await readTextIfExists(
|
|
2575
|
+
const dir = path8.join(runsDir(root), runId);
|
|
2576
|
+
const stateRaw = await readTextIfExists(path8.join(dir, "state.json"));
|
|
2467
2577
|
if (stateRaw === void 0) {
|
|
2468
2578
|
return { diagnostics: [diag("ATLAS-RUN-001", "error", `No existe el run ${runId}`, { path: dir })] };
|
|
2469
2579
|
}
|
|
2470
2580
|
const state = JSON.parse(stateRaw);
|
|
2471
|
-
const eventsRaw = await readTextIfExists(
|
|
2581
|
+
const eventsRaw = await readTextIfExists(path8.join(dir, "events.jsonl")) ?? "";
|
|
2472
2582
|
const events = [];
|
|
2473
2583
|
for (const line of eventsRaw.split("\n")) {
|
|
2474
2584
|
if (line.trim() === "") continue;
|
|
@@ -2485,7 +2595,7 @@ async function listRuns(root, filter = {}) {
|
|
|
2485
2595
|
const ids = await listDirs(dir);
|
|
2486
2596
|
const out = [];
|
|
2487
2597
|
for (const id of ids) {
|
|
2488
|
-
const raw = await readTextIfExists(
|
|
2598
|
+
const raw = await readTextIfExists(path8.join(dir, id, "state.json"));
|
|
2489
2599
|
if (raw === void 0) continue;
|
|
2490
2600
|
try {
|
|
2491
2601
|
const state = JSON.parse(raw);
|
|
@@ -2499,11 +2609,11 @@ async function listRuns(root, filter = {}) {
|
|
|
2499
2609
|
}
|
|
2500
2610
|
|
|
2501
2611
|
// src/analyze.ts
|
|
2502
|
-
import
|
|
2612
|
+
import path11 from "path";
|
|
2503
2613
|
|
|
2504
2614
|
// src/mockups.ts
|
|
2505
|
-
import
|
|
2506
|
-
import { parse as parseYaml6, stringify as
|
|
2615
|
+
import path9 from "path";
|
|
2616
|
+
import { parse as parseYaml6, stringify as stringifyYaml5 } from "yaml";
|
|
2507
2617
|
import { z as z4 } from "zod";
|
|
2508
2618
|
var mockupManifestSchema = z4.object({
|
|
2509
2619
|
schema_version: z4.number().int().positive().default(1),
|
|
@@ -2528,20 +2638,20 @@ var mockupManifestSchema = z4.object({
|
|
|
2528
2638
|
});
|
|
2529
2639
|
var TOKEN_CANDIDATES = ["design/tokens.json", "DESIGN.md", "design/DESIGN.md", ".sdd/design/tokens.json"];
|
|
2530
2640
|
function mockupsDir(root, slug) {
|
|
2531
|
-
return
|
|
2641
|
+
return path9.join(path9.resolve(root), ".sdd", "changes", slug, "mockups");
|
|
2532
2642
|
}
|
|
2533
2643
|
async function tokensFile(root) {
|
|
2534
2644
|
for (const candidate of TOKEN_CANDIDATES) {
|
|
2535
|
-
const abs =
|
|
2645
|
+
const abs = path9.join(root, candidate);
|
|
2536
2646
|
if (await exists(abs)) return candidate;
|
|
2537
2647
|
}
|
|
2538
2648
|
return void 0;
|
|
2539
2649
|
}
|
|
2540
2650
|
async function computeInputsHash(root, change) {
|
|
2541
|
-
const deltaPath =
|
|
2651
|
+
const deltaPath = path9.join(change.dir, "spec.md");
|
|
2542
2652
|
const delta = await readTextIfExists(deltaPath) ?? "";
|
|
2543
2653
|
const tokens = await tokensFile(root);
|
|
2544
|
-
const tokensContent = tokens ? await readTextIfExists(
|
|
2654
|
+
const tokensContent = tokens ? await readTextIfExists(path9.join(root, tokens)) ?? "" : "";
|
|
2545
2655
|
return artifactHash(`${delta}
|
|
2546
2656
|
---tokens---
|
|
2547
2657
|
${tokensContent}`);
|
|
@@ -2566,7 +2676,7 @@ function planMockups(change, platform = "web") {
|
|
|
2566
2676
|
return { platform, screens };
|
|
2567
2677
|
}
|
|
2568
2678
|
async function readMockupManifest(root, slug) {
|
|
2569
|
-
const file =
|
|
2679
|
+
const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
|
|
2570
2680
|
const raw = await readTextIfExists(file);
|
|
2571
2681
|
if (raw === void 0) return { diagnostics: [] };
|
|
2572
2682
|
let data;
|
|
@@ -2658,7 +2768,7 @@ async function checkMockups(root, slug, change) {
|
|
|
2658
2768
|
const { manifest, path: manifestPath, diagnostics } = await readMockupManifest(root, slug);
|
|
2659
2769
|
const findings = [...diagnostics];
|
|
2660
2770
|
if (!manifest || !manifestPath) {
|
|
2661
|
-
return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path:
|
|
2771
|
+
return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path: path9.join(dir, "manifest.yaml") })], stale: false };
|
|
2662
2772
|
}
|
|
2663
2773
|
const known = /* @__PURE__ */ new Set();
|
|
2664
2774
|
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
@@ -2666,12 +2776,12 @@ async function checkMockups(root, slug, change) {
|
|
|
2666
2776
|
}
|
|
2667
2777
|
findings.push(...lintMockupManifest(manifest, known, manifestPath));
|
|
2668
2778
|
for (const screen of manifest.screens) {
|
|
2669
|
-
const html = await readTextIfExists(
|
|
2779
|
+
const html = await readTextIfExists(path9.join(dir, screen.file));
|
|
2670
2780
|
if (html === void 0) {
|
|
2671
|
-
findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path:
|
|
2781
|
+
findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path: path9.join(dir, screen.file) }));
|
|
2672
2782
|
continue;
|
|
2673
2783
|
}
|
|
2674
|
-
findings.push(...lintMockupHtml(html,
|
|
2784
|
+
findings.push(...lintMockupHtml(html, path9.join(dir, screen.file)));
|
|
2675
2785
|
}
|
|
2676
2786
|
const inputsHash = await computeInputsHash(root, change);
|
|
2677
2787
|
const stale = manifest.inputsHash !== void 0 && manifest.inputsHash !== inputsHash;
|
|
@@ -2686,7 +2796,7 @@ async function checkMockups(root, slug, change) {
|
|
|
2686
2796
|
return { findings, stale, manifest, manifestPath };
|
|
2687
2797
|
}
|
|
2688
2798
|
async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date()) {
|
|
2689
|
-
const file =
|
|
2799
|
+
const file = path9.join(mockupsDir(root, slug), "plan.yaml");
|
|
2690
2800
|
if (await exists(file)) return file;
|
|
2691
2801
|
const doc = {
|
|
2692
2802
|
schema_version: 1,
|
|
@@ -2694,11 +2804,11 @@ async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date(
|
|
|
2694
2804
|
platform: plan.platform,
|
|
2695
2805
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints }))
|
|
2696
2806
|
};
|
|
2697
|
-
await writeText(file,
|
|
2807
|
+
await writeText(file, stringifyYaml5(doc, { lineWidth: 120 }));
|
|
2698
2808
|
return file;
|
|
2699
2809
|
}
|
|
2700
2810
|
async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PURE__ */ new Date()) {
|
|
2701
|
-
const file =
|
|
2811
|
+
const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
|
|
2702
2812
|
if (await exists(file)) return file;
|
|
2703
2813
|
const doc = {
|
|
2704
2814
|
schema_version: 1,
|
|
@@ -2710,16 +2820,16 @@ async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PUR
|
|
|
2710
2820
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints })),
|
|
2711
2821
|
screenshots: []
|
|
2712
2822
|
};
|
|
2713
|
-
await writeText(file,
|
|
2823
|
+
await writeText(file, stringifyYaml5(doc, { lineWidth: 120 }));
|
|
2714
2824
|
return file;
|
|
2715
2825
|
}
|
|
2716
2826
|
async function updateMockupScreenshots(root, slug, screenshots) {
|
|
2717
|
-
const file =
|
|
2827
|
+
const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
|
|
2718
2828
|
const raw = await readTextIfExists(file);
|
|
2719
2829
|
if (raw === void 0) return void 0;
|
|
2720
2830
|
const data = parseYaml6(raw) ?? {};
|
|
2721
2831
|
data["screenshots"] = screenshots;
|
|
2722
|
-
await writeText(file,
|
|
2832
|
+
await writeText(file, stringifyYaml5(data, { lineWidth: 120 }));
|
|
2723
2833
|
return file;
|
|
2724
2834
|
}
|
|
2725
2835
|
async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Date()) {
|
|
@@ -2744,9 +2854,9 @@ async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Da
|
|
|
2744
2854
|
const page2 = await browser.newPage();
|
|
2745
2855
|
for (const bp of screen.breakpoints && screen.breakpoints.length > 0 ? screen.breakpoints : [1440]) {
|
|
2746
2856
|
await page2.setViewportSize({ width: bp, height: 900 });
|
|
2747
|
-
await page2.goto(`file://${
|
|
2748
|
-
const rel =
|
|
2749
|
-
await page2.screenshot({ path:
|
|
2857
|
+
await page2.goto(`file://${path9.join(dir, screen.file).split(path9.sep).join("/")}`);
|
|
2858
|
+
const rel = path9.posix.join("screens", `${screen.id}-${bp}.png`);
|
|
2859
|
+
await page2.screenshot({ path: path9.join(dir, rel) });
|
|
2750
2860
|
screenshots.push(rel);
|
|
2751
2861
|
}
|
|
2752
2862
|
await page2.close();
|
|
@@ -2762,7 +2872,7 @@ async function mockupsReady(root, slug, change) {
|
|
|
2762
2872
|
return Boolean(check.manifest && check.manifest.screens.length > 0 && !check.stale);
|
|
2763
2873
|
}
|
|
2764
2874
|
async function setMockupRequirement(root, slug, value) {
|
|
2765
|
-
const file =
|
|
2875
|
+
const file = path9.join(root, ".sdd", "changes", slug, "meta.yaml");
|
|
2766
2876
|
const raw = await readTextIfExists(file) ?? `schema_version: 1
|
|
2767
2877
|
slug: ${slug}
|
|
2768
2878
|
lane: standard
|
|
@@ -2781,7 +2891,7 @@ lane: standard
|
|
|
2781
2891
|
}
|
|
2782
2892
|
|
|
2783
2893
|
// src/packs.ts
|
|
2784
|
-
import
|
|
2894
|
+
import path10 from "path";
|
|
2785
2895
|
import { parse as parseYaml7 } from "yaml";
|
|
2786
2896
|
import { z as z5 } from "zod";
|
|
2787
2897
|
var checkSchema = z5.object({
|
|
@@ -2997,12 +3107,12 @@ ${texts.scenarios}`);
|
|
|
2997
3107
|
}
|
|
2998
3108
|
}
|
|
2999
3109
|
async function loadProjectPacks(sddDir) {
|
|
3000
|
-
const dir =
|
|
3110
|
+
const dir = path10.join(sddDir, "packs");
|
|
3001
3111
|
const diagnostics = [];
|
|
3002
3112
|
const packs = [];
|
|
3003
3113
|
for (const entry of await listDir(dir)) {
|
|
3004
3114
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
3005
|
-
const file =
|
|
3115
|
+
const file = path10.join(dir, entry);
|
|
3006
3116
|
const raw = await readTextIfExists(file);
|
|
3007
3117
|
if (raw === void 0) continue;
|
|
3008
3118
|
let data;
|
|
@@ -3091,7 +3201,7 @@ function packFindings(evaluations, change) {
|
|
|
3091
3201
|
// src/analyze.ts
|
|
3092
3202
|
var UI_DOMAINS = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
3093
3203
|
async function runAnalyze(opts) {
|
|
3094
|
-
const root =
|
|
3204
|
+
const root = path11.resolve(opts.root);
|
|
3095
3205
|
const { workspace, config } = await loadWorkspace(root);
|
|
3096
3206
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
3097
3207
|
if (!change) {
|
|
@@ -3109,7 +3219,7 @@ async function runAnalyze(opts) {
|
|
|
3109
3219
|
for (const req of spec.spec.requirements) living.set(req.id, req);
|
|
3110
3220
|
}
|
|
3111
3221
|
if (change.delta) {
|
|
3112
|
-
findings.push(...lintDelta(change.delta, living,
|
|
3222
|
+
findings.push(...lintDelta(change.delta, living, path11.join(change.dir, "spec.md"), { language: config.spec.language }));
|
|
3113
3223
|
}
|
|
3114
3224
|
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false });
|
|
3115
3225
|
findings.push(...trace.findings);
|
|
@@ -3158,7 +3268,7 @@ async function runAnalyze(opts) {
|
|
|
3158
3268
|
if (mockups) result.mockups = mockups;
|
|
3159
3269
|
if (packs) result.packs = packs;
|
|
3160
3270
|
if (opts.write !== false) {
|
|
3161
|
-
const file =
|
|
3271
|
+
const file = path11.join(change.dir, "analyze.md");
|
|
3162
3272
|
await writeText(file, renderAnalyze(change, result, localStamp(opts.now)));
|
|
3163
3273
|
result.path = file;
|
|
3164
3274
|
}
|
|
@@ -3201,9 +3311,9 @@ function renderAnalyze(change, result, generatedAt) {
|
|
|
3201
3311
|
}
|
|
3202
3312
|
|
|
3203
3313
|
// src/metrics.ts
|
|
3204
|
-
import
|
|
3314
|
+
import path12 from "path";
|
|
3205
3315
|
async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
3206
|
-
const resolved =
|
|
3316
|
+
const resolved = path12.resolve(root);
|
|
3207
3317
|
const { workspace, config } = await loadWorkspace(resolved);
|
|
3208
3318
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
3209
3319
|
const living = /* @__PURE__ */ new Map();
|
|
@@ -3218,7 +3328,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
3218
3328
|
const deltaContent = await readDelta(change);
|
|
3219
3329
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
3220
3330
|
const findings = [];
|
|
3221
|
-
if (change.delta) findings.push(...lintDelta(change.delta, living,
|
|
3331
|
+
if (change.delta) findings.push(...lintDelta(change.delta, living, path12.join(change.dir, "spec.md"), { language: config.spec.language }));
|
|
3222
3332
|
const trace = checkTrace({
|
|
3223
3333
|
specs: workspace.specs,
|
|
3224
3334
|
change,
|
|
@@ -3283,7 +3393,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
3283
3393
|
attention.push({ slug: change.slug, kind: "missing-evidence", detail: `evidencia ${change.scenariosPassed}/${change.scenariosTotal}` });
|
|
3284
3394
|
}
|
|
3285
3395
|
}
|
|
3286
|
-
const archiveDir =
|
|
3396
|
+
const archiveDir = path12.join(workspace.sddDir, "changes", "archive");
|
|
3287
3397
|
const archivedEntries = await listDirs(archiveDir);
|
|
3288
3398
|
const throughputByMonth = {};
|
|
3289
3399
|
for (const entry of archivedEntries) {
|
|
@@ -3321,15 +3431,15 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
3321
3431
|
}
|
|
3322
3432
|
async function readDelta(change) {
|
|
3323
3433
|
const { readTextIfExists: readTextIfExists2 } = await import("./fsx-VF2P7ALA.js");
|
|
3324
|
-
return readTextIfExists2(
|
|
3434
|
+
return readTextIfExists2(path12.join(change.dir, "spec.md"));
|
|
3325
3435
|
}
|
|
3326
3436
|
|
|
3327
3437
|
// src/present.ts
|
|
3328
|
-
import
|
|
3438
|
+
import path13 from "path";
|
|
3329
3439
|
import { escapeHtml, inlineMarkdown, renderMarkdown as renderRich } from "@specatlas/render";
|
|
3330
3440
|
import { defaultTokens, renderDocument, renderStyles } from "@specatlas/render";
|
|
3331
3441
|
async function generatePresentation(opts) {
|
|
3332
|
-
const root =
|
|
3442
|
+
const root = path13.resolve(opts.root);
|
|
3333
3443
|
const { workspace, config } = await loadWorkspace(root);
|
|
3334
3444
|
const language = config.project.language;
|
|
3335
3445
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
@@ -3339,17 +3449,17 @@ async function generatePresentation(opts) {
|
|
|
3339
3449
|
if (!change.delta) {
|
|
3340
3450
|
return { slug: opts.slug, diagnostics: [diag("ATLAS-PRESENT-001", "error", `El cambio "${opts.slug}" no tiene spec.md (delta)`)] };
|
|
3341
3451
|
}
|
|
3342
|
-
const deltaPath =
|
|
3452
|
+
const deltaPath = path13.join(change.dir, "spec.md");
|
|
3343
3453
|
const deltaContent = await readTextIfExists(deltaPath) ?? "";
|
|
3344
3454
|
const hash = artifactHash(deltaContent);
|
|
3345
3455
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
3346
|
-
const presentationDir =
|
|
3347
|
-
const mockupsCopyDir =
|
|
3456
|
+
const presentationDir = path13.join(change.dir, "presentation");
|
|
3457
|
+
const mockupsCopyDir = path13.join(presentationDir, "mockups");
|
|
3348
3458
|
await ensureDir(presentationDir);
|
|
3349
3459
|
const mockupInfo = await copyMockups(root, change, mockupsCopyDir);
|
|
3350
|
-
const proposalRaw = await readTextIfExists(
|
|
3460
|
+
const proposalRaw = await readTextIfExists(path13.join(change.dir, "proposal.md"));
|
|
3351
3461
|
const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
|
|
3352
|
-
const approvals = await loadApprovals(
|
|
3462
|
+
const approvals = await loadApprovals(path13.join(root, ".sdd"));
|
|
3353
3463
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
3354
3464
|
const labels = labelsFor(language);
|
|
3355
3465
|
const html = page({
|
|
@@ -3370,7 +3480,7 @@ async function generatePresentation(opts) {
|
|
|
3370
3480
|
approveHint: `satlas approve ${change.slug} --by "<nombre>" --channel presentation`,
|
|
3371
3481
|
...approval.status === "valid" && approval.approvedBy ? { approval: { by: approval.approvedBy, at: approval.approvedAt ?? "" } } : {}
|
|
3372
3482
|
});
|
|
3373
|
-
const outFile =
|
|
3483
|
+
const outFile = path13.join(presentationDir, "index.html");
|
|
3374
3484
|
await writeText(outFile, html);
|
|
3375
3485
|
return { slug: change.slug, path: outFile, hash, diagnostics: mockupInfo.diagnostics };
|
|
3376
3486
|
}
|
|
@@ -3382,12 +3492,12 @@ async function copyMockups(root, change, destDir) {
|
|
|
3382
3492
|
await ensureDir(destDir);
|
|
3383
3493
|
const screens = [];
|
|
3384
3494
|
for (const screen of manifest.screens) {
|
|
3385
|
-
const src =
|
|
3495
|
+
const src = path13.join(dir, screen.file);
|
|
3386
3496
|
if (!await exists(src)) {
|
|
3387
3497
|
diagnostics.push(diag("ATLAS-PRESENT-002", "warning", `El mockup ${screen.file} no existe y no se incluir\xE1`, { path: src }));
|
|
3388
3498
|
continue;
|
|
3389
3499
|
}
|
|
3390
|
-
await copyFile(src,
|
|
3500
|
+
await copyFile(src, path13.join(destDir, screen.file));
|
|
3391
3501
|
screens.push({
|
|
3392
3502
|
id: screen.id,
|
|
3393
3503
|
title: screen.title ?? screen.id,
|
|
@@ -3397,13 +3507,13 @@ async function copyMockups(root, change, destDir) {
|
|
|
3397
3507
|
});
|
|
3398
3508
|
}
|
|
3399
3509
|
const screenshots = [];
|
|
3400
|
-
const shotsDir =
|
|
3510
|
+
const shotsDir = path13.join(dir, "screens");
|
|
3401
3511
|
if (await exists(shotsDir)) {
|
|
3402
|
-
await ensureDir(
|
|
3512
|
+
await ensureDir(path13.join(destDir, "screens"));
|
|
3403
3513
|
const { listDir: listDir4 } = await import("./fsx-VF2P7ALA.js");
|
|
3404
3514
|
for (const file of await listDir4(shotsDir)) {
|
|
3405
3515
|
if (!/\.(png|jpe?g|webp)$/i.test(file)) continue;
|
|
3406
|
-
await copyFile(
|
|
3516
|
+
await copyFile(path13.join(shotsDir, file), path13.join(destDir, "screens", file));
|
|
3407
3517
|
screenshots.push(`mockups/screens/${file}`);
|
|
3408
3518
|
}
|
|
3409
3519
|
}
|
|
@@ -3578,7 +3688,7 @@ function renderMarkdown(markdown) {
|
|
|
3578
3688
|
}
|
|
3579
3689
|
|
|
3580
3690
|
// src/adopt.ts
|
|
3581
|
-
import
|
|
3691
|
+
import path14 from "path";
|
|
3582
3692
|
var COMMON_ROOTS = ["src", "app", "lib", "modules", "services", "packages", "api", "features", "domain", "internal", "components"];
|
|
3583
3693
|
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
3694
|
var SOURCE_EXT = /* @__PURE__ */ new Set([
|
|
@@ -3622,11 +3732,11 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([
|
|
|
3622
3732
|
".groovy"
|
|
3623
3733
|
]);
|
|
3624
3734
|
async function adoptWorkspace(opts) {
|
|
3625
|
-
const root =
|
|
3735
|
+
const root = path14.resolve(opts.root);
|
|
3626
3736
|
const dryRun = opts.dryRun ?? false;
|
|
3627
3737
|
const diagnostics = [];
|
|
3628
|
-
const sddDir =
|
|
3629
|
-
if (!await exists(
|
|
3738
|
+
const sddDir = path14.join(root, ".sdd");
|
|
3739
|
+
if (!await exists(path14.join(sddDir, "config.yaml"))) {
|
|
3630
3740
|
return {
|
|
3631
3741
|
root,
|
|
3632
3742
|
domains: [],
|
|
@@ -3637,8 +3747,8 @@ async function adoptWorkspace(opts) {
|
|
|
3637
3747
|
}
|
|
3638
3748
|
const { config } = await loadConfig(sddDir);
|
|
3639
3749
|
const files = await walkFiles(root, { skipDirs: [...SKIP_DIRS] });
|
|
3640
|
-
const inventoried = files.map((f) => toPosix(
|
|
3641
|
-
const relative = inventoried.filter((f) => SOURCE_EXT.has(
|
|
3750
|
+
const inventoried = files.map((f) => toPosix(path14.relative(root, f.path)));
|
|
3751
|
+
const relative = inventoried.filter((f) => SOURCE_EXT.has(path14.extname(f).toLowerCase()));
|
|
3642
3752
|
if (relative.length === 0) {
|
|
3643
3753
|
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
3754
|
}
|
|
@@ -3666,13 +3776,13 @@ async function adoptWorkspace(opts) {
|
|
|
3666
3776
|
const createdSpecs = [];
|
|
3667
3777
|
for (const name of domainNames.sort()) {
|
|
3668
3778
|
const domainFiles = (counts.get(name) ?? []).sort();
|
|
3669
|
-
const existingSpec = await exists(
|
|
3779
|
+
const existingSpec = await exists(path14.join(sddDir, "specs", name, "spec.md"));
|
|
3670
3780
|
domains.push({ name, files: domainFiles.length, samples: domainFiles.slice(0, 8), existingSpec });
|
|
3671
3781
|
if (existingSpec) {
|
|
3672
|
-
diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path:
|
|
3782
|
+
diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path: path14.join(sddDir, "specs", name, "spec.md") }));
|
|
3673
3783
|
continue;
|
|
3674
3784
|
}
|
|
3675
|
-
const specPath =
|
|
3785
|
+
const specPath = path14.join(sddDir, "specs", name, "spec.md");
|
|
3676
3786
|
if (!dryRun) {
|
|
3677
3787
|
await writeText(specPath, baselineSpec(name, domainFiles));
|
|
3678
3788
|
createdSpecs.push(specPath);
|
|
@@ -3680,7 +3790,7 @@ async function adoptWorkspace(opts) {
|
|
|
3680
3790
|
createdSpecs.push(specPath);
|
|
3681
3791
|
}
|
|
3682
3792
|
}
|
|
3683
|
-
const reportPath =
|
|
3793
|
+
const reportPath = path14.join(sddDir, "adopt-report.md");
|
|
3684
3794
|
if (!dryRun) {
|
|
3685
3795
|
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
3796
|
}
|
|
@@ -3700,12 +3810,12 @@ async function loadProjectProfiles(root, dirs) {
|
|
|
3700
3810
|
if (!await exists(dir)) continue;
|
|
3701
3811
|
out.push(...await loadProfilesFromDir(dir));
|
|
3702
3812
|
}
|
|
3703
|
-
const custom =
|
|
3813
|
+
const custom = path14.join(root, ".sdd", "profiles", "custom");
|
|
3704
3814
|
if (await exists(custom)) out.push(...await loadProfilesFromDir(custom));
|
|
3705
3815
|
return out;
|
|
3706
3816
|
}
|
|
3707
3817
|
async function fallbackDomainName(root, projectName) {
|
|
3708
|
-
const pkg = await readTextIfExists(
|
|
3818
|
+
const pkg = await readTextIfExists(path14.join(root, "package.json"));
|
|
3709
3819
|
if (pkg) {
|
|
3710
3820
|
try {
|
|
3711
3821
|
const data = JSON.parse(pkg);
|
|
@@ -3793,13 +3903,13 @@ function adoptReport(input) {
|
|
|
3793
3903
|
}
|
|
3794
3904
|
|
|
3795
3905
|
// src/init.ts
|
|
3796
|
-
import
|
|
3906
|
+
import path15 from "path";
|
|
3797
3907
|
async function initWorkspace(opts) {
|
|
3798
|
-
const root =
|
|
3799
|
-
const sddDir =
|
|
3908
|
+
const root = path15.resolve(opts.root);
|
|
3909
|
+
const sddDir = path15.join(root, ".sdd");
|
|
3800
3910
|
const diagnostics = [];
|
|
3801
3911
|
const created = [];
|
|
3802
|
-
if (await exists(
|
|
3912
|
+
if (await exists(path15.join(sddDir, "config.yaml"))) {
|
|
3803
3913
|
return {
|
|
3804
3914
|
sddDir,
|
|
3805
3915
|
created,
|
|
@@ -3809,29 +3919,29 @@ async function initWorkspace(opts) {
|
|
|
3809
3919
|
created.push(...await ensureSddDirs(sddDir));
|
|
3810
3920
|
const language = opts.language ?? "es";
|
|
3811
3921
|
const cfg = defaultConfig({ name: opts.name, language });
|
|
3812
|
-
cfg.project.name = opts.name ??
|
|
3922
|
+
cfg.project.name = opts.name ?? path15.basename(root);
|
|
3813
3923
|
await writeConfig(sddDir, cfg);
|
|
3814
|
-
created.push(
|
|
3924
|
+
created.push(path15.join(sddDir, "config.yaml"));
|
|
3815
3925
|
const templates = templatesFor(language);
|
|
3816
|
-
await writeText(
|
|
3817
|
-
created.push(
|
|
3818
|
-
await writeText(
|
|
3819
|
-
created.push(
|
|
3926
|
+
await writeText(path15.join(sddDir, "constitution.md"), templates.constitution);
|
|
3927
|
+
created.push(path15.join(sddDir, "constitution.md"));
|
|
3928
|
+
await writeText(path15.join(sddDir, "glossary.md"), templates.glossary);
|
|
3929
|
+
created.push(path15.join(sddDir, "glossary.md"));
|
|
3820
3930
|
const detected = await detectIfPossible(root, opts.profilesDir);
|
|
3821
3931
|
if (detected) {
|
|
3822
3932
|
const yaml = detectionToYaml(detected, localStamp(opts.now));
|
|
3823
|
-
await writeText(
|
|
3824
|
-
created.push(
|
|
3933
|
+
await writeText(path15.join(sddDir, "profiles", "detected.yaml"), yaml);
|
|
3934
|
+
created.push(path15.join(sddDir, "profiles", "detected.yaml"));
|
|
3825
3935
|
if (detected.best) {
|
|
3826
3936
|
diagnostics.push(diag("ATLAS-INIT-002", "info", `Stack detectado: ${detected.best.displayName} (${detected.best.score} puntos)`, { suggestion: "Revisa .sdd/profiles/detected.yaml" }));
|
|
3827
3937
|
} else {
|
|
3828
3938
|
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
3939
|
}
|
|
3830
3940
|
}
|
|
3831
|
-
await writeText(
|
|
3832
|
-
created.push(
|
|
3941
|
+
await writeText(path15.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
|
|
3942
|
+
created.push(path15.join(sddDir, "INDEX.md"));
|
|
3833
3943
|
if (opts.local) {
|
|
3834
|
-
const gitignore =
|
|
3944
|
+
const gitignore = path15.join(root, ".gitignore");
|
|
3835
3945
|
const current = await readTextIfExists(gitignore) ?? "";
|
|
3836
3946
|
if (!/(^|\n)\.sdd\/?(\n|$)/.test(current)) {
|
|
3837
3947
|
const next = current.endsWith("\n") || current === "" ? `${current}.sdd/
|
|
@@ -3866,13 +3976,13 @@ ${es ? "## Archivados\n\n0 cambio(s) en `changes/archive/`." : "## Archived\n\n0
|
|
|
3866
3976
|
}
|
|
3867
3977
|
|
|
3868
3978
|
// src/new.ts
|
|
3869
|
-
import
|
|
3979
|
+
import path16 from "path";
|
|
3870
3980
|
var SLUG_RE = /^[a-z0-9][a-z0-9-]{1,49}$/;
|
|
3871
3981
|
async function createChange(opts) {
|
|
3872
3982
|
const diagnostics = [];
|
|
3873
3983
|
const slug = opts.slug.trim().toLowerCase();
|
|
3874
3984
|
const language = opts.language ?? opts.cfg?.project.language ?? "es";
|
|
3875
|
-
const dir =
|
|
3985
|
+
const dir = path16.join(path16.resolve(opts.root), ".sdd", "changes", slug);
|
|
3876
3986
|
if (!SLUG_RE.test(slug)) {
|
|
3877
3987
|
return {
|
|
3878
3988
|
slug,
|
|
@@ -3903,20 +4013,20 @@ async function createChange(opts) {
|
|
|
3903
4013
|
await ensureDir(dir);
|
|
3904
4014
|
const templates = templatesFor(language);
|
|
3905
4015
|
const files = [];
|
|
3906
|
-
const metaFile =
|
|
4016
|
+
const metaFile = path16.join(dir, "meta.yaml");
|
|
3907
4017
|
await writeText(metaFile, changeMetaYaml(meta));
|
|
3908
4018
|
files.push(metaFile);
|
|
3909
4019
|
if (lane === "fix") {
|
|
3910
|
-
const fixFile =
|
|
4020
|
+
const fixFile = path16.join(dir, "fix.md");
|
|
3911
4021
|
await writeText(fixFile, renderTemplate(templates.fix, { TITLE: title }));
|
|
3912
4022
|
files.push(fixFile);
|
|
3913
4023
|
return { slug, dir, files, diagnostics };
|
|
3914
4024
|
}
|
|
3915
|
-
const proposalFile =
|
|
4025
|
+
const proposalFile = path16.join(dir, "proposal.md");
|
|
3916
4026
|
await writeText(proposalFile, renderTemplate(templates.proposal, { TITLE: title }));
|
|
3917
4027
|
files.push(proposalFile);
|
|
3918
4028
|
const domainUpper = domain.replace(/[^a-z0-9]/gi, "").toUpperCase() || "GEN";
|
|
3919
|
-
const deltaFile =
|
|
4029
|
+
const deltaFile = path16.join(dir, "spec.md");
|
|
3920
4030
|
await writeText(deltaFile, templates.specDelta({ title, domainUpper, domain }));
|
|
3921
4031
|
files.push(deltaFile);
|
|
3922
4032
|
return { slug, dir, files, diagnostics };
|
|
@@ -3925,7 +4035,7 @@ async function createChange(opts) {
|
|
|
3925
4035
|
// src/archive.ts
|
|
3926
4036
|
import { promises as fs } from "fs";
|
|
3927
4037
|
import { cp, rename, rm } from "fs/promises";
|
|
3928
|
-
import
|
|
4038
|
+
import path17 from "path";
|
|
3929
4039
|
var REQ_HEADER_RE = /^###\s+(?:Requisito|Requirement):\s+(REQ-[A-Z0-9-]+)\s*(?:—|-|–)\s*(.*)$/;
|
|
3930
4040
|
function foldDelta(livingBody, delta, language = "es") {
|
|
3931
4041
|
const diagnostics = [];
|
|
@@ -4034,7 +4144,7 @@ async function restoreFile(file, previous) {
|
|
|
4034
4144
|
}
|
|
4035
4145
|
}
|
|
4036
4146
|
async function archiveChange(opts) {
|
|
4037
|
-
const root =
|
|
4147
|
+
const root = path17.resolve(opts.root);
|
|
4038
4148
|
const diagnostics = [];
|
|
4039
4149
|
const { config } = await loadWorkspace(root);
|
|
4040
4150
|
const language = opts.language ?? config.project.language;
|
|
@@ -4050,16 +4160,43 @@ async function archiveChange(opts) {
|
|
|
4050
4160
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
4051
4161
|
}
|
|
4052
4162
|
const nowFix = opts.now ?? /* @__PURE__ */ new Date();
|
|
4053
|
-
const targetFix =
|
|
4163
|
+
const targetFix = path17.join(root, ".sdd", "changes", "archive", `${localMonth(nowFix)}-${opts.slug}`);
|
|
4054
4164
|
if (await exists(targetFix)) {
|
|
4055
4165
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${targetFix}`, { path: targetFix }));
|
|
4056
4166
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
4057
4167
|
}
|
|
4168
|
+
let livingFix;
|
|
4169
|
+
let livingCreated = false;
|
|
4170
|
+
if (!opts.dryRun) {
|
|
4171
|
+
const fixRaw = await readTextIfExists(path17.join(change.dir, "fix.md")) ?? "";
|
|
4172
|
+
try {
|
|
4173
|
+
const written = await writeLivingFix(root, {
|
|
4174
|
+
slug: opts.slug,
|
|
4175
|
+
date: localDate(nowFix),
|
|
4176
|
+
result: "pass",
|
|
4177
|
+
...change.meta.domain !== void 0 ? { domain: change.meta.domain } : {},
|
|
4178
|
+
...change.meta.title !== void 0 ? { title: change.meta.title } : {},
|
|
4179
|
+
covers: change.fixCovers ?? [],
|
|
4180
|
+
content: fixRaw
|
|
4181
|
+
});
|
|
4182
|
+
livingFix = written.relativePath;
|
|
4183
|
+
livingCreated = written.created;
|
|
4184
|
+
} catch (error) {
|
|
4185
|
+
diagnostics.push(
|
|
4186
|
+
diag("ATLAS-ARCH-006", "error", `No se pudo conservar el fix vivo: ${error.message}`, {
|
|
4187
|
+
path: path17.join(root, ".sdd", "fixes"),
|
|
4188
|
+
suggestion: "Revisa los permisos de .sdd/fixes y vuelve a intentar; el fix sigue sin archivar"
|
|
4189
|
+
})
|
|
4190
|
+
);
|
|
4191
|
+
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: false };
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4058
4194
|
if (!opts.dryRun) {
|
|
4059
|
-
await ensureDir(
|
|
4195
|
+
await ensureDir(path17.dirname(targetFix));
|
|
4060
4196
|
try {
|
|
4061
4197
|
await moveDirectory(change.dir, targetFix);
|
|
4062
4198
|
} catch (error) {
|
|
4199
|
+
if (livingCreated && livingFix) await removeFile(path17.join(root, livingFix));
|
|
4063
4200
|
diagnostics.push(
|
|
4064
4201
|
diag("ATLAS-ARCH-004", "error", `No se pudo mover el cambio al hist\xF3rico: ${error.message}`, {
|
|
4065
4202
|
path: change.dir,
|
|
@@ -4068,9 +4205,16 @@ async function archiveChange(opts) {
|
|
|
4068
4205
|
);
|
|
4069
4206
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: false };
|
|
4070
4207
|
}
|
|
4071
|
-
await regenerateIndex(root, config);
|
|
4208
|
+
await regenerateIndex(root, config, nowFix);
|
|
4072
4209
|
}
|
|
4073
|
-
return {
|
|
4210
|
+
return {
|
|
4211
|
+
slug: opts.slug,
|
|
4212
|
+
archivedTo: targetFix,
|
|
4213
|
+
...livingFix !== void 0 ? { livingFix } : {},
|
|
4214
|
+
fold: emptyFold,
|
|
4215
|
+
diagnostics,
|
|
4216
|
+
dryRun: opts.dryRun ?? false
|
|
4217
|
+
};
|
|
4074
4218
|
}
|
|
4075
4219
|
if (!change.delta) {
|
|
4076
4220
|
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 +4222,10 @@ async function archiveChange(opts) {
|
|
|
4078
4222
|
}
|
|
4079
4223
|
const domain = change.meta.domain;
|
|
4080
4224
|
if (!domain) {
|
|
4081
|
-
diagnostics.push(diag("ATLAS-ARCH-001", "error", `El cambio "${opts.slug}" no declara domain en meta.yaml`, { path:
|
|
4225
|
+
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
4226
|
return { slug: opts.slug, fold: { content: "", applied: { added: [], modified: [], removed: [], renamed: [] }, diagnostics }, diagnostics, dryRun: opts.dryRun ?? false };
|
|
4083
4227
|
}
|
|
4084
|
-
const specFile =
|
|
4228
|
+
const specFile = path17.join(root, ".sdd", "specs", domain, "spec.md");
|
|
4085
4229
|
const existing = await readTextIfExists(specFile);
|
|
4086
4230
|
const base = existing ?? `---
|
|
4087
4231
|
domain: ${domain}
|
|
@@ -4111,8 +4255,8 @@ ${Object.entries(nextFm).map(([k, v]) => `${k}: ${String(v)}`).join("\n")}
|
|
|
4111
4255
|
|
|
4112
4256
|
${body}`;
|
|
4113
4257
|
const month = localMonth(now);
|
|
4114
|
-
const archiveDir =
|
|
4115
|
-
const target =
|
|
4258
|
+
const archiveDir = path17.join(root, ".sdd", "changes", "archive");
|
|
4259
|
+
const target = path17.join(archiveDir, `${month}-${opts.slug}`);
|
|
4116
4260
|
if (await exists(target)) {
|
|
4117
4261
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${target}`, { path: target }));
|
|
4118
4262
|
return { slug: opts.slug, domain, fold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -4138,21 +4282,382 @@ ${body}`;
|
|
|
4138
4282
|
}
|
|
4139
4283
|
async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
|
|
4140
4284
|
const { workspace, config } = cfg ? { workspace: (await loadWorkspace(root)).workspace, config: cfg } : await loadWorkspace(root);
|
|
4141
|
-
const archiveDir =
|
|
4285
|
+
const archiveDir = path17.join(root, ".sdd", "changes", "archive");
|
|
4142
4286
|
const archived = await exists(archiveDir) ? (await fs.readdir(archiveDir)).filter((e) => !e.startsWith(".")).length : 0;
|
|
4287
|
+
const fixes = await loadLivingFixes(root);
|
|
4143
4288
|
const markdown = indexMarkdown({
|
|
4144
4289
|
projectName: config.project.name,
|
|
4145
4290
|
language: config.project.language,
|
|
4146
4291
|
specs: workspace.specs.map((s) => ({ domain: s.domain, requirements: s.spec.requirements.length })),
|
|
4147
4292
|
changes: workspace.changes.map((c) => ({ slug: c.slug, lane: c.meta?.lane ?? config.lanes.default })),
|
|
4293
|
+
fixes: fixes.map((fix) => ({ slug: fix.slug, date: fix.date, result: fix.result, ...fix.domain !== void 0 ? { domain: fix.domain } : {} })),
|
|
4148
4294
|
archived
|
|
4149
4295
|
});
|
|
4150
4296
|
void now;
|
|
4151
|
-
await writeText(
|
|
4297
|
+
await writeText(path17.join(root, ".sdd", "INDEX.md"), markdown);
|
|
4298
|
+
}
|
|
4299
|
+
async function removeFile(file) {
|
|
4300
|
+
try {
|
|
4301
|
+
await rm(file, { force: true });
|
|
4302
|
+
} catch {
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
// src/migrations.ts
|
|
4307
|
+
import { promises as fs2 } from "fs";
|
|
4308
|
+
import path18 from "path";
|
|
4309
|
+
import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
|
|
4310
|
+
var SCHEMA_VERSION = 1;
|
|
4311
|
+
var BACKUP_DIRNAME = ".backup";
|
|
4312
|
+
var BACKUP_POINTER = ".latest";
|
|
4313
|
+
var SCHEMA_MIGRATIONS = [
|
|
4314
|
+
{
|
|
4315
|
+
id: "0001-sellar-version-de-esquema",
|
|
4316
|
+
description: "Sella la versi\xF3n de esquema vigente en los elementos que no la declaran",
|
|
4317
|
+
from: 0,
|
|
4318
|
+
to: SCHEMA_VERSION
|
|
4319
|
+
}
|
|
4320
|
+
];
|
|
4321
|
+
var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml", path18.join("profiles", "detected.yaml")];
|
|
4322
|
+
var CHANGE_ARTIFACTS = ["meta.yaml", path18.join("mockups", "manifest.yaml")];
|
|
4323
|
+
async function pushChangeArtifacts(out, sddDir, relDir) {
|
|
4324
|
+
for (const rel of CHANGE_ARTIFACTS) {
|
|
4325
|
+
const abs = path18.join(sddDir, relDir, rel);
|
|
4326
|
+
if (await exists(abs)) out.push({ artifact: toPosix(path18.join(relDir, rel)), path: abs });
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
async function collectVersionedArtifacts(root) {
|
|
4330
|
+
const sddDir = path18.join(root, ".sdd");
|
|
4331
|
+
const out = [];
|
|
4332
|
+
for (const rel of FIXED_ARTIFACTS) {
|
|
4333
|
+
const abs = path18.join(sddDir, rel);
|
|
4334
|
+
if (await exists(abs)) out.push({ artifact: toPosix(rel), path: abs });
|
|
4335
|
+
}
|
|
4336
|
+
const changesDir = path18.join(sddDir, "changes");
|
|
4337
|
+
for (const slug of await listDirs(changesDir)) {
|
|
4338
|
+
if (slug === "archive") continue;
|
|
4339
|
+
await pushChangeArtifacts(out, sddDir, path18.join("changes", slug));
|
|
4340
|
+
}
|
|
4341
|
+
for (const entry of await listDirs(path18.join(changesDir, "archive"))) {
|
|
4342
|
+
await pushChangeArtifacts(out, sddDir, path18.join("changes", "archive", entry));
|
|
4343
|
+
}
|
|
4344
|
+
return out;
|
|
4345
|
+
}
|
|
4346
|
+
function readArtifactVersion(content) {
|
|
4347
|
+
let data;
|
|
4348
|
+
try {
|
|
4349
|
+
data = parseYaml8(content);
|
|
4350
|
+
} catch (err) {
|
|
4351
|
+
return { state: "unreadable", reason: `YAML inv\xE1lido (${err.message})` };
|
|
4352
|
+
}
|
|
4353
|
+
if (data === null || data === void 0) return { state: "absent" };
|
|
4354
|
+
if (typeof data !== "object" || Array.isArray(data)) return { state: "unreadable", reason: "la ra\xEDz no es un mapa de claves y valores" };
|
|
4355
|
+
const raw = data["schema_version"];
|
|
4356
|
+
if (raw === void 0) return { state: "absent" };
|
|
4357
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) return { state: "unreadable", reason: `schema_version con valor no v\xE1lido: ${String(raw)}` };
|
|
4358
|
+
return { state: "ok", version: raw };
|
|
4359
|
+
}
|
|
4360
|
+
function stampSchemaVersion(content, version) {
|
|
4361
|
+
const line = `schema_version: ${version}`;
|
|
4362
|
+
const bom = content.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
4363
|
+
const body = bom ? content.slice(1) : content;
|
|
4364
|
+
if (body.trim() === "") return `${bom}${line}
|
|
4365
|
+
`;
|
|
4366
|
+
const eol = body.includes("\r\n") ? "\r\n" : "\n";
|
|
4367
|
+
const lines = body.split(/\r?\n/);
|
|
4368
|
+
const trailing = lines.length > 1 && lines[lines.length - 1] === "";
|
|
4369
|
+
let index = 0;
|
|
4370
|
+
while (index < lines.length) {
|
|
4371
|
+
const trimmed = (lines[index] ?? "").trim();
|
|
4372
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
4373
|
+
index += 1;
|
|
4374
|
+
continue;
|
|
4375
|
+
}
|
|
4376
|
+
break;
|
|
4377
|
+
}
|
|
4378
|
+
if ((lines[index] ?? "").trim() === "---") index += 1;
|
|
4379
|
+
const insertAt = trailing ? Math.min(index, lines.length - 1) : index;
|
|
4380
|
+
lines.splice(insertAt, 0, line);
|
|
4381
|
+
const next = bom + lines.join(eol);
|
|
4382
|
+
const check = readArtifactVersion(next);
|
|
4383
|
+
if (check.state !== "ok" || check.version !== version) return void 0;
|
|
4384
|
+
return next;
|
|
4385
|
+
}
|
|
4386
|
+
async function planUpgrade(root) {
|
|
4387
|
+
const artifacts = await collectVersionedArtifacts(root);
|
|
4388
|
+
const pending = [];
|
|
4389
|
+
const newer = [];
|
|
4390
|
+
const unreadable = [];
|
|
4391
|
+
for (const found of artifacts) {
|
|
4392
|
+
const content = await readTextIfExists(found.path);
|
|
4393
|
+
if (content === void 0) continue;
|
|
4394
|
+
const state = readArtifactVersion(content);
|
|
4395
|
+
if (state.state === "unreadable") {
|
|
4396
|
+
unreadable.push({ ...found, reason: state.reason });
|
|
4397
|
+
continue;
|
|
4398
|
+
}
|
|
4399
|
+
if (state.state === "ok") {
|
|
4400
|
+
if (state.version > SCHEMA_VERSION) {
|
|
4401
|
+
newer.push({ ...found, reason: `produce la versi\xF3n ${state.version}, m\xE1s nueva que la vigente (${SCHEMA_VERSION})` });
|
|
4402
|
+
continue;
|
|
4403
|
+
}
|
|
4404
|
+
if (state.version === SCHEMA_VERSION) continue;
|
|
4405
|
+
}
|
|
4406
|
+
const from = state.state === "ok" ? state.version : 0;
|
|
4407
|
+
const migration = SCHEMA_MIGRATIONS.find((m) => m.from === from);
|
|
4408
|
+
if (!migration) {
|
|
4409
|
+
unreadable.push({ ...found, reason: `no hay migraci\xF3n registrada desde la versi\xF3n ${from}` });
|
|
4410
|
+
continue;
|
|
4411
|
+
}
|
|
4412
|
+
const stamped = stampSchemaVersion(content, migration.to);
|
|
4413
|
+
if (stamped === void 0) {
|
|
4414
|
+
unreadable.push({ ...found, reason: "no se pudo sellar con seguridad sin alterar el resto del contenido" });
|
|
4415
|
+
continue;
|
|
4416
|
+
}
|
|
4417
|
+
pending.push({ ...found, from, to: migration.to, migration: migration.id, summary: migration.description });
|
|
4418
|
+
}
|
|
4419
|
+
return { root, currentVersion: SCHEMA_VERSION, pending, newer, unreadable, upToDate: pending.length === 0 };
|
|
4420
|
+
}
|
|
4421
|
+
async function removeDir(dir) {
|
|
4422
|
+
try {
|
|
4423
|
+
await fs2.rm(dir, { recursive: true, force: true });
|
|
4424
|
+
} catch {
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
async function removeFile2(file) {
|
|
4428
|
+
try {
|
|
4429
|
+
await fs2.rm(file, { force: true });
|
|
4430
|
+
} catch {
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
function backupName(now) {
|
|
4434
|
+
return `${localCompact(now)}${localOffset(now).replace(":", "")}`;
|
|
4435
|
+
}
|
|
4436
|
+
async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
4437
|
+
const plan = await planUpgrade(root);
|
|
4438
|
+
if (plan.pending.length === 0) return { status: "up-to-date", plan, applied: [] };
|
|
4439
|
+
const sddDir = path18.join(root, ".sdd");
|
|
4440
|
+
const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
|
|
4441
|
+
const name = backupName(now);
|
|
4442
|
+
const backupDir = path18.join(backupRoot, name);
|
|
4443
|
+
const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
|
|
4444
|
+
const previous = [];
|
|
4445
|
+
try {
|
|
4446
|
+
for (const item of plan.pending) {
|
|
4447
|
+
const content = await readTextIfExists(item.path);
|
|
4448
|
+
if (content === void 0) throw new Error("el elemento desapareci\xF3 mientras se respaldaba");
|
|
4449
|
+
previous.push({ item, contents: content });
|
|
4450
|
+
await writeText(path18.join(backupDir, "files", ...item.artifact.split("/")), content);
|
|
4451
|
+
}
|
|
4452
|
+
await writeText(
|
|
4453
|
+
path18.join(backupDir, "backup.yaml"),
|
|
4454
|
+
stringifyYaml6(
|
|
4455
|
+
{
|
|
4456
|
+
schema_version: SCHEMA_VERSION,
|
|
4457
|
+
created_at: localStamp(now),
|
|
4458
|
+
to_version: SCHEMA_VERSION,
|
|
4459
|
+
files: plan.pending.map((i) => ({ artifact: i.artifact, from: i.from, to: i.to, migration: i.migration }))
|
|
4460
|
+
},
|
|
4461
|
+
{ lineWidth: 120 }
|
|
4462
|
+
)
|
|
4463
|
+
);
|
|
4464
|
+
await writeText(pointerFile, `${name}
|
|
4465
|
+
`);
|
|
4466
|
+
} catch (err) {
|
|
4467
|
+
await removeDir(backupDir);
|
|
4468
|
+
await removeFile2(pointerFile);
|
|
4469
|
+
return { status: "failed", plan, applied: [], failure: { artifact: "(respaldo)", message: err.message } };
|
|
4470
|
+
}
|
|
4471
|
+
const written = [];
|
|
4472
|
+
for (const { item, contents } of previous) {
|
|
4473
|
+
try {
|
|
4474
|
+
const next = stampSchemaVersion(contents, item.to);
|
|
4475
|
+
if (next === void 0) throw new Error("no se pudo sellar con seguridad");
|
|
4476
|
+
await writeText(item.path, next);
|
|
4477
|
+
written.push(item);
|
|
4478
|
+
} catch (err) {
|
|
4479
|
+
for (const w of written) {
|
|
4480
|
+
const prev = previous.find((p) => p.item.artifact === w.artifact);
|
|
4481
|
+
if (prev) {
|
|
4482
|
+
try {
|
|
4483
|
+
await writeText(prev.item.path, prev.contents);
|
|
4484
|
+
} catch {
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
await removeFile2(pointerFile);
|
|
4489
|
+
await removeDir(backupDir);
|
|
4490
|
+
return { status: "failed", plan, applied: [], failure: { artifact: item.artifact, message: err.message } };
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
for (const dir of await listDirs(backupRoot)) {
|
|
4494
|
+
if (dir !== name) await removeDir(path18.join(backupRoot, dir));
|
|
4495
|
+
}
|
|
4496
|
+
return {
|
|
4497
|
+
status: "applied",
|
|
4498
|
+
plan,
|
|
4499
|
+
applied: written,
|
|
4500
|
+
backup: {
|
|
4501
|
+
name,
|
|
4502
|
+
dir: backupDir,
|
|
4503
|
+
relativeDir: toPosix(path18.relative(root, backupDir)),
|
|
4504
|
+
createdAt: localStamp(now),
|
|
4505
|
+
files: plan.pending.map((i) => i.artifact)
|
|
4506
|
+
}
|
|
4507
|
+
};
|
|
4508
|
+
}
|
|
4509
|
+
async function rollbackUpgrade(root) {
|
|
4510
|
+
const sddDir = path18.join(root, ".sdd");
|
|
4511
|
+
const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
|
|
4512
|
+
const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
|
|
4513
|
+
const name = (await readTextIfExists(pointerFile))?.trim();
|
|
4514
|
+
if (!name) return { status: "no-backup", restored: [] };
|
|
4515
|
+
const backupDir = path18.join(backupRoot, name);
|
|
4516
|
+
const manifestRaw = await readTextIfExists(path18.join(backupDir, "backup.yaml"));
|
|
4517
|
+
if (manifestRaw === void 0) return { status: "no-backup", restored: [] };
|
|
4518
|
+
let files = [];
|
|
4519
|
+
try {
|
|
4520
|
+
const parsed = parseYaml8(manifestRaw);
|
|
4521
|
+
files = (parsed?.files ?? []).filter((f) => typeof f?.artifact === "string");
|
|
4522
|
+
} catch {
|
|
4523
|
+
return { status: "no-backup", restored: [] };
|
|
4524
|
+
}
|
|
4525
|
+
const restored = [];
|
|
4526
|
+
for (const file of files) {
|
|
4527
|
+
const from = path18.join(backupDir, "files", ...file.artifact.split("/"));
|
|
4528
|
+
const to = path18.join(sddDir, ...file.artifact.split("/"));
|
|
4529
|
+
const content = await readTextIfExists(from);
|
|
4530
|
+
if (content === void 0) continue;
|
|
4531
|
+
try {
|
|
4532
|
+
await writeText(to, content);
|
|
4533
|
+
restored.push(file.artifact);
|
|
4534
|
+
} catch (err) {
|
|
4535
|
+
return { status: "failed", restored, backup: name, failure: { artifact: file.artifact, message: err.message } };
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
await removeDir(backupDir);
|
|
4539
|
+
await removeFile2(pointerFile);
|
|
4540
|
+
return { status: "restored", restored, backup: name };
|
|
4541
|
+
}
|
|
4542
|
+
async function upgradeAdvisory(root) {
|
|
4543
|
+
const plan = await planUpgrade(root);
|
|
4544
|
+
const diagnostics = [];
|
|
4545
|
+
if (plan.pending.length > 0) {
|
|
4546
|
+
diagnostics.push(
|
|
4547
|
+
diag("ATLAS-UPGRADE-001", "warning", `El estado del proyecto no corresponde a la versi\xF3n vigente: ${plan.pending.length} elemento(s) por actualizar`, {
|
|
4548
|
+
suggestion: "Vista previa: `satlas upgrade` \xB7 Aplicar: `satlas upgrade --apply`"
|
|
4549
|
+
})
|
|
4550
|
+
);
|
|
4551
|
+
}
|
|
4552
|
+
for (const item of plan.newer) {
|
|
4553
|
+
diagnostics.push(
|
|
4554
|
+
diag("ATLAS-UPGRADE-002", "warning", `El proyecto fue producido por una versi\xF3n m\xE1s nueva: "${item.artifact}" (${item.reason})`, {
|
|
4555
|
+
path: item.path,
|
|
4556
|
+
suggestion: "Actualiza la herramienta; no se actualiza hacia atr\xE1s"
|
|
4557
|
+
})
|
|
4558
|
+
);
|
|
4559
|
+
}
|
|
4560
|
+
for (const item of plan.unreadable) {
|
|
4561
|
+
diagnostics.push(
|
|
4562
|
+
diag("ATLAS-UPGRADE-003", "warning", `No se pudo interpretar "${item.artifact}": ${item.reason}`, {
|
|
4563
|
+
path: item.path,
|
|
4564
|
+
suggestion: "Revisa el elemento; no se modificar\xE1"
|
|
4565
|
+
})
|
|
4566
|
+
);
|
|
4567
|
+
}
|
|
4568
|
+
return { plan, diagnostics };
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
// src/sarif.ts
|
|
4572
|
+
import path19 from "path";
|
|
4573
|
+
var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
4574
|
+
var SARIF_VERSION = "2.1.0";
|
|
4575
|
+
var TOOL_NAME = "SpecAtlas";
|
|
4576
|
+
var TOOL_URL = "https://github.com/AlonsoAM/specatlas";
|
|
4577
|
+
var RULE_FAMILIES = [
|
|
4578
|
+
[/^LINT-STR-/, "Estructura de los artefactos"],
|
|
4579
|
+
[/^LINT-BIZ-/, "Lenguaje de negocio de la especificaci\xF3n"],
|
|
4580
|
+
[/^LINT-DLT-/, "Delta de especificaci\xF3n"],
|
|
4581
|
+
[/^LINT-EVD-/, "Evidencia registrada"],
|
|
4582
|
+
[/^LINT-TSK-/, "Tareas declaradas"],
|
|
4583
|
+
[/^LINT-PLN-/, "Plan t\xE9cnico"],
|
|
4584
|
+
[/^(LINT-MKP|MKP)-/, "Mockups"],
|
|
4585
|
+
[/^TRACE-/, "Trazabilidad requisito, escenario, tarea y evidencia"],
|
|
4586
|
+
[/^PACK-/, "Pack de cumplimiento"],
|
|
4587
|
+
[/^ATLAS-UPGRADE-/, "Actualizaci\xF3n del estado del proyecto"],
|
|
4588
|
+
[/^ATLAS-ADAPTERS-/, "Adaptadores de agente"],
|
|
4589
|
+
[/^ATLAS-CI-/, "Comprobaci\xF3n continua"],
|
|
4590
|
+
[/^ATLAS-/, "Estado del proyecto"]
|
|
4591
|
+
];
|
|
4592
|
+
function ruleDescription(code) {
|
|
4593
|
+
for (const [pattern, description] of RULE_FAMILIES) {
|
|
4594
|
+
if (pattern.test(code)) return description;
|
|
4595
|
+
}
|
|
4596
|
+
return code;
|
|
4597
|
+
}
|
|
4598
|
+
function sarifLevel(severity) {
|
|
4599
|
+
if (severity === "error") return "error";
|
|
4600
|
+
if (severity === "warning") return "warning";
|
|
4601
|
+
return "note";
|
|
4602
|
+
}
|
|
4603
|
+
function resultOf(diagnostic, root) {
|
|
4604
|
+
const result = {
|
|
4605
|
+
ruleId: diagnostic.code,
|
|
4606
|
+
level: sarifLevel(diagnostic.severity),
|
|
4607
|
+
message: { text: diagnostic.message }
|
|
4608
|
+
};
|
|
4609
|
+
if (diagnostic.path) {
|
|
4610
|
+
const rel = path19.relative(root, diagnostic.path);
|
|
4611
|
+
if (rel !== "") {
|
|
4612
|
+
const physicalLocation = {
|
|
4613
|
+
artifactLocation: { uri: toPosix(rel) },
|
|
4614
|
+
...diagnostic.line ? { region: { startLine: diagnostic.line } } : {}
|
|
4615
|
+
};
|
|
4616
|
+
result.locations = [{ physicalLocation }];
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
return result;
|
|
4620
|
+
}
|
|
4621
|
+
function toSarifReport(opts) {
|
|
4622
|
+
const rules = /* @__PURE__ */ new Map();
|
|
4623
|
+
const results = [];
|
|
4624
|
+
for (const diagnostic of opts.diagnostics) {
|
|
4625
|
+
let rule = rules.get(diagnostic.code);
|
|
4626
|
+
if (!rule) {
|
|
4627
|
+
rule = { id: diagnostic.code, shortDescription: { text: ruleDescription(diagnostic.code) } };
|
|
4628
|
+
if (diagnostic.suggestion) rule.help = { text: diagnostic.suggestion };
|
|
4629
|
+
rules.set(diagnostic.code, rule);
|
|
4630
|
+
} else if (!rule.help && diagnostic.suggestion) {
|
|
4631
|
+
rule.help = { text: diagnostic.suggestion };
|
|
4632
|
+
}
|
|
4633
|
+
results.push(resultOf(diagnostic, opts.root));
|
|
4634
|
+
}
|
|
4635
|
+
return {
|
|
4636
|
+
$schema: SARIF_SCHEMA,
|
|
4637
|
+
version: SARIF_VERSION,
|
|
4638
|
+
runs: [
|
|
4639
|
+
{
|
|
4640
|
+
tool: {
|
|
4641
|
+
driver: {
|
|
4642
|
+
name: TOOL_NAME,
|
|
4643
|
+
informationUri: TOOL_URL,
|
|
4644
|
+
version: opts.version,
|
|
4645
|
+
rules: [...rules.values()]
|
|
4646
|
+
}
|
|
4647
|
+
},
|
|
4648
|
+
results,
|
|
4649
|
+
invocations: [{ executionSuccessful: !(opts.failed ?? false) }]
|
|
4650
|
+
}
|
|
4651
|
+
]
|
|
4652
|
+
};
|
|
4653
|
+
}
|
|
4654
|
+
function toSarifText(opts) {
|
|
4655
|
+
return `${JSON.stringify(toSarifReport(opts), null, 2)}
|
|
4656
|
+
`;
|
|
4152
4657
|
}
|
|
4153
4658
|
|
|
4154
4659
|
// src/doctor.ts
|
|
4155
|
-
import
|
|
4660
|
+
import path20 from "path";
|
|
4156
4661
|
async function runDoctor(root) {
|
|
4157
4662
|
const findings = [];
|
|
4158
4663
|
const { workspace, config } = await loadWorkspace(root);
|
|
@@ -4160,7 +4665,7 @@ async function runDoctor(root) {
|
|
|
4160
4665
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
4161
4666
|
findings.push(...approvals.diagnostics);
|
|
4162
4667
|
for (const change of workspace.changes) {
|
|
4163
|
-
const deltaPath =
|
|
4668
|
+
const deltaPath = path20.join(change.dir, "spec.md");
|
|
4164
4669
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
4165
4670
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
|
|
4166
4671
|
if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
|
|
@@ -4182,7 +4687,7 @@ async function runDoctor(root) {
|
|
|
4182
4687
|
}
|
|
4183
4688
|
for (const override of change.meta?.overrides ?? []) {
|
|
4184
4689
|
if (!override.reason.trim() || !override.by.trim()) {
|
|
4185
|
-
findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path:
|
|
4690
|
+
findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path20.join(change.dir, "meta.yaml") }));
|
|
4186
4691
|
}
|
|
4187
4692
|
}
|
|
4188
4693
|
}
|
|
@@ -4205,7 +4710,7 @@ async function specHashOf(filePath) {
|
|
|
4205
4710
|
}
|
|
4206
4711
|
|
|
4207
4712
|
// src/gate.ts
|
|
4208
|
-
import
|
|
4713
|
+
import path21 from "path";
|
|
4209
4714
|
var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
4210
4715
|
function count(name, diagnostics) {
|
|
4211
4716
|
return {
|
|
@@ -4222,7 +4727,7 @@ function livingRequirementsMap(specs) {
|
|
|
4222
4727
|
return map;
|
|
4223
4728
|
}
|
|
4224
4729
|
async function runCiGate(opts) {
|
|
4225
|
-
const root =
|
|
4730
|
+
const root = path21.resolve(opts.root);
|
|
4226
4731
|
const { workspace, config } = await loadWorkspace(root);
|
|
4227
4732
|
const diagnostics = [];
|
|
4228
4733
|
const checks = [];
|
|
@@ -4233,7 +4738,7 @@ async function runCiGate(opts) {
|
|
|
4233
4738
|
let changesErrors = 0;
|
|
4234
4739
|
let changesWarnings = 0;
|
|
4235
4740
|
for (const change of workspace.changes) {
|
|
4236
|
-
const lintFindings = change.delta ? lintDelta(change.delta, living,
|
|
4741
|
+
const lintFindings = change.delta ? lintDelta(change.delta, living, path21.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
|
|
4237
4742
|
const trace = checkTrace({
|
|
4238
4743
|
specs: workspace.specs,
|
|
4239
4744
|
change,
|
|
@@ -4277,23 +4782,33 @@ async function runCiGate(opts) {
|
|
|
4277
4782
|
return { checks, diagnostics, errors, warnings, failed };
|
|
4278
4783
|
}
|
|
4279
4784
|
export {
|
|
4785
|
+
BACKUP_DIRNAME,
|
|
4786
|
+
BACKUP_POINTER,
|
|
4280
4787
|
BLOCK_HEAD_RE,
|
|
4281
4788
|
BUILTIN_PACKS,
|
|
4282
4789
|
CONFIG_FILE,
|
|
4283
4790
|
CORE_VERSION,
|
|
4791
|
+
LIVING_FIXES_DIR,
|
|
4284
4792
|
REQ_HEAD_RE,
|
|
4285
4793
|
REQ_ID_RE,
|
|
4286
4794
|
RULE_ID_RE,
|
|
4287
4795
|
RULE_RE,
|
|
4288
4796
|
RUN_EVENT_TYPES,
|
|
4797
|
+
SARIF_SCHEMA,
|
|
4798
|
+
SARIF_VERSION,
|
|
4289
4799
|
SCENARIO_HEAD_RE,
|
|
4290
4800
|
SCENARIO_ID_RE,
|
|
4801
|
+
SCHEMA_MIGRATIONS,
|
|
4802
|
+
SCHEMA_VERSION,
|
|
4291
4803
|
SDD_DIR,
|
|
4292
4804
|
SLUG_RE,
|
|
4293
4805
|
TASK_ID_RE,
|
|
4294
4806
|
TASK_LINE_RE,
|
|
4807
|
+
TOOL_NAME,
|
|
4808
|
+
TOOL_URL,
|
|
4295
4809
|
adoptWorkspace,
|
|
4296
4810
|
appendRunEvent,
|
|
4811
|
+
applyUpgrade,
|
|
4297
4812
|
approvalsSchema,
|
|
4298
4813
|
approveFromGithub,
|
|
4299
4814
|
archiveChange,
|
|
@@ -4308,6 +4823,7 @@ export {
|
|
|
4308
4823
|
checkMockups,
|
|
4309
4824
|
checkTrace,
|
|
4310
4825
|
collectMetrics,
|
|
4826
|
+
collectVersionedArtifacts,
|
|
4311
4827
|
commentIssue,
|
|
4312
4828
|
compareTaskIds,
|
|
4313
4829
|
computeInputsHash,
|
|
@@ -4372,6 +4888,7 @@ export {
|
|
|
4372
4888
|
loadChange,
|
|
4373
4889
|
loadConfig,
|
|
4374
4890
|
loadDetectedBest,
|
|
4891
|
+
loadLivingFixes,
|
|
4375
4892
|
loadProfileFile,
|
|
4376
4893
|
loadProfilesFromDir,
|
|
4377
4894
|
loadProjectPacks,
|
|
@@ -4392,10 +4909,12 @@ export {
|
|
|
4392
4909
|
parseChangeMeta,
|
|
4393
4910
|
parseConfig,
|
|
4394
4911
|
parseDelta,
|
|
4912
|
+
parseFixCovers,
|
|
4395
4913
|
parseFrontmatter,
|
|
4396
4914
|
parseGitHubRemote,
|
|
4397
4915
|
parseGlossary,
|
|
4398
4916
|
parseIssueUrl,
|
|
4917
|
+
parseLivingFix,
|
|
4399
4918
|
parseRequirementBlocks,
|
|
4400
4919
|
parseSpecFile,
|
|
4401
4920
|
parseTasksFile,
|
|
@@ -4403,8 +4922,10 @@ export {
|
|
|
4403
4922
|
patchChangeMeta,
|
|
4404
4923
|
planBlock,
|
|
4405
4924
|
planMockups,
|
|
4925
|
+
planUpgrade,
|
|
4406
4926
|
planWaves,
|
|
4407
4927
|
profileSchema,
|
|
4928
|
+
readArtifactVersion,
|
|
4408
4929
|
readMockupManifest,
|
|
4409
4930
|
readRun,
|
|
4410
4931
|
readText,
|
|
@@ -4418,26 +4939,34 @@ export {
|
|
|
4418
4939
|
renderTemplate,
|
|
4419
4940
|
requiresMockups,
|
|
4420
4941
|
resolvePacks,
|
|
4942
|
+
rollbackUpgrade,
|
|
4943
|
+
ruleDescription,
|
|
4421
4944
|
runAnalyze,
|
|
4422
4945
|
runCiGate,
|
|
4423
4946
|
runDoctor,
|
|
4424
4947
|
runProcess,
|
|
4948
|
+
sarifLevel,
|
|
4425
4949
|
setMockupRequirement,
|
|
4426
4950
|
sha256,
|
|
4427
4951
|
shortHash,
|
|
4428
4952
|
signApproval,
|
|
4429
4953
|
specHashOf,
|
|
4430
4954
|
splitCommand,
|
|
4955
|
+
stampSchemaVersion,
|
|
4431
4956
|
stateLabel,
|
|
4432
4957
|
syncGithubIssue,
|
|
4433
4958
|
templatesFor,
|
|
4434
4959
|
toPosix,
|
|
4960
|
+
toSarifReport,
|
|
4961
|
+
toSarifText,
|
|
4435
4962
|
tokensFile,
|
|
4436
4963
|
updateMockupScreenshots,
|
|
4437
4964
|
updateRunStatus,
|
|
4965
|
+
upgradeAdvisory,
|
|
4438
4966
|
verifyApproval,
|
|
4439
4967
|
walkFiles,
|
|
4440
4968
|
writeConfig,
|
|
4969
|
+
writeLivingFix,
|
|
4441
4970
|
writeMockupManifest,
|
|
4442
4971
|
writeMockupPlan,
|
|
4443
4972
|
writeText
|