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-HYN75DTZ.js";
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 path39 from "path";
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, path192, line) {
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: path192,
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, path192, opts = {}) {
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", path192, part.line));
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", path192, part.line));
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: path192, line: req.line, suggestion: "A\xF1ade al menos un escenario CUANDO/ENTONCES" }));
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, path192, opts = {}) {
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, path192, opts));
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: path192, line: req.line }));
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: path192,
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: path192, line: req.line }));
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: path192, line: rename2.line }));
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: path192, line: rename2.line }));
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, path192) {
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: path192,
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: path192,
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: path192,
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-54L7NGZ5.js");
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 = path17.join(change.dir, "spec.md");
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: path17.join(change.dir, "meta.yaml") }));
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 = path18.resolve(opts.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, path18.join(change.dir, "spec.md"), { language: config.spec.language }) : [];
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 path21 from "path";
13633
+ import path24 from "path";
13127
13634
 
13128
13635
  // ../adapters/dist/index.js
13129
- import path19 from "path";
13130
- import { stringify as stringifyYaml5 } from "yaml";
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 = path19.join(workflowDir, "phases");
13134
- const snippetsDir = path19.join(workflowDir, "snippets");
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 = path19.join(phasesDir, entry);
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"] : path19.basename(entry, ".md");
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(path19.join(snippetsDir, entry)) ?? "";
13666
+ const raw = await readTextIfExists(path21.join(snippetsDir, entry)) ?? "";
13160
13667
  hashParts.push(raw);
13161
- snippets.set(path19.basename(entry, ".md"), raw.trim());
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 = stringifyYaml5(data, { lineWidth: 0 }).trimEnd();
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 path20 from "path";
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(path20.join("workflow", "phases"));
13982
+ return walkUpFor(path23.join("workflow", "phases"));
13476
13983
  }
13477
13984
  async function walkUpFor(relative) {
13478
- let dir = path20.dirname(fileURLToPath2(import.meta.url));
13985
+ let dir = path23.dirname(fileURLToPath2(import.meta.url));
13479
13986
  for (let i = 0; i < 8; i += 1) {
13480
- const candidate = path20.join(dir, relative);
13987
+ const candidate = path23.join(dir, relative);
13481
13988
  if (await exists(candidate)) {
13482
- return relative.includes(path20.sep) ? path20.dirname(candidate) : candidate;
13989
+ return relative.includes(path23.sep) ? path23.dirname(candidate) : candidate;
13483
13990
  }
13484
- const parent = path20.dirname(dir);
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(` + ${path21.relative(ctx.cwd, path21.join(root, p))}`);
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 path23 from "path";
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] " : "+ "}${path23.relative(ctx.cwd, spec)}`);
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: ${path23.relative(ctx.cwd, result.reportPath)}`);
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) => path23.relative(ctx.cwd, s)),
13611
- reportPath: result.reportPath ? path23.relative(ctx.cwd, result.reportPath) : void 0,
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 path24 from "path";
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: ${path24.relative(ctx.cwd, result.path)}`);
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: path24.relative(ctx.cwd, result.path) } : result,
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 path26 from "path";
14151
+ import path28 from "path";
13645
14152
 
13646
14153
  // src/commands/issue.ts
13647
- import path25 from "path";
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(path25.join(root, ".sdd"));
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 : path26.join("changes", target, "spec.md");
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: path26.relative(ctx.cwd, result.file) } : void 0,
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 path27 from "path";
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: ${path27.relative(ctx.cwd, result.archivedTo)}`);
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: gate.diagnostics,
13867
- data: { checks: gate.checks, errors: gate.errors, warnings: gate.warnings, strict, failed: gate.failed },
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 path28 from "path";
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 = path28.join(workspace.sddDir, "profiles", "custom");
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 = path28.join(customDir, `${name}.yaml`);
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: ${path28.relative(ctx.cwd, file)}`, "", "Ed\xEDtalo con los comandos reales de tu stack (build/lint/test) y tus anti-patrones."]
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(path28.join(dir, `${name}.yaml`));
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 path29 from "path";
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 ? ` ${path29.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""}` : "";
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 path30 from "path";
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(` + ${path30.relative(ctx.cwd, file)}`);
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(` + ${path30.relative(ctx.cwd, path30.join(ctx.cwd, file))}`);
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: path30.relative(ctx.cwd, result.sddDir),
14111
- created: result.created.map((f) => path30.relative(ctx.cwd, 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 path31 from "path";
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: ${path31.relative(ctx.cwd, planFile)}`,
14202
- ` manifiesto: ${path31.relative(ctx.cwd, manifestFile)}`,
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: path31.relative(ctx.cwd, planFile), manifestFile: path31.relative(ctx.cwd, manifestFile) }, text: lines };
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 path32 from "path";
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: path32.posix.join(".sdd", "changes", slug, "analyze.md")
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 path33 from "path";
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) => ` + ${path33.relative(ctx.cwd, 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) => path33.relative(ctx.cwd, 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 path35 from "path";
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: ${path35.relative(ctx.cwd, result.path)}`);
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: path35.relative(ctx.cwd, result.path), hash: result.hash } : void 0,
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 errors = workspace.diagnostics.filter((d) => d.severity === "error").length;
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: workspace.diagnostics,
14509
- data: { changes, specs: workspace.specs.map((s) => ({ domain: s.domain, requirements: s.spec.requirements.length })) },
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 path36 from "path";
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 ? ` (${path36.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
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 path37 from "path";
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: ${path37.relative(ctx.cwd, path37.join(change.dir, "spec.md"))}`);
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: { changes, errors, warnings },
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 path38 from "path";
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(path38.join(root, ".sdd"), [profilesDir ?? "", path38.join(root, ".sdd", "profiles", "custom")].filter(Boolean));
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: ${path38.relative(ctx.cwd, record.path)}`);
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: path38.relative(ctx.cwd, record.path), exitCode: record.exitCode },
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 HANDLERS = {
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: HANDLERS[spec.name] }])
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(path39.join(root, ".sdd"));
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 ? ` ${path39.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
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
  }