specatlas 0.1.25 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js
CHANGED
|
@@ -6,13 +6,14 @@ import {
|
|
|
6
6
|
localCompact,
|
|
7
7
|
localDate,
|
|
8
8
|
localMonth,
|
|
9
|
+
localOffset,
|
|
9
10
|
localStamp,
|
|
10
11
|
parseApprovals,
|
|
11
12
|
parseChangeMeta,
|
|
12
13
|
sha256,
|
|
13
14
|
shortHash,
|
|
14
15
|
signApproval
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-L2BWIT44.js";
|
|
16
17
|
import {
|
|
17
18
|
__commonJS,
|
|
18
19
|
__toESM,
|
|
@@ -1585,7 +1586,7 @@ var require_core = __commonJS({
|
|
|
1585
1586
|
});
|
|
1586
1587
|
|
|
1587
1588
|
// src/cli.ts
|
|
1588
|
-
import
|
|
1589
|
+
import path46 from "path";
|
|
1589
1590
|
|
|
1590
1591
|
// ../core/dist/index.js
|
|
1591
1592
|
import path from "path";
|
|
@@ -1598,23 +1599,25 @@ import path2 from "path";
|
|
|
1598
1599
|
import path3 from "path";
|
|
1599
1600
|
import { parse as parseYaml4 } from "yaml";
|
|
1600
1601
|
import { z as z3 } from "zod";
|
|
1602
|
+
import path5 from "path";
|
|
1601
1603
|
import path4 from "path";
|
|
1602
1604
|
import { stringify as stringifyYaml3 } from "yaml";
|
|
1603
|
-
import
|
|
1605
|
+
import { stringify as stringifyYaml4 } from "yaml";
|
|
1606
|
+
import path6 from "path";
|
|
1604
1607
|
import { parse as parseYaml5 } from "yaml";
|
|
1605
1608
|
import { execFile } from "child_process";
|
|
1606
|
-
import path6 from "path";
|
|
1607
1609
|
import path7 from "path";
|
|
1608
|
-
import { randomBytes } from "crypto";
|
|
1609
|
-
import path10 from "path";
|
|
1610
1610
|
import path8 from "path";
|
|
1611
|
-
import {
|
|
1612
|
-
import
|
|
1611
|
+
import { randomBytes } from "crypto";
|
|
1612
|
+
import path11 from "path";
|
|
1613
1613
|
import path9 from "path";
|
|
1614
|
+
import { parse as parseYaml6, stringify as stringifyYaml5 } from "yaml";
|
|
1615
|
+
import { z as z4 } from "zod";
|
|
1616
|
+
import path10 from "path";
|
|
1614
1617
|
import { parse as parseYaml7 } from "yaml";
|
|
1615
1618
|
import { z as z5 } from "zod";
|
|
1616
|
-
import path11 from "path";
|
|
1617
1619
|
import path12 from "path";
|
|
1620
|
+
import path13 from "path";
|
|
1618
1621
|
|
|
1619
1622
|
// ../../node_modules/.pnpm/highlight.js@11.12.0/node_modules/highlight.js/es/core.js
|
|
1620
1623
|
var import_core = __toESM(require_core(), 1);
|
|
@@ -9019,14 +9022,18 @@ function renderMarkdown(markdown2, opts = {}) {
|
|
|
9019
9022
|
}
|
|
9020
9023
|
|
|
9021
9024
|
// ../core/dist/index.js
|
|
9022
|
-
import path13 from "path";
|
|
9023
9025
|
import path14 from "path";
|
|
9024
9026
|
import path15 from "path";
|
|
9027
|
+
import path16 from "path";
|
|
9025
9028
|
import { promises as fs } from "fs";
|
|
9026
9029
|
import { cp, rename, rm } from "fs/promises";
|
|
9027
|
-
import path16 from "path";
|
|
9028
9030
|
import path17 from "path";
|
|
9031
|
+
import { promises as fs2 } from "fs";
|
|
9029
9032
|
import path18 from "path";
|
|
9033
|
+
import { parse as parseYaml8, stringify as stringifyYaml6 } from "yaml";
|
|
9034
|
+
import path19 from "path";
|
|
9035
|
+
import path20 from "path";
|
|
9036
|
+
import path21 from "path";
|
|
9030
9037
|
var CORE_VERSION = "0.0.1";
|
|
9031
9038
|
var REQ_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}$/;
|
|
9032
9039
|
var SCENARIO_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}-S\d+$/;
|
|
@@ -9742,14 +9749,14 @@ function wordBoundary(text, term) {
|
|
|
9742
9749
|
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
|
|
9743
9750
|
return re.test(text);
|
|
9744
9751
|
}
|
|
9745
|
-
function lintText(text, code, terms, label,
|
|
9752
|
+
function lintText(text, code, terms, label, path222, line) {
|
|
9746
9753
|
const out = [];
|
|
9747
9754
|
const lower = text.toLowerCase();
|
|
9748
9755
|
for (const term of terms) {
|
|
9749
9756
|
if (wordBoundary(lower, term)) {
|
|
9750
9757
|
out.push(
|
|
9751
9758
|
diag(code, "error", `${label}: "${term}"`, {
|
|
9752
|
-
path:
|
|
9759
|
+
path: path222,
|
|
9753
9760
|
line,
|
|
9754
9761
|
suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
|
|
9755
9762
|
})
|
|
@@ -9758,7 +9765,7 @@ function lintText(text, code, terms, label, path192, line) {
|
|
|
9758
9765
|
}
|
|
9759
9766
|
return out;
|
|
9760
9767
|
}
|
|
9761
|
-
function lintRequirement(req,
|
|
9768
|
+
function lintRequirement(req, path222, opts = {}) {
|
|
9762
9769
|
const out = [];
|
|
9763
9770
|
const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
|
|
9764
9771
|
const tech = opts.language === "en" ? TECH_EN : TECH_ES;
|
|
@@ -9769,32 +9776,32 @@ function lintRequirement(req, path192, opts = {}) {
|
|
|
9769
9776
|
...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
|
|
9770
9777
|
];
|
|
9771
9778
|
for (const part of parts) {
|
|
9772
|
-
out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n",
|
|
9779
|
+
out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path222, part.line));
|
|
9773
9780
|
if (opts.businessOnly !== false) {
|
|
9774
|
-
out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio",
|
|
9781
|
+
out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path222, part.line));
|
|
9775
9782
|
}
|
|
9776
9783
|
}
|
|
9777
9784
|
if (req.scenarios.length === 0) {
|
|
9778
|
-
out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path:
|
|
9785
|
+
out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path222, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
|
|
9779
9786
|
}
|
|
9780
9787
|
return out;
|
|
9781
9788
|
}
|
|
9782
|
-
function lintDelta(delta, livingRequirements,
|
|
9789
|
+
function lintDelta(delta, livingRequirements, path222, opts = {}) {
|
|
9783
9790
|
const out = [...delta.diagnostics];
|
|
9784
9791
|
for (const req of [...delta.added, ...delta.modified]) {
|
|
9785
|
-
out.push(...lintRequirement(req,
|
|
9792
|
+
out.push(...lintRequirement(req, path222, opts));
|
|
9786
9793
|
}
|
|
9787
9794
|
for (const req of delta.modified) {
|
|
9788
9795
|
const living = livingRequirements.get(req.id);
|
|
9789
9796
|
if (!living) {
|
|
9790
|
-
out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path:
|
|
9797
|
+
out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path222, line: req.line }));
|
|
9791
9798
|
continue;
|
|
9792
9799
|
}
|
|
9793
9800
|
for (const existing of living.scenarios) {
|
|
9794
9801
|
if (!req.scenarios.some((s) => s.id === existing.id)) {
|
|
9795
9802
|
out.push(
|
|
9796
9803
|
diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
|
|
9797
|
-
path:
|
|
9804
|
+
path: path222,
|
|
9798
9805
|
line: req.line,
|
|
9799
9806
|
suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
|
|
9800
9807
|
})
|
|
@@ -9805,15 +9812,15 @@ function lintDelta(delta, livingRequirements, path192, opts = {}) {
|
|
|
9805
9812
|
for (const req of delta.removed) {
|
|
9806
9813
|
const living = livingRequirements.get(req.id);
|
|
9807
9814
|
if (!living) {
|
|
9808
|
-
out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path:
|
|
9815
|
+
out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path222, line: req.line }));
|
|
9809
9816
|
}
|
|
9810
9817
|
}
|
|
9811
9818
|
for (const rename2 of delta.renamed) {
|
|
9812
9819
|
if (rename2.from.id !== rename2.to.id) {
|
|
9813
|
-
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path:
|
|
9820
|
+
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path222, line: rename2.line }));
|
|
9814
9821
|
}
|
|
9815
9822
|
if (!livingRequirements.has(rename2.from.id)) {
|
|
9816
|
-
out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path:
|
|
9823
|
+
out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path222, line: rename2.line }));
|
|
9817
9824
|
}
|
|
9818
9825
|
}
|
|
9819
9826
|
return out;
|
|
@@ -9837,7 +9844,7 @@ var MERMAID_KEYWORDS = [
|
|
|
9837
9844
|
"architecture-beta"
|
|
9838
9845
|
];
|
|
9839
9846
|
var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
|
|
9840
|
-
function lintPlan(planText,
|
|
9847
|
+
function lintPlan(planText, path222) {
|
|
9841
9848
|
const out = [];
|
|
9842
9849
|
const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
|
|
9843
9850
|
for (const [index, block] of blocks.entries()) {
|
|
@@ -9847,7 +9854,7 @@ function lintPlan(planText, path192) {
|
|
|
9847
9854
|
if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
|
|
9848
9855
|
out.push(
|
|
9849
9856
|
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)}"`, {
|
|
9850
|
-
path:
|
|
9857
|
+
path: path222,
|
|
9851
9858
|
suggestion: "Corrige el tipo del diagrama o elimina el bloque"
|
|
9852
9859
|
})
|
|
9853
9860
|
);
|
|
@@ -9861,7 +9868,7 @@ function lintPlan(planText, path192) {
|
|
|
9861
9868
|
if (open !== 0) {
|
|
9862
9869
|
out.push(
|
|
9863
9870
|
diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
|
|
9864
|
-
path:
|
|
9871
|
+
path: path222,
|
|
9865
9872
|
suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
|
|
9866
9873
|
})
|
|
9867
9874
|
);
|
|
@@ -9873,7 +9880,7 @@ function lintPlan(planText, path192) {
|
|
|
9873
9880
|
if (message.includes(";")) {
|
|
9874
9881
|
out.push(
|
|
9875
9882
|
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`, {
|
|
9876
|
-
path:
|
|
9883
|
+
path: path222,
|
|
9877
9884
|
suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
|
|
9878
9885
|
})
|
|
9879
9886
|
);
|
|
@@ -10028,6 +10035,16 @@ function checkTrace(input) {
|
|
|
10028
10035
|
findings.push(diag("TRACE-006", "warning", `Evidencia hu\xE9rfana: ${ev.scenario} no existe`, { path: change.verify?.path, line: ev.line }));
|
|
10029
10036
|
}
|
|
10030
10037
|
}
|
|
10038
|
+
for (const cover of change.fixCovers ?? []) {
|
|
10039
|
+
if (!livingReqs.has(cover)) {
|
|
10040
|
+
findings.push(
|
|
10041
|
+
diag("TRACE-011", "warning", `El fix ${change.slug} declara ${cover}, que no existe en las specs vivas`, {
|
|
10042
|
+
path: change.fix?.path,
|
|
10043
|
+
suggestion: "Corrige el identificador o qu\xEDtalo: la declaraci\xF3n es opcional"
|
|
10044
|
+
})
|
|
10045
|
+
);
|
|
10046
|
+
}
|
|
10047
|
+
}
|
|
10031
10048
|
const summary = countBySeverity(findings);
|
|
10032
10049
|
return { graph: buildTraceGraph(input), findings, summary };
|
|
10033
10050
|
}
|
|
@@ -10391,34 +10408,110 @@ function detectionToYaml(result, generatedAt) {
|
|
|
10391
10408
|
}
|
|
10392
10409
|
return lines.join("\n") + "\n";
|
|
10393
10410
|
}
|
|
10411
|
+
var LIVING_FIXES_DIR = path4.join(".sdd", "fixes");
|
|
10412
|
+
var COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
10413
|
+
function parseFixCovers(content) {
|
|
10414
|
+
const clean = content.replace(COMMENT_RE, "");
|
|
10415
|
+
const covers = [];
|
|
10416
|
+
for (const line of clean.replace(/\r\n?/g, "\n").split("\n")) {
|
|
10417
|
+
const match = /^\s*-?\s*Cubre:\s*(.+?)\s*$/.exec(line);
|
|
10418
|
+
if (!match) continue;
|
|
10419
|
+
for (const raw of (match[1] ?? "").split(",")) {
|
|
10420
|
+
const id = raw.trim().toUpperCase();
|
|
10421
|
+
if (/^REQ-[A-Z0-9-]+$/.test(id) && !covers.includes(id)) covers.push(id);
|
|
10422
|
+
}
|
|
10423
|
+
}
|
|
10424
|
+
return covers;
|
|
10425
|
+
}
|
|
10426
|
+
function coversOf(value) {
|
|
10427
|
+
if (Array.isArray(value)) {
|
|
10428
|
+
return value.map((v) => String(v).trim().toUpperCase()).filter((v) => /^REQ-[A-Z0-9-]+$/.test(v));
|
|
10429
|
+
}
|
|
10430
|
+
if (typeof value === "string") {
|
|
10431
|
+
return value.split(",").map((v) => v.trim().toUpperCase()).filter((v) => /^REQ-[A-Z0-9-]+$/.test(v));
|
|
10432
|
+
}
|
|
10433
|
+
return [];
|
|
10434
|
+
}
|
|
10435
|
+
function parseLivingFix(content, file) {
|
|
10436
|
+
const fm = parseFrontmatter(content, file);
|
|
10437
|
+
const data = fm.data;
|
|
10438
|
+
const base = path4.basename(file).replace(/\.md$/i, "");
|
|
10439
|
+
const slug = typeof data["slug"] === "string" && data["slug"].trim() !== "" ? data["slug"].trim() : base.replace(/^\d{4}-\d{2}-/, "");
|
|
10440
|
+
const fix = {
|
|
10441
|
+
slug,
|
|
10442
|
+
file,
|
|
10443
|
+
date: typeof data["date"] === "string" ? data["date"] : "",
|
|
10444
|
+
result: typeof data["result"] === "string" ? data["result"] : "pass",
|
|
10445
|
+
covers: coversOf(data["covers"]),
|
|
10446
|
+
content: fm.body.trimStart()
|
|
10447
|
+
};
|
|
10448
|
+
if (typeof data["domain"] === "string") fix.domain = data["domain"];
|
|
10449
|
+
if (typeof data["title"] === "string") fix.title = data["title"];
|
|
10450
|
+
return fix;
|
|
10451
|
+
}
|
|
10452
|
+
async function loadLivingFixes(root) {
|
|
10453
|
+
const dir = path4.join(root, LIVING_FIXES_DIR);
|
|
10454
|
+
const entries = (await listDir(dir)).filter((entry) => entry.toLowerCase().endsWith(".md")).sort();
|
|
10455
|
+
const fixes = [];
|
|
10456
|
+
for (const entry of entries) {
|
|
10457
|
+
const file = path4.join(dir, entry);
|
|
10458
|
+
const content = await readTextIfExists(file);
|
|
10459
|
+
if (content === void 0) continue;
|
|
10460
|
+
fixes.push(parseLivingFix(content, file));
|
|
10461
|
+
}
|
|
10462
|
+
return fixes.sort((a, b) => b.date.localeCompare(a.date) || b.slug.localeCompare(a.slug));
|
|
10463
|
+
}
|
|
10464
|
+
async function writeLivingFix(root, input) {
|
|
10465
|
+
const dir = path4.join(root, LIVING_FIXES_DIR);
|
|
10466
|
+
const month = input.date.slice(0, 7);
|
|
10467
|
+
const file = path4.join(dir, `${month}-${input.slug}.md`);
|
|
10468
|
+
const relativePath = toPosix(path4.relative(root, file));
|
|
10469
|
+
if (await exists(file)) return { file, relativePath, created: false };
|
|
10470
|
+
const data = {
|
|
10471
|
+
slug: input.slug,
|
|
10472
|
+
date: input.date,
|
|
10473
|
+
result: input.result ?? "pass"
|
|
10474
|
+
};
|
|
10475
|
+
if (input.domain) data["domain"] = input.domain;
|
|
10476
|
+
if (input.title) data["title"] = input.title;
|
|
10477
|
+
if (input.covers && input.covers.length > 0) data["covers"] = input.covers;
|
|
10478
|
+
const body = input.content.replace(/^[\r\n]+/, "").trimEnd();
|
|
10479
|
+
const text = `---
|
|
10480
|
+
${stringifyYaml3(data, { lineWidth: 120 })}---
|
|
10481
|
+
|
|
10482
|
+
${body}
|
|
10483
|
+
`;
|
|
10484
|
+
await writeText(file, text);
|
|
10485
|
+
return { file, relativePath, created: true };
|
|
10486
|
+
}
|
|
10394
10487
|
var SDD_DIR = ".sdd";
|
|
10395
10488
|
async function findWorkspaceRoot(start) {
|
|
10396
|
-
let current =
|
|
10489
|
+
let current = path5.resolve(start);
|
|
10397
10490
|
for (let i = 0; i < 40; i += 1) {
|
|
10398
|
-
if (await isDirectory(
|
|
10399
|
-
const parent =
|
|
10491
|
+
if (await isDirectory(path5.join(current, SDD_DIR))) return current;
|
|
10492
|
+
const parent = path5.dirname(current);
|
|
10400
10493
|
if (parent === current) return void 0;
|
|
10401
10494
|
current = parent;
|
|
10402
10495
|
}
|
|
10403
10496
|
return void 0;
|
|
10404
10497
|
}
|
|
10405
10498
|
async function loadApprovals(sddDir) {
|
|
10406
|
-
const file =
|
|
10499
|
+
const file = path5.join(sddDir, "approvals.yaml");
|
|
10407
10500
|
const raw = await readTextIfExists(file);
|
|
10408
10501
|
if (raw === void 0) return { byArtifact: /* @__PURE__ */ new Map(), diagnostics: [] };
|
|
10409
10502
|
const parsed = parseApprovals(raw, file);
|
|
10410
10503
|
const map = /* @__PURE__ */ new Map();
|
|
10411
10504
|
for (const a of parsed.approvals?.approvals ?? []) {
|
|
10412
|
-
map.set(a.artifact.split(
|
|
10505
|
+
map.set(a.artifact.split(path5.sep).join("/"), { hash: a.artifactHash, by: a.approvedBy, at: a.approvedAt });
|
|
10413
10506
|
}
|
|
10414
10507
|
return { byArtifact: map, diagnostics: parsed.diagnostics };
|
|
10415
10508
|
}
|
|
10416
10509
|
async function loadSpecs(sddDir) {
|
|
10417
|
-
const specsDir =
|
|
10510
|
+
const specsDir = path5.join(sddDir, "specs");
|
|
10418
10511
|
const domains = await listDirs(specsDir);
|
|
10419
10512
|
const out = [];
|
|
10420
10513
|
for (const domain of domains) {
|
|
10421
|
-
const file =
|
|
10514
|
+
const file = path5.join(specsDir, domain, "spec.md");
|
|
10422
10515
|
if (!await exists(file)) continue;
|
|
10423
10516
|
const content = await readText(file);
|
|
10424
10517
|
out.push({ domain, path: file, spec: parseSpecFile(content, file) });
|
|
@@ -10426,10 +10519,10 @@ async function loadSpecs(sddDir) {
|
|
|
10426
10519
|
return out;
|
|
10427
10520
|
}
|
|
10428
10521
|
async function loadChange(root, slug, relDir) {
|
|
10429
|
-
const dir =
|
|
10522
|
+
const dir = path5.join(root, SDD_DIR, "changes", relDir ?? slug);
|
|
10430
10523
|
const diagnostics = [];
|
|
10431
10524
|
const change = { slug, dir, diagnostics };
|
|
10432
|
-
const metaFile =
|
|
10525
|
+
const metaFile = path5.join(dir, "meta.yaml");
|
|
10433
10526
|
const metaRaw = await readTextIfExists(metaFile);
|
|
10434
10527
|
if (metaRaw === void 0) {
|
|
10435
10528
|
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" }));
|
|
@@ -10438,7 +10531,7 @@ async function loadChange(root, slug, relDir) {
|
|
|
10438
10531
|
diagnostics.push(...parsed.diagnostics);
|
|
10439
10532
|
if (parsed.meta) change.meta = parsed.meta;
|
|
10440
10533
|
}
|
|
10441
|
-
const deltaFile =
|
|
10534
|
+
const deltaFile = path5.join(dir, "spec.md");
|
|
10442
10535
|
const deltaRaw = await readTextIfExists(deltaFile);
|
|
10443
10536
|
if (deltaRaw !== void 0) {
|
|
10444
10537
|
change.delta = parseDelta(deltaRaw, deltaFile);
|
|
@@ -10446,41 +10539,42 @@ async function loadChange(root, slug, relDir) {
|
|
|
10446
10539
|
} else {
|
|
10447
10540
|
diagnostics.push(diag("ATLAS-FILES-002", "warning", `El cambio "${slug}" no tiene spec.md (delta)`, { path: deltaFile }));
|
|
10448
10541
|
}
|
|
10449
|
-
const planFile =
|
|
10542
|
+
const planFile = path5.join(dir, "plan.md");
|
|
10450
10543
|
if (await exists(planFile)) change.planPath = planFile;
|
|
10451
|
-
const reviewFile =
|
|
10544
|
+
const reviewFile = path5.join(dir, "review.md");
|
|
10452
10545
|
if (await exists(reviewFile)) change.reviewPath = reviewFile;
|
|
10453
|
-
const presentationFile =
|
|
10546
|
+
const presentationFile = path5.join(dir, "presentation", "index.html");
|
|
10454
10547
|
if (await exists(presentationFile)) change.presentationPath = presentationFile;
|
|
10455
|
-
const tasksFile =
|
|
10548
|
+
const tasksFile = path5.join(dir, "tasks.md");
|
|
10456
10549
|
const tasksRaw = await readTextIfExists(tasksFile);
|
|
10457
10550
|
if (tasksRaw !== void 0) {
|
|
10458
10551
|
change.tasks = parseTasksFile(tasksRaw, tasksFile);
|
|
10459
10552
|
diagnostics.push(...change.tasks.diagnostics);
|
|
10460
10553
|
}
|
|
10461
|
-
const verifyFile =
|
|
10554
|
+
const verifyFile = path5.join(dir, "verify.md");
|
|
10462
10555
|
const verifyRaw = await readTextIfExists(verifyFile);
|
|
10463
10556
|
if (verifyRaw !== void 0) {
|
|
10464
10557
|
change.verify = parseVerifyFile(verifyRaw, verifyFile);
|
|
10465
10558
|
diagnostics.push(...change.verify.diagnostics);
|
|
10466
10559
|
}
|
|
10467
|
-
const fixFile =
|
|
10560
|
+
const fixFile = path5.join(dir, "fix.md");
|
|
10468
10561
|
const fixRaw = await readTextIfExists(fixFile);
|
|
10469
10562
|
if (fixRaw !== void 0) {
|
|
10470
10563
|
change.fix = parseVerifyFile(fixRaw, fixFile);
|
|
10471
10564
|
diagnostics.push(...change.fix.diagnostics);
|
|
10565
|
+
change.fixCovers = parseFixCovers(fixRaw);
|
|
10472
10566
|
}
|
|
10473
|
-
const mockupManifest =
|
|
10567
|
+
const mockupManifest = path5.join(dir, "mockups", "manifest.yaml");
|
|
10474
10568
|
if (await exists(mockupManifest)) change.mockupManifestPath = mockupManifest;
|
|
10475
10569
|
return change;
|
|
10476
10570
|
}
|
|
10477
10571
|
async function listChangeSlugs(root, opts = {}) {
|
|
10478
|
-
const changesDir =
|
|
10572
|
+
const changesDir = path5.join(root, SDD_DIR, "changes");
|
|
10479
10573
|
const dirs = await listDirs(changesDir);
|
|
10480
10574
|
return dirs.filter((d) => opts.includeArchived ? true : d !== "archive");
|
|
10481
10575
|
}
|
|
10482
10576
|
async function loadWorkspace(root) {
|
|
10483
|
-
const sddDir =
|
|
10577
|
+
const sddDir = path5.join(root, SDD_DIR);
|
|
10484
10578
|
const { config, diagnostics: configDiags } = await loadConfig(sddDir);
|
|
10485
10579
|
const diagnostics = [...configDiags];
|
|
10486
10580
|
const specs = await loadSpecs(sddDir);
|
|
@@ -10493,20 +10587,20 @@ async function loadWorkspace(root) {
|
|
|
10493
10587
|
diagnostics.push(...change.diagnostics);
|
|
10494
10588
|
}
|
|
10495
10589
|
const archived = [];
|
|
10496
|
-
const archivedDir =
|
|
10590
|
+
const archivedDir = path5.join(sddDir, "changes", "archive");
|
|
10497
10591
|
if (await isDirectory(archivedDir)) {
|
|
10498
10592
|
for (const entry of await listDirs(archivedDir)) {
|
|
10499
|
-
archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""),
|
|
10593
|
+
archived.push(await loadChange(root, entry.replace(/^\d{4}-\d{2}-/, ""), path5.join("archive", entry)));
|
|
10500
10594
|
}
|
|
10501
10595
|
}
|
|
10502
10596
|
return { workspace: { root, sddDir, specs, changes, archived, diagnostics }, config };
|
|
10503
10597
|
}
|
|
10504
10598
|
async function ensureSddDirs(sddDir) {
|
|
10505
10599
|
const created = [];
|
|
10506
|
-
const dirs = ["specs", "changes", "runs", "metrics",
|
|
10600
|
+
const dirs = ["specs", "changes", "runs", "metrics", "fixes", path5.join("profiles", "custom")];
|
|
10507
10601
|
const { ensureDir: ensureDir2 } = await import("./fsx-VF2P7ALA-XKEWTMDH.js");
|
|
10508
10602
|
for (const d of dirs) {
|
|
10509
|
-
const full =
|
|
10603
|
+
const full = path5.join(sddDir, d);
|
|
10510
10604
|
if (!await exists(full)) {
|
|
10511
10605
|
await ensureDir2(full);
|
|
10512
10606
|
created.push(full);
|
|
@@ -10674,6 +10768,10 @@ notes: 12/12 casos
|
|
|
10674
10768
|
|
|
10675
10769
|
## Evidencia
|
|
10676
10770
|
|
|
10771
|
+
<!-- Opcional: declara los requisitos que este fix afecta (aparecen en su trazabilidad).
|
|
10772
|
+
Cubre: REQ-DOMINIO-001
|
|
10773
|
+
-->
|
|
10774
|
+
|
|
10677
10775
|
<!-- Registra la evidencia real con:
|
|
10678
10776
|
satlas verify <slug> --file fix --scenario REQ-DOMINIO-001-S1 --command "<comando>" --by "<nombre>"
|
|
10679
10777
|
o, si es manual:
|
|
@@ -10797,6 +10895,10 @@ by: your-name
|
|
|
10797
10895
|
## Rollback
|
|
10798
10896
|
## Evidence
|
|
10799
10897
|
|
|
10898
|
+
<!-- Optional: declare the requirements this fix affects (they show up in its traceability).
|
|
10899
|
+
Cubre: REQ-DOMAIN-001
|
|
10900
|
+
-->
|
|
10901
|
+
|
|
10800
10902
|
<!-- Register real evidence with:
|
|
10801
10903
|
satlas verify <slug> --file fix --scenario REQ-DOMAIN-001-S1 --command "<command>" --by "<name>"
|
|
10802
10904
|
-->
|
|
@@ -10821,7 +10923,7 @@ function changeMetaYaml(meta) {
|
|
|
10821
10923
|
if (meta.owner) doc["owner"] = meta.owner;
|
|
10822
10924
|
if (meta.mockups) doc["mockups"] = meta.mockups;
|
|
10823
10925
|
return `# Estado del cambio. La fase se DERIVA de los artefactos; aqu\xED solo hechos.
|
|
10824
|
-
` +
|
|
10926
|
+
` + stringifyYaml4(doc, { lineWidth: 120 });
|
|
10825
10927
|
}
|
|
10826
10928
|
function indexMarkdown(input) {
|
|
10827
10929
|
const es = input.language !== "en";
|
|
@@ -10835,6 +10937,15 @@ function indexMarkdown(input) {
|
|
|
10835
10937
|
if (input.specs.length === 0) lines.push(es ? "_Sin specs todav\xEDa._" : "_No specs yet._");
|
|
10836
10938
|
for (const spec of input.specs) lines.push(`- \`${spec.domain}\` \u2014 ${spec.requirements} ${es ? "requisitos" : "requirements"}`);
|
|
10837
10939
|
lines.push("");
|
|
10940
|
+
lines.push(es ? "## Fixes vivos" : "## Living fixes");
|
|
10941
|
+
lines.push("");
|
|
10942
|
+
const fixes = input.fixes ?? [];
|
|
10943
|
+
if (fixes.length === 0) lines.push(es ? "_Sin fixes archivados todav\xEDa._" : "_No archived fixes yet._");
|
|
10944
|
+
for (const fix of fixes) {
|
|
10945
|
+
const parts = [fix.date, fix.domain ? `\`${fix.domain}\`` : "", `\`${fix.slug}\``, fix.result].filter((part) => part !== "");
|
|
10946
|
+
lines.push(`- ${parts.join(" \xB7 ")}`);
|
|
10947
|
+
}
|
|
10948
|
+
lines.push("");
|
|
10838
10949
|
lines.push(es ? "## Cambios activos" : "## Active changes");
|
|
10839
10950
|
lines.push("");
|
|
10840
10951
|
if (input.changes.length === 0) lines.push(es ? "_Sin cambios activos._" : "_No active changes._");
|
|
@@ -10986,8 +11097,8 @@ function issueBody(input) {
|
|
|
10986
11097
|
lines.push(`- Siguiente: \`${input.next}\` \u2014 ${input.nextDescription}`);
|
|
10987
11098
|
lines.push("");
|
|
10988
11099
|
lines.push("### Artefactos");
|
|
10989
|
-
lines.push(`- Spec: \`${
|
|
10990
|
-
if (change.planPath) lines.push(`- Plan: \`${
|
|
11100
|
+
lines.push(`- Spec: \`${path6.posix.join(".sdd", "changes", change.slug, "spec.md")}\``);
|
|
11101
|
+
if (change.planPath) lines.push(`- Plan: \`${path6.posix.join(".sdd", "changes", change.slug, "plan.md")}\``);
|
|
10991
11102
|
if (change.tasks) lines.push(`- Tareas: ${change.tasks.counts.done}/${change.tasks.counts.total}`);
|
|
10992
11103
|
if (change.verify) lines.push(`- Evidencia registrada: ${change.verify.evidence.length}`);
|
|
10993
11104
|
lines.push("");
|
|
@@ -11051,10 +11162,10 @@ async function issueLabels(root, number, runner = defaultRunner) {
|
|
|
11051
11162
|
}
|
|
11052
11163
|
}
|
|
11053
11164
|
async function patchChangeMeta(root, slug, patch) {
|
|
11054
|
-
const file =
|
|
11165
|
+
const file = path6.join(path6.resolve(root), ".sdd", "changes", slug, "meta.yaml");
|
|
11055
11166
|
const raw = await readTextIfExists(file);
|
|
11056
11167
|
if (raw === void 0) {
|
|
11057
|
-
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${
|
|
11168
|
+
return { path: file, diagnostics: [diag("ATLAS-GH-007", "error", `No existe ${path6.relative(root, file)}`)] };
|
|
11058
11169
|
}
|
|
11059
11170
|
let data;
|
|
11060
11171
|
try {
|
|
@@ -11105,10 +11216,10 @@ async function approveFromGithub(opts) {
|
|
|
11105
11216
|
if (!by) {
|
|
11106
11217
|
return { approved: false, label, issueNumber: number, diagnostics: [diag("ATLAS-GH-011", "error", "No se pudo determinar qui\xE9n aprueba (--by ni `gh api user`)")] };
|
|
11107
11218
|
}
|
|
11108
|
-
const { signApproval: signApproval2 } = await import("./approvals-2PO6223I-
|
|
11219
|
+
const { signApproval: signApproval2 } = await import("./approvals-2PO6223I-DENKLEXU.js");
|
|
11109
11220
|
const signed = await signApproval2({
|
|
11110
11221
|
root: opts.root,
|
|
11111
|
-
artifact:
|
|
11222
|
+
artifact: path6.posix.join("changes", opts.slug, "spec.md"),
|
|
11112
11223
|
by,
|
|
11113
11224
|
channel: "tracker",
|
|
11114
11225
|
note: `Aprobado v\xEDa GitHub issue #${number} (etiqueta "${label}")`
|
|
@@ -11203,10 +11314,10 @@ function buildEvidenceYaml(evidence) {
|
|
|
11203
11314
|
return lines.join("\n");
|
|
11204
11315
|
}
|
|
11205
11316
|
async function recordEvidence(opts) {
|
|
11206
|
-
const root =
|
|
11317
|
+
const root = path7.resolve(opts.root);
|
|
11207
11318
|
const slug = opts.slug;
|
|
11208
11319
|
const fileKind = opts.file ?? "verify";
|
|
11209
|
-
const file =
|
|
11320
|
+
const file = path7.join(root, ".sdd", "changes", slug, fileKind === "fix" ? "fix.md" : "verify.md");
|
|
11210
11321
|
const diagnostics = [];
|
|
11211
11322
|
const scenario = opts.scenario.toUpperCase();
|
|
11212
11323
|
if (!SCENARIO_ID_RE.test(scenario)) {
|
|
@@ -11296,7 +11407,7 @@ function generateRunId(now = /* @__PURE__ */ new Date()) {
|
|
|
11296
11407
|
return `${iso.slice(0, 8)}-${iso.slice(8, 14)}-${randomBytes(3).toString("hex")}`;
|
|
11297
11408
|
}
|
|
11298
11409
|
function runsDir(root) {
|
|
11299
|
-
return
|
|
11410
|
+
return path8.join(path8.resolve(root), ".sdd", "runs");
|
|
11300
11411
|
}
|
|
11301
11412
|
async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Date()) {
|
|
11302
11413
|
const runId = generateRunId(now);
|
|
@@ -11309,25 +11420,25 @@ async function createRun(root, slug, phase, inputs, now = /* @__PURE__ */ new Da
|
|
|
11309
11420
|
updatedAt: localStamp(now)
|
|
11310
11421
|
};
|
|
11311
11422
|
if (inputs) state.inputs = inputs;
|
|
11312
|
-
const dir =
|
|
11423
|
+
const dir = path8.join(runsDir(root), runId);
|
|
11313
11424
|
await ensureDir(dir);
|
|
11314
|
-
await writeText(
|
|
11425
|
+
await writeText(path8.join(dir, "state.json"), `${JSON.stringify(state, null, 2)}
|
|
11315
11426
|
`);
|
|
11316
|
-
await writeText(
|
|
11427
|
+
await writeText(path8.join(dir, "events.jsonl"), "");
|
|
11317
11428
|
await appendRunEvent(root, runId, "run_started", { slug, phase });
|
|
11318
11429
|
return state;
|
|
11319
11430
|
}
|
|
11320
11431
|
async function appendRunEvent(root, runId, type, data, now = /* @__PURE__ */ new Date()) {
|
|
11321
11432
|
const event = { eventId: randomBytes(4).toString("hex"), at: localStamp(now), type };
|
|
11322
11433
|
if (data) event.data = data;
|
|
11323
|
-
const file =
|
|
11434
|
+
const file = path8.join(runsDir(root), runId, "events.jsonl");
|
|
11324
11435
|
const previous = await readTextIfExists(file) ?? "";
|
|
11325
11436
|
await writeText(file, `${previous}${JSON.stringify(event)}
|
|
11326
11437
|
`);
|
|
11327
11438
|
return event;
|
|
11328
11439
|
}
|
|
11329
11440
|
async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Date()) {
|
|
11330
|
-
const file =
|
|
11441
|
+
const file = path8.join(runsDir(root), runId, "state.json");
|
|
11331
11442
|
const raw = await readTextIfExists(file);
|
|
11332
11443
|
if (raw === void 0) return;
|
|
11333
11444
|
const state = JSON.parse(raw);
|
|
@@ -11337,13 +11448,13 @@ async function updateRunStatus(root, runId, status, now = /* @__PURE__ */ new Da
|
|
|
11337
11448
|
`);
|
|
11338
11449
|
}
|
|
11339
11450
|
async function readRun(root, runId) {
|
|
11340
|
-
const dir =
|
|
11341
|
-
const stateRaw = await readTextIfExists(
|
|
11451
|
+
const dir = path8.join(runsDir(root), runId);
|
|
11452
|
+
const stateRaw = await readTextIfExists(path8.join(dir, "state.json"));
|
|
11342
11453
|
if (stateRaw === void 0) {
|
|
11343
11454
|
return { diagnostics: [diag("ATLAS-RUN-001", "error", `No existe el run ${runId}`, { path: dir })] };
|
|
11344
11455
|
}
|
|
11345
11456
|
const state = JSON.parse(stateRaw);
|
|
11346
|
-
const eventsRaw = await readTextIfExists(
|
|
11457
|
+
const eventsRaw = await readTextIfExists(path8.join(dir, "events.jsonl")) ?? "";
|
|
11347
11458
|
const events = [];
|
|
11348
11459
|
for (const line of eventsRaw.split("\n")) {
|
|
11349
11460
|
if (line.trim() === "") continue;
|
|
@@ -11360,7 +11471,7 @@ async function listRuns(root, filter = {}) {
|
|
|
11360
11471
|
const ids = await listDirs(dir);
|
|
11361
11472
|
const out = [];
|
|
11362
11473
|
for (const id of ids) {
|
|
11363
|
-
const raw = await readTextIfExists(
|
|
11474
|
+
const raw = await readTextIfExists(path8.join(dir, id, "state.json"));
|
|
11364
11475
|
if (raw === void 0) continue;
|
|
11365
11476
|
try {
|
|
11366
11477
|
const state = JSON.parse(raw);
|
|
@@ -11395,20 +11506,20 @@ var mockupManifestSchema = z4.object({
|
|
|
11395
11506
|
});
|
|
11396
11507
|
var TOKEN_CANDIDATES = ["design/tokens.json", "DESIGN.md", "design/DESIGN.md", ".sdd/design/tokens.json"];
|
|
11397
11508
|
function mockupsDir(root, slug) {
|
|
11398
|
-
return
|
|
11509
|
+
return path9.join(path9.resolve(root), ".sdd", "changes", slug, "mockups");
|
|
11399
11510
|
}
|
|
11400
11511
|
async function tokensFile(root) {
|
|
11401
11512
|
for (const candidate of TOKEN_CANDIDATES) {
|
|
11402
|
-
const abs =
|
|
11513
|
+
const abs = path9.join(root, candidate);
|
|
11403
11514
|
if (await exists(abs)) return candidate;
|
|
11404
11515
|
}
|
|
11405
11516
|
return void 0;
|
|
11406
11517
|
}
|
|
11407
11518
|
async function computeInputsHash(root, change) {
|
|
11408
|
-
const deltaPath =
|
|
11519
|
+
const deltaPath = path9.join(change.dir, "spec.md");
|
|
11409
11520
|
const delta = await readTextIfExists(deltaPath) ?? "";
|
|
11410
11521
|
const tokens = await tokensFile(root);
|
|
11411
|
-
const tokensContent = tokens ? await readTextIfExists(
|
|
11522
|
+
const tokensContent = tokens ? await readTextIfExists(path9.join(root, tokens)) ?? "" : "";
|
|
11412
11523
|
return artifactHash(`${delta}
|
|
11413
11524
|
---tokens---
|
|
11414
11525
|
${tokensContent}`);
|
|
@@ -11433,7 +11544,7 @@ function planMockups(change, platform = "web") {
|
|
|
11433
11544
|
return { platform, screens };
|
|
11434
11545
|
}
|
|
11435
11546
|
async function readMockupManifest(root, slug) {
|
|
11436
|
-
const file =
|
|
11547
|
+
const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
|
|
11437
11548
|
const raw = await readTextIfExists(file);
|
|
11438
11549
|
if (raw === void 0) return { diagnostics: [] };
|
|
11439
11550
|
let data;
|
|
@@ -11525,7 +11636,7 @@ async function checkMockups(root, slug, change) {
|
|
|
11525
11636
|
const { manifest, path: manifestPath, diagnostics } = await readMockupManifest(root, slug);
|
|
11526
11637
|
const findings = [...diagnostics];
|
|
11527
11638
|
if (!manifest || !manifestPath) {
|
|
11528
|
-
return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path:
|
|
11639
|
+
return { findings: [...findings, diag("LINT-MKP-004", "error", "No existe mockups/manifest.yaml", { path: path9.join(dir, "manifest.yaml") })], stale: false };
|
|
11529
11640
|
}
|
|
11530
11641
|
const known = /* @__PURE__ */ new Set();
|
|
11531
11642
|
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
@@ -11533,12 +11644,12 @@ async function checkMockups(root, slug, change) {
|
|
|
11533
11644
|
}
|
|
11534
11645
|
findings.push(...lintMockupManifest(manifest, known, manifestPath));
|
|
11535
11646
|
for (const screen of manifest.screens) {
|
|
11536
|
-
const html = await readTextIfExists(
|
|
11647
|
+
const html = await readTextIfExists(path9.join(dir, screen.file));
|
|
11537
11648
|
if (html === void 0) {
|
|
11538
|
-
findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path:
|
|
11649
|
+
findings.push(diag("ATLAS-MKP-004", "error", `Falta el archivo del mockup: ${screen.file}`, { path: path9.join(dir, screen.file) }));
|
|
11539
11650
|
continue;
|
|
11540
11651
|
}
|
|
11541
|
-
findings.push(...lintMockupHtml(html,
|
|
11652
|
+
findings.push(...lintMockupHtml(html, path9.join(dir, screen.file)));
|
|
11542
11653
|
}
|
|
11543
11654
|
const inputsHash = await computeInputsHash(root, change);
|
|
11544
11655
|
const stale = manifest.inputsHash !== void 0 && manifest.inputsHash !== inputsHash;
|
|
@@ -11553,7 +11664,7 @@ async function checkMockups(root, slug, change) {
|
|
|
11553
11664
|
return { findings, stale, manifest, manifestPath };
|
|
11554
11665
|
}
|
|
11555
11666
|
async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date()) {
|
|
11556
|
-
const file =
|
|
11667
|
+
const file = path9.join(mockupsDir(root, slug), "plan.yaml");
|
|
11557
11668
|
if (await exists(file)) return file;
|
|
11558
11669
|
const doc = {
|
|
11559
11670
|
schema_version: 1,
|
|
@@ -11561,11 +11672,11 @@ async function writeMockupPlan(root, slug, plan, now = /* @__PURE__ */ new Date(
|
|
|
11561
11672
|
platform: plan.platform,
|
|
11562
11673
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints }))
|
|
11563
11674
|
};
|
|
11564
|
-
await writeText(file,
|
|
11675
|
+
await writeText(file, stringifyYaml5(doc, { lineWidth: 120 }));
|
|
11565
11676
|
return file;
|
|
11566
11677
|
}
|
|
11567
11678
|
async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PURE__ */ new Date()) {
|
|
11568
|
-
const file =
|
|
11679
|
+
const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
|
|
11569
11680
|
if (await exists(file)) return file;
|
|
11570
11681
|
const doc = {
|
|
11571
11682
|
schema_version: 1,
|
|
@@ -11577,16 +11688,16 @@ async function writeMockupManifest(root, slug, plan, inputsHash, now = /* @__PUR
|
|
|
11577
11688
|
screens: plan.screens.map((s) => ({ id: s.id, title: s.title, file: s.file, illustrates: s.illustrates, states: s.states, breakpoints: s.breakpoints })),
|
|
11578
11689
|
screenshots: []
|
|
11579
11690
|
};
|
|
11580
|
-
await writeText(file,
|
|
11691
|
+
await writeText(file, stringifyYaml5(doc, { lineWidth: 120 }));
|
|
11581
11692
|
return file;
|
|
11582
11693
|
}
|
|
11583
11694
|
async function updateMockupScreenshots(root, slug, screenshots) {
|
|
11584
|
-
const file =
|
|
11695
|
+
const file = path9.join(mockupsDir(root, slug), "manifest.yaml");
|
|
11585
11696
|
const raw = await readTextIfExists(file);
|
|
11586
11697
|
if (raw === void 0) return void 0;
|
|
11587
11698
|
const data = parseYaml6(raw) ?? {};
|
|
11588
11699
|
data["screenshots"] = screenshots;
|
|
11589
|
-
await writeText(file,
|
|
11700
|
+
await writeText(file, stringifyYaml5(data, { lineWidth: 120 }));
|
|
11590
11701
|
return file;
|
|
11591
11702
|
}
|
|
11592
11703
|
async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Date()) {
|
|
@@ -11611,9 +11722,9 @@ async function captureMockups(root, slug, manifest, now = /* @__PURE__ */ new Da
|
|
|
11611
11722
|
const page2 = await browser.newPage();
|
|
11612
11723
|
for (const bp of screen.breakpoints && screen.breakpoints.length > 0 ? screen.breakpoints : [1440]) {
|
|
11613
11724
|
await page2.setViewportSize({ width: bp, height: 900 });
|
|
11614
|
-
await page2.goto(`file://${
|
|
11615
|
-
const rel =
|
|
11616
|
-
await page2.screenshot({ path:
|
|
11725
|
+
await page2.goto(`file://${path9.join(dir, screen.file).split(path9.sep).join("/")}`);
|
|
11726
|
+
const rel = path9.posix.join("screens", `${screen.id}-${bp}.png`);
|
|
11727
|
+
await page2.screenshot({ path: path9.join(dir, rel) });
|
|
11617
11728
|
screenshots.push(rel);
|
|
11618
11729
|
}
|
|
11619
11730
|
await page2.close();
|
|
@@ -11629,7 +11740,7 @@ async function mockupsReady(root, slug, change) {
|
|
|
11629
11740
|
return Boolean(check.manifest && check.manifest.screens.length > 0 && !check.stale);
|
|
11630
11741
|
}
|
|
11631
11742
|
async function setMockupRequirement(root, slug, value) {
|
|
11632
|
-
const file =
|
|
11743
|
+
const file = path9.join(root, ".sdd", "changes", slug, "meta.yaml");
|
|
11633
11744
|
const raw = await readTextIfExists(file) ?? `schema_version: 1
|
|
11634
11745
|
slug: ${slug}
|
|
11635
11746
|
lane: standard
|
|
@@ -11859,12 +11970,12 @@ ${texts.scenarios}`);
|
|
|
11859
11970
|
}
|
|
11860
11971
|
}
|
|
11861
11972
|
async function loadProjectPacks(sddDir) {
|
|
11862
|
-
const dir =
|
|
11973
|
+
const dir = path10.join(sddDir, "packs");
|
|
11863
11974
|
const diagnostics = [];
|
|
11864
11975
|
const packs = [];
|
|
11865
11976
|
for (const entry of await listDir(dir)) {
|
|
11866
11977
|
if (!/\.ya?ml$/i.test(entry)) continue;
|
|
11867
|
-
const file =
|
|
11978
|
+
const file = path10.join(dir, entry);
|
|
11868
11979
|
const raw = await readTextIfExists(file);
|
|
11869
11980
|
if (raw === void 0) continue;
|
|
11870
11981
|
let data;
|
|
@@ -11951,7 +12062,7 @@ function packFindings(evaluations, change) {
|
|
|
11951
12062
|
}
|
|
11952
12063
|
var UI_DOMAINS = /* @__PURE__ */ new Set(["frontend", "mobile", "fullstack"]);
|
|
11953
12064
|
async function runAnalyze(opts) {
|
|
11954
|
-
const root =
|
|
12065
|
+
const root = path11.resolve(opts.root);
|
|
11955
12066
|
const { workspace, config } = await loadWorkspace(root);
|
|
11956
12067
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
11957
12068
|
if (!change) {
|
|
@@ -11969,7 +12080,7 @@ async function runAnalyze(opts) {
|
|
|
11969
12080
|
for (const req of spec.spec.requirements) living.set(req.id, req);
|
|
11970
12081
|
}
|
|
11971
12082
|
if (change.delta) {
|
|
11972
|
-
findings.push(...lintDelta(change.delta, living,
|
|
12083
|
+
findings.push(...lintDelta(change.delta, living, path11.join(change.dir, "spec.md"), { language: config.spec.language }));
|
|
11973
12084
|
}
|
|
11974
12085
|
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false });
|
|
11975
12086
|
findings.push(...trace.findings);
|
|
@@ -12018,7 +12129,7 @@ async function runAnalyze(opts) {
|
|
|
12018
12129
|
if (mockups) result.mockups = mockups;
|
|
12019
12130
|
if (packs) result.packs = packs;
|
|
12020
12131
|
if (opts.write !== false) {
|
|
12021
|
-
const file =
|
|
12132
|
+
const file = path11.join(change.dir, "analyze.md");
|
|
12022
12133
|
await writeText(file, renderAnalyze(change, result, localStamp(opts.now)));
|
|
12023
12134
|
result.path = file;
|
|
12024
12135
|
}
|
|
@@ -12060,7 +12171,7 @@ function renderAnalyze(change, result, generatedAt) {
|
|
|
12060
12171
|
return lines.join("\n");
|
|
12061
12172
|
}
|
|
12062
12173
|
async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
12063
|
-
const resolved =
|
|
12174
|
+
const resolved = path12.resolve(root);
|
|
12064
12175
|
const { workspace, config } = await loadWorkspace(resolved);
|
|
12065
12176
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
12066
12177
|
const living = /* @__PURE__ */ new Map();
|
|
@@ -12075,7 +12186,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
12075
12186
|
const deltaContent = await readDelta(change);
|
|
12076
12187
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
12077
12188
|
const findings = [];
|
|
12078
|
-
if (change.delta) findings.push(...lintDelta(change.delta, living,
|
|
12189
|
+
if (change.delta) findings.push(...lintDelta(change.delta, living, path12.join(change.dir, "spec.md"), { language: config.spec.language }));
|
|
12079
12190
|
const trace = checkTrace({
|
|
12080
12191
|
specs: workspace.specs,
|
|
12081
12192
|
change,
|
|
@@ -12140,7 +12251,7 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
12140
12251
|
attention.push({ slug: change.slug, kind: "missing-evidence", detail: `evidencia ${change.scenariosPassed}/${change.scenariosTotal}` });
|
|
12141
12252
|
}
|
|
12142
12253
|
}
|
|
12143
|
-
const archiveDir =
|
|
12254
|
+
const archiveDir = path12.join(workspace.sddDir, "changes", "archive");
|
|
12144
12255
|
const archivedEntries = await listDirs(archiveDir);
|
|
12145
12256
|
const throughputByMonth = {};
|
|
12146
12257
|
for (const entry of archivedEntries) {
|
|
@@ -12178,10 +12289,10 @@ async function collectMetrics(root, now = /* @__PURE__ */ new Date()) {
|
|
|
12178
12289
|
}
|
|
12179
12290
|
async function readDelta(change) {
|
|
12180
12291
|
const { readTextIfExists: readTextIfExists2 } = await import("./fsx-VF2P7ALA-XKEWTMDH.js");
|
|
12181
|
-
return readTextIfExists2(
|
|
12292
|
+
return readTextIfExists2(path12.join(change.dir, "spec.md"));
|
|
12182
12293
|
}
|
|
12183
12294
|
async function generatePresentation(opts) {
|
|
12184
|
-
const root =
|
|
12295
|
+
const root = path13.resolve(opts.root);
|
|
12185
12296
|
const { workspace, config } = await loadWorkspace(root);
|
|
12186
12297
|
const language = config.project.language;
|
|
12187
12298
|
const change = workspace.changes.find((c) => c.slug === opts.slug);
|
|
@@ -12191,17 +12302,17 @@ async function generatePresentation(opts) {
|
|
|
12191
12302
|
if (!change.delta) {
|
|
12192
12303
|
return { slug: opts.slug, diagnostics: [diag("ATLAS-PRESENT-001", "error", `El cambio "${opts.slug}" no tiene spec.md (delta)`)] };
|
|
12193
12304
|
}
|
|
12194
|
-
const deltaPath =
|
|
12305
|
+
const deltaPath = path13.join(change.dir, "spec.md");
|
|
12195
12306
|
const deltaContent = await readTextIfExists(deltaPath) ?? "";
|
|
12196
12307
|
const hash = artifactHash(deltaContent);
|
|
12197
12308
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
12198
|
-
const presentationDir =
|
|
12199
|
-
const mockupsCopyDir =
|
|
12309
|
+
const presentationDir = path13.join(change.dir, "presentation");
|
|
12310
|
+
const mockupsCopyDir = path13.join(presentationDir, "mockups");
|
|
12200
12311
|
await ensureDir(presentationDir);
|
|
12201
12312
|
const mockupInfo = await copyMockups(root, change, mockupsCopyDir);
|
|
12202
|
-
const proposalRaw = await readTextIfExists(
|
|
12313
|
+
const proposalRaw = await readTextIfExists(path13.join(change.dir, "proposal.md"));
|
|
12203
12314
|
const proposalBody = proposalRaw ? parseFrontmatter(proposalRaw).body : "";
|
|
12204
|
-
const approvals = await loadApprovals(
|
|
12315
|
+
const approvals = await loadApprovals(path13.join(root, ".sdd"));
|
|
12205
12316
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent);
|
|
12206
12317
|
const labels = labelsFor(language);
|
|
12207
12318
|
const html = page({
|
|
@@ -12222,7 +12333,7 @@ async function generatePresentation(opts) {
|
|
|
12222
12333
|
approveHint: `satlas approve ${change.slug} --by "<nombre>" --channel presentation`,
|
|
12223
12334
|
...approval.status === "valid" && approval.approvedBy ? { approval: { by: approval.approvedBy, at: approval.approvedAt ?? "" } } : {}
|
|
12224
12335
|
});
|
|
12225
|
-
const outFile =
|
|
12336
|
+
const outFile = path13.join(presentationDir, "index.html");
|
|
12226
12337
|
await writeText(outFile, html);
|
|
12227
12338
|
return { slug: change.slug, path: outFile, hash, diagnostics: mockupInfo.diagnostics };
|
|
12228
12339
|
}
|
|
@@ -12234,12 +12345,12 @@ async function copyMockups(root, change, destDir) {
|
|
|
12234
12345
|
await ensureDir(destDir);
|
|
12235
12346
|
const screens = [];
|
|
12236
12347
|
for (const screen of manifest.screens) {
|
|
12237
|
-
const src =
|
|
12348
|
+
const src = path13.join(dir, screen.file);
|
|
12238
12349
|
if (!await exists(src)) {
|
|
12239
12350
|
diagnostics.push(diag("ATLAS-PRESENT-002", "warning", `El mockup ${screen.file} no existe y no se incluir\xE1`, { path: src }));
|
|
12240
12351
|
continue;
|
|
12241
12352
|
}
|
|
12242
|
-
await copyFile(src,
|
|
12353
|
+
await copyFile(src, path13.join(destDir, screen.file));
|
|
12243
12354
|
screens.push({
|
|
12244
12355
|
id: screen.id,
|
|
12245
12356
|
title: screen.title ?? screen.id,
|
|
@@ -12249,13 +12360,13 @@ async function copyMockups(root, change, destDir) {
|
|
|
12249
12360
|
});
|
|
12250
12361
|
}
|
|
12251
12362
|
const screenshots = [];
|
|
12252
|
-
const shotsDir =
|
|
12363
|
+
const shotsDir = path13.join(dir, "screens");
|
|
12253
12364
|
if (await exists(shotsDir)) {
|
|
12254
|
-
await ensureDir(
|
|
12365
|
+
await ensureDir(path13.join(destDir, "screens"));
|
|
12255
12366
|
const { listDir: listDir4 } = await import("./fsx-VF2P7ALA-XKEWTMDH.js");
|
|
12256
12367
|
for (const file of await listDir4(shotsDir)) {
|
|
12257
12368
|
if (!/\.(png|jpe?g|webp)$/i.test(file)) continue;
|
|
12258
|
-
await copyFile(
|
|
12369
|
+
await copyFile(path13.join(shotsDir, file), path13.join(destDir, "screens", file));
|
|
12259
12370
|
screenshots.push(`mockups/screens/${file}`);
|
|
12260
12371
|
}
|
|
12261
12372
|
}
|
|
@@ -12471,11 +12582,11 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([
|
|
|
12471
12582
|
".groovy"
|
|
12472
12583
|
]);
|
|
12473
12584
|
async function adoptWorkspace(opts) {
|
|
12474
|
-
const root =
|
|
12585
|
+
const root = path14.resolve(opts.root);
|
|
12475
12586
|
const dryRun = opts.dryRun ?? false;
|
|
12476
12587
|
const diagnostics = [];
|
|
12477
|
-
const sddDir =
|
|
12478
|
-
if (!await exists(
|
|
12588
|
+
const sddDir = path14.join(root, ".sdd");
|
|
12589
|
+
if (!await exists(path14.join(sddDir, "config.yaml"))) {
|
|
12479
12590
|
return {
|
|
12480
12591
|
root,
|
|
12481
12592
|
domains: [],
|
|
@@ -12486,8 +12597,8 @@ async function adoptWorkspace(opts) {
|
|
|
12486
12597
|
}
|
|
12487
12598
|
const { config } = await loadConfig(sddDir);
|
|
12488
12599
|
const files = await walkFiles(root, { skipDirs: [...SKIP_DIRS] });
|
|
12489
|
-
const inventoried = files.map((f) => toPosix(
|
|
12490
|
-
const relative = inventoried.filter((f) => SOURCE_EXT.has(
|
|
12600
|
+
const inventoried = files.map((f) => toPosix(path14.relative(root, f.path)));
|
|
12601
|
+
const relative = inventoried.filter((f) => SOURCE_EXT.has(path14.extname(f).toLowerCase()));
|
|
12491
12602
|
if (relative.length === 0) {
|
|
12492
12603
|
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>" }));
|
|
12493
12604
|
}
|
|
@@ -12515,13 +12626,13 @@ async function adoptWorkspace(opts) {
|
|
|
12515
12626
|
const createdSpecs = [];
|
|
12516
12627
|
for (const name of domainNames.sort()) {
|
|
12517
12628
|
const domainFiles = (counts.get(name) ?? []).sort();
|
|
12518
|
-
const existingSpec = await exists(
|
|
12629
|
+
const existingSpec = await exists(path14.join(sddDir, "specs", name, "spec.md"));
|
|
12519
12630
|
domains.push({ name, files: domainFiles.length, samples: domainFiles.slice(0, 8), existingSpec });
|
|
12520
12631
|
if (existingSpec) {
|
|
12521
|
-
diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path:
|
|
12632
|
+
diagnostics.push(diag("ATLAS-ADOPT-003", "info", `La spec de "${name}" ya existe; se conserva`, { path: path14.join(sddDir, "specs", name, "spec.md") }));
|
|
12522
12633
|
continue;
|
|
12523
12634
|
}
|
|
12524
|
-
const specPath =
|
|
12635
|
+
const specPath = path14.join(sddDir, "specs", name, "spec.md");
|
|
12525
12636
|
if (!dryRun) {
|
|
12526
12637
|
await writeText(specPath, baselineSpec(name, domainFiles));
|
|
12527
12638
|
createdSpecs.push(specPath);
|
|
@@ -12529,7 +12640,7 @@ async function adoptWorkspace(opts) {
|
|
|
12529
12640
|
createdSpecs.push(specPath);
|
|
12530
12641
|
}
|
|
12531
12642
|
}
|
|
12532
|
-
const reportPath =
|
|
12643
|
+
const reportPath = path14.join(sddDir, "adopt-report.md");
|
|
12533
12644
|
if (!dryRun) {
|
|
12534
12645
|
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() }));
|
|
12535
12646
|
}
|
|
@@ -12549,12 +12660,12 @@ async function loadProjectProfiles(root, dirs) {
|
|
|
12549
12660
|
if (!await exists(dir)) continue;
|
|
12550
12661
|
out.push(...await loadProfilesFromDir(dir));
|
|
12551
12662
|
}
|
|
12552
|
-
const custom =
|
|
12663
|
+
const custom = path14.join(root, ".sdd", "profiles", "custom");
|
|
12553
12664
|
if (await exists(custom)) out.push(...await loadProfilesFromDir(custom));
|
|
12554
12665
|
return out;
|
|
12555
12666
|
}
|
|
12556
12667
|
async function fallbackDomainName(root, projectName) {
|
|
12557
|
-
const pkg = await readTextIfExists(
|
|
12668
|
+
const pkg = await readTextIfExists(path14.join(root, "package.json"));
|
|
12558
12669
|
if (pkg) {
|
|
12559
12670
|
try {
|
|
12560
12671
|
const data = JSON.parse(pkg);
|
|
@@ -12641,11 +12752,11 @@ function adoptReport(input) {
|
|
|
12641
12752
|
return lines.join("\n");
|
|
12642
12753
|
}
|
|
12643
12754
|
async function initWorkspace(opts) {
|
|
12644
|
-
const root =
|
|
12645
|
-
const sddDir =
|
|
12755
|
+
const root = path15.resolve(opts.root);
|
|
12756
|
+
const sddDir = path15.join(root, ".sdd");
|
|
12646
12757
|
const diagnostics = [];
|
|
12647
12758
|
const created = [];
|
|
12648
|
-
if (await exists(
|
|
12759
|
+
if (await exists(path15.join(sddDir, "config.yaml"))) {
|
|
12649
12760
|
return {
|
|
12650
12761
|
sddDir,
|
|
12651
12762
|
created,
|
|
@@ -12655,29 +12766,29 @@ async function initWorkspace(opts) {
|
|
|
12655
12766
|
created.push(...await ensureSddDirs(sddDir));
|
|
12656
12767
|
const language = opts.language ?? "es";
|
|
12657
12768
|
const cfg = defaultConfig({ name: opts.name, language });
|
|
12658
|
-
cfg.project.name = opts.name ??
|
|
12769
|
+
cfg.project.name = opts.name ?? path15.basename(root);
|
|
12659
12770
|
await writeConfig(sddDir, cfg);
|
|
12660
|
-
created.push(
|
|
12771
|
+
created.push(path15.join(sddDir, "config.yaml"));
|
|
12661
12772
|
const templates = templatesFor(language);
|
|
12662
|
-
await writeText(
|
|
12663
|
-
created.push(
|
|
12664
|
-
await writeText(
|
|
12665
|
-
created.push(
|
|
12773
|
+
await writeText(path15.join(sddDir, "constitution.md"), templates.constitution);
|
|
12774
|
+
created.push(path15.join(sddDir, "constitution.md"));
|
|
12775
|
+
await writeText(path15.join(sddDir, "glossary.md"), templates.glossary);
|
|
12776
|
+
created.push(path15.join(sddDir, "glossary.md"));
|
|
12666
12777
|
const detected = await detectIfPossible(root, opts.profilesDir);
|
|
12667
12778
|
if (detected) {
|
|
12668
12779
|
const yaml2 = detectionToYaml(detected, localStamp(opts.now));
|
|
12669
|
-
await writeText(
|
|
12670
|
-
created.push(
|
|
12780
|
+
await writeText(path15.join(sddDir, "profiles", "detected.yaml"), yaml2);
|
|
12781
|
+
created.push(path15.join(sddDir, "profiles", "detected.yaml"));
|
|
12671
12782
|
if (detected.best) {
|
|
12672
12783
|
diagnostics.push(diag("ATLAS-INIT-002", "info", `Stack detectado: ${detected.best.displayName} (${detected.best.score} puntos)`, { suggestion: "Revisa .sdd/profiles/detected.yaml" }));
|
|
12673
12784
|
} else {
|
|
12674
12785
|
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/" }));
|
|
12675
12786
|
}
|
|
12676
12787
|
}
|
|
12677
|
-
await writeText(
|
|
12678
|
-
created.push(
|
|
12788
|
+
await writeText(path15.join(sddDir, "INDEX.md"), initialIndex(language, cfg.project.name));
|
|
12789
|
+
created.push(path15.join(sddDir, "INDEX.md"));
|
|
12679
12790
|
if (opts.local) {
|
|
12680
|
-
const gitignore =
|
|
12791
|
+
const gitignore = path15.join(root, ".gitignore");
|
|
12681
12792
|
const current = await readTextIfExists(gitignore) ?? "";
|
|
12682
12793
|
if (!/(^|\n)\.sdd\/?(\n|$)/.test(current)) {
|
|
12683
12794
|
const next = current.endsWith("\n") || current === "" ? `${current}.sdd/
|
|
@@ -12715,7 +12826,7 @@ async function createChange(opts) {
|
|
|
12715
12826
|
const diagnostics = [];
|
|
12716
12827
|
const slug = opts.slug.trim().toLowerCase();
|
|
12717
12828
|
const language = opts.language ?? opts.cfg?.project.language ?? "es";
|
|
12718
|
-
const dir =
|
|
12829
|
+
const dir = path16.join(path16.resolve(opts.root), ".sdd", "changes", slug);
|
|
12719
12830
|
if (!SLUG_RE.test(slug)) {
|
|
12720
12831
|
return {
|
|
12721
12832
|
slug,
|
|
@@ -12746,20 +12857,20 @@ async function createChange(opts) {
|
|
|
12746
12857
|
await ensureDir(dir);
|
|
12747
12858
|
const templates = templatesFor(language);
|
|
12748
12859
|
const files = [];
|
|
12749
|
-
const metaFile =
|
|
12860
|
+
const metaFile = path16.join(dir, "meta.yaml");
|
|
12750
12861
|
await writeText(metaFile, changeMetaYaml(meta));
|
|
12751
12862
|
files.push(metaFile);
|
|
12752
12863
|
if (lane === "fix") {
|
|
12753
|
-
const fixFile =
|
|
12864
|
+
const fixFile = path16.join(dir, "fix.md");
|
|
12754
12865
|
await writeText(fixFile, renderTemplate(templates.fix, { TITLE: title }));
|
|
12755
12866
|
files.push(fixFile);
|
|
12756
12867
|
return { slug, dir, files, diagnostics };
|
|
12757
12868
|
}
|
|
12758
|
-
const proposalFile =
|
|
12869
|
+
const proposalFile = path16.join(dir, "proposal.md");
|
|
12759
12870
|
await writeText(proposalFile, renderTemplate(templates.proposal, { TITLE: title }));
|
|
12760
12871
|
files.push(proposalFile);
|
|
12761
12872
|
const domainUpper = domain.replace(/[^a-z0-9]/gi, "").toUpperCase() || "GEN";
|
|
12762
|
-
const deltaFile =
|
|
12873
|
+
const deltaFile = path16.join(dir, "spec.md");
|
|
12763
12874
|
await writeText(deltaFile, templates.specDelta({ title, domainUpper, domain }));
|
|
12764
12875
|
files.push(deltaFile);
|
|
12765
12876
|
return { slug, dir, files, diagnostics };
|
|
@@ -12872,7 +12983,7 @@ async function restoreFile(file, previous) {
|
|
|
12872
12983
|
}
|
|
12873
12984
|
}
|
|
12874
12985
|
async function archiveChange(opts) {
|
|
12875
|
-
const root =
|
|
12986
|
+
const root = path17.resolve(opts.root);
|
|
12876
12987
|
const diagnostics = [];
|
|
12877
12988
|
const { config } = await loadWorkspace(root);
|
|
12878
12989
|
const language = opts.language ?? config.project.language;
|
|
@@ -12888,16 +12999,43 @@ async function archiveChange(opts) {
|
|
|
12888
12999
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
12889
13000
|
}
|
|
12890
13001
|
const nowFix = opts.now ?? /* @__PURE__ */ new Date();
|
|
12891
|
-
const targetFix =
|
|
13002
|
+
const targetFix = path17.join(root, ".sdd", "changes", "archive", `${localMonth(nowFix)}-${opts.slug}`);
|
|
12892
13003
|
if (await exists(targetFix)) {
|
|
12893
13004
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${targetFix}`, { path: targetFix }));
|
|
12894
13005
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
12895
13006
|
}
|
|
13007
|
+
let livingFix;
|
|
13008
|
+
let livingCreated = false;
|
|
13009
|
+
if (!opts.dryRun) {
|
|
13010
|
+
const fixRaw = await readTextIfExists(path17.join(change.dir, "fix.md")) ?? "";
|
|
13011
|
+
try {
|
|
13012
|
+
const written = await writeLivingFix(root, {
|
|
13013
|
+
slug: opts.slug,
|
|
13014
|
+
date: localDate(nowFix),
|
|
13015
|
+
result: "pass",
|
|
13016
|
+
...change.meta.domain !== void 0 ? { domain: change.meta.domain } : {},
|
|
13017
|
+
...change.meta.title !== void 0 ? { title: change.meta.title } : {},
|
|
13018
|
+
covers: change.fixCovers ?? [],
|
|
13019
|
+
content: fixRaw
|
|
13020
|
+
});
|
|
13021
|
+
livingFix = written.relativePath;
|
|
13022
|
+
livingCreated = written.created;
|
|
13023
|
+
} catch (error) {
|
|
13024
|
+
diagnostics.push(
|
|
13025
|
+
diag("ATLAS-ARCH-006", "error", `No se pudo conservar el fix vivo: ${error.message}`, {
|
|
13026
|
+
path: path17.join(root, ".sdd", "fixes"),
|
|
13027
|
+
suggestion: "Revisa los permisos de .sdd/fixes y vuelve a intentar; el fix sigue sin archivar"
|
|
13028
|
+
})
|
|
13029
|
+
);
|
|
13030
|
+
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: false };
|
|
13031
|
+
}
|
|
13032
|
+
}
|
|
12896
13033
|
if (!opts.dryRun) {
|
|
12897
|
-
await ensureDir(
|
|
13034
|
+
await ensureDir(path17.dirname(targetFix));
|
|
12898
13035
|
try {
|
|
12899
13036
|
await moveDirectory(change.dir, targetFix);
|
|
12900
13037
|
} catch (error) {
|
|
13038
|
+
if (livingCreated && livingFix) await removeFile(path17.join(root, livingFix));
|
|
12901
13039
|
diagnostics.push(
|
|
12902
13040
|
diag("ATLAS-ARCH-004", "error", `No se pudo mover el cambio al hist\xF3rico: ${error.message}`, {
|
|
12903
13041
|
path: change.dir,
|
|
@@ -12906,9 +13044,16 @@ async function archiveChange(opts) {
|
|
|
12906
13044
|
);
|
|
12907
13045
|
return { slug: opts.slug, fold: emptyFold, diagnostics, dryRun: false };
|
|
12908
13046
|
}
|
|
12909
|
-
await regenerateIndex(root, config);
|
|
13047
|
+
await regenerateIndex(root, config, nowFix);
|
|
12910
13048
|
}
|
|
12911
|
-
return {
|
|
13049
|
+
return {
|
|
13050
|
+
slug: opts.slug,
|
|
13051
|
+
archivedTo: targetFix,
|
|
13052
|
+
...livingFix !== void 0 ? { livingFix } : {},
|
|
13053
|
+
fold: emptyFold,
|
|
13054
|
+
diagnostics,
|
|
13055
|
+
dryRun: opts.dryRun ?? false
|
|
13056
|
+
};
|
|
12912
13057
|
}
|
|
12913
13058
|
if (!change.delta) {
|
|
12914
13059
|
diagnostics.push(diag("ATLAS-ARCH-003", "error", `El cambio "${opts.slug}" no tiene spec.md (delta): no hay nada que plegar`, { path: change.dir }));
|
|
@@ -12916,10 +13061,10 @@ async function archiveChange(opts) {
|
|
|
12916
13061
|
}
|
|
12917
13062
|
const domain = change.meta.domain;
|
|
12918
13063
|
if (!domain) {
|
|
12919
|
-
diagnostics.push(diag("ATLAS-ARCH-001", "error", `El cambio "${opts.slug}" no declara domain en meta.yaml`, { path:
|
|
13064
|
+
diagnostics.push(diag("ATLAS-ARCH-001", "error", `El cambio "${opts.slug}" no declara domain en meta.yaml`, { path: path17.join(change.dir, "meta.yaml") }));
|
|
12920
13065
|
return { slug: opts.slug, fold: { content: "", applied: { added: [], modified: [], removed: [], renamed: [] }, diagnostics }, diagnostics, dryRun: opts.dryRun ?? false };
|
|
12921
13066
|
}
|
|
12922
|
-
const specFile =
|
|
13067
|
+
const specFile = path17.join(root, ".sdd", "specs", domain, "spec.md");
|
|
12923
13068
|
const existing = await readTextIfExists(specFile);
|
|
12924
13069
|
const base = existing ?? `---
|
|
12925
13070
|
domain: ${domain}
|
|
@@ -12949,8 +13094,8 @@ ${Object.entries(nextFm).map(([k, v]) => `${k}: ${String(v)}`).join("\n")}
|
|
|
12949
13094
|
|
|
12950
13095
|
${body}`;
|
|
12951
13096
|
const month = localMonth(now);
|
|
12952
|
-
const archiveDir =
|
|
12953
|
-
const target =
|
|
13097
|
+
const archiveDir = path17.join(root, ".sdd", "changes", "archive");
|
|
13098
|
+
const target = path17.join(archiveDir, `${month}-${opts.slug}`);
|
|
12954
13099
|
if (await exists(target)) {
|
|
12955
13100
|
diagnostics.push(diag("ATLAS-ARCH-002", "error", `Ya existe un cambio archivado en ${target}`, { path: target }));
|
|
12956
13101
|
return { slug: opts.slug, domain, fold, diagnostics, dryRun: opts.dryRun ?? false };
|
|
@@ -12976,17 +13121,370 @@ ${body}`;
|
|
|
12976
13121
|
}
|
|
12977
13122
|
async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
|
|
12978
13123
|
const { workspace, config } = cfg ? { workspace: (await loadWorkspace(root)).workspace, config: cfg } : await loadWorkspace(root);
|
|
12979
|
-
const archiveDir =
|
|
13124
|
+
const archiveDir = path17.join(root, ".sdd", "changes", "archive");
|
|
12980
13125
|
const archived = await exists(archiveDir) ? (await fs.readdir(archiveDir)).filter((e) => !e.startsWith(".")).length : 0;
|
|
13126
|
+
const fixes = await loadLivingFixes(root);
|
|
12981
13127
|
const markdown2 = indexMarkdown({
|
|
12982
13128
|
projectName: config.project.name,
|
|
12983
13129
|
language: config.project.language,
|
|
12984
13130
|
specs: workspace.specs.map((s) => ({ domain: s.domain, requirements: s.spec.requirements.length })),
|
|
12985
13131
|
changes: workspace.changes.map((c) => ({ slug: c.slug, lane: c.meta?.lane ?? config.lanes.default })),
|
|
13132
|
+
fixes: fixes.map((fix) => ({ slug: fix.slug, date: fix.date, result: fix.result, ...fix.domain !== void 0 ? { domain: fix.domain } : {} })),
|
|
12986
13133
|
archived
|
|
12987
13134
|
});
|
|
12988
13135
|
void now;
|
|
12989
|
-
await writeText(
|
|
13136
|
+
await writeText(path17.join(root, ".sdd", "INDEX.md"), markdown2);
|
|
13137
|
+
}
|
|
13138
|
+
async function removeFile(file) {
|
|
13139
|
+
try {
|
|
13140
|
+
await rm(file, { force: true });
|
|
13141
|
+
} catch {
|
|
13142
|
+
}
|
|
13143
|
+
}
|
|
13144
|
+
var SCHEMA_VERSION = 1;
|
|
13145
|
+
var BACKUP_DIRNAME = ".backup";
|
|
13146
|
+
var BACKUP_POINTER = ".latest";
|
|
13147
|
+
var SCHEMA_MIGRATIONS = [
|
|
13148
|
+
{
|
|
13149
|
+
id: "0001-sellar-version-de-esquema",
|
|
13150
|
+
description: "Sella la versi\xF3n de esquema vigente en los elementos que no la declaran",
|
|
13151
|
+
from: 0,
|
|
13152
|
+
to: SCHEMA_VERSION
|
|
13153
|
+
}
|
|
13154
|
+
];
|
|
13155
|
+
var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml", path18.join("profiles", "detected.yaml")];
|
|
13156
|
+
var CHANGE_ARTIFACTS = ["meta.yaml", path18.join("mockups", "manifest.yaml")];
|
|
13157
|
+
async function pushChangeArtifacts(out, sddDir, relDir) {
|
|
13158
|
+
for (const rel of CHANGE_ARTIFACTS) {
|
|
13159
|
+
const abs = path18.join(sddDir, relDir, rel);
|
|
13160
|
+
if (await exists(abs)) out.push({ artifact: toPosix(path18.join(relDir, rel)), path: abs });
|
|
13161
|
+
}
|
|
13162
|
+
}
|
|
13163
|
+
async function collectVersionedArtifacts(root) {
|
|
13164
|
+
const sddDir = path18.join(root, ".sdd");
|
|
13165
|
+
const out = [];
|
|
13166
|
+
for (const rel of FIXED_ARTIFACTS) {
|
|
13167
|
+
const abs = path18.join(sddDir, rel);
|
|
13168
|
+
if (await exists(abs)) out.push({ artifact: toPosix(rel), path: abs });
|
|
13169
|
+
}
|
|
13170
|
+
const changesDir = path18.join(sddDir, "changes");
|
|
13171
|
+
for (const slug of await listDirs(changesDir)) {
|
|
13172
|
+
if (slug === "archive") continue;
|
|
13173
|
+
await pushChangeArtifacts(out, sddDir, path18.join("changes", slug));
|
|
13174
|
+
}
|
|
13175
|
+
for (const entry of await listDirs(path18.join(changesDir, "archive"))) {
|
|
13176
|
+
await pushChangeArtifacts(out, sddDir, path18.join("changes", "archive", entry));
|
|
13177
|
+
}
|
|
13178
|
+
return out;
|
|
13179
|
+
}
|
|
13180
|
+
function readArtifactVersion(content) {
|
|
13181
|
+
let data;
|
|
13182
|
+
try {
|
|
13183
|
+
data = parseYaml8(content);
|
|
13184
|
+
} catch (err) {
|
|
13185
|
+
return { state: "unreadable", reason: `YAML inv\xE1lido (${err.message})` };
|
|
13186
|
+
}
|
|
13187
|
+
if (data === null || data === void 0) return { state: "absent" };
|
|
13188
|
+
if (typeof data !== "object" || Array.isArray(data)) return { state: "unreadable", reason: "la ra\xEDz no es un mapa de claves y valores" };
|
|
13189
|
+
const raw = data["schema_version"];
|
|
13190
|
+
if (raw === void 0) return { state: "absent" };
|
|
13191
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) return { state: "unreadable", reason: `schema_version con valor no v\xE1lido: ${String(raw)}` };
|
|
13192
|
+
return { state: "ok", version: raw };
|
|
13193
|
+
}
|
|
13194
|
+
function stampSchemaVersion(content, version) {
|
|
13195
|
+
const line = `schema_version: ${version}`;
|
|
13196
|
+
const bom = content.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
13197
|
+
const body = bom ? content.slice(1) : content;
|
|
13198
|
+
if (body.trim() === "") return `${bom}${line}
|
|
13199
|
+
`;
|
|
13200
|
+
const eol = body.includes("\r\n") ? "\r\n" : "\n";
|
|
13201
|
+
const lines = body.split(/\r?\n/);
|
|
13202
|
+
const trailing = lines.length > 1 && lines[lines.length - 1] === "";
|
|
13203
|
+
let index = 0;
|
|
13204
|
+
while (index < lines.length) {
|
|
13205
|
+
const trimmed = (lines[index] ?? "").trim();
|
|
13206
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
13207
|
+
index += 1;
|
|
13208
|
+
continue;
|
|
13209
|
+
}
|
|
13210
|
+
break;
|
|
13211
|
+
}
|
|
13212
|
+
if ((lines[index] ?? "").trim() === "---") index += 1;
|
|
13213
|
+
const insertAt = trailing ? Math.min(index, lines.length - 1) : index;
|
|
13214
|
+
lines.splice(insertAt, 0, line);
|
|
13215
|
+
const next = bom + lines.join(eol);
|
|
13216
|
+
const check = readArtifactVersion(next);
|
|
13217
|
+
if (check.state !== "ok" || check.version !== version) return void 0;
|
|
13218
|
+
return next;
|
|
13219
|
+
}
|
|
13220
|
+
async function planUpgrade(root) {
|
|
13221
|
+
const artifacts = await collectVersionedArtifacts(root);
|
|
13222
|
+
const pending = [];
|
|
13223
|
+
const newer = [];
|
|
13224
|
+
const unreadable = [];
|
|
13225
|
+
for (const found of artifacts) {
|
|
13226
|
+
const content = await readTextIfExists(found.path);
|
|
13227
|
+
if (content === void 0) continue;
|
|
13228
|
+
const state = readArtifactVersion(content);
|
|
13229
|
+
if (state.state === "unreadable") {
|
|
13230
|
+
unreadable.push({ ...found, reason: state.reason });
|
|
13231
|
+
continue;
|
|
13232
|
+
}
|
|
13233
|
+
if (state.state === "ok") {
|
|
13234
|
+
if (state.version > SCHEMA_VERSION) {
|
|
13235
|
+
newer.push({ ...found, reason: `produce la versi\xF3n ${state.version}, m\xE1s nueva que la vigente (${SCHEMA_VERSION})` });
|
|
13236
|
+
continue;
|
|
13237
|
+
}
|
|
13238
|
+
if (state.version === SCHEMA_VERSION) continue;
|
|
13239
|
+
}
|
|
13240
|
+
const from = state.state === "ok" ? state.version : 0;
|
|
13241
|
+
const migration = SCHEMA_MIGRATIONS.find((m) => m.from === from);
|
|
13242
|
+
if (!migration) {
|
|
13243
|
+
unreadable.push({ ...found, reason: `no hay migraci\xF3n registrada desde la versi\xF3n ${from}` });
|
|
13244
|
+
continue;
|
|
13245
|
+
}
|
|
13246
|
+
const stamped = stampSchemaVersion(content, migration.to);
|
|
13247
|
+
if (stamped === void 0) {
|
|
13248
|
+
unreadable.push({ ...found, reason: "no se pudo sellar con seguridad sin alterar el resto del contenido" });
|
|
13249
|
+
continue;
|
|
13250
|
+
}
|
|
13251
|
+
pending.push({ ...found, from, to: migration.to, migration: migration.id, summary: migration.description });
|
|
13252
|
+
}
|
|
13253
|
+
return { root, currentVersion: SCHEMA_VERSION, pending, newer, unreadable, upToDate: pending.length === 0 };
|
|
13254
|
+
}
|
|
13255
|
+
async function removeDir(dir) {
|
|
13256
|
+
try {
|
|
13257
|
+
await fs2.rm(dir, { recursive: true, force: true });
|
|
13258
|
+
} catch {
|
|
13259
|
+
}
|
|
13260
|
+
}
|
|
13261
|
+
async function removeFile2(file) {
|
|
13262
|
+
try {
|
|
13263
|
+
await fs2.rm(file, { force: true });
|
|
13264
|
+
} catch {
|
|
13265
|
+
}
|
|
13266
|
+
}
|
|
13267
|
+
function backupName(now) {
|
|
13268
|
+
return `${localCompact(now)}${localOffset(now).replace(":", "")}`;
|
|
13269
|
+
}
|
|
13270
|
+
async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
13271
|
+
const plan = await planUpgrade(root);
|
|
13272
|
+
if (plan.pending.length === 0) return { status: "up-to-date", plan, applied: [] };
|
|
13273
|
+
const sddDir = path18.join(root, ".sdd");
|
|
13274
|
+
const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
|
|
13275
|
+
const name = backupName(now);
|
|
13276
|
+
const backupDir = path18.join(backupRoot, name);
|
|
13277
|
+
const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
|
|
13278
|
+
const previous = [];
|
|
13279
|
+
try {
|
|
13280
|
+
for (const item of plan.pending) {
|
|
13281
|
+
const content = await readTextIfExists(item.path);
|
|
13282
|
+
if (content === void 0) throw new Error("el elemento desapareci\xF3 mientras se respaldaba");
|
|
13283
|
+
previous.push({ item, contents: content });
|
|
13284
|
+
await writeText(path18.join(backupDir, "files", ...item.artifact.split("/")), content);
|
|
13285
|
+
}
|
|
13286
|
+
await writeText(
|
|
13287
|
+
path18.join(backupDir, "backup.yaml"),
|
|
13288
|
+
stringifyYaml6(
|
|
13289
|
+
{
|
|
13290
|
+
schema_version: SCHEMA_VERSION,
|
|
13291
|
+
created_at: localStamp(now),
|
|
13292
|
+
to_version: SCHEMA_VERSION,
|
|
13293
|
+
files: plan.pending.map((i) => ({ artifact: i.artifact, from: i.from, to: i.to, migration: i.migration }))
|
|
13294
|
+
},
|
|
13295
|
+
{ lineWidth: 120 }
|
|
13296
|
+
)
|
|
13297
|
+
);
|
|
13298
|
+
await writeText(pointerFile, `${name}
|
|
13299
|
+
`);
|
|
13300
|
+
} catch (err) {
|
|
13301
|
+
await removeDir(backupDir);
|
|
13302
|
+
await removeFile2(pointerFile);
|
|
13303
|
+
return { status: "failed", plan, applied: [], failure: { artifact: "(respaldo)", message: err.message } };
|
|
13304
|
+
}
|
|
13305
|
+
const written = [];
|
|
13306
|
+
for (const { item, contents } of previous) {
|
|
13307
|
+
try {
|
|
13308
|
+
const next = stampSchemaVersion(contents, item.to);
|
|
13309
|
+
if (next === void 0) throw new Error("no se pudo sellar con seguridad");
|
|
13310
|
+
await writeText(item.path, next);
|
|
13311
|
+
written.push(item);
|
|
13312
|
+
} catch (err) {
|
|
13313
|
+
for (const w of written) {
|
|
13314
|
+
const prev = previous.find((p) => p.item.artifact === w.artifact);
|
|
13315
|
+
if (prev) {
|
|
13316
|
+
try {
|
|
13317
|
+
await writeText(prev.item.path, prev.contents);
|
|
13318
|
+
} catch {
|
|
13319
|
+
}
|
|
13320
|
+
}
|
|
13321
|
+
}
|
|
13322
|
+
await removeFile2(pointerFile);
|
|
13323
|
+
await removeDir(backupDir);
|
|
13324
|
+
return { status: "failed", plan, applied: [], failure: { artifact: item.artifact, message: err.message } };
|
|
13325
|
+
}
|
|
13326
|
+
}
|
|
13327
|
+
for (const dir of await listDirs(backupRoot)) {
|
|
13328
|
+
if (dir !== name) await removeDir(path18.join(backupRoot, dir));
|
|
13329
|
+
}
|
|
13330
|
+
return {
|
|
13331
|
+
status: "applied",
|
|
13332
|
+
plan,
|
|
13333
|
+
applied: written,
|
|
13334
|
+
backup: {
|
|
13335
|
+
name,
|
|
13336
|
+
dir: backupDir,
|
|
13337
|
+
relativeDir: toPosix(path18.relative(root, backupDir)),
|
|
13338
|
+
createdAt: localStamp(now),
|
|
13339
|
+
files: plan.pending.map((i) => i.artifact)
|
|
13340
|
+
}
|
|
13341
|
+
};
|
|
13342
|
+
}
|
|
13343
|
+
async function rollbackUpgrade(root) {
|
|
13344
|
+
const sddDir = path18.join(root, ".sdd");
|
|
13345
|
+
const backupRoot = path18.join(sddDir, BACKUP_DIRNAME);
|
|
13346
|
+
const pointerFile = path18.join(backupRoot, BACKUP_POINTER);
|
|
13347
|
+
const name = (await readTextIfExists(pointerFile))?.trim();
|
|
13348
|
+
if (!name) return { status: "no-backup", restored: [] };
|
|
13349
|
+
const backupDir = path18.join(backupRoot, name);
|
|
13350
|
+
const manifestRaw = await readTextIfExists(path18.join(backupDir, "backup.yaml"));
|
|
13351
|
+
if (manifestRaw === void 0) return { status: "no-backup", restored: [] };
|
|
13352
|
+
let files = [];
|
|
13353
|
+
try {
|
|
13354
|
+
const parsed = parseYaml8(manifestRaw);
|
|
13355
|
+
files = (parsed?.files ?? []).filter((f) => typeof f?.artifact === "string");
|
|
13356
|
+
} catch {
|
|
13357
|
+
return { status: "no-backup", restored: [] };
|
|
13358
|
+
}
|
|
13359
|
+
const restored = [];
|
|
13360
|
+
for (const file of files) {
|
|
13361
|
+
const from = path18.join(backupDir, "files", ...file.artifact.split("/"));
|
|
13362
|
+
const to = path18.join(sddDir, ...file.artifact.split("/"));
|
|
13363
|
+
const content = await readTextIfExists(from);
|
|
13364
|
+
if (content === void 0) continue;
|
|
13365
|
+
try {
|
|
13366
|
+
await writeText(to, content);
|
|
13367
|
+
restored.push(file.artifact);
|
|
13368
|
+
} catch (err) {
|
|
13369
|
+
return { status: "failed", restored, backup: name, failure: { artifact: file.artifact, message: err.message } };
|
|
13370
|
+
}
|
|
13371
|
+
}
|
|
13372
|
+
await removeDir(backupDir);
|
|
13373
|
+
await removeFile2(pointerFile);
|
|
13374
|
+
return { status: "restored", restored, backup: name };
|
|
13375
|
+
}
|
|
13376
|
+
async function upgradeAdvisory(root) {
|
|
13377
|
+
const plan = await planUpgrade(root);
|
|
13378
|
+
const diagnostics = [];
|
|
13379
|
+
if (plan.pending.length > 0) {
|
|
13380
|
+
diagnostics.push(
|
|
13381
|
+
diag("ATLAS-UPGRADE-001", "warning", `El estado del proyecto no corresponde a la versi\xF3n vigente: ${plan.pending.length} elemento(s) por actualizar`, {
|
|
13382
|
+
suggestion: "Vista previa: `satlas upgrade` \xB7 Aplicar: `satlas upgrade --apply`"
|
|
13383
|
+
})
|
|
13384
|
+
);
|
|
13385
|
+
}
|
|
13386
|
+
for (const item of plan.newer) {
|
|
13387
|
+
diagnostics.push(
|
|
13388
|
+
diag("ATLAS-UPGRADE-002", "warning", `El proyecto fue producido por una versi\xF3n m\xE1s nueva: "${item.artifact}" (${item.reason})`, {
|
|
13389
|
+
path: item.path,
|
|
13390
|
+
suggestion: "Actualiza la herramienta; no se actualiza hacia atr\xE1s"
|
|
13391
|
+
})
|
|
13392
|
+
);
|
|
13393
|
+
}
|
|
13394
|
+
for (const item of plan.unreadable) {
|
|
13395
|
+
diagnostics.push(
|
|
13396
|
+
diag("ATLAS-UPGRADE-003", "warning", `No se pudo interpretar "${item.artifact}": ${item.reason}`, {
|
|
13397
|
+
path: item.path,
|
|
13398
|
+
suggestion: "Revisa el elemento; no se modificar\xE1"
|
|
13399
|
+
})
|
|
13400
|
+
);
|
|
13401
|
+
}
|
|
13402
|
+
return { plan, diagnostics };
|
|
13403
|
+
}
|
|
13404
|
+
var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
13405
|
+
var SARIF_VERSION = "2.1.0";
|
|
13406
|
+
var TOOL_NAME = "SpecAtlas";
|
|
13407
|
+
var TOOL_URL = "https://github.com/AlonsoAM/specatlas";
|
|
13408
|
+
var RULE_FAMILIES = [
|
|
13409
|
+
[/^LINT-STR-/, "Estructura de los artefactos"],
|
|
13410
|
+
[/^LINT-BIZ-/, "Lenguaje de negocio de la especificaci\xF3n"],
|
|
13411
|
+
[/^LINT-DLT-/, "Delta de especificaci\xF3n"],
|
|
13412
|
+
[/^LINT-EVD-/, "Evidencia registrada"],
|
|
13413
|
+
[/^LINT-TSK-/, "Tareas declaradas"],
|
|
13414
|
+
[/^LINT-PLN-/, "Plan t\xE9cnico"],
|
|
13415
|
+
[/^(LINT-MKP|MKP)-/, "Mockups"],
|
|
13416
|
+
[/^TRACE-/, "Trazabilidad requisito, escenario, tarea y evidencia"],
|
|
13417
|
+
[/^PACK-/, "Pack de cumplimiento"],
|
|
13418
|
+
[/^ATLAS-UPGRADE-/, "Actualizaci\xF3n del estado del proyecto"],
|
|
13419
|
+
[/^ATLAS-ADAPTERS-/, "Adaptadores de agente"],
|
|
13420
|
+
[/^ATLAS-CI-/, "Comprobaci\xF3n continua"],
|
|
13421
|
+
[/^ATLAS-/, "Estado del proyecto"]
|
|
13422
|
+
];
|
|
13423
|
+
function ruleDescription(code) {
|
|
13424
|
+
for (const [pattern, description] of RULE_FAMILIES) {
|
|
13425
|
+
if (pattern.test(code)) return description;
|
|
13426
|
+
}
|
|
13427
|
+
return code;
|
|
13428
|
+
}
|
|
13429
|
+
function sarifLevel(severity) {
|
|
13430
|
+
if (severity === "error") return "error";
|
|
13431
|
+
if (severity === "warning") return "warning";
|
|
13432
|
+
return "note";
|
|
13433
|
+
}
|
|
13434
|
+
function resultOf(diagnostic, root) {
|
|
13435
|
+
const result = {
|
|
13436
|
+
ruleId: diagnostic.code,
|
|
13437
|
+
level: sarifLevel(diagnostic.severity),
|
|
13438
|
+
message: { text: diagnostic.message }
|
|
13439
|
+
};
|
|
13440
|
+
if (diagnostic.path) {
|
|
13441
|
+
const rel = path19.relative(root, diagnostic.path);
|
|
13442
|
+
if (rel !== "") {
|
|
13443
|
+
const physicalLocation = {
|
|
13444
|
+
artifactLocation: { uri: toPosix(rel) },
|
|
13445
|
+
...diagnostic.line ? { region: { startLine: diagnostic.line } } : {}
|
|
13446
|
+
};
|
|
13447
|
+
result.locations = [{ physicalLocation }];
|
|
13448
|
+
}
|
|
13449
|
+
}
|
|
13450
|
+
return result;
|
|
13451
|
+
}
|
|
13452
|
+
function toSarifReport(opts) {
|
|
13453
|
+
const rules = /* @__PURE__ */ new Map();
|
|
13454
|
+
const results = [];
|
|
13455
|
+
for (const diagnostic of opts.diagnostics) {
|
|
13456
|
+
let rule = rules.get(diagnostic.code);
|
|
13457
|
+
if (!rule) {
|
|
13458
|
+
rule = { id: diagnostic.code, shortDescription: { text: ruleDescription(diagnostic.code) } };
|
|
13459
|
+
if (diagnostic.suggestion) rule.help = { text: diagnostic.suggestion };
|
|
13460
|
+
rules.set(diagnostic.code, rule);
|
|
13461
|
+
} else if (!rule.help && diagnostic.suggestion) {
|
|
13462
|
+
rule.help = { text: diagnostic.suggestion };
|
|
13463
|
+
}
|
|
13464
|
+
results.push(resultOf(diagnostic, opts.root));
|
|
13465
|
+
}
|
|
13466
|
+
return {
|
|
13467
|
+
$schema: SARIF_SCHEMA,
|
|
13468
|
+
version: SARIF_VERSION,
|
|
13469
|
+
runs: [
|
|
13470
|
+
{
|
|
13471
|
+
tool: {
|
|
13472
|
+
driver: {
|
|
13473
|
+
name: TOOL_NAME,
|
|
13474
|
+
informationUri: TOOL_URL,
|
|
13475
|
+
version: opts.version,
|
|
13476
|
+
rules: [...rules.values()]
|
|
13477
|
+
}
|
|
13478
|
+
},
|
|
13479
|
+
results,
|
|
13480
|
+
invocations: [{ executionSuccessful: !(opts.failed ?? false) }]
|
|
13481
|
+
}
|
|
13482
|
+
]
|
|
13483
|
+
};
|
|
13484
|
+
}
|
|
13485
|
+
function toSarifText(opts) {
|
|
13486
|
+
return `${JSON.stringify(toSarifReport(opts), null, 2)}
|
|
13487
|
+
`;
|
|
12990
13488
|
}
|
|
12991
13489
|
async function runDoctor(root) {
|
|
12992
13490
|
const findings = [];
|
|
@@ -12995,7 +13493,7 @@ async function runDoctor(root) {
|
|
|
12995
13493
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
12996
13494
|
findings.push(...approvals.diagnostics);
|
|
12997
13495
|
for (const change of workspace.changes) {
|
|
12998
|
-
const deltaPath =
|
|
13496
|
+
const deltaPath = path20.join(change.dir, "spec.md");
|
|
12999
13497
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
13000
13498
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
|
|
13001
13499
|
if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
|
|
@@ -13017,7 +13515,7 @@ async function runDoctor(root) {
|
|
|
13017
13515
|
}
|
|
13018
13516
|
for (const override of change.meta?.overrides ?? []) {
|
|
13019
13517
|
if (!override.reason.trim() || !override.by.trim()) {
|
|
13020
|
-
findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path:
|
|
13518
|
+
findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path20.join(change.dir, "meta.yaml") }));
|
|
13021
13519
|
}
|
|
13022
13520
|
}
|
|
13023
13521
|
}
|
|
@@ -13049,7 +13547,7 @@ function livingRequirementsMap(specs) {
|
|
|
13049
13547
|
return map;
|
|
13050
13548
|
}
|
|
13051
13549
|
async function runCiGate(opts) {
|
|
13052
|
-
const root =
|
|
13550
|
+
const root = path21.resolve(opts.root);
|
|
13053
13551
|
const { workspace, config } = await loadWorkspace(root);
|
|
13054
13552
|
const diagnostics = [];
|
|
13055
13553
|
const checks = [];
|
|
@@ -13060,7 +13558,7 @@ async function runCiGate(opts) {
|
|
|
13060
13558
|
let changesErrors = 0;
|
|
13061
13559
|
let changesWarnings = 0;
|
|
13062
13560
|
for (const change of workspace.changes) {
|
|
13063
|
-
const lintFindings = change.delta ? lintDelta(change.delta, living,
|
|
13561
|
+
const lintFindings = change.delta ? lintDelta(change.delta, living, path21.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
|
|
13064
13562
|
const trace = checkTrace({
|
|
13065
13563
|
specs: workspace.specs,
|
|
13066
13564
|
change,
|
|
@@ -13169,6 +13667,7 @@ var CATALOG = [
|
|
|
13169
13667
|
{ name: "trace", description: "Verifica la trazabilidad requisito \u2192 escenario \u2192 tarea \u2192 evidencia", flags: ["change", "require-evidence", "json"], usage: "satlas trace [--change <slug>] [--require-evidence] [--json]" },
|
|
13170
13668
|
{ name: "waves", description: "Calcula las olas paralelas de construcci\xF3n", flags: ["change", "max-parallel", "json"], usage: "satlas waves [--change <slug>] [--max-parallel N] [--json]" },
|
|
13171
13669
|
{ name: "doctor", description: "Diagn\xF3stico de salud del workspace", flags: ["json"], usage: "satlas doctor [--json]" },
|
|
13670
|
+
{ name: "upgrade", description: "Actualiza el estado del proyecto a la versi\xF3n vigente (vista previa por defecto)", flags: ["apply", "rollback", "json"], usage: "satlas upgrade [--apply | --rollback] [--json]" },
|
|
13172
13671
|
{ name: "approve", description: "Firma la aprobaci\xF3n de un artefacto (spec): local o desde la etiqueta de un issue de GitHub", flags: ["by", "channel", "note", "dry-run", "from-github", "label", "json"], usage: 'satlas approve <slug|ruta> --by "<nombre>" | satlas approve <slug> --from-github' },
|
|
13173
13672
|
{ name: "issue", description: "Sincroniza el cambio con un issue de GitHub (tracker, no gate)", flags: ["labels", "json"], usage: "satlas issue sync <slug> [--labels a,b] | satlas issue status <slug>" },
|
|
13174
13673
|
{ name: "adapters", description: "Compila (o verifica) los prompts a artefactos de agente", flags: ["targets", "check", "json"], usage: "satlas adapters [--targets opencode,claude-code,cursor,copilot,gemini,codex,generic] [--check]" },
|
|
@@ -13179,7 +13678,7 @@ var CATALOG = [
|
|
|
13179
13678
|
{ name: "analyze", description: "Chequeo cruzado (lint + trace + waves + mockups) y escribe analyze.md", flags: ["json"], usage: "satlas analyze <slug>" },
|
|
13180
13679
|
{ name: "mockup", description: "Planifica, valida o captura los mockups del cambio", flags: ["plan", "check", "capture", "require", "json"], usage: "satlas mockup <slug> [--plan|--check|--capture|--require]" },
|
|
13181
13680
|
{ name: "present", description: "Genera el paquete de propuesta para el stakeholder (HTML autocontenido)", flags: ["json"], usage: "satlas present <slug>" },
|
|
13182
|
-
{ name: "ci", description: "Gate de pipeline: specs + cambios + doctor + adaptadores (sin agente)", flags: ["strict", "json"], usage: "satlas ci [--strict]" },
|
|
13681
|
+
{ name: "ci", description: "Gate de pipeline: specs + cambios + doctor + adaptadores (sin agente)", flags: ["strict", "sarif", "json"], usage: "satlas ci [--strict] [--sarif <ruta>]" },
|
|
13183
13682
|
{ name: "mcp", description: "V\xEDa de consulta de solo lectura para asistentes (protocolo MCP sobre entrada/salida est\xE1ndar)", flags: [], usage: "satlas mcp" },
|
|
13184
13683
|
{ name: "metrics", description: "M\xE9tricas locales del workspace (progreso, WIP, throughput, evidencia)", flags: ["json"], usage: "satlas metrics [--json]" },
|
|
13185
13684
|
{ name: "run", description: "Persiste runs y eventos de ejecuci\xF3n (auditor\xEDa y reanudaci\xF3n)", flags: ["phase", "inputs", "data", "slug", "status", "json"], usage: "satlas run start|event|status|show|list" },
|
|
@@ -13201,6 +13700,7 @@ var ES2 = {
|
|
|
13201
13700
|
"status.title": "Estado de los cambios",
|
|
13202
13701
|
"status.changes": "Cambios activos",
|
|
13203
13702
|
"status.specs": "Specs vivas",
|
|
13703
|
+
"status.fixes": "Fixes vivos",
|
|
13204
13704
|
"status.next": "siguiente:",
|
|
13205
13705
|
"status.tasks": "tareas",
|
|
13206
13706
|
"status.evidence": "evidencia",
|
|
@@ -13216,6 +13716,8 @@ var ES2 = {
|
|
|
13216
13716
|
"init.done": "Workspace SDD creado.",
|
|
13217
13717
|
"new.title": "Nuevo cambio",
|
|
13218
13718
|
"new.done": "Cambio creado. Siguiente: escribe la spec funcional.",
|
|
13719
|
+
"upgrade.title": "Actualizar el estado del proyecto",
|
|
13720
|
+
"upgrade.preview": "Vista previa",
|
|
13219
13721
|
"summary.findings": "hallazgos",
|
|
13220
13722
|
"label.errors": "errores",
|
|
13221
13723
|
"label.warnings": "avisos",
|
|
@@ -13233,6 +13735,7 @@ var EN2 = {
|
|
|
13233
13735
|
"status.title": "Change status",
|
|
13234
13736
|
"status.changes": "Active changes",
|
|
13235
13737
|
"status.specs": "Living specs",
|
|
13738
|
+
"status.fixes": "Living fixes",
|
|
13236
13739
|
"status.next": "next:",
|
|
13237
13740
|
"status.tasks": "tasks",
|
|
13238
13741
|
"status.evidence": "evidence",
|
|
@@ -13248,6 +13751,8 @@ var EN2 = {
|
|
|
13248
13751
|
"init.done": "SDD workspace created.",
|
|
13249
13752
|
"new.title": "New change",
|
|
13250
13753
|
"new.done": "Change created. Next: write the business spec.",
|
|
13754
|
+
"upgrade.title": "Upgrade project state",
|
|
13755
|
+
"upgrade.preview": "Preview",
|
|
13251
13756
|
"summary.findings": "findings",
|
|
13252
13757
|
"label.errors": "errors",
|
|
13253
13758
|
"label.warnings": "warnings",
|
|
@@ -13275,26 +13780,26 @@ function cliVersion() {
|
|
|
13275
13780
|
}
|
|
13276
13781
|
|
|
13277
13782
|
// src/commands/adapters.ts
|
|
13278
|
-
import
|
|
13783
|
+
import path25 from "path";
|
|
13279
13784
|
|
|
13280
13785
|
// ../adapters/dist/index.js
|
|
13281
|
-
import path19 from "path";
|
|
13282
|
-
import { stringify as stringifyYaml5 } from "yaml";
|
|
13283
13786
|
import path22 from "path";
|
|
13787
|
+
import { stringify as stringifyYaml7 } from "yaml";
|
|
13788
|
+
import path23 from "path";
|
|
13284
13789
|
async function loadWorkflow(workflowDir) {
|
|
13285
|
-
const phasesDir =
|
|
13286
|
-
const snippetsDir =
|
|
13790
|
+
const phasesDir = path22.join(workflowDir, "phases");
|
|
13791
|
+
const snippetsDir = path22.join(workflowDir, "snippets");
|
|
13287
13792
|
const phases = [];
|
|
13288
13793
|
const snippets = /* @__PURE__ */ new Map();
|
|
13289
13794
|
const hashParts = [];
|
|
13290
13795
|
for (const entry of (await listDir(phasesDir)).sort()) {
|
|
13291
13796
|
if (!entry.endsWith(".md")) continue;
|
|
13292
|
-
const filePath =
|
|
13797
|
+
const filePath = path22.join(phasesDir, entry);
|
|
13293
13798
|
const raw = await readText(filePath);
|
|
13294
13799
|
hashParts.push(raw);
|
|
13295
13800
|
const fm = parseFrontmatter(raw, filePath);
|
|
13296
13801
|
const data = fm.data;
|
|
13297
|
-
const id = typeof data["id"] === "string" ? data["id"] :
|
|
13802
|
+
const id = typeof data["id"] === "string" ? data["id"] : path22.basename(entry, ".md");
|
|
13298
13803
|
phases.push({
|
|
13299
13804
|
id,
|
|
13300
13805
|
title: typeof data["title"] === "string" ? data["title"] : id,
|
|
@@ -13308,9 +13813,9 @@ async function loadWorkflow(workflowDir) {
|
|
|
13308
13813
|
}
|
|
13309
13814
|
for (const entry of (await listDir(snippetsDir)).sort()) {
|
|
13310
13815
|
if (!entry.endsWith(".md")) continue;
|
|
13311
|
-
const raw = await readTextIfExists(
|
|
13816
|
+
const raw = await readTextIfExists(path22.join(snippetsDir, entry)) ?? "";
|
|
13312
13817
|
hashParts.push(raw);
|
|
13313
|
-
snippets.set(
|
|
13818
|
+
snippets.set(path22.basename(entry, ".md"), raw.trim());
|
|
13314
13819
|
}
|
|
13315
13820
|
return { phases: phases.sort((a, b) => a.id.localeCompare(b.id)), snippets, sourceHash: sha256(hashParts.join("\n---\n")) };
|
|
13316
13821
|
}
|
|
@@ -13334,7 +13839,7 @@ function vars(language, slugToken, commandPrefix) {
|
|
|
13334
13839
|
return { SLUG: slugToken, LANGUAGE: language, LANGUAGE_NAME: LANGUAGE_NAMES[language], SDD_DIR: ".sdd", COMMAND_PREFIX: commandPrefix };
|
|
13335
13840
|
}
|
|
13336
13841
|
function frontmatter(data, body) {
|
|
13337
|
-
const yaml2 =
|
|
13842
|
+
const yaml2 = stringifyYaml7(data, { lineWidth: 0 }).trimEnd();
|
|
13338
13843
|
return `---
|
|
13339
13844
|
${yaml2}
|
|
13340
13845
|
---
|
|
@@ -13492,11 +13997,11 @@ Reglas: la spec es funcional y de negocio (sin tecnolog\xEDa); la trazabilidad e
|
|
|
13492
13997
|
function tomlString(value) {
|
|
13493
13998
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
13494
13999
|
}
|
|
13495
|
-
var MANIFEST_REL =
|
|
14000
|
+
var MANIFEST_REL = path23.join(".sdd", ".generated", "manifest.json");
|
|
13496
14001
|
var BEGIN = "<!-- BEGIN specatlas -->";
|
|
13497
14002
|
var END = "<!-- END specatlas -->";
|
|
13498
14003
|
async function compileTargets(opts) {
|
|
13499
|
-
const root =
|
|
14004
|
+
const root = path23.resolve(opts.root);
|
|
13500
14005
|
const language = opts.language ?? "es";
|
|
13501
14006
|
const diagnostics = [];
|
|
13502
14007
|
const sources = await loadWorkflow(opts.workflowDir);
|
|
@@ -13504,7 +14009,7 @@ async function compileTargets(opts) {
|
|
|
13504
14009
|
diagnostics.push({
|
|
13505
14010
|
code: "ATLAS-ADAPTERS-002",
|
|
13506
14011
|
severity: "error",
|
|
13507
|
-
message: `No hay fases en ${
|
|
14012
|
+
message: `No hay fases en ${path23.join(opts.workflowDir, "phases")}`,
|
|
13508
14013
|
suggestion: "Revisa la carpeta workflow/phases del proyecto"
|
|
13509
14014
|
});
|
|
13510
14015
|
}
|
|
@@ -13516,7 +14021,7 @@ async function compileTargets(opts) {
|
|
|
13516
14021
|
}
|
|
13517
14022
|
}
|
|
13518
14023
|
const compiled = [...deduped.values()];
|
|
13519
|
-
const manifestPath =
|
|
14024
|
+
const manifestPath = path23.join(root, MANIFEST_REL);
|
|
13520
14025
|
const previous = await readManifest(manifestPath);
|
|
13521
14026
|
const previousHashes = /* @__PURE__ */ new Map();
|
|
13522
14027
|
for (const files2 of Object.values(previous?.targets ?? {})) {
|
|
@@ -13527,7 +14032,7 @@ async function compileTargets(opts) {
|
|
|
13527
14032
|
const stale = [];
|
|
13528
14033
|
const missing = [];
|
|
13529
14034
|
for (const file of compiled) {
|
|
13530
|
-
const abs =
|
|
14035
|
+
const abs = path23.join(root, file.path);
|
|
13531
14036
|
const finalContent = file.path === "AGENTS.md" ? mergeAgentsBlock(await readTextIfExists(abs) ?? "", file.content) : file.content;
|
|
13532
14037
|
const hash = artifactHash(finalContent);
|
|
13533
14038
|
const existing = await readTextIfExists(abs);
|
|
@@ -13543,7 +14048,7 @@ async function compileTargets(opts) {
|
|
|
13543
14048
|
stale.push(file.path);
|
|
13544
14049
|
}
|
|
13545
14050
|
if (!opts.check && status !== "unchanged") {
|
|
13546
|
-
await ensureDir(
|
|
14051
|
+
await ensureDir(path23.dirname(abs));
|
|
13547
14052
|
await writeText(abs, finalContent);
|
|
13548
14053
|
written.push(file.path);
|
|
13549
14054
|
}
|
|
@@ -13597,7 +14102,7 @@ async function readManifest(manifestPath) {
|
|
|
13597
14102
|
}
|
|
13598
14103
|
}
|
|
13599
14104
|
async function checkAdapters(opts) {
|
|
13600
|
-
const manifestPath =
|
|
14105
|
+
const manifestPath = path23.join(path23.resolve(opts.root), MANIFEST_REL);
|
|
13601
14106
|
const manifestRaw = await readTextIfExists(manifestPath);
|
|
13602
14107
|
if (manifestRaw === void 0) return { ok: false, manifest: false, stale: [], missing: [], orphaned: [] };
|
|
13603
14108
|
const manifest = await readManifest(manifestPath);
|
|
@@ -13614,7 +14119,7 @@ async function checkAdapters(opts) {
|
|
|
13614
14119
|
}
|
|
13615
14120
|
|
|
13616
14121
|
// src/paths.ts
|
|
13617
|
-
import
|
|
14122
|
+
import path24 from "path";
|
|
13618
14123
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13619
14124
|
async function resolveProfilesDir() {
|
|
13620
14125
|
const env = process.env["SPECATLAS_PROFILES_DIR"];
|
|
@@ -13624,16 +14129,16 @@ async function resolveProfilesDir() {
|
|
|
13624
14129
|
async function resolveWorkflowDir() {
|
|
13625
14130
|
const env = process.env["SPECATLAS_WORKFLOW_DIR"];
|
|
13626
14131
|
if (env && await exists(env)) return env;
|
|
13627
|
-
return walkUpFor(
|
|
14132
|
+
return walkUpFor(path24.join("workflow", "phases"));
|
|
13628
14133
|
}
|
|
13629
14134
|
async function walkUpFor(relative) {
|
|
13630
|
-
let dir =
|
|
14135
|
+
let dir = path24.dirname(fileURLToPath2(import.meta.url));
|
|
13631
14136
|
for (let i = 0; i < 8; i += 1) {
|
|
13632
|
-
const candidate =
|
|
14137
|
+
const candidate = path24.join(dir, relative);
|
|
13633
14138
|
if (await exists(candidate)) {
|
|
13634
|
-
return relative.includes(
|
|
14139
|
+
return relative.includes(path24.sep) ? path24.dirname(candidate) : candidate;
|
|
13635
14140
|
}
|
|
13636
|
-
const parent =
|
|
14141
|
+
const parent = path24.dirname(dir);
|
|
13637
14142
|
if (parent === dir) break;
|
|
13638
14143
|
dir = parent;
|
|
13639
14144
|
}
|
|
@@ -13711,7 +14216,7 @@ async function runAdapters(ctx) {
|
|
|
13711
14216
|
for (const [target, count2] of byTarget) lines.push(` ${target}: ${count2} archivo(s)`);
|
|
13712
14217
|
if (report.written.length > 0) {
|
|
13713
14218
|
lines.push("");
|
|
13714
|
-
for (const p of report.written) lines.push(` + ${
|
|
14219
|
+
for (const p of report.written) lines.push(` + ${path25.relative(ctx.cwd, path25.join(root, p))}`);
|
|
13715
14220
|
} else {
|
|
13716
14221
|
lines.push("");
|
|
13717
14222
|
lines.push("Sin cambios: los adaptadores ya estaban al d\xEDa.");
|
|
@@ -13722,7 +14227,7 @@ async function runAdapters(ctx) {
|
|
|
13722
14227
|
}
|
|
13723
14228
|
|
|
13724
14229
|
// src/commands/adopt.ts
|
|
13725
|
-
import
|
|
14230
|
+
import path26 from "path";
|
|
13726
14231
|
async function runAdopt(ctx) {
|
|
13727
14232
|
const { root } = await requireWorkspace(ctx);
|
|
13728
14233
|
const domainsFlag = flagString(ctx.flags, "domains") ?? flagString(ctx.flags, "domain");
|
|
@@ -13742,11 +14247,11 @@ async function runAdopt(ctx) {
|
|
|
13742
14247
|
}
|
|
13743
14248
|
if (result.createdSpecs.length > 0) {
|
|
13744
14249
|
lines.push("");
|
|
13745
|
-
for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${
|
|
14250
|
+
for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${path26.relative(ctx.cwd, spec)}`);
|
|
13746
14251
|
}
|
|
13747
14252
|
if (result.reportPath && !result.dryRun) {
|
|
13748
14253
|
lines.push("");
|
|
13749
|
-
lines.push(` Informe: ${
|
|
14254
|
+
lines.push(` Informe: ${path26.relative(ctx.cwd, result.reportPath)}`);
|
|
13750
14255
|
}
|
|
13751
14256
|
lines.push("");
|
|
13752
14257
|
lines.push("Siguiente: fase /satlas-adopt por dominio (requisitos AS-IS) y luego:");
|
|
@@ -13759,8 +14264,8 @@ async function runAdopt(ctx) {
|
|
|
13759
14264
|
diagnostics: result.diagnostics,
|
|
13760
14265
|
data: {
|
|
13761
14266
|
domains: result.domains.map((d) => ({ name: d.name, files: d.files, existingSpec: d.existingSpec })),
|
|
13762
|
-
createdSpecs: result.createdSpecs.map((s) =>
|
|
13763
|
-
reportPath: result.reportPath ?
|
|
14267
|
+
createdSpecs: result.createdSpecs.map((s) => path26.relative(ctx.cwd, s)),
|
|
14268
|
+
reportPath: result.reportPath ? path26.relative(ctx.cwd, result.reportPath) : void 0,
|
|
13764
14269
|
stack: result.stack,
|
|
13765
14270
|
dryRun: result.dryRun
|
|
13766
14271
|
},
|
|
@@ -13769,7 +14274,7 @@ async function runAdopt(ctx) {
|
|
|
13769
14274
|
}
|
|
13770
14275
|
|
|
13771
14276
|
// src/commands/analyze.ts
|
|
13772
|
-
import
|
|
14277
|
+
import path27 from "path";
|
|
13773
14278
|
async function runAnalyzeCommand(ctx) {
|
|
13774
14279
|
const { root } = await requireWorkspace(ctx);
|
|
13775
14280
|
const slug = ctx.positionals[0];
|
|
@@ -13780,23 +14285,23 @@ async function runAnalyzeCommand(ctx) {
|
|
|
13780
14285
|
const lines = [`An\xE1lisis \u2014 ${slug}`, "", ` estado: ${result.status}`, ` hallazgos: ${result.summary.errors} errores, ${result.summary.warnings} avisos`, ` evidencia: ${result.evidence.done}/${result.evidence.total} escenarios`];
|
|
13781
14286
|
if (result.waves) lines.push(` olas: ${result.waves.blocks} bloque(s), ${result.waves.waves} ola(s), ${result.waves.tasks} tarea(s)`);
|
|
13782
14287
|
if (result.mockups) lines.push(` mockups: ${result.mockups.screens} pantalla(s)${result.mockups.stale ? " (desactualizados)" : ""}`);
|
|
13783
|
-
if (result.path) lines.push("", ` informe: ${
|
|
14288
|
+
if (result.path) lines.push("", ` informe: ${path27.relative(ctx.cwd, result.path)}`);
|
|
13784
14289
|
for (const finding of result.findings) {
|
|
13785
14290
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}`);
|
|
13786
14291
|
}
|
|
13787
14292
|
return {
|
|
13788
14293
|
exitCode: result.status === "blocked" ? 1 : 0,
|
|
13789
14294
|
diagnostics: result.findings,
|
|
13790
|
-
data: result.path ? { ...result, path:
|
|
14295
|
+
data: result.path ? { ...result, path: path27.relative(ctx.cwd, result.path) } : result,
|
|
13791
14296
|
text: lines
|
|
13792
14297
|
};
|
|
13793
14298
|
}
|
|
13794
14299
|
|
|
13795
14300
|
// src/commands/approve.ts
|
|
13796
|
-
import
|
|
14301
|
+
import path29 from "path";
|
|
13797
14302
|
|
|
13798
14303
|
// src/commands/issue.ts
|
|
13799
|
-
import
|
|
14304
|
+
import path28 from "path";
|
|
13800
14305
|
async function runIssue(ctx) {
|
|
13801
14306
|
const action = ctx.positionals[0] ?? "status";
|
|
13802
14307
|
const slug = ctx.positionals[1];
|
|
@@ -13824,7 +14329,7 @@ async function runIssue(ctx) {
|
|
|
13824
14329
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-ISSUE-000", severity: "error", message: "Falta el slug: satlas issue status <slug>" }] };
|
|
13825
14330
|
}
|
|
13826
14331
|
const change = await loadChange(root, slug);
|
|
13827
|
-
const { config } = await loadConfig(
|
|
14332
|
+
const { config } = await loadConfig(path28.join(root, ".sdd"));
|
|
13828
14333
|
const tracker = change.meta?.tracker;
|
|
13829
14334
|
const lines = [`Issue de ${slug}`, ""];
|
|
13830
14335
|
if (!tracker || tracker.provider !== "github") {
|
|
@@ -13895,7 +14400,7 @@ async function runApprove(ctx) {
|
|
|
13895
14400
|
};
|
|
13896
14401
|
}
|
|
13897
14402
|
const { root, workspace, config } = await requireWorkspace(ctx);
|
|
13898
|
-
const artifact = target.includes("/") || target.includes("\\") ? target :
|
|
14403
|
+
const artifact = target.includes("/") || target.includes("\\") ? target : path29.join("changes", target, "spec.md");
|
|
13899
14404
|
const channelFlag = flagString(ctx.flags, "channel");
|
|
13900
14405
|
const channel = channelFlag === "presentation" || channelFlag === "editor" || channelFlag === "pr" || channelFlag === "tracker" ? channelFlag : "cli";
|
|
13901
14406
|
const change = workspace.changes.find(
|
|
@@ -13936,13 +14441,13 @@ async function runApprove(ctx) {
|
|
|
13936
14441
|
return {
|
|
13937
14442
|
exitCode: hasErrors2 ? 1 : 0,
|
|
13938
14443
|
diagnostics: result.diagnostics,
|
|
13939
|
-
data: result.approval ? { ...result.approval, file:
|
|
14444
|
+
data: result.approval ? { ...result.approval, file: path29.relative(ctx.cwd, result.file) } : void 0,
|
|
13940
14445
|
text: lines
|
|
13941
14446
|
};
|
|
13942
14447
|
}
|
|
13943
14448
|
|
|
13944
14449
|
// src/commands/archive.ts
|
|
13945
|
-
import
|
|
14450
|
+
import path30 from "path";
|
|
13946
14451
|
async function runArchive(ctx) {
|
|
13947
14452
|
const slug = ctx.positionals[0];
|
|
13948
14453
|
if (!slug) {
|
|
@@ -13966,29 +14471,35 @@ async function runArchive(ctx) {
|
|
|
13966
14471
|
}
|
|
13967
14472
|
const result = await archiveChange({ root, slug, language: config.project.language, dryRun });
|
|
13968
14473
|
const lines = [msg("archive.title", ctx.language), ""];
|
|
13969
|
-
|
|
14474
|
+
const operations = result.fold.applied.added.length + result.fold.applied.modified.length + result.fold.applied.removed.length + result.fold.applied.renamed.length;
|
|
14475
|
+
if (operations > 0) {
|
|
13970
14476
|
lines.push(`${dryRun ? "[dry-run] " : ""}${msg("archive.done", ctx.language)}`);
|
|
13971
14477
|
lines.push(` agregados: ${result.fold.applied.added.join(", ") || "\u2014"}`);
|
|
13972
14478
|
lines.push(` modificados: ${result.fold.applied.modified.join(", ") || "\u2014"}`);
|
|
13973
14479
|
lines.push(` eliminados: ${result.fold.applied.removed.join(", ") || "\u2014"}`);
|
|
13974
14480
|
lines.push(` renombrados: ${result.fold.applied.renamed.join(", ") || "\u2014"}`);
|
|
13975
|
-
|
|
14481
|
+
} else if (result.livingFix) {
|
|
14482
|
+
lines.push(`${dryRun ? "[dry-run] " : ""}Fix archivado y conservado como fix vivo.`);
|
|
14483
|
+
lines.push(` fix vivo: ${result.livingFix}`);
|
|
13976
14484
|
} else {
|
|
13977
14485
|
lines.push("El delta no contiene operaciones (ADDED/MODIFIED/REMOVED/RENAMED).");
|
|
13978
14486
|
}
|
|
14487
|
+
if (result.archivedTo) lines.push(` archivado en: ${path30.relative(ctx.cwd, result.archivedTo)}`);
|
|
13979
14488
|
const hasErrors2 = result.diagnostics.some((d) => d.severity === "error");
|
|
13980
14489
|
return {
|
|
13981
14490
|
exitCode: hasErrors2 ? 1 : 0,
|
|
13982
14491
|
diagnostics: result.diagnostics,
|
|
13983
|
-
data: { slug: result.slug, domain: result.domain, archivedTo: result.archivedTo, applied: result.fold.applied, dryRun: result.dryRun },
|
|
14492
|
+
data: { slug: result.slug, domain: result.domain, archivedTo: result.archivedTo, livingFix: result.livingFix, applied: result.fold.applied, dryRun: result.dryRun },
|
|
13984
14493
|
text: lines
|
|
13985
14494
|
};
|
|
13986
14495
|
}
|
|
13987
14496
|
|
|
13988
14497
|
// src/commands/ci.ts
|
|
14498
|
+
import path31 from "path";
|
|
13989
14499
|
async function runCi(ctx) {
|
|
13990
14500
|
const { root, config } = await requireWorkspace(ctx);
|
|
13991
14501
|
const strict = flagBool(ctx.flags, "strict");
|
|
14502
|
+
const sarifPath = flagString(ctx.flags, "sarif");
|
|
13992
14503
|
const extra = [];
|
|
13993
14504
|
const workflowDir = await resolveWorkflowDir();
|
|
13994
14505
|
if (workflowDir) {
|
|
@@ -14006,28 +14517,54 @@ async function runCi(ctx) {
|
|
|
14006
14517
|
});
|
|
14007
14518
|
}
|
|
14008
14519
|
const gate = await runCiGate({ root, strict, extra });
|
|
14520
|
+
const diagnostics = [...gate.diagnostics];
|
|
14009
14521
|
const lines = ["CI de SpecAtlas", ""];
|
|
14522
|
+
let sarifWritten;
|
|
14523
|
+
if (sarifPath) {
|
|
14524
|
+
const target = path31.resolve(ctx.cwd, sarifPath);
|
|
14525
|
+
try {
|
|
14526
|
+
await writeText(target, toSarifText({ diagnostics: gate.diagnostics, root, version: cliVersion(), failed: gate.failed }));
|
|
14527
|
+
sarifWritten = path31.relative(ctx.cwd, target);
|
|
14528
|
+
} catch (err) {
|
|
14529
|
+
diagnostics.push({
|
|
14530
|
+
code: "ATLAS-CI-SARIF-001",
|
|
14531
|
+
severity: "warning",
|
|
14532
|
+
message: `No se pudo escribir el informe SARIF en "${sarifPath}": ${err.message}`,
|
|
14533
|
+
suggestion: "Revisa que la ruta sea escribible; el veredicto de la comprobaci\xF3n no cambia"
|
|
14534
|
+
});
|
|
14535
|
+
}
|
|
14536
|
+
}
|
|
14010
14537
|
for (const check of gate.checks) {
|
|
14011
14538
|
lines.push(` ${check.errors === 0 && check.warnings === 0 ? "OK " : "FALLA"} ${check.name}: ${check.errors} errores, ${check.warnings} avisos`);
|
|
14012
14539
|
}
|
|
14013
14540
|
lines.push("");
|
|
14014
14541
|
lines.push(gate.failed ? "Resultado: BLOQUEADO" : "Resultado: OK");
|
|
14015
14542
|
if (strict) lines.push("(modo estricto: los avisos tambi\xE9n bloquean)");
|
|
14543
|
+
if (sarifPath) {
|
|
14544
|
+
lines.push(sarifWritten ? `Informe SARIF: ${sarifWritten} (${gate.diagnostics.length} hallazgo(s))` : `Informe SARIF: no se pudo escribir en "${sarifPath}"`);
|
|
14545
|
+
}
|
|
14016
14546
|
return {
|
|
14017
14547
|
exitCode: gate.failed ? 1 : 0,
|
|
14018
|
-
diagnostics
|
|
14019
|
-
data: {
|
|
14548
|
+
diagnostics,
|
|
14549
|
+
data: {
|
|
14550
|
+
checks: gate.checks,
|
|
14551
|
+
errors: gate.errors,
|
|
14552
|
+
warnings: gate.warnings,
|
|
14553
|
+
strict,
|
|
14554
|
+
failed: gate.failed,
|
|
14555
|
+
...sarifWritten ? { sarif: sarifWritten } : {}
|
|
14556
|
+
},
|
|
14020
14557
|
text: lines
|
|
14021
14558
|
};
|
|
14022
14559
|
}
|
|
14023
14560
|
|
|
14024
14561
|
// src/commands/profile.ts
|
|
14025
|
-
import
|
|
14562
|
+
import path32 from "path";
|
|
14026
14563
|
async function runProfile(ctx) {
|
|
14027
14564
|
const action = ctx.positionals[0] ?? "detect";
|
|
14028
14565
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
14029
14566
|
const officialDir = await resolveProfilesDir();
|
|
14030
|
-
const customDir =
|
|
14567
|
+
const customDir = path32.join(workspace.sddDir, "profiles", "custom");
|
|
14031
14568
|
const profiles = [...officialDir ? await loadProfilesFromDir(officialDir) : [], ...await loadProfilesFromDir(customDir)];
|
|
14032
14569
|
if (action === "detect") {
|
|
14033
14570
|
const result = await detectProfiles(root, profiles);
|
|
@@ -14056,7 +14593,7 @@ async function runProfile(ctx) {
|
|
|
14056
14593
|
if (!name) {
|
|
14057
14594
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-001", severity: "error", message: "Falta el nombre del perfil: satlas profile create <nombre>" }] };
|
|
14058
14595
|
}
|
|
14059
|
-
const file =
|
|
14596
|
+
const file = path32.join(customDir, `${name}.yaml`);
|
|
14060
14597
|
if (await exists(file)) {
|
|
14061
14598
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-002", severity: "error", message: `El perfil ya existe: ${file}` }] };
|
|
14062
14599
|
}
|
|
@@ -14065,7 +14602,7 @@ async function runProfile(ctx) {
|
|
|
14065
14602
|
exitCode: 0,
|
|
14066
14603
|
diagnostics: [],
|
|
14067
14604
|
data: { file },
|
|
14068
|
-
text: [`Perfil creado: ${
|
|
14605
|
+
text: [`Perfil creado: ${path32.relative(ctx.cwd, file)}`, "", "Ed\xEDtalo con los comandos reales de tu stack (build/lint/test) y tus anti-patrones."]
|
|
14069
14606
|
};
|
|
14070
14607
|
}
|
|
14071
14608
|
return {
|
|
@@ -14074,7 +14611,7 @@ async function runProfile(ctx) {
|
|
|
14074
14611
|
};
|
|
14075
14612
|
}
|
|
14076
14613
|
async function isInside(dir, name) {
|
|
14077
|
-
return exists(
|
|
14614
|
+
return exists(path32.join(dir, `${name}.yaml`));
|
|
14078
14615
|
}
|
|
14079
14616
|
function profileTemplate(name) {
|
|
14080
14617
|
return `name: ${name}
|
|
@@ -14102,7 +14639,7 @@ assumptions: []
|
|
|
14102
14639
|
}
|
|
14103
14640
|
|
|
14104
14641
|
// src/commands/doctor.ts
|
|
14105
|
-
import
|
|
14642
|
+
import path33 from "path";
|
|
14106
14643
|
async function runDoctorCommand(ctx) {
|
|
14107
14644
|
const { root, config } = await requireWorkspace(ctx);
|
|
14108
14645
|
const report = await runDoctor(root);
|
|
@@ -14119,12 +14656,15 @@ async function runDoctorCommand(ctx) {
|
|
|
14119
14656
|
report.summary.warnings += 1;
|
|
14120
14657
|
}
|
|
14121
14658
|
}
|
|
14659
|
+
const advisory = await upgradeAdvisory(root);
|
|
14660
|
+
report.findings.push(...advisory.diagnostics);
|
|
14661
|
+
report.summary = countBySeverity(report.findings);
|
|
14122
14662
|
const lines = [msg("doctor.title", ctx.language), ""];
|
|
14123
14663
|
if (report.findings.length === 0) {
|
|
14124
14664
|
lines.push("Sin hallazgos. El workspace est\xE1 sano.");
|
|
14125
14665
|
} else {
|
|
14126
14666
|
for (const finding of report.findings) {
|
|
14127
|
-
const location = finding.path ? ` ${
|
|
14667
|
+
const location = finding.path ? ` ${path33.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""}` : "";
|
|
14128
14668
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${location} \u2014 ${finding.message}`);
|
|
14129
14669
|
if (finding.suggestion) lines.push(` \u21B3 ${finding.suggestion}`);
|
|
14130
14670
|
}
|
|
@@ -14198,7 +14738,7 @@ async function runHelp(ctx, commandName) {
|
|
|
14198
14738
|
}
|
|
14199
14739
|
|
|
14200
14740
|
// src/commands/init.ts
|
|
14201
|
-
import
|
|
14741
|
+
import path34 from "path";
|
|
14202
14742
|
async function runInit(ctx) {
|
|
14203
14743
|
const languageFlag = flagString(ctx.flags, "language");
|
|
14204
14744
|
const language = languageFlag === "en" ? "en" : languageFlag === "es" ? "es" : void 0;
|
|
@@ -14245,11 +14785,11 @@ async function runInit(ctx) {
|
|
|
14245
14785
|
const lines = [msg("init.title", ctx.language), ""];
|
|
14246
14786
|
if (result.created.length > 0) {
|
|
14247
14787
|
lines.push(msg("init.done", ctx.language));
|
|
14248
|
-
for (const file of result.created) lines.push(` + ${
|
|
14788
|
+
for (const file of result.created) lines.push(` + ${path34.relative(ctx.cwd, file)}`);
|
|
14249
14789
|
if (compiled.length > 0) {
|
|
14250
14790
|
lines.push("");
|
|
14251
14791
|
lines.push("Adaptadores compilados:");
|
|
14252
|
-
for (const file of compiled) lines.push(` + ${
|
|
14792
|
+
for (const file of compiled) lines.push(` + ${path34.relative(ctx.cwd, path34.join(ctx.cwd, file))}`);
|
|
14253
14793
|
}
|
|
14254
14794
|
lines.push("");
|
|
14255
14795
|
lines.push("Siguiente: crea un cambio con `satlas new <slug>` y escribe la spec funcional.");
|
|
@@ -14259,8 +14799,8 @@ async function runInit(ctx) {
|
|
|
14259
14799
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14260
14800
|
diagnostics: result.diagnostics,
|
|
14261
14801
|
data: {
|
|
14262
|
-
sddDir:
|
|
14263
|
-
created: result.created.map((f) =>
|
|
14802
|
+
sddDir: path34.relative(ctx.cwd, result.sddDir),
|
|
14803
|
+
created: result.created.map((f) => path34.relative(ctx.cwd, f)),
|
|
14264
14804
|
detected: result.detected?.matches.map((m) => ({ name: m.name, score: m.score })),
|
|
14265
14805
|
best: result.detected?.best?.name
|
|
14266
14806
|
},
|
|
@@ -14301,7 +14841,7 @@ async function runMetrics(ctx) {
|
|
|
14301
14841
|
}
|
|
14302
14842
|
|
|
14303
14843
|
// src/commands/mockup.ts
|
|
14304
|
-
import
|
|
14844
|
+
import path35 from "path";
|
|
14305
14845
|
async function runMockup(ctx) {
|
|
14306
14846
|
const { root, workspace, config } = await requireWorkspace(ctx);
|
|
14307
14847
|
const slug = ctx.positionals[0];
|
|
@@ -14350,17 +14890,17 @@ async function runMockup(ctx) {
|
|
|
14350
14890
|
` pantallas: ${plan.screens.length}`,
|
|
14351
14891
|
...plan.screens.map((s) => ` - ${s.id}: ${s.title} (${s.illustrates.length} escenario(s))`),
|
|
14352
14892
|
"",
|
|
14353
|
-
` plan: ${
|
|
14354
|
-
` manifiesto: ${
|
|
14893
|
+
` plan: ${path35.relative(ctx.cwd, planFile)}`,
|
|
14894
|
+
` manifiesto: ${path35.relative(ctx.cwd, manifestFile)}`,
|
|
14355
14895
|
"",
|
|
14356
14896
|
"Siguiente: genera el HTML de cada pantalla (fase /satlas-mockup) y valida con `satlas mockup " + slug + " --check`."
|
|
14357
14897
|
];
|
|
14358
|
-
return { exitCode: 0, diagnostics: [], data: { plan, planFile:
|
|
14898
|
+
return { exitCode: 0, diagnostics: [], data: { plan, planFile: path35.relative(ctx.cwd, planFile), manifestFile: path35.relative(ctx.cwd, manifestFile) }, text: lines };
|
|
14359
14899
|
}
|
|
14360
14900
|
|
|
14361
14901
|
// src/mcp/server.ts
|
|
14362
|
-
import { promises as
|
|
14363
|
-
import
|
|
14902
|
+
import { promises as fs3 } from "fs";
|
|
14903
|
+
import path38 from "path";
|
|
14364
14904
|
import { createInterface } from "readline/promises";
|
|
14365
14905
|
|
|
14366
14906
|
// src/mcp/protocol.ts
|
|
@@ -14421,13 +14961,39 @@ function stringArg(args, key) {
|
|
|
14421
14961
|
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
14422
14962
|
}
|
|
14423
14963
|
|
|
14964
|
+
// src/mcp/tools/fixes.ts
|
|
14965
|
+
async function runAtlasFixes(_args, host) {
|
|
14966
|
+
const resolved = await host.getWorkspace();
|
|
14967
|
+
if (!resolved) return noWorkspaceResult();
|
|
14968
|
+
const fixes = await loadLivingFixes(resolved.root);
|
|
14969
|
+
if (fixes.length === 0) {
|
|
14970
|
+
const activeFix = resolved.workspace.changes.find((change) => change.meta?.lane === "fix");
|
|
14971
|
+
return jsonResult({
|
|
14972
|
+
fixes: [],
|
|
14973
|
+
message: "No hay fixes vivos registrados.",
|
|
14974
|
+
action: activeFix ? `satlas archive ${activeFix.slug}` : "satlas new <slug> --lane fix"
|
|
14975
|
+
});
|
|
14976
|
+
}
|
|
14977
|
+
return jsonResult({
|
|
14978
|
+
fixes: fixes.map((fix) => ({
|
|
14979
|
+
slug: fix.slug,
|
|
14980
|
+
date: fix.date,
|
|
14981
|
+
result: fix.result,
|
|
14982
|
+
domain: fix.domain,
|
|
14983
|
+
title: fix.title,
|
|
14984
|
+
covers: fix.covers,
|
|
14985
|
+
content: fix.content
|
|
14986
|
+
}))
|
|
14987
|
+
});
|
|
14988
|
+
}
|
|
14989
|
+
|
|
14424
14990
|
// src/mcp/tools/glossary.ts
|
|
14425
|
-
import
|
|
14991
|
+
import path36 from "path";
|
|
14426
14992
|
async function runAtlasGlossary(_args, host) {
|
|
14427
14993
|
const resolved = await host.getWorkspace();
|
|
14428
14994
|
if (!resolved) return noWorkspaceResult();
|
|
14429
14995
|
const { root, config } = resolved;
|
|
14430
|
-
const glossaryPath =
|
|
14996
|
+
const glossaryPath = path36.resolve(root, config.spec.glossary);
|
|
14431
14997
|
const raw = await readTextIfExists(glossaryPath);
|
|
14432
14998
|
if (raw === void 0) {
|
|
14433
14999
|
return jsonResult({
|
|
@@ -14473,7 +15039,7 @@ async function runAtlasImpact(args, host) {
|
|
|
14473
15039
|
}
|
|
14474
15040
|
|
|
14475
15041
|
// src/evaluate.ts
|
|
14476
|
-
import
|
|
15042
|
+
import path37 from "path";
|
|
14477
15043
|
function livingRequirementsMap2(specs) {
|
|
14478
15044
|
const map = /* @__PURE__ */ new Map();
|
|
14479
15045
|
for (const spec of specs) {
|
|
@@ -14482,7 +15048,7 @@ function livingRequirementsMap2(specs) {
|
|
|
14482
15048
|
return map;
|
|
14483
15049
|
}
|
|
14484
15050
|
async function evaluateChange(workspace, config, change, approvals) {
|
|
14485
|
-
const deltaPath =
|
|
15051
|
+
const deltaPath = path37.join(change.dir, "spec.md");
|
|
14486
15052
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
14487
15053
|
const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
|
|
14488
15054
|
const living = livingRequirementsMap2(workspace.specs);
|
|
@@ -14658,7 +15224,8 @@ var HANDLERS = {
|
|
|
14658
15224
|
atlas_validate: runAtlasValidate,
|
|
14659
15225
|
atlas_trace: runAtlasTrace,
|
|
14660
15226
|
atlas_impact: runAtlasImpact,
|
|
14661
|
-
atlas_glossary: runAtlasGlossary
|
|
15227
|
+
atlas_glossary: runAtlasGlossary,
|
|
15228
|
+
atlas_fixes: runAtlasFixes
|
|
14662
15229
|
};
|
|
14663
15230
|
async function runToolByName(name, args, host) {
|
|
14664
15231
|
const handler = HANDLERS[name];
|
|
@@ -14727,6 +15294,15 @@ function listTools() {
|
|
|
14727
15294
|
properties: {},
|
|
14728
15295
|
additionalProperties: false
|
|
14729
15296
|
}
|
|
15297
|
+
},
|
|
15298
|
+
{
|
|
15299
|
+
name: "atlas_fixes",
|
|
15300
|
+
description: "Fixes vivos (carril express ya archivado): identidad, resultado de su evidencia, contenido y requisitos que declaran. Solo lectura.",
|
|
15301
|
+
inputSchema: {
|
|
15302
|
+
type: "object",
|
|
15303
|
+
properties: {},
|
|
15304
|
+
additionalProperties: false
|
|
15305
|
+
}
|
|
14730
15306
|
}
|
|
14731
15307
|
];
|
|
14732
15308
|
}
|
|
@@ -14744,12 +15320,12 @@ var WorkspaceCache = class {
|
|
|
14744
15320
|
}
|
|
14745
15321
|
};
|
|
14746
15322
|
async function workspaceStamp(root) {
|
|
14747
|
-
const sddDir =
|
|
14748
|
-
const markers = [sddDir,
|
|
15323
|
+
const sddDir = path38.join(root, ".sdd");
|
|
15324
|
+
const markers = [sddDir, path38.join(sddDir, "config.yaml"), path38.join(sddDir, "changes"), path38.join(sddDir, "specs"), path38.join(sddDir, "approvals.yaml")];
|
|
14749
15325
|
let stamp = 0;
|
|
14750
15326
|
for (const marker of markers) {
|
|
14751
15327
|
try {
|
|
14752
|
-
const st = await
|
|
15328
|
+
const st = await fs3.stat(marker);
|
|
14753
15329
|
stamp = Math.max(stamp, st.mtimeMs);
|
|
14754
15330
|
} catch {
|
|
14755
15331
|
}
|
|
@@ -14763,7 +15339,7 @@ async function resolveMcpWorkspace(cwd, cache) {
|
|
|
14763
15339
|
const cached2 = cache.get(root);
|
|
14764
15340
|
if (cached2 && cached2.mtimeMs === stamp) return cached2.value;
|
|
14765
15341
|
const { workspace, config } = await loadWorkspace(root);
|
|
14766
|
-
const approvals = await loadApprovals(
|
|
15342
|
+
const approvals = await loadApprovals(path38.join(root, ".sdd"));
|
|
14767
15343
|
const value = { root, workspace, config, approvals: approvals.byArtifact };
|
|
14768
15344
|
cache.set(root, stamp, value);
|
|
14769
15345
|
return value;
|
|
@@ -14855,7 +15431,7 @@ async function runMcp(ctx) {
|
|
|
14855
15431
|
}
|
|
14856
15432
|
|
|
14857
15433
|
// src/commands/packs.ts
|
|
14858
|
-
import
|
|
15434
|
+
import path39 from "path";
|
|
14859
15435
|
async function runPacks(ctx) {
|
|
14860
15436
|
const { root, config, workspace } = await requireWorkspace(ctx);
|
|
14861
15437
|
const slug = flagString(ctx.flags, "check");
|
|
@@ -14913,14 +15489,14 @@ async function runPacks(ctx) {
|
|
|
14913
15489
|
data: {
|
|
14914
15490
|
slug,
|
|
14915
15491
|
packs: evaluations.map((evaluation) => ({ id: evaluation.pack.id, status: evaluation.status, passed: evaluation.passed, failed: evaluation.failed, results: evaluation.results })),
|
|
14916
|
-
analyzePath:
|
|
15492
|
+
analyzePath: path39.posix.join(".sdd", "changes", slug, "analyze.md")
|
|
14917
15493
|
},
|
|
14918
15494
|
text: lines
|
|
14919
15495
|
};
|
|
14920
15496
|
}
|
|
14921
15497
|
|
|
14922
15498
|
// src/commands/new.ts
|
|
14923
|
-
import
|
|
15499
|
+
import path40 from "path";
|
|
14924
15500
|
async function runNew(ctx) {
|
|
14925
15501
|
const slug = ctx.positionals[0];
|
|
14926
15502
|
if (!slug) {
|
|
@@ -14946,14 +15522,14 @@ async function runNew(ctx) {
|
|
|
14946
15522
|
msg("new.title", ctx.language),
|
|
14947
15523
|
"",
|
|
14948
15524
|
msg("new.done", ctx.language),
|
|
14949
|
-
...result.files.map((f) => ` + ${
|
|
15525
|
+
...result.files.map((f) => ` + ${path40.relative(ctx.cwd, f)}`),
|
|
14950
15526
|
"",
|
|
14951
15527
|
`Siguiente: /satlas.specify ${result.slug} \u2014 escribe la especificaci\xF3n 100% funcional y de negocio.`
|
|
14952
15528
|
];
|
|
14953
15529
|
return {
|
|
14954
15530
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14955
15531
|
diagnostics: result.diagnostics,
|
|
14956
|
-
data: { slug: result.slug, files: result.files.map((f) =>
|
|
15532
|
+
data: { slug: result.slug, files: result.files.map((f) => path40.relative(ctx.cwd, f)) },
|
|
14957
15533
|
text: lines
|
|
14958
15534
|
};
|
|
14959
15535
|
}
|
|
@@ -14988,7 +15564,7 @@ async function runNext(ctx) {
|
|
|
14988
15564
|
}
|
|
14989
15565
|
|
|
14990
15566
|
// src/commands/present.ts
|
|
14991
|
-
import
|
|
15567
|
+
import path41 from "path";
|
|
14992
15568
|
async function runPresentCommand(ctx) {
|
|
14993
15569
|
const { root } = await requireWorkspace(ctx);
|
|
14994
15570
|
const slug = ctx.positionals[0];
|
|
@@ -15000,7 +15576,7 @@ async function runPresentCommand(ctx) {
|
|
|
15000
15576
|
if (result.path) {
|
|
15001
15577
|
lines.push("Propuesta generada");
|
|
15002
15578
|
lines.push("");
|
|
15003
|
-
lines.push(` archivo: ${
|
|
15579
|
+
lines.push(` archivo: ${path41.relative(ctx.cwd, result.path)}`);
|
|
15004
15580
|
if (result.hash) lines.push(` hash de la spec: ${shortHash(result.hash)}`);
|
|
15005
15581
|
lines.push("");
|
|
15006
15582
|
lines.push("\xC1brela en el navegador y comp\xE1rtela con el stakeholder.");
|
|
@@ -15010,7 +15586,7 @@ async function runPresentCommand(ctx) {
|
|
|
15010
15586
|
return {
|
|
15011
15587
|
exitCode: hasErrors2 ? 1 : 0,
|
|
15012
15588
|
diagnostics: result.diagnostics,
|
|
15013
|
-
data: result.path ? { path:
|
|
15589
|
+
data: result.path ? { path: path41.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
|
|
15014
15590
|
text: lines
|
|
15015
15591
|
};
|
|
15016
15592
|
}
|
|
@@ -15086,7 +15662,8 @@ function usage(usageText) {
|
|
|
15086
15662
|
|
|
15087
15663
|
// src/commands/status.ts
|
|
15088
15664
|
async function runStatus(ctx) {
|
|
15089
|
-
const { workspace, config, approvals } = await requireWorkspace(ctx);
|
|
15665
|
+
const { root, workspace, config, approvals } = await requireWorkspace(ctx);
|
|
15666
|
+
const advisory = await upgradeAdvisory(root);
|
|
15090
15667
|
const lines = [msg("status.title", ctx.language), ""];
|
|
15091
15668
|
const changes = [];
|
|
15092
15669
|
if (workspace.changes.length === 0) {
|
|
@@ -15118,17 +15695,44 @@ async function runStatus(ctx) {
|
|
|
15118
15695
|
for (const spec of workspace.specs) {
|
|
15119
15696
|
lines.push(` ${spec.domain} \u2014 ${spec.spec.requirements.length} requisitos`);
|
|
15120
15697
|
}
|
|
15121
|
-
const
|
|
15698
|
+
const fixes = await loadLivingFixes(root);
|
|
15699
|
+
lines.push("");
|
|
15700
|
+
lines.push(`${msg("status.fixes", ctx.language)}: ${fixes.length}`);
|
|
15701
|
+
if (fixes.length === 0) {
|
|
15702
|
+
lines.push(" (se llenan al archivar un fix)");
|
|
15703
|
+
} else {
|
|
15704
|
+
for (const fix of fixes) {
|
|
15705
|
+
lines.push(` ${fix.date || "\u2014"} ${(fix.domain ?? "\u2014").padEnd(10)} ${fix.slug} \u2014 ${fix.result}`);
|
|
15706
|
+
}
|
|
15707
|
+
}
|
|
15708
|
+
const diagnostics = [...workspace.diagnostics, ...advisory.diagnostics];
|
|
15709
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
15122
15710
|
return {
|
|
15123
15711
|
exitCode: errors > 0 ? 1 : 0,
|
|
15124
|
-
diagnostics
|
|
15125
|
-
data: {
|
|
15712
|
+
diagnostics,
|
|
15713
|
+
data: {
|
|
15714
|
+
changes,
|
|
15715
|
+
specs: workspace.specs.map((s) => ({ domain: s.domain, requirements: s.spec.requirements.length })),
|
|
15716
|
+
fixes: fixes.map((fix) => ({
|
|
15717
|
+
slug: fix.slug,
|
|
15718
|
+
date: fix.date,
|
|
15719
|
+
result: fix.result,
|
|
15720
|
+
...fix.domain !== void 0 ? { domain: fix.domain } : {},
|
|
15721
|
+
covers: fix.covers
|
|
15722
|
+
})),
|
|
15723
|
+
upgrade: {
|
|
15724
|
+
currentVersion: advisory.plan.currentVersion,
|
|
15725
|
+
pending: advisory.plan.pending.length,
|
|
15726
|
+
newer: advisory.plan.newer.length,
|
|
15727
|
+
unreadable: advisory.plan.unreadable.length
|
|
15728
|
+
}
|
|
15729
|
+
},
|
|
15126
15730
|
text: lines
|
|
15127
15731
|
};
|
|
15128
15732
|
}
|
|
15129
15733
|
|
|
15130
15734
|
// src/commands/trace.ts
|
|
15131
|
-
import
|
|
15735
|
+
import path42 from "path";
|
|
15132
15736
|
async function runTrace(ctx) {
|
|
15133
15737
|
const { workspace, config } = await requireWorkspace(ctx);
|
|
15134
15738
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -15148,7 +15752,7 @@ async function runTrace(ctx) {
|
|
|
15148
15752
|
data.push({ slug: change.slug, nodes: result.graph.nodes.length, edges: result.graph.edges.length, summary: result.summary, findings: result.findings });
|
|
15149
15753
|
lines.push(` ${change.slug}: ${result.graph.nodes.length} nodos, ${result.graph.edges.length} aristas, ${errors} errores, ${result.summary.warnings} avisos`);
|
|
15150
15754
|
for (const finding of result.findings) {
|
|
15151
|
-
const location = finding.path ? ` (${
|
|
15755
|
+
const location = finding.path ? ` (${path42.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
|
|
15152
15756
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}${location}`);
|
|
15153
15757
|
}
|
|
15154
15758
|
}
|
|
@@ -15157,12 +15761,130 @@ async function runTrace(ctx) {
|
|
|
15157
15761
|
return { exitCode: blockingErrors > 0 ? 1 : 0, diagnostics, data: { changes: data }, text: lines };
|
|
15158
15762
|
}
|
|
15159
15763
|
|
|
15764
|
+
// src/commands/upgrade.ts
|
|
15765
|
+
import path43 from "path";
|
|
15766
|
+
async function runUpgrade(ctx) {
|
|
15767
|
+
const { root } = await requireWorkspace(ctx);
|
|
15768
|
+
const apply = flagBool(ctx.flags, "apply");
|
|
15769
|
+
const rollback = flagBool(ctx.flags, "rollback");
|
|
15770
|
+
if (apply && rollback) {
|
|
15771
|
+
return {
|
|
15772
|
+
exitCode: 2,
|
|
15773
|
+
diagnostics: [
|
|
15774
|
+
{
|
|
15775
|
+
code: "ATLAS-UPGRADE-CLI-001",
|
|
15776
|
+
severity: "error",
|
|
15777
|
+
message: "Elige una sola acci\xF3n: --apply o --rollback",
|
|
15778
|
+
suggestion: "Vista previa: `satlas upgrade` \xB7 Aplicar: `satlas upgrade --apply` \xB7 Revertir: `satlas upgrade --rollback`"
|
|
15779
|
+
}
|
|
15780
|
+
]
|
|
15781
|
+
};
|
|
15782
|
+
}
|
|
15783
|
+
if (rollback) return renderRollback(await rollbackUpgrade(root), ctx);
|
|
15784
|
+
if (apply) return renderApply(await applyUpgrade(root), ctx);
|
|
15785
|
+
return renderPreview(await planUpgrade(root), root, ctx);
|
|
15786
|
+
}
|
|
15787
|
+
function summarizable(plan) {
|
|
15788
|
+
return {
|
|
15789
|
+
currentVersion: plan.currentVersion,
|
|
15790
|
+
upToDate: plan.upToDate,
|
|
15791
|
+
pending: plan.pending.map((i) => ({ artifact: i.artifact, from: i.from, to: i.to, migration: i.migration })),
|
|
15792
|
+
newer: plan.newer.map((n) => ({ artifact: n.artifact, reason: n.reason })),
|
|
15793
|
+
unreadable: plan.unreadable.map((u) => ({ artifact: u.artifact, reason: u.reason }))
|
|
15794
|
+
};
|
|
15795
|
+
}
|
|
15796
|
+
function renderPreview(plan, root, ctx) {
|
|
15797
|
+
const lines = [msg("upgrade.title", ctx.language), ""];
|
|
15798
|
+
if (plan.upToDate && plan.newer.length === 0 && plan.unreadable.length === 0) {
|
|
15799
|
+
lines.push(`El estado del proyecto est\xE1 al d\xEDa (versi\xF3n ${plan.currentVersion}). No hay nada que actualizar.`);
|
|
15800
|
+
return { exitCode: 0, diagnostics: [], data: summarizable(plan), text: lines };
|
|
15801
|
+
}
|
|
15802
|
+
lines.push(`${msg("upgrade.preview", ctx.language)}: no se modifica nada.`);
|
|
15803
|
+
lines.push(` Versi\xF3n vigente: ${plan.currentVersion}`);
|
|
15804
|
+
if (plan.pending.length === 0) {
|
|
15805
|
+
lines.push(" Elementos por actualizar: 0");
|
|
15806
|
+
} else {
|
|
15807
|
+
lines.push(` Elementos por actualizar: ${plan.pending.length}`);
|
|
15808
|
+
for (const item of plan.pending) {
|
|
15809
|
+
lines.push(` ${item.artifact} ${item.from} \u2192 ${item.to} (${item.migration})`);
|
|
15810
|
+
}
|
|
15811
|
+
lines.push("");
|
|
15812
|
+
lines.push(`Aplica la actualizaci\xF3n con \`satlas upgrade --apply\`.`);
|
|
15813
|
+
}
|
|
15814
|
+
for (const item of plan.newer) {
|
|
15815
|
+
lines.push(` Aviso: "${item.artifact}" ${item.reason}`);
|
|
15816
|
+
}
|
|
15817
|
+
for (const item of plan.unreadable) {
|
|
15818
|
+
lines.push(` Ilegible: "${item.artifact}" \u2014 ${item.reason}`);
|
|
15819
|
+
lines.push(` \u21B3 ${path43.relative(ctx.cwd, item.path)}`);
|
|
15820
|
+
}
|
|
15821
|
+
return { exitCode: 0, diagnostics: [], data: summarizable(plan), text: lines };
|
|
15822
|
+
}
|
|
15823
|
+
function renderApply(report, ctx) {
|
|
15824
|
+
const lines = [msg("upgrade.title", ctx.language), ""];
|
|
15825
|
+
if (report.status === "up-to-date") {
|
|
15826
|
+
lines.push("No hab\xEDa nada que actualizar; no se escribi\xF3 ning\xFAn elemento.");
|
|
15827
|
+
return { exitCode: 0, diagnostics: [], data: { status: report.status, plan: summarizable(report.plan) }, text: lines };
|
|
15828
|
+
}
|
|
15829
|
+
if (report.status === "failed") {
|
|
15830
|
+
lines.push(`La actualizaci\xF3n fall\xF3 en "${report.failure?.artifact}": ${report.failure?.message}`);
|
|
15831
|
+
lines.push("El proyecto qued\xF3 exactamente como estaba.");
|
|
15832
|
+
return {
|
|
15833
|
+
exitCode: 1,
|
|
15834
|
+
diagnostics: [{ code: "ATLAS-UPGRADE-101", severity: "error", message: `Fall\xF3 la actualizaci\xF3n en "${report.failure?.artifact}": ${report.failure?.message}` }],
|
|
15835
|
+
data: { status: report.status, failure: report.failure },
|
|
15836
|
+
text: lines
|
|
15837
|
+
};
|
|
15838
|
+
}
|
|
15839
|
+
lines.push(`Actualizaci\xF3n aplicada: ${report.applied.length} elemento(s) a la versi\xF3n ${report.plan.currentVersion}.`);
|
|
15840
|
+
lines.push(`Respaldo recuperable: ${report.backup?.relativeDir}`);
|
|
15841
|
+
if (report.plan.unreadable.length > 0) {
|
|
15842
|
+
lines.push(`No se tocaron ${report.plan.unreadable.length} elemento(s) ilegible(s).`);
|
|
15843
|
+
}
|
|
15844
|
+
return {
|
|
15845
|
+
exitCode: 0,
|
|
15846
|
+
diagnostics: [],
|
|
15847
|
+
data: {
|
|
15848
|
+
status: report.status,
|
|
15849
|
+
applied: report.applied.map((i) => i.artifact),
|
|
15850
|
+
backup: report.backup,
|
|
15851
|
+
plan: summarizable(report.plan)
|
|
15852
|
+
},
|
|
15853
|
+
text: lines
|
|
15854
|
+
};
|
|
15855
|
+
}
|
|
15856
|
+
function renderRollback(report, ctx) {
|
|
15857
|
+
const lines = [msg("upgrade.title", ctx.language), ""];
|
|
15858
|
+
if (report.status === "no-backup") {
|
|
15859
|
+
lines.push("No hay respaldo disponible para revertir; no se modific\xF3 nada.");
|
|
15860
|
+
return {
|
|
15861
|
+
exitCode: 1,
|
|
15862
|
+
diagnostics: [{ code: "ATLAS-UPGRADE-102", severity: "warning", message: "No hay respaldo disponible para revertir la \xFAltima actualizaci\xF3n" }],
|
|
15863
|
+
data: { status: report.status },
|
|
15864
|
+
text: lines
|
|
15865
|
+
};
|
|
15866
|
+
}
|
|
15867
|
+
if (report.status === "failed") {
|
|
15868
|
+
lines.push(`La reversi\xF3n fall\xF3 en "${report.failure?.artifact}": ${report.failure?.message}`);
|
|
15869
|
+
lines.push("El respaldo se conserva para reintentar.");
|
|
15870
|
+
return {
|
|
15871
|
+
exitCode: 1,
|
|
15872
|
+
diagnostics: [{ code: "ATLAS-UPGRADE-103", severity: "error", message: `Fall\xF3 la reversi\xF3n en "${report.failure?.artifact}": ${report.failure?.message}` }],
|
|
15873
|
+
data: { status: report.status, failure: report.failure },
|
|
15874
|
+
text: lines
|
|
15875
|
+
};
|
|
15876
|
+
}
|
|
15877
|
+
lines.push(`Se restaur\xF3 el estado previo: ${report.restored.length} elemento(s). El respaldo qued\xF3 consumido.`);
|
|
15878
|
+
return { exitCode: 0, diagnostics: [], data: { status: report.status, restored: report.restored, backup: report.backup }, text: lines };
|
|
15879
|
+
}
|
|
15880
|
+
|
|
15160
15881
|
// src/commands/validate.ts
|
|
15161
|
-
import
|
|
15882
|
+
import path44 from "path";
|
|
15162
15883
|
async function runValidate(ctx) {
|
|
15163
|
-
const { workspace, config, approvals } = await requireWorkspace(ctx);
|
|
15884
|
+
const { root, workspace, config, approvals } = await requireWorkspace(ctx);
|
|
15164
15885
|
const slug = flagString(ctx.flags, "change");
|
|
15165
15886
|
const strict = flagBool(ctx.flags, "strict");
|
|
15887
|
+
const advisory = slug ? void 0 : await upgradeAdvisory(root);
|
|
15166
15888
|
const diagnostics = [];
|
|
15167
15889
|
const lines = [msg("validate.title", ctx.language), ""];
|
|
15168
15890
|
for (const spec of workspace.specs) {
|
|
@@ -15184,22 +15906,35 @@ async function runValidate(ctx) {
|
|
|
15184
15906
|
for (const finding of evaluation.lintFindings) {
|
|
15185
15907
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${finding.line ? `:${finding.line}` : ""} \u2014 ${finding.message}`);
|
|
15186
15908
|
}
|
|
15187
|
-
lines.push(` archivo: ${
|
|
15909
|
+
lines.push(` archivo: ${path44.relative(ctx.cwd, path44.join(change.dir, "spec.md"))}`);
|
|
15188
15910
|
}
|
|
15189
15911
|
if (targets.length === 0) lines.push("Sin cambios activos.");
|
|
15912
|
+
if (advisory && advisory.plan.pending.length > 0) {
|
|
15913
|
+
lines.push(`Actualizaci\xF3n pendiente: ${advisory.plan.pending.length} elemento(s) (vista previa: \`satlas upgrade\`).`);
|
|
15914
|
+
}
|
|
15190
15915
|
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
15191
15916
|
const warnings = diagnostics.filter((d) => d.severity === "warning").length;
|
|
15192
15917
|
if (errors === 0 && warnings === 0) lines.push(msg("validate.ok", ctx.language));
|
|
15193
15918
|
return {
|
|
15194
15919
|
exitCode: errors > 0 || strict && warnings > 0 ? 1 : 0,
|
|
15195
|
-
diagnostics,
|
|
15196
|
-
data: {
|
|
15920
|
+
diagnostics: advisory ? [...diagnostics, ...advisory.diagnostics] : diagnostics,
|
|
15921
|
+
data: {
|
|
15922
|
+
changes,
|
|
15923
|
+
errors,
|
|
15924
|
+
warnings,
|
|
15925
|
+
upgrade: advisory ? {
|
|
15926
|
+
currentVersion: advisory.plan.currentVersion,
|
|
15927
|
+
pending: advisory.plan.pending.length,
|
|
15928
|
+
newer: advisory.plan.newer.length,
|
|
15929
|
+
unreadable: advisory.plan.unreadable.length
|
|
15930
|
+
} : void 0
|
|
15931
|
+
},
|
|
15197
15932
|
text: lines
|
|
15198
15933
|
};
|
|
15199
15934
|
}
|
|
15200
15935
|
|
|
15201
15936
|
// src/commands/verify.ts
|
|
15202
|
-
import
|
|
15937
|
+
import path45 from "path";
|
|
15203
15938
|
async function runVerify(ctx) {
|
|
15204
15939
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
15205
15940
|
const slug = ctx.positionals[0];
|
|
@@ -15242,7 +15977,7 @@ async function runVerify(ctx) {
|
|
|
15242
15977
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-VERIFY-001", severity: "error", message: "Falta --by <nombre> (o define meta.owner)" }] };
|
|
15243
15978
|
}
|
|
15244
15979
|
const profilesDir = await resolveProfilesDir();
|
|
15245
|
-
const profile = await loadActiveProfile(
|
|
15980
|
+
const profile = await loadActiveProfile(path45.join(root, ".sdd"), [profilesDir ?? "", path45.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
|
|
15246
15981
|
const record = await recordEvidence({
|
|
15247
15982
|
root,
|
|
15248
15983
|
slug,
|
|
@@ -15264,7 +15999,7 @@ async function runVerify(ctx) {
|
|
|
15264
15999
|
if (record.evidence.command) lines.push(` comando: ${record.evidence.command}`);
|
|
15265
16000
|
lines.push(` resultado: ${record.evidence.result}`);
|
|
15266
16001
|
if (record.evidence.outputHash) lines.push(` hash: ${record.evidence.outputHash}`);
|
|
15267
|
-
lines.push(` archivo: ${
|
|
16002
|
+
lines.push(` archivo: ${path45.relative(ctx.cwd, record.path)}`);
|
|
15268
16003
|
if (record.stdout) {
|
|
15269
16004
|
lines.push("");
|
|
15270
16005
|
lines.push(" salida (\xFAltimas l\xEDneas):");
|
|
@@ -15279,7 +16014,7 @@ async function runVerify(ctx) {
|
|
|
15279
16014
|
return {
|
|
15280
16015
|
exitCode: record.exitCode === 0 ? 0 : 1,
|
|
15281
16016
|
diagnostics: record.diagnostics,
|
|
15282
|
-
data: { evidence: record.evidence, path:
|
|
16017
|
+
data: { evidence: record.evidence, path: path45.relative(ctx.cwd, record.path), exitCode: record.exitCode },
|
|
15283
16018
|
text: lines
|
|
15284
16019
|
};
|
|
15285
16020
|
}
|
|
@@ -15334,6 +16069,7 @@ var HANDLERS2 = {
|
|
|
15334
16069
|
trace: runTrace,
|
|
15335
16070
|
waves: runWaves,
|
|
15336
16071
|
doctor: runDoctorCommand,
|
|
16072
|
+
upgrade: runUpgrade,
|
|
15337
16073
|
approve: runApprove,
|
|
15338
16074
|
issue: runIssue,
|
|
15339
16075
|
adapters: runAdapters,
|
|
@@ -15378,7 +16114,7 @@ async function requireWorkspace(ctx) {
|
|
|
15378
16114
|
throw new CliError("ATLAS-WS-001", msg("cli.noWorkspace", ctx.language), 2);
|
|
15379
16115
|
}
|
|
15380
16116
|
const { workspace, config } = await loadWorkspace(root);
|
|
15381
|
-
const approvals = await loadApprovals(
|
|
16117
|
+
const approvals = await loadApprovals(path46.join(root, ".sdd"));
|
|
15382
16118
|
return { root, workspace, config, approvals: approvals.byArtifact };
|
|
15383
16119
|
}
|
|
15384
16120
|
var CliError = class extends Error {
|
|
@@ -15445,7 +16181,7 @@ function printResult(command, result, json2, language) {
|
|
|
15445
16181
|
}
|
|
15446
16182
|
const lines = [...result.text ?? []];
|
|
15447
16183
|
for (const d of result.diagnostics) {
|
|
15448
|
-
const location = d.path ? ` ${
|
|
16184
|
+
const location = d.path ? ` ${path46.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
|
|
15449
16185
|
lines.push(`${d.severity === "error" ? "ERROR" : d.severity === "warning" ? "AVISO" : "NOTA"} ${d.code}${location} \u2014 ${d.message}`);
|
|
15450
16186
|
if (d.suggestion) lines.push(` \u21B3 ${d.suggestion}`);
|
|
15451
16187
|
}
|