specatlas 0.1.25 → 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 path41 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+$/;
@@ -9742,14 +9747,14 @@ function wordBoundary(text, term) {
9742
9747
  const re = new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu");
9743
9748
  return re.test(text);
9744
9749
  }
9745
- function lintText(text, code, terms, label, path192, line) {
9750
+ function lintText(text, code, terms, label, path212, line) {
9746
9751
  const out = [];
9747
9752
  const lower = text.toLowerCase();
9748
9753
  for (const term of terms) {
9749
9754
  if (wordBoundary(lower, term)) {
9750
9755
  out.push(
9751
9756
  diag(code, "error", `${label}: "${term}"`, {
9752
- path: path192,
9757
+ path: path212,
9753
9758
  line,
9754
9759
  suggestion: "La especificaci\xF3n es funcional y de negocio: describe comportamiento, no tecnolog\xEDa ni adjetivos vagos"
9755
9760
  })
@@ -9758,7 +9763,7 @@ function lintText(text, code, terms, label, path192, line) {
9758
9763
  }
9759
9764
  return out;
9760
9765
  }
9761
- function lintRequirement(req, path192, opts = {}) {
9766
+ function lintRequirement(req, path212, opts = {}) {
9762
9767
  const out = [];
9763
9768
  const vague = opts.language === "en" ? VAGUE_EN : VAGUE_ES;
9764
9769
  const tech = opts.language === "en" ? TECH_EN : TECH_ES;
@@ -9769,32 +9774,32 @@ function lintRequirement(req, path192, opts = {}) {
9769
9774
  ...req.scenarios.flatMap((s) => [...s.when.map((w) => ({ text: w, line: s.line })), ...s.then.map((t) => ({ text: t, line: s.line }))])
9770
9775
  ];
9771
9776
  for (const part of parts) {
9772
- 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));
9773
9778
  if (opts.businessOnly !== false) {
9774
- 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));
9775
9780
  }
9776
9781
  }
9777
9782
  if (req.scenarios.length === 0) {
9778
- 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" }));
9779
9784
  }
9780
9785
  return out;
9781
9786
  }
9782
- function lintDelta(delta, livingRequirements, path192, opts = {}) {
9787
+ function lintDelta(delta, livingRequirements, path212, opts = {}) {
9783
9788
  const out = [...delta.diagnostics];
9784
9789
  for (const req of [...delta.added, ...delta.modified]) {
9785
- out.push(...lintRequirement(req, path192, opts));
9790
+ out.push(...lintRequirement(req, path212, opts));
9786
9791
  }
9787
9792
  for (const req of delta.modified) {
9788
9793
  const living = livingRequirements.get(req.id);
9789
9794
  if (!living) {
9790
- 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 }));
9791
9796
  continue;
9792
9797
  }
9793
9798
  for (const existing of living.scenarios) {
9794
9799
  if (!req.scenarios.some((s) => s.id === existing.id)) {
9795
9800
  out.push(
9796
9801
  diag("TRACE-007", "error", `MODIFIED ${req.id} pierde el escenario ${existing.id}: copia el bloque completo`, {
9797
- path: path192,
9802
+ path: path212,
9798
9803
  line: req.line,
9799
9804
  suggestion: "Copia el bloque completo de la spec viva y ed\xEDtalo; para quitarlo, decl\xE1ralo en REMOVED"
9800
9805
  })
@@ -9805,15 +9810,15 @@ function lintDelta(delta, livingRequirements, path192, opts = {}) {
9805
9810
  for (const req of delta.removed) {
9806
9811
  const living = livingRequirements.get(req.id);
9807
9812
  if (!living) {
9808
- 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 }));
9809
9814
  }
9810
9815
  }
9811
9816
  for (const rename2 of delta.renamed) {
9812
9817
  if (rename2.from.id !== rename2.to.id) {
9813
- out.push(diag("LINT-DLT-003", "error", `RENAMED cambia el id (${rename2.from.id} \u2192 ${rename2.to.id}); los ids son inmutables`, { path: 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 }));
9814
9819
  }
9815
9820
  if (!livingRequirements.has(rename2.from.id)) {
9816
- 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 }));
9817
9822
  }
9818
9823
  }
9819
9824
  return out;
@@ -9837,7 +9842,7 @@ var MERMAID_KEYWORDS = [
9837
9842
  "architecture-beta"
9838
9843
  ];
9839
9844
  var MERMAID_BLOCKS = /^\s*(alt|loop|opt|par|rect|critical|break|subgraph)\b/;
9840
- function lintPlan(planText, path192) {
9845
+ function lintPlan(planText, path212) {
9841
9846
  const out = [];
9842
9847
  const blocks = [...planText.matchAll(/```mermaid\r?\n([\s\S]*?)```/g)];
9843
9848
  for (const [index, block] of blocks.entries()) {
@@ -9847,7 +9852,7 @@ function lintPlan(planText, path192) {
9847
9852
  if (!MERMAID_KEYWORDS.some((keyword) => first.startsWith(keyword))) {
9848
9853
  out.push(
9849
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)}"`, {
9850
- path: path192,
9855
+ path: path212,
9851
9856
  suggestion: "Corrige el tipo del diagrama o elimina el bloque"
9852
9857
  })
9853
9858
  );
@@ -9861,7 +9866,7 @@ function lintPlan(planText, path192) {
9861
9866
  if (open !== 0) {
9862
9867
  out.push(
9863
9868
  diag("LINT-PLN-002", "error", `Diagrama mermaid ${index + 1}: faltan ${Math.abs(open)} \`end\` (bloques alt/loop/subgraph sin cerrar)`, {
9864
- path: path192,
9869
+ path: path212,
9865
9870
  suggestion: "Cierra cada bloque alt/loop/opt/par/rect/subgraph con `end`"
9866
9871
  })
9867
9872
  );
@@ -9873,7 +9878,7 @@ function lintPlan(planText, path192) {
9873
9878
  if (message.includes(";")) {
9874
9879
  out.push(
9875
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`, {
9876
- path: path192,
9881
+ path: path212,
9877
9882
  suggestion: "Sustituye `;` por `\xB7` o `,` en los mensajes de sequenceDiagram"
9878
9883
  })
9879
9884
  );
@@ -11105,7 +11110,7 @@ async function approveFromGithub(opts) {
11105
11110
  if (!by) {
11106
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`)")] };
11107
11112
  }
11108
- const { signApproval: signApproval2 } = await import("./approvals-2PO6223I-54L7NGZ5.js");
11113
+ const { signApproval: signApproval2 } = await import("./approvals-2PO6223I-DENKLEXU.js");
11109
11114
  const signed = await signApproval2({
11110
11115
  root: opts.root,
11111
11116
  artifact: path5.posix.join("changes", opts.slug, "spec.md"),
@@ -12988,6 +12993,351 @@ async function regenerateIndex(root, cfg, now = /* @__PURE__ */ new Date()) {
12988
12993
  void now;
12989
12994
  await writeText(path16.join(root, ".sdd", "INDEX.md"), markdown2);
12990
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
+ }
12991
13341
  async function runDoctor(root) {
12992
13342
  const findings = [];
12993
13343
  const { workspace, config } = await loadWorkspace(root);
@@ -12995,7 +13345,7 @@ async function runDoctor(root) {
12995
13345
  const approvals = await loadApprovals(workspace.sddDir);
12996
13346
  findings.push(...approvals.diagnostics);
12997
13347
  for (const change of workspace.changes) {
12998
- const deltaPath = path17.join(change.dir, "spec.md");
13348
+ const deltaPath = path19.join(change.dir, "spec.md");
12999
13349
  const deltaContent = await readTextIfExists(deltaPath);
13000
13350
  const approval = verifyApproval(change, approvals.byArtifact, config, deltaContent ?? void 0);
13001
13351
  if ((change.planPath || change.tasks) && (approval.status === "missing" || approval.status === "stale")) {
@@ -13017,7 +13367,7 @@ async function runDoctor(root) {
13017
13367
  }
13018
13368
  for (const override of change.meta?.overrides ?? []) {
13019
13369
  if (!override.reason.trim() || !override.by.trim()) {
13020
- findings.push(diag("ATLAS-LIFECYCLE-004", "error", `Override del gate "${override.gate}" sin motivo o autor`, { path: 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") }));
13021
13371
  }
13022
13372
  }
13023
13373
  }
@@ -13049,7 +13399,7 @@ function livingRequirementsMap(specs) {
13049
13399
  return map;
13050
13400
  }
13051
13401
  async function runCiGate(opts) {
13052
- const root = path18.resolve(opts.root);
13402
+ const root = path20.resolve(opts.root);
13053
13403
  const { workspace, config } = await loadWorkspace(root);
13054
13404
  const diagnostics = [];
13055
13405
  const checks = [];
@@ -13060,7 +13410,7 @@ async function runCiGate(opts) {
13060
13410
  let changesErrors = 0;
13061
13411
  let changesWarnings = 0;
13062
13412
  for (const change of workspace.changes) {
13063
- 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 }) : [];
13064
13414
  const trace = checkTrace({
13065
13415
  specs: workspace.specs,
13066
13416
  change,
@@ -13169,6 +13519,7 @@ var CATALOG = [
13169
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]" },
13170
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]" },
13171
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]" },
13172
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' },
13173
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>" },
13174
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]" },
@@ -13179,7 +13530,7 @@ var CATALOG = [
13179
13530
  { name: "analyze", description: "Chequeo cruzado (lint + trace + waves + mockups) y escribe analyze.md", flags: ["json"], usage: "satlas analyze <slug>" },
13180
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]" },
13181
13532
  { name: "present", description: "Genera el paquete de propuesta para el stakeholder (HTML autocontenido)", flags: ["json"], usage: "satlas present <slug>" },
13182
- { name: "ci", description: "Gate de pipeline: specs + cambios + doctor + adaptadores (sin agente)", flags: ["strict", "json"], usage: "satlas ci [--strict]" },
13533
+ { name: "ci", description: "Gate de pipeline: specs + cambios + doctor + adaptadores (sin agente)", flags: ["strict", "sarif", "json"], usage: "satlas ci [--strict] [--sarif <ruta>]" },
13183
13534
  { name: "mcp", description: "V\xEDa de consulta de solo lectura para asistentes (protocolo MCP sobre entrada/salida est\xE1ndar)", flags: [], usage: "satlas mcp" },
13184
13535
  { name: "metrics", description: "M\xE9tricas locales del workspace (progreso, WIP, throughput, evidencia)", flags: ["json"], usage: "satlas metrics [--json]" },
13185
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" },
@@ -13216,6 +13567,8 @@ var ES2 = {
13216
13567
  "init.done": "Workspace SDD creado.",
13217
13568
  "new.title": "Nuevo cambio",
13218
13569
  "new.done": "Cambio creado. Siguiente: escribe la spec funcional.",
13570
+ "upgrade.title": "Actualizar el estado del proyecto",
13571
+ "upgrade.preview": "Vista previa",
13219
13572
  "summary.findings": "hallazgos",
13220
13573
  "label.errors": "errores",
13221
13574
  "label.warnings": "avisos",
@@ -13248,6 +13601,8 @@ var EN2 = {
13248
13601
  "init.done": "SDD workspace created.",
13249
13602
  "new.title": "New change",
13250
13603
  "new.done": "Change created. Next: write the business spec.",
13604
+ "upgrade.title": "Upgrade project state",
13605
+ "upgrade.preview": "Preview",
13251
13606
  "summary.findings": "findings",
13252
13607
  "label.errors": "errors",
13253
13608
  "label.warnings": "warnings",
@@ -13275,26 +13630,26 @@ function cliVersion() {
13275
13630
  }
13276
13631
 
13277
13632
  // src/commands/adapters.ts
13278
- import path21 from "path";
13633
+ import path24 from "path";
13279
13634
 
13280
13635
  // ../adapters/dist/index.js
13281
- import path19 from "path";
13282
- import { stringify as stringifyYaml5 } from "yaml";
13636
+ import path21 from "path";
13637
+ import { stringify as stringifyYaml6 } from "yaml";
13283
13638
  import path22 from "path";
13284
13639
  async function loadWorkflow(workflowDir) {
13285
- const phasesDir = path19.join(workflowDir, "phases");
13286
- const snippetsDir = path19.join(workflowDir, "snippets");
13640
+ const phasesDir = path21.join(workflowDir, "phases");
13641
+ const snippetsDir = path21.join(workflowDir, "snippets");
13287
13642
  const phases = [];
13288
13643
  const snippets = /* @__PURE__ */ new Map();
13289
13644
  const hashParts = [];
13290
13645
  for (const entry of (await listDir(phasesDir)).sort()) {
13291
13646
  if (!entry.endsWith(".md")) continue;
13292
- const filePath = path19.join(phasesDir, entry);
13647
+ const filePath = path21.join(phasesDir, entry);
13293
13648
  const raw = await readText(filePath);
13294
13649
  hashParts.push(raw);
13295
13650
  const fm = parseFrontmatter(raw, filePath);
13296
13651
  const data = fm.data;
13297
- 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");
13298
13653
  phases.push({
13299
13654
  id,
13300
13655
  title: typeof data["title"] === "string" ? data["title"] : id,
@@ -13308,9 +13663,9 @@ async function loadWorkflow(workflowDir) {
13308
13663
  }
13309
13664
  for (const entry of (await listDir(snippetsDir)).sort()) {
13310
13665
  if (!entry.endsWith(".md")) continue;
13311
- const raw = await readTextIfExists(path19.join(snippetsDir, entry)) ?? "";
13666
+ const raw = await readTextIfExists(path21.join(snippetsDir, entry)) ?? "";
13312
13667
  hashParts.push(raw);
13313
- snippets.set(path19.basename(entry, ".md"), raw.trim());
13668
+ snippets.set(path21.basename(entry, ".md"), raw.trim());
13314
13669
  }
13315
13670
  return { phases: phases.sort((a, b) => a.id.localeCompare(b.id)), snippets, sourceHash: sha256(hashParts.join("\n---\n")) };
13316
13671
  }
@@ -13334,7 +13689,7 @@ function vars(language, slugToken, commandPrefix) {
13334
13689
  return { SLUG: slugToken, LANGUAGE: language, LANGUAGE_NAME: LANGUAGE_NAMES[language], SDD_DIR: ".sdd", COMMAND_PREFIX: commandPrefix };
13335
13690
  }
13336
13691
  function frontmatter(data, body) {
13337
- const yaml2 = stringifyYaml5(data, { lineWidth: 0 }).trimEnd();
13692
+ const yaml2 = stringifyYaml6(data, { lineWidth: 0 }).trimEnd();
13338
13693
  return `---
13339
13694
  ${yaml2}
13340
13695
  ---
@@ -13614,7 +13969,7 @@ async function checkAdapters(opts) {
13614
13969
  }
13615
13970
 
13616
13971
  // src/paths.ts
13617
- import path20 from "path";
13972
+ import path23 from "path";
13618
13973
  import { fileURLToPath as fileURLToPath2 } from "url";
13619
13974
  async function resolveProfilesDir() {
13620
13975
  const env = process.env["SPECATLAS_PROFILES_DIR"];
@@ -13624,16 +13979,16 @@ async function resolveProfilesDir() {
13624
13979
  async function resolveWorkflowDir() {
13625
13980
  const env = process.env["SPECATLAS_WORKFLOW_DIR"];
13626
13981
  if (env && await exists(env)) return env;
13627
- return walkUpFor(path20.join("workflow", "phases"));
13982
+ return walkUpFor(path23.join("workflow", "phases"));
13628
13983
  }
13629
13984
  async function walkUpFor(relative) {
13630
- let dir = path20.dirname(fileURLToPath2(import.meta.url));
13985
+ let dir = path23.dirname(fileURLToPath2(import.meta.url));
13631
13986
  for (let i = 0; i < 8; i += 1) {
13632
- const candidate = path20.join(dir, relative);
13987
+ const candidate = path23.join(dir, relative);
13633
13988
  if (await exists(candidate)) {
13634
- return relative.includes(path20.sep) ? path20.dirname(candidate) : candidate;
13989
+ return relative.includes(path23.sep) ? path23.dirname(candidate) : candidate;
13635
13990
  }
13636
- const parent = path20.dirname(dir);
13991
+ const parent = path23.dirname(dir);
13637
13992
  if (parent === dir) break;
13638
13993
  dir = parent;
13639
13994
  }
@@ -13711,7 +14066,7 @@ async function runAdapters(ctx) {
13711
14066
  for (const [target, count2] of byTarget) lines.push(` ${target}: ${count2} archivo(s)`);
13712
14067
  if (report.written.length > 0) {
13713
14068
  lines.push("");
13714
- 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))}`);
13715
14070
  } else {
13716
14071
  lines.push("");
13717
14072
  lines.push("Sin cambios: los adaptadores ya estaban al d\xEDa.");
@@ -13722,7 +14077,7 @@ async function runAdapters(ctx) {
13722
14077
  }
13723
14078
 
13724
14079
  // src/commands/adopt.ts
13725
- import path23 from "path";
14080
+ import path25 from "path";
13726
14081
  async function runAdopt(ctx) {
13727
14082
  const { root } = await requireWorkspace(ctx);
13728
14083
  const domainsFlag = flagString(ctx.flags, "domains") ?? flagString(ctx.flags, "domain");
@@ -13742,11 +14097,11 @@ async function runAdopt(ctx) {
13742
14097
  }
13743
14098
  if (result.createdSpecs.length > 0) {
13744
14099
  lines.push("");
13745
- 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)}`);
13746
14101
  }
13747
14102
  if (result.reportPath && !result.dryRun) {
13748
14103
  lines.push("");
13749
- lines.push(` Informe: ${path23.relative(ctx.cwd, result.reportPath)}`);
14104
+ lines.push(` Informe: ${path25.relative(ctx.cwd, result.reportPath)}`);
13750
14105
  }
13751
14106
  lines.push("");
13752
14107
  lines.push("Siguiente: fase /satlas-adopt por dominio (requisitos AS-IS) y luego:");
@@ -13759,8 +14114,8 @@ async function runAdopt(ctx) {
13759
14114
  diagnostics: result.diagnostics,
13760
14115
  data: {
13761
14116
  domains: result.domains.map((d) => ({ name: d.name, files: d.files, existingSpec: d.existingSpec })),
13762
- createdSpecs: result.createdSpecs.map((s) => path23.relative(ctx.cwd, s)),
13763
- 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,
13764
14119
  stack: result.stack,
13765
14120
  dryRun: result.dryRun
13766
14121
  },
@@ -13769,7 +14124,7 @@ async function runAdopt(ctx) {
13769
14124
  }
13770
14125
 
13771
14126
  // src/commands/analyze.ts
13772
- import path24 from "path";
14127
+ import path26 from "path";
13773
14128
  async function runAnalyzeCommand(ctx) {
13774
14129
  const { root } = await requireWorkspace(ctx);
13775
14130
  const slug = ctx.positionals[0];
@@ -13780,23 +14135,23 @@ async function runAnalyzeCommand(ctx) {
13780
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`];
13781
14136
  if (result.waves) lines.push(` olas: ${result.waves.blocks} bloque(s), ${result.waves.waves} ola(s), ${result.waves.tasks} tarea(s)`);
13782
14137
  if (result.mockups) lines.push(` mockups: ${result.mockups.screens} pantalla(s)${result.mockups.stale ? " (desactualizados)" : ""}`);
13783
- 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)}`);
13784
14139
  for (const finding of result.findings) {
13785
14140
  lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}`);
13786
14141
  }
13787
14142
  return {
13788
14143
  exitCode: result.status === "blocked" ? 1 : 0,
13789
14144
  diagnostics: result.findings,
13790
- 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,
13791
14146
  text: lines
13792
14147
  };
13793
14148
  }
13794
14149
 
13795
14150
  // src/commands/approve.ts
13796
- import path26 from "path";
14151
+ import path28 from "path";
13797
14152
 
13798
14153
  // src/commands/issue.ts
13799
- import path25 from "path";
14154
+ import path27 from "path";
13800
14155
  async function runIssue(ctx) {
13801
14156
  const action = ctx.positionals[0] ?? "status";
13802
14157
  const slug = ctx.positionals[1];
@@ -13824,7 +14179,7 @@ async function runIssue(ctx) {
13824
14179
  return { exitCode: 2, diagnostics: [{ code: "ATLAS-ISSUE-000", severity: "error", message: "Falta el slug: satlas issue status <slug>" }] };
13825
14180
  }
13826
14181
  const change = await loadChange(root, slug);
13827
- const { config } = await loadConfig(path25.join(root, ".sdd"));
14182
+ const { config } = await loadConfig(path27.join(root, ".sdd"));
13828
14183
  const tracker = change.meta?.tracker;
13829
14184
  const lines = [`Issue de ${slug}`, ""];
13830
14185
  if (!tracker || tracker.provider !== "github") {
@@ -13895,7 +14250,7 @@ async function runApprove(ctx) {
13895
14250
  };
13896
14251
  }
13897
14252
  const { root, workspace, config } = await requireWorkspace(ctx);
13898
- 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");
13899
14254
  const channelFlag = flagString(ctx.flags, "channel");
13900
14255
  const channel = channelFlag === "presentation" || channelFlag === "editor" || channelFlag === "pr" || channelFlag === "tracker" ? channelFlag : "cli";
13901
14256
  const change = workspace.changes.find(
@@ -13936,13 +14291,13 @@ async function runApprove(ctx) {
13936
14291
  return {
13937
14292
  exitCode: hasErrors2 ? 1 : 0,
13938
14293
  diagnostics: result.diagnostics,
13939
- 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,
13940
14295
  text: lines
13941
14296
  };
13942
14297
  }
13943
14298
 
13944
14299
  // src/commands/archive.ts
13945
- import path27 from "path";
14300
+ import path29 from "path";
13946
14301
  async function runArchive(ctx) {
13947
14302
  const slug = ctx.positionals[0];
13948
14303
  if (!slug) {
@@ -13972,7 +14327,7 @@ async function runArchive(ctx) {
13972
14327
  lines.push(` modificados: ${result.fold.applied.modified.join(", ") || "\u2014"}`);
13973
14328
  lines.push(` eliminados: ${result.fold.applied.removed.join(", ") || "\u2014"}`);
13974
14329
  lines.push(` renombrados: ${result.fold.applied.renamed.join(", ") || "\u2014"}`);
13975
- 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)}`);
13976
14331
  } else {
13977
14332
  lines.push("El delta no contiene operaciones (ADDED/MODIFIED/REMOVED/RENAMED).");
13978
14333
  }
@@ -13986,9 +14341,11 @@ async function runArchive(ctx) {
13986
14341
  }
13987
14342
 
13988
14343
  // src/commands/ci.ts
14344
+ import path30 from "path";
13989
14345
  async function runCi(ctx) {
13990
14346
  const { root, config } = await requireWorkspace(ctx);
13991
14347
  const strict = flagBool(ctx.flags, "strict");
14348
+ const sarifPath = flagString(ctx.flags, "sarif");
13992
14349
  const extra = [];
13993
14350
  const workflowDir = await resolveWorkflowDir();
13994
14351
  if (workflowDir) {
@@ -14006,28 +14363,54 @@ async function runCi(ctx) {
14006
14363
  });
14007
14364
  }
14008
14365
  const gate = await runCiGate({ root, strict, extra });
14366
+ const diagnostics = [...gate.diagnostics];
14009
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
+ }
14010
14383
  for (const check of gate.checks) {
14011
14384
  lines.push(` ${check.errors === 0 && check.warnings === 0 ? "OK " : "FALLA"} ${check.name}: ${check.errors} errores, ${check.warnings} avisos`);
14012
14385
  }
14013
14386
  lines.push("");
14014
14387
  lines.push(gate.failed ? "Resultado: BLOQUEADO" : "Resultado: OK");
14015
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
+ }
14016
14392
  return {
14017
14393
  exitCode: gate.failed ? 1 : 0,
14018
- diagnostics: gate.diagnostics,
14019
- 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
+ },
14020
14403
  text: lines
14021
14404
  };
14022
14405
  }
14023
14406
 
14024
14407
  // src/commands/profile.ts
14025
- import path28 from "path";
14408
+ import path31 from "path";
14026
14409
  async function runProfile(ctx) {
14027
14410
  const action = ctx.positionals[0] ?? "detect";
14028
14411
  const { root, workspace } = await requireWorkspace(ctx);
14029
14412
  const officialDir = await resolveProfilesDir();
14030
- const customDir = path28.join(workspace.sddDir, "profiles", "custom");
14413
+ const customDir = path31.join(workspace.sddDir, "profiles", "custom");
14031
14414
  const profiles = [...officialDir ? await loadProfilesFromDir(officialDir) : [], ...await loadProfilesFromDir(customDir)];
14032
14415
  if (action === "detect") {
14033
14416
  const result = await detectProfiles(root, profiles);
@@ -14056,7 +14439,7 @@ async function runProfile(ctx) {
14056
14439
  if (!name) {
14057
14440
  return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-001", severity: "error", message: "Falta el nombre del perfil: satlas profile create <nombre>" }] };
14058
14441
  }
14059
- const file = path28.join(customDir, `${name}.yaml`);
14442
+ const file = path31.join(customDir, `${name}.yaml`);
14060
14443
  if (await exists(file)) {
14061
14444
  return { exitCode: 2, diagnostics: [{ code: "ATLAS-PROFILE-002", severity: "error", message: `El perfil ya existe: ${file}` }] };
14062
14445
  }
@@ -14065,7 +14448,7 @@ async function runProfile(ctx) {
14065
14448
  exitCode: 0,
14066
14449
  diagnostics: [],
14067
14450
  data: { file },
14068
- 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."]
14069
14452
  };
14070
14453
  }
14071
14454
  return {
@@ -14074,7 +14457,7 @@ async function runProfile(ctx) {
14074
14457
  };
14075
14458
  }
14076
14459
  async function isInside(dir, name) {
14077
- return exists(path28.join(dir, `${name}.yaml`));
14460
+ return exists(path31.join(dir, `${name}.yaml`));
14078
14461
  }
14079
14462
  function profileTemplate(name) {
14080
14463
  return `name: ${name}
@@ -14102,7 +14485,7 @@ assumptions: []
14102
14485
  }
14103
14486
 
14104
14487
  // src/commands/doctor.ts
14105
- import path29 from "path";
14488
+ import path32 from "path";
14106
14489
  async function runDoctorCommand(ctx) {
14107
14490
  const { root, config } = await requireWorkspace(ctx);
14108
14491
  const report = await runDoctor(root);
@@ -14119,12 +14502,15 @@ async function runDoctorCommand(ctx) {
14119
14502
  report.summary.warnings += 1;
14120
14503
  }
14121
14504
  }
14505
+ const advisory = await upgradeAdvisory(root);
14506
+ report.findings.push(...advisory.diagnostics);
14507
+ report.summary = countBySeverity(report.findings);
14122
14508
  const lines = [msg("doctor.title", ctx.language), ""];
14123
14509
  if (report.findings.length === 0) {
14124
14510
  lines.push("Sin hallazgos. El workspace est\xE1 sano.");
14125
14511
  } else {
14126
14512
  for (const finding of report.findings) {
14127
- 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}` : ""}` : "";
14128
14514
  lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${location} \u2014 ${finding.message}`);
14129
14515
  if (finding.suggestion) lines.push(` \u21B3 ${finding.suggestion}`);
14130
14516
  }
@@ -14198,7 +14584,7 @@ async function runHelp(ctx, commandName) {
14198
14584
  }
14199
14585
 
14200
14586
  // src/commands/init.ts
14201
- import path30 from "path";
14587
+ import path33 from "path";
14202
14588
  async function runInit(ctx) {
14203
14589
  const languageFlag = flagString(ctx.flags, "language");
14204
14590
  const language = languageFlag === "en" ? "en" : languageFlag === "es" ? "es" : void 0;
@@ -14245,11 +14631,11 @@ async function runInit(ctx) {
14245
14631
  const lines = [msg("init.title", ctx.language), ""];
14246
14632
  if (result.created.length > 0) {
14247
14633
  lines.push(msg("init.done", ctx.language));
14248
- 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)}`);
14249
14635
  if (compiled.length > 0) {
14250
14636
  lines.push("");
14251
14637
  lines.push("Adaptadores compilados:");
14252
- 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))}`);
14253
14639
  }
14254
14640
  lines.push("");
14255
14641
  lines.push("Siguiente: crea un cambio con `satlas new <slug>` y escribe la spec funcional.");
@@ -14259,8 +14645,8 @@ async function runInit(ctx) {
14259
14645
  exitCode: hasErrors2 ? 1 : 0,
14260
14646
  diagnostics: result.diagnostics,
14261
14647
  data: {
14262
- sddDir: path30.relative(ctx.cwd, result.sddDir),
14263
- 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)),
14264
14650
  detected: result.detected?.matches.map((m) => ({ name: m.name, score: m.score })),
14265
14651
  best: result.detected?.best?.name
14266
14652
  },
@@ -14301,7 +14687,7 @@ async function runMetrics(ctx) {
14301
14687
  }
14302
14688
 
14303
14689
  // src/commands/mockup.ts
14304
- import path31 from "path";
14690
+ import path34 from "path";
14305
14691
  async function runMockup(ctx) {
14306
14692
  const { root, workspace, config } = await requireWorkspace(ctx);
14307
14693
  const slug = ctx.positionals[0];
@@ -14350,17 +14736,17 @@ async function runMockup(ctx) {
14350
14736
  ` pantallas: ${plan.screens.length}`,
14351
14737
  ...plan.screens.map((s) => ` - ${s.id}: ${s.title} (${s.illustrates.length} escenario(s))`),
14352
14738
  "",
14353
- ` plan: ${path31.relative(ctx.cwd, planFile)}`,
14354
- ` manifiesto: ${path31.relative(ctx.cwd, manifestFile)}`,
14739
+ ` plan: ${path34.relative(ctx.cwd, planFile)}`,
14740
+ ` manifiesto: ${path34.relative(ctx.cwd, manifestFile)}`,
14355
14741
  "",
14356
14742
  "Siguiente: genera el HTML de cada pantalla (fase /satlas-mockup) y valida con `satlas mockup " + slug + " --check`."
14357
14743
  ];
14358
- 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 };
14359
14745
  }
14360
14746
 
14361
14747
  // src/mcp/server.ts
14362
- import { promises as fs2 } from "fs";
14363
- import path34 from "path";
14748
+ import { promises as fs3 } from "fs";
14749
+ import path37 from "path";
14364
14750
  import { createInterface } from "readline/promises";
14365
14751
 
14366
14752
  // src/mcp/protocol.ts
@@ -14422,12 +14808,12 @@ function stringArg(args, key) {
14422
14808
  }
14423
14809
 
14424
14810
  // src/mcp/tools/glossary.ts
14425
- import path32 from "path";
14811
+ import path35 from "path";
14426
14812
  async function runAtlasGlossary(_args, host) {
14427
14813
  const resolved = await host.getWorkspace();
14428
14814
  if (!resolved) return noWorkspaceResult();
14429
14815
  const { root, config } = resolved;
14430
- const glossaryPath = path32.resolve(root, config.spec.glossary);
14816
+ const glossaryPath = path35.resolve(root, config.spec.glossary);
14431
14817
  const raw = await readTextIfExists(glossaryPath);
14432
14818
  if (raw === void 0) {
14433
14819
  return jsonResult({
@@ -14473,7 +14859,7 @@ async function runAtlasImpact(args, host) {
14473
14859
  }
14474
14860
 
14475
14861
  // src/evaluate.ts
14476
- import path33 from "path";
14862
+ import path36 from "path";
14477
14863
  function livingRequirementsMap2(specs) {
14478
14864
  const map = /* @__PURE__ */ new Map();
14479
14865
  for (const spec of specs) {
@@ -14482,7 +14868,7 @@ function livingRequirementsMap2(specs) {
14482
14868
  return map;
14483
14869
  }
14484
14870
  async function evaluateChange(workspace, config, change, approvals) {
14485
- const deltaPath = path33.join(change.dir, "spec.md");
14871
+ const deltaPath = path36.join(change.dir, "spec.md");
14486
14872
  const deltaContent = await readTextIfExists(deltaPath);
14487
14873
  const approval = verifyApproval(change, approvals, config, deltaContent ?? void 0);
14488
14874
  const living = livingRequirementsMap2(workspace.specs);
@@ -14744,12 +15130,12 @@ var WorkspaceCache = class {
14744
15130
  }
14745
15131
  };
14746
15132
  async function workspaceStamp(root) {
14747
- const sddDir = path34.join(root, ".sdd");
14748
- const markers = [sddDir, path34.join(sddDir, "config.yaml"), path34.join(sddDir, "changes"), path34.join(sddDir, "specs"), path34.join(sddDir, "approvals.yaml")];
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")];
14749
15135
  let stamp = 0;
14750
15136
  for (const marker of markers) {
14751
15137
  try {
14752
- const st = await fs2.stat(marker);
15138
+ const st = await fs3.stat(marker);
14753
15139
  stamp = Math.max(stamp, st.mtimeMs);
14754
15140
  } catch {
14755
15141
  }
@@ -14763,7 +15149,7 @@ async function resolveMcpWorkspace(cwd, cache) {
14763
15149
  const cached2 = cache.get(root);
14764
15150
  if (cached2 && cached2.mtimeMs === stamp) return cached2.value;
14765
15151
  const { workspace, config } = await loadWorkspace(root);
14766
- const approvals = await loadApprovals(path34.join(root, ".sdd"));
15152
+ const approvals = await loadApprovals(path37.join(root, ".sdd"));
14767
15153
  const value = { root, workspace, config, approvals: approvals.byArtifact };
14768
15154
  cache.set(root, stamp, value);
14769
15155
  return value;
@@ -14855,7 +15241,7 @@ async function runMcp(ctx) {
14855
15241
  }
14856
15242
 
14857
15243
  // src/commands/packs.ts
14858
- import path35 from "path";
15244
+ import path38 from "path";
14859
15245
  async function runPacks(ctx) {
14860
15246
  const { root, config, workspace } = await requireWorkspace(ctx);
14861
15247
  const slug = flagString(ctx.flags, "check");
@@ -14913,14 +15299,14 @@ async function runPacks(ctx) {
14913
15299
  data: {
14914
15300
  slug,
14915
15301
  packs: evaluations.map((evaluation) => ({ id: evaluation.pack.id, status: evaluation.status, passed: evaluation.passed, failed: evaluation.failed, results: evaluation.results })),
14916
- analyzePath: path35.posix.join(".sdd", "changes", slug, "analyze.md")
15302
+ analyzePath: path38.posix.join(".sdd", "changes", slug, "analyze.md")
14917
15303
  },
14918
15304
  text: lines
14919
15305
  };
14920
15306
  }
14921
15307
 
14922
15308
  // src/commands/new.ts
14923
- import path36 from "path";
15309
+ import path39 from "path";
14924
15310
  async function runNew(ctx) {
14925
15311
  const slug = ctx.positionals[0];
14926
15312
  if (!slug) {
@@ -14946,14 +15332,14 @@ async function runNew(ctx) {
14946
15332
  msg("new.title", ctx.language),
14947
15333
  "",
14948
15334
  msg("new.done", ctx.language),
14949
- ...result.files.map((f) => ` + ${path36.relative(ctx.cwd, f)}`),
15335
+ ...result.files.map((f) => ` + ${path39.relative(ctx.cwd, f)}`),
14950
15336
  "",
14951
15337
  `Siguiente: /satlas.specify ${result.slug} \u2014 escribe la especificaci\xF3n 100% funcional y de negocio.`
14952
15338
  ];
14953
15339
  return {
14954
15340
  exitCode: hasErrors2 ? 1 : 0,
14955
15341
  diagnostics: result.diagnostics,
14956
- data: { slug: result.slug, files: result.files.map((f) => path36.relative(ctx.cwd, f)) },
15342
+ data: { slug: result.slug, files: result.files.map((f) => path39.relative(ctx.cwd, f)) },
14957
15343
  text: lines
14958
15344
  };
14959
15345
  }
@@ -14988,7 +15374,7 @@ async function runNext(ctx) {
14988
15374
  }
14989
15375
 
14990
15376
  // src/commands/present.ts
14991
- import path37 from "path";
15377
+ import path40 from "path";
14992
15378
  async function runPresentCommand(ctx) {
14993
15379
  const { root } = await requireWorkspace(ctx);
14994
15380
  const slug = ctx.positionals[0];
@@ -15000,7 +15386,7 @@ async function runPresentCommand(ctx) {
15000
15386
  if (result.path) {
15001
15387
  lines.push("Propuesta generada");
15002
15388
  lines.push("");
15003
- lines.push(` archivo: ${path37.relative(ctx.cwd, result.path)}`);
15389
+ lines.push(` archivo: ${path40.relative(ctx.cwd, result.path)}`);
15004
15390
  if (result.hash) lines.push(` hash de la spec: ${shortHash(result.hash)}`);
15005
15391
  lines.push("");
15006
15392
  lines.push("\xC1brela en el navegador y comp\xE1rtela con el stakeholder.");
@@ -15010,7 +15396,7 @@ async function runPresentCommand(ctx) {
15010
15396
  return {
15011
15397
  exitCode: hasErrors2 ? 1 : 0,
15012
15398
  diagnostics: result.diagnostics,
15013
- data: result.path ? { path: path37.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,
15014
15400
  text: lines
15015
15401
  };
15016
15402
  }
@@ -15086,7 +15472,8 @@ function usage(usageText) {
15086
15472
 
15087
15473
  // src/commands/status.ts
15088
15474
  async function runStatus(ctx) {
15089
- const { workspace, config, approvals } = await requireWorkspace(ctx);
15475
+ const { root, workspace, config, approvals } = await requireWorkspace(ctx);
15476
+ const advisory = await upgradeAdvisory(root);
15090
15477
  const lines = [msg("status.title", ctx.language), ""];
15091
15478
  const changes = [];
15092
15479
  if (workspace.changes.length === 0) {
@@ -15118,17 +15505,27 @@ async function runStatus(ctx) {
15118
15505
  for (const spec of workspace.specs) {
15119
15506
  lines.push(` ${spec.domain} \u2014 ${spec.spec.requirements.length} requisitos`);
15120
15507
  }
15121
- 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;
15122
15510
  return {
15123
15511
  exitCode: errors > 0 ? 1 : 0,
15124
- diagnostics: workspace.diagnostics,
15125
- 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
+ },
15126
15523
  text: lines
15127
15524
  };
15128
15525
  }
15129
15526
 
15130
15527
  // src/commands/trace.ts
15131
- import path38 from "path";
15528
+ import path41 from "path";
15132
15529
  async function runTrace(ctx) {
15133
15530
  const { workspace, config } = await requireWorkspace(ctx);
15134
15531
  const slug = flagString(ctx.flags, "change");
@@ -15148,7 +15545,7 @@ async function runTrace(ctx) {
15148
15545
  data.push({ slug: change.slug, nodes: result.graph.nodes.length, edges: result.graph.edges.length, summary: result.summary, findings: result.findings });
15149
15546
  lines.push(` ${change.slug}: ${result.graph.nodes.length} nodos, ${result.graph.edges.length} aristas, ${errors} errores, ${result.summary.warnings} avisos`);
15150
15547
  for (const finding of result.findings) {
15151
- const location = finding.path ? ` (${path38.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
15548
+ const location = finding.path ? ` (${path41.relative(ctx.cwd, finding.path)}${finding.line ? `:${finding.line}` : ""})` : "";
15152
15549
  lines.push(` ${finding.severity.toUpperCase()} ${finding.code} \u2014 ${finding.message}${location}`);
15153
15550
  }
15154
15551
  }
@@ -15157,12 +15554,130 @@ async function runTrace(ctx) {
15157
15554
  return { exitCode: blockingErrors > 0 ? 1 : 0, diagnostics, data: { changes: data }, text: lines };
15158
15555
  }
15159
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
+
15160
15674
  // src/commands/validate.ts
15161
- import path39 from "path";
15675
+ import path43 from "path";
15162
15676
  async function runValidate(ctx) {
15163
- const { workspace, config, approvals } = await requireWorkspace(ctx);
15677
+ const { root, workspace, config, approvals } = await requireWorkspace(ctx);
15164
15678
  const slug = flagString(ctx.flags, "change");
15165
15679
  const strict = flagBool(ctx.flags, "strict");
15680
+ const advisory = slug ? void 0 : await upgradeAdvisory(root);
15166
15681
  const diagnostics = [];
15167
15682
  const lines = [msg("validate.title", ctx.language), ""];
15168
15683
  for (const spec of workspace.specs) {
@@ -15184,22 +15699,35 @@ async function runValidate(ctx) {
15184
15699
  for (const finding of evaluation.lintFindings) {
15185
15700
  lines.push(` ${finding.severity.toUpperCase()} ${finding.code}${finding.line ? `:${finding.line}` : ""} \u2014 ${finding.message}`);
15186
15701
  }
15187
- lines.push(` archivo: ${path39.relative(ctx.cwd, path39.join(change.dir, "spec.md"))}`);
15702
+ lines.push(` archivo: ${path43.relative(ctx.cwd, path43.join(change.dir, "spec.md"))}`);
15188
15703
  }
15189
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
+ }
15190
15708
  const errors = diagnostics.filter((d) => d.severity === "error").length;
15191
15709
  const warnings = diagnostics.filter((d) => d.severity === "warning").length;
15192
15710
  if (errors === 0 && warnings === 0) lines.push(msg("validate.ok", ctx.language));
15193
15711
  return {
15194
15712
  exitCode: errors > 0 || strict && warnings > 0 ? 1 : 0,
15195
- diagnostics,
15196
- 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
+ },
15197
15725
  text: lines
15198
15726
  };
15199
15727
  }
15200
15728
 
15201
15729
  // src/commands/verify.ts
15202
- import path40 from "path";
15730
+ import path44 from "path";
15203
15731
  async function runVerify(ctx) {
15204
15732
  const { root, workspace } = await requireWorkspace(ctx);
15205
15733
  const slug = ctx.positionals[0];
@@ -15242,7 +15770,7 @@ async function runVerify(ctx) {
15242
15770
  return { exitCode: 2, diagnostics: [{ code: "ATLAS-VERIFY-001", severity: "error", message: "Falta --by <nombre> (o define meta.owner)" }] };
15243
15771
  }
15244
15772
  const profilesDir = await resolveProfilesDir();
15245
- const profile = await loadActiveProfile(path40.join(root, ".sdd"), [profilesDir ?? "", path40.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));
15246
15774
  const record = await recordEvidence({
15247
15775
  root,
15248
15776
  slug,
@@ -15264,7 +15792,7 @@ async function runVerify(ctx) {
15264
15792
  if (record.evidence.command) lines.push(` comando: ${record.evidence.command}`);
15265
15793
  lines.push(` resultado: ${record.evidence.result}`);
15266
15794
  if (record.evidence.outputHash) lines.push(` hash: ${record.evidence.outputHash}`);
15267
- lines.push(` archivo: ${path40.relative(ctx.cwd, record.path)}`);
15795
+ lines.push(` archivo: ${path44.relative(ctx.cwd, record.path)}`);
15268
15796
  if (record.stdout) {
15269
15797
  lines.push("");
15270
15798
  lines.push(" salida (\xFAltimas l\xEDneas):");
@@ -15279,7 +15807,7 @@ async function runVerify(ctx) {
15279
15807
  return {
15280
15808
  exitCode: record.exitCode === 0 ? 0 : 1,
15281
15809
  diagnostics: record.diagnostics,
15282
- data: { evidence: record.evidence, path: path40.relative(ctx.cwd, record.path), exitCode: record.exitCode },
15810
+ data: { evidence: record.evidence, path: path44.relative(ctx.cwd, record.path), exitCode: record.exitCode },
15283
15811
  text: lines
15284
15812
  };
15285
15813
  }
@@ -15334,6 +15862,7 @@ var HANDLERS2 = {
15334
15862
  trace: runTrace,
15335
15863
  waves: runWaves,
15336
15864
  doctor: runDoctorCommand,
15865
+ upgrade: runUpgrade,
15337
15866
  approve: runApprove,
15338
15867
  issue: runIssue,
15339
15868
  adapters: runAdapters,
@@ -15378,7 +15907,7 @@ async function requireWorkspace(ctx) {
15378
15907
  throw new CliError("ATLAS-WS-001", msg("cli.noWorkspace", ctx.language), 2);
15379
15908
  }
15380
15909
  const { workspace, config } = await loadWorkspace(root);
15381
- const approvals = await loadApprovals(path41.join(root, ".sdd"));
15910
+ const approvals = await loadApprovals(path45.join(root, ".sdd"));
15382
15911
  return { root, workspace, config, approvals: approvals.byArtifact };
15383
15912
  }
15384
15913
  var CliError = class extends Error {
@@ -15445,7 +15974,7 @@ function printResult(command, result, json2, language) {
15445
15974
  }
15446
15975
  const lines = [...result.text ?? []];
15447
15976
  for (const d of result.diagnostics) {
15448
- const location = d.path ? ` ${path41.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
15977
+ const location = d.path ? ` ${path45.relative(process.cwd(), d.path)}${d.line ? `:${d.line}` : ""}` : "";
15449
15978
  lines.push(`${d.severity === "error" ? "ERROR" : d.severity === "warning" ? "AVISO" : "NOTA"} ${d.code}${location} \u2014 ${d.message}`);
15450
15979
  if (d.suggestion) lines.push(` \u21B3 ${d.suggestion}`);
15451
15980
  }