specatlas 0.1.24 → 0.1.27
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 path45 from "path";
|
|
1589
1590
|
|
|
1590
1591
|
// ../core/dist/index.js
|
|
1591
1592
|
import path from "path";
|
|
@@ -9025,8 +9026,12 @@ import path15 from "path";
|
|
|
9025
9026
|
import { promises as fs } from "fs";
|
|
9026
9027
|
import { cp, rename, rm } from "fs/promises";
|
|
9027
9028
|
import path16 from "path";
|
|
9029
|
+
import { promises as fs2 } from "fs";
|
|
9028
9030
|
import path17 from "path";
|
|
9031
|
+
import { parse as parseYaml8, stringify as stringifyYaml5 } from "yaml";
|
|
9029
9032
|
import path18 from "path";
|
|
9033
|
+
import path19 from "path";
|
|
9034
|
+
import path20 from "path";
|
|
9030
9035
|
var CORE_VERSION = "0.0.1";
|
|
9031
9036
|
var REQ_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}$/;
|
|
9032
9037
|
var SCENARIO_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}-S\d+$/;
|
|
@@ -9593,6 +9598,146 @@ function findScenarioHeading(lines, blockLineIndex) {
|
|
|
9593
9598
|
}
|
|
9594
9599
|
return void 0;
|
|
9595
9600
|
}
|
|
9601
|
+
var HEADER_KEYS = /* @__PURE__ */ new Set(["t\xE9rmino", "termino", "term", "definici\xF3n", "definicion", "definition", "sin\xF3nimos", "sinonimos", "synonyms"]);
|
|
9602
|
+
var SEPARATOR_RE = /^:?-{2,}:?$/;
|
|
9603
|
+
function parseGlossary(md, filePath) {
|
|
9604
|
+
const diagnostics = [];
|
|
9605
|
+
const terms = [];
|
|
9606
|
+
const lines = md.replace(/\r\n?/g, "\n").split("\n");
|
|
9607
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
9608
|
+
const raw = lines[i] ?? "";
|
|
9609
|
+
if (!raw.trim().startsWith("|")) continue;
|
|
9610
|
+
const inner = raw.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
9611
|
+
const cells = inner.split("|").map((c) => c.trim());
|
|
9612
|
+
if (cells.length < 2) {
|
|
9613
|
+
diagnostics.push(diag("LINT-GLO-001", "warning", "Fila del glosario sin columnas suficientes", { path: filePath, line: i + 1 }));
|
|
9614
|
+
continue;
|
|
9615
|
+
}
|
|
9616
|
+
const first = cells[0] ?? "";
|
|
9617
|
+
if (HEADER_KEYS.has(first.toLowerCase()) || SEPARATOR_RE.test(first)) continue;
|
|
9618
|
+
const term = first;
|
|
9619
|
+
const definition = cells[1] ?? "";
|
|
9620
|
+
if (term === "" || definition === "") {
|
|
9621
|
+
diagnostics.push(diag("LINT-GLO-002", "warning", "T\xE9rmino o definici\xF3n vac\xEDos en el glosario", { path: filePath, line: i + 1 }));
|
|
9622
|
+
continue;
|
|
9623
|
+
}
|
|
9624
|
+
const synonyms = (cells[2] ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
9625
|
+
terms.push({ term, definition, synonyms });
|
|
9626
|
+
}
|
|
9627
|
+
return { terms, diagnostics };
|
|
9628
|
+
}
|
|
9629
|
+
function normalizePath(p) {
|
|
9630
|
+
return toPosix(p.trim()).replace(/^\.\//, "").replace(/\/+$/, "").toLowerCase();
|
|
9631
|
+
}
|
|
9632
|
+
function fileMatches(query, candidate) {
|
|
9633
|
+
if (query === candidate) return true;
|
|
9634
|
+
return candidate.endsWith(`/${query}`);
|
|
9635
|
+
}
|
|
9636
|
+
function scenarioToReq(workspace) {
|
|
9637
|
+
const map = /* @__PURE__ */ new Map();
|
|
9638
|
+
for (const spec of workspace.specs) {
|
|
9639
|
+
for (const req of spec.spec.requirements) {
|
|
9640
|
+
for (const sc of req.scenarios) map.set(sc.id, req.id);
|
|
9641
|
+
}
|
|
9642
|
+
}
|
|
9643
|
+
for (const change of workspace.changes) {
|
|
9644
|
+
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
9645
|
+
for (const sc of req.scenarios) map.set(sc.id, req.id);
|
|
9646
|
+
}
|
|
9647
|
+
}
|
|
9648
|
+
return map;
|
|
9649
|
+
}
|
|
9650
|
+
function emptyReport(target, kind) {
|
|
9651
|
+
return { target, kind, exists: false, scenarios: [], tasks: [], requirements: [], changes: [], files: [], evidence: [] };
|
|
9652
|
+
}
|
|
9653
|
+
function collect(workspace, report, covers, reqOf) {
|
|
9654
|
+
const requirements = /* @__PURE__ */ new Set();
|
|
9655
|
+
for (const change of workspace.changes) {
|
|
9656
|
+
let changeMatches = false;
|
|
9657
|
+
for (const block of change.tasks?.blocks ?? []) {
|
|
9658
|
+
for (const task of block.tasks) {
|
|
9659
|
+
const hit = task.covers.some((c) => covers.has(c.toUpperCase()));
|
|
9660
|
+
if (!hit) continue;
|
|
9661
|
+
changeMatches = true;
|
|
9662
|
+
report.tasks.push({ id: task.id, change: change.slug, text: task.text, files: task.files, covers: task.covers });
|
|
9663
|
+
for (const c of task.covers) {
|
|
9664
|
+
const upper = c.toUpperCase();
|
|
9665
|
+
if (covers.has(upper)) {
|
|
9666
|
+
const req = reqOf.get(upper) ?? upper;
|
|
9667
|
+
requirements.add(req);
|
|
9668
|
+
}
|
|
9669
|
+
}
|
|
9670
|
+
for (const f of task.files) report.files.push(f);
|
|
9671
|
+
}
|
|
9672
|
+
}
|
|
9673
|
+
if (changeMatches) report.changes.push(change.slug);
|
|
9674
|
+
for (const ev of change.verify?.evidence ?? []) {
|
|
9675
|
+
if (covers.has(ev.scenario.toUpperCase())) {
|
|
9676
|
+
report.evidence.push({ scenario: ev.scenario, result: ev.result, change: change.slug });
|
|
9677
|
+
}
|
|
9678
|
+
}
|
|
9679
|
+
}
|
|
9680
|
+
report.requirements = [...requirements].sort();
|
|
9681
|
+
report.changes = [...new Set(report.changes)].sort();
|
|
9682
|
+
report.files = [...new Set(report.files)].sort();
|
|
9683
|
+
report.evidence.sort((a, b) => a.scenario.localeCompare(b.scenario));
|
|
9684
|
+
}
|
|
9685
|
+
function impactOfRequirement(workspace, reqId) {
|
|
9686
|
+
const id = reqId.toUpperCase();
|
|
9687
|
+
const scenarios = /* @__PURE__ */ new Set();
|
|
9688
|
+
let exists2 = false;
|
|
9689
|
+
for (const spec of workspace.specs) {
|
|
9690
|
+
for (const req of spec.spec.requirements) {
|
|
9691
|
+
if (req.id !== id) continue;
|
|
9692
|
+
exists2 = true;
|
|
9693
|
+
for (const sc of req.scenarios) scenarios.add(sc.id);
|
|
9694
|
+
}
|
|
9695
|
+
}
|
|
9696
|
+
for (const change of workspace.changes) {
|
|
9697
|
+
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
9698
|
+
if (req.id !== id) continue;
|
|
9699
|
+
exists2 = true;
|
|
9700
|
+
for (const sc of req.scenarios) scenarios.add(sc.id);
|
|
9701
|
+
}
|
|
9702
|
+
if ((change.delta?.removed ?? []).some((r) => r.id === id)) exists2 = true;
|
|
9703
|
+
if ((change.delta?.renamed ?? []).some((r) => r.from.id === id || r.to.id === id)) exists2 = true;
|
|
9704
|
+
}
|
|
9705
|
+
const report = emptyReport(id, "requirement");
|
|
9706
|
+
if (!exists2) return report;
|
|
9707
|
+
report.exists = true;
|
|
9708
|
+
report.scenarios = [...scenarios].sort();
|
|
9709
|
+
const reqOf = scenarioToReq(workspace);
|
|
9710
|
+
collect(workspace, report, /* @__PURE__ */ new Set([id, ...scenarios]), reqOf);
|
|
9711
|
+
if (!report.requirements.includes(id)) report.requirements.unshift(id);
|
|
9712
|
+
return report;
|
|
9713
|
+
}
|
|
9714
|
+
function impactOfFile(workspace, file) {
|
|
9715
|
+
const query = normalizePath(file);
|
|
9716
|
+
const report = emptyReport(file, "file");
|
|
9717
|
+
const covers = /* @__PURE__ */ new Set();
|
|
9718
|
+
const matched = /* @__PURE__ */ new Set();
|
|
9719
|
+
const changes = /* @__PURE__ */ new Set();
|
|
9720
|
+
for (const change of workspace.changes) {
|
|
9721
|
+
for (const block of change.tasks?.blocks ?? []) {
|
|
9722
|
+
for (const task of block.tasks) {
|
|
9723
|
+
if (!task.files.some((f) => fileMatches(query, normalizePath(f)))) continue;
|
|
9724
|
+
changes.add(change.slug);
|
|
9725
|
+
for (const f of task.files) matched.add(f);
|
|
9726
|
+
for (const c of task.covers) covers.add(c.toUpperCase());
|
|
9727
|
+
report.tasks.push({ id: task.id, change: change.slug, text: task.text, files: task.files, covers: task.covers });
|
|
9728
|
+
}
|
|
9729
|
+
}
|
|
9730
|
+
}
|
|
9731
|
+
if (matched.size === 0) return report;
|
|
9732
|
+
report.exists = true;
|
|
9733
|
+
report.files = [...matched].sort();
|
|
9734
|
+
report.changes = [...changes].sort();
|
|
9735
|
+
const reqOf = scenarioToReq(workspace);
|
|
9736
|
+
const requirements = /* @__PURE__ */ new Set();
|
|
9737
|
+
for (const c of covers) requirements.add(reqOf.get(c) ?? c);
|
|
9738
|
+
report.requirements = [...requirements].sort();
|
|
9739
|
+
return report;
|
|
9740
|
+
}
|
|
9596
9741
|
var VAGUE_ES = ["r\xE1pido", "r\xE1pida", "r\xE1pidos", "r\xE1pidas", "f\xE1cil", "f\xE1ciles", "varios", "varias", "\xF3ptimo", "\xF3ptima", "robusto", "robusta", "adecuado", "adecuada", "eficiente", "amigable", "moderno", "moderna", "mejor", "mejores", "simple", "sencillo", "intuitivo", "intuitiva", "apropiado", "apropiada", "suficiente", "razonable"];
|
|
9597
9742
|
var VAGUE_EN = ["fast", "quick", "easy", "several", "optimal", "robust", "adequate", "efficient", "friendly", "modern", "better", "best", "simple", "intuitive", "appropriate", "sufficient", "reasonable", "nice", "clean"];
|
|
9598
9743
|
var TECH_ES = ["api", "endpoint", "tabla", "base de datos", "sql", "json", "xml", "frontend", "backend", "react", "vue", "angular", "node", "npm", "docker", "kubernetes", "servidor", "columna", "query", "script", "framework", "librer\xEDa", "microservicio", "cache", "cach\xE9", "deploy", "despliegue", "commit", "merge", "rama", "branch", "endpoint", "crud", "rest", "http", "sdk", "webhook", "orm", "job", "cron", "websocket", "graphql", "api rest", "tablas", "\xEDndice", "primary key", "foreign key"];
|
|
@@ -9602,14 +9747,14 @@ function wordBoundary(text, term) {
|
|
|
9602
9747
|
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
|
|
9603
9748
|
return re.test(text);
|
|
9604
9749
|
}
|
|
9605
|
-
function lintText(text, code, terms, label,
|
|
9750
|
+
function lintText(text, code, terms, label, path212, line) {
|
|
9606
9751
|
const out = [];
|
|
9607
9752
|
const lower = text.toLowerCase();
|
|
9608
9753
|
for (const term of terms) {
|
|
9609
9754
|
if (wordBoundary(lower, term)) {
|
|
9610
9755
|
out.push(
|
|
9611
9756
|
diag(code, "error", `${label}: "${term}"`, {
|
|
9612
|
-
path:
|
|
9757
|
+
path: path212,
|
|
9613
9758
|
line,
|
|
9614
9759
|
suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
|
|
9615
9760
|
})
|
|
@@ -9618,7 +9763,7 @@ function lintText(text, code, terms, label, path192, line) {
|
|
|
9618
9763
|
}
|
|
9619
9764
|
return out;
|
|
9620
9765
|
}
|
|
9621
|
-
function lintRequirement(req,
|
|
9766
|
+
function lintRequirement(req, path212, opts = {}) {
|
|
9622
9767
|
const out = [];
|
|
9623
9768
|
const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
|
|
9624
9769
|
const tech = opts.language === "en" ? TECH_EN : TECH_ES;
|
|
@@ -9629,32 +9774,32 @@ function lintRequirement(req, path192, opts = {}) {
|
|
|
9629
9774
|
...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
|
|
9630
9775
|
];
|
|
9631
9776
|
for (const part of parts) {
|
|
9632
|
-
out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n",
|
|
9777
|
+
out.push(...lintText(part.text, "LINT-BIZ-002", vague, "Palabra vaga en la especificaci\xF3n", path212, part.line));
|
|
9633
9778
|
if (opts.businessOnly !== false) {
|
|
9634
|
-
out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio",
|
|
9779
|
+
out.push(...lintText(part.text, "LINT-BIZ-001", tech, "Jerga t\xE9cnica en la especificaci\xF3n de negocio", path212, part.line));
|
|
9635
9780
|
}
|
|
9636
9781
|
}
|
|
9637
9782
|
if (req.scenarios.length === 0) {
|
|
9638
|
-
out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path:
|
|
9783
|
+
out.push(diag("TRACE-001", "error", `El requisito ${req.id} no tiene ning\xFAn escenario`, { path: path212, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
|
|
9639
9784
|
}
|
|
9640
9785
|
return out;
|
|
9641
9786
|
}
|
|
9642
|
-
function lintDelta(delta, livingRequirements,
|
|
9787
|
+
function lintDelta(delta, livingRequirements, path212, opts = {}) {
|
|
9643
9788
|
const out = [...delta.diagnostics];
|
|
9644
9789
|
for (const req of [...delta.added, ...delta.modified]) {
|
|
9645
|
-
out.push(...lintRequirement(req,
|
|
9790
|
+
out.push(...lintRequirement(req, path212, opts));
|
|
9646
9791
|
}
|
|
9647
9792
|
for (const req of delta.modified) {
|
|
9648
9793
|
const living = livingRequirements.get(req.id);
|
|
9649
9794
|
if (!living) {
|
|
9650
|
-
out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path:
|
|
9795
|
+
out.push(diag("TRACE-007", "error", `MODIFIED ${req.id} no existe en la spec viva; usa ADDED`, { path: path212, line: req.line }));
|
|
9651
9796
|
continue;
|
|
9652
9797
|
}
|
|
9653
9798
|
for (const existing of living.scenarios) {
|
|
9654
9799
|
if (!req.scenarios.some((s) => s.id === existing.id)) {
|
|
9655
9800
|
out.push(
|
|
9656
9801
|
diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
|
|
9657
|
-
path:
|
|
9802
|
+
path: path212,
|
|
9658
9803
|
line: req.line,
|
|
9659
9804
|
suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
|
|
9660
9805
|
})
|
|
@@ -9665,15 +9810,15 @@ function lintDelta(delta, livingRequirements, path192, opts = {}) {
|
|
|
9665
9810
|
for (const req of delta.removed) {
|
|
9666
9811
|
const living = livingRequirements.get(req.id);
|
|
9667
9812
|
if (!living) {
|
|
9668
|
-
out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path:
|
|
9813
|
+
out.push(diag("TRACE-007", "error", `REMOVED ${req.id} no existe en la spec viva`, { path: path212, line: req.line }));
|
|
9669
9814
|
}
|
|
9670
9815
|
}
|
|
9671
9816
|
for (const rename2 of delta.renamed) {
|
|
9672
9817
|
if (rename2.from.id !== rename2.to.id) {
|
|
9673
|
-
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path:
|
|
9818
|
+
out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: path212, line: rename2.line }));
|
|
9674
9819
|
}
|
|
9675
9820
|
if (!livingRequirements.has(rename2.from.id)) {
|
|
9676
|
-
out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path:
|
|
9821
|
+
out.push(diag("TRACE-007", "error", `RENAMED ${rename2.from.id} no existe en la spec viva`, { path: path212, line: rename2.line }));
|
|
9677
9822
|
}
|
|
9678
9823
|
}
|
|
9679
9824
|
return out;
|
|
@@ -9697,7 +9842,7 @@ var MERMAID_KEYWORDS = [
|
|
|
9697
9842
|
"architecture-beta"
|
|
9698
9843
|
];
|
|
9699
9844
|
var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
|
|
9700
|
-
function lintPlan(planText,
|
|
9845
|
+
function lintPlan(planText, path212) {
|
|
9701
9846
|
const out = [];
|
|
9702
9847
|
const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
|
|
9703
9848
|
for (const [index, block] of blocks.entries()) {
|
|
@@ -9707,7 +9852,7 @@ function lintPlan(planText, path192) {
|
|
|
9707
9852
|
if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
|
|
9708
9853
|
out.push(
|
|
9709
9854
|
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)}"`, {
|
|
9710
|
-
path:
|
|
9855
|
+
path: path212,
|
|
9711
9856
|
suggestion: "Corrige el tipo del diagrama o elimina el bloque"
|
|
9712
9857
|
})
|
|
9713
9858
|
);
|
|
@@ -9721,7 +9866,7 @@ function lintPlan(planText, path192) {
|
|
|
9721
9866
|
if (open !== 0) {
|
|
9722
9867
|
out.push(
|
|
9723
9868
|
diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
|
|
9724
|
-
path:
|
|
9869
|
+
path: path212,
|
|
9725
9870
|
suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
|
|
9726
9871
|
})
|
|
9727
9872
|
);
|
|
@@ -9733,7 +9878,7 @@ function lintPlan(planText, path192) {
|
|
|
9733
9878
|
if (message.includes(";")) {
|
|
9734
9879
|
out.push(
|
|
9735
9880
|
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`, {
|
|
9736
|
-
path:
|
|
9881
|
+
path: path212,
|
|
9737
9882
|
suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
|
|
9738
9883
|
})
|
|
9739
9884
|
);
|
|
@@ -10055,6 +10200,17 @@ function deriveState(input) {
|
|
|
10055
10200
|
}
|
|
10056
10201
|
if (blockingFindings > 0) {
|
|
10057
10202
|
blockedBy.push(`${blockingFindings} hallazgo(s) bloqueante(s)`);
|
|
10203
|
+
if (tasksTotal > 0 && tasksDone < tasksTotal) {
|
|
10204
|
+
return {
|
|
10205
|
+
state: "building",
|
|
10206
|
+
blockedBy,
|
|
10207
|
+
nextAction: next(`/satlas.build ${change.slug}`, `Construir en olas (${tasksDone}/${tasksTotal} tareas) \xB7 ${blockingFindings} hallazgo(s) pendientes`, true),
|
|
10208
|
+
progress
|
|
10209
|
+
};
|
|
10210
|
+
}
|
|
10211
|
+
if (tasksTotal > 0) {
|
|
10212
|
+
return { state: "built", blockedBy, nextAction: next(`satlas verify ${change.slug}`, "Registrar evidencia por escenario"), progress };
|
|
10213
|
+
}
|
|
10058
10214
|
return { state: "spec_draft", blockedBy, nextAction: next(`satlas validate --change ${change.slug}`, "Corregir los hallazgos de la especificaci\xF3n"), progress };
|
|
10059
10215
|
}
|
|
10060
10216
|
if ((approval.status === "missing" || approval.status === "stale") && requiresMockups(change.meta, cfg) && input.mockupsReady !== true && !mockupOverride(change)) {
|
|
@@ -10954,7 +11110,7 @@ async function approveFromGithub(opts) {
|
|
|
10954
11110
|
if (!by) {
|
|
10955
11111
|
return { approved: false, label, issueNumber: number, diagnostics: [diag("ATLAS-GH-011", "error", "No se pudo determinar qui\xE9n aprueba (--by ni `gh api user`)")] };
|
|
10956
11112
|
}
|
|
10957
|
-
const { signApproval: signApproval2 } = await import("./approvals-2PO6223I-
|
|
11113
|
+
const { signApproval: signApproval2 } = await import("./approvals-2PO6223I-DENKLEXU.js");
|
|
10958
11114
|
const signed = await signApproval2({
|
|
10959
11115
|
root: opts.root,
|
|
10960
11116
|
artifact: path5.posix.join("changes", opts.slug, "spec.md"),
|
|
@@ -12837,6 +12993,351 @@ async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
|
|
|
12837
12993
|
void now;
|
|
12838
12994
|
await writeText(path16.join(root, ".sdd", "INDEX.md"), markdown2);
|
|
12839
12995
|
}
|
|
12996
|
+
var SCHEMA_VERSION = 1;
|
|
12997
|
+
var BACKUP_DIRNAME = ".backup";
|
|
12998
|
+
var BACKUP_POINTER = ".latest";
|
|
12999
|
+
var SCHEMA_MIGRATIONS = [
|
|
13000
|
+
{
|
|
13001
|
+
id: "0001-sellar-version-de-esquema",
|
|
13002
|
+
description: "Sella la versi\xF3n de esquema vigente en los elementos que no la declaran",
|
|
13003
|
+
from: 0,
|
|
13004
|
+
to: SCHEMA_VERSION
|
|
13005
|
+
}
|
|
13006
|
+
];
|
|
13007
|
+
var FIXED_ARTIFACTS = ["config.yaml", "approvals.yaml", path17.join("profiles", "detected.yaml")];
|
|
13008
|
+
var CHANGE_ARTIFACTS = ["meta.yaml", path17.join("mockups", "manifest.yaml")];
|
|
13009
|
+
async function pushChangeArtifacts(out, sddDir, relDir) {
|
|
13010
|
+
for (const rel of CHANGE_ARTIFACTS) {
|
|
13011
|
+
const abs = path17.join(sddDir, relDir, rel);
|
|
13012
|
+
if (await exists(abs)) out.push({ artifact: toPosix(path17.join(relDir, rel)), path: abs });
|
|
13013
|
+
}
|
|
13014
|
+
}
|
|
13015
|
+
async function collectVersionedArtifacts(root) {
|
|
13016
|
+
const sddDir = path17.join(root, ".sdd");
|
|
13017
|
+
const out = [];
|
|
13018
|
+
for (const rel of FIXED_ARTIFACTS) {
|
|
13019
|
+
const abs = path17.join(sddDir, rel);
|
|
13020
|
+
if (await exists(abs)) out.push({ artifact: toPosix(rel), path: abs });
|
|
13021
|
+
}
|
|
13022
|
+
const changesDir = path17.join(sddDir, "changes");
|
|
13023
|
+
for (const slug of await listDirs(changesDir)) {
|
|
13024
|
+
if (slug === "archive") continue;
|
|
13025
|
+
await pushChangeArtifacts(out, sddDir, path17.join("changes", slug));
|
|
13026
|
+
}
|
|
13027
|
+
for (const entry of await listDirs(path17.join(changesDir, "archive"))) {
|
|
13028
|
+
await pushChangeArtifacts(out, sddDir, path17.join("changes", "archive", entry));
|
|
13029
|
+
}
|
|
13030
|
+
return out;
|
|
13031
|
+
}
|
|
13032
|
+
function readArtifactVersion(content) {
|
|
13033
|
+
let data;
|
|
13034
|
+
try {
|
|
13035
|
+
data = parseYaml8(content);
|
|
13036
|
+
} catch (err) {
|
|
13037
|
+
return { state: "unreadable", reason: `YAML inv\xE1lido (${err.message})` };
|
|
13038
|
+
}
|
|
13039
|
+
if (data === null || data === void 0) return { state: "absent" };
|
|
13040
|
+
if (typeof data !== "object" || Array.isArray(data)) return { state: "unreadable", reason: "la ra\xEDz no es un mapa de claves y valores" };
|
|
13041
|
+
const raw = data["schema_version"];
|
|
13042
|
+
if (raw === void 0) return { state: "absent" };
|
|
13043
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) return { state: "unreadable", reason: `schema_version con valor no v\xE1lido: ${String(raw)}` };
|
|
13044
|
+
return { state: "ok", version: raw };
|
|
13045
|
+
}
|
|
13046
|
+
function stampSchemaVersion(content, version) {
|
|
13047
|
+
const line = `schema_version: ${version}`;
|
|
13048
|
+
const bom = content.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
13049
|
+
const body = bom ? content.slice(1) : content;
|
|
13050
|
+
if (body.trim() === "") return `${bom}${line}
|
|
13051
|
+
`;
|
|
13052
|
+
const eol = body.includes("\r\n") ? "\r\n" : "\n";
|
|
13053
|
+
const lines = body.split(/\r?\n/);
|
|
13054
|
+
const trailing = lines.length > 1 && lines[lines.length - 1] === "";
|
|
13055
|
+
let index = 0;
|
|
13056
|
+
while (index < lines.length) {
|
|
13057
|
+
const trimmed = (lines[index] ?? "").trim();
|
|
13058
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
13059
|
+
index += 1;
|
|
13060
|
+
continue;
|
|
13061
|
+
}
|
|
13062
|
+
break;
|
|
13063
|
+
}
|
|
13064
|
+
if ((lines[index] ?? "").trim() === "---") index += 1;
|
|
13065
|
+
const insertAt = trailing ? Math.min(index, lines.length - 1) : index;
|
|
13066
|
+
lines.splice(insertAt, 0, line);
|
|
13067
|
+
const next = bom + lines.join(eol);
|
|
13068
|
+
const check = readArtifactVersion(next);
|
|
13069
|
+
if (check.state !== "ok" || check.version !== version) return void 0;
|
|
13070
|
+
return next;
|
|
13071
|
+
}
|
|
13072
|
+
async function planUpgrade(root) {
|
|
13073
|
+
const artifacts = await collectVersionedArtifacts(root);
|
|
13074
|
+
const pending = [];
|
|
13075
|
+
const newer = [];
|
|
13076
|
+
const unreadable = [];
|
|
13077
|
+
for (const found of artifacts) {
|
|
13078
|
+
const content = await readTextIfExists(found.path);
|
|
13079
|
+
if (content === void 0) continue;
|
|
13080
|
+
const state = readArtifactVersion(content);
|
|
13081
|
+
if (state.state === "unreadable") {
|
|
13082
|
+
unreadable.push({ ...found, reason: state.reason });
|
|
13083
|
+
continue;
|
|
13084
|
+
}
|
|
13085
|
+
if (state.state === "ok") {
|
|
13086
|
+
if (state.version > SCHEMA_VERSION) {
|
|
13087
|
+
newer.push({ ...found, reason: `produce la versi\xF3n ${state.version}, m\xE1s nueva que la vigente (${SCHEMA_VERSION})` });
|
|
13088
|
+
continue;
|
|
13089
|
+
}
|
|
13090
|
+
if (state.version === SCHEMA_VERSION) continue;
|
|
13091
|
+
}
|
|
13092
|
+
const from = state.state === "ok" ? state.version : 0;
|
|
13093
|
+
const migration = SCHEMA_MIGRATIONS.find((m) => m.from === from);
|
|
13094
|
+
if (!migration) {
|
|
13095
|
+
unreadable.push({ ...found, reason: `no hay migraci\xF3n registrada desde la versi\xF3n ${from}` });
|
|
13096
|
+
continue;
|
|
13097
|
+
}
|
|
13098
|
+
const stamped = stampSchemaVersion(content, migration.to);
|
|
13099
|
+
if (stamped === void 0) {
|
|
13100
|
+
unreadable.push({ ...found, reason: "no se pudo sellar con seguridad sin alterar el resto del contenido" });
|
|
13101
|
+
continue;
|
|
13102
|
+
}
|
|
13103
|
+
pending.push({ ...found, from, to: migration.to, migration: migration.id, summary: migration.description });
|
|
13104
|
+
}
|
|
13105
|
+
return { root, currentVersion: SCHEMA_VERSION, pending, newer, unreadable, upToDate: pending.length === 0 };
|
|
13106
|
+
}
|
|
13107
|
+
async function removeDir(dir) {
|
|
13108
|
+
try {
|
|
13109
|
+
await fs2.rm(dir, { recursive: true, force: true });
|
|
13110
|
+
} catch {
|
|
13111
|
+
}
|
|
13112
|
+
}
|
|
13113
|
+
async function removeFile(file) {
|
|
13114
|
+
try {
|
|
13115
|
+
await fs2.rm(file, { force: true });
|
|
13116
|
+
} catch {
|
|
13117
|
+
}
|
|
13118
|
+
}
|
|
13119
|
+
function backupName(now) {
|
|
13120
|
+
return `${localCompact(now)}${localOffset(now).replace(":", "")}`;
|
|
13121
|
+
}
|
|
13122
|
+
async function applyUpgrade(root, now = /* @__PURE__ */ new Date()) {
|
|
13123
|
+
const plan = await planUpgrade(root);
|
|
13124
|
+
if (plan.pending.length === 0) return { status: "up-to-date", plan, applied: [] };
|
|
13125
|
+
const sddDir = path17.join(root, ".sdd");
|
|
13126
|
+
const backupRoot = path17.join(sddDir, BACKUP_DIRNAME);
|
|
13127
|
+
const name = backupName(now);
|
|
13128
|
+
const backupDir = path17.join(backupRoot, name);
|
|
13129
|
+
const pointerFile = path17.join(backupRoot, BACKUP_POINTER);
|
|
13130
|
+
const previous = [];
|
|
13131
|
+
try {
|
|
13132
|
+
for (const item of plan.pending) {
|
|
13133
|
+
const content = await readTextIfExists(item.path);
|
|
13134
|
+
if (content === void 0) throw new Error("el elemento desapareci\xF3 mientras se respaldaba");
|
|
13135
|
+
previous.push({ item, contents: content });
|
|
13136
|
+
await writeText(path17.join(backupDir, "files", ...item.artifact.split("/")), content);
|
|
13137
|
+
}
|
|
13138
|
+
await writeText(
|
|
13139
|
+
path17.join(backupDir, "backup.yaml"),
|
|
13140
|
+
stringifyYaml5(
|
|
13141
|
+
{
|
|
13142
|
+
schema_version: SCHEMA_VERSION,
|
|
13143
|
+
created_at: localStamp(now),
|
|
13144
|
+
to_version: SCHEMA_VERSION,
|
|
13145
|
+
files: plan.pending.map((i) => ({ artifact: i.artifact, from: i.from, to: i.to, migration: i.migration }))
|
|
13146
|
+
},
|
|
13147
|
+
{ lineWidth: 120 }
|
|
13148
|
+
)
|
|
13149
|
+
);
|
|
13150
|
+
await writeText(pointerFile, `${name}
|
|
13151
|
+
`);
|
|
13152
|
+
} catch (err) {
|
|
13153
|
+
await removeDir(backupDir);
|
|
13154
|
+
await removeFile(pointerFile);
|
|
13155
|
+
return { status: "failed", plan, applied: [], failure: { artifact: "(respaldo)", message: err.message } };
|
|
13156
|
+
}
|
|
13157
|
+
const written = [];
|
|
13158
|
+
for (const { item, contents } of previous) {
|
|
13159
|
+
try {
|
|
13160
|
+
const next = stampSchemaVersion(contents, item.to);
|
|
13161
|
+
if (next === void 0) throw new Error("no se pudo sellar con seguridad");
|
|
13162
|
+
await writeText(item.path, next);
|
|
13163
|
+
written.push(item);
|
|
13164
|
+
} catch (err) {
|
|
13165
|
+
for (const w of written) {
|
|
13166
|
+
const prev = previous.find((p) => p.item.artifact === w.artifact);
|
|
13167
|
+
if (prev) {
|
|
13168
|
+
try {
|
|
13169
|
+
await writeText(prev.item.path, prev.contents);
|
|
13170
|
+
} catch {
|
|
13171
|
+
}
|
|
13172
|
+
}
|
|
13173
|
+
}
|
|
13174
|
+
await removeFile(pointerFile);
|
|
13175
|
+
await removeDir(backupDir);
|
|
13176
|
+
return { status: "failed", plan, applied: [], failure: { artifact: item.artifact, message: err.message } };
|
|
13177
|
+
}
|
|
13178
|
+
}
|
|
13179
|
+
for (const dir of await listDirs(backupRoot)) {
|
|
13180
|
+
if (dir !== name) await removeDir(path17.join(backupRoot, dir));
|
|
13181
|
+
}
|
|
13182
|
+
return {
|
|
13183
|
+
status: "applied",
|
|
13184
|
+
plan,
|
|
13185
|
+
applied: written,
|
|
13186
|
+
backup: {
|
|
13187
|
+
name,
|
|
13188
|
+
dir: backupDir,
|
|
13189
|
+
relativeDir: toPosix(path17.relative(root, backupDir)),
|
|
13190
|
+
createdAt: localStamp(now),
|
|
13191
|
+
files: plan.pending.map((i) => i.artifact)
|
|
13192
|
+
}
|
|
13193
|
+
};
|
|
13194
|
+
}
|
|
13195
|
+
async function rollbackUpgrade(root) {
|
|
13196
|
+
const sddDir = path17.join(root, ".sdd");
|
|
13197
|
+
const backupRoot = path17.join(sddDir, BACKUP_DIRNAME);
|
|
13198
|
+
const pointerFile = path17.join(backupRoot, BACKUP_POINTER);
|
|
13199
|
+
const name = (await readTextIfExists(pointerFile))?.trim();
|
|
13200
|
+
if (!name) return { status: "no-backup", restored: [] };
|
|
13201
|
+
const backupDir = path17.join(backupRoot, name);
|
|
13202
|
+
const manifestRaw = await readTextIfExists(path17.join(backupDir, "backup.yaml"));
|
|
13203
|
+
if (manifestRaw === void 0) return { status: "no-backup", restored: [] };
|
|
13204
|
+
let files = [];
|
|
13205
|
+
try {
|
|
13206
|
+
const parsed = parseYaml8(manifestRaw);
|
|
13207
|
+
files = (parsed?.files ?? []).filter((f) => typeof f?.artifact === "string");
|
|
13208
|
+
} catch {
|
|
13209
|
+
return { status: "no-backup", restored: [] };
|
|
13210
|
+
}
|
|
13211
|
+
const restored = [];
|
|
13212
|
+
for (const file of files) {
|
|
13213
|
+
const from = path17.join(backupDir, "files", ...file.artifact.split("/"));
|
|
13214
|
+
const to = path17.join(sddDir, ...file.artifact.split("/"));
|
|
13215
|
+
const content = await readTextIfExists(from);
|
|
13216
|
+
if (content === void 0) continue;
|
|
13217
|
+
try {
|
|
13218
|
+
await writeText(to, content);
|
|
13219
|
+
restored.push(file.artifact);
|
|
13220
|
+
} catch (err) {
|
|
13221
|
+
return { status: "failed", restored, backup: name, failure: { artifact: file.artifact, message: err.message } };
|
|
13222
|
+
}
|
|
13223
|
+
}
|
|
13224
|
+
await removeDir(backupDir);
|
|
13225
|
+
await removeFile(pointerFile);
|
|
13226
|
+
return { status: "restored", restored, backup: name };
|
|
13227
|
+
}
|
|
13228
|
+
async function upgradeAdvisory(root) {
|
|
13229
|
+
const plan = await planUpgrade(root);
|
|
13230
|
+
const diagnostics = [];
|
|
13231
|
+
if (plan.pending.length > 0) {
|
|
13232
|
+
diagnostics.push(
|
|
13233
|
+
diag("ATLAS-UPGRADE-001", "warning", `El estado del proyecto no corresponde a la versi\xF3n vigente: ${plan.pending.length} elemento(s) por actualizar`, {
|
|
13234
|
+
suggestion: "Vista previa: `satlas upgrade` \xB7 Aplicar: `satlas upgrade --apply`"
|
|
13235
|
+
})
|
|
13236
|
+
);
|
|
13237
|
+
}
|
|
13238
|
+
for (const item of plan.newer) {
|
|
13239
|
+
diagnostics.push(
|
|
13240
|
+
diag("ATLAS-UPGRADE-002", "warning", `El proyecto fue producido por una versi\xF3n m\xE1s nueva: "${item.artifact}" (${item.reason})`, {
|
|
13241
|
+
path: item.path,
|
|
13242
|
+
suggestion: "Actualiza la herramienta; no se actualiza hacia atr\xE1s"
|
|
13243
|
+
})
|
|
13244
|
+
);
|
|
13245
|
+
}
|
|
13246
|
+
for (const item of plan.unreadable) {
|
|
13247
|
+
diagnostics.push(
|
|
13248
|
+
diag("ATLAS-UPGRADE-003", "warning", `No se pudo interpretar "${item.artifact}": ${item.reason}`, {
|
|
13249
|
+
path: item.path,
|
|
13250
|
+
suggestion: "Revisa el elemento; no se modificar\xE1"
|
|
13251
|
+
})
|
|
13252
|
+
);
|
|
13253
|
+
}
|
|
13254
|
+
return { plan, diagnostics };
|
|
13255
|
+
}
|
|
13256
|
+
var SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
13257
|
+
var SARIF_VERSION = "2.1.0";
|
|
13258
|
+
var TOOL_NAME = "SpecAtlas";
|
|
13259
|
+
var TOOL_URL = "https://github.com/AlonsoAM/specatlas";
|
|
13260
|
+
var RULE_FAMILIES = [
|
|
13261
|
+
[/^LINT-STR-/, "Estructura de los artefactos"],
|
|
13262
|
+
[/^LINT-BIZ-/, "Lenguaje de negocio de la especificaci\xF3n"],
|
|
13263
|
+
[/^LINT-DLT-/, "Delta de especificaci\xF3n"],
|
|
13264
|
+
[/^LINT-EVD-/, "Evidencia registrada"],
|
|
13265
|
+
[/^LINT-TSK-/, "Tareas declaradas"],
|
|
13266
|
+
[/^LINT-PLN-/, "Plan t\xE9cnico"],
|
|
13267
|
+
[/^(LINT-MKP|MKP)-/, "Mockups"],
|
|
13268
|
+
[/^TRACE-/, "Trazabilidad requisito, escenario, tarea y evidencia"],
|
|
13269
|
+
[/^PACK-/, "Pack de cumplimiento"],
|
|
13270
|
+
[/^ATLAS-UPGRADE-/, "Actualizaci\xF3n del estado del proyecto"],
|
|
13271
|
+
[/^ATLAS-ADAPTERS-/, "Adaptadores de agente"],
|
|
13272
|
+
[/^ATLAS-CI-/, "Comprobaci\xF3n continua"],
|
|
13273
|
+
[/^ATLAS-/, "Estado del proyecto"]
|
|
13274
|
+
];
|
|
13275
|
+
function ruleDescription(code) {
|
|
13276
|
+
for (const [pattern, description] of RULE_FAMILIES) {
|
|
13277
|
+
if (pattern.test(code)) return description;
|
|
13278
|
+
}
|
|
13279
|
+
return code;
|
|
13280
|
+
}
|
|
13281
|
+
function sarifLevel(severity) {
|
|
13282
|
+
if (severity === "error") return "error";
|
|
13283
|
+
if (severity === "warning") return "warning";
|
|
13284
|
+
return "note";
|
|
13285
|
+
}
|
|
13286
|
+
function resultOf(diagnostic, root) {
|
|
13287
|
+
const result = {
|
|
13288
|
+
ruleId: diagnostic.code,
|
|
13289
|
+
level: sarifLevel(diagnostic.severity),
|
|
13290
|
+
message: { text: diagnostic.message }
|
|
13291
|
+
};
|
|
13292
|
+
if (diagnostic.path) {
|
|
13293
|
+
const rel = path18.relative(root, diagnostic.path);
|
|
13294
|
+
if (rel !== "") {
|
|
13295
|
+
const physicalLocation = {
|
|
13296
|
+
artifactLocation: { uri: toPosix(rel) },
|
|
13297
|
+
...diagnostic.line ? { region: { startLine: diagnostic.line } } : {}
|
|
13298
|
+
};
|
|
13299
|
+
result.locations = [{ physicalLocation }];
|
|
13300
|
+
}
|
|
13301
|
+
}
|
|
13302
|
+
return result;
|
|
13303
|
+
}
|
|
13304
|
+
function toSarifReport(opts) {
|
|
13305
|
+
const rules = /* @__PURE__ */ new Map();
|
|
13306
|
+
const results = [];
|
|
13307
|
+
for (const diagnostic of opts.diagnostics) {
|
|
13308
|
+
let rule = rules.get(diagnostic.code);
|
|
13309
|
+
if (!rule) {
|
|
13310
|
+
rule = { id: diagnostic.code, shortDescription: { text: ruleDescription(diagnostic.code) } };
|
|
13311
|
+
if (diagnostic.suggestion) rule.help = { text: diagnostic.suggestion };
|
|
13312
|
+
rules.set(diagnostic.code, rule);
|
|
13313
|
+
} else if (!rule.help && diagnostic.suggestion) {
|
|
13314
|
+
rule.help = { text: diagnostic.suggestion };
|
|
13315
|
+
}
|
|
13316
|
+
results.push(resultOf(diagnostic, opts.root));
|
|
13317
|
+
}
|
|
13318
|
+
return {
|
|
13319
|
+
$schema: SARIF_SCHEMA,
|
|
13320
|
+
version: SARIF_VERSION,
|
|
13321
|
+
runs: [
|
|
13322
|
+
{
|
|
13323
|
+
tool: {
|
|
13324
|
+
driver: {
|
|
13325
|
+
name: TOOL_NAME,
|
|
13326
|
+
informationUri: TOOL_URL,
|
|
13327
|
+
version: opts.version,
|
|
13328
|
+
rules: [...rules.values()]
|
|
13329
|
+
}
|
|
13330
|
+
},
|
|
13331
|
+
results,
|
|
13332
|
+
invocations: [{ executionSuccessful: !(opts.failed ?? false) }]
|
|
13333
|
+
}
|
|
13334
|
+
]
|
|
13335
|
+
};
|
|
13336
|
+
}
|
|
13337
|
+
function toSarifText(opts) {
|
|
13338
|
+
return `${JSON.stringify(toSarifReport(opts), null, 2)}
|
|
13339
|
+
`;
|
|
13340
|
+
}
|
|
12840
13341
|
async function runDoctor(root) {
|
|
12841
13342
|
const findings = [];
|
|
12842
13343
|
const { workspace, config } = await loadWorkspace(root);
|
|
@@ -12844,7 +13345,7 @@ async function runDoctor(root) {
|
|
|
12844
13345
|
const approvals = await loadApprovals(workspace.sddDir);
|
|
12845
13346
|
findings.push(...approvals.diagnostics);
|
|
12846
13347
|
for (const change of workspace.changes) {
|
|
12847
|
-
const deltaPath =
|
|
13348
|
+
const deltaPath = path19.join(change.dir, "spec.md");
|
|
12848
13349
|
const deltaContent = await readTextIfExists(deltaPath);
|
|
12849
13350
|
const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
|
|
12850
13351
|
if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
|
|
@@ -12866,7 +13367,7 @@ async function runDoctor(root) {
|
|
|
12866
13367
|
}
|
|
12867
13368
|
for (const override of change.meta?.overrides ?? []) {
|
|
12868
13369
|
if (!override.reason.trim() || !override.by.trim()) {
|
|
12869
|
-
findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path:
|
|
13370
|
+
findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: path19.join(change.dir, "meta.yaml") }));
|
|
12870
13371
|
}
|
|
12871
13372
|
}
|
|
12872
13373
|
}
|
|
@@ -12898,7 +13399,7 @@ function livingRequirementsMap(specs) {
|
|
|
12898
13399
|
return map;
|
|
12899
13400
|
}
|
|
12900
13401
|
async function runCiGate(opts) {
|
|
12901
|
-
const root =
|
|
13402
|
+
const root = path20.resolve(opts.root);
|
|
12902
13403
|
const { workspace, config } = await loadWorkspace(root);
|
|
12903
13404
|
const diagnostics = [];
|
|
12904
13405
|
const checks = [];
|
|
@@ -12909,7 +13410,7 @@ async function runCiGate(opts) {
|
|
|
12909
13410
|
let changesErrors = 0;
|
|
12910
13411
|
let changesWarnings = 0;
|
|
12911
13412
|
for (const change of workspace.changes) {
|
|
12912
|
-
const lintFindings = change.delta ? lintDelta(change.delta, living,
|
|
13413
|
+
const lintFindings = change.delta ? lintDelta(change.delta, living, path20.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
|
|
12913
13414
|
const trace = checkTrace({
|
|
12914
13415
|
specs: workspace.specs,
|
|
12915
13416
|
change,
|
|
@@ -13018,6 +13519,7 @@ var CATALOG = [
|
|
|
13018
13519
|
{ 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]" },
|
|
13019
13520
|
{ name: "waves", description: "Calcula las olas paralelas de construcci\xF3n", flags: ["change", "max-parallel", "json"], usage: "satlas waves [--change <slug>] [--max-parallel N] [--json]" },
|
|
13020
13521
|
{ name: "doctor", description: "Diagn\xF3stico de salud del workspace", flags: ["json"], usage: "satlas doctor [--json]" },
|
|
13522
|
+
{ 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]" },
|
|
13021
13523
|
{ 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' },
|
|
13022
13524
|
{ 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>" },
|
|
13023
13525
|
{ 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]" },
|
|
@@ -13028,7 +13530,8 @@ var CATALOG = [
|
|
|
13028
13530
|
{ name: "analyze", description: "Chequeo cruzado (lint + trace + waves + mockups) y escribe analyze.md", flags: ["json"], usage: "satlas analyze <slug>" },
|
|
13029
13531
|
{ 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]" },
|
|
13030
13532
|
{ name: "present", description: "Genera el paquete de propuesta para el stakeholder (HTML autocontenido)", flags: ["json"], usage: "satlas present <slug>" },
|
|
13031
|
-
{ name: "ci", description: "Gate de pipeline: specs + cambios + doctor + adaptadores (sin agente)", flags: ["strict", "json"], usage: "satlas ci [--strict]" },
|
|
13533
|
+
{ name: "ci", description: "Gate de pipeline: specs + cambios + doctor + adaptadores (sin agente)", flags: ["strict", "sarif", "json"], usage: "satlas ci [--strict] [--sarif <ruta>]" },
|
|
13534
|
+
{ name: "mcp", description: "V\xEDa de consulta de solo lectura para asistentes (protocolo MCP sobre entrada/salida est\xE1ndar)", flags: [], usage: "satlas mcp" },
|
|
13032
13535
|
{ name: "metrics", description: "M\xE9tricas locales del workspace (progreso, WIP, throughput, evidencia)", flags: ["json"], usage: "satlas metrics [--json]" },
|
|
13033
13536
|
{ 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" },
|
|
13034
13537
|
{ name: "hash", description: "Calcula el sha256 de un archivo o texto (para evidencia)", flags: ["text", "json"], usage: 'satlas hash <archivo> | satlas hash --text "salida"' },
|
|
@@ -13064,6 +13567,8 @@ var ES2 = {
|
|
|
13064
13567
|
"init.done": "Workspace SDD creado.",
|
|
13065
13568
|
"new.title": "Nuevo cambio",
|
|
13066
13569
|
"new.done": "Cambio creado. Siguiente: escribe la spec funcional.",
|
|
13570
|
+
"upgrade.title": "Actualizar el estado del proyecto",
|
|
13571
|
+
"upgrade.preview": "Vista previa",
|
|
13067
13572
|
"summary.findings": "hallazgos",
|
|
13068
13573
|
"label.errors": "errores",
|
|
13069
13574
|
"label.warnings": "avisos",
|
|
@@ -13096,6 +13601,8 @@ var EN2 = {
|
|
|
13096
13601
|
"init.done": "SDD workspace created.",
|
|
13097
13602
|
"new.title": "New change",
|
|
13098
13603
|
"new.done": "Change created. Next: write the business spec.",
|
|
13604
|
+
"upgrade.title": "Upgrade project state",
|
|
13605
|
+
"upgrade.preview": "Preview",
|
|
13099
13606
|
"summary.findings": "findings",
|
|
13100
13607
|
"label.errors": "errors",
|
|
13101
13608
|
"label.warnings": "warnings",
|
|
@@ -13123,26 +13630,26 @@ function cliVersion() {
|
|
|
13123
13630
|
}
|
|
13124
13631
|
|
|
13125
13632
|
// src/commands/adapters.ts
|
|
13126
|
-
import
|
|
13633
|
+
import path24 from "path";
|
|
13127
13634
|
|
|
13128
13635
|
// ../adapters/dist/index.js
|
|
13129
|
-
import
|
|
13130
|
-
import { stringify as
|
|
13636
|
+
import path21 from "path";
|
|
13637
|
+
import { stringify as stringifyYaml6 } from "yaml";
|
|
13131
13638
|
import path22 from "path";
|
|
13132
13639
|
async function loadWorkflow(workflowDir) {
|
|
13133
|
-
const phasesDir =
|
|
13134
|
-
const snippetsDir =
|
|
13640
|
+
const phasesDir = path21.join(workflowDir, "phases");
|
|
13641
|
+
const snippetsDir = path21.join(workflowDir, "snippets");
|
|
13135
13642
|
const phases = [];
|
|
13136
13643
|
const snippets = /* @__PURE__ */ new Map();
|
|
13137
13644
|
const hashParts = [];
|
|
13138
13645
|
for (const entry of (await listDir(phasesDir)).sort()) {
|
|
13139
13646
|
if (!entry.endsWith(".md")) continue;
|
|
13140
|
-
const filePath =
|
|
13647
|
+
const filePath = path21.join(phasesDir, entry);
|
|
13141
13648
|
const raw = await readText(filePath);
|
|
13142
13649
|
hashParts.push(raw);
|
|
13143
13650
|
const fm = parseFrontmatter(raw, filePath);
|
|
13144
13651
|
const data = fm.data;
|
|
13145
|
-
const id = typeof data["id"] === "string" ? data["id"] :
|
|
13652
|
+
const id = typeof data["id"] === "string" ? data["id"] : path21.basename(entry, ".md");
|
|
13146
13653
|
phases.push({
|
|
13147
13654
|
id,
|
|
13148
13655
|
title: typeof data["title"] === "string" ? data["title"] : id,
|
|
@@ -13156,9 +13663,9 @@ async function loadWorkflow(workflowDir) {
|
|
|
13156
13663
|
}
|
|
13157
13664
|
for (const entry of (await listDir(snippetsDir)).sort()) {
|
|
13158
13665
|
if (!entry.endsWith(".md")) continue;
|
|
13159
|
-
const raw = await readTextIfExists(
|
|
13666
|
+
const raw = await readTextIfExists(path21.join(snippetsDir, entry)) ?? "";
|
|
13160
13667
|
hashParts.push(raw);
|
|
13161
|
-
snippets.set(
|
|
13668
|
+
snippets.set(path21.basename(entry, ".md"), raw.trim());
|
|
13162
13669
|
}
|
|
13163
13670
|
return { phases: phases.sort((a, b) => a.id.localeCompare(b.id)), snippets, sourceHash: sha256(hashParts.join("\n---\n")) };
|
|
13164
13671
|
}
|
|
@@ -13182,7 +13689,7 @@ function vars(language, slugToken, commandPrefix) {
|
|
|
13182
13689
|
return { SLUG: slugToken, LANGUAGE: language, LANGUAGE_NAME: LANGUAGE_NAMES[language], SDD_DIR: ".sdd", COMMAND_PREFIX: commandPrefix };
|
|
13183
13690
|
}
|
|
13184
13691
|
function frontmatter(data, body) {
|
|
13185
|
-
const yaml2 =
|
|
13692
|
+
const yaml2 = stringifyYaml6(data, { lineWidth: 0 }).trimEnd();
|
|
13186
13693
|
return `---
|
|
13187
13694
|
${yaml2}
|
|
13188
13695
|
---
|
|
@@ -13462,7 +13969,7 @@ async function checkAdapters(opts) {
|
|
|
13462
13969
|
}
|
|
13463
13970
|
|
|
13464
13971
|
// src/paths.ts
|
|
13465
|
-
import
|
|
13972
|
+
import path23 from "path";
|
|
13466
13973
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13467
13974
|
async function resolveProfilesDir() {
|
|
13468
13975
|
const env = process.env["SPECATLAS_PROFILES_DIR"];
|
|
@@ -13472,16 +13979,16 @@ async function resolveProfilesDir() {
|
|
|
13472
13979
|
async function resolveWorkflowDir() {
|
|
13473
13980
|
const env = process.env["SPECATLAS_WORKFLOW_DIR"];
|
|
13474
13981
|
if (env && await exists(env)) return env;
|
|
13475
|
-
return walkUpFor(
|
|
13982
|
+
return walkUpFor(path23.join("workflow", "phases"));
|
|
13476
13983
|
}
|
|
13477
13984
|
async function walkUpFor(relative) {
|
|
13478
|
-
let dir =
|
|
13985
|
+
let dir = path23.dirname(fileURLToPath2(import.meta.url));
|
|
13479
13986
|
for (let i = 0; i < 8; i += 1) {
|
|
13480
|
-
const candidate =
|
|
13987
|
+
const candidate = path23.join(dir, relative);
|
|
13481
13988
|
if (await exists(candidate)) {
|
|
13482
|
-
return relative.includes(
|
|
13989
|
+
return relative.includes(path23.sep) ? path23.dirname(candidate) : candidate;
|
|
13483
13990
|
}
|
|
13484
|
-
const parent =
|
|
13991
|
+
const parent = path23.dirname(dir);
|
|
13485
13992
|
if (parent === dir) break;
|
|
13486
13993
|
dir = parent;
|
|
13487
13994
|
}
|
|
@@ -13559,7 +14066,7 @@ async function runAdapters(ctx) {
|
|
|
13559
14066
|
for (const [target, count2] of byTarget) lines.push(` ${target}: ${count2} archivo(s)`);
|
|
13560
14067
|
if (report.written.length > 0) {
|
|
13561
14068
|
lines.push("");
|
|
13562
|
-
for (const p of report.written) lines.push(` + ${
|
|
14069
|
+
for (const p of report.written) lines.push(` + ${path24.relative(ctx.cwd, path24.join(root, p))}`);
|
|
13563
14070
|
} else {
|
|
13564
14071
|
lines.push("");
|
|
13565
14072
|
lines.push("Sin cambios: los adaptadores ya estaban al d\xEDa.");
|
|
@@ -13570,7 +14077,7 @@ async function runAdapters(ctx) {
|
|
|
13570
14077
|
}
|
|
13571
14078
|
|
|
13572
14079
|
// src/commands/adopt.ts
|
|
13573
|
-
import
|
|
14080
|
+
import path25 from "path";
|
|
13574
14081
|
async function runAdopt(ctx) {
|
|
13575
14082
|
const { root } = await requireWorkspace(ctx);
|
|
13576
14083
|
const domainsFlag = flagString(ctx.flags, "domains") ?? flagString(ctx.flags, "domain");
|
|
@@ -13590,11 +14097,11 @@ async function runAdopt(ctx) {
|
|
|
13590
14097
|
}
|
|
13591
14098
|
if (result.createdSpecs.length > 0) {
|
|
13592
14099
|
lines.push("");
|
|
13593
|
-
for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${
|
|
14100
|
+
for (const spec of result.createdSpecs) lines.push(` ${result.dryRun ? "[dry-run] " : "+ "}${path25.relative(ctx.cwd, spec)}`);
|
|
13594
14101
|
}
|
|
13595
14102
|
if (result.reportPath && !result.dryRun) {
|
|
13596
14103
|
lines.push("");
|
|
13597
|
-
lines.push(` Informe: ${
|
|
14104
|
+
lines.push(` Informe: ${path25.relative(ctx.cwd, result.reportPath)}`);
|
|
13598
14105
|
}
|
|
13599
14106
|
lines.push("");
|
|
13600
14107
|
lines.push("Siguiente: fase /satlas-adopt por dominio (requisitos AS-IS) y luego:");
|
|
@@ -13607,8 +14114,8 @@ async function runAdopt(ctx) {
|
|
|
13607
14114
|
diagnostics: result.diagnostics,
|
|
13608
14115
|
data: {
|
|
13609
14116
|
domains: result.domains.map((d) => ({ name: d.name, files: d.files, existingSpec: d.existingSpec })),
|
|
13610
|
-
createdSpecs: result.createdSpecs.map((s) =>
|
|
13611
|
-
reportPath: result.reportPath ?
|
|
14117
|
+
createdSpecs: result.createdSpecs.map((s) => path25.relative(ctx.cwd, s)),
|
|
14118
|
+
reportPath: result.reportPath ? path25.relative(ctx.cwd, result.reportPath) : void 0,
|
|
13612
14119
|
stack: result.stack,
|
|
13613
14120
|
dryRun: result.dryRun
|
|
13614
14121
|
},
|
|
@@ -13617,7 +14124,7 @@ async function runAdopt(ctx) {
|
|
|
13617
14124
|
}
|
|
13618
14125
|
|
|
13619
14126
|
// src/commands/analyze.ts
|
|
13620
|
-
import
|
|
14127
|
+
import path26 from "path";
|
|
13621
14128
|
async function runAnalyzeCommand(ctx) {
|
|
13622
14129
|
const { root } = await requireWorkspace(ctx);
|
|
13623
14130
|
const slug = ctx.positionals[0];
|
|
@@ -13628,23 +14135,23 @@ async function runAnalyzeCommand(ctx) {
|
|
|
13628
14135
|
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`];
|
|
13629
14136
|
if (result.waves) lines.push(` olas: ${result.waves.blocks} bloque(s), ${result.waves.waves} ola(s), ${result.waves.tasks} tarea(s)`);
|
|
13630
14137
|
if (result.mockups) lines.push(` mockups: ${result.mockups.screens} pantalla(s)${result.mockups.stale ? " (desactualizados)" : ""}`);
|
|
13631
|
-
if (result.path) lines.push("", ` informe: ${
|
|
14138
|
+
if (result.path) lines.push("", ` informe: ${path26.relative(ctx.cwd, result.path)}`);
|
|
13632
14139
|
for (const finding of result.findings) {
|
|
13633
14140
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}`);
|
|
13634
14141
|
}
|
|
13635
14142
|
return {
|
|
13636
14143
|
exitCode: result.status === "blocked" ? 1 : 0,
|
|
13637
14144
|
diagnostics: result.findings,
|
|
13638
|
-
data: result.path ? { ...result, path:
|
|
14145
|
+
data: result.path ? { ...result, path: path26.relative(ctx.cwd, result.path) } : result,
|
|
13639
14146
|
text: lines
|
|
13640
14147
|
};
|
|
13641
14148
|
}
|
|
13642
14149
|
|
|
13643
14150
|
// src/commands/approve.ts
|
|
13644
|
-
import
|
|
14151
|
+
import path28 from "path";
|
|
13645
14152
|
|
|
13646
14153
|
// src/commands/issue.ts
|
|
13647
|
-
import
|
|
14154
|
+
import path27 from "path";
|
|
13648
14155
|
async function runIssue(ctx) {
|
|
13649
14156
|
const action = ctx.positionals[0] ?? "status";
|
|
13650
14157
|
const slug = ctx.positionals[1];
|
|
@@ -13672,7 +14179,7 @@ async function runIssue(ctx) {
|
|
|
13672
14179
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-ISSUE-000", severity: "error", message: "Falta el slug: satlas issue status <slug>" }] };
|
|
13673
14180
|
}
|
|
13674
14181
|
const change = await loadChange(root, slug);
|
|
13675
|
-
const { config } = await loadConfig(
|
|
14182
|
+
const { config } = await loadConfig(path27.join(root, ".sdd"));
|
|
13676
14183
|
const tracker = change.meta?.tracker;
|
|
13677
14184
|
const lines = [`Issue de ${slug}`, ""];
|
|
13678
14185
|
if (!tracker || tracker.provider !== "github") {
|
|
@@ -13743,7 +14250,7 @@ async function runApprove(ctx) {
|
|
|
13743
14250
|
};
|
|
13744
14251
|
}
|
|
13745
14252
|
const { root, workspace, config } = await requireWorkspace(ctx);
|
|
13746
|
-
const artifact = target.includes("/") || target.includes("\\") ? target :
|
|
14253
|
+
const artifact = target.includes("/") || target.includes("\\") ? target : path28.join("changes", target, "spec.md");
|
|
13747
14254
|
const channelFlag = flagString(ctx.flags, "channel");
|
|
13748
14255
|
const channel = channelFlag === "presentation" || channelFlag === "editor" || channelFlag === "pr" || channelFlag === "tracker" ? channelFlag : "cli";
|
|
13749
14256
|
const change = workspace.changes.find(
|
|
@@ -13784,13 +14291,13 @@ async function runApprove(ctx) {
|
|
|
13784
14291
|
return {
|
|
13785
14292
|
exitCode: hasErrors2 ? 1 : 0,
|
|
13786
14293
|
diagnostics: result.diagnostics,
|
|
13787
|
-
data: result.approval ? { ...result.approval, file:
|
|
14294
|
+
data: result.approval ? { ...result.approval, file: path28.relative(ctx.cwd, result.file) } : void 0,
|
|
13788
14295
|
text: lines
|
|
13789
14296
|
};
|
|
13790
14297
|
}
|
|
13791
14298
|
|
|
13792
14299
|
// src/commands/archive.ts
|
|
13793
|
-
import
|
|
14300
|
+
import path29 from "path";
|
|
13794
14301
|
async function runArchive(ctx) {
|
|
13795
14302
|
const slug = ctx.positionals[0];
|
|
13796
14303
|
if (!slug) {
|
|
@@ -13820,7 +14327,7 @@ async function runArchive(ctx) {
|
|
|
13820
14327
|
lines.push(` modificados: ${result.fold.applied.modified.join(", ") || "\u2014"}`);
|
|
13821
14328
|
lines.push(` eliminados: ${result.fold.applied.removed.join(", ") || "\u2014"}`);
|
|
13822
14329
|
lines.push(` renombrados: ${result.fold.applied.renamed.join(", ") || "\u2014"}`);
|
|
13823
|
-
if (result.archivedTo) lines.push(` archivado en: ${
|
|
14330
|
+
if (result.archivedTo) lines.push(` archivado en: ${path29.relative(ctx.cwd, result.archivedTo)}`);
|
|
13824
14331
|
} else {
|
|
13825
14332
|
lines.push("El delta no contiene operaciones (ADDED/MODIFIED/REMOVED/RENAMED).");
|
|
13826
14333
|
}
|
|
@@ -13834,9 +14341,11 @@ async function runArchive(ctx) {
|
|
|
13834
14341
|
}
|
|
13835
14342
|
|
|
13836
14343
|
// src/commands/ci.ts
|
|
14344
|
+
import path30 from "path";
|
|
13837
14345
|
async function runCi(ctx) {
|
|
13838
14346
|
const { root, config } = await requireWorkspace(ctx);
|
|
13839
14347
|
const strict = flagBool(ctx.flags, "strict");
|
|
14348
|
+
const sarifPath = flagString(ctx.flags, "sarif");
|
|
13840
14349
|
const extra = [];
|
|
13841
14350
|
const workflowDir = await resolveWorkflowDir();
|
|
13842
14351
|
if (workflowDir) {
|
|
@@ -13854,28 +14363,54 @@ async function runCi(ctx) {
|
|
|
13854
14363
|
});
|
|
13855
14364
|
}
|
|
13856
14365
|
const gate = await runCiGate({ root, strict, extra });
|
|
14366
|
+
const diagnostics = [...gate.diagnostics];
|
|
13857
14367
|
const lines = ["CI de SpecAtlas", ""];
|
|
14368
|
+
let sarifWritten;
|
|
14369
|
+
if (sarifPath) {
|
|
14370
|
+
const target = path30.resolve(ctx.cwd, sarifPath);
|
|
14371
|
+
try {
|
|
14372
|
+
await writeText(target, toSarifText({ diagnostics: gate.diagnostics, root, version: cliVersion(), failed: gate.failed }));
|
|
14373
|
+
sarifWritten = path30.relative(ctx.cwd, target);
|
|
14374
|
+
} catch (err) {
|
|
14375
|
+
diagnostics.push({
|
|
14376
|
+
code: "ATLAS-CI-SARIF-001",
|
|
14377
|
+
severity: "warning",
|
|
14378
|
+
message: `No se pudo escribir el informe SARIF en "${sarifPath}": ${err.message}`,
|
|
14379
|
+
suggestion: "Revisa que la ruta sea escribible; el veredicto de la comprobaci\xF3n no cambia"
|
|
14380
|
+
});
|
|
14381
|
+
}
|
|
14382
|
+
}
|
|
13858
14383
|
for (const check of gate.checks) {
|
|
13859
14384
|
lines.push(` ${check.errors === 0 && check.warnings === 0 ? "OK " : "FALLA"} ${check.name}: ${check.errors} errores, ${check.warnings} avisos`);
|
|
13860
14385
|
}
|
|
13861
14386
|
lines.push("");
|
|
13862
14387
|
lines.push(gate.failed ? "Resultado: BLOQUEADO" : "Resultado: OK");
|
|
13863
14388
|
if (strict) lines.push("(modo estricto: los avisos tambi\xE9n bloquean)");
|
|
14389
|
+
if (sarifPath) {
|
|
14390
|
+
lines.push(sarifWritten ? `Informe SARIF: ${sarifWritten} (${gate.diagnostics.length} hallazgo(s))` : `Informe SARIF: no se pudo escribir en "${sarifPath}"`);
|
|
14391
|
+
}
|
|
13864
14392
|
return {
|
|
13865
14393
|
exitCode: gate.failed ? 1 : 0,
|
|
13866
|
-
diagnostics
|
|
13867
|
-
data: {
|
|
14394
|
+
diagnostics,
|
|
14395
|
+
data: {
|
|
14396
|
+
checks: gate.checks,
|
|
14397
|
+
errors: gate.errors,
|
|
14398
|
+
warnings: gate.warnings,
|
|
14399
|
+
strict,
|
|
14400
|
+
failed: gate.failed,
|
|
14401
|
+
...sarifWritten ? { sarif: sarifWritten } : {}
|
|
14402
|
+
},
|
|
13868
14403
|
text: lines
|
|
13869
14404
|
};
|
|
13870
14405
|
}
|
|
13871
14406
|
|
|
13872
14407
|
// src/commands/profile.ts
|
|
13873
|
-
import
|
|
14408
|
+
import path31 from "path";
|
|
13874
14409
|
async function runProfile(ctx) {
|
|
13875
14410
|
const action = ctx.positionals[0] ?? "detect";
|
|
13876
14411
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
13877
14412
|
const officialDir = await resolveProfilesDir();
|
|
13878
|
-
const customDir =
|
|
14413
|
+
const customDir = path31.join(workspace.sddDir, "profiles", "custom");
|
|
13879
14414
|
const profiles = [...officialDir ? await loadProfilesFromDir(officialDir) : [], ...await loadProfilesFromDir(customDir)];
|
|
13880
14415
|
if (action === "detect") {
|
|
13881
14416
|
const result = await detectProfiles(root, profiles);
|
|
@@ -13904,7 +14439,7 @@ async function runProfile(ctx) {
|
|
|
13904
14439
|
if (!name) {
|
|
13905
14440
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-001", severity: "error", message: "Falta el nombre del perfil: satlas profile create <nombre>" }] };
|
|
13906
14441
|
}
|
|
13907
|
-
const file =
|
|
14442
|
+
const file = path31.join(customDir, `${name}.yaml`);
|
|
13908
14443
|
if (await exists(file)) {
|
|
13909
14444
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-002", severity: "error", message: `El perfil ya existe: ${file}` }] };
|
|
13910
14445
|
}
|
|
@@ -13913,7 +14448,7 @@ async function runProfile(ctx) {
|
|
|
13913
14448
|
exitCode: 0,
|
|
13914
14449
|
diagnostics: [],
|
|
13915
14450
|
data: { file },
|
|
13916
|
-
text: [`Perfil creado: ${
|
|
14451
|
+
text: [`Perfil creado: ${path31.relative(ctx.cwd, file)}`, "", "Ed\xEDtalo con los comandos reales de tu stack (build/lint/test) y tus anti-patrones."]
|
|
13917
14452
|
};
|
|
13918
14453
|
}
|
|
13919
14454
|
return {
|
|
@@ -13922,7 +14457,7 @@ async function runProfile(ctx) {
|
|
|
13922
14457
|
};
|
|
13923
14458
|
}
|
|
13924
14459
|
async function isInside(dir, name) {
|
|
13925
|
-
return exists(
|
|
14460
|
+
return exists(path31.join(dir, `${name}.yaml`));
|
|
13926
14461
|
}
|
|
13927
14462
|
function profileTemplate(name) {
|
|
13928
14463
|
return `name: ${name}
|
|
@@ -13950,7 +14485,7 @@ assumptions: []
|
|
|
13950
14485
|
}
|
|
13951
14486
|
|
|
13952
14487
|
// src/commands/doctor.ts
|
|
13953
|
-
import
|
|
14488
|
+
import path32 from "path";
|
|
13954
14489
|
async function runDoctorCommand(ctx) {
|
|
13955
14490
|
const { root, config } = await requireWorkspace(ctx);
|
|
13956
14491
|
const report = await runDoctor(root);
|
|
@@ -13967,12 +14502,15 @@ async function runDoctorCommand(ctx) {
|
|
|
13967
14502
|
report.summary.warnings += 1;
|
|
13968
14503
|
}
|
|
13969
14504
|
}
|
|
14505
|
+
const advisory = await upgradeAdvisory(root);
|
|
14506
|
+
report.findings.push(...advisory.diagnostics);
|
|
14507
|
+
report.summary = countBySeverity(report.findings);
|
|
13970
14508
|
const lines = [msg("doctor.title", ctx.language), ""];
|
|
13971
14509
|
if (report.findings.length === 0) {
|
|
13972
14510
|
lines.push("Sin hallazgos. El workspace est\xE1 sano.");
|
|
13973
14511
|
} else {
|
|
13974
14512
|
for (const finding of report.findings) {
|
|
13975
|
-
const location = finding.path ? ` ${
|
|
14513
|
+
const location = finding.path ? ` ${path32.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""}` : "";
|
|
13976
14514
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${location} \u2014 ${finding.message}`);
|
|
13977
14515
|
if (finding.suggestion) lines.push(` \u21B3 ${finding.suggestion}`);
|
|
13978
14516
|
}
|
|
@@ -14046,7 +14584,7 @@ async function runHelp(ctx, commandName) {
|
|
|
14046
14584
|
}
|
|
14047
14585
|
|
|
14048
14586
|
// src/commands/init.ts
|
|
14049
|
-
import
|
|
14587
|
+
import path33 from "path";
|
|
14050
14588
|
async function runInit(ctx) {
|
|
14051
14589
|
const languageFlag = flagString(ctx.flags, "language");
|
|
14052
14590
|
const language = languageFlag === "en" ? "en" : languageFlag === "es" ? "es" : void 0;
|
|
@@ -14093,11 +14631,11 @@ async function runInit(ctx) {
|
|
|
14093
14631
|
const lines = [msg("init.title", ctx.language), ""];
|
|
14094
14632
|
if (result.created.length > 0) {
|
|
14095
14633
|
lines.push(msg("init.done", ctx.language));
|
|
14096
|
-
for (const file of result.created) lines.push(` + ${
|
|
14634
|
+
for (const file of result.created) lines.push(` + ${path33.relative(ctx.cwd, file)}`);
|
|
14097
14635
|
if (compiled.length > 0) {
|
|
14098
14636
|
lines.push("");
|
|
14099
14637
|
lines.push("Adaptadores compilados:");
|
|
14100
|
-
for (const file of compiled) lines.push(` + ${
|
|
14638
|
+
for (const file of compiled) lines.push(` + ${path33.relative(ctx.cwd, path33.join(ctx.cwd, file))}`);
|
|
14101
14639
|
}
|
|
14102
14640
|
lines.push("");
|
|
14103
14641
|
lines.push("Siguiente: crea un cambio con `satlas new <slug>` y escribe la spec funcional.");
|
|
@@ -14107,8 +14645,8 @@ async function runInit(ctx) {
|
|
|
14107
14645
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14108
14646
|
diagnostics: result.diagnostics,
|
|
14109
14647
|
data: {
|
|
14110
|
-
sddDir:
|
|
14111
|
-
created: result.created.map((f) =>
|
|
14648
|
+
sddDir: path33.relative(ctx.cwd, result.sddDir),
|
|
14649
|
+
created: result.created.map((f) => path33.relative(ctx.cwd, f)),
|
|
14112
14650
|
detected: result.detected?.matches.map((m) => ({ name: m.name, score: m.score })),
|
|
14113
14651
|
best: result.detected?.best?.name
|
|
14114
14652
|
},
|
|
@@ -14149,7 +14687,7 @@ async function runMetrics(ctx) {
|
|
|
14149
14687
|
}
|
|
14150
14688
|
|
|
14151
14689
|
// src/commands/mockup.ts
|
|
14152
|
-
import
|
|
14690
|
+
import path34 from "path";
|
|
14153
14691
|
async function runMockup(ctx) {
|
|
14154
14692
|
const { root, workspace, config } = await requireWorkspace(ctx);
|
|
14155
14693
|
const slug = ctx.positionals[0];
|
|
@@ -14198,16 +14736,512 @@ async function runMockup(ctx) {
|
|
|
14198
14736
|
` pantallas: ${plan.screens.length}`,
|
|
14199
14737
|
...plan.screens.map((s) => ` - ${s.id}: ${s.title} (${s.illustrates.length} escenario(s))`),
|
|
14200
14738
|
"",
|
|
14201
|
-
` plan: ${
|
|
14202
|
-
` manifiesto: ${
|
|
14739
|
+
` plan: ${path34.relative(ctx.cwd, planFile)}`,
|
|
14740
|
+
` manifiesto: ${path34.relative(ctx.cwd, manifestFile)}`,
|
|
14203
14741
|
"",
|
|
14204
14742
|
"Siguiente: genera el HTML de cada pantalla (fase /satlas-mockup) y valida con `satlas mockup " + slug + " --check`."
|
|
14205
14743
|
];
|
|
14206
|
-
return { exitCode: 0, diagnostics: [], data: { plan, planFile:
|
|
14744
|
+
return { exitCode: 0, diagnostics: [], data: { plan, planFile: path34.relative(ctx.cwd, planFile), manifestFile: path34.relative(ctx.cwd, manifestFile) }, text: lines };
|
|
14745
|
+
}
|
|
14746
|
+
|
|
14747
|
+
// src/mcp/server.ts
|
|
14748
|
+
import { promises as fs3 } from "fs";
|
|
14749
|
+
import path37 from "path";
|
|
14750
|
+
import { createInterface } from "readline/promises";
|
|
14751
|
+
|
|
14752
|
+
// src/mcp/protocol.ts
|
|
14753
|
+
var JSONRPC_VERSION = "2.0";
|
|
14754
|
+
var PARSE_ERROR = -32700;
|
|
14755
|
+
var INVALID_REQUEST = -32600;
|
|
14756
|
+
var METHOD_NOT_FOUND = -32601;
|
|
14757
|
+
var INTERNAL_ERROR = -32603;
|
|
14758
|
+
function isObject(value) {
|
|
14759
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14760
|
+
}
|
|
14761
|
+
function makeResult(id, result) {
|
|
14762
|
+
return { jsonrpc: JSONRPC_VERSION, id, result };
|
|
14763
|
+
}
|
|
14764
|
+
function makeError(id, code, message, data) {
|
|
14765
|
+
const error = { code, message };
|
|
14766
|
+
if (data !== void 0) error.data = data;
|
|
14767
|
+
return { jsonrpc: JSONRPC_VERSION, id, error };
|
|
14768
|
+
}
|
|
14769
|
+
function parseMessage(line) {
|
|
14770
|
+
let value;
|
|
14771
|
+
try {
|
|
14772
|
+
value = JSON.parse(line);
|
|
14773
|
+
} catch {
|
|
14774
|
+
return { error: { code: PARSE_ERROR, message: "El mensaje no es JSON v\xE1lido" } };
|
|
14775
|
+
}
|
|
14776
|
+
if (!isObject(value) || value["jsonrpc"] !== JSONRPC_VERSION) {
|
|
14777
|
+
return { error: { code: INVALID_REQUEST, message: "Mensaje no es una petici\xF3n JSON-RPC 2.0 v\xE1lida" } };
|
|
14778
|
+
}
|
|
14779
|
+
if (typeof value["method"] !== "string") {
|
|
14780
|
+
return { error: { code: INVALID_REQUEST, message: "El mensaje no declara un m\xE9todo" } };
|
|
14781
|
+
}
|
|
14782
|
+
const idValue = value["id"] ?? null;
|
|
14783
|
+
const id = typeof idValue === "number" || typeof idValue === "string" || idValue === null ? idValue : null;
|
|
14784
|
+
const request = { jsonrpc: JSONRPC_VERSION, id, method: value["method"] };
|
|
14785
|
+
if (value["params"] !== void 0) request.params = value["params"];
|
|
14786
|
+
return { request };
|
|
14787
|
+
}
|
|
14788
|
+
function jsonResult(data) {
|
|
14789
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
14790
|
+
}
|
|
14791
|
+
function errorResult(code, message, extra) {
|
|
14792
|
+
return {
|
|
14793
|
+
isError: true,
|
|
14794
|
+
content: [{ type: "text", text: JSON.stringify({ error: code, message, ...extra }, null, 2) }]
|
|
14795
|
+
};
|
|
14796
|
+
}
|
|
14797
|
+
function noWorkspaceResult() {
|
|
14798
|
+
return errorResult(
|
|
14799
|
+
"ATLAS-MCP-WS-001",
|
|
14800
|
+
"El proyecto no est\xE1 inicializado: no se encontr\xF3 el estado del proyecto en esta carpeta.",
|
|
14801
|
+
{ action: "satlas init" }
|
|
14802
|
+
);
|
|
14803
|
+
}
|
|
14804
|
+
function stringArg(args, key) {
|
|
14805
|
+
if (!isObject(args)) return void 0;
|
|
14806
|
+
const value = args[key];
|
|
14807
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
14808
|
+
}
|
|
14809
|
+
|
|
14810
|
+
// src/mcp/tools/glossary.ts
|
|
14811
|
+
import path35 from "path";
|
|
14812
|
+
async function runAtlasGlossary(_args, host) {
|
|
14813
|
+
const resolved = await host.getWorkspace();
|
|
14814
|
+
if (!resolved) return noWorkspaceResult();
|
|
14815
|
+
const { root, config } = resolved;
|
|
14816
|
+
const glossaryPath = path35.resolve(root, config.spec.glossary);
|
|
14817
|
+
const raw = await readTextIfExists(glossaryPath);
|
|
14818
|
+
if (raw === void 0) {
|
|
14819
|
+
return jsonResult({
|
|
14820
|
+
terms: [],
|
|
14821
|
+
message: "El glosario no tiene t\xE9rminos definidos.",
|
|
14822
|
+
action: "Define los t\xE9rminos del negocio en el glosario del proyecto."
|
|
14823
|
+
});
|
|
14824
|
+
}
|
|
14825
|
+
const parsed = parseGlossary(raw, glossaryPath);
|
|
14826
|
+
if (parsed.terms.length === 0) {
|
|
14827
|
+
return jsonResult({
|
|
14828
|
+
terms: [],
|
|
14829
|
+
message: "El glosario no tiene t\xE9rminos definidos.",
|
|
14830
|
+
action: "Define los t\xE9rminos del negocio en el glosario del proyecto."
|
|
14831
|
+
});
|
|
14832
|
+
}
|
|
14833
|
+
return jsonResult({ terms: parsed.terms });
|
|
14834
|
+
}
|
|
14835
|
+
|
|
14836
|
+
// src/mcp/tools/impact.ts
|
|
14837
|
+
var REQ_TARGET_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}(?:-S\d+)?$/i;
|
|
14838
|
+
async function runAtlasImpact(args, host) {
|
|
14839
|
+
const target = stringArg(args, "target");
|
|
14840
|
+
if (!target) {
|
|
14841
|
+
return errorResult("ATLAS-MCP-IMPACT-001", "Falta el requisito o el archivo a consultar", {
|
|
14842
|
+
action: "atlas_impact con el argumento target (REQ-\u2026 o ruta de archivo)"
|
|
14843
|
+
});
|
|
14844
|
+
}
|
|
14845
|
+
const resolved = await host.getWorkspace();
|
|
14846
|
+
if (!resolved) return noWorkspaceResult();
|
|
14847
|
+
const { workspace } = resolved;
|
|
14848
|
+
let report;
|
|
14849
|
+
if (REQ_TARGET_RE.test(target)) {
|
|
14850
|
+
const reqId = target.toUpperCase().replace(/-S\d+$/, "");
|
|
14851
|
+
report = impactOfRequirement(workspace, reqId);
|
|
14852
|
+
} else {
|
|
14853
|
+
report = impactOfFile(workspace, target);
|
|
14854
|
+
}
|
|
14855
|
+
if (!report.exists) {
|
|
14856
|
+
return jsonResult({ target, exists: false, message: `Sin relaciones registradas para "${target}".` });
|
|
14857
|
+
}
|
|
14858
|
+
return jsonResult(report);
|
|
14859
|
+
}
|
|
14860
|
+
|
|
14861
|
+
// src/evaluate.ts
|
|
14862
|
+
import path36 from "path";
|
|
14863
|
+
function livingRequirementsMap2(specs) {
|
|
14864
|
+
const map = /* @__PURE__ */ new Map();
|
|
14865
|
+
for (const spec of specs) {
|
|
14866
|
+
for (const req of spec.spec.requirements) map.set(req.id, req);
|
|
14867
|
+
}
|
|
14868
|
+
return map;
|
|
14869
|
+
}
|
|
14870
|
+
async function evaluateChange(workspace, config, change, approvals) {
|
|
14871
|
+
const deltaPath = path36.join(change.dir, "spec.md");
|
|
14872
|
+
const deltaContent = await readTextIfExists(deltaPath);
|
|
14873
|
+
const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
|
|
14874
|
+
const living = livingRequirementsMap2(workspace.specs);
|
|
14875
|
+
const lintFindings = change.delta ? lintDelta(change.delta, living, deltaPath, { language: config.spec.language }) : [];
|
|
14876
|
+
const trace = checkTrace({
|
|
14877
|
+
specs: workspace.specs,
|
|
14878
|
+
change,
|
|
14879
|
+
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
14880
|
+
});
|
|
14881
|
+
const blocking = lintFindings.filter((d) => d.severity === "error").length + trace.findings.filter((d) => d.severity === "error").length;
|
|
14882
|
+
const mockupsAreReady = requiresMockups(change.meta, config) ? await mockupsReady(workspace.root, change.slug, change) : void 0;
|
|
14883
|
+
const state = deriveState({
|
|
14884
|
+
change,
|
|
14885
|
+
cfg: config,
|
|
14886
|
+
approval,
|
|
14887
|
+
blockingFindings: change.delta ? blocking : 0,
|
|
14888
|
+
...mockupsAreReady !== void 0 ? { mockupsReady: mockupsAreReady } : {}
|
|
14889
|
+
});
|
|
14890
|
+
return { change, approval, lintFindings, trace, blocking, state };
|
|
14891
|
+
}
|
|
14892
|
+
|
|
14893
|
+
// src/mcp/tools/next.ts
|
|
14894
|
+
var HUMAN_COMMANDS = [/^satlas approve/, /^satlas archive/, /^satlas resume/, /^satlas present/];
|
|
14895
|
+
function requiresPerson(nextCommand, state) {
|
|
14896
|
+
if (state === "awaiting_approval" || state === "ready") return true;
|
|
14897
|
+
return HUMAN_COMMANDS.some((re) => re.test(nextCommand));
|
|
14898
|
+
}
|
|
14899
|
+
async function runAtlasNext(args, host) {
|
|
14900
|
+
const slug = stringArg(args, "slug");
|
|
14901
|
+
if (!slug) {
|
|
14902
|
+
return errorResult("ATLAS-MCP-NEXT-001", "Falta el nombre del cambio", { action: "atlas_next con el argumento slug" });
|
|
14903
|
+
}
|
|
14904
|
+
const resolved = await host.getWorkspace();
|
|
14905
|
+
if (!resolved) return noWorkspaceResult();
|
|
14906
|
+
const { workspace, config, approvals } = resolved;
|
|
14907
|
+
const change = workspace.changes.find((c) => c.slug === slug);
|
|
14908
|
+
if (!change) {
|
|
14909
|
+
return errorResult("ATLAS-MCP-NEXT-002", `El cambio "${slug}" no existe`, {
|
|
14910
|
+
available: workspace.changes.map((c) => c.slug)
|
|
14911
|
+
});
|
|
14912
|
+
}
|
|
14913
|
+
const evaluation = await evaluateChange(workspace, config, change, approvals);
|
|
14914
|
+
const { state, nextAction, blockedBy } = evaluation.state;
|
|
14915
|
+
const humanRequired = requiresPerson(nextAction.command, state);
|
|
14916
|
+
return jsonResult({
|
|
14917
|
+
slug,
|
|
14918
|
+
state,
|
|
14919
|
+
label: stateLabel(state),
|
|
14920
|
+
next: { command: nextAction.command, description: nextAction.description, requiresAgent: nextAction.requiresAgent },
|
|
14921
|
+
requiresPerson: humanRequired,
|
|
14922
|
+
blockedBy
|
|
14923
|
+
});
|
|
14924
|
+
}
|
|
14925
|
+
|
|
14926
|
+
// src/mcp/tools/status.ts
|
|
14927
|
+
async function runAtlasStatus(args, host) {
|
|
14928
|
+
const resolved = await host.getWorkspace();
|
|
14929
|
+
if (!resolved) return noWorkspaceResult();
|
|
14930
|
+
const { workspace, config, approvals } = resolved;
|
|
14931
|
+
const slug = stringArg(args, "slug");
|
|
14932
|
+
if (slug && !workspace.changes.some((c) => c.slug === slug)) {
|
|
14933
|
+
return errorResult("ATLAS-MCP-STATUS-001", `El cambio "${slug}" no existe`, {
|
|
14934
|
+
available: workspace.changes.map((c) => c.slug)
|
|
14935
|
+
});
|
|
14936
|
+
}
|
|
14937
|
+
const changes = [];
|
|
14938
|
+
for (const change of workspace.changes) {
|
|
14939
|
+
if (slug && change.slug !== slug) continue;
|
|
14940
|
+
const evaluation = await evaluateChange(workspace, config, change, approvals);
|
|
14941
|
+
const { state, progress, nextAction, blockedBy } = evaluation.state;
|
|
14942
|
+
changes.push({
|
|
14943
|
+
slug: change.slug,
|
|
14944
|
+
lane: change.meta?.lane ?? config.lanes.default,
|
|
14945
|
+
domain: change.meta?.domain,
|
|
14946
|
+
state,
|
|
14947
|
+
label: stateLabel(state),
|
|
14948
|
+
progress,
|
|
14949
|
+
blocking: evaluation.blocking,
|
|
14950
|
+
blockedBy,
|
|
14951
|
+
next: { command: nextAction.command, description: nextAction.description, requiresAgent: nextAction.requiresAgent }
|
|
14952
|
+
});
|
|
14953
|
+
}
|
|
14954
|
+
if (changes.length === 0) {
|
|
14955
|
+
return jsonResult({
|
|
14956
|
+
changes: [],
|
|
14957
|
+
specs: workspace.specs.map((s) => ({ domain: s.domain, requirements: s.spec.requirements.length })),
|
|
14958
|
+
message: "Sin cambios activos.",
|
|
14959
|
+
action: "satlas new <slug>"
|
|
14960
|
+
});
|
|
14961
|
+
}
|
|
14962
|
+
return jsonResult({
|
|
14963
|
+
changes,
|
|
14964
|
+
specs: workspace.specs.map((s) => ({ domain: s.domain, requirements: s.spec.requirements.length }))
|
|
14965
|
+
});
|
|
14966
|
+
}
|
|
14967
|
+
|
|
14968
|
+
// src/mcp/tools/trace.ts
|
|
14969
|
+
async function runAtlasTrace(args, host) {
|
|
14970
|
+
const resolved = await host.getWorkspace();
|
|
14971
|
+
if (!resolved) return noWorkspaceResult();
|
|
14972
|
+
const { workspace } = resolved;
|
|
14973
|
+
const slug = stringArg(args, "slug");
|
|
14974
|
+
const changes = slug ? workspace.changes.filter((c) => c.slug === slug) : workspace.changes;
|
|
14975
|
+
const result = [];
|
|
14976
|
+
for (const change of changes) {
|
|
14977
|
+
const trace = checkTrace({ specs: workspace.specs, change, requireEvidence: false });
|
|
14978
|
+
const coversByTask = /* @__PURE__ */ new Map();
|
|
14979
|
+
for (const block of change.tasks?.blocks ?? []) {
|
|
14980
|
+
for (const task of block.tasks) coversByTask.set(task.id, task.covers.map((c) => c.toUpperCase()));
|
|
14981
|
+
}
|
|
14982
|
+
const evidenceByScenario = /* @__PURE__ */ new Map();
|
|
14983
|
+
for (const ev of change.verify?.evidence ?? []) {
|
|
14984
|
+
const known = evidenceByScenario.get(ev.scenario);
|
|
14985
|
+
if (known === void 0 || ev.result === "pass") evidenceByScenario.set(ev.scenario, ev.result);
|
|
14986
|
+
}
|
|
14987
|
+
const scenarios = [];
|
|
14988
|
+
for (const req of [...change.delta?.added ?? [], ...change.delta?.modified ?? []]) {
|
|
14989
|
+
for (const sc of req.scenarios) {
|
|
14990
|
+
const parent = sc.reqId.toUpperCase();
|
|
14991
|
+
const coveredBy = [...coversByTask.entries()].filter(([, covers]) => covers.includes(sc.id.toUpperCase()) || covers.includes(parent)).map(([taskId]) => taskId);
|
|
14992
|
+
const evidence = evidenceByScenario.get(sc.id) ?? null;
|
|
14993
|
+
scenarios.push({ id: sc.id, title: sc.title, coveredBy, evidence });
|
|
14994
|
+
}
|
|
14995
|
+
}
|
|
14996
|
+
const gaps = trace.findings.filter((f) => f.code === "TRACE-002").map((f) => String(f.target ?? f.message));
|
|
14997
|
+
result.push({ slug: change.slug, scenarios, gaps, summary: { errors: trace.summary.errors, warnings: trace.summary.warnings } });
|
|
14998
|
+
}
|
|
14999
|
+
if (changes.length === 0) {
|
|
15000
|
+
return jsonResult({ changes: [], message: "Sin cambios activos.", action: "satlas new <slug>" });
|
|
15001
|
+
}
|
|
15002
|
+
return jsonResult({ changes: result });
|
|
15003
|
+
}
|
|
15004
|
+
|
|
15005
|
+
// src/mcp/tools/validate.ts
|
|
15006
|
+
async function runAtlasValidate(args, host) {
|
|
15007
|
+
const slug = stringArg(args, "slug");
|
|
15008
|
+
if (!slug) {
|
|
15009
|
+
return errorResult("ATLAS-MCP-VALIDATE-001", "Falta el nombre del cambio", { action: "atlas_validate con el argumento slug" });
|
|
15010
|
+
}
|
|
15011
|
+
const resolved = await host.getWorkspace();
|
|
15012
|
+
if (!resolved) return noWorkspaceResult();
|
|
15013
|
+
const { workspace, config, approvals } = resolved;
|
|
15014
|
+
const change = workspace.changes.find((c) => c.slug === slug);
|
|
15015
|
+
if (!change) {
|
|
15016
|
+
return errorResult("ATLAS-MCP-VALIDATE-002", `El cambio "${slug}" no existe`, {
|
|
15017
|
+
available: workspace.changes.map((c) => c.slug)
|
|
15018
|
+
});
|
|
15019
|
+
}
|
|
15020
|
+
const evaluation = await evaluateChange(workspace, config, change, approvals);
|
|
15021
|
+
const findings = [...evaluation.lintFindings, ...evaluation.trace.findings].map((d) => ({
|
|
15022
|
+
code: d.code,
|
|
15023
|
+
severity: d.severity,
|
|
15024
|
+
message: d.message,
|
|
15025
|
+
path: d.path,
|
|
15026
|
+
line: d.line,
|
|
15027
|
+
suggestion: d.suggestion
|
|
15028
|
+
}));
|
|
15029
|
+
if (findings.length === 0) {
|
|
15030
|
+
return jsonResult({ slug, conforming: true, message: "Sin hallazgos: el cambio est\xE1 conforme.", findings: [] });
|
|
15031
|
+
}
|
|
15032
|
+
return jsonResult({
|
|
15033
|
+
slug,
|
|
15034
|
+
conforming: findings.every((f) => f.severity !== "error"),
|
|
15035
|
+
findings,
|
|
15036
|
+
summary: evaluation.trace.summary
|
|
15037
|
+
});
|
|
15038
|
+
}
|
|
15039
|
+
|
|
15040
|
+
// src/mcp/tools/index.ts
|
|
15041
|
+
var HANDLERS = {
|
|
15042
|
+
atlas_status: runAtlasStatus,
|
|
15043
|
+
atlas_next: runAtlasNext,
|
|
15044
|
+
atlas_validate: runAtlasValidate,
|
|
15045
|
+
atlas_trace: runAtlasTrace,
|
|
15046
|
+
atlas_impact: runAtlasImpact,
|
|
15047
|
+
atlas_glossary: runAtlasGlossary
|
|
15048
|
+
};
|
|
15049
|
+
async function runToolByName(name, args, host) {
|
|
15050
|
+
const handler = HANDLERS[name];
|
|
15051
|
+
if (!handler) {
|
|
15052
|
+
return errorResult("ATLAS-MCP-TOOL-002", `Operaci\xF3n desconocida: ${name}`, { available: Object.keys(HANDLERS).sort() });
|
|
15053
|
+
}
|
|
15054
|
+
return handler(args, host);
|
|
15055
|
+
}
|
|
15056
|
+
|
|
15057
|
+
// src/mcp/tools/list.ts
|
|
15058
|
+
function listTools() {
|
|
15059
|
+
return [
|
|
15060
|
+
{
|
|
15061
|
+
name: "atlas_status",
|
|
15062
|
+
description: "Estado del proyecto: cambios activos con fase, avance de tareas y evidencia, bloqueos y siguiente paso. Opcional: nombre de un cambio.",
|
|
15063
|
+
inputSchema: {
|
|
15064
|
+
type: "object",
|
|
15065
|
+
properties: { slug: { type: "string", description: "Nombre del cambio (opcional): si se indica, solo se informa de ese cambio" } },
|
|
15066
|
+
additionalProperties: false
|
|
15067
|
+
}
|
|
15068
|
+
},
|
|
15069
|
+
{
|
|
15070
|
+
name: "atlas_next",
|
|
15071
|
+
description: "Siguiente acci\xF3n recomendada para un cambio, con la indicaci\xF3n de si requiere una persona o la puede realizar el asistente.",
|
|
15072
|
+
inputSchema: {
|
|
15073
|
+
type: "object",
|
|
15074
|
+
properties: { slug: { type: "string", description: "Nombre del cambio" } },
|
|
15075
|
+
required: ["slug"],
|
|
15076
|
+
additionalProperties: false
|
|
15077
|
+
}
|
|
15078
|
+
},
|
|
15079
|
+
{
|
|
15080
|
+
name: "atlas_validate",
|
|
15081
|
+
description: "Hallazgos vigentes de un cambio (validaci\xF3n y trazabilidad), id\xE9nticos a los que reporta la herramienta. Nunca modifica nada.",
|
|
15082
|
+
inputSchema: {
|
|
15083
|
+
type: "object",
|
|
15084
|
+
properties: { slug: { type: "string", description: "Nombre del cambio" } },
|
|
15085
|
+
required: ["slug"],
|
|
15086
|
+
additionalProperties: false
|
|
15087
|
+
}
|
|
15088
|
+
},
|
|
15089
|
+
{
|
|
15090
|
+
name: "atlas_trace",
|
|
15091
|
+
description: "Cobertura de trazabilidad: por escenario, qu\xE9 tarea lo cubre y su evidencia, y los huecos vigentes.",
|
|
15092
|
+
inputSchema: {
|
|
15093
|
+
type: "object",
|
|
15094
|
+
properties: { slug: { type: "string", description: "Nombre del cambio (opcional)" } },
|
|
15095
|
+
additionalProperties: false
|
|
15096
|
+
}
|
|
15097
|
+
},
|
|
15098
|
+
{
|
|
15099
|
+
name: "atlas_impact",
|
|
15100
|
+
description: "Impacto registrado de un requisito (REQ-\u2026) o de un archivo: escenarios, tareas, cambios, archivos y evidencia relacionados. Solo relaciones registradas.",
|
|
15101
|
+
inputSchema: {
|
|
15102
|
+
type: "object",
|
|
15103
|
+
properties: { target: { type: "string", description: "Id de requisito (por ejemplo REQ-AUTH-001) o ruta de archivo" } },
|
|
15104
|
+
required: ["target"],
|
|
15105
|
+
additionalProperties: false
|
|
15106
|
+
}
|
|
15107
|
+
},
|
|
15108
|
+
{
|
|
15109
|
+
name: "atlas_glossary",
|
|
15110
|
+
description: "T\xE9rminos del glosario del negocio con su definici\xF3n vigente y sin\xF3nimos aceptados. No inventa definiciones.",
|
|
15111
|
+
inputSchema: {
|
|
15112
|
+
type: "object",
|
|
15113
|
+
properties: {},
|
|
15114
|
+
additionalProperties: false
|
|
15115
|
+
}
|
|
15116
|
+
}
|
|
15117
|
+
];
|
|
15118
|
+
}
|
|
15119
|
+
|
|
15120
|
+
// src/mcp/server.ts
|
|
15121
|
+
var PROTOCOL_VERSION = "2024-11-05";
|
|
15122
|
+
var SERVER_NAME = "specatlas";
|
|
15123
|
+
var WorkspaceCache = class {
|
|
15124
|
+
entries = /* @__PURE__ */ new Map();
|
|
15125
|
+
get(root) {
|
|
15126
|
+
return this.entries.get(root);
|
|
15127
|
+
}
|
|
15128
|
+
set(root, mtimeMs, value) {
|
|
15129
|
+
this.entries.set(root, { mtimeMs, value });
|
|
15130
|
+
}
|
|
15131
|
+
};
|
|
15132
|
+
async function workspaceStamp(root) {
|
|
15133
|
+
const sddDir = path37.join(root, ".sdd");
|
|
15134
|
+
const markers = [sddDir, path37.join(sddDir, "config.yaml"), path37.join(sddDir, "changes"), path37.join(sddDir, "specs"), path37.join(sddDir, "approvals.yaml")];
|
|
15135
|
+
let stamp = 0;
|
|
15136
|
+
for (const marker of markers) {
|
|
15137
|
+
try {
|
|
15138
|
+
const st = await fs3.stat(marker);
|
|
15139
|
+
stamp = Math.max(stamp, st.mtimeMs);
|
|
15140
|
+
} catch {
|
|
15141
|
+
}
|
|
15142
|
+
}
|
|
15143
|
+
return stamp;
|
|
15144
|
+
}
|
|
15145
|
+
async function resolveMcpWorkspace(cwd, cache) {
|
|
15146
|
+
const root = await findWorkspaceRoot(cwd);
|
|
15147
|
+
if (!root) return void 0;
|
|
15148
|
+
const stamp = await workspaceStamp(root);
|
|
15149
|
+
const cached2 = cache.get(root);
|
|
15150
|
+
if (cached2 && cached2.mtimeMs === stamp) return cached2.value;
|
|
15151
|
+
const { workspace, config } = await loadWorkspace(root);
|
|
15152
|
+
const approvals = await loadApprovals(path37.join(root, ".sdd"));
|
|
15153
|
+
const value = { root, workspace, config, approvals: approvals.byArtifact };
|
|
15154
|
+
cache.set(root, stamp, value);
|
|
15155
|
+
return value;
|
|
15156
|
+
}
|
|
15157
|
+
function createHost(cwd, cache) {
|
|
15158
|
+
return {
|
|
15159
|
+
cwd,
|
|
15160
|
+
language: "es",
|
|
15161
|
+
getWorkspace: () => resolveMcpWorkspace(cwd, cache)
|
|
15162
|
+
};
|
|
15163
|
+
}
|
|
15164
|
+
async function handleRequest(req, host) {
|
|
15165
|
+
const id = req.id;
|
|
15166
|
+
if (req.method === "initialize") {
|
|
15167
|
+
return makeResult(id, {
|
|
15168
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
15169
|
+
capabilities: { tools: { listChanged: false } },
|
|
15170
|
+
serverInfo: { name: SERVER_NAME, version: cliVersion() }
|
|
15171
|
+
});
|
|
15172
|
+
}
|
|
15173
|
+
if (req.method === "ping") {
|
|
15174
|
+
return makeResult(id, {});
|
|
15175
|
+
}
|
|
15176
|
+
if (req.method === "tools/list") {
|
|
15177
|
+
return makeResult(id, { tools: listTools() });
|
|
15178
|
+
}
|
|
15179
|
+
if (req.method === "tools/call") {
|
|
15180
|
+
const params = isObject(req.params) ? req.params : {};
|
|
15181
|
+
const name = typeof params["name"] === "string" ? params["name"] : "";
|
|
15182
|
+
if (name === "") {
|
|
15183
|
+
return makeResult(id, {
|
|
15184
|
+
content: [{ type: "text", text: JSON.stringify({ error: "ATLAS-MCP-TOOL-001", message: "Falta el nombre de la operaci\xF3n" }, null, 2) }],
|
|
15185
|
+
isError: true
|
|
15186
|
+
});
|
|
15187
|
+
}
|
|
15188
|
+
try {
|
|
15189
|
+
const result = await runToolByName(name, params["arguments"], host);
|
|
15190
|
+
return makeResult(id, result);
|
|
15191
|
+
} catch (err) {
|
|
15192
|
+
return makeError(id, INTERNAL_ERROR, "Error interno al ejecutar la operaci\xF3n", { message: err instanceof Error ? err.message : String(err) });
|
|
15193
|
+
}
|
|
15194
|
+
}
|
|
15195
|
+
if (req.method.startsWith("notifications/")) {
|
|
15196
|
+
return void 0;
|
|
15197
|
+
}
|
|
15198
|
+
return makeError(id, METHOD_NOT_FOUND, `M\xE9todo desconocido: ${req.method}`);
|
|
15199
|
+
}
|
|
15200
|
+
function log(stderr, message) {
|
|
15201
|
+
stderr.write(`[specatlas mcp] ${message}
|
|
15202
|
+
`);
|
|
15203
|
+
}
|
|
15204
|
+
async function runMcpServer(opts) {
|
|
15205
|
+
const { cwd, stdin, stdout, stderr } = opts;
|
|
15206
|
+
const cache = new WorkspaceCache();
|
|
15207
|
+
const host = createHost(cwd, cache);
|
|
15208
|
+
log(stderr, "v\xEDa de consulta iniciada (solo lectura)");
|
|
15209
|
+
const rl = createInterface({ input: stdin, crlfDelay: Infinity });
|
|
15210
|
+
for await (const line of rl) {
|
|
15211
|
+
if (line.trim() === "") continue;
|
|
15212
|
+
const parsed = parseMessage(line);
|
|
15213
|
+
if (parsed.error) {
|
|
15214
|
+
const response = makeError(null, parsed.error.code, parsed.error.message);
|
|
15215
|
+
stdout.write(`${JSON.stringify(response)}
|
|
15216
|
+
`);
|
|
15217
|
+
continue;
|
|
15218
|
+
}
|
|
15219
|
+
const request = parsed.request;
|
|
15220
|
+
try {
|
|
15221
|
+
const response = await handleRequest(request, host);
|
|
15222
|
+
if (response) stdout.write(`${JSON.stringify(response)}
|
|
15223
|
+
`);
|
|
15224
|
+
} catch (err) {
|
|
15225
|
+
log(stderr, err instanceof Error ? err.message : String(err));
|
|
15226
|
+
stdout.write(`${JSON.stringify(makeError(request.id, INTERNAL_ERROR, "Error interno"))}
|
|
15227
|
+
`);
|
|
15228
|
+
}
|
|
15229
|
+
}
|
|
15230
|
+
}
|
|
15231
|
+
|
|
15232
|
+
// src/commands/mcp.ts
|
|
15233
|
+
async function runMcp(ctx) {
|
|
15234
|
+
await runMcpServer({
|
|
15235
|
+
cwd: ctx.cwd,
|
|
15236
|
+
stdin: process.stdin,
|
|
15237
|
+
stdout: process.stdout,
|
|
15238
|
+
stderr: process.stderr
|
|
15239
|
+
});
|
|
15240
|
+
return { exitCode: 0, diagnostics: [], data: { served: true } };
|
|
14207
15241
|
}
|
|
14208
15242
|
|
|
14209
15243
|
// src/commands/packs.ts
|
|
14210
|
-
import
|
|
15244
|
+
import path38 from "path";
|
|
14211
15245
|
async function runPacks(ctx) {
|
|
14212
15246
|
const { root, config, workspace } = await requireWorkspace(ctx);
|
|
14213
15247
|
const slug = flagString(ctx.flags, "check");
|
|
@@ -14265,14 +15299,14 @@ async function runPacks(ctx) {
|
|
|
14265
15299
|
data: {
|
|
14266
15300
|
slug,
|
|
14267
15301
|
packs: evaluations.map((evaluation) => ({ id: evaluation.pack.id, status: evaluation.status, passed: evaluation.passed, failed: evaluation.failed, results: evaluation.results })),
|
|
14268
|
-
analyzePath:
|
|
15302
|
+
analyzePath: path38.posix.join(".sdd", "changes", slug, "analyze.md")
|
|
14269
15303
|
},
|
|
14270
15304
|
text: lines
|
|
14271
15305
|
};
|
|
14272
15306
|
}
|
|
14273
15307
|
|
|
14274
15308
|
// src/commands/new.ts
|
|
14275
|
-
import
|
|
15309
|
+
import path39 from "path";
|
|
14276
15310
|
async function runNew(ctx) {
|
|
14277
15311
|
const slug = ctx.positionals[0];
|
|
14278
15312
|
if (!slug) {
|
|
@@ -14298,50 +15332,18 @@ async function runNew(ctx) {
|
|
|
14298
15332
|
msg("new.title", ctx.language),
|
|
14299
15333
|
"",
|
|
14300
15334
|
msg("new.done", ctx.language),
|
|
14301
|
-
...result.files.map((f) => ` + ${
|
|
15335
|
+
...result.files.map((f) => ` + ${path39.relative(ctx.cwd, f)}`),
|
|
14302
15336
|
"",
|
|
14303
15337
|
`Siguiente: /satlas.specify ${result.slug} \u2014 escribe la especificaci\xF3n 100% funcional y de negocio.`
|
|
14304
15338
|
];
|
|
14305
15339
|
return {
|
|
14306
15340
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14307
15341
|
diagnostics: result.diagnostics,
|
|
14308
|
-
data: { slug: result.slug, files: result.files.map((f) =>
|
|
15342
|
+
data: { slug: result.slug, files: result.files.map((f) => path39.relative(ctx.cwd, f)) },
|
|
14309
15343
|
text: lines
|
|
14310
15344
|
};
|
|
14311
15345
|
}
|
|
14312
15346
|
|
|
14313
|
-
// src/evaluate.ts
|
|
14314
|
-
import path34 from "path";
|
|
14315
|
-
function livingRequirementsMap2(specs) {
|
|
14316
|
-
const map = /* @__PURE__ */ new Map();
|
|
14317
|
-
for (const spec of specs) {
|
|
14318
|
-
for (const req of spec.spec.requirements) map.set(req.id, req);
|
|
14319
|
-
}
|
|
14320
|
-
return map;
|
|
14321
|
-
}
|
|
14322
|
-
async function evaluateChange(workspace, config, change, approvals) {
|
|
14323
|
-
const deltaPath = path34.join(change.dir, "spec.md");
|
|
14324
|
-
const deltaContent = await readTextIfExists(deltaPath);
|
|
14325
|
-
const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
|
|
14326
|
-
const living = livingRequirementsMap2(workspace.specs);
|
|
14327
|
-
const lintFindings = change.delta ? lintDelta(change.delta, living, deltaPath, { language: config.spec.language }) : [];
|
|
14328
|
-
const trace = checkTrace({
|
|
14329
|
-
specs: workspace.specs,
|
|
14330
|
-
change,
|
|
14331
|
-
requireEvidence: config.gates.verify.mode !== "off" && config.gates.verify.require_evidence
|
|
14332
|
-
});
|
|
14333
|
-
const blocking = lintFindings.filter((d) => d.severity === "error").length + trace.findings.filter((d) => d.severity === "error").length;
|
|
14334
|
-
const mockupsAreReady = requiresMockups(change.meta, config) ? await mockupsReady(workspace.root, change.slug, change) : void 0;
|
|
14335
|
-
const state = deriveState({
|
|
14336
|
-
change,
|
|
14337
|
-
cfg: config,
|
|
14338
|
-
approval,
|
|
14339
|
-
blockingFindings: change.delta ? blocking : 0,
|
|
14340
|
-
...mockupsAreReady !== void 0 ? { mockupsReady: mockupsAreReady } : {}
|
|
14341
|
-
});
|
|
14342
|
-
return { change, approval, lintFindings, trace, blocking, state };
|
|
14343
|
-
}
|
|
14344
|
-
|
|
14345
15347
|
// src/commands/next.ts
|
|
14346
15348
|
async function runNext(ctx) {
|
|
14347
15349
|
const { workspace, config, approvals } = await requireWorkspace(ctx);
|
|
@@ -14372,7 +15374,7 @@ async function runNext(ctx) {
|
|
|
14372
15374
|
}
|
|
14373
15375
|
|
|
14374
15376
|
// src/commands/present.ts
|
|
14375
|
-
import
|
|
15377
|
+
import path40 from "path";
|
|
14376
15378
|
async function runPresentCommand(ctx) {
|
|
14377
15379
|
const { root } = await requireWorkspace(ctx);
|
|
14378
15380
|
const slug = ctx.positionals[0];
|
|
@@ -14384,7 +15386,7 @@ async function runPresentCommand(ctx) {
|
|
|
14384
15386
|
if (result.path) {
|
|
14385
15387
|
lines.push("Propuesta generada");
|
|
14386
15388
|
lines.push("");
|
|
14387
|
-
lines.push(` archivo: ${
|
|
15389
|
+
lines.push(` archivo: ${path40.relative(ctx.cwd, result.path)}`);
|
|
14388
15390
|
if (result.hash) lines.push(` hash de la spec: ${shortHash(result.hash)}`);
|
|
14389
15391
|
lines.push("");
|
|
14390
15392
|
lines.push("\xC1brela en el navegador y comp\xE1rtela con el stakeholder.");
|
|
@@ -14394,7 +15396,7 @@ async function runPresentCommand(ctx) {
|
|
|
14394
15396
|
return {
|
|
14395
15397
|
exitCode: hasErrors2 ? 1 : 0,
|
|
14396
15398
|
diagnostics: result.diagnostics,
|
|
14397
|
-
data: result.path ? { path:
|
|
15399
|
+
data: result.path ? { path: path40.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
|
|
14398
15400
|
text: lines
|
|
14399
15401
|
};
|
|
14400
15402
|
}
|
|
@@ -14470,7 +15472,8 @@ function usage(usageText) {
|
|
|
14470
15472
|
|
|
14471
15473
|
// src/commands/status.ts
|
|
14472
15474
|
async function runStatus(ctx) {
|
|
14473
|
-
const { workspace, config, approvals } = await requireWorkspace(ctx);
|
|
15475
|
+
const { root, workspace, config, approvals } = await requireWorkspace(ctx);
|
|
15476
|
+
const advisory = await upgradeAdvisory(root);
|
|
14474
15477
|
const lines = [msg("status.title", ctx.language), ""];
|
|
14475
15478
|
const changes = [];
|
|
14476
15479
|
if (workspace.changes.length === 0) {
|
|
@@ -14502,17 +15505,27 @@ async function runStatus(ctx) {
|
|
|
14502
15505
|
for (const spec of workspace.specs) {
|
|
14503
15506
|
lines.push(` ${spec.domain} \u2014 ${spec.spec.requirements.length} requisitos`);
|
|
14504
15507
|
}
|
|
14505
|
-
const
|
|
15508
|
+
const diagnostics = [...workspace.diagnostics, ...advisory.diagnostics];
|
|
15509
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
14506
15510
|
return {
|
|
14507
15511
|
exitCode: errors > 0 ? 1 : 0,
|
|
14508
|
-
diagnostics
|
|
14509
|
-
data: {
|
|
15512
|
+
diagnostics,
|
|
15513
|
+
data: {
|
|
15514
|
+
changes,
|
|
15515
|
+
specs: workspace.specs.map((s) => ({ domain: s.domain, requirements: s.spec.requirements.length })),
|
|
15516
|
+
upgrade: {
|
|
15517
|
+
currentVersion: advisory.plan.currentVersion,
|
|
15518
|
+
pending: advisory.plan.pending.length,
|
|
15519
|
+
newer: advisory.plan.newer.length,
|
|
15520
|
+
unreadable: advisory.plan.unreadable.length
|
|
15521
|
+
}
|
|
15522
|
+
},
|
|
14510
15523
|
text: lines
|
|
14511
15524
|
};
|
|
14512
15525
|
}
|
|
14513
15526
|
|
|
14514
15527
|
// src/commands/trace.ts
|
|
14515
|
-
import
|
|
15528
|
+
import path41 from "path";
|
|
14516
15529
|
async function runTrace(ctx) {
|
|
14517
15530
|
const { workspace, config } = await requireWorkspace(ctx);
|
|
14518
15531
|
const slug = flagString(ctx.flags, "change");
|
|
@@ -14532,7 +15545,7 @@ async function runTrace(ctx) {
|
|
|
14532
15545
|
data.push({ slug: change.slug, nodes: result.graph.nodes.length, edges: result.graph.edges.length, summary: result.summary, findings: result.findings });
|
|
14533
15546
|
lines.push(` ${change.slug}: ${result.graph.nodes.length} nodos, ${result.graph.edges.length} aristas, ${errors} errores, ${result.summary.warnings} avisos`);
|
|
14534
15547
|
for (const finding of result.findings) {
|
|
14535
|
-
const location = finding.path ? ` (${
|
|
15548
|
+
const location = finding.path ? ` (${path41.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
|
|
14536
15549
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}${location}`);
|
|
14537
15550
|
}
|
|
14538
15551
|
}
|
|
@@ -14541,12 +15554,130 @@ async function runTrace(ctx) {
|
|
|
14541
15554
|
return { exitCode: blockingErrors > 0 ? 1 : 0, diagnostics, data: { changes: data }, text: lines };
|
|
14542
15555
|
}
|
|
14543
15556
|
|
|
15557
|
+
// src/commands/upgrade.ts
|
|
15558
|
+
import path42 from "path";
|
|
15559
|
+
async function runUpgrade(ctx) {
|
|
15560
|
+
const { root } = await requireWorkspace(ctx);
|
|
15561
|
+
const apply = flagBool(ctx.flags, "apply");
|
|
15562
|
+
const rollback = flagBool(ctx.flags, "rollback");
|
|
15563
|
+
if (apply && rollback) {
|
|
15564
|
+
return {
|
|
15565
|
+
exitCode: 2,
|
|
15566
|
+
diagnostics: [
|
|
15567
|
+
{
|
|
15568
|
+
code: "ATLAS-UPGRADE-CLI-001",
|
|
15569
|
+
severity: "error",
|
|
15570
|
+
message: "Elige una sola acci\xF3n: --apply o --rollback",
|
|
15571
|
+
suggestion: "Vista previa: `satlas upgrade` \xB7 Aplicar: `satlas upgrade --apply` \xB7 Revertir: `satlas upgrade --rollback`"
|
|
15572
|
+
}
|
|
15573
|
+
]
|
|
15574
|
+
};
|
|
15575
|
+
}
|
|
15576
|
+
if (rollback) return renderRollback(await rollbackUpgrade(root), ctx);
|
|
15577
|
+
if (apply) return renderApply(await applyUpgrade(root), ctx);
|
|
15578
|
+
return renderPreview(await planUpgrade(root), root, ctx);
|
|
15579
|
+
}
|
|
15580
|
+
function summarizable(plan) {
|
|
15581
|
+
return {
|
|
15582
|
+
currentVersion: plan.currentVersion,
|
|
15583
|
+
upToDate: plan.upToDate,
|
|
15584
|
+
pending: plan.pending.map((i) => ({ artifact: i.artifact, from: i.from, to: i.to, migration: i.migration })),
|
|
15585
|
+
newer: plan.newer.map((n) => ({ artifact: n.artifact, reason: n.reason })),
|
|
15586
|
+
unreadable: plan.unreadable.map((u) => ({ artifact: u.artifact, reason: u.reason }))
|
|
15587
|
+
};
|
|
15588
|
+
}
|
|
15589
|
+
function renderPreview(plan, root, ctx) {
|
|
15590
|
+
const lines = [msg("upgrade.title", ctx.language), ""];
|
|
15591
|
+
if (plan.upToDate && plan.newer.length === 0 && plan.unreadable.length === 0) {
|
|
15592
|
+
lines.push(`El estado del proyecto est\xE1 al d\xEDa (versi\xF3n ${plan.currentVersion}). No hay nada que actualizar.`);
|
|
15593
|
+
return { exitCode: 0, diagnostics: [], data: summarizable(plan), text: lines };
|
|
15594
|
+
}
|
|
15595
|
+
lines.push(`${msg("upgrade.preview", ctx.language)}: no se modifica nada.`);
|
|
15596
|
+
lines.push(` Versi\xF3n vigente: ${plan.currentVersion}`);
|
|
15597
|
+
if (plan.pending.length === 0) {
|
|
15598
|
+
lines.push(" Elementos por actualizar: 0");
|
|
15599
|
+
} else {
|
|
15600
|
+
lines.push(` Elementos por actualizar: ${plan.pending.length}`);
|
|
15601
|
+
for (const item of plan.pending) {
|
|
15602
|
+
lines.push(` ${item.artifact} ${item.from} \u2192 ${item.to} (${item.migration})`);
|
|
15603
|
+
}
|
|
15604
|
+
lines.push("");
|
|
15605
|
+
lines.push(`Aplica la actualizaci\xF3n con \`satlas upgrade --apply\`.`);
|
|
15606
|
+
}
|
|
15607
|
+
for (const item of plan.newer) {
|
|
15608
|
+
lines.push(` Aviso: "${item.artifact}" ${item.reason}`);
|
|
15609
|
+
}
|
|
15610
|
+
for (const item of plan.unreadable) {
|
|
15611
|
+
lines.push(` Ilegible: "${item.artifact}" \u2014 ${item.reason}`);
|
|
15612
|
+
lines.push(` \u21B3 ${path42.relative(ctx.cwd, item.path)}`);
|
|
15613
|
+
}
|
|
15614
|
+
return { exitCode: 0, diagnostics: [], data: summarizable(plan), text: lines };
|
|
15615
|
+
}
|
|
15616
|
+
function renderApply(report, ctx) {
|
|
15617
|
+
const lines = [msg("upgrade.title", ctx.language), ""];
|
|
15618
|
+
if (report.status === "up-to-date") {
|
|
15619
|
+
lines.push("No hab\xEDa nada que actualizar; no se escribi\xF3 ning\xFAn elemento.");
|
|
15620
|
+
return { exitCode: 0, diagnostics: [], data: { status: report.status, plan: summarizable(report.plan) }, text: lines };
|
|
15621
|
+
}
|
|
15622
|
+
if (report.status === "failed") {
|
|
15623
|
+
lines.push(`La actualizaci\xF3n fall\xF3 en "${report.failure?.artifact}": ${report.failure?.message}`);
|
|
15624
|
+
lines.push("El proyecto qued\xF3 exactamente como estaba.");
|
|
15625
|
+
return {
|
|
15626
|
+
exitCode: 1,
|
|
15627
|
+
diagnostics: [{ code: "ATLAS-UPGRADE-101", severity: "error", message: `Fall\xF3 la actualizaci\xF3n en "${report.failure?.artifact}": ${report.failure?.message}` }],
|
|
15628
|
+
data: { status: report.status, failure: report.failure },
|
|
15629
|
+
text: lines
|
|
15630
|
+
};
|
|
15631
|
+
}
|
|
15632
|
+
lines.push(`Actualizaci\xF3n aplicada: ${report.applied.length} elemento(s) a la versi\xF3n ${report.plan.currentVersion}.`);
|
|
15633
|
+
lines.push(`Respaldo recuperable: ${report.backup?.relativeDir}`);
|
|
15634
|
+
if (report.plan.unreadable.length > 0) {
|
|
15635
|
+
lines.push(`No se tocaron ${report.plan.unreadable.length} elemento(s) ilegible(s).`);
|
|
15636
|
+
}
|
|
15637
|
+
return {
|
|
15638
|
+
exitCode: 0,
|
|
15639
|
+
diagnostics: [],
|
|
15640
|
+
data: {
|
|
15641
|
+
status: report.status,
|
|
15642
|
+
applied: report.applied.map((i) => i.artifact),
|
|
15643
|
+
backup: report.backup,
|
|
15644
|
+
plan: summarizable(report.plan)
|
|
15645
|
+
},
|
|
15646
|
+
text: lines
|
|
15647
|
+
};
|
|
15648
|
+
}
|
|
15649
|
+
function renderRollback(report, ctx) {
|
|
15650
|
+
const lines = [msg("upgrade.title", ctx.language), ""];
|
|
15651
|
+
if (report.status === "no-backup") {
|
|
15652
|
+
lines.push("No hay respaldo disponible para revertir; no se modific\xF3 nada.");
|
|
15653
|
+
return {
|
|
15654
|
+
exitCode: 1,
|
|
15655
|
+
diagnostics: [{ code: "ATLAS-UPGRADE-102", severity: "warning", message: "No hay respaldo disponible para revertir la \xFAltima actualizaci\xF3n" }],
|
|
15656
|
+
data: { status: report.status },
|
|
15657
|
+
text: lines
|
|
15658
|
+
};
|
|
15659
|
+
}
|
|
15660
|
+
if (report.status === "failed") {
|
|
15661
|
+
lines.push(`La reversi\xF3n fall\xF3 en "${report.failure?.artifact}": ${report.failure?.message}`);
|
|
15662
|
+
lines.push("El respaldo se conserva para reintentar.");
|
|
15663
|
+
return {
|
|
15664
|
+
exitCode: 1,
|
|
15665
|
+
diagnostics: [{ code: "ATLAS-UPGRADE-103", severity: "error", message: `Fall\xF3 la reversi\xF3n en "${report.failure?.artifact}": ${report.failure?.message}` }],
|
|
15666
|
+
data: { status: report.status, failure: report.failure },
|
|
15667
|
+
text: lines
|
|
15668
|
+
};
|
|
15669
|
+
}
|
|
15670
|
+
lines.push(`Se restaur\xF3 el estado previo: ${report.restored.length} elemento(s). El respaldo qued\xF3 consumido.`);
|
|
15671
|
+
return { exitCode: 0, diagnostics: [], data: { status: report.status, restored: report.restored, backup: report.backup }, text: lines };
|
|
15672
|
+
}
|
|
15673
|
+
|
|
14544
15674
|
// src/commands/validate.ts
|
|
14545
|
-
import
|
|
15675
|
+
import path43 from "path";
|
|
14546
15676
|
async function runValidate(ctx) {
|
|
14547
|
-
const { workspace, config, approvals } = await requireWorkspace(ctx);
|
|
15677
|
+
const { root, workspace, config, approvals } = await requireWorkspace(ctx);
|
|
14548
15678
|
const slug = flagString(ctx.flags, "change");
|
|
14549
15679
|
const strict = flagBool(ctx.flags, "strict");
|
|
15680
|
+
const advisory = slug ? void 0 : await upgradeAdvisory(root);
|
|
14550
15681
|
const diagnostics = [];
|
|
14551
15682
|
const lines = [msg("validate.title", ctx.language), ""];
|
|
14552
15683
|
for (const spec of workspace.specs) {
|
|
@@ -14568,22 +15699,35 @@ async function runValidate(ctx) {
|
|
|
14568
15699
|
for (const finding of evaluation.lintFindings) {
|
|
14569
15700
|
lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${finding.line ? `:${finding.line}` : ""} \u2014 ${finding.message}`);
|
|
14570
15701
|
}
|
|
14571
|
-
lines.push(` archivo: ${
|
|
15702
|
+
lines.push(` archivo: ${path43.relative(ctx.cwd, path43.join(change.dir, "spec.md"))}`);
|
|
14572
15703
|
}
|
|
14573
15704
|
if (targets.length === 0) lines.push("Sin cambios activos.");
|
|
15705
|
+
if (advisory && advisory.plan.pending.length > 0) {
|
|
15706
|
+
lines.push(`Actualizaci\xF3n pendiente: ${advisory.plan.pending.length} elemento(s) (vista previa: \`satlas upgrade\`).`);
|
|
15707
|
+
}
|
|
14574
15708
|
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
14575
15709
|
const warnings = diagnostics.filter((d) => d.severity === "warning").length;
|
|
14576
15710
|
if (errors === 0 && warnings === 0) lines.push(msg("validate.ok", ctx.language));
|
|
14577
15711
|
return {
|
|
14578
15712
|
exitCode: errors > 0 || strict && warnings > 0 ? 1 : 0,
|
|
14579
|
-
diagnostics,
|
|
14580
|
-
data: {
|
|
15713
|
+
diagnostics: advisory ? [...diagnostics, ...advisory.diagnostics] : diagnostics,
|
|
15714
|
+
data: {
|
|
15715
|
+
changes,
|
|
15716
|
+
errors,
|
|
15717
|
+
warnings,
|
|
15718
|
+
upgrade: advisory ? {
|
|
15719
|
+
currentVersion: advisory.plan.currentVersion,
|
|
15720
|
+
pending: advisory.plan.pending.length,
|
|
15721
|
+
newer: advisory.plan.newer.length,
|
|
15722
|
+
unreadable: advisory.plan.unreadable.length
|
|
15723
|
+
} : void 0
|
|
15724
|
+
},
|
|
14581
15725
|
text: lines
|
|
14582
15726
|
};
|
|
14583
15727
|
}
|
|
14584
15728
|
|
|
14585
15729
|
// src/commands/verify.ts
|
|
14586
|
-
import
|
|
15730
|
+
import path44 from "path";
|
|
14587
15731
|
async function runVerify(ctx) {
|
|
14588
15732
|
const { root, workspace } = await requireWorkspace(ctx);
|
|
14589
15733
|
const slug = ctx.positionals[0];
|
|
@@ -14626,7 +15770,7 @@ async function runVerify(ctx) {
|
|
|
14626
15770
|
return { exitCode: 2, diagnostics: [{ code: "ATLAS-VERIFY-001", severity: "error", message: "Falta --by <nombre> (o define meta.owner)" }] };
|
|
14627
15771
|
}
|
|
14628
15772
|
const profilesDir = await resolveProfilesDir();
|
|
14629
|
-
const profile = await loadActiveProfile(
|
|
15773
|
+
const profile = await loadActiveProfile(path44.join(root, ".sdd"), [profilesDir ?? "", path44.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
|
|
14630
15774
|
const record = await recordEvidence({
|
|
14631
15775
|
root,
|
|
14632
15776
|
slug,
|
|
@@ -14648,7 +15792,7 @@ async function runVerify(ctx) {
|
|
|
14648
15792
|
if (record.evidence.command) lines.push(` comando: ${record.evidence.command}`);
|
|
14649
15793
|
lines.push(` resultado: ${record.evidence.result}`);
|
|
14650
15794
|
if (record.evidence.outputHash) lines.push(` hash: ${record.evidence.outputHash}`);
|
|
14651
|
-
lines.push(` archivo: ${
|
|
15795
|
+
lines.push(` archivo: ${path44.relative(ctx.cwd, record.path)}`);
|
|
14652
15796
|
if (record.stdout) {
|
|
14653
15797
|
lines.push("");
|
|
14654
15798
|
lines.push(" salida (\xFAltimas l\xEDneas):");
|
|
@@ -14663,7 +15807,7 @@ async function runVerify(ctx) {
|
|
|
14663
15807
|
return {
|
|
14664
15808
|
exitCode: record.exitCode === 0 ? 0 : 1,
|
|
14665
15809
|
diagnostics: record.diagnostics,
|
|
14666
|
-
data: { evidence: record.evidence, path:
|
|
15810
|
+
data: { evidence: record.evidence, path: path44.relative(ctx.cwd, record.path), exitCode: record.exitCode },
|
|
14667
15811
|
text: lines
|
|
14668
15812
|
};
|
|
14669
15813
|
}
|
|
@@ -14708,7 +15852,7 @@ El cambio ${slug} no tiene tareas todav\xEDa.`] };
|
|
|
14708
15852
|
}
|
|
14709
15853
|
|
|
14710
15854
|
// src/cli.ts
|
|
14711
|
-
var
|
|
15855
|
+
var HANDLERS2 = {
|
|
14712
15856
|
init: runInit,
|
|
14713
15857
|
adopt: runAdopt,
|
|
14714
15858
|
new: runNew,
|
|
@@ -14718,6 +15862,7 @@ var HANDLERS = {
|
|
|
14718
15862
|
trace: runTrace,
|
|
14719
15863
|
waves: runWaves,
|
|
14720
15864
|
doctor: runDoctorCommand,
|
|
15865
|
+
upgrade: runUpgrade,
|
|
14721
15866
|
approve: runApprove,
|
|
14722
15867
|
issue: runIssue,
|
|
14723
15868
|
adapters: runAdapters,
|
|
@@ -14729,6 +15874,7 @@ var HANDLERS = {
|
|
|
14729
15874
|
mockup: runMockup,
|
|
14730
15875
|
present: runPresentCommand,
|
|
14731
15876
|
ci: runCi,
|
|
15877
|
+
mcp: runMcp,
|
|
14732
15878
|
metrics: runMetrics,
|
|
14733
15879
|
run: runRun,
|
|
14734
15880
|
archive: runArchive,
|
|
@@ -14736,7 +15882,7 @@ var HANDLERS = {
|
|
|
14736
15882
|
help: (ctx) => runHelp(ctx)
|
|
14737
15883
|
};
|
|
14738
15884
|
var COMMANDS = Object.fromEntries(
|
|
14739
|
-
CATALOG.map((spec) => [spec.name, { description: spec.description, flags: spec.flags, handler:
|
|
15885
|
+
CATALOG.map((spec) => [spec.name, { description: spec.description, flags: spec.flags, handler: HANDLERS2[spec.name] }])
|
|
14740
15886
|
);
|
|
14741
15887
|
function toEnvelope(command, result) {
|
|
14742
15888
|
const warnings = result.diagnostics.filter((d) => d.severity !== "error").map(stripSeverity);
|
|
@@ -14761,7 +15907,7 @@ async function requireWorkspace(ctx) {
|
|
|
14761
15907
|
throw new CliError("ATLAS-WS-001", msg("cli.noWorkspace", ctx.language), 2);
|
|
14762
15908
|
}
|
|
14763
15909
|
const { workspace, config } = await loadWorkspace(root);
|
|
14764
|
-
const approvals = await loadApprovals(
|
|
15910
|
+
const approvals = await loadApprovals(path45.join(root, ".sdd"));
|
|
14765
15911
|
return { root, workspace, config, approvals: approvals.byArtifact };
|
|
14766
15912
|
}
|
|
14767
15913
|
var CliError = class extends Error {
|
|
@@ -14828,7 +15974,7 @@ function printResult(command, result, json2, language) {
|
|
|
14828
15974
|
}
|
|
14829
15975
|
const lines = [...result.text ?? []];
|
|
14830
15976
|
for (const d of result.diagnostics) {
|
|
14831
|
-
const location = d.path ? ` ${
|
|
15977
|
+
const location = d.path ? ` ${path45.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
|
|
14832
15978
|
lines.push(`${d.severity === "error" ? "ERROR" : d.severity === "warning" ? "AVISO" : "NOTA"} ${d.code}${location} \u2014 ${d.message}`);
|
|
14833
15979
|
if (d.suggestion) lines.push(` \u21B3 ${d.suggestion}`);
|
|
14834
15980
|
}
|