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-
|
|
16
|
+
} from "./chunk-L2BWIT44.js";
|
|
16
17
|
import {
|
|
17
18
|
__commonJS,
|
|
18
19
|
__toESM,
|
|
@@ -1585,7 +1586,7 @@ var require_core = __commonJS({
|
|
|
1585
1586
|
});
|
|
1586
1587
|
|
|
1587
1588
|
// src/cli.ts
|
|
1588
|
-
import
|
|
1589
|
+
import path45 from "path";
|
|
1589
1590
|
|
|
1590
1591
|
// ../core/dist/index.js
|
|
1591
1592
|
import path from "path";
|
|
@@ -9025,8 +9026,12 @@ import path15 from "path";
|
|
|
9025
9026
|
import { promises as fs } from "fs";
|
|
9026
9027
|
import { cp, rename, rm } from "fs/promises";
|
|
9027
9028
|
import path16 from "path";
|
|
9029
|
+
import { promises as fs2 } from "fs";
|
|
9028
9030
|
import path17 from "path";
|
|
9031
|
+
import { parse as parseYaml8, stringify as stringifyYaml5 } from "yaml";
|
|
9029
9032
|
import path18 from "path";
|
|
9033
|
+
import path19 from "path";
|
|
9034
|
+
import path20 from "path";
|
|
9030
9035
|
var CORE_VERSION = "0.0.1";
|
|
9031
9036
|
var REQ_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}$/;
|
|
9032
9037
|
var SCENARIO_ID_RE = /^REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}-S\d+$/;
|
|
@@ -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,
|
|
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:
|
|
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,
|
|
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",
|
|
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",
|
|
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:
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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-
|
|
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 =
|
|
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:
|
|
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 =
|
|
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,
|
|
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
|
|
13633
|
+
import path24 from "path";
|
|
13279
13634
|
|
|
13280
13635
|
// ../adapters/dist/index.js
|
|
13281
|
-
import
|
|
13282
|
-
import { stringify as
|
|
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 =
|
|
13286
|
-
const snippetsDir =
|
|
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 =
|
|
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"] :
|
|
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(
|
|
13666
|
+
const raw = await readTextIfExists(path21.join(snippetsDir, entry)) ?? "";
|
|
13312
13667
|
hashParts.push(raw);
|
|
13313
|
-
snippets.set(
|
|
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 =
|
|
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
|
|
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(
|
|
13982
|
+
return walkUpFor(path23.join("workflow", "phases"));
|
|
13628
13983
|
}
|
|
13629
13984
|
async function walkUpFor(relative) {
|
|
13630
|
-
let dir =
|
|
13985
|
+
let dir = path23.dirname(fileURLToPath2(import.meta.url));
|
|
13631
13986
|
for (let i = 0; i < 8; i += 1) {
|
|
13632
|
-
const candidate =
|
|
13987
|
+
const candidate = path23.join(dir, relative);
|
|
13633
13988
|
if (await exists(candidate)) {
|
|
13634
|
-
return relative.includes(
|
|
13989
|
+
return relative.includes(path23.sep) ? path23.dirname(candidate) : candidate;
|
|
13635
13990
|
}
|
|
13636
|
-
const parent =
|
|
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(` + ${
|
|
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
|
|
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] " : "+ "}${
|
|
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: ${
|
|
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) =>
|
|
13763
|
-
reportPath: result.reportPath ?
|
|
14117
|
+
createdSpecs: result.createdSpecs.map((s) => path25.relative(ctx.cwd, s)),
|
|
14118
|
+
reportPath: result.reportPath ? path25.relative(ctx.cwd, result.reportPath) : void 0,
|
|
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
|
|
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: ${
|
|
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:
|
|
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
|
|
14151
|
+
import path28 from "path";
|
|
13797
14152
|
|
|
13798
14153
|
// src/commands/issue.ts
|
|
13799
|
-
import
|
|
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(
|
|
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 :
|
|
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:
|
|
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
|
|
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: ${
|
|
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
|
|
14019
|
-
data: {
|
|
14394
|
+
diagnostics,
|
|
14395
|
+
data: {
|
|
14396
|
+
checks: gate.checks,
|
|
14397
|
+
errors: gate.errors,
|
|
14398
|
+
warnings: gate.warnings,
|
|
14399
|
+
strict,
|
|
14400
|
+
failed: gate.failed,
|
|
14401
|
+
...sarifWritten ? { sarif: sarifWritten } : {}
|
|
14402
|
+
},
|
|
14020
14403
|
text: lines
|
|
14021
14404
|
};
|
|
14022
14405
|
}
|
|
14023
14406
|
|
|
14024
14407
|
// src/commands/profile.ts
|
|
14025
|
-
import
|
|
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 =
|
|
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 =
|
|
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: ${
|
|
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(
|
|
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
|
|
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 ? ` ${
|
|
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
|
|
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(` + ${
|
|
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(` + ${
|
|
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:
|
|
14263
|
-
created: result.created.map((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
|
|
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: ${
|
|
14354
|
-
` manifiesto: ${
|
|
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:
|
|
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
|
|
14363
|
-
import
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
14748
|
-
const markers = [sddDir,
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
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) => ` + ${
|
|
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) =>
|
|
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
|
|
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: ${
|
|
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:
|
|
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
|
|
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
|
|
15125
|
-
data: {
|
|
15512
|
+
diagnostics,
|
|
15513
|
+
data: {
|
|
15514
|
+
changes,
|
|
15515
|
+
specs: workspace.specs.map((s) => ({ domain: s.domain, requirements: s.spec.requirements.length })),
|
|
15516
|
+
upgrade: {
|
|
15517
|
+
currentVersion: advisory.plan.currentVersion,
|
|
15518
|
+
pending: advisory.plan.pending.length,
|
|
15519
|
+
newer: advisory.plan.newer.length,
|
|
15520
|
+
unreadable: advisory.plan.unreadable.length
|
|
15521
|
+
}
|
|
15522
|
+
},
|
|
15126
15523
|
text: lines
|
|
15127
15524
|
};
|
|
15128
15525
|
}
|
|
15129
15526
|
|
|
15130
15527
|
// src/commands/trace.ts
|
|
15131
|
-
import
|
|
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 ? ` (${
|
|
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
|
|
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: ${
|
|
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: {
|
|
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
|
|
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(
|
|
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: ${
|
|
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:
|
|
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(
|
|
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 ? ` ${
|
|
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
|
}
|