@specatlas/core 0.1.24 → 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 +39 -1
- package/dist/index.js +350 -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,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,23 +4282,31 @@ ${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
|
+
}
|
|
4152
4304
|
}
|
|
4153
4305
|
|
|
4154
4306
|
// src/migrations.ts
|
|
4155
4307
|
import { promises as fs2 } from "fs";
|
|
4156
|
-
import
|
|
4157
|
-
import { parse as parseYaml8, stringify as
|
|
4308
|
+
import path18 from "path";
|
|
4309
|
+
import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
|
|
4158
4310
|
var SCHEMA_VERSION = 1;
|
|
4159
4311
|
var BACKUP_DIRNAME = ".backup";
|
|
4160
4312
|
var BACKUP_POINTER = ".latest";
|
|
@@ -4166,28 +4318,28 @@ var SCHEMA_MIGRATIONS = [
|
|
|
4166
4318
|
to: SCHEMA_VERSION
|
|
4167
4319
|
}
|
|
4168
4320
|
];
|
|
4169
|
-
var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml",
|
|
4170
|
-
var CHANGE_ARTIFACTS = ["meta.yaml",
|
|
4321
|
+
var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml", path18.join("profiles", "detected.yaml")];
|
|
4322
|
+
var CHANGE_ARTIFACTS = ["meta.yaml", path18.join("mockups", "manifest.yaml")];
|
|
4171
4323
|
async function pushChangeArtifacts(out, sddDir, relDir) {
|
|
4172
4324
|
for (const rel of CHANGE_ARTIFACTS) {
|
|
4173
|
-
const abs =
|
|
4174
|
-
if (await exists(abs)) out.push({ artifact: toPosix(
|
|
4325
|
+
const abs = path18.join(sddDir, relDir, rel);
|
|
4326
|
+
if (await exists(abs)) out.push({ artifact: toPosix(path18.join(relDir, rel)), path: abs });
|
|
4175
4327
|
}
|
|
4176
4328
|
}
|
|
4177
4329
|
async function collectVersionedArtifacts(root) {
|
|
4178
|
-
const sddDir =
|
|
4330
|
+
const sddDir = path18.join(root, ".sdd");
|
|
4179
4331
|
const out = [];
|
|
4180
4332
|
for (const rel of FIXED_ARTIFACTS) {
|
|
4181
|
-
const abs =
|
|
4333
|
+
const abs = path18.join(sddDir, rel);
|
|
4182
4334
|
if (await exists(abs)) out.push({ artifact: toPosix(rel), path: abs });
|
|
4183
4335
|
}
|
|
4184
|
-
const changesDir =
|
|
4336
|
+
const changesDir = path18.join(sddDir, "changes");
|
|
4185
4337
|
for (const slug of await listDirs(changesDir)) {
|
|
4186
4338
|
if (slug === "archive") continue;
|
|
4187
|
-
await pushChangeArtifacts(out, sddDir,
|
|
4339
|
+
await pushChangeArtifacts(out, sddDir, path18.join("changes", slug));
|
|
4188
4340
|
}
|
|
4189
|
-
for (const entry of await listDirs(
|
|
4190
|
-
await pushChangeArtifacts(out, sddDir,
|
|
4341
|
+
for (const entry of await listDirs(path18.join(changesDir, "archive"))) {
|
|
4342
|
+
await pushChangeArtifacts(out, sddDir, path18.join("changes", "archive", entry));
|
|
4191
4343
|
}
|
|
4192
4344
|
return out;
|
|
4193
4345
|
}
|
|
@@ -4272,7 +4424,7 @@ async function removeDir(dir) {
|
|
|
4272
4424
|
} catch {
|
|
4273
4425
|
}
|
|
4274
4426
|
}
|
|
4275
|
-
async function
|
|
4427
|
+
async function removeFile2(file) {
|
|
4276
4428
|
try {
|
|
4277
4429
|
await fs2.rm(file, { force: true });
|
|
4278
4430
|
} catch {
|
|
@@ -4284,22 +4436,22 @@ function backupName(now) {
|
|
|
4284
4436
|
async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
4285
4437
|
const plan = await planUpgrade(root);
|
|
4286
4438
|
if (plan.pending.length === 0) return { status: "up-to-date", plan, applied: [] };
|
|
4287
|
-
const sddDir =
|
|
4288
|
-
const backupRoot =
|
|
4439
|
+
const sddDir = path18.join(root, ".sdd");
|
|
4440
|
+
const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
|
|
4289
4441
|
const name = backupName(now);
|
|
4290
|
-
const backupDir =
|
|
4291
|
-
const pointerFile =
|
|
4442
|
+
const backupDir = path18.join(backupRoot, name);
|
|
4443
|
+
const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
|
|
4292
4444
|
const previous = [];
|
|
4293
4445
|
try {
|
|
4294
4446
|
for (const item of plan.pending) {
|
|
4295
4447
|
const content = await readTextIfExists(item.path);
|
|
4296
4448
|
if (content === void 0) throw new Error("el elemento desapareci\xF3 mientras se respaldaba");
|
|
4297
4449
|
previous.push({ item, contents: content });
|
|
4298
|
-
await writeText(
|
|
4450
|
+
await writeText(path18.join(backupDir, "files", ...item.artifact.split("/")), content);
|
|
4299
4451
|
}
|
|
4300
4452
|
await writeText(
|
|
4301
|
-
|
|
4302
|
-
|
|
4453
|
+
path18.join(backupDir, "backup.yaml"),
|
|
4454
|
+
stringifyYaml6(
|
|
4303
4455
|
{
|
|
4304
4456
|
schema_version: SCHEMA_VERSION,
|
|
4305
4457
|
created_at: localStamp(now),
|
|
@@ -4313,7 +4465,7 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
|
4313
4465
|
`);
|
|
4314
4466
|
} catch (err) {
|
|
4315
4467
|
await removeDir(backupDir);
|
|
4316
|
-
await
|
|
4468
|
+
await removeFile2(pointerFile);
|
|
4317
4469
|
return { status: "failed", plan, applied: [], failure: { artifact: "(respaldo)", message: err.message } };
|
|
4318
4470
|
}
|
|
4319
4471
|
const written = [];
|
|
@@ -4333,13 +4485,13 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
|
4333
4485
|
}
|
|
4334
4486
|
}
|
|
4335
4487
|
}
|
|
4336
|
-
await
|
|
4488
|
+
await removeFile2(pointerFile);
|
|
4337
4489
|
await removeDir(backupDir);
|
|
4338
4490
|
return { status: "failed", plan, applied: [], failure: { artifact: item.artifact, message: err.message } };
|
|
4339
4491
|
}
|
|
4340
4492
|
}
|
|
4341
4493
|
for (const dir of await listDirs(backupRoot)) {
|
|
4342
|
-
if (dir !== name) await removeDir(
|
|
4494
|
+
if (dir !== name) await removeDir(path18.join(backupRoot, dir));
|
|
4343
4495
|
}
|
|
4344
4496
|
return {
|
|
4345
4497
|
status: "applied",
|
|
@@ -4348,20 +4500,20 @@ async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
|
4348
4500
|
backup: {
|
|
4349
4501
|
name,
|
|
4350
4502
|
dir: backupDir,
|
|
4351
|
-
relativeDir: toPosix(
|
|
4503
|
+
relativeDir: toPosix(path18.relative(root, backupDir)),
|
|
4352
4504
|
createdAt: localStamp(now),
|
|
4353
4505
|
files: plan.pending.map((i) => i.artifact)
|
|
4354
4506
|
}
|
|
4355
4507
|
};
|
|
4356
4508
|
}
|
|
4357
4509
|
async function rollbackUpgrade(root) {
|
|
4358
|
-
const sddDir =
|
|
4359
|
-
const backupRoot =
|
|
4360
|
-
const pointerFile =
|
|
4510
|
+
const sddDir = path18.join(root, ".sdd");
|
|
4511
|
+
const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
|
|
4512
|
+
const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
|
|
4361
4513
|
const name = (await readTextIfExists(pointerFile))?.trim();
|
|
4362
4514
|
if (!name) return { status: "no-backup", restored: [] };
|
|
4363
|
-
const backupDir =
|
|
4364
|
-
const manifestRaw = await readTextIfExists(
|
|
4515
|
+
const backupDir = path18.join(backupRoot, name);
|
|
4516
|
+
const manifestRaw = await readTextIfExists(path18.join(backupDir, "backup.yaml"));
|
|
4365
4517
|
if (manifestRaw === void 0) return { status: "no-backup", restored: [] };
|
|
4366
4518
|
let files = [];
|
|
4367
4519
|
try {
|
|
@@ -4372,8 +4524,8 @@ async function rollbackUpgrade(root) {
|
|
|
4372
4524
|
}
|
|
4373
4525
|
const restored = [];
|
|
4374
4526
|
for (const file of files) {
|
|
4375
|
-
const from =
|
|
4376
|
-
const to =
|
|
4527
|
+
const from = path18.join(backupDir, "files", ...file.artifact.split("/"));
|
|
4528
|
+
const to = path18.join(sddDir, ...file.artifact.split("/"));
|
|
4377
4529
|
const content = await readTextIfExists(from);
|
|
4378
4530
|
if (content === void 0) continue;
|
|
4379
4531
|
try {
|
|
@@ -4384,7 +4536,7 @@ async function rollbackUpgrade(root) {
|
|
|
4384
4536
|
}
|
|
4385
4537
|
}
|
|
4386
4538
|
await removeDir(backupDir);
|
|
4387
|
-
await
|
|
4539
|
+
await removeFile2(pointerFile);
|
|
4388
4540
|
return { status: "restored", restored, backup: name };
|
|
4389
4541
|
}
|
|
4390
4542
|
async function upgradeAdvisory(root) {
|
|
@@ -4417,7 +4569,7 @@ async function upgradeAdvisory(root) {
|
|
|
4417
4569
|
}
|
|
4418
4570
|
|
|
4419
4571
|
// src/sarif.ts
|
|
4420
|
-
import
|
|
4572
|
+
import path19 from "path";
|
|
4421
4573
|
var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
4422
4574
|
var SARIF_VERSION = "2.1.0";
|
|
4423
4575
|
var TOOL_NAME = "SpecAtlas";
|
|
@@ -4455,7 +4607,7 @@ function resultOf(diagnostic, root) {
|
|
|
4455
4607
|
message: { text: diagnostic.message }
|
|
4456
4608
|
};
|
|
4457
4609
|
if (diagnostic.path) {
|
|
4458
|
-
const rel =
|
|
4610
|
+
const rel = path19.relative(root, diagnostic.path);
|
|
4459
4611
|
if (rel !== "") {
|
|
4460
4612
|
const physicalLocation = {
|
|
4461
4613
|
artifactLocation: { uri: toPosix(rel) },
|
|
@@ -4505,7 +4657,7 @@ function toSarifText(opts) {
|
|
|
4505
4657
|
}
|
|
4506
4658
|
|
|
4507
4659
|
// src/doctor.ts
|
|
4508
|
-
import
|
|
4660
|
+
import path20 from "path";
|
|
4509
4661
|
async function runDoctor(root) {
|
|
4510
4662
|
const findings = [];
|
|
4511
4663
|
const { workspace, config } = await loadWorkspace(root);
|
|
@@ -4513,7 +4665,7 @@ async function runDoctor(root) {
|
|
|
4513
4665
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
4514
4666
|
findings.push(...approvals.diagnostics);
|
|
4515
4667
|
for (const change of workspace.changes) {
|
|
4516
|
-
const deltaPath =
|
|
4668
|
+
const deltaPath = path20.join(change.dir, "spec.md");
|
|
4517
4669
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
4518
4670
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
|
|
4519
4671
|
if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
|
|
@@ -4535,7 +4687,7 @@ async function runDoctor(root) {
|
|
|
4535
4687
|
}
|
|
4536
4688
|
for (const override of change.meta?.overrides ?? []) {
|
|
4537
4689
|
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:
|
|
4690
|
+
findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path20.join(change.dir, "meta.yaml") }));
|
|
4539
4691
|
}
|
|
4540
4692
|
}
|
|
4541
4693
|
}
|
|
@@ -4558,7 +4710,7 @@ async function specHashOf(filePath) {
|
|
|
4558
4710
|
}
|
|
4559
4711
|
|
|
4560
4712
|
// src/gate.ts
|
|
4561
|
-
import
|
|
4713
|
+
import path21 from "path";
|
|
4562
4714
|
var UI_DOMAINS2 = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
4563
4715
|
function count(name, diagnostics) {
|
|
4564
4716
|
return {
|
|
@@ -4575,7 +4727,7 @@ function livingRequirementsMap(specs) {
|
|
|
4575
4727
|
return map;
|
|
4576
4728
|
}
|
|
4577
4729
|
async function runCiGate(opts) {
|
|
4578
|
-
const root =
|
|
4730
|
+
const root = path21.resolve(opts.root);
|
|
4579
4731
|
const { workspace, config } = await loadWorkspace(root);
|
|
4580
4732
|
const diagnostics = [];
|
|
4581
4733
|
const checks = [];
|
|
@@ -4586,7 +4738,7 @@ async function runCiGate(opts) {
|
|
|
4586
4738
|
let changesErrors = 0;
|
|
4587
4739
|
let changesWarnings = 0;
|
|
4588
4740
|
for (const change of workspace.changes) {
|
|
4589
|
-
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 }) : [];
|
|
4590
4742
|
const trace = checkTrace({
|
|
4591
4743
|
specs: workspace.specs,
|
|
4592
4744
|
change,
|
|
@@ -4636,6 +4788,7 @@ export {
|
|
|
4636
4788
|
BUILTIN_PACKS,
|
|
4637
4789
|
CONFIG_FILE,
|
|
4638
4790
|
CORE_VERSION,
|
|
4791
|
+
LIVING_FIXES_DIR,
|
|
4639
4792
|
REQ_HEAD_RE,
|
|
4640
4793
|
REQ_ID_RE,
|
|
4641
4794
|
RULE_ID_RE,
|
|
@@ -4735,6 +4888,7 @@ export {
|
|
|
4735
4888
|
loadChange,
|
|
4736
4889
|
loadConfig,
|
|
4737
4890
|
loadDetectedBest,
|
|
4891
|
+
loadLivingFixes,
|
|
4738
4892
|
loadProfileFile,
|
|
4739
4893
|
loadProfilesFromDir,
|
|
4740
4894
|
loadProjectPacks,
|
|
@@ -4755,10 +4909,12 @@ export {
|
|
|
4755
4909
|
parseChangeMeta,
|
|
4756
4910
|
parseConfig,
|
|
4757
4911
|
parseDelta,
|
|
4912
|
+
parseFixCovers,
|
|
4758
4913
|
parseFrontmatter,
|
|
4759
4914
|
parseGitHubRemote,
|
|
4760
4915
|
parseGlossary,
|
|
4761
4916
|
parseIssueUrl,
|
|
4917
|
+
parseLivingFix,
|
|
4762
4918
|
parseRequirementBlocks,
|
|
4763
4919
|
parseSpecFile,
|
|
4764
4920
|
parseTasksFile,
|
|
@@ -4810,6 +4966,7 @@ export {
|
|
|
4810
4966
|
verifyApproval,
|
|
4811
4967
|
walkFiles,
|
|
4812
4968
|
writeConfig,
|
|
4969
|
+
writeLivingFix,
|
|
4813
4970
|
writeMockupManifest,
|
|
4814
4971
|
writeMockupPlan,
|
|
4815
4972
|
writeText
|